-
Notifications
You must be signed in to change notification settings - Fork 6
Architecture interface design
- 客户端不再独立承担全部语音理解和提醒编排。
- 客户端进入界面后建立 WebSocket 连接,作为后续实时通信主通道。
- 音频通过 WebSocket Binary Frame 流式传输到服务端。
- 服务端完成 ASR + LLM 结构化提取后,通过 WebSocket 回传结果。
- 客户端用统一表单完成二次确认、地理位置补全、时间补全和提醒参数补全。
- 日程最终落到单一
schedules表中,状态和关联信息一并保存。 - 提醒采用三段式策略,优先保证触达,再处理重复提醒控制。
- TTS 是客户端提醒增强通道:文案使用固定模板,不调用 LLM 生成;TTS 失败不影响主提醒。
这一版的重点不是“完整日历”,而是“日程创建 + 状态驱动提醒 + 语音结构化”闭环。
本版明确包含:
- 单用户、单主要设备上的日程创建、编辑、查询、完成和删除。
- 一条活动 WebSocket 连接上的语音流、业务命令、查询和服务端推送。
- 时间提醒、地点提醒,以及同时包含时间和地点的日程。
- 系统日程和系统闹钟作为客户端兜底,并将系统引用回写服务端。
- 断线重连、重复消息和进程重启下的最小恢复能力。
- 前台 TTS 播报,以及用户显式开启后的后台 TTS 尝试。
本版暂不包含:
- 多用户共享日历。
- 多设备之间的系统日历/闹钟引用同步。
- 周期性日程和复杂重复规则。
- 服务端直接操作 Android 系统日历、系统闹钟或系统通知。
- 用 LLM 自主决定最终日程或提醒时间;LLM 只生成待用户确认的草稿。
- 锁屏或 App 进程死亡后的 TTS 播报;这些状态继续使用系统通知、提示音或震动。
MVP 要验证三件事:
- 用户能不能快速创建日程。
- 系统能不能在不同场景下使用不同提醒方式。
- 语音创建能不能稳定转成可确认、可执行的日程。
触发方式:
- 应用内弹窗。
- 页面内高亮提示。
- 伴随声音或震动。
- 用户开启 TTS 后,按固定模板播报提醒内容。
适用场景:
- 用户正在使用 App。
- 可以给出最完整的上下文。
- 不需要系统级强打断。
触发方式:
- 系统级全局弹窗。
- 系统通知栏提醒。
- 只有用户显式开启后台 TTS、设备未锁屏且平台允许时,才尝试固定模板语音播报。
适用场景:
- App 仍保持在线。
- 需要由系统层接管提醒触达。
- 可以通过 WS 收到实时控制消息。
这里的“网络通畅”必须同时满足:客户端网络可用、WebSocket 心跳正常、服务端会话有效。 仅检测到 Wi-Fi/蜂窝网络不能判定为此情况;后台进程或 WS 不可用时立即按情况三降级。
客户端根据自身能力自行决定是否降级为普通通知、提示音或震动,不执行 TTS 播报。
日程支持两种入口:
- 手动创建。
- 语音创建。
二者共用一个表单。
语音创建流程:
- 客户端录音。
- 客户端通过 WebSocket Binary Frame 流式发送音频。
- 服务端返回上传受理结果。
- 服务端完成 ASR。
- 服务端完成 LLM 结构化提取。
- 服务端通过 WS 推送结构化日程草稿。
- 客户端弹出统一表单。
- 用户补全信息并确认。
- 客户端提交日程最终结果。
- 如果日程包含时间,服务端做时间冲突检测并返回提示。
- 创建成功后,服务端和客户端分别进入自己的提醒准备状态。
日程允许两种主类型:
| 形态 | schedule_type |
必要信息 | 触发条件 |
|---|---|---|---|
| 时间类 | time |
start_time |
进入时间提醒窗口 |
| 地点类 | location |
latitude + longitude
|
用户进入地理围栏 |
语音创建时,schedule_type 由 LLM 根据用户语义做意图识别后输出。手动创建时,由前端根据用户选择或填写内容确定。
schedule_type 只表示主意图类型,时间和地点同时存在时仍按 time 落库,是否触发地点提醒由字段本身判断。
表单内部支持:
- 地理位置选择。
- 地理围栏设置。
- 默认提醒参数补全。
默认值建议:
- 时间提前提醒:
15min - 地理围栏半径:
100m
创建日程时如果填写了时间,必须做时间冲突检测。
如果目标时间段已有日程,服务端返回:
- 冲突提示。
- 冲突项列表。
- 是否允许继续创建的建议。
冲突检测是提示,不一定是硬拦截。
客户端负责:
- 界面展示。
- WebSocket 长连接。
- 音频录制。
- 音频上传。
- 表单编辑和二次确认。
- 本地日程缓存。
- 系统日程和系统闹钟创建、查询与删除。
- 前台弹窗提醒。
- 后台系统弹窗/通知提醒。
- 位置信息上报。
- 根据前后台状态和系统可用性选择具体提醒通道。
- 使用固定模板渲染通知与 TTS 共用文案,并按客户端设置执行可选 TTS 播报。
服务端负责:
- 音频文件接收。
- ASR 调用。
- LLM 结构化提取。
- WebSocket 消息分发。
- 日程冲突检测。
- 日程状态监控。
- 地点与时间窗口判断。
- 提醒控制消息下发。
- 提醒通道选择由客户端自行完成。
- 选择固定提醒模板和参数;服务端不调用 LLM 生成提醒播报文案。
- 第三方 ASR 服务。
- 大模型服务。
- 第三方地图 SDK。
- Android 系统提醒能力。
- 界面模块只能通过 WebSocket 模块提交业务命令,不直接调用 ASR、LLM 或数据库。
- 语音解析模块只产出草稿,不创建正式日程;只有用户确认后的
schedule.upsert.command才能写入schedules。 - 监控与调度模块只读取有效日程并生成提醒控制决策,不直接选择 Android 展示通道。
- 提醒执行模块只执行客户端能力并回传结果,不自行修改服务端日程状态。
- 地点判定模块只输出“围栏外/围栏内/位置不可用”的判定,不直接发送提醒。
- 系统日程和系统闹钟属于客户端平台资源;服务端只保存引用 ID 和协调检查、清理流程。
- 服务端只选择 TTS 模板和参数;模板渲染、播放策略、引擎生命周期和结果上报均由客户端负责。
- TTS 不拥有独立提醒状态,不得因播报成功或失败修改
schedules.status或主提醒 ACK。
职责:
- 显示日程列表。
- 显示创建/编辑表单。
- 显示语音解析结果。
- 显示冲突提示。
职责:
- 录音。
- 音频格式转换。
- 发起上传。
职责:
- 建立和维护连接。
- 接收结构化草稿。
- 接收提醒控制消息。
- 上传位置信息。
- 上传日程确认结果。
职责:
- 缓存日程。
- 缓存解析结果。
- 缓存提醒状态。
- 缓存提醒通道状态。
- 保存客户端设置
tts_enabled=false和tts_background_enabled=false。 - 按
message_id保存本次 TTS 终态,防止 WebSocket 重试造成重复播报。
职责:
- 前台弹窗。
- 后台浮窗。
- 系统通知。
- 根据当前应用状态决定提醒展示方式。
-
ReminderTemplateRenderer根据模板编号、版本和参数生成通知与 TTS 共用文案。 -
TtsPlaybackPolicy根据全局开关、前后台、锁屏、音频焦点和平台能力决定是否播报。 -
TtsReminderAdapter管理 TTS 初始化、语言检查、播报、停止、回调和资源释放。
固定约束:
-
tts_enabled默认false;关闭时不初始化或调用 TTS。 - 前台只有
tts_enabled=true时允许播报。 - 后台必须同时满足
tts_enabled=true、tts_background_enabled=true、设备未锁屏和平台允许。 - 锁屏、进程死亡或后台音频受限时跳过 TTS,主提醒继续使用系统通知、提示音或震动。
- 不强制修改系统音量,不绕过勿扰模式;无法取得音频焦点时跳过播报。
- TTS 使用稳定
utterance_id;同一message_id进入终态后不得重复朗读。
职责:
- 地点搜索。
- 地点确认。
- 地理围栏设置。
- 实时位置上报。
职责:
- 接收音频文件。
- 校验格式和大小。
- 生成处理任务 ID。
职责:
- 调用 ASR。
- 调用 LLM。
- 识别
schedule_type。 - 生成结构化草稿。
职责:
- 建立客户端会话。
- 维护设备在线状态。
- 分发解析结果和提醒控制消息。
- 接收位置上报。
职责:
- 创建日程。
- 编辑日程。
- 查询日程。
- 冲突检测。
职责:
- 监听日程时间。
- 监听地理位置。
- 判断时间、空间以及组合触发条件。
- 进入提醒窗口后先触发系统引用检查。
- 根据检查结果决定是否触发软件提醒。
- 触发系统日程和系统闹钟删除指令。
- 根据触发原因选择固定模板编号、模板版本和允许的参数,不生成自然语言文案。
职责:
- 根据上报位置和日程坐标计算距离。
- 判断是否进入地理围栏。
MVP 阶段只保留一张核心业务表。
用途:
- 存储日程本体。
- 存储日程状态。
- 存储地点与提醒关联信息。
- 存储地理围栏布防状态。
| 字段 | 类型 | 说明 |
|---|---|---|
id |
text | 主键,日程 ID |
user_id |
text | 默认用户 ID |
source_mode |
text |
manual / voice
|
schedule_type |
text |
time / location
|
status |
text |
scheduled / done / deleted
|
title |
text | 标题 |
notes |
text | 备注 |
start_time |
text | 开始时间,ISO-8601,可以为空 |
end_time |
text | 结束时间,可为空 |
timezone |
text | 时区,可为空 |
location_name |
text | 地点名称 |
location_address |
text | 地点地址 |
latitude |
real | 纬度 |
longitude |
real | 经度 |
geofence_radius_meters |
integer | 地理围栏半径,默认 100m |
geofence_armed |
integer | 地理围栏是否已布防,默认由服务端根据创建时用户位置计算 |
time_remind_offset_minutes |
integer | 时间提前提醒,默认 15min |
time_triggered_at |
text | 时间提醒触发时间,可为空 |
geo_triggered_at |
text | 地点提醒触发时间,可为空 |
system_schedule_ref_id |
text | 系统日历日程 ID,可为空 |
system_alarm_ref_id |
text | 系统闹钟 ID,可为空 |
created_at |
text | 创建时间 |
updated_at |
text | 更新时间 |
- 核心字段尽量扁平化。
- 语音解析草稿只通过 WS 传递,不落库。
- 仅保存系统日历日程 ID 和系统闹钟 ID,不保存其他系统侧兜底对象。
- 时间提醒和地理提醒可以同时存在。
- 只保留一张主表,MVP 不拆分额外业务表。
-
status只表达日程本体是否还有效,不表达监听中、已触发、已过期等过程状态。 - MVP 不保存用户自定义的重要程度,提醒方式由应用前后台状态、网络状态和时间/空间窗口决定。
- 冲突检测结果只在接口响应中返回,不写入
schedules表。 -
start_time和地点信息都允许为空,但二者不能同时为空。 -
geofence_armed用于避免“用户在目标地点创建日程后立刻触发位置提醒”。 - 最近一次位置只在会话内或内存中计算,不落库。
-
schedule_type是前端表单必填项控制和后端校验的依据,不再使用全天字段区分业务类型。 - TTS 开关和播报终态属于客户端基础设施状态,不写入
schedules。 - 固定模板不是业务事实,不增加
tts_text、tts_template等数据库字段。
数据库迁移和服务端模型必须同时实现以下约束:
-
id、user_id、source_mode、schedule_type、status、title、created_at、updated_at不为空。 -
source_mode只能是manual或voice。 -
schedule_type只能是time或location。 -
status只能是scheduled、done或deleted。 -
title去除首尾空白后不能为空。 -
start_time和end_time同时存在时,end_time >= start_time。 -
latitude和longitude必须同时为空或同时有值;纬度范围为[-90, 90],经度范围为[-180, 180]。 -
start_time与经纬度不能同时为空。 -
schedule_type=time时start_time必填;schedule_type=location时经纬度必填。 -
geofence_radius_meters > 0,time_remind_offset_minutes >= 0。 - 所有时间统一按带时区 ISO-8601 接口值解析,数据库内部按 UTC 保存;返回客户端时保留
timezone用于展示。 - 更新时由服务端生成新的
updated_at,客户端传入值不得覆盖。
MVP 至少建立:
-
(user_id, status, start_time):日程列表、时间窗口扫描和冲突检测。 -
(user_id, updated_at):重连后的增量对齐。 -
system_schedule_ref_id非空值索引:系统日程引用定位。 -
system_alarm_ref_id非空值索引:系统闹钟引用定位。
索引是查询和调度约束,不改变“一张核心业务表”的方案。
建议状态流转:
scheduled -> done
scheduled -> deleted
状态说明:
| 状态 | 含义 |
|---|---|
scheduled |
已创建,等待提醒或正在监听 |
done |
用户已确认完成 |
deleted |
用户删除 |
不进入 status 的过程信息:
| 信息 | 处理方式 |
|---|---|
| 草稿待确认 | 语音解析结果通过 WS 推给前端,用户确认前不创建正式日程 |
| 时间监听中 | 根据 start_time、time_remind_offset_minutes 和当前时间动态判断 |
| 地理监听中 | 根据经纬度、围栏半径、最近位置和 geofence_armed 动态判断 |
| 已触发提醒 | 写入 time_triggered_at 或 geo_triggered_at
|
| 已过期 | 根据 start_time 或 end_time 动态判断 |
| 创建冲突 | 只在接口响应中返回 conflicts,不持久化 |
业务接口统一使用 WebSocket。JSON Text Frame 承载控制消息、命令、查询、结果和错误;Binary Frame 承载音频数据。
客户端请求信封:
{
"type": "schedule.upsert.command",
"request_id": "req_schedule_001",
"payload": {}
}服务端成功响应:
{
"type": "schedule.upsert.result",
"request_id": "req_schedule_001",
"ok": true,
"payload": {}
}服务端失败响应:
{
"type": "schedule.upsert.error",
"request_id": "req_schedule_001",
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数不合法",
"details": {
"field": "start_time"
}
}
}字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
type |
string | 消息类型 |
request_id |
string | 请求关联和幂等 ID |
ok |
boolean | 服务端是否成功处理 |
payload |
object/null | 业务数据 |
error.code |
string | 程序可识别的错误码 |
error.message |
string | 面向前端展示或日志记录的错误说明 |
error.details |
object/null | 具体错误上下文,可为空 |
WebSocket 业务失败不依赖 HTTP 状态码,通过 *.error、ok=false 和 error 对象表达。
补充约定:
- 客户端发起的命令、查询和 ACK 都必须携带
request_id;服务端使用同一个request_id返回结果,客户端重试时不得生成新值。 - 服务端主动推送必须携带全局唯一
message_id;客户端对同一message_id只执行一次,并可重复回传同一 ACK。 -
request_id的幂等范围是“当前用户 + 消息类型”;服务端至少保存到该操作进入终态。 - 同一
request_id若收到不同payload,返回IDEMPOTENCY_CONFLICT,不得覆盖第一次结果。 -
*.command、*.query、*.result和*.error的业务字段统一放入payload; 7.4—7.11 已有事件型消息保留根部业务字段以避免大改,后续不得在同一消息类型中混用两种结构。 - 未知
type返回UNSUPPORTED_MESSAGE_TYPE;多余字段按协议版本策略处理, 缺少必填字段返回VALIDATION_ERROR。 - 服务端推送只有在收到业务 ACK 后才视为客户端已执行;“WebSocket 已发送”不等于“提醒已展示”。
- 建立单次语音流。
- 使用 Binary Frame 持续发送音频分片。
- 触发后续 ASR + LLM 流程。
客户端发送 JSON Text Frame:
{
"type": "voice.stream.start",
"request_id": "req_audio_001",
"payload": {
"audio_format": "pcm_s16le",
"sample_rate_hz": 16000,
"channels": 1
}
}服务端响应:
{
"type": "voice.stream.started",
"request_id": "req_audio_001",
"ok": true,
"payload": {
"stream_id": "stream_audio_001",
"job_id": "job_audio_001"
}
}voice.stream.started 成功后,客户端通过同一条 WebSocket 连接发送 Binary Frame。
每个 Binary Frame 必须归属于当前活动的 stream_id。同一设备连接同一时刻只允许一个活动音频流;客户端应按服务端 ACK 和背压信号控制发送速度。
{
"type": "voice.stream.end",
"request_id": "req_audio_001",
"payload": {
"stream_id": "stream_audio_001"
}
}服务端受理:
{
"type": "voice.stream.ended",
"request_id": "req_audio_001",
"ok": true,
"payload": {
"stream_id": "stream_audio_001",
"job_id": "job_audio_001",
"status": "processing"
}
}音频格式不支持:
{
"type": "voice.stream.error",
"request_id": "req_audio_001",
"ok": false,
"error": {
"code": "UNSUPPORTED_AUDIO_FORMAT",
"message": "音频格式不支持",
"details": {
"audio_format": "aac",
"supported_formats": ["pcm_s16le"]
}
}
}音频流过大或超过时长限制:
{
"type": "voice.stream.error",
"request_id": "req_audio_001",
"ok": false,
"error": {
"code": "AUDIO_STREAM_LIMIT_EXCEEDED",
"message": "音频流超过限制",
"details": {
"max_duration_ms": 120000
}
}
}说明:
- 不再提供 HTTP 音频上传接口。
- JSON Text Frame 只传控制信息,音频内容只通过 Binary Frame 发送。
- 真正的结构化结果仍通过
voice.parse.result返回。
schedule.upsert.command
- 手动创建日程。
- 语音表单确认后提交日程。
- 写入最终状态。
- 执行时间冲突检测。
{
"type": "schedule.upsert.command",
"request_id": "req_schedule_001",
"payload": {
"schedule_id": null,
"source_mode": "voice",
"schedule_type": "time",
"title": "开会",
"notes": null,
"start_time": "2026-07-29T15:00:00+08:00",
"end_time": null,
"timezone": "Asia/Shanghai",
"location_name": "陆家嘴",
"location_address": null,
"latitude": 31.2451,
"longitude": 121.5067,
"geofence_radius_meters": 100,
"geofence_armed": true,
"time_remind_offset_minutes": 15
}
}请求字段保持原日程接口定义:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
request_id |
string | 是 | 幂等 ID,位于消息信封 |
schedule_id |
string/null | 否 | 编辑时传 |
source_mode |
string | 是 |
manual / voice
|
schedule_type |
string | 是 |
time / location
|
title |
string | 是 | 标题 |
notes |
string/null | 否 | 备注 |
start_time |
string/null | 否 | 开始时间;只有地点的日程可为空 |
end_time |
string/null | 否 | 结束时间 |
timezone |
string/null | 否 | 时区 |
location_name |
string/null | 否 | 地点名称 |
location_address |
string/null | 否 | 地点地址 |
latitude |
number/null | 否 | 纬度 |
longitude |
number/null | 否 | 经度 |
geofence_radius_meters |
integer/null | 否 | 地理围栏半径,默认 100
|
geofence_armed |
boolean/null | 否 | 地理围栏是否已布防,不传则服务端计算 |
time_remind_offset_minutes |
integer/null | 否 | 时间提醒提前量,默认 15
|
{
"type": "schedule.upsert.result",
"request_id": "req_schedule_001",
"ok": true,
"payload": {
"schedule_id": "schedule_001",
"schedule_type": "time",
"status": "scheduled",
"conflicts": [],
"geofence_armed": true
}
}{
"type": "schedule.upsert.result",
"request_id": "req_schedule_001",
"ok": true,
"payload": {
"schedule_id": "schedule_001",
"schedule_type": "time",
"status": "scheduled",
"conflicts": [
{
"schedule_id": "schedule_older",
"title": "已有日程",
"start_time": "2026-07-28T15:00:00+08:00",
"end_time": "2026-07-28T16:00:00+08:00"
}
],
"geofence_armed": true
}
}{
"type": "schedule.upsert.error",
"request_id": "req_schedule_001",
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数不合法",
"details": {
"field": "schedule_type",
"reason": "schedule_type 为 time 时 start_time 必填;为 location 时 latitude 和 longitude 必填"
}
}
}规则:
- 手动创建和语音确认都使用
schedule.upsert.command。 - 时间冲突只给提示,不默认阻断。
-
schedule_type=time时,前端表单要求填写时间,地点可选。 -
schedule_type=location时,前端表单要求填写地点,时间可选。 - 用户同时填写时间和地点时,
schedule_type仍按time处理。 - 只有
start_time存在时才做时间冲突检测。 - 只有经纬度存在时才做地理围栏监听。
- 如果
geofence_armed不传,服务端根据最近一次位置上报与目标地点距离计算默认值。
schedule.list.query
- 获取当前用户的日程数据。
- 支持前端进入页面后初始化列表。
- 支持前端恢复本地状态和服务端状态对齐。
{
"type": "schedule.list.query",
"request_id": "req_schedule_list_001",
"payload": {
"status": null,
"include_deleted": false
}
}{
"type": "schedule.list.result",
"request_id": "req_schedule_list_001",
"ok": true,
"payload": {
"schedules": [
{
"id": "schedule_001",
"user_id": "default_user",
"source_mode": "voice",
"schedule_type": "time",
"status": "scheduled",
"title": "开会",
"notes": null,
"start_time": "2026-07-29T15:00:00+08:00",
"end_time": null,
"timezone": "Asia/Shanghai",
"location_name": "陆家嘴",
"location_address": null,
"latitude": 31.2451,
"longitude": 121.5067,
"geofence_radius_meters": 100,
"geofence_armed": true,
"time_remind_offset_minutes": 15,
"time_triggered_at": null,
"geo_triggered_at": null,
"system_schedule_ref_id": "system_schedule_001",
"system_alarm_ref_id": "system_alarm_001",
"created_at": "2026-07-28T12:00:00+08:00",
"updated_at": "2026-07-28T12:00:00+08:00"
}
]
}
}{
"type": "schedule.list.error",
"request_id": "req_schedule_list_001",
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数不合法",
"details": {
"field": "status"
}
}
}规则:
- 用户身份由 WebSocket 会话上下文确定,客户端不传
user_id。 - 默认只返回
scheduled和done。 -
include_deleted=true时才返回deleted数据。 - 返回结果按
start_time asc nulls last, created_at desc排序。 - 返回日程时包含
system_schedule_ref_id和system_alarm_ref_id。
生产环境:wss://<host>/ws?device_id=xxx
仅本地开发可使用:ws://<host>/ws?device_id=xxx
- 保持设备在线。
- 回传语音结构化结果。
- 下发提醒控制消息。
- 下发系统日程删除指令。
- 下发系统闹钟删除指令。
- 接收位置信息和确认事件。
客户端进入界面后先发:
{
"type": "session.hello",
"device_id": "android_abc123",
"app_version": "1.0.0"
}服务端返回:
{
"type": "session.ready",
"device_id": "android_abc123",
"server_time": "2026-07-28T12:00:00Z",
"heartbeat_interval_seconds": 30
}连接失败或鉴权失败时,服务端返回错误消息后关闭连接:
{
"type": "session.error",
"ok": false,
"error": {
"code": "INVALID_DEVICE_ID",
"message": "设备 ID 不合法",
"details": null
}
}{
"type": "voice.parse.result",
"request_id": "req_audio_001",
"job_id": "job_audio_001",
"status": "ready_for_confirmation",
"draft": {
"schedule_type": "time",
"title": "开会",
"start_time": "2026-07-29T15:00:00+08:00",
"end_time": null,
"timezone": "Asia/Shanghai",
"location_name": "陆家嘴",
"geofence_radius_meters": 100,
"time_remind_offset_minutes": 15
},
"missing_fields": ["location_address"],
"ambiguous_fields": [],
"needs_confirmation": true
}ASR 或 LLM 处理失败时,服务端通过 WS 推送失败结果:
{
"type": "voice.parse.result",
"request_id": "req_audio_001",
"job_id": "job_audio_001",
"status": "failed",
"error": {
"code": "VOICE_PARSE_FAILED",
"message": "语音解析失败,请重新录音或手动创建",
"details": {
"stage": "asr"
}
}
}- 弹出统一表单。
- 默认填充结构化字段。
- 允许用户补全地点、时间和提醒参数。
{
"type": "location.report",
"request_id": "req_location_001",
"schedule_scope": "current",
"latitude": 31.2451,
"longitude": 121.5067,
"accuracy": 18,
"timestamp": "2026-07-28T12:01:00+08:00"
}{
"type": "location.report.ack",
"request_id": "req_location_001",
"ok": true
}失败响应:
{
"type": "location.report.ack",
"request_id": "req_location_001",
"ok": false,
"error": {
"code": "INVALID_LOCATION",
"message": "位置信息不合法",
"details": {
"field": "latitude"
}
}
}- 服务端根据当前位置计算日程距离。
- 如果日程包含地点且命中围栏,先检查系统引用是否仍存在。
- 若日程还包含时间,再结合时间窗口决定是否下发提醒控制消息。
服务端只下发“该提醒了”、固定模板编号和模板参数。具体走哪种展示通道、是否执行 TTS 以及如何渲染模板,均由客户端决定。
template_id |
适用场景 | 版本 | 固定文案 |
|---|---|---|---|
TIME_ADVANCE |
提前提醒 | 1 |
提醒:{title}将在{minutes_before}分钟后开始。 |
TIME_DUE |
到点提醒 | 1 |
提醒:{title}现在开始。 |
LOCATION_ENTER |
进入地点 | 1 |
提醒:你已到达{location_name},请处理{title}。 |
GENERIC |
参数缺失或未知场景 | 1 |
提醒:请查看日程“{title}”。 |
模板选择规则:
- 时间提醒窗口到达且
minutes_before > 0时使用TIME_ADVANCE。 - 日程开始时间到达时使用
TIME_DUE。 - 用户进入有效地理围栏时使用
LOCATION_ENTER。 - 无法确定场景时使用
GENERIC。 - 服务端不得调用 LLM 生成提醒文案,只能选择模板和填充允许的参数。
客户端渲染规则:
-
title去除首尾空白后为空时替换为“一项日程”。 -
minutes_before不是非负整数时,TIME_ADVANCE降级为GENERIC。 -
location_name去除首尾空白后为空时,LOCATION_ENTER降级为GENERIC。 - 未识别的
template_id或template_version降级为GENERIC。 - 模板参数只允许
title、minutes_before和location_name;备注、详细地址、经纬度不得下发或播报。 - 模板当前版本为
1;修改既有模板含义必须递增template_version。 - 通知、应用内弹窗和 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,也不得阻塞系统兜底清理。
播报完成:
{
"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"
}字段和状态约束:
-
utterance_id固定为tts_{message_id};相同message_id不得生成不同值。 -
status只能是completed、skipped或failed。 -
reason只能是null、disabled、background_disabled、device_locked、engine_unavailable、language_unsupported、audio_focus_denied、platform_restricted或synthesis_failed。 -
completed时reason必须为null;skipped和failed时reason必填。 - 客户端必须根据
UtteranceProgressListener.onDone/onError上报真实结果;TextToSpeech.speak()返回成功只表示请求进入队列,不表示已经完成播报。 - 同一
utterance_id一旦进入completed、skipped或failed,WebSocket 重试不得再次朗读。
在服务端准备下发软件提醒或删除系统兜底提醒之前,先确认用户是否已经在系统日程或系统闹钟中手动删除了对应事项。
如果用户已经删除系统侧提醒,说明用户可能已经表达“不再需要提醒”的意图,服务端不应继续下发应用内弹窗、系统级全局弹窗或删除指令。
服务端不能直接查询 Android 系统日程或系统闹钟,这个检查必须由客户端完成,再通过 WS 回传结果。
该指令作为独立保留的系统侧存在性检查流程,不因提醒策略调整而删除。
{
"type": "system.refs.check",
"message_id": "msg_refs_check_001",
"schedule_id": "schedule_001",
"system_schedule_ref_id": "system_schedule_001",
"system_alarm_ref_id": "system_alarm_001",
"reason": "before_reminder_control"
}- 根据
system_schedule_ref_id查询系统日程是否仍存在。 - 根据
system_alarm_ref_id查询系统闹钟是否仍存在。 - 将查询结果通过 WS 回传服务端。
- 只做存在性检查,不重新创建系统日程或系统闹钟。
{
"type": "system.refs.check.result",
"message_id": "msg_refs_check_001",
"schedule_id": "schedule_001",
"ok": true,
"system_schedule_ref_id": "system_schedule_001",
"system_schedule_exists": false,
"system_alarm_ref_id": "system_alarm_001",
"system_alarm_exists": false,
"checked_at": "2026-07-28T14:45:00+08:00"
}查询失败时,客户端仍需回传失败原因:
{
"type": "system.refs.check.result",
"message_id": "msg_refs_check_001",
"schedule_id": "schedule_001",
"ok": false,
"error": {
"code": "SYSTEM_REF_CHECK_FAILED",
"message": "系统引用检查失败",
"details": {
"reason": "permission_denied"
}
}
}- 如果
system_schedule_exists=true或system_alarm_exists=true,说明兜底提醒仍存在,继续执行软件提醒和重复提醒删除流程。 - 如果此前成功登记的引用均返回
exists=false,且本次检查成功,才可解释为用户可能已手动删除系统侧提醒。 - 满足第 2 条时,服务端取消本次软件提醒;MVP 可将日程更新为
deleted并停止后续监听。 - 从未登记引用、引用为空、权限不足或检查失败时,不得据此删除业务日程。
- 如果客户端超时未响应,服务端按当前在线状态继续软件提醒,但保留系统兜底,避免漏提醒。
{
"type": "system.schedule.delete",
"message_id": "msg_schedule_delete_001",
"schedule_id": "schedule_001",
"system_schedule_ref_id": "system_schedule_001",
"reason": "ws_connected_and_time_near"
}- 删除系统日程。
- 取消重复通知。
- 继续保留业务日程本体。
{
"type": "system.schedule.delete.ack",
"message_id": "msg_schedule_delete_001",
"schedule_id": "schedule_001",
"ok": true,
"system_schedule_ref_id": "system_schedule_001"
}删除失败时:
{
"type": "system.schedule.delete.ack",
"message_id": "msg_schedule_delete_001",
"schedule_id": "schedule_001",
"ok": false,
"system_schedule_ref_id": "system_schedule_001",
"error": {
"code": "SYSTEM_SCHEDULE_DELETE_FAILED",
"message": "系统日程删除失败",
"details": {
"reason": "not_found"
}
}
}{
"type": "system.alarm.delete",
"message_id": "msg_alarm_delete_001",
"schedule_id": "schedule_001",
"system_alarm_ref_id": "system_alarm_001",
"reason": "ws_connected_and_time_near"
}- 删除系统闹钟。
- 取消重复闹钟提醒。
- 继续保留业务日程本体。
{
"type": "system.alarm.delete.ack",
"message_id": "msg_alarm_delete_001",
"schedule_id": "schedule_001",
"ok": true,
"system_alarm_ref_id": "system_alarm_001"
}删除失败时:
{
"type": "system.alarm.delete.ack",
"message_id": "msg_alarm_delete_001",
"schedule_id": "schedule_001",
"ok": false,
"system_alarm_ref_id": "system_alarm_001",
"error": {
"code": "SYSTEM_ALARM_DELETE_FAILED",
"message": "系统闹钟删除失败",
"details": {
"reason": "not_found"
}
}
}作用:
- 由客户端主动告诉服务端,用户已经在应用内确认该日程完成。
- 关闭后续监听和所有提醒。
- 与
system.refs.check不同,这不是系统侧存在性检查,而是用户显式完成动作。
{
"type": "schedule.confirmed",
"request_id": "req_schedule_confirm_001",
"schedule_id": "schedule_001",
"confirmed": true,
"timestamp": "2026-07-28T12:05:00+08:00"
}- 取消监听。
- 终止后续提醒。
- 如系统日程或系统闹钟仍存在,按需删除。
{
"type": "schedule.confirmed.ack",
"request_id": "req_schedule_confirm_001",
"schedule_id": "schedule_001",
"ok": true
}失败响应:
{
"type": "schedule.confirmed.ack",
"request_id": "req_schedule_confirm_001",
"schedule_id": "schedule_001",
"ok": false,
"error": {
"code": "SCHEDULE_CONFIRM_FAILED",
"message": "日程确认失败",
"details": {
"reason": "schedule_not_found"
}
}
}客户端成功创建系统日程或系统闹钟后,必须把引用 ID 回写服务端。服务端收到并持久化引用前, 不能把系统兜底视为已建立。
{
"type": "system.refs.register.command",
"request_id": "req_refs_register_001",
"payload": {
"schedule_id": "schedule_001",
"system_schedule_ref_id": "system_schedule_001",
"system_alarm_ref_id": "system_alarm_001"
}
}{
"type": "system.refs.register.result",
"request_id": "req_refs_register_001",
"ok": true,
"payload": {
"schedule_id": "schedule_001",
"system_schedule_ref_id": "system_schedule_001",
"system_alarm_ref_id": "system_alarm_001",
"registered_at": "2026-07-28T12:00:03+08:00",
"replaced_refs": []
}
}规则:
- 两个引用可以分别登记,但至少一个不为空。
- 服务端校验日程属于当前 WebSocket 会话用户。
- 同一日程重复登记相同引用返回原结果。
- 同一日程登记不同引用时视为替换;服务端通过
replaced_refs返回旧引用,由客户端清理旧平台资源。 - 客户端创建平台资源成功但登记失败时,将待登记记录保留在本地并重试。
完成和删除统一使用显式状态命令;schedule.confirmed 作为现有完成消息继续兼容,
但新客户端优先使用本节命令。
{
"type": "schedule.status.command",
"request_id": "req_schedule_status_001",
"payload": {
"schedule_id": "schedule_001",
"target_status": "done",
"reason": "user_confirmed",
"expected_updated_at": "2026-07-28T12:00:00+08:00"
}
}{
"type": "schedule.status.result",
"request_id": "req_schedule_status_001",
"ok": true,
"payload": {
"schedule_id": "schedule_001",
"status": "done",
"updated_at": "2026-07-28T12:05:00+08:00",
"cleanup_required": true
}
}规则:
- 允许
scheduled -> done和scheduled -> deleted。 - 相同目标状态重复提交返回当前结果。
- 已进入
done或deleted后,不允许直接恢复为scheduled;恢复需求通过复制或重新创建处理。 -
expected_updated_at不匹配时返回VERSION_CONFLICT和当前日程摘要,不执行覆盖。 - 状态事务提交后才发送系统资源清理指令;清理失败不回滚业务状态,但必须继续重试并可观测。
- 客户端按服务端
session.ready返回的心跳间隔发送session.ping。 - 服务端回复
session.pong并带回server_time。 - 连续超过两个心跳周期未收到有效响应时,客户端将连接标记为断开,不再把网络可用等同于服务端在线。
客户端重连成功后发送:
{
"type": "session.resume",
"request_id": "req_resume_001",
"payload": {
"device_id": "android_abc123",
"last_schedule_updated_at": "2026-07-28T12:00:00+08:00",
"pending_request_ids": [
"req_schedule_001",
"req_refs_register_001"
],
"pending_message_ids": [
"msg_reminder_001"
]
}
}服务端处理:
- 返回各
pending_request_ids的已知终态;未知请求由客户端按原request_id重发。 - 返回
last_schedule_updated_at之后发生变化的日程。 - 重发未收到 ACK 且仍有效的服务端推送,保持原
message_id。 - 已完成、已删除、已过期或已 ACK 的提醒不得再次激活。
- 音频 Binary Frame 不做断点续传;断线中的语音流标记失败,客户端重新录音。
规则:
- 只有
start_time存在的日程才参与时间监听。 - 时间到达前进入监测窗口。
- 默认提前
15min。 - 进入窗口后优先判断 WS 在线状态和前后台状态。
- 如果 WS 在线,服务端先下发
system.refs.check。 - 如果客户端确认系统日程和系统闹钟都已不存在,服务端取消本次软件提醒并停止监听。
- 如果系统侧兜底提醒仍存在,服务端下发
reminder.control;只有收到展示成功 ACK 后,才按需下发系统日程和系统闹钟删除指令。
规则:
- 只有存在经纬度的日程才参与空间监听。
- 默认围栏半径
100m。 - 客户端持续上传位置,服务端判定是否进入围栏。
- 创建日程时,如果用户当前位置已经在目标围栏内,服务端将
geofence_armed=false。 - 当用户离开目标围栏后,服务端将
geofence_armed=true。 - 只有
geofence_armed=true且用户再次进入围栏时,才允许触发地点提醒。 - 如果创建时无法取得用户当前位置,服务端默认
geofence_armed=true,避免错过后续进入提醒。
schedule_type |
日程形态 | 触发规则 |
|---|---|---|
time |
只有时间 | 时间窗口到达后触发 |
location |
只有地点 |
geofence_armed=true 且用户进入地理围栏后触发 |
time |
同时有时间和地点 | 时间窗口和地理围栏分别形成可触发条件,任一条件首次满足即可提醒 |
组合日程细化规则:
-
schedule_type仍为time,不新增第三种类型。 - 时间条件和地点条件是 OR,不是必须同时满足的 AND;否则用户未在目标地点时可能错过时间提醒。
-
time_triggered_at和geo_triggered_at分别记录两个条件首次命中的时间。 - 同一日程在一次有效提醒周期内只展示一次。一个条件已经成功展示后,另一个条件随后命中只记录
*_triggered_at,不重复展示。 - 提醒展示失败时,尚未展示成功的另一条件仍可触发补偿提醒。
满足以下任一条件时取消监听:
- 用户通过确认接口标记已完成。
- 有时间的日程时间已过。
- 日程被删除。
- 客户端确认系统日程和系统闹钟均已被用户手动删除。
为同时满足“优先触达”和“避免重复”,执行顺序固定为:
- 服务端判定提醒条件满足。
- 服务端下发
system.refs.check。 - 客户端返回引用存在性;引用检查失败或超时时,保留系统兜底。
- 服务端下发带
message_id的reminder.control。 - 客户端渲染固定模板,由应用内弹窗和系统通知复用同一份文案。
- 客户端主提醒实际展示成功后返回
reminder.control.ack(ok=true)。 - 客户端按 TTS 策略异步决定播报、跳过或降级,并单独发送
reminder.tts.result。 - 服务端收到主提醒成功 ACK 后,才下发系统日程/系统闹钟删除指令。
- 客户端删除成功后回传 ACK;服务端清空对应引用 ID。
- 主提醒失败、ACK 超时或删除失败时,不提前清空引用,并按幂等规则重试。
禁止仅凭“WS 在线”或“消息已写入 Socket”删除系统兜底。 禁止等待 TTS 播报完成后才确认主提醒;TTS 失败不能阻止已经展示成功的主提醒进入清理流程。
system_schedule_ref_id 或 system_alarm_ref_id 为空表示该兜底从未登记,不能等同于“用户手动删除”。
只有满足以下全部条件,服务端才可把双引用不存在解释为用户取消意图:
- 两个引用此前至少有一个成功登记。
- 本次检查本身
ok=true。 - 已登记的引用均返回
exists=false。 - 客户端没有返回权限缺失、查询失败或平台不支持。
条件不满足时保留业务日程,按可用通道继续提醒或提示用户修复权限,不自动把 status 改为 deleted。
执行顺序:
- 客户端收到
reminder.control,先按message_id检查主提醒和 TTS 是否已经进入终态。 -
ReminderTemplateRenderer校验模板、版本和参数,并生成唯一rendered_text。 - 应用内弹窗或系统通知使用
rendered_text展示主提醒。 - 主提醒展示成功后立即发送
reminder.control.ack,不等待 TTS。 -
TtsPlaybackPolicy依次检查tts_enabled、前后台、tts_background_enabled、锁屏、 音频焦点和平台限制。 - 策略不允许时不初始化 TTS,发送
status=skipped和对应reason。 - 策略允许时,
TtsReminderAdapter初始化引擎、校验zh-CN、设置稳定utterance_id并播报同一rendered_text。 -
onDone上报completed;onError上报failed;完成后释放音频焦点和 TTS 资源。 - 任一 TTS 终态都写入客户端最小投递状态;相同
message_id重连或重试时只重发结果,不重复朗读。
TTS 降级边界:
- 全局开关关闭:
skipped/disabled。 - 后台开关关闭:
skipped/background_disabled。 - 设备锁屏:
skipped/device_locked。 - 平台禁止后台音频:
skipped/platform_restricted。 - 引擎或语言不可用、音频焦点失败、合成失败:上报对应结果,继续保留已经展示的主提醒。
- App 进程死亡时不执行 TTS,由预注册的系统通知、提示音或震动兜底。
- 只有
start_time存在时才检查时间区间重叠。 - 如果已有日程落在同一时间段,返回冲突提示。
- 冲突结果包含已有日程的标题、时间和 ID。
- 只有地点、没有时间的日程不做时间冲突检测。
- 提示用户“当前时段已有日程”。
- 用户可继续保存,也可修改时间。
- 冲突不是强制失败,但必须明确提示。
- 两个有结束时间的日程在
new_start < existing_end且existing_start < new_end时冲突。 - 缺少
end_time时,MVP 使用可配置的默认占用时长进行检测;默认值必须由产品确认, 在确认前不得写死为数据库规则。 - 边界相接(例如一个日程 10:00 结束、另一个 10:00 开始)不算冲突。
- 只比较同一用户、
status=scheduled且未删除的日程。 - 服务端统一转换为 UTC 后计算,响应按各日程原
timezone展示。
| 场景 | 必须行为 | 禁止行为 |
|---|---|---|
| ASR 失败 | 返回失败阶段,允许重录或手动创建 | 创建不完整正式日程 |
| LLM 输出缺字段/歧义 | 返回草稿、缺失项和歧义项,等待用户确认 | 模型自行补全关键时间或地点后直接写库 |
| WebSocket 断开 | 本地保留待提交命令,重连后按原 request_id 恢复 |
仅凭网络连接状态声明在线 |
| 系统日程/闹钟创建失败 | 保留失败原因并提示可用降级通道 | 保存不存在的引用 ID |
| 系统引用检查失败 | 保留兜底并继续安全提醒策略 | 推断用户已经取消提醒 |
| 软件提醒展示失败 | 保留系统兜底,允许幂等重试 | 先删除系统兜底 |
| 系统资源删除失败 | 保留引用并重试,避免再次发送软件提醒 | 清空引用后宣称删除成功 |
| TTS 模板未知或参数非法 | 降级为 GENERIC,继续展示主提醒 |
阻塞通知或临时调用 LLM 生成文案 |
| TTS 开关关闭、后台未授权或设备锁屏 | 跳过播报并上报明确原因 | 强制播报或修改系统音量 |
| TTS 引擎、语言或音频焦点不可用 | 保留主提醒并上报失败原因 | 把 TTS 失败当作主提醒失败 |
| WebSocket 重复下发相同 TTS | 返回已保存的 TTS 终态 | 使用相同 message_id 再次朗读 |
| 位置权限不可用 | 暂停地点监听并提示权限状态;有时间条件时继续时间提醒 | 把权限失败当作未进入围栏 |
| 服务端重启 | 从 schedules 恢复时间扫描;位置监听等待新位置 |
依赖仅存在于内存的触发状态宣称完整恢复 |
一致性边界:
- 日程写入和
request_id幂等结果必须在同一事务边界内提交,或使用能保证原子可见性的等价实现。 - 服务端事务提交成功后才能下发
schedule.upsert.result。 - WebSocket 推送采用至少一次投递;客户端依靠
message_id去重。 -
time_triggered_at、geo_triggered_at只表示条件已命中,不等于客户端已经展示成功。 - 若实现需要区分“已命中”和“已展示”,MVP 可先保存在可靠消息记录中;不得复用
status表达投递过程。 - TTS 使用主提醒的
message_id去重,但 TTS 终态与主提醒 ACK 相互独立。 -
TextToSpeech.speak()仅表示进入合成队列,必须等待回调后才能记录 TTS 终态。
- WebSocket 生产环境只允许
wss://。 - 用户身份必须来自鉴权会话;
device_id只标识设备,不能代替用户认证。 - 服务端对每次日程读写、引用登记和状态变更校验资源归属。
- 音频只为本次解析处理;是否持久化、保留时长和删除策略必须显式配置,默认不长期保存原始音频。
- 日志不得记录原始音频、完整转写文本、精确经纬度或令牌;排障使用
request_id、message_id、job_id和脱敏错误信息。 - 位置上报只在存在有效地点日程且用户授权时启用;无有效监听对象时停止高频上报。
- ASR、LLM 和地图依赖必须配置超时、有限重试和熔断;重试不得绕过用户确认。
- TTS 固定模板只允许标题、提前分钟数和地点名称,不得播报备注、详细地址、经纬度或令牌。
- 锁屏状态不播报 TTS;后台播报必须由用户显式开启,关闭开关后立即停止待播报内容。
- TTS 不得绕过系统静音或勿扰策略,不得为了播报强制修改媒体或闹钟音量。
- 所有命令/查询均能按同一
request_id重放并得到一致结果。 - 相同
request_id、不同载荷返回IDEMPOTENCY_CONFLICT。 - 服务端推送重复到达时,客户端按
message_id只执行一次。 - 未知消息、缺失字段、非法状态和版本冲突都有稳定错误码。
- ASR/LLM 成功只生成草稿,用户确认前
schedules无新增记录。 - 缺少时间或地点时表单明确补全,不能静默创建无触发条件的日程。
- 音频流中断不会创建正式日程,重连后可重新录音。
- 前台在线、后台在线和离线/服务不可达三类场景均有可验证触达路径。
- 软件提醒 ACK 成功前,系统兜底不会被删除。
- 删除系统资源失败时引用仍保留,重试不会重复展示软件提醒。
- 系统引用从未登记、已被用户删除、无权限查询三种情况能被区分。
- 设备重启、App 进程死亡和服务端重启后,未完成日程仍能恢复到正确监听状态。
- TTS 完成、跳过或失败均不改变主提醒 ACK 和系统兜底清理判断。
- 时间-only、地点-only、时间+地点三类日程分别覆盖。
- 组合日程任一条件命中可提醒,但一次有效周期只展示一次。
- 围栏内创建不会立即触发;离开再进入后可以触发。
- 时区转换、夏令时边界、结束时间为空和区间边界相接均有测试。
- 文档和 JSON 示例通过静态格式检查。
- 接口模型可生成 Schema,并用成功、失败、重复和重连样例做契约测试。
- Android 系统日程、闹钟、通知、TTS、后台限制和位置权限必须在真机或目标模拟器验证。
- 只有静态文档检查时,结论必须写为“设计已细化/静态检查通过”,不得写成“提醒能力已实现”。
-
TIME_ADVANCE、TIME_DUE、LOCATION_ENTER和GENERIC分别渲染为文档定义的固定文案。 - 通知、应用内弹窗和 TTS 使用同一次
rendered_text,不存在通道间文案差异。 - 标题为空时使用“一项日程”;提前分钟数非法、地点为空、模板未知或版本未知时降级为
GENERIC。 -
tts_enabled=false时不初始化 TTS,主提醒正常展示。 - 后台未授权、锁屏或平台受限时返回对应
skipped,不得自动播报。 - 引擎、语言、音频焦点或合成失败时返回对应结果,主提醒状态不受影响。
- 相同
message_id重复到达时不重复朗读,只重发已保存的 TTS 结果。 -
speak()入队后发生onError时不得上报completed;只有onDone可以上报完成。 - App 前台、后台、锁屏、静音、勿扰、引擎缺失和进程死亡场景均有目标设备验证记录。