-
Notifications
You must be signed in to change notification settings - Fork 6
Architecture interface design
- 客户端不再独立承担全部语音理解和提醒编排。
- 客户端进入界面后建立 WebSocket 连接,作为后续实时通信主通道。
- 音频通过 WebSocket Binary Frame 流式传输到服务端。
- 服务端完成 ASR + LLM 结构化提取后,通过 WebSocket 回传结果。
- 客户端用统一表单完成二次确认、地理位置补全、时间补全和提醒参数补全。
- 日程最终落到单一
schedules表中,状态和关联信息一并保存。 - 提醒采用三段式策略,优先保证触达,再处理重复提醒控制。
- TTS 是提醒增强通道:文案使用固定模板,由阿里云百炼
qwen3-tts-vd-realtime-2026-01-15实时合成,不调用 LLM 生成;TTS 失败不影响主提醒。 - TimeFlow 服务端先生成固定模板的唯一
rendered_text,将同一文本用于客户端通知和 阿里云 TTS 请求;TTS 输入不是客户端录音。TimeFlow 服务端接收供应商流式音频, 转换后通过现有业务 WebSocket Binary Frame 下发,客户端边收边播。
这一版的重点不是“完整日历”,而是“日程创建 + 状态驱动提醒 + 语音结构化”闭环。
本版明确包含:
- 单用户、单主要设备上的日程创建、编辑、查询、完成和删除。
- 一条活动 WebSocket 连接上的语音流、业务命令、查询和服务端推送。
- 时间提醒、地点提醒,以及同时包含时间和地点的日程。
- 系统日程和系统闹钟作为客户端兜底,并将系统引用回写服务端。
- 断线重连、重复消息和进程重启下的最小恢复能力。
- 前台 TTS 播报,以及用户显式开启后的后台 TTS 尝试。
本版暂不包含:
- 多用户共享日历。
- 多设备之间的系统日历/闹钟引用同步。
- 周期性日程和复杂重复规则。
- 服务端直接操作 Android 系统日历、系统闹钟或系统通知。
- 用 LLM 自主决定最终日程或提醒时间;LLM 只生成待用户确认的草稿。
- 锁屏或 App 进程死亡后的 TTS 播报;这些状态继续使用系统通知、提示音或震动。
MVP 要验证三件事:
- 用户能不能快速创建日程。
- 系统能不能在不同场景下使用不同提醒方式。
- 语音创建能不能稳定转成可确认、可执行的日程。
触发方式:
- 应用内弹窗。
- 页面内高亮提示。
- 伴随声音或震动。
- 用户开启 TTS 后,按固定模板播报提醒内容。
适用场景:
- 用户正在使用 App。
- 可以给出最完整的上下文。
- 不需要系统级强打断。
触发方式:
- 系统级全局弹窗。
- 系统通知栏提醒。
- 只有用户显式开启后台 TTS、设备未锁屏且平台允许时,才尝试固定模板语音播报。
适用场景:
- App 仍保持在线。
- 需要由系统层接管提醒触达。
- 可以通过 WS 收到实时控制消息。
这里的“网络通畅”必须同时满足:客户端网络可用、WebSocket 心跳正常、服务端会话有效。 仅检测到 Wi-Fi/蜂窝网络不能判定为此情况;后台进程或 WS 不可用时立即按情况三降级。
客户端根据自身能力自行决定是否降级为普通通知、提示音或震动,不执行 TTS 播报。
日程支持两种入口:
- 手动创建。
- 语音创建。
二者共用一个表单。
语音创建流程:
- 客户端录音。
- 客户端通过 WebSocket Binary Frame 流式发送音频。
- 服务端返回上传受理结果。
- 服务端完成 ASR。
- 服务端完成 LLM 结构化提取。
- 服务端通过 WS 推送结构化日程草稿。
- 客户端弹出统一表单。
- 用户补全信息并确认。
- 客户端提交日程最终结果。
- 如果日程包含时间,服务端做时间冲突检测并返回提示。
- 创建成功后,服务端和客户端分别进入自己的提醒准备状态。
日程允许两种主类型:
| 形态 | schedule_type |
必要信息 | 触发条件 |
|---|---|---|---|
| 时间类 | time |
start_time |
进入时间提醒窗口 |
| 地点类 | location |
latitude + longitude
|
用户进入地理围栏 |
语音创建时,schedule_type 由 LLM 根据用户语义做意图识别后输出。手动创建时,由前端根据用户选择或填写内容确定。
schedule_type 只表示主意图类型,时间和地点同时存在时仍按 time 落库,是否触发地点提醒由字段本身判断。
表单内部支持:
- 地理位置选择。
- 地理围栏设置。
- 默认提醒参数补全。
默认值建议:
- 时间提前提醒:
15min - 地理围栏半径:
100m
创建日程时如果填写了时间,必须做时间冲突检测。
如果目标时间段已有日程,服务端返回:
- 冲突提示。
- 冲突项列表。
- 是否允许继续创建的建议。
冲突检测是提示,不一定是硬拦截。
客户端负责:
- 界面展示。
- WebSocket 长连接。
- 音频录制。
- 音频上传。
- 表单编辑和二次确认。
- 本地日程缓存。
- 系统日程和系统闹钟创建、查询与删除。
- 前台弹窗提醒。
- 后台系统弹窗/通知提醒。
- 位置信息上报。
- 根据前后台状态和系统可用性选择具体提醒通道。
- 使用固定模板渲染通知与 TTS 共用文案,并按客户端设置执行可选 TTS 播报。
- 接收 TimeFlow WebSocket 下行的 PCM Binary Frame,使用有界缓冲流式播放。
服务端负责:
- 音频文件接收。
- ASR 调用。
- LLM 结构化提取。
- WebSocket 消息分发。
- 日程冲突检测。
- 日程状态监控。
- 地点与时间窗口判断。
- 提醒控制消息下发。
- 提醒通道选择由客户端自行完成。
- 选择固定提醒模板和参数,通过
ReminderTemplateRenderer生成唯一rendered_text; 服务端不调用 LLM 生成提醒播报文案。 - 安全保管
DASHSCOPE_API_KEY,代理千问 3 TTS VD 实时会话并向客户端流式转发 PCM。
- 第三方 ASR 服务。
- 大模型服务。
- 第三方地图 SDK。
- Android 系统提醒能力。
- 阿里云百炼 Model Studio / DashScope 千问 3 TTS VD 实时语音合成服务。
阿里云接入边界:
- TimeFlow 服务端通过 DashScope 实时 WebSocket 调用
qwen3-tts-vd-realtime-2026-01-15;客户端不得直连阿里云。 -
DASHSCOPE_API_KEY和可选的DASHSCOPE_WORKSPACE_ID只保存在服务端环境或密钥管理系统中, 不下发到客户端。 - 服务端固定配置地域网关、模型 ID 和
DASHSCOPE_TTS_VOICE_ID;客户端只接收不含密钥的 流开始元数据和 PCM 音频分片。 - 合成使用适合客户端流式播放的 PCM 格式;具体采样参数属于接口实现配置,不写入日程业务数据。
-
voice_id必须由千问 Voice Design 预先生成,并与qwen3-tts-vd-realtime-2026-01-15绑定;提醒触发时不得临时设计声音。 - 固定模板文本必须先通过字段白名单、长度和 Unicode 安全校验。
模型选型固定为:
| 用途 | 模型 | 使用方式 |
|---|---|---|
| 提醒实时合成 | qwen3-tts-vd-realtime-2026-01-15 |
DashScope 实时 WebSocket,流式返回音频 |
| 声音设计 | qwen-voice-design |
部署前一次性生成并人工审批 voice_id
|
qwen3-tts-vd-2026-01-26 是非实时版本,不用于本方案的流式提醒链路。模型快照升级必须经过
声音兼容、契约、延迟和真机播放验证,不得依赖“latest”隐式漂移。
- 界面模块只能通过 WebSocket 模块提交业务命令,不直接调用 ASR、LLM 或数据库。
- 语音解析模块只产出草稿,不创建正式日程;只有用户确认后的
schedule.upsert.command才能写入schedules。 - 监控与调度模块只读取有效日程并生成提醒控制决策,不直接选择 Android 展示通道。
- 提醒执行模块只执行客户端能力并回传结果,不自行修改服务端日程状态。
- 地点判定模块只输出“围栏外/围栏内/位置不可用”的判定,不直接发送提醒。
- 系统日程和系统闹钟属于客户端平台资源;服务端只保存引用 ID 和协调检查、清理流程。
- 服务端
ReminderTemplateRenderer生成通知与 TTS 共用的唯一rendered_text; 客户端只做模板一致性校验和本地降级,不得为 TTS 另行拼接文本。 - 服务端
Qwen3TtsGateway管理阿里云会话,客户端TtsPlaybackPolicy决定是否启动,TtsStreamPlayer管理播放和结果上报。 - TTS 不拥有独立提醒状态,不得因播报成功或失败修改
schedules.status或主提醒 ACK。 - 客户端不得持有或接收
DASHSCOPE_API_KEY;服务端是阿里云实时合成的唯一调用方。 - DashScope 鉴权或调用失败不得阻塞主提醒。
两条音频链路必须严格区分:
ASR 上行:
用户说话
-> 客户端麦克风/语音录制模块
-> PCM Binary Frame
-> TimeFlow 业务 WebSocket
-> TimeFlow 服务端
-> ASR 服务
-> 文本与结构化日程草稿
TTS 下行:
TimeFlow 服务端 ReminderTemplateRenderer
-> 固定模板 rendered_text(UTF-8 文本)
-> reminder.control(模板、参数、同一 rendered_text)
-> 客户端展示通知
-> reminder.tts.start.command(仅引用 message_id)
-> TimeFlow 服务端读取已保存的同一 rendered_text
-> Qwen3TtsGateway
-> DashScope 实时语音合成
-> TtsStreamRelay 接收并转换供应商音频流
-> TimeFlow 业务 WebSocket Binary Frame
-> 客户端 TtsStreamPlayer
-> 扬声器/耳机
边界规则:
- ASR 输入来自用户麦克风;TTS 输入来自固定模板文本,TTS 不接收用户录音。
- ASR 音频是客户端到服务端的上行 Binary Frame;TTS 音频是服务端到客户端的下行 Binary Frame,均复用 TimeFlow WebSocket,但方向和控制状态不同。
- TimeFlow 服务端负责连接 DashScope、接收并转发流式音频,不持久化合成音频。
- 阿里云只接收服务端生成并保存的固定模板
rendered_text,不接收客户端录音或自由文本。 - 客户端负责有界缓冲、音频焦点、播放完成判定和资源释放。
- 每个设备同一时刻只允许一个活动 TTS 下行流;ASR 与 TTS 的
stream_id和 Binary Frame 状态机不得混用。
职责:
- 显示日程列表。
- 显示创建/编辑表单。
- 显示语音解析结果。
- 显示冲突提示。
职责:
- 录音。
- 音频格式转换。
- 发起上传。
职责:
- 建立和维护连接。
- 接收结构化草稿。
- 接收提醒控制消息。
- 上传位置信息。
- 上传日程确认结果。
职责:
- 缓存日程。
- 缓存解析结果。
- 缓存提醒状态。
- 缓存提醒通道状态。
- 保存客户端设置
tts_enabled=false和tts_background_enabled=false。 - 按
message_id保存本次 TTS 终态,防止 WebSocket 重试造成重复播报。
职责:
- 前台弹窗。
- 后台浮窗。
- 系统通知。
- 根据当前应用状态决定提醒展示方式。
-
ReminderTemplateRenderer校验服务端下发的模板、参数和rendered_text是否一致; 仅在离线兜底或版本兼容时按同一规范本地渲染。 -
TtsPlaybackPolicy根据全局开关、前后台、锁屏、音频焦点和平台能力决定是否播报。 -
TtsStreamPlayer管理 TimeFlow PCM 下行流、音频焦点、有界缓冲、播放、停止、 完成回调和资源释放。
固定约束:
-
tts_enabled默认false;关闭时不申请 TTS 流。 - 前台只有
tts_enabled=true时允许播报。 - 后台必须同时满足
tts_enabled=true、tts_background_enabled=true、设备未锁屏和平台允许。 - 锁屏、进程死亡或后台音频受限时跳过 TTS,主提醒继续使用系统通知、提示音或震动。
- 不强制修改系统音量,不绕过勿扰模式;无法取得音频焦点时跳过播报。
- TTS 使用稳定
utterance_id;同一message_id进入终态后不得重复朗读。 - 只有通过播放策略后才发送
reminder.tts.start.command,避免为必然跳过的提醒产生外部调用和费用。 - 客户端不接触阿里云凭证;流结束、失败或取消后立即清空 PCM 缓冲。
职责:
- 地点搜索。
- 地点确认。
- 地理围栏设置。
- 实时位置上报。
职责:
- 接收音频文件。
- 校验格式和大小。
- 生成处理任务 ID。
职责:
- 调用 ASR。
- 调用 LLM。
- 识别
schedule_type。 - 生成结构化草稿。
职责:
- 建立客户端会话。
- 维护设备在线状态。
- 分发解析结果和提醒控制消息。
- 接收位置上报。
职责:
- 创建日程。
- 编辑日程。
- 查询日程。
- 冲突检测。
职责:
- 监听日程时间。
- 监听地理位置。
- 判断时间、空间以及组合触发条件。
- 进入提醒窗口后先触发系统引用检查。
- 根据检查结果决定是否触发软件提醒。
- 触发系统日程和系统闹钟删除指令。
- 根据触发原因选择固定模板编号、模板版本和允许的参数,不生成自然语言文案。
- 调用服务端
ReminderTemplateRenderer生成唯一rendered_text,写入可靠提醒消息记录, 供客户端展示和后续 TTS 合成共同引用。
职责:
- 根据上报位置和日程坐标计算距离。
- 判断是否进入地理围栏。
职责:
-
ReminderTemplateRenderer固定使用开头提醒:和结尾。,根据template_id、template_version与白名单参数生成中间reminder_body,再组合出 唯一rendered_text,不调用 LLM。 - 将模板字段、
reminder_body、rendered_text和message_id一起写入可靠提醒消息记录; 不写入schedules业务正文。 -
reminder.control、主提醒 ACK 校验和Qwen3TtsGateway只能引用这一记录, 不得各自重新拼接提醒内容。 - 客户端降级渲染规范与服务端保持同版本;发现内容不一致时禁止启动 TTS,并上报契约错误。
职责:
-
Qwen3TtsGateway从服务端密钥与配置系统读取 DashScope 凭证、模型和voice_id, 建立阿里云实时合成会话。 - 网关只发送可靠提醒消息记录中的
rendered_text,不接受客户端自由文本。 -
TtsStreamRelay将供应商音频流转换为 TimeFlow WebSocket 下行 Binary Frame, 并执行有界背压。 - 客户端断线、取消、缓冲超限或播放失败时终止供应商会话。
- 不记录密钥或完整合成文本,不持久化音频,只记录脱敏的请求、流和错误标识。
- Voice Design 只在部署准备阶段执行,生成声音后必须经过人工试听确认。
- 审批后的
voice_id写入服务端配置,并与运行时模型保持匹配。 - 提醒执行期间不创建设计声音;声音变更按配置版本发布并可回滚。
MVP 阶段只保留一张核心业务表。
用途:
- 存储日程本体。
- 存储日程状态。
- 存储地点与提醒关联信息。
- 存储地理围栏布防状态。
| 字段 | 类型 | 说明 |
|---|---|---|
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 | 系统闹钟 ID,可为空 |
created_at |
text | 创建时间 |
updated_at |
text | 更新时间 |
- 核心字段尽量扁平化。
- 语音解析草稿只通过 WS 传递,不落库。
- 仅保存系统日历日程 ID 和系统闹钟 ID,不保存其他系统侧兜底对象。
- 时间提醒和地理提醒可以同时存在。
- 只保留一张主表,MVP 不拆分额外业务表。
-
status只表达日程本体是否还有效,不表达监听中、已触发、已过期等过程状态。 - MVP 不保存用户自定义的重要程度,提醒方式由应用前后台状态、网络状态和时间/空间窗口决定。
- 冲突检测结果只在接口响应中返回,不写入
schedules表。 -
start_time和地点信息都允许为空,但二者不能同时为空。 -
geofence_armed用于避免“用户在目标地点创建日程后立刻触发位置提醒”。 - 最近一次位置只在会话内或内存中计算,不落库。
-
schedule_type是前端表单必填项控制和后端校验的依据,不再使用全天字段区分业务类型。 - TTS 开关和播报终态属于客户端基础设施状态,不写入
schedules。 - 固定模板不是业务事实,不增加
tts_text、tts_template等数据库字段。
数据库迁移和服务端模型必须同时实现以下约束:
-
id、user_id、source_mode、schedule_type、status、title、created_at、updated_at不为空。 -
source_mode只能是manual或voice。 -
schedule_type只能是time或location。 -
status只能是scheduled、done或deleted。 -
title去除首尾空白后不能为空。 -
start_time和end_time同时存在时,end_time >= start_time。 -
latitude和longitude必须同时为空或同时有值;纬度范围为[-90, 90],经度范围为[-180, 180]。 -
start_time与经纬度不能同时为空。 -
schedule_type=time时start_time必填;schedule_type=location时经纬度必填。 -
geofence_radius_meters > 0,time_remind_offset_minutes >= 0。 - 所有时间统一按带时区 ISO-8601 接口值解析,数据库内部按 UTC 保存;返回客户端时保留
timezone用于展示。 - 更新时由服务端生成新的
updated_at,客户端传入值不得覆盖。
MVP 至少建立:
-
(user_id, status, start_time):日程列表、时间窗口扫描和冲突检测。 -
(user_id, updated_at):重连后的增量对齐。 -
system_schedule_ref_id非空值索引:系统日程引用定位。 -
system_alarm_ref_id非空值索引:系统闹钟引用定位。
索引是查询和调度约束,不改变“一张核心业务表”的方案。
建议状态流转:
scheduled -> done
scheduled -> deleted
状态说明:
| 状态 | 含义 |
|---|---|
scheduled |
已创建,等待提醒或正在监听 |
done |
用户已确认完成 |
deleted |
用户删除 |
不进入 status 的过程信息:
| 信息 | 处理方式 |
|---|---|
| 草稿待确认 | 语音解析结果通过 WS 推给前端,用户确认前不创建正式日程 |
| 时间监听中 | 根据 start_time、time_remind_offset_minutes 和当前时间动态判断 |
| 地理监听中 | 根据经纬度、围栏半径、最近位置和 geofence_armed 动态判断 |
| 已触发提醒 | 写入 time_triggered_at 或 geo_triggered_at
|
| 已过期 | 根据 start_time 或 end_time 动态判断 |
| 创建冲突 | 只在接口响应中返回 conflicts,不持久化 |
业务接口统一使用 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 状态码,通过 *.error、ok=false 和 error 对象表达。
补充约定:
- 客户端发起的命令、查询和 ACK 都必须携带
request_id;服务端使用同一个request_id返回结果,客户端重试时不得生成新值。 - 服务端主动推送必须携带全局唯一
message_id;客户端对同一message_id只执行一次,并可重复回传同一 ACK。 -
request_id的幂等范围是“当前用户 + 消息类型”;服务端至少保存到该操作进入终态。 - 同一
request_id若收到不同payload,返回IDEMPOTENCY_CONFLICT,不得覆盖第一次结果。 -
*.command、*.query、*.result和*.error的业务字段统一放入payload; 7.4—7.11 已有事件型消息保留根部业务字段以避免大改,后续不得在同一消息类型中混用两种结构。 - 未知
type返回UNSUPPORTED_MESSAGE_TYPE;多余字段按协议版本策略处理, 缺少必填字段返回VALIDATION_ERROR。 - 服务端推送只有在收到业务 ACK 后才视为客户端已执行;“WebSocket 已发送”不等于“提醒已展示”。
- 建立单次语音流。
- 使用 Binary Frame 持续发送音频分片。
- 触发后续 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
}
}
}说明:
- 不再提供 HTTP 音频上传接口。
- JSON Text Frame 只传控制信息,音频内容只通过 Binary Frame 发送。
- 真正的结构化结果仍通过
voice.parse.result返回。
schedule.upsert.command
- 手动创建日程。
- 语音表单确认后提交日程。
- 写入最终状态。
- 执行时间冲突检测。
{
"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 必填"
}
}
}规则:
- 手动创建和语音确认都使用
schedule.upsert.command。 - 时间冲突只给提示,不默认阻断。
-
schedule_type=time时,前端表单要求填写时间,地点可选。 -
schedule_type=location时,前端表单要求填写地点,时间可选。 - 用户同时填写时间和地点时,
schedule_type仍按time处理。 - 只有
start_time存在时才做时间冲突检测。 - 只有经纬度存在时才做地理围栏监听。
- 如果
geofence_armed不传,服务端根据最近一次位置上报与目标地点距离计算默认值。
schedule.list.query
- 获取当前用户的日程数据。
- 支持前端进入页面后初始化列表。
- 支持前端恢复本地状态和服务端状态对齐。
{
"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_schedule_ref_id": "system_schedule_001",
"system_alarm_ref_id": "system_alarm_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"
}
}
}规则:
- 用户身份由 WebSocket 会话上下文确定,客户端不传
user_id。 - 默认只返回
scheduled和done。 -
include_deleted=true时才返回deleted数据。 - 返回结果按
start_time asc nulls last, created_at desc排序。 - 返回日程时包含
system_schedule_ref_id和system_alarm_ref_id。
生产环境:wss://<host>/ws?device_id=xxx
仅本地开发可使用:ws://<host>/ws?device_id=xxx
- 保持设备在线。
- 回传语音结构化结果。
- 下发提醒控制消息。
- 下发系统日程删除指令。
- 下发系统闹钟删除指令。
- 接收位置信息和确认事件。
客户端进入界面后先发:
{
"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
}
}{
"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"
}
}
}- 弹出统一表单。
- 默认填充结构化字段。
- 允许用户补全地点、时间和提醒参数。
{
"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"
}
}
}- 服务端根据当前位置计算日程距离。
- 如果日程包含地点且命中围栏,先检查系统引用是否仍存在。
- 若日程还包含时间,再结合时间窗口决定是否下发提醒控制消息。
服务端下发固定模板信息和唯一 rendered_text。客户端决定展示通道及是否启动 TTS,
但通知、应用内弹窗和 TTS 必须复用同一个 rendered_text。
所有播报内容统一由三部分组成:
rendered_text = fixed_header + reminder_body + fixed_footer
| 部分 | 固定值或来源 | 约束 |
|---|---|---|
fixed_header |
提醒: |
全部模板固定 |
reminder_body |
模板和白名单参数 | 实际提醒内容,不包含固定头尾 |
fixed_footer |
。 |
全部模板固定,只追加一次 |
template_id |
版本 | reminder_body |
最终 rendered_text
|
|---|---|---|---|
TIME_ADVANCE |
1 |
{title}将在{minutes_before}分钟后开始 |
提醒:{title}将在{minutes_before}分钟后开始。 |
TIME_DUE |
1 |
{title}现在开始 |
提醒:{title}现在开始。 |
LOCATION_ENTER |
1 |
你已到达{location_name},请处理{title} |
提醒:你已到达{location_name},请处理{title}。 |
GENERIC |
1 |
请查看日程“{title}” |
提醒:请查看日程“{title}”。 |
渲染规则:
- 标题为空时使用“一项日程”。
-
minutes_before非法时,TIME_ADVANCE降级为GENERIC。 - 地点为空时,
LOCATION_ENTER降级为GENERIC。 - 未知模板或版本降级为
GENERIC。 - 只允许标题、提前分钟数和地点名称;不使用备注、详细地址或坐标。
- 模板含义变化必须增加版本。
- 服务端只组合一次固定头、中间内容和固定尾,并保存结果。
- 客户端只校验并使用服务端结果,不为 TTS 重新拼接文案。
{
"type": "reminder.control",
"message_id": "msg_reminder_001",
"schedule_id": "schedule_001",
"reason": "time_window_reached",
"action": "show",
"content": {
"template_id": "TIME_ADVANCE",
"template_version": 1,
"locale": "zh-CN",
"rendered_text": "提醒:项目会议将在15分钟后开始。",
"params": {
"title": "项目会议",
"minutes_before": 15,
"location_name": "一号会议室"
}
}
}客户端展示成功后回传 reminder.control.ack。该 ACK 只表示主提醒已展示,
不得等待 TTS,也不得因 TTS 失败而改为失败。
| 消息 | 方向 | 架构用途 |
|---|---|---|
reminder.tts.start.command |
客户端 → 服务端 | 通过播放策略后,引用 message_id 请求启动播报 |
reminder.tts.stream.started |
服务端 → 客户端 | 声明 stream_id、模型、音色和 PCM 播放参数 |
| WebSocket Binary Frame | 服务端 → 客户端 | 流式传输当前 stream_id 的 PCM 音频 |
reminder.tts.stream.ended |
服务端 → 客户端 | 表示云端合成及服务端转发结束 |
reminder.tts.stream.error |
服务端 → 客户端 | 表示配置、供应商或传输失败 |
reminder.tts.cancel.command |
客户端 → 服务端 | 用户关闭播报或播放器终止时取消活动流 |
reminder.tts.cancel.result |
服务端 → 客户端 | 确认流已取消或已处于终态 |
reminder.tts.result |
客户端 → 服务端 | 上报真实播放完成、跳过或失败结果 |
reminder.tts.result.ack |
服务端 → 客户端 | 确认 TTS 终态已可靠接收 |
{
"type": "reminder.tts.start.command",
"request_id": "req_tts_001",
"payload": {
"message_id": "msg_reminder_001",
"schedule_id": "schedule_001",
"utterance_id": "tts_msg_reminder_001"
}
}客户端不得提交 rendered_text、模型、音色或阿里云凭证。服务端通过 message_id
读取已保存的提醒文本和 TTS 配置。
{
"type": "reminder.tts.stream.started",
"request_id": "req_tts_001",
"ok": true,
"payload": {
"message_id": "msg_reminder_001",
"schedule_id": "schedule_001",
"utterance_id": "tts_msg_reminder_001",
"stream_id": "tts_stream_001",
"provider": "aliyun_dashscope",
"model": "qwen3-tts-vd-realtime-2026-01-15",
"voice_id": "configured_voice_id",
"audio_format": "pcm_s16le",
"sample_rate_hz": 24000,
"channels": 1
}
}stream.started 成功后,后续 Binary Frame 均属于该设备当前唯一活动的 stream_id。
WebSocket 保证消息顺序;本方案不允许同一连接并发传输多条 TTS 流。
{
"type": "reminder.tts.start.error",
"request_id": "req_tts_001",
"ok": false,
"error": {
"code": "TTS_TEXT_NOT_READY",
"message": "提醒文本尚未准备完成",
"details": {
"message_id": "msg_reminder_001"
}
}
}启动错误码:
error.code |
含义 |
|---|---|
TTS_REMINDER_NOT_FOUND |
找不到提醒或提醒不属于当前用户 |
TTS_TEXT_NOT_READY |
可靠消息记录中没有可合成的 rendered_text
|
TTS_ALREADY_TERMINAL |
相同 utterance_id 已完成、跳过或失败 |
TTS_STREAM_BUSY |
当前设备已有活动 TTS 流且队列不可接收 |
TTS_PROVIDER_AUTH_FAILED |
服务端供应商鉴权失败 |
TTS_VOICE_INVALID |
voice_id 缺失或与模型不匹配 |
TTS_PROVIDER_RATE_LIMITED |
供应商限流 |
TTS_PROVIDER_FAILED |
供应商不可用或合成启动失败 |
- 方向固定为服务端到客户端。
- 载荷是
pcm_s16le、24000Hz、单声道原始音频字节。 - Binary Frame 必须出现在
stream.started之后、stream.ended或stream.error之前。 - 空 Binary Frame 非法;不同
stream_id的数据不得合并。 - 客户端只把 Binary Frame 交给当前活动
stream_id的TtsStreamPlayer。 - 断线后的音频分片不做续传;是否重试由防重复规则决定。
{
"type": "reminder.tts.stream.ended",
"message_id": "msg_reminder_001",
"schedule_id": "schedule_001",
"utterance_id": "tts_msg_reminder_001",
"stream_id": "tts_stream_001",
"status": "completed"
}stream.ended 只表示服务端已经发送完音频。客户端必须继续播放缓冲区中的剩余音频,
播放器完成后才能发送 reminder.tts.result(status=completed)。
{
"type": "reminder.tts.stream.error",
"message_id": "msg_reminder_001",
"schedule_id": "schedule_001",
"utterance_id": "tts_msg_reminder_001",
"stream_id": "tts_stream_001",
"error": {
"code": "TTS_STREAM_INTERRUPTED",
"message": "TTS 音频流中断"
}
}活动流错误码:
error.code |
含义 |
|---|---|
TTS_PROVIDER_FAILED |
合成过程中供应商失败 |
TTS_STREAM_INTERRUPTED |
供应商或 TimeFlow 音频流中断 |
TTS_BACKPRESSURE_LIMIT |
客户端消费过慢且超过有界缓冲 |
TTS_CANCELLED |
用户操作、客户端状态变化或连接关闭导致取消 |
{
"type": "reminder.tts.cancel.command",
"request_id": "req_tts_cancel_001",
"payload": {
"message_id": "msg_reminder_001",
"utterance_id": "tts_msg_reminder_001",
"stream_id": "tts_stream_001",
"reason": "user_disabled"
}
}{
"type": "reminder.tts.cancel.result",
"request_id": "req_tts_cancel_001",
"ok": true,
"payload": {
"message_id": "msg_reminder_001",
"utterance_id": "tts_msg_reminder_001",
"stream_id": "tts_stream_001",
"status": "cancelled"
}
}取消必须幂等。流已取消或已经结束时,重复取消返回当前终态,不重新创建或恢复流。
约束:
- 启动命令只引用已有提醒,不接受客户端自由文本。
- 服务端按
message_id读取已保存的rendered_text并发送给阿里云。 - 客户端不接收
DASHSCOPE_API_KEY,也不直接连接阿里云。 - 每个设备同一时刻最多有一个活动 TTS 下行流。
- 同一
message_id固定生成utterance_id=tts_{message_id},不得重复播报。 - 服务端流结束不等于客户端播放完成;最终结果以前端播放器回调为准。
- 所有客户端命令都必须携带
request_id;相同请求重放返回原结果。 - 服务端推送使用原
message_id、utterance_id和stream_id关联同一次播报。
{
"type": "reminder.tts.result",
"request_id": "req_tts_result_001",
"message_id": "msg_reminder_001",
"schedule_id": "schedule_001",
"utterance_id": "tts_msg_reminder_001",
"provider": "aliyun_dashscope",
"model": "qwen3-tts-vd-realtime-2026-01-15",
"stream_id": "tts_stream_001",
"status": "completed",
"reason": null
}服务端确认:
{
"type": "reminder.tts.result.ack",
"request_id": "req_tts_result_001",
"ok": true,
"payload": {
"message_id": "msg_reminder_001",
"utterance_id": "tts_msg_reminder_001",
"status": "completed"
}
}结果规则:
-
status只能是completed、skipped或failed。 -
completed时reason=null且stream_id必填。 -
skipped时流尚未启动,stream_id=null;原因只能是disabled、background_disabled、device_locked或platform_restricted。 -
failed时原因只能是engine_unavailable、language_unsupported、audio_focus_denied、synthesis_failed、stream_interrupted或playback_failed。 -
completed只能由客户端播放器完成回调确认。 - TTS 结果与
reminder.control.ack相互独立,不改变日程状态或系统兜底清理判断。 - 客户端在收到
result.ack前按原request_id重试;服务端必须幂等返回同一结果。
IDLE
├─ 策略拒绝 ─> SKIPPED
└─ start.command ─> STARTING
├─ start.error ─> FAILED
└─ stream.started ─> STREAMING
├─ stream.error ─> FAILED
├─ cancel.command ─> CANCELLED
└─ stream.ended ─> DRAINING
├─ 播放完成 ─> COMPLETED
└─ 播放失败 ─> FAILED
SKIPPED、COMPLETED、FAILED 和 CANCELLED 是终态;相同 utterance_id
进入终态后不得再次启动播报。CANCELLED 通过取消结果记录,不转换成“已播放完成”。
在服务端准备下发软件提醒或删除系统兜底提醒之前,先确认用户是否已经在系统日程或系统闹钟中手动删除了对应事项。
如果用户已经删除系统侧提醒,说明用户可能已经表达“不再需要提醒”的意图,服务端不应继续下发应用内弹窗、系统级全局弹窗或删除指令。
服务端不能直接查询 Android 系统日程或系统闹钟,这个检查必须由客户端完成,再通过 WS 回传结果。
该指令作为独立保留的系统侧存在性检查流程,不因提醒策略调整而删除。
{
"type": "system.refs.check",
"message_id": "msg_refs_check_001",
"schedule_id": "schedule_001",
"system_schedule_ref_id": "system_schedule_001",
"system_alarm_ref_id": "system_alarm_001",
"reason": "before_reminder_control"
}- 根据
system_schedule_ref_id查询系统日程是否仍存在。 - 根据
system_alarm_ref_id查询系统闹钟是否仍存在。 - 将查询结果通过 WS 回传服务端。
- 只做存在性检查,不重新创建系统日程或系统闹钟。
{
"type": "system.refs.check.result",
"message_id": "msg_refs_check_001",
"schedule_id": "schedule_001",
"ok": true,
"system_schedule_ref_id": "system_schedule_001",
"system_schedule_exists": false,
"system_alarm_ref_id": "system_alarm_001",
"system_alarm_exists": false,
"checked_at": "2026-07-28T14:45:00+08:00"
}查询失败时,客户端仍需回传失败原因:
{
"type": "system.refs.check.result",
"message_id": "msg_refs_check_001",
"schedule_id": "schedule_001",
"ok": false,
"error": {
"code": "SYSTEM_REF_CHECK_FAILED",
"message": "系统引用检查失败",
"details": {
"reason": "permission_denied"
}
}
}- 如果
system_schedule_exists=true或system_alarm_exists=true,说明兜底提醒仍存在,继续执行软件提醒和重复提醒删除流程。 - 如果此前成功登记的引用均返回
exists=false,且本次检查成功,才可解释为用户可能已手动删除系统侧提醒。 - 满足第 2 条时,服务端取消本次软件提醒;MVP 可将日程更新为
deleted并停止后续监听。 - 从未登记引用、引用为空、权限不足或检查失败时,不得据此删除业务日程。
- 如果客户端超时未响应,服务端按当前在线状态继续软件提醒,但保留系统兜底,避免漏提醒。
{
"type": "system.schedule.delete",
"message_id": "msg_schedule_delete_001",
"schedule_id": "schedule_001",
"system_schedule_ref_id": "system_schedule_001",
"reason": "ws_connected_and_time_near"
}- 删除系统日程。
- 取消重复通知。
- 继续保留业务日程本体。
{
"type": "system.schedule.delete.ack",
"message_id": "msg_schedule_delete_001",
"schedule_id": "schedule_001",
"ok": true,
"system_schedule_ref_id": "system_schedule_001"
}删除失败时:
{
"type": "system.schedule.delete.ack",
"message_id": "msg_schedule_delete_001",
"schedule_id": "schedule_001",
"ok": false,
"system_schedule_ref_id": "system_schedule_001",
"error": {
"code": "SYSTEM_SCHEDULE_DELETE_FAILED",
"message": "系统日程删除失败",
"details": {
"reason": "not_found"
}
}
}{
"type": "system.alarm.delete",
"message_id": "msg_alarm_delete_001",
"schedule_id": "schedule_001",
"system_alarm_ref_id": "system_alarm_001",
"reason": "ws_connected_and_time_near"
}- 删除系统闹钟。
- 取消重复闹钟提醒。
- 继续保留业务日程本体。
{
"type": "system.alarm.delete.ack",
"message_id": "msg_alarm_delete_001",
"schedule_id": "schedule_001",
"ok": true,
"system_alarm_ref_id": "system_alarm_001"
}删除失败时:
{
"type": "system.alarm.delete.ack",
"message_id": "msg_alarm_delete_001",
"schedule_id": "schedule_001",
"ok": false,
"system_alarm_ref_id": "system_alarm_001",
"error": {
"code": "SYSTEM_ALARM_DELETE_FAILED",
"message": "系统闹钟删除失败",
"details": {
"reason": "not_found"
}
}
}作用:
- 由客户端主动告诉服务端,用户已经在应用内确认该日程完成。
- 关闭后续监听和所有提醒。
- 与
system.refs.check不同,这不是系统侧存在性检查,而是用户显式完成动作。
{
"type": "schedule.confirmed",
"request_id": "req_schedule_confirm_001",
"schedule_id": "schedule_001",
"confirmed": true,
"timestamp": "2026-07-28T12:05:00+08:00"
}- 取消监听。
- 终止后续提醒。
- 如系统日程或系统闹钟仍存在,按需删除。
{
"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"
}
}
}客户端成功创建系统日程或系统闹钟后,必须把引用 ID 回写服务端。服务端收到并持久化引用前, 不能把系统兜底视为已建立。
{
"type": "system.refs.register.command",
"request_id": "req_refs_register_001",
"payload": {
"schedule_id": "schedule_001",
"system_schedule_ref_id": "system_schedule_001",
"system_alarm_ref_id": "system_alarm_001"
}
}{
"type": "system.refs.register.result",
"request_id": "req_refs_register_001",
"ok": true,
"payload": {
"schedule_id": "schedule_001",
"system_schedule_ref_id": "system_schedule_001",
"system_alarm_ref_id": "system_alarm_001",
"registered_at": "2026-07-28T12:00:03+08:00",
"replaced_refs": []
}
}规则:
- 两个引用可以分别登记,但至少一个不为空。
- 服务端校验日程属于当前 WebSocket 会话用户。
- 同一日程重复登记相同引用返回原结果。
- 同一日程登记不同引用时视为替换;服务端通过
replaced_refs返回旧引用,由客户端清理旧平台资源。 - 客户端创建平台资源成功但登记失败时,将待登记记录保留在本地并重试。
完成和删除统一使用显式状态命令;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
}
}规则:
- 允许
scheduled -> done和scheduled -> deleted。 - 相同目标状态重复提交返回当前结果。
- 已进入
done或deleted后,不允许直接恢复为scheduled;恢复需求通过复制或重新创建处理。 -
expected_updated_at不匹配时返回VERSION_CONFLICT和当前日程摘要,不执行覆盖。 - 状态事务提交后才发送系统资源清理指令;清理失败不回滚业务状态,但必须继续重试并可观测。
- 客户端按服务端
session.ready返回的心跳间隔发送session.ping。 - 服务端回复
session.pong并带回server_time。 - 连续超过两个心跳周期未收到有效响应时,客户端将连接标记为断开,不再把网络可用等同于服务端在线。
客户端重连成功后发送:
{
"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",
"req_refs_register_001"
],
"pending_message_ids": [
"msg_reminder_001"
]
}
}服务端处理:
- 返回各
pending_request_ids的已知终态;未知请求由客户端按原request_id重发。 - 返回
last_schedule_updated_at之后发生变化的日程。 - 重发未收到 ACK 且仍有效的服务端推送,保持原
message_id。 - 已完成、已删除、已过期或已 ACK 的提醒不得再次激活。
- 音频 Binary Frame 不做断点续传;断线中的语音流标记失败,客户端重新录音。
规则:
- 只有
start_time存在的日程才参与时间监听。 - 时间到达前进入监测窗口。
- 默认提前
15min。 - 进入窗口后优先判断 WS 在线状态和前后台状态。
- 如果 WS 在线,服务端先下发
system.refs.check。 - 如果客户端确认系统日程和系统闹钟都已不存在,服务端取消本次软件提醒并停止监听。
- 如果系统侧兜底提醒仍存在,服务端下发
reminder.control;只有收到展示成功 ACK 后,才按需下发系统日程和系统闹钟删除指令。
规则:
- 只有存在经纬度的日程才参与空间监听。
- 默认围栏半径
100m。 - 客户端持续上传位置,服务端判定是否进入围栏。
- 创建日程时,如果用户当前位置已经在目标围栏内,服务端将
geofence_armed=false。 - 当用户离开目标围栏后,服务端将
geofence_armed=true。 - 只有
geofence_armed=true且用户再次进入围栏时,才允许触发地点提醒。 - 如果创建时无法取得用户当前位置,服务端默认
geofence_armed=true,避免错过后续进入提醒。
schedule_type |
日程形态 | 触发规则 |
|---|---|---|
time |
只有时间 | 时间窗口到达后触发 |
location |
只有地点 |
geofence_armed=true 且用户进入地理围栏后触发 |
time |
同时有时间和地点 | 时间窗口和地理围栏分别形成可触发条件,任一条件首次满足即可提醒 |
组合日程细化规则:
-
schedule_type仍为time,不新增第三种类型。 - 时间条件和地点条件是 OR,不是必须同时满足的 AND;否则用户未在目标地点时可能错过时间提醒。
-
time_triggered_at和geo_triggered_at分别记录两个条件首次命中的时间。 - 同一日程在一次有效提醒周期内只展示一次。一个条件已经成功展示后,另一个条件随后命中只记录
*_triggered_at,不重复展示。 - 提醒展示失败时,尚未展示成功的另一条件仍可触发补偿提醒。
满足以下任一条件时取消监听:
- 用户通过确认接口标记已完成。
- 有时间的日程时间已过。
- 日程被删除。
- 客户端确认系统日程和系统闹钟均已被用户手动删除。
为同时满足“优先触达”和“避免重复”,执行顺序固定为:
- 服务端判定提醒条件满足。
- 服务端下发
system.refs.check。 - 客户端返回引用存在性;引用检查失败或超时时,保留系统兜底。
- 服务端下发带
message_id的reminder.control。 - 服务端渲染固定模板并随
reminder.control下发唯一rendered_text;客户端校验后由 应用内弹窗和系统通知直接复用。 - 客户端主提醒实际展示成功后返回包含同一
rendered_text的reminder.control.ack(ok=true)。 - 客户端按 TTS 策略异步决定跳过或发送
reminder.tts.start.command;播放终态再单独发送reminder.tts.result。 - 服务端收到主提醒成功 ACK 后,才下发系统日程/系统闹钟删除指令。
- 客户端删除成功后回传 ACK;服务端清空对应引用 ID。
- 主提醒失败、ACK 超时或删除失败时,不提前清空引用,并按幂等规则重试。
禁止仅凭“WS 在线”或“消息已写入 Socket”删除系统兜底。 禁止等待 TTS 播报完成后才确认主提醒;TTS 失败不能阻止已经展示成功的主提醒进入清理流程。
system_schedule_ref_id 或 system_alarm_ref_id 为空表示该兜底从未登记,不能等同于“用户手动删除”。
只有满足以下全部条件,服务端才可把双引用不存在解释为用户取消意图:
- 两个引用此前至少有一个成功登记。
- 本次检查本身
ok=true。 - 已登记的引用均返回
exists=false。 - 客户端没有返回权限缺失、查询失败或平台不支持。
条件不满足时保留业务日程,按可用通道继续提醒或提示用户修复权限,不自动把 status 改为 deleted。
执行顺序:
- 客户端收到
reminder.control,先按message_id检查主提醒和 TTS 是否已经进入终态。 - 客户端
ReminderTemplateRenderer校验服务端文本满足提醒: + reminder_body + 。,并校验模板、版本和参数一致;不一致时拒绝启动 TTS, 按安全降级规则展示GENERIC。 - 应用内弹窗或系统通知直接使用校验后的服务端
rendered_text展示主提醒。 - 主提醒展示成功后立即发送
reminder.control.ack,不等待 TTS。 -
TtsPlaybackPolicy依次检查tts_enabled、前后台、tts_background_enabled、锁屏、 音频焦点和平台限制。 - 策略不允许时不发送启动命令、不调用外部服务,发送
status=skipped和对应reason。 - 策略允许时,客户端使用稳定
utterance_id发送reminder.tts.start.command,不携带 API Key 或自由文本。 - 服务端读取提醒消息记录中已经生成的
rendered_text,核对主提醒 ACK 后,通过Qwen3TtsGateway将该文本发送给qwen3-tts-vd-realtime-2026-01-15流式合成。 - 服务端把供应商音频流转换为 TimeFlow WebSocket Binary Frame 下发,客户端
TtsStreamPlayer边接收边播放。 - 只有本地播放器完成回调上报
completed;供应商、传输或播放失败上报对应failed。 - 完成或失败后释放音频焦点、PCM 缓冲、TimeFlow 流状态和 DashScope 会话,不持久化合成音频。
- 任一 TTS 终态都写入客户端最小投递状态;相同
message_id重连或重试时只重发结果,不重复朗读。
TTS 降级边界:
- 全局开关关闭:
skipped/disabled。 - 后台开关关闭:
skipped/background_disabled。 - 设备锁屏:
skipped/device_locked。 - 平台禁止后台音频:
skipped/platform_restricted。 - 网络不可用、DashScope 鉴权失败、声音配置无效、阿里云不可用或限流、音频焦点失败、 合成、转发或播放失败:上报对应结果,继续保留已经展示的主提醒。
- App 进程死亡时不执行 TTS,由预注册的系统通知、提示音或震动兜底。
- 只有
start_time存在时才检查时间区间重叠。 - 如果已有日程落在同一时间段,返回冲突提示。
- 冲突结果包含已有日程的标题、时间和 ID。
- 只有地点、没有时间的日程不做时间冲突检测。
- 提示用户“当前时段已有日程”。
- 用户可继续保存,也可修改时间。
- 冲突不是强制失败,但必须明确提示。
- 两个有结束时间的日程在
new_start < existing_end且existing_start < new_end时冲突。 - 缺少
end_time时,MVP 使用可配置的默认占用时长进行检测;默认值必须由产品确认, 在确认前不得写死为数据库规则。 - 边界相接(例如一个日程 10:00 结束、另一个 10:00 开始)不算冲突。
- 只比较同一用户、
status=scheduled且未删除的日程。 - 服务端统一转换为 UTC 后计算,响应按各日程原
timezone展示。
| 场景 | 必须行为 | 禁止行为 |
|---|---|---|
| ASR 失败 | 返回失败阶段,允许重录或手动创建 | 创建不完整正式日程 |
| LLM 输出缺字段/歧义 | 返回草稿、缺失项和歧义项,等待用户确认 | 模型自行补全关键时间或地点后直接写库 |
| WebSocket 断开 | 本地保留待提交命令,重连后按原 request_id 恢复 |
仅凭网络连接状态声明在线 |
| 系统日程/闹钟创建失败 | 保留失败原因并提示可用降级通道 | 保存不存在的引用 ID |
| 系统引用检查失败 | 保留兜底并继续安全提醒策略 | 推断用户已经取消提醒 |
| 软件提醒展示失败 | 保留系统兜底,允许幂等重试 | 先删除系统兜底 |
| 系统资源删除失败 | 保留引用并重试,避免再次发送软件提醒 | 清空引用后宣称删除成功 |
| TTS 模板未知或参数非法 | 降级为 GENERIC,继续展示主提醒 |
阻塞通知或临时调用 LLM 生成文案 |
| TTS 开关关闭、后台未授权或设备锁屏 | 跳过播报并上报明确原因 | 强制播报或修改系统音量 |
| TTS 配置或鉴权失败 | 禁用本次云端合成、告警并保留主提醒 | 把 API Key 放入客户端或运行时切换未知模型 |
| 供应商不可用、超时或限流 | 结束本次 TTS,执行有限重试策略 | 无限重试、重复计费或重复朗读 |
| 音频流中断或背压超限 | 取消供应商与客户端流并上报失败 | 使用无界缓冲或合并不同 stream_id
|
| 音频焦点或客户端播放失败 | 上报播放失败并保留主提醒 | 把云端合成完成当作用户已听到 |
| WebSocket 重复下发相同 TTS | 返回已保存的 TTS 终态 | 使用相同 message_id 再次朗读 |
| 位置权限不可用 | 暂停地点监听并提示权限状态;有时间条件时继续时间提醒 | 把权限失败当作未进入围栏 |
| 服务端重启 | 从 schedules 恢复时间扫描;位置监听等待新位置 |
依赖仅存在于内存的触发状态宣称完整恢复 |
一致性边界:
- 日程写入和
request_id幂等结果必须在同一事务边界内提交,或使用能保证原子可见性的等价实现。 - 服务端事务提交成功后才能下发
schedule.upsert.result。 - WebSocket 推送采用至少一次投递;客户端依靠
message_id去重。 -
time_triggered_at、geo_triggered_at只表示条件已命中,不等于客户端已经展示成功。 - 若实现需要区分“已命中”和“已展示”,MVP 可先保存在可靠消息记录中;不得复用
status表达投递过程。 - TTS 使用主提醒的
message_id去重,但 TTS 终态与主提醒 ACK 相互独立。 - 供应商完成只表示音频生成完成;必须等待服务端转发结束和客户端播放器完成回调后,
才能记录 TTS
completed。
- WebSocket 生产环境只允许
wss://。 - 用户身份必须来自鉴权会话;
device_id只标识设备,不能代替用户认证。 - 服务端对每次日程读写、引用登记和状态变更校验资源归属。
- 音频只为本次解析处理;是否持久化、保留时长和删除策略必须显式配置,默认不长期保存原始音频。
- 日志不得记录原始音频、完整转写文本、精确经纬度或令牌;排障使用
request_id、message_id、job_id和脱敏错误信息。 - 位置上报只在存在有效地点日程且用户授权时启用;无有效监听对象时停止高频上报。
- ASR、LLM 和地图依赖必须配置超时、有限重试和熔断;重试不得绕过用户确认。
- TTS 固定模板只允许标题、提前分钟数和地点名称,不得播报备注、详细地址、经纬度或令牌。
- 锁屏状态不播报 TTS;后台播报必须由用户显式开启,关闭开关后立即停止待播报内容。
- TTS 不得绕过系统静音或勿扰策略,不得为了播报强制修改媒体或闹钟音量。
-
DASHSCOPE_API_KEY和可选 Workspace ID 只存在于服务端密钥管理系统,不进入客户端、 WebSocket 业务响应、日志或崩溃报告。 - 合成文本发送给阿里云前必须来自创建
reminder.control时保存的唯一rendered_text,并与主提醒 ACK 一致;不得接受启动命令中的自由文本,也不得发送备注、 地址、坐标或其他上下文。 - 供应商音频只在服务端内存中转换并实时转发;服务端和客户端均默认不落盘、 不缓存到业务数据库。
- 客户端断开、取消、缓冲超限或播放失败时,服务端必须取消对应 DashScope 会话, 避免继续生成和计费。
- 所有命令/查询均能按同一
request_id重放并得到一致结果。 - 相同
request_id、不同载荷返回IDEMPOTENCY_CONFLICT。 - 服务端推送重复到达时,客户端按
message_id只执行一次。 - 未知消息、缺失字段、非法状态和版本冲突都有稳定错误码。
- ASR/LLM 成功只生成草稿,用户确认前
schedules无新增记录。 - 缺少时间或地点时表单明确补全,不能静默创建无触发条件的日程。
- 音频流中断不会创建正式日程,重连后可重新录音。
- 前台在线、后台在线和离线/服务不可达三类场景均有可验证触达路径。
- 软件提醒 ACK 成功前,系统兜底不会被删除。
- 删除系统资源失败时引用仍保留,重试不会重复展示软件提醒。
- 系统引用从未登记、已被用户删除、无权限查询三种情况能被区分。
- 设备重启、App 进程死亡和服务端重启后,未完成日程仍能恢复到正确监听状态。
- TTS 完成、跳过或失败均不改变主提醒 ACK 和系统兜底清理判断。
- 时间-only、地点-only、时间+地点三类日程分别覆盖。
- 组合日程任一条件命中可提醒,但一次有效周期只展示一次。
- 围栏内创建不会立即触发;离开再进入后可以触发。
- 时区转换、夏令时边界、结束时间为空和区间边界相接均有测试。
- 文档和 JSON 示例通过静态格式检查。
- 接口模型可生成 Schema,并用成功、失败、重复和重连样例做契约测试。
- Android 系统日程、闹钟、通知、千问 3 TTS VD 流式播放、后台限制和位置权限必须在真机或 目标模拟器验证。
- 只有静态文档检查时,结论必须写为“设计已细化/静态检查通过”,不得写成“提醒能力已实现”。
- 所有模板均使用固定开头
提醒:和固定结尾。,中间reminder_body才是实际提醒内容。 -
TIME_ADVANCE、TIME_DUE、LOCATION_ENTER和GENERIC的reminder_body及最终rendered_text分别与文档定义完全一致。 - 通知、应用内弹窗和 TTS 使用同一次
rendered_text,不存在通道间文案差异。 - 标题为空时使用“一项日程”;提前分钟数非法、地点为空、模板未知或版本未知时降级为
GENERIC。 -
tts_enabled=false时不初始化 TTS,主提醒正常展示。 - 后台未授权、锁屏或平台受限时返回对应
skipped,不得自动播报。 -
DASHSCOPE_API_KEY不出服务端;客户端收到的开始消息只包含模型、voice_id、stream_id和 PCM 播放参数。 - 相同
message_id重复到达时不重复朗读,只重发已保存的 TTS 结果。 - 网络不可用、DashScope 鉴权失败、阿里云拒绝或限流、合成、转发和播放失败时, 返回对应结果且主提醒状态不受影响。
- 阿里云返回合成完成但本地播放失败时不得上报
completed;只有播放器完成回调可以上报完成。 - 超过服务端安全长度限制的合成文本按规则降级或截断,不得直接发送给阿里云。
- 运行时模型严格为
qwen3-tts-vd-realtime-2026-01-15,已审批voice_id与模型匹配。 - 启动、流开始、Binary Frame、流结束、流错误、取消和最终结果接口均通过成功、失败、 重复和乱序契约测试。
- 播放器在完整音频接收完成前开始播放,所有音频严格绑定当前唯一活动
stream_id。 - 供应商完成但客户端未播放完成时不得上报
completed;客户端断线会终止供应商会话。 - App 前台、后台、锁屏、静音、勿扰、断网、API Key 无效、Voice ID 无效和进程死亡场景 均有目标设备或集成环境验证记录。
架构文档已细化并通过静态检查。