Просмотр исходного кода

docs: plan unified entry client registration demo

Covers SQLite Mock HIS scope, AI platform registration flow, Web demo UI spec, .superpowers visual references, and vertical task breakdown with effort estimates.
WangKang 1 неделя назад
Родитель
Сommit
3be29be8c4

+ 106 - 0
.superpowers/brainstorm/86383-1780383912/content/final-web-demo-design-v2.html

@@ -0,0 +1,106 @@
+<h2>修订版:品牌配色 + 对话优先</h2>
+<p class="subtitle">主题色使用浅色 #3ad4d8、深色 #2b1f99。左侧不再做功能按钮,改为助手上下文与可说示例。</p>
+
+<div class="mockup">
+  <div class="mockup-header">主界面结构 v2</div>
+  <div class="mockup-body">
+    <div style="height:540px;border:1px solid #dfe7f7;border-radius:18px;background:#f7fbff;overflow:hidden;font-family:Inter,Arial,sans-serif;color:#16133a">
+      <div style="height:58px;background:white;border-bottom:1px solid #e7ebf5;display:flex;align-items:center;justify-content:space-between;padding:0 22px">
+        <div style="display:flex;align-items:center;gap:12px">
+          <div style="width:34px;height:34px;border-radius:10px;background:#2b1f99;color:white;display:grid;place-items:center;font-weight:900">医</div>
+          <div style="font-weight:900">医梦门诊助手</div>
+        </div>
+        <div style="font-size:13px;color:#69708a">Web Demo · 门诊大厅 · 联调演示</div>
+      </div>
+
+      <div style="display:grid;grid-template-columns:280px 1fr 390px;gap:16px;padding:16px;height:482px">
+        <aside style="background:linear-gradient(180deg,#ffffff,#f1fbff);border:1px solid #dfe7f7;border-radius:16px;padding:18px;display:flex;flex-direction:column">
+          <div style="display:flex;gap:12px;align-items:center;margin-bottom:18px">
+            <div style="width:70px;height:70px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#ffffff,#3ad4d8 45%,#2b1f99);display:grid;place-items:center;font-size:24px;font-weight:900;color:white;box-shadow:0 12px 30px rgba(43,31,153,.18)">AI</div>
+            <div>
+              <div style="font-weight:900;font-size:18px">你好,我在</div>
+              <div style="font-size:12px;color:#68708c;margin-top:4px">说出需求,我来判断下一步</div>
+            </div>
+          </div>
+
+          <div style="border:1px solid #dce7f8;border-radius:14px;background:white;padding:14px;margin-bottom:14px">
+            <div style="font-size:12px;color:#69708a;margin-bottom:8px">可以这样说</div>
+            <div style="display:grid;gap:8px;font-size:13px">
+              <div style="background:#f4fbff;border-radius:10px;padding:9px">“我头疼三天,想挂号”</div>
+              <div style="background:#f4fbff;border-radius:10px;padding:9px">“明天上午有神经内科的号吗”</div>
+              <div style="background:#f4fbff;border-radius:10px;padding:9px">“我想找李明医生”</div>
+            </div>
+          </div>
+
+          <div style="border:1px solid #dce7f8;border-radius:14px;background:white;padding:14px">
+            <div style="font-size:12px;color:#69708a;margin-bottom:8px">当前可办理</div>
+            <div style="display:flex;flex-wrap:wrap;gap:8px">
+              <span style="border:1px solid #cceff1;color:#156e83;border-radius:999px;padding:6px 9px;font-size:12px">挂号</span>
+              <span style="border:1px solid #e2e5f2;color:#5d5a82;border-radius:999px;padding:6px 9px;font-size:12px">导诊</span>
+              <span style="border:1px solid #e2e5f2;color:#5d5a82;border-radius:999px;padding:6px 9px;font-size:12px">医生查询</span>
+            </div>
+          </div>
+
+          <div style="margin-top:auto;background:#fff8e8;border:1px solid #f0d89c;border-radius:12px;padding:12px;font-size:12px;color:#735b14">
+            后端返回 mock:true 时展示“联调演示”,不伪装成真实业务。
+          </div>
+        </aside>
+
+        <main style="background:white;border:1px solid #dfe7f7;border-radius:16px;display:flex;flex-direction:column;overflow:hidden">
+          <div style="padding:16px;border-bottom:1px solid #edf1f7;display:flex;justify-content:space-between;align-items:center">
+            <div>
+              <div style="font-size:22px;font-weight:900">对话入口</div>
+              <div style="font-size:13px;color:#69708a;margin-top:4px">系统会识别意图并进入对应流程</div>
+            </div>
+            <div style="background:#f1f0ff;color:#2b1f99;border-radius:999px;padding:7px 11px;font-size:12px;font-weight:800">REGISTRATION</div>
+          </div>
+          <div style="flex:1;padding:16px;display:flex;flex-direction:column;gap:12px;background:#fbfdff">
+            <div style="max-width:72%;background:white;border:1px solid #e1e8f5;border-radius:14px;padding:12px">请直接告诉我你想办理什么,也可以描述症状。</div>
+            <div style="max-width:76%;align-self:flex-end;background:#2b1f99;color:white;border-radius:14px;padding:12px">我头疼三天,还有点恶心,想挂号。</div>
+            <div style="max-width:80%;background:white;border:1px solid #e1e8f5;border-radius:14px;padding:12px">已识别为挂号需求。根据描述,建议优先选择神经内科。</div>
+          </div>
+          <div style="padding:14px;border-top:1px solid #edf1f7;display:flex;gap:10px;background:white">
+            <div style="flex:1;border:1px solid #dce7f7;border-radius:999px;padding:12px 14px;color:#98a1b8">继续输入症状、科室、医生或时间...</div>
+            <div style="background:#3ad4d8;color:#10144a;border-radius:999px;padding:12px 18px;font-weight:900">语音</div>
+            <div style="background:#2b1f99;color:white;border-radius:999px;padding:12px 20px;font-weight:900">发送</div>
+          </div>
+        </main>
+
+        <section style="display:grid;grid-template-rows:auto 1fr;gap:12px">
+          <div style="background:white;border:1px solid #dfe7f7;border-radius:16px;padding:14px">
+            <div style="display:flex;justify-content:space-between;align-items:center">
+              <strong>挂号进度</strong>
+              <span style="font-size:12px;color:#69708a">科室推荐</span>
+            </div>
+            <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:5px;margin-top:12px">
+              <div style="height:8px;border-radius:99px;background:#2b1f99"></div>
+              <div style="height:8px;border-radius:99px;background:#3ad4d8"></div>
+              <div style="height:8px;border-radius:99px;background:#dfe7f7"></div>
+              <div style="height:8px;border-radius:99px;background:#dfe7f7"></div>
+              <div style="height:8px;border-radius:99px;background:#dfe7f7"></div>
+            </div>
+          </div>
+
+          <div style="background:white;border:1px solid #dfe7f7;border-radius:16px;padding:16px;overflow:hidden">
+            <div style="font-weight:900;font-size:18px;margin-bottom:12px">推荐科室</div>
+            <div style="border:2px solid #3ad4d8;border-radius:14px;padding:12px;margin-bottom:10px;background:#f4fdff">
+              <div style="font-weight:900;color:#2b1f99">神经内科</div>
+              <div style="font-size:12px;color:#68708c;margin-top:6px">头痛伴恶心,建议优先排查神经系统相关问题。</div>
+              <div style="margin-top:12px;background:#2b1f99;color:white;border-radius:999px;text-align:center;padding:9px;font-weight:900">选择神经内科</div>
+            </div>
+            <div style="border:1px solid #dfe7f7;border-radius:14px;padding:12px">
+              <div style="font-weight:900">普通内科</div>
+              <div style="font-size:12px;color:#68708c;margin-top:6px">可作为综合初筛科室。</div>
+              <div style="margin-top:12px;border:1px solid #c8d2ea;border-radius:999px;text-align:center;padding:9px;font-weight:900;color:#2b1f99">选择普通内科</div>
+            </div>
+          </div>
+        </section>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="section">
+  <h3>修订原则</h3>
+  <p>左侧不再是功能菜单,而是“智能体说明 + 可说示例 + 能力范围”。患者仍然可以看到系统能做什么,但不会被要求先点菜单。真正的任务入口来自对话与后端意图识别。</p>
+</div>

+ 87 - 0
.superpowers/brainstorm/86383-1780383912/content/final-web-demo-design-v3.html

@@ -0,0 +1,87 @@
+<h2>最终修订:右侧上下文面板</h2>
+<p class="subtitle">右侧不是固定卡片列表,而是 Context Panel:无卡片时展示静态医生助手形象;有任务卡片时切换为卡片专区。</p>
+
+<div class="split">
+  <div class="mockup">
+    <div class="mockup-header">空闲 / 普通问答状态</div>
+    <div class="mockup-body">
+      <div style="height:420px;border:1px solid #dfe7f7;border-radius:18px;background:#f7fbff;padding:16px;font-family:Inter,Arial,sans-serif;color:#16133a">
+        <div style="display:grid;grid-template-columns:260px 1fr 360px;gap:14px;height:100%">
+          <div style="background:white;border:1px solid #dfe7f7;border-radius:16px;padding:16px">
+            <div style="width:62px;height:62px;border-radius:50%;background:radial-gradient(circle,#fff,#3ad4d8 50%,#2b1f99);color:white;display:grid;place-items:center;font-weight:900;margin-bottom:14px">AI</div>
+            <strong>医梦门诊助手</strong>
+            <p style="font-size:12px;color:#69708a">直接说出需求,我来识别。</p>
+          </div>
+          <div style="background:white;border:1px solid #dfe7f7;border-radius:16px;padding:16px;display:flex;flex-direction:column">
+            <strong style="font-size:20px">对话入口</strong>
+            <div style="margin-top:18px;background:#f5f9ff;border-radius:14px;padding:12px">你好,我可以帮你完成挂号、导诊、医生查询。</div>
+            <div style="margin-top:auto;border:1px solid #dce7f7;border-radius:999px;padding:12px;color:#98a1b8">说出你的需求...</div>
+          </div>
+          <div style="background:white;border:1px solid #dfe7f7;border-radius:16px;padding:18px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center">
+            <div style="width:150px;height:170px;border-radius:42% 42% 28% 28%;background:linear-gradient(180deg,#f9fbff,#e8f6ff);border:2px solid #dce7f7;position:relative;margin-bottom:18px">
+              <div style="position:absolute;left:45px;top:34px;width:60px;height:60px;border-radius:50%;background:#ffe7d6;border:2px solid #ead2bf"></div>
+              <div style="position:absolute;left:57px;top:54px;width:8px;height:8px;border-radius:50%;background:#2b1f99;box-shadow:28px 0 #2b1f99"></div>
+              <div style="position:absolute;left:66px;top:76px;width:28px;height:10px;border-bottom:3px solid #2b1f99;border-radius:0 0 20px 20px"></div>
+              <div style="position:absolute;left:34px;top:96px;width:82px;height:70px;border-radius:26px 26px 18px 18px;background:#2b1f99"></div>
+              <div style="position:absolute;left:48px;top:106px;width:54px;height:18px;border-radius:999px;background:#3ad4d8"></div>
+            </div>
+            <div style="font-weight:900;font-size:18px">医生助手待命中</div>
+            <div style="font-size:12px;color:#69708a;margin-top:8px">后续可接入 TTS / STT,展示正在听、正在说等状态。</div>
+            <div style="margin-top:16px;display:flex;gap:8px">
+              <span style="background:#f1f0ff;color:#2b1f99;border-radius:999px;padding:7px 10px;font-size:12px">可听</span>
+              <span style="background:#e9fbfc;color:#146f82;border-radius:999px;padding:7px 10px;font-size:12px">可说</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div class="mockup">
+    <div class="mockup-header">挂号任务 / 卡片状态</div>
+    <div class="mockup-body">
+      <div style="height:420px;border:1px solid #dfe7f7;border-radius:18px;background:#f7fbff;padding:16px;font-family:Inter,Arial,sans-serif;color:#16133a">
+        <div style="display:grid;grid-template-columns:260px 1fr 360px;gap:14px;height:100%">
+          <div style="background:white;border:1px solid #dfe7f7;border-radius:16px;padding:16px">
+            <div style="width:62px;height:62px;border-radius:50%;background:radial-gradient(circle,#fff,#3ad4d8 50%,#2b1f99);color:white;display:grid;place-items:center;font-weight:900;margin-bottom:14px">AI</div>
+            <strong>医梦门诊助手</strong>
+            <p style="font-size:12px;color:#69708a">当前已识别:挂号。</p>
+          </div>
+          <div style="background:white;border:1px solid #dfe7f7;border-radius:16px;padding:16px;display:flex;flex-direction:column">
+            <strong style="font-size:20px">挂号对话</strong>
+            <div style="margin-top:18px;background:#2b1f99;color:white;border-radius:14px;padding:12px;align-self:flex-end">我头疼三天,想挂号。</div>
+            <div style="margin-top:12px;background:#f5f9ff;border-radius:14px;padding:12px">建议优先选择神经内科。</div>
+            <div style="margin-top:auto;border:1px solid #dce7f7;border-radius:999px;padding:12px;color:#98a1b8">继续输入...</div>
+          </div>
+          <div style="display:grid;grid-template-rows:auto 1fr;gap:12px">
+            <div style="background:white;border:1px solid #dfe7f7;border-radius:16px;padding:14px">
+              <strong>挂号进度</strong>
+              <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:5px;margin-top:12px">
+                <div style="height:8px;border-radius:99px;background:#2b1f99"></div>
+                <div style="height:8px;border-radius:99px;background:#3ad4d8"></div>
+                <div style="height:8px;border-radius:99px;background:#dfe7f7"></div>
+                <div style="height:8px;border-radius:99px;background:#dfe7f7"></div>
+                <div style="height:8px;border-radius:99px;background:#dfe7f7"></div>
+              </div>
+            </div>
+            <div style="background:white;border:1px solid #dfe7f7;border-radius:16px;padding:16px">
+              <div style="font-weight:900;font-size:18px;margin-bottom:12px">推荐科室</div>
+              <div style="border:2px solid #3ad4d8;border-radius:14px;padding:12px;background:#f4fdff">
+                <div style="font-weight:900;color:#2b1f99">神经内科</div>
+                <div style="font-size:12px;color:#68708c;margin-top:6px">头痛伴恶心,建议优先排查神经系统相关问题。</div>
+                <div style="margin-top:12px;background:#2b1f99;color:white;border-radius:999px;text-align:center;padding:9px;font-weight:900">选择神经内科</div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="section">
+  <h3>状态切换规则</h3>
+  <p><strong>无 activeTask / 无 card_created:</strong>右侧展示医生助手形象。<br>
+  <strong>有 task_updated 或 card_created:</strong>右侧切换为任务进度 + 卡片专区。<br>
+  <strong>流程完成:</strong>展示挂号成功卡片,用户点击“完成”后回到医生助手形象。</p>
+</div>

+ 74 - 0
.superpowers/brainstorm/86383-1780383912/content/final-web-demo-design-v4.html

@@ -0,0 +1,74 @@
+<h2>最终版交互定稿:医生形象仅展示</h2>
+<p class="subtitle">右侧空闲态只放静态医生形象;语音与发送按钮保留在中间对话输入区。</p>
+
+<div class="mockup">
+  <div class="mockup-header">空闲态:右侧只展示形象,不承载输入操作</div>
+  <div class="mockup-body">
+    <div style="height:520px;border:1px solid #dfe7f7;border-radius:18px;background:#f7fbff;overflow:hidden;font-family:Inter,Arial,sans-serif;color:#16133a">
+      <div style="height:58px;background:white;border-bottom:1px solid #e7ebf5;display:flex;align-items:center;justify-content:space-between;padding:0 22px">
+        <div style="display:flex;align-items:center;gap:12px">
+          <div style="width:34px;height:34px;border-radius:10px;background:#2b1f99;color:white;display:grid;place-items:center;font-weight:900">医</div>
+          <div style="font-weight:900">医梦门诊助手</div>
+        </div>
+        <div style="font-size:13px;color:#69708a">Web Demo · 门诊大厅 · 联调演示</div>
+      </div>
+
+      <div style="display:grid;grid-template-columns:280px 1fr 390px;gap:16px;padding:16px;height:462px">
+        <aside style="background:linear-gradient(180deg,#ffffff,#f1fbff);border:1px solid #dfe7f7;border-radius:16px;padding:18px;display:flex;flex-direction:column">
+          <div style="display:flex;gap:12px;align-items:center;margin-bottom:18px">
+            <div style="width:70px;height:70px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#ffffff,#3ad4d8 45%,#2b1f99);display:grid;place-items:center;font-size:24px;font-weight:900;color:white">AI</div>
+            <div>
+              <div style="font-weight:900;font-size:18px">你好,我在</div>
+              <div style="font-size:12px;color:#68708c;margin-top:4px">说出需求,我来判断下一步</div>
+            </div>
+          </div>
+          <div style="border:1px solid #dce7f8;border-radius:14px;background:white;padding:14px">
+            <div style="font-size:12px;color:#69708a;margin-bottom:8px">可以这样说</div>
+            <div style="display:grid;gap:8px;font-size:13px">
+              <div style="background:#f4fbff;border-radius:10px;padding:9px">“我头疼三天,想挂号”</div>
+              <div style="background:#f4fbff;border-radius:10px;padding:9px">“明天上午有神经内科的号吗”</div>
+              <div style="background:#f4fbff;border-radius:10px;padding:9px">“我想找李明医生”</div>
+            </div>
+          </div>
+          <div style="margin-top:auto;background:#fff8e8;border:1px solid #f0d89c;border-radius:12px;padding:12px;font-size:12px;color:#735b14">
+            Mock 结果显示“联调演示”,不伪装成真实业务。
+          </div>
+        </aside>
+
+        <main style="background:white;border:1px solid #dfe7f7;border-radius:16px;display:flex;flex-direction:column;overflow:hidden">
+          <div style="padding:16px;border-bottom:1px solid #edf1f7">
+            <div style="font-size:22px;font-weight:900">对话入口</div>
+            <div style="font-size:13px;color:#69708a;margin-top:4px">系统会识别意图并进入对应流程</div>
+          </div>
+          <div style="flex:1;padding:16px;display:flex;flex-direction:column;gap:12px;background:#fbfdff">
+            <div style="max-width:72%;background:white;border:1px solid #e1e8f5;border-radius:14px;padding:12px">请直接告诉我你想办理什么,也可以描述症状。</div>
+            <div style="max-width:76%;align-self:flex-end;background:#2b1f99;color:white;border-radius:14px;padding:12px">我头疼三天,还有点恶心,想挂号。</div>
+            <div style="max-width:80%;background:white;border:1px solid #e1e8f5;border-radius:14px;padding:12px">已识别为挂号需求。根据描述,建议优先选择神经内科。</div>
+          </div>
+          <div style="padding:14px;border-top:1px solid #edf1f7;display:flex;gap:10px;background:white">
+            <div style="flex:1;border:1px solid #dce7f7;border-radius:999px;padding:12px 14px;color:#98a1b8">继续输入症状、科室、医生或时间...</div>
+            <div style="background:#3ad4d8;color:#10144a;border-radius:999px;padding:12px 18px;font-weight:900">语音</div>
+            <div style="background:#2b1f99;color:white;border-radius:999px;padding:12px 20px;font-weight:900">发送</div>
+          </div>
+        </main>
+
+        <section style="background:white;border:1px solid #dfe7f7;border-radius:16px;padding:18px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center">
+          <div style="width:180px;height:210px;border-radius:42% 42% 28% 28%;background:linear-gradient(180deg,#f9fbff,#e8f6ff);border:2px solid #dce7f7;position:relative;margin-bottom:18px;box-shadow:0 20px 48px rgba(43,31,153,.12)">
+            <div style="position:absolute;left:55px;top:40px;width:70px;height:70px;border-radius:50%;background:#ffe7d6;border:2px solid #ead2bf"></div>
+            <div style="position:absolute;left:70px;top:64px;width:8px;height:8px;border-radius:50%;background:#2b1f99;box-shadow:32px 0 #2b1f99"></div>
+            <div style="position:absolute;left:79px;top:88px;width:32px;height:10px;border-bottom:3px solid #2b1f99;border-radius:0 0 20px 20px"></div>
+            <div style="position:absolute;left:44px;top:116px;width:92px;height:86px;border-radius:28px 28px 18px 18px;background:#2b1f99"></div>
+            <div style="position:absolute;left:58px;top:128px;width:64px;height:20px;border-radius:999px;background:#3ad4d8"></div>
+          </div>
+          <div style="font-weight:900;font-size:20px">医生助手</div>
+          <div style="font-size:13px;color:#69708a;margin-top:8px;max-width:260px">当前仅作为静态形象展示。语音与发送操作仍在中间输入区完成。</div>
+        </section>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="section">
+  <h3>定稿规则</h3>
+  <p>右侧空闲态只展示静态医生形象和一句说明;不放语音按钮、不放发送按钮、不放可听/可说标签。只有收到 `task_updated` 或 `card_created` 后,右侧才切换为任务进度和业务卡片。</p>
+</div>

+ 89 - 0
.superpowers/brainstorm/86383-1780383912/content/final-web-demo-design.html

@@ -0,0 +1,89 @@
+<h2>最终设计:A+B 单应用 Web Demo</h2>
+<p class="subtitle">面向 1440×900 / 1920×1080 演示视口,完整打穿挂号闭环。</p>
+
+<div class="mockup">
+  <div class="mockup-header">主界面结构</div>
+  <div class="mockup-body">
+    <div style="height:520px;border:1px solid #dfe8e5;border-radius:18px;background:#f6fbfa;overflow:hidden;font-family:Inter,Arial,sans-serif;color:#17211f">
+      <div style="height:58px;background:white;border-bottom:1px solid #e4eeeb;display:flex;align-items:center;justify-content:space-between;padding:0 22px">
+        <div style="display:flex;align-items:center;gap:12px">
+          <div style="width:34px;height:34px;border-radius:10px;background:#16a37b;color:white;display:grid;place-items:center;font-weight:900">医</div>
+          <div style="font-weight:900">医梦门诊助手</div>
+        </div>
+        <div style="font-size:13px;color:#667773">Web Demo · 门诊大厅 · 联调演示</div>
+      </div>
+
+      <div style="display:grid;grid-template-columns:260px 1fr 380px;gap:16px;padding:16px;height:462px">
+        <aside style="background:white;border:1px solid #dfe9e6;border-radius:16px;padding:16px;display:flex;flex-direction:column">
+          <div style="display:flex;gap:12px;align-items:center;margin-bottom:18px">
+            <div style="width:62px;height:62px;border-radius:50%;background:linear-gradient(145deg,#e2fbf2,#8ee0c3);display:grid;place-items:center;font-size:28px;font-weight:900;color:#0d765e">AI</div>
+            <div>
+              <div style="font-weight:900">你好</div>
+              <div style="font-size:12px;color:#697a76">我来帮你完成挂号</div>
+            </div>
+          </div>
+          <div style="display:grid;gap:10px">
+            <div style="background:#eaf8f3;border:1px solid #bfeadc;border-radius:12px;padding:12px;font-weight:900">智能挂号</div>
+            <div style="border:1px solid #e3ece9;border-radius:12px;padding:12px;color:#667773">科室导诊</div>
+            <div style="border:1px solid #e3ece9;border-radius:12px;padding:12px;color:#667773">查医生</div>
+            <div style="border:1px solid #e3ece9;border-radius:12px;padding:12px;color:#667773">路线引导</div>
+          </div>
+          <div style="margin-top:auto;background:#fff8e6;border:1px solid #f1d99c;border-radius:12px;padding:12px;font-size:12px;color:#765b12">
+            Mock 支付与 Mock HIS 结果必须标注“联调演示”。
+          </div>
+        </aside>
+
+        <main style="background:white;border:1px solid #dfe9e6;border-radius:16px;display:flex;flex-direction:column;overflow:hidden">
+          <div style="padding:16px;border-bottom:1px solid #edf3f1">
+            <div style="font-size:22px;font-weight:900">挂号对话</div>
+            <div style="font-size:13px;color:#667773;margin-top:4px">可以说症状、科室、医生或时间</div>
+          </div>
+          <div style="flex:1;padding:16px;display:flex;flex-direction:column;gap:12px">
+            <div style="max-width:70%;background:#f0f6f4;border-radius:14px;padding:12px">我可以帮你推荐科室、选择医生和预约时间。</div>
+            <div style="max-width:76%;align-self:flex-end;background:#16a37b;color:white;border-radius:14px;padding:12px">我头疼三天,还有点恶心,想挂号。</div>
+            <div style="max-width:78%;background:#f0f6f4;border-radius:14px;padding:12px">建议优先选择神经内科,也可选择普通内科。</div>
+          </div>
+          <div style="padding:14px;border-top:1px solid #edf3f1;display:flex;gap:10px">
+            <div style="flex:1;border:1px solid #dce8e5;border-radius:999px;padding:12px 14px;color:#98a7a3">输入你的需求...</div>
+            <div style="background:#16a37b;color:white;border-radius:999px;padding:12px 20px;font-weight:900">发送</div>
+          </div>
+        </main>
+
+        <section style="display:grid;grid-template-rows:auto 1fr;gap:12px">
+          <div style="background:white;border:1px solid #dfe9e6;border-radius:16px;padding:14px">
+            <div style="display:flex;justify-content:space-between;align-items:center">
+              <strong>挂号进度</strong>
+              <span style="font-size:12px;color:#667773">2 / 5</span>
+            </div>
+            <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:5px;margin-top:12px">
+              <div style="height:8px;border-radius:99px;background:#16a37b"></div>
+              <div style="height:8px;border-radius:99px;background:#16a37b"></div>
+              <div style="height:8px;border-radius:99px;background:#dfe8e5"></div>
+              <div style="height:8px;border-radius:99px;background:#dfe8e5"></div>
+              <div style="height:8px;border-radius:99px;background:#dfe8e5"></div>
+            </div>
+          </div>
+
+          <div style="background:white;border:1px solid #dfe9e6;border-radius:16px;padding:16px;overflow:hidden">
+            <div style="font-weight:900;font-size:18px;margin-bottom:12px">推荐科室</div>
+            <div style="border:1px solid #dfe8e5;border-radius:14px;padding:12px;margin-bottom:10px">
+              <div style="font-weight:900">神经内科</div>
+              <div style="font-size:12px;color:#667773;margin-top:6px">头痛伴恶心,建议优先排查神经系统相关问题。</div>
+              <div style="margin-top:12px;background:#16a37b;color:white;border-radius:999px;text-align:center;padding:9px;font-weight:900">选择</div>
+            </div>
+            <div style="border:1px solid #dfe8e5;border-radius:14px;padding:12px">
+              <div style="font-weight:900">普通内科</div>
+              <div style="font-size:12px;color:#667773;margin-top:6px">可作为综合初筛科室。</div>
+              <div style="margin-top:12px;border:1px solid #b9d9cf;border-radius:999px;text-align:center;padding:9px;font-weight:900">选择</div>
+            </div>
+          </div>
+        </section>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="section">
+  <h3>设计边界</h3>
+  <p>首版只有一个 Web 应用,但目录按未来可拆包组织。核心功能只做挂号闭环;科室导诊、查医生、路线引导在侧栏可见,但点击后展示“演示暂未开放/可后续接入”。</p>
+</div>

+ 82 - 0
.superpowers/brainstorm/86383-1780383912/content/registration-flow-ab.html

@@ -0,0 +1,82 @@
+<h2>A+B 混合方案:挂号闭环交互图</h2>
+<p class="subtitle">第一屏亲和助手,进入挂号后切成“对话 + 流程进度 + 业务卡片”的工作台。</p>
+
+<div class="mockup">
+  <div class="mockup-header">推荐交互流:Web 演示优先 · 挂号闭环</div>
+  <div class="mockup-body">
+    <div style="display:grid;grid-template-columns:1fr 34px 1fr 34px 1fr;gap:10px;align-items:stretch">
+      <div style="border:1px solid #dce8e4;border-radius:14px;background:#f8fcfb;padding:16px">
+        <div style="font-size:12px;color:#6b7b78;margin-bottom:8px">01 首页</div>
+        <div style="font-weight:900;font-size:20px;margin-bottom:10px">亲和助手入口</div>
+        <div style="height:72px;border-radius:16px;background:linear-gradient(135deg,#e8fbf4,#b4ead9);display:flex;align-items:center;padding:12px;font-weight:800">你好,我来帮你挂号</div>
+        <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px">
+          <div style="background:white;border:1px solid #dce8e4;border-radius:10px;padding:10px">我要挂号</div>
+          <div style="background:white;border:1px solid #dce8e4;border-radius:10px;padding:10px">科室导诊</div>
+        </div>
+      </div>
+      <div style="display:grid;place-items:center;font-size:24px;color:#7b8b87">→</div>
+      <div style="border:1px solid #dce8e4;border-radius:14px;background:#ffffff;padding:16px">
+        <div style="font-size:12px;color:#6b7b78;margin-bottom:8px">02 对话启动</div>
+        <div style="font-weight:900;font-size:20px;margin-bottom:10px">症状或科室进入</div>
+        <div style="background:#f0f6f4;border-radius:12px;padding:10px;margin-bottom:8px">“我头疼三天,想挂号”</div>
+        <div style="background:#159b78;color:white;border-radius:12px;padding:10px">SSE 返回任务 + 首张卡片</div>
+      </div>
+      <div style="display:grid;place-items:center;font-size:24px;color:#7b8b87">→</div>
+      <div style="border:1px solid #dce8e4;border-radius:14px;background:#f7fafc;padding:16px">
+        <div style="font-size:12px;color:#6b7b78;margin-bottom:8px">03 工作台</div>
+        <div style="font-weight:900;font-size:20px;margin-bottom:10px">对话 + 流程 + 卡片</div>
+        <div style="display:grid;grid-template-columns:1fr 120px;gap:8px">
+          <div style="height:96px;background:white;border:1px solid #dfe8ec;border-radius:12px;padding:10px">对话消息流</div>
+          <div style="height:96px;background:white;border:1px solid #dfe8ec;border-radius:12px;padding:10px">挂号进度<br>2/5</div>
+        </div>
+      </div>
+    </div>
+
+    <div style="margin-top:22px;display:grid;grid-template-columns:repeat(5,1fr);gap:10px">
+      <div style="background:white;border:1px solid #dfe8ec;border-radius:14px;padding:12px">
+        <strong>科室</strong>
+        <p style="margin:6px 0 0;color:#65736f">推荐理由 + 选择</p>
+      </div>
+      <div style="background:white;border:1px solid #dfe8ec;border-radius:14px;padding:12px">
+        <strong>医生</strong>
+        <p style="margin:6px 0 0;color:#65736f">职称、擅长、诊室</p>
+      </div>
+      <div style="background:white;border:1px solid #dfe8ec;border-radius:14px;padding:12px">
+        <strong>时间</strong>
+        <p style="margin:6px 0 0;color:#65736f">号源剩余 + 锁号</p>
+      </div>
+      <div style="background:white;border:1px solid #dfe8ec;border-radius:14px;padding:12px">
+        <strong>Mock 支付</strong>
+        <p style="margin:6px 0 0;color:#65736f">二维码 + 模拟支付</p>
+      </div>
+      <div style="background:white;border:1px solid #dfe8ec;border-radius:14px;padding:12px">
+        <strong>成功</strong>
+        <p style="margin:6px 0 0;color:#65736f">预约号 + Mock 标识</p>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="options">
+  <div class="option" data-choice="route-1" onclick="toggleSelect(this)">
+    <div class="letter">1</div>
+    <div class="content">
+      <h3>推荐:单应用 Web Demo</h3>
+      <p>一个 Vite/Vue 应用,固定 Web 演示视口,内置 mock/fallback 数据,同时接真实后端 API。最快产出演示效果。</p>
+    </div>
+  </div>
+  <div class="option" data-choice="route-2" onclick="toggleSelect(this)">
+    <div class="letter">2</div>
+    <div class="content">
+      <h3>Monorepo 正式版</h3>
+      <p>直接搭 `apps/kiosk-client + packages/api-client + medical-cards`。更接近长期工程,但首版搭建和测试成本更高。</p>
+    </div>
+  </div>
+  <div class="option" data-choice="route-3" onclick="toggleSelect(this)">
+    <div class="letter">3</div>
+    <div class="content">
+      <h3>纯静态原型先行</h3>
+      <p>先做 HTML/静态交互演示,再接 API。视觉最快,但很容易偏离真实后端协议,不建议作为开发计划主线。</p>
+    </div>
+  </div>
+</div>

+ 132 - 0
.superpowers/brainstorm/86383-1780383912/content/ui-direction-options.html

@@ -0,0 +1,132 @@
+<h2>统一入口客户端 UI 方向</h2>
+<p class="subtitle">三种“AI 健康朋友 + 医院服务办理”的首页与对话布局。请选择最接近你想要的方向。</p>
+
+<div class="cards">
+  <div class="card" data-choice="a" onclick="toggleSelect(this)">
+    <div class="card-image">
+      <div style="height:420px;border-radius:18px;background:#f7fbfa;border:1px solid #dbe8e4;overflow:hidden;font-family:Inter,Arial,sans-serif;color:#17211f">
+        <div style="display:flex;justify-content:space-between;align-items:center;padding:18px 22px;background:white;border-bottom:1px solid #e8efed">
+          <div style="font-weight:800;font-size:18px">医梦门诊助手</div>
+          <div style="font-size:12px;color:#6c7b77">门诊大厅 · 自助服务</div>
+        </div>
+        <div style="padding:26px 26px 18px">
+          <div style="display:flex;gap:18px;align-items:center">
+            <div style="width:86px;height:86px;border-radius:50%;background:linear-gradient(145deg,#dff7ef,#8ee0c3);display:grid;place-items:center;font-size:38px">+</div>
+            <div>
+              <div style="font-size:26px;font-weight:900;line-height:1.25">你好,我来帮你挂号</div>
+              <div style="margin-top:8px;font-size:14px;color:#667773">可以说症状、科室、医生或时间</div>
+            </div>
+          </div>
+          <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:24px">
+            <div style="background:white;border:1px solid #dfecea;border-radius:14px;padding:14px;font-weight:700">我要挂号</div>
+            <div style="background:white;border:1px solid #dfecea;border-radius:14px;padding:14px;font-weight:700">科室导诊</div>
+            <div style="background:white;border:1px solid #dfecea;border-radius:14px;padding:14px;font-weight:700">查医生</div>
+            <div style="background:white;border:1px solid #dfecea;border-radius:14px;padding:14px;font-weight:700">路线引导</div>
+          </div>
+          <div style="margin-top:24px;background:white;border:1px solid #dce9e6;border-radius:18px;padding:16px">
+            <div style="font-size:13px;color:#60706c;margin-bottom:8px">你可以直接说</div>
+            <div style="font-size:20px;font-weight:800">“我头疼三天,还有点恶心,想挂号”</div>
+          </div>
+        </div>
+        <div style="margin:0 26px 24px;display:flex;background:white;border:1px solid #dce9e6;border-radius:999px;padding:10px 12px;align-items:center;gap:10px">
+          <div style="flex:1;color:#9aa6a3">输入或语音说明你的需求</div>
+          <div style="background:#16a37b;color:white;border-radius:999px;padding:9px 16px;font-weight:800">发送</div>
+        </div>
+      </div>
+    </div>
+    <div class="card-body">
+      <h3>A. 亲和陪伴式</h3>
+      <p>大助手形象 + 少量快捷入口 + 大输入框。最像“健康朋友”,适合甲方演示第一眼建立亲近感。</p>
+    </div>
+  </div>
+
+  <div class="card" data-choice="b" onclick="toggleSelect(this)">
+    <div class="card-image">
+      <div style="height:420px;border-radius:18px;background:#f5f8ff;border:1px solid #dbe4f5;overflow:hidden;font-family:Inter,Arial,sans-serif;color:#172033">
+        <div style="display:flex;height:100%">
+          <div style="width:118px;background:#ffffff;border-right:1px solid #e4ebf5;padding:18px 12px">
+            <div style="font-weight:900;margin-bottom:24px">医梦</div>
+            <div style="display:grid;gap:10px;font-size:13px">
+              <div style="background:#e9f1ff;border-radius:12px;padding:10px;font-weight:800">挂号</div>
+              <div style="padding:10px;color:#68758a">导诊</div>
+              <div style="padding:10px;color:#68758a">医生</div>
+              <div style="padding:10px;color:#68758a">路线</div>
+            </div>
+          </div>
+          <div style="flex:1;padding:18px">
+            <div style="display:flex;justify-content:space-between;margin-bottom:14px">
+              <div style="font-size:20px;font-weight:900">门诊统一入口</div>
+              <div style="font-size:12px;color:#637186">EMOON-KIOSK-001</div>
+            </div>
+            <div style="display:grid;grid-template-columns:1.1fr .9fr;gap:14px;height:335px">
+              <div style="background:white;border:1px solid #dfe7f2;border-radius:16px;padding:16px;display:flex;flex-direction:column">
+                <div style="background:#f2f6ff;border-radius:14px;padding:12px;margin-bottom:10px;font-size:14px">您好,请描述症状或选择服务。</div>
+                <div style="align-self:flex-end;background:#316ff6;color:white;border-radius:14px;padding:12px;font-size:14px">我想挂神经内科</div>
+                <div style="margin-top:auto;border:1px solid #e2e8f2;border-radius:999px;padding:10px;color:#8a96a8">继续输入...</div>
+              </div>
+              <div style="display:grid;gap:12px">
+                <div style="background:white;border:1px solid #dfe7f2;border-radius:16px;padding:14px">
+                  <div style="font-size:13px;color:#6c798c">当前流程</div>
+                  <div style="font-weight:900;margin-top:6px">挂号 · 选择科室</div>
+                  <div style="height:8px;background:#e8eef7;border-radius:99px;margin-top:14px"><div style="width:38%;height:8px;background:#316ff6;border-radius:99px"></div></div>
+                </div>
+                <div style="background:white;border:1px solid #dfe7f2;border-radius:16px;padding:14px">
+                  <div style="font-weight:900;margin-bottom:10px">推荐科室</div>
+                  <div style="border:1px solid #e4ebf5;border-radius:12px;padding:10px;margin-bottom:8px">神经内科</div>
+                  <div style="border:1px solid #e4ebf5;border-radius:12px;padding:10px">普通内科</div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="card-body">
+      <h3>B. 办理工作台式</h3>
+      <p>左侧服务导航 + 中间对话 + 右侧流程/卡片。更像自助机业务界面,现场办理效率高。</p>
+    </div>
+  </div>
+
+  <div class="card" data-choice="c" onclick="toggleSelect(this)">
+    <div class="card-image">
+      <div style="height:420px;border-radius:18px;background:#fbfbf8;border:1px solid #e7e3d8;overflow:hidden;font-family:Inter,Arial,sans-serif;color:#28251f">
+        <div style="padding:18px 22px;display:flex;justify-content:space-between;align-items:center">
+          <div>
+            <div style="font-size:13px;color:#817a6b">医梦未来医院</div>
+            <div style="font-size:24px;font-weight:900">今天想办理什么?</div>
+          </div>
+          <div style="width:54px;height:54px;border-radius:50%;background:#1f9d7a;color:white;display:grid;place-items:center;font-weight:900">AI</div>
+        </div>
+        <div style="padding:0 22px;display:grid;grid-template-columns:repeat(3,1fr);gap:12px">
+          <div style="background:white;border:1px solid #e6e0d5;border-radius:16px;padding:16px;height:86px"><b>智能挂号</b><br><span style="font-size:12px;color:#7b7468">症状导诊选医生</span></div>
+          <div style="background:white;border:1px solid #e6e0d5;border-radius:16px;padding:16px;height:86px"><b>查科室</b><br><span style="font-size:12px;color:#7b7468">位置与擅长</span></div>
+          <div style="background:white;border:1px solid #e6e0d5;border-radius:16px;padding:16px;height:86px"><b>查路线</b><br><span style="font-size:12px;color:#7b7468">楼层导航</span></div>
+        </div>
+        <div style="margin:18px 22px;background:white;border:1px solid #e6e0d5;border-radius:20px;padding:16px;height:180px">
+          <div style="display:flex;gap:12px;margin-bottom:12px">
+            <div style="width:38px;height:38px;border-radius:50%;background:#dff3ea"></div>
+            <div style="background:#f5f3ed;border-radius:14px;padding:10px 12px;flex:1">可以直接告诉我症状,我会推荐科室,再帮你选择医生和时间。</div>
+          </div>
+          <div style="display:flex;gap:8px;flex-wrap:wrap">
+            <span style="border:1px solid #ded7cb;border-radius:999px;padding:8px 10px;font-size:12px">头疼想挂号</span>
+            <span style="border:1px solid #ded7cb;border-radius:999px;padding:8px 10px;font-size:12px">明天上午有号吗</span>
+            <span style="border:1px solid #ded7cb;border-radius:999px;padding:8px 10px;font-size:12px">找李明医生</span>
+          </div>
+        </div>
+        <div style="margin:0 22px;background:#202720;color:white;border-radius:20px;padding:14px;display:flex;justify-content:space-between;align-items:center">
+          <span>按住说话或输入文字</span>
+          <span style="background:white;color:#202720;border-radius:999px;padding:8px 14px;font-weight:800">开始</span>
+        </div>
+      </div>
+    </div>
+    <div class="card-body">
+      <h3>C. 服务入口式</h3>
+      <p>先服务卡片,后对话承接。更清晰、更不容易迷路,适合大屏/自助机,但“朋友感”弱一些。</p>
+    </div>
+  </div>
+</div>
+
+<div class="section">
+  <h3>建议</h3>
+  <p>我建议用 <strong>A + B 的混合</strong>:第一屏保持 A 的亲和感,对话进入挂号后切到 B 的右侧流程卡片区。这样既像“AI 健康朋友”,又能支撑真实挂号办理。</p>
+</div>

+ 575 - 0
docs/superpowers/plans/2026-06-02-terminal-demo-vertical-task-breakdown.md

@@ -0,0 +1,575 @@
+# 统一入口客户端演示闭环纵向任务拆分
+
+> 用途:团队任务分配。每个任务包按“用户可感知的纵向能力”拆分,尽量覆盖前端、后端、Mock HIS、测试和验收,而不是按纯技术层横切。
+
+## 公共必读文档
+
+所有参与人员开工前必须读透:
+
+| 文档 | 必须理解的点 |
+| --- | --- |
+| `docs/架构文档/AI中台工程约束.md` | 模块边界、依赖方向、Controller 不直连 Mapper、Card 不绕过 Agent 调 MCP |
+| `docs/架构文档/工程规约生效说明.md` | 架构测试和新 AI 模块落位规则 |
+| `docs/接口文档/医梦AI中台对外接口设计文档_v1.2_正式联调基准版.md` | 终端调用白名单、SSE、Card Action、HIS Tool 风险分级、Mock/真实适配边界 |
+| `docs/架构文档/统一入口客户端技术设计文档_v1.0.md` | 统一入口整体架构、AgentRouter、TaskState、Card Runtime、MCP/HIS Adapter 职责 |
+| `docs/接口文档/terminal-client-mvp-contract.md` | 前端真实调用接口、SSE 事件、卡片动作、错误码 |
+| `docs/需求文档/统一入口客户端v0.1设计方案.md` | 演示范围冻结、DeepSeek/Dify/Card/MCP 分工、SQLite Mock HIS 口径 |
+| `docs/superpowers/specs/2026-06-02-terminal-web-demo-design.md` | 前端 UI 定稿:A+B、品牌色、对话优先、右侧医生形象/卡片状态 |
+| `docs/superpowers/plans/2026-06-02-unified-entry-registration-demo.md` | 后端挂号演示闭环详细任务 |
+| `docs/superpowers/plans/2026-06-02-terminal-web-demo.md` | 前端 Web Demo 详细任务 |
+| `.superpowers/brainstorm/86383-1780383912/content/ui-direction-options.html` | 初始 UI 方向对比,理解为什么最终选择单应用 Web Demo 和 A+B 混合布局 |
+| `.superpowers/brainstorm/86383-1780383912/content/registration-flow-ab.html` | A+B 挂号交互流,理解对话区、卡片区、流程状态的协同方式 |
+| `.superpowers/brainstorm/86383-1780383912/content/final-web-demo-design-v4.html` | 最终视觉基线,重点理解品牌色、无左侧功能菜单、右侧医生静态形象 idle 状态 |
+
+## 任务包总览
+
+| 编号 | 任务包 | 推荐负责人 | 主要产物 | 依赖 | 预估工作量 |
+| --- | --- | --- | --- | --- | --- |
+| T0 | 演示契约与联调基线 | 技术负责人/后端负责人 | 冻结接口、卡片 schema、演示数据口径 | 无 | 0.5 人日 |
+| T1 | SQLite Mock HIS 挂号服务 | 后端 | 独立 Mock HIS 服务、SQLite seed、HIS API | T0 | 1.5 人日 |
+| T2 | MCP Tool + HIS Adapter 纵向工具链 | 后端 | HIS 工具注册、MockHisClient、工具调用日志 | T1 | 1 人日 |
+| T3 | OpenPlatform 终端入口与 SSE 会话 | 后端 | 设备/鉴权/SSE/会话入口可用 | T0 | 1 人日 |
+| T4 | AgentRouter + TaskState 挂号任务主链路 | 后端/AI | 意图识别、任务状态、Dify 输出归一 | T3 | 1 人日 |
+| T5 | Card Runtime 挂号动作闭环 | 后端 | 科室/医生/时间/支付/成功卡片动作链 | T2、T4 | 1.5 人日 |
+| T6 | Web Demo UI Shell 与品牌视觉 | 前端 | 三栏布局、医生静态形象、主题色 | T0 | 1 人日 |
+| T7 | Web Demo 对话入口与 SSE 消费 | 前端 | 中间对话流、输入/语音按钮、SSE parser | T3、T6 | 1 人日 |
+| T8 | Web Demo 挂号卡片流 | 前端 | 右侧卡片专区、卡片组件、幂等提交 | T5、T7 | 1.5 人日 |
+| T9 | Mock 支付与挂号成功演示 | 前后端联合 | 支付二维码/模拟支付/成功卡/Mock 标识 | T1、T5、T8 | 1 人日 |
+| T10 | 端到端验收与演示脚本 | QA/全栈 | curl 脚本、Playwright、演示证据包 | T1-T9 | 1 人日 |
+
+---
+
+## T0. 演示契约与联调基线
+
+**目标**  
+冻结前后端统一口径,避免开发过程中各自发明字段、卡片、状态和 Mock 文案。
+
+**预估工作量**  
+0.5 人日。
+
+**交付范围**
+
+- 确认本轮只做 Web 浏览器演示优先,视口 `1440x900` / `1920x1080`。
+- 确认本轮只完整做挂号闭环。
+- 冻结 cardKey:
+  - `department-selection`
+  - `doctor-selection`
+  - `time-slot-selection`
+  - `confirm-appointment`
+  - `payment-qrcode`
+  - `appointment-success`
+- 冻结 SSE 事件:
+  - `task_updated`
+  - `message_delta`
+  - `message_completed`
+  - `card_created`
+  - `error`
+  - `completed`
+- 冻结 Mock 标识文案:`联调演示 / Mock 支付`、`联调演示`。
+
+**开发前必须彻底理解**
+
+- `docs/接口文档/terminal-client-mvp-contract.md`
+- `docs/需求文档/统一入口客户端v0.1设计方案.md`
+- `docs/superpowers/specs/2026-06-02-terminal-web-demo-design.md`
+- `docs/superpowers/plans/2026-06-02-unified-entry-registration-demo.md`
+- `docs/superpowers/plans/2026-06-02-terminal-web-demo.md`
+
+**验收标准**
+
+- 前后端共同确认一份字段表和卡片链路。
+- 任何新增 cardKey/actionName 必须先更新契约文档。
+
+---
+
+## T1. SQLite Mock HIS 挂号服务
+
+**目标**  
+提供真实可演示的 HIS 业务语义,不能用随机数据或内存假成功。
+
+**预估工作量**  
+1.5 人日。
+
+**交付范围**
+
+- 独立 `mock-his-service`,不进入后端 Maven reactor。
+- SQLite 数据库和 seed 数据。
+- 科室、医生、排班、号源、锁号、Mock 支付、预约接口。
+- 号源锁定 5 分钟过期。
+- 支付成功后才能创建预约。
+- 幂等键重复提交返回首次结果。
+- 号源余量真实扣减。
+
+**涉及模块/路径**
+
+- `mock-his-service/`
+- `mock-his-service/src/main/resources/schema.sql`
+- `mock-his-service/src/main/resources/seed.sql`
+- `mock-his-service/src/test/java/com/emoon/mockhis/*`
+
+**开发前必须彻底理解**
+
+- `docs/需求文档/统一入口客户端v0.1设计方案.md` 的 `3.1 Step 1`、`10.1 Mock HIS`
+- `docs/superpowers/plans/2026-06-02-unified-entry-registration-demo.md` 的 `Task 1: Build SQLite Mock HIS Service`
+- `docs/接口文档/医梦AI中台对外接口设计文档_v1.2_正式联调基准版.md` 中写操作幂等和 Mock/Real Adapter 边界
+
+**验收标准**
+
+- `mvn -f mock-his-service/pom.xml -DskipTests=false test` 通过。
+- 重复锁号/预约不会重复扣号。
+- 未支付订单不能创建预约。
+- 返回结果包含 `mock:true`。
+
+---
+
+## T2. MCP Tool + HIS Adapter 纵向工具链
+
+**目标**  
+AI 中台通过统一工具出口调用 Mock HIS,Dify 和前端都不能绕过。
+
+**预估工作量**  
+1 人日。
+
+**交付范围**
+
+- 扩展 `McpToolService` 挂号工具:
+  - 查询科室/医生/排班
+  - 锁号/释放锁
+  - 创建 Mock 支付订单
+  - 标记 Mock 支付成功
+  - 创建预约
+- `MockHisClient` 对接 SQLite Mock HIS。
+- 工具调用带 `traceId`、风险等级、幂等键。
+- 写操作只允许 Card Action 链路触发。
+
+**涉及模块/路径**
+
+- `emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp`
+- `com.emoon.ai.mcp.application.McpToolService`
+- `com.emoon.mcp.his.client.MockHisClient`
+- `emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/test/java/com/emoon/ai/mcp/application/McpToolServiceTest.java`
+
+**开发前必须彻底理解**
+
+- `docs/架构文档/AI中台工程约束.md` 中 MCP、Card、Agent 依赖方向
+- `docs/架构文档/统一入口客户端技术设计文档_v1.0.md` 的 `11. MCP/HIS Adapter 设计`
+- `docs/需求文档/统一入口客户端v0.1设计方案.md` 的 `3.2 Step 2`
+- `docs/superpowers/plans/2026-06-02-unified-entry-registration-demo.md` 的 `Task 2`
+
+**验收标准**
+
+- `mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp -DskipTests=false test` 通过。
+- 日志能追踪每次工具调用的 `traceId`。
+- 工具返回 Mock 标识,不伪装真实支付/真实 HIS。
+
+---
+
+## T3. OpenPlatform 终端入口与 SSE 会话
+
+**目标**  
+Web 客户端能通过统一入口发起对话,并收到稳定 SSE 事件和卡片创建事件。
+
+**预估工作量**  
+1 人日。
+
+**交付范围**
+
+- 新终端端点鉴权/HMAC 或演示模式兼容。
+- `/api/v1/agent/chat/stream`。
+- 设备上下文解析。
+- conversation 创建/恢复。
+- SSE 事件顺序稳定:
+  - `task_updated`
+  - `message_delta`
+  - `message_completed`
+  - `card_created`
+  - `completed`
+- 错误事件包含可展示信息和 `traceId`。
+
+**涉及模块/路径**
+
+- `emoon-openplatform/src/main/java/com/emoon/openplatform/controller/v1/AgentChatController.java`
+- `emoon-openplatform/src/main/java/com/emoon/openplatform/service/impl/AgentChatApplicationServiceImpl.java`
+- `emoon-openplatform/src/test/java/com/emoon/openplatform/acceptance/TerminalMvpAcceptanceTest.java`
+
+**开发前必须彻底理解**
+
+- `docs/接口文档/terminal-client-mvp-contract.md` 的 `4. 流式对话`
+- `docs/架构文档/统一入口客户端前端接入指引_v1.0.md` 的 SSE 部分
+- `docs/架构文档/AI中台工程约束.md` 中 openplatform 禁止直连 Mapper
+- `docs/superpowers/plans/2026-06-02-unified-entry-registration-demo.md` 的 `Task 4`
+
+**验收标准**
+
+- `mvn -pl emoon-openplatform -DskipTests=false -Dtest=TerminalMvpAcceptanceTest test` 通过。
+- 前端不传 `agentId` 也能正确路由到挂号任务。
+- SSE 断开时前端能保留 `conversationId`。
+
+---
+
+## T4. AgentRouter + TaskState 挂号任务主链路
+
+**目标**  
+让系统表现得“智能”:由自然语言识别任务,而不是靠左侧菜单按钮进入流程。
+
+**预估工作量**  
+1 人日。
+
+**交付范围**
+
+- AgentRouter 优先级:
+  - 设备策略
+  - activeTask
+  - waitingCard
+  - 确定性规则
+  - DeepSeek JSON 分类
+  - 低置信度澄清
+- REGISTRATION task 创建和状态推进。
+- 用户说“下午”等上下文输入时,不重新走 DeepSeek 路由。
+- Dify 输出校验/归一化,非法 JSON 降级。
+
+**涉及模块/路径**
+
+- `emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent`
+- `AgentRouterService`
+- `TaskStateService`
+- `IntentClassifier`
+- `DifyOutputNormalizer`
+- `TerminalReplyTemplateService`
+
+**开发前必须彻底理解**
+
+- `docs/需求文档/统一入口客户端v0.1设计方案.md` 的 `3.3 Step 3`、`6. AgentRouter`
+- `docs/架构文档/统一入口客户端技术设计文档_v1.0.md` 的 `7. AgentRouter 设计`、`9. TaskStateService 设计`
+- `docs/superpowers/plans/2026-06-02-unified-entry-registration-demo.md` 的 `Task 4`、`Task 5`
+
+**验收标准**
+
+- `mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent -DskipTests=false test` 通过。
+- activeTask 场景不被 DeepSeek 错误切换。
+- Dify 不直接写 HIS。
+
+---
+
+## T5. Card Runtime 挂号动作闭环
+
+**目标**  
+让每一次用户选择和确认都经过 Card Action,保障幂等、审计、状态推进。
+
+**预估工作量**  
+1.5 人日。
+
+**交付范围**
+
+- 卡片动作链:
+  - `select_department`
+  - `select_doctor`
+  - `select_time_slot`
+  - `confirm_appointment`
+  - `mock_payment_paid`
+- `select_time_slot` 后锁号。
+- `confirm_appointment` 后创建 Mock 支付订单。
+- `mock_payment_paid` 后创建预约。
+- 重复点击同一动作不创建重复业务结果。
+- Card Runtime 不直接调用 MCP,必须经 AgentActionOrchestrator。
+
+**涉及模块/路径**
+
+- `emoon-infra/emoon-modules/emoon-ai/emoon-ai-card`
+- `emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent/src/main/java/com/emoon/ai/agent/application/AgentActionOrchestrator.java`
+- `CardActionService`
+- `CardInstanceService`
+
+**开发前必须彻底理解**
+
+- `docs/架构文档/AI中台工程约束.md` 中 Card 不绕过 Agent 调 MCP 红线
+- `docs/架构文档/统一入口客户端技术设计文档_v1.0.md` 的 `10. Card Runtime 设计`
+- `docs/接口文档/terminal-client-mvp-contract.md` 的 `5. 卡片动作`
+- `docs/superpowers/plans/2026-06-02-unified-entry-registration-demo.md` 的 `Task 3`
+
+**验收标准**
+
+- `mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent,emoon-infra/emoon-modules/emoon-ai/emoon-ai-card -DskipTests=false test` 通过。
+- 重复 idempotencyKey 返回首次结果。
+- `appointment-success` 来自 Mock HIS 真实预约结果。
+
+---
+
+## T6. Web Demo UI Shell 与品牌视觉
+
+**目标**  
+做出甲方能看的第一屏:对话优先、品牌色正确、右侧静态医生形象,不再使用粗糙 CSS 图形。
+
+**预估工作量**  
+1 人日。
+
+**交付范围**
+
+- 独立 `emoon-terminal-client` 单应用。
+- Vue 3 + TypeScript + Vite。
+- 三栏布局:
+  - 左:助手上下文,不是菜单
+  - 中:对话和输入
+  - 右:Context Panel
+- 品牌色:
+  - `#2b1f99`
+  - `#3ad4d8`
+- 医生形象使用真实 PNG 资产。
+- 右侧 idle 状态只展示医生形象,不放语音/发送按钮。
+
+**涉及模块/路径**
+
+- `/Users/destiny/dev/emoon/emoon-terminal-client`
+- `src/theme/tokens.css`
+- `src/layouts/AppShell.vue`
+- `src/layouts/AssistantContextPanel.vue`
+- `src/layouts/DoctorAssistantFigure.vue`
+- `src/assets/doctor-assistant.png`
+
+**开发前必须彻底理解**
+
+- `docs/superpowers/specs/2026-06-02-terminal-web-demo-design.md`
+- `docs/superpowers/plans/2026-06-02-terminal-web-demo.md` 的 `Task 1`、`Task 2`、`Task 3`
+- `docs/架构文档/统一入口客户端前端工程启动指南.md`
+- `.superpowers/brainstorm/86383-1780383912/content/ui-direction-options.html`
+- `.superpowers/brainstorm/86383-1780383912/content/final-web-demo-design-v4.html`
+
+**验收标准**
+
+- `pnpm build` 能通过。
+- 页面在 `1440x900` 无横向滚动。
+- 左侧没有大功能按钮或 agent 菜单。
+- 右侧医生形象质量达标,不使用 CSS 拼图。
+
+---
+
+## T7. Web Demo 对话入口与 SSE 消费
+
+**目标**  
+前端中间区域承载输入、语音按钮、发送按钮,并能消费后端 SSE。
+
+**预估工作量**  
+1 人日。
+
+**交付范围**
+
+- `ConversationPanel`
+- `MessageBubble`
+- `ChatInput`
+- SSE parser。
+- 语音按钮首版只展示提示,不实现 STT。
+- 用户输入后能追加消息。
+- 收到 `card_created` 后切换右侧为卡片。
+
+**涉及模块/路径**
+
+- `src/chat/ConversationPanel.vue`
+- `src/chat/ChatInput.vue`
+- `src/chat/MessageBubble.vue`
+- `src/api/sse.ts`
+- `src/state/terminalStore.ts`
+
+**开发前必须彻底理解**
+
+- `docs/superpowers/specs/2026-06-02-terminal-web-demo-design.md` 的 `Center Conversation Panel`、`Interaction Flow`
+- `docs/接口文档/terminal-client-mvp-contract.md` 的 `4. 流式对话`
+- `docs/架构文档/统一入口客户端前端接入指引_v1.0.md` 的 `4. 流式对话(SSE)`
+- `docs/superpowers/plans/2026-06-02-terminal-web-demo.md` 的 `Task 4`、`Task 5`
+- `.superpowers/brainstorm/86383-1780383912/content/registration-flow-ab.html`
+
+**验收标准**
+
+- `pnpm vitest run tests/unit/sse.spec.ts` 通过。
+- 语音和发送按钮都在中间输入区。
+- 右侧 idle 不出现语音/发送交互。
+
+---
+
+## T8. Web Demo 挂号卡片流
+
+**目标**  
+前端右侧 Context Panel 能完整渲染挂号卡片并提交动作。
+
+**预估工作量**  
+1.5 人日。
+
+**交付范围**
+
+- Context Panel 状态:
+  - `idle`
+  - `task`
+  - `error`
+  - `completed`
+- 卡片组件:
+  - DepartmentSelectionCard
+  - DoctorSelectionCard
+  - TimeSlotSelectionCard
+  - ConfirmAppointmentCard
+  - PaymentQrCard
+  - AppointmentSuccessCard
+  - ErrorCard
+- 每个动作带 `idempotencyKey`。
+- 成功后点击 `完成` 回到 idle 医生形象。
+
+**涉及模块/路径**
+
+- `src/layouts/ContextPanel.vue`
+- `src/layouts/RegistrationProgress.vue`
+- `src/cards/*`
+- `src/api/client.ts`
+- `src/api/demoFixtures.ts`
+
+**开发前必须彻底理解**
+
+- `docs/superpowers/specs/2026-06-02-terminal-web-demo-design.md` 的 `Right Context Panel`、`Card Flow`
+- `docs/接口文档/terminal-client-mvp-contract.md` 的 `5. 卡片动作`、`11. 前端卡片组件映射`
+- `docs/superpowers/plans/2026-06-02-terminal-web-demo.md` 的 `Task 6`、`Task 7`
+- `.superpowers/brainstorm/86383-1780383912/content/final-web-demo-design-v4.html`
+
+**验收标准**
+
+- `pnpm vitest run tests/unit/cardRenderer.spec.ts` 通过。
+- 卡片顺序正确。
+- `PaymentQrCard` 必须显示 `联调演示 / Mock 支付`。
+- `AppointmentSuccessCard` 必须显示 `联调演示`。
+
+---
+
+## T9. Mock 支付与挂号成功联合闭环
+
+**目标**  
+打通“确认挂号 → Mock 支付 → 支付完成 → 预约成功”这一段最容易被甲方追问的链路。
+
+**预估工作量**  
+1 人日。
+
+**交付范围**
+
+- 后端:
+  - 创建 Mock 支付订单。
+  - 标记 Mock 支付完成。
+  - 支付成功后创建预约。
+- 前端:
+  - 显示 Mock 支付卡。
+  - 显示模拟支付完成按钮。
+  - 显示挂号成功卡。
+- 演示文案明确当前是 Mock 支付和 Mock HIS。
+
+**涉及模块/路径**
+
+- `mock-his-service`
+- `emoon-ai-mcp`
+- `emoon-ai-agent`
+- `emoon-ai-card`
+- `emoon-terminal-client/src/cards/PaymentQrCard.vue`
+- `emoon-terminal-client/src/cards/AppointmentSuccessCard.vue`
+
+**开发前必须彻底理解**
+
+- `docs/需求文档/统一入口客户端v0.1设计方案.md` 的 `4.7 支付二维码`、`4.8 支付成功后创建预约`
+- `docs/superpowers/plans/2026-06-02-unified-entry-registration-demo.md` 的 `Task 1`、`Task 2`、`Task 3`
+- `docs/superpowers/plans/2026-06-02-terminal-web-demo.md` 的 `Task 6`
+- `docs/接口文档/医梦AI中台对外接口设计文档_v1.2_正式联调基准版.md` 中写操作和财务动作风险控制
+
+**验收标准**
+
+- 未支付不能创建预约。
+- 支付卡和成功卡都显示 Mock/联调演示。
+- 预约号来自后端/Mock HIS 结果,不由前端编造。
+
+---
+
+## T10. 端到端验收与演示脚本
+
+**目标**  
+形成可重复的演示证据,保证甲方现场不会只靠手动点击“碰巧成功”。
+
+**预估工作量**  
+1 人日。
+
+**交付范围**
+
+- 后端 curl 脚本:
+  - happy path
+  - edge cases
+- 前端 Playwright:
+  - 1440×900 视觉检查
+  - 1920×1080 视觉检查
+  - 挂号闭环点击流程
+- 演示证据包:
+  - conversationId
+  - taskId
+  - cardInstanceId 序列
+  - traceId
+  - appointmentId
+  - appointmentNo
+  - mock:true 响应或截图
+
+**涉及模块/路径**
+
+- `scripts/demo-registration/*`
+- `emoon-terminal-client/tests/e2e/*`
+- `emoon-openplatform/src/test/java/com/emoon/openplatform/acceptance/TerminalMvpAcceptanceTest.java`
+
+**开发前必须彻底理解**
+
+- `docs/superpowers/plans/2026-06-02-unified-entry-registration-demo.md` 的 `Task 6`、`Task 7`
+- `docs/superpowers/plans/2026-06-02-terminal-web-demo.md` 的 `Task 8`、`Task 9`
+- `docs/接口文档/terminal-client-mvp-contract.md` 全文
+- `docs/需求文档/统一入口客户端v0.1设计方案.md` 的 `7. 核心 Case 验证清单`
+
+**验收标准**
+
+- 后端模块测试通过。
+- `mvn -pl emoon-openplatform -DskipTests=false test` 通过。
+- `mvn -pl emoon-admin -DskipTests=false -Dprofiles.active= -Dtest=AiPlatformArchitectureTest test` 通过。
+- 前端 `pnpm build && pnpm test:unit && pnpm test:e2e` 通过。
+- 演示前能一键重置 Mock HIS 数据。
+
+---
+
+## 推荐分配方式
+
+| 小组/人 | 建议负责 |
+| --- | --- |
+| 后端 A | T1 SQLite Mock HIS |
+| 后端 B | T2 MCP Tool + T5 Card Action |
+| 后端 C / AI | T3 OpenPlatform SSE + T4 AgentRouter/Task/Dify |
+| 前端 A | T6 UI Shell + 医生形象 + 品牌视觉 |
+| 前端 B | T7 对话/SSE + T8 卡片流 |
+| 全栈/QA | T9 支付成功联合闭环 + T10 验收脚本 |
+| 技术负责人 | T0 契约冻结、跨任务 review、最终架构测试 |
+
+## 工作量汇总
+
+| 范围 | 包含任务 | 预估工作量 |
+| --- | --- | --- |
+| 契约冻结 | T0 | 0.5 人日 |
+| 后端/Mock HIS/AI 中台 | T1、T2、T3、T4、T5 | 6 人日 |
+| 前端 Web Demo | T6、T7、T8 | 3.5 人日 |
+| 前后端联调和验收 | T9、T10 | 2 人日 |
+| 合计 | T0-T10 | 12 人日 |
+| 建议风险缓冲 | Dify 输出不稳定、SSE 兼容、Mock HIS 边界补漏、视觉微调 | 2-3 人日 |
+
+建议按 14-15 人日安排总容量。若投入 3 名后端、2 名前端、1 名全栈/QA,并且 T0 在首日上午冻结,理想情况下 4-5 个自然工作日可以完成可演示闭环;如果 Dify 工作流需要重新设计或真实 MCP 注册环境不可用,需要额外预留 1-2 天。
+
+## 关键依赖顺序
+
+```text
+T0
+├── T1 ── T2 ── T5 ── T9
+├── T3 ── T4 ────────┘
+└── T6 ── T7 ── T8 ─┘
+                    ↓
+                   T10
+```
+
+## 每日联调检查点
+
+| 时间点 | 必查内容 |
+| --- | --- |
+| Day 1 | T0 契约冻结,Mock HIS seed 数据可跑,前端 shell 可启动 |
+| Day 2 | SSE 能返回 `task_updated/card_created`,前端能显示医生 idle 形象和首张科室卡 |
+| Day 3 | 科室→医生→时间卡片链路跑通,锁号真实落库 |
+| Day 4 | Mock 支付→预约成功跑通,前端显示 Mock 标识 |
+| Day 5 | curl + Playwright 全链路验收,准备演示证据包 |

+ 2109 - 0
docs/superpowers/plans/2026-06-02-terminal-web-demo.md

@@ -0,0 +1,2109 @@
+# Terminal Web Demo Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Build a single Vue 3 Web demo for the unified entry terminal client that presents a dialogue-first AI hospital assistant and completes the outpatient registration card flow.
+
+**Architecture:** Create an independent frontend app in `emoon-terminal-client`, outside the backend Maven reactor. The app has a three-column desktop layout: left assistant context, center conversation input/stream, right context panel. It talks only to OpenPlatform terminal endpoints through an API client, supports local demo fixtures when backend is unavailable, and uses Playwright to verify desktop visual and interaction behavior.
+
+**Tech Stack:** Vue 3, TypeScript, Vite, Pinia, Vue Router only if needed, native `fetch` streaming parser for SSE, CSS variables for theme tokens, Vitest, Vue Test Utils, Playwright, generated/static TypeScript API types from backend contract.
+
+---
+
+## Source Documents
+
+Read these before implementation:
+
+- `docs/superpowers/specs/2026-06-02-terminal-web-demo-design.md`
+- `docs/接口文档/terminal-client-mvp-contract.md`
+- `docs/架构文档/统一入口客户端前端工程启动指南.md`
+- `docs/架构文档/统一入口客户端前端接入指引_v1.0.md`
+- `docs/superpowers/plans/2026-06-02-unified-entry-registration-demo.md`
+
+## Target Repository
+
+Create or work in:
+
+```text
+/Users/destiny/dev/emoon/emoon-terminal-client
+```
+
+This frontend repository is intentionally separate from:
+
+```text
+/Users/destiny/dev/emoon/emoon-backend
+```
+
+Do not add frontend source files to the backend Maven reactor.
+
+## Target File Structure
+
+```text
+emoon-terminal-client
+├── package.json
+├── pnpm-lock.yaml
+├── vite.config.ts
+├── tsconfig.json
+├── index.html
+├── playwright.config.ts
+├── src
+│   ├── App.vue
+│   ├── main.ts
+│   ├── assets
+│   │   └── doctor-assistant.png
+│   ├── api
+│   │   ├── client.ts
+│   │   ├── demoFixtures.ts
+│   │   ├── hmac.ts
+│   │   ├── sse.ts
+│   │   └── types.ts
+│   ├── cards
+│   │   ├── CardRenderer.vue
+│   │   ├── DepartmentSelectionCard.vue
+│   │   ├── DoctorSelectionCard.vue
+│   │   ├── TimeSlotSelectionCard.vue
+│   │   ├── ConfirmAppointmentCard.vue
+│   │   ├── PaymentQrCard.vue
+│   │   ├── AppointmentSuccessCard.vue
+│   │   └── ErrorCard.vue
+│   ├── chat
+│   │   ├── ConversationPanel.vue
+│   │   ├── MessageBubble.vue
+│   │   └── ChatInput.vue
+│   ├── layouts
+│   │   ├── AppShell.vue
+│   │   ├── AssistantContextPanel.vue
+│   │   ├── ContextPanel.vue
+│   │   ├── DoctorAssistantFigure.vue
+│   │   └── RegistrationProgress.vue
+│   ├── state
+│   │   └── terminalStore.ts
+│   ├── theme
+│   │   └── tokens.css
+│   └── demo
+│       └── demoMode.ts
+├── tests
+│   ├── unit
+│   │   ├── cardRenderer.spec.ts
+│   │   ├── sse.spec.ts
+│   │   └── terminalStore.spec.ts
+│   └── e2e
+│       ├── registration-flow.spec.ts
+│       └── visual-layout.spec.ts
+└── README.md
+```
+
+## Task 1: Scaffold The Vue Web Demo App
+
+**Files:**
+
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/package.json`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/vite.config.ts`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/tsconfig.json`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/index.html`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/main.ts`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/App.vue`
+
+- [ ] **Step 1: Create package manifest**
+
+Use this dependency baseline:
+
+```json
+{
+  "name": "emoon-terminal-client",
+  "version": "0.1.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite --host 127.0.0.1",
+    "build": "vue-tsc --noEmit && vite build",
+    "preview": "vite preview --host 127.0.0.1",
+    "test:unit": "vitest run",
+    "test:e2e": "playwright test",
+    "test": "pnpm test:unit && pnpm test:e2e"
+  },
+  "dependencies": {
+    "@vitejs/plugin-vue": "^5.2.0",
+    "pinia": "^2.3.1",
+    "vue": "^3.5.13"
+  },
+  "devDependencies": {
+    "@playwright/test": "^1.49.1",
+    "@types/node": "^22.10.2",
+    "@vue/test-utils": "^2.4.6",
+    "jsdom": "^25.0.1",
+    "typescript": "^5.7.2",
+    "vite": "^6.0.5",
+    "vitest": "^2.1.8",
+    "vue-tsc": "^2.2.0"
+  }
+}
+```
+
+- [ ] **Step 2: Install dependencies**
+
+Run:
+
+```bash
+cd /Users/destiny/dev/emoon/emoon-terminal-client
+pnpm install
+```
+
+Expected: `pnpm-lock.yaml` is created and install succeeds.
+
+- [ ] **Step 3: Create minimal Vue entry**
+
+`src/main.ts`:
+
+```ts
+import { createPinia } from 'pinia';
+import { createApp } from 'vue';
+import App from './App.vue';
+import './theme/tokens.css';
+
+createApp(App).use(createPinia()).mount('#app');
+```
+
+`src/App.vue`:
+
+```vue
+<template>
+  <AppShell />
+</template>
+
+<script setup lang="ts">
+import AppShell from './layouts/AppShell.vue';
+</script>
+```
+
+- [ ] **Step 4: Run build and verify failure until later layout files exist**
+
+Run:
+
+```bash
+pnpm build
+```
+
+Expected: fails with missing `./layouts/AppShell.vue`. This confirms the app entry is wired to the planned layout component.
+
+**Commit:** `feat: scaffold terminal web demo`
+
+## Task 2: Add Theme Tokens And Doctor Asset
+
+**Files:**
+
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/theme/tokens.css`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/assets/doctor-assistant.png`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/layouts/DoctorAssistantFigure.vue`
+- Test: `/Users/destiny/dev/emoon/emoon-terminal-client/tests/unit/doctorAssistantFigure.spec.ts`
+
+- [ ] **Step 1: Copy generated doctor image**
+
+Copy, do not move or delete the original:
+
+```bash
+cp /Users/destiny/.codex/generated_images/019e8719-cee0-7723-83a4-ac1a9e0a3bd4/ig_037570e350a4f508016a1e882c0c188191b10d3dbe9b5f776a.png /Users/destiny/dev/emoon/emoon-terminal-client/src/assets/doctor-assistant.png
+```
+
+Expected: `src/assets/doctor-assistant.png` exists.
+
+- [ ] **Step 2: Add CSS token file**
+
+`src/theme/tokens.css`:
+
+```css
+:root {
+  --brand-primary: #2b1f99;
+  --brand-accent: #3ad4d8;
+  --page-bg: #f7fbff;
+  --surface: #ffffff;
+  --soft-tint: #f4fdff;
+  --border: #dfe7f7;
+  --text-primary: #16133a;
+  --text-secondary: #69708a;
+  --warning-bg: #fff8e8;
+  --warning-text: #735b14;
+  --shadow-soft: 0 20px 48px rgba(43, 31, 153, 0.12);
+  --radius-panel: 16px;
+  --radius-card: 14px;
+  font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+html,
+body,
+#app {
+  width: 100%;
+  min-width: 1280px;
+  min-height: 100%;
+  margin: 0;
+  background: var(--page-bg);
+  color: var(--text-primary);
+}
+
+button,
+input,
+textarea {
+  font: inherit;
+}
+```
+
+- [ ] **Step 3: Write failing doctor component test**
+
+`tests/unit/doctorAssistantFigure.spec.ts`:
+
+```ts
+import { mount } from '@vue/test-utils';
+import { describe, expect, it } from 'vitest';
+import DoctorAssistantFigure from '../../src/layouts/DoctorAssistantFigure.vue';
+
+describe('DoctorAssistantFigure', () => {
+  it('renders the static doctor assistant image and no voice controls', () => {
+    const wrapper = mount(DoctorAssistantFigure);
+
+    expect(wrapper.find('img[alt="医生助手"]').exists()).toBe(true);
+    expect(wrapper.text()).toContain('医生助手');
+    expect(wrapper.text()).toContain('静态形象展示');
+    expect(wrapper.text()).not.toContain('可听');
+    expect(wrapper.text()).not.toContain('可说');
+    expect(wrapper.find('button').exists()).toBe(false);
+  });
+});
+```
+
+- [ ] **Step 4: Run test and verify failure**
+
+Run:
+
+```bash
+pnpm vitest run tests/unit/doctorAssistantFigure.spec.ts
+```
+
+Expected: fails because `DoctorAssistantFigure.vue` does not exist.
+
+- [ ] **Step 5: Implement static doctor figure**
+
+`src/layouts/DoctorAssistantFigure.vue`:
+
+```vue
+<template>
+  <div class="doctor-figure" aria-label="医生助手静态形象">
+    <div class="imageFrame">
+      <img src="../assets/doctor-assistant.png" alt="医生助手" />
+    </div>
+    <h2>医生助手</h2>
+    <p>当前仅作为静态形象展示。语音与发送操作仍在中间输入区完成。</p>
+  </div>
+</template>
+
+<style scoped>
+.doctor-figure {
+  display: flex;
+  min-height: 100%;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 28px;
+  text-align: center;
+}
+
+.imageFrame {
+  display: grid;
+  width: min(280px, 72%);
+  aspect-ratio: 1 / 1;
+  place-items: center;
+  border: 1px solid var(--border);
+  border-radius: 50%;
+  background:
+    radial-gradient(circle at 40% 30%, rgba(58, 212, 216, 0.2), transparent 42%),
+    linear-gradient(180deg, #ffffff, #eef8ff);
+  box-shadow: var(--shadow-soft);
+}
+
+img {
+  max-width: 86%;
+  max-height: 86%;
+  object-fit: contain;
+}
+
+h2 {
+  margin: 22px 0 8px;
+  font-size: 24px;
+}
+
+p {
+  max-width: 280px;
+  margin: 0;
+  color: var(--text-secondary);
+  font-size: 14px;
+  line-height: 1.7;
+}
+```
+
+- [ ] **Step 6: Run test**
+
+Run:
+
+```bash
+pnpm vitest run tests/unit/doctorAssistantFigure.spec.ts
+```
+
+Expected: pass.
+
+**Commit:** `feat: add brand tokens and doctor assistant asset`
+
+## Task 3: Build Three-Column App Shell
+
+**Files:**
+
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/layouts/AppShell.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/layouts/AssistantContextPanel.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/layouts/ContextPanel.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/layouts/RegistrationProgress.vue`
+- Test: `/Users/destiny/dev/emoon/emoon-terminal-client/tests/unit/appShell.spec.ts`
+
+- [ ] **Step 1: Write failing layout test**
+
+`tests/unit/appShell.spec.ts`:
+
+```ts
+import { mount } from '@vue/test-utils';
+import { describe, expect, it } from 'vitest';
+import AppShell from '../../src/layouts/AppShell.vue';
+
+describe('AppShell', () => {
+  it('renders header, assistant context, conversation area, and right context panel', () => {
+    const wrapper = mount(AppShell, {
+      global: {
+        stubs: {
+          ConversationPanel: { template: '<section data-test="conversation-panel" />' },
+          ContextPanel: { template: '<aside data-test="context-panel" />' }
+        }
+      }
+    });
+
+    expect(wrapper.text()).toContain('医梦门诊助手');
+    expect(wrapper.text()).toContain('说出需求,我来判断下一步');
+    expect(wrapper.find('[data-test="conversation-panel"]').exists()).toBe(true);
+    expect(wrapper.find('[data-test="context-panel"]').exists()).toBe(true);
+    expect(wrapper.text()).not.toContain('智能挂号');
+  });
+});
+```
+
+- [ ] **Step 2: Run test and verify failure**
+
+Run:
+
+```bash
+pnpm vitest run tests/unit/appShell.spec.ts
+```
+
+Expected: fails until layout files exist.
+
+- [ ] **Step 3: Implement left assistant panel without workflow buttons**
+
+`src/layouts/AssistantContextPanel.vue`:
+
+```vue
+<template>
+  <aside class="assistant-panel" aria-label="助手上下文">
+    <div class="identity">
+      <div class="avatar">AI</div>
+      <div>
+        <h2>你好,我在</h2>
+        <p>说出需求,我来判断下一步</p>
+      </div>
+    </div>
+
+    <section class="examples">
+      <p class="section-label">可以这样说</p>
+      <ul>
+        <li>“我头疼三天,想挂号”</li>
+        <li>“明天上午有神经内科的号吗”</li>
+        <li>“我想找李明医生”</li>
+      </ul>
+    </section>
+
+    <section class="capabilities">
+      <p class="section-label">当前可办理</p>
+      <div class="chips" aria-label="能力提示">
+        <span>挂号</span>
+        <span>导诊</span>
+        <span>医生查询</span>
+      </div>
+    </section>
+
+    <p class="mock-note">Mock 结果显示“联调演示”,不伪装成真实业务。</p>
+  </aside>
+</template>
+
+<style scoped>
+.assistant-panel {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  min-height: 0;
+  padding: 18px;
+  border: 1px solid var(--border);
+  border-radius: var(--radius-panel);
+  background: linear-gradient(180deg, #ffffff, #f1fbff);
+}
+
+.identity {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.avatar {
+  display: grid;
+  width: 70px;
+  height: 70px;
+  flex: 0 0 auto;
+  place-items: center;
+  border-radius: 50%;
+  background: radial-gradient(circle at 35% 30%, #ffffff, var(--brand-accent) 45%, var(--brand-primary));
+  color: #ffffff;
+  font-weight: 900;
+}
+
+h2 {
+  margin: 0;
+  font-size: 18px;
+}
+
+p {
+  margin: 4px 0 0;
+  color: var(--text-secondary);
+  font-size: 13px;
+}
+
+.section-label {
+  margin-bottom: 10px;
+  color: var(--text-secondary);
+  font-size: 12px;
+}
+
+.examples,
+.capabilities {
+  padding: 14px;
+  border: 1px solid #dce7f8;
+  border-radius: var(--radius-card);
+  background: var(--surface);
+}
+
+ul {
+  display: grid;
+  gap: 8px;
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+li {
+  padding: 9px;
+  border-radius: 10px;
+  background: #f4fbff;
+  font-size: 13px;
+}
+
+.chips {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.chips span {
+  padding: 6px 9px;
+  border: 1px solid #cceff1;
+  border-radius: 999px;
+  color: #156e83;
+  font-size: 12px;
+}
+
+.mock-note {
+  margin-top: auto;
+  padding: 12px;
+  border: 1px solid #f0d89c;
+  border-radius: 12px;
+  background: var(--warning-bg);
+  color: var(--warning-text);
+  line-height: 1.6;
+}
+```
+
+- [ ] **Step 4: Implement app shell**
+
+`src/layouts/AppShell.vue`:
+
+```vue
+<template>
+  <div class="app-shell">
+    <header class="header">
+      <div class="brand">
+        <span class="logo">医</span>
+        <strong>医梦门诊助手</strong>
+      </div>
+      <span class="env">Web Demo · 门诊大厅 · 联调演示</span>
+    </header>
+
+    <div class="workspace">
+      <AssistantContextPanel />
+      <ConversationPanel />
+      <ContextPanel />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import ConversationPanel from '../chat/ConversationPanel.vue';
+import AssistantContextPanel from './AssistantContextPanel.vue';
+import ContextPanel from './ContextPanel.vue';
+</script>
+
+<style scoped>
+.app-shell {
+  min-height: 100vh;
+  padding: 0;
+  background: var(--page-bg);
+}
+
+.header {
+  display: flex;
+  height: 58px;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 22px;
+  border-bottom: 1px solid #e7ebf5;
+  background: var(--surface);
+}
+
+.brand {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.logo {
+  display: grid;
+  width: 34px;
+  height: 34px;
+  place-items: center;
+  border-radius: 10px;
+  background: var(--brand-primary);
+  color: #ffffff;
+  font-weight: 900;
+}
+
+.env {
+  color: var(--text-secondary);
+  font-size: 13px;
+}
+
+.workspace {
+  display: grid;
+  grid-template-columns: 280px minmax(520px, 1fr) 390px;
+  gap: 16px;
+  height: calc(100vh - 58px);
+  min-height: 720px;
+  padding: 16px;
+}
+```
+
+- [ ] **Step 5: Run layout test**
+
+Run:
+
+```bash
+pnpm vitest run tests/unit/appShell.spec.ts
+```
+
+Expected: fails only until `ConversationPanel` and `ContextPanel` are implemented in later tasks.
+
+**Commit:** `feat: build terminal app shell`
+
+## Task 4: Implement Store And Demo Fixtures
+
+**Files:**
+
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/state/terminalStore.ts`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/api/types.ts`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/api/demoFixtures.ts`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/demo/demoMode.ts`
+- Test: `/Users/destiny/dev/emoon/emoon-terminal-client/tests/unit/terminalStore.spec.ts`
+
+- [ ] **Step 1: Write failing store tests**
+
+`tests/unit/terminalStore.spec.ts`:
+
+```ts
+import { createPinia, setActivePinia } from 'pinia';
+import { beforeEach, describe, expect, it } from 'vitest';
+import { useTerminalStore } from '../../src/state/terminalStore';
+
+describe('terminalStore', () => {
+  beforeEach(() => setActivePinia(createPinia()));
+
+  it('starts in idle right-panel mode with no active card', () => {
+    const store = useTerminalStore();
+
+    expect(store.contextPanelMode).toBe('idle');
+    expect(store.activeCard).toBeNull();
+  });
+
+  it('switches to task mode when a card is active', () => {
+    const store = useTerminalStore();
+
+    store.setActiveCard({
+      cardInstanceId: 'card_001',
+      cardKey: 'department-selection',
+      status: 'active',
+      cardData: { departments: [] },
+      actions: [{ actionName: 'select_department', label: '选择科室' }]
+    });
+
+    expect(store.contextPanelMode).toBe('task');
+  });
+
+  it('switches to completed mode for appointment-success card', () => {
+    const store = useTerminalStore();
+
+    store.setActiveCard({
+      cardInstanceId: 'card_005',
+      cardKey: 'appointment-success',
+      status: 'completed',
+      cardData: { appointmentNo: 'A023', mock: true },
+      actions: []
+    });
+
+    expect(store.contextPanelMode).toBe('completed');
+  });
+});
+```
+
+- [ ] **Step 2: Run store test and verify failure**
+
+Run:
+
+```bash
+pnpm vitest run tests/unit/terminalStore.spec.ts
+```
+
+Expected: fails until store exists.
+
+- [ ] **Step 3: Define frontend API types**
+
+`src/api/types.ts`:
+
+```ts
+export type SseEventType =
+  | 'task_updated'
+  | 'message_delta'
+  | 'message_completed'
+  | 'card_created'
+  | 'error'
+  | 'completed';
+
+export interface ChatMessage {
+  id: string;
+  role: 'user' | 'assistant' | 'system';
+  content: string;
+  streaming?: boolean;
+}
+
+export interface CardAction {
+  actionName: string;
+  label: string;
+  requiredConfirm?: boolean;
+}
+
+export interface CardInstance {
+  cardInstanceId: string;
+  cardKey:
+    | 'department-selection'
+    | 'doctor-selection'
+    | 'time-slot-selection'
+    | 'confirm-appointment'
+    | 'payment-qrcode'
+    | 'appointment-success'
+    | 'route-card'
+    | 'tongue-capture'
+    | string;
+  status: 'active' | 'submitted' | 'processing' | 'completed' | 'expired' | 'failed';
+  expiresAt?: string;
+  cardData: Record<string, unknown>;
+  actions: CardAction[];
+}
+
+export interface TaskState {
+  taskId: string;
+  taskType: 'REGISTRATION' | 'GUIDE' | 'TRIAGE' | 'TONGUE_DIAGNOSIS' | string;
+  currentStep: string;
+  status: 'ACTIVE' | 'WAITING_CARD_ACTION' | 'PROCESSING' | 'COMPLETED' | 'CANCELLED' | 'FAILED' | string;
+}
+```
+
+- [ ] **Step 4: Implement store**
+
+`src/state/terminalStore.ts`:
+
+```ts
+import { defineStore } from 'pinia';
+import type { CardInstance, ChatMessage, TaskState } from '../api/types';
+
+export type ContextPanelMode = 'idle' | 'task' | 'error' | 'completed';
+
+export const useTerminalStore = defineStore('terminal', {
+  state: () => ({
+    deviceId: 'EMOON-WEB-DEMO-001',
+    deviceType: 'self_service_kiosk',
+    conversationId: '' as string,
+    currentTraceId: '' as string,
+    messages: [] as ChatMessage[],
+    streaming: false,
+    task: null as TaskState | null,
+    activeCard: null as CardInstance | null,
+    errorMessage: '' as string,
+    inputText: '',
+    pendingActionKey: '',
+    demoMode: true
+  }),
+  getters: {
+    contextPanelMode(state): ContextPanelMode {
+      if (state.errorMessage) return 'error';
+      if (state.activeCard?.cardKey === 'appointment-success') return 'completed';
+      if (state.activeCard) return 'task';
+      return 'idle';
+    }
+  },
+  actions: {
+    addMessage(message: ChatMessage) {
+      this.messages.push(message);
+    },
+    appendAssistantDelta(text: string) {
+      const last = this.messages[this.messages.length - 1];
+      if (last?.role === 'assistant' && last.streaming) {
+        last.content += text;
+        return;
+      }
+      this.messages.push({
+        id: crypto.randomUUID(),
+        role: 'assistant',
+        content: text,
+        streaming: true
+      });
+    },
+    completeAssistantMessage() {
+      const last = this.messages[this.messages.length - 1];
+      if (last?.role === 'assistant') last.streaming = false;
+    },
+    setTask(task: TaskState) {
+      this.task = task;
+    },
+    setActiveCard(card: CardInstance) {
+      this.activeCard = card;
+      this.errorMessage = '';
+    },
+    setError(message: string, traceId = '') {
+      this.errorMessage = message;
+      this.currentTraceId = traceId;
+    },
+    resetToIdle() {
+      this.task = null;
+      this.activeCard = null;
+      this.errorMessage = '';
+      this.pendingActionKey = '';
+    }
+  }
+});
+```
+
+- [ ] **Step 5: Add demo fixtures for registration cards**
+
+`src/api/demoFixtures.ts`:
+
+```ts
+import type { CardInstance } from './types';
+
+export const demoCards: Record<string, CardInstance> = {
+  card_department: {
+    cardInstanceId: 'card_department',
+    cardKey: 'department-selection',
+    status: 'active',
+    cardData: {
+      departments: [
+        { departmentId: 'neurology', name: '神经内科', reason: '头痛伴恶心,建议优先排查神经系统相关问题。' },
+        { departmentId: 'general_internal', name: '普通内科', reason: '可作为综合初筛科室。' }
+      ]
+    },
+    actions: [{ actionName: 'select_department', label: '选择科室' }]
+  },
+  card_doctor: {
+    cardInstanceId: 'card_doctor',
+    cardKey: 'doctor-selection',
+    status: 'active',
+    cardData: {
+      doctors: [
+        { doctorId: 'DOC001', name: '李明', title: '主任医师', specialty: '头痛、眩晕、脑血管病', room: '门诊三楼 302' },
+        { doctorId: 'DOC002', name: '王佳', title: '副主任医师', specialty: '慢性头痛、神经系统常见病', room: '门诊三楼 306' }
+      ]
+    },
+    actions: [{ actionName: 'select_doctor', label: '选择医生' }]
+  },
+  card_slot: {
+    cardInstanceId: 'card_slot',
+    cardKey: 'time-slot-selection',
+    status: 'active',
+    cardData: {
+      slots: [
+        { slotId: 'slot_0930', timePeriod: '明天 09:30-09:45', remaining: 3 },
+        { slotId: 'slot_0945', timePeriod: '明天 09:45-10:00', remaining: 2 }
+      ]
+    },
+    actions: [{ actionName: 'select_time_slot', label: '选择时间' }]
+  }
+};
+```
+
+- [ ] **Step 6: Run store tests**
+
+Run:
+
+```bash
+pnpm vitest run tests/unit/terminalStore.spec.ts
+```
+
+Expected: pass.
+
+**Commit:** `feat: add terminal state and demo fixtures`
+
+## Task 5: Implement Conversation Panel And SSE Parser
+
+**Files:**
+
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/chat/ConversationPanel.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/chat/MessageBubble.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/chat/ChatInput.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/api/sse.ts`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/tests/unit/sse.spec.ts`
+
+- [ ] **Step 1: Write failing SSE parser test**
+
+`tests/unit/sse.spec.ts`:
+
+```ts
+import { describe, expect, it } from 'vitest';
+import { parseSseChunk } from '../../src/api/sse';
+
+describe('parseSseChunk', () => {
+  it('parses named SSE events with JSON data and preserves remainder', () => {
+    const result = parseSseChunk('', 'event: message_delta\ndata: {"text":"好的"}\n\npartial');
+
+    expect(result.events).toEqual([{ type: 'message_delta', data: { text: '好的' } }]);
+    expect(result.remainder).toBe('partial');
+  });
+
+  it('ignores malformed data events without throwing', () => {
+    const result = parseSseChunk('', 'event: error\ndata: not-json\n\n');
+
+    expect(result.events).toEqual([{ type: 'error', data: { message: 'SSE 数据解析失败' } }]);
+  });
+});
+```
+
+- [ ] **Step 2: Run test and verify failure**
+
+Run:
+
+```bash
+pnpm vitest run tests/unit/sse.spec.ts
+```
+
+Expected: fails until `src/api/sse.ts` exists.
+
+- [ ] **Step 3: Implement SSE parser**
+
+`src/api/sse.ts`:
+
+```ts
+import type { SseEventType } from './types';
+
+export interface ParsedSseEvent {
+  type: SseEventType;
+  data: Record<string, unknown>;
+}
+
+export function parseSseChunk(previous: string, chunk: string): { events: ParsedSseEvent[]; remainder: string } {
+  const buffer = previous + chunk;
+  const rawEvents = buffer.split('\n\n');
+  const remainder = rawEvents.pop() ?? '';
+  const events: ParsedSseEvent[] = [];
+
+  for (const raw of rawEvents) {
+    const eventMatch = raw.match(/^event:\s*(.+)$/m);
+    const dataMatch = raw.match(/^data:\s*(.+)$/m);
+    if (!eventMatch || !dataMatch) continue;
+
+    const type = eventMatch[1].trim() as SseEventType;
+    try {
+      events.push({ type, data: JSON.parse(dataMatch[1]) });
+    } catch {
+      events.push({ type: 'error', data: { message: 'SSE 数据解析失败' } });
+    }
+  }
+
+  return { events, remainder };
+}
+```
+
+- [ ] **Step 4: Implement message bubble and input**
+
+`src/chat/MessageBubble.vue`:
+
+```vue
+<template>
+  <div class="bubble" :class="message.role">
+    {{ message.content }}
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { ChatMessage } from '../api/types';
+
+defineProps<{ message: ChatMessage }>();
+</script>
+
+<style scoped>
+.bubble {
+  max-width: 78%;
+  padding: 12px 14px;
+  border-radius: 14px;
+  line-height: 1.65;
+  word-break: break-word;
+}
+
+.assistant,
+.system {
+  align-self: flex-start;
+  border: 1px solid #e1e8f5;
+  background: #ffffff;
+}
+
+.user {
+  align-self: flex-end;
+  background: var(--brand-primary);
+  color: #ffffff;
+}
+```
+
+`src/chat/ChatInput.vue`:
+
+```vue
+<template>
+  <form class="chat-input" @submit.prevent="submit">
+    <input v-model="store.inputText" placeholder="继续输入症状、科室、医生或时间..." />
+    <button type="button" class="voice" @click="showVoiceHint">语音</button>
+    <button type="submit" class="send" :disabled="!store.inputText.trim()">发送</button>
+  </form>
+</template>
+
+<script setup lang="ts">
+import { useTerminalStore } from '../state/terminalStore';
+
+const emit = defineEmits<{ submit: [text: string] }>();
+const store = useTerminalStore();
+
+function submit() {
+  const text = store.inputText.trim();
+  if (!text) return;
+  store.inputText = '';
+  emit('submit', text);
+}
+
+function showVoiceHint() {
+  store.addMessage({
+    id: crypto.randomUUID(),
+    role: 'system',
+    content: '语音能力将在后续版本接入,当前请使用文字输入。'
+  });
+}
+</script>
+
+<style scoped>
+.chat-input {
+  display: flex;
+  gap: 10px;
+  padding: 14px;
+  border-top: 1px solid #edf1f7;
+  background: var(--surface);
+}
+
+input {
+  flex: 1;
+  min-width: 0;
+  padding: 12px 14px;
+  border: 1px solid #dce7f7;
+  border-radius: 999px;
+  outline: none;
+}
+
+button {
+  border: 0;
+  border-radius: 999px;
+  padding: 0 18px;
+  font-weight: 900;
+  cursor: pointer;
+}
+
+.voice {
+  background: var(--brand-accent);
+  color: #10144a;
+}
+
+.send {
+  background: var(--brand-primary);
+  color: #ffffff;
+}
+
+.send:disabled {
+  cursor: not-allowed;
+  opacity: 0.45;
+}
+```
+
+- [ ] **Step 5: Implement conversation panel with demo start behavior**
+
+`src/chat/ConversationPanel.vue`:
+
+```vue
+<template>
+  <main class="conversation-panel">
+    <header>
+      <div>
+        <h1>对话入口</h1>
+        <p>系统会识别意图并进入对应流程</p>
+      </div>
+      <span v-if="store.task" class="task-pill">{{ store.task.taskType }}</span>
+    </header>
+
+    <section class="messages" aria-label="对话消息">
+      <MessageBubble v-for="message in store.messages" :key="message.id" :message="message" />
+    </section>
+
+    <ChatInput @submit="handleSubmit" />
+  </main>
+</template>
+
+<script setup lang="ts">
+import { demoCards } from '../api/demoFixtures';
+import ChatInput from './ChatInput.vue';
+import MessageBubble from './MessageBubble.vue';
+import { useTerminalStore } from '../state/terminalStore';
+
+const store = useTerminalStore();
+
+if (store.messages.length === 0) {
+  store.addMessage({
+    id: 'welcome',
+    role: 'assistant',
+    content: '请直接告诉我你想办理什么,也可以描述症状。'
+  });
+}
+
+function handleSubmit(text: string) {
+  store.addMessage({ id: crypto.randomUUID(), role: 'user', content: text });
+  store.setTask({
+    taskId: 'task_demo_registration',
+    taskType: 'REGISTRATION',
+    currentStep: 'TRIAGE_RECOMMEND_DEPARTMENT',
+    status: 'ACTIVE'
+  });
+  store.addMessage({
+    id: crypto.randomUUID(),
+    role: 'assistant',
+    content: '已识别为挂号需求。根据描述,建议优先选择神经内科。'
+  });
+  store.setActiveCard(demoCards.card_department);
+}
+</script>
+
+<style scoped>
+.conversation-panel {
+  display: flex;
+  min-height: 0;
+  flex-direction: column;
+  overflow: hidden;
+  border: 1px solid var(--border);
+  border-radius: var(--radius-panel);
+  background: var(--surface);
+}
+
+header {
+  display: flex;
+  justify-content: space-between;
+  padding: 16px;
+  border-bottom: 1px solid #edf1f7;
+}
+
+h1 {
+  margin: 0;
+  font-size: 22px;
+}
+
+p {
+  margin: 4px 0 0;
+  color: var(--text-secondary);
+  font-size: 13px;
+}
+
+.task-pill {
+  align-self: flex-start;
+  padding: 7px 11px;
+  border-radius: 999px;
+  background: #f1f0ff;
+  color: var(--brand-primary);
+  font-size: 12px;
+  font-weight: 800;
+}
+
+.messages {
+  display: flex;
+  flex: 1;
+  min-height: 0;
+  flex-direction: column;
+  gap: 12px;
+  overflow-y: auto;
+  padding: 16px;
+  background: #fbfdff;
+}
+```
+
+- [ ] **Step 6: Run unit tests**
+
+Run:
+
+```bash
+pnpm vitest run tests/unit/sse.spec.ts
+```
+
+Expected: pass.
+
+**Commit:** `feat: add conversation panel and sse parser`
+
+## Task 6: Implement Context Panel And Registration Cards
+
+**Files:**
+
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/cards/CardRenderer.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/cards/DepartmentSelectionCard.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/cards/DoctorSelectionCard.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/cards/TimeSlotSelectionCard.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/cards/ConfirmAppointmentCard.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/cards/PaymentQrCard.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/cards/AppointmentSuccessCard.vue`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/cards/ErrorCard.vue`
+- Modify: `/Users/destiny/dev/emoon/emoon-terminal-client/src/layouts/ContextPanel.vue`
+- Modify: `/Users/destiny/dev/emoon/emoon-terminal-client/src/layouts/RegistrationProgress.vue`
+- Test: `/Users/destiny/dev/emoon/emoon-terminal-client/tests/unit/cardRenderer.spec.ts`
+
+- [ ] **Step 1: Write failing card renderer test**
+
+`tests/unit/cardRenderer.spec.ts`:
+
+```ts
+import { mount } from '@vue/test-utils';
+import { describe, expect, it } from 'vitest';
+import CardRenderer from '../../src/cards/CardRenderer.vue';
+
+describe('CardRenderer', () => {
+  it('renders department card by cardKey', () => {
+    const wrapper = mount(CardRenderer, {
+      props: {
+        card: {
+          cardInstanceId: 'card_department',
+          cardKey: 'department-selection',
+          status: 'active',
+          cardData: { departments: [{ departmentId: 'neurology', name: '神经内科', reason: '头痛伴恶心' }] },
+          actions: [{ actionName: 'select_department', label: '选择科室' }]
+        }
+      }
+    });
+
+    expect(wrapper.text()).toContain('推荐科室');
+    expect(wrapper.text()).toContain('神经内科');
+  });
+});
+```
+
+- [ ] **Step 2: Run test and verify failure**
+
+Run:
+
+```bash
+pnpm vitest run tests/unit/cardRenderer.spec.ts
+```
+
+Expected: fails until cards exist.
+
+- [ ] **Step 3: Implement ContextPanel**
+
+`src/layouts/ContextPanel.vue`:
+
+```vue
+<template>
+  <aside class="context-panel" aria-label="上下文面板">
+    <DoctorAssistantFigure v-if="store.contextPanelMode === 'idle'" />
+
+    <template v-else-if="store.contextPanelMode === 'task' || store.contextPanelMode === 'completed'">
+      <RegistrationProgress :current-step="store.task?.currentStep || ''" />
+      <CardRenderer v-if="store.activeCard" :card="store.activeCard" @action="handleAction" @finish="store.resetToIdle" />
+    </template>
+
+    <ErrorCard
+      v-else
+      :message="store.errorMessage"
+      :trace-id="store.currentTraceId"
+      @restart="store.resetToIdle"
+    />
+  </aside>
+</template>
+
+<script setup lang="ts">
+import CardRenderer from '../cards/CardRenderer.vue';
+import ErrorCard from '../cards/ErrorCard.vue';
+import { demoCards } from '../api/demoFixtures';
+import { useTerminalStore } from '../state/terminalStore';
+import DoctorAssistantFigure from './DoctorAssistantFigure.vue';
+import RegistrationProgress from './RegistrationProgress.vue';
+
+const store = useTerminalStore();
+
+function handleAction(actionName: string) {
+  store.pendingActionKey = `${store.activeCard?.cardInstanceId}-${actionName}`;
+  if (actionName === 'select_department') store.setActiveCard(demoCards.card_doctor);
+  if (actionName === 'select_doctor') store.setActiveCard(demoCards.card_slot);
+  if (actionName === 'select_time_slot') {
+    store.setActiveCard({
+      cardInstanceId: 'card_confirm',
+      cardKey: 'confirm-appointment',
+      status: 'active',
+      cardData: {
+        summary: {
+          departmentName: '神经内科',
+          doctorName: '李明',
+          visitTime: '明天 09:30-09:45',
+          amount: 25
+        }
+      },
+      actions: [{ actionName: 'confirm_appointment', label: '确认挂号', requiredConfirm: true }]
+    });
+  }
+  if (actionName === 'confirm_appointment') {
+    store.setActiveCard({
+      cardInstanceId: 'card_payment',
+      cardKey: 'payment-qrcode',
+      status: 'active',
+      cardData: { orderId: 'PAY202606020001', amount: 25, qrContent: 'demo-payment://orderId=PAY202606020001', mock: true },
+      actions: [{ actionName: 'mock_payment_paid', label: '模拟支付完成', requiredConfirm: true }]
+    });
+  }
+  if (actionName === 'mock_payment_paid') {
+    store.setActiveCard({
+      cardInstanceId: 'card_success',
+      cardKey: 'appointment-success',
+      status: 'completed',
+      cardData: {
+        appointmentNo: 'A023',
+        departmentName: '神经内科',
+        doctorName: '李明',
+        visitTime: '明天 09:30-09:45',
+        room: '门诊三楼 302 诊室',
+        mock: true
+      },
+      actions: []
+    });
+  }
+}
+</script>
+
+<style scoped>
+.context-panel {
+  display: grid;
+  min-height: 0;
+  gap: 12px;
+}
+```
+
+- [ ] **Step 4: Implement RegistrationProgress**
+
+`src/layouts/RegistrationProgress.vue`:
+
+```vue
+<template>
+  <section class="progress-panel">
+    <div class="heading">
+      <strong>挂号进度</strong>
+      <span>{{ currentLabel }}</span>
+    </div>
+    <div class="bar" aria-label="挂号进度">
+      <i v-for="step in 5" :key="step" :class="{ active: step <= activeIndex }" />
+    </div>
+  </section>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+
+const props = defineProps<{ currentStep: string }>();
+
+const stepMap: Record<string, { index: number; label: string }> = {
+  TRIAGE_RECOMMEND_DEPARTMENT: { index: 1, label: '科室推荐' },
+  WAIT_SELECT_DOCTOR: { index: 2, label: '选择医生' },
+  WAIT_SELECT_TIME_SLOT: { index: 3, label: '选择时间' },
+  WAIT_CONFIRM_APPOINTMENT: { index: 4, label: '确认支付' },
+  APPOINTMENT_SUCCESS: { index: 5, label: '挂号成功' }
+};
+
+const current = computed(() => stepMap[props.currentStep] ?? stepMap.TRIAGE_RECOMMEND_DEPARTMENT);
+const activeIndex = computed(() => current.value.index);
+const currentLabel = computed(() => current.value.label);
+</script>
+
+<style scoped>
+.progress-panel {
+  padding: 14px;
+  border: 1px solid var(--border);
+  border-radius: var(--radius-panel);
+  background: var(--surface);
+}
+
+.heading {
+  display: flex;
+  justify-content: space-between;
+}
+
+.heading span {
+  color: var(--text-secondary);
+  font-size: 12px;
+}
+
+.bar {
+  display: grid;
+  grid-template-columns: repeat(5, 1fr);
+  gap: 5px;
+  margin-top: 12px;
+}
+
+.bar i {
+  height: 8px;
+  border-radius: 999px;
+  background: var(--border);
+}
+
+.bar i.active:first-child {
+  background: var(--brand-primary);
+}
+
+.bar i.active {
+  background: var(--brand-accent);
+}
+```
+
+- [ ] **Step 5: Implement CardRenderer and one reusable card pattern**
+
+`src/cards/CardRenderer.vue`:
+
+```vue
+<template>
+  <DepartmentSelectionCard v-if="card.cardKey === 'department-selection'" :card="card" @action="emit('action', $event)" />
+  <DoctorSelectionCard v-else-if="card.cardKey === 'doctor-selection'" :card="card" @action="emit('action', $event)" />
+  <TimeSlotSelectionCard v-else-if="card.cardKey === 'time-slot-selection'" :card="card" @action="emit('action', $event)" />
+  <ConfirmAppointmentCard v-else-if="card.cardKey === 'confirm-appointment'" :card="card" @action="emit('action', $event)" />
+  <PaymentQrCard v-else-if="card.cardKey === 'payment-qrcode'" :card="card" @action="emit('action', $event)" />
+  <AppointmentSuccessCard v-else-if="card.cardKey === 'appointment-success'" :card="card" @finish="emit('finish')" />
+  <ErrorCard v-else message="暂不支持的卡片类型" :trace-id="card.cardInstanceId" />
+</template>
+
+<script setup lang="ts">
+import type { CardInstance } from '../api/types';
+import AppointmentSuccessCard from './AppointmentSuccessCard.vue';
+import ConfirmAppointmentCard from './ConfirmAppointmentCard.vue';
+import DepartmentSelectionCard from './DepartmentSelectionCard.vue';
+import DoctorSelectionCard from './DoctorSelectionCard.vue';
+import ErrorCard from './ErrorCard.vue';
+import PaymentQrCard from './PaymentQrCard.vue';
+import TimeSlotSelectionCard from './TimeSlotSelectionCard.vue';
+
+defineProps<{ card: CardInstance }>();
+const emit = defineEmits<{ action: [actionName: string]; finish: [] }>();
+</script>
+```
+
+`src/cards/DepartmentSelectionCard.vue`:
+
+```vue
+<template>
+  <section class="business-card">
+    <h2>推荐科室</h2>
+    <article v-for="dept in departments" :key="dept.departmentId" class="option">
+      <h3>{{ dept.name }}</h3>
+      <p>{{ dept.reason }}</p>
+      <button @click="$emit('action', 'select_department')">选择{{ dept.name }}</button>
+    </article>
+  </section>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import type { CardInstance } from '../api/types';
+
+const props = defineProps<{ card: CardInstance }>();
+defineEmits<{ action: [actionName: string] }>();
+
+interface DepartmentOption {
+  departmentId: string;
+  name: string;
+  reason: string;
+}
+
+const departments = computed(() => (props.card.cardData.departments as DepartmentOption[]) ?? []);
+</script>
+
+<style scoped>
+.business-card {
+  min-height: 0;
+  overflow: auto;
+  padding: 16px;
+  border: 1px solid var(--border);
+  border-radius: var(--radius-panel);
+  background: var(--surface);
+}
+
+h2 {
+  margin: 0 0 12px;
+  font-size: 18px;
+}
+
+.option {
+  padding: 12px;
+  border: 1px solid var(--border);
+  border-radius: var(--radius-card);
+  margin-bottom: 10px;
+  background: var(--soft-tint);
+}
+
+h3 {
+  margin: 0;
+  color: var(--brand-primary);
+}
+
+p {
+  color: var(--text-secondary);
+  font-size: 12px;
+  line-height: 1.6;
+}
+
+button {
+  width: 100%;
+  border: 0;
+  border-radius: 999px;
+  padding: 9px 12px;
+  background: var(--brand-primary);
+  color: #ffffff;
+  font-weight: 900;
+  cursor: pointer;
+}
+```
+
+- [ ] **Step 6: Implement remaining card components**
+
+`src/cards/DoctorSelectionCard.vue`:
+
+```vue
+<template>
+  <section class="business-card">
+    <h2>选择医生</h2>
+    <article v-for="doctor in doctors" :key="doctor.doctorId" class="option">
+      <h3>{{ doctor.name }} <small>{{ doctor.title }}</small></h3>
+      <p>{{ doctor.specialty }}</p>
+      <p>{{ doctor.room }}</p>
+      <button @click="$emit('action', 'select_doctor')">选择{{ doctor.name }}</button>
+    </article>
+  </section>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import type { CardInstance } from '../api/types';
+
+const props = defineProps<{ card: CardInstance }>();
+defineEmits<{ action: [actionName: string] }>();
+
+interface DoctorOption {
+  doctorId: string;
+  name: string;
+  title: string;
+  specialty: string;
+  room: string;
+}
+
+const doctors = computed(() => (props.card.cardData.doctors as DoctorOption[]) ?? []);
+</script>
+
+<style scoped>
+.business-card { min-height: 0; overflow: auto; padding: 16px; border: 1px solid var(--border); border-radius: var(--radius-panel); background: var(--surface); }
+h2 { margin: 0 0 12px; font-size: 18px; }
+.option { padding: 12px; border: 1px solid var(--border); border-radius: var(--radius-card); margin-bottom: 10px; background: var(--surface); }
+h3 { margin: 0; color: var(--brand-primary); }
+small { color: var(--text-secondary); font-size: 12px; }
+p { color: var(--text-secondary); font-size: 12px; line-height: 1.6; }
+button { width: 100%; border: 0; border-radius: 999px; padding: 9px 12px; background: var(--brand-primary); color: #fff; font-weight: 900; cursor: pointer; }
+```
+
+`src/cards/TimeSlotSelectionCard.vue`:
+
+```vue
+<template>
+  <section class="business-card">
+    <h2>选择时间</h2>
+    <article v-for="slot in slots" :key="slot.slotId" class="option">
+      <h3>{{ slot.timePeriod }}</h3>
+      <p>剩余号源:{{ slot.remaining }}</p>
+      <button @click="$emit('action', 'select_time_slot')">选择{{ slot.timePeriod }}</button>
+    </article>
+  </section>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import type { CardInstance } from '../api/types';
+
+const props = defineProps<{ card: CardInstance }>();
+defineEmits<{ action: [actionName: string] }>();
+
+interface SlotOption {
+  slotId: string;
+  timePeriod: string;
+  remaining: number;
+}
+
+const slots = computed(() => (props.card.cardData.slots as SlotOption[]) ?? []);
+</script>
+
+<style scoped>
+.business-card { min-height: 0; overflow: auto; padding: 16px; border: 1px solid var(--border); border-radius: var(--radius-panel); background: var(--surface); }
+h2 { margin: 0 0 12px; font-size: 18px; }
+.option { padding: 12px; border: 1px solid var(--border); border-radius: var(--radius-card); margin-bottom: 10px; background: var(--soft-tint); }
+h3 { margin: 0; color: var(--brand-primary); }
+p { color: var(--text-secondary); font-size: 12px; line-height: 1.6; }
+button { width: 100%; border: 0; border-radius: 999px; padding: 9px 12px; background: var(--brand-primary); color: #fff; font-weight: 900; cursor: pointer; }
+```
+
+`src/cards/ConfirmAppointmentCard.vue`:
+
+```vue
+<template>
+  <section class="business-card">
+    <h2>确认挂号信息</h2>
+    <dl>
+      <div><dt>科室</dt><dd>{{ summary.departmentName }}</dd></div>
+      <div><dt>医生</dt><dd>{{ summary.doctorName }}</dd></div>
+      <div><dt>时间</dt><dd>{{ summary.visitTime }}</dd></div>
+      <div><dt>费用</dt><dd>{{ summary.amount }} 元</dd></div>
+    </dl>
+    <p class="notice">确认后将生成 Mock 支付订单。</p>
+    <button @click="$emit('action', 'confirm_appointment')">确认挂号</button>
+  </section>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import type { CardInstance } from '../api/types';
+
+const props = defineProps<{ card: CardInstance }>();
+defineEmits<{ action: [actionName: string] }>();
+
+interface Summary {
+  departmentName?: string;
+  doctorName?: string;
+  visitTime?: string;
+  amount?: number;
+}
+
+const summary = computed(() => (props.card.cardData.summary as Summary) ?? {});
+</script>
+
+<style scoped>
+.business-card { padding: 16px; border: 1px solid var(--border); border-radius: var(--radius-panel); background: var(--surface); }
+h2 { margin: 0 0 12px; font-size: 18px; }
+dl { display: grid; gap: 10px; margin: 0; }
+dl div { display: flex; justify-content: space-between; gap: 12px; padding: 10px; border-radius: 10px; background: #f8fbff; }
+dt { color: var(--text-secondary); }
+dd { margin: 0; font-weight: 800; text-align: right; }
+.notice { padding: 10px; border-radius: 10px; background: var(--warning-bg); color: var(--warning-text); font-size: 12px; }
+button { width: 100%; border: 0; border-radius: 999px; padding: 11px 12px; background: var(--brand-primary); color: #fff; font-weight: 900; cursor: pointer; }
+```
+
+`src/cards/PaymentQrCard.vue`:
+
+```vue
+<template>
+  <section class="business-card">
+    <div class="mock-label">联调演示 / Mock 支付</div>
+    <h2>Mock 支付</h2>
+    <div class="qr">{{ qrText }}</div>
+    <p>订单号:{{ card.cardData.orderId }}</p>
+    <p>金额:{{ card.cardData.amount }} 元</p>
+    <button @click="$emit('action', 'mock_payment_paid')">模拟支付完成</button>
+  </section>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import type { CardInstance } from '../api/types';
+
+const props = defineProps<{ card: CardInstance }>();
+defineEmits<{ action: [actionName: string] }>();
+
+const qrText = computed(() => String(props.card.cardData.qrContent ?? 'demo-payment://orderId=unknown'));
+</script>
+
+<style scoped>
+.business-card { padding: 16px; border: 1px solid var(--border); border-radius: var(--radius-panel); background: var(--surface); text-align: center; }
+.mock-label { display: inline-flex; padding: 6px 10px; border-radius: 999px; background: var(--warning-bg); color: var(--warning-text); font-size: 12px; font-weight: 900; }
+h2 { margin: 14px 0 12px; font-size: 18px; }
+.qr { display: grid; min-height: 128px; place-items: center; padding: 12px; border: 1px dashed var(--brand-primary); border-radius: 14px; background: var(--soft-tint); color: var(--brand-primary); font-size: 12px; word-break: break-all; }
+p { color: var(--text-secondary); font-size: 13px; }
+button { width: 100%; border: 0; border-radius: 999px; padding: 11px 12px; background: var(--brand-primary); color: #fff; font-weight: 900; cursor: pointer; }
+```
+
+`src/cards/AppointmentSuccessCard.vue`:
+
+```vue
+<template>
+  <section class="business-card success">
+    <div class="mock-label" v-if="card.cardData.mock">联调演示</div>
+    <h2>挂号成功</h2>
+    <strong class="appointment-no">{{ card.cardData.appointmentNo }}</strong>
+    <p>{{ card.cardData.departmentName }} · {{ card.cardData.doctorName }}</p>
+    <p>{{ card.cardData.visitTime }}</p>
+    <p>{{ card.cardData.room }}</p>
+    <button @click="$emit('finish')">完成</button>
+  </section>
+</template>
+
+<script setup lang="ts">
+import type { CardInstance } from '../api/types';
+
+defineProps<{ card: CardInstance }>();
+defineEmits<{ finish: [] }>();
+</script>
+
+<style scoped>
+.business-card { padding: 16px; border: 1px solid var(--border); border-radius: var(--radius-panel); background: var(--surface); text-align: center; }
+.success { border-color: rgba(58, 212, 216, 0.8); background: var(--soft-tint); }
+.mock-label { display: inline-flex; padding: 6px 10px; border-radius: 999px; background: var(--warning-bg); color: var(--warning-text); font-size: 12px; font-weight: 900; }
+h2 { margin: 14px 0 8px; color: var(--brand-primary); }
+.appointment-no { display: block; margin: 12px 0; font-size: 34px; color: var(--brand-primary); }
+p { color: var(--text-secondary); font-size: 13px; }
+button { width: 100%; border: 0; border-radius: 999px; padding: 11px 12px; background: var(--brand-primary); color: #fff; font-weight: 900; cursor: pointer; }
+```
+
+`src/cards/ErrorCard.vue`:
+
+```vue
+<template>
+  <section class="error-card">
+    <h2>处理异常</h2>
+    <p>{{ message }}</p>
+    <small v-if="traceId">traceId: {{ traceId }}</small>
+    <button @click="$emit('restart')">重新开始</button>
+  </section>
+</template>
+
+<script setup lang="ts">
+defineProps<{ message: string; traceId?: string }>();
+defineEmits<{ restart: [] }>();
+</script>
+
+<style scoped>
+.error-card { padding: 16px; border: 1px solid #f0d89c; border-radius: var(--radius-panel); background: var(--warning-bg); color: var(--warning-text); }
+h2 { margin: 0 0 10px; }
+p { line-height: 1.6; }
+small { display: block; margin: 12px 0; word-break: break-all; }
+button { width: 100%; border: 0; border-radius: 999px; padding: 10px 12px; background: var(--brand-primary); color: #fff; font-weight: 900; cursor: pointer; }
+```
+
+- [ ] **Step 7: Run card renderer test**
+
+Run:
+
+```bash
+pnpm vitest run tests/unit/cardRenderer.spec.ts
+```
+
+Expected: pass.
+
+**Commit:** `feat: implement registration context panel cards`
+
+## Task 7: Add API Client With Demo And Real Modes
+
+**Files:**
+
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/api/client.ts`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/src/api/hmac.ts`
+- Modify: `/Users/destiny/dev/emoon/emoon-terminal-client/src/demo/demoMode.ts`
+- Test: `/Users/destiny/dev/emoon/emoon-terminal-client/tests/unit/apiClient.spec.ts`
+
+- [ ] **Step 1: Write failing HMAC helper test**
+
+`tests/unit/apiClient.spec.ts`:
+
+```ts
+import { describe, expect, it } from 'vitest';
+import { sha256Hex } from '../../src/api/hmac';
+
+describe('hmac helpers', () => {
+  it('hashes body as lowercase hex sha256', async () => {
+    await expect(sha256Hex('{"a":1}')).resolves.toBe('015abd7f5cc57a2dd94b7590f04ad8084273905ee33ec5cebeae62276a97f862');
+  });
+});
+```
+
+- [ ] **Step 2: Run test and verify failure**
+
+Run:
+
+```bash
+pnpm vitest run tests/unit/apiClient.spec.ts
+```
+
+Expected: fails until `hmac.ts` exists.
+
+- [ ] **Step 3: Implement HMAC hash helper**
+
+`src/api/hmac.ts`:
+
+```ts
+export async function sha256Hex(data: string): Promise<string> {
+  const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data));
+  return Array.from(new Uint8Array(hashBuffer))
+    .map((value) => value.toString(16).padStart(2, '0'))
+    .join('');
+}
+
+export async function buildDemoAuthHeaders(method: string, path: string, body: string): Promise<Record<string, string>> {
+  const timestamp = String(Date.now());
+  const nonce = crypto.randomUUID();
+  const bodyHash = await sha256Hex(body);
+
+  return {
+    'X-Emoon-Access-Key': 'pk-web-demo',
+    'X-Emoon-Timestamp': timestamp,
+    'X-Emoon-Nonce': nonce,
+    'X-Emoon-Signature': `demo-signature:${method}:${path}:${bodyHash}`
+  };
+}
+```
+
+This demo signature is only for local Web demo mode. Real terminal shell signing remains out of scope for this frontend plan.
+
+- [ ] **Step 4: Implement API client shape**
+
+`src/api/client.ts`:
+
+```ts
+import { demoCards } from './demoFixtures';
+import { buildDemoAuthHeaders } from './hmac';
+import type { CardInstance } from './types';
+
+const API_BASE = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8080/api/v1';
+const DEMO_MODE = import.meta.env.VITE_DEMO_MODE !== 'false';
+
+export async function fetchCard(cardInstanceId: string): Promise<CardInstance> {
+  if (DEMO_MODE) {
+    const card = demoCards[cardInstanceId];
+    if (!card) throw new Error(`演示卡片不存在: ${cardInstanceId}`);
+    return card;
+  }
+
+  const path = `/cards/${cardInstanceId}`;
+  const headers = await buildDemoAuthHeaders('GET', `/api/v1${path}`, '');
+  const response = await fetch(`${API_BASE}${path}`, { headers });
+  const body = await response.json();
+  if (!response.ok || body.code !== 200) throw new Error(body.msg ?? '查询卡片失败');
+  return body.data;
+}
+
+export async function submitCardAction(
+  cardInstanceId: string,
+  actionName: string,
+  payload: Record<string, unknown>,
+  confirm = true
+) {
+  const body = JSON.stringify({
+    idempotencyKey: `${cardInstanceId}-${actionName}-${JSON.stringify(payload)}`,
+    confirm,
+    payload
+  });
+
+  if (DEMO_MODE) {
+    return { status: 'completed', actionId: `action_${Date.now()}` };
+  }
+
+  const path = `/cards/${cardInstanceId}/actions/${actionName}`;
+  const headers = await buildDemoAuthHeaders('POST', `/api/v1${path}`, body);
+  const response = await fetch(`${API_BASE}${path}`, {
+    method: 'POST',
+    headers: { ...headers, 'Content-Type': 'application/json' },
+    body
+  });
+  const data = await response.json();
+  if (!response.ok || data.code !== 200) throw new Error(data.msg ?? '提交卡片动作失败');
+  return data.data;
+}
+```
+
+- [ ] **Step 5: Run API test**
+
+Run:
+
+```bash
+pnpm vitest run tests/unit/apiClient.spec.ts
+```
+
+Expected: pass.
+
+**Commit:** `feat: add api client demo and real modes`
+
+## Task 8: Add Playwright Visual And Interaction QA
+
+**Files:**
+
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/playwright.config.ts`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/tests/e2e/visual-layout.spec.ts`
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/tests/e2e/registration-flow.spec.ts`
+
+- [ ] **Step 1: Add Playwright config**
+
+`playwright.config.ts`:
+
+```ts
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+  testDir: './tests/e2e',
+  webServer: {
+    command: 'pnpm dev',
+    url: 'http://127.0.0.1:5173',
+    reuseExistingServer: true
+  },
+  use: {
+    baseURL: 'http://127.0.0.1:5173',
+    trace: 'retain-on-failure'
+  },
+  projects: [
+    {
+      name: 'desktop-1440',
+      use: { ...devices['Desktop Chrome'], viewport: { width: 1440, height: 900 } }
+    },
+    {
+      name: 'desktop-1920',
+      use: { ...devices['Desktop Chrome'], viewport: { width: 1920, height: 1080 } }
+    }
+  ]
+});
+```
+
+- [ ] **Step 2: Add visual layout test**
+
+`tests/e2e/visual-layout.spec.ts`:
+
+```ts
+import { expect, test } from '@playwright/test';
+
+test('desktop layout has no horizontal overflow and keeps right panel as static doctor idle state', async ({ page }) => {
+  await page.goto('/');
+
+  await expect(page.getByText('医梦门诊助手')).toBeVisible();
+  await expect(page.getByText('说出需求,我来判断下一步')).toBeVisible();
+  await expect(page.getByAltText('医生助手')).toBeVisible();
+  await expect(page.getByText('当前仅作为静态形象展示')).toBeVisible();
+  await expect(page.getByRole('button', { name: '语音' })).toBeVisible();
+  await expect(page.getByRole('button', { name: '发送' })).toBeVisible();
+  await expect(page.getByText('智能挂号')).toHaveCount(0);
+
+  const horizontalOverflow = await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth);
+  expect(horizontalOverflow).toBe(false);
+});
+```
+
+- [ ] **Step 3: Add registration flow test**
+
+`tests/e2e/registration-flow.spec.ts`:
+
+```ts
+import { expect, test } from '@playwright/test';
+
+test('demo registration flow reaches mock appointment success', async ({ page }) => {
+  await page.goto('/');
+
+  await page.getByPlaceholder('继续输入症状、科室、医生或时间...').fill('我头疼三天,还有点恶心,想挂号');
+  await page.getByRole('button', { name: '发送' }).click();
+
+  await expect(page.getByText('推荐科室')).toBeVisible();
+  await page.getByRole('button', { name: '选择神经内科' }).click();
+
+  await expect(page.getByText('选择医生')).toBeVisible();
+  await page.getByRole('button', { name: /选择李明/ }).click();
+
+  await expect(page.getByText('选择时间')).toBeVisible();
+  await page.getByRole('button', { name: /09:30/ }).click();
+
+  await expect(page.getByText('确认挂号信息')).toBeVisible();
+  await page.getByRole('button', { name: '确认挂号' }).click();
+
+  await expect(page.getByText('联调演示 / Mock 支付')).toBeVisible();
+  await page.getByRole('button', { name: '模拟支付完成' }).click();
+
+  await expect(page.getByText('挂号成功')).toBeVisible();
+  await expect(page.getByText('A023')).toBeVisible();
+  await expect(page.getByText('联调演示')).toBeVisible();
+});
+```
+
+- [ ] **Step 4: Run Playwright tests**
+
+Run:
+
+```bash
+pnpm test:e2e
+```
+
+Expected: both viewport projects pass.
+
+**Commit:** `test: add terminal web demo playwright qa`
+
+## Task 9: Final Build, Documentation, And Handoff
+
+**Files:**
+
+- Create: `/Users/destiny/dev/emoon/emoon-terminal-client/README.md`
+- Modify: `/Users/destiny/dev/emoon/emoon-terminal-client/package.json`
+- Review: all source files.
+
+- [ ] **Step 1: Add README**
+
+`README.md`:
+
+````md
+# 医梦统一入口客户端 Web Demo
+
+单应用 Web Demo,用于演示患者通过对话完成门诊挂号闭环。
+
+## 启动
+
+```bash
+pnpm install
+pnpm dev
+```
+
+访问:`http://127.0.0.1:5173`
+
+## 演示范围
+
+- 对话优先,不展示左侧功能菜单。
+- 右侧 idle 状态展示静态医生助手形象。
+- 完整挂号闭环:科室、医生、时间、确认、Mock 支付、挂号成功。
+- Mock 结果必须展示“联调演示”。
+
+## 验证
+
+```bash
+pnpm build
+pnpm test:unit
+pnpm test:e2e
+```
+
+## 重要边界
+
+- 前端不传 `agentId`。
+- 前端不直连 Dify、HIS、MCP Tool、舌诊底层接口。
+- 所有卡片动作都必须带 `idempotencyKey`。
+- 真实签名密钥不得放进 `VITE_*` 环境变量。
+```
+````
+
+- [ ] **Step 2: Run full validation**
+
+Run:
+
+```bash
+pnpm build
+pnpm test:unit
+pnpm test:e2e
+```
+
+Expected: all commands pass.
+
+- [ ] **Step 3: Start dev server for user review**
+
+Run:
+
+```bash
+pnpm dev
+```
+
+Expected: dev server starts at `http://127.0.0.1:5173`.
+
+- [ ] **Step 4: Commit final frontend demo**
+
+Run:
+
+```bash
+git add .
+git commit -m "feat: build terminal web registration demo"
+```
+
+Expected: commit succeeds in `/Users/destiny/dev/emoon/emoon-terminal-client`.
+
+## Self-Review
+
+Spec coverage:
+
+- A+B mixed layout is covered by `AppShell`, `AssistantContextPanel`, `ConversationPanel`, and `ContextPanel`.
+- Brand colors are covered by `tokens.css`.
+- Dialogue-first behavior is enforced by removing left workflow buttons and keeping intent entry in the center conversation input.
+- Right idle panel shows only static doctor image via `DoctorAssistantFigure`.
+- Voice and send buttons stay in `ChatInput`.
+- Registration card flow is implemented through card components and demo state transitions.
+- Mock labels are required in `PaymentQrCard` and `AppointmentSuccessCard`.
+- Desktop QA is covered by Playwright at `1440x900` and `1920x1080`.
+
+Placeholder scan:
+
+- No unresolved placeholder instructions remain.
+- Demo mode is explicit and bounded.
+
+Type consistency:
+
+- `CardInstance`, `CardAction`, `TaskState`, `contextPanelMode`, `cardKey`, `actionName`, and `idempotencyKey` names match across tasks.
+
+## Execution Choice
+
+Plan complete and saved to:
+
+```text
+docs/superpowers/plans/2026-06-02-terminal-web-demo.md
+```
+
+Two execution options:
+
+1. **Subagent-Driven (recommended)** - dispatch a fresh subagent per task, review between tasks, fast iteration.
+2. **Inline Execution** - execute tasks in this session using executing-plans, batch execution with checkpoints.
+
+Recommended for this frontend: Subagent-Driven for Tasks 1-8, because layout, state/API, cards, and Playwright QA are distinct review surfaces.

+ 711 - 0
docs/superpowers/plans/2026-06-02-unified-entry-registration-demo.md

@@ -0,0 +1,711 @@
+# Unified Entry Registration Demo Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Build a next-week demonstrable outpatient registration loop from unified Web client → AI platform → MCP tool service → SQLite Mock HIS, with card confirmation, idempotency, mock payment, and reproducible acceptance scripts.
+
+**Architecture:** Keep production-facing backend capabilities inside the existing AI platform modules. Add a separate `mock-his-service` demo service using SQLite; do not add it to the root Maven reactor and do not use MySQL. `emoon-openplatform` remains the only terminal-facing API, `emoon-ai-agent` owns routing/task orchestration, `emoon-ai-card` owns card state/actions, and `emoon-ai-mcp` owns the HIS tool facade and adapter boundary.
+
+**Tech Stack:** Java 17, Spring Boot 3, Maven, SQLite for Mock HIS, existing RuoYi-Vue-Plus backend modules, MyBatis-Plus where already used, OkHttp/RestTemplate-style HTTP adapter according to existing project conventions, JUnit 5, Maven module tests, curl-based acceptance scripts.
+
+---
+
+## Scope
+
+This plan implements one frozen demo flow:
+
+```text
+patient message
+→ POST /api/v1/agent/chat/stream
+→ AgentRouter creates/continues REGISTRATION task
+→ Dify or deterministic demo response suggests department card
+→ card actions select department / doctor / time slot / confirm appointment
+→ MCP tool facade calls SQLite Mock HIS
+→ Mock HIS locks slot, creates mock payment order, marks mock paid, creates appointment
+→ appointment-success card returns with mock label and traceId
+```
+
+Do not implement real HIS, real payment,医保,退号退费,三端完整硬件接入,舌诊,报告解读, or a public `/tasks` API in this plan.
+
+## Existing Baseline
+
+The repo already contains useful skeletons and tests:
+
+- `emoon-openplatform/src/main/java/com/emoon/openplatform/controller/v1/AgentChatController.java`
+- `emoon-openplatform/src/main/java/com/emoon/openplatform/controller/v1/CardActionController.java`
+- `emoon-openplatform/src/main/java/com/emoon/openplatform/controller/v1/DeviceController.java`
+- `emoon-openplatform/src/main/java/com/emoon/openplatform/auth/HmacAuthInterceptor.java`
+- `emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent/src/main/java/com/emoon/ai/agent/application/AgentRouterService.java`
+- `emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent/src/main/java/com/emoon/ai/agent/application/AgentActionOrchestrator.java`
+- `emoon-infra/emoon-modules/emoon-ai/emoon-ai-card/src/main/java/com/emoon/ai/card/application/CardActionService.java`
+- `emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/ai/mcp/application/McpToolService.java`
+- `emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/client/MockHisClient.java`
+- `emoon-openplatform/src/test/java/com/emoon/openplatform/acceptance/TerminalMvpAcceptanceTest.java`
+
+Treat these as the starting point. Do not recreate modules that already exist.
+
+## Task 1: Build SQLite Mock HIS Service
+
+**Files:**
+
+- Create: `mock-his-service/pom.xml`
+- Create: `mock-his-service/src/main/java/com/emoon/mockhis/MockHisApplication.java`
+- Create: `mock-his-service/src/main/java/com/emoon/mockhis/controller/PatientController.java`
+- Create: `mock-his-service/src/main/java/com/emoon/mockhis/controller/CatalogController.java`
+- Create: `mock-his-service/src/main/java/com/emoon/mockhis/controller/SlotController.java`
+- Create: `mock-his-service/src/main/java/com/emoon/mockhis/controller/PaymentController.java`
+- Create: `mock-his-service/src/main/java/com/emoon/mockhis/controller/AppointmentController.java`
+- Create: `mock-his-service/src/main/java/com/emoon/mockhis/service/RegistrationFlowService.java`
+- Create: `mock-his-service/src/main/java/com/emoon/mockhis/domain/*.java`
+- Create: `mock-his-service/src/main/resources/application.yml`
+- Create: `mock-his-service/src/main/resources/schema.sql`
+- Create: `mock-his-service/src/main/resources/seed.sql`
+- Create: `mock-his-service/src/test/java/com/emoon/mockhis/RegistrationFlowServiceTest.java`
+- Create: `mock-his-service/src/test/java/com/emoon/mockhis/MockHisApiTest.java`
+
+- [ ] **Step 1: Write failing service tests for real HIS semantics**
+
+Add tests covering:
+
+```java
+@Test
+void lockSlotDecreasesRemainingCountAndExpiresAfterFiveMinutes() {
+    LockSlotResult first = service.lockSlot("slot_neuro_001", "patient_demo_001", "idem-lock-001");
+    assertThat(first.lockId()).isNotBlank();
+    assertThat(first.expiresAt()).isAfter(Instant.now());
+    assertThat(service.getSlot("slot_neuro_001").remainingCount()).isEqualTo(2);
+
+    LockSlotResult replay = service.lockSlot("slot_neuro_001", "patient_demo_001", "idem-lock-001");
+    assertThat(replay.lockId()).isEqualTo(first.lockId());
+    assertThat(service.getSlot("slot_neuro_001").remainingCount()).isEqualTo(2);
+}
+
+@Test
+void createAppointmentRequiresPaidMockOrderAndIsIdempotent() {
+    LockSlotResult lock = service.lockSlot("slot_neuro_001", "patient_demo_001", "idem-lock-002");
+    PaymentOrder order = service.createPaymentOrder(lock.lockId(), "idem-pay-001");
+
+    assertThatThrownBy(() -> service.createAppointment(lock.lockId(), order.orderId(), "idem-appt-001"))
+        .hasMessageContaining("PAYMENT_NOT_PAID");
+
+    service.markMockPaid(order.orderId());
+    Appointment first = service.createAppointment(lock.lockId(), order.orderId(), "idem-appt-001");
+    Appointment replay = service.createAppointment(lock.lockId(), order.orderId(), "idem-appt-001");
+
+    assertThat(replay.appointmentId()).isEqualTo(first.appointmentId());
+    assertThat(first.mock()).isTrue();
+}
+```
+
+- [ ] **Step 2: Run tests and verify they fail**
+
+Run:
+
+```bash
+mvn -f mock-his-service/pom.xml -DskipTests=false test
+```
+
+Expected: compilation fails until the service, schema, and DTOs are implemented.
+
+- [ ] **Step 3: Implement SQLite schema and seed data**
+
+`schema.sql` must include these tables:
+
+```sql
+create table if not exists his_patient (
+  patient_id text primary key,
+  name text not null,
+  identity_no text,
+  phone text,
+  created_at text not null
+);
+create unique index if not exists uk_his_patient_identity on his_patient(identity_no) where identity_no is not null;
+create unique index if not exists uk_his_patient_phone on his_patient(phone) where phone is not null;
+
+create table if not exists his_department (
+  department_id text primary key,
+  department_code text not null unique,
+  name text not null,
+  floor text not null
+);
+
+create table if not exists his_doctor (
+  doctor_id text primary key,
+  department_id text not null,
+  name text not null,
+  title text not null,
+  specialties text not null,
+  room text not null
+);
+
+create table if not exists his_schedule (
+  schedule_id text primary key,
+  doctor_id text not null,
+  visit_date text not null,
+  session_name text not null,
+  amount_cent integer not null
+);
+
+create table if not exists his_slot (
+  slot_id text primary key,
+  schedule_id text not null,
+  time_period text not null,
+  total_count integer not null,
+  remaining_count integer not null,
+  version integer not null default 0
+);
+
+create table if not exists his_slot_lock (
+  lock_id text primary key,
+  slot_id text not null,
+  patient_id text not null,
+  status text not null,
+  expires_at text not null,
+  idempotency_key text not null unique,
+  created_at text not null
+);
+
+create table if not exists his_payment_order (
+  order_id text primary key,
+  lock_id text not null,
+  amount_cent integer not null,
+  status text not null,
+  qr_content text not null,
+  mock integer not null,
+  idempotency_key text not null unique,
+  created_at text not null
+);
+
+create table if not exists his_appointment (
+  appointment_id text primary key,
+  lock_id text not null,
+  order_id text not null,
+  appointment_no text not null,
+  status text not null,
+  mock integer not null,
+  idempotency_key text not null unique,
+  created_at text not null
+);
+```
+
+Seed at least:
+
+```text
+神经内科: 李明 主任医师, 王佳 副主任医师
+普通内科: 陈涛 主治医师
+tomorrow morning slots: 09:30-09:45, 09:45-10:00
+patient_demo_001: 张三 / 13800000000
+```
+
+- [ ] **Step 4: Implement REST endpoints**
+
+Implement exactly these demo endpoints:
+
+```text
+POST /mock-his/patients/search
+POST /mock-his/patients
+GET  /mock-his/departments
+GET  /mock-his/doctors?departmentId=
+GET  /mock-his/schedules?doctorId=&date=
+POST /mock-his/slots/lock
+POST /mock-his/slots/release
+POST /mock-his/payments/orders
+POST /mock-his/payments/mock-paid
+GET  /mock-his/payments/{orderId}
+POST /mock-his/appointments
+GET  /mock-his/appointments/{appointmentId}
+```
+
+- [ ] **Step 5: Run Mock HIS tests**
+
+Run:
+
+```bash
+mvn -f mock-his-service/pom.xml -DskipTests=false test
+```
+
+Expected: all Mock HIS tests pass.
+
+**Commit:** `feat(mock-his): add sqlite registration demo service`
+
+## Task 2: Extend MCP Tool Facade For Registration
+
+**Files:**
+
+- Modify: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/ai/mcp/application/McpToolService.java`
+- Modify: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/client/MockHisClient.java`
+- Create: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/domain/HisSlotLockResult.java`
+- Create: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/domain/HisPaymentOrder.java`
+- Create: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/mcp/his/domain/HisAppointment.java`
+- Modify: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/test/java/com/emoon/ai/mcp/application/McpToolServiceTest.java`
+
+- [ ] **Step 1: Write failing MCP tests**
+
+Add tests that assert:
+
+```java
+@Test
+void lockSlotDelegatesToMockHisAndReturnsLockId() {
+    HisSlotLockResult result = service.lockSlot("slot_neuro_001", "patient_demo_001", "trace-001", "idem-lock-001");
+    assertThat(result.lockId()).startsWith("LOCK");
+    assertThat(result.expiresAt()).isNotNull();
+}
+
+@Test
+void paymentAndAppointmentUseMockFlagAndIdempotencyKey() {
+    HisPaymentOrder order = service.createPaymentOrder("LOCK202606020001", "trace-001", "idem-pay-001");
+    assertThat(order.qrContent()).startsWith("demo-payment://orderId=");
+    assertThat(order.mock()).isTrue();
+
+    service.markMockPaid(order.orderId(), "trace-001");
+    HisAppointment appt = service.createAppointment("LOCK202606020001", order.orderId(), "trace-001", "idem-appt-001");
+    assertThat(appt.mock()).isTrue();
+    assertThat(appt.appointmentNo()).isNotBlank();
+}
+```
+
+- [ ] **Step 2: Run MCP tests and verify failure**
+
+Run:
+
+```bash
+mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp -DskipTests=false test
+```
+
+Expected: tests fail until new methods and DTOs exist.
+
+- [ ] **Step 3: Implement new tool methods**
+
+Add methods to `McpToolService`:
+
+```java
+HisSlotLockResult lockSlot(String slotId, String patientId, String traceId, String idempotencyKey);
+void releaseSlot(String lockId, String traceId, String idempotencyKey);
+HisPaymentOrder createPaymentOrder(String lockId, String traceId, String idempotencyKey);
+void markMockPaid(String orderId, String traceId);
+HisPaymentOrder queryPaymentStatus(String orderId, String traceId);
+HisAppointment createAppointment(String lockId, String orderId, String traceId, String idempotencyKey);
+```
+
+These methods must log tool name, traceId, risk level, mock flag, and result status. Query methods are allowed from Dify workflow; write/financial methods must be called only from Card Action orchestration.
+
+- [ ] **Step 4: Run MCP tests**
+
+Run:
+
+```bash
+mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp -DskipTests=false test
+```
+
+Expected: pass.
+
+**Commit:** `feat(mcp): add registration mock his tools`
+
+## Task 3: Fix Card Action Registration Flow
+
+**Files:**
+
+- Modify: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent/src/main/java/com/emoon/ai/agent/application/AgentActionOrchestrator.java`
+- Modify: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-card/src/main/java/com/emoon/ai/card/application/CardActionService.java`
+- Modify: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-card/src/main/java/com/emoon/ai/card/application/CardInstanceService.java`
+- Modify: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent/src/test/java/com/emoon/ai/agent/application/AgentActionOrchestratorTest.java`
+- Modify: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-card/src/test/java/com/emoon/ai/card/application/CardActionServiceTest.java`
+
+- [ ] **Step 1: Write failing orchestration test for lock → payment → appointment**
+
+Assert the exact card sequence:
+
+```text
+select_department -> doctor-selection
+select_doctor -> time-slot-selection
+select_time_slot -> confirm-appointment with lockId and expiresAt
+confirm_appointment -> payment-qrcode with mock qrContent
+mock_payment_paid -> appointment-success
+```
+
+Test expectation:
+
+```java
+assertThat(confirmCard.nextCard().get("cardKey")).isEqualTo("payment-qrcode");
+assertThat(paymentCard.nextCard().get("cardKey")).isEqualTo("appointment-success");
+assertThat(paymentCard.nextCardData().get("mock")).isEqualTo(true);
+```
+
+- [ ] **Step 2: Run agent/card tests and verify failure**
+
+Run:
+
+```bash
+mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent,emoon-infra/emoon-modules/emoon-ai/emoon-ai-card -DskipTests=false test
+```
+
+Expected: fail until the current direct `createAppointment` shortcut is replaced.
+
+- [ ] **Step 3: Implement proper action handlers**
+
+Change `AgentActionOrchestrator` behavior:
+
+```text
+select_time_slot:
+  read patientId from task context or use patient_demo_001 for demo
+  call mcpToolService.lockSlot(slotId, patientId, traceId, idempotencyKey)
+  patch task context with slotId, lockId, lockExpiresAt
+  create confirm-appointment card
+
+confirm_appointment:
+  read lockId from task context
+  call mcpToolService.createPaymentOrder(lockId, traceId, idempotencyKey)
+  patch task context with orderId
+  create payment-qrcode card with mock:true
+
+mock_payment_paid:
+  call mcpToolService.markMockPaid(orderId, traceId)
+  call mcpToolService.createAppointment(lockId, orderId, traceId, idempotencyKey)
+  complete task
+  create appointment-success card with mock:true
+```
+
+Keep Card Runtime from directly calling MCP. The call chain must remain:
+
+```text
+CardActionController -> CardActionService -> AgentActionOrchestrator -> McpToolService -> Mock HIS
+```
+
+- [ ] **Step 4: Enforce action idempotency**
+
+`CardActionService.submit()` must return the first result for repeated `idempotencyKey` on the same card/action. It must reject:
+
+```text
+expired card
+completed card with a new idempotencyKey
+unknown actionName
+high-risk action without confirm:true
+```
+
+- [ ] **Step 5: Run tests**
+
+Run:
+
+```bash
+mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent,emoon-infra/emoon-modules/emoon-ai/emoon-ai-card -DskipTests=false test
+```
+
+Expected: pass.
+
+**Commit:** `feat(agent): complete registration card action flow`
+
+## Task 4: Make Agent/SSE Demo Entry Stable
+
+**Files:**
+
+- Modify: `emoon-openplatform/src/main/java/com/emoon/openplatform/controller/v1/AgentChatController.java`
+- Modify: `emoon-openplatform/src/main/java/com/emoon/openplatform/service/impl/AgentChatApplicationServiceImpl.java`
+- Modify: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent/src/main/java/com/emoon/ai/agent/application/AgentRouterService.java`
+- Modify: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent/src/main/java/com/emoon/ai/agent/application/TaskStateService.java`
+- Modify: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent/src/main/java/com/emoon/ai/agent/application/TerminalReplyTemplateService.java`
+- Modify: `emoon-openplatform/src/test/java/com/emoon/openplatform/acceptance/TerminalMvpAcceptanceTest.java`
+
+- [ ] **Step 1: Add failing acceptance tests**
+
+Cover:
+
+```text
+POST /agent/chat/stream with "我头疼三天想挂号"
+  emits task_updated REGISTRATION
+  emits message_delta
+  emits card_created department-selection
+  emits completed with conversationId and traceId
+
+POST /agent/chat/stream with "下午" while WAIT_SELECT_TIME_SLOT
+  does not call DeepSeek
+  routes to current REGISTRATION task
+
+guide_screen attempts registration
+  emits blocked message and no registration card
+```
+
+- [ ] **Step 2: Run acceptance test and verify failure**
+
+Run:
+
+```bash
+mvn -pl emoon-openplatform -DskipTests=false -Dtest=TerminalMvpAcceptanceTest test
+```
+
+Expected: fail until SSE events and task/card behavior match the frozen contract.
+
+- [ ] **Step 3: Implement stable SSE event order**
+
+The stream must emit:
+
+```text
+event: task_updated
+event: message_delta
+event: message_completed
+event: card_created
+event: completed
+```
+
+For errors:
+
+```text
+event: error
+event: completed
+```
+
+Do not expose Dify raw event names to the frontend.
+
+- [ ] **Step 4: Implement router priority**
+
+Enforce:
+
+```text
+1. device policy
+2. activeTask unless explicit cancel/switch
+3. waitingCard
+4. deterministic registration/payment/cancel rules
+5. DeepSeek JSON classification
+6. clarification
+```
+
+- [ ] **Step 5: Run acceptance test**
+
+Run:
+
+```bash
+mvn -pl emoon-openplatform -DskipTests=false -Dtest=TerminalMvpAcceptanceTest test
+```
+
+Expected: pass.
+
+**Commit:** `feat(openplatform): stabilize registration sse entry`
+
+## Task 5: Wire Dify As Optional Demo Engine
+
+**Files:**
+
+- Modify: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent/src/main/java/com/emoon/mcp/engine/impl/DifyAgentEngine.java`
+- Create: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent/src/main/java/com/emoon/ai/agent/infrastructure/normalizer/DifyOutputNormalizer.java`
+- Create: `emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent/src/test/java/com/emoon/ai/agent/infrastructure/normalizer/DifyOutputNormalizerTest.java`
+- Modify: `docs/需求文档/统一入口客户端v0.1设计方案.md` only if implementation finds a contract mismatch.
+
+- [ ] **Step 1: Write failing normalizer tests**
+
+Add tests:
+
+```java
+@Test
+void validDepartmentCardOutputNormalizesToPlatformCard() {
+    NormalizedAgentResponse result = normalizer.normalize("""
+      {"schemaVersion":"1.0","answer":"建议优先看神经内科",
+       "taskUpdate":{"taskType":"REGISTRATION","currentStep":"TRIAGE_RECOMMEND_DEPARTMENT"},
+       "cards":[{"cardKey":"department-selection","cardData":{"departments":[{"departmentId":"D010","name":"神经内科","reason":"头痛伴恶心"}]}}],
+       "safety":{"riskLevel":"LOW","needEmergencyWarning":false}}
+      """);
+    assertThat(result.cards()).hasSize(1);
+    assertThat(result.cards().get(0).cardKey()).isEqualTo("department-selection");
+}
+
+@Test
+void invalidJsonFallsBackToTextWithTraceId() {
+    NormalizedAgentResponse result = normalizer.normalize("not json");
+    assertThat(result.cards()).isEmpty();
+    assertThat(result.degraded()).isTrue();
+}
+```
+
+- [ ] **Step 2: Run normalizer tests and verify failure**
+
+Run:
+
+```bash
+mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent -DskipTests=false -Dtest=DifyOutputNormalizerTest test
+```
+
+Expected: fail until the normalizer exists.
+
+- [ ] **Step 3: Implement normalizer**
+
+Normalizer rules:
+
+```text
+invalid JSON -> text-only degraded response
+unknown cardKey -> no card, warning
+cardData schema mismatch -> no card, error message with traceId
+sensitive identity fields in Dify output -> reject card
+high risk output -> force confirm card or manual warning
+```
+
+- [ ] **Step 4: Configure one Dify app for demo**
+
+Use one Dify app:
+
+```text
+outpatient-registration-agent
+```
+
+Branches inside the app:
+
+```text
+registration_intake
+triage_recommendation
+registration_reply
+```
+
+Dify must not directly call write/financial HIS tools. Dify can suggest a card and explanation; Card Action performs writes.
+
+- [ ] **Step 5: Run agent tests**
+
+Run:
+
+```bash
+mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent -DskipTests=false test
+```
+
+Expected: pass.
+
+**Commit:** `feat(agent): normalize dify registration output`
+
+## Task 6: Add End-To-End Acceptance Scripts
+
+**Files:**
+
+- Create: `scripts/demo-registration/README.md`
+- Create: `scripts/demo-registration/start-demo.sh`
+- Create: `scripts/demo-registration/reset-mock-his.sh`
+- Create: `scripts/demo-registration/registration-happy-path.sh`
+- Create: `scripts/demo-registration/registration-edge-cases.sh`
+- Create: `scripts/demo-registration/assertions.sh`
+
+- [ ] **Step 1: Write executable happy-path script**
+
+`registration-happy-path.sh` must perform:
+
+```text
+1. health check Mock HIS
+2. health check AI platform
+3. call /agent/chat/stream with "我头疼三天,还有点恶心,想挂号"
+4. extract department cardInstanceId
+5. POST select_department
+6. POST select_doctor
+7. POST select_time_slot
+8. POST confirm_appointment
+9. POST mock_payment_paid
+10. assert appointment-success card contains mock:true and appointmentNo
+```
+
+- [ ] **Step 2: Write edge-case script**
+
+`registration-edge-cases.sh` must cover:
+
+```text
+duplicate confirm returns same result
+unpaid order cannot create appointment
+expired lock forces reselect time slot
+guide screen cannot register
+Mock HIS timeout returns error card with traceId
+Dify invalid JSON degrades to text and no card
+```
+
+- [ ] **Step 3: Run scripts locally**
+
+Run:
+
+```bash
+bash scripts/demo-registration/reset-mock-his.sh
+bash scripts/demo-registration/registration-happy-path.sh
+bash scripts/demo-registration/registration-edge-cases.sh
+```
+
+Expected: all scripts exit 0 and print the final `traceId`, `appointmentId`, `appointmentNo`, and `mock:true`.
+
+**Commit:** `test(demo): add registration acceptance scripts`
+
+## Task 7: Final Verification
+
+**Files:**
+
+- Review: all files changed in Tasks 1-6.
+- Update only if needed: `docs/需求文档/统一入口客户端v0.1设计方案.md`
+- Update only if needed: `docs/接口文档/terminal-client-mvp-contract.md`
+
+- [ ] **Step 1: Run module tests**
+
+Run:
+
+```bash
+mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp -DskipTests=false test
+mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-agent -DskipTests=false test
+mvn -pl emoon-infra/emoon-modules/emoon-ai/emoon-ai-card -DskipTests=false test
+mvn -pl emoon-openplatform -DskipTests=false test
+```
+
+Expected: all pass.
+
+- [ ] **Step 2: Run compile check**
+
+Run:
+
+```bash
+mvn -pl emoon-openplatform -am -DskipTests compile
+```
+
+Expected: compile succeeds.
+
+- [ ] **Step 3: Run architecture test**
+
+Run:
+
+```bash
+mvn -pl emoon-admin -DskipTests=false -Dprofiles.active= -Dtest=AiPlatformArchitectureTest test
+```
+
+Expected: architecture test passes. No Controller imports Mapper. No API module contains implementation classes. Card module does not call MCP directly.
+
+- [ ] **Step 4: Prepare demo evidence**
+
+Collect:
+
+```text
+Mock HIS sqlite db path
+seed data version
+conversationId
+taskId
+cardInstanceId sequence
+traceId
+appointmentId
+appointmentNo
+mock:true screenshot or API response
+```
+
+**Commit:** `chore(demo): verify registration demo loop`
+
+## Self-Review
+
+Spec coverage:
+
+- SQLite Mock HIS replaces the MySQL assumption.
+- Dify remains optional/controlled and cannot write HIS.
+- DeepSeek is a fallback classifier, not the task controller.
+- Card Action owns write confirmation and idempotency.
+- Mock payment is explicitly marked and cannot impersonate real payment.
+- Acceptance scripts cover happy path and core failure cases.
+
+Placeholder scan:
+
+- No unresolved placeholder terms or unspecified implementation steps remain.
+
+Type consistency:
+
+- `lockId`, `orderId`, `appointmentId`, `appointmentNo`, `traceId`, `idempotencyKey`, and `mock:true` are used consistently across Mock HIS, MCP, Card Action, and scripts.
+
+## Execution Choice
+
+Plan complete and saved to `docs/superpowers/plans/2026-06-02-unified-entry-registration-demo.md`.
+
+Two execution options:
+
+1. **Subagent-Driven (recommended)** - dispatch a fresh subagent per task, review between tasks, fast iteration.
+2. **Inline Execution** - execute tasks in this session using executing-plans, batch execution with checkpoints.
+
+Recommended for this repo: Subagent-Driven, because Mock HIS, MCP/Card, OpenPlatform SSE, and Dify normalizer are separate review surfaces.

+ 372 - 0
docs/superpowers/specs/2026-06-02-terminal-web-demo-design.md

@@ -0,0 +1,372 @@
+# Unified Entry Terminal Web Demo Design
+
+## 1. Objective
+
+Build a single-application Web demo for the unified entry terminal client. The demo should feel like an intelligent hospital service entrance rather than a traditional menu system, and it must support one complete outpatient registration flow for next-week customer demonstration.
+
+The first version prioritizes desktop Web demonstration at `1440x900` and `1920x1080`. It does not target vertical self-service kiosk screens yet.
+
+## 2. Product Positioning
+
+The Web demo represents a hospital-facing AI service entrance similar in spirit to a friendly healthcare assistant. The patient should express intent through natural language. The system then recognizes intent, routes to the right backend task, and presents deterministic business cards only when the flow requires selection or confirmation.
+
+The UI must not look like a traditional feature menu. It should not make registration, triage, doctor lookup, and route guidance appear as a growing left navigation tree of agents. The visible UI can explain what the assistant can handle, but the primary entry point is conversation.
+
+## 3. Scope
+
+### In Scope
+
+- Single Vue 3 + TypeScript + Vite Web application.
+- A+B layout direction:
+  - A: friendly AI assistant entry feel.
+  - B: task workspace once registration begins.
+- Complete registration flow:
+  - user message or typed intent
+  - department recommendation card
+  - doctor selection card
+  - time slot selection card
+  - appointment confirmation card
+  - mock payment card
+  - appointment success card
+- SSE conversation handling for `message_delta`, `message_completed`, `task_updated`, `card_created`, `error`, and `completed`.
+- Card action handling with `idempotencyKey`.
+- Static cartoon doctor assistant image in the right context panel when no card is active.
+- Mock result labeling whenever backend returns `mock:true`.
+- Desktop visual QA for `1440x900` and `1920x1080`.
+
+### Out Of Scope
+
+- Real HIS integration.
+- Real payment,医保,退号退费.
+- Full robot, kiosk, guide-screen multi-app monorepo.
+- TTS/STT implementation.
+- Dynamic avatar animation, lip sync, listening animation, or voice state transitions.
+- Medical diagnosis text generated by frontend.
+- Direct Dify, HIS, MCP Tool, tongue diagnosis, or metering calls from frontend.
+
+## 4. Brand Color System
+
+Company theme colors:
+
+| Token | Value | Usage |
+| --- | --- | --- |
+| `brandPrimary` | `#2b1f99` | Main action, selected task, user message, active progress |
+| `brandAccent` | `#3ad4d8` | Accent border, secondary action, progress highlight, assistant glow |
+| `pageBg` | `#f7fbff` | App background |
+| `surface` | `#ffffff` | Panels and cards |
+| `softTint` | `#f4fdff` | Selected card background and assistant hint background |
+| `border` | `#dfe7f7` | Panel borders |
+| `textPrimary` | `#16133a` | Main text |
+| `textSecondary` | `#69708a` | Secondary text |
+| `warningBg` | `#fff8e8` | Mock/payment warning surface |
+| `warningText` | `#735b14` | Mock/payment warning text |
+
+The palette should feel clean and medical-tech oriented. It should not drift into a generic blue dashboard or use heavy gradients across the whole page. Gradients are allowed only on the assistant identity mark and doctor image environment.
+
+## 5. Layout
+
+Use a three-column desktop layout:
+
+```text
+Header
+├── Left Assistant Context Panel
+├── Center Conversation Panel
+└── Right Context Panel
+```
+
+### Header
+
+The header contains:
+
+- product name: `医梦门诊助手`
+- small environment label: `Web Demo · 门诊大厅 · 联调演示`
+- optional device identifier when available
+
+### Left Assistant Context Panel
+
+The left panel is not a functional menu. It contains:
+
+- assistant identity block
+- short explanation: `说出需求,我来判断下一步`
+- examples the patient can say:
+  - `我头疼三天,想挂号`
+  - `明天上午有神经内科的号吗`
+  - `我想找李明医生`
+- current capability tags, rendered as informational chips:
+  - `挂号`
+  - `导诊`
+  - `医生查询`
+- mock/demo warning note
+
+It must not render large clickable buttons for each agent or workflow.
+
+### Center Conversation Panel
+
+The center panel is the only input area in the first version.
+
+It contains:
+
+- conversation title and brief instruction
+- streamed assistant messages
+- user messages
+- input field
+- voice button
+- send button
+
+The voice button is visible for future STT affordance but does not implement real STT in this version. It can show a disabled/demo behavior or a short toast depending on implementation readiness.
+
+### Right Context Panel
+
+The right panel is a stateful context panel.
+
+State `idle`:
+
+- shows static cartoon doctor assistant image only
+- shows short supporting text:
+  - `医生助手`
+  - `当前仅作为静态形象展示。语音与发送操作仍在中间输入区完成。`
+- no voice button
+- no send button
+- no `可听` / `可说` interaction chips
+
+State `task`:
+
+- shows registration progress
+- shows current business card
+- card actions are rendered from backend `actions`
+
+State `error`:
+
+- shows error card with user-readable message
+- includes `traceId` when available
+- offers a retry or restart action if the backend response supports it
+
+State `completed`:
+
+- shows appointment success card
+- includes `mock:true` label when present
+- user can click `完成` to return right panel to `idle`
+
+## 6. Doctor Assistant Asset
+
+Do not draw the doctor assistant with CSS primitives.
+
+Use a real static image asset. A generated candidate already exists at:
+
+```text
+/Users/destiny/.codex/generated_images/019e8719-cee0-7723-83a4-ac1a9e0a3bd4/ig_037570e350a4f508016a1e882c0c188191b10d3dbe9b5f776a.png
+```
+
+Implementation should copy this image into the frontend project as:
+
+```text
+src/assets/doctor-assistant.png
+```
+
+The original generated image must remain in place unless explicitly deleted by the user.
+
+Asset requirements:
+
+- polished 2D cartoon doctor assistant
+- white coat and friendly expression
+- uses `#2b1f99` and `#3ad4d8` as accents
+- no text in image
+- no third-party logo
+- fits the right panel without cropping at `1440x900`
+
+## 7. Interaction Flow
+
+### Startup
+
+```text
+load app config
+→ register device or use demo device
+→ heartbeat every 30 seconds
+→ load scene profile
+→ render idle layout
+```
+
+For Web demo, a demo signer or dev-only bearer/HMAC helper may be used, but signing secrets must not be placed in browser-exposed `VITE_*` variables for real deployment.
+
+### Registration Start
+
+User can type:
+
+```text
+我头疼三天,还有点恶心,想挂号
+```
+
+Frontend calls:
+
+```text
+POST /api/v1/agent/chat/stream
+```
+
+Frontend must not pass `agentId`.
+
+SSE handling:
+
+```text
+task_updated -> set active task and right panel task state
+message_delta -> append assistant text
+message_completed -> finalize assistant message
+card_created -> fetch card instance and render right panel card
+completed -> store conversationId and traceId
+error -> show error state
+```
+
+### Card Flow
+
+Card flow:
+
+```text
+department-selection
+→ doctor-selection
+→ time-slot-selection
+→ confirm-appointment
+→ payment-qrcode
+→ appointment-success
+```
+
+Every card action posts:
+
+```text
+POST /api/v1/cards/{cardInstanceId}/actions/{actionName}
+```
+
+The request must include:
+
+```json
+{
+  "idempotencyKey": "stable-key-for-this-logical-action",
+  "confirm": true,
+  "payload": {}
+}
+```
+
+### Mock Payment
+
+The payment card must clearly label:
+
+```text
+联调演示 / Mock 支付
+```
+
+The mock payment action can be rendered as a visible demo button only in Web demo mode. It must not use WeChat, Alipay,医保, or any real payment brand.
+
+## 8. Components
+
+Core components:
+
+| Component | Responsibility |
+| --- | --- |
+| `AppShell` | Header and three-column layout |
+| `AssistantContextPanel` | Left assistant identity, examples, capability chips |
+| `ConversationPanel` | Message stream, input, voice button, send button |
+| `ContextPanel` | Right panel state switch: idle/task/error/completed |
+| `DoctorAssistantFigure` | Static doctor image display |
+| `RegistrationProgress` | Step progress for registration |
+| `CardRenderer` | Switches cardKey to card component |
+| `DepartmentSelectionCard` | Renders departments and select action |
+| `DoctorSelectionCard` | Renders doctors and select action |
+| `TimeSlotSelectionCard` | Renders slots and select action |
+| `ConfirmAppointmentCard` | Summary and second confirmation |
+| `PaymentQrCard` | Mock payment QR/content and mock paid action |
+| `AppointmentSuccessCard` | Appointment result and mock label |
+| `ErrorCard` | Error message, traceId, retry/restart action |
+
+## 9. State Model
+
+Use a small frontend store with these slices:
+
+```text
+device:
+  deviceId
+  deviceType
+  sceneProfile
+  heartbeatStatus
+
+conversation:
+  conversationId
+  messages[]
+  streaming
+  currentTraceId
+
+task:
+  taskId
+  taskType
+  currentStep
+  status
+
+contextPanel:
+  mode: idle | task | error | completed
+  activeCardInstanceId
+  activeCard
+
+ui:
+  inputText
+  pendingActionKey
+  demoMode
+```
+
+The right panel mode is derived primarily from task/card state:
+
+```text
+no active task and no active card -> idle
+active card -> task
+error event -> error
+appointment-success card -> completed
+```
+
+## 10. Error Handling
+
+Required behavior:
+
+- Auth error: show startup blocking message.
+- Device pending/rejected: show startup blocking state, not the main UI.
+- SSE disconnect: keep existing messages and offer retry.
+- Invalid card: show error card with `traceId`.
+- Expired card: show card disabled state and ask user to restart current step.
+- Duplicate submit: keep first returned result, do not visually create a second card.
+- Mock HIS timeout: show error card, do not pretend success.
+
+## 11. Testing And QA
+
+Use Playwright for visual and interaction QA.
+
+Viewports:
+
+```text
+1440x900
+1920x1080
+```
+
+Minimum checks:
+
+- Header and three columns fit without horizontal scroll.
+- Left panel has no large workflow buttons.
+- Center panel owns voice and send buttons.
+- Right idle panel shows the static doctor image only.
+- Starting registration switches right panel from idle to task.
+- All card actions use idempotency keys.
+- Mock payment and appointment success show `联调演示` or `Mock`.
+- Text does not overflow buttons/cards at desktop viewport.
+
+## 12. Open Decisions
+
+None for the first implementation plan.
+
+The first implementation plan should build the Web demo as a single application. Later work can split it into `packages/api-client`, `packages/medical-cards`, and multi-terminal apps when the first Web demo is accepted.
+
+## 13. Approval Summary
+
+Approved direction:
+
+- A+B mixed layout.
+- Web browser demo first.
+- Registration loop first.
+- Brand colors `#3ad4d8` and `#2b1f99`.
+- Dialogue-first design, no left workflow menu.
+- Right panel is context panel.
+- Idle right panel shows a static cartoon doctor only.
+- Voice and send controls stay in center conversation input.

+ 1 - 1
docs/架构文档/统一入口客户端前端接入指引_v1.0.md

@@ -729,7 +729,7 @@ npx @openapitools/openapi-generator-cli generate \
 ```bash
 # 后端
 cd emoon-backend
-# 确保 application-dev.yml 中 datasource 指向本地 MySQL
+# AI 中台后端沿用本地 MySQL;独立 Mock HIS 服务使用 SQLite,不接入 AI 中台 datasource
 mvn -pl emoon-admin spring-boot:run
 
 # 前端

+ 806 - 0
docs/需求文档/统一入口客户端v0.1设计方案.md

@@ -0,0 +1,806 @@
+## 结论:方向可行,但第 3 步必须收敛
+
+你的 4 步整体方向是合理的,但要做三个关键修正:
+
+| 你原来的想法                            | 判断                | 修正方案                                                                                  |
+| --------------------------------- | ----------------- | ------------------------------------------------------------------------------------- |
+| 部署极简 HIS Mock 服务                  | 合理                | 必须模拟真实 HIS 的关键语义:患者、科室、医生、排班、号源锁定、订单、支付、预约确认                                          |
+| AI 中台通过 MCP 注册 HIS 接口             | 合理                | 下周演示不必完整实现标准 MCP 协议,可先实现 `MCP Tool Service / HTTP Tool Facade`,但接口形态要按未来 MCP 治理设计     |
+| DeepSeek 意图识别后转发 Dify 不同 workflow | **部分合理,但不能直接这么做** | DeepSeek 只能作为“无活跃任务时”的兜底分类器;优先级必须是:任务状态机 > 卡片状态 > 确定性规则 > DeepSeek 分类 > Dify Workflow |
+| Web 端类似蚂蚁阿福完成挂号闭环                 | 合理                | Web 端只负责对话和卡片交互,不直连 Dify、不直连 HIS、不直接完成业务写操作                                           |
+
+这个边界和你们现有中台技术方案一致:OpenPlatform 负责入口、权限、会话、卡片状态、审计和事实记录;Dify 负责流程编排;MCP/HIS Adapter 负责院内系统工具调用;统一入口客户端负责终端交互,而不是业务事实判断。
+
+---
+
+# 一、最终建议方案
+
+下周演示建议只做一个清晰闭环:
+
+> **患者通过统一入口 Web 客户端自然语言发起挂号诉求 → AI 中台识别任务 → Dify 完成问诊/导诊/话术编排 → MCP 调用 Mock HIS 查询科室、医生、号源 → 前端卡片让患者选择 → Card Runtime 执行建档、锁号、支付、预约确认 → 返回挂号成功卡片。**
+
+不要做成“Dify 直接把所有事情干完”。原因是挂号、建档、支付、锁号都属于确定性业务动作,必须经过卡片确认、幂等、状态机和后端工具调用。你们对外接口文档也明确:普通终端不得直接调用 HIS Tool,不直接持有 Dify Key;写操作必须幂等;设备和客户端只能走 Agent API、Card Action API、Device API 等受控入口。
+
+---
+
+# 二、系统分层
+
+## 2.1 演示版系统组成
+
+```text
+统一入口 Web 客户端
+  ↓
+AI 中台 OpenPlatform
+  ↓
+AgentRouter / TaskStateService / Card Runtime
+  ↓
+Dify 挂号智能体 Workflow
+  ↓
+MCP Tool Service
+  ↓
+Mock HIS Service
+```
+
+## 2.2 各层职责
+
+| 层                        | 负责                           | 不负责                 |
+| ------------------------ | ---------------------------- | ------------------- |
+| 统一入口 Web 客户端             | 聊天 UI、虚拟助手、业务卡片、二维码展示、用户点击确认 | 不做业务判断,不直连 Dify/HIS |
+| OpenPlatform             | 鉴权、会话、任务状态、路由、SSE、卡片实例、审计    | 不写复杂自然语言流程          |
+| AgentRouter              | 判断当前消息应进入哪个业务流程              | 不直接生成 AI 回复         |
+| DeepSeekIntentClassifier | 无活跃任务时做意图识别和槽位抽取             | 不直接调用 HIS,不决定业务写操作  |
+| Dify Workflow            | 问诊追问、导诊话术、科室推荐解释、卡片输出建议      | 不直接写 HIS,不直接完成支付    |
+| Card Runtime             | 创建卡片实例、处理用户动作、幂等、推进任务状态      | 不做自然语言理解            |
+| MCP Tool Service         | 封装 HIS 工具、审计、幂等、错误码映射        | 不做 UI,不做对话          |
+| Mock HIS Service         | 模拟真实 HIS 业务接口                | 不包装成真实支付/医保/医院接口    |
+
+当前 Demo 已经有类似“前端负责统一交互入口,后端负责对话意图路由与业务流程编排”的雏形,但它现在仍是 Demo 级:会话状态在内存里,建档写 CSV,医生排班模拟,卡片协议和任务状态机不完整。下周演示可以借鉴这个交互资产,但不能继续沿用 Demo 后端架构。
+
+---
+
+# 三、关键流程是否合理
+
+## 3.1 Step 1:极简 HIS Mock 服务
+
+**合理,而且必须先做。**
+
+否则 Dify 和前端都只能“假装挂号成功”,甲方追问“科室、医生、号源、支付、预约号从哪里来”时会露馅。
+
+### Mock HIS 最小接口
+
+| 模块 | 接口                                           | 说明               |
+| -- | -------------------------------------------- | ---------------- |
+| 患者 | `POST /mock-his/patients/search`             | 根据身份证号/手机号查询患者   |
+| 患者 | `POST /mock-his/patients`                    | 创建患者档案           |
+| 科室 | `GET /mock-his/departments`                  | 查询科室列表           |
+| 医生 | `GET /mock-his/doctors?departmentId=`        | 查询科室医生           |
+| 排班 | `GET /mock-his/schedules?doctorId=&date=`    | 查询医生排班           |
+| 号源 | `POST /mock-his/slots/lock`                  | 锁定号源,返回 `lockId` |
+| 号源 | `POST /mock-his/slots/release`               | 释放号源             |
+| 支付 | `POST /mock-his/payments/orders`             | 创建挂号支付订单,返回二维码   |
+| 支付 | `POST /mock-his/payments/mock-paid`          | 演示用:模拟支付成功       |
+| 支付 | `GET /mock-his/payments/{orderId}`           | 查询支付状态           |
+| 挂号 | `POST /mock-his/appointments`                | 支付成功后创建预约        |
+| 挂号 | `GET /mock-his/appointments/{appointmentId}` | 查询预约详情           |
+
+### Mock HIS 数据表
+
+| 表                   | 用途      |
+| ------------------- | ------- |
+| `his_patient`       | 模拟患者主索引 |
+| `his_department`    | 科室字典    |
+| `his_doctor`        | 医生信息    |
+| `his_schedule`      | 医生排班    |
+| `his_slot`          | 号源库存    |
+| `his_slot_lock`     | 号源锁定记录  |
+| `his_payment_order` | 支付订单    |
+| `his_appointment`   | 挂号记录    |
+
+### 关键实现约束
+
+1. **号源锁定必须有过期时间**:例如 5 分钟。
+2. **创建预约必须校验支付成功**。
+3. **重复提交必须幂等**:同一个 `idempotencyKey` 不能创建多条预约。
+4. **支付必须明确是模拟支付**:二维码可以是 `demo-payment://orderId=xxx`,不要伪装成真实微信/支付宝/医保支付。
+5. **号源扣减必须真实发生**:甲方反复演示时,应能看到剩余号源减少。
+
+---
+
+## 3.2 Step 2:AI 中台通过 MCP 注册 HIS 接口
+
+**合理,但下周演示不要追求完整 MCP 协议。**
+
+建议做一个“符合未来 MCP 思路的 HTTP Tool Service”:
+
+```text
+AI 中台 MCP Tool Service
+  ├── his.searchPatient
+  ├── his.createPatient
+  ├── his.listDepartments
+  ├── his.listDoctors
+  ├── his.listSchedules
+  ├── his.lockSlot
+  ├── his.releaseSlot
+  ├── his.createPaymentOrder
+  ├── his.queryPaymentStatus
+  └── his.createAppointment
+```
+
+你们中台方案本身也写清楚了:MCP 是医梦内部 AI 工具网关设计,MVP 阶段可以先用 HTTP/JSON Tool Service 实现,不必一次性实现完整 Model Context Protocol 生态;核心目的是让 Dify 工具调用、卡片动作和 HIS Adapter 都进入同一个可授权、可审计、可幂等的工具出口。
+
+### 工具注册结构
+
+每个工具至少包含:
+
+```json
+{
+  "toolName": "his.lockSlot",
+  "riskLevel": "BUSINESS_WRITE",
+  "method": "POST",
+  "path": "/mock-his/slots/lock",
+  "timeoutMs": 3000,
+  "idempotent": true,
+  "requiredContext": ["projectId", "conversationId", "patientId"],
+  "inputSchema": {},
+  "outputSchema": {}
+}
+```
+
+### 风险分级
+
+| 风险级别   | 工具          | 控制策略                          |
+| ------ | ----------- | ----------------------------- |
+| 查询     | 查询科室、医生、排班  | 可由 Dify 调用                    |
+| 敏感查询   | 查询患者档案      | 必须有患者身份上下文                    |
+| 业务写入   | 建档、锁号、预约    | 必须通过 Card Action,不能由 Dify 直接写 |
+| 财务动作   | 创建支付订单、确认支付 | 必须幂等,必须卡片确认                   |
+| 医疗正式写入 | 暂无          | 下周演示不做                        |
+
+---
+
+## 3.3 Step 3:Dify 挂号智能体与 DeepSeek 意图识别
+
+你的想法里最危险的是这一句:
+
+> “调用 DeepSeek 进行意图识别,然后再转发给 Dify 的不同 workflow。”
+
+这句话如果直接照做,会出现几个问题:
+
+1. 用户已经在选时间,输入“下午”,DeepSeek 可能无法判断上下文。
+2. 用户正在建档,突然问“医保能报销吗”,系统可能错误切换流程。
+3. Dify Workflow 太多,状态分散,难以保证挂号闭环。
+4. 写操作如果由 Dify 直接触发,会绕开幂等、卡片确认和审计。
+5. 多轮对话中最重要的是任务状态,不是每句话重新识别意图。
+
+### 正确方案
+
+```text
+优先级 1:是否有 activeTask
+优先级 2:是否有 waitingCard
+优先级 3:确定性规则
+优先级 4:DeepSeek 意图分类
+优先级 5:Dify Workflow
+```
+
+也就是:
+
+> **DeepSeek 只做“新任务入口”和“意图兜底”,不做整个挂号流程的主控。**
+
+### DeepSeek 分类输出
+
+只输出结构化 JSON:
+
+```json
+{
+  "intent": "REGISTRATION",
+  "subIntent": "SYMPTOM_TRIAGE",
+  "confidence": 0.91,
+  "slots": {
+    "symptom": "头疼三天,有点恶心",
+    "departmentName": null,
+    "doctorName": null,
+    "dateText": null,
+    "timePreference": null
+  },
+  "risk": {
+    "emergencyLevel": "NORMAL",
+    "needEmergencyWarning": false
+  },
+  "shouldAskClarification": false
+}
+```
+
+### 不允许 DeepSeek 做的事
+
+| 禁止项          | 原因           |
+| ------------ | ------------ |
+| 直接返回“挂号成功”   | 它没有真实 HIS 结果 |
+| 直接决定扣号源      | 必须走 MCP 和锁号  |
+| 直接创建支付订单     | 财务动作必须幂等     |
+| 当前任务中每轮都重新路由 | 会破坏多轮流程      |
+| 替代任务状态机      | 无法保证流程可恢复    |
+
+---
+
+## 3.4 Step 4:统一入口 Web 客户端
+
+**合理。下周演示只做 Web,不要扩展机器人、自助机硬件。**
+
+统一入口客户端的真实目标是多终端运行时,但下周甲方演示只需要一个 Web 端先证明“蚂蚁阿福式入口 + 挂号闭环”成立。门诊方案里,统一入口本来就是门诊入口控制点,导诊大屏、自助机、机器人等都属于 P0 入口,但下周不必全部硬件落地。
+
+### Web 端必须包含
+
+| 页面/组件   | 说明                    |
+| ------- | --------------------- |
+| 虚拟助手区域  | 类似蚂蚁阿福的人机对话入口         |
+| 聊天消息流   | 支持用户输入、AI 回复、SSE 流式   |
+| 快捷入口    | 我要挂号、我要建档、科室导诊、查医生    |
+| 建档卡片    | 姓名、身份证、手机号、性别、出生日期    |
+| 科室推荐卡片  | 推荐科室、推荐理由、备选科室        |
+| 医生选择卡片  | 医生、职称、专长、可预约日期        |
+| 时间选择卡片  | 上午/下午/具体时段、剩余号源       |
+| 支付二维码卡片 | 二维码、金额、倒计时、模拟支付按钮     |
+| 挂号成功卡片  | 预约号、医生、科室、时间、诊区、二维码凭证 |
+| 异常提示卡片  | 无号源、支付失败、信息缺失、流程取消    |
+
+---
+
+# 四、确定的端到端流程
+
+## 4.1 启动前准备
+
+```text
+1. 启动 Mock HIS
+2. 导入科室、医生、排班、号源、示例患者
+3. AI 中台注册 HIS 工具
+4. AI 中台配置 DifyAgentEngine
+5. Dify 配置挂号相关 Workflow
+6. Web 端配置默认 agentCode = outpatient-registration-agent
+```
+
+---
+
+## 4.2 用户发起挂号
+
+用户输入:
+
+```text
+我头疼三天,还有点恶心,想挂号
+```
+
+流程:
+
+```text
+Web Client
+→ POST /agent/chat/stream
+→ OpenPlatform 创建 conversation
+→ TaskStateService 判断无 activeTask
+→ 规则未完全命中
+→ DeepSeekIntentClassifier 分类为 REGISTRATION + SYMPTOM_TRIAGE
+→ 创建 REGISTRATION task
+→ 调用 Dify triage workflow
+→ Dify 追问或推荐科室
+→ 创建 department-selection 卡片
+→ SSE 返回文本 + 卡片
+```
+
+---
+
+## 4.3 建档检查
+
+系统发现当前没有患者身份:
+
+```text
+TaskStateService
+→ currentStep = CHECK_PATIENT_IDENTITY
+→ MCP Tool: his.searchPatient
+→ 未找到 patient
+→ 创建 patient-record 卡片
+```
+
+用户填写:
+
+```text
+姓名:张三
+身份证:620xxxxxxxxxxxxx
+手机号:13800000000
+```
+
+点击确认:
+
+```text
+Web Client
+→ POST /cards/{cardInstanceId}/actions/confirm_create_patient
+→ Card Runtime 校验状态 + 幂等
+→ MCP Tool: his.createPatient
+→ Mock HIS 创建 patientId
+→ TaskStateService 写入 patientId
+→ 继续挂号任务
+```
+
+---
+
+## 4.4 症状分诊与科室推荐
+
+```text
+Dify triage workflow
+→ 读取 symptom slots
+→ 判断是否需要追问
+→ 若信息足够,输出推荐科室
+→ MCP Tool: his.listDepartments
+→ Card Runtime 创建 department-selection 卡片
+```
+
+卡片示例:
+
+```json
+{
+  "cardKey": "department-selection",
+  "data": {
+    "title": "为您推荐以下科室",
+    "departments": [
+      {
+        "departmentId": "D010",
+        "name": "神经内科",
+        "reason": "头痛伴恶心,建议优先排查神经系统相关问题"
+      },
+      {
+        "departmentId": "D001",
+        "name": "普通内科",
+        "reason": "可作为综合初筛科室"
+      }
+    ]
+  }
+}
+```
+
+---
+
+## 4.5 选择医生
+
+用户点击“神经内科”。
+
+```text
+Web Client
+→ Card Action: select_department
+→ Card Runtime 校验
+→ MCP Tool: his.listDoctors(departmentId)
+→ Mock HIS 返回医生列表
+→ 创建 doctor-selection 卡片
+```
+
+医生卡片:
+
+```json
+{
+  "cardKey": "doctor-selection",
+  "data": {
+    "departmentName": "神经内科",
+    "doctors": [
+      {
+        "doctorId": "DOC001",
+        "name": "李明",
+        "title": "主任医师",
+        "specialties": ["头痛", "眩晕", "脑血管病"],
+        "availableDateCount": 3
+      }
+    ]
+  }
+}
+```
+
+---
+
+## 4.6 选择时间与锁号
+
+用户选择医生和日期:
+
+```text
+Card Action: select_doctor
+→ MCP Tool: his.listSchedules
+→ 返回可用日期和时段
+→ 创建 time-slot-selection 卡片
+```
+
+用户选择时段:
+
+```text
+Card Action: select_time_slot
+→ MCP Tool: his.lockSlot
+→ Mock HIS 创建 lockId,锁定 5 分钟
+→ 创建 confirm-registration 卡片
+```
+
+这里必须有锁号,否则演示逻辑不真实。真实 HIS 场景里,号源并发和过期释放是挂号流程的核心风险点;你们技术方案里也明确排班号源工具需要幂等、短锁、超时释放和并发控制。
+
+---
+
+## 4.7 支付二维码
+
+用户确认挂号:
+
+```text
+Card Action: confirm_registration
+→ MCP Tool: his.createPaymentOrder
+→ Mock HIS 创建 orderId
+→ 返回 qrCodeBase64 / qrContent
+→ 创建 payment-qrcode 卡片
+```
+
+支付卡片:
+
+```json
+{
+  "cardKey": "payment-qrcode",
+  "data": {
+    "orderId": "PAY202606020001",
+    "amount": 25.0,
+    "qrContent": "demo-payment://PAY202606020001",
+    "expireSeconds": 300,
+    "debugAction": "mock_paid"
+  }
+}
+```
+
+演示中可以提供一个“模拟支付完成”按钮,但必须向团队明确:这只是 Mock 支付,不是实际支付。
+
+---
+
+## 4.8 支付成功后创建预约
+
+```text
+用户点击模拟支付完成
+→ MCP Tool: his.mockPaymentPaid
+→ 前端轮询或主动触发 queryPaymentStatus
+→ 支付成功
+→ MCP Tool: his.createAppointment(lockId, orderId)
+→ Mock HIS 消耗号源
+→ 生成 appointmentId
+→ 创建 appointment-success 卡片
+```
+
+挂号成功卡片:
+
+```json
+{
+  "cardKey": "appointment-success",
+  "data": {
+    "appointmentId": "APT202606020001",
+    "patientName": "张三",
+    "departmentName": "神经内科",
+    "doctorName": "李明",
+    "visitDate": "2026-06-05",
+    "timeSlot": "09:30-09:45",
+    "room": "门诊三楼 302 诊室",
+    "queueNo": "A023",
+    "tips": "请提前 15 分钟到诊区签到候诊"
+  }
+}
+```
+
+---
+
+# 五、推荐的 Dify Workflow 设计
+
+## 5.1 不建议一开始拆太多 Workflow
+
+为了下周演示稳定,建议先做 **1 个 Dify App + 3 个 Workflow 场景分支**,而不是做一堆互相跳转的 App。
+
+### Dify App
+
+```text
+outpatient-registration-agent
+```
+
+### 内部分支
+
+| 分支                      | 负责                        |
+| ----------------------- | ------------------------- |
+| `registration_intake`   | 判断是否挂号、建档、查医生、查科室         |
+| `triage_recommendation` | 症状追问、科室推荐、风险提示            |
+| `registration_reply`    | 围绕卡片动作生成自然语言说明、失败解释、下一步引导 |
+
+## 5.2 Dify 不负责的内容
+
+| 内容      | 归属                       |
+| ------- | ------------------------ |
+| 是否有患者身份 | OpenPlatform / TaskState |
+| 患者建档写入  | Card Runtime + MCP       |
+| 查询医生排班  | MCP Tool                 |
+| 锁号      | Card Runtime + MCP       |
+| 支付订单    | Card Runtime + MCP       |
+| 预约创建    | Card Runtime + MCP       |
+| 幂等      | Card Runtime / MCP       |
+| 审计      | OpenPlatform             |
+| 任务状态    | TaskStateService         |
+
+## 5.3 Dify 输出必须结构化
+
+Dify 每次输出必须包含:
+
+```json
+{
+  "answer": "我先为您推荐合适的科室。",
+  "taskUpdate": {
+    "taskType": "REGISTRATION",
+    "currentStep": "RECOMMEND_DEPARTMENT"
+  },
+  "cards": [
+    {
+      "cardKey": "department-selection",
+      "cardData": {}
+    }
+  ],
+  "safety": {
+    "riskLevel": "LOW",
+    "needEmergencyWarning": false
+  }
+}
+```
+
+OpenPlatform 必须对这个输出做校验。不能让 Dify 随便输出一个 JSON,前端就直接渲染。
+
+---
+
+# 六、AgentRouter 的确定实现
+
+## 6.1 路由优先级
+
+```text
+1. 当前是否有 activeTask
+2. 当前是否有 waitingCard
+3. 用户是否明确取消/切换任务
+4. 确定性规则:挂号、建档、医生、科室、支付、取消
+5. DeepSeek 意图分类
+6. 低置信度澄清
+```
+
+## 6.2 路由示例
+
+| 用户输入   | 当前状态          | 路由结果                          |
+| ------ | ------------- | ----------------------------- |
+| 我要挂号   | 无任务           | REGISTRATION_START            |
+| 头疼三天   | 无任务           | SYMPTOM_TRIAGE                |
+| 下午     | 正在选时间         | SELECT_TIME_SLOT,不调用 DeepSeek |
+| 换个医生   | 已选择医生         | 回退到 SELECT_DOCTOR             |
+| 取消     | 任意 activeTask | CANCEL_TASK,释放号源              |
+| 支付了吗   | 等待支付          | QUERY_PAYMENT_STATUS          |
+| 医保能报销吗 | 挂号中           | 临时 FAQ,不中断挂号;回答后提示继续          |
+| 胸痛喘不上气 | 任意            | EMERGENCY_WARNING,提示急诊/人工     |
+
+## 6.3 DeepSeek 分类 Prompt 重点
+
+DeepSeek 分类器不要写成聊天助手,而要写成严格分类器:
+
+```text
+你是医院统一入口客户端的意图分类器。
+只输出 JSON。
+不要回答用户问题。
+不要编造医院信息。
+不要判断挂号成功。
+如果用户处于 activeTask,优先根据 currentStep 解释用户输入。
+```
+
+---
+
+# 七、核心 Case 验证清单
+
+下周要经得起使用,至少要覆盖这些 case。
+
+## 7.1 正向链路
+
+| Case  | 输入        | 预期          |
+| ----- | --------- | ----------- |
+| 直接挂号  | 我要挂号      | 追问症状或科室     |
+| 症状挂号  | 我头疼三天想看病  | 推荐科室        |
+| 指定科室  | 我要挂神经内科   | 展示神经内科医生    |
+| 指定医生  | 我要挂李明医生   | 查询该医生排班     |
+| 指定时间  | 我要明天上午    | 筛选明天上午号源    |
+| 已建档患者 | 输入手机号/身份证 | 识别已有患者,跳过建档 |
+| 新患者   | 未查到患者     | 引导建档        |
+| 支付成功  | 扫码/模拟支付   | 创建预约并返回成功卡  |
+
+## 7.2 异常链路
+
+| Case        | 风险           | 处理                     |
+| ----------- | ------------ | ---------------------- |
+| 无号源         | 医生无可用号       | 推荐其他医生/日期              |
+| 号源锁过期       | 用户支付太慢       | 释放号源,提示重新选择            |
+| 重复点击确认      | 创建多条预约       | 幂等返回同一结果               |
+| 支付失败        | 订单未支付        | 提示重试/重新生成二维码           |
+| 建档信息缺失      | 无法创建 patient | 返回字段校验卡片               |
+| 用户中途取消      | 锁号未释放        | 释放 lockId,任务 cancelled |
+| 用户切换科室      | 状态混乱         | 回退到科室选择并清理后续状态         |
+| 急症描述        | 医疗风险         | 不走普通挂号,提示急诊/人工         |
+| Dify 输出异常   | 卡片无法解析       | 降级文本提示 + traceId       |
+| Mock HIS 超时 | 工具失败         | 返回可解释错误,不假装成功          |
+
+---
+
+# 八、演示范围必须冻结
+
+## 8.1 下周必须做
+
+| 模块               | 必做内容                                    |
+| ---------------- | --------------------------------------- |
+| Mock HIS         | 独立极简服务,使用 SQLite,覆盖患者、科室、医生、排班、号源、锁号、Mock 支付、预约 |
+| MCP Tool Service | 注册并调用 Mock HIS 工具,写操作带幂等键和 traceId           |
+| OpenPlatform     | `/agent/chat/stream`、会话、任务状态、路由、Dify 调用、卡片创建 |
+| AgentRouter      | activeTask / waitingCard 优先,规则兜底,必要时 DeepSeek 分类 |
+| Card Runtime MVP | 卡片实例、卡片动作、幂等、状态更新、下一张卡片返回                  |
+| Dify             | 一个挂号智能体 App,支持症状追问、科室推荐和失败解释,不直接写 HIS    |
+| Web 客户端          | 对话、卡片、二维码、模拟支付、成功页,所有业务按钮走 Card Action       |
+| 测试脚本             | 至少 20 个 case,覆盖正向和异常                    |
+
+## 8.2 下周不要做
+
+| 暂缓项                | 原因                        |
+| ------------------ | ------------------------- |
+| 真实 HIS 对接          | 你们现在没有接口资料                |
+| 真实支付               | 周期和合规风险高                  |
+| 医保结算               | 复杂度远超演示需要                 |
+| 机器人硬件联调            | 会拖慢挂号闭环                   |
+| 自助机读卡/打印           | 可用 Web 表单和二维码模拟           |
+| 完整 Device Registry | 下周只需固定演示设备和默认 deviceContext      |
+| 完整计费账本             | 先记录 usage/audit,不做真实能力值扣费 |
+| 多院多租户              | 演示只做单医院单项目                |
+| 舌诊/报告解读混入          | 容易稀释挂号主线                  |
+
+---
+
+# 九、最终架构流程
+
+```mermaid
+flowchart TD
+    A["患者在统一入口 Web 输入诉求"] --> B["OpenPlatform /agent/chat/stream"]
+    B --> C["加载 conversation 和 activeTask"]
+    C --> D{"是否有活跃任务"}
+    D -->|是| E["按任务状态继续流程"]
+    D -->|否| F["规则 + DeepSeek 意图分类"]
+    F --> G["创建 REGISTRATION task"]
+    E --> H["调用 Dify 挂号智能体"]
+    G --> H
+    H --> I["Dify 输出 answer + card suggestion"]
+    I --> J["OpenPlatform 校验输出"]
+    J --> K["Card Runtime 创建卡片实例"]
+    K --> L["Web 客户端展示卡片"]
+    L --> M["用户点击卡片动作"]
+    M --> N["Card Runtime 校验幂等和状态"]
+    N --> O["MCP Tool Service 调用 Mock HIS"]
+    O --> P["Mock HIS 返回业务结果"]
+    P --> Q["更新 task/card/conversation"]
+    Q --> R{"流程是否完成"}
+    R -->|否| H
+    R -->|是| S["返回挂号成功卡片"]
+```
+
+---
+
+# 十、推荐技术实现
+
+## 10.1 Mock HIS
+
+建议:
+
+```text
+Spring Boot 3 + SQLite
+```
+
+这里明确改为 SQLite,不使用 MySQL。原因:
+
+1. 下周演示目标是验证真实业务语义,不是验证数据库运维能力。
+2. SQLite 单文件部署,方便 Claude/DeepSeek/交付人员本地启动和重置演示数据。
+3. Mock HIS 是可替换的演示服务,不进入 AI 中台后端 Maven reactor,也不作为长期生产 HIS 数据库。
+4. 后续真实 HIS 接入通过 `HospitalAdapter` 替换,不依赖 Mock HIS 的数据库选型。
+
+SQLite 必须仍然模拟真实 HIS 语义:号源锁定、锁过期释放、支付状态、预约创建、幂等键、剩余号源扣减都要真实落库,不能用内存随机数据。
+
+### 服务名
+
+```text
+emoon-mock-his
+```
+
+### 部署
+
+```text
+docker-compose:
+  - emoon-mock-his
+  - emoon-ai-platform
+  - dify
+  - emoon-terminal-web
+```
+
+`emoon-mock-his` 挂载一个 SQLite 数据文件,例如 `./data/mock-his.db`。演示前提供 seed 脚本初始化科室、医生、排班、号源和示例患者;需要重置演示环境时删除或重新生成该 db 文件即可。
+
+### SQLite 表和约束
+
+| 表 | 必要约束 |
+| --- | --- |
+| `his_patient` | `identity_no`、`phone` 至少一个唯一索引,避免重复建档 |
+| `his_department` | `department_code` 唯一 |
+| `his_doctor` | 关联 `department_id`,包含职称、专长、诊室 |
+| `his_schedule` | 关联医生和日期,包含午别、价格 |
+| `his_slot` | 关联排班,包含 `total_count`、`remaining_count`、`version` |
+| `his_slot_lock` | `lock_id` 唯一,包含 `expires_at`、`status`、`idempotency_key` |
+| `his_payment_order` | `order_id` 唯一,包含 `mock:true`、`status`、`qr_content` |
+| `his_appointment` | `appointment_id` 唯一,`idempotency_key` 唯一 |
+
+号源锁定和预约创建要用 SQLite 事务包住。`his_slot.remaining_count` 扣减必须和锁记录/预约记录在同一事务中完成,避免重复点击或并发演示时生成多条预约。
+
+### Mock 支付口径
+
+Mock 支付只允许返回 `demo-payment://orderId=...` 或二维码图片内容,不允许使用微信、支付宝、医保等真实品牌或真实支付文案。前端必须展示“联调演示 / Mock 支付”标识。
+
+---
+
+## 10.2 AI 中台模块
+
+建议最小新增/改造:
+
+| 模块          | 类                                         |
+| ----------- | ----------------------------------------- |
+| 路由          | `AgentRouterService`                      |
+| 分类          | `DeepSeekIntentClassifier`                |
+| 任务          | `TaskStateService`                        |
+| Dify        | `DifyAgentEngine`                         |
+| 输出校验        | `DifyOutputNormalizer`                    |
+| 卡片          | `CardInstanceService`、`CardActionService` |
+| 工具          | `McpToolRegistry`、`McpToolInvokeService`  |
+| HIS Adapter | `MockHisAdapter`                          |
+| 支付          | `MockPaymentTool`                         |
+
+你们现有中台工程已经具备 `AgentEngine` 抽象、Mock 引擎、OpenPlatform 对话入口、项目签名鉴权、会话记录、初步卡片表等基础,但缺口是 `DifyAgentEngine` 尚未进入统一主链路、MCP 工具协议未形成生产工具集、卡片动作闭环不足。下周工作应优先补这几项,而不是扩展业务场景。
+
+---
+
+# 十一、演示脚本建议
+
+## 11.1 标准演示链路
+
+```text
+用户:我头疼三天,还有点恶心,想挂号。
+AI:我先了解一下情况……是否伴随发热、视物模糊?
+用户:没有发热,就是头痛恶心。
+AI:建议优先选择神经内科,也可选择普通内科。
+用户点击:神经内科。
+AI:以下是神经内科可预约医生。
+用户点击:李明 主任医师。
+AI:以下是可预约时间。
+用户点击:明天 09:30。
+AI:请确认挂号信息。
+用户点击:确认挂号。
+AI:请扫码支付挂号费 25 元。
+用户点击:模拟支付完成。
+AI:挂号成功,预约号 A023,请明天 09:15 到门诊三楼神经内科候诊区签到。
+```
+
+## 11.2 甲方可能追问
+
+| 甲方问题          | 建议回答                                                         |
+| ------------- | ------------------------------------------------------------ |
+| 这是真实 HIS 吗?   | 目前是 Mock HIS,用于演示完整接口闭环;真实项目会替换为医院 HIS Adapter,AI 中台和前端不需要大改 |
+| 支付是真的吗?       | 当前是模拟支付二维码,演示业务状态流转;真实支付会接医院现有支付平台                           |
+| AI 会不会乱挂号?    | 不会。AI 只负责理解诉求和生成建议,挂号、锁号、支付都必须经过卡片确认和后端工具调用                  |
+| 患者信息安全吗?      | 建档信息不进入 Dify,由后端卡片动作链路提交到 HIS;Dify 只处理必要上下文                  |
+| Dify 出错怎么办?   | OpenPlatform 有输出校验和降级;卡片必须通过 schema 校验后才会落库和展示               |
+| 后续能接真实 HIS 吗? | 可以。Mock HIS 和真实 HIS 都通过同一套 MCP Tool / HospitalAdapter 抽象接入   |
+
+---
+
+# 十二、最终判定
+
+这个需求可以做,而且下周演示可行。但必须按下面的边界执行:
+
+1. **Mock HIS 先行**,覆盖患者、科室、医生、排班、号源、支付、预约。
+2. **AI 中台作为唯一入口**,前端不直连 Dify,不直连 HIS。
+3. **DeepSeek 只做兜底意图识别和槽位抽取**,不做主流程控制。
+4. **Dify 负责编排和话术**,不直接完成建档、锁号、支付、预约写操作。
+5. **Card Runtime 承接所有关键业务动作**,点击卡片后再通过 MCP 调 Mock HIS。
+6. **MCP Tool Service 是 HIS 唯一工具出口**,查询可以给 Dify 用,写操作必须卡片确认。
+7. **支付明确为 Mock**,只演示二维码、订单状态和预约确认流程。
+8. **演示范围只做挂号闭环**,不要混入舌诊、报告、机器人硬件、自助机读卡等支线。
+
+冻结后的方案一句话:
+
+> **统一入口 Web 端负责像蚂蚁阿福一样与患者对话;OpenPlatform 负责会话、任务、路由、卡片和审计;DeepSeek 负责新任务意图分类;Dify 负责挂号流程的话术和分支编排;Card Runtime 负责所有确认动作;MCP Tool Service 负责调用 Mock HIS;Mock HIS 负责模拟真实挂号业务状态。**