-
Notifications
You must be signed in to change notification settings - Fork 6
New Architecture interface design
版本:v2.9
日期:2026-08-07
适用范围:语音驱动的日程创建、提醒配置、本地提醒与云端备份恢复
TimeFlow 是一款以语音为核心交互方式的时间管理 App。用户通过语音创建、查询、修改和删除时间日程、地点日程及其提醒配置;当信息缺失、存在多个匹配结果、涉及重复日程范围或需要二次确认时,系统通过语音反问和语音输出继续完成对话。日历视图只负责展示和状态反馈,不提供日程业务编辑表单。
系统将“云端确认写入”和“客户端运行执行”分离:语音操作经过多轮补全和用户确认后,由服务端业务层校验并写入云端数据库;当前客户端直接应用服务端返回的最终快照,首次安装、换设备或需要恢复时再通过 HTTP 全量同步本地数据。客户端本地数据库负责日常读取,时间和地点监听全部由客户端实现。断网时客户端可以查看本地数据并继续执行已经注册的提醒,但不能创建、修改或删除日程,也不能执行依赖服务端的 ASR、LLM 和 TTS。
| 原则 | 设计结论 |
|---|---|
| 用户真正要完成的是管理时间 | 日程、提醒、查询和处置是核心业务;日历只是展示载体 |
| 语音是主要交互方式 | 所有核心业务操作走语音,不维护重复的 GUI 表单流程 |
| 提醒必须不依赖云端实时在线 | 已同步日程由客户端本地监听和本地送达 |
| 云端服务集中处理高价值智能能力 | ASR、LLM、TTS 在服务端统一调用和升级 |
| 业务写入必须有唯一权威 | 云端数据库负责确认后的日程与提醒写入,本地只应用云端确认快照 |
| 数据模型应服务于当前功能 | 只保存日程、提醒、账号和全量恢复所需字段,不持久化登录会话,不提前引入画像、导航或复杂历史 |
| 复杂情况通过对话解决 | 缺失信息、目标歧义、重复范围和危险操作由语音追问确认 |
| 要做 | 不做 |
|---|---|
| 一次性、周期性和全天时间日程 | 独立的离线智能创建 |
| 地点日程和返回记录地点提醒 | 服务端持续监听所有日程 |
| 最多两个时间或地点提醒 | 独立的“离开地点时提醒” |
| 低、中、高三级提醒强度 | 本期智能提醒规则引擎 |
| 语音创建、查询、修改、删除和确认 | GUI 表单创建和编辑 |
| 账号、云端确认写入和单设备全量恢复 | 多设备实时同步、增量变更和过早引入天气、路线、通勤等上下文 |
| 本地时间与地点监听 | 本地 ASR、LLM、TTS |
模块按完整用户能力划分。一个模块必须能够独立说明输入、处理和输出,不以某一个 DTO、WebSocket 消息或第三方 SDK 作为模块边界。第三方接口适配属于基础设施层,网关层只负责对外网络协议。
| 模块 | 包含的小功能 | 输入 | 输出 | 边界 |
|---|---|---|---|---|
| 语音交互模块 | 麦克风采集、WebSocket 音频流、对话上下文、问题播放、TTS 播放、语音结果接收 | 用户语音、服务端语音消息 | 音频流、对话回答、业务结果 | 不执行 ASR、LLM、TTS 模型,不直接写云端数据库 |
| 本地日程模块 | 本地日程存储、日历展示、日程快照应用、周期日程计算所需的本地数据读取 | 语音操作结果、同步结果 | 日程列表、日程详情、本地快照 | 不提供 GUI 编辑;不自行生成未被服务端确认的云端日程 |
| 本地提醒配置模块 | 最多两个提醒的保存、时间提醒和地点提醒配置展示、提醒强度保存 | 语音操作结果、同步结果 | 本地提醒配置 | 不负责判断是否到期;不创建第三个提醒 |
| 本地监听与提醒执行模块 | 时间监听、地点距离计算、返回地点布防、延期、强度分级、弹窗、震动、屏幕展示和音频播放 | 本地日程、提醒配置、当前时间、定位数据、用户处置 | 本地提醒送达、延期结果、送达状态 | 不依赖服务端到点消息;提醒业务规则由客户端实现 |
| 账号与同步模块 | 账号创建或登录、访问凭证保存、首次安装或需要恢复时应用全量快照、提醒处置状态上传、离线只读状态 | HTTP 账号响应、HTTP 全量快照响应、提醒状态响应、WebSocket 云端确认快照 | 本地数据恢复、处置状态同步、网络状态 | 不执行日程业务语义;不在离线时创建或修改日程 |
| 模块 | 包含的小功能 | 输入 | 输出 | 边界 |
|---|---|---|---|---|
| 账号与同步服务 | 账号创建或登录、JWT 签发与校验、首次安装或需要恢复时的全量快照查询和提醒处置状态同步 | HTTP 认证请求、提醒处置状态 | 访问令牌、云端完整快照、状态同步结果 | 不维护登录会话;不处理语音语义;不负责时间和地点监听 |
| 语音交互模块 | 音频流接收、ASR 转写和 TTS 音频输出 | WebSocket 文本帧和 Binary Frame | 语音问题、结构化操作结果、错误、TTS 音频 | 不直接操作客户端 GUI;不持久化日程;不在服务端执行提醒 |
| 日程==数据==管理模块 | 管理用户确认后的日程和提醒配置,提供查询、创建、修改和删除能力 | 语音交互模块输出的查询请求和已确认结构化操作 | 日程与提醒数据、查询结果 | 不负责语音理解、本地监听和提醒送达 |
| 对话管理模块 | 会话建立、LLM 多轮理解、缺失信息追问、二次确认、结构化结果返回 |
“语音交互模块”是产品功能边界,内部仍然保持三类技术职责:gateway/ 负责 WebSocket 会话、消息路由==和协议转换==;==intelligence/ 负责智能能力编排、提示词和结构化结果处理;==infrastructure/ 负责阿里云 ASR、OpenAI LLM、阿里云 TTS 等第三方接口适配。合并模块名称是为了避免把一个完整的用户能力拆成两个产品模块,并不表示把协议处理和模型调用代码写在同一层。
| 能力 | 前端负责 | 后端负责 |
|---|---|---|
| 语音输入 | 录音、分片、上传 | 接收音频、调用 ASR |
| 语义理解 | 保存对话 UI 状态、播放问题的音频 | 调用 LLM、补全字段、追问和消歧 |
| 日程变更 | 应用云端确认快照到本地数据库 | 校验业务命令并在云端事务写入 |
| 提醒监听 | 时间、地点、返回地点和延期逻辑 | 不参与到点判断 |
| 提醒送达 | TTS 播放、屏幕、震动、强度策略 | 生成 TTS 音频并提供缓存/同步来源 |
| 数据可靠性 | 本地运行读取、离线提醒、首次安装或需要恢复时全量恢复 | 云端写入权威和账号隔离 |
| 数据位置 | 作用 | 权威范围 |
|---|---|---|
| 客户端本地数据库 | 日常日程读取、日历展示、提醒监听和本地提醒状态 | 当前设备的运行读取权威,不接受独立业务写入 |
| 云端数据库 | 已确认日程与提醒写入、账号隔离和登录恢复 | 业务写入和备份数据的唯一权威 |
| 客户端安全存储 | access token 和当前 account_id | 不进入普通业务表 |
| 服务端内存 | 当前 WebSocket 会话和多轮对话短状态 | 仅限连接生命周期,不作为持久化数据 |
本地和云端不是两套独立业务数据。语音操作必须先由服务端业务层完成云端事务,再把持久化后的最终快照返回当前客户端;当前客户端直接应用该快照,不再通过 HTTP 重复拉取或上传。首次安装、换设备、账号切换或本地数据丢失时,客户端通过 HTTP 全量快照恢复本地数据;正常登录同一账号且本地数据完整时,直接使用本地数据。本期只支持单设备业务流程,不设计多设备实时同步、增量游标和客户端与云端的双向字段合并。
数据按语义分成两类:
| 数据类别 | 示例 | 写入与同步规则 |
|---|---|---|
| 账号级共享业务状态 | 日程标题、时间、地点、周期、删除状态、提醒类型、强度、启用状态、用户主动延期时间 | 必须先写云端,再返回并同步到所有设备 |
| 设备级提醒运行状态 |
next_trigger_at、geofence_armed
|
只由当前设备维护,不上传云端 |
| 提醒处置状态 |
disposition_state、snoozed_until
|
客户端立即更新,通过 HTTP 异步同步云端 |
本期离线不允许创建、修改、删除或语音延期,因此不会产生等待上传的账号级业务状态。客户端本地发生的变化仅限提醒监听和送达运行状态。
| 类型 | 必填字段 | 可选字段 | 说明 |
|---|---|---|---|
time |
title、start_time、is_all_day
|
end_time、recurrence_rule、地点字段 |
一次性、周期性或全天时间日程 |
location |
title、有效地点坐标 |
地点名称 | 以到达地点为主要指向的日程 |
所有时间保存为带时区的 UTC 时间;timezone 保留用户创建时的 IANA 时区,用于周期展开和语音表达。is_all_day=true 时,start_time 和 end_time 仅作为本地日期的起止边界保存:单日全天的 end_time 是下一本地日期的零点,多日全天的 end_time 是结束日期的下一日零点;客户端展示为全天,不将午夜表达为用户指定的时间。地点日程必须有经纬度,地点名称可以由地址解析结果补全。
| 缺失信息 | 默认处理 |
|---|---|
| 日程类型 | 根据有效时间或地点字段推断;两者都不存在时必须追问 |
| 日程周期 | 未表达周期时按一次性处理 |
| 只有日期没有具体时分 | 设置 is_all_day=true,按本地日期边界保存,不追问具体开始时刻 |
| 时区 | 使用客户端当前 IANA 时区 |
| 时间提醒 | 非全天时间日程默认提前 15 分钟提醒;全天日程默认在开始日期前一天 09:00 提醒;用户明确不提醒时不创建 |
| 地点提醒 | 只有用户明确表达地点提醒或创建地点日程时才创建 |
| 提醒强度 | 使用中强度 |
| 修改未提及字段 | 保持原值,不使用默认值覆盖 |
| 类型 | 触发条件 | 必要数据 |
|---|---|---|
at_time |
到达指定绝对时间 | trigger_at |
before_start |
日程开始边界前指定分钟数 | 时间日程、offset_minutes
|
arrive_location |
进入日程目标地点围栏 | 日程经纬度 |
return_to_recorded_location |
创建时记录当前位置,离开范围后再次进入 | 记录点经纬度 |
不提供独立的离开地点提醒。返回记录地点提醒的客户端流程为:
创建提醒时保存当前位置
↓
当前位置在记录范围内:不触发
↓
检测到用户离开范围:geofence_armed = true
↓
再次进入记录范围:触发提醒并取消本轮监听
at_time 表示一个明确的绝对时间点,全天日程可以在用户明确指定提醒时刻时使用该类型。周期日程如果需要每次发生都提醒,应使用 before_start。全天日程的技术开始边界是开始日期本地 00:00,默认 offset_minutes=900,即在前一天本地 09:00 提醒;周期性全天日程对每次发生都按该规则计算。默认提醒时间已经过去时不补发,由语音交互询问用户是否重新指定提醒时间。本期不把一个绝对 trigger_at 自动复制到周期日程的所有发生实例。
提醒强度直接决定客户端的送达方式。下表中的“普通弹窗”是应用自定义的全局弹窗,不是系统通知。
| 强度 | 送达方式 | 适用语义 |
|---|---|---|
low |
系统通知 | 非紧急提醒 |
medium |
普通弹窗 + 短震动 | 默认提醒强度 |
high |
普通弹窗 + 短震动 + TTS | 用户明确要求高强度提醒 |
low 和 medium 不依赖 TTS。high 的 TTS 音频不可用时,使用普通弹窗、短震动和本地提示音完成提醒。
本版先定义最小提醒状态,避免把提醒送达状态误当成日程完成状态:
| 状态 | 含义 |
|---|---|
pending |
已配置,等待条件满足 |
confirmed |
用户确认已处理本轮提醒,不表示日程业务完成 |
snoozed |
用户选择延期,或提醒超时未操作,等待 snoozed_until
|
提醒尚未触发或本轮尚未处置时,disposition_state 为空;它不是提醒配置的启用状态。用户没有指定延期时间时,默认延期十分钟。confirmed 不等同于日程完成;“完成”动作暂不定义为待办事项完成,当前不纳入服务端和客户端的核心状态机。
HTTP 只承担账号认证、云端全量快照恢复和提醒处置状态同步,不提供日程业务 CRUD,也不接收客户端上传的日程变更。所有日程与提醒写入都由已认证 WebSocket 语音命令触发,并由后端业务层完成云端事务。
基础地址:https://<host>/v1
POST /auth/access
本期采用登录和注册一体化的简化流程,不引入验证码、邮箱验证、手机号验证或独立的登录会话表。服务端根据 login_identifier 查询账号:账号不存在时创建账号并登录;账号已存在时校验密码,校验成功后登录。
请求:
{
"login_identifier": "user@example.com",
"password": "strong-password"
}处理规则:
- 新账号:校验账号和密码格式,创建账号后签发 JWT。
- 已有账号:校验密码后签发 JWT。
- 已有账号但密码错误:返回认证错误,不创建新账号。
- 数据库对
login_identifier保持唯一约束,避免并发创建重复账号。
响应 200 OK:
{
"account_id": "acc_001",
"access_token": "access-token",
"expires_in": 3600
}服务端签发包含 account_id 和 exp 的无状态 JWT,不保存登录会话,也不提供 Refresh Token。客户端将 Access Token 保存到平台安全存储;Token 到期后重新登录。退出登录只删除客户端 Token,不调用服务端登出接口,本地日程不删除。本期不设计多设备同时编辑和数据冲突处理。
GET /sync/snapshot
==schedule/snapshot== 请求头:Authorization: Bearer <access_token>`
客户端仅在首次安装、换设备、账号切换、本地数据丢失或用户主动恢复时调用该接口。正常登录同一账号且本地数据完整时不调用该接口,直接使用本地 SQLite 数据。客户端在本地安全存储中保留当前账号标识和本地数据初始化状态,用于判断是否需要恢复。服务端返回当前账号的全部日程及其提醒配置。
响应 200 OK:
{
"schedules": [
{
"id": "schedule_001",
"schedule_type": "time",
"title": "项目评审",
"is_all_day": false,
"start_time": "2026-08-07T07:00:00Z",
"end_time": null,
"timezone": "Asia/Shanghai",
"recurrence_rule": null,
"location_name": "203会议室",
"latitude": 31.2304,
"longitude": 121.4737,
"status": "active",
"reminders": [
{
"id": "reminder_001",
"reminder_type": "before_start",
"trigger_at": null,
"offset_minutes": 15,
"target_latitude": null,
"target_longitude": null,
"strength": "medium",
"enabled": true,
"snoozed_until": null
}
],
"occurrence_overrides": []
}
]
}客户端在一个本地事务中用该快照覆盖本地日程和提醒数据:快照中不存在的本地业务数据删除,快照中的数据插入或更新,事务提交后重新注册本地监听。当前发起语音操作的设备不需要在写入成功后调用该接口重复拉取,它直接应用 WebSocket 返回的云端持久化快照。
PUT /sync/reminder-state
==/schedule/reminder-state==
该接口只同步本地提醒处置结果,不创建新的日程,也不负责判断时间或地点条件。客户端在本地完成确认、延期或超时处理后立即更新本地状态,再异步调用该接口。
请求头:Authorization: Bearer <access_token>
确认请求:
{
"schedule_id": "schedule_001",
"reminder_id": "reminder_001",
"disposition_state": "confirmed",
"snoozed_until": null,
"occurred_at": "2026-08-06T08:00:00Z"
}延期或超时请求:
{
"schedule_id": "schedule_001",
"reminder_id": "reminder_001",
"disposition_state": "snoozed",
"snoozed_until": "2026-08-06T08:10:00Z",
"occurred_at": "2026-08-06T08:00:00Z"
}服务端校验账号、日程和提醒归属关系,并校验 snoozed_until 的时间范围。接口采用“设置目标状态”语义:重复提交相同请求只返回当前状态,不重新计算 server_now + 10,因此不需要 event_id。
响应:
{
"ok": true,
"schedule_id": "schedule_001",
"reminder_id": "reminder_001",
"disposition_state": "snoozed",
"snoozed_until": "2026-08-06T08:10:00Z",
"updated_at": "2026-08-06T08:00:02Z"
}客户端本地处置后,先将 disposition_state 更新为用户选择的 confirmed 或 snoozed,同时将 sync_status 标记为 pending。HTTP 请求成功后,客户端根据服务端返回的 disposition_state 和 snoozed_until 覆盖本地处置值,再将 sync_status 改为 synced;服务端没有正常响应时,不得把同步标志改为 synced,本地保持 pending 并在网络恢复后重试。重试不会阻塞本地监听。
WebSocket 用于会话建立、流式音频、语音多轮交互、结构化业务结果和 TTS 音频。文本控制消息使用 JSON Text Frame,音频使用 Binary Frame。
连接地址:
wss://<host>/ws?device_id=<device_id>
连接后第一条消息完成访问令牌校验。device_id 只用于识别设备,真正的访问控制由 access_token 完成。
客户端发送:session.hello
{
"type": "session.hello",
"request_id": "req_session_001",
"payload": {
"access_token": "access-token",
"device_id": "device_001",
"app_version": "2.0.0",
"timezone": "Asia/Shanghai"
}
}服务端响应:session.ready
{
"type": "session.ready",
"request_id": "req_session_001",
"ok": true,
"payload": {
"session_id": "ws_session_001",
"server_time": "2026-08-06T03:00:00Z"
}
}未通过鉴权或协议版本不支持时返回 session.error 并关闭连接。服务端不通过 WebSocket 接收账号密码。
客户端发送:voice.stream.start
{
"type": "voice.stream.start",
"request_id": "req_voice_001",
"payload": {
"conversation_id": null,
"audio_format": "pcm_s16le",
"sample_rate_hz": 16000,
"channels": 1
}
}服务端响应:voice.stream.started
{
"type": "voice.stream.started",
"request_id": "req_voice_001",
"ok": true,
"payload": {
"stream_id": "stream_001",
"conversation_id": "conversation_001"
}
}客户端随后连续发送 Binary Frame。每个 Binary Frame 都是原始 PCM 音频分片,不额外包装 JSON;WebSocket 保证同一连接内消息顺序,服务端按接收顺序交给 ASR。
客户端发送:voice.stream.end
{
"type": "voice.stream.end",
"request_id": "req_voice_001",
"payload": {
"stream_id": "stream_001"
}
}服务端先返回最终 ASR:voice.asr.completed
{
"type": "voice.asr.completed",
"request_id": "req_voice_001",
"conversation_id": "conversation_001",
"payload": {
"transcript": "明天下午三点在203开会",
"language": "zh",
"duration_ms": 2100
}
}当信息缺失、多个日程匹配、重复范围不明确或删除需要确认时,服务端发送:voice.dialogue.question
{
"type": "voice.dialogue.question",
"request_id": "req_voice_001",
"conversation_id": "conversation_001",
"payload": {
"question_id": "question_001",
"question_kind": "missing_field",
"speech_text": "请问会议在哪里举行?",
"required_response": "location",
"candidates": []
}
}question_kind 取值:
| 值 | 使用场景 |
|---|---|
missing_field |
创建或修改缺少必要字段 |
ambiguous_target |
多个日程与用户表达匹配 |
recurrence_scope |
周期日程修改或删除范围不明确 |
confirmation |
删除、批量修改等需要二次确认 |
服务端同时通过 TTS 音频播放 speech_text。客户端不需要 GUI 表单补齐字段,用户直接再次发送 voice.stream.start,并携带上一次的 conversation_id。
LLM 结构化输出只是一条候选业务命令。服务端必须依次完成账号权限校验、业务规则校验、幂等检查和云端数据库事务;只有事务提交成功后,才能发送 voice.command.result,且返回内容必须来自持久化后的数据库快照。
服务端发送:voice.command.result
{
"type": "voice.command.result",
"message_id": "msg_001",
"request_id": "req_voice_001",
"conversation_id": "conversation_001",
"payload": {
"operation": "create_schedule",
"status": "applied",
"schedule": {
"id": "schedule_001",
"schedule_type": "time",
"title": "开会",
"is_all_day": false,
"start_time": "2026-08-07T07:00:00Z",
"end_time": null,
"timezone": "Asia/Shanghai",
"recurrence_rule": null,
"location_name": "203",
"latitude": null,
"longitude": null,
"status": "active",
"revision": 13,
"updated_at": "2026-08-06T03:01:00Z"
},
"reminders": [
{
"id": "reminder_001",
"reminder_type": "before_start",
"trigger_at": null,
"offset_minutes": 15,
"strength": "medium",
"enabled": true,
"snoozed_until": null
}
],
"occurrence_overrides": []
}
}客户端处理顺序:验证 status=applied 和实体 revision,在同一个本地事务中应用日程与提醒快照,提交成功后注册或更新本地监听。事务成功后,客户端发送 message.ack 确认该消息已经应用。当前设备不再调用 HTTP 上传,也不需要重复拉取刚完成的写入;如果响应丢失或本地事务失败,客户端标记本地数据需要恢复,下一次满足恢复条件时通过 HTTP 全量快照重新恢复本地数据。
查询使用同一消息类型,operation=list_schedules:
{
"type": "voice.command.result",
"message_id": "msg_002",
"request_id": "req_voice_002",
"conversation_id": "conversation_002",
"payload": {
"operation": "list_schedules",
"status": "applied",
"schedules": [
{
"id": "schedule_001",
"title": "开会",
"schedule_type": "time",
"is_all_day": false,
"start_time": "2026-08-07T07:00:00Z",
"location_name": "203",
"status": "active"
}
]
}
}服务端根据查询结果生成简短 TTS 汇总。客户端可以展示列表,但不要求用户通过点击完成下一步业务操作。
语音解析后使用以下 operation,不新增一套独立 HTTP CRUD:
operation |
作用 | 关键处理 |
|---|---|---|
create_schedule |
创建一次性或周期性日程 | 缺失字段追问;默认值只补缺失字段 |
update_schedule |
修改日程字段 | 只修改用户明确提及的字段 |
delete_schedule |
删除日程 | 必须确认目标;周期日程必须确认范围 |
create_reminder |
创建提醒 | 校验每条日程最多两个提醒 |
list_reminders |
查询提醒 | 支持“第一个提醒”“地点提醒”等指代 |
update_reminder |
修改时间、地点或强度 | 保留未提及字段 |
delete_reminder |
删除提醒 | 必须确认目标提醒 |
snooze_reminder |
延期本轮提醒 | 未给时间时使用十分钟默认值;云端写入 snoozed_until 后返回所有设备 |
需要确认时先返回 status=needs_confirmation 和 voice.dialogue.question,用户的肯定或否定通过下一轮语音提交。确认之前不得修改本地或云端业务数据。
周期日程执行 delete_schedule 时,必须先通过语音确认 recurrence_scope:
recurrence_scope |
用户语义 | 数据处理 | 客户端处理 |
|---|---|---|---|
this_occurrence |
仅删除本次 | 在 schedule_occurrence_overrides 新增 action=cancel,原周期日程保持不变 |
跳过当前实例并计算下一次触发时间 |
this_and_future |
删除本次及以后 | 将原日程 RRULE 的 UNTIL 截止到当前实例之前最后一次保留的发生时间 |
保留历史实例,取消当前及后续监听 |
entire_series |
删除整个系列 | 将原日程设为 status=deleted 并写入 deleted_at,关联提醒停止生效 |
删除本地日程、提醒和监听任务 |
如果 this_and_future 选中的实例是周期系列第一次发生,则按 entire_series 处理。三种删除都必须在云端写入成功后返回最终结果,再由客户端更新 SQLite 和本地监听状态。
服务端在语音问题、命令结果摘要和本地提醒音频准备时调用 TTS。文本控制消息:voice.tts.start
{
"type": "voice.tts.start",
"conversation_id": "conversation_001",
"audio_id": "audio_001",
"payload": {
"format": "wav",
"sample_rate_hz": 24000,
"purpose": "dialogue_question",
"schedule_id": null,
"reminder_id": null,
"audio_version": null
}
}purpose 可为 dialogue_question、command_result 或 reminder。当 purpose=reminder 时,服务端必须带上 schedule_id、reminder_id 和 audio_version,客户端将音频缓存到对应提醒;缓存缺失或版本不匹配时,提醒执行模块使用本地兜底音频。随后发送音频 Binary Frame,结束时发送:
{
"type": "voice.tts.end",
"conversation_id": "conversation_001",
"audio_id": "audio_001"
}提醒 TTS 音频只用于 high 强度。本期文案只使用日程标题、开始时间和位置;预计通勤、路线、天气、关联日程、交互模式和停车位置均不进入当前 TTS 输入。TTS 生成失败时,客户端使用普通弹窗、短震动和本地提示音继续完成提醒。
对于需要确认处理结果的消息,发送方在消息外层提供唯一的 message_id;发送方可以是客户端,也可以是服务端。接收方使用不承载业务数据的 message.ack 回复:
{
"type": "message.ack",
"message_id": "msg_001",
"status": "applied"
}status 取值:
| 值 | 含义 |
|---|---|
received |
已收到并解析消息,但尚未完成业务应用 |
applied |
已完成本地事务或对应的协议处理 |
对于 voice.command.result 这类包含云端日程快照的消息,客户端只能在本地数据库事务提交成功后发送 status=applied。如果服务端已完成云端写入,但没有收到 ACK,不回滚云端数据;客户端下次登录后通过 HTTP 全量快照恢复本地数据。ACK 本身不需要再次确认,也不按音频 Binary Frame 逐帧发送 ACK。
{
"type": "voice.command.error",
"request_id": "req_voice_003",
"conversation_id": "conversation_003",
"ok": false,
"error": {
"code": "MISSING_REQUIRED_INFORMATION",
"message": "无法确定日程开始时间",
"retryable": true
}
}错误码至少包括:
| 错误码 | 含义 |
|---|---|
AUDIO_INVALID |
音频格式或音频流无效 |
SPEECH_RECOGNITION_FAILED |
ASR 失败 |
UNDERSTANDING_FAILED |
LLM 无法形成有效意图 |
云端数据库使用 PostgreSQL,客户端数据库使用 SQLite。两端表结构保持可同步字段一致,客户端额外保存少量本地运行状态。云端使用 PostgreSQL 原生类型,客户端使用 SQLite 类型亲和性保存对应数据。
| 数据含义 | PostgreSQL | SQLite | 说明 |
|---|---|---|---|
| ID 和文本 | VARCHAR(n) |
TEXT |
统一使用带业务前缀的字符串 ID |
| 带时区时间 | TIMESTAMPTZ |
TEXT |
SQLite 保存 UTC 的 ISO-8601 文本 |
| 经纬度 | NUMERIC(9,6) |
REAL |
客户端用于距离计算 |
| 整数和版本 |
INTEGER、BIGINT
|
INTEGER |
SQLite 使用 INTEGER 亲和类型 |
| 布尔值 | BOOLEAN |
INTEGER |
SQLite 使用 0 和 1
|
| 字段 | 类型 | 约束 | 用途 |
|---|---|---|---|
id |
varchar(64) | PK | 账号 ID |
login_identifier |
varchar(255) | UNIQUE, NOT NULL | ==登录标识,具体可为邮箱或手机号(用户名)== |
password_hash |
varchar(255) | NOT NULL | 密码哈希,不保存明文密码 |
created_at |
timestamptz | NOT NULL | 创建时间 |
updated_at |
timestamptz | NOT NULL | 账号更新时间 |
| 字段 | 类型 | 约束 | 用途 |
|---|---|---|---|
id |
varchar(64) | PK | 日程 ID,客户端和云端一致 |
account_id |
varchar(64) | FK, NOT NULL | 数据归属账号 |
schedule_type |
varchar(16) | NOT NULL |
time 或 location
|
title |
varchar(255) | NOT NULL | 日程标题 |
is_all_day |
boolean | NOT NULL | 是否为全天日程;默认 false
|
start_time |
timestamptz | NULL | 时间日程开始时间 |
end_time |
timestamptz | NULL | 时间日程结束时间 |
timezone |
varchar(64) | NOT NULL | 周期展开和语音表达使用的 IANA 时区 |
recurrence_rule |
varchar(512) | NULL | RFC 5545 RRULE;NULL 表示一次性 |
location_name |
varchar(255) | NULL | 地点名称 |
latitude |
numeric(9,6) | NULL | 日程地点纬度 |
longitude |
numeric(9,6) | NULL | 日程地点经度 |
status |
varchar(16) | NOT NULL |
active 或 deleted
|
revision |
bigint | NOT NULL | 云端日程版本,用于识别快照数据的新旧 |
created_at |
timestamptz | NOT NULL | 创建时间 |
updated_at |
timestamptz | NOT NULL | 最后修改时间 |
deleted_at |
timestamptz | NULL | 软删除时间,供同步下发删除变更 |
| 日程类型(单次,周期) |
约束:schedule_type=time 必须有 start_time;is_all_day=true 时必须为时间日程,必须有表示排他结束边界的 end_time,默认 before_start 提醒使用 offset_minutes=900;schedule_type=location 必须有有效经纬度且 is_all_day=false;recurrence_rule 只允许用于时间日程;status=deleted 时保留整行一段同步窗口,不能立即物理删除。
| 字段 | 类型 | 约束 | 用途 |
|---|---|---|---|
id |
varchar(64) | PK | 提醒 ID |
schedule_id |
varchar(64) | FK, NOT NULL | 所属日程 |
==reminder_type== |
==varchar(32)== | ==NOT NULL== | ==at_time、before_start、arrive_location、return_to_recorded_location== |
trigger_at |
timestamptz | NULL |
at_time 的绝对提醒时间 |
offset_minutes |
integer | NULL |
before_start 的开始前偏移量 |
target_latitude |
numeric(9,6) | NULL | 返回记录地点的纬度;到达日程地点时复用日程坐标 |
target_longitude |
numeric(9,6) | NULL | 返回记录地点的经度 |
strength |
varchar(16) | NOT NULL |
low、medium、high
|
enabled |
boolean | NOT NULL | 是否启用 |
snoozed_until |
timestamptz | NULL | 用户主动延期到期时间;NULL 表示未延期 |
created_at |
timestamptz | NOT NULL | 创建时间 |
updated_at |
timestamptz | NOT NULL | 最后修改时间 |
约束:同一个 schedule_id 最多两个提醒;at_time 必须有 trigger_at;before_start 必须有 offset_minutes;arrive_location 必须关联有坐标的日程;return_to_recorded_location 必须有目标坐标;提醒强度必须为三级枚举。提醒增删改必须在同一事务中递增所属日程的 revision,同步时始终返回日程及其完整提醒集合,因此提醒不需要独立版本和软删除字段。snoozed_until 是用户主动处置结果,通过语音命令写入云端;到期后按时间比较恢复正常监听,不需要额外状态字段。
该表只记录周期日程中被单独修改或删除的发生实例,不保存正常展开的所有实例。周期日程正常触发、用户确认提醒或延期提醒时,都不会写入该表。
| 字段 | 类型 | 约束 | 用途 |
|---|---|---|---|
id |
varchar(64) | PK | 例外记录 ID |
schedule_id |
varchar(64) | FK, NOT NULL | 原周期日程 ID |
occurrence_start |
timestamptz | NOT NULL | 被处理实例原本的开始时间 |
action |
varchar(16) | NOT NULL |
cancel 或 replace
|
replacement_schedule_id |
varchar(64) | FK, NULL |
replace 时指向新建的一次性日程 |
created_at |
timestamptz | NOT NULL | 创建时间 |
updated_at |
timestamptz | NOT NULL | 更新时间 |
约束:同一 schedule_id + occurrence_start 只能存在一条例外;action=replace 时必须有 replacement_schedule_id,action=cancel 时必须为空。新增或修改例外时递增原周期日程的 revision,不为例外单独维护同步版本。
客户端镜像云端 schedules 的业务字段,并增加本地同步所需字段。
| 字段 | 类型 | 用途 |
|---|---|---|
id |
text PK | 与云端 schedules.id 相同 |
account_id |
text | 当前设备上的账号隔离 |
schedule_type |
text |
time 或 location
|
title |
text | 日程标题 |
is_all_day |
integer | 是否为全天日程,0 或 1
|
start_time |
text NULL | UTC 时间 |
end_time |
text NULL | UTC 时间 |
timezone |
text | IANA 时区 |
recurrence_rule |
text NULL | 周期规则 |
location_name |
text NULL | 地点名称 |
latitude |
real NULL | 纬度 |
longitude |
real NULL | 经度 |
status |
text |
active 或 deleted
|
cloud_revision |
integer | 最近应用的云端版本 |
updated_at |
text | 云端业务更新时间 |
客户端不生成服务端不存在的新日程。在线语音写入成功后,客户端只应用 WebSocket 返回的云端确认快照;满足恢复条件时应用 HTTP 全量快照。离线状态只读取 active 数据,不允许修改这张表中的业务字段。
客户端镜像云端提醒配置,并保存监听运行状态;运行状态不回写为日程业务字段。
| 字段 | 类型 | 用途 |
|---|---|---|
id |
text PK | 与云端提醒 ID 相同 |
schedule_id |
text | 所属本地日程 |
reminder_type |
text | 四种提醒类型 |
trigger_at |
text NULL | 绝对时间提醒点 |
offset_minutes |
integer NULL | 开始前偏移量 |
==target_latitude== |
==real NULL== | ==返回地点纬度== |
==target_longitude== |
==real NULL== | ==返回地点经度== |
strength |
text |
low、medium、high
|
enabled |
integer | 是否启用 |
next_trigger_at |
text NULL | 时间提醒下一触发点 |
snoozed_until |
text NULL | 云端确认并同步的用户延期到期时间 |
geofence_armed |
integer | 返回地点是否已经完成离开布防 |
disposition_state |
text NULL |
confirmed、snoozed;本轮提醒尚未处置时为空 |
disposition_updated_at |
text NULL | 最近一次本地处置时间 |
sync_status |
text |
pending 或 synced;仅表示处置状态是否已获得服务端确认 |
updated_at |
text | 本地配置更新时间 |
reminder_type、时间、地点、强度、enabled 和 snoozed_until 属于云端确认的共享业务状态;next_trigger_at 和 geofence_armed 属于当前设备的监听运行状态。disposition_state 是本地立即生效的提醒处置状态,通过 HTTP 异步同步云端;sync_status 只有在服务端成功响应后才变为 synced,不建立单独事件表。
| 字段 | 类型 | 用途 |
|---|---|---|
id |
text PK | 与云端例外记录相同 |
schedule_id |
text | 原周期日程 ID |
occurrence_start |
text | 被处理实例原本的 UTC 开始时间 |
action |
text |
cancel 或 replace
|
replacement_schedule_id |
text NULL | 替换后的一次性日程 ID |
客户端展开周期日程时先计算 RRULE,再排除 cancel 和 replace 对应的原实例;replace 指向的一次性日程按普通日程展示和监听。
用户:“每周一上午九点项目例会。”
↓
客户端上传音频
↓
ASR 转写
↓
LLM 识别 create_schedule + recurrence_rule
↓
如果缺少时区等信息,服务端通过 TTS 追问
↓
业务层校验并在云端事务中写入日程和默认提醒
↓
服务端返回持久化后的快照和 revision
↓
客户端应用快照到本地数据库并注册本地监听
用户:“把项目例会改到十点。”
↓
LLM 根据当前对话和服务端从云端数据库查询到的日程匹配目标
↓
如果存在多个例会,语音追问
↓
服务端询问修改范围:本次、后续全部或整个系列
↓
用户语音确认范围
↓
业务层只修改时间,保留标题、地点和提醒等未提及字段
↓
云端事务更新成功并返回新 revision
↓
当前客户端应用新快照并重算本地监听
↓
换设备或本地数据需要恢复时,通过 HTTP 全量快照恢复本地数据
用户:“我回到刚才记录的地方时提醒我取快递。”
↓
客户端提供当前位置给语音会话,服务端生成 return_to_recorded_location 提醒
↓
业务层校验并把提醒配置写入云端
↓
客户端应用云端确认快照,并在本地设置 geofence_armed = false
↓
客户端自行检测离开范围并设置 geofence_armed = true
↓
客户端自行检测再次进入范围
↓
根据提醒强度执行系统通知,或执行普通弹窗、短震动和 TTS
- 账号登录后,客户端使用带过期时间的 JWT 访问 HTTP 和 WebSocket;
device_id只用于标识 WebSocket 客户端,不对应服务端登录会话。 - 日程和提醒业务变更只有在云端事务提交成功并返回
status=applied后才算成功。 - 本地监听使用本地数据库快照;云端同步不会在提醒触发时阻塞本地提醒。
- 客户端在本地事务中完整应用 WebSocket 快照或登录后的 HTTP 全量快照;快照应用成功后才重新注册本地监听。
- 用户确认期间目标日程发生变化时,服务端返回
STALE_CONFIRMATION并重新进行语音确认,不自动套用旧命令。 - 断网时只读本地日程和提醒配置,不允许语音变更,不调用 ASR、LLM 和 TTS。
- 设备完成提醒送达后记录
delivered,不自动改变日程状态。 - 用户确认或延期后,客户端立即更新
disposition_state;延期写入snoozed_until,再通过 HTTP 异步同步云端;下一触发时间和围栏布防只保存在本地。 - TTS 音频和语音转写不作为云端日程主数据保存;是否缓存音频由客户端缓存策略决定。
- WebSocket 只承载已认证会话;访问令牌不写入普通日志。
- 地点提醒只保存必要的目标坐标,不保存连续位置历史。
- 账号创建或登录和无状态 JWT 鉴权。
- WebSocket 音频流、ASR、LLM 创建日程。
- 缺失字段追问和语音确认。
- 云端日程/提醒事务写入和版本生成。
- 本地日程和提醒数据库,以及云端快照应用。
- 一次性时间提醒、到达地点提醒和三档强度送达。
- HTTP 登录后的全量快照恢复。
- 语音查询、修改、删除。
- 周期日程和修改/删除范围确认。
- 多目标匹配、指代消解。
- 最多两个提醒及提醒配置修改。
- 返回记录地点提醒和延期。
- TTS 个性化缓存和失效更新。
- 旧确认失效后的语音恢复流程。
- 全天日程的多日范围、跨时区和更复杂提醒策略评估与补充。
- 评估通勤、路线、天气等上下文是否真正产生产品价值;未形成明确触发场景前不接入。
| 目标 | 验收标准 |
|---|---|
| 语音优先 | 创建、查询、修改、删除和提醒配置均可通过语音完成,不依赖 GUI 表单 |
| 对话完整 | 缺失信息、多个匹配、周期范围和删除确认均能通过语音追问完成 |
| 本地提醒 | 断网或 WebSocket 断开时,已同步日程仍能按时间或地点触发 |
| 云端确认写入 | LLM 输出不能直接落库;业务校验和云端事务成功后才向客户端返回 applied
|
| 数据同步 | 当前设备可应用 WebSocket 云端快照,登录或本地数据丢失后可通过 HTTP 全量快照恢复 |
| 提醒边界 | 每条日程最多两个提醒,地点返回提醒不会变成独立离开提醒 |
| 强度可控 | 低、中、高三档送达方式可区分,TTS 失败不阻塞高强度提醒 |
| 离线边界 | 离线只读和提醒,不支持语音操作、日程修改或 TTS |
| 数据最小化 | 不保存位置历史、对话历史、TTS 历史和智能规则决策历史 |
后端目录以依赖方向为约束:入口负责组装各层;网关只依赖业务用例和智能能力端口;业务与智能层依赖抽象端口;数据层实现数据库端口;基础设施层实现第三方接口和运行时端口。业务层和智能层不得反向依赖 WebSocket、HTTP、SQLAlchemy 或第三方 SDK。
backend/src/timeflow/
├── business/
│ ├── accounts/
│ ├── calendar/
│ └── sync/
├── data/
│ ├── models/
│ ├── repositories/
│ └── migrations/
├── gateway/
│ ├── http/
│ └── websocket/
├── infrastructure/
│ ├── config/
│ ├── database/
│ ├── security/
│ ├── clock/
│ ├── logging/
│ └── external/
│ ├── asr/
│ ├── llm/
│ └── tts/
├── intelligence/
│ ├── conversation/
│ ├── transcription/
│ ├── command_parser/
│ └── speech_synthesis/
└── main.py
**职责:**承载可测试、可复用的业务规则和用例编排==。包括账号用例、日程数据管理用例和同步用例==(“用例”表述不准确)。
允许包含:
- 用例服务,例如创建日程、查询日程、修改周期范围、执行已确认删除、管理提醒配置、同步提醒处置和查询全量快照。
- 领域实体和值对象,例如
Schedule、Reminder、ScheduleAggregate、RecurrenceRule、ReminderStrength。 - 业务命令、结果 DTO 和端口接口,例如
ScheduleRepository、ReminderRepository。 - 必填字段、周期日程范围、提醒数量上限、提醒类型组合等业务校验。
禁止包含:
- FastAPI 路由、WebSocket 连接对象和 JSON 序列化代码。
- SQLAlchemy Model、数据库 Session、文件系统和环境变量读取。
- ==阿里云、OpenAI 等第三方 SDK 调用(业务逻辑可以调用三方SDK,只调用必须的api,AI相关内容不应该在这里调用)。==
- 具体的 ASR、LLM、TTS Prompt 和音频协议。
- 时间轮询、地理定位采集和客户端提醒执行。
**职责:**实现 PostgreSQL 云端数据库的持久化模型、Repository 和数据库迁移。
允许包含:
- SQLAlchemy ORM Model 和 PostgreSQL 字段映射。
-
AccountRepository、ScheduleRepository、ReminderRepository的实现。 - 全量快照查询、版本和软删除记录读取。
- Alembic 迁移脚本。
禁止包含:
- 业务规则判断,例如“缺失地点就追问”。
- WebSocket/HTTP 请求模型。
- LLM 输出解析、Prompt 拼装和第三方网络请求。
- 客户端 SQLite 数据库、客户端定位和客户端提醒状态。
**职责:**将 HTTP 和 WebSocket 等外部网络协议适配为系统内部可调用的请求和消息。网关是网络输入输出边界,不拥有业务决策,也不直接适配第三方模型服务。
允许包含:
- HTTP 路由、认证依赖、请求/响应模型和错误映射。
- WebSocket 连接生命周期、文本帧与 Binary Frame 接收发送。
- 网络协议字段到内部命令和结果的转换。
禁止包含:
- 日程创建和提醒配置业务规则。
- 直接拼接 SQL 或直接修改 ORM Model。
- 在 Handler 中实现多轮对话、重复日程范围或提醒数量判断。
- 时间监听、地点监听和本地提醒送达。
**职责:**提供运行时通用能力和应用装配所需的技术组件,不表达产品业务。
允许包含:
- 配置加载、环境变量、日志、异常追踪。
- 数据库 Engine、Session 工厂和事务管理。
- 密码哈希、令牌签发与校验、随机 ID 和时间时钟。
- HTTP/WebSocket 客户端连接池、重试和超时的通用封装。
- 阿里云 ASR、OpenAI LLM、阿里云 TTS 等第三方接口适配器。
- 任务执行器、资源生命周期和健康检查。
禁止包含:
- 创建日程、删除确认和提醒强度等业务规则。
- ASR/LLM/TTS Prompt 和模型选择决策。
- 任何具体的 WebSocket 消息处理流程。
- 直接向客户端发送产品消息。
infrastructure/external/ 下的第三方适配器只实现 intelligence/ 定义的调用端口,负责供应商协议、鉴权、超时、重试和外部错误转换,不包含日程业务规则,也不直接处理客户端 WebSocket 消息。
**职责:**将语音和对话上下文转化为可供业务层执行的结构化意图,并把业务结果转化为语音输出。该层只负责智能理解和生成,不负责业务持久化。
允许包含:
- 对话轮次状态、当前问题、候选日程和确认上下文。
- ASR 文本标准化和最终转写结果处理。
- LLM 日历意图解析:创建、查询、修改、删除日程和提醒。
- 缺失字段识别、追问生成、指代消解和多结果消歧。
- TTS 文案生成和 TTS 调用端口编排。
- 受约束的结构化输出 Schema。
禁止包含:
- 直接保存日程和提醒到数据库。
- 自行决定业务默认值之外的日程持久化规则。
- 直接执行账号鉴权和同步事务。
- 决定本地监听何时触发或替用户完成提醒处置。
main.py 只负责创建应用和依赖注入:读取配置、创建 PostgreSQL 数据库会话、初始化 Repository、第三方客户端、业务用例和 HTTP/WebSocket 路由。它不实现业务逻辑,不解析 LLM 结果,不执行 SQL。