Files
mai-bot/计划.md

231 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 私聊定时消息 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. 主程序启动
- 在现有后台任务体系中注册该调度器
## 当前建议的落地顺序
- 第一步:先把工具接口和任务表定下来
- 第二步:把任务创建打通
- 第三步:把后台调度发送打通
- 第四步:确认发送后消息能按现有链路进入上下文
- 第五步:再考虑取消/查看任务工具