Skip to content

多维感知智能提醒方案深化设计(草稿) #147

Description

@yyy-router

TimeFlow 多维感知智能提醒设计

1. 设计目标

当前系统已经具备时间和空间两类提醒触发条件。本阶段新增以下四类维度:

  1. 客户端交互:确认、稍后提醒、超时未操作。
  2. 系统状态:静音、电量、充电状态。
  3. 日程上下文:前后相邻日程及时间关系。
  4. 外部环境:天气。

设计分为两个部分:

  • 规则引擎决定是否提醒、何时提醒以及是否抑制或补发提醒。
  • 内容生成根据规则引擎确认的事实,生成更自然、更符合当前场景的提醒文案。

时间和空间仍然是基础触发条件。新增维度用于对基础触发进行修正和补充,不取代现有时间窗口和地理围栏机制。

系统必须满足“最小闭环优先”原则:只有基础日程数据时,规则引擎、提醒控制和基础文案仍然可以正常运行;客户端交互、系统状态、相邻日程和天气全部属于可选上下文,缺失时不得阻断提醒。

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.confirmedsnoozedtimeout 来源于 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.statussnoozed,不修改原始 start_time。即使原始开始时间已经过去,只要存在有效的 snoozed_until,监听任务也不能按普通时间过期规则提前取消。延迟到期后将状态恢复为 scheduled,用户可以再次选择“稍后提醒”或通过 schedule.confirmed 确认完成。

超时未操作本身是一个交互事件,不直接等同于用户主动延迟。但如果产品启用自动重试策略,服务端可以在记录 timeout 后计算下一次重试时间,将 status 设为 snoozed,并复用 snoozed_until 作为系统重试时间。用户主动延迟和超时重试都使用同一套暂停监听机制,但规则引擎仍通过 actiontimeout_count 区分两种来源。

网络不可用时,客户端必须同步登记本地延迟触发条件;网络恢复后再把最新延迟状态同步到服务端,避免仅依赖服务端 WebSocket 调度而漏提醒。

3.2 系统状态

客户端通过系统能力获取:

数据 获取内容 说明
响铃模式 normal / vibrate / silent Android 可通过 AudioManager 获取
电量 0 到 1 的电量值或电量档位 建议只在档位变化时上报
充电状态 charging / unplugged / unknown 用于调整后台采集策略

勿扰模式与静音模式不是同一概念。勿扰模式需要额外的系统授权,首期不将“无法读取”解释为“未开启”。

3.3 日程上下文

该数据由服务端根据已有日程数据计算,不需要客户端新增权限:

  1. 查询当前用户的有效日程。
  2. 按规范化后的 UTC start_time 排序。
  3. 找到当前日程的前序和后序日程。
  4. 计算相邻日程的时间间隔、重叠关系和前序日程状态。

只有时间信息的日程可以参与时间排序;只有地点没有时间的日程不参与相邻时间计算。

3.4 天气

服务端通过 WeatherProviderPort 调用第三方天气服务,具体供应商由 gateway/ 适配:

  1. 有目标经纬度时,查询目标地点天气。
  2. 没有目标地点但有有效当前位置时,查询当前位置天气。
  3. 两者都没有时,不使用天气维度。
  4. 有开始时间时查询目标时间附近的天气预报。
  5. 只有地点时,在接近围栏或准备提醒时查询实时天气。
  6. 按地点网格和时间区间缓存结果,避免监听轮询重复请求。

首期只需要归一化以下状态:

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 前必须校验:

  1. 输出是合法 JSON,text 非空且长度受限。
  2. 文案只使用输入事实,不新增路况、距离或用户意图。
  3. 文案不能改变时间、地点和日程标题的含义。
  4. 校验失败时不写入当前音频,也不覆盖当前有效音频;服务端照常发送 reminder.control,避免播放与当前上下文不匹配的旧任务结果。

4.4 缺失数据兜底

规则引擎先执行基础规则,再应用可选上下文:

基础日程事实
    ↓
时间窗口或地理围栏判断
    ↓
得到基础提醒决策
    ↓
按可用性加载交互、系统、相邻日程和天气
    ↓
仅使用有效上下文修正决策和文案

基础闭环只依赖:

  • 日程 ID、标题和有效状态;
  • 时间类日程的开始时间与提醒提前量,或地点类日程的目标坐标与围栏;
  • 服务端当前时间或客户端本地触发条件;
  • 客户端最基本的提醒展示能力。

可选维度统一遵守以下规则:

  1. 没有权限、没有上报、数据过期或第三方服务失败时,将该维度视为 unknown
  2. unknown 不参与规则判断,也不进入 LLM 事实数据。
  3. 一个可选维度失败不能使整个规则引擎返回失败。
  4. LLM 在没有可选上下文时,仍使用标题、时间或地点生成基础文案。
  5. LLM 调用失败时,使用现有确定性基础模板生成弹窗文案;提醒控制不能依赖 LLM 成功。
  6. 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_untiltimeout_countlast_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 只作为文案生成能力,不能绕过业务规则直接修改日程。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions