File:
phase-7a-plan.md
Date: 2026-09-18(修订:场景参数 / 泛听命名 / 临时 vs 计划 / 闹钟只写下一次)
产品:① 创建并下发任务 ② 看历史;**计量评估不做**。
约束:Capability Spec v1.0;锁定 chat / 翻译 / 中英热路径与欢迎词时序。
给 Coding Agent:先读本文 §0–§5,再读system.cprogram_alarm1(DS3231 本来就只编程下一次)、app_task.cc唯一 pending、runtime.pystart_app_task。
实施顺序:**先 Dispatch(A/B/C),再 History(D)。禁止先做 metrics UI。**
先选 场景,再填该场景的参数,最后选 怎么跑:
| 怎么跑 | 含义 | 优先级 |
|---|---|---|
| 临时任务 | 创建后 立即下发并执行 | 最高(source=mqtt_immediate) |
| 计划任务 | 周期到点执行(每天某时刻、每小时整点、工作日…) | 其次(source=schedule) |
同一条任务定义(场景+参数)可以「现在就练」也可以「存成每天 7:30」。不要做成两套互不相干的对象:云端一行任务,run_mode=immediate|scheduled。
内部 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 别名保留。
「复读」和「跟读」在口语里都像「跟着读」,家长分不清。对家长用 泛听 / 跟读(只听 vs 跟着读),不要用听读、不要用复读机。
| 对家长 | 含义 | 内部(冻结) |
|---|---|---|
| 泛听 | 只听课文,不录音、不跟说 | repeater / 路径仍 /english_learning/repeater/ |
| 跟读 | 播一句 → 听窗录音 → 下一句 | shadowing |
进场 TTS / 小程序 / 历史页 / 离线口令一律用 泛听。不再保留「开始复读 / 退出复读 / 复读机」。
不要新造第三种 Mode。
临时:家长点「立即开始 · 跟读 · 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 条闹钟。
上一版「每个计划一个 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(),以免把「下一计划」丢掉。
| 能力 | 现网 | 缺口 |
|---|---|---|
| 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 |
on_complete。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 拒绝。
文件例如 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 同步路径不走它。
GET /user/scenes 场景列表 + 每个场景的表单 schema(课文? 遍数? 语言对? 时长? 结束动作?)
GET /user/lessons?scene= extensive / shadowing 的课文
GET /user/language-pairs zh-en, zh-fr, zh-ja…(展示名:中英/中法/中日)
优先级:临时 mqtt_immediate > 计划 schedule > 语音/默认 chat
小程序「立即开始」
→ POST task run_mode=immediate
→ MQTT START_APP_TASK {app,task,params, source:mqtt_immediate}
→ 设备 Stop 当前计划 Skill(若有)再进入场景
不修改周期表。不 program 闹钟。结束后 recompute_next_alarm()。
家长保存/删除/停用计划
→ 云端权威更新
→ 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(),这样没网也能被下一笔计划唤醒。
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 分钟里。
只读本次 params,禁止 if (shadowing) power_off。
| on_complete | 行为 |
|---|---|
chat |
回默认对话(现网「跟读结束」「泛听结束」提示可保留,文案泛听不要说复读机) |
power_off |
自然结束后软关机;口令退出则 不关 |
| duration 到点 | 等同自然结束,再走 on_complete |
时长定时:设备墙钟(关机/没网也要准)。不要只靠云端 sleep。
start_app_task 把 language_pair 放进 bilingual/翻译已有 prompt 拼装。未实现的语种:仍进入该 Mode,TTS 说明「先用中英」,不要崩。完整 phrase pack 仍是 Phase 6,不挡 7a 存 pair。
| 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 |
| ID | 任务 |
|---|---|
| S1 | lesson_id + repeat → playlist(替换写死 TEMP 为缺省) |
| S2 | on_complete / duration / language_pair 传入执行 |
| S3 | 进场 announce;家长文案「泛听」 |
| S4 | pytest:展开课文;immediate 不写周期表 |
不改 ASR→LLM 锁定路径。
| 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 条计划在真机上仍是错的。
| ID | 任务 |
|---|---|
| M1 | 创建:场景 → 动态表单 → 「立即开始」或「每天/每小时…」 |
| M2 | 场景名:**泛听 / 跟读 / 双语对话 / 翻译 / 对话**。禁止写复读机 |
| M3 | 泛听/跟读:课文、遍数、结束动作。双语:语言对、可选时长 |
| M4 | 计划列表(可很多条);立即任务显示「正在执行」 |
| M5 | (后)历史:跟读句文本+录音;泛听完成态;对话类聊天记录 |
7a-A 固件:schedule.json + 只编程下一次 Alarm1 + 已开机执行 + 软关机前重算
7a-B Java 任务表 + SYNC_SCHEDULE / 立即 START
Python:lesson/repeat/on_complete/language_pair
7a-C 小程序:场景表单 + 立即 + 计划
7a-D 历史(无分数)
锁定回归:欢迎词、CHAT、翻译、中英。
| 场景 | 家长看见 |
|---|---|
| 跟读 | 每句 expected / ASR / 可选录音;无分数 |
| 泛听 | 完成/时长;通常无 turns |
| 双语/翻译/对话 | 该时段聊天文本 + 现有音频 API |
ALARM_MAX_NUM 限制家长任务。source 优先级,不新造 Mode。