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

Phase 7a Coding Plan — 家长:任务下发(先)+ 执行记录(后)

File: phase-7a-plan.md
Date: 2026-09-18(修订:场景参数 / 泛听命名 / 临时 vs 计划 / 闹钟只写下一次)
产品:① 创建并下发任务 ② 看历史;**计量评估不做**。
约束:Capability Spec v1.0;锁定 chat / 翻译 / 中英热路径与欢迎词时序。
给 Coding Agent:先读本文 §0–§5,再读 system.c program_alarm1(DS3231 本来就只编程下一次)、app_task.cc 唯一 pending、runtime.py start_app_task。

实施顺序:**先 Dispatch(A/B/C),再 History(D)。禁止先做 metrics UI。**


0. 家长怎么创建任务

先选 场景,再填该场景的参数,最后选 怎么跑:

怎么跑 含义 优先级
临时任务 创建后 立即下发并执行 最高(source=mqtt_immediate)
计划任务 周期到点执行(每天某时刻、每小时整点、工作日…) 其次(source=schedule)

同一条任务定义(场景+参数)可以「现在就练」也可以「存成每天 7:30」。不要做成两套互不相干的对象:云端一行任务,run_mode=immediate|scheduled。

0.1 场景与参数(家长 UI)

内部 app_id/task_id/Mode 不要改名(避免 SD 路径与口令断裂)。对外文案如下。

家长场景 内部 家长要填的参数
泛听 english_learning / repeater / PLAY_AUDIO 课文、遍数、结束后:**软关机** 或 回默认对话
跟读 english_learning / shadowing / SHADOWING 同上(课文、遍数、结束后关机或回对话)
双语对话 english_conversation / bilingual_chat / BILINGUAL 语言对:中英 / 中法 / 中日…;可选时长(如聊 10 分钟);结束后关机或保持/回默认对话
翻译 现网 translation app 可选语言对;可选时长与结束动作
对话 chat / conversation 可选时长(如每小时整点聊 10 分钟);结束后软关机或保持

课文:catalog 选 lesson_id(unit01),不要勾 58 个文件。遍数落到 playlist 每项 repeat(泛听/跟读默认 1,家长可改)。

双语语言对:写入 params.language_pair(如 zh-en / zh-fr / zh-ja)。**不改** ASR→LLM 锁定路由;只把 pair 传进已有 bilingual/翻译 prompt。现网口令仍叫「中英对话」的,当作 zh-en 别名保留。

0.2 命名:不要再对家长说「复读 / 复读机」

「复读」和「跟读」在口语里都像「跟着读」,家长分不清。对家长用 泛听 / 跟读(只听 vs 跟着读),不要用听读、不要用复读机。

对家长 含义 内部(冻结)
泛听 只听课文,不录音、不跟说 repeater / 路径仍 /english_learning/repeater/
跟读 播一句 → 听窗录音 → 下一句 shadowing

进场 TTS / 小程序 / 历史页 / 离线口令一律用 泛听。不再保留「开始复读 / 退出复读 / 复读机」。

不要新造第三种 Mode。

0.3 验收场景(Dispatch)

临时:家长点「立即开始 · 跟读 · unit01」→ 设备马上跟读(可打断当前计划任务)

计划 A:每天 07:30 跟读 unit01 ×1,结束后回对话
计划 B:每天 10:00 泛听 unit01 ×1,提示后播,结束后软关机
计划 C:每小时整点 双语(中英) 10 分钟,结束后软关机
计划 D:每天 19:00 双语(中日) ,结束后保持对话直到下一条

软关机后:设备从本地任务表挑 **下一次最近** 的计划,只把这一次写入 DS3231 Alarm1
  (例如刚播完 10:00 泛听关机 → 闹钟写成 11:00 双语,而不是再占 10 个闹钟槽)

计划数量 不是 10。每小时整点是 一条 周期任务,不是 24 条闹钟。


1. 闹钟模型(必须改掉上一版)

上一版「每个计划一个 alarm id、最多 10 条」是错的。

硬件事实: DS3231 只有 Alarm1;program_alarm1() 已经只写入下一次触发时刻。ALARM_MAX_NUM=10 是软件里的闹钟条目表,不是 RTC 能力,**不能**当家长任务上限。

正确模型:

云端权威:任务表(临时一次 / 计划多条,数量远大于 10)
    ↓ 全量同步(替换,不是逐条 SET_ALARM)
设备本地:schedule.json(完整计划列表 + 每条 params)
    ↓ 每次需要闹钟时(同步后、一次任务结束、即将软关机)
挑 next = 所有计划里墙钟上最近的一次发生
    ↓
只 program DS3231 Alarm1 = next.time
同时把 next 的 {app,task,params} 写成「下一次唤醒载荷」(一份 pending 即可)
不要 要
家长 30 条计划 → 30 个 SET_ALARM 30 条进文件;RTC 永远 1 个「下一次」
每小时整点 = 24 个 id 一条 hourly,每次关机后算下一个 :00
用满 10 槽就 400 软上限(建议云端 100、设备 JSON 同样),与闹钟芯片无关
每个闹钟 id 绑一份 playlist 计划里只存 lesson_id;真正 playlist 执行时再展开(MQTT 载荷上限 8KB)

临时任务 不写 周期表、**不写** RTC。只发 START_APP_TASK mqtt_immediate。跑完后设备再 recompute_next_alarm(),以免把「下一计划」丢掉。


2. 现网缺口

能力 现网 缺口
RTC 只编程下一次 Alarm1 对。 7a 应继续只用这一个槽
闹钟条目表 最多 10 条时刻 不要当任务库。改成本地计划文件
pending SET_ALARM 覆盖唯一 NVS 唤醒载荷 = 当前算出来的下一次计划,不是 10 份 map
已开机到点 未在 power-off 则不 StartAppTask 已开机也要执行当前这笔载荷
课文/遍数/结束 TEMP 清单;结束动作不完整 场景 params:lesson、repeat、on_complete
语言对 双语写死中英文案 params.language_pair;不改锁定路由
时长结束 无 对话/双语/翻译可 duration_minutes 到点结束
云端表 无 ai_parent_task(名称可定,禁止改旧 changeset)
历史 GET 仅 server secret 7a-D;无 metrics

3. 目标与非目标

3.1 Dispatch 必须 Done

  1. 家长选场景 → 填参数 → 选「立即执行」或「存为计划」。
  2. 立即:设备马上进入该场景;优先级高于计划。
  3. 计划:全量下发到设备;RTC 只存最近一次;软关机后本地重算下一个。
  4. 泛听/跟读:课文、遍数、结束后关机或回默认 chat;进场有提示。
  5. 双语:可选语言对;可选时长。
  6. 计划条数不被 10 限制;每小时整点是一条周期规则。
  7. 已开机到点同样执行;不可打断正在跑的临时任务。

3.2 Out of Scope

  • metrics UI
  • 课文云下载(SD 已有 unit01)
  • 倒背/口算;新 Mode;新 WS type
  • 改 hello / CHAT / 翻译 / 双语 热路径(只允许 params 进 prompt / playlist)
  • 农历;把 58 个 mp3 塞进 SYNC 包

3.3 Acceptance(Dispatch)

  1. 立即跟读:点完设备进跟读,课文对。
  2. ≥15 条计划(含一条「每小时整点聊 10 分钟后关机」)保存成功,设备 JSON 有全量;DS3231 只有最近一次。
  3. 软关机 → 到点唤醒进的是那条最近计划,不是列表里第一条。
  4. 该条结束后(或再关机)闹钟变成再下一条时刻。
  5. 临时任务执行中,计划到点:**不抢占**;临时结束后再执行或重算(见 §5.3)。
  6. 计划执行中来了临时任务:**打断计划**,跑临时;结束后重算 RTC。
  7. 泛听结束关机 vs 跟读结束回 chat,按各自 on_complete。
  8. Chat / 翻译 / 中英口令回归不坏。

4. 数据模型

4.1 云端 ai_parent_task

列 说明
id 任务 id(UUID),**不是** 闹钟 id
user_id / mac_address / agent_id 绑定校验
run_mode immediate 一次性;scheduled 周期
scene extensive / shadowing / bilingual / translation / chat (extensive = 泛听)
app_id / task_id 由 scene 映射,勿让家长填
params JSON 见下
schedule JSON 仅 scheduled;见下
enabled 计划可停用
status immediate:queued/running/done/canceled
updated_at

params:

{
  "lesson_id": "unit01",
  "repeat": 1,
  "language_pair": "zh-en",
  "duration_minutes": 10,
  "on_complete": "power_off",
  "announce": true
}

on_complete:power_off | chat(回默认对话)。未填:泛听/跟读默认 chat;带时长的对话类默认 power_off 若家长选了关机,否则保持该场景直到被抢占。

schedule:

{ "type": "daily", "time": "07:30", "weekdays": [1,2,3,4,5,6,7] }
{ "type": "hourly", "minute": 0 }
{ "type": "interval", "every_minutes": 60 }

weekdays:1=周一 … 7=周日。hourly = 每小时的第 minute 分。

immediate 行:无 schedule;下发即 START_APP_TASK,不进设备周期表(或进表但 enabled 且一次性、跑完 disable)。推荐:**周期表只含 scheduled**,immediate 不落 RTC。

软上限 100 条/设备(400 超出)。**禁止**按 10 拒绝。

4.2 设备本地周期表

文件例如 SD 或 NVS 旁路:schedule.json = 云端 scheduled 的紧凑拷贝(id、scene、app、task、params、schedule)。
禁止把展开后的 58 句 playlist 写入该文件。

pending(一份):仅「下一次 Alarm1 对应的那条任务」的 app/task/params。同步或跑完后重算覆盖。这是唯一 pending,语义变了:不是「最后一次 SET_ALARM」,而是「最近一次将发生的计划」。

ALARM_MAX_NUM=10 的时钟条目表:**不要**给家长计划用。旧 SET_ALARM 可留作普通闹钟,7a 同步路径不走它。

4.3 Catalog

GET /user/scenes              场景列表 + 每个场景的表单 schema(课文? 遍数? 语言对? 时长? 结束动作?)
GET /user/lessons?scene=      extensive / shadowing 的课文
GET /user/language-pairs      zh-en, zh-fr, zh-ja…(展示名:中英/中法/中日)

5. 执行与优先级

优先级:临时 mqtt_immediate  >  计划 schedule  >  语音/默认 chat

5.1 立即

小程序「立即开始」
  → POST task run_mode=immediate
  → MQTT START_APP_TASK {app,task,params, source:mqtt_immediate}
  → 设备 Stop 当前计划 Skill(若有)再进入场景

不修改周期表。不 program 闹钟。结束后 recompute_next_alarm()。

5.2 计划同步与 RTC

家长保存/删除/停用计划
  → 云端权威更新
  → MQTT SYNC_SCHEDULE { tasks: [ ...紧凑列表 ] }   // 全量替换
  → 设备写 schedule.json
  → recompute_next_alarm()
       next = 墙钟上最近一次 occurrence
       pending = next 的载荷
       program Alarm1 = next 的时刻

SYNC_SCHEDULE 是给「整表替换」用的一条 MQTT action(现网没有「下发 100 条计划」的命令)。不要用 10 次 SET_ALARM 冒充。不新增 Xiaozhi WS type。若 8KB 不够:先推文件再 SYNC_SCHEDULE 只带版本号——施工时选一种,不要两种并行。

软关机前必须 recompute_next_alarm(),这样没网也能被下一笔计划唤醒。

5.3 到点

Alarm1 触发
  → 读 pending(这一笔计划)
  → 若正在跑临时任务:不抢占;临时结束后 recompute(若仍到期则补跑或跳过本 occurrence,建议:仍在宽限窗口内则补跑,否则等下一周期)
  → 若软关机:Restore + StartAppTask(schedule)
  → 若已开机且非临时:Stop 当前 + StartAppTask(schedule)
  → 本笔开始后即可 recompute「再下一次」写入 Alarm1(时长 10 分钟的任务不要把闹钟设在这 10 分钟内的下一个整点,除非产品就要重叠——默认:进行中的这条占用设备,下一个 Alarm 至少在本笔 on_complete 之后)

进行中占用: 一条任务没结束(清单没完 / duration 没到)时,不要用下一整点把它切掉,除非来了临时任务。每小时整点「聊 10 分钟」:10:00 开始,10:10 关机;11:00 再开。不要 10:00 开了又被 11:00 的计算误写进当前这 10 分钟里。

5.4 结束

只读本次 params,禁止 if (shadowing) power_off。

on_complete 行为
chat 回默认对话(现网「跟读结束」「泛听结束」提示可保留,文案泛听不要说复读机)
power_off 自然结束后软关机;口令退出则 不关
duration 到点 等同自然结束,再走 on_complete

时长定时:设备墙钟(关机/没网也要准)。不要只靠云端 sleep。

5.5 语言对

start_app_task 把 language_pair 放进 bilingual/翻译已有 prompt 拼装。未实现的语种:仍进入该 Mode,TTS 说明「先用中英」,不要崩。完整 phrase pack 仍是 Phase 6,不挡 7a 存 pair。


6. 改动清单

6.1 manager-api

ID 任务
J1 Liquibase ai_parent_task
J2 oauth2 CRUD;scene 白名单;计划上限 100 不是 10
J3 scenes / lessons / language-pairs catalog
J4 计划变更 → SYNC_SCHEDULE 全量
J5 立即 → START_APP_TASK mqtt_immediate
J6 (后)oauth2 读 Instance / 聊天;不渲染 metrics

6.2 xiaozhi-server

ID 任务
S1 lesson_id + repeat → playlist(替换写死 TEMP 为缺省)
S2 on_complete / duration / language_pair 传入执行
S3 进场 announce;家长文案「泛听」
S4 pytest:展开课文;immediate 不写周期表

不改 ASR→LLM 锁定路径。

6.3 固件

ID 任务
C1 本地 schedule.json 全量;**禁止**用 10 槽闹钟表当任务库
C2 recompute_next_alarm():最近一次 → 唯一 pending + Alarm1
C3 触发时已开机也执行;软关机唤醒走这笔 pending
C4 临时不抢?否:临时可打断计划;计划不可打断临时
C5 duration_minutes 墙钟到点结束;on_complete
C6 软关机前重算下一次 Alarm1
C7 禁止新 DeviceState / 业务 if 树;课文不进固件

C1/C2/C6 是 P0。没有「本地全表 + 只写下一次」,云端 100 条计划在真机上仍是错的。

6.4 manager-mobile

ID 任务
M1 创建:场景 → 动态表单 → 「立即开始」或「每天/每小时…」
M2 场景名:**泛听 / 跟读 / 双语对话 / 翻译 / 对话**。禁止写复读机
M3 泛听/跟读:课文、遍数、结束动作。双语:语言对、可选时长
M4 计划列表(可很多条);立即任务显示「正在执行」
M5 (后)历史:跟读句文本+录音;泛听完成态;对话类聊天记录

7. 实施顺序

7a-A  固件:schedule.json + 只编程下一次 Alarm1 + 已开机执行 + 软关机前重算
7a-B  Java 任务表 + SYNC_SCHEDULE / 立即 START
      Python:lesson/repeat/on_complete/language_pair
7a-C  小程序:场景表单 + 立即 + 计划
7a-D  历史(无分数)

8. 测试

  • 15+ 条计划含 hourly:设备文件全在,芯片里只有一个最近时刻
  • 泛听关机后再开,进的是下一笔最近计划,不是列表[0]
  • 立即跟读打断泛听计划;计划到点不打断立即
  • 中英/中日 pair 写入 params;热路径不回归失败
  • 未知课文:提示,不误关机
  • 口令「退出泛听」退出泛听

锁定回归:欢迎词、CHAT、翻译、中英。


9. History(7a-D)

场景 家长看见
跟读 每句 expected / ASR / 可选录音;无分数
泛听 完成/时长;通常无 turns
双语/翻译/对话 该时段聊天文本 + 现有音频 API

10. 编码约束

  1. 不改 hello/listen/tts 语义。
  2. 不把课文和 58 句清单写入 RTC/闹钟结构体。
  3. 不用 ALARM_MAX_NUM 限制家长任务。
  4. 临时 vs 计划只通过 source 优先级,不新造 Mode。
  5. 设备离线:云端仍保存计划;上线再全量 SYNC;没网软关机只靠本地表 + Alarm1。
  6. 每个 PR 先 Chat 回归。