Explorar o código

整理感知动线视频脚本与本地工具配置

WangKang hai 3 semanas
pai
achega
91d4555bcf

+ 6 - 0
.gitignore

@@ -63,3 +63,9 @@ openspec
 
 back-info/*
 .env
+
+reasonix.toml
+.reasonix
+video
+
+.cursor

+ 43 - 0
CLAUDE.md

@@ -0,0 +1,43 @@
+<!-- superpowers-zh:begin (do not edit between these markers) -->
+# Superpowers-ZH 中文增强版
+
+本项目已安装 superpowers-zh 技能框架(20 个 skills)。
+
+## 核心规则
+
+1. **收到任务时,先检查是否有匹配的 skill** — 哪怕只有 1% 的可能性也要检查
+2. **设计先于编码** — 收到功能需求时,先用 brainstorming skill 做需求分析
+3. **测试先于实现** — 写代码前先写测试(TDD)
+4. **验证先于完成** — 声称完成前必须运行验证命令
+
+## 可用 Skills
+
+Skills 位于 `.claude/skills/` 目录,每个 skill 有独立的 `SKILL.md` 文件。
+
+- **brainstorming**: 在任何创造性工作之前必须使用此技能——创建功能、构建组件、添加功能或修改行为。在实现之前先探索用户意图、需求和设计。
+- **chinese-code-review**: 中文 review 沟通参考——话术模板、分级标注(必须修复/建议修改/仅供参考)、国内团队常见反模式应对。仅在用户显式 /chinese-code-review 时调用,不要根据上下文自动触发。
+- **chinese-commit-conventions**: 中文 commit 与 changelog 配置参考——Conventional Commits 中文适配、commitlint/husky/commitizen 中文模板、conventional-changelog 中文配置。仅在用户显式 /chinese-commit-conventions 时调用,不要根据上下文自动触发。
+- **chinese-documentation**: 中文文档排版参考——中英文空格、全半角标点、术语保留、链接格式、中文文案排版指北约定。仅在用户显式 /chinese-documentation 时调用,不要根据上下文自动触发。
+- **chinese-git-workflow**: 国内 Git 平台配置参考——Gitee、Coding.net、极狐 GitLab、CNB 的 SSH/HTTPS/凭据/CI 接入差异与镜像同步配置。仅在用户显式 /chinese-git-workflow 时调用,不要根据上下文自动触发。
+- **dispatching-parallel-agents**: 当面对 2 个以上可以独立进行、无共享状态或顺序依赖的任务时使用
+- **executing-plans**: 当你有一份书面实现计划需要在单独的会话中执行,并设有审查检查点时使用
+- **finishing-a-development-branch**: 当实现完成、所有测试通过、需要决定如何集成工作时使用——通过提供合并、PR 或清理等结构化选项来引导开发工作的收尾
+- **mcp-builder**: MCP 服务器构建方法论 — 系统化构建生产级 MCP 工具,让 AI 助手连接外部能力
+- **receiving-code-review**: 收到代码审查反馈后、实施建议之前使用,尤其当反馈不明确或技术上有疑问时——需要技术严谨性和验证,而非敷衍附和或盲目执行
+- **requesting-code-review**: 完成任务、实现重要功能或合并前使用,用于验证工作成果是否符合要求
+- **subagent-driven-development**: 当在当前会话中执行包含独立任务的实现计划时使用
+- **systematic-debugging**: 遇到任何 bug、测试失败或异常行为时使用,在提出修复方案之前执行
+- **test-driven-development**: 在实现任何功能或修复 bug 时使用,在编写实现代码之前
+- **using-git-worktrees**: 当需要开始与当前工作区隔离的功能开发或执行实现计划之前使用——创建具有智能目录选择和安全验证的隔离 git 工作树
+- **using-superpowers**: 在开始任何对话时使用——确立如何查找和使用技能,要求在任何响应(包括澄清性问题)之前调用 Skill 工具
+- **verification-before-completion**: 在宣称工作完成、已修复或测试通过之前使用,在提交或创建 PR 之前——必须运行验证命令并确认输出后才能声称成功;始终用证据支撑断言
+- **workflow-runner**: 在 Claude Code / OpenClaw / Cursor 中直接运行 agency-orchestrator YAML 工作流——无需 API key,使用当前会话的 LLM 作为执行引擎。当用户提供 .yaml 工作流文件或要求多角色协作完成任务时触发。
+- **writing-plans**: 当你有规格说明或需求用于多步骤任务时使用,在动手写代码之前
+- **writing-skills**: 当创建新技能、编辑现有技能或在部署前验证技能是否有效时使用
+
+## 如何使用
+
+当任务匹配某个 skill 时,使用 `Skill` 工具加载对应 skill 并严格遵循其流程。绝不要用 Read 工具读取 SKILL.md 文件。
+
+如果你认为哪怕只有 1% 的可能性某个 skill 适用于你正在做的事情,你必须调用该 skill 检查。
+<!-- superpowers-zh:end -->

+ 134 - 0
docs/superpowers/specs/2026-06-05-perception-journey-video-script.md

@@ -0,0 +1,134 @@
+# 医梦 AI 未来医院 · 感知动线视频脚本 v2
+
+| 项目 | 内容 |
+| --- | --- |
+| 视频标题 | 医梦 AI 未来医院 — 感知驱动、动线引领、服务主动找人 |
+| 时长 | 约 55 秒 |
+| 风格 | 三屏分屏叙事(护士站 25% + 统一入口客户端 50% + 诊室屏 25%),黑边分隔 |
+| 品牌色 | 深色 `#2b1f99`、浅色 `#3ad4d8` |
+| 患者名称 | 医小梦(核心患者,全流程追踪) |
+| 医院 | 空海医院 |
+| 目标科室 | 神经内科(301 诊室) |
+| 目标医生 | 李明(主任医师) |
+
+## 三个屏幕的角色定义
+
+### 护士站(左屏 25%)
+科室共用设备,显示全院门诊患者总览。
+- **静态患者**(全程不变):王建国(神经内科,就诊中)、李秀英(呼吸内科,候诊中)、赵大伟(神经内科,候诊中)、钱晓红(消化内科,候诊中)
+- **核心患者 医小梦**:随流程更新状态
+
+### 统一入口客户端(中屏 50%)
+患者端主交互界面,三栏布局。参照 idle-v2.html 原型。
+
+### 诊室屏(右屏 25%)
+神经内科诊区共用电子屏(301 诊室),不是某位医生的个人设备。
+- **标题**:神经内科诊区 · 301诊室
+- **静态患者**(全程不变):王建国(A018,就诊中)、李秀英(A019,候诊)、赵大伟(A020,候诊)、钱晓红(A021,候诊)
+- **核心患者 医小梦**:仅在挂号神经内科后才出现在列表中,随流程更新状态
+- **底部**:当前就诊患者
+
+---
+
+## 第一幕:开场亮相(0:00 - 0:05)
+
+三屏同时从全黑淡入。
+
+### 中央主屏
+患者未识别,感知 L0。快捷入口灰色不可用。底部 CTA「绑定设备,开启感知服务」。右侧动线区仅「入院」高亮。
+
+### 护士站
+标题「门诊患者总览」。列表显示 4 位静态患者。底部统计:在线设备 0 · 今日接诊 4。
+
+### 诊室屏
+标题「神经内科诊区 · 301诊室」。列表显示 4 位静态患者(王建国就诊中,其余候诊)。医小梦不在列表中。底部显示当前就诊:王建国。
+
+**旁白**:患者来到医院门诊大厅,打开统一入口客户端。此时系统尚不知道他是谁,感知等级为 L0。护士站和诊室屏上已有其他患者,一切正常运转。
+
+---
+
+## 第二幕:感知绑定(0:05 - 0:14)
+
+### 中央主屏
+NFC 碰一碰 → 身份识别 → 感知 L0→L1。患者名更新为「医小梦」。快捷入口点亮。AI 推送问候卡片。
+
+### 护士站
+医小梦插入列表(顶部,带青色左边条高亮):已入院 · 门诊大厅。静态患者不变。在线设备:1 · 今日接诊:5。
+
+### 诊室屏
+医小梦**不在列表中**(未挂神经内科)。右上角弹出小通知「新患者入院 · 医小梦」,2 秒后消失。静态患者不变。
+
+**旁白**:医小梦 NFC 碰一碰,身份绑定。感知从 L0 升级到 L1。统一数字身份在全院设备间同步——护士站收到入院事件,诊室屏感知到新患者但暂不加入本科室列表。
+
+---
+
+## 第三幕:AI 多轮导诊挂号(0:14 - 0:32)
+
+### 阶段 3.1:症状输入 & AI 追问(0:14 - 0:19)
+- 中央主屏:患者输入"我头疼三天,还有点恶心" → AI 追问"有没有伴随发热、视物模糊?"
+- 护士站:医小梦 → 导诊中
+
+### 阶段 3.2:患者回应(0:19 - 0:22)
+- 中央主屏:患者"没有发热,就是头疼恶心想吐"
+- 护士站:医小梦 → AI 分析症状中
+
+### 阶段 3.3:科室推荐(0:22 - 0:26)
+- 中央主屏:AI 弹出科室选择卡——推荐「神经内科(专长头痛眩晕)」,备选「普通内科(综合初筛)」;患者点击「神经内科」
+- 护士站:医小梦 → 选择科室:神经内科
+
+### 阶段 3.4:医生选择(0:26 - 0:29)
+- 中央主屏:AI 列出神经内科可约医生——李明 主任医师(10:30 有号)、王芳 副主任医师(11:00 有号);患者点击「李明 主任医师」
+- 护士站:医小梦 → 选择医生:李明
+
+### 阶段 3.5:确认挂号(0:29 - 0:32)
+- 中央主屏:确认卡——神经内科 · 李明 主任医师 · 10:30 · 301诊室 · ¥25;点击确认 → 🎉 挂号成功
+- 护士站:医小梦 → 已挂号 · A023 · 10:30
+- **诊室屏**:医小梦首次出现在患者列表中(带青色左边条高亮)——`医小梦 · 李明 · A023 · 10:30 · 待签到`
+
+**旁白**:医小梦说出症状。AI 多轮追问后,给出科室和医生建议,医小梦自主选择。挂号完成——医小梦正式加入神经内科队列,诊室屏首次出现她的名字。
+
+---
+
+## 第四幕:动线推进 → 诊区签到(0:32 - 0:43)
+
+### 阶段 4.1:位置感知触发(0:32 - 0:36)
+- 中央主屏:位置感知变化 → "已进入神经内科诊区"
+- 护士站:医小梦位置 → 神经内科诊区
+- 诊室屏:医小梦标记「已到达诊区」
+
+### 阶段 4.2:主动签到(0:36 - 0:40)
+- 中央主屏:系统推送签到卡片 → 患者点击「一键签到」 → 签到成功
+- 护士站:医小梦 → 候诊中 · 第 23 位
+- 诊室屏:医小梦 → 候诊中 A023。底部当前就诊更新。
+
+### 阶段 4.3:候诊就绪(0:40 - 0:43)
+- 中央主屏:显示候诊卡——当前叫号 019,预计等候 15 分钟
+- 三屏稳定显示候诊状态
+
+**旁白**:动线推进到诊区签到。系统感知位置,主动推送签到卡片。护士站和诊室屏同步刷新——医小梦已在队列中。
+
+---
+
+## 第五幕:结尾定版(0:43 - 0:55)
+
+三屏汇聚缩小、淡出。品牌理念文字淡入:「感知 → 动线 → AI 调度 → 服务外显」。「患者少找路、少找窗口、少找设备」。EMOON · 医梦 Logo 淡入定版。
+
+**旁白**:感知知道谁在、在哪、该做什么。动线判断下一步该去哪里。AI 中台调度智能体。统一入口客户端让一切服务外显。医梦 AI 未来医院——服务主动找人,患者少走一步。
+
+---
+
+## 三屏联动总表
+
+| 时间 | 统一入口客户端 | 护士站(医小梦) | 诊室屏(医小梦) |
+|------|--------------|----------------|-----------------|
+| 0-5s | 未识别,L0 | (不在列表中) | (不在列表中) |
+| 6.5s | NFC绑定,身份=医小梦 | 已入院 · 门诊大厅 | 不在列表中(通知浮现后消失) |
+| 14-19s | AI追问症状 | 导诊中 | 不在列表中 |
+| 19-22s | 患者回应 | AI分析中 | 不在列表中 |
+| 22-26s | 科室选择 → 神经内科 | 选择科室:神经内科 | 不在列表中 |
+| 26-29s | 医生选择 → 李明 | 选择医生:李明 | 不在列表中 |
+| 29-32s | 确认挂号成功 A023 | 已挂号 · A023 · 10:30 | 加入列表:A023 · 待签到 |
+| 32-36s | 位置感知:已到诊区 | 位置→神经内科诊区 | 已到达诊区 |
+| 36-40s | 一键签到成功 | 候诊中 · 第23位 | 候诊中 A023 |
+| 40-43s | 候诊卡显示 | 候诊中 | 候诊中 |
+| 43-55s | 品牌定版 | — | — |

+ 563 - 0
docs/superpowers/specs/perception-journey-demo.html

@@ -0,0 +1,563 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>医梦 AI 未来医院 · 感知动线故事板</title>
+<style>
+:root {
+  --accent: #2b1f99; --accent-light: #3ad4d8;
+  --bg: #f5f3fb; --surface: #ffffff; --fg: #1a1835; --muted: #7a7698; --border: #e6e3f0;
+  --warn: #f0a020; --warn-bg: #fff8e8; --warn-text: #735b14;
+  --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-pill: 9999px;
+}
+*{margin:0;padding:0;box-sizing:border-box}
+html{-webkit-text-size-adjust:100%}
+body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC',system-ui,sans-serif;font-size:14px;background:#0a0a14;color:var(--fg);overflow:hidden;height:100vh;display:flex;flex-direction:column}
+
+.timeline-bar{height:26px;background:var(--accent);display:flex;align-items:center;padding:0 14px;gap:8px;flex-shrink:0}
+.tl-act{font-size:9px;color:rgba(255,255,255,.65);font-weight:500;letter-spacing:.03em;white-space:nowrap}
+.tl-steps{display:flex;gap:3px;flex:1}
+.tl-step{flex:1;height:3px;border-radius:2px;background:rgba(255,255,255,.2);transition:all .4s}
+.tl-step.active{background:#fff;box-shadow:0 0 6px rgba(255,255,255,.4)}
+.tl-step.done{background:rgba(255,255,255,.5)}
+.tl-time{font-size:9px;color:rgba(255,255,255,.65);font-weight:500;font-variant-numeric:tabular-nums}
+
+.app-shell{display:grid;grid-template-columns:21% 58% 21%;flex:1;min-height:0;padding:4px 6px 6px 6px;gap:5px;background:var(--bg)}
+.panel{border-radius:var(--radius-md);overflow:hidden;border:1px solid var(--border);background:var(--surface);display:flex;flex-direction:column;min-height:0}
+.ph{height:32px;display:flex;align-items:center;padding:0 10px;border-bottom:1px solid var(--border);flex-shrink:0;gap:5px}
+.ph .pi{width:16px;height:16px;border-radius:4px;display:grid;place-items:center;font-size:9px;flex-shrink:0}
+.ph .pt{font-size:10px;font-weight:600;color:var(--fg)}
+.ph .ps{font-size:8px;color:var(--muted);margin-left:auto}
+
+.pb{padding:8px;flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:6px;background:var(--bg);font-size:10px}
+
+.ns-stats{display:flex;gap:4px}
+.ns-stat{flex:1;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px;text-align:center}
+.ns-num{font-size:14px;font-weight:700;color:var(--accent)}
+.ns-lbl{font-size:7px;color:var(--muted);margin-top:1px}
+.ns-sec{font-size:8px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin-top:2px}
+.ns-patient{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 8px;display:flex;flex-direction:column;gap:2px;transition:all .5s}
+.ns-patient.highlight{border-color:var(--accent-light);box-shadow:0 0 0 1px var(--accent-light)}
+.np-row{display:flex;align-items:center;gap:5px}
+.np-name{font-size:11px;font-weight:600}
+.np-tag{font-size:7px;padding:1px 5px;border-radius:var(--radius-pill);font-weight:500}
+.np-tag.w{background:color-mix(in oklch,var(--warn) 12%,transparent);color:var(--warn-text)}
+.np-tag.ok{background:color-mix(in oklch,var(--accent-light) 12%,transparent);color:var(--accent)}
+.np-tag.done{background:color-mix(in oklch,var(--accent) 8%,transparent);color:var(--accent)}
+.np-detail{font-size:8px;color:var(--muted);display:flex;gap:6px;flex-wrap:wrap}
+.ns-notif{font-size:8px;padding:4px 7px;border-radius:var(--radius-sm);background:color-mix(in oklch,var(--accent) 5%,transparent);color:var(--accent);display:flex;align-items:center;gap:4px;border-left:2px solid var(--accent-light);animation:fadeUp .4s}
+
+.center-body{flex:1;display:flex;flex-direction:column;min-height:0;background:var(--surface);position:relative}
+.uc-topbar{height:34px;display:flex;align-items:center;padding:0 12px;border-bottom:1px solid var(--border);gap:6px;flex-shrink:0}
+.uc-logo{font-size:10px;font-weight:600;color:var(--accent);display:flex;align-items:center;gap:4px;white-space:nowrap}
+.uc-div{width:1px;height:14px;background:var(--border)}
+.uc-id{font-size:9px;color:var(--muted);display:flex;align-items:center;gap:3px}
+.uc-scene{padding:1px 6px;border-radius:var(--radius-pill);font-size:7px;font-weight:600;background:color-mix(in oklch,var(--accent) 8%,transparent);color:var(--accent)}
+.uc-perc{display:flex;align-items:center;gap:4px;margin-left:auto;font-size:7px}
+.ptag{border-radius:var(--radius-pill);padding:1px 5px;font-weight:500}
+.ptag.l0{background:color-mix(in oklch,var(--warn) 12%,transparent);color:var(--warn-text)}
+.ptag.l1{background:color-mix(in oklch,var(--accent) 8%,transparent);color:var(--accent)}
+.ptag.off{background:color-mix(in oklch,var(--muted) 8%,transparent);color:var(--muted)}
+
+.uc-main{flex:1;display:flex;min-height:0}
+.uc-left{width:170px;border-right:1px solid var(--border);padding:10px 8px;display:flex;flex-direction:column;gap:8px;overflow-y:auto;flex-shrink:0;background:var(--surface)}
+.uc-avatar{width:40px;height:40px;border-radius:50%;background:var(--accent);display:grid;place-items:center;margin:0 auto}
+.uc-avatar svg{width:22px;height:22px;stroke:#fff;stroke-width:2;fill:none}
+.uc-assist-info{text-align:center;font-size:10px}
+.uc-assist-name{font-weight:600;font-size:11px}
+.uc-assist-tag{font-size:8px;color:var(--muted);margin-top:1px}
+.uc-sep{height:0;border-top:1px solid var(--border);margin:0 -2px}
+.uc-label{font-size:7px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-weight:600}
+.uc-ex{font-size:9px;padding:4px 6px;background:var(--bg);border-radius:var(--radius-sm);color:var(--muted)}
+.uc-qbtn{font-size:9px;padding:4px 7px;border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--muted);display:flex;align-items:center;gap:4px;transition:all .2s}
+.uc-qbtn.on{color:var(--fg)}
+.uc-bind{padding:7px;border:1px dashed var(--accent);border-radius:var(--radius-md);background:color-mix(in oklch,var(--accent) 4%,transparent);text-align:center;margin-top:auto}
+.uc-bind .ub-title{font-size:9px;font-weight:500;color:var(--accent)}
+.uc-bind .ub-desc{font-size:7px;color:var(--muted);margin:3px 0 5px 0;line-height:1.3}
+.btn-sm{font-size:8px;padding:4px 10px;border-radius:var(--radius-pill);font-weight:500;cursor:pointer;transition:all .15s}
+.btn-primary{background:var(--accent);color:#fff;border:none}
+.btn-secondary{border:1px solid var(--border);color:var(--muted);background:var(--surface)}
+
+.uc-center{flex:1;display:flex;flex-direction:column;min-height:0;background:var(--bg)}
+.uc-chat-h{height:28px;display:flex;align-items:center;padding:0 10px;border-bottom:1px solid var(--border);flex-shrink:0;background:var(--surface);font-size:9px;color:var(--muted);gap:5px}
+.uc-chat-h .dot{width:5px;height:5px;border-radius:50%;background:var(--accent-light)}
+.uc-msgs{flex:1;padding:10px 14px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;overflow-y:auto}
+.uc-empty{display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center}
+.uc-empty-icon{width:42px;height:42px;border-radius:50%;background:color-mix(in oklch,var(--accent) 10%,transparent);display:grid;place-items:center}
+.uc-empty-text{font-size:14px;font-weight:600;color:var(--fg)}
+.uc-empty-hint{font-size:10px;color:var(--muted);max-width:260px;line-height:1.5}
+.uc-empty-chips{display:flex;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:2px}
+.uc-chip{padding:3px 8px;border-radius:var(--radius-pill);font-size:9px;color:var(--accent);background:color-mix(in oklch,var(--accent) 10%,transparent);border:1px solid color-mix(in oklch,var(--accent) 15%,transparent)}
+
+.uc-chat-content{display:none;flex-direction:column;gap:6px;width:100%;overflow-y:auto;flex:1;padding:4px 0}
+.uc-input-bar{height:38px;display:flex;align-items:center;padding:0 10px;border-top:1px solid var(--border);flex-shrink:0;background:var(--surface);gap:4px}
+.uc-input-bar .ub-inp{flex:1;height:28px;padding:0 10px;border:1px solid var(--border);border-radius:var(--radius-pill);font-size:10px;background:var(--bg);color:var(--fg);outline:none}
+.ub-send{width:28px;height:28px;border-radius:50%;background:var(--accent);color:#fff;display:grid;place-items:center;font-size:11px}
+
+.uc-right{width:170px;border-left:1px solid var(--border);padding:10px 8px;display:flex;flex-direction:column;gap:8px;overflow-y:auto;flex-shrink:0;background:var(--surface)}
+.ur-dev{font-size:9px;padding:5px 7px;border:1px solid var(--border);border-radius:var(--radius-sm);display:flex;align-items:center;gap:5px;background:var(--bg)}
+.ur-dev .ud-icon{width:20px;height:20px;border-radius:4px;display:grid;place-items:center;font-size:8px;flex-shrink:0}
+.ur-dev .ud-info{flex:1;min-width:0}
+.ur-dev .ud-name{font-size:9px;font-weight:500}
+.ur-dev .ud-sub{font-size:7px;color:var(--muted)}
+.ud-tag{font-size:7px;padding:1px 5px;border-radius:var(--radius-pill);font-weight:500}
+.ud-tag.unbound{background:color-mix(in oklch,var(--warn) 12%,transparent);color:var(--warn-text)}
+.ud-tag.bound{background:color-mix(in oklch,var(--accent-light) 12%,transparent);color:var(--accent)}
+
+.ur-journey{padding-left:16px;position:relative}
+.ur-journey::before{content:'';position:absolute;left:5px;top:5px;bottom:5px;border-left:1.5px dotted var(--border)}
+.ur-jn{position:relative;padding:2px 0 2px 7px;font-size:8px;color:var(--muted);line-height:1.3;transition:all .4s}
+.ur-jn::before{content:'';position:absolute;left:-12px;top:4px;width:5px;height:5px;border-radius:50%;background:var(--border);border:1.5px solid var(--surface);z-index:1;transition:all .4s}
+.ur-jn.current{color:var(--accent);font-weight:600}
+.ur-jn.current::before{background:var(--accent);box-shadow:0 0 0 3px color-mix(in oklch,var(--accent) 15%,transparent)}
+.ur-jn.done{color:var(--fg)}
+.ur-jn.done::before{background:var(--accent-light)}
+
+.ur-next{border:1px solid var(--accent);border-radius:var(--radius-md);padding:7px;background:color-mix(in oklch,var(--accent) 4%,transparent)}
+.ur-next .un-badge{font-size:7px;font-weight:600;color:var(--accent);text-transform:uppercase;letter-spacing:.03em}
+.ur-next .un-title{font-size:9px;font-weight:500;margin:2px 0}
+.ur-next .un-desc{font-size:7px;color:var(--muted);line-height:1.4}
+.ur-next .un-acts{display:flex;gap:3px;margin-top:4px}
+.ur-perc-grid{display:grid;grid-template-columns:1fr 1fr;gap:3px}
+.ur-perc{padding:3px 5px;border:1px solid var(--border);border-radius:var(--radius-sm);display:flex;flex-direction:column;gap:1px;background:var(--surface)}
+.ur-perc .up-l{font-size:6px;color:var(--muted);text-transform:uppercase;letter-spacing:.03em}
+.ur-perc .up-v{font-size:8px;font-weight:500}
+.up-v.on{color:var(--accent)} .up-v.off{color:var(--muted)} .up-v.warn{color:var(--warn-text)}
+.ur-hint{padding:5px 7px;background:color-mix(in oklch,var(--accent-light) 6%,transparent);border-radius:var(--radius-sm);font-size:7px;color:var(--muted);line-height:1.3;display:flex;gap:4px}
+
+.dr-info{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:7px}
+.dr-name{font-size:12px;font-weight:700}
+.dr-dept{font-size:8px;color:var(--muted);margin-top:1px}
+.dr-stats{font-size:8px;display:flex;gap:8px;color:var(--muted);margin-top:2px}
+.dr-patient{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-sm);padding:5px 7px;display:flex;flex-direction:column;gap:2px;transition:all .5s}
+.dr-patient.highlight{border-color:var(--accent-light)}
+.dp-row{display:flex;align-items:center;gap:5px}
+.dp-name{font-size:10px;font-weight:500}
+.dp-time{font-size:7px;color:var(--muted);margin-left:auto}
+.dp-tag{font-size:7px;padding:0 5px;border-radius:var(--radius-pill);font-weight:500}
+.dp-tag.pending{background:color-mix(in oklch,var(--warn) 12%,transparent);color:var(--warn-text)}
+.dp-tag.ok{background:color-mix(in oklch,var(--accent-light) 12%,transparent);color:var(--accent)}
+.dp-tag.tmr{background:color-mix(in oklch,var(--accent) 8%,transparent);color:var(--accent)}
+.dr-notif{font-size:8px;padding:3px 6px;border-radius:var(--radius-sm);background:color-mix(in oklch,var(--accent-light) 8%,transparent);color:var(--accent);display:flex;align-items:center;gap:3px;border-left:2px solid var(--accent-light);animation:fadeUp .4s}
+
+.bottombar{height:28px;background:var(--surface);border-top:1px solid var(--border);display:flex;align-items:center;padding:0 14px;gap:8px;flex-shrink:0}
+.bbl{font-size:8px;color:var(--muted);font-weight:500;white-space:nowrap}
+.bbn{display:flex;align-items:center;gap:4px;padding:2px 7px;border-radius:var(--radius-pill);font-size:7px;white-space:nowrap;background:color-mix(in oklch,var(--accent) 10%,transparent);color:var(--accent)}
+.bbn .b-dot{width:3px;height:3px;border-radius:50%;background:currentColor;opacity:.6}
+
+@DataFlow{0%{background-position:200% 0}100%{background-position:-200% 0}}
+.data-flow{position:absolute;height:2px;background:linear-gradient(90deg,transparent,var(--accent-light),transparent);background-size:200% 100%;animation:DataFlow 1s linear infinite;opacity:.4;pointer-events:none;z-index:5;display:none}
+
+@keyframes fadeUp{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}
+@keyframes fadeIn{from{opacity:0}to{opacity:1}}
+@keyframes slideUp{from{transform:translateY(15px);opacity:0}to{transform:translateY(0);opacity:1}}
+@keyframes ripple{0%{transform:scale(.4);opacity:.7}100%{transform:scale(2.6);opacity:0}}
+@keyframes breathe{0%,100%{opacity:1}50%{opacity:.5}}
+@keyframes glow{0%,100%{box-shadow:0 0 4px rgba(58,212,216,.2)}50%{box-shadow:0 0 12px rgba(58,212,216,.5)}}
+</style>
+</head>
+<body>
+
+<div class="timeline-bar" id="tlBar">
+  <span class="tl-act" id="tlAct">第一幕 · 开场亮相</span>
+  <div class="tl-steps" id="tlSteps">
+    <div class="tl-step active"></div><div class="tl-step"></div><div class="tl-step"></div><div class="tl-step"></div><div class="tl-step"></div>
+  </div>
+  <span class="tl-time" id="tlTime">0:00</span>
+</div>
+
+<div class="app-shell" id="appShell">
+  <!-- LEFT: Nurse Station -->
+  <div class="panel"><div class="ph"><span class="pi" style="background:color-mix(in oklch,var(--accent-light) 15%,transparent)">🏥</span><span class="pt">护士站 · 患者总览</span><span class="ps" id="nsStatus">等待患者</span></div>
+  <div class="pb" id="nsBody">
+    <div class="ns-stats"><div class="ns-stat"><div class="ns-num" id="nsOnline">0</div><div class="ns-lbl">在线设备</div></div><div class="ns-stat"><div class="ns-num" id="nsToday">0</div><div class="ns-lbl">今日接诊</div></div><div class="ns-stat"><div class="ns-num" id="nsTomorrow">0</div><div class="ns-lbl">明日预约</div></div></div>
+    <div class="ns-sec">患者队列</div>
+    <div id="nsQueue"><div style="padding:16px;text-align:center;color:var(--muted);font-size:10px">等待患者入院</div></div>
+    <div id="nsNotifs"></div>
+  </div></div>
+
+  <!-- CENTER: Unified Entry Client -->
+  <div class="panel" style="position:relative;overflow:hidden">
+    <div class="data-flow" id="dataFlow" style="top:0;left:0;right:0"></div>
+    <div class="center-body">
+      <div class="uc-topbar">
+        <span class="uc-logo"><svg width="12" height="12" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="20" rx="4" fill="none" stroke="currentColor" stroke-width="1.5"/><line x1="8" y1="2" x2="8" y2="22" stroke="currentColor" stroke-width="1.5"/></svg>空海医院</span>
+        <span class="uc-div"></span>
+        <span class="uc-id"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 4-7 8-7s8 3 8 7"/></svg><span id="ucPatient">患者:<strong>—</strong></span></span>
+        <span class="uc-scene">门诊</span>
+        <span class="uc-perc" id="ucPerc"><span class="ptag l0">L0</span><span class="ptag off">NFC 待触发</span><span class="ptag off">BLE 未连接</span></span>
+      </div>
+      <div class="uc-main">
+        <div class="uc-left">
+          <div class="uc-avatar"><svg viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="20" rx="4"/><line x1="12" y1="2" x2="12" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/></svg></div>
+          <div class="uc-assist-info"><div class="uc-assist-name">空海医院医疗助手</div><div class="uc-assist-tag" id="ucAssistTag">感知您的状态,主动推送服务</div></div>
+          <div class="uc-sep"></div>
+          <div class="uc-label">你可以这样说</div>
+          <div class="uc-ex">我头疼三天,想挂号</div>
+          <div class="uc-ex">找神经内科李明医生</div>
+          <div class="uc-sep"></div>
+          <div class="uc-label">高频功能</div>
+          <div class="uc-qbtn">📋 挂号预约</div>
+          <div class="uc-qbtn">🧭 科室导诊</div>
+          <div class="uc-qbtn">💳 门诊缴费</div>
+          <div class="uc-qbtn">📊 查报告</div>
+          <div class="uc-sep"></div>
+          <div class="uc-label">设备与服务</div>
+          <div class="uc-qbtn">📱 NFC 碰一碰</div>
+          <div class="uc-qbtn">🤖 呼叫机器人</div>
+          <div class="uc-bind" id="ucBind">
+            <div class="ub-title">绑定设备,开启感知</div>
+            <div class="ub-desc">NFC碰一碰或扫码绑定,系统自动感知身份和位置</div>
+            <button class="btn-sm btn-primary" style="width:100%">NFC 碰一碰绑定</button>
+          </div>
+        </div>
+        <div class="uc-center">
+          <div class="uc-chat-h"><span class="dot"></span>空海医院医疗助手 · 在线</div>
+          <div class="uc-msgs" id="ucMsgs">
+            <div class="uc-empty" id="ucEmpty">
+              <div class="uc-empty-icon"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="1.6"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></div>
+              <div class="uc-empty-text">您好,我是门诊助手</div>
+              <div class="uc-empty-hint" id="ucEmptyHint">请说出您的就诊需求。绑定设备后可自动识别身份。</div>
+              <div class="uc-empty-chips"><span class="uc-chip">我头疼想挂号</span><span class="uc-chip">找神经内科</span></div>
+            </div>
+            <div class="uc-chat-content" id="ucChat"></div>
+          </div>
+          <div class="uc-input-bar"><input class="ub-inp" placeholder="请说出您的就诊需求…" readonly><div class="ub-send">➤</div></div>
+        </div>
+        <div class="uc-right">
+          <div class="uc-label">设备绑定</div>
+          <div class="ur-dev"><div class="ud-icon" style="background:color-mix(in oklch,var(--accent-light) 12%,transparent)">⌚</div><div class="ud-info"><div class="ud-name">华为手表</div><div class="ud-sub">未检测到</div></div><span class="ud-tag unbound" id="udTag1">未绑定</span></div>
+          <div class="ur-dev"><div class="ud-icon" style="background:color-mix(in oklch,var(--accent) 10%,transparent)">📱</div><div class="ud-info"><div class="ud-name">Mate 60 Pro</div><div class="ud-sub">未检测到</div></div><span class="ud-tag unbound" id="udTag2">未绑定</span></div>
+          <div class="uc-sep"></div>
+          <div class="uc-label">就诊动线</div>
+          <div class="ur-journey" id="urJourney">
+            <div class="ur-jn current" data-jn="0">入院</div>
+            <div class="ur-jn" data-jn="1">身份绑定</div>
+            <div class="ur-jn" data-jn="2">任务加载</div>
+            <div class="ur-jn" data-jn="3">挂号/分诊</div>
+            <div class="ur-jn" data-jn="4">诊区签到</div>
+            <div class="ur-jn" data-jn="5">候诊→就诊</div>
+            <div class="ur-jn" data-jn="6">缴费→离院</div>
+          </div>
+          <div class="uc-sep"></div>
+          <div class="uc-label">下一步</div>
+          <div class="ur-next" id="urNext">
+            <div class="un-badge" id="unBadge">⟳ 待完成</div>
+            <div class="un-title" id="unTitle">绑定设备与身份</div>
+            <div class="un-desc" id="unDesc">请使用手机 NFC 碰一碰自助终端感应区完成绑定。</div>
+            <div class="un-acts" id="unActs"><button class="btn-sm btn-primary" style="flex:1;text-align:center">NFC 碰一碰</button><button class="btn-sm btn-secondary" style="flex:1;text-align:center">扫码绑定</button></div>
+          </div>
+          <div class="uc-sep"></div>
+          <div class="uc-label">感知状态</div>
+          <div class="ur-perc-grid" id="urPercGrid">
+            <div class="ur-perc"><span class="up-l">身份</span><span class="up-v on">已识别</span></div>
+            <div class="ur-perc"><span class="up-l">设备</span><span class="up-v off" id="percDev">未绑定</span></div>
+            <div class="ur-perc"><span class="up-l">位置</span><span class="up-v warn" id="percLoc">L0 手动</span></div>
+            <div class="ur-perc"><span class="up-l">业务</span><span class="up-v off" id="percBiz">无任务</span></div>
+          </div>
+          <div class="ur-hint"><span>💡</span><span id="urHint">设备绑定后进入 L1 感知模式。</span></div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- RIGHT: Doctor Station -->
+  <div class="panel"><div class="ph"><span class="pi" style="background:color-mix(in oklch,var(--accent-light) 15%,transparent)">🩺</span><span class="pt">医生工作站</span><span class="ps" id="drStatus">待诊中</span></div>
+  <div class="pb" id="drBody">
+    <div class="dr-info"><div class="dr-name">李明 <span style="font-size:10px;font-weight:400;color:var(--muted)">主任医师</span></div><div class="dr-dept">神经内科 · 门诊三楼 301 诊室</div><div class="dr-stats"><span id="drQCount">候诊 0 人</span><span id="drTodayCount">今日接诊 0 人</span></div></div>
+    <div class="ns-sec" id="drListLabel">今日待诊</div>
+    <div id="drList"><div style="padding:16px;text-align:center;color:var(--muted);font-size:10px">等待患者到诊</div></div>
+    <div id="drNotifs"></div>
+  </div></div>
+</div>
+
+<footer class="bottombar" id="bottomBar">
+  <span class="bbl">服务通知</span>
+  <div id="bottomNotifs"><span class="bbn"><span class="b-dot"></span>系统就绪 — 等待患者操作</span></div>
+</footer>
+
+<script>
+const $=id=>document.getElementById(id);
+function delay(ms){return new Promise(r=>setTimeout(r,ms))}
+
+// ─── helpers ───
+function addNotif(contId, txt){
+  const el=document.createElement('div');
+  el.className=contId==='nsNotifs'?'ns-notif':'dr-notif';
+  el.textContent=txt;
+  $(contId).appendChild(el);
+  setTimeout(()=>el.remove(),5000);
+}
+function setBottom(txt){$('bottomNotifs').innerHTML=`<span class="bbn"><span class="b-dot"></span>${txt}</span>`}
+
+function addMsg(role, txt){
+  return new Promise(r=>setTimeout(()=>{
+    const cc=$('ucChat'), d=document.createElement('div');
+    d.style.cssText='display:flex;gap:6px;align-items:flex-start;animation:fadeUp .3s ease;margin-bottom:2px';
+    d.innerHTML=`<span style="font-size:10px;font-weight:600;color:${role==='ai'?'var(--accent)':'var(--fg)'};flex-shrink:0;width:24px">${role==='ai'?'🤖':'👤'}</span><span style="font-size:10px;color:var(--fg);line-height:1.5">${txt}</span>`;
+    cc.appendChild(d);cc.scrollTop=cc.scrollHeight; r();
+  },0));
+}
+function typeMsg(role, txt, chDelay=25, stDelay=0){
+  return new Promise(r=>{setTimeout(()=>{
+    const cc=$('ucChat'), d=document.createElement('div');
+    d.style.cssText='display:flex;gap:6px;align-items:flex-start;animation:fadeUp .3s ease;margin-bottom:2px';
+    const sp=document.createElement('span');
+    sp.style.cssText=`font-size:10px;font-weight:600;color:${role==='ai'?'var(--accent)':'var(--fg)'};flex-shrink:0;width:24px`;
+    sp.textContent=role==='ai'?'🤖':'👤';
+    const ts=document.createElement('span');
+    ts.style.cssText='font-size:10px;color:var(--fg);line-height:1.5';
+    d.appendChild(sp);d.appendChild(ts);cc.appendChild(d);
+    let i=0;
+    function type(){if(i<txt.length){ts.innerHTML=txt.slice(0,i+1);cc.scrollTop=cc.scrollHeight;i++;setTimeout(type,chDelay)}else r()}
+    type();
+  },stDelay)});
+}
+
+function setJourney(n){
+  for(let i=0;i<=6;i++){const el=document.querySelector(`[data-jn="${i}"]`);el.classList.remove('current','done');
+    if(i<n)el.classList.add('done');else if(i===n)el.classList.add('current')}
+}
+function setPerc(tagId, txt, cls){const el=$(tagId);el.textContent=txt;el.className='up-v '+cls}
+
+// ─── ACTS ───
+async function act0(){
+  // reset everything
+  $('nsOnline').textContent='0';$('nsToday').textContent='0';$('nsTomorrow').textContent='0';
+  $('nsQueue').innerHTML='<div style="padding:16px;text-align:center;color:var(--muted);font-size:10px">等待患者入院</div>';
+  $('nsNotifs').innerHTML='';$('nsStatus').textContent='等待患者';
+  $('drList').innerHTML='<div style="padding:16px;text-align:center;color:var(--muted);font-size:10px">等待患者到诊</div>';
+  $('drNotifs').innerHTML='';$('drQCount').textContent='候诊 0 人';$('drTodayCount').textContent='今日接诊 0 人';
+  $('drListLabel').textContent='今日待诊';$('drStatus').textContent='待诊中';
+  $('ucPatient').innerHTML='患者:<strong>—</strong>';
+  $('ucPerc').innerHTML='<span class="ptag l0">L0</span><span class="ptag off">NFC 待触发</span><span class="ptag off">BLE 未连接</span>';
+  $('ucEmpty').style.display='flex';$('ucChat').style.display='none';$('ucChat').innerHTML='';
+  $('ucAssistTag').textContent='感知您的状态,主动推送服务';
+  $('udTag1').textContent='未绑定';$('udTag1').className='ud-tag unbound';
+  $('udTag2').textContent='未绑定';$('udTag2').className='ud-tag unbound';
+  setJourney(0);
+  $('unBadge').textContent='⟳ 待完成';$('unTitle').textContent='绑定设备与身份';
+  $('unDesc').textContent='请使用手机 NFC 碰一碰自助终端感应区完成绑定。';
+  $('unActs').innerHTML='<button class="btn-sm btn-primary" style="flex:1;text-align:center">NFC 碰一碰</button><button class="btn-sm btn-secondary" style="flex:1;text-align:center">扫码绑定</button>';
+  setPerc('percDev','未绑定','off');setPerc('percLoc','L0 手动','warn');setPerc('percBiz','无任务','off');
+  $('urHint').textContent='设备绑定后进入 L1 感知模式。';
+  $('ucBind').innerHTML='<div class="ub-title">绑定设备,开启感知</div><div class="ub-desc">NFC碰一碰或扫码绑定,系统自动感知身份和位置</div><button class="btn-sm btn-primary" style="width:100%">NFC 碰一碰绑定</button>';
+  document.querySelectorAll('.uc-qbtn').forEach(b=>b.classList.remove('on'));
+  setBottom('系统就绪 — 等待患者操作');
+  $('dataFlow').style.display='none';
+  $('appShell').style.display='grid';$('appShell').style.opacity='1';
+  $('tlBar').style.display='flex';$('bottomBar').style.display='flex';
+  const ending=document.querySelector('[data-ending]');if(ending)ending.remove();
+}
+
+async function act1(){
+  setJourney(0);
+  $('ucEmpty').style.display='flex';$('ucChat').style.display='none';$('ucChat').innerHTML='';
+
+  // NFC animation
+  const overlay=document.createElement('div');
+  overlay.id='nfcOverlay';overlay.style.cssText='position:absolute;top:47%;left:50%;transform:translate(-50%,-50%);z-index:10;pointer-events:none;text-align:center';
+  overlay.innerHTML=`<div style="position:relative;width:70px;height:70px;margin:0 auto"><div style="position:absolute;inset:0;border-radius:50%;border:2px solid var(--accent-light);animation:ripple 1.2s ease-out infinite"></div><div style="position:absolute;inset:0;border-radius:50%;border:2px solid var(--accent-light);animation:ripple 1.2s ease-out infinite;animation-delay:.4s"></div><div style="position:absolute;inset:0;border-radius:50%;border:2px solid var(--accent-light);animation:ripple 1.2s ease-out infinite;animation-delay:.8s"></div><div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:24px">📱</div></div><div style="font-size:9px;color:var(--accent-light);margin-top:4px;font-weight:500">碰一碰连接中…</div>`;
+  document.querySelector('.center-body').appendChild(overlay);
+  await delay(2500);
+  const nfo=document.getElementById('nfcOverlay');if(nfo)nfo.remove();
+
+  // Identity bound
+  $('ucPatient').innerHTML='患者:<strong>医小梦</strong>';
+  $('ucPerc').innerHTML='<span class="ptag l1">L1</span><span class="ptag l1">NFC 已连接</span><span class="ptag l1">BLE 在线</span>';
+  setPerc('percDev','已绑定','on');setPerc('percLoc','L1 门诊大厅','on');
+  $('udTag1').textContent='已绑定';$('udTag1').className='ud-tag bound';
+  $('udTag2').textContent='已绑定';$('udTag2').className='ud-tag bound';
+  $('ucBind').innerHTML='<div style="font-size:9px;color:var(--accent);font-weight:500">✅ 已绑定 · Mate 60 Pro</div>';
+  $('ucAssistTag').textContent='医小梦您好!欢迎来到空海医院';
+  document.querySelectorAll('.uc-qbtn').forEach(b=>b.classList.add('on'));
+  setJourney(2);
+
+  // Nurse
+  $('nsOnline').textContent='1';$('nsToday').textContent='1';
+  $('nsQueue').innerHTML='<div class="ns-patient highlight" style="animation:fadeUp .4s"><div class="np-row"><span class="np-name">医小梦</span><span class="np-tag ok">已入院</span></div><div class="np-detail"><span>📍 门诊大厅</span><span>📱 设备已绑定</span></div></div>';
+  $('nsStatus').textContent='1 名患者';
+  addNotif('nsNotifs','📋 设备绑定事件 · 医小梦');
+
+  // Doctor
+  $('drList').innerHTML='<div class="dr-patient" style="animation:fadeUp .4s"><div class="dp-row"><span class="dp-name">医小梦</span><span class="dp-time">— 未挂号</span></div><div style="font-size:7px;color:var(--muted);display:flex;gap:4px;margin-top:1px"><span>📍 门诊大厅</span><span>还未挂号</span></div></div>';
+  $('drQCount').textContent='候诊 1 人';$('drStatus').textContent='有患者';
+  addNotif('drNotifs','📋 新患者入院 · 医小梦');
+
+  // Chat greeting
+  $('ucEmpty').style.display='none';$('ucChat').style.display='flex';
+  await typeMsg('ai','医小梦您好!欢迎来到空海医院。已为您自动加载信息。检测到您还没有挂号,需要帮您看看挂哪个科室吗?',22,300);
+  await delay(500);
+  const btnRow=document.createElement('div');
+  btnRow.style.cssText='display:flex;gap:5px;margin-top:2px;animation:fadeUp .4s';
+  btnRow.innerHTML='<button class="btn-sm btn-primary" style="flex:1;text-align:center">我要挂号</button><button class="btn-sm btn-secondary" style="flex:1;text-align:center">科室导诊</button>';
+  $('ucChat').appendChild(btnRow);
+
+  $('unBadge').textContent='⬆ 下一步';
+  $('unTitle').textContent='选择服务';
+  $('unDesc').textContent='是否要挂号或查询科室信息?';
+  $('unActs').innerHTML='<button class="btn-sm btn-primary" style="flex:1;text-align:center">我要挂号</button>';
+  $('urHint').textContent='已进入 L1 感知模式,系统可自动识别身份和位置。';
+  setBottom('📱 设备绑定成功 · 感知 L1 · 门诊大厅');
+
+  $('dataFlow').style.display='block';await delay(1500);$('dataFlow').style.display='none';
+}
+
+async function act2(){
+  $('ucChat').style.display='flex';
+  setJourney(3);setPerc('percBiz','AI 导诊中','warn');
+
+  await addMsg('user','我头疼三天,还有点恶心');
+  await delay(1000);
+  await typeMsg('ai','好的,我来了解一下。请问有没有伴随发热、视物模糊呢?',25,300);
+  await delay(2000);
+  await addMsg('user','没有发热,就是头疼恶心想吐');
+  await delay(1200);
+  await typeMsg('ai','明白了。根据您的症状描述,建议优先选择神经内科。李明主任医师今天上午有号,需要帮您预约吗?',22,500);
+  await delay(800);
+
+  const rec=document.createElement('div');
+  rec.style.cssText='background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-md);padding:10px 12px;animation:slideUp .4s;max-width:260px';
+  rec.innerHTML='<span style="display:inline-block;font-size:7px;padding:1px 5px;border-radius:var(--radius-pill);background:var(--accent-light);color:#fff;font-weight:600;margin-bottom:4px">🏆 推荐</span><div style="font-size:12px;font-weight:700">神经内科 · 李明 主任医师</div><div style="font-size:9px;color:var(--muted);margin:2px 0">今天 10:30 · ¥25(模拟)</div><div style="display:flex;gap:5px;margin-top:5px"><button class="btn-sm btn-primary" style="flex:1;text-align:center">确认挂号</button><button class="btn-sm btn-secondary" style="flex:1;text-align:center">换医生看看</button></div>';
+  $('ucChat').appendChild(rec);
+
+  // Nurse
+  $('nsQueue').innerHTML='<div class="ns-patient highlight" style="animation:fadeUp .4s"><div class="np-row"><span class="np-name">医小梦</span><span class="np-tag ok">导诊完成</span></div><div class="np-detail"><span>🏥 推荐:神经内科</span><span>👨‍⚕️ 李明</span></div></div>';
+  addNotif('nsNotifs','📋 导诊完成 · 医小梦 → 神经内科');
+
+  // Doctor
+  $('drList').innerHTML='<div class="dr-patient highlight" style="animation:fadeUp .4s"><div class="dp-row"><span class="dp-name">医小梦</span><span class="dp-tag pending">推荐中</span><span class="dp-time">10:30</span></div><div style="font-size:7px;color:var(--muted);margin-top:1px"><span>🏥 神经内科 · 李明</span></div></div>';
+  addNotif('drNotifs','📋 导诊完成 · 医小梦 → 神经内科');
+
+  setBottom('📋 推荐科室:神经内科 · 李明 主任医师');
+  await delay(2500);
+
+  // Loading
+  const ld=document.createElement('div');
+  ld.id='loading';ld.style.cssText='text-align:center;padding:6px;animation:fadeIn .3s';
+  ld.innerHTML='<span style="font-size:9px;color:var(--muted)">⏳ 正在锁定号源…</span>';
+  $('ucChat').appendChild(ld);
+  await delay(1500);
+  const ldEl=document.getElementById('loading');if(ldEl)ldEl.remove();
+
+  // Success
+  const suc=document.createElement('div');
+  suc.style.cssText='background:linear-gradient(135deg,color-mix(in oklch,var(--accent-light) 10%,var(--surface)),var(--surface));border:1px solid var(--accent-light);border-radius:var(--radius-md);padding:12px;animation:slideUp .4s;max-width:260px';
+  suc.innerHTML='<div style="font-size:28px;text-align:center">🎉</div><div style="font-size:14px;font-weight:700;text-align:center">挂号成功!</div><div style="font-size:10px;color:var(--muted);text-align:center;line-height:1.6;margin:4px 0">神经内科 · 李明 主任医师<br>今天 10:30 · 门诊三楼 301 诊室<br>预约号:<strong>A023</strong><br>请前往诊区,到达后自动引导签到</div><div style="text-align:center;margin-top:4px"><button class="btn-sm btn-primary">查看我的就诊动线</button></div>';
+  $('ucChat').appendChild(suc);
+
+  setJourney(4);
+  setPerc('percBiz','已挂号 · A023','on');
+
+  // Nurse
+  $('nsTomorrow').textContent='1';
+  $('nsQueue').innerHTML='<div class="ns-patient" style="border-color:var(--accent-light);animation:fadeUp .4s"><div class="np-row"><span class="np-name">医小梦</span><span class="np-tag done">✅ 已挂号</span></div><div class="np-detail"><span>🏥 神经内科</span><span>👨‍⚕️ 李明</span><span>⏰ 10:30</span><span>#A023</span></div></div>';
+  addNotif('nsNotifs','✅ 医小梦 已挂号 · 10:30 神经内科');
+
+  // Doctor
+  $('drList').innerHTML='<div class="dr-patient highlight" style="animation:fadeUp .4s"><div class="dp-row"><span class="dp-name">医小梦</span><span class="dp-tag ok">已挂号</span><span class="dp-time">10:30</span></div><div style="font-size:7px;color:var(--muted);margin-top:1px"><span>预约号 A023</span></div></div>';
+  $('drQCount').textContent='候诊 1 人';$('drTodayCount').textContent='今日接诊 1 人';$('drStatus').textContent='待诊中';
+  addNotif('drNotifs','📋 +1 预约 · 医小梦 10:30');
+
+  $('unBadge').textContent='⬆ 下一步';
+  $('unTitle').textContent='前往诊区签到';
+  $('unDesc').textContent='请前往三楼神经内科301诊室。到达后系统自动感知并推送签到。';
+  $('unActs').innerHTML='';
+  setBottom('📋 预约号 A023 · 10:30 · 神经内科 · 请前往诊区签到');
+
+  $('dataFlow').style.display='block';await delay(1500);$('dataFlow').style.display='none';
+}
+
+async function act3(){
+  // Location update
+  $('ucPerc').innerHTML='<span class="ptag l1">L1</span><span class="ptag l1">神经内科诊区</span><span class="ptag l1">BLE 在线</span>';
+  setPerc('percLoc','L1 神经内科诊区','on');
+  setBottom('📍 位置感知:已进入神经内科诊区');
+  await delay(1500);
+
+  // Active check-in card
+  $('ucChat').innerHTML='';
+  await typeMsg('ai','检测到您已到达神经内科诊区——',22,0);
+  await delay(500);
+
+  const chk=document.createElement('div');
+  chk.style.cssText='background:var(--surface);border:2px solid var(--accent);border-radius:var(--radius-md);padding:12px;animation:slideUp .4s;max-width:260px';
+  chk.innerHTML='<div style="font-size:13px;font-weight:700;margin-bottom:6px">📍 主动签到</div><div style="display:flex;justify-content:space-between;font-size:10px;padding:3px 0"><span style="color:var(--muted)">预约号</span><span><strong>A023</strong></span></div><div style="display:flex;justify-content:space-between;font-size:10px;padding:3px 0"><span style="color:var(--muted)">当前叫号</span><span>第 018 号</span></div><div style="font-size:9px;color:var(--accent);font-weight:500;margin:6px 0">⚡ 无需找签到机,点击即可签到</div><button class="btn-sm btn-primary" style="width:100%;text-align:center;margin-top:4px">一键签到</button>';
+  $('ucChat').appendChild(chk);
+
+  $('unBadge').innerHTML='⚡ 服务找人';
+  $('unBadge').style.color='var(--accent)';
+  $('unTitle').textContent='系统主动推送签到';
+  $('unDesc').textContent='感知到位于神经内科诊区,无需寻找签到机——系统正在主动为您服务。';
+  setBottom('📍 检测到诊区到达 · 推送主动签到');
+
+  // Nurse station location update
+  $('nsQueue').querySelector('.np-detail span:first-child').textContent='📍 神经内科诊区';
+
+  await delay(2500);
+
+  // Check-in success
+  $('ucChat').innerHTML='';
+  await typeMsg('ai','检测到您已到达神经内科诊区——',22,0);
+  await delay(200);
+  const suc=document.createElement('div');
+  suc.style.cssText='background:linear-gradient(135deg,color-mix(in oklch,var(--accent-light) 10%,var(--surface)),var(--surface));border:1px solid var(--accent-light);border-radius:var(--radius-md);padding:12px;animation:slideUp .4s;max-width:260px';
+  suc.innerHTML='<div style="font-size:24px;text-align:center">✅</div><div style="font-size:14px;font-weight:700;text-align:center;color:var(--accent-light)">签到成功!</div><div style="font-size:10px;color:var(--muted);text-align:center;line-height:1.6;margin:4px 0">预约号 A023 · 当前叫号 019<br>前面还有 <strong>4 位</strong>患者<br>预计等候约 15 分钟</div>';
+  $('ucChat').appendChild(suc);
+
+  setJourney(5);
+  setPerc('percBiz','候诊中 · 第23位','on');
+
+  $('unBadge').textContent='⏳ 候诊中';$('unBadge').style.color='var(--accent)';
+  $('unTitle').textContent='当前叫号 019 · 第 23 位';
+  $('unDesc').textContent='预计等候 15 分钟。请留意叫号屏和手机通知。';
+  $('unActs').innerHTML='';
+  setBottom('✅ 签到成功 · 候诊中 · 当前叫号 019 · 第 23 位');
+
+  // Nurse
+  $('nsQueue').innerHTML='<div class="ns-patient" style="border-color:var(--accent-light);animation:fadeUp .4s"><div class="np-row"><span class="np-name">医小梦</span><span class="np-tag ok">候诊中</span></div><div class="np-detail"><span>📍 神经内科诊区</span><span>🔢 第 23 位</span><span>⏳ 约 15 分钟</span></div></div>';
+
+  // Doctor
+  $('drList').innerHTML='<div class="dr-patient" style="animation:fadeUp .4s"><div class="dp-row"><span class="dp-name">019 · 赵丽华</span><span style="font-size:7px;color:var(--muted)">候诊中</span></div></div><div class="dr-patient highlight" style="animation:fadeUp .4s"><div class="dp-row"><span class="dp-name">A023 · 医小梦</span><span class="dp-tag pending">候诊中</span><span class="dp-time">第23位</span></div></div>';
+  $('drQCount').textContent='候诊 5 人';$('drStatus').textContent='候诊中';
+}
+
+async function act4(){
+  await delay(500);
+  $('appShell').style.transition='all .8s ease';$('appShell').style.opacity='0';
+  await delay(800);$('appShell').style.display='none';$('tlBar').style.display='none';$('bottomBar').style.display='none';
+
+  const ending=document.createElement('div');
+  ending.setAttribute('data-ending','');
+  ending.style.cssText='position:fixed;inset:0;background:var(--accent);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:100;animation:fadeIn .6s';
+  ending.innerHTML='<div style="text-align:center;animation:slideUp .6s"><div style="font-size:26px;font-weight:700;color:var(--accent-light);letter-spacing:.02em;margin-bottom:8px">感知 <span style="color:rgba(255,255,255,.35)">→</span> 动线 <span style="color:rgba(255,255,255,.35)">→</span> AI 调度 <span style="color:rgba(255,255,255,.35)">→</span> 服务外显</div><div style="font-size:15px;color:#fff;margin-bottom:4px;opacity:.8">患者少找路、少找窗口、少找设备</div><div style="width:100px;height:2px;background:var(--accent-light);margin:14px auto;border-radius:1px"></div><div style="font-size:18px;color:#fff;font-weight:600;letter-spacing:.04em">EMOON · 医梦</div><div style="font-size:11px;color:rgba(255,255,255,.45);margin-top:4px;letter-spacing:.05em">AI 未来医院 · 服务主动找人</div></div>';
+  document.body.appendChild(ending);
+}
+
+// ─── AUTO-ADVANCE ───
+const ACTS=[
+  {name:'第一幕 · 开场亮相',  start:0,  dur:5000,  fn:act0},
+  {name:'第二幕 · 感知绑定',  start:5,  dur:9000,  fn:act1},
+  {name:'第三幕 · AI 导诊挂号',start:14, dur:15000, fn:act2},
+  {name:'第四幕 · 诊区签到',  start:29, dur:10000, fn:act3},
+  {name:'第五幕 · 结尾定版',  start:39, dur:7000,  fn:act4},
+];
+let cur=-1, timer=null;
+
+function fmt(s){return Math.floor(s/60)+':'+(s%60).toString().padStart(2,'0')}
+
+async function go(idx){
+  clearTimeout(timer);cur=idx;
+  document.querySelectorAll('.tl-step').forEach((e,i)=>{e.classList.toggle('active',i===idx);e.classList.toggle('done',i<idx)});
+  $('tlAct').textContent=ACTS[idx].name;$('tlTime').textContent=fmt(ACTS[idx].start);
+  query('.scene-label',true).forEach(e=>e.remove());
+  await ACTS[idx].fn();
+  if(idx+1<ACTS.length) timer=setTimeout(()=>go(idx+1),ACTS[idx].dur);
+}
+function query(s,all){return all?document.querySelectorAll(s):document.querySelector(s)}
+
+document.addEventListener('DOMContentLoaded',()=>go(0));
+</script>
+</body>
+</html>

+ 15 - 22
reasonix.toml

@@ -2,13 +2,10 @@
 # Resolution order: flag > ./reasonix.toml > ~/.config/reasonix/config.toml > built-in defaults.
 # Secrets come from the environment via api_key_env; never put keys here.
 
+config_version = 2   # schema marker for diagnostics; old versions may ignore it
 default_model = "deepseek-flash"
 language      = "zh"   # ui/model language; empty = auto-detect from $LANG / $REASONIX_LANG
 
-[ui]
-theme = "auto"   # auto|dark|light; CLI colors only; REASONIX_THEME can override per run
-# theme_style = "graphite"   # graphite|ember|aurora|midnight|sandstone|porcelain|linen|glacier
-
 [network]
 proxy_mode = "auto"   # auto|env|custom|off; auto currently uses env proxy
 # proxy_url  = "socks5://127.0.0.1:7890"   # optional custom override
@@ -22,26 +19,15 @@ type = "socks5"   # http|https|socks5|socks5h
 # password = "${REASONIX_PROXY_PASSWORD}"   # optional; supports ${VAR} expansion
 
 [agent]
-system_prompt = """
-You are Reasonix, a coding agent focused on executing code tasks.
-Use the provided tools to read and write files and run shell commands.
-Principles: understand the request before acting; verify with tools instead of
-guessing; keep changes minimal and correct; briefly summarize what you did.
-When the request leaves a real choice to the user — which approach or library,
-the scope, or a consequential or ambiguous decision — call the ask tool to offer
-2-4 concrete options rather than guessing or burying the question in prose. Skip
-it when there's an obvious default; don't ask just to confirm.
-For multi-step work, track progress with the todo_write tool: lay out the steps,
-keep exactly one in_progress, and flip each to completed as you finish it — update
-the list as you go, not just at the end.
-In plan mode the harness blocks writer tools: do read-only research, then write a
-concise plan as your reply and stop. The user is asked to approve before anything
-is changed; once approved, work through the steps, updating the task list as you go."""
+# system_prompt = """..."""   # omit to use the built-in prompt for this version
 # system_prompt_file = "prompts/system.md"   # overrides system_prompt when set
 max_steps   = 0
 temperature = 0.0
-auto_plan   = "ask"   # off|ask|on; ask/on auto-enter plan mode for complex tasks
+auto_plan   = "on"   # off|on; off keeps plan mode manual
 # auto_plan_classifier = "deepseek-flash"   # optional; only used for borderline tasks
+soft_compact_ratio  = 0.5   # notice only; keeps cache-first prefix intact
+compact_ratio       = 0.8   # try compacting when prompt reaches this fraction
+compact_force_ratio = 0.9   # force compacting at this high-water mark
 # planner_model = "mimo"   # optional: enable two-model collaboration
 # subagent_model = "deepseek-pro"   # optional default for runAs=subagent skills
 # subagent_models = { review = "deepseek-pro", security_review = "deepseek-pro" }   # per-skill overrides
@@ -61,8 +47,15 @@ price       = { cache_hit = 0.02, input = 1, output = 2, currency = "¥" }   # p
 [tools]
 enabled = []   # empty = all built-in tools
 
+[codegraph]
+enabled      = true   # built-in MCP server; off by default for first-run sessions
+auto_install = true   # fetch the runtime when CodeGraph is enabled but missing
+# path       = ""   # empty = cache, then PATH, then a bundle beside reasonix
+# tier       = "lazy"   # lazy|background|eager
+
 [skills]
 # paths = ["~/my-skills", "../shared/skills"]   # extra custom skill roots
+# disabled_skills = ["review"]   # hide noisy or unwanted skills
 
 [permissions]
 # Per-call gating. mode = writer fallback when no rule matches: ask|allow|deny.
@@ -70,7 +63,7 @@ enabled = []   # empty = all built-in tools
 # Rules are "ToolName" or "ToolName(glob)"; '*' matches any run, '?' one char.
 mode  = "ask"
 # deny = ["bash(rm -rf*)", "bash(git push*)"]   # hard-blocked in every mode
-# allow = ["bash(go test*)", "bash(git status*)"]   # never prompted
+allow = ["bash=cd /Users/destiny/dev/emoon/emoon-backend && git pull", "bash=cd /Users/destiny/dev/emoon/emoon-backend && git log --all --since=\"7 days ago\" --format=\"%h %s <%an> %ai\" --no-merges", "bash=cd /Users/destiny/dev/emoon/emoon-backend && for hash in e54eb183 078d19c0 bb5b01d0 56a745d5 cf8e91bf; do echo \"========================================\"; echo \"Commit: $hash\"; echo \"========================================\"; git log -1 --format=\"%h %s <%an> %ai%n%b\" $hash; echo; git diff-tree --no-commit-id -r --name-status $hash; echo; done", "bash=cd /Users/destiny/dev/emoon/emoon-backend && git log --all --since=\"7 days ago\" --format=\"%an\" --no-merges | sort -u"]
 # ask = ["write_file"]   # force a prompt even if otherwise allowed
 
 [sandbox]
@@ -85,7 +78,7 @@ network = true
 
 [statusline]
 # A custom status line: a command whose first stdout line replaces the built-in
-# data row. It receives {"model","contextUsed","contextWindow"} as JSON on stdin.
+# data row. It receives {"model","contextUsed","contextWindow","cwd"} as JSON on stdin.
 # command = "my-statusline.sh"
 
 # External MCP servers. type: "stdio" (default, a subprocess) | "http" | "sse".