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 |
口令底表(本阶段不重烧) |
xiaozhi-esp32-2.2.6)无业务提交。 跟读/复读仍用 Phase 2 bin:[skill_event]、一句一 MP3、ensure_app mkdir。
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() 上。
| 文件 | 角色 |
|---|---|
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 |
零提交。 查询靠 curl / Swagger / server secret。
| 模块 | 位置 | 职责 |
|---|---|---|
| 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 跟读页。
| 类型 | 名称 | 说明 |
|---|---|---|
| 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。
| 表 | 主键 | 内容 |
|---|---|---|
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 竞态)。
相对 V3:**无。** 设备不持有 instance_id,不读库。
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 跑。
与 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。
| 能力 | 状态 |
|---|---|
| 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。
| 项 | 现状 |
|---|---|
pytest tests/test_app_runtime.py |
有(15 条量级:口令、session、skill_event、SD 路径、Phase 3 schema、upsert 失败隔离) |
| Java 单测 / Maven 在本仓库对话中的编译记录 | 无 |
phase-3-test.md 语音 + GET 对账 |
有书面清单;仓库内无跑测勾选记录 |
| 固件测试 | 不适用(零 diff) |
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)。
phase-3-plan.md)score 列。 计划允许可空且恒 NULL。实施选择不建,避免空列被当成「已经有评估」。Phase 4 用 新 changeset 加列。audio_id。 计划写先删后插;实施删插但空 audio 保留旧值,减少与 report 竞态。这些应视为 Phase 3 的真实架构(Current Architecture V4)。
score 列(有意推迟)phase-3-test.md 人工勾选(语音 + GET)prompt_text 在 SD 跟读下是文件路径(如 /english_learning/shadowing/lessons/unit01/01.mp3),不是英语原文。Phase 4 禁止拿路径做文本比对。必须另有 expected_text。_is_clip_path)。不要把「评分用 expected_text」和「0.82 回声拒绝」绑成同一条:听窗内用户跟对了也会像范读。评分阈值必须独立,且只在已经过听窗门闩的有效轮上算。uk_task_step_instance_index 唯一。 同一听窗多次 ASR 必须 按 index 更新同一 turn,不能追加。save(单连接单 session,现网如此)。| 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 | 满足 |
V1.1 把评分和表绑在同一 Phase。Spec 已拆开。本阶段 没有 评分,正确。
场景切换词、租户:**本阶段未做、也不该做。** 词表仍在 runtime.py;tenant_id 未进表、未进固件。见 §5 纪律。
| 抽象 | 当前是否存在 | 实现位置 | 完成程度 | 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 | 否 | — | — | 禁止 |
与 V3 相同。固件冻结面不因落库而移动。
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
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。
仍为 V3:
/{app_id}/{task_id}/lessons/{lesson_id}/file.mp3
| ID | 债务 | 放哪 |
|---|---|---|
| P0-1 | 跟读 prompt_text = SD 路径,不能当评分原文 |
Phase 4 用同 stem 的 01.txt / transcripts 作 expected_text;评估不挡播放 |
| 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 | 双技能表 | 保持 |
| 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 跟读页 |
现在就遵守、不必编码:
tenant_id功能 Done(带条件):
task_session_id 可在 manager-api 按 id / mac 查询ai_task_instance工程未 Done:
phase-3-test.md 依赖人工 GET / 语音勾选因此: 可以宣称 Task Runtime 可归档且可查询。
不可以宣称已评估、已打卡产品化、已多租户。
不可以为分数新增 JUDGE 命令或改 C++。
下一阶段只做一件事:把跟读热路径收成 MP3 序列 + 定时录音,分析全部后置为定量 metrics。见 phase-4-plan.md。