Skip to content

feat(schedule): 完成持久化提醒链路与 IM 联动 - #343

Open
HuXiaohui424 wants to merge 11 commits into
1024XEngineer:mainfrom
HuXiaohui424:main
Open

feat(schedule): 完成持久化提醒链路与 IM 联动#343
HuXiaohui424 wants to merge 11 commits into
1024XEngineer:mainfrom
HuXiaohui424:main

Conversation

@HuXiaohui424

@HuXiaohui424 HuXiaohui424 commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

结论

本 PR 完成链式日程提醒机制的持久化重构,以及与现有 IM Gateway 能力的联动:

  • Schedule 与 ReminderTask 执行状态解耦。
  • 每条真实提醒 Timing task 拥有独立、持久化的 ReminderTask 记录。
  • ReminderTask 使用 SQLite 作为设备生产存储。
  • 支持最多三次、间隔 10 分钟的自动提醒链。
  • 支持 acknowledge 和 snooze 两个无参数 Schedule MCP 工具。
  • 通过现有 IM 公开能力发送强提醒并接收动作窗口。
  • 完成 Runtime 侧提醒通知适配和动作 worker。
  • PCM-PCB Profile 已完成 ESP-IDF 固件构建,可进入硬件烧录测试。

Reviewer 重点判断:

  • ReminderTask 的持久化模型、迁移和状态语义是否符合链式提醒要求。
  • acknowledge / snooze 的行为是否正确区分。
  • IM 调用是否仅依赖现有公开能力,未修改 IM 模块。
  • Runtime worker 的生命周期、断线重试和销毁顺序是否安全。
  • 设备 Profile 是否始终使用 FATFS + SQLite 持久化存储。

Refs #344

变更

  • 新增独立的 ScheduleReminderTask 领域模型和 Repository 接口。
  • 新增 ReminderTask 业务状态和 Timer 状态:
    • scheduled
    • waiting_acknowledgement
    • acknowledged
    • exhausted
    • cancelled
    • pending
    • triggered
    • cancelled
    • completed
    • failed
  • 从 Schedule 中删除旧的 reminder_task_id 字段。
  • 新增 SQLite schedule_reminder_task 表及唯一约束:
    • (chain_id, attempt) 唯一;
    • timing_task_id 唯一;
    • attempt 限制为 1~3;
    • 业务状态和 Timer 状态限制在合法枚举范围内。
  • 新增 v006 SQLite migration:
    • 创建独立 ReminderTask 表;
    • 迁移历史 schedule.reminder_task_id 数据;
    • 重建 Schedule 表并移除旧字段;
    • 保留历史 v005 migration。
  • 新增 SQLite ReminderTask Repository、Row Mapper 和 SQL 实现。
  • 新增 Memory ReminderTask Repository,仅用于 Host 测试或非生产替身。
  • 更新 ScheduleReminderService:
    • 创建首条提醒任务;
    • 触发语音提醒;
    • 发送 IM 强提醒;
    • 自动创建后续 10 分钟提醒;
    • 第三次提醒后不再创建第四次;
    • Schedule 触发后保持 active;
    • acknowledge 批量确认最近 10 分钟内触发的提醒;
    • snooze 不新增、取消或修改默认后续 Timer;
    • acknowledge 取消 pending 后续任务并完成相关 Schedule。
  • 新增 MCP 工具:
    • schedule.reminder_acknowledge
    • schedule.reminder_snooze
  • 新增 Runtime IM Reminder Notification Adapter。
  • 新增 Runtime IM Action Executor。
  • 复用现有 IM Gateway 的:
    • NotificationIntent
    • SubmitNotification
    • ActionWindow
    • ImActionChannel
    • ImActionExecutor
  • 新增 Reminder Action worker:
    • IM 未 ready 或连接失败时保留动作窗口;
    • 使用退避重试;
    • 复用 ActionChannel 以保留 operation ID cache 和 Last-Event-ID cursor;
    • 停止时等待 worker 完整退出后再释放依赖。
  • 更新设备 Profile 校验:
    • 禁止设备 Profile 使用 memory storage;
    • 要求 persistent-sqlite;
    • 要求 FATFS 和 SQLite 配置启用。
  • 禁止 FATFS 挂载失败时自动格式化,避免静默清除 Schedule、ReminderTask 和其他设备数据。
  • 新增和更新 Schedule、ReminderTask、MCP、IM Adapter、Schema Migration 相关 Host 测试。

明确未包含:

  • 未修改 components/voicelife_im/**,仅调用现有 IM 模块公开能力。
  • 未修改以下用户明确排除的文件:
    components/voicelife_storage_memory/include/voicelife/storage_memory/memory_schedule_repository.h
  • 未新增 IM MCP 工具。
  • 未提交设备凭据、用户隐私、设备备份或硬件日志。
  • 未提交 build/ 下的固件构建产物。
  • 未执行真实硬件烧录。
  • 未完成真机上的 IM Gateway、语音提醒和重启恢复验证。
  • 未完全解决跨线程状态迁移的 CAS/事务化增强问题。

架构与兼容

  • Port / Adapter:
    • Schedule 领域层通过 ScheduleReminderNotificationPort 依赖通知能力;
    • Runtime 通过 ImScheduleReminderNotification 适配现有 IM Gateway;
    • 未反向修改 IM 模块,也未将 Schedule 业务逻辑耦合到 IM 实现内部。
  • 数据模型:
    • Schedule 仅保存日程事实;
    • ReminderTask 独立保存提醒执行状态、链路、attempt、Timing task ID 和触发时间;
    • SQLite schema 从 v005 正向迁移至 v006,并兼容历史 reminder_task_id 数据迁移。
  • 生产存储:
    • 设备 Profile 使用 FATFS + SQLite;
    • Memory Repository 仅用于 Host 测试或替身,不作为设备生产路径。
  • Profile:
    • 新增/强化设备 Profile 存储能力校验;
    • PCM-PCB 使用 esp32s3-voicelife-pcb-pcm;
    • PCM-PCB 使用 esp32s3-pcm-port 和 config/partitions/voicelife-pcb.csv;
    • SparkBot Profile 保持原有音频驱动和分区表。
  • 协议:
    • 使用现有 IM Notification 和 Action Stream contract;
    • 强提醒携带 acknowledge 和 snooze 动作;
    • 未改变既有 IM 协议定义。
  • 兼容性:
    • SQLite migration 保留历史 v005,并通过 v006 迁移旧数据;
    • 旧 Schedule 表中的 reminder_task_id 会被迁移为第一条 ReminderTask;
    • 终态 ReminderTask 不参与启动恢复;
    • FATFS 挂载失败不再自动格式化,可能使损坏卷显式启动失败,但避免数据静默丢失。

验证

  • ./scripts/run_pre_submit_checks.sh
  • 远端 CI 的工作流、格式、IM Gateway、主机测试、架构、ESP-IDF 和 CodeQL 均通过;依赖图已启用时依赖审查也通过,未启用时已记录跳过原因
  • ESP-IDF 对应 Profile 构建
  • 真机或外部服务验证(如适用)

证据:

  • Host 测试:
    100% tests passed out of 83
  • Firmware Profile 校验:
    PASS esp32s3-voicelife-pcb-pcm
  • Firmware Python 测试:
    Ran 12 tests
    OK
  • SparkBot Profile 构建成功:
    Project build complete.
  • PCM-PCB Profile 构建成功:
    Project build complete.
    Generated .../build/esp32s3-voicelife-pcb-pcm/voicelife.bin
  • PCM-PCB 固件输出:
    voicelife.bin binary size 0x2bda50 bytes
  • PCM-PCB 烧录地址已根据 config/partitions/voicelife-pcb.csv 核对:
    0x000000 bootloader
    0x008000 partition table
    0x00f000 OTA data
    0x020000 application
    0xa10000 ESP-SR models

TDD 记录

  • RED:
    • schedule_reminder_im_adapter_test 初始 fixture 使用错误的通知响应字段,无法提取 ActionWindow,测试失败。
    • MCP 工具排序测试仍断言旧的 4 个工具,新增提醒动作工具后失败。
    • SQLite Mapper 更新时间字段测试传入参数不足,过早命中前序绑定错误,未能验证 updated_at 字段。
    • ESP 固件构建暴露 ScheduleReminderTask::triggered_at 缺失初始化和 Runtime worker 残留字段引用问题。
  • GREEN:
    • 修正 Notification Submission fixture 为真实 actionStream contract 后,IM Adapter 测试通过。
    • 更新 MCP 工具排序断言为 7 个工具后,工具注册测试通过。
    • 将 Mapper 测试参数调整为 8 个后,成功验证 updated_at 错误信息。
    • 显式初始化 ReminderTask 全部字段并清理 worker 残留引用后,ESP-IDF 构建通过。
    • SQLite Repository、Migration、ReminderService、MCP 和 Runtime Adapter 测试全部通过。
  • 显式初始化 ReminderTask 全部字段并清理 worker 残留引用后,ESP-IDF 构建通过。
  • SQLite Repository、Migration、ReminderService、MCP 和 Runtime Adapter 测试全部通过。
  • REFACTOR:
    • 将提醒执行状态从 Schedule 中拆分到独立 ReminderTask 表。
    • 抽取 SQLite ReminderTask Repository、Row Mapper 和 Runtime IM Adapter。
    • 统一 ReminderTask 状态校验和链式提醒注册逻辑。
    • 增加 Runtime Action worker 的停止信号、退避和资源释放顺序。
    • 强化 Profile 存储能力校验,并关闭 FATFS 挂载失败自动格式化。

风险与回退

  • 当前尚未完成真实硬件烧录和外部 IM 服务验证,硬件上的音频链路、网络配置、IM Gateway 凭据和动作流仍需现场验证。
  • acknowledge、snooze、Timer trigger 的跨线程严格串行化和 CAS 保护仍有进一步增强空间。
  • ReminderTask 的批量确认目前尚未完全采用事务化批量更新。
  • Timing task 注册和取消的异步最终结果持久化仍需后续补强。
  • IM Gateway 未 ready、网络断线或 ActionWindow 过期时,Runtime 会保留或重试动作窗口,但相关边界场景仍需更多真机测试。
  • PCM-PCB 和 SparkBot 使用不同的音频驱动及分区表,烧录时必须使用对应 Profile 目录下的文件,不能交叉使用。
  • 回退方式:
    • 回退本 PR 的两个 commit;
    • 保留历史 v005 migration,不执行破坏性数据清理;
    • 在硬件上重新烧录与目标 Profile 匹配的上一版本固件;
    • 不执行 erase-flash,避免丢失设备持久化数据。

Review 清单

  • Issue 的验收标准已经逐项回应
  • 没有提交凭据、设备备份、用户隐私或构建产物
  • 硬件日志保留了必要的明文诊断上下文;仅替换秘密和隐私字段
  • 新增/迁移的第三方代码记录了上游 commit 和许可
  • 文档和注释与实际行为一致
  • AI 生成内容已经由作者理解、测试并承担责任

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已检查提醒领域服务、SQLite/FATFS 持久化迁移、Runtime/IM 适配、MCP 工具及设备 Profile。当前实现有两处会影响交付:重启后可能丢失活动日程提醒,且启用 -Werror 的 Host 测试目标无法编译。python3 scripts/firmware.py validate 通过;针对性 Host 构建在以下新增提醒任务字段的初始化问题处失败。

Additional findings

  • components/voicelife_storage_sqlite/test/sqlite_schedule_reminder_task_repository_test.cc:?: [P1] Initialize triggered_at in all new task fixtures: Adding triggered_at before created_at makes the designated initializers in the new SQLite repository test (and the updated schedule reminder service test) omit a field under the repository's -Werror=missing-field-initializers flags. The targeted build fails before running any tests (sqlite_schedule_reminder_task_repository_test.cc:53, with the same error at three service-test fixtures). Add .triggered_at = std::nullopt to every fixture initializer so the Host test targets compile.

if (!synchronized.ok() && first_failure.ok()) first_failure = synchronized;
std::unordered_set<ScheduleId> schedules_with_tasks;
for (const auto& task : *reminders.value) {
schedules_with_tasks.insert(task.schedule_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Do not suppress reminder recreation with cancelled history

schedules_with_tasks is populated for every persisted row before filtering to pending tasks. After CancelScheduleReminder() or a failed registration, an active future schedule can therefore retain only a cancelled/failed historical row; on the next Start(), line 76 sees that schedule as already having a task and skips RegisterReminder(). The schedule remains active but never receives another timer after reboot or later service restart. Track only a restorable pending task (or explicitly recreate when no pending task exists).

@HuXiaohui424 HuXiaohui424 linked an issue Aug 21, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[remind] 实现持久化链式日程提醒与 IM 动作闭环

1 participant