Skip to content

New Architecture interface design

hqy edited this page Aug 6, 2026 · 5 revisions

TimeFlow 时间管理 App 产品架构设计

版本:v2.7
日期:2026-08-06
适用范围:语音驱动的日程创建、提醒配置、本地提醒与云端同步

0. 设计摘要

TimeFlow 是一款以语音为核心交互方式的时间管理 App。用户通过语音创建、查询、修改和删除时间日程、地点日程及其提醒配置;当信息缺失、存在多个匹配结果、涉及重复日程范围或需要二次确认时,系统通过语音反问和语音输出继续完成对话。日历视图只负责展示和状态反馈,不提供日程业务编辑表单。

系统将“云端确认写入”和“客户端运行执行”分离:语音操作经过多轮补全和用户确认后,由服务端业务层校验并写入云端数据库;当前客户端直接应用服务端返回的最终快照,其他设备通过 HTTP 增量同步。客户端本地数据库负责日常读取,时间和地点监听全部由客户端实现。断网时客户端可以查看本地数据并继续执行已经注册的提醒,但不能创建、修改或删除日程,也不能执行依赖服务端的 ASR、LLM 和 TTS。

0.1 第一性原则

原则 设计结论
用户真正要完成的是管理时间 日程、提醒、查询和处置是核心业务;日历只是展示载体
语音是主要交互方式 所有核心业务操作走语音,不维护重复的 GUI 表单流程
提醒必须不依赖云端实时在线 已同步日程由客户端本地监听和本地送达
云端服务集中处理高价值智能能力 ASR、LLM、TTS 在服务端统一调用和升级
业务写入必须有唯一权威 云端数据库负责确认后的日程与提醒写入,本地只应用云端确认快照
数据模型应服务于当前功能 只保存日程、提醒、账号和同步所需字段,不持久化登录会话,不提前引入画像、导航或复杂历史
复杂情况通过对话解决 缺失信息、目标歧义、重复范围和危险操作由语音追问确认

0.2 明确要做与不做

要做 不做
一次性、周期性和全天时间日程 独立的离线智能创建
地点日程和返回记录地点提醒 服务端持续监听所有日程
最多两个时间或地点提醒 独立的“离开地点时提醒”
低、中、高三级提醒强度 本期智能提醒规则引擎
语音创建、查询、修改、删除和确认 GUI 表单创建和编辑
账号、云端确认写入和多设备同步 过早引入天气、路线、通勤等上下文
本地时间与地点监听 本地 ASR、LLM、TTS

1. 产品功能模块划分

模块按完整用户能力划分。一个模块必须能够独立说明输入、处理和输出,不以某一个 DTO、WebSocket 消息或第三方 SDK 作为模块边界。第三方接口适配属于基础设施层,网关层只负责对外网络协议。

1.1 前端功能模块

模块 包含的小功能 输入 输出 边界
语音交互模块 麦克风采集、WebSocket 音频流、对话上下文、问题播放、TTS 播放、语音结果接收 用户语音、服务端语音消息 音频流、对话回答、业务结果 不执行 ASR、LLM、TTS 模型,不直接写云端数据库
本地日程模块 本地日程存储、日历展示、日程快照应用、周期日程计算所需的本地数据读取 语音操作结果、同步结果 日程列表、日程详情、本地快照 不提供 GUI 编辑;不自行生成未被服务端确认的云端日程
本地提醒配置模块 最多两个提醒的保存、时间提醒和地点提醒配置展示、提醒强度保存 语音操作结果、同步结果 本地提醒配置 不负责判断是否到期;不创建第三个提醒
本地监听与提醒执行模块 时间监听、地点距离计算、返回地点布防、延期、强度分级、弹窗、震动、屏幕展示和音频播放 本地日程、提醒配置、当前时间、定位数据、用户处置 本地提醒送达、延期结果、送达状态 不依赖服务端到点消息;提醒业务规则由客户端实现
==账号与同步模块== 注册登录、访问凭证保存、当前设备快照应用、其他设备增量拉取、提醒处置状态上传、离线只读状态 HTTP 账号响应、HTTP 同步响应、提醒状态响应、WebSocket 云端确认快照 本地数据同步、处置状态同步、网络状态 不执行日程业务语义;不在离线时创建或修改日程

1.2 后端功能模块

模块 包含的小功能 输入 输出 边界
账号与同步服务 账号注册登录、JWT 签发与校验、全量恢复、增量变更查询和提醒处置状态同步 HTTP 认证请求、同步游标、提醒处置状态 访问令牌、云端变更、状态同步结果 不维护登录会话;不处理语音语义;不负责时间和地点监听
语音交互模块 会话建立、音频流接收、ASR 转写、LLM 多轮理解、缺失信息追问、二次确认、结构化结果返回和 TTS 音频输出 WebSocket 文本帧和 Binary Frame 语音问题、结构化操作结果、错误、TTS 音频 不直接操作客户端 GUI;不持久化日程;不在服务端执行提醒
日程数据管理模块 管理用户确认后的日程和提醒配置,提供查询、创建、修改和删除能力 语音交互模块输出的查询请求和已确认结构化操作 日程与提醒数据、查询结果 不负责语音理解、本地监听和提醒送达

“语音交互模块”是产品功能边界,内部仍然保持三类技术职责:gateway/ 负责 WebSocket 会话、消息编排和协议转换;intelligence/ 负责智能能力编排、提示词和结构化结果处理;infrastructure/ 负责阿里云 ASR、OpenAI LLM、阿里云 TTS 等第三方接口适配。合并模块名称是为了避免把一个完整的用户能力拆成两个产品模块,并不表示把协议处理和模型调用代码写在同一层。

“日程数据管理模块”只负责日程和提醒配置的数据管理,不执行实际提醒。时间监听、地点监听、弹窗、震动和音频播放均由客户端完成。

1.3 前后端职责边界

能力 前端负责 后端负责
语音输入 录音、分片、上传 接收音频、调用 ASR
语义理解 保存对话 UI 状态、播放问题的音频 调用 LLM、补全字段、追问和消歧
日程变更 应用云端确认快照到本地数据库 校验业务命令并在云端事务写入
提醒监听 时间、地点、返回地点和延期逻辑 不参与到点判断
提醒送达 TTS 播放、屏幕、震动、强度策略 生成 TTS 音频并提供缓存/同步来源
数据可靠性 本地运行读取、离线提醒、恢复同步 云端写入权威、账号隔离和跨设备变更分发

2. 数据设计原则

2.1 数据归属

数据位置 作用 权威范围
客户端本地数据库 日常日程读取、日历展示、提醒监听和本地提醒状态 当前设备的运行读取权威,不接受独立业务写入
云端数据库 已确认日程与提醒写入、账号隔离和跨设备同步 业务写入和同步版本的唯一权威
客户端安全存储 access token 和当前 account_id 不进入普通业务表
服务端内存 当前 WebSocket 会话和多轮对话短状态 仅限连接生命周期,不作为持久化数据

本地和云端不是两套独立业务数据。语音操作必须先由服务端业务层完成云端事务,再把持久化后的最终快照返回当前客户端;当前客户端直接应用该快照,不再通过 HTTP 重复拉取或上传。其他设备、重新安装和数据恢复通过同步游标拉取云端变更。由于客户端不独立产生业务变更,本期不设计客户端与云端的双向字段合并和写入冲突解决。

数据按语义分成两类:

数据类别 示例 写入与同步规则
账号级共享业务状态 日程标题、时间、地点、周期、删除状态、提醒类型、强度、启用状态、用户主动延期时间 必须先写云端,再返回并同步到所有设备
设备级提醒运行状态 next_trigger_atgeofence_armed 只由当前设备维护,不上传云端
提醒处置状态 disposition_statesnoozed_until 客户端立即更新,通过 HTTP 异步同步云端

本期离线不允许创建、修改、删除或语音延期,因此不会产生等待上传的账号级业务状态。客户端本地发生的变化仅限提醒监听和送达运行状态。

2.2 时间与地点规则

类型 必填字段 可选字段 说明
time titlestart_timeis_all_day end_timerecurrence_rule、地点字段 一次性、周期性或全天时间日程
location title、有效地点坐标 地点名称 以到达地点为主要指向的日程

所有时间保存为带时区的 UTC 时间;timezone 保留用户创建时的 IANA 时区,用于周期展开和语音表达。is_all_day=true 时,start_timeend_time 仅作为本地日期的起止边界保存:单日全天的 end_time 是下一本地日期的零点,多日全天的 end_time 是结束日期的下一日零点;客户端展示为全天,不将午夜表达为用户指定的时间。地点日程必须有经纬度,地点名称可以由地址解析结果补全。

2.3 默认创建规则

缺失信息 默认处理
日程类型 根据有效时间或地点字段推断;两者都不存在时必须追问
日程周期 未表达周期时按一次性处理
只有日期没有具体时分 设置 is_all_day=true,按本地日期边界保存,不追问具体开始时刻
时区 使用客户端当前 IANA 时区
时间提醒 非全天时间日程默认提前 15 分钟提醒;全天日程默认在开始日期前一天 09:00 提醒;用户明确不提醒时不创建
地点提醒 只有用户明确表达地点提醒或创建地点日程时才创建
提醒强度 使用中强度
修改未提及字段 保持原值,不使用默认值覆盖

3. 提醒模型与本地执行

3.1 提醒类型

类型 触发条件 必要数据
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 自动复制到周期日程的所有发生实例。

3.2 强度与送达方式

提醒强度直接决定客户端的送达方式。下表中的“普通弹窗”是应用自定义的全局弹窗,不是系统通知。

强度 送达方式 适用语义
low 系统通知 非紧急提醒
medium 普通弹窗 + 短震动 默认提醒强度
high 普通弹窗 + 短震动 + TTS 用户明确要求高强度提醒

lowmedium 不依赖 TTS。high 的 TTS 音频不可用时,使用普通弹窗、短震动和本地提示音完成提醒。

3.3 延期与状态

本版先定义最小提醒状态,避免把提醒送达状态误当成日程完成状态:

状态 含义
pending 已配置,等待条件满足
confirmed 用户确认已处理本轮提醒,不表示日程业务完成
snoozed 用户选择延期,或提醒超时未操作,等待 snoozed_until

提醒尚未触发或本轮尚未处置时,disposition_state 为空;它不是提醒配置的启用状态。用户没有指定延期时间时,默认延期十分钟。confirmed 不等同于日程完成;“完成”动作暂不定义为待办事项完成,当前不纳入服务端和客户端的核心状态机。

4. 后端 HTTP 接口设计

HTTP 只承担账号认证和云端数据到设备的恢复/增量同步,不提供日程业务 CRUD,也不接收客户端上传的日程变更。所有日程与提醒写入都由已认证 WebSocket 语音命令触发,并由后端业务层完成云端事务。

基础地址:https://<host>/v1

4.1 账号注册

POST /auth/register

请求:

{
  "login_identifier": "user@example.com",
  "password": "strong-password"
}

响应 201 Created

{
  "account_id": "acc_001",
  "access_token": "access-token",
  "expires_in": 3600
}

4.2 账号登录

POST /auth/login

请求结构与注册相同。成功响应与注册相同。账号不存在和密码错误统一映射为认证错误,不返回敏感的账号存在性信息。

服务端签发包含 account_idexp 的无状态 JWT,不保存登录会话,也不提供 Refresh Token。客户端将 Access Token 保存到平台安全存储;Token 到期后重新登录。退出登录只删除客户端 Token,不调用服务端登出接口,本地日程不删除。该方案不支持立即撤销尚未过期的 Token,也不在服务端强制单设备登录。

4.3 拉取云端变更

GET /sync/changes?cursor=<cursor>&limit=100

请求头:Authorization: Bearer <access_token>

首次同步不传 cursor。响应:

{
  "next_cursor": "cursor_002",
  "has_more": false,
  "changes": [
    {
      "entity": "schedule",
      "operation": "upsert",
      "revision": 12,
      "data": {
        "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",
        "updated_at": "2026-08-06T03:00:00Z",
        "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,
            "updated_at": "2026-08-06T03:00:00Z"
          }
        ],
        "occurrence_overrides": []
      }
    }
  ]
}

删除变更使用 operation=delete 和实体 ID,客户端收到后删除本地镜像并取消本地监听。客户端只有完整应用当前批次后,才能持久化 next_cursor

当前发起语音操作的设备不需要在写入成功后调用该接口重复拉取。它直接应用 WebSocket 返回的云端持久化快照,并把响应中的 revision 写入 cloud_revision。该接口服务于新设备、重新安装、断线恢复以及同一账号的其他设备。

4.4 同步提醒处置状态

PUT /sync/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 更新为用户选择的 confirmedsnoozed,同时将 sync_status 标记为 pending。HTTP 请求成功后,客户端根据服务端返回的 disposition_statesnoozed_until 覆盖本地处置值,再将 sync_status 改为 synced;服务端没有正常响应时,不得把同步标志改为 synced,本地保持 pending 并在网络恢复后重试。重试不会阻塞本地监听。

5. 后端 WebSocket 接口设计

WebSocket 用于会话建立、流式音频、语音多轮交互、结构化业务结果和 TTS 音频。文本控制消息使用 JSON Text Frame,音频使用 Binary Frame。

连接地址:

wss://<host>/ws?device_id=<device_id>

连接后第一条消息完成访问令牌校验。device_id 只用于识别设备,真正的访问控制由 access_token 完成。

5.1 会话建立

客户端发送: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 接收账号密码。

5.2 开始语音操作

客户端发送: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。

5.3 结束语音操作

客户端发送: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
  }
}

5.4 语音追问

当信息缺失、多个日程匹配、重复范围不明确或删除需要确认时,服务端发送: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

5.5 语音操作成功

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 增量同步重新恢复云端快照。

5.6 语音查询成功

查询使用同一消息类型,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 汇总。客户端可以展示列表,但不要求用户通过点击完成下一步业务操作。

5.7 修改、删除和提醒操作

语音解析后使用以下 operation,不新增一套独立 HTTP CRUD:

operation 作用 关键处理
create_schedule 创建一次性或周期性日程 缺失字段追问;默认值只补缺失字段
update_schedule 修改日程字段 只修改用户明确提及的字段
delete_schedule 删除日程 必须确认目标;周期日程必须确认范围
create_reminder 创建提醒 校验每条日程最多两个提醒
list_reminders 查询提醒 支持“第一个提醒”“地点提醒”等指代
update_reminder 修改时间、地点或强度 保留未提及字段
delete_reminder 删除提醒 必须确认目标提醒
snooze_reminder 延期本轮提醒 未给时间时使用十分钟默认值;云端写入 snoozed_until 后返回所有设备

需要确认时先返回 status=needs_confirmationvoice.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 和本地监听状态。

5.8 TTS 音频输出

服务端在语音问题、命令结果摘要和本地提醒音频准备时调用 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_questioncommand_resultreminder。当 purpose=reminder 时,服务端必须带上 schedule_idreminder_idaudio_version,客户端将音频缓存到对应提醒;缓存缺失或版本不匹配时,提醒执行模块使用本地兜底音频。随后发送音频 Binary Frame,结束时发送:

{
  "type": "voice.tts.end",
  "conversation_id": "conversation_001",
  "audio_id": "audio_001"
}

提醒 TTS 音频只用于 high 强度。本期文案只使用日程标题、开始时间和位置;预计通勤、路线、天气、关联日程、交互模式和停车位置均不进入当前 TTS 输入。TTS 生成失败时,客户端使用普通弹窗、短震动和本地提示音继续完成提醒。

5.9 WebSocket 消息确认

对于需要确认处理结果的消息,发送方在消息外层提供唯一的 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。

5.10 语音错误

{
  "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 无法形成有效意图
MISSING_REQUIRED_INFORMATION 缺少必要字段
AMBIGUOUS_TARGET 匹配到多个目标日程
CONFIRMATION_REQUIRED 需要用户确认
SCHEDULE_NOT_FOUND 目标日程不存在
REMINDER_LIMIT_REACHED 已达到两个提醒上限
STALE_CONFIRMATION 用户确认前目标日程已经发生变化,需要重新确认

6. 数据库设计

云端数据库使用 PostgreSQL,客户端数据库使用 SQLite。两端表结构保持可同步字段一致,客户端额外保存少量本地运行状态。云端使用 PostgreSQL 原生类型,客户端使用 SQLite 类型亲和性保存对应数据。

数据含义 PostgreSQL SQLite 说明
ID 和文本 VARCHAR(n) TEXT 统一使用带业务前缀的字符串 ID
带时区时间 TIMESTAMPTZ TEXT SQLite 保存 UTC 的 ISO-8601 文本
经纬度 NUMERIC(9,6) REAL 客户端用于距离计算
整数和版本 INTEGERBIGINT INTEGER SQLite 使用 INTEGER 亲和类型
布尔值 BOOLEAN INTEGER SQLite 使用 01

6.1 云端 PostgreSQL accounts

字段 类型 约束 用途
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 账号更新时间

6.2 云端 PostgreSQL schedules

字段 类型 约束 用途
id varchar(64) PK 日程 ID,客户端和云端一致
account_id varchar(64) FK, NOT NULL 数据归属账号
schedule_type varchar(16) NOT NULL timelocation
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 activedeleted
revision bigint NOT NULL 云端同步版本,单调递增
created_at timestamptz NOT NULL 创建时间
updated_at timestamptz NOT NULL 最后修改时间
deleted_at timestamptz NULL 软删除时间,供同步下发删除变更

约束:schedule_type=time 必须有 start_timeis_all_day=true 时必须为时间日程,必须有表示排他结束边界的 end_time,默认 before_start 提醒使用 offset_minutes=900schedule_type=location 必须有有效经纬度且 is_all_day=falserecurrence_rule 只允许用于时间日程;status=deleted 时保留整行一段同步窗口,不能立即物理删除。

6.3 云端 PostgreSQL reminders

字段 类型 约束 用途
id varchar(64) PK 提醒 ID
schedule_id varchar(64) FK, NOT NULL 所属日程
==reminder_type== ==varchar(32)== ==NOT NULL== ==at_timebefore_startarrive_locationreturn_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 lowmediumhigh
enabled boolean NOT NULL 是否启用
snoozed_until timestamptz NULL 用户主动延期到期时间;NULL 表示未延期
created_at timestamptz NOT NULL 创建时间
updated_at timestamptz NOT NULL 最后修改时间

约束:同一个 schedule_id 最多两个提醒;at_time 必须有 trigger_atbefore_start 必须有 offset_minutesarrive_location 必须关联有坐标的日程;return_to_recorded_location 必须有目标坐标;提醒强度必须为三级枚举。提醒增删改必须在同一事务中递增所属日程的 revision,同步时始终返回日程及其完整提醒集合,因此提醒不需要独立版本和软删除字段。snoozed_until 是用户主动处置结果,通过语音命令写入云端;到期后按时间比较恢复正常监听,不需要额外状态字段。

6.4 云端 PostgreSQL schedule_occurrence_overrides

该表只记录周期日程中被单独修改或删除的发生实例,不保存正常展开的所有实例。周期日程正常触发、用户确认提醒或延期提醒时,都不会写入该表。

字段 类型 约束 用途
id varchar(64) PK 例外记录 ID
schedule_id varchar(64) FK, NOT NULL 原周期日程 ID
occurrence_start timestamptz NOT NULL 被处理实例原本的开始时间
action varchar(16) NOT NULL cancelreplace
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_idaction=cancel 时必须为空。新增或修改例外时递增原周期日程的 revision,不为例外单独维护同步版本。

6.5 客户端 SQLite local_schedules

客户端镜像云端 schedules 的业务字段,并增加本地同步所需字段。

字段 类型 用途
id text PK 与云端 schedules.id 相同
account_id text 当前设备上的账号隔离
schedule_type text timelocation
title text 日程标题
is_all_day integer 是否为全天日程,01
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 activedeleted
cloud_revision integer 最近应用的云端版本
updated_at text 云端业务更新时间

客户端不生成服务端不存在的新日程。在线语音写入成功后,客户端只应用 WebSocket 返回的云端确认快照;其他设备只应用 HTTP 同步快照。离线状态只读取 active 数据,不允许修改这张表中的业务字段。

6.6 客户端 SQLite local_reminders

客户端镜像云端提醒配置,并保存监听运行状态;运行状态不回写为日程业务字段。

字段 类型 用途
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 lowmediumhigh
enabled integer 是否启用
next_trigger_at text NULL 时间提醒下一触发点
snoozed_until text NULL 云端确认并同步的用户延期到期时间
geofence_armed integer 返回地点是否已经完成离开布防
disposition_state text NULL confirmedsnoozed;本轮提醒尚未处置时为空
disposition_updated_at text NULL 最近一次本地处置时间
sync_status text pendingsynced;仅表示处置状态是否已获得服务端确认
updated_at text 本地配置更新时间

reminder_type、时间、地点、强度、enabledsnoozed_until 属于云端确认的共享业务状态;next_trigger_atgeofence_armed 属于当前设备的监听运行状态。disposition_state 是本地立即生效的提醒处置状态,通过 HTTP 异步同步云端;sync_status 只有在服务端成功响应后才变为 synced,不建立单独事件表。

6.7 客户端 SQLite local_schedule_occurrence_overrides

字段 类型 用途
id text PK 与云端例外记录相同
schedule_id text 原周期日程 ID
occurrence_start text 被处理实例原本的 UTC 开始时间
action text cancelreplace
replacement_schedule_id text NULL 替换后的一次性日程 ID

客户端展开周期日程时先计算 RRULE,再排除 cancelreplace 对应的原实例;replace 指向的一次性日程按普通日程展示和监听。

6.8 客户端 SQLite sync_state

客户端每个账号保留一行同步状态,不单独建立同步日志表。

字段 类型 用途
account_id text PK 当前账号
cursor text NULL 最近一次成功拉取的云端游标
last_sync_at text NULL 最近同步时间

7. 典型使用流程

7.1 创建时间日程

用户:“每周一上午九点项目例会。”
    ↓
客户端上传音频
    ↓
ASR 转写
    ↓
LLM 识别 create_schedule + recurrence_rule
    ↓
如果缺少时区等信息,服务端通过 TTS 追问
    ↓
业务层校验并在云端事务中写入日程和默认提醒
    ↓
服务端返回持久化后的快照和 revision
    ↓
客户端应用快照到本地数据库并注册本地监听

7.2 修改周期日程

用户:“把项目例会改到十点。”
    ↓
LLM 根据当前对话和服务端从云端数据库查询到的日程匹配目标
    ↓
如果存在多个例会,语音追问
    ↓
服务端询问修改范围:本次、后续全部或整个系列
    ↓
用户语音确认范围
    ↓
业务层只修改时间,保留标题、地点和提醒等未提及字段
    ↓
云端事务更新成功并返回新 revision
    ↓
当前客户端应用新快照并重算本地监听
    ↓
其他设备通过 HTTP 增量同步获得变更

7.3 地点返回提醒

用户:“我回到刚才记录的地方时提醒我取快递。”
    ↓
客户端提供当前位置给语音会话,服务端生成 return_to_recorded_location 提醒
    ↓
业务层校验并把提醒配置写入云端
    ↓
客户端应用云端确认快照,并在本地设置 geofence_armed = false
    ↓
客户端自行检测离开范围并设置 geofence_armed = true
    ↓
客户端自行检测再次进入范围
    ↓
根据提醒强度执行系统通知,或执行普通弹窗、短震动和 TTS

8. 一致性、离线与安全约束

  1. 账号登录后,客户端使用带过期时间的 JWT 访问 HTTP 和 WebSocket;device_id 只用于标识 WebSocket 客户端,不对应服务端登录会话。
  2. 日程和提醒业务变更只有在云端事务提交成功并返回 status=applied 后才算成功。
  3. 本地监听使用本地数据库快照;云端同步不会在提醒触发时阻塞本地提醒。
  4. 客户端只应用高于当前 cloud_revision 的快照,旧响应和旧同步批次不得覆盖新数据。
  5. 用户确认期间目标日程发生变化时,服务端返回 STALE_CONFIRMATION 并重新进行语音确认,不自动套用旧命令。
  6. 断网时只读本地日程和提醒配置,不允许语音变更,不调用 ASR、LLM 和 TTS。
  7. 设备完成提醒送达后记录 delivered,不自动改变日程状态。
  8. 用户确认或延期后,客户端立即更新 disposition_state;延期写入 snoozed_until,再通过 HTTP 异步同步云端;下一触发时间和围栏布防只保存在本地。
  9. TTS 音频和语音转写不作为云端日程主数据保存;是否缓存音频由客户端缓存策略决定。
  10. WebSocket 只承载已认证会话;访问令牌不写入普通日志。
  11. 地点提醒只保存必要的目标坐标,不保存连续位置历史。

9. 实施优先级

P0:语音日程闭环

  1. 账号注册登录和无状态 JWT 鉴权。
  2. WebSocket 音频流、ASR、LLM 创建日程。
  3. 缺失字段追问和语音确认。
  4. 云端日程/提醒事务写入和版本生成。
  5. 本地日程和提醒数据库,以及云端快照应用。
  6. 一次性时间提醒、到达地点提醒和三档强度送达。
  7. HTTP 首次全量同步和增量同步。

P1:完整日程操作

  1. 语音查询、修改、删除。
  2. 周期日程和修改/删除范围确认。
  3. 多目标匹配、指代消解。
  4. 最多两个提醒及提醒配置修改。
  5. 返回记录地点提醒和延期。

P2:体验完善

  1. TTS 个性化缓存和失效更新。
  2. 旧确认失效后的语音恢复流程。
  3. 全天日程的多日范围、跨时区和更复杂提醒策略评估与补充。
  4. 评估通勤、路线、天气等上下文是否真正产生产品价值;未形成明确触发场景前不接入。

10. 验收标准

目标 验收标准
语音优先 创建、查询、修改、删除和提醒配置均可通过语音完成,不依赖 GUI 表单
对话完整 缺失信息、多个匹配、周期范围和删除确认均能通过语音追问完成
本地提醒 断网或 WebSocket 断开时,已同步日程仍能按时间或地点触发
云端确认写入 LLM 输出不能直接落库;业务校验和云端事务成功后才向客户端返回 applied
数据同步 当前设备可应用 WebSocket 云端快照,新设备可按游标和版本拉取同一数据
提醒边界 每条日程最多两个提醒,地点返回提醒不会变成独立离开提醒
强度可控 低、中、高三档送达方式可区分,TTS 失败不阻塞高强度提醒
离线边界 离线只读和提醒,不支持语音操作、日程修改或 TTS
数据最小化 不保存位置历史、对话历史、TTS 历史和智能规则决策历史

附录 A:后端五层目录与工程协作约束

后端目录以依赖方向为约束:入口负责组装各层;网关只依赖业务用例和智能能力端口;业务与智能层依赖抽象端口;数据层实现数据库端口;基础设施层实现第三方接口和运行时端口。业务层和智能层不得反向依赖 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

A.1 business/ 业务逻辑层

**职责:**承载可测试、可复用的业务规则和用例编排。包括账号用例、日程数据管理用例和同步用例。

允许包含:

  • 用例服务,例如创建日程、查询日程、修改周期范围、执行已确认删除、管理提醒配置、同步提醒处置和查询同步变更。
  • 领域实体和值对象,例如 ScheduleReminderScheduleAggregateRecurrenceRuleReminderStrength
  • 业务命令、结果 DTO 和端口接口,例如 ScheduleRepositoryReminderRepositoryChangeFeedRepository
  • 必填字段、周期日程范围、提醒数量上限、提醒类型组合等业务校验。

禁止包含:

  • FastAPI 路由、WebSocket 连接对象和 JSON 序列化代码。
  • SQLAlchemy Model、数据库 Session、文件系统和环境变量读取。
  • 阿里云、OpenAI 等第三方 SDK 调用。
  • 具体的 ASR、LLM、TTS Prompt 和音频协议。
  • 时间轮询、地理定位采集和客户端提醒执行。

A.2 data/ 数据层

**职责:**实现 PostgreSQL 云端数据库的持久化模型、Repository 和数据库迁移。

允许包含:

  • SQLAlchemy ORM Model 和 PostgreSQL 字段映射。
  • AccountRepositoryScheduleRepositoryReminderRepository 的实现。
  • 同步变更的查询、版本和软删除记录读取。
  • Alembic 迁移脚本。

禁止包含:

  • 业务规则判断,例如“缺失地点就追问”。
  • WebSocket/HTTP 请求模型。
  • LLM 输出解析、Prompt 拼装和第三方网络请求。
  • 客户端 SQLite 数据库、客户端定位和客户端提醒状态。

A.3 gateway/ 网关层

**职责:**将 HTTP 和 WebSocket 等外部网络协议适配为系统内部可调用的请求和消息。网关是网络输入输出边界,不拥有业务决策,也不直接适配第三方模型服务。

允许包含:

  • HTTP 路由、认证依赖、请求/响应模型和错误映射。
  • WebSocket 连接生命周期、文本帧与 Binary Frame 接收发送。
  • 网络协议字段到内部命令和结果的转换。

禁止包含:

  • 日程创建和提醒配置业务规则。
  • 直接拼接 SQL 或直接修改 ORM Model。
  • 在 Handler 中实现多轮对话、重复日程范围或提醒数量判断。
  • 时间监听、地点监听和本地提醒送达。

A.4 infrastructure/ 基础设施层

**职责:**提供运行时通用能力和应用装配所需的技术组件,不表达产品业务。

允许包含:

  • 配置加载、环境变量、日志、异常追踪。
  • 数据库 Engine、Session 工厂和事务管理。
  • 密码哈希、令牌签发与校验、随机 ID 和时间时钟。
  • HTTP/WebSocket 客户端连接池、重试和超时的通用封装。
  • 阿里云 ASR、OpenAI LLM、阿里云 TTS 等第三方接口适配器。
  • 任务执行器、资源生命周期和健康检查。

禁止包含:

  • 创建日程、删除确认和提醒强度等业务规则。
  • ASR/LLM/TTS Prompt 和模型选择决策。
  • 任何具体的 WebSocket 消息处理流程。
  • 直接向客户端发送产品消息。

infrastructure/external/ 下的第三方适配器只实现 intelligence/ 定义的调用端口,负责供应商协议、鉴权、超时、重试和外部错误转换,不包含日程业务规则,也不直接处理客户端 WebSocket 消息。

A.5 intelligence/ 智能理解层

**职责:**将语音和对话上下文转化为可供业务层执行的结构化意图,并把业务结果转化为语音输出。该层只负责智能理解和生成,不负责业务持久化。

允许包含:

  • 对话轮次状态、当前问题、候选日程和确认上下文。
  • ASR 文本标准化和最终转写结果处理。
  • LLM 日历意图解析:创建、查询、修改、删除日程和提醒。
  • 缺失字段识别、追问生成、指代消解和多结果消歧。
  • TTS 文案生成和 TTS 调用端口编排。
  • 受约束的结构化输出 Schema。

禁止包含:

  • 直接保存日程和提醒到数据库。
  • 自行决定业务默认值之外的日程持久化规则。
  • 直接执行账号鉴权和同步事务。
  • 决定本地监听何时触发或替用户完成提醒处置。

A.6 main.py 应用组装入口

main.py 只负责创建应用和依赖注入:读取配置、创建 PostgreSQL 数据库会话、初始化 Repository、第三方客户端、业务用例和 HTTP/WebSocket 路由。它不实现业务逻辑,不解析 LLM 结果,不执行 SQL。

Clone this wiki locally