-
Notifications
You must be signed in to change notification settings - Fork 6
Architecture interface design
- 客户端不再独立承担全部语音理解和提醒编排。
- 客户端进入界面后建立 WebSocket 连接,作为后续实时通信主通道。
- 音频通过 WebSocket Binary Frame 流式传输到服务端。
- 服务端完成 ASR + LLM 结构化提取后,通过 WebSocket 回传结果。
- 客户端用统一表单完成二次确认、地理位置补全、时间补全和提醒参数补全。
- 日程最终落到单一
schedules表中,状态和关联信息一并保存。 - 提醒采用三段式策略,优先保证触达,再处理重复提醒控制。
这一版的重点不是“完整日历”,而是“日程创建 + 状态驱动提醒 + 语音结构化”闭环。
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 文件流并播放;离线时播放客户端预置固定铃声。
- 在操作系统允许范围内,客户端预先注册自启动能力,用于进程异常终止后恢复客户端运行。
服务端负责:
- 音频文件接收。
- ASR 调用。
- LLM 结构化提取。
- WebSocket 消息分发。
- 日程冲突检测。
- 日程状态监控。
- 地点与时间窗口判断。
- 提醒控制消息下发。
- 提醒通道选择由客户端自行完成。
- 根据日程类型、标题和提醒提前量,通过
ReminderTemplateRenderer生成固定格式的提醒文案,不调用 LLM。 - 日程创建成功后异步调用 TTS 生成音频;日程每次更新成功后都重新调用 TTS 合成流程。
- 每个日程使用唯一音频对象标识,将新生成的音频覆盖写入云端音频存储。
- 在线提醒触发时,根据日程 ID 从对象存储读取对应音频并通过 WebSocket 流式下发。
- 第三方 ASR 服务。
- 大模型服务。
- 第三方地图 SDK。
- Android 客户端震动、音频播放和后台运行能力。
- 外部 TTS 服务。
- 云端音频存储。
职责:
- 显示日程列表。
- 显示创建/编辑表单。
- 显示语音解析结果。
- 显示冲突提示。
职责:
- 录音。
- 音频格式转换。
- 发起上传。
职责:
- 建立和维护连接。
- 接收结构化草稿。
- 接收提醒控制消息。
- 上传位置信息。
- 上传日程确认结果。
职责:
- 缓存日程。
- 缓存解析结果。
- 缓存提醒状态。
- 缓存提醒通道状态。
职责:
- 前台弹窗。
- 后台浮窗。
- 系统通知。
- 根据当前应用状态决定提醒展示方式。
职责:
- 地点搜索。
- 地点确认。
- 地理围栏设置。
- 实时位置上报。
职责:
- 接收音频文件。
- 校验格式和大小。
- 生成处理任务 ID。
职责:
- 调用 ASR。
- 调用 LLM。
- 识别
schedule_type。 - 生成结构化草稿。
职责:
- 建立客户端会话。
- 维护设备在线状态。
- 分发解析结果和提醒控制消息。
- 接收位置上报。
职责:
- 创建日程。
- 编辑日程。
- 查询日程。
- 冲突检测。
职责:
- 监听日程时间。
- 监听地理位置。
- 判断时间、空间以及组合触发条件。
- 进入提醒窗口后判断设备在线状态。
- 根据设备状态决定是否触发软件提醒。
职责:
- 根据上报位置和日程坐标计算距离。
- 判断是否进入地理围栏。
职责:
- 根据日程类型使用固定模板生成提醒文案,不调用 LLM。
- 时间类模板为
您有一个日程,{相对时间描述},{title}。;相对时间根据time_remind_offset_minutes生成,不按照日程创建时刻计算。 - 地点类模板为
您已到达目标地点附近,别忘了{title}。。 - 日程创建成功后立即异步生成音频;日程每次更新成功后都重新调用 TTS 合成流程,其中
title、schedule_type和time_remind_offset_minutes是影响提醒内容的字段。 - 每个日程使用
reminder-audio/{schedule_id}.{audio_format}作为音频对象标识,audio_format必须与实际存储文件格式一致。 - 日程更新后,旧音频在新 TTS 任务开始时立即失效;新生成的完整音频通过原子写入成为当前音频,每个日程只保留一个音频文件。
- 异步任务写入前必须重新校验日程
updated_at;任务对应的版本不是最新版本时,丢弃生成结果,禁止覆盖当前音频。 - 不向
schedules或其他业务表写入 TTS 文案、文件路径和生成状态字段。 - 在线提醒触发时,根据
schedule_id读取并下发对应音频;音频不存在时仍下发reminder.control,但不发送音频流。
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 | 开始时间,持久化为 UTC ISO-8601,可以为空 |
end_time |
text | 结束时间,持久化为 UTC ISO-8601,可以为空 |
timezone |
text | 原始 IANA 时区,用于前端本地化展示,可为空 |
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 | 预留字段 |
created_at |
text | 创建时间 |
updated_at |
text | 更新时间 |
- 核心字段尽量扁平化。
- 语音解析草稿只通过 WS 传递,不落库。
- 时间提醒和地理提醒可以同时存在。
- 只保留一张主表,MVP 不拆分额外业务表。
-
status只表达日程本体是否还有效,不表达监听中、已触发、已过期等过程状态。 - MVP 不保存用户自定义的重要程度,提醒方式由应用前后台状态、网络状态和时间/空间窗口决定。
- 冲突检测结果只在接口响应中返回,不写入
schedules表。 -
start_time和地点信息都允许为空,但二者不能同时为空。 -
geofence_armed用于避免“用户在目标地点创建日程后立刻触发位置提醒”。 - 最近一次位置只在会话内或内存中计算,不落库。
-
schedule_type是前端表单必填项控制和后端校验的依据,不再使用全天字段区分业务类型。 - LLM 草稿时间使用
YYYY-MM-DDTHH:mm本地时间格式,由程序注入 IANA 时区。 - 最终提交的无偏移时间必须同时提供
timezone;带偏移时间可直接提交。 - 服务端校验时间后统一转换为固定 UTC ISO-8601 格式持久化。
- TTS 音频使用日程数据和固定模板派生,不在
schedules表中增加音频路径、文案或生成状态字段。 - 每个日程只保留一个 TTS 文件,日程更新后使用新生成的完整音频覆盖原文件。
建议状态流转:
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 对象表达。
- 建立单次语音流。
- 使用 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,帧内不重复携带流 ID。服务端不发送逐分片 ACK,客户端通过 WebSocket
传输层背压控制发送速度。
{
"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
}
}
}其他错误码:
| 错误码 | 含义 |
|---|---|
UNSUPPORTED_AUDIO_CONFIG |
采样率或声道数不受支持 |
VOICE_STREAM_ALREADY_ACTIVE |
当前设备已有活动音频流 |
VOICE_STREAM_NOT_ACTIVE |
结束流时不存在活动音频流 |
VOICE_STREAM_ID_MISMATCH |
结束消息中的流 ID 与活动流不一致 |
EMPTY_AUDIO_CHUNK |
Binary Frame 不包含音频数据 |
EMPTY_AUDIO_STREAM |
音频流结束前未收到有效音频数据 |
UNEXPECTED_BINARY_FRAME |
尚未开始音频流便发送 Binary Frame |
说明:
- 不再提供 HTTP 音频上传接口。
- JSON Text Frame 只传控制信息,音频内容只通过 Binary Frame 发送。
- 真正的结构化结果仍通过
voice.parse.result返回。 - MVP 只接收
pcm_s16le、16000Hz、单声道音频,最长120000ms。 -
voice.stream.ended必须先于对应的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-31T15: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 | 否 | ISO-8601 开始时间;无 UTC 偏移时必须同时提供 timezone
|
end_time |
string/null | 否 | ISO-8601 结束时间;无 UTC 偏移时必须同时提供 timezone
|
timezone |
string/null | 否 | IANA 时区,例如 Asia/Shanghai
|
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不传,服务端根据最近一次位置上报与目标地点距离计算默认值。 - 服务端拒绝非法日期、无时区上下文的本地时间以及早于
start_time的end_time。 - 服务端在冲突检测和持久化前统一将时间转换为 UTC。
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-29T07:00:00+00: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数据。 - 返回结果按规范化后的 UTC
start_time asc nulls last, created_at desc排序。
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"
}连接失败或鉴权失败时,服务端返回错误消息后关闭连接:
{
"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",
"schedule_scope": "current",
"latitude": 31.2451,
"longitude": 121.5067,
"accuracy": 18,
"timestamp": "2026-07-28T12:01:00+08:00"
}{
"type": "location.report.ack",
"ok": true
}失败响应:
{
"type": "location.report.ack",
"ok": false,
"error": {
"code": "INVALID_LOCATION",
"message": "位置信息不合法",
"details": {
"field": "latitude"
}
}
}- 服务端根据当前位置计算日程距离。
- 如果日程包含地点且命中围栏,判断当前是否满足提醒条件。
- 若日程还包含时间,再结合时间窗口决定是否下发提醒控制消息。
服务端只下发“该提醒了”,具体走哪种展示通道由客户端根据前后台状态自行决定。
{
"type": "reminder.control",
"schedule_id": "schedule_001",
"reason": "time_window_reached",
"action": "show"
}客户端执行成功时回传:
{
"type": "reminder.control.ack",
"schedule_id": "schedule_001",
"ok": true
}客户端执行失败时回传:
{
"type": "reminder.control.ack",
"schedule_id": "schedule_001",
"ok": false,
"error": {
"code": "REMINDER_DISPLAY_FAILED",
"message": "提醒展示失败"
}
}作用:
- 由客户端主动告诉服务端,用户已经在应用内确认该日程完成。
- 关闭后续监听和所有提醒。
{
"type": "schedule.confirmed",
"schedule_id": "schedule_001",
"confirmed": true,
"timestamp": "2026-07-28T12:05:00+08:00"
}- 取消监听。
- 终止后续提醒。
{
"type": "schedule.confirmed.ack",
"schedule_id": "schedule_001",
"ok": true
}失败响应:
{
"type": "schedule.confirmed.ack",
"schedule_id": "schedule_001",
"ok": false,
"error": {
"code": "SCHEDULE_CONFIRM_FAILED",
"message": "日程确认失败",
"details": {
"reason": "schedule_not_found"
}
}
}作用:
- 日程触发提醒时,由服务端向在线客户端下发云端预生成的 TTS 音频。
- 音频通过 WebSocket Binary Frame 发送。
- 客户端完成播放后向服务端返回播放结果。
- 日程创建成功后,服务端立即提交异步 TTS 生成任务,不等待音频生成完成再返回日程创建结果。
- 时间类日程使用
您有一个日程,{相对时间描述},{title}。模板。 - 服务端直接将
time_remind_offset_minutes格式化为相对时间描述。例如字段值为15时,文案为您有一个日程,十五分钟后,项目评审会议。。 -
start_time只用于计算提醒触发时刻,不参与 TTS 文案计算;相对时间也不按照音频生成时刻与start_time的实际差值计算。 - 地点类日程使用
您已到达目标地点附近,别忘了{title}。模板。 - 时间和地点同时存在且
schedule_type=time时,使用时间类模板。 - 日程每次更新成功后,服务端都必须提交新的异步 TTS 生成任务;任务确认自身对应当前
updated_at后,先使旧音频失效,再调用 TTS。 - 日程更新与 TTS 生成解耦;TTS 生成失败不影响日程创建或更新,也不得恢复或继续发送旧版本音频。
- 每个日程使用
reminder-audio/{schedule_id}.{audio_format}作为音频对象标识;audio_format来源于实际文件后缀,不通过音频内容猜测。 - TTS 生成完成后,将完整音频原子写入当前对象,不保留历史版本;同一日程不得同时保留多个格式的音频文件。
- 异步生成任务必须携带任务创建时的日程
updated_at。 - 覆盖写入前,服务端重新读取日程;只有任务携带的
updated_at与当前值一致时才允许写入,否则直接丢弃该过期任务的生成结果。 - 文件必须在生成完整后再执行原子覆盖,客户端不得读取到未完成的音频内容。
- 日程被删除时,同时删除其对应的 TTS 音频文件。
- 日程更新开始生成新音频时,必须先删除旧音频;生成失败时保持无音频状态,禁止继续使用旧内容。
提醒触发时,服务端先下发 reminder.control,再发送音频流开始消息:
{
"type": "reminder.audio.start",
"schedule_id": "schedule_001",
"stream_id": "stream_audio_001",
"audio_format": "mp3"
}随后,服务端通过同一条 WebSocket 连接发送 Binary Frame。
音频发送完成后,服务端发送:
{
"type": "reminder.audio.end",
"schedule_id": "schedule_001",
"stream_id": "stream_audio_001"
}如果提醒触发时音频不存在、尚未生成完成或生成失败,服务端仍发送 reminder.control,但跳过 reminder.audio.start、Binary Frame 和 reminder.audio.end。客户端继续执行普通提醒,不将其视为设备离线。
- 接收服务端下发的音频。
- 音频接收完成后播放,并返回播放结果。
- 音频接收或播放失败时,返回明确的失败响应。
接收成功:
{
"type": "reminder.audio.ack",
"schedule_id": "schedule_001",
"stream_id": "stream_audio_001",
"ok": true
}接收失败:
{
"type": "reminder.audio.ack",
"schedule_id": "schedule_001",
"stream_id": "stream_audio_001",
"ok": false,
"error": {
"code": "REMINDER_AUDIO_PLAY_FAILED",
"message": "提醒音频接收或播放失败"
}
}规则:
- 只有
start_time存在的日程才参与时间监听。 - 时间到达前进入监测窗口。
- 默认提前
15min。 - 进入窗口后优先判断 WS 在线状态和前后台状态。
- 如果 WS 在线,服务端直接下发
reminder.control。 - 服务端在
reminder.control后通过提醒音频下发接口发送该日程对应的 TTS 音频;音频不可用时只发送控制消息。 - 如果 WS 不在线,客户端根据自身能力降级为普通通知。
规则:
- 只有存在经纬度的日程才参与空间监听。
- 默认围栏半径
100m。 - 客户端持续上传位置,服务端判定是否进入围栏。
- 创建日程时,如果用户当前位置已经在目标围栏内,服务端将
geofence_armed=false。 - 当用户离开目标围栏后,服务端将
geofence_armed=true。 - 只有
geofence_armed=true且用户再次进入围栏时,才允许触发地点提醒。 - 如果创建时无法取得用户当前位置,服务端默认
geofence_armed=true,避免错过后续进入提醒。 - 地点提醒触发且 WS 在线时,服务端在
reminder.control后下发地点类日程的预生成 TTS 音频;音频不可用时只发送控制消息。
schedule_type |
日程形态 | 触发规则 |
|---|---|---|
time |
只有时间 | 时间窗口到达后触发 |
location |
只有地点 |
geofence_armed=true 且用户进入地理围栏后触发 |
如果日程同时包含时间和地点,则 schedule_type 仍为 time,但提醒执行时同时参考时间窗口和地理围栏条件。
满足以下任一条件时取消监听:
- 用户通过确认接口标记已完成。
- 有时间的日程时间已过。
- 日程被删除。
- 只有
start_time存在时才检查时间区间重叠。 - 如果已有日程落在同一时间段,返回冲突提示。
- 冲突结果包含已有日程的标题、时间和 ID。
- 只有地点、没有时间的日程不做时间冲突检测。
- 提示用户“当前时段已有日程”。
- 用户可继续保存,也可修改时间。
- 冲突不是强制失败,但必须明确提示。