-
Notifications
You must be signed in to change notification settings - Fork 6
Architecture interface design
- 客户端不再独立承担全部语音理解和提醒编排。
- 客户端进入界面后建立 WebSocket 连接,作为后续实时通信主通道。
- 音频通过 WebSocket Binary Frame 流式传输到服务端。
- 服务端完成 ASR + LLM 结构化提取后,通过 WebSocket 回传结果。
- 客户端用统一表单完成二次确认、地理位置补全、时间补全和提醒参数补全。
- 日程最终落到单一
schedules表中,状态和关联信息一并保存。 - 提醒采用三段式策略,优先保证触达,再处理重复提醒控制。
- TTS 是智能提醒模块的第三方附加通道:智能提醒模块提供
reminder_body, 服务端组合固定头尾后通过 TTS 网关生成音频;TTS 失败不影响事项保存或主提醒。 - 所有有效提醒在日程或待办创建、确认或相关编辑提交后异步预生成音频,并以确定性文件名 存入云端对象存储;数据库不保存 TTS 文件名、URL、状态或音频数据。
- 在线提醒组合为“震动 + 云端 TTS + 客户端自定义弹窗”;离线提醒组合为 “震动 + 客户端固定铃声 + 客户端自定义弹窗”。
这一版的重点不是“完整日历”,而是“日程创建 + 状态驱动提醒 + 语音结构化”闭环。
本版明确包含:
- 单用户、单主要设备上的日程创建、编辑、查询、完成和删除。
- 一条活动 WebSocket 连接上的语音流、业务命令、查询和服务端推送。
- 时间提醒、地点提醒,以及同时包含时间和地点的日程。
- 断线重连、重复消息和离线提醒终态的最小恢复能力。
- TTS 音频预生成和云端对象存储,以及在线云端 TTS、离线固定铃声两种提醒组合。
本版暂不包含:
- 多用户共享日历。
- 系统日历、系统闹钟及其引用同步与兜底。
- 周期性日程和复杂重复规则。
- 平台后台能力不可用时的系统级兜底提醒。
- 用 LLM 自主决定最终日程或提醒时间;LLM 只生成待用户确认的草稿。
- 断网时访问云端 TTS 音频;断网提醒只使用客户端预置固定铃声。
触发方式:
- 应用内弹窗。
- 页面内高亮提示。
- 伴随声音或震动。
- 按智能提醒内容播放云端预生成 TTS。
适用场景:
- 用户正在使用 App。
- 可以给出最完整的上下文。
- 不需要系统级强打断。
触发方式:
- 客户端自定义弹窗。
- 震动。
- 播放云端预生成 TTS。
适用场景:
- App 仍保持在线。
- 需要由系统层接管提醒触达。
- 可以通过 WS 收到实时控制消息。
这里的“网络通畅”必须同时满足:客户端网络可用、WebSocket 心跳正常、服务端会话有效。 仅检测到 Wi-Fi/蜂窝网络不能判定为此情况;后台进程或 WS 不可用时立即按情况三降级。
客户端通过已登记的本地触发条件执行震动、客户端预置固定铃声和客户端自定义弹窗; 断网时不访问云端音频或外部 TTS 服务。
日程支持两种入口:
- 手动创建。
- 语音创建。
二者共用一个表单。
语音创建流程:
- 客户端录音。
- 客户端通过 WebSocket Binary Frame 流式发送音频。
- 服务端返回上传受理结果。
- 服务端完成 ASR。
- 服务端完成 LLM 结构化提取。
- 服务端通过 WS 推送结构化日程草稿。
- 客户端弹出统一表单。
- 用户补全信息并确认。
- 客户端提交日程最终结果。
- 如果日程包含时间,服务端做时间冲突检测并返回提示。
- 创建成功后,服务端和客户端分别进入自己的提醒准备状态;服务端异步预生成关联 TTS 音频, 不阻塞创建结果。
日程允许两种主类型:
| 形态 | schedule_type |
必要信息 | 触发条件 |
|---|---|---|---|
| 时间类 | time |
start_time |
进入时间提醒窗口 |
| 地点类 | location |
latitude + longitude
|
用户进入地理围栏 |
语音创建时,schedule_type 由 LLM 根据用户语义做意图识别后输出。手动创建时,由前端根据用户选择或填写内容确定。
schedule_type 只表示主意图类型,时间和地点同时存在时仍按 time 落库,是否触发地点提醒由字段本身判断。
客户端负责:
- 界面展示。
- WebSocket 长连接。
- 音频录制。
- 音频上传。
- 表单编辑和二次确认。
- 本地日程缓存。
- 前台和后台客户端自定义弹窗提醒。
- 位置信息上报。
- 根据前后台状态和系统可用性选择具体提醒通道。
- 展示客户端自定义提醒弹窗并执行震动。
- 在线时接收云端 TTS 文件流并播放;离线时播放客户端预置固定铃声。
服务端负责:
- 音频文件接收。
- ASR 调用。
- LLM 结构化提取。
- WebSocket 消息分发。
- 日程冲突检测。
- 日程状态监控。
- 地点与时间窗口判断。
- 提醒控制消息下发。
- 提醒通道选择由客户端自行完成。
- 接收智能提醒模块的
reminder_body,通过ReminderTemplateRenderer组合固定头尾; TTS 不调用 LLM 生成提醒规则或提醒内容。 - 在事项创建、确认或相关编辑提交后,通过 TTS 生成网关异步生成音频,并按确定性标识写入 云端音频存储。
- 在线提醒触发时校验事项和云端文件,从对象存储读取音频并通过 WebSocket 流式下发。
- 第三方 ASR 服务。
- 大模型服务。
- 地图与位置服务。
- Android 客户端震动、音频播放和后台运行能力。
- 外部 TTS 服务。
- 云端音频存储。
外部 TTS 接入边界:
- 客户端不得直连外部 TTS 服务;所有调用由服务端 TTS 网关封装。
- 供应商凭证只存在于服务端密钥管理边界,不下发到客户端。
- 固定模板文本必须先通过字段白名单、长度和 Unicode 安全校验。
- 外部 TTS 生成结果进入云端音频存储;数据库不保存文件名、URL、状态或二进制。
- TTS 供应商、模型和音色属于可替换实现配置,不进入业务契约。
- 界面模块只能通过 WebSocket 模块提交业务命令,不直接调用 ASR、LLM 或数据库。
- 语音解析模块只产出草稿,不创建正式日程;只有用户确认后的
schedule.upsert.command才能写入schedules。 - 监控与调度模块只读取有效日程并生成提醒控制决策,不直接选择 Android 展示通道。
- 提醒执行模块只执行客户端能力并回传结果,不自行修改服务端日程状态。
- 地点判定模块只输出“围栏外/围栏内/位置不可用”的判定,不直接发送提醒。
- 智能提醒模块拥有提醒内容和触发决策;
ReminderTemplateRenderer只组合提醒: + reminder_body + 。。 - 服务端
TtsGenerationGateway负责预生成,TtsAssetStorage负责云端音频生命周期, 客户端ReminderChannelPolicy负责在线/离线提醒组合。 - TTS 不拥有独立提醒状态,不得因播报成功或失败修改
schedules.status或主提醒 ACK。 - 客户端不得持有或接收外部 TTS 凭证;服务端网关是外部 TTS 的唯一调用方。
- 外部 TTS 鉴权、生成或云端存储失败不得阻塞事项保存或主提醒。
ASR 上行与 TTS 提醒链路必须严格区分:
ASR 上行:
用户说话
-> 客户端录音
-> TimeFlow WebSocket Binary Frame
-> 服务端 ASR
-> 文本与结构化草稿
TTS 预生成:
日程/待办创建、确认或相关编辑提交
-> 智能提醒模块生成 reminder_body
-> ReminderTemplateRenderer 组合固定头尾
-> TtsGenerationGateway 生成音频
-> TtsAssetStorage 按确定性标识写入云端
在线提醒:
智能提醒模块命中条件
-> 服务端校验事项和云端文件
-> 震动 + 客户端自定义弹窗
-> 服务端从对象存储读取音频
-> TimeFlow WebSocket Binary Frame
-> 客户端播放云端 TTS
离线提醒:
客户端本地触发条件命中
-> 震动 + 客户端固定铃声 + 客户端自定义弹窗
边界规则:
- TTS 只消费智能提醒模块的提醒内容,不重新决定提醒时间、优先级或通道。
- 在线必须同时满足网络可用、WebSocket 心跳正常和客户端会话有效。
- 断网时不访问云端音频存储或外部 TTS 服务,也不补播已完成的离线提醒。
- 云端音频通过现有 TTS 流接口下发;客户端不直接访问外部 TTS 服务。
- TTS、固定铃声和震动都不改变主提醒 ACK 或事项状态。
- 相同事项、内容哈希和提醒消息必须幂等,避免重复生成和重复播放。
职责:
- 显示日程列表。
- 显示创建/编辑表单。
- 显示语音解析结果。
- 显示冲突提示。
职责:
- 录音。
- 音频格式转换。
- 发起上传。
职责:
- 建立和维护连接。
- 接收结构化草稿。
- 接收提醒控制消息。
- 上传位置信息。
- 上传日程确认结果。
职责:
- 缓存日程。
- 缓存解析结果。
- 缓存提醒状态。
- 缓存提醒通道状态。
- 按现有
schedule_id、updated_at、触发类型和对应*_triggered_at组成的复合键保存 本次 TTS 终态,防止 WebSocket 重试造成重复播报。 - 保存本地提醒触发信息;不缓存云端 TTS 音频。
职责:
- 前台弹窗。
- 后台浮窗。
- 根据当前应用状态决定提醒展示方式。
- 客户端自定义提醒弹窗的样式、布局和交互。
-
ReminderChannelPolicy根据在线状态选择“震动 + 云端 TTS + 自定义弹窗”或 “震动 + 固定铃声 + 自定义弹窗”。 -
TtsStreamPlayer管理云端音频下行流和播放结果。 - 离线提醒执行能力负责震动、客户端预置固定铃声和自定义弹窗。
固定约束:
- 在线状态使用云端 TTS,离线状态固定使用客户端铃声,不在两条路径间重复播放声音。
- 客户端弹窗、震动和固定铃声不依赖外部 TTS 凭证。
- 不强制修改系统音量,不绕过勿扰模式。
- 同一
schedule_id + updated_at + trigger_kind + triggered_at只执行一次可见提醒和一次声音提醒。 - TTS 失败只影响语音通道,不影响震动、自定义弹窗或主提醒。
职责:
- 地点搜索。
- 地点确认。
- 地理围栏设置。
- 实时位置上报。
职责:
- 接收音频文件。
- 校验格式和大小。
- 生成处理任务 ID。
职责:
- 调用 ASR。
- 调用 LLM。
- 识别
schedule_type。 - 生成结构化草稿。
职责:
- 建立客户端会话。
- 维护设备在线状态。
- 分发解析结果和提醒控制消息。
- 接收位置上报。
职责:
- 创建日程。
- 编辑日程。
- 查询日程。
- 冲突检测。
职责:
- 监听日程时间。
- 监听地理位置。
- 判断时间、空间以及组合触发条件。
- 条件满足后生成软件提醒控制消息。
- 调用智能提醒模块取得
reminder_body和触发决策,不由 TTS 模块生成提醒规则。 - 事项创建、确认或相关编辑提交后发布异步 TTS 预生成任务。
职责:
- 根据上报位置和日程坐标计算距离。
- 判断是否进入地理围栏。
职责:
- 接收智能提醒模块产出的
reminder_body,固定组合为提醒: + reminder_body + 。,不调用 LLM 生成提醒内容。 - 使用现有用户、事项、版本或
updated_at、提醒内容和触发字段完成校验。 - 根据固定头、
reminder_body、固定尾、模板版本和音色版本计算content_hash。 - 不向
schedules、todos或其他业务表写入 TTS 字段。
职责:
-
TtsGenerationGateway封装外部 TTS 调用,不向业务模块暴露供应商协议。 - 在异步预生成任务中发送经校验的
rendered_text;生成失败不回滚事项事务。 - 将生成结果交给
TtsAssetStorage,不直接写业务数据库。 - 相同事项和
content_hash已存在云端音频时直接复用,不重复生成。
职责:
-
TtsAssetStorage使用由user_id + object_type + object_id + content_hash组成的确定性音频标识。 - 音频标识不得包含标题、地点、备注等用户明文内容。
- 在线触发时根据当前事项重新计算标识,校验对象存在后流式读取。
- 编辑产生新哈希时写入新文件;删除、完成、取消和失效事项按事项前缀进入清理流程。
- 文件存在性、复用和清理通过对象存储完成,不新增数据库状态字段或音频资产表。
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 | 地点提醒触发时间,可为空 |
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等数据库字段。 - 不增加
audio_file_name、audio_url、content_hash、音频状态或音频二进制字段, 也不增加 TTS 音频资产表。 - 云端文件名由现有
user_id、事项类型、事项 ID、提醒内容、模板版本和音色版本运行时计算。 - 云端文件存在性通过对象存储检查,不通过数据库记录判断。
数据库迁移和服务端模型必须同时实现以下约束:
-
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):重连后的增量对齐。
索引是查询和调度约束,不改变“一张核心业务表”的方案。
建议状态流转:
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只标识本次 WebSocket 发送,用于链路追踪;消息重发时允许 生成新值,不得作为提醒展示或 TTS 播放的业务幂等键。 -
request_id的幂等范围是“当前用户 + 消息类型”;服务端至少保存到该操作进入终态。 - 同一
request_id若收到不同payload,返回IDEMPOTENCY_CONFLICT,不得覆盖第一次结果。 - 提醒业务去重使用现有数据库字段组成的
schedule_id + updated_at + trigger_kind + triggered_at复合键,不新增数据库字段。 -
trigger_kind和triggered_at只是对现有time_triggered_at或geo_triggered_at的运行时表达,不写入新字段。 -
*.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,
"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排序。 - 返回日程不包含系统日历 ID、系统闹钟 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"
}
}
}- 服务端根据当前位置计算日程距离。
- 如果日程包含地点且命中围栏,先检查系统引用是否仍存在。
- 若日程还包含时间,再结合时间窗口决定是否下发提醒控制消息。
reminder.controlreminder.control.ackreminder.tts.start.commandreminder.tts.stream.startedreminder.tts.stream.endedreminder.tts.stream.errorreminder.tts.resultreminder.tts.result.ack
- 下发主提醒并返回执行结果。
- 在线时通过 WebSocket 下发 TTS 音频流。
- 返回 TTS 播放结果。
{
"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分钟后开始。"
}
}{
"type": "reminder.control.ack",
"message_id": "msg_reminder_001",
"request_id": "req_reminder_ack_001",
"schedule_id": "schedule_001",
"updated_at": "2026-07-29T14:45:00+08:00",
"trigger_kind": "time",
"triggered_at": "2026-07-29T14:45:00+08:00",
"ok": true,
"connectivity_mode": "online",
"channels_executed": [
"vibration",
"tts",
"custom_popup"
],
"rendered_text": "提醒:项目会议将在15分钟后开始。"
}{
"type": "reminder.tts.start.command",
"request_id": "req_tts_001",
"payload": {
"schedule_id": "schedule_001",
"updated_at": "2026-07-29T14:45:00+08:00",
"trigger_kind": "time",
"triggered_at": "2026-07-29T14:45:00+08:00"
}
}流开始:
{
"type": "reminder.tts.stream.started",
"request_id": "req_tts_001",
"ok": true,
"payload": {
"schedule_id": "schedule_001",
"updated_at": "2026-07-29T14:45:00+08:00",
"trigger_kind": "time",
"triggered_at": "2026-07-29T14:45:00+08:00",
"stream_id": "tts_stream_001",
"audio_format": "wav"
}
}流结束:
{
"type": "reminder.tts.stream.ended",
"schedule_id": "schedule_001",
"updated_at": "2026-07-29T14:45:00+08:00",
"trigger_kind": "time",
"triggered_at": "2026-07-29T14:45:00+08:00",
"stream_id": "tts_stream_001",
"status": "completed"
}{
"type": "reminder.tts.stream.error",
"schedule_id": "schedule_001",
"updated_at": "2026-07-29T14:45:00+08:00",
"trigger_kind": "time",
"triggered_at": "2026-07-29T14:45:00+08:00",
"stream_id": "tts_stream_001",
"error": {
"code": "TTS_AUDIO_FILE_UNAVAILABLE",
"message": "云端 TTS 音频不可用"
}
}{
"type": "reminder.tts.result",
"request_id": "req_tts_result_001",
"schedule_id": "schedule_001",
"updated_at": "2026-07-29T14:45:00+08:00",
"trigger_kind": "time",
"triggered_at": "2026-07-29T14:45:00+08:00",
"stream_id": "tts_stream_001",
"status": "completed",
"reason": null
}{
"type": "reminder.tts.result.ack",
"request_id": "req_tts_result_001",
"ok": true
}-
message_id用于服务端推送追踪;request_id用于客户端请求幂等。 - 提醒身份由
schedule_id + updated_at + trigger_kind + triggered_at确定。 - 服务端必须校验日程归属、状态和版本;客户端不能指定云端音频对象。
- Binary Frame 只能在
reminder.tts.stream.started成功后发送。 - 只有客户端播放器完成回调可以上报
status=completed。 - TTS 失败不改变
reminder.control.ack或日程状态。 - 离线模式不发起 TTS 流请求。
作用:
- 由客户端主动告诉服务端,用户已经在应用内确认该日程完成。
- 关闭后续监听和所有提醒。
- 该消息只表达用户显式完成动作。
{
"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"
}
}
}完成和删除统一使用显式状态命令;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和当前日程摘要,不执行覆盖。 - 状态事务提交后停止后续提醒和监听;TTS 云端文件按事项前缀进入异步清理流程。
- 客户端按服务端
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"
]
}
}服务端处理:
- 返回各
pending_request_ids的已知终态;未知请求由客户端按原request_id重发。 - 返回
last_schedule_updated_at之后发生变化的日程。 - 服务端根据现有
status、updated_at、time_triggered_at和geo_triggered_at恢复仍有效的提醒; 重发可以生成新的message_id。 - 客户端按现有字段复合键识别已经执行的提醒,不因新的
message_id重复展示。 - 音频 Binary Frame 不做断点续传;断线中的语音流标记失败,客户端重新录音。
规则:
- 只有
start_time存在的日程才参与时间监听。 - 时间到达前进入监测窗口。
- 默认提前
15min。 - 进入窗口后优先判断 WS 在线状态和前后台状态。
- 如果 WS 在线,服务端校验事项和云端音频后下发
reminder.control。 - 如果 WS 不在线,由客户端本地触发条件执行离线提醒。
规则:
- 只有存在经纬度的日程才参与空间监听。
- 默认围栏半径
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,不重复展示。 - 提醒展示失败时,尚未展示成功的另一条件仍可触发补偿提醒。
满足以下任一条件时取消监听:
- 用户通过确认接口标记已完成。
- 有时间的日程时间已过。
- 日程被删除。
为同时满足“优先触达”和“避免重复”,执行顺序固定为:
- 服务端判定提醒条件满足。
- 服务端下发
reminder.control;message_id只记录本次发送。 - 服务端随
reminder.control下发智能提醒内容、唯一rendered_text和运行时计算的audio_file_name。 - 客户端主提醒实际展示成功后返回包含同一
rendered_text的reminder.control.ack(ok=true)。 - 在线时客户端执行震动、自定义弹窗和云端 TTS;离线时执行震动、固定铃声和自定义弹窗。
- 在线 TTS 播放终态单独发送
reminder.tts.result,但不阻塞主提醒 ACK。 - 主提醒失败或 ACK 超时时按同一事项状态复合键重试;重发可使用新的
message_id,但不得 重复播放声音。
禁止等待 TTS 播报完成后才确认主提醒;TTS 失败不能阻止已经展示成功的主提醒返回 ACK。
预生成:
- 事项事务成功后,智能提醒模块提供
reminder_body并发布异步 TTS 任务。 - 服务端根据现有事项字段校验归属、版本、提醒内容和触发条件。
- 服务端计算
content_hash和确定性文件名;文件存在则复用,否则通过 TTS 生成网关创建音频。 - 预生成失败不影响事项保存,后续任务可以按同一文件名幂等重试。
在线触发:
- 服务端确认网络、WebSocket 和会话均有效,重新校验事项并计算
audio_file_name。 - 客户端立即执行震动和自定义弹窗,并请求播放关联云端 TTS。
- 服务端从对象存储读取音频,通过现有 TTS Binary Frame 流下发。
- 客户端播放器真实完成后上报
completed;TTS 失败不改变震动、弹窗和主提醒 ACK。
离线触发:
- 客户端本地提醒执行器根据预先登记的触发条件运行。
- 客户端执行震动、预置固定铃声和自定义弹窗,不访问云端音频或外部 TTS 服务。
- 平台后台能力不可用时记录为当前版本的触达限制。
- 恢复联网后只同步提醒终态,不补播已经执行的离线提醒。
防重复:
- 同一事项和
content_hash只生成一个云端文件。 - 同一
schedule_id + updated_at + trigger_kind + triggered_at只执行一次可见提醒和一次声音提醒。 - 在线与离线路径以触发时的连接状态选择一次,不同时播放 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 恢复 |
仅凭网络连接状态声明在线 |
| 软件提醒展示失败 | 按现有事项状态复合键幂等重试 | 把新的 message_id 当作新提醒并重复播放 |
| TTS 模板未知或参数非法 | 降级为 GENERIC,继续展示主提醒 |
阻塞通知或临时调用 LLM 生成文案 |
| TTS 预生成失败 | 保留事项和其他提醒通道,按确定性文件名幂等重试 | 回滚日程或待办创建 |
| 云端文件不存在或对象存储不可用 | 在线 TTS 失败,但继续震动和自定义弹窗 | 触发时临时信任客户端文件名 |
| 在线 TTS 音频流或播放失败 | 上报失败并保留震动、自定义弹窗和主提醒 | 把文件读取完成当作用户已听到 |
| 断网 | 执行震动、固定铃声和自定义弹窗,不访问云端 | 同时尝试云端 TTS |
| 平台后台能力不可用 | 记录当前版本触达限制 | 绕过平台限制或宣称提醒已触达 |
| WebSocket 重复下发相同 TTS | 按现有事项状态复合键返回已保存终态 | 因 message_id 变化再次朗读 |
| 位置权限不可用 | 暂停地点监听并提示权限状态;有时间条件时继续时间提醒 | 把权限失败当作未进入围栏 |
| 服务端重启 | 从 schedules 恢复时间扫描;位置监听等待新位置 |
依赖仅存在于内存的触发状态宣称完整恢复 |
一致性边界:
- 日程写入和
request_id幂等结果必须在同一事务边界内提交,或使用能保证原子可见性的等价实现。 - 服务端事务提交成功后才能下发
schedule.upsert.result。 - WebSocket 推送采用至少一次投递;
message_id只用于链路追踪,客户端依靠schedule_id + updated_at + trigger_kind + triggered_at去重。 -
time_triggered_at、geo_triggered_at只表示条件已命中,不等于客户端已经展示成功。 - “已展示”和 TTS 播放终态保存在客户端最小基础设施账本,不修改业务数据库,也不得复用
status表达投递过程。 - TTS 生成按事项和
content_hash去重,播放按现有事项状态复合键去重。 - 对象文件存在只表示音频已准备;必须等待客户端播放器完成回调后才能记录 TTS
completed。 - 在线和离线通道只选择一次;相同提醒不得同时播放云端 TTS 和固定铃声。
- WebSocket 生产环境只允许
wss://。 - 用户身份必须来自鉴权会话;
device_id只标识设备,不能代替用户认证。 - 服务端对每次日程读写和状态变更校验资源归属。
- 音频只为本次解析处理;是否持久化、保留时长和删除策略必须显式配置,默认不长期保存原始音频。
- 日志不得记录原始音频、完整转写文本、精确经纬度或令牌;排障使用
request_id、message_id、job_id和脱敏错误信息。 - 位置上报只在存在有效地点日程且用户授权时启用;无有效监听对象时停止高频上报。
- ASR、LLM 和地图依赖必须配置超时、有限重试和熔断;重试不得绕过用户确认。
- TTS 只使用智能提醒模块允许的
reminder_body,不得播报备注、详细地址、经纬度或令牌。 - 在线云端 TTS、离线固定铃声和震动不得绕过系统静音或勿扰策略。
- 客户端自定义弹窗只展示经过校验的当前事项内容。
- 外部 TTS 凭证只存在于服务端密钥管理系统,不进入客户端、WebSocket 业务响应、 日志或崩溃报告。
- 合成文本必须来自智能提醒模块的当前内容并通过现有事项字段校验,不接受客户端自由文本。
- 对象文件名不得包含用户明文;对象存储使用私有访问控制,客户端不直接获得供应商凭证。
- 音频文件、文件名、URL、哈希和状态不得写入业务数据库;对象生命周期由文件前缀和存储策略管理。
- 所有命令/查询均能按同一
request_id重放并得到一致结果。 - 相同
request_id、不同载荷返回IDEMPOTENCY_CONFLICT。 - 服务端推送重复到达时,即使
message_id不同,客户端也按现有事项状态复合键只执行一次。 - 未知消息、缺失字段、非法状态和版本冲突都有稳定错误码。
- ASR/LLM 成功只生成草稿,用户确认前
schedules无新增记录。 - 缺少时间或地点时表单明确补全,不能静默创建无触发条件的日程。
- 音频流中断不会创建正式日程,重连后可重新录音。
- 前台在线、后台在线和离线/服务不可达三类场景均有可验证触达路径。
- 软件提醒失败后按现有事项状态复合键重试,不重复展示或重复播放。
- 数据库和 WebSocket 契约均不包含系统日历 ID、系统闹钟 ID 或系统引用流程。
- 服务端重启后,未完成日程能恢复必要的服务端监听。
- TTS 完成、跳过或失败均不改变主提醒 ACK。
- 在线状态只执行“震动 + 云端 TTS + 自定义弹窗”,离线状态只执行 “震动 + 固定铃声 + 自定义弹窗”。
- 平台后台能力不可用时明确标记为当前版本的触达限制。
- 时间-only、地点-only、时间+地点三类日程分别覆盖。
- 组合日程任一条件命中可提醒,但一次有效周期只展示一次。
- 围栏内创建不会立即触发;离开再进入后可以触发。
- 时区转换、夏令时边界、结束时间为空和区间边界相接均有测试。
- 文档和 JSON 示例通过静态格式检查。
- 接口模型可生成 Schema,并用成功、失败、重复和重连样例做契约测试。
- Android 自定义弹窗、震动、固定铃声、云端 TTS 流式播放、后台限制和位置权限必须在真机或 目标模拟器验证。
- 只有静态文档检查时,结论必须写为“设计已细化/静态检查通过”,不得写成“提醒能力已实现”。
- 固定头、智能提醒
reminder_body和固定尾组成的最终文本与定义完全一致。 - 日程和待办保存先成功,TTS 在事务提交后异步生成;第三方失败不回滚事项。
- 对象文件名严格为
tts/{user_id}/{object_type}/{object_id}/{content_hash}.wav,且不含用户明文。 - 数据库结构和记录中不存在新增 TTS 文件名、URL、哈希、状态、音频字段或资产表。
- 相同事项和内容重复处理时复用云端文件;内容变化时生成新哈希和新文件。
- 删除、完成、取消和失效事项可以按事项前缀清理关联文件。
- 服务端拒绝与当前事项归属、版本、提醒内容或哈希不一致的播放请求。
- 在线状态执行震动、云端 TTS 和客户端自定义弹窗。
- 离线状态执行震动、客户端固定铃声和客户端自定义弹窗,且不访问云端音频或外部 TTS 服务。
- 恢复联网后不补播已经完成的离线提醒。
- WebSocket 重发不会重复生成云端文件或重复播放声音。
- 云端文件读取完成不等于用户已听到,只有播放器完成回调可以上报
completed。 - TTS 失败不改变智能提醒状态、主提醒 ACK 和其他提醒通道。