Skip to content

Architecture interface design

zhenghaotao edited this page Jul 30, 2026 · 14 revisions

语音日程系统新版架构文档

1. 方案结论

  1. 客户端不再独立承担全部语音理解和提醒编排。
  2. 客户端进入界面后建立 WebSocket 连接,作为后续实时通信主通道。
  3. 音频通过 WebSocket Binary Frame 流式传输到服务端。
  4. 服务端完成 ASR + LLM 结构化提取后,通过 WebSocket 回传结果。
  5. 客户端用统一表单完成二次确认、地理位置补全、时间补全和提醒参数补全。
  6. 日程最终落到单一 schedules 表中,状态和关联信息一并保存。
  7. 提醒采用三段式策略,优先保证触达,再处理重复提醒控制。
  8. TTS 是智能提醒模块的第三方附加通道:智能提醒模块提供 reminder_body, 服务端组合固定头尾后通过 TTS 网关生成音频;TTS 失败不影响事项保存或主提醒。
  9. 所有有效提醒在日程或待办创建、确认或相关编辑提交后异步预生成音频,并以确定性文件名 存入云端对象存储;数据库不保存 TTS 文件名、URL、状态或音频数据。
  10. 在线提醒组合为“震动 + 云端 TTS + 客户端自定义弹窗”;离线提醒组合为 “震动 + 客户端固定铃声 + 客户端自定义弹窗”。

这一版的重点不是“完整日历”,而是“日程创建 + 状态驱动提醒 + 语音结构化”闭环。

1.1 MVP 边界

本版明确包含:

  1. 单用户、单主要设备上的日程创建、编辑、查询、完成和删除。
  2. 一条活动 WebSocket 连接上的语音流、业务命令、查询和服务端推送。
  3. 时间提醒、地点提醒,以及同时包含时间和地点的日程。
  4. 断线重连、重复消息和离线提醒终态的最小恢复能力。
  5. TTS 音频预生成和云端对象存储,以及在线云端 TTS、离线固定铃声两种提醒组合。

本版暂不包含:

  1. 多用户共享日历。
  2. 系统日历、外部闹钟 App 及其引用同步与兜底。
  3. 周期性日程和复杂重复规则。
  4. 平台后台能力不可用时的系统级兜底提醒。
  5. 用 LLM 自主决定最终日程或提醒时间;LLM 只生成待用户确认的草稿。
  6. 断网时访问云端 TTS 音频;断网提醒只使用客户端预置固定铃声。

2. 关键运行场景

2.1 三种提醒情况

情况一:应用在前台且网络通畅

触发方式:

  1. 应用内弹窗。
  2. 页面内高亮提示。
  3. 伴随声音或震动。
  4. 按智能提醒内容播放云端预生成 TTS。

适用场景:

  1. 用户正在使用 App。
  2. 可以给出最完整的上下文。
  3. 不需要系统级强打断。

情况二:应用在后台且网络通畅

触发方式:

  1. 客户端自定义弹窗。
  2. 震动。
  3. 播放云端预生成 TTS。

适用场景:

  1. App 仍保持在线。
  2. 需要由系统层接管提醒触达。
  3. 可以通过 WS 收到实时控制消息。

这里的“网络通畅”必须同时满足:客户端网络可用、WebSocket 心跳正常、服务端会话有效。 仅检测到 Wi-Fi/蜂窝网络不能判定为此情况;后台进程或 WS 不可用时立即按情况三降级。

情况三:不满足前两种条件

客户端通过已登记的本地触发条件执行震动、客户端预置固定铃声和客户端自定义弹窗; 断网时不访问云端音频或外部 TTS 服务。

2.2 创建日程

日程支持两种入口:

  1. 手动创建。
  2. 语音创建。

二者共用一个表单。

语音创建流程:

  1. 客户端录音。
  2. 客户端通过 WebSocket Binary Frame 流式发送音频。
  3. 服务端返回上传受理结果。
  4. 服务端完成 ASR。
  5. 服务端完成 LLM 结构化提取。
  6. 服务端通过 WS 推送结构化日程草稿。
  7. 客户端弹出统一表单。
  8. 用户补全信息并确认。
  9. 客户端提交日程最终结果。
  10. 如果日程包含时间,服务端做时间冲突检测并返回提示。
  11. 创建成功后,服务端和客户端分别进入自己的提醒准备状态;服务端异步预生成关联 TTS 音频, 不阻塞创建结果。

日程允许两种主类型:

形态 schedule_type 必要信息 触发条件
时间类 time start_time 进入时间提醒窗口
地点类 location latitude + longitude 用户进入地理围栏

语音创建时,schedule_type 由 LLM 根据用户语义做意图识别后输出。手动创建时,由前端根据用户选择或填写内容确定。 schedule_type 只表示主意图类型,时间和地点同时存在时仍按 time 落库,是否触发地点提醒由字段本身判断。

3. 架构总览

3.1 客户端

客户端负责:

  1. 界面展示。
  2. WebSocket 长连接。
  3. 音频录制。
  4. 音频上传。
  5. 表单编辑和二次确认。
  6. 本地日程缓存。
  7. 前台和后台客户端自定义弹窗提醒。
  8. 位置信息上报。
  9. 根据前后台状态和系统可用性选择具体提醒通道。
  10. 展示客户端自定义提醒弹窗并执行震动。
  11. 在线时接收云端 TTS 文件流并播放;离线时播放客户端预置固定铃声。
  12. 在操作系统允许范围内,客户端预先注册自启动能力,用于进程异常终止后恢复客户端运行。

3.2 服务端

服务端负责:

  1. 音频文件接收。
  2. ASR 调用。
  3. LLM 结构化提取。
  4. WebSocket 消息分发。
  5. 日程冲突检测。
  6. 日程状态监控。
  7. 地点与时间窗口判断。
  8. 提醒控制消息下发。
  9. 提醒通道选择由客户端自行完成。
  10. 接收智能提醒模块的 reminder_body,通过 ReminderTemplateRenderer 组合固定头尾; TTS 不调用 LLM 生成提醒规则或提醒内容。
  11. 在事项创建、确认或相关编辑提交后,通过 TTS 生成网关异步生成音频,并按确定性标识写入 云端音频存储。
  12. 在线提醒触发时校验事项和云端文件,从对象存储读取音频并通过 WebSocket 流式下发。

3.3 外部依赖

  1. 第三方 ASR 服务。
  2. 大模型服务。
  3. 地图与位置服务。
  4. Android 客户端震动、音频播放和后台运行能力。
  5. 外部 TTS 服务。
  6. 云端音频存储。

外部 TTS 接入边界:

  1. 客户端不得直连外部 TTS 服务;所有调用由服务端 TTS 网关封装。
  2. 供应商凭证只存在于服务端密钥管理边界,不下发到客户端。
  3. 固定模板文本必须先通过字段白名单、长度和 Unicode 安全校验。
  4. 外部 TTS 生成结果进入云端音频存储;数据库不保存文件名、URL、状态或二进制。
  5. TTS 供应商、模型和音色属于可替换实现配置,不进入业务契约。

3.4 模块协作边界

  1. 界面模块只能通过 WebSocket 模块提交业务命令,不直接调用 ASR、LLM 或数据库。
  2. 语音解析模块只产出草稿,不创建正式日程;只有用户确认后的 schedule.upsert.command 才能写入 schedules
  3. 监控与调度模块只读取有效日程并生成提醒控制决策,不直接选择 Android 展示通道。
  4. 提醒执行模块只执行客户端能力并回传结果,不自行修改服务端日程状态。
  5. 地点判定模块只输出“围栏外/围栏内/位置不可用”的判定,不直接发送提醒。
  6. 智能提醒模块拥有提醒内容和触发决策;ReminderTemplateRenderer 只组合 提醒: + reminder_body + 。
  7. 服务端 TtsGenerationGateway 负责预生成,TtsAssetStorage 负责云端音频生命周期, 客户端 ReminderChannelPolicy 负责在线/离线提醒组合。
  8. TTS 不拥有独立提醒状态,不得因播报成功或失败修改 schedules.status 或主提醒 ACK。
  9. 客户端不得持有或接收外部 TTS 凭证;服务端网关是外部 TTS 的唯一调用方。
  10. 外部 TTS 鉴权、生成或云端存储失败不得阻塞事项保存或主提醒。

3.5 ASR 与 TTS 数据方向

ASR 上行与 TTS 提醒链路必须严格区分:

ASR 上行:
用户说话
-> 客户端录音
-> TimeFlow WebSocket Binary Frame
-> 服务端 ASR
-> 文本与结构化草稿

TTS 预生成:
日程/待办创建、确认或相关编辑提交
-> 智能提醒模块生成 reminder_body
-> ReminderTemplateRenderer 组合固定头尾
-> TtsGenerationGateway 生成音频
-> TtsAssetStorage 按确定性标识写入云端

在线提醒:
智能提醒模块命中条件
-> 服务端校验事项和云端文件
-> 震动 + 客户端自定义弹窗
-> 服务端从对象存储读取音频
-> TimeFlow WebSocket Binary Frame
-> 客户端播放云端 TTS

离线提醒:
客户端本地触发条件命中
-> 震动 + 客户端固定铃声 + 客户端自定义弹窗

边界规则:

  1. TTS 只消费智能提醒模块的提醒内容,不重新决定提醒时间、优先级或通道。
  2. 在线必须同时满足网络可用、WebSocket 心跳正常和客户端会话有效。
  3. 断网时不访问云端音频存储或外部 TTS 服务,也不补播已完成的离线提醒。
  4. 云端音频通过现有 TTS 流接口下发;客户端不直接访问外部 TTS 服务。
  5. TTS、固定铃声和震动都不改变主提醒 ACK 或事项状态。
  6. 相同事项、内容哈希和提醒消息必须幂等,避免重复生成和重复播放。

4. 模块拆分

4.1 客户端模块

4.1.1 界面模块

职责:

  1. 显示日程列表。
  2. 显示创建/编辑表单。
  3. 显示语音解析结果。
  4. 显示冲突提示。

4.1.2 语音录制模块

职责:

  1. 录音。
  2. 音频格式转换。
  3. 发起上传。

4.1.3 WebSocket 模块

职责:

  1. 建立和维护连接。
  2. 接收结构化草稿。
  3. 接收提醒控制消息。
  4. 上传位置信息。
  5. 上传日程确认结果。

4.1.4 本地存储模块

职责:

  1. 缓存日程。
  2. 缓存解析结果。
  3. 缓存提醒状态。
  4. 缓存提醒通道状态。
  5. 按现有 schedule_idupdated_at、触发类型和对应 *_triggered_at 组成的复合键保存 本次 TTS 终态,防止 WebSocket 重试造成重复播报。
  6. 保存 schedule_id + system_alarm_ref_id + trigger_time 作为最小本地闹钟记录,用于进程恢复 和设备重启后重新登记;不保存日程正文,也不缓存云端 TTS 音频。
  7. AlarmManager 取消成功、日程失效或 trigger_time 已过期后删除对应最小记录,避免设备重启后误登记。

4.1.5 提醒执行模块

职责:

  1. 前台弹窗。
  2. 后台浮窗。
  3. 根据当前应用状态决定提醒展示方式。
  4. 客户端自定义提醒弹窗的样式、布局和交互。
  5. ReminderChannelPolicy 根据在线状态选择“震动 + 云端 TTS + 自定义弹窗”或 “震动 + 固定铃声 + 自定义弹窗”。
  6. TtsStreamPlayer 管理云端音频下行流和播放结果。
  7. 离线提醒执行能力负责震动、客户端预置固定铃声和自定义弹窗。

固定约束:

  1. 在线状态使用云端 TTS,离线状态固定使用客户端铃声,不在两条路径间重复播放声音。
  2. 客户端弹窗、震动和固定铃声不依赖外部 TTS 凭证。
  3. 不强制修改系统音量,不绕过勿扰模式。
  4. 同一 schedule_id + updated_at + trigger_kind + triggered_at 只执行一次可见提醒和一次声音提醒。
  5. TTS 失败只影响语音通道,不影响震动、自定义弹窗或主提醒。

4.1.6 地图与位置模块

职责:

  1. 地点搜索。
  2. 地点确认。
  3. 地理围栏设置。
  4. 实时位置上报。

4.2 服务端模块

4.2.1 音频接入模块

职责:

  1. 接收音频文件。
  2. 校验格式和大小。
  3. 生成处理任务 ID。

4.2.2 语音解析模块

职责:

  1. 调用 ASR。
  2. 调用 LLM。
  3. 识别 schedule_type
  4. 生成结构化草稿。

4.2.3 WebSocket 网关

职责:

  1. 建立客户端会话。
  2. 维护设备在线状态。
  3. 分发解析结果和提醒控制消息。
  4. 接收位置上报。

4.2.4 日程服务模块

职责:

  1. 创建日程。
  2. 编辑日程。
  3. 查询日程。
  4. 冲突检测。

4.2.5 监控与调度模块

职责:

  1. 监听日程时间。
  2. 监听地理位置。
  3. 判断时间、空间以及组合触发条件。
  4. 条件满足后生成软件提醒控制消息。
  5. 调用智能提醒模块取得 reminder_body 和触发决策,不由 TTS 模块生成提醒规则。
  6. 事项创建、确认或相关编辑提交后发布异步 TTS 预生成任务。

4.2.6 地点判定模块

职责:

  1. 根据上报位置和日程坐标计算距离。
  2. 判断是否进入地理围栏。

4.2.7 服务端提醒模板渲染模块

职责:

  1. 接收智能提醒模块产出的 reminder_body,固定组合为 提醒: + reminder_body + 。,不调用 LLM 生成提醒内容。
  2. 使用现有用户、事项、版本或 updated_at、提醒内容和触发字段完成校验。
  3. 根据固定头、reminder_body、固定尾、模板版本和音色版本计算 content_hash
  4. 不向 schedulestodos 或其他业务表写入 TTS 字段。

4.2.8 TTS 生成网关与流式转发模块

职责:

  1. TtsGenerationGateway 封装外部 TTS 调用,不向业务模块暴露供应商协议。
  2. 在异步预生成任务中发送经校验的 rendered_text;生成失败不回滚事项事务。
  3. 将生成结果交给 TtsAssetStorage,不直接写业务数据库。
  4. 相同事项和 content_hash 已存在云端音频时直接复用,不重复生成。

4.2.9 云端 TTS 文件存储模块

职责:

  1. TtsAssetStorage 使用由 user_id + object_type + object_id + content_hash 组成的确定性音频标识。
  2. 音频标识不得包含标题、地点、备注等用户明文内容。
  3. 在线触发时根据当前事项重新计算标识,校验对象存在后流式读取。
  4. 编辑产生新哈希时写入新文件;删除、完成、取消和失效事项按事项前缀进入清理流程。
  5. 文件存在性、复用和清理通过对象存储完成,不新增数据库状态字段或音频资产表。

5. 数据库设计

5.1 表名:schedules

MVP 阶段只保留一张核心业务表。

用途:

  1. 存储日程本体。
  2. 存储日程状态。
  3. 存储地点与提醒关联信息。
  4. 存储地理围栏布防状态。
  5. 存储服务端事件绑定和客户端 AlarmManager 闹钟引用。

5.2 推荐字段

字段 类型 说明
id text 主键,日程 ID
user_id text 默认用户 ID
source_mode text manual / voice
schedule_type text time / location
status text scheduled / done / deleted
title text 标题
notes text 备注
start_time text 开始时间,ISO-8601,可以为空
end_time text 结束时间,可为空
timezone text 时区,可为空
location_name text 地点名称
location_address text 地点地址
latitude real 纬度
longitude real 经度
geofence_radius_meters integer 地理围栏半径,默认 100m
geofence_armed integer 地理围栏是否已布防,默认由服务端根据创建时用户位置计算
time_remind_offset_minutes integer 时间提前提醒,默认 15min
time_triggered_at text 时间提醒触发时间,可为空
geo_triggered_at text 地点提醒触发时间,可为空
system_schedule_ref_id text 服务端内部提醒事件绑定 ID,可为空
system_alarm_ref_id text Android AlarmManager 的 alarmId,可为空
created_at text 创建时间
updated_at text 更新时间

5.3 设计原则

  1. 核心字段尽量扁平化。
  2. 语音解析草稿只通过 WS 传递,不落库。
  3. system_schedule_ref_id 由服务端维护;system_alarm_ref_id 等于客户端已登记到 AlarmManager 的 alarmId。二者均不表示系统日历或外部闹钟 App 引用。
  4. 时间提醒和地理提醒可以同时存在。
  5. 只保留一张主表,MVP 不拆分额外业务表。
  6. status 只表达日程本体是否还有效,不表达监听中、已触发、已过期等过程状态。
  7. MVP 不保存用户自定义的重要程度,提醒方式由应用前后台状态、网络状态和时间/空间窗口决定。
  8. 冲突检测结果只在接口响应中返回,不写入 schedules 表。
  9. start_time 和地点信息都允许为空,但二者不能同时为空。
  10. geofence_armed 用于避免“用户在目标地点创建日程后立刻触发位置提醒”。
  11. 最近一次位置只在会话内或内存中计算,不落库。
  12. schedule_type 是前端表单必填项控制和后端校验的依据,不再使用全天字段区分业务类型。
  13. TTS 播报终态属于客户端基础设施状态,不写入 schedules
  14. 固定模板不是业务事实,不增加 tts_texttts_template 等数据库字段。
  15. 不增加 audio_file_nameaudio_urlcontent_hash、音频状态或音频二进制字段, 也不增加 TTS 音频资产表。
  16. 云端文件名由现有 user_id、事项类型、事项 ID、提醒内容、模板版本和音色版本运行时计算。
  17. 云端文件存在性通过对象存储检查,不通过数据库记录判断。

5.4 字段约束

数据库迁移和服务端模型必须同时实现以下约束:

  1. iduser_idsource_modeschedule_typestatustitlecreated_atupdated_at 不为空。
  2. source_mode 只能是 manualvoice
  3. schedule_type 只能是 timelocation
  4. status 只能是 scheduleddonedeleted
  5. title 去除首尾空白后不能为空。
  6. start_timeend_time 同时存在时,end_time >= start_time
  7. latitudelongitude 必须同时为空或同时有值;纬度范围为 [-90, 90],经度范围为 [-180, 180]
  8. start_time 与经纬度不能同时为空。
  9. schedule_type=timestart_time 必填;schedule_type=location 时经纬度必填。
  10. geofence_radius_meters > 0time_remind_offset_minutes >= 0
  11. 所有时间统一按带时区 ISO-8601 接口值解析,数据库内部按 UTC 保存;返回客户端时保留 timezone 用于展示。
  12. 更新时由服务端生成新的 updated_at,客户端传入值不得覆盖。
  13. system_schedule_ref_idsystem_alarm_ref_id 均允许为空;非空时去除首尾空白后不能为空。
  14. system_schedule_ref_id 只能由服务端提醒调度模块写入,不接受客户端传值。
  15. system_alarm_ref_id 只在客户端成功登记 AlarmManager 后写入;登记失败时必须保持为空。

5.5 最小索引

MVP 至少建立:

  1. (user_id, status, start_time):日程列表、时间窗口扫描和冲突检测。
  2. (user_id, updated_at):重连后的增量对齐。
  3. system_schedule_ref_id 非空唯一索引:服务端事件绑定定位。
  4. (user_id, system_alarm_ref_id) 非空唯一索引:当前用户 AlarmManager 闹钟定位。

索引是查询和调度约束,不改变“一张核心业务表”的方案。

6. 状态机

6.1 日程状态

建议状态流转:

scheduled -> done
scheduled -> deleted

状态说明:

状态 含义
scheduled 已创建,等待提醒或正在监听
done 用户已确认完成
deleted 用户删除

不进入 status 的过程信息:

信息 处理方式
草稿待确认 语音解析结果通过 WS 推给前端,用户确认前不创建正式日程
时间监听中 根据 start_timetime_remind_offset_minutes 和当前时间动态判断
地理监听中 根据经纬度、围栏半径、最近位置和 geofence_armed 动态判断
已触发提醒 写入 time_triggered_atgeo_triggered_at
已过期 根据 start_timeend_time 动态判断
创建冲突 只在接口响应中返回 conflicts,不持久化

7. 接口文档

7.0 WebSocket 消息约定

业务接口统一使用 WebSocket。JSON Text Frame 承载控制消息、命令、查询、结果和错误;Binary Frame 承载音频数据。

客户端请求信封:

{
  "type": "schedule.upsert.command",
  "request_id": "req_schedule_001",
  "payload": {}
}

服务端成功响应:

{
  "type": "schedule.upsert.result",
  "request_id": "req_schedule_001",
  "ok": true,
  "payload": {}
}

服务端失败响应:

{
  "type": "schedule.upsert.error",
  "request_id": "req_schedule_001",
  "ok": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数不合法",
    "details": {
      "field": "start_time"
    }
  }
}

字段说明:

字段 类型 说明
type string 消息类型
request_id string 请求关联和幂等 ID
ok boolean 服务端是否成功处理
payload object/null 业务数据
error.code string 程序可识别的错误码
error.message string 面向前端展示或日志记录的错误说明
error.details object/null 具体错误上下文,可为空

WebSocket 业务失败不依赖 HTTP 状态码,通过 *.errorok=falseerror 对象表达。

补充约定:

  1. 客户端发起的命令、查询和 ACK 都必须携带 request_id;服务端使用同一个 request_id 返回结果,客户端重试时不得生成新值。
  2. 服务端主动推送的 message_id 只标识本次 WebSocket 发送,用于链路追踪;消息重发时允许 生成新值,不得作为提醒展示或 TTS 播放的业务幂等键。
  3. request_id 的幂等范围是“当前用户 + 消息类型”;服务端至少保存到该操作进入终态。
  4. 同一 request_id 若收到不同 payload,返回 IDEMPOTENCY_CONFLICT,不得覆盖第一次结果。
  5. 提醒业务去重使用现有数据库字段组成的 schedule_id + updated_at + trigger_kind + triggered_at 复合键,不新增数据库字段。
  6. trigger_kindtriggered_at 只是对现有 time_triggered_atgeo_triggered_at 的运行时表达,不写入新字段。
  7. *.command*.query*.result*.error 的业务字段统一放入 payload; 7.4—7.11 已有事件型消息保留根部业务字段以避免大改,后续不得在同一消息类型中混用两种结构。
  8. 未知 type 返回 UNSUPPORTED_MESSAGE_TYPE;多余字段按协议版本策略处理, 缺少必填字段返回 VALIDATION_ERROR
  9. 服务端推送只有在收到业务 ACK 后才视为客户端已执行;“WebSocket 已发送”不等于“提醒已展示”。

7.1 WebSocket 音频流

作用

  1. 建立单次语音流。
  2. 使用 Binary Frame 持续发送音频分片。
  3. 触发后续 ASR + LLM 流程。

开始音频流

客户端发送 JSON Text Frame:

{
  "type": "voice.stream.start",
  "request_id": "req_audio_001",
  "payload": {
    "audio_format": "pcm_s16le",
    "sample_rate_hz": 16000,
    "channels": 1
  }
}

服务端响应:

{
  "type": "voice.stream.started",
  "request_id": "req_audio_001",
  "ok": true,
  "payload": {
    "stream_id": "stream_audio_001",
    "job_id": "job_audio_001"
  }
}

发送音频数据

voice.stream.started 成功后,客户端通过同一条 WebSocket 连接发送 Binary Frame。

每个 Binary Frame 必须归属于当前活动的 stream_id。同一设备连接同一时刻只允许一个活动音频流;客户端应按服务端 ACK 和背压信号控制发送速度。

结束音频流

{
  "type": "voice.stream.end",
  "request_id": "req_audio_001",
  "payload": {
    "stream_id": "stream_audio_001"
  }
}

服务端受理:

{
  "type": "voice.stream.ended",
  "request_id": "req_audio_001",
  "ok": true,
  "payload": {
    "stream_id": "stream_audio_001",
    "job_id": "job_audio_001",
    "status": "processing"
  }
}

失败响应

音频格式不支持:

{
  "type": "voice.stream.error",
  "request_id": "req_audio_001",
  "ok": false,
  "error": {
    "code": "UNSUPPORTED_AUDIO_FORMAT",
    "message": "音频格式不支持",
    "details": {
      "audio_format": "aac",
      "supported_formats": ["pcm_s16le"]
    }
  }
}

音频流过大或超过时长限制:

{
  "type": "voice.stream.error",
  "request_id": "req_audio_001",
  "ok": false,
  "error": {
    "code": "AUDIO_STREAM_LIMIT_EXCEEDED",
    "message": "音频流超过限制",
    "details": {
      "max_duration_ms": 120000
    }
  }
}

说明:

  1. 不再提供 HTTP 音频上传接口。
  2. JSON Text Frame 只传控制信息,音频内容只通过 Binary Frame 发送。
  3. 真正的结构化结果仍通过 voice.parse.result 返回。

7.2 WebSocket 日程创建/更新

消息类型

schedule.upsert.command

作用

  1. 手动创建日程。
  2. 语音表单确认后提交日程。
  3. 写入最终状态。
  4. 执行时间冲突检测。

客户端消息

{
  "type": "schedule.upsert.command",
  "request_id": "req_schedule_001",
  "payload": {
    "schedule_id": null,
    "source_mode": "voice",
    "schedule_type": "time",
    "title": "开会",
    "notes": null,
    "start_time": "2026-07-29T15:00:00+08:00",
    "end_time": null,
    "timezone": "Asia/Shanghai",
    "location_name": "陆家嘴",
    "location_address": null,
    "latitude": 31.2451,
    "longitude": 121.5067,
    "geofence_radius_meters": 100,
    "geofence_armed": true,
    "time_remind_offset_minutes": 15,
    "system_alarm_ref_id": null
  }
}

请求字段保持原日程接口定义:

字段 类型 必填 说明
request_id string 幂等 ID,位于消息信封
schedule_id string/null 编辑时传
source_mode string manual / voice
schedule_type string time / location
title string 标题
notes string/null 备注
start_time string/null 开始时间;只有地点的日程可为空
end_time string/null 结束时间
timezone string/null 时区
location_name string/null 地点名称
location_address string/null 地点地址
latitude number/null 纬度
longitude number/null 经度
geofence_radius_meters integer/null 地理围栏半径,默认 100
geofence_armed boolean/null 地理围栏是否已布防,不传则服务端计算
time_remind_offset_minutes integer/null 时间提醒提前量,默认 15
system_alarm_ref_id string/null AlarmManager 登记成功后回写的 alarmId

成功响应

{
  "type": "schedule.upsert.result",
  "request_id": "req_schedule_001",
  "ok": true,
  "payload": {
    "schedule_id": "schedule_001",
    "schedule_type": "time",
    "status": "scheduled",
    "conflicts": [],
    "geofence_armed": true,
    "system_alarm_ref_id": null
  }
}

冲突响应

{
  "type": "schedule.upsert.result",
  "request_id": "req_schedule_001",
  "ok": true,
  "payload": {
    "schedule_id": "schedule_001",
    "schedule_type": "time",
    "status": "scheduled",
    "conflicts": [
      {
        "schedule_id": "schedule_older",
        "title": "已有日程",
        "start_time": "2026-07-28T15:00:00+08:00",
        "end_time": "2026-07-28T16:00:00+08:00"
      }
    ],
    "geofence_armed": true,
    "system_alarm_ref_id": null
  }
}

失败响应

{
  "type": "schedule.upsert.error",
  "request_id": "req_schedule_001",
  "ok": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数不合法",
    "details": {
      "field": "schedule_type",
      "reason": "schedule_type 为 time 时 start_time 必填;为 location 时 latitude 和 longitude 必填"
    }
  }
}

规则:

  1. 手动创建和语音确认都使用 schedule.upsert.command
  2. 时间冲突只给提示,不默认阻断。
  3. schedule_type=time 时,前端表单要求填写时间,地点可选。
  4. schedule_type=location 时,前端表单要求填写地点,时间可选。
  5. 用户同时填写时间和地点时,schedule_type 仍按 time 处理。
  6. 只有 start_time 存在时才做时间冲突检测。
  7. 只有经纬度存在时才做地理围栏监听。
  8. 如果 geofence_armed 不传,服务端根据最近一次位置上报与目标地点距离计算默认值。
  9. 新建日程时 system_alarm_ref_id 必须为空;客户端成功登记 AlarmManager 后,使用带 schedule_id 的同一更新接口回写 alarmId
  10. 更新既有日程时,未传或传入空的 system_alarm_ref_id 均不得清空已有引用;引用只能在 对应取消 ACK 成功后清空。
  11. 不同于当前值的新非空引用只允许回写到仍为 scheduled 且存在 start_time 的日程; 提交的时间相关字段与当前记录不一致时返回 VERSION_CONFLICT,不得覆盖并发业务修改。
  12. 相同日程重复回写相同 system_alarm_ref_id 返回当前结果,不重复创建业务日程。
  13. AlarmManager 登记失败时不得回写不存在的 system_alarm_ref_id
  14. 编辑导致提醒时间变化时必须重新登记;若生成新 alarmId,先登记并回写新引用,再通过 现有 reminder.control(action=cancel) 清理旧引用。
  15. system_schedule_ref_id 由服务端维护,不属于客户端请求字段。

7.3 WebSocket 日程列表查询

消息类型

schedule.list.query

作用

  1. 获取当前用户的日程数据。
  2. 支持前端进入页面后初始化列表。
  3. 支持前端恢复本地状态和服务端状态对齐。

客户端消息

{
  "type": "schedule.list.query",
  "request_id": "req_schedule_list_001",
  "payload": {
    "status": null,
    "include_deleted": false
  }
}

服务端响应

{
  "type": "schedule.list.result",
  "request_id": "req_schedule_list_001",
  "ok": true,
  "payload": {
    "schedules": [
      {
        "id": "schedule_001",
        "user_id": "default_user",
        "source_mode": "voice",
        "schedule_type": "time",
        "status": "scheduled",
        "title": "开会",
        "notes": null,
        "start_time": "2026-07-29T15:00:00+08:00",
        "end_time": null,
        "timezone": "Asia/Shanghai",
        "location_name": "陆家嘴",
        "location_address": null,
        "latitude": 31.2451,
        "longitude": 121.5067,
        "geofence_radius_meters": 100,
        "geofence_armed": true,
        "time_remind_offset_minutes": 15,
        "time_triggered_at": null,
        "geo_triggered_at": null,
        "system_alarm_ref_id": "alarm_schedule_001",
        "created_at": "2026-07-28T12:00:00+08:00",
        "updated_at": "2026-07-28T12:00:00+08:00"
      }
    ]
  }
}

失败响应

{
  "type": "schedule.list.error",
  "request_id": "req_schedule_list_001",
  "ok": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数不合法",
    "details": {
      "field": "status"
    }
  }
}

规则:

  1. 用户身份由 WebSocket 会话上下文确定,客户端不传 user_id
  2. 默认只返回 scheduleddone
  3. include_deleted=true 时才返回 deleted 数据。
  4. 返回结果按 start_time asc nulls last, created_at desc 排序。
  5. 返回 system_alarm_ref_id 用于客户端与本地 AlarmManager 状态对齐。
  6. system_schedule_ref_id 是服务端内部字段,不返回客户端;本接口不包含系统日历或外部闹钟 App 引用。

7.4 WebSocket 连接

地址

生产环境:wss://<host>/ws?device_id=xxx

仅本地开发可使用:ws://<host>/ws?device_id=xxx

作用

  1. 保持设备在线。
  2. 回传语音结构化结果。
  3. 下发提醒控制消息。
  4. 接收位置信息和确认事件。

连接建立

客户端进入界面后先发:

{
  "type": "session.hello",
  "device_id": "android_abc123",
  "app_version": "1.0.0"
}

服务端返回:

{
  "type": "session.ready",
  "device_id": "android_abc123",
  "server_time": "2026-07-28T12:00:00Z",
  "heartbeat_interval_seconds": 30
}

连接失败或鉴权失败时,服务端返回错误消息后关闭连接:

{
  "type": "session.error",
  "ok": false,
  "error": {
    "code": "INVALID_DEVICE_ID",
    "message": "设备 ID 不合法",
    "details": null
  }
}

7.5 语音结构化结果推送

服务端消息

{
  "type": "voice.parse.result",
  "request_id": "req_audio_001",
  "job_id": "job_audio_001",
  "status": "ready_for_confirmation",
  "draft": {
    "schedule_type": "time",
    "title": "开会",
    "start_time": "2026-07-29T15:00:00+08:00",
    "end_time": null,
    "timezone": "Asia/Shanghai",
    "location_name": "陆家嘴",
    "geofence_radius_meters": 100,
    "time_remind_offset_minutes": 15
  },
  "missing_fields": ["location_address"],
  "ambiguous_fields": [],
  "needs_confirmation": true
}

失败消息

ASR 或 LLM 处理失败时,服务端通过 WS 推送失败结果:

{
  "type": "voice.parse.result",
  "request_id": "req_audio_001",
  "job_id": "job_audio_001",
  "status": "failed",
  "error": {
    "code": "VOICE_PARSE_FAILED",
    "message": "语音解析失败,请重新录音或手动创建",
    "details": {
      "stage": "asr"
    }
  }
}

前端处理

  1. 弹出统一表单。
  2. 默认填充结构化字段。
  3. 允许用户补全地点、时间和提醒参数。

7.6 位置信息上报

客户端消息

{
  "type": "location.report",
  "request_id": "req_location_001",
  "schedule_scope": "current",
  "latitude": 31.2451,
  "longitude": 121.5067,
  "accuracy": 18,
  "timestamp": "2026-07-28T12:01:00+08:00"
}

服务端响应

{
  "type": "location.report.ack",
  "request_id": "req_location_001",
  "ok": true
}

失败响应:

{
  "type": "location.report.ack",
  "request_id": "req_location_001",
  "ok": false,
  "error": {
    "code": "INVALID_LOCATION",
    "message": "位置信息不合法",
    "details": {
      "field": "latitude"
    }
  }
}

说明

  1. 服务端根据当前位置计算日程距离。
  2. 如果日程包含地点且命中围栏,先检查系统引用是否仍存在。
  3. 若日程还包含时间,再结合时间窗口决定是否下发提醒控制消息。

7.7 提醒控制与 TTS 接口

消息类型

  1. reminder.control
  2. reminder.control.ack
  3. reminder.tts.start.command
  4. reminder.tts.stream.started
  5. reminder.tts.stream.ended
  6. reminder.tts.stream.error
  7. reminder.tts.result
  8. reminder.tts.result.ack

作用

  1. 下发主提醒并返回执行结果。
  2. 在线时通过 WebSocket 下发 TTS 音频流。
  3. 返回 TTS 播放结果。
  4. 使用现有提醒控制消息取消客户端 AlarmManager 闹钟。

服务端消息

{
  "type": "reminder.control",
  "message_id": "msg_reminder_001",
  "schedule_id": "schedule_001",
  "updated_at": "2026-07-29T14:45:00+08:00",
  "trigger_kind": "time",
  "triggered_at": "2026-07-29T14:45:00+08:00",
  "reason": "time_window_reached",
  "action": "show",
  "content": {
    "object_type": "schedule",
    "object_id": "schedule_001",
    "template_id": "TIME_ADVANCE",
    "template_version": 1,
    "locale": "zh-CN",
    "rendered_text": "提醒:项目会议将在15分钟后开始。"
  }
}

取消 AlarmManager 闹钟:

{
  "type": "reminder.control",
  "message_id": "msg_alarm_cancel_001",
  "schedule_id": "schedule_001",
  "reason": "schedule_done",
  "action": "cancel",
  "system_alarm_ref_id": "alarm_schedule_001"
}

客户端响应

{
  "type": "reminder.control.ack",
  "message_id": "msg_reminder_001",
  "request_id": "req_reminder_ack_001",
  "schedule_id": "schedule_001",
  "updated_at": "2026-07-29T14:45:00+08:00",
  "trigger_kind": "time",
  "triggered_at": "2026-07-29T14:45:00+08:00",
  "ok": true,
  "connectivity_mode": "online",
  "channels_executed": [
    "vibration",
    "tts",
    "custom_popup"
  ],
  "rendered_text": "提醒:项目会议将在15分钟后开始。"
}

取消结果:

{
  "type": "reminder.control.ack",
  "message_id": "msg_alarm_cancel_001",
  "request_id": "req_alarm_cancel_ack_001",
  "schedule_id": "schedule_001",
  "action": "cancel",
  "system_alarm_ref_id": "alarm_schedule_001",
  "ok": true
}

取消失败:

{
  "type": "reminder.control.ack",
  "message_id": "msg_alarm_cancel_001",
  "request_id": "req_alarm_cancel_ack_001",
  "schedule_id": "schedule_001",
  "action": "cancel",
  "system_alarm_ref_id": "alarm_schedule_001",
  "ok": false,
  "error": {
    "code": "ALARM_CANCEL_FAILED",
    "message": "AlarmManager 闹钟取消失败"
  }
}

客户端消息

{
  "type": "reminder.tts.start.command",
  "request_id": "req_tts_001",
  "payload": {
    "schedule_id": "schedule_001",
    "updated_at": "2026-07-29T14:45:00+08:00",
    "trigger_kind": "time",
    "triggered_at": "2026-07-29T14:45:00+08:00"
  }
}

服务端响应

流开始:

{
  "type": "reminder.tts.stream.started",
  "request_id": "req_tts_001",
  "ok": true,
  "payload": {
    "schedule_id": "schedule_001",
    "updated_at": "2026-07-29T14:45:00+08:00",
    "trigger_kind": "time",
    "triggered_at": "2026-07-29T14:45:00+08:00",
    "stream_id": "tts_stream_001",
    "audio_format": "wav"
  }
}

流结束:

{
  "type": "reminder.tts.stream.ended",
  "schedule_id": "schedule_001",
  "updated_at": "2026-07-29T14:45:00+08:00",
  "trigger_kind": "time",
  "triggered_at": "2026-07-29T14:45:00+08:00",
  "stream_id": "tts_stream_001",
  "status": "completed"
}

失败响应

{
  "type": "reminder.tts.stream.error",
  "schedule_id": "schedule_001",
  "updated_at": "2026-07-29T14:45:00+08:00",
  "trigger_kind": "time",
  "triggered_at": "2026-07-29T14:45:00+08:00",
  "stream_id": "tts_stream_001",
  "error": {
    "code": "TTS_AUDIO_FILE_UNAVAILABLE",
    "message": "云端 TTS 音频不可用"
  }
}

客户端播放结果

{
  "type": "reminder.tts.result",
  "request_id": "req_tts_result_001",
  "schedule_id": "schedule_001",
  "updated_at": "2026-07-29T14:45:00+08:00",
  "trigger_kind": "time",
  "triggered_at": "2026-07-29T14:45:00+08:00",
  "stream_id": "tts_stream_001",
  "status": "completed",
  "reason": null
}

服务端确认

{
  "type": "reminder.tts.result.ack",
  "request_id": "req_tts_result_001",
  "ok": true
}

规则

  1. message_id 用于服务端推送追踪;request_id 用于客户端请求幂等。
  2. 提醒身份由 schedule_id + updated_at + trigger_kind + triggered_at 确定。
  3. 服务端必须校验日程归属、状态和版本;客户端不能指定云端音频对象。
  4. Binary Frame 只能在 reminder.tts.stream.started 成功后发送。
  5. 只有客户端播放器完成回调可以上报 status=completed
  6. TTS 失败不改变 reminder.control.ack 或日程状态。
  7. 离线模式不发起 TTS 流请求。
  8. action=cancel 时,客户端使用 system_alarm_ref_id 重建同一 PendingIntent 并取消 AlarmManager 闹钟。
  9. 客户端完成 AlarmManager 取消并删除对应本地最小记录后才返回 ok=true;任务已不存在时按幂等成功处理。
  10. 服务端只有在成功 ACK 携带的引用仍等于当前 system_alarm_ref_id 时才清空;旧引用的延迟 ACK 不得清空已回写的新引用。
  11. 取消失败返回 ok=false;失败或超时必须保留引用以便重试。

7.8 日程确认消息

作用:

  1. 由客户端主动告诉服务端,用户已经在应用内确认该日程完成。
  2. 关闭后续监听和所有提醒。
  3. 该消息只表达用户显式完成动作。

客户端消息

{
  "type": "schedule.confirmed",
  "request_id": "req_schedule_confirm_001",
  "schedule_id": "schedule_001",
  "confirmed": true,
  "timestamp": "2026-07-28T12:05:00+08:00"
}

服务端处理

  1. 取消监听。
  2. 终止后续提醒。

服务端响应

{
  "type": "schedule.confirmed.ack",
  "request_id": "req_schedule_confirm_001",
  "schedule_id": "schedule_001",
  "ok": true
}

失败响应:

{
  "type": "schedule.confirmed.ack",
  "request_id": "req_schedule_confirm_001",
  "schedule_id": "schedule_001",
  "ok": false,
  "error": {
    "code": "SCHEDULE_CONFIRM_FAILED",
    "message": "日程确认失败",
    "details": {
      "reason": "schedule_not_found"
    }
  }
}

7.9 日程状态变更

完成和删除统一使用显式状态命令;schedule.confirmed 作为现有完成消息继续兼容, 但新客户端优先使用本节命令。

客户端消息

{
  "type": "schedule.status.command",
  "request_id": "req_schedule_status_001",
  "payload": {
    "schedule_id": "schedule_001",
    "target_status": "done",
    "reason": "user_confirmed",
    "expected_updated_at": "2026-07-28T12:00:00+08:00"
  }
}

服务端响应

{
  "type": "schedule.status.result",
  "request_id": "req_schedule_status_001",
  "ok": true,
  "payload": {
    "schedule_id": "schedule_001",
    "status": "done",
    "updated_at": "2026-07-28T12:05:00+08:00",
    "cleanup_required": true
  }
}

规则:

  1. 允许 scheduled -> donescheduled -> deleted
  2. 相同目标状态重复提交返回当前结果。
  3. 已进入 donedeleted 后,不允许直接恢复为 scheduled;恢复需求通过复制或重新创建处理。
  4. expected_updated_at 不匹配时返回 VERSION_CONFLICT 和当前日程摘要,不执行覆盖。
  5. 状态事务提交后停止后续提醒和监听;服务端事件绑定取消成功后清空 system_schedule_ref_id,取消失败则保留引用并重试;TTS 云端文件按事项前缀进入异步清理流程。
  6. system_alarm_ref_id 非空时,通过 reminder.control(action=cancel) 通知客户端取消 AlarmManager 闹钟。
  7. 服务端收到取消成功 ACK 并确认其引用仍为当前值后清空 system_alarm_ref_id;取消失败或超时 保留引用并重试,不回滚业务状态。

7.10 连接保活、重连与恢复

心跳

  1. 客户端按服务端 session.ready 返回的心跳间隔发送 session.ping
  2. 服务端回复 session.pong 并带回 server_time
  3. 连续超过两个心跳周期未收到有效响应时,客户端将连接标记为断开,不再把网络可用等同于服务端在线。

重连

客户端重连成功后发送:

{
  "type": "session.resume",
  "request_id": "req_resume_001",
  "payload": {
    "device_id": "android_abc123",
    "last_schedule_updated_at": "2026-07-28T12:00:00+08:00",
    "pending_request_ids": [
      "req_schedule_001"
    ]
  }
}

服务端处理:

  1. 返回各 pending_request_ids 的已知终态;未知请求由客户端按原 request_id 重发。
  2. 返回 last_schedule_updated_at 之后发生变化的日程。
  3. 服务端根据现有 statusupdated_attime_triggered_atgeo_triggered_at 恢复仍有效的提醒; 重发可以生成新的 message_id
  4. 客户端按现有字段复合键识别已经执行的提醒,不因新的 message_id 重复展示。
  5. 音频 Binary Frame 不做断点续传;断线中的语音流标记失败,客户端重新录音。
  6. 客户端使用增量返回的 system_alarm_ref_id 与最小本地闹钟记录对齐:服务端为空而本地存在 有效记录时重试回写;服务端存在而本地缺失时,仅为仍有效且未过期的日程重新登记同一引用。
  7. 两侧引用不一致时不得直接清空当前引用;先取消旧引用,再同步客户端当前成功登记的引用。
  8. 设备重启后只按本地记录重新登记仍有效且未过期的 AlarmManager 闹钟。

8. 监听与提醒策略

8.1 时间监听

规则:

  1. 只有 start_time 存在的日程才参与时间监听。
  2. 时间到达前进入监测窗口。
  3. 默认提前 15min
  4. 进入窗口后优先判断 WS 在线状态和前后台状态。
  5. 如果 WS 在线,服务端校验事项和云端音频后下发 reminder.control
  6. 如果 WS 不在线,由客户端本地触发条件执行离线提醒。

8.2 空间监听

规则:

  1. 只有存在经纬度的日程才参与空间监听。
  2. 默认围栏半径 100m
  3. 客户端持续上传位置,服务端判定是否进入围栏。
  4. 创建日程时,如果用户当前位置已经在目标围栏内,服务端将 geofence_armed=false
  5. 当用户离开目标围栏后,服务端将 geofence_armed=true
  6. 只有 geofence_armed=true 且用户再次进入围栏时,才允许触发地点提醒。
  7. 如果创建时无法取得用户当前位置,服务端默认 geofence_armed=true,避免错过后续进入提醒。

8.3 触发规则

schedule_type 日程形态 触发规则
time 只有时间 时间窗口到达后触发
location 只有地点 geofence_armed=true 且用户进入地理围栏后触发
time 同时有时间和地点 时间窗口和地理围栏分别形成可触发条件,任一条件首次满足即可提醒

组合日程细化规则:

  1. schedule_type 仍为 time,不新增第三种类型。
  2. 时间条件和地点条件是 OR,不是必须同时满足的 AND;否则用户未在目标地点时可能错过时间提醒。
  3. time_triggered_atgeo_triggered_at 分别记录两个条件首次命中的时间。
  4. 同一日程在一次有效提醒周期内只展示一次。一个条件已经成功展示后,另一个条件随后命中只记录 *_triggered_at,不重复展示。
  5. 提醒展示失败时,尚未展示成功的另一条件仍可触发补偿提醒。

8.4 监听取消

满足以下任一条件时取消监听:

  1. 用户通过确认接口标记已完成。
  2. 有时间的日程时间已过。
  3. 日程被删除。

8.5 提醒执行顺序

为同时满足“优先触达”和“避免重复”,执行顺序固定为:

  1. 服务端判定提醒条件满足。
  2. 服务端下发 reminder.controlmessage_id 只记录本次发送。
  3. 服务端随 reminder.control 下发智能提醒内容、唯一 rendered_text 和运行时计算的 audio_file_name
  4. 客户端主提醒实际展示成功后返回包含同一 rendered_textreminder.control.ack(ok=true)
  5. 在线时客户端执行震动、自定义弹窗和云端 TTS;离线时执行震动、固定铃声和自定义弹窗。
  6. 在线 TTS 播放终态单独发送 reminder.tts.result,但不阻塞主提醒 ACK。
  7. 主提醒失败或 ACK 超时时按同一事项状态复合键重试;重发可使用新的 message_id,但不得 重复播放声音。

禁止等待 TTS 播报完成后才确认主提醒;TTS 失败不能阻止已经展示成功的主提醒返回 ACK。

8.6 固定模板 TTS 执行流程

预生成:

  1. 事项事务成功后,智能提醒模块提供 reminder_body 并发布异步 TTS 任务。
  2. 服务端根据现有事项字段校验归属、版本、提醒内容和触发条件。
  3. 服务端计算 content_hash 和确定性文件名;文件存在则复用,否则通过 TTS 生成网关创建音频。
  4. 预生成失败不影响事项保存,后续任务可以按同一文件名幂等重试。

在线触发:

  1. 服务端确认网络、WebSocket 和会话均有效,重新校验事项并计算 audio_file_name
  2. 客户端立即执行震动和自定义弹窗,并请求播放关联云端 TTS。
  3. 服务端从对象存储读取音频,通过现有 TTS Binary Frame 流下发。
  4. 客户端播放器真实完成后上报 completed;TTS 失败不改变震动、弹窗和主提醒 ACK。

离线触发:

  1. 客户端本地提醒执行器根据预先登记的触发条件运行。
  2. 客户端执行震动、预置固定铃声和自定义弹窗,不访问云端音频或外部 TTS 服务。
  3. 平台后台能力不可用时记录为当前版本的触达限制。
  4. 恢复联网后只同步提醒终态,不补播已经执行的离线提醒。

防重复:

  1. 同一事项和 content_hash 只生成一个云端文件。
  2. 同一 schedule_id + updated_at + trigger_kind + triggered_at 只执行一次可见提醒和一次声音提醒。
  3. 在线与离线路径以触发时的连接状态选择一次,不同时播放 TTS 和固定铃声。

9. 冲突检测

规则

  1. 只有 start_time 存在时才检查时间区间重叠。
  2. 如果已有日程落在同一时间段,返回冲突提示。
  3. 冲突结果包含已有日程的标题、时间和 ID。
  4. 只有地点、没有时间的日程不做时间冲突检测。

9.1 时间区间算法

  1. 两个有结束时间的日程在 new_start < existing_endexisting_start < new_end 时冲突。
  2. 缺少 end_time 时,MVP 使用可配置的默认占用时长进行检测;默认值必须由产品确认, 在确认前不得写死为数据库规则。
  3. 边界相接(例如一个日程 10:00 结束、另一个 10:00 开始)不算冲突。
  4. 只比较同一用户、status=scheduled 且未删除的日程。
  5. 服务端统一转换为 UTC 后计算,响应按各日程原 timezone 展示。

10. 失败、降级与一致性

场景 必须行为 禁止行为
ASR 失败 返回失败阶段,允许重录或手动创建 创建不完整正式日程
LLM 输出缺字段/歧义 返回草稿、缺失项和歧义项,等待用户确认 模型自行补全关键时间或地点后直接写库
WebSocket 断开 本地保留待提交命令,重连后按原 request_id 恢复 仅凭网络连接状态声明在线
软件提醒展示失败 按现有事项状态复合键幂等重试 把新的 message_id 当作新提醒并重复播放
TTS 模板未知或参数非法 降级为 GENERIC,继续展示主提醒 阻塞通知或临时调用 LLM 生成文案
TTS 预生成失败 保留事项和其他提醒通道,按确定性文件名幂等重试 回滚日程或待办创建
云端文件不存在或对象存储不可用 在线 TTS 失败,但继续震动和自定义弹窗 触发时临时信任客户端文件名
在线 TTS 音频流或播放失败 上报失败并保留震动、自定义弹窗和主提醒 把文件读取完成当作用户已听到
断网 执行震动、固定铃声和自定义弹窗,不访问云端 同时尝试云端 TTS
AlarmManager 登记失败 保留日程并使 system_alarm_ref_id 为空 写入不存在的 alarmId
AlarmManager 取消、本地记录清理失败或 ACK 超时 保留 system_alarm_ref_id 并重试 提前清空引用并宣称取消成功
服务端事件绑定取消失败 保留 system_schedule_ref_id 并重试 丢失引用后宣称清理完成
平台后台能力不可用 记录当前版本触达限制 绕过平台限制或宣称提醒已触达
WebSocket 重复下发相同 TTS 按现有事项状态复合键返回已保存终态 message_id 变化再次朗读
位置权限不可用 暂停地点监听并提示权限状态;有时间条件时继续时间提醒 把权限失败当作未进入围栏
服务端重启 schedules 恢复时间扫描和缺失的 system_schedule_ref_id;位置监听等待新位置 依赖仅存在于内存的触发状态宣称完整恢复

一致性边界:

  1. 日程写入和 request_id 幂等结果必须在同一事务边界内提交,或使用能保证原子可见性的等价实现。
  2. 服务端事务提交成功后才能下发 schedule.upsert.result
  3. WebSocket 推送采用至少一次投递;message_id 只用于链路追踪,客户端依靠 schedule_id + updated_at + trigger_kind + triggered_at 去重。
  4. time_triggered_atgeo_triggered_at 只表示条件已命中,不等于客户端已经展示成功。
  5. “已展示”和 TTS 播放终态保存在客户端最小基础设施账本,不修改业务数据库,也不得复用 status 表达投递过程。
  6. TTS 生成按事项和 content_hash 去重,播放按现有事项状态复合键去重。
  7. 对象文件存在只表示音频已准备;必须等待客户端播放器完成回调后才能记录 TTS completed
  8. 在线和离线通道只选择一次;相同提醒不得同时播放云端 TTS 和固定铃声。
  9. system_schedule_ref_idsystem_alarm_ref_id 是基础设施引用,不改变 schedules.status; 引用清理失败不回滚日程状态。

11. 安全、隐私与运维边界

  1. WebSocket 生产环境只允许 wss://
  2. 用户身份必须来自鉴权会话;device_id 只标识设备,不能代替用户认证。
  3. 服务端对每次日程读写和状态变更校验资源归属。
  4. 音频只为本次解析处理;是否持久化、保留时长和删除策略必须显式配置,默认不长期保存原始音频。
  5. 日志不得记录原始音频、完整转写文本、精确经纬度或令牌;排障使用 request_idmessage_idjob_id 和脱敏错误信息。
  6. 位置上报只在存在有效地点日程且用户授权时启用;无有效监听对象时停止高频上报。
  7. ASR、LLM 和地图依赖必须配置超时、有限重试和熔断;重试不得绕过用户确认。
  8. TTS 只使用智能提醒模块允许的 reminder_body,不得播报备注、详细地址、经纬度或令牌。
  9. 在线云端 TTS、离线固定铃声和震动不得绕过系统静音或勿扰策略。
  10. 客户端自定义弹窗只展示经过校验的当前事项内容。
  11. 外部 TTS 凭证只存在于服务端密钥管理系统,不进入客户端、WebSocket 业务响应、 日志或崩溃报告。
  12. 合成文本必须来自智能提醒模块的当前内容并通过现有事项字段校验,不接受客户端自由文本。
  13. 对象文件名不得包含用户明文;对象存储使用私有访问控制,客户端不直接获得供应商凭证。
  14. 音频文件、文件名、URL、哈希和状态不得写入业务数据库;对象生命周期由文件前缀和存储策略管理。

12. 最小验收清单

12.1 契约

  1. 所有命令/查询均能按同一 request_id 重放并得到一致结果。
  2. 相同 request_id、不同载荷返回 IDEMPOTENCY_CONFLICT
  3. 服务端推送重复到达时,即使 message_id 不同,客户端也按现有事项状态复合键只执行一次。
  4. 未知消息、缺失字段、非法状态和版本冲突都有稳定错误码。

12.2 语音与确认

  1. ASR/LLM 成功只生成草稿,用户确认前 schedules 无新增记录。
  2. 缺少时间或地点时表单明确补全,不能静默创建无触发条件的日程。
  3. 音频流中断不会创建正式日程,重连后可重新录音。

12.3 提醒可靠性

  1. 前台在线、后台在线和离线/服务不可达三类场景均有可验证触达路径。
  2. 软件提醒失败后按现有事项状态复合键重试,不重复展示或重复播放。
  3. system_schedule_ref_id 只由服务端维护;system_alarm_ref_id 与客户端 AlarmManager 的 alarmId 一致,二者均不表示系统日历或外部闹钟 App 引用。
  4. 服务端重启后,未完成日程能恢复必要监听和缺失的服务端事件绑定。
  5. TTS 完成、跳过或失败均不改变主提醒 ACK。
  6. 在线状态只执行“震动 + 云端 TTS + 自定义弹窗”,离线状态只执行 “震动 + 固定铃声 + 自定义弹窗”。
  7. 平台后台能力不可用时明确标记为当前版本的触达限制。
  8. 在目标设备允许自启动的条件下,客户端进程异常终止后可以恢复运行。
  9. AlarmManager 登记失败时不会保存虚假的 system_alarm_ref_id
  10. App 被系统普通回收后,已登记的 AlarmManager 闹钟仍可触发客户端进程。
  11. 设备重启后可以根据最小本地闹钟记录重新登记有效闹钟。
  12. 用户强行停止 App 时不承诺闹钟触发;取消失败时保留 system_alarm_ref_id 以便重试。
  13. 修改提醒时间后只保留当前有效的 AlarmManager 登记,旧引用的延迟 ACK 不会清空新引用。
  14. 取消成功后本地最小记录同步删除,已失效或已过期记录不会在设备重启后重新登记。

12.4 时间与地点

  1. 时间-only、地点-only、时间+地点三类日程分别覆盖。
  2. 组合日程任一条件命中可提醒,但一次有效周期只展示一次。
  3. 围栏内创建不会立即触发;离开再进入后可以触发。
  4. 时区转换、夏令时边界、结束时间为空和区间边界相接均有测试。

12.5 验收证据

  1. 文档和 JSON 示例通过静态格式检查。
  2. 接口模型可生成 Schema,并用成功、失败、重复和重连样例做契约测试。
  3. Android 自定义弹窗、震动、固定铃声、云端 TTS 流式播放、后台限制和位置权限必须在真机或 目标模拟器验证。
  4. 只有静态文档检查时,结论必须写为“设计已细化/静态检查通过”,不得写成“提醒能力已实现”。

12.6 固定模板 TTS

  1. 固定头、智能提醒 reminder_body 和固定尾组成的最终文本与定义完全一致。
  2. 日程和待办保存先成功,TTS 在事务提交后异步生成;第三方失败不回滚事项。
  3. 对象文件名严格为 tts/{user_id}/{object_type}/{object_id}/{content_hash}.wav,且不含用户明文。
  4. 数据库结构和记录中不存在新增 TTS 文件名、URL、哈希、状态、音频字段或资产表。
  5. 相同事项和内容重复处理时复用云端文件;内容变化时生成新哈希和新文件。
  6. 删除、完成、取消和失效事项可以按事项前缀清理关联文件。
  7. 服务端拒绝与当前事项归属、版本、提醒内容或哈希不一致的播放请求。
  8. 在线状态执行震动、云端 TTS 和客户端自定义弹窗。
  9. 离线状态执行震动、客户端固定铃声和客户端自定义弹窗,且不访问云端音频或外部 TTS 服务。
  10. 恢复联网后不补播已经完成的离线提醒。
  11. WebSocket 重发不会重复生成云端文件或重复播放声音。
  12. 云端文件读取完成不等于用户已听到,只有播放器完成回调可以上报 completed
  13. TTS 失败不改变智能提醒状态、主提醒 ACK 和其他提醒通道。

Clone this wiki locally