Repository navigation
Architecture interface design
版本:v4.0 日期:2026-08-26 适用范围:语音驱动的日程创建、提醒配置、本地提醒与云端备份恢复 说明:本文档在 v3.10 基础上,根据当前代码实现反向梳理更新。相较 v3.10 的主要结构性变化:语音交互从单一 ASR→LLM→TTS 流水线升级为「实时端到端 / 组合式」双模式;供应商从「阿里云 ASR / OpenAI LLM / 阿里云 TTS」更新为「Qwen3-ASR / OpenAI 兼容 LLM / Qwen-Audio-TTS / Qwen-Audio 实时」;客户端提醒去掉原生 geofencing,改为后台定位守卫轮询。
TimeFlow 是一款以语音为核心交互方式的时间管理 App。用户通过语音创建、查询、修改和删除时间日程、地点日程及其提醒配置;当信息缺失、存在多个匹配结果、涉及重复日程范围或需要二次确认时,系统通过语音反问和语音输出继续完成对话。日历视图只负责展示和状态反馈,不提供日程业务编辑表单。
系统将「云端确认写入」和「客户端运行执行」分离:语音操作经过多轮补全和用户确认后,由服务端业务层校验并写入云端数据库;当前客户端直接应用服务端返回的最终快照,首次安装、换设备或需要恢复时再通过 HTTP 全量同步本地数据。客户端本地数据库负责日常读取,时间和地点监听全部由客户端实现。断网时客户端可以查看本地数据并继续执行已经注册的提醒,但不能创建、修改或删除日程,也不能执行依赖服务端的 ASR、LLM 和 TTS。
语音智能层提供两种后端实现(由 TIMEFLOW_VOICE_AGENT_MODE 切换),两者对客户端暴露同一套线上协议:
-
模式一(实时端到端,
mode=1):单个 Qwen-Audio 实时模型直接完成「音频进 → 语音出 + 函数调用」,服务端仅做会话预算管理与工具调用转译。 -
模式二(组合式,
mode=2):三段式流水线 Qwen3-ASR → OpenAI 兼容 LLM(ReAct 对话循环)→ Qwen-Audio-TTS,由ComposedVoiceAgent编排,并通过语音管线做分段与截断控制。
| 原则 | 设计结论 |
|---|---|
| 用户真正要完成的是管理时间 | 日程、提醒、查询和处置是核心业务;日历只是展示载体 |
| 语音是主要交互方式 | 所有核心业务操作走语音,不维护重复的 GUI 表单流程 |
| 提醒必须不依赖云端实时在线 | 已同步日程由客户端本地监听和本地送达 |
| 云端服务集中处理高价值智能能力 | ASR、LLM、TTS 在服务端统一调用和升级 |
| 业务写入必须有唯一权威 | 云端数据库负责确认后的日程与提醒写入,本地只应用云端确认快照 |
| 数据模型应服务于当前功能 | 每条日程最多一个提醒,提醒配置作为日程属性保存;不持久化登录会话,不提前引入画像、导航或复杂历史 |
| 复杂情况通过对话解决 | 缺失信息、目标歧义、重复范围和危险操作由语音追问确认 |
| 语音 Agent 实现可替换 | 双模式(实时 / 组合式)共用同一套线上协议与工具契约,客户端对模式无感知 |
| 要做 | 不做 |
|---|---|
| 一次性、周期性和全天时间日程 | 独立的离线智能创建 |
| 地点日程和返回记录地点提醒 | 服务端持续监听所有日程 |
| 每条日程最多一个时间或地点提醒 | 独立的「离开地点时提醒」 |
| 低、中、高三级提醒强度 | 本期智能提醒规则引擎 |
| 语音创建、查询、修改、删除和确认 | GUI 表单创建和编辑 |
| 地点搜索与逆地理编码(服务端) | 客户端独立维护 POI 数据 |
| 日程自动分类(工作/学习等八类) | 用户手工分类工作流 |
| 账号、云端确认写入和单设备全量恢复 | 多设备实时同步、增量变更和过早引入天气、路线、通勤等上下文 |
| 本地时间与地点监听 | 本地 ASR、LLM、TTS |
模块按完整用户能力划分。一个模块必须能够独立说明输入、处理和输出,不以某一个 DTO、WebSocket 消息或第三方 SDK 作为模块边界。第三方接口适配属于基础设施层,网关层只负责对外网络协议。
| 模块 | 包含的小功能 | 输入 | 输出 | 边界 |
|---|---|---|---|---|
| 语音交互模块 | 麦克风采集、WebSocket 音频流、对话上下文、问题播放、TTS 播放、语音结果接收(按讲即止 / 连续两种模式) | 用户语音、服务端语音消息 | 音频流、对话回答、业务结果 | 不执行 ASR、LLM、TTS 模型,不直接写云端数据库 |
| 本地日程模块 | 本地日程及其单一提醒配置存储、日历展示、快照应用、周期日程计算所需的本地数据读取 | 语音操作结果、同步结果 | 日程列表、日程详情、提醒配置、本地快照 | 不提供 GUI 编辑;每条日程最多保存一个提醒;不自行生成未被服务端确认的云端日程 |
| 本地监听与提醒执行模块 | 时间监听、地点距离计算、返回地点布防、延期、强度分级、弹窗、震动、屏幕展示和音频播放 | 本地日程、提醒配置、当前时间、定位数据、用户处置 | 本地提醒送达、延期结果、送达状态 | 不依赖服务端到点消息;提醒业务规则由客户端实现 |
| 账号与同步模块 | 账号创建或登录、访问凭证保存、首次安装或需要恢复时应用全量快照、提醒处置状态上传、离线只读状态 | HTTP 账号响应、HTTP 全量快照响应、提醒状态响应、WebSocket 云端确认快照 | 本地数据恢复、处置状态同步、网络状态 | 不执行日程业务语义;不在离线时创建或修改日程 |
| 模块 | 包含的小功能 | 输入 | 输出 | 边界 |
|---|---|---|---|---|
| 账号与同步服务 | 账号创建或登录、JWT 签发与校验、首次安装或需要恢复时的全量快照查询和提醒处置状态同步 | HTTP 认证请求、提醒处置状态 | 访问令牌、云端完整快照、状态同步结果 | 不维护登录会话;不处理语音语义;不负责时间和地点监听 |
| 语音交互模块 | 接收和发送语音内容,处理音频流、ASR 转写和 TTS 音频输出 | 音频流、ASR/TTS 调用请求 | 转写文本、TTS 音频、语音传输错误 | 不负责多轮对话、意图理解、业务确认和日程持久化 |
| 语音 Agent 编排模块 | 按模式编排语音工作流:模式一转发音频到 Qwen-Audio 实时模型并转译工具调用;模式二串联 ASR→LLM→TTS 并维护会话 | 音频流、ASR 转写、LLM 工具调用结果、TTS 音频 | 文本回复、语音问题、业务命令结果、TTS 音频 | 不直接持久化日程;不决定提醒触发;模式差异封装在本层,对网关透明 |
| 对话管理模块 | 管理会话上下文,调用 LLM 完成意图理解、多轮追问、信息补全、指代消解和二次确认,输出结构化业务命令 | ASR 转写文本、会话上下文、日程查询结果、用户确认语音 | 对话问题、已确认结构化业务命令、对话错误 | 不接收原始音频;不直接调用第三方语音接口;不直接持久化日程 |
| 日程数据管理模块 | 管理用户确认后的日程及其单一提醒配置,提供查询、创建、修改和删除能力,并做日程自动分类 | 对话管理模块输出的查询请求和已确认结构化操作 | 包含提醒字段的日程数据、查询结果 | 不负责语音内容、多轮对话、本地监听和提醒送达 |
「语音交互模块」「语音 Agent 编排模块」和「对话管理模块」是相邻的后端产品模块。它们内部仍保持技术职责分离:gateway/ 负责 WebSocket 会话、消息路由和协议转换;intelligence/ 负责双模式智能编排、语音对话工作流、提示词和结构化结果处理;infrastructure/ 负责 Qwen3-ASR、OpenAI 兼容 LLM、Qwen-Audio 实时、Qwen-Audio-TTS、腾讯地图等第三方接口适配。产品模块拆分不表示把协议处理和模型调用代码写在产品模块目录中。
| 能力 | 前端负责 | 后端负责 |
|---|---|---|
| 语音输入 | 录音、分片、上传 | 接收音频并完成 ASR(模式二)或端到端识别(模式一) |
| 语义理解 | 保存对话 UI 状态、播放问题的音频 | 调用 LLM、补全字段、追问和消歧 |
| 日程变更 | 应用云端确认快照到本地数据库 | 校验业务命令并在云端事务写入 |
| 提醒监听 | 时间、地点、返回地点和延期逻辑 | 不参与到点判断 |
| 提醒送达 | TTS 播放、屏幕、震动、强度策略 | 生成 TTS 音频并提供缓存/同步来源 |
| 数据可靠性 | 本地运行读取、离线提醒、首次安装或需要恢复时全量恢复 | 云端写入权威和账号隔离 |
这是相较 v3.10 最重要的新增章节。服务端在 main.py 组装阶段依据 settings.voice_agent_mode 选择语音后端,两模式都实现同一 Agent 端口(handle_audio)并通过同一 ResultSink 输出同一套客户端消息,因此网关层和客户端对模式无感知。
| 维度 | 模式一:实时端到端(mode=1) | 模式二:组合式(mode=2) |
|---|---|---|
| 编排类 | RealtimeAgent |
ComposedVoiceAgent |
| 核心模型 | 阿里云 Qwen-Audio 实时(qwen-audio-3.0-realtime-plus) |
Qwen3-ASR → OpenAI 兼容 LLM → Qwen-Audio-TTS 三段 |
| 音频链路 | 16 kHz 音频进,24 kHz PCM 语音出(单会话完成 ASR+对话+TTS) | 16 kHz ASR 进 → LLM 文本 → 24 kHz PCM TTS 出 |
| 会话状态 | 按 (account_id, conversation_id) 持有 _Held 会话,轮次/年龄预算控制 |
按 session_id 持有 ComposedSession,随 WebSocket 断开关闭 |
| 工具调用 | 模型函数调用经 ToolBox 同步转译并回填 send_tool_result
|
LLM ReAct 循环,每轮最多一次工具调用,max_tool_rounds 兜底 |
| 供应商 |
QwenAudioSession(infrastructure/external/realtime/) |
QwenRealtimeAsr + OpenAICompatibleLlm + QwenAudioTts
|
| 兜底 | 未配置时开发环境用 FakeAgent
|
无 |
RealtimeAgent.handle_audio 为每个 (account_id, conversation_id) 打开一个 QwenAudioSession,两条任务并行:一条把音频分片 send_audio 后 finish_input,另一条 pump 消费模型事件。_Turn 观察者把模型事件翻译成 ResultSink 调用:
- 模型转写 →
deliver_transcript(时长按_INPUT_BYTES_PER_MS=32估算)。 - 模型语音 → 按 reply 队列
deliver_audio。 - 模型函数调用 →
ToolBox.run(同步完成业务写库)→deliver_result+deliver_question→send_tool_result(..., respond=not ends_conversation)。
会话预算:SESSION_MAX_TURNS = 40 或 SESSION_MAX_AGE_SECONDS = 240.0 任一到顶即判定 _spent,每次 handle_audio 开头扫描并丢弃过期会话。语音模式(voice_mode)变化时强制重建会话。
ComposedVoiceAgent.handle_audio 在连续模式(voice_mode == "continuous")下进入 _run_continuous 主循环,否则单次转写即执行。
连续模式数据流:
- 打开 ASR 流,
pump()任务消费 ASR 事件;TranscriptCompleted入队,SpeechStarted记录 barge-in 时间并置barge_in。 - 主循环取出最终转写文本,启动
_act_on_transcript:投递Transcript→ 取/建会话 →Agent.run_turn产出AgentEvent流 →_deliver_agent_events分发给文本与语音两条通路。 - 一轮 turn 与
barge_in竞速:若用户在音频播放中途抢话,投递AudioCanceled并取消当前 turn(否则让 turn 完成)。 - 模式二本轮回复结束后,会话继续挂在 WebSocket 上等下一句,直到客户端断开触发
close_session。
_deliver_agent_events 用两个有界队列(maxsize=8)并行扇出:producer 迭代 AgentEvent;delivery 通路把 AgentTextDelta/AgentQuestion/AgentCompleted 转成 deliver_reply_text/deliver_question;speaking 通路把事件喂给 SpeechPipeline 并转成 deliver_audio。
两模式共用同一套工具契约(尽管 schema 表达重复为 OpenAI JSON-schema 与 {"type":"function"} 两套,参数映射也各有一份,注释互相引用):
| 工具名 | 作用 | 备注 |
|---|---|---|
schedule_create / schedule_query / schedule_update / schedule_delete
|
日程 CRUD | 由 build_schedule_tools(模式二)与 ToolBox(模式一)分别映射到 ScheduleAgentService
|
location_search |
地点/POI 搜索 | 基于腾讯地图,real/lazy/unavailable 三种解析 |
request_user_input |
反问用户 | 严格 schema:question_kind 四枚举 + speech_text + required_response + candidates
|
end_conversation |
结束对话 | 触发 voice.session.end
|
四种问题类型 QuestionKind:missing_field、ambiguous_target、recurrence_scope、confirmation。
所有工具最终都调用同一个业务边界 ScheduleAgentService(business/calendar/service.py),在 worker 线程(asyncio.to_thread)执行,保证业务层不依赖 async 框架、不接触任何第三方 SDK。
模式二的 TTS 通过 SpeechPipeline 把 AgentEvent 转成有界语音分片:
-
TextSegmenter以强边界(。!?;\n.?!;)与弱边界(,、:,:)增量切分,target_length=20、max_length=120。 -
SpeechPipeline默认max_total_characters=100,单轮回复累计超限即截断并追加_TRUNCATION_NOTICE = ",后面省略",保证一旦截断必能播报省略提示,避免长回复让客户端 TTS 卡死。 - 输出事件:
SpeechAudioStarted("pcm", 24000, purpose, speech_text)→SpeechAudioChunk× N →SpeechAudioCompleted。
SpeechPurpose 目前实际使用 dialogue_question 与 command_result 两类(reminder 为保留枚举)。
| 机制 | 位置 | 行为 |
|---|---|---|
| 工具轮数上限 |
conversation/agent.py,max_tool_rounds=4(TIMEFLOW_AGENT_MAX_TOOL_ROUNDS) |
第 5 次工具调用抛 AgentToolRoundLimitError,防止批量操作失控 |
| 轮数上限语音兜底 | composed/agent.py |
捕获 AgentToolRoundLimitError,向用户播报「一次操作太多了,请拆成几次再试。」而非静默 |
| 删除确认守卫 |
conversation/agent.py _is_affirmative + _delete_authorized
|
否定词守卫(不/别/取消/算了/误会),仅拒绝明确否定;只有刚答完 confirmation(肯定)或 recurrence_scope 才授权删除 |
| 噪音输入处理 |
conversation/agent.py SYSTEM_PROMPT |
无动作、无时间、非答问的孤立输入归为噪音,回「抱歉,我没听清,能再说一遍吗?」,不擅自创建日程 |
| 成功误报纠正 |
conversation/agent.py _claims_success
|
模型未调工具却声称「已创建/已删除」时注入纠正消息强制重试 |
| 类型 | 触发条件 | 必要数据 |
|---|---|---|
at_time |
到达指定绝对时间 | reminder_trigger_at |
before_start |
日程开始边界前指定分钟数 | 时间日程、reminder_offset_minutes
|
arrive_location |
进入日程目标地点范围 | 日程经纬度 |
return_to_recorded_location |
创建时记录当前位置,离开范围后再次进入 | 记录点经纬度 |
不提供独立的离开地点提醒。返回记录地点提醒的客户端流程为:创建时保存当前位置 → 当前位置在记录范围内不触发 → 检测到离开范围 geofence_armed=true → 再次进入触发并取消本轮监听。
at_time 表示明确的绝对时间点。周期日程如果需要每次发生都提醒,应使用 before_start。全天日程默认 reminder_offset_minutes=900(前一天本地 09:00)。默认提醒时间已过时不补发,由语音交互询问用户是否重新指定。
| 强度 | 送达方式 | 适用语义 |
|---|---|---|
low |
系统通知 | 非紧急提醒 |
medium |
普通弹窗 + 短震动 | 默认提醒强度 |
high |
普通弹窗 + 短震动 + TTS | 用户明确要求高强度提醒 |
low 和 medium 不依赖 TTS。high 的 TTS 音频不可用时,使用普通弹窗、短震动和本地提示音完成提醒。送达计划由 resolveStrengthDeliveryPlan 决定(通知/弹窗/震动/音频组合 + AlarmSoundTier)。
相较 v3.10,客户端提醒监听有重要变化:原生 geofencing 已移除,地点触发改为「后台定位守卫前台服务 + 轮询 + 纯函数地理围栏状态机」。
三条触发路径:
-
时间触发(
at_time/before_start):原生闹钟TimeflowAlarm(AlarmSchedulerPort.schedule)为主,JS 侧IntervalTimeListener(30 s)兜底;兜底会跳过已装原生闹钟的日程,避免重复响铃。 -
地点触发(
arrive_location/return_to_recorded_location):ReminderGuardCoordinator复用expo-location的startLocationUpdatesAsync({ foregroundService })常驻进程(Doze 豁免),按接近边界的距离把轮询间隔在 15 s–300 s 间插值;每次心跳喂给LocalReminderApplication.handleLocation→applyLocationSample→evaluateGeofence状态机(geofence_armed=false → armed → triggered)。DEFAULT_GEOFENCE_RADIUS_METERS=400。 -
恢复兜底:
runStuckPendingRescue(前台)/runStuckPendingPass(无前台时 headless)重新呈现停留pending超过 2 分钟的提醒;runTimeFallbackPass覆盖原生闹钟未装的情况;hydrateNativeDispositions冷启动回放原生处置。
关键组件:
| 组件 | 位置 | 职责 |
|---|---|---|
evaluateGeofence / resolveWatchMode / distanceMeters
|
features/reminder/domain/geofence.ts |
纯函数地理围栏状态机与哈弗辛距离,arrive/return 两种观察模式 |
ExpoLocationMonitor |
infrastructure/location/ |
实现 LocationMonitorPort + LocationProvider,只维护被观察点集合并在 watch/rebuild 时取一次样本做 armed 种子,不判断 enter/exit |
ReminderGuardCoordinator |
features/reminder/application/ |
JS 侧前台服务协调,区分 absent/foreground/degraded/unknown 注册状态并修复 degraded |
reminderGuardTask |
infrastructure/location/ |
TaskManager 后台任务(timeflow-reminder-guard),唤醒时分发样本、headless 兜底、重算轮询间隔 |
移除原生 geofencing 的原因(代码注释记录):原生回调伪造坐标(enter→中心、exit→约 1.1 km 外)再重跑状态机,没有额外价值;每次刷新重注册并触发虚假 exit;在守卫前台服务已轮询的前提下没有省电收益。代价是若系统杀掉进程 + 前台服务,地点提醒要等下次启动才可用。
| 状态 | 含义 |
|---|---|
pending |
已配置,等待条件满足 |
confirmed |
用户确认已处理本轮提醒,不表示日程业务完成 |
snoozed |
用户选择延期,或提醒超时未操作,等待 snoozed_until
|
用户没有指定延期时间时,默认延期十分钟(DEFAULT_SNOOZE_MINUTES=10)。延期和超时只由客户端更新本地 reminder_disposition_state=snoozed、snoozed_until 并重新注册本地监听,不向云端上传;本轮最终确认后才将 disposition_state=confirmed 发送到云端。confirmed 不等同于日程完成。
HTTP 只承担账号认证、云端全量快照恢复和提醒处置状态同步,不提供日程业务 CRUD。所有日程与提醒写入都由已认证 WebSocket 语音命令触发。基础地址:https://<host>/api/v1。
POST /api/v1/auth/access
登录和注册一体化,账号只用用户名和密码。服务端根据 username 查询账号:不存在则创建并登录;已存在则校验密码。密码用 Argon2 哈希(在数据库事务外完成)。
请求:
{
"username": "timeflow_user",
"password": "strong-password"
}响应 200 OK:
{
"account_id": "acc_001",
"access_token": "access-token",
"expires_in": 2592000
}expires_in 为 30 天(可配置 TIMEFLOW_JWT_ACCESS_TTL_SECONDS,默认 2592000)。JWT 为无状态 HS256,声明 sub/iat/exp/iss/aud,issuer="timeflow-api"、audience="timeflow-app",密钥至少 32 字节。错误:400(字段非法)、401(AUTH_INVALID_CREDENTIALS)、429(AUTH_RATE_LIMITED,滑动窗口每客户端 IP 20 次/60 s、全局 300 次/60 s)、500(AUTH_INTERNAL_ERROR,带 X-Auth-Event-Id)。
GET /api/v1/schedule/snapshot(Bearer 鉴权)
客户端仅在首次安装、换设备、账号切换、本地数据丢失或主动恢复时调用。响应 200 OK:
{
"schedules": [
{
"id": "schedule_001",
"schedule_type": "time",
"schedule_kind": "once",
"title": "项目评审",
"category": "work",
"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",
"reminder_type": "before_start",
"reminder_trigger_at": null,
"reminder_offset_minutes": 15,
"reminder_strength": "medium",
"reminder_disposition_state": null
}
],
"occurrence_overrides": []
}相较 v3.10 的变化:occurrence_overrides 提升为顶层数组;schedules 不再内嵌 occurrence_overrides;新增 category 字段。
PUT /api/v1/schedule/reminder-state(Bearer 鉴权)
只同步本轮提醒最终处置结果。请求:
{
"schedule_id": "schedule_001",
"disposition_state": "confirmed"
}响应 200 OK:
{
"schedule_id": "schedule_001",
"disposition_state": "confirmed",
"updated_at": "2026-08-06T08:00:02Z"
}错误:404(SCHEDULE_NOT_FOUND)、409(REMINDER_NOT_CONFIGURED)。延期、超时、本地重听时间不进入该接口。
WebSocket 承载会话建立、流式音频、语音多轮交互、结构化业务结果和 TTS 音频。文本控制消息用 JSON Text Frame,音频用 Binary Frame。连接地址 wss://<host>/ws?device_id=<device_id>,首条消息完成访问令牌校验。
客户端发送 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",
"voice_mode": "continuous",
"latitude": 31.2304,
"longitude": 121.4737,
"coordinate_system": "WGS84"
}
}相较 v3.10,session.hello 新增 voice_mode(push_to_talk / continuous)、latitude/longitude/coordinate_system(服务端地点搜索的客户端位置,coordinate_system 当前仅 WGS84)。device_id 必须与查询参数一致。timezone 缺省 Asia/Shanghai,voice_mode 缺省 push_to_talk。
服务端响应 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(UNAUTHENTICATED / MALFORMED_MESSAGE)并关闭。
客户端发送 voice.stream.start(payload:conversation_id?、audio_format、sample_rate_hz、channels),服务端响应 voice.stream.started(stream_id、conversation_id)。音频为 16 kHz 单声道 pcm_s16le,Binary Frame 按序交给后端。结束发 voice.stream.end(stream_id)。
服务端发送 voice.asr.completed:
{
"type": "voice.asr.completed",
"request_id": "req_voice_001",
"conversation_id": "conversation_001",
"payload": {
"transcript": "明天下午三点在203开会",
"language": "zh",
"duration_ms": 2100
}
}相较 v3.10 新增的消息类型 voice.dialogue.reply,用于流式文本回复(模式二 AgentTextDelta 累积投递,done=true 表示本轮文本结束):
{
"type": "voice.dialogue.reply",
"conversation_id": "conversation_001",
"payload": {
"reply_id": "reply_001",
"speech_text": "已为你创建明天下午三点的日程。",
"done": true
}
}服务端发送 voice.dialogue.question:
{
"type": "voice.dialogue.question",
"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。用户直接再次发送 voice.stream.start 并携带 conversation_id 作答。
服务端发送 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",
"schedule_kind": "once",
"title": "开会",
"category": "work",
"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",
"reminder_type": "before_start",
"reminder_trigger_at": null,
"reminder_offset_minutes": 15,
"reminder_strength": "medium",
"reminder_disposition_state": null,
"revision": 13,
"updated_at": "2026-08-06T03:01:00Z"
}
}
}客户端在本地事务中应用快照成功后发送 message.ack(message_id + status),ACK 只确认已应用会改变本地镜像的命令结果。
查询使用同一消息类型,operation=list_schedules,payload 携带 schedules 数组。
语音解析后使用以下 operation,不新增独立 HTTP CRUD。相较 v3.10 的简化:提醒配置已折叠为日程字段(reminder_type / reminder_trigger_at / reminder_offset_minutes / reminder_strength),随 schedule_create / schedule_update 一并写入,不再有独立的提醒 CRUD operation;延期是纯客户端动作,没有对应的服务端 operation。
operation |
对应工具 | 作用 | 关键处理 |
|---|---|---|---|
create_schedule |
schedule_create |
创建一次性或周期性日程,可同时带提醒字段 | 缺失字段追问;默认值只补缺失字段 |
list_schedules |
schedule_query |
查询日程 | 支持 schedule_id 或关键词候选,返回零或多个日程 |
update_schedule |
schedule_update |
修改日程字段(含提醒字段) | 只修改 changes 中明确提及的字段,expected_revision 乐观锁校验 |
delete_schedule |
schedule_delete |
删除日程(含其提醒) | 必须确认目标;周期日程必须确认范围 |
周期日程删除的 recurrence_scope:this_occurrence(写 occurrence_override action=cancel)、this_and_future(RRULE UNTIL 截断)、entire_series(软删 status=deleted)。
文本控制消息 voice.tts.start(audio_id、format、sample_rate_hz、purpose、speech_text,提醒场景带 schedule_id/audio_version),随后 Binary Frame 音频,结束发 voice.tts.end。相较 v3.10,新增 voice.tts.canceled(audio_id),用于连续模式下用户抢话(barge-in)时取消在播音频。
相较 v3.10 新增两类消息:
-
voice.session.end(conversation_id):模型判定对话应结束(end_conversation工具或告别语)时下发,客户端复用endTurn收尾。 -
schedule.category.updated(schedule_id+category):服务端异步完成日程分类后推送,客户端更新本地展示。
{
"type": "voice.command.error",
"request_id": "req_voice_003",
"ok": false,
"error": {
"code": "UNDERSTANDING_FAILED",
"message": "无法理解语音意图",
"retryable": false
}
}错误码包括:UNAUTHENTICATED、AUDIO_INVALID、MALFORMED_MESSAGE、UNKNOWN_MESSAGE_TYPE、INTERNAL_ERROR。
本章合并原「数据设计原则」与「数据库设计」两节:先说明数据归属与规则,再给出云端与客户端的表结构。
| 数据位置 | 作用 | 权威范围 |
|---|---|---|
| 客户端本地数据库 | 日常日程读取、日历展示、提醒监听和本地提醒状态 | 当前设备的运行读取权威,不接受独立业务写入 |
| 云端数据库 | 已确认日程与提醒写入、账号隔离和登录恢复 | 业务写入和备份数据的唯一权威 |
| 客户端安全存储 | access token 和当前 account_id | 不进入普通业务表 |
| 服务端内存 | 当前 WebSocket 会话、多轮对话短状态、双模式语音会话 | 仅限连接生命周期,不作为持久化数据 |
本地和云端不是两套独立业务数据。语音操作必须先由服务端业务层完成云端事务,再把持久化后的最终快照返回当前客户端;当前客户端直接应用该快照,不再通过 HTTP 重复拉取或上传。首次安装、换设备、账号切换或本地数据丢失时,客户端通过 HTTP 全量快照恢复本地数据;正常登录同一账号且本地数据完整时,直接使用本地数据。本期只支持单设备业务流程,不设计多设备实时同步、增量游标和客户端与云端的双向字段合并。
数据按语义分成三类:
| 数据类别 | 示例 | 写入与同步规则 |
|---|---|---|
| 账号级共享业务状态 | 日程标题、时间、地点、周期、删除状态、提醒类型和强度、日程分类 | 必须先写云端,再返回客户端 |
| 设备级提醒运行状态 |
next_trigger_at、snoozed_until、geofence_armed
|
只由当前设备维护,不上传云端 |
| 提醒最终处置状态 | reminder_disposition_state=confirmed |
本轮提醒最终确认后,通过 HTTP 一次性同步云端;延期和超时过程只保存在本地 |
本期离线不允许创建、修改或删除日程;提醒延期和超时属于本地监听过程,不产生等待上传的过程状态。只有本轮提醒最终确认后,客户端才上传最终处置状态。
| 类型 | 必填字段 | 可选字段 | 说明 |
|---|---|---|---|
time |
title、start_time、is_all_day
|
end_time、recurrence_rule、地点字段 |
一次性、周期性或全天时间日程 |
location |
title、有效地点坐标 |
地点名称 | 以到达地点为主要指向的日程 |
schedule_type 表示日程的触发维度;schedule_kind 表示日程是一次性还是周期性。schedule_kind=recurring 时必须使用 recurrence_rule,schedule_kind=once 时不使用周期规则。
所有时间保存为带时区的 UTC 时间;timezone 保留用户创建时的 IANA 时区,用于周期展开和语音表达。is_all_day=true 时,start_time 和 end_time 仅作为本地日期的起止边界保存。地点日程必须有经纬度,地点名称可以由地址解析结果补全。
地点搜索(location_search)作为服务端能力:用户位置经 session.hello 的 latitude/longitude/coordinate_system 传入,服务端逆地理编码得到粗粒度范围(当前省/市),POI 搜索结果按用户坐标系(WGS84 / GCJ-02)投影后返回候选。用户当前位置不等于目标地点,模型不生成经纬度。
| 缺失信息 | 默认处理 |
|---|---|
| 日程类型 | 根据有效时间或地点字段推断;两者都不存在时必须追问 |
| 日程周期 | 未表达周期时按一次性处理 |
| 只有日期没有具体时分 | 设置 is_all_day=true,按本地日期边界保存,不追问具体开始时刻 |
| 时区 | 使用客户端当前 IANA 时区 |
| 时间提醒 | 非全天时间日程默认提前 15 分钟提醒;全天日程默认在开始日期前一天 09:00 提醒;用户明确不提醒时不创建 |
| 地点提醒 | 只有用户明确表达地点提醒或创建地点日程时才创建 |
| 提醒强度 | 使用中强度 |
| 日程分类 | 服务端 LLM 分类器异步自动打标(八类之一),失败时置空 |
| 修改未提及字段 | 保持原值,不使用默认值覆盖 |
云端使用 PostgreSQL,客户端使用 SQLite。两端表结构保持可同步字段一致,客户端额外保存本地运行状态。
| 数据含义 | PostgreSQL | SQLite |
|---|---|---|
| ID 和文本 | VARCHAR(n) |
TEXT |
| 带时区时间 | TIMESTAMPTZ |
TEXT(UTC ISO-8601) |
| 经纬度 | NUMERIC(9,6) |
REAL |
| 整数和版本 |
INTEGER、BIGINT
|
INTEGER |
| 布尔值 | BOOLEAN |
INTEGER(0/1) |
| 字段 | 类型 | 约束 | 用途 |
|---|---|---|---|
id |
varchar(64) | PK | 账号 ID |
username |
varchar(255) | UNIQUE, NOT NULL | 用户名 |
password_hash |
varchar(255) | NOT NULL | Argon2 密码哈希 |
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
|
schedule_kind |
varchar(16) | NOT NULL DEFAULT once
|
once / recurring
|
title |
varchar(255) | NOT NULL | 标题 |
category |
varchar(16) | NULL | 八类分类,LLM 异步打标 |
is_all_day |
boolean | NOT NULL | 是否全天 |
start_time |
timestamptz | NULL | 开始时间 |
end_time |
timestamptz | NULL | 结束时间 |
timezone |
varchar(64) | NOT NULL | IANA 时区 |
recurrence_rule |
varchar(512) | NULL | RFC 5545 RRULE |
location_name |
varchar(255) | NULL | 地点名称 |
latitude |
numeric(9,6) | NULL | 纬度 |
longitude |
numeric(9,6) | NULL | 经度 |
reminder_type |
varchar(32) | NULL | 单一提醒类型 |
reminder_trigger_at |
timestamptz | NULL |
at_time 绝对提醒时间 |
reminder_offset_minutes |
integer | NULL |
before_start 偏移量 |
reminder_strength |
varchar(16) | NULL | 三级强度 |
reminder_disposition_state |
varchar(16) | NULL | 最终处置状态 |
status |
varchar(16) | NOT NULL |
active / deleted
|
revision |
bigint | NOT NULL | 乐观锁版本 |
created_at / updated_at
|
timestamptz | NOT NULL | 时间戳 |
deleted_at |
timestamptz | NULL | 软删除时间 |
相较 v3.10 新增 category 字段。约束与提醒规则同 v3.10:每条日程最多一个提醒;reminder_type 为空时 reminder_* 字段为空;at_time/before_start/地点提醒的必要字段组合;提醒增删改与日程更新同事务并递增 revision。
| 字段 | 类型 | 约束 | 用途 |
|---|---|---|---|
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 / updated_at
|
timestamptz | NOT NULL | 时间戳 |
约束同 v3.10:(schedule_id, occurrence_start) 唯一;action=replace 必须有 replacement_schedule_id。
镜像云端 schedules 业务字段,并增加本地运行字段:next_trigger_at、snoozed_until、geofence_armed、disposition_updated_at、sync_status、cloud_revision。相较 v3.10 新增 category 字段;recorded_location 与 next_trigger_at 均属本地运行状态(当前 recorded_location 尚未持久化,return_to_recorded_location 在 headless 路径存在已知缺口)。
字段与云端一致:id、schedule_id、occurrence_start、action、replacement_schedule_id。客户端展开周期日程时先计算 RRULE,再排除 cancel/replace 对应实例。
flowchart TD
A["用户语音:明天下午三点203开会"] --> B["客户端流式上传 16kHz PCM"]
B --> C{"voice_agent_mode"}
C -->|模式一| C1["Qwen-Audio 实时模型端到端识别+函数调用"]
C -->|模式二| C2["Qwen3-ASR 转写 → LLM ReAct"]
C1 --> D["LLM 解析并调用 schedule_create 工具"]
C2 --> D
D --> E{"信息是否完整?"}
E -->|否| F["request_user_input 生成追问 + TTS 播放"]
F --> G["用户语音补充"]
G --> D
E -->|是| H["业务层校验并云端事务写入"]
H --> I["返回持久化快照 voice.command.result"]
I --> J["客户端本地事务应用快照 + 注册监听 + message.ack"]
flowchart TD
A["用户语音:把每周一项目例会改到十点"] --> B["ASR 转写 / 实时识别"]
B --> C["LLM 查询候选日程 schedule_query"]
C --> D{"目标或范围明确?"}
D -->|否| E["语音追问 ambiguous_target / recurrence_scope"]
E --> F["用户回答消歧"]
F --> C
D -->|是| G["LLM 生成 update_schedule 命令"]
G --> H["云端事务更新 + 返回快照"]
H --> I["客户端应用快照 + 重算监听 + message.ack"]
flowchart TD
A["用户停车后说:提醒我回来取车"] --> B["session.hello 传入客户端位置 + 语音上传"]
B --> C["LLM 生成 return_to_recorded_location 提醒日程"]
C --> D["云端保存 + 返回快照"]
D --> E["客户端注册地点守卫 watch"]
E --> F["守卫前台服务按 15s-300s 轮询定位"]
F --> G["evaluateGeofence:离开范围 → geofence_armed=true"]
G --> H["再次进入范围 → triggered → 送达提醒"]
- 账号登录后,客户端使用 30 天有效期 JWT 访问 HTTP 和 WebSocket;
device_id只用于标识 WebSocket 客户端,不对应服务端登录会话。 - 日程和提醒业务变更只有在云端事务提交成功并返回
status=applied后才算成功。 - 本地监听使用本地数据库快照;云端同步不会在提醒触发时阻塞本地提醒。
- 客户端在本地事务中完整应用 WebSocket 快照或登录后的 HTTP 全量快照;快照应用成功后才重新注册本地监听。
- 用户确认期间目标日程发生变化时,服务端返回
STALE_CONFIRMATION并重新进行语音确认,不自动套用旧命令。 - 断网时只读本地日程和提醒配置,不允许语音变更,不调用 ASR、LLM 和 TTS。
- 设备完成提醒送达后记录
delivered,不自动改变日程状态。 - 用户延期或超时时,客户端只在本地更新
disposition_state、snoozed_until;本轮最终确认后,再通过 HTTP 异步同步confirmed。 - TTS 音频和语音转写不作为云端日程主数据保存;是否缓存音频由客户端策略决定。
- WebSocket 只承载已认证会话;访问令牌不写入普通日志。
- 地点提醒只保存必要目标坐标,不保存连续位置历史;用户位置不暴露给 LLM Prompt,只用于服务端逆地理编码粗筛。
- 语音 Agent 的工具轮数受
max_tool_rounds上限约束,超限转语音提示,避免批量操作失控。
- 账号创建或登录和无状态 JWT 鉴权。
- WebSocket 音频流、ASR/实时识别、LLM 创建日程。
- 缺失字段追问和语音确认。
- 云端日程事务写入和版本生成,提醒字段随日程保存。
- 本地日程数据库及其提醒字段,以及云端快照应用。
- 一次性时间提醒、到达地点提醒和三档强度送达。
- HTTP 登录后的全量快照恢复。
- 语音查询、修改、删除。
- 周期日程和修改/删除范围确认。
- 多目标匹配、指代消解。
- 单一提醒配置及其修改。
- 返回记录地点提醒和延期。
- 双模式语音 Agent(实时端到端 + 组合式)。
- TTS 个性化缓存和失效更新。
- 旧确认失效后的语音恢复流程。
- 组合式语音的健壮性优化(工具轮数兜底、删除确认守卫、噪音输入处理、TTS 截断提示)。
- 地点搜索与日程自动分类。
- 客户端提醒监听去原生 geofencing、改后台定位守卫。
- 全天日程的多日范围、跨时区和更复杂提醒策略评估与补充。
-
return_to_recorded_location的recorded_location持久化补齐。 - 评估通勤、路线、天气等上下文是否真正产生产品价值;未形成明确触发场景前不接入。
| 目标 | 验收标准 |
|---|---|
| 语音优先 | 创建、查询、修改、删除和提醒配置均可通过语音完成,不依赖 GUI 表单 |
| 对话完整 | 缺失信息、多个匹配、周期范围和删除确认均能通过语音追问完成 |
| 本地提醒 | 断网或 WebSocket 断开时,已同步日程仍能按时间或地点触发 |
| 云端确认写入 | LLM 输出不能直接落库;业务校验和云端事务成功后才向客户端返回 applied
|
| 数据同步 | 当前设备可应用 WebSocket 云端快照,登录或本地数据丢失后可通过 HTTP 全量快照恢复 |
| 提醒边界 | 每条日程最多一个提醒,地点返回提醒不会变成独立离开提醒 |
| 强度可控 | 低、中、高三档送达方式可区分,TTS 失败不阻塞高强度提醒 |
| 双模式等价 | 实时端到端与组合式两种语音后端对客户端暴露同一套线上协议 |
| 健壮性 | 批量操作超工具轮数上限转语音提示;删除前二次确认;噪音输入不误创建日程 |
| 离线边界 | 离线只读和提醒,不支持语音操作、日程修改或 TTS |
| 数据最小化 | 不保存位置历史、对话历史、TTS 历史和智能规则决策历史 |
后端目录以依赖方向为约束(洋葱式):business/ 为无框架用例层,只依赖标准库与 dateutil;data/ 实现业务端口;gateway/ 负责网络协议;intelligence/ 负责双模式智能编排;infrastructure/ 实现第三方适配。业务层与智能层不反向依赖 WebSocket、HTTP、SQLAlchemy 或第三方 SDK。
backend/src/timeflow/
├── business/
│ ├── auth/ # 账号创建/登录用例
│ ├── calendar/ # 日程/提醒用例、周期、快照、处置、分类
│ └── health.py
├── data/
│ ├── models.py # accounts / schedules / schedule_occurrence_overrides
│ ├── repositories/ # account / schedule
│ ├── account_uow.py
│ ├── schedule_unit_of_work.py
│ └── database.py
├── gateway/
│ ├── http/ # auth / schedule_snapshot / reminder_state / rate_limit
│ └── websocket/ # endpoint / router / connection_manager / handlers / messages
├── infrastructure/
│ ├── security/ # access_token(JWT)/ password_hasher(Argon2)
│ ├── audio/ # null_sink
│ ├── settings.py
│ └── external/
│ ├── asr/qwen_realtime.py # Qwen3-ASR
│ ├── llm/openai_compatible.py # OpenAI 兼容 LLM(流式 + JSON)
│ ├── tts/qwen_audio_tts.py # Qwen-Audio-TTS
│ ├── realtime/qwen_audio.py # Qwen-Audio 实时(端到端)
│ └── location/tencent_maps.py # 腾讯地图逆地理/POI
├── intelligence/
│ ├── ports.py # StreamInfo / ResultSink / AgentPort + 结果数据类
│ ├── conversation/ # 对话 Agent(ReAct)、LLM/ASR 类型、工具
│ ├── composed/ # 组合式语音 agent(ASR→LLM→TTS)、delivery、session
│ ├── realtime/ # 实时端到端 agent、instructions、工具、tool_mapping
│ ├── speech/ # 语音管线、分段器、TTS 类型
│ ├── location/ # 地点搜索服务/工具、坐标转换
│ ├── fake_agent.py # 开发环境兜底 agent
│ └── schedule_category.py # LLM 日程分类器
├── composition.py # 模式二组合根 build_composed_voice_agent
└── main.py # 应用组装 + 模式一构建
各层职责与 v3.10 附录 A 一致(business/ 无框架用例、data/ 持久化、gateway/ 协议适配、infrastructure/ 第三方适配、intelligence/ 编排),此处仅更新目录与文件清单,不再逐层重复「允许/禁止」清单。要点:intelligence/ 通过 business.calendar.ScheduleAgentService 执行日程写入,不直接持久化;infrastructure/external/* 实现 intelligence 定义的 ASR/LLM/TTS/位置端口。
前端是 Expo/React Native 客户端,按产品功能组织。相较 v3.10 的变化:新增 screens/(HomeScreen/LoginScreen)、infrastructure/websocket/、infrastructure/appState/、infrastructure/time/;infrastructure/secure-storage/ 实为空,安全存储实际位于 features/auth/data/SecureAuthSessionStore.ts。
frontend/src/
├── app/ # AppRoot / AppProviders / composition / orchestration / authRuntime
├── contracts/ # 纯协议类型:auth / authWebSocket / conversation / reminder / schedule / sync / transport
├── features/
│ ├── auth/ # 登录、会话、令牌生命周期、鉴权失效协调
│ ├── schedule/ # 本地日程读取、日历展示、周期展开
│ ├── assistant/ # 语音入口、双模式会话(push-to-talk / continuous)
│ ├── reminder/ # 提醒生命周期、地理围栏状态机、守卫协调
│ └── sync/ # 快照应用、ACK、恢复、对账
├── infrastructure/
│ ├── network/ # HTTP 客户端
│ ├── database/ # SQLite 连接、事务、迁移
│ ├── audio/ # 录音/播放封装
│ ├── location/ # ExpoLocationMonitor / reminderGuardTask / LocationProvider
│ ├── notifications/ # 原生闹钟、系统通知、弹窗、震动
│ ├── websocket/ # AuthenticatedWebSocketClient
│ ├── appState/ # AppStateProvider / RNAppStateProvider
│ ├── time/ # IntervalTimeListener
│ └── secure-storage/ # (空,安全存储见 features/auth/data)
├── screens/ # HomeScreen / LoginScreen(组合层)
├── shared/ # ui / errors / time
└── types/
依赖方向:contracts(叶子)→ 功能 domain(纯函数)→ 功能 application(用例 + 端口)→ 功能 data + infrastructure(适配器)→ 功能 presentation + screens(React UI)→ app(组合根)。assistant 同时持有 push-to-talk(AssistantConversationService)与 continuous(AssistantContinuousConversationService)两条路径,UI 互斥;continuous 侧带 3 分钟 SESSION_IDLE_TIMEOUT_MS 空闲超时,voice.session.end 与用户点结束走同一条 endTurn 收尾。已知层例外:infrastructure/location/ 的守卫模块直接引用 reminder/domain 的 evaluateGeofence 等纯函数(跨横切模块参与提醒领域,代码注释有意为之)。