File:
phase-3-plan.md
Date: 2026-08-31
状态:已实施。 对照事实见phase-3-review.md。下一阶段phase-4-plan.md。
约束文档:Xiaozhi Device Capability Specification v1.0.md
代码事实(开工时):phase-2-review.md(Current Architecture V3)
路线图:roadmap.md
验收:phase-3-test.md
与原文 V1.1「Phase 2 = type=task + 表 + 评分」冲突时,以 Spec + 本文为准:本阶段只有表和查询,没有新 Device 能力,没有评分。
本阶段 不是 再做一个 App,也 不是 做倒背/口算/接龙,也 不是 改固件。
本阶段把 Phase 2 已经写进 data/task_sessions/*.json 的字段,落到 manager-api,让一次跟读/复读可以按 id 查到。
给 Coding Agent:先读 Spec §0、§4–5、§17、§20;再读 phase-2-review.md §4;再读 session.py、runtime.py、manage_api_client.py、AgentChatHistoryController.java、ShiroConfig.java、现有 Liquibase 风格。
| Spec ID | 能力 | Phase 3 做什么 | 不做什么 |
|---|---|---|---|
| — | Task Runtime 中层 | Session → task_instance / task_step / task_event |
不把 Instance 权威放到设备 |
| D1 / D6 / D16 | 已落地 | 只入库 events / turns | 不改完成事件运输、不改跟读步进 |
| D8 v1 | 音频归档 | 尽量把现有 report 的 audio_id 写到 step |
不做 v1.1 本地文件上传;不改 Opus |
| D13 | SD 布局 | 沿用 Phase 2 /{app}/{task}/lessons |
不改 mkdir MCP/MQTT;不做 resource_id |
| D11 / D14 | wait / TASK_PAUSE | 不做 | — |
| S4 / S8 | 评估 / branch | 禁止 | 表上可以留空 score 列,但本阶段不写值、不跑比对 |
| §22 | 新 App 零新传感器 | 遵守 | 不新增 WS type、不为 Instance 加 Device 命令 |
运输可变、语义不变:Instance 走 HTTP(已有 manage_api 通道)。禁止为了 instance_id 新增 type=task。设备 不必 缓存 instance_id。
audio.completed / listen_timeout)。task_session_id(uuid),不要再用 WS session_id 当长期主键。ai_agent_chat_history 冒充正式模型;chat report 管道保留。Phase 2 字段已稳定。没有 Instance,Phase 4 分数无处挂,Phase 5 调度无法对账。现在建表不会空。
能力:
GET /task/instance/{id} 的 steps 条数 = 有效跟读句数(含最后一轮),prompt_text / asr_text 与 data/task_sessions/{id}.json 一致。end_reason=play_completed,events 含 audio.completed。cancelled + 对应 end_reason(user_exit / disconnected),已有 turns 不丢。GET 按 macAddress 可列出最近 instance(最少 20 条)。ai_agent_chat_history。冻结纪律:
app_task.cc / MCP / MQTT)。Phase 2 已烧录的 bin 必须能对 Phase 3 Server 工作。type、不新增 MQTT action、不新增 MCP 工具。repeat、不改 SD 路径约定、不把 apps.json 下发到 SD。hello/listen/tts/stt 与 Opus。type=task / mode.set / 设备缓存 instance_idwait Skill、TASK_PAUSE、indicate、长按退出Device(Phase 2 bin,不动)
[skill_event] / Opus listen / MCP play
↓
xiaozhi-server App Runtime
TaskSession.save() → data/task_sessions/{id}.json (保留)
↘ HTTP upsert(server secret)
manager-api
ai_task_instance
ai_task_step
ai_task_event
(可选)step.audio_id → ai_agent_chat_audio.id
查询:oauth2 GET(家长/控制台以后用;本阶段用 token 或先 server 内网测)
task_session_id 在 begin_session 已经生成。Phase 3 不要再发一个 id 给设备。
跟读下一句仍然是 Spec D16 Server loop。表只是投影,不是新的 Step 引擎。
Liquibase:manager-api/src/main/resources/db/changelog/202608311800.sql
必须登记 db.changelog-master.yaml。**禁止改已经执行过的 changeset**(含 202504151206.sql 的 URL 占位)。
命名与现网一致:utf8mb4、DATETIME(3)、InnoDB。
不要 DROP TABLE IF EXISTS 业务表。
ai_task_instance| 列 | 类型 | 说明 |
|---|---|---|
id |
VARCHAR(32) PK | = task_session_id |
mac_address |
VARCHAR(50) | 设备 |
agent_id |
VARCHAR(32) NULL | 若连接能取到则填;没有就空 |
ws_session_id |
VARCHAR(64) | Phase 2 ws_session_id |
app_id |
VARCHAR(32) | |
task_id |
VARCHAR(32) | |
source |
VARCHAR(24) | voice / mqtt_immediate / schedule / … |
interaction_mode |
VARCHAR(32) | 存代码名 SHADOWING 即可 |
status |
VARCHAR(16) | running / completed / cancelled |
end_reason |
VARCHAR(32) | 与 JSON 相同 |
started_at |
BIGINT | epoch ms |
ended_at |
BIGINT NULL | |
created_at / updated_at |
DATETIME(3) |
索引:(mac_address, started_at)、(app_id, task_id, started_at)、(status)。
ai_task_step对应 turns[]。一条有效跟读一轮一行。
| 列 | 类型 | 说明 |
|---|---|---|
id |
BIGINT PK AI | |
instance_id |
VARCHAR(32) | FK 逻辑关联即可(InnoDB FK 可选;建议有) |
step_index |
INT | turns[].index |
prompt_text |
TEXT | 跟读可能是 SD 路径 |
asr_text |
TEXT | |
echo_rejected |
TINYINT(1) | 默认 0 |
audio_id |
VARCHAR(32) NULL | 指向 ai_agent_chat_audio.id |
recorded_at |
BIGINT |
唯一:(instance_id, step_index)。
复读没有 turns 是合法的(只有 events)。
ai_task_event对应 events[]。
| 列 | 类型 | 说明 |
|---|---|---|
id |
BIGINT PK AI | |
instance_id |
VARCHAR(32) | |
name |
VARCHAR(64) | audio.completed / audio.stopped / listen_timeout / echo_rejected |
app_id / task_id |
VARCHAR(32) | 可空则继承 instance |
at_ms |
BIGINT | JSON at |
禁止把 score、parser、difficulty 做成 NOT NULL。若预留 score 列,必须可空且本阶段恒为 NULL。
鉴权对齐现网:
| 路径 | 过滤器 | 调用方 |
|---|---|---|
PUT /task/instance/{id} |
server(ServerSecretFilter,与 chat report 相同) |
xiaozhi-server |
GET /task/instance/{id} |
oauth2(控制台用户) |
测试 / 以后家长 |
GET /task/instance |
oauth2 |
列表;query: macAddress 必填,可选 appId、limit |
不要把 PUT 设成 anon。
不要新建第二套密钥。
与 TaskSession.to_dict() 对齐,camelCase 或 snake 二选一,**Python / Java 必须同一套**。推荐 snake_case 与 JSON 文件一致,减少映射 bug:
{
"task_session_id": "uuid",
"ws_session_id": "",
"device_id": "mac",
"agent_id": "",
"app_id": "english_learning",
"task_id": "shadowing",
"source": "voice",
"interaction_mode": "SHADOWING",
"started_at": 0,
"ended_at": 0,
"status": "running",
"end_reason": "",
"turns": [
{
"index": 0,
"prompt_text": "/english_learning/shadowing/lessons/unit01/01.mp3",
"asr_text": "...",
"echo_rejected": false,
"recorded_at": 0,
"audio_id": null
}
],
"events": [
{ "name": "audio.completed", "app_id": "", "task_id": "", "at": 0 }
]
}
语义:按 id 整单覆盖 steps/events(先删后插或按 unique key upsert)。调用方每次 session.save() 都可 PUT 全文。不要做复杂 patch 协议。
幂等:同一 id 重复 PUT 成功,最终与最后一次 body 一致。
返回上表同一 JSON,外加 mac_address 若与 device_id 需要并存则只保留一个字段并在文档写死。
列表:{ "list": [ { id, app_id, task_id, status, end_reason, started_at } ], "total": n }
不要在列表里返回全部 turns。
audio_id(尽力而为)现有 POST /agent/chat-history/report 返回 Result<Boolean>,Python 拿不到 id。
本阶段允许两条路径,**选工作量小的一条,不要两条都做**:
A(推荐)。 report DTO 增加可选 taskInstanceId + stepIndex;Java 写入 chat_audio 后更新对应 ai_task_step.audio_id。跟读 enqueue_asr_report 带上这两个可选字段。
B。 本阶段 step.audio_id 恒空;GET 仍能查文本。在 PR 说明留给 Phase 4。验收 #1 不强制音频。
禁止为了挂音频再上传一份 WAV。
| ID | 任务 | 说明 |
|---|---|---|
| J1 | Liquibase SQL + master yaml | 只增新文件 |
| J2 | Entity / Dao / Service | 模块建议 xiaozhi.modules.task,不要塞进 agent chat 包当「一种聊天」 |
| J3 | TaskInstanceController |
PUT upsert、GET one、GET list |
| J4 | ShiroConfig |
PUT /task/instance/** → server;GET 走默认 oauth2 |
| J5 | 校验 | app_id/task_id 长度与字符与固件约定一致;非法 400;缺 id 400 |
| J6 | 可选 A | report DTO 扩展 + 回写 audio_id |
manager-web:**零提交**(除非 Swagger 自动生成,不要手改 Vue)。
| ID | 任务 | 文件 | 说明 |
|---|---|---|---|
| S1 | HTTP 客户端 | config/manage_api_client.py |
upsert_task_instance(payload);无 client 或异常 → log,return None |
| S2 | 投影 | core/app_runtime/session.py |
TaskSession.save() 成功写盘后 异步/同步均可 调用 upsert;失败不影响 save |
| S3 | 连接字段 | 若 conn.agent_id 已有则写入 payload |
没有就空 |
| S4 | 可选 A | enqueue_asr_report / report() |
附带 instance/step |
| S5 | pytest | tests/test_app_runtime.py |
to_dict 字段;upsert 被调用;upsert 抛错时 save 仍成功(mock) |
不要在 runtime.py 再复制一套 schema。upsert 只从 TaskSession.to_dict() 出发。
跟读/复读/skill_event 的 begin/end/add_turn 不要改行为,只借现有 save() 挂钩。
空。 禁止改 xiaozhi-esp32-2.2.6。
旧固件无 duration_ms、无 ensure_app 时:Server 仍按 Phase 2 兼容(时长缺省、建目录失败忽略)。Phase 3 不得依赖新固件字段。
详见 phase-3-test.md。本 PR 最低限度:
phase-2-voice-test.md 第 0、2、5 节(跟读 + 复读结束事件)不得回退。不要本阶段验收倒背/口算/接龙。
P3-A Liquibase + Entity + PUT upsert + GET
用 curl + server secret 写入一条手工 JSON,能读回
P3-B Python manage_api_client + TaskSession.save() 挂钩
跟读一次,库里出现行;关掉 manager-api 再跟读,设备不停
P3-C (可选)report 回写 audio_id
做不完就留空列,不要卡住 A/B
P3-D pytest + phase-3-test.md 勾选
未完成 A 不要改 Python 热路径。未完成 B 不得宣称 Phase 3 Done。未完成 D 不得开 Phase 4 评分。
ai_agent_chat_history 当权威。save() / report 旁路加上报。| 风险 | 缓解 |
|---|---|
| 每次 save 全量删插 steps 并发丢行 | 单连接单 session;不要并行 save 同一 id |
| manage_api 超时拖死语音 | upsert 必须 catch;必要时 create_task 后台发,但要保证 end 时最终一致(可在 end_session 再 PUT 一次) |
| 与 chat_history 双写让产品以为「聊天记录就是跟读」 | API 路径分开;文档写清 Agent ≠ App |
| 把 Phase 3 做成评分 | 验收表不含 score;列可空 |
| 为 instance_id 改 detect 文本 | 禁止 |
| 新 App 又去改 C++ 建目录 | 已有 ensure_app;只改 apps.json |
开始跟读
begin_session → 写 JSON + PUT running(turns=[])
每句有效 ASR
add_turn → save → PUT 全文
enqueue_asr_report(可选带 instance/step)
audio.completed(跟读)
add_event → save → PUT;开听窗(逻辑不变)
结束
finish(reason) → save → PUT completed/cancelled
GET /task/instance/{id} 可查
复读:可能零 turn、若干 audio.* event。同样 upsert。
DeviceState / InteractionMode:与 Phase 2 相同,禁止加边。
Instance:
(无)──begin──► running ──finish(finished|play_completed)──► completed
└──finish(user_exit|disconnected|play_stopped|preempted)──► cancelled
设备执行器状态不是本阶段工作。
Coding
→ 勾选 phase-3-test.md
→ Completion Review(对照本文 §1.3) ✅ phase-3-review.md
→ Architecture V4(表 + HTTP)
→ Roadmap Review
→ phase-4-plan.md(时钟步进 + 后评估;无 JUDGE、无当场再播)
禁止本阶段「顺便」评分、倒背、Scheduler、SD 搬家、type=task、manager-web 跟读页。