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, 服务端组合固定头尾后调用阿里云千问 3 TTS VD;TTS 失败不影响事项保存或主提醒。
  9. 所有有效提醒在日程或待办创建、确认或相关编辑提交后异步预生成音频,并以确定性文件名 存入云端对象存储;数据库不保存 TTS 文件名、URL、状态或音频数据。
  10. 在线提醒组合为“震动 + 云端 TTS + 客户端自定义弹窗”;离线提醒组合为 “震动 + 客户端固定铃声 + 客户端自定义弹窗”。

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

1.1 MVP 边界

本版明确包含:

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

本版暂不包含:

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

2. 产品功能描述

2.1 核心目标

MVP 要验证三件事:

  1. 用户能不能快速创建日程。
  2. 系统能不能在不同场景下使用不同提醒方式。
  3. 语音创建能不能稳定转成可确认、可执行的日程。

2.2 三种提醒情况

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

触发方式:

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

适用场景:

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

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

触发方式:

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

适用场景:

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

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

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

客户端通过已登记的本地触发条件执行震动、客户端预置固定铃声和客户端自定义弹窗; 断网时不访问云端音频,也不调用阿里云。

2.3 创建日程

日程支持两种入口:

  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 落库,是否触发地点提醒由字段本身判断。

2.4 地理位置

表单内部支持:

  1. 地理位置选择。
  2. 地理围栏设置。
  3. 默认提醒参数补全。

默认值建议:

  1. 时间提前提醒:15min
  2. 地理围栏半径:100m

2.5 冲突检测

创建日程时如果填写了时间,必须做时间冲突检测。

如果目标时间段已有日程,服务端返回:

  1. 冲突提示。
  2. 冲突项列表。
  3. 是否允许继续创建的建议。

冲突检测是提示,不一定是硬拦截。

3. 架构总览

3.1 客户端

客户端负责:

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

3.2 服务端

服务端负责:

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

3.3 外部依赖

  1. 第三方 ASR 服务。
  2. 大模型服务。
  3. 第三方地图 SDK。
  4. Android 客户端震动、音频播放和后台运行能力。
  5. 阿里云百炼 Model Studio / DashScope 千问 3 TTS VD 实时语音合成服务。
  6. 云端对象存储。

阿里云接入边界:

  1. TimeFlow 服务端通过 DashScope 实时 WebSocket 调用 qwen3-tts-vd-realtime-2026-01-15;客户端不得直连阿里云。
  2. DASHSCOPE_API_KEY 和可选的 DASHSCOPE_WORKSPACE_ID 只保存在服务端环境或密钥管理系统中, 不下发到客户端。
  3. 服务端固定配置地域网关、模型 ID 和 DASHSCOPE_TTS_VOICE_ID;客户端不接触供应商凭证。
  4. 合成音频以可播放 .wav 文件写入云端对象存储;数据库不保存文件名、URL、状态或二进制。
  5. voice_id 必须由千问 Voice Design 预先生成,并与 qwen3-tts-vd-realtime-2026-01-15 绑定;提醒触发时不得临时设计声音。
  6. 固定模板文本必须先通过字段白名单、长度和 Unicode 安全校验。

模型选型固定为:

用途 模型 使用方式
提醒音频预生成 qwen3-tts-vd-realtime-2026-01-15 DashScope 实时 WebSocket,生成流写入对象存储
声音设计 qwen-voice-design 部署前一次性生成并人工审批 voice_id

qwen3-tts-vd-2026-01-26 是非实时版本,不用于本方案的流式提醒链路。模型快照升级必须经过 声音兼容、契约、延迟和真机播放验证,不得依赖“latest”隐式漂移。

3.4 模块协作边界

  1. 界面模块只能通过 WebSocket 模块提交业务命令,不直接调用 ASR、LLM 或数据库。
  2. 语音解析模块只产出草稿,不创建正式日程;只有用户确认后的 schedule.upsert.command 才能写入 schedules
  3. 监控与调度模块只读取有效日程并生成提醒控制决策,不直接选择 Android 展示通道。
  4. 提醒执行模块只执行客户端能力并回传结果,不自行修改服务端日程状态。
  5. 地点判定模块只输出“围栏外/围栏内/位置不可用”的判定,不直接发送提醒。
  6. 智能提醒模块拥有提醒内容和触发决策;ReminderTemplateRenderer 只组合 提醒: + reminder_body + 。
  7. 服务端 Qwen3TtsGateway 负责预生成,TtsObjectStorage 负责云端文件生命周期, 客户端 ReminderChannelPolicy 负责在线/离线提醒组合。
  8. TTS 不拥有独立提醒状态,不得因播报成功或失败修改 schedules.status 或主提醒 ACK。
  9. 客户端不得持有或接收 DASHSCOPE_API_KEY;服务端是阿里云实时合成的唯一调用方。
  10. DashScope 鉴权、生成或云端存储失败不得阻塞事项保存或主提醒。
  11. AppProcessSupervisor 只负责主进程健康检查、受控拉起和提醒执行器恢复,不决定提醒内容、 触发条件或在线/离线通道。
  12. 自启动与后台驻留必须服从 Android 和设备厂商限制;用户强制停止或系统拒绝后台启动时, 将本次恢复标记为失败,不设计系统日历、系统闹钟或系统通知兜底。

3.5 ASR 与 TTS 数据方向

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

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

TTS 预生成:
日程/待办创建、确认或相关编辑提交
-> 智能提醒模块生成 reminder_body
-> ReminderTemplateRenderer 组合固定头尾
-> Qwen3TtsGateway 调用阿里云
-> TtsObjectStorage 按确定性文件名写入云端

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

离线提醒:
客户端本地触发条件命中
-> AppProcessSupervisor 检查主进程和提醒执行器
-> 异常时在平台允许范围内拉起客户端并恢复后台驻留
-> 震动 + 客户端固定铃声 + 客户端自定义弹窗

边界规则:

  1. TTS 只消费智能提醒模块的提醒内容,不重新决定提醒时间、优先级或通道。
  2. 在线必须同时满足网络可用、WebSocket 心跳正常和客户端会话有效。
  3. 断网时不访问对象存储、不调用阿里云,也不补播已完成的离线提醒。
  4. 云端音频通过现有 TTS 流接口下发;客户端不直接访问阿里云。
  5. TTS、固定铃声和震动都不改变主提醒 ACK 或事项状态。
  6. 相同事项、内容哈希和提醒消息必须幂等,避免重复生成和重复播放。
  7. 进程拉起失败不改变事项状态;该场景是当前版本的已知触达限制,不触发系统级兜底。

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. 保存本地提醒触发信息;不缓存云端 TTS 音频。

4.1.5 提醒执行模块

职责:

  1. 前台弹窗。
  2. 后台浮窗。
  3. 根据当前应用状态决定提醒展示方式。
  4. 客户端自定义提醒弹窗的样式、布局和交互。
  5. ReminderChannelPolicy 根据在线状态选择“震动 + 云端 TTS + 自定义弹窗”或 “震动 + 固定铃声 + 自定义弹窗”。
  6. TtsStreamPlayer 管理云端音频下行流和播放结果。
  7. LocalReminderExecutor 管理离线震动、客户端预置固定铃声和自定义弹窗。
  8. AppProcessSupervisor 负责检测主进程并恢复提醒执行器,TTS 模块不实现保活。

固定约束:

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

4.1.6 客户端进程监督与自启动模块

组件:AppProcessSupervisor

职责:

  1. 由独立于主界面生命周期的平台执行入口监测客户端主进程心跳和 LocalReminderExecutor 可用性。
  2. 心跳超过阈值后先二次确认进程状态,避免因短暂卡顿、系统调度延迟或切换前后台误判。
  3. 确认异常后生成确定性 restart_request_id,调用平台许可的启动入口,并恢复本地提醒注册、 WebSocket 连接和后台提醒执行器。
  4. 恢复成功后进入后台驻留状态;恢复失败时记录稳定原因码和当前版本触达限制。
  5. 对连续失败实施冷却和指数退避,禁止无限拉起、并发启动和崩溃循环。

状态:

HEALTHY
-> SUSPECTED
-> RESTARTING
-> RESIDENT

RESTARTING
-> BLOCKED
-> BACKOFF
-> RESTARTING

固定约束:

  1. 正常心跳期间不得重复拉起客户端;同一异常周期只允许一个有效启动请求。
  2. 用户退出登录、显式关闭后台运行或撤销必要权限后停止自动拉起,并清理仅属于该会话的监听。
  3. 监测数据只包含进程标识、心跳时间、启动次数和结果,不包含标题、提醒正文、位置或音频。
  4. Android 后台启动、前台服务及厂商保活策略可能拒绝拉起;用户强制停止后也不保证自动恢复。
  5. 系统拒绝时上报 KEEPALIVE_RESTART_BLOCKED,不得绕过平台安全策略;本版本暂不设计 系统通知、系统闹钟或系统日历兜底。
  6. 本模块只恢复执行环境,不补播已错过或已完成的提醒;是否仍应触发由现有提醒状态和 幂等规则判断。

4.1.7 地图与位置模块

职责:

  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 Qwen3-TTS-VD Gateway 与流式转发模块

职责:

  1. Qwen3TtsGateway 从服务端密钥与配置系统读取 DashScope 凭证、模型和 voice_id
  2. 在异步预生成任务中发送经校验的 rendered_text,第三方失败不回滚事项事务。
  3. 将生成结果交给 TtsObjectStorage,不直接写业务数据库。
  4. 相同事项和 content_hash 已存在云端文件时直接复用,不重复调用第三方。

4.2.9 云端 TTS 文件存储模块

职责:

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

4.2.10 Voice Design 配置流程

  1. Voice Design 只在部署准备阶段执行,生成声音后必须经过人工试听确认。
  2. 审批后的 voice_id 写入服务端配置,并与运行时模型保持匹配。
  3. 提醒执行期间不创建设计声音;声音变更按配置版本发布并可回滚。

5. 数据库设计

5.1 表名:schedules

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

用途:

  1. 存储日程本体。
  2. 存储日程状态。
  3. 存储地点与提醒关联信息。
  4. 存储地理围栏布防状态。

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 地点提醒触发时间,可为空
created_at text 创建时间
updated_at text 更新时间

5.3 设计原则

  1. 核心字段尽量扁平化。
  2. 语音解析草稿只通过 WS 传递,不落库。
  3. 不保存系统日历 ID、系统闹钟 ID 或其他系统侧兜底引用。
  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. 云端文件存在性通过对象存储检查,不通过数据库记录判断。
  18. 不增加 delivery_id、提醒实例 ID 或提醒投递表;提醒执行复合键根据现有 idupdated_attime_triggered_atgeo_triggered_at 在运行时计算。

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,客户端传入值不得覆盖。

5.5 最小索引

MVP 至少建立:

  1. (user_id, status, start_time):日程列表、时间窗口扫描和冲突检测。
  2. (user_id, updated_at):重连后的增量对齐。

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

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
  }
}

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

字段 类型 必填 说明
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

成功响应

{
  "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
  }
}

冲突响应

{
  "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
  }
}

失败响应

{
  "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 不传,服务端根据最近一次位置上报与目标地点距离计算默认值。

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,
        "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. 返回日程不包含系统日历 ID、系统闹钟 ID 或系统兜底引用。

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 架构契约

TTS 只增加语音提醒维度。智能提醒模块提供 reminder_body 和触发决策;服务端负责固定头尾、 预生成云端音频和在线下发;客户端负责在线/离线通道执行。

固定播报内容

rendered_text = “提醒:” + reminder_body + “。”
template_id reminder_body 最终 rendered_text
TIME_ADVANCE {title}将在{minutes_before}分钟后开始 提醒:{title}将在{minutes_before}分钟后开始。
TIME_DUE {title}现在开始 提醒:{title}现在开始。
LOCATION_ENTER 你已到达{location_name},请处理{title} 提醒:你已到达{location_name},请处理{title}。
GENERIC 请查看日程“{title}” 提醒:请查看日程“{title}”。

规则:

  1. reminder_body 由智能提醒模块提供,TTS 模块不重新生成提醒内容或规则。
  2. 标题为空时使用“一项日程”;提前分钟或地点非法时降级为 GENERIC
  3. 不播报备注、详细地址、坐标或其他非白名单内容。
  4. 通知、自定义弹窗和 TTS 使用同一个 rendered_text

云端音频文件关联

确定性对象文件名:

tts/{user_id}/{object_type}/{object_id}/{content_hash}.wav
组成 来源
user_id 当前鉴权用户
object_type scheduletodo
object_id 已有日程或待办 ID
content_hash 固定头、reminder_body、固定尾、模板版本和音色版本的哈希

约束:

  1. 文件名不得包含标题、地点、备注等用户明文内容。
  2. 相同事项和内容得到相同文件名,文件已存在时直接复用。
  3. 服务端根据当前事项重新计算文件名,不信任客户端提交的任意文件名。
  4. 数据库不保存文件名、URL、哈希、生成状态或音频二进制。
  5. 编辑产生新哈希时生成新文件;删除、完成、取消或失效时按事项前缀清理旧文件。
  6. 云端文件存在性通过对象存储检查,不通过数据库字段判断。

异步预生成

  1. 日程或待办创建、确认或相关编辑事务提交成功后,发布异步 TTS 预生成任务。
  2. 任务读取智能提醒模块的当前提醒内容,完成校验和文件名计算。
  3. 目标文件已存在时结束任务;不存在时调用千问 3 TTS VD 并写入对象存储。
  4. 第三方生成或对象存储失败只记录任务失败,不回滚事项事务。
  5. 相同事项和 content_hash 的重复任务必须幂等。

主提醒消息

{
  "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分钟后开始。",
    "audio_file_name": "tts/user_001/schedule/schedule_001/a82f1c9d.wav",
    "params": {
      "title": "项目会议",
      "minutes_before": 15,
      "location_name": "一号会议室"
    }
  }
}

audio_file_name 是运行时派生的消息字段,不写入数据库。服务端下发前必须重新校验事项归属、 有效状态、updated_at、对应 *_triggered_at、提醒内容和对象文件是否存在。 message_id 只追踪本次发送;提醒业务身份由 schedule_id + updated_at + trigger_kind + triggered_at 确定。

在线与离线执行结果

客户端在线时执行“震动 + 云端 TTS + 自定义弹窗”;离线时执行 “震动 + 固定铃声 + 自定义弹窗”。

{
  "type": "reminder.control.ack",
  "message_id": "msg_reminder_ack_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分钟后开始。"
}

connectivity_mode 只能是 onlineoffline。离线时 channels_executed 固定为 vibrationfixed_ringtonecustom_popup, 不得访问云端文件或调用阿里云。

在线 TTS 流接口

消息 方向 用途
reminder.tts.start.command 客户端 → 服务端 使用现有事项和触发状态请求播放已生成的云端文件
reminder.tts.stream.started 服务端 → 客户端 声明流、云端文件名和音频格式
WebSocket Binary Frame 服务端 → 客户端 流式下发对象存储中的音频数据
reminder.tts.stream.ended 服务端 → 客户端 表示云端文件发送完成
reminder.tts.stream.error 服务端 → 客户端 表示校验、文件读取或传输失败
reminder.tts.cancel.command/result 双向 取消活动播放流
reminder.tts.result/result.ack 双向 上报并确认真实播放结果

启动请求:

{
  "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",
    "source": "cloud_object_storage",
    "audio_file_name": "tts/user_001/schedule/schedule_001/a82f1c9d.wav",
    "audio_format": "wav"
  }
}

服务端不得直接采用客户端提供的文件名;必须通过 schedule_id 查询当前事项,并校验 updated_attrigger_kind 和对应 *_triggered_at 后重新计算。 同一设备同一时刻只允许一个活动 TTS 下行流。

流结束:

{
  "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 音频不可用"
  }
}

架构级错误包括事项校验失败、云端文件不存在、对象存储不可用、音频流中断和播放失败。 TTS 错误不得改变主提醒 ACK 或事项状态。

播放结果:

{
  "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
}

只有客户端播放器完成回调可以上报 completed。相同 request_id 必须返回同一结果; 相同 schedule_id + updated_at + trigger_kind + triggered_at 不得重复播放。

离线执行约束

  1. 创建或同步提醒时,客户端预先登记本地触发条件。
  2. 离线命中后由 LocalReminderExecutor 执行震动、预置固定铃声和自定义弹窗。
  3. AppProcessSupervisor 持续检查主进程心跳;确认异常后在平台允许范围内幂等拉起客户端, 恢复 LocalReminderExecutor 和后台驻留。
  4. 离线提醒不发送 TTS 启动命令,不读取 audio_file_name
  5. 恢复联网后只同步提醒终态,不补播已经执行的离线提醒。
  6. 用户强制停止或系统拒绝后台启动时上报 KEEPALIVE_RESTART_BLOCKED;该情况作为当前版本 已知触达限制记录,不调用系统级兜底。

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. 状态事务提交后停止后续提醒和监听;TTS 云端文件按事项前缀进入异步清理流程。

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 不做断点续传;断线中的语音流标记失败,客户端重新录音。

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 和确定性文件名;文件存在则复用,否则调用阿里云并写入对象存储。
  4. 预生成失败不影响事项保存,后续任务可以按同一文件名幂等重试。

在线触发:

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

离线触发:

  1. 客户端本地提醒执行器根据预先登记的触发条件运行。
  2. AppProcessSupervisor 持续检查主进程心跳;异常超过阈值并经二次确认后,使用同一 restart_request_id 发起一次受控拉起。
  3. 平台允许启动时恢复客户端后台驻留、本地触发注册和 LocalReminderExecutor
  4. 客户端执行震动、预置固定铃声和自定义弹窗,不访问云端或阿里云。
  5. 平台拒绝启动或用户强制停止时上报 KEEPALIVE_RESTART_BLOCKED,记录为当前版本已知触达限制。
  6. 恢复联网后只同步提醒终态,不补播已经执行的离线提醒。

防重复:

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

9. 冲突检测

规则

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

交互建议

  1. 提示用户“当前时段已有日程”。
  2. 用户可继续保存,也可修改时间。
  3. 冲突不是强制失败,但必须明确提示。

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
客户端主进程心跳异常 二次确认后按 restart_request_id 幂等拉起并恢复提醒执行器 并发启动、无限拉起或崩溃循环
系统拒绝后台启动或用户强制停止 上报 KEEPALIVE_RESTART_BLOCKED 并记录当前版本触达失败 绕过平台限制或宣称客户端已恢复
WebSocket 重复下发相同 TTS 按现有事项状态复合键返回已保存终态 message_id 变化再次朗读
位置权限不可用 暂停地点监听并提示权限状态;有时间条件时继续时间提醒 把权限失败当作未进入围栏
服务端重启 schedules 恢复时间扫描;位置监听等待新位置 依赖仅存在于内存的触发状态宣称完整恢复

一致性边界:

  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 和固定铃声。

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. DASHSCOPE_API_KEY 和可选 Workspace ID 只存在于服务端密钥管理系统,不进入客户端、 WebSocket 业务响应、日志或崩溃报告。
  12. 合成文本必须来自智能提醒模块的当前内容并通过现有事项字段校验,不接受客户端自由文本。
  13. 对象文件名不得包含用户明文;对象存储使用私有访问控制,客户端不直接获得供应商凭证。
  14. 音频文件、文件名、URL、哈希和状态不得写入业务数据库;对象生命周期由文件前缀和存储策略管理。
  15. 进程监督仅采集最小健康指标;启动尝试必须限频、可审计,并在退出登录或用户关闭后台运行后停止。

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. 数据库和 WebSocket 契约均不包含系统日历 ID、系统闹钟 ID 或系统引用流程。
  4. 设备重启、App 进程死亡和服务端重启后,未完成日程能在平台允许范围内恢复监听。
  5. TTS 完成、跳过或失败均不改变主提醒 ACK。
  6. 在线状态只执行“震动 + 云端 TTS + 自定义弹窗”,离线状态只执行 “震动 + 固定铃声 + 自定义弹窗”。
  7. 主进程心跳正常时不重复拉起;心跳异常经二次确认后只产生一个幂等启动请求。
  8. 启动成功后能恢复本地触发注册和提醒执行器;连续失败进入退避,不形成重启循环。
  9. 用户强制停止或系统拒绝后台启动时能记录 KEEPALIVE_RESTART_BLOCKED,并明确标记为 当前版本的触达限制。

12.4 时间与地点

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

12.5 验收证据

  1. 文档和 JSON 示例通过静态格式检查。
  2. 接口模型可生成 Schema,并用成功、失败、重复和重连样例做契约测试。
  3. Android 自定义弹窗、震动、固定铃声、千问 3 TTS VD 流式播放、后台限制和位置权限必须在真机或 目标模拟器验证。
  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. 离线状态执行震动、客户端固定铃声和客户端自定义弹窗,且不访问云端或阿里云。
  10. 恢复联网后不补播已经完成的离线提醒。
  11. WebSocket 重发不会重复生成云端文件或重复播放声音。
  12. 云端文件读取完成不等于用户已听到,只有播放器完成回调可以上报 completed
  13. TTS 失败不改变智能提醒状态、主提醒 ACK 和其他提醒通道。
  14. 进程监督、自启动、后台驻留、在线/离线切换、固定铃声和自定义弹窗均有目标设备及主要 厂商系统验证记录。

13. 官方参考

  1. 千问 3 TTS VD 模型说明
  2. 语音合成模型列表
  3. 千问实时语音合成 WebSocket 交互流程
  4. 千问实时语音合成客户端事件
  5. 声音设计使用指南
  6. Android 后台启动前台服务限制
  7. Android 后台 Activity 启动限制

Clone this wiki locally