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

Phase 3 Completion Review

Date: 2026-08-31
Basis: current code(不是 phase-3-plan.md 的原文愿望)
Compared against: phase-3-plan.md · phase-3-test.md · phase-2-review.md · roadmap.md · Xiaozhi Device Capability Specification v1.0.md
Repositories: xiaozhi-esp32-2.2.6(Client,本阶段零 diff)· xiaozhi-esp32-server(Python + manager-api)

本文件只记录事实、差异、债务与对 Spec / Roadmap 的修正。下一阶段施工见 phase-4-plan.md,验收见 phase-4-test.md。

相关文档索引:

文档 角色
Xiaozhi Device Capability Specification v1.0.md Device 语义冻结
roadmap.md 阶段切分
phase-2-review.md Current Architecture V3(文件 Session)
phase-3-plan.md 本阶段原文施工单(Instance 落库)
phase-3-test.md GET 对账 + 语音回归 + 失败隔离
phase-2-voice-test.md 口令底表(本阶段不重烧)

1. 当前代码盘点(以仓库为准)

1.1 Phase 3 实际改动的文件

Client(xiaozhi-esp32-2.2.6)

无业务提交。 跟读/复读仍用 Phase 2 bin:[skill_event]、一句一 MP3、ensure_app mkdir。

Server Python(xiaozhi-server)

文件 角色
core/app_runtime/session.py agent_id;save() 写盘后 _schedule_upsert(create_task,无 running loop 则跳过)
config/manage_api_client.py upsert_task_instance:单次 _async_request PUT;report() 可选 taskInstanceId/stepIndex
core/handle/reportHandle.py enqueue_asr_report 六元组透传 instance/step
core/connection.py _process_report 默认参数,转发到 manage_api
core/app_runtime/runtime.py 跟读有效轮 enqueue_asr_report(..., task_session_id, step_index)
tests/test_app_runtime.py to_dict 含 Phase 3 字段;upsert 调度抛错时 save 仍成功

跟读步进、听窗、begin_session/add_turn 语义 未改。upsert 只挂在已有 save() 上。

manager-api(Java)

文件 角色
db/changelog/202608311800.sql 新表;已登记 db.changelog-master.yaml。**未改**已执行 changeset
modules/task/** Entity / Dao / DTO / TaskInstanceService / TaskInstanceController
security/config/ShiroConfig.java /task/instance 与 /task/instance/** → server
agent/dto/AgentChatHistoryReportDTO.java 可选 taskInstanceId / stepIndex
agent/.../AgentChatHistoryBizServiceImpl.java report 成功后 attachTaskStepAudio

manager-web / manager-mobile

零提交。 查询靠 curl / Swagger / server secret。

1.2 实际增加的模块

模块 位置 职责
Task Instance 投影 manager-api xiaozhi.modules.task Session JSON → 三表;权威仍是 Server Runtime 内存 + 文件,库是查询副本
HTTP upsert PUT /xiaozhi/task/instance/{id} 全文覆盖 turns/events
HTTP GET GET .../{id}、GET ...?macAddress= 详情含 turns;列表不含正文
audio_id 挂钩 计划 §4.3 路径 A chat report 回写 ai_task_step.audio_id

没有:Assessment、type=task、设备缓存 instance_id、新 WS/MQTT/MCP、租户列、manager-web 跟读页。

1.3 实际增加的 API / 协议

类型 名称 说明
REST PUT /task/instance/{id} server secret;snake_case 与 JSON 文件对齐
REST GET /task/instance/{id} 计划写 oauth2,实施为 server(Shiro 按路径不能按方法拆)
REST GET /task/instance?macAddress=&appId=&limit= 同上
REST 现有 POST /agent/chat-history/report 增可选字段,无新路径
WS / MQTT / MCP 无 设备仍不知 instance_id

响应包在现网 Result<T> 里(code + data),不是裸 DTO。context-path:/xiaozhi。

1.4 数据库

表 主键 内容
ai_task_instance id = task_session_id(32 hex) mac / agent_id / app_id / task_id / status / end_reason / 时间戳
ai_task_step BIGINT + UNIQUE (instance_id, step_index) prompt / asr / echo / audio_id / recorded_at
ai_task_event BIGINT name / app / task / at_ms;FK CASCADE

没有 score / expected_text / tenant_id 列(计划允许预留 score 且恒 NULL;实施选择 不建该列,留给 Phase 4 新 changeset)。

覆盖 steps 时:新 audio_id 为空则保留旧行(避免与 report 竞态)。

1.5 Client 架构变化

相对 V3:**无。** 设备不持有 instance_id,不读库。

1.6 Server 架构变化

TaskSession.save()
  ├── data/task_sessions/{id}.json     (保留,失败只 log)
  └── create_task → PUT /task/instance/{id}
        失败 print/log,不抛回热路径
        无 ManageApiClient / 无 running loop → 跳过

跟读有效 ASR
  add_turn → save → PUT
  enqueue_asr_report(instance, step) → 现有 WAV BLOB → Java attach audio_id

权威状态:**当前连接内存 + JSON 文件**;manager-api 是最终一致投影。manage_api 挂了,跟读仍按 Phase 2 跑。

1.7 Client ↔ Server 实际通信流程

与 Phase 2 相同。新增的只是 Server→manager-api 的 HTTP,设备看不见。

开始跟读
  begin_session → JSON + PUT running(turns=[])

每句有效 ASR
  add_turn → save → PUT 全文
  report(可选 audio_id 回写)

audio.completed
  add_event → save → PUT;开听窗(逻辑不变)

结束
  finish → save → PUT completed/cancelled
  GET /xiaozhi/task/instance/{id} 可查(需 server secret)

复读:零 turn、若干 audio.* event,同样 upsert。Chat 不 begin_session。

1.8 Task Runtime 当前实现状态

能力 状态
TaskSession JSON 有(Phase 2)
TaskInstance 可查询 有(HTTP GET;双写)
Step 行 = 跟读一轮 有(投影,不是 Step 引擎)
Event 入库 有
D8 audio 挂 step 尽力:路径 A 已接;取决于 report 配置与竞态
Assessment / score 无
expected_text(可评分原文) 无;prompt_text 跟读时是 SD 路径
Scheduler / 资源商店 / 租户 无

结论:这是 可查询的薄 Runtime(JSON + 三表),仍不是 V1.1 的「Phase 2 = 评分」。评分已正确留到 Phase 4。

1.9 测试情况

项 现状
pytest tests/test_app_runtime.py 有(15 条量级:口令、session、skill_event、SD 路径、Phase 3 schema、upsert 失败隔离)
Java 单测 / Maven 在本仓库对话中的编译记录 无
phase-3-test.md 语音 + GET 对账 有书面清单;仓库内无跑测勾选记录
固件测试 不适用(零 diff)

2. Phase 3 Implementation Review(计划 vs 代码)

2.1 对照 phase-3-plan.md §1.3 验收

# 计划验收 代码事实 结论
1 跟读 GET steps = 有效句数,prompt/asr 与 JSON 一致 upsert 全文覆盖;prompt 为 SD 路径 代码满足(需按 test.md 人工 GET)
2 复读 end_reason=play_completed + audio.completed 未改结束路径,只多 PUT 代码满足
3 退出/断线 cancelled + 已有 turns 不丢 finish + save;断线 end_session(disconnected) 代码满足
4 GET 按 mac 列出最近 ≥20 limit 默认 20、上限 100 满足
5 JSON 双写;Chat 仍走 chat_history chat 不 begin_session 满足
6 manage_api 挂了语音仍跑 upsert catch + 后台 task 代码满足
7 固件零提交 无 Client 业务 diff 满足
8 无新 WS/MQTT/MCP 无 满足
9 不评分、不改 repeat/SD、不下发 apps.json 无 score 列、无评分调用 满足
10 不改 hello/listen/tts/stt 与 Opus 未改 满足

计划 §4 GET 走 oauth2:**未按原文做**。见 2.2。

计划 §4.3 选 A 或 B:实施了 A(report 回写 audio_id)。

2.2 哪些设计被修改(相对 phase-3-plan.md)

  1. GET 鉴权 = server,不是 oauth2。 Shiro 按路径挂过滤器,不能按 HTTP 方法拆。与 chat report 同一密钥即可测通。家长/控制台 oauth2 留给 Phase 7,不要为本阶段再做第二套过滤器。
  2. 未预留 score 列。 计划允许可空且恒 NULL。实施选择不建,避免空列被当成「已经有评估」。Phase 4 用 新 changeset 加列。
  3. PUT 单次请求,不做 6×10s 重试。 避免刷爆 manage_api;失败只 log。
  4. 覆盖 steps 合并旧 audio_id。 计划写先删后插;实施删插但空 audio 保留旧值,减少与 report 竞态。

这些应视为 Phase 3 的真实架构(Current Architecture V4)。

2.3 哪些设计没有实现(原文有、代码无或未验证)

  • GET oauth2(有意改为 server)
  • 表上的空 score 列(有意推迟)
  • 仓库内 Java 编译 / 真实 HTTP 联调记录
  • phase-3-test.md 人工勾选(语音 + GET)
  • manager-web 页面(计划本就 Out of Scope)

2.4 实施中发现的问题(对 Phase 4 有约束)

  1. prompt_text 在 SD 跟读下是文件路径(如 /english_learning/shadowing/lessons/unit01/01.mp3),不是英语原文。Phase 4 禁止拿路径做文本比对。必须另有 expected_text。
  2. 回声检测对路径是关掉的(_is_clip_path)。不要把「评分用 expected_text」和「0.82 回声拒绝」绑成同一条:听窗内用户跟对了也会像范读。评分阈值必须独立,且只在已经过听窗门闩的有效轮上算。
  3. uk_task_step_instance_index 唯一。 同一听窗多次 ASR 必须 按 index 更新同一 turn,不能追加。
  4. 无 running asyncio loop 时 upsert 静默跳过(例如某些同步测试路径);热路径有 loop,正常。
  5. 全量删插同一 id 不要并行 save(单连接单 session,现网如此)。

2.5 与 Spec v1.0 的对照

Spec ID Phase 3 计划 代码 宣称
中层 Task Runtime Session → 表 有 可查询副本已落地;权威仍在 Server 进程
D1 / D6 / D16 只入库,不改运输/步进 遵守 保持
D8 v1 尽量挂 audio_id 路径 A 关联管道有;非每条必有 id
D13 沿用 /{app}/{task}/lessons 未搬目录 保持
D11 / D14 不做 无 —
S4 / S8 禁止 无评分、无 JUDGE 正确
§22 无新传感器 固件零 diff 满足

2.6 与原文 V1.1 / 租户讨论的差异

V1.1 把评分和表绑在同一 Phase。Spec 已拆开。本阶段 没有 评分,正确。

场景切换词、租户:**本阶段未做、也不该做。** 词表仍在 runtime.py;tenant_id 未进表、未进固件。见 §5 纪律。


3. 与 Master Plan 核心抽象对照(更新)

抽象 当前是否存在 实现位置 完成程度 Phase 4 是否动
App 是 apps.json 连接级;无订阅表 可加 assessment 块;不升级商店
Task 是(薄) apps.json tasks 一种动作 跟读加评估策略 JSON
Step 是(行) ai_task_step / turns[] 投影 写入 score / expected_text
Skill 是 MCP + 固件查找表 双表仍在 不合并;不新命令
InteractionMode 是 SHADOWING = Spec FOLLOW_READ 不重命名
TaskInstance 是 三表 + JSON 可 GET 不改主键;不把分数改挂 chat_history
Event 是 表 + JSON 无第四套总线 可加 assessment.* 事件名(仍走现有 events[])
Audio 聊天附件 + 可选挂 step report 尽力 不新上传通道
Assessment 否 — 0% 后评估定量 metrics;不指挥步进
Schedule / Tenant 否 — — 禁止

4. Current Architecture V4(基于代码)

4.1 Client V4

与 V3 相同。固件冻结面不因落库而移动。

4.2 Server V4

xiaozhi-server
├── App Runtime
│     apps.json
│     SHADOWING:SD 句文件 + 听窗 + D16(无对错)
│     TaskSession → JSON + 异步 PUT
├── detect:[skill_event] → [app_task] → [voice_entry]
└── report:chat-history;可选回写 step.audio_id

manager-api
├── PUT/GET /task/instance  (server secret)
├── ai_task_instance / step / event
└── 不计算分数、不执行 Skill

4.3 通信 V4

Device  ──WS──►  xiaozhi-server Runtime
                    ├── JSON 文件
                    └── HTTP PUT ──► manager-api Instance
                    └── HTTP report ──► chat_audio ──► step.audio_id

查询:server secret GET /xiaozhi/task/instance/{id}
设备从不调用上述 HTTP。

4.4 SD 布局

仍为 V3:

/{app_id}/{task_id}/lessons/{lesson_id}/file.mp3

5. 技术债务与架构纪律(Phase 3 之后)

P0 — 不解决就无法安全做 Phase 4

ID 债务 放哪
P0-1 跟读 prompt_text = SD 路径,不能当评分原文 Phase 4 用同 stem 的 01.txt / transcripts 作 expected_text;评估不挡播放

P1 — 近期

ID 债务 放哪
P1-1 GET 仅 server secret Phase 7 家长 UI 再拆 oauth2
P1-2 人工未勾选 phase-3-test.md 开 Phase 4 编码前建议先对一次 GET;不挡写计划
P1-3 无 Java 编译记录 加列 changeset 时用现网风格,禁止改 202608311800.sql
P1-4 词表硬编码在 runtime.py 保持到 Phase 6 catalog;**禁止**把「开始跟读」写进固件
P1-5 双技能表 保持

P2 — 以后再说

ID 债务
P2-1 D11 wait / D14 PAUSE
P2-2 租户 / 机构教材隔离(最早缝在 Phase 6,产品化 Phase 7)
P2-3 音素/发音/情绪分析
P2-4 倒背 / 口算 / 接龙(Phase 4b)
P2-5 type=task、对象存储、manager-web 跟读页

现在就遵守、不必编码:

  • 固件不再增加业务 App 口令;系统级仅 Chat + 离线复读机 + 「切换模式」总入口
  • 设备永不持久化 tenant_id
  • 打卡/分析只消费 Instance + audio,不加新 Device 能力

6. 结论:Phase 3 是否 Done

功能 Done(带条件):

  • task_session_id 可在 manager-api 按 id / mac 查询
  • JSON 双写保留;PUT 失败不拖死语音(pytest 覆盖调度失败)
  • Agent ≠ App:Chat 不进 ai_task_instance
  • 固件零 diff;无新运输种类;无评分
  • audio_id 挂钩(路径 A)已接

工程未 Done:

  • phase-3-test.md 依赖人工 GET / 语音勾选
  • Java 侧无仓库内编译证明
  • 跟读原文与路径未分离(P0-1,交给 Phase 4)

因此: 可以宣称 Task Runtime 可归档且可查询。
不可以宣称已评估、已打卡产品化、已多租户。
不可以为分数新增 JUDGE 命令或改 C++。

下一阶段只做一件事:把跟读热路径收成 MP3 序列 + 定时录音,分析全部后置为定量 metrics。见 phase-4-plan.md。