编辑 | blame | 历史 | 原始文档

Phase 1 Coding Plan

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 冲突时,以本文为准。


0. Cross-System Review 结论

Client Plan 与 Server Plan 方向可协同(Server 编排、Client 执行、Chat 热路径不动、MCP 优先),但不能按两份文档的「原文 Phase 1」各自开工。必须先对齐下面三条,否则复读和翻译都会做不完或破坏 Chat。

0.1 已经对齐(保持)

契约 含义
权威状态在 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

0.2 原文冲突与 Phase 1 决议

缺口 原文 决议
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」。

0.3 协同总图(Phase 1)

进入通道(优先级从高到低)
  ① 小程序即时开始  ──色宁 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,不要先闲聊

1. Phase 1 目标

做 全面但薄的 App 框架,并在框架上跑三个 App:

  1. chat(default App + default Task) — 原有小智对话能力;配网后与每次默认开机都进入并 主动发起聊天,不是只等用户提问
  2. english_learning — 听·复读机:本地 SD 卡 MP3 播放 N 次,结束后 可选软关机
  3. english_translation — 英语翻译 App:Server 插入 只做翻译的 MCP;该 App 下不闲聊、不调其他插件

1.1 非目标(明确不做)

  • 跟读、录音归档、Assessment、TaskInstance 正式表
  • Xiaozhi 协议新 type=task(小程序走色宁 MQTT,见 §2.7)
  • 家长小程序 UI / 完整 Java Scheduler 微服务 / App Store 上架
  • 成语接龙、数字背诵、口算等尚未存在的 Task 本体(语音菜单只预留意图槽)
  • 合并 Xiaozhi MQTT 与 snlp/v1 为一条物理连接
  • 改 Opus 热路径、DeviceState 合法迁移表
  • 非 Sening 板的 SD 复读(翻译 App 不依赖 SD,可全板)
  • 用 play_music(Server TTS 通道)冒充复读

必须写入本阶段框架(挂钩可测,小程序可后做): 三种进入通道、优先级抢占、关机唤醒后续跑计划任务、离线口令不再是空回调。


2. 框架设计(本阶段重点)

2.1 概念

概念 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**,不要停在「配好网后干等用户先说话」。

2.1.1 启动解析:默认永远是 chat.conversation

设备本地持久化一份 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

因此:

  • 第一次配网成功 → 进入第一个 App 的 default Task:chat.conversation
  • 之后关机再开 → 默认仍是这份 chat pending(继续聊天 Task),不是空白 Idle
  • 只有 小程序即时、小程序计划、离线语音切换 才会改写 pending 并离开 chat
  • 计划 Task 跑完若配置「回到 default」→ pending 写回 chat.conversation

conversation 也是一个正式 Task,不是「没有 Task」。三通道切换时,chat 与复读、翻译同一套 start_app_task。

2.2 App 注册表(Server,配置驱动)

建议文件: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 / 编排器
  • 固件 不读取 此文件

2.3 连接态(Server 内存,不建表)

在 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 白名单中都可用,以便「退出翻译 / 退出复读,回到聊天」。

2.4 Mode 对实时链路的分流

插入点:startToChat / ConnectionHandler.chat 之前(receiveAudioHandle.py、connection.py)。

mode ASR LLM chat() TTS 设备播放
CHAT 是 是(现网) 是 否(除非插件)
PLAY_AUDIO 否(忽略上行当对话) 否 仅启动时一句提示(可选) SD MP3
TRANSLATION 是 否 是(翻译结果) 否

PLAY_AUDIO 期间若收到**用户唤醒词** / 用户主动 listen:复位 chat,走现网。不要在复读中把用户说话送进 LLM。
RTC 闹钟唤醒走通道②,不走这条复位。

2.5 工具过滤

插入点:UnifiedToolHandler.get_functions() / ToolManager.get_all_tools()。

if tool_policy == inherit_agent:
    现网全集
else:
    只保留 allowlist 中存在的工具(含设备 MCP 名 sanitize 后)

翻译 App 下 禁止 出现 weather / play_music / 设备拍照等。
这是「只做翻译的 MCP」能成立的前提:不是 MCP 自己谦虚,而是 App 白名单把别的工具摘掉。

2.6 Client 框架职责(薄)

设备 不实现业务 App(无 EnglishLearningApp 类),但必须有 进入通道网关:

  1. Chat Runtime 协议与热路径保持(hello/listen/tts)
  2. 配网成功与默认开机进入 chat.conversation,并让小智 主动开口(见 §3.1)
  3. Skill 可被 MCP 调用
  4. self.mp3.play 支持 repeat + power_off_after(仅当 Task 配置了该 Skill)
  5. 音频互斥继续用 PauseSdCardMp3
  6. 软关机继续用 RequestPowerOffStandby
  7. 色宁 MQTT 能下发 START_APP_TASK 并抢占当前任务(§2.7 通道①)
  8. 开机解析 pending:无更高优先级则恢复 chat.conversation;闹钟非 chat 计划则执行计划(§2.1.1)
  9. 离线语音命令回调进入切换入口;「我们来聊天吧 / 聊会天吧」映射回 chat.conversation

2.7 三种进入 App / Task 的方式(框架核心)

将来进入「某 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)

要点:

  • 通道只负责「何时、用什么优先级启动哪个 Task」,不负责「这个 Task 播放 MP3」。
  • self.mp3.play 只是 english_learning.repeater 这份配置里的 Skill,不是所有 App/Task 的默认技能。
  • 翻译 Task 配的是 Server MCP 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 客户端联调即可。


通道② 小程序 Schedule(优先级第二)——关机自动执行的主目标

小程序设置计划
  例如:每天 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

判定原则:

  • 优先本地 MultiNet 口令(低延迟、可在未开 Xiaozhi 对话通道时工作)
  • 口令表按「当前交互步」加载(菜单步只有几个候选,不要一次注册全部业务句)
  • 本地无法判定时,才允许走云端 ASR+意图(此时已在 chat 管线,priority 仍算 voice)
  • 语音 不能 打断通道① MQTT 即时任务,也 不能 打断正在执行的通道② 计划任务

Phase 1 编码(挂钩,不是做完整游戏平台):

  1. Board_Handle_Offline_Command 不再空转:进入 voice_entry(例如打开「选 App」菜单态)
  2. Phase 1 菜单必须包含回到聊天:「我们来聊天吧」「聊会天吧」→ chat.conversation(最常用)
  3. 其它候选:english_learning.repeater / english_translation;「数字背诵 / 成语接龙 / 口算」作意图槽预留
  4. 不在固件写死游戏业务;口令 → app_id+task_id 映射表(JSON/Assets),与 MQTT 载荷同一套

与唤醒词分工:

配网成功 / 普通开机     → chat.conversation(主动开口)
「我们来聊天吧」等口令  → chat.conversation
唤醒词                   → 回到或保持 chat.conversation(可接着主动一句)
离线「切换模式」         → 通道③ 选其它 App/Task
RTC 闹钟 + 非 chat 计划  → 通道② 计划 Task,不要先闲聊

软关机期间两者都关闭(现有逻辑),只有闹钟能恢复。


2.8 进入通道 vs Server App Runtime

通道 设备是否必须在线 Xiaozhi WS 权威
① MQTT 即时 不必先开对话通道 小程序请求;设备立即抢占执行
② Schedule 到点时再联网即可 Server 存计划;设备存闹钟 + pending
③ 语音 纯口令可离线判定;澄清对话需要 WS Server 注册表;本地只做口令→id

三通道汇合到同一套 start_app_task → 查 Task 配置 → 调 skills[]。MQTT 不要再为「播放」单独走 PLAY_MEDIA 业务分支,否则同一复读 Task 会出现两套执行器。其它 Task 也一样:通道不绑定某种 Skill。


3. App 规格

3.1 chat · default Task conversation

这是设备上的 第一个 App、默认 Task,对应原有小智 AI 硬件的对话功能。

何时进入(只应这些,外加启动解析):

  • 首次配网成功
  • 之后每次开机 / 软硬关机再启动,且没有更高优先级的计划 Task
  • 口令或对话:「我们来聊天吧」「聊会天吧」、唤醒回聊
  • switch_app("chat") / MQTT app_id=chat, task_id=conversation
  • 其它 Task 正常结束且配置回到 default

何时不进 chat: 小程序即时开始了别的 Task;闹钟唤醒的 pending 是复读/翻译等非 chat 计划。

协议与状态机: Listening ↔ Speaking、hello/listen/tts、Agent 插件 保持。框架只把这条链路登记为 chat.conversation。

与原程序的差异(本阶段要做):小智更多地发起,而不是完全被动等用户问。

原行为:配网后 Idle → 等唤醒 → 等用户说话 → 再答。
目标行为:进入 conversation 后,小智 先开口 邀请聊天,并在冷场时可以再发起一轮,而不是一直沉默。

Phase 1 主动发起的最小实现(不另造 App):

进入 chat.conversation
  → 打开音频通道(若尚未打开)
  → TTS 主动句(配置或角色提示驱动)
     例:「我们来聊天吧。」「想聊点什么呀?」
  → 进入 Listening,等用户
  → 之后按现网 ASR → LLM → TTS
  → 若长时间无用户说话:可再主动一句(次数/间隔可配,避免死循环催促)

实现落点建议:

  • Server:system_prompt_mode = agent_proactive(鼓励先问、短句发起);可选连接建立后下发一句 tts 而不等用户 STT
  • Client:配网成功 / 默认开机解析到 conversation 后,不要只停在 Idle 等按键;应走现有「开始听」路径并允许先播 TTS
  • 口令「我们来聊天吧」与主动句共用同一 Task,不要做成第二个聊天 App

pending: 关机前若正在 chat,pending 保持 chat.conversation。再开机继续该 Task(可再主动打个招呼,不必复述关机前最后一句,但角色记忆仍走现有 Server memory)。

回归红线: 用户一旦开始说话,打断、TTS、已有插件与现网一致。主动开口 不得 改 Opus 热路径或 listen 语义。

3.2 english_learning · 听-复读机

用户体验

进入英语学习 / 开始复读
  → 播放 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):

  1. active_app_id = english_learning,mode = PLAY_AUDIO
  2. 可选 TTS:「开始复读 N 次」(短、可关)
  3. Device MCP:self.mp3.play + 新参数
  4. 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。

3.3 english_translation

用户体验

进入英语翻译
用户说中文 → 设备播英文
用户说英文 → 设备播中文
不闲聊、不讲笑话、不调天气
「退出翻译」→ 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?)

规则:

  • 未指定语言:中文→en,英文→zh;其它语言默认译成中文
  • 只返回译文,无解释、无拼音课、无 follow-up 提问
  • 实现:MCP 进程内用独立「翻译系统提示」调现有 LLM HTTP(读 xiaozhi 配置),**不要**走 ConnectionHandler.chat(避免套娃闲聊)
  • 该 MCP 不注册 play_music / 设备控制

实时路径

ASR 文本
  → interaction_mode == TRANSLATION
  → 直接 ServerMCPExecutor.execute("translate", {text})
  → TTS 播 result
  → 不进入 chat()、不写长 Dialogue(可选记一条 history)

不要:先让闲聊 LLM「决定是否翻译」。工具白名单 + 旁路 chat() 双保险。


4. 时序

4.1 复读

用户(在 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
  → 现网对话

4.2 翻译

用户:「打开翻译」
  → switch_app(english_translation)
  → 工具列表仅 switch_app + translate
用户说话 → ASR
  → translate MCP → TTS
用户:「退出翻译」
  → switch_app(chat)

4.3 Default:配网与每次开机

首次配网成功
  → pending = chat.conversation
  → start_app_task(chat, conversation, source=default_chat)
  → 小智主动:「我们来聊天吧」
  → Listening

之后关机再启动(无到期的非 chat 计划)
  → 读取 pending = chat.conversation
  → 再次进入该 Task,小智可再主动招呼
  → 不是空白 Idle 干等问话

4.3b 用户要聊天

离线或对话:「我们来聊天吧 / 聊会天吧」
  → start_app_task(chat, conversation, source=voice)
  → 打断当前更低优先级;若正在 MQTT 即时任务则不抢
  → 小智主动接话

4.4 小程序即时开始(通道①)

MQTT START_APP_TASK app=english_learning task=repeater
  → 若正在翻译或 chat 或语音菜单:全部中断
  → 播 SD 复读

4.5 计划任务 + 关机唤醒(通道②)

SET_ALARM 07:00 + pending_task=repeater
  → 夜间软关机(power_off)
  → 07:00 DS3231 闹钟
  → RestoreFromPowerOffStandby
  → 执行 pending_task(不要先 chat)

4.6 离线语音切换(通道③)

用户说「切换模式」(现有 MultiNet 口令)
  → Board_Handle_Offline_Command 进入 voice_entry
  → (Phase 1)可再听「打开翻译 / 开始复读 / 回到聊天」
  → start_app_task(source=voice)
  → 若当前是 MQTT 即时任务:忽略语音切换

5. Client 改动清单

仓库: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 在其它板若存在,同样只做挂钩、不做英语业务。


6. Server 改动清单

仓库:xiaozhi-esp32-server
物理边界:只动 xiaozhi-server (Python)。manager-api 不改表。

6.1 框架

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 验收失败。

6.2 复读

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

6.3 翻译 MCP

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。

6.4 配置示例(S11)

"english-translate": {
  "command": "python",
  "args": ["mcp_servers/translate/server.py"]
}

工作目录为 xiaozhi-server。依赖:现有 mcp Python 包(Server 已用)。


7. 协议与消息

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 次、不关机。


8. 数据库 / API

项 Phase 1
MySQL 新表 无
/task/instance 无
chat_history 翻译可继续按现网上报文本;不强制新 audio 字段
Redis 不存 App 状态(连接内存即可;断线即回 default)

Xiaozhi WS 断线重连 = 新 session_id = 默认 chat。
例外: 设备本地 pending_task(通道②)在闹钟唤醒后仍然执行,不依赖旧 session_id。不恢复「半截被用户打断的复读」。


9. 实施顺序(建议 PR 切片)

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 上先测框架。


10. 测试计划

10.1 chat default(conversation)

  • [ ] 首次配网成功后自动进入 chat.conversation,且小智 先说话
  • [ ] 关机再开(无其它计划)仍进入 chat.conversation,不是纯 Idle
  • [ ] 用户说话后的打断、TTS、原插件与现网一致
  • [ ] 「我们来聊天吧 / 聊会天吧」切回 conversation
  • [ ] 从不调用其它通道时,不会自己跳到复读/翻译

10.2 框架

  • [ ] switch_app("english_translation") 后工具列表不含 weather
  • [ ] switch_app("chat") 后工具列表恢复
  • [ ] 非法 app_id 被拒绝并留在当前 App
  • [ ] MQTT START_APP_TASK 正在翻译时能中断并改去复读(通道① > ③/chat)
  • [ ] 复读由 MQTT 拉起时,离线「切换模式」不能把它切走
  • [ ] 软关机 + SET_ALARM + pending 复读:闹钟唤醒后直接播放,不先进入闲聊
  • [ ] 用户唤醒词仍回到 chat(与闹钟唤醒区分)
  • [ ] 「切换模式」触发 voice_entry(不再只打日志)

10.3 翻译

  • [ ] 中文句 → 英文 TTS
  • [ ] 英文句 → 中文 TTS
  • [ ] 不回答「讲个笑话」类闲聊(应翻译该句或提示说要译的内容,由 translate prompt 定一条并写进测试)
  • [ ] 「退出翻译」回到 chat,笑话类恢复可用

10.4 复读(Sening + 卡上已有 mp3)

  • [ ] get_list 有文件
  • [ ] N=2 完整听完两遍
  • [ ] power_off_after=false 结束后可唤醒进 chat
  • [ ] power_off_after=true 结束后进现有软关机(唤醒词关闭,与今日 self.power_off 一致)
  • [ ] 播放中 stop 或唤醒:剩余遍数取消、**不**关机
  • [ ] 复读中若有 Chat TTS,MP3 pause/resume 行为不回归

10.5 负例

  • [ ] 无 self.mp3.play 的板调用 start_repeater → 语音错误,Chat 仍可用
  • [ ] 不出现 play_music 播课程

11. 验收标准(Phase 1 Done)

  1. 框架: 第一个 App 的 default Task 是 chat.conversation;配网后与默认开机都进入它;同时只有一个 active。
  2. 主动聊天: 进入 conversation 后小智先发起,不是完全被动等问。用户应答后的对话链路与现网一致。
  3. 三通道: 只有小程序即时、计划、离线切换才会离开 chat;闹钟非 chat pending 才跳过闲聊。
  4. 复读 / 翻译: 见原验收。
  5. 边界: 无 Java 新表、无 Xiaozhi type=task、无固件英语业务类、无音频热路径重写。

12. 风险与技术债(记入 Phase 2,本阶段接受)

债 说明
复读仅 Sening 其它板需后续把 play_audio 提为核心 Skill
无 play.completed 协议 Server 不知道播到第几遍;状态以设备为准
App 状态不落库 WS 断线即回 chat;**pending_task 必须在设备本地**,否则闹钟唤醒丢失
翻译 MCP 再调一次 LLM 比「改系统提示硬译」多一跳;换的是工具隔离与「只做翻译」
双控制面 小程序/闹钟走 snlp;对话/技能走 Xiaozhi WS+MCP。不在 Phase 1 合并
离线命令词很少 现仅「切换模式」;菜单口令与游戏 Task 后续加 Assets/命令表
小程序未做 通道①②用 MQTT/闹钟联调;UI 后置

13. 编码约束(给实施 Agent)

  1. 先读再改:application.cc JSON/MCP、sening_xiaozhi_s3.cc mp3/power_off、connection.py、receiveAudioHandle.py、unified_tool_handler.py、server_mcp/mcp_manager.py。
  2. 每个 PR 先跑 Chat 回归再合业务。
  3. 禁止为 Demo 新增 EnglishRepeatManager / TranslationApp 固件类。
  4. 禁止修改 hello/listen/tts/stt 语义。
  5. repeat 默认 1,保证旧 MCP 调用者兼容。
  6. 进入通道必须带 source 与优先级;禁止三个入口各写一套 stop/play。
  7. 普通开机 / 配网成功禁止做成「只 Idle 等用户」;默认 pending 是 chat.conversation 并允许先 TTS。
  8. 仅当闹钟 pending 为 非 chat 计划 Task 时,禁止先走闲聊。

实施从 P1-A 开始。未完成 A(含进入通道挂钩与默认 conversation)之前不要改播放器循环。