# 私聊定时消息 V1 计划 ## 目标 - 只支持私聊,不支持群聊。 - 由 LLM 在当前轮直接写好未来要发送的文本。 - 到点后不再唤醒 LLM,不再二次规划,直接发送。 - 尽量复用现有消息发送、消息存储、Maisaka 上下文、tool call 记录链路。 ## 明确不做 - 不做群聊定时发言。 - 不做“到点后再让 LLM 判断该不该发”。 - 不做 `intent_key` 这类没有明确行为差异的分类字段。 - 不做复杂任务编排,只做“一条任务,到点发一条文本消息”。 ## 端到端流转链路 ### 1. 用户触发 - 用户在私聊中表达未来某个时间点需要 bot 主动发一条消息。 - 当前消息照常进入现有接收链路: - 消息接收 - HeartFlow runtime - Timing Gate - Planner ### 2. Planner 决策 - Planner 在分析当前局势后,直接调用新的定时消息工具。 - 工具由 Planner 直接可见,不走 deferred tools。 - 理由: - 这是明确的主流程动作,不是低频扩展工具。 - 用户说“明天早上提醒我”时,Planner 应当能直接执行,不应先 `tool_search`。 ### 3. 工具执行 - 新工具暂定名:`schedule_private_message` - 工具接收参数后,直接写入任务存储。 - 工具返回成功结果给当前 Planner。 - 该 tool result 和其他工具一样,走现有 tool call 记录与上下文写回链路。 - Planner 下一轮可继续: - 调用 `reply` - 告知用户“已经帮你定好了” - 或继续补充说明 ### 4. 调度器轮询 - 独立后台调度器定期扫描到期任务。 - 扫描条件: - `status = pending` - `send_at <= now` - `chat_type = private` - 调度器取出任务后尝试执行发送。 ### 5. 到点发送 - 调度器直接调用现有 `send_service.text_to_stream_with_message(...)` - 发送参数要求: - `text = 任务里保存的 message_text` - `stream_id = 任务对应的私聊 session_id` - `storage_message = True` - `sync_to_maisaka_history = True` - `maisaka_source_kind = "scheduled_send"` - 这样发送后会自动: - 走现有平台发送链路 - 写入现有消息存储 - 触发现有 memory automation - 在 runtime 仍存活时同步写入 Maisaka 上下文 ### 6. 发送后的上下文表现 - 如果该 session 的 runtime 当前仍在 `heartflow_manager.heartflow_chat_list` 中: - 发送完成后,消息会立刻追加进 `_chat_history` - 如果 runtime 当前不在内存中: - 这条消息仍会进入现有消息存储 - 之后会话重新活跃时,至少数据库层面仍保留这条 bot 已发送消息 - V1 先不为“runtime 不在内存时立即补上下文”额外新造链路 - 先复用现有发送与存储通道 ## 工具设计 ### 工具名 - `schedule_private_message` ### 工具职责 - 为当前私聊创建一条未来发送任务 - 可选择覆盖当前私聊中尚未执行的旧任务 ### 工具参数 - `send_at` - 类型:`string` - 含义:发送时间 - 约定:先存标准化后的绝对时间字符串,内部落库再转时间戳 - `message_text` - 类型:`string` - 含义:到点时直接发送的文本 - `replace_existing` - 类型:`boolean` - 含义:是否覆盖当前私聊里尚未执行的旧任务 - 默认建议:`false` ### 工具隐含上下文 - 不要求 LLM 传 `session_id` - 工具从当前 `ToolExecutionContext` / runtime 中直接拿当前私聊 `session_id` - 如果当前不是私聊,工具直接失败 ### 工具返回 - 成功时至少返回: - `task_id` - `session_id` - `send_at` - `message_text` - `replace_existing` - 若发生覆盖,返回被取消的旧任务 ID 列表 - 失败时返回明确原因: - 非私聊 - 时间非法 - 文本为空 - 存储失败 ## 任务数据模型 ### 建议字段 - `id` - `session_id` - `chat_type` - `message_text` - `send_at_ts` - `status` - `created_at_ts` - `updated_at_ts` - `created_by_tool_call_id` - `cancelled_by_tool_call_id` - `sent_message_id` - `sent_at_ts` - `last_error` - `replace_existing` ### 状态 - `pending` - `sent` - `cancelled` - `failed` ### 状态语义 - `pending`:已创建,等待发送 - `sent`:已成功发出 - `cancelled`:被显式取消或被新任务覆盖 - `failed`:尝试发送失败,保留错误信息 ## 覆盖规则 ### V1 定义 - `replace_existing = true` 时: - 将当前 `session_id` 下所有 `pending` 任务置为 `cancelled` - 再创建当前新任务 - `replace_existing = false` 时: - 不取消旧任务 - 直接新增当前任务 ### 这样做的原因 - 不引入模糊分类 - 不猜“哪个旧任务和哪个新任务算同类” - 规则简单、确定、易解释 ## 取消与改期 ### V1 建议 - 不单独做“改期”底层能力 - 改期等价于: - 取消旧任务 - 新建新任务 - 后续可再补专用工具: - `cancel_scheduled_private_message` - `list_scheduled_private_messages` ### 当前阶段 - 当前文档先只覆盖“创建并发送”的最小闭环 - 取消工具可以作为下一步扩展 ## 到点前的最小状态校验 - 任务当前仍是 `pending` - 任务所属 `chat_type = private` - 当前时间已到 `send_at_ts` - 任务尚未发送过 - 任务未被取消 ## 明确不作为校验项的内容 - 不检查“用户后来是否又聊天” - 不让 LLM 到点时重新读上下文判断 - 不根据最近聊天内容自动反悔 ## 与现有链路的复用点 ### 发送 - 复用 `send_service.text_to_stream_with_message(...)` ### 存储 - 复用现有消息发送成功后的消息存储链路 ### 上下文 - 复用 `sync_to_maisaka_history=True` - 复用 runtime 的 `append_sent_message_to_chat_history(...)` ### Tool call 记录 - 创建任务时,复用现有 Planner tool 执行记录链路 - 到点执行时,不强行伪造一条新的 LLM tool call - 到点执行属于调度器行为,不属于当轮 Planner 推理 ## 需要补的实现模块 ### 1. 新内置工具 - 新增 `schedule_private_message` builtin tool ### 2. 任务存储 - 新增一张定时消息任务表 - 提供最小 CRUD: - create - cancel pending by session - list due pending - mark sent - mark failed ### 3. 后台调度器 - 独立异步循环 - 固定间隔扫描 due tasks - 执行发送并更新状态 ### 4. 主程序启动 - 在现有后台任务体系中注册该调度器 ## 当前建议的落地顺序 - 第一步:先把工具接口和任务表定下来 - 第二步:把任务创建打通 - 第三步:把后台调度发送打通 - 第四步:确认发送后消息能按现有链路进入上下文 - 第五步:再考虑取消/查看任务工具