TimeFlow 多维感知智能提醒设计
1. 设计目标
当前系统已经具备时间和空间两类提醒触发条件。本阶段新增以下四类维度:
- 客户端交互:确认、稍后提醒、超时未操作。
- 系统状态:静音、电量、充电状态。
- 日程上下文:前后相邻日程及时间关系。
- 外部环境:天气。
设计分为两个部分:
- 规则引擎决定是否提醒、何时提醒以及是否抑制或补发提醒。
- 内容生成根据规则引擎确认的事实,生成更自然、更符合当前场景的提醒文案。
时间和空间仍然是基础触发条件。新增维度用于对基础触发进行修正和补充,不取代现有时间窗口和地理围栏机制。
系统必须满足“最小闭环优先”原则:只有基础日程数据时,规则引擎、提醒控制和基础文案仍然可以正常运行;客户端交互、系统状态、相邻日程和天气全部属于可选上下文,缺失时不得阻断提醒。
2. 新增维度总表
| 维度 |
数据如何获取 |
负责规则引擎的内容 |
负责提醒文案的内容 |
| 客户端交互 |
用户点击提醒弹窗或弹窗超时,通过 WebSocket 上报 |
确认后完成日程并停止提醒;延迟时到期重触发;超时用于统计无响应次数 |
通常不影响本次文案;可用于后续减少冗余表达 |
| 系统状态 |
客户端读取响铃模式、电量和充电状态,通过 WebSocket 上报 |
通常不改变日程触发时间;低电量时降低位置上报频率 |
一般不进入语义文案,主要影响弹窗、震动和 TTS 是否播放 |
| 日程上下文 |
服务端查询当前用户 schedules,按 UTC 时间计算前后日程 |
上一日程完成且下一日程已进入提醒窗口时,可以触发下一日程提醒;防止重复触发 |
“上一项日程已完成”“下一项日程将在十分钟后开始”等关系说明 |
| 天气 |
服务端通过天气服务查询日程地点或当前位置,并缓存结果 |
恶劣天气可以在正常提醒前增加一次准备提醒;天气无效时不参与判断 |
“预计有雨,请携带雨具”“天气炎热,请注意防暑”等事实描述 |
3. 数据获取方案
3.1 客户端交互
提醒界面只提供两个用户操作,并额外记录一个自动状态:
- 稍后提醒:用户选择新的提醒时间,日程继续保持
scheduled;
- 确认:复用现有
schedule.confirmed,日程进入 done,停止后续监听和提醒。
- 超时未操作:用户没有点击任何按钮,弹窗到达展示时长后自动关闭,客户端上报
timeout。
现有 reminder.control.ack 继续保留,但它只表示客户端成功执行弹窗、震动等提醒动作,由客户端自动回传,不对应界面按钮,也不是用户交互状态。
稍后提醒和超时未操作使用统一的 WebSocket 交互消息:
{
"type": "reminder.interaction.command",
"request_id": "req_interaction_001",
"payload": {
"schedule_id": "schedule_001",
"action": "snoozed",
"delay_minutes": 10,
"occurred_at": "2026-08-03T08:15:00Z"
}
}
超时未操作示例:
{
"type": "reminder.interaction.command",
"request_id": "req_interaction_002",
"payload": {
"schedule_id": "schedule_001",
"action": "timeout",
"occurred_at": "2026-08-03T08:16:00Z"
}
}
服务端通过 reminder.interaction.result/error 返回成功或失败响应。延迟状态和超时统计直接持久化到现有 schedules 表,不能只保存在 WebSocket 连接内存中。
延迟时长采用以下交互方式:
- 点击“稍后提醒”直接使用默认值
10 分钟,不要求用户额外输入;
- 客户端可以提供
5 / 10 / 15 / 30 / 60 分钟等快捷选项;
- 自定义时仍然提交
delay_minutes,服务端负责校验允许范围;
- 服务端根据服务端当前时间计算权威的
snoozed_until,并在成功响应中返回;
time_remind_offset_minutes=15 是创建日程时的提前提醒量,不等同于稍后提醒时长。
这样既保留闹钟式的一键操作,也允许用户在需要时调整延迟时间。
交互行为统一归一化为:
confirmed / snoozed / timeout
其中 confirmed 来源于现有 schedule.confirmed,snoozed 和 timeout 来源于 reminder.interaction.command。系统不单独设计“已读”状态。timeout 是一次提醒轮次的交互事件,不是日程生命周期状态。
各枚举的归属不同:
| 所属层 |
字段或消息 |
枚举值 |
用途 |
| 数据库 |
schedules.status |
scheduled / snoozed / done / deleted |
保存日程生命周期和当前提醒是否处于延迟状态 |
| WebSocket |
reminder.interaction.command.payload.action |
snoozed / timeout |
描述客户端本次用户交互或自动超时事件 |
| WebSocket |
schedule.confirmed |
消息类型 |
表示用户确认完成,服务端将数据库状态改为 done |
| 业务内部 |
ReminderDecision.action |
show / suppress / defer |
规则引擎的临时决策,不写入数据库,也不直接作为 WS 消息 |
稍后提醒不是重新进入普通时间/空间监听,而是进入独立的延迟等待状态:
当前提醒触发
↓
用户选择稍后提醒
↓
关闭当前提醒轮次,保存 snoozed_until
↓
延迟期间抑制该日程的时间、空间和天气候选提醒
↓
snoozed_until 到达
↓
以 snooze_due 原因创建新的提醒轮次
↓
下发该日程当前的正常提醒文案和音频
延迟只改变下一次触发时间,不改变提醒内容,也不需要重新调用 LLM 或 TTS。延迟期间数据库中的 schedules.status 为 snoozed,不修改原始 start_time。即使原始开始时间已经过去,只要存在有效的 snoozed_until,监听任务也不能按普通时间过期规则提前取消。延迟到期后将状态恢复为 scheduled,用户可以再次选择“稍后提醒”或通过 schedule.confirmed 确认完成。
超时未操作本身是一个交互事件,不直接等同于用户主动延迟。但如果产品启用自动重试策略,服务端可以在记录 timeout 后计算下一次重试时间,将 status 设为 snoozed,并复用 snoozed_until 作为系统重试时间。用户主动延迟和超时重试都使用同一套暂停监听机制,但规则引擎仍通过 action 和 timeout_count 区分两种来源。
网络不可用时,客户端必须同步登记本地延迟触发条件;网络恢复后再把最新延迟状态同步到服务端,避免仅依赖服务端 WebSocket 调度而漏提醒。
3.2 系统状态
客户端通过系统能力获取:
| 数据 |
获取内容 |
说明 |
| 响铃模式 |
normal / vibrate / silent |
Android 可通过 AudioManager 获取 |
| 电量 |
0 到 1 的电量值或电量档位 |
建议只在档位变化时上报 |
| 充电状态 |
charging / unplugged / unknown |
用于调整后台采集策略 |
勿扰模式与静音模式不是同一概念。勿扰模式需要额外的系统授权,首期不将“无法读取”解释为“未开启”。
3.3 日程上下文
该数据由服务端根据已有日程数据计算,不需要客户端新增权限:
- 查询当前用户的有效日程。
- 按规范化后的 UTC
start_time 排序。
- 找到当前日程的前序和后序日程。
- 计算相邻日程的时间间隔、重叠关系和前序日程状态。
只有时间信息的日程可以参与时间排序;只有地点没有时间的日程不参与相邻时间计算。
3.4 天气
服务端通过 WeatherProviderPort 调用第三方天气服务,具体供应商由 gateway/ 适配:
- 有目标经纬度时,查询目标地点天气。
- 没有目标地点但有有效当前位置时,查询当前位置天气。
- 两者都没有时,不使用天气维度。
- 有开始时间时查询目标时间附近的天气预报。
- 只有地点时,在接近围栏或准备提醒时查询实时天气。
- 按地点网格和时间区间缓存结果,避免监听轮询重复请求。
首期只需要归一化以下状态:
clear / rain / snow / high_temperature / low_temperature / severe / unknown
天气超时、服务失败或结果过期时,规则引擎忽略天气数据,不影响基础提醒。
4. 规则引擎与内容文案的职责
4.1 规则引擎负责什么
规则引擎只处理可验证的事实和业务决策:
| 判断 |
示例 |
| 是否应取消 |
日程已经完成或删除 |
| 是否应抑制 |
用户设置延迟或超时重试,且 snoozed_until 尚未到达 |
| 是否应重新触发 |
延迟时间到达 |
| 是否应降频 |
同一日程或同类提醒多次超时未操作 |
| 是否应提前 |
目标时间天气恶劣,需要提前准备 |
| 是否应组合上下文 |
上一项日程完成,下一项日程已经进入提醒窗口 |
| 是否应去重 |
同一日程、同一提醒轮次已经下发 |
规则引擎输出一个内部提醒决策:
schedule_id
decision_id
action: show / suppress / defer
reason
effective_at
content_facts
服务端仍然只通过 reminder.control 告诉客户端“该提醒了”,不由服务端决定应用前台、后台等界面状态,也不把提醒方式写入 notify_mode。
4.2 文案生成负责什么
文案生成只接收规则引擎确认过的 content_facts,负责:
- 组织时间、地点和事项标题;
- 描述触发原因;
- 加入已确认的天气信息;
- 加入有效的前后日程信息;
- 输出适合弹窗和 TTS 的简短中文文案。
文案生成不得:
- 决定是否触发提醒;
- 修改日程时间或地点;
- 自行生成天气、距离、交通时间等事实;
- 输出结构化日程写入数据库。
4.3 LLM 调用方式
LLM 是异步内容处理步骤,可以在提醒窗口之前执行:
规则引擎确认提醒候选
↓
组装结构化事实数据
↓
异步调用 LLM 润色文案
↓
校验文案与事实一致
↓
调用 TTS
↓
覆盖当前日程音频
输入示例:
{
"intent": "weather_preparation_reminder",
"facts": {
"title": "去医院",
"start_time": "2026-08-03T09:00+08:00",
"minutes_before_start": 30,
"location_name": "浦东新区人民医院",
"weather": {
"condition": "rain",
"description": "预计有雨"
}
},
"output_requirements": {
"language": "zh-CN",
"max_characters": 80,
"json_only": true
}
}
输出示例:
{
"text": "您九点需要前往浦东新区人民医院,预计有雨,请记得携带雨具。"
}
程序在 TTS 前必须校验:
- 输出是合法 JSON,
text 非空且长度受限。
- 文案只使用输入事实,不新增路况、距离或用户意图。
- 文案不能改变时间、地点和日程标题的含义。
- 校验失败时不写入当前音频,也不覆盖当前有效音频;服务端照常发送
reminder.control,避免播放与当前上下文不匹配的旧任务结果。
4.4 缺失数据兜底
规则引擎先执行基础规则,再应用可选上下文:
基础日程事实
↓
时间窗口或地理围栏判断
↓
得到基础提醒决策
↓
按可用性加载交互、系统、相邻日程和天气
↓
仅使用有效上下文修正决策和文案
基础闭环只依赖:
- 日程 ID、标题和有效状态;
- 时间类日程的开始时间与提醒提前量,或地点类日程的目标坐标与围栏;
- 服务端当前时间或客户端本地触发条件;
- 客户端最基本的提醒展示能力。
可选维度统一遵守以下规则:
- 没有权限、没有上报、数据过期或第三方服务失败时,将该维度视为
unknown。
unknown 不参与规则判断,也不进入 LLM 事实数据。
- 一个可选维度失败不能使整个规则引擎返回失败。
- LLM 在没有可选上下文时,仍使用标题、时间或地点生成基础文案。
- LLM 调用失败时,使用现有确定性基础模板生成弹窗文案;提醒控制不能依赖 LLM 成功。
- TTS 不可用时仍发送
reminder.control,保证弹窗和震动可以执行。
需要明确:地点类日程的地理触发本身依赖定位权限。如果用户拒绝定位权限,系统只能提示地点提醒不可用,或在该日程同时存在时间时继续执行时间提醒,不能使用其他上下文伪造位置触发。
5. 对现有功能的影响
5.1 对日程创建和更新的影响
- 日程创建和更新接口的核心字段不变。
- 创建或更新成功后,除了提交 TTS 任务,还要提交提醒文案生成任务。
schedule.updated_at 继续作为异步任务的版本依据。
- LLM 只能生成提醒文案,不能修改或写入日程字段。
- 日程创建/更新接口不等待 LLM 和 TTS 完成,保持原有异步返回行为。
5.2 对提醒触发的影响
- 现有时间窗口和地理围栏仍是基础触发机制。
- 用户延迟会新增一个
snoozed_until 触发点。
- 相邻日程完成和恶劣天气可以产生新的候选提醒。
- 静音和低电量默认不改变服务端业务触发时间,只影响客户端执行和上下文采集频率。
5.3 对 WebSocket 消息的影响
新增消息:
| 消息类型 |
方向 |
用途 |
device.context.report |
客户端 -> 服务端 |
上报系统状态 |
device.context.report.ack |
服务端 -> 客户端 |
确认上下文接收结果 |
reminder.interaction.command |
客户端 -> 服务端 |
上报稍后提醒或超时未操作 |
reminder.interaction.result/error |
服务端 -> 客户端 |
返回交互处理结果 |
现有消息保持不变:
reminder.control 仍只表示服务端确认需要提醒;
reminder.control.ack 仍只表示客户端执行成功或失败;
reminder.audio.start、Binary Frame、reminder.audio.end 继续承担音频下发;
schedule.confirmed 继续表示日程完成。
5.4 对数据库和缓存的影响
schedules 不增加电量、静音和天气字段。
snoozed_until、timeout_count 和 last_timeout_at 直接加入现有 schedules 表,不新增超时记录表。
timeout_count 用于阈值判断,last_timeout_at 用于时间窗口判断;timeout 仍然是 WebSocket 事件,不是数据库状态,但自动重试策略可以让它触发 schedules.status=snoozed。
- 系统状态、最近位置和天气只保存最新快照并设置 TTL。
- 相邻日程通过查询动态计算,不复制关系数据到
schedules。
- TTS 音频仍使用
schedule_id 对应的当前文件;异步任务必须校验日程版本和上下文版本,避免旧文案覆盖新文案。
5.5 对客户端能力和权限的影响
- 客户端提醒弹窗只提供“确认”和“稍后提醒”两个操作。
- 电量和充电状态通常可以通过系统能力获取。
- 静音状态在 Android 上可读取响铃模式;勿扰模式需要单独授权,不能默认支持。
- 离线时无法实时获取服务端天气和 LLM 文案,继续使用客户端固定提醒能力。
5.6 对目录和模块的影响
按照现有子目录边界落位:
| 目录 |
新增职责 |
business/ |
上下文 DTO、提醒决策、延迟用例、相邻日程规则、文案事实组装 |
data/ |
提醒运行状态 Repository、相邻日程 Query 实现和数据库迁移 |
gateway/ |
天气服务适配器、LLM 文案适配器、现有 TTS 适配器 |
infrastructure/websocket/ |
新 WebSocket 消息 Schema、Handler 和错误映射 |
infrastructure/workers/ |
延迟到期、天气准备窗口和异步文案/TTS 调度 |
infrastructure/runtime/ |
上下文 TTL、采样间隔和提醒策略配置 |
main.py |
所有 Port 与适配器的依赖组装 |
禁止新增 HTTP 业务 API,不恢复 api/ 目录。WebSocket Handler 不直接访问数据库或调用第三方 SDK;LLM 只作为文案生成能力,不能绕过业务规则直接修改日程。
TimeFlow 多维感知智能提醒设计
1. 设计目标
当前系统已经具备时间和空间两类提醒触发条件。本阶段新增以下四类维度:
设计分为两个部分:
时间和空间仍然是基础触发条件。新增维度用于对基础触发进行修正和补充,不取代现有时间窗口和地理围栏机制。
系统必须满足“最小闭环优先”原则:只有基础日程数据时,规则引擎、提醒控制和基础文案仍然可以正常运行;客户端交互、系统状态、相邻日程和天气全部属于可选上下文,缺失时不得阻断提醒。
2. 新增维度总表
schedules,按 UTC 时间计算前后日程3. 数据获取方案
3.1 客户端交互
提醒界面只提供两个用户操作,并额外记录一个自动状态:
scheduled;schedule.confirmed,日程进入done,停止后续监听和提醒。timeout。现有
reminder.control.ack继续保留,但它只表示客户端成功执行弹窗、震动等提醒动作,由客户端自动回传,不对应界面按钮,也不是用户交互状态。稍后提醒和超时未操作使用统一的 WebSocket 交互消息:
{ "type": "reminder.interaction.command", "request_id": "req_interaction_001", "payload": { "schedule_id": "schedule_001", "action": "snoozed", "delay_minutes": 10, "occurred_at": "2026-08-03T08:15:00Z" } }超时未操作示例:
{ "type": "reminder.interaction.command", "request_id": "req_interaction_002", "payload": { "schedule_id": "schedule_001", "action": "timeout", "occurred_at": "2026-08-03T08:16:00Z" } }服务端通过
reminder.interaction.result/error返回成功或失败响应。延迟状态和超时统计直接持久化到现有schedules表,不能只保存在 WebSocket 连接内存中。延迟时长采用以下交互方式:
10分钟,不要求用户额外输入;5 / 10 / 15 / 30 / 60分钟等快捷选项;delay_minutes,服务端负责校验允许范围;snoozed_until,并在成功响应中返回;time_remind_offset_minutes=15是创建日程时的提前提醒量,不等同于稍后提醒时长。这样既保留闹钟式的一键操作,也允许用户在需要时调整延迟时间。
交互行为统一归一化为:
其中
confirmed来源于现有schedule.confirmed,snoozed和timeout来源于reminder.interaction.command。系统不单独设计“已读”状态。timeout是一次提醒轮次的交互事件,不是日程生命周期状态。各枚举的归属不同:
schedules.statusscheduled / snoozed / done / deletedreminder.interaction.command.payload.actionsnoozed / timeoutschedule.confirmeddoneReminderDecision.actionshow / suppress / defer稍后提醒不是重新进入普通时间/空间监听,而是进入独立的延迟等待状态:
延迟只改变下一次触发时间,不改变提醒内容,也不需要重新调用 LLM 或 TTS。延迟期间数据库中的
schedules.status为snoozed,不修改原始start_time。即使原始开始时间已经过去,只要存在有效的snoozed_until,监听任务也不能按普通时间过期规则提前取消。延迟到期后将状态恢复为scheduled,用户可以再次选择“稍后提醒”或通过schedule.confirmed确认完成。超时未操作本身是一个交互事件,不直接等同于用户主动延迟。但如果产品启用自动重试策略,服务端可以在记录
timeout后计算下一次重试时间,将status设为snoozed,并复用snoozed_until作为系统重试时间。用户主动延迟和超时重试都使用同一套暂停监听机制,但规则引擎仍通过action和timeout_count区分两种来源。网络不可用时,客户端必须同步登记本地延迟触发条件;网络恢复后再把最新延迟状态同步到服务端,避免仅依赖服务端 WebSocket 调度而漏提醒。
3.2 系统状态
客户端通过系统能力获取:
normal / vibrate / silentAudioManager获取charging / unplugged / unknown勿扰模式与静音模式不是同一概念。勿扰模式需要额外的系统授权,首期不将“无法读取”解释为“未开启”。
3.3 日程上下文
该数据由服务端根据已有日程数据计算,不需要客户端新增权限:
start_time排序。只有时间信息的日程可以参与时间排序;只有地点没有时间的日程不参与相邻时间计算。
3.4 天气
服务端通过
WeatherProviderPort调用第三方天气服务,具体供应商由gateway/适配:首期只需要归一化以下状态:
天气超时、服务失败或结果过期时,规则引擎忽略天气数据,不影响基础提醒。
4. 规则引擎与内容文案的职责
4.1 规则引擎负责什么
规则引擎只处理可验证的事实和业务决策:
snoozed_until尚未到达规则引擎输出一个内部提醒决策:
服务端仍然只通过
reminder.control告诉客户端“该提醒了”,不由服务端决定应用前台、后台等界面状态,也不把提醒方式写入notify_mode。4.2 文案生成负责什么
文案生成只接收规则引擎确认过的
content_facts,负责:文案生成不得:
4.3 LLM 调用方式
LLM 是异步内容处理步骤,可以在提醒窗口之前执行:
输入示例:
{ "intent": "weather_preparation_reminder", "facts": { "title": "去医院", "start_time": "2026-08-03T09:00+08:00", "minutes_before_start": 30, "location_name": "浦东新区人民医院", "weather": { "condition": "rain", "description": "预计有雨" } }, "output_requirements": { "language": "zh-CN", "max_characters": 80, "json_only": true } }输出示例:
{ "text": "您九点需要前往浦东新区人民医院,预计有雨,请记得携带雨具。" }程序在 TTS 前必须校验:
text非空且长度受限。reminder.control,避免播放与当前上下文不匹配的旧任务结果。4.4 缺失数据兜底
规则引擎先执行基础规则,再应用可选上下文:
基础闭环只依赖:
可选维度统一遵守以下规则:
unknown。unknown不参与规则判断,也不进入 LLM 事实数据。reminder.control,保证弹窗和震动可以执行。需要明确:地点类日程的地理触发本身依赖定位权限。如果用户拒绝定位权限,系统只能提示地点提醒不可用,或在该日程同时存在时间时继续执行时间提醒,不能使用其他上下文伪造位置触发。
5. 对现有功能的影响
5.1 对日程创建和更新的影响
schedule.updated_at继续作为异步任务的版本依据。5.2 对提醒触发的影响
snoozed_until触发点。5.3 对 WebSocket 消息的影响
新增消息:
device.context.reportdevice.context.report.ackreminder.interaction.commandreminder.interaction.result/error现有消息保持不变:
reminder.control仍只表示服务端确认需要提醒;reminder.control.ack仍只表示客户端执行成功或失败;reminder.audio.start、Binary Frame、reminder.audio.end继续承担音频下发;schedule.confirmed继续表示日程完成。5.4 对数据库和缓存的影响
schedules不增加电量、静音和天气字段。snoozed_until、timeout_count和last_timeout_at直接加入现有schedules表,不新增超时记录表。timeout_count用于阈值判断,last_timeout_at用于时间窗口判断;timeout仍然是 WebSocket 事件,不是数据库状态,但自动重试策略可以让它触发schedules.status=snoozed。schedules。schedule_id对应的当前文件;异步任务必须校验日程版本和上下文版本,避免旧文案覆盖新文案。5.5 对客户端能力和权限的影响
5.6 对目录和模块的影响
按照现有子目录边界落位:
business/data/gateway/infrastructure/websocket/infrastructure/workers/infrastructure/runtime/main.py禁止新增 HTTP 业务 API,不恢复
api/目录。WebSocket Handler 不直接访问数据库或调用第三方 SDK;LLM 只作为文案生成能力,不能绕过业务规则直接修改日程。