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 是客户端提醒增强通道:文案使用固定模板,不调用 LLM 生成;TTS 失败不影响主提醒。

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

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 播报。

3.2 服务端

服务端负责:

  1. 音频文件接收。
  2. ASR 调用。
  3. LLM 结构化提取。
  4. WebSocket 消息分发。
  5. 日程冲突检测。
  6. 日程状态监控。
  7. 地点与时间窗口判断。
  8. 提醒控制消息下发。
  9. 提醒通道选择由客户端自行完成。
  10. 选择固定提醒模板和参数;服务端不调用 LLM 生成提醒播报文案。

3.3 外部依赖

  1. 第三方 ASR 服务。
  2. 大模型服务。
  3. 第三方地图 SDK。
  4. Android 系统提醒能力。

3.4 模块协作边界

  1. 界面模块只能通过 WebSocket 模块提交业务命令,不直接调用 ASR、LLM 或数据库。
  2. 语音解析模块只产出草稿,不创建正式日程;只有用户确认后的 schedule.upsert.command 才能写入 schedules
  3. 监控与调度模块只读取有效日程并生成提醒控制决策,不直接选择 Android 展示通道。
  4. 提醒执行模块只执行客户端能力并回传结果,不自行修改服务端日程状态。
  5. 地点判定模块只输出“围栏外/围栏内/位置不可用”的判定,不直接发送提醒。
  6. 系统日程和系统闹钟属于客户端平台资源;服务端只保存引用 ID 和协调检查、清理流程。
  7. 服务端只选择 TTS 模板和参数;模板渲染、播放策略、引擎生命周期和结果上报均由客户端负责。
  8. TTS 不拥有独立提醒状态,不得因播报成功或失败修改 schedules.status 或主提醒 ACK。

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 根据模板编号、版本和参数生成通知与 TTS 共用文案。
  6. TtsPlaybackPolicy 根据全局开关、前后台、锁屏、音频焦点和平台能力决定是否播报。
  7. TtsReminderAdapter 管理 TTS 初始化、语言检查、播报、停止、回调和资源释放。

固定约束:

  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 进入终态后不得重复朗读。

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. 根据触发原因选择固定模板编号、模板版本和允许的参数,不生成自然语言文案。

4.2.6 地点判定模块

职责:

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

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 以及如何渲染模板,均由客户端决定。

固定模板

template_id 适用场景 版本 固定文案
TIME_ADVANCE 提前提醒 1 提醒:{title}将在{minutes_before}分钟后开始。
TIME_DUE 到点提醒 1 提醒:{title}现在开始。
LOCATION_ENTER 进入地点 1 提醒:你已到达{location_name},请处理{title}。
GENERIC 参数缺失或未知场景 1 提醒:请查看日程“{title}”。

模板选择规则:

  1. 时间提醒窗口到达且 minutes_before > 0 时使用 TIME_ADVANCE
  2. 日程开始时间到达时使用 TIME_DUE
  3. 用户进入有效地理围栏时使用 LOCATION_ENTER
  4. 无法确定场景时使用 GENERIC
  5. 服务端不得调用 LLM 生成提醒文案,只能选择模板和填充允许的参数。

客户端渲染规则:

  1. title 去除首尾空白后为空时替换为“一项日程”。
  2. minutes_before 不是非负整数时,TIME_ADVANCE 降级为 GENERIC
  3. location_name 去除首尾空白后为空时,LOCATION_ENTER 降级为 GENERIC
  4. 未识别的 template_idtemplate_version 降级为 GENERIC
  5. 模板参数只允许 titleminutes_beforelocation_name;备注、详细地址、经纬度不得下发或播报。
  6. 模板当前版本为 1;修改既有模板含义必须递增 template_version
  7. 通知、应用内弹窗和 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",
    "params": {
      "title": "项目会议",
      "minutes_before": 15,
      "location_name": "一号会议室"
    }
  }
}

主提醒执行成功时回传:

{
  "type": "reminder.control.ack",
  "message_id": "msg_reminder_001",
  "schedule_id": "schedule_001",
  "ok": true,
  "rendered_text": "提醒:项目会议将在15分钟后开始。"
}

主提醒执行失败时回传:

{
  "type": "reminder.control.ack",
  "message_id": "msg_reminder_001",
  "schedule_id": "schedule_001",
  "ok": false,
  "error": {
    "code": "REMINDER_DISPLAY_FAILED",
    "message": "提醒展示失败"
  }
}

reminder.control.ack 只表达应用内弹窗或系统通知等主提醒是否已经展示。TTS 是异步附加通道, 其结果不得改变主提醒 ACK,也不得阻塞系统兜底清理。

TTS 播报结果

播报完成:

{
  "type": "reminder.tts.result",
  "message_id": "msg_reminder_001",
  "schedule_id": "schedule_001",
  "utterance_id": "tts_msg_reminder_001",
  "status": "completed",
  "reason": null
}

按策略跳过:

{
  "type": "reminder.tts.result",
  "message_id": "msg_reminder_001",
  "schedule_id": "schedule_001",
  "utterance_id": "tts_msg_reminder_001",
  "status": "skipped",
  "reason": "device_locked"
}

播报失败:

{
  "type": "reminder.tts.result",
  "message_id": "msg_reminder_001",
  "schedule_id": "schedule_001",
  "utterance_id": "tts_msg_reminder_001",
  "status": "failed",
  "reason": "synthesis_failed"
}

字段和状态约束:

  1. utterance_id 固定为 tts_{message_id};相同 message_id 不得生成不同值。
  2. status 只能是 completedskippedfailed
  3. reason 只能是 nulldisabledbackground_disableddevice_lockedengine_unavailablelanguage_unsupportedaudio_focus_deniedplatform_restrictedsynthesis_failed
  4. completedreason 必须为 nullskippedfailedreason 必填。
  5. 客户端必须根据 UtteranceProgressListener.onDone/onError 上报真实结果; TextToSpeech.speak() 返回成功只表示请求进入队列,不表示已经完成播报。
  6. 同一 utterance_id 一旦进入 completedskippedfailed,WebSocket 重试不得再次朗读。

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. 客户端渲染固定模板,由应用内弹窗和系统通知复用同一份文案。
  6. 客户端主提醒实际展示成功后返回 reminder.control.ack(ok=true)
  7. 客户端按 TTS 策略异步决定播报、跳过或降级,并单独发送 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 校验模板、版本和参数,并生成唯一 rendered_text
  3. 应用内弹窗或系统通知使用 rendered_text 展示主提醒。
  4. 主提醒展示成功后立即发送 reminder.control.ack,不等待 TTS。
  5. TtsPlaybackPolicy 依次检查 tts_enabled、前后台、tts_background_enabled、锁屏、 音频焦点和平台限制。
  6. 策略不允许时不初始化 TTS,发送 status=skipped 和对应 reason
  7. 策略允许时,TtsReminderAdapter 初始化引擎、校验 zh-CN、设置稳定 utterance_id 并播报同一 rendered_text
  8. onDone 上报 completedonError 上报 failed;完成后释放音频焦点和 TTS 资源。
  9. 任一 TTS 终态都写入客户端最小投递状态;相同 message_id 重连或重试时只重发结果,不重复朗读。

TTS 降级边界:

  1. 全局开关关闭:skipped/disabled
  2. 后台开关关闭:skipped/background_disabled
  3. 设备锁屏:skipped/device_locked
  4. 平台禁止后台音频:skipped/platform_restricted
  5. 引擎或语言不可用、音频焦点失败、合成失败:上报对应结果,继续保留已经展示的主提醒。
  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 引擎、语言或音频焦点不可用 保留主提醒并上报失败原因 把 TTS 失败当作主提醒失败
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. TextToSpeech.speak() 仅表示进入合成队列,必须等待回调后才能记录 TTS 终态。

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

  1. WebSocket 生产环境只允许 wss://
  2. 用户身份必须来自鉴权会话;device_id 只标识设备,不能代替用户认证。
  3. 服务端对每次日程读写、引用登记和状态变更校验资源归属。
  4. 音频只为本次解析处理;是否持久化、保留时长和删除策略必须显式配置,默认不长期保存原始音频。
  5. 日志不得记录原始音频、完整转写文本、精确经纬度或令牌;排障使用 request_idmessage_idjob_id 和脱敏错误信息。
  6. 位置上报只在存在有效地点日程且用户授权时启用;无有效监听对象时停止高频上报。
  7. ASR、LLM 和地图依赖必须配置超时、有限重试和熔断;重试不得绕过用户确认。
  8. TTS 固定模板只允许标题、提前分钟数和地点名称,不得播报备注、详细地址、经纬度或令牌。
  9. 锁屏状态不播报 TTS;后台播报必须由用户显式开启,关闭开关后立即停止待播报内容。
  10. TTS 不得绕过系统静音或勿扰策略,不得为了播报强制修改媒体或闹钟音量。

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 系统日程、闹钟、通知、TTS、后台限制和位置权限必须在真机或目标模拟器验证。
  4. 只有静态文档检查时,结论必须写为“设计已细化/静态检查通过”,不得写成“提醒能力已实现”。

12.6 固定模板 TTS

  1. TIME_ADVANCETIME_DUELOCATION_ENTERGENERIC 分别渲染为文档定义的固定文案。
  2. 通知、应用内弹窗和 TTS 使用同一次 rendered_text,不存在通道间文案差异。
  3. 标题为空时使用“一项日程”;提前分钟数非法、地点为空、模板未知或版本未知时降级为 GENERIC
  4. tts_enabled=false 时不初始化 TTS,主提醒正常展示。
  5. 后台未授权、锁屏或平台受限时返回对应 skipped,不得自动播报。
  6. 引擎、语言、音频焦点或合成失败时返回对应结果,主提醒状态不受影响。
  7. 相同 message_id 重复到达时不重复朗读,只重发已保存的 TTS 结果。
  8. speak() 入队后发生 onError 时不得上报 completed;只有 onDone 可以上报完成。
  9. App 前台、后台、锁屏、静音、勿扰、引擎缺失和进程死亡场景均有目标设备验证记录。

Clone this wiki locally