Date: 2026-08-31
Basis: current code (not the original Phase 1 wish-list)
Compared against:phase-1.md· Master Plan · Architecture Audit · Client Plan V1.1 · Server Plan V1.1
Repositories:xiaozhi-esp32-2.2.6(Client) ·xiaozhi-esp32-server(Server)
本文件只记录事实、差异、债务与对 Master Plan 的修正建议。不包含下一阶段的详细编码任务(见 phase-2-plan.md)。
xiaozhi-esp32-2.2.6)| 文件 | 角色 |
|---|---|
main/boards/sening-xiaozhi-s3/app_task.h |
新增。 设备侧 Task 启动 / pending / 离线口令 / 复读记忆 |
main/boards/sening-xiaozhi-s3/app_task.cc |
新增。 Board_StartAppTask、优先级、NVS pending、last_rep、技能分发 |
main/boards/sening-xiaozhi-s3/mqtt.c |
START_APP_TASK;SET_ALARM 可附带 app_id/task_id/params 写入 pending |
main/boards/sening-xiaozhi-s3/sening_xiaozhi_s3.cc |
MCP self.mp3.play 增加 repeat / power_off_after / 空 name 回退;开机 Board_ResolveBootOrRestore |
main/boards/sening-xiaozhi-s3/mp3_player.cc |
整首循环 N 次;自然结束才 power_off;repeat_left |
main/boards/sening-xiaozhi-s3/system.h / system.c |
离线口令宏;SDCard_Play_MP3_With_Speed_Repeat |
main/audio/wake_words/afe_wake_word.cc |
MultiNet 注册 4 条离线口令 |
main/audio/wake_words/custom_wake_word.cc |
同上 |
main/application.cc |
SendListenDetect:没网且通道未开时不 WakeWordInvoke |
未改:Opus 热路径、DeviceState 迁移表、hello/listen/tts/stt 字段含义。非 Sening 板无 App Task 执行器。
xiaozhi-esp32-server,仅 Python xiaozhi-server)| 文件 | 角色 |
|---|---|
core/app_runtime/__init__.py |
新增模块。 |
core/app_runtime/registry.py |
加载 apps.json(bundled;可选 data/apps.json) |
core/app_runtime/apps.json |
App / Task 注册表 |
core/app_runtime/runtime.py |
start_app_task / switch_app / mode 分流 / 口令 / 跟读轮次 / 翻译 |
plugins_func/functions/switch_app.py |
LLM 可调的切 App |
plugins_func/functions/start_repeater.py |
打开复读机 |
plugins_func/functions/start_shadowing.py |
打开跟读 |
plugins_func/functions/translate.py |
连接 LLM 翻译(智控台 Key) |
mcp_servers/translate/server.py |
独立 translate MCP(回退) |
mcp_server_settings.json |
注册 english-translate |
core/connection.py |
连接内存:active_app_id / interaction_mode / tool_policy 等 |
core/handle/receiveAudioHandle.py |
startToChat 前按 mode 分流;口令优先于 LLM |
core/handle/textHandler/listenMessageHandler.py |
[app_task] / [voice_entry] |
core/providers/tools/unified_tool_manager.py |
filter_tools 白名单 |
config.yaml |
加载 switch_app / start_repeater / start_shadowing |
无 App Runtime 相关改动。 无新表、无新 REST。智控台仍只管理人设 / 插件绑定。
(历史对话里 manager-web 的 vue.config.js / teleport 修复与 Phase 1 业务无关,不计入本阶段架构。)
| 模块 | 位置 | 职责 |
|---|---|---|
| App Runtime | Server core/app_runtime/ |
注册表 + 统一 start_app_task + mode 行为 |
| Device Task Gateway | Client app_task.cc |
三通道汇合、抢占、pending、本地 Skill 执行 |
| Translate plugin + MCP | Server plugins + mcp_servers/translate |
翻译旁路 chat() |
| Repeater / Shadowing plugins | Server plugins | 进入对应 Task |
没有:Task Runtime 状态机类、Step 引擎、Assessment 服务、Scheduler 服务、App Store。
| 类型 | 名称 | 说明 |
|---|---|---|
| REST | 无 | manager-api 未增接口 |
| Python 插件 | switch_app |
app_id + 可选 task_id |
| Python 插件 | start_repeater |
可选 name/repeat/power_off_after |
| Python 插件 | start_shadowing |
无参数 |
| Python 插件 | translate |
text + 可选语言 |
| Device MCP | self.mp3.play 扩展 |
repeat、power_off_after;name 可空 |
| Device MCP | self.mp3.get_status |
已有;增加 repeat_left |
没有新的 Xiaozhi WS type。热路径语义冻结得到遵守。实际采用的是「复用 listen detect 文本」和「色宁 MQTT 新 action」。
| 通道 | 载荷 | 用途 |
|---|---|---|
WS listen state=detect |
[app_task]{app}\|{task}\|{source} |
设备通知 Server 进入某 Task(execute_skills=false,避免复读双播) |
WS listen state=detect |
[voice_entry] |
离线「切换模式」且**已联网**时要菜单 |
MQTT snlp/v1/.../cmd |
action=START_APP_TASK |
通道①即时启动 |
MQTT SET_ALARM |
可选 app_id/task_id/params |
写入设备 pending,通道② |
未增加:type=task、mode.set、play.completed 独立消息、speech.stopped。
无。 App 状态在 ConnectionHandler 内存。设备 pending / last_rep 在 NVS。WS 断线后 Server 回到 default chat;设备 pending 仍在本地。
Phase 1 之前(Audit):
Application 单例 + DeviceState + Opus 双工 + MCP
Sening 旁路:self.mp3.* / snlp MQTT PLAY_MEDIA / RTC 闹钟
离线「切换模式」只打日志
Phase 1 之后:
Application(Chat 热路径不变)
└── Board_StartAppTask(Sening 板级 Task Gateway)
├── source 优先级 mqtt_immediate > schedule > voice > default_chat
├── NVS pending + last_rep
├── 技能查找表 k_task_skills[](固件内硬编码 id,不是业务 if 树)
└── 本地执行 self.mp3.play / 或只 notify Server
MultiNet:切换模式 / 复读机 / 开始复读 / 退出复读
没网:切换模式不连云;复读机可播上次音频或 SD 第一首
InteractionMode 没有成为固件枚举。设备不持久化 App 历史。DeviceState 仍只管听/说/升级。
Phase 1 之前:
ConnectionHandler.chat() 是唯一对话编排
Agent + Plugin + MCP
无 App / 无 InteractionMode
Phase 1 之后:
receiveAudioHandle.startToChat
→ 口令 match_voice_switch(先于 LLM)
→ mode 分流:
PLAY_AUDIO 忽略上行
SHADOWING handle_shadowing_turn(不 chat)
TRANSLATION run_translation(不 chat)
BILINGUAL wrap 后仍 chat()
CHAT 现网 intent/LLM
App Runtime:apps.json + start_app_task
工具:inherit_agent | allowlist
权威状态仍在 当前 WS 连接内存,不是 TaskInstance 表。
① 小程序 / MQTT 调试客户端
START_APP_TASK
↓ snlp MQTT
Board_StartAppTask
├─ 本地 Skill(复读:SD MP3)
└─ 若 Xiaozhi 通道已开:listen detect [app_task]
↓
Server start_app_task(execute_skills=false)
更新 active_app_id / interaction_mode,TTS 播报
② SET_ALARM + pending
到点 RTC 唤醒 → Board_ResolveBootOrRestore
非 chat 计划 → 直接 StartAppTask(schedule)
否则 → chat.conversation
③ 离线语音
MultiNet 复读机 → 本地播放,不依赖 Server
MultiNet 切换模式 → 仅 IsNetworkReady 时 [voice_entry]
在线 ASR 短句 → match_voice_switch → start_app_task
④ Chat 热路径(未离开 CHAT)
Mic → Opus → ASR → LLM → TTS → Speaker
与 Phase 1 前相同
| 能力 | 状态 |
|---|---|
统一入口 start_app_task |
有(Server + 设备各一份) |
| App / Task 注册表 | 有(Server JSON;固件另有技能表) |
| InteractionMode 分流 | 有(连接字符串,5 种 mode) |
| Step / StepInstance | 无 |
| TaskInstance 持久化 | 无 |
| Event(play.completed 等) | 无正式协议;播放结束设备会 [app_task]chat 通知回聊 |
| 跟读 | 有 Demo:TTS 范读 + ASR 轮次 + 回声抑制;无归档、无评分 |
| Assessment / Metric | 无 |
结论:这是 薄 App Runtime + 单 Task 动作,不是 Master Plan 的 Task Runtime。
| 项 | 现状 |
|---|---|
phase-1.md §10 验收清单 |
文档存在,**仓库内无对应自动化用例** |
| xiaozhi-server pytest / unittest(App Runtime) | 无 |
| 固件单元测试(app_task / MultiNet) | 无 |
| 现有测试资产 | 仅 ASR/TTS/LLM performance_tester |
| 实际验证方式 | 人工烧录 + 重启 Server + MQTT/口令联调 |
Phase 1 在工程上「做完了功能」,没有「测完了回归」。 这是进入 Phase 2 前最大的过程风险,不是功能缺口。
| 项目 | 原计划(phase-1.md) |
实际实现 | 差异原因 | 后续处理 |
|---|---|---|---|---|
| 薄 App 框架 | Server JSON + active_app_id;设备无 App 目录 |
已实现;注册表在 core/app_runtime/apps.json |
— | 保持;不要下发到 SD |
Default chat.conversation |
配网/开机进入并主动开口 | pending 默认 chat;agent_proactive + 进入 CHAT 时 TTS 招呼 |
冷场再催促未做成可配循环 | Phase 2 不优先;可配次数留债 |
| 三通道 + 优先级 | MQTT > Schedule > Voice | 设备 Board_AppTask_SourcePriority;Server SOURCE_PRIORITY |
两端各算一次,可能短暂不一致 | Phase 2 用 skill_event 对齐结束态即可,不合并 MQTT |
START_APP_TASK |
新 MQTT action | 已实现 | — | 保持 |
| pending + 闹钟唤醒 | NVS;非 chat 计划不先闲聊 | Board_ResolveBootOrRestore + SET_ALARM 写 pending |
Server 无 Schedule 权威表 | 接受;权威表放到 Scheduler Phase |
| 离线「切换模式」 | 不再空 log;进菜单 | 有网才 [voice_entry];没网跳过 |
实施中发现 WakeWordInvoke 会干等 Server | 正确修正,写入架构 V2 |
| 复读 N 次在设备 | repeat 参数 |
mp3_player 循环 | — | 保持 |
power_off_after |
自然结束才软关机 | 已实现 | — | 保持 |
| 翻译旁路 LLM chat | 独立 MCP | 主路径改为 conn.llm + translate 插件;MCP 仅回退 |
独立 MCP 依赖占位 Key,智控台 Key 才是现网 | 正确修正;MCP 可降级为可选 |
| 工具白名单 | allowlist | filter_tools |
— | 保持 |
Xiaozhi type=task |
明确不做 | 未做 | — | Phase 2 仍不做完整 type=task |
| Java 新表 | 明确不做 | 未做 | — | Phase 3 再做 Instance 表 |
| 三个 App | chat / 复读 / 翻译 | 四个 App + 跟读 Task:另增 english_conversation、shadowing |
产品要把跟读/中英对话尽早可演示 | 跟读仍是 Demo;正式采集是 Phase 2 |
| 固件无业务名 | 禁止 if (app == english) |
固件 k_task_skills[] 含 app_id 字符串 |
MQTT 离线启动必须能查 Skill,无法只问 Server | 接受为「配置表」;禁止再加英语评分/音频逻辑 |
| 口令表在 Assets JSON | 映射表可扩 | 固件硬编码 4 条拼音;在线口令在 runtime.py |
MultiNet 容量与误触发 | 正确收窄;不要把翻译/跟读塞进 MultiNet |
| apps.json 路径 | 建议 data/apps.json |
bundled core/app_runtime/apps.json,可选 data 覆盖 |
部署更简单 | 保持 |
| PLAY_AUDIO 忽略 ASR | 是 | 是 | — | 保持 |
| 用户唤醒复位 chat | 是;闹钟不复位 | on_user_wakeup;[app_task] 不走唤醒 |
— | 保持 |
| 无 play.completed | 接受 | 播完设备直接切回 chat 并 detect | Server 仍不知道第几遍 | Phase 2 补 skill_event |
| 小程序 UI | 后置 | 未做 | — | Parent UI 很后 |
| 测试清单 | 详细 checkbox | 无自动化、无书面跑测记录 | 赶功能 | Phase 2 必须带 pytest + 手工回归表 |
SendListenDetect 在无网且通道未开时直接 return。last_rep + SD 第一首;注册表仍只在 Server。SHADOWING、BILINGUAL 提前落地(Client Plan 把 TRANSLATION/GAME 放在 Phase 3)。[app_task] / [voice_entry] 寄生在 listen/detect,而不是新 type。这些修改应视为 Phase 1 的真实架构,后续 Phase 不得假装「设备仍只有 CHAT+PLAY_AUDIO」。
play.completed / 遍数上报app_id 的系统化错误码(MQTT 有 ack;语音多为 TTS 句子)self.mp3.play 立即 accepted,Server 无法编排 N 次循环 → 循环必须在设备(Plan 已预见,代码证实)。conn.llm。{content,language,emotion} → 必须拆 content,否则翻译/口令失败。CHAT/BILINGUAL;跟读再加文本相似度过滤。WakeWordInvoke 会挂起 → 必须短路。execute_skills=true 会 播两遍 → [app_task] 必须 execute_skills=false。见第 5 节分级。核心:双技能表、寄生协议、无测试、跟读不是音频任务、App 状态不落库。
InteractionMode / App 命名冲突(已规定:对用户仍说「模式」,对代码仍用 App/Task)。english_learning 才能离线执行,与「业务名不上固件」理想不完全一致。usedExports)与 App 无关,但说明控制台栈也有债,不要在 Phase 2 顺手大改。| 抽象 | 当前是否存在 | 实现位置 | 完成程度 | 是否需要调整 | 未来是否扩展 |
|---|---|---|---|---|---|
| App | 是(薄) | Server apps.json + active_app_id;设备技能表有 app_id |
连接级容器,无安装/订阅/资源包 | 保持「Server 权威、不下发 SD」 | Phase 6 再做资源与清单 |
| Task | 是(薄) | apps.json tasks + start_app_task |
一个 Task ≈ 一种动作,无 Step 图 | 不要把跟读 Demo 当成完整 Task Definition | Phase 3 起 JSON Step |
| Step | 否 | — | 0% | Phase 2 不要先上完整 Step 引擎 | Phase 3+ 跟读一步一 Step |
| Skill | 是(MCP + 板级) | 设备 MCP;Server 调 DeviceMCP;固件 k_task_skills |
能跑 play/stop/power_off/translate | 双表必须当契约维护 | 核心 play_audio 平台化后移 |
| InteractionMode | 是(Server 字符串) | conn.interaction_mode |
CHAT/PLAY_AUDIO/TRANSLATION/SHADOWING/BILINGUAL |
固件仍无 Mode 枚举,可保持正交 | 正式 FOLLOW_READ 可与 SHADOWING 合并命名 |
| TaskInstance | 否 | — | 0% | Phase 2 只用内存/文件 Session,不建 Java 表 | Phase 3 落 manager-api |
| StepInstance | 否 | — | 0% | 同上 | 跟读采集后才有意义 |
| Event | 部分 | [app_task] / [voice_entry];MQTT ack |
无统一 Event 模型 | Phase 2 增加 [skill_event] 即可,不上第四套总线 |
以后再考虑 type=task |
| Audio | 聊天附件级 | ai_agent_chat_audio LONGBLOB;跟读未强制归档 |
跟读轮次未必走 report | Phase 2 必须把跟读 ASR/音频挂上 Session | 再迁对象存储 |
| Assessment | 否 | — | 0% | 不要在 Phase 2 做评分 | Phase 4 文本比对 v0 |
| Metric | 否 | — | 0% | — | Assessment 之后 |
| Schedule | 半截 | 设备 RTC + pending;无 Server 日历 | 能 7 点唤醒复读 | 不要用 RTC 冒充家长计划权威 | Phase 5 Server Scheduler |
| Plan | 否 | — | 0% | — | 家长任务管理 |
| Subscription | 否 | — | 0% | — | 平台后期 |
符合 Master Plan 的方向: 权威在 Server、设备执行、Chat 热路径保留、App 不是固件、复读不走 play_music。
不符合或提前/滞后:
activeAppId → 实际设备有 pending app_id,Server 有 active_app_id。ESP32-S3 (sening-xiaozhi-s3)
├── Application::Run DeviceState: Idle/Listening/Speaking/...
│ ├── AudioService Opus 16k 热路径(未改语义)
│ ├── Protocol (Xiaozhi WS/MQTT) hello/listen/abort/tts/stt/mcp
│ └── MCP Server self.mp3.* / self.power_off / alarm / ...
│
├── WakeNet + MultiNet
│ 口令: 切换模式 | 复读机 | 开始复读 | 退出复读
│ → Board_Handle_Voice_Command
│
└── Task Gateway (app_task.cc) ← Phase 1 新增
Board_StartAppTask(app, task, params, source)
├── 优先级抢占 / preempt skills
├── NVS pending + last_rep
├── k_task_skills[] 查 Skill
└── 执行: SD MP3 循环 | notify Server | 空 Skill 只切态
旁路(仍并存):
snlp MQTT START_APP_TASK / SET_ALARM / PLAY_MEDIA / DOWNLOAD
DS3231 RTC 闹钟唤醒 → OnRestoredFromPowerOff → ResolveBootOrRestore
非 Sening 板:无 Task Gateway;翻译等纯 Server Mode 仍可走 Xiaozhi WS。
xiaozhi-server (Python)
├── ConnectionHandler
│ session_id (WS 连接级,≠ TaskInstance)
│ active_app_id / current_task_id / interaction_mode / tool_policy
│ shadowing_index / shadowing_prompt
│
├── receiveAudioHandle.startToChat
│ 1. match_voice_switch
│ 2. mode 分流(不进 chat 的: PLAY_AUDIO / SHADOWING / TRANSLATION)
│ 3. CHAT / BILINGUAL → intent + LLM
│
├── app_runtime
│ registry(apps.json)
│ start_app_task / switch_app / filter_tools
│ handle_shadowing_turn / run_translation
│
├── Plugins: switch_app, start_repeater, start_shadowing, translate
├── Device MCP executor → self.mp3.play/stop
└── 可选 Server MCP translate 进程
manager-api (Java) 用户/设备/Agent/聊天 WAV ← 未参与 App Runtime
manager-web 智控台 ← 未参与
┌──────────── 小程序 / MQTT 客户端 ────────────┐
│ START_APP_TASK / SET_ALARM │
└──────────────────┬──────────────────────────┘
│ snlp MQTT
▼
┌──────── Client ────────┐ Xiaozhi WS ┌──────── Server ────────┐
│ Task Gateway │◄── listen detect ──►│ App Runtime │
│ [app_task] [voice_entry] │ start_app_task │
│ SD MP3 Skill │◄── type=mcp ───────►│ DeviceMCP play/stop │
│ Chat AudioService │◄── opus/tts/stt ───►│ ASR / LLM / TTS │
│ pending NVS │ │ connection memory │
└────────────────────────┘ └──────────┬─────────────┘
│ 现有 report
▼
manager-api
chat_history + audio BLOB
同一时刻:一个设备一个 active App/Task。Chat 与 Task 音频互斥沿用 PauseSdCardMp3。
| ID | 债务 | 立即修? |
|---|---|---|
| P0-1 | 无 App Runtime 自动化测试,Phase 2 极易回归 Chat / 口令 / 复读双播 | 是。 Phase 2 第一刀就是 pytest + 手工回归表,否则不准加表/加协议 |
| P0-2 | 跟读 Demo 被误当成「已有 FOLLOW_READ 采集」——没有 prompt/asr/audio 绑定,Assessment 无处落点 | 是(作为 Phase 2 目标)。 允许暂时存在评分,不允许继续假装已采集 |
P0 不是「系统不能跑」,而是「下一阶段会建在流沙上」。
| ID | 债务 | 立即修? |
|---|---|---|
| P1-1 | 无 play.completed:Server 靠设备主动切回 chat |
Phase 2 用 [skill_event],不要新 WS type |
| P1-2 | 双技能表:apps.json vs k_task_skills[] 会漂 |
Phase 2 写契约检查清单;不同步下载 JSON |
| P1-3 | 寄生协议 [app_task] 与唤醒词/ASR 文本空间重叠 |
保持前缀约定;文档化;Phase 2 加 skill_event 前缀 |
| P1-4 | 离线复读与 Server 连接态分叉 | 允许暂时存在;重连后以设备 notify 或下次 detect 对齐 |
| P1-5 | 跟读范读走 TTS 而非音频音频,和复读资源模型不一致 | Phase 2 可先采集 TTS 跟读;SD 范读放到资源 Phase |
| P1-6 | 翻译双实现(plugin + MCP) | 允许暂时存在;Phase 2 不删 MCP,测试以 plugin 为准 |
| P1-7 | BILINGUAL 仍走 chat(),靠 prompt 约束,易夹杂语言 |
允许暂时存在;不是 Phase 2 主路径 |
| ID | 债务 | 立即修? |
|---|---|---|
| P2-1 | 双 MQTT(Xiaozhi vs snlp) | 否 |
| P2-2 | App 状态不落库 | 否(Phase 3 Instance) |
| P2-3 | 复读仅 Sening | 否 |
| P2-4 | 聊天 WAV LONGBLOB | 否 |
| P2-5 | smartlearning 课程云未吸收 | 否(Phase 6) |
| P2-6 | 固件认识业务 app_id 字符串 | 否;保持查找表,禁止业务分支膨胀 |
| P2-7 | 无 type=task / mode.set |
否;等 Instance 真正需要步进再加 |
| P2-8 | 小程序 / 家长日历 UI | 否 |
apps.json)content 规范化(所有非 Chat Mode 都要)功能 Done(带条件): 薄框架 + chat 默认 + 复读 + 翻译 + 三通道挂钩 + 离线复读,且未破坏协议语义、未改 Java DDL。
工程未 Done: 无自动化测试、无跟读采集、无播放完成事件、双表漂移风险。
因此: 不开启 Java Task 表、不开启 Assessment、不开启 App Store。
下一阶段只做一件事:把「音频任务」从 Demo 变成 可归档的跟读会话(见 roadmap.md / phase-2-plan.md)。