File:
phase-1.md
Date: 2026-08-30
Updated: 默认 chat.conversation pending;配网/开机主动发起;三通道才切换
Based on: Master Plan · Architecture Audit · Client Plan V1.1 · Server Plan V1.1 · Cross-System Review
本阶段产出设计与编码任务清单。按本文件实施;与 V1.1 Plan 冲突时,以本文为准。
Client Plan 与 Server Plan 方向可协同(Server 编排、Client 执行、Chat 热路径不动、MCP 优先),但不能按两份文档的「原文 Phase 1」各自开工。必须先对齐下面三条,否则复读和翻译都会做不完或破坏 Chat。
| 契约 | 含义 |
|---|---|
| 权威状态在 Server | 设备不持久化 App/Task 历史 |
| 固件无业务分支 | 禁止 if (app == english) |
| DeviceState ≠ InteractionMode | 不把复读/翻译塞进 Listening/Speaking 迁移表 |
| 协议语义冻结 | hello/listen/abort/tts/stt/mcp 不改字段含义 |
| 可复用硬件 | Sening self.mp3.play / self.mp3.stop / self.power_off;PauseSdCardMp3 |
| 缺口 | 原文 | 决议 |
|---|---|---|
| App 层 | Client 说 Phase 1 不做 App;Server 说 App 表 Phase 3 | 做薄 App Runtime(仅 Server JSON + active_app_id)。设备无 App 目录、无 App Store |
| 复读 N 次 | 双方都假设 Server 循环等 play.completed |
当前 self.mp3.play 立即返回 accepted。N 次循环放进**设备 Skill 参数** repeat;Server 只下发 name/repeat/power_off_after |
| 翻译 | Client 把 TRANSLATION Mode 放到 Phase 3 | App 复用 ASR+TTS 管线,跳过闲聊 LLM,只暴露翻译 MCP |
| 软关机 | 两份 Plan 都未写入复读验收 | 复用现有 self.power_off → RequestPowerOffStandby |
| 双 MQTT | 允许并存 | 色宁 snlp MQTT 是小程序即时启动 / 计划任务的控制面(优先级最高)。START_APP_TASK 只表达「启动哪个 App/Task」,**具体调用哪些 Skill 由该 Task 的配置决定**。禁止用 PLAY_MEDIA 冒充「启动 App」。 |
进入通道(优先级从高到低)
① 小程序即时开始 ──色宁 MQTT──► 最高,打断一切
② 小程序 Schedule ──到点/闹钟唤醒──► 可打断 chat
③ 离线语音切换 ──MultiNet 口令──► 交互选 App/Task
│
▼
统一 start_app_task(app, task, params, source)
│
▼
App Runtime (Server + 设备执行器)
active_app_id 默认 = chat
┌──────────────────┼──────────────────┐
│ │ │
chat english_learning english_translation
mode=CHAT mode=PLAY_AUDIO mode=TRANSLATION
│ │ │
└──────── Device Skill(MCP / 本地播放器)──────┘
同一时刻只运行一个 App / 一个 Task。
默认(没有更高优先级入口时):永远是第一个 App 的 default Task = chat.conversation(原有小智对话能力,但更主动开口)。
| 事件 | 进入 |
|---|---|
| 首次配网成功 | chat / default task conversation,小智先发起(如「我们来聊天吧」) |
| 之后普通开机 / 软硬关机再启动 | 继续 这份 chat pending task,不要停在纯等待问话的 Idle |
| 用户说「我们来聊天吧 / 聊会天吧」或唤醒回聊天 | 切回(或保持)chat.conversation |
| 小程序即时 / 计划到点 / 离线切 App | 才离开 chat |
| RTC 闹钟且 pending 不是 chat(例如复读) | 执行该计划 Task,不要先闲聊 |
做 全面但薄的 App 框架,并在框架上跑三个 App:
chat(default App + default Task) — 原有小智对话能力;配网后与每次默认开机都进入并 主动发起聊天,不是只等用户提问english_learning — 听·复读机:本地 SD 卡 MP3 播放 N 次,结束后 可选软关机english_translation — 英语翻译 App:Server 插入 只做翻译的 MCP;该 App 下不闲聊、不调其他插件type=task(小程序走色宁 MQTT,见 §2.7)play_music(Server TTS 通道)冒充复读必须写入本阶段框架(挂钩可测,小程序可后做): 三种进入通道、优先级抢占、关机唤醒后续跑计划任务、离线口令不再是空回调。
| 概念 | Phase 1 落地 | 不落地 |
|---|---|---|
| App | Server 注册表 + 连接上 active_app_id |
固件 App Manager、SD /apps 安装 |
| Task | english_learning 内一个「复读」动作 |
Workflow / StepInstance |
| InteractionMode | 连接字段 CHAT \| PLAY_AUDIO \| TRANSLATION |
新 DeviceState |
| Skill | 现有 MCP tools + 扩展参数 | 设备侧 ASR/评分 |
| Default App / Task | chat + conversation |
无 App 的空 Idle |
Chat 就是 设备上的第一个 App,其 default Task 名为 conversation:等于今天小智硬件的听/说/LLM 对话,加上「小智更多地主动开口」。
没有小程序即时、没有到期计划、没有离线切 App 时,**任何启动都进入或恢复这一 Task**,不要停在「配好网后干等用户先说话」。
设备本地持久化一份 current/pending:
pending_task = {
app_id, task_id, params, source, updated_at
}
出厂 / 首次配网成功后的初值:
app_id = chat
task_id = conversation
source = default_chat
每次开机(含软关机再启动、断电再上电)按下面解析,**从上到下第一条命中**:
1. 通道① 缓存的即时 MQTT 指令尚未执行 → 执行它
2. 通道② 闹钟到期且 pending 不是 chat.conversation → 执行计划 Task
3. 否则 → 恢复 pending;若空或非法 → chat.conversation
因此:
chat.conversationchat.conversationconversation 也是一个正式 Task,不是「没有 Task」。三通道切换时,chat 与复读、翻译同一套 start_app_task。
建议文件:xiaozhi-esp32-server/main/xiaozhi-server/data/apps.json
(或 config.yaml 下 apps:。优先独立 JSON,避免和模型配置缠在一起。)
{
"default_app_id": "chat",
"apps": {
"chat": {
"name": "聊天",
"mode": "CHAT",
"default_task": "conversation",
"tool_policy": "inherit_agent",
"system_prompt_mode": "agent_proactive",
"tasks": {
"conversation": {
"interaction_mode": "CHAT",
"skills": [],
"note": "原有 Xiaozhi Chat Runtime;由 Runtime 主动发起,不另挂 MCP"
}
}
},
"english_learning": {
"name": "英语学习",
"mode": "PLAY_AUDIO",
"tool_policy": "allowlist",
"tools": ["switch_app", "start_repeater", "self.mp3.play", "self.mp3.stop", "self.mp3.get_list", "self.power_off"],
"system_prompt_mode": "none",
"tasks": {
"repeater": {
"interaction_mode": "PLAY_AUDIO",
"skills": [
{ "name": "self.mp3.play", "args_from": ["name", "repeat", "power_off_after"] }
]
}
}
},
"english_translation": {
"name": "英语翻译",
"mode": "TRANSLATION",
"tool_policy": "allowlist",
"tools": ["switch_app", "translate"],
"system_prompt_mode": "translator",
"tasks": {
"live_translate": {
"interaction_mode": "TRANSLATION",
"skills": [
{ "name": "translate", "side": "server_mcp" }
]
}
}
}
}
}
inherit_agent:沿用当前 Agent 已绑定的插件 + 设备 MCP(**现网行为**)allowlist:只把名单里的工具交给 LLM / 编排器在 ConnectionHandler 增加:
active_app_id: str = "chat"
interaction_mode: str = "CHAT" # 与当前 App.mode 同步
切换 App:
switch_app(app_id)
→ 校验注册表
→ 停当前动作(复读则 MCP self.mp3.stop)
→ active_app_id / interaction_mode 更新
→ 刷新工具列表(invalidate ToolManager cache)
→ 按 system_prompt_mode 切换提示词
switch_app 在 chat 与各 App 白名单中都可用,以便「退出翻译 / 退出复读,回到聊天」。
插入点:startToChat / ConnectionHandler.chat 之前(receiveAudioHandle.py、connection.py)。
| mode | ASR | LLM chat() | TTS | 设备播放 |
|---|---|---|---|---|
| CHAT | 是 | 是(现网) | 是 | 否(除非插件) |
| PLAY_AUDIO | 否(忽略上行当对话) | 否 | 仅启动时一句提示(可选) | SD MP3 |
| TRANSLATION | 是 | 否 | 是(翻译结果) | 否 |
PLAY_AUDIO 期间若收到**用户唤醒词** / 用户主动 listen:复位 chat,走现网。不要在复读中把用户说话送进 LLM。
RTC 闹钟唤醒走通道②,不走这条复位。
插入点:UnifiedToolHandler.get_functions() / ToolManager.get_all_tools()。
if tool_policy == inherit_agent:
现网全集
else:
只保留 allowlist 中存在的工具(含设备 MCP 名 sanitize 后)
翻译 App 下 禁止 出现 weather / play_music / 设备拍照等。
这是「只做翻译的 MCP」能成立的前提:不是 MCP 自己谦虚,而是 App 白名单把别的工具摘掉。
设备 不实现业务 App(无 EnglishLearningApp 类),但必须有 进入通道网关:
chat.conversation,并让小智 主动开口(见 §3.1)self.mp3.play 支持 repeat + power_off_after(仅当 Task 配置了该 Skill)PauseSdCardMp3RequestPowerOffStandbySTART_APP_TASK 并抢占当前任务(§2.7 通道①)chat.conversation将来进入「某 App 的某 Task」只有三条通道。Phase 1 就把 优先级、抢占、载荷、关机唤醒 定死,避免以后每个 App 自己抢设备。
统一入口:
start_app_task(app_id, task_id, params, source)
source:
mqtt_immediate 优先级 3 最高
schedule 优先级 2
voice 优先级 1
default_chat 优先级 0
抢占规则:
新请求.priority > 当前.priority → 中断当前 App/Task,立刻切换
新请求.priority = 当前.priority → 同通道后到覆盖(小程序连点两次:后者生效)
新请求.priority < 当前.priority → 拒绝或忽略(例如复读由 MQTT 拉起时,语音切翻译无效)
「中断」统一动作(设备侧,与业务无关):
停止当前正在跑的 Skill(播、听、TTS、显示任务等)
→ 再按「新 Task 配置」执行其 skills[]
固件仍然 不写 if english;MQTT/语音/闹钟只带 app_id + task_id + JSON params。
执行哪些 Skill 不由通道决定,只由该 App/Task 的配置决定。
START_APP_TASK / start_app_task
│
▼
查 App 注册表[app_id].tasks[task_id]
│
▼
读取该 Task 的:
interaction_mode
skills[] ← 真正要调用的能力
params schema
│
▼
按 skills[] 依次(或按 Step)调用
设备 MCP Skill(如 self.mp3.* / self.power_off / display)
和/或 Server Skill(如 translate MCP / ASR / TTS)
要点:
self.mp3.play 只是 english_learning.repeater 这份配置里的 Skill,不是所有 App/Task 的默认技能。translate;将来跟读会配 play + listen;口算会配 speak + listen + Server 判题。同一条 MQTT START_APP_TASK 都能启动。PLAY_MEDIA 是色宁历史上的「播本地文件」指令,**不是 App 协议**。即使某 Task 的 Skill 语义上也是播放,也应走该 Task 配置的 Skill 名(Phase 1 复读用 self.mp3.play),不要让小程序直接发 PLAY_MEDIA 冒充「打开英语 App」。Phase 1 已有 Task 与 Skill 对照:
| App | Task | 配置中的 skills(示例) |
|---|---|---|
chat |
conversation(**default**) |
现有 Chat Runtime;小智主动发起,无强制额外 MCP |
english_learning |
repeater |
self.mp3.play(+ 可选 self.power_off) |
english_translation |
live_translate |
Server MCP translate + 现有 TTS |
手机小程序
选 App → 选 Task → 立即开始
→ 色宁 MQTT 下发 START_APP_TASK(只带 app_id / task_id / params / source)
→ 无论当前是 chat、翻译还是别的 Task,全部中断
→ start_app_task(...)
→ 按该 Task 配置的 skills[] 执行(复读才会调到 self.mp3.play;翻译则调 translate)
为什么走色宁 MQTT 而不是 Xiaozhi WS: 小程序可能在设备 Idle、软关机恢复前、或对话通道未打开时就要能下发;色宁 snlp/v1/{device_id}/cmd/ctl|task 已常驻。Xiaozhi type=mcp / Server MCP 仍是 Skill 调用面。MQTT 的 START_APP_TASK 是 Task 启动面。二者不要混成一条「播放指令」。
建议新 action(挂在现有 BoardMqtt_DispatchAction):
{
"header": { "device_id": "...", "req_id": "...", "timestamp": 0 },
"action": "START_APP_TASK",
"payload": {
"app_id": "english_learning",
"task_id": "repeater",
"params": {
"name": "lesson001.mp3",
"repeat": 10,
"power_off_after": false
},
"source": "mqtt_immediate"
}
}
payload.params 的字段由 该 Task 的 schema 决定,不是全局固定为 MP3 文件名。复读 Task 才需要 name/repeat/power_off_after;翻译 Task 可以几乎无 params;跟读以后会有 resource_id 等。
设备 ack 走现有 rpt/ack。若当前有低优先级任务:先停再按新 Task 的 skills[] 启动。
Phase 1 编码: 协议 + 抢占 + 能启动本阶段已有 task(repeater / translate / 回 chat)。小程序 UI 可后做,用 MQTT 客户端联调即可。
小程序设置计划
例如:每天 07:00 英语学习 / 听-复读 或 跟读
→ Server 保存 Schedule(权威)
→ 同步到设备:RTC 闹钟 SET_ALARM + 本地 pending_task
→ 到点:
若设备开着且当前是 chat(或更低优先级)→ 切换并开始
若设备软关机 / 硬件关机后由 RTC 唤醒 → 联网后立刻执行 pending,不进 chat
这是 小智硬件重启 / 闹钟唤醒的主要产品目标:不是为了回到待机聊天,而是到点自动泛听、跟读等。
现有代码已经具备半截能力,**尚未接到 App/Task**:
| 已有 | 缺口 |
|---|---|
MQTT SET_ALARM / UNSET_ALARM + DS3231 |
闹钟只唤醒,不带 app/task |
软关机 RequestPowerOffStandby,闹钟 schedule_power_off_restore_from_alarm |
RestoreFromPowerOffStandby 只 ResumePlaylistIfUnfinished |
| 软关机期间关闭唤醒词与离线命令 | 符合「关机后只有闹钟能拉起来」 |
Phase 1 必须补的设备语义:
NVS / SD 保存 pending_task.json
{ app_id, task_id, params, source: "schedule", alarm_id }
闹钟触发且从 power-off 恢复
→ 若 pending 是非 chat 的计划 Task → 执行它,禁止先闲聊
→ 若 pending 是 chat.conversation 或没有非 chat 计划 → 进入 chat.conversation 并主动开口
→ 计划 Task 成功结束后按配置写回 chat.conversation
开机且已在 chat:到点同等抢占(priority 2 > 0)。
正在执行 MQTT 即时任务(priority 3)时:本次 Schedule 触发跳过或延后,不抢 MQTT。
Server 侧完整 Plan 表、家长日历 UI:**本阶段不建 Java 表**;可用 apps.json + 设备闹钟 + pending 文件先跑通「7 点唤醒后自动复读」。跟读 Task 本体仍非本阶段,但 pending 的 task_id 字段必须预留。
硬件 已经实现离线语音识别(ESP-SR MultiNet),目的就是为将来切 App / Task / InteractionMode 做准备。**当前版本没有接到切换。**
现状(Sening):
| 位置 | 行为 |
|---|---|
afe_wake_word.cc / custom_wake_word.cc |
MultiNet 检测离线命令 |
| 口令 | 仅 切换模式(拼音 qie huan mo shi) |
Application → Board_Handle_Offline_Command |
匹配后只 ESP_LOGI,**无切换** |
CustomWakeWord 的 index.json commands |
已有 command / text / action 结构,可扩展,尚未用于 App |
目标体验(将来,不限于本阶段三个 App):
小智:现在我们来做个游戏吧,
是想做数字背诵、成语接龙,
还是 10 以内加减法口算,还是 xx?
用户:(离线命令 / 短句)
程序能判定意图 → start_app_task(对应 app, 对应 task, source=voice)
意图不清 → 再问一次 / 留在当前 App
判定原则:
Phase 1 编码(挂钩,不是做完整游戏平台):
Board_Handle_Offline_Command 不再空转:进入 voice_entry(例如打开「选 App」菜单态)chat.conversation(最常用)english_learning.repeater / english_translation;「数字背诵 / 成语接龙 / 口算」作意图槽预留app_id+task_id 映射表(JSON/Assets),与 MQTT 载荷同一套与唤醒词分工:
配网成功 / 普通开机 → chat.conversation(主动开口)
「我们来聊天吧」等口令 → chat.conversation
唤醒词 → 回到或保持 chat.conversation(可接着主动一句)
离线「切换模式」 → 通道③ 选其它 App/Task
RTC 闹钟 + 非 chat 计划 → 通道② 计划 Task,不要先闲聊
软关机期间两者都关闭(现有逻辑),只有闹钟能恢复。
| 通道 | 设备是否必须在线 Xiaozhi WS | 权威 |
|---|---|---|
| ① MQTT 即时 | 不必先开对话通道 | 小程序请求;设备立即抢占执行 |
| ② Schedule | 到点时再联网即可 | Server 存计划;设备存闹钟 + pending |
| ③ 语音 | 纯口令可离线判定;澄清对话需要 WS | Server 注册表;本地只做口令→id |
三通道汇合到同一套 start_app_task → 查 Task 配置 → 调 skills[]。MQTT 不要再为「播放」单独走 PLAY_MEDIA 业务分支,否则同一复读 Task 会出现两套执行器。其它 Task 也一样:通道不绑定某种 Skill。
conversation这是设备上的 第一个 App、默认 Task,对应原有小智 AI 硬件的对话功能。
何时进入(只应这些,外加启动解析):
switch_app("chat") / MQTT app_id=chat, task_id=conversation何时不进 chat: 小程序即时开始了别的 Task;闹钟唤醒的 pending 是复读/翻译等非 chat 计划。
协议与状态机: Listening ↔ Speaking、hello/listen/tts、Agent 插件 保持。框架只把这条链路登记为 chat.conversation。
与原程序的差异(本阶段要做):小智更多地发起,而不是完全被动等用户问。
原行为:配网后 Idle → 等唤醒 → 等用户说话 → 再答。
目标行为:进入 conversation 后,小智 先开口 邀请聊天,并在冷场时可以再发起一轮,而不是一直沉默。
Phase 1 主动发起的最小实现(不另造 App):
进入 chat.conversation
→ 打开音频通道(若尚未打开)
→ TTS 主动句(配置或角色提示驱动)
例:「我们来聊天吧。」「想聊点什么呀?」
→ 进入 Listening,等用户
→ 之后按现网 ASR → LLM → TTS
→ 若长时间无用户说话:可再主动一句(次数/间隔可配,避免死循环催促)
实现落点建议:
system_prompt_mode = agent_proactive(鼓励先问、短句发起);可选连接建立后下发一句 tts 而不等用户 STTconversation 后,不要只停在 Idle 等按键;应走现有「开始听」路径并允许先播 TTSpending: 关机前若正在 chat,pending 保持 chat.conversation。再开机继续该 Task(可再主动打个招呼,不必复述关机前最后一句,但角色记忆仍走现有 Server memory)。
回归红线: 用户一旦开始说话,打断、TTS、已有插件与现网一致。主动开口 不得 改 Opus 热路径或 listen 语义。
用户体验
进入英语学习 / 开始复读
→ 播放 SD 卡上指定 MP3
→ 完整播放 N 次(文件已在本地,不下载)
→ 若 power_off_after=true:软关机(现有 power-off standby)
→ 否则停在 Idle;唤醒回到 chat
参数(Server 下发,设备执行)
| 参数 | 说明 |
|---|---|
name |
与 self.mp3.get_list 的 files[].name 完全一致 |
repeat |
正整数 N,整首循环次数(不是服务器连调 N 次 play) |
power_off_after |
bool,默认 false |
为什么循环在设备上
self.mp3.play 今日在文件开始播放后即返回 {"accepted":true},15s 内同名重复调用还会 duplicate_ignored。Server 无法可靠「播完再调下一次」。把 repeat 放进播放器循环,不依赖新协议事件,也不把「英语」写进固件。
编排(Server)
插件 start_repeater(name, repeat, power_off_after):
active_app_id = english_learning,mode = PLAY_AUDIOself.mp3.play + 新参数Action.RECORD 或 NONE,**不再** chat()缺文件:先 self.mp3.get_list,没有则 TTS 提示并保持 chat。
软关机
设备在第 N 次自然结束(非 stop、非 TTS 打断)后,若 power_off_after:调用现有 Application::RequestPowerOffStandby()(与 self.power_off 同一路径)。
被唤醒词/按键/self.mp3.stop 打断则 不关机,并应让 Server 在下次 listen 时回到 chat。
目标板: sening-xiaozhi-s3。其他板无 SD MP3 时:start_repeater 返回明确错误,不得改 Chat。
用户体验
进入英语翻译
用户说中文 → 设备播英文
用户说英文 → 设备播中文
不闲聊、不讲笑话、不调天气
「退出翻译」→ chat
Server MCP(本阶段核心新增)
在 data/.mcp_server_settings.json 注册独立 MCP Server,例如名 english-translate:
{
"mcpServers": {
"english-translate": {
"command": "python",
"args": ["mcp_servers/translate/server.py"]
}
}
}
MCP 工具(建议只暴露一个):
translate(text, source_lang?, target_lang?)
规则:
ConnectionHandler.chat(避免套娃闲聊)实时路径
ASR 文本
→ interaction_mode == TRANSLATION
→ 直接 ServerMCPExecutor.execute("translate", {text})
→ TTS 播 result
→ 不进入 chat()、不写长 Dialogue(可选记一条 history)
不要:先让闲聊 LLM「决定是否翻译」。工具白名单 + 旁路 chat() 双保险。
用户(在 chat):「复读 lesson001 三遍然后关机」
→ LLM function call start_repeater
→ Server: app=english_learning, mode=PLAY_AUDIO
→ MCP self.mp3.play(name=lesson001.mp3, repeat=3, power_off_after=true)
→ 设备本地播放 3 次
→ RequestPowerOffStandby
用户唤醒
→ listen/detect
→ Server 复位 app=chat, mode=CHAT
→ 现网对话
用户:「打开翻译」
→ switch_app(english_translation)
→ 工具列表仅 switch_app + translate
用户说话 → ASR
→ translate MCP → TTS
用户:「退出翻译」
→ switch_app(chat)
首次配网成功
→ pending = chat.conversation
→ start_app_task(chat, conversation, source=default_chat)
→ 小智主动:「我们来聊天吧」
→ Listening
之后关机再启动(无到期的非 chat 计划)
→ 读取 pending = chat.conversation
→ 再次进入该 Task,小智可再主动招呼
→ 不是空白 Idle 干等问话
离线或对话:「我们来聊天吧 / 聊会天吧」
→ start_app_task(chat, conversation, source=voice)
→ 打断当前更低优先级;若正在 MQTT 即时任务则不抢
→ 小智主动接话
MQTT START_APP_TASK app=english_learning task=repeater
→ 若正在翻译或 chat 或语音菜单:全部中断
→ 播 SD 复读
SET_ALARM 07:00 + pending_task=repeater
→ 夜间软关机(power_off)
→ 07:00 DS3231 闹钟
→ RestoreFromPowerOffStandby
→ 执行 pending_task(不要先 chat)
用户说「切换模式」(现有 MultiNet 口令)
→ Board_Handle_Offline_Command 进入 voice_entry
→ (Phase 1)可再听「打开翻译 / 开始复读 / 回到聊天」
→ start_app_task(source=voice)
→ 若当前是 MQTT 即时任务:忽略语音切换
仓库:xiaozhi-esp32-2.2.6
原则:最小改动;业务名不上固件。
| ID | 任务 | 文件 | 说明 |
|---|---|---|---|
| C1 | self.mp3.play 增加 repeat(int,默认 1)、power_off_after(bool,默认 false) |
main/boards/sening-xiaozhi-s3/sening_xiaozhi_s3.cc |
工具描述同步更新;repeat=1 兼容旧调用 |
| C2 | SD 播放器支持循环 N 次 | sening 的 SDCard_Play_MP3* 实现处(system.c / 播放任务) |
自然播完才计数;self.mp3.stop、TTS Pause 打断则取消剩余次数且不关机 |
| C3 | N 次完成且 power_off_after → RequestPowerOffStandby |
同上 + application.cc 已有接口 |
禁止新写一套关机 |
| C4 | 保持 PauseSdCardMp3 与 Chat TTS 互斥 |
application.cc |
不改热路径;复读中 TTS 仍可暂停 MP3 |
| C5 | 确认 self.mp3.get_status 能区分 playing / 循环剩余(可选) |
sening_xiaozhi_s3.cc |
Phase 1 可选字段 repeat_left;非必须 |
| C6 | 禁止 新增 English App 类、DeviceState、Xiaozhi type=task |
— | Review 红线 |
| C7 | MQTT START_APP_TASK 分发 + 抢占 + 按 task 配置调 skills[] |
mqtt.c 分发;执行走统一 start_app_task |
不要在 MQTT 里写死 play MP3;ack 走 rpt/ack |
| C8 | pending 持久化;开机解析 §2.1.1 | NVS/SD + RestoreFromPowerOffStandby |
默认恢复 chat.conversation;仅非 chat 计划闹钟才不进闲聊 |
| C9 | 离线命令回调改为进入通道③,不再只打日志 | system.c Board_Handle_Offline_Command;口令表可扩 |
先支持切到已有 App/Task;游戏类 task_id 预留 |
非 Sening 板:不改 SD/MQTT/RTC。MCP 无 self.mp3.play 时由 Server 报错。离线 MultiNet 在其它板若存在,同样只做挂钩、不做英语业务。
仓库:xiaozhi-esp32-server
物理边界:只动 xiaozhi-server (Python)。manager-api 不改表。
| ID | 任务 | 建议路径 | |
|---|---|---|---|
| S1 | App 注册表加载 | core/app_runtime/registry.py + data/apps.json |
|
| S2 | 连接态默认 chat / conversation / CHAT |
core/connection.py |
与设备 pending 对齐 |
| S2b | 进入 conversation 后主动 TTS 发起(我们来聊天吧) | 连接欢迎 / 角色 prompt | 不要等用户第一句才说话;可配置开关 |
| S3 | switch_app 插件 |
plugins_func/functions/switch_app.py |
|
| S4 | 工具白名单过滤 | unified_tool_handler.py 或 unified_tool_manager.py;切换后 refresh_tools() |
|
| S5 | startToChat / chat 前按 mode 分流 |
receiveAudioHandle.py、connection.py |
|
| S6 | 用户唤醒 / listen.state=detect 复位 default app |
listenMessageHandler.py |
不要在闹钟 pending 执行期间当成用户唤醒清任务 |
| S14 | 统一 start_app_task(app, task, params, source) + 优先级 |
core/app_runtime/ |
MQTT/语音/插件都走这里 |
| S15 | 通道③:云端仅作口令不够时的澄清;默认不覆盖 MQTT/Schedule | 同上 | 与离线命令映射表对齐 |
chat 下 S4 必须等价于现网(inherit_agent),否则 default App 验收失败。
| ID | 任务 | 建议路径 |
|---|---|---|
| S7 | start_repeater 插件 |
plugins_func/functions/start_repeater.py |
| S8 | 通过 DeviceMCPExecutor 调 self.mp3.play |
复用现有 device MCP,不要走 play_music |
| S9 | Agent 插件绑定 | 管理端或配置把 switch_app、start_repeater 加到目标 Agent;chat 需要能切 App |
| ID | 任务 | 建议路径 |
|---|---|---|
| S10 | 独立 MCP Server 进程,只注册 translate |
main/xiaozhi-server/mcp_servers/translate/server.py |
| S11 | 写入 data/.mcp_server_settings.json |
现有 ServerMCPManager 会加载 |
| S12 | TRANSLATION 模式:ASR 后直接 execute_tool(translate) 再 TTS |
不要先 chat() |
| S13 | translator 系统提示(仅 switch_app 时给 LLM 用) | 短:说明当前是翻译机、退出需调用 switch_app |
translate 的 LLM 调用应使用独立短 prompt,温度低,禁止 function call。
"english-translate": {
"command": "python",
"args": ["mcp_servers/translate/server.py"]
}
工作目录为 xiaozhi-server。依赖:现有 mcp Python 包(Server 已用)。
Phase 1 不新增 Xiaozhi WS type=task。小程序/计划走 色宁 MQTT。
| 方向 | 消息 | 用途 |
|---|---|---|
| 下行 WS | 现有 type=mcp tools/call |
play / stop / power_off / 设备列表 |
| 下行 WS | 现有 type=tts |
翻译结果、复读提示、语音菜单提问 |
| 上行 WS | 现有 listen / mcp result |
用户唤醒复位、工具返回 |
| 下行 MQTT | 新 START_APP_TASK |
通道①即时开始;可带 source=schedule 作补发 |
| 下行 MQTT | 现有 SET_ALARM |
通道②到点唤醒 |
| 上行 MQTT | 现有 rpt/ack |
启动/失败回执 |
| — | 不新增 play.completed | 由设备内部 repeat 消化 |
MCP play 参数向后兼容:旧客户端无 repeat 时按 1 次、不关机。
| 项 | Phase 1 |
|---|---|
| MySQL 新表 | 无 |
/task/instance |
无 |
| chat_history | 翻译可继续按现网上报文本;不强制新 audio 字段 |
| Redis | 不存 App 状态(连接内存即可;断线即回 default) |
Xiaozhi WS 断线重连 = 新 session_id = 默认 chat。
例外: 设备本地 pending_task(通道②)在闹钟唤醒后仍然执行,不依赖旧 session_id。不恢复「半截被用户打断的复读」。
P1-A 框架骨架
S1–S5、S14、switch_app
+ 进入通道:优先级、C7 抢占、C9 离线口令挂钩、C8 pending 形状
验收:默认仍是完整 Chat;switch_app 往返不破坏对话
验收:MQTT START_APP_TASK 能打断 chat;离线「切换模式」不再是空 log
P1-B 翻译 App
S10–S13
验收:翻译 App 内只出译文;无天气/音乐工具;退出回 chat
P1-C 复读机
C1–C4 + S7–S9
验收:SD 文件播 N 次;可选软关机;打断不关机;Chat TTS 仍能暂停 MP3
P1-D 回归
不切 App 的长对话、打断、已有插件、OTA、配网
先 A 再 B 再 C:翻译不依赖 SD,可在非 Sening 上先测框架。
conversation)switch_app("english_translation") 后工具列表不含 weatherswitch_app("chat") 后工具列表恢复app_id 被拒绝并留在当前 AppSTART_APP_TASK 正在翻译时能中断并改去复读(通道① > ③/chat)SET_ALARM + pending 复读:闹钟唤醒后直接播放,不先进入闲聊voice_entry(不再只打日志)get_list 有文件power_off_after=false 结束后可唤醒进 chatpower_off_after=true 结束后进现有软关机(唤醒词关闭,与今日 self.power_off 一致)stop 或唤醒:剩余遍数取消、**不**关机self.mp3.play 的板调用 start_repeater → 语音错误,Chat 仍可用play_music 播课程chat.conversation;配网后与默认开机都进入它;同时只有一个 active。type=task、无固件英语业务类、无音频热路径重写。| 债 | 说明 |
|---|---|
| 复读仅 Sening | 其它板需后续把 play_audio 提为核心 Skill |
| 无 play.completed 协议 | Server 不知道播到第几遍;状态以设备为准 |
| App 状态不落库 | WS 断线即回 chat;**pending_task 必须在设备本地**,否则闹钟唤醒丢失 |
| 翻译 MCP 再调一次 LLM | 比「改系统提示硬译」多一跳;换的是工具隔离与「只做翻译」 |
| 双控制面 | 小程序/闹钟走 snlp;对话/技能走 Xiaozhi WS+MCP。不在 Phase 1 合并 |
| 离线命令词很少 | 现仅「切换模式」;菜单口令与游戏 Task 后续加 Assets/命令表 |
| 小程序未做 | 通道①②用 MQTT/闹钟联调;UI 后置 |
application.cc JSON/MCP、sening_xiaozhi_s3.cc mp3/power_off、connection.py、receiveAudioHandle.py、unified_tool_handler.py、server_mcp/mcp_manager.py。EnglishRepeatManager / TranslationApp 固件类。hello/listen/tts/stt 语义。repeat 默认 1,保证旧 MCP 调用者兼容。source 与优先级;禁止三个入口各写一套 stop/play。chat.conversation 并允许先 TTS。实施从 P1-A 开始。未完成 A(含进入通道挂钩与默认 conversation)之前不要改播放器循环。