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 是提醒增强通道:文案使用固定模板,由阿里云百炼 qwen3-tts-vd-realtime-2026-01-15 实时合成,不调用 LLM 生成;TTS 失败不影响主提醒。
  9. TimeFlow 服务端先生成固定模板的唯一 rendered_text,将同一文本用于客户端通知和 阿里云 TTS 请求;TTS 输入不是客户端录音。TimeFlow 服务端接收供应商流式音频, 转换后通过现有业务 WebSocket Binary Frame 下发,客户端边收边播。

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

1.1 MVP 边界

本版明确包含:

  1. 单用户、单主要设备上的日程创建、编辑、查询、完成和删除。
  2. 一条活动 WebSocket 连接上的语音流、业务命令、查询和服务端推送。
  3. 时间提醒、地点提醒,以及同时包含时间和地点的日程。
  4. 系统日程和系统闹钟作为客户端兜底,并将系统引用回写服务端。
  5. 断线重连、重复消息和进程重启下的最小恢复能力。
  6. 前台 TTS 播报,以及用户显式开启后的后台 TTS 尝试。

本版暂不包含:

  1. 多用户共享日历。
  2. 多设备之间的系统日历/闹钟引用同步。
  3. 周期性日程和复杂重复规则。
  4. 服务端直接操作 Android 系统日历、系统闹钟或系统通知。
  5. 用 LLM 自主决定最终日程或提醒时间;LLM 只生成待用户确认的草稿。
  6. 锁屏或 App 进程死亡后的 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 不可用时立即按情况三降级。

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

客户端根据自身能力自行决定是否降级为普通通知、提示音或震动,不执行 TTS 播报。

2.3 创建日程

日程支持两种入口:

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

二者共用一个表单。

语音创建流程:

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

日程允许两种主类型:

形态 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. 根据前后台状态和系统可用性选择具体提醒通道。
  12. 使用固定模板渲染通知与 TTS 共用文案,并按客户端设置执行可选 TTS 播报。
  13. 接收 TimeFlow WebSocket 下行的 PCM Binary Frame,使用有界缓冲流式播放。

3.2 服务端

服务端负责:

  1. 音频文件接收。
  2. ASR 调用。
  3. LLM 结构化提取。
  4. WebSocket 消息分发。
  5. 日程冲突检测。
  6. 日程状态监控。
  7. 地点与时间窗口判断。
  8. 提醒控制消息下发。
  9. 提醒通道选择由客户端自行完成。
  10. 选择固定提醒模板和参数,通过 ReminderTemplateRenderer 生成唯一 rendered_text; 服务端不调用 LLM 生成提醒播报文案。
  11. 安全保管 DASHSCOPE_API_KEY,代理千问 3 TTS VD 实时会话并向客户端流式转发 PCM。

3.3 外部依赖

  1. 第三方 ASR 服务。
  2. 大模型服务。
  3. 第三方地图 SDK。
  4. Android 系统提醒能力。
  5. 阿里云百炼 Model Studio / DashScope 千问 3 TTS VD 实时语音合成服务。

阿里云接入边界:

  1. TimeFlow 服务端通过 DashScope 实时 WebSocket 调用 qwen3-tts-vd-realtime-2026-01-15;客户端不得直连阿里云。
  2. DASHSCOPE_API_KEY 和可选的 DASHSCOPE_WORKSPACE_ID 只保存在服务端环境或密钥管理系统中, 不下发到客户端。
  3. 服务端固定配置地域网关、模型 ID 和 DASHSCOPE_TTS_VOICE_ID;客户端只接收不含密钥的 流开始元数据和 PCM 音频分片。
  4. 合成使用适合客户端流式播放的 PCM 格式;具体采样参数属于接口实现配置,不写入日程业务数据。
  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. 系统日程和系统闹钟属于客户端平台资源;服务端只保存引用 ID 和协调检查、清理流程。
  7. 服务端 ReminderTemplateRenderer 生成通知与 TTS 共用的唯一 rendered_text; 客户端只做模板一致性校验和本地降级,不得为 TTS 另行拼接文本。
  8. 服务端 Qwen3TtsGateway 管理阿里云会话,客户端 TtsPlaybackPolicy 决定是否启动, TtsStreamPlayer 管理播放和结果上报。
  9. TTS 不拥有独立提醒状态,不得因播报成功或失败修改 schedules.status 或主提醒 ACK。
  10. 客户端不得持有或接收 DASHSCOPE_API_KEY;服务端是阿里云实时合成的唯一调用方。
  11. DashScope 鉴权或调用失败不得阻塞主提醒。

3.5 ASR 与 TTS 音频方向

两条音频链路必须严格区分:

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
-> 扬声器/耳机

边界规则:

  1. ASR 输入来自用户麦克风;TTS 输入来自固定模板文本,TTS 不接收用户录音。
  2. ASR 音频是客户端到服务端的上行 Binary Frame;TTS 音频是服务端到客户端的下行 Binary Frame,均复用 TimeFlow WebSocket,但方向和控制状态不同。
  3. TimeFlow 服务端负责连接 DashScope、接收并转发流式音频,不持久化合成音频。
  4. 阿里云只接收服务端生成并保存的固定模板 rendered_text,不接收客户端录音或自由文本。
  5. 客户端负责有界缓冲、音频焦点、播放完成判定和资源释放。
  6. 每个设备同一时刻只允许一个活动 TTS 下行流;ASR 与 TTS 的 stream_id 和 Binary Frame 状态机不得混用。

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. 保存客户端设置 tts_enabled=falsetts_background_enabled=false
  6. message_id 保存本次 TTS 终态,防止 WebSocket 重试造成重复播报。

4.1.5 提醒执行模块

职责:

  1. 前台弹窗。
  2. 后台浮窗。
  3. 系统通知。
  4. 根据当前应用状态决定提醒展示方式。
  5. ReminderTemplateRenderer 校验服务端下发的模板、参数和 rendered_text 是否一致; 仅在离线兜底或版本兼容时按同一规范本地渲染。
  6. TtsPlaybackPolicy 根据全局开关、前后台、锁屏、音频焦点和平台能力决定是否播报。
  7. TtsStreamPlayer 管理 TimeFlow PCM 下行流、音频焦点、有界缓冲、播放、停止、 完成回调和资源释放。

固定约束:

  1. tts_enabled 默认 false;关闭时不申请 TTS 流。
  2. 前台只有 tts_enabled=true 时允许播报。
  3. 后台必须同时满足 tts_enabled=truetts_background_enabled=true、设备未锁屏和平台允许。
  4. 锁屏、进程死亡或后台音频受限时跳过 TTS,主提醒继续使用系统通知、提示音或震动。
  5. 不强制修改系统音量,不绕过勿扰模式;无法取得音频焦点时跳过播报。
  6. TTS 使用稳定 utterance_id;同一 message_id 进入终态后不得重复朗读。
  7. 只有通过播放策略后才发送 reminder.tts.start.command,避免为必然跳过的提醒产生外部调用和费用。
  8. 客户端不接触阿里云凭证;流结束、失败或取消后立即清空 PCM 缓冲。

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. 根据检查结果决定是否触发软件提醒。
  6. 触发系统日程和系统闹钟删除指令。
  7. 根据触发原因选择固定模板编号、模板版本和允许的参数,不生成自然语言文案。
  8. 调用服务端 ReminderTemplateRenderer 生成唯一 rendered_text,写入可靠提醒消息记录, 供客户端展示和后续 TTS 合成共同引用。

4.2.6 地点判定模块

职责:

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

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

职责:

  1. ReminderTemplateRenderer 固定使用开头 提醒: 和结尾 ,根据 template_idtemplate_version 与白名单参数生成中间 reminder_body,再组合出 唯一 rendered_text,不调用 LLM。
  2. 将模板字段、reminder_bodyrendered_textmessage_id 一起写入可靠提醒消息记录; 不写入 schedules 业务正文。
  3. reminder.control、主提醒 ACK 校验和 Qwen3TtsGateway 只能引用这一记录, 不得各自重新拼接提醒内容。
  4. 客户端降级渲染规范与服务端保持同版本;发现内容不一致时禁止启动 TTS,并上报契约错误。

4.2.8 Qwen3-TTS-VD Gateway 与流式转发模块

职责:

  1. Qwen3TtsGateway 从服务端密钥与配置系统读取 DashScope 凭证、模型和 voice_id, 建立阿里云实时合成会话。
  2. 网关只发送可靠提醒消息记录中的 rendered_text,不接受客户端自由文本。
  3. TtsStreamRelay 将供应商音频流转换为 TimeFlow WebSocket 下行 Binary Frame, 并执行有界背压。
  4. 客户端断线、取消、缓冲超限或播放失败时终止供应商会话。
  5. 不记录密钥或完整合成文本,不持久化音频,只记录脱敏的请求、流和错误标识。

4.2.9 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 地点提醒触发时间,可为空
system_schedule_ref_id text 系统日历日程 ID,可为空
system_alarm_ref_id text 系统闹钟 ID,可为空
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 等数据库字段。

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):重连后的增量对齐。
  3. system_schedule_ref_id 非空值索引:系统日程引用定位。
  4. system_alarm_ref_id 非空值索引:系统闹钟引用定位。

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

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;客户端对同一 message_id 只执行一次,并可重复回传同一 ACK。
  3. request_id 的幂等范围是“当前用户 + 消息类型”;服务端至少保存到该操作进入终态。
  4. 同一 request_id 若收到不同 payload,返回 IDEMPOTENCY_CONFLICT,不得覆盖第一次结果。
  5. *.command*.query*.result*.error 的业务字段统一放入 payload; 7.4—7.11 已有事件型消息保留根部业务字段以避免大改,后续不得在同一消息类型中混用两种结构。
  6. 未知 type 返回 UNSUPPORTED_MESSAGE_TYPE;多余字段按协议版本策略处理, 缺少必填字段返回 VALIDATION_ERROR
  7. 服务端推送只有在收到业务 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,
        "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"
    }
  }
}

规则:

  1. 用户身份由 WebSocket 会话上下文确定,客户端不传 user_id
  2. 默认只返回 scheduleddone
  3. include_deleted=true 时才返回 deleted 数据。
  4. 返回结果按 start_time asc nulls last, created_at desc 排序。
  5. 返回日程时包含 system_schedule_ref_idsystem_alarm_ref_id

7.4 WebSocket 连接

地址

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

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

作用

  1. 保持设备在线。
  2. 回传语音结构化结果。
  3. 下发提醒控制消息。
  4. 下发系统日程删除指令。
  5. 下发系统闹钟删除指令。
  6. 接收位置信息和确认事件。

连接建立

客户端进入界面后先发:

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

服务端下发固定模板信息和唯一 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}”。

渲染规则:

  1. 标题为空时使用“一项日程”。
  2. minutes_before 非法时,TIME_ADVANCE 降级为 GENERIC
  3. 地点为空时,LOCATION_ENTER 降级为 GENERIC
  4. 未知模板或版本降级为 GENERIC
  5. 只允许标题、提前分钟数和地点名称;不使用备注、详细地址或坐标。
  6. 模板含义变化必须增加版本。
  7. 服务端只组合一次固定头、中间内容和固定尾,并保存结果。
  8. 客户端只校验并使用服务端结果,不为 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 失败而改为失败。

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 终态已可靠接收

启动 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 供应商不可用或合成启动失败

音频 Binary Frame

  1. 方向固定为服务端到客户端。
  2. 载荷是 pcm_s16le24000Hz、单声道原始音频字节。
  3. Binary Frame 必须出现在 stream.started 之后、stream.endedstream.error 之前。
  4. 空 Binary Frame 非法;不同 stream_id 的数据不得合并。
  5. 客户端只把 Binary Frame 交给当前活动 stream_idTtsStreamPlayer
  6. 断线后的音频分片不做续传;是否重试由防重复规则决定。

流结束

{
  "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 用户操作、客户端状态变化或连接关闭导致取消

取消 TTS

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

取消必须幂等。流已取消或已经结束时,重复取消返回当前终态,不重新创建或恢复流。

约束:

  1. 启动命令只引用已有提醒,不接受客户端自由文本。
  2. 服务端按 message_id 读取已保存的 rendered_text 并发送给阿里云。
  3. 客户端不接收 DASHSCOPE_API_KEY,也不直接连接阿里云。
  4. 每个设备同一时刻最多有一个活动 TTS 下行流。
  5. 同一 message_id 固定生成 utterance_id=tts_{message_id},不得重复播报。
  6. 服务端流结束不等于客户端播放完成;最终结果以前端播放器回调为准。
  7. 所有客户端命令都必须携带 request_id;相同请求重放返回原结果。
  8. 服务端推送使用原 message_idutterance_idstream_id 关联同一次播报。

TTS 播报结果

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

结果规则:

  1. status 只能是 completedskippedfailed
  2. completedreason=nullstream_id 必填。
  3. skipped 时流尚未启动,stream_id=null;原因只能是 disabledbackground_disableddevice_lockedplatform_restricted
  4. failed 时原因只能是 engine_unavailablelanguage_unsupportedaudio_focus_deniedsynthesis_failedstream_interruptedplayback_failed
  5. completed 只能由客户端播放器完成回调确认。
  6. TTS 结果与 reminder.control.ack 相互独立,不改变日程状态或系统兜底清理判断。
  7. 客户端在收到 result.ack 前按原 request_id 重试;服务端必须幂等返回同一结果。

TTS 状态机

IDLE
├─ 策略拒绝 ─> SKIPPED
└─ start.command ─> STARTING
                     ├─ start.error ─> FAILED
                     └─ stream.started ─> STREAMING
                                          ├─ stream.error ─> FAILED
                                          ├─ cancel.command ─> CANCELLED
                                          └─ stream.ended ─> DRAINING
                                                               ├─ 播放完成 ─> COMPLETED
                                                               └─ 播放失败 ─> FAILED

SKIPPEDCOMPLETEDFAILEDCANCELLED 是终态;相同 utterance_id 进入终态后不得再次启动播报。CANCELLED 通过取消结果记录,不转换成“已播放完成”。

7.8 系统引用检查指令

作用

在服务端准备下发软件提醒或删除系统兜底提醒之前,先确认用户是否已经在系统日程或系统闹钟中手动删除了对应事项。

如果用户已经删除系统侧提醒,说明用户可能已经表达“不再需要提醒”的意图,服务端不应继续下发应用内弹窗、系统级全局弹窗或删除指令。

服务端不能直接查询 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"
}

客户端处理

  1. 根据 system_schedule_ref_id 查询系统日程是否仍存在。
  2. 根据 system_alarm_ref_id 查询系统闹钟是否仍存在。
  3. 将查询结果通过 WS 回传服务端。
  4. 只做存在性检查,不重新创建系统日程或系统闹钟。

客户端响应

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

服务端处理

  1. 如果 system_schedule_exists=truesystem_alarm_exists=true,说明兜底提醒仍存在,继续执行软件提醒和重复提醒删除流程。
  2. 如果此前成功登记的引用均返回 exists=false,且本次检查成功,才可解释为用户可能已手动删除系统侧提醒。
  3. 满足第 2 条时,服务端取消本次软件提醒;MVP 可将日程更新为 deleted 并停止后续监听。
  4. 从未登记引用、引用为空、权限不足或检查失败时,不得据此删除业务日程。
  5. 如果客户端超时未响应,服务端按当前在线状态继续软件提醒,但保留系统兜底,避免漏提醒。

7.9 系统日程删除指令

服务端消息

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

前端处理

  1. 删除系统日程。
  2. 取消重复通知。
  3. 继续保留业务日程本体。

客户端响应

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

7.10 系统闹钟删除指令

服务端消息

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

前端处理

  1. 删除系统闹钟。
  2. 取消重复闹钟提醒。
  3. 继续保留业务日程本体。

客户端响应

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

7.11 日程确认消息

作用:

  1. 由客户端主动告诉服务端,用户已经在应用内确认该日程完成。
  2. 关闭后续监听和所有提醒。
  3. 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"
}

服务端处理

  1. 取消监听。
  2. 终止后续提醒。
  3. 如系统日程或系统闹钟仍存在,按需删除。

服务端响应

{
  "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.12 系统引用登记

作用

客户端成功创建系统日程或系统闹钟后,必须把引用 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": []
  }
}

规则:

  1. 两个引用可以分别登记,但至少一个不为空。
  2. 服务端校验日程属于当前 WebSocket 会话用户。
  3. 同一日程重复登记相同引用返回原结果。
  4. 同一日程登记不同引用时视为替换;服务端通过 replaced_refs 返回旧引用,由客户端清理旧平台资源。
  5. 客户端创建平台资源成功但登记失败时,将待登记记录保留在本地并重试。

7.13 日程状态变更

完成和删除统一使用显式状态命令;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. 状态事务提交后才发送系统资源清理指令;清理失败不回滚业务状态,但必须继续重试并可观测。

7.14 连接保活、重连与恢复

心跳

  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",
      "req_refs_register_001"
    ],
    "pending_message_ids": [
      "msg_reminder_001"
    ]
  }
}

服务端处理:

  1. 返回各 pending_request_ids 的已知终态;未知请求由客户端按原 request_id 重发。
  2. 返回 last_schedule_updated_at 之后发生变化的日程。
  3. 重发未收到 ACK 且仍有效的服务端推送,保持原 message_id
  4. 已完成、已删除、已过期或已 ACK 的提醒不得再次激活。
  5. 音频 Binary Frame 不做断点续传;断线中的语音流标记失败,客户端重新录音。

8. 监听与提醒策略

8.1 时间监听

规则:

  1. 只有 start_time 存在的日程才参与时间监听。
  2. 时间到达前进入监测窗口。
  3. 默认提前 15min
  4. 进入窗口后优先判断 WS 在线状态和前后台状态。
  5. 如果 WS 在线,服务端先下发 system.refs.check
  6. 如果客户端确认系统日程和系统闹钟都已不存在,服务端取消本次软件提醒并停止监听。
  7. 如果系统侧兜底提醒仍存在,服务端下发 reminder.control;只有收到展示成功 ACK 后,才按需下发系统日程和系统闹钟删除指令。

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. 日程被删除。
  4. 客户端确认系统日程和系统闹钟均已被用户手动删除。

8.5 提醒与系统兜底清理顺序

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

  1. 服务端判定提醒条件满足。
  2. 服务端下发 system.refs.check
  3. 客户端返回引用存在性;引用检查失败或超时时,保留系统兜底。
  4. 服务端下发带 message_idreminder.control
  5. 服务端渲染固定模板并随 reminder.control 下发唯一 rendered_text;客户端校验后由 应用内弹窗和系统通知直接复用。
  6. 客户端主提醒实际展示成功后返回包含同一 rendered_textreminder.control.ack(ok=true)
  7. 客户端按 TTS 策略异步决定跳过或发送 reminder.tts.start.command;播放终态再单独发送 reminder.tts.result
  8. 服务端收到主提醒成功 ACK 后,才下发系统日程/系统闹钟删除指令。
  9. 客户端删除成功后回传 ACK;服务端清空对应引用 ID。
  10. 主提醒失败、ACK 超时或删除失败时,不提前清空引用,并按幂等规则重试。

禁止仅凭“WS 在线”或“消息已写入 Socket”删除系统兜底。 禁止等待 TTS 播报完成后才确认主提醒;TTS 失败不能阻止已经展示成功的主提醒进入清理流程。

8.6 系统引用不存在的判定

system_schedule_ref_idsystem_alarm_ref_id 为空表示该兜底从未登记,不能等同于“用户手动删除”。 只有满足以下全部条件,服务端才可把双引用不存在解释为用户取消意图:

  1. 两个引用此前至少有一个成功登记。
  2. 本次检查本身 ok=true
  3. 已登记的引用均返回 exists=false
  4. 客户端没有返回权限缺失、查询失败或平台不支持。

条件不满足时保留业务日程,按可用通道继续提醒或提示用户修复权限,不自动把 status 改为 deleted

8.7 固定模板 TTS 执行流程

执行顺序:

  1. 客户端收到 reminder.control,先按 message_id 检查主提醒和 TTS 是否已经进入终态。
  2. 客户端 ReminderTemplateRenderer 校验服务端文本满足 提醒: + reminder_body + 。,并校验模板、版本和参数一致;不一致时拒绝启动 TTS, 按安全降级规则展示 GENERIC
  3. 应用内弹窗或系统通知直接使用校验后的服务端 rendered_text 展示主提醒。
  4. 主提醒展示成功后立即发送 reminder.control.ack,不等待 TTS。
  5. TtsPlaybackPolicy 依次检查 tts_enabled、前后台、tts_background_enabled、锁屏、 音频焦点和平台限制。
  6. 策略不允许时不发送启动命令、不调用外部服务,发送 status=skipped 和对应 reason
  7. 策略允许时,客户端使用稳定 utterance_id 发送 reminder.tts.start.command,不携带 API Key 或自由文本。
  8. 服务端读取提醒消息记录中已经生成的 rendered_text,核对主提醒 ACK 后,通过 Qwen3TtsGateway 将该文本发送给 qwen3-tts-vd-realtime-2026-01-15 流式合成。
  9. 服务端把供应商音频流转换为 TimeFlow WebSocket Binary Frame 下发,客户端 TtsStreamPlayer 边接收边播放。
  10. 只有本地播放器完成回调上报 completed;供应商、传输或播放失败上报对应 failed
  11. 完成或失败后释放音频焦点、PCM 缓冲、TimeFlow 流状态和 DashScope 会话,不持久化合成音频。
  12. 任一 TTS 终态都写入客户端最小投递状态;相同 message_id 重连或重试时只重发结果,不重复朗读。

TTS 降级边界:

  1. 全局开关关闭:skipped/disabled
  2. 后台开关关闭:skipped/background_disabled
  3. 设备锁屏:skipped/device_locked
  4. 平台禁止后台音频:skipped/platform_restricted
  5. 网络不可用、DashScope 鉴权失败、声音配置无效、阿里云不可用或限流、音频焦点失败、 合成、转发或播放失败:上报对应结果,继续保留已经展示的主提醒。
  6. App 进程死亡时不执行 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 恢复 仅凭网络连接状态声明在线
系统日程/闹钟创建失败 保留失败原因并提示可用降级通道 保存不存在的引用 ID
系统引用检查失败 保留兜底并继续安全提醒策略 推断用户已经取消提醒
软件提醒展示失败 保留系统兜底,允许幂等重试 先删除系统兜底
系统资源删除失败 保留引用并重试,避免再次发送软件提醒 清空引用后宣称删除成功
TTS 模板未知或参数非法 降级为 GENERIC,继续展示主提醒 阻塞通知或临时调用 LLM 生成文案
TTS 开关关闭、后台未授权或设备锁屏 跳过播报并上报明确原因 强制播报或修改系统音量
TTS 配置或鉴权失败 禁用本次云端合成、告警并保留主提醒 把 API Key 放入客户端或运行时切换未知模型
供应商不可用、超时或限流 结束本次 TTS,执行有限重试策略 无限重试、重复计费或重复朗读
音频流中断或背压超限 取消供应商与客户端流并上报失败 使用无界缓冲或合并不同 stream_id
音频焦点或客户端播放失败 上报播放失败并保留主提醒 把云端合成完成当作用户已听到
WebSocket 重复下发相同 TTS 返回已保存的 TTS 终态 使用相同 message_id 再次朗读
位置权限不可用 暂停地点监听并提示权限状态;有时间条件时继续时间提醒 把权限失败当作未进入围栏
服务端重启 schedules 恢复时间扫描;位置监听等待新位置 依赖仅存在于内存的触发状态宣称完整恢复

一致性边界:

  1. 日程写入和 request_id 幂等结果必须在同一事务边界内提交,或使用能保证原子可见性的等价实现。
  2. 服务端事务提交成功后才能下发 schedule.upsert.result
  3. WebSocket 推送采用至少一次投递;客户端依靠 message_id 去重。
  4. time_triggered_atgeo_triggered_at 只表示条件已命中,不等于客户端已经展示成功。
  5. 若实现需要区分“已命中”和“已展示”,MVP 可先保存在可靠消息记录中;不得复用 status 表达投递过程。
  6. TTS 使用主提醒的 message_id 去重,但 TTS 终态与主提醒 ACK 相互独立。
  7. 供应商完成只表示音频生成完成;必须等待服务端转发结束和客户端播放器完成回调后, 才能记录 TTS completed

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

  1. WebSocket 生产环境只允许 wss://
  2. 用户身份必须来自鉴权会话;device_id 只标识设备,不能代替用户认证。
  3. 服务端对每次日程读写、引用登记和状态变更校验资源归属。
  4. 音频只为本次解析处理;是否持久化、保留时长和删除策略必须显式配置,默认不长期保存原始音频。
  5. 日志不得记录原始音频、完整转写文本、精确经纬度或令牌;排障使用 request_idmessage_idjob_id 和脱敏错误信息。
  6. 位置上报只在存在有效地点日程且用户授权时启用;无有效监听对象时停止高频上报。
  7. ASR、LLM 和地图依赖必须配置超时、有限重试和熔断;重试不得绕过用户确认。
  8. TTS 固定模板只允许标题、提前分钟数和地点名称,不得播报备注、详细地址、经纬度或令牌。
  9. 锁屏状态不播报 TTS;后台播报必须由用户显式开启,关闭开关后立即停止待播报内容。
  10. TTS 不得绕过系统静音或勿扰策略,不得为了播报强制修改媒体或闹钟音量。
  11. DASHSCOPE_API_KEY 和可选 Workspace ID 只存在于服务端密钥管理系统,不进入客户端、 WebSocket 业务响应、日志或崩溃报告。
  12. 合成文本发送给阿里云前必须来自创建 reminder.control 时保存的唯一 rendered_text,并与主提醒 ACK 一致;不得接受启动命令中的自由文本,也不得发送备注、 地址、坐标或其他上下文。
  13. 供应商音频只在服务端内存中转换并实时转发;服务端和客户端均默认不落盘、 不缓存到业务数据库。
  14. 客户端断开、取消、缓冲超限或播放失败时,服务端必须取消对应 DashScope 会话, 避免继续生成和计费。

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. 软件提醒 ACK 成功前,系统兜底不会被删除。
  3. 删除系统资源失败时引用仍保留,重试不会重复展示软件提醒。
  4. 系统引用从未登记、已被用户删除、无权限查询三种情况能被区分。
  5. 设备重启、App 进程死亡和服务端重启后,未完成日程仍能恢复到正确监听状态。
  6. TTS 完成、跳过或失败均不改变主提醒 ACK 和系统兜底清理判断。

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. TIME_ADVANCETIME_DUELOCATION_ENTERGENERICreminder_body 及最终 rendered_text 分别与文档定义完全一致。
  3. 通知、应用内弹窗和 TTS 使用同一次 rendered_text,不存在通道间文案差异。
  4. 标题为空时使用“一项日程”;提前分钟数非法、地点为空、模板未知或版本未知时降级为 GENERIC
  5. tts_enabled=false 时不初始化 TTS,主提醒正常展示。
  6. 后台未授权、锁屏或平台受限时返回对应 skipped,不得自动播报。
  7. DASHSCOPE_API_KEY 不出服务端;客户端收到的开始消息只包含模型、voice_idstream_id 和 PCM 播放参数。
  8. 相同 message_id 重复到达时不重复朗读,只重发已保存的 TTS 结果。
  9. 网络不可用、DashScope 鉴权失败、阿里云拒绝或限流、合成、转发和播放失败时, 返回对应结果且主提醒状态不受影响。
  10. 阿里云返回合成完成但本地播放失败时不得上报 completed;只有播放器完成回调可以上报完成。
  11. 超过服务端安全长度限制的合成文本按规则降级或截断,不得直接发送给阿里云。
  12. 运行时模型严格为 qwen3-tts-vd-realtime-2026-01-15,已审批 voice_id 与模型匹配。
  13. 启动、流开始、Binary Frame、流结束、流错误、取消和最终结果接口均通过成功、失败、 重复和乱序契约测试。
  14. 播放器在完整音频接收完成前开始播放,所有音频严格绑定当前唯一活动 stream_id
  15. 供应商完成但客户端未播放完成时不得上报 completed;客户端断线会终止供应商会话。
  16. App 前台、后台、锁屏、静音、勿扰、断网、API Key 无效、Voice ID 无效和进程死亡场景 均有目标设备或集成环境验证记录。

13. 官方参考

  1. 千问 3 TTS VD 模型说明
  2. 语音合成模型列表
  3. 千问实时语音合成 WebSocket 交互流程
  4. 千问实时语音合成客户端事件
  5. 声音设计使用指南

14. 结论

架构文档已细化并通过静态检查。

Clone this wiki locally