跨会话消息发送设计
记录时间:2026-05-20 最近更新:2026-05-24 状态:部分实现,target 已统一为 typed ChatAddress 相关文档:
1. 背景与动机
nahida-bot 当前所有 LLM 工具都绑定在 current_session 上下文中,Agent 无法在对话中主动向其他会话/平台发送消息。但底层管道已经打通:
ChannelRegistry可按 platform 名查找任意通道服务BotAPI.send_message(target, message, channel=...)支持指定 channel + typed targetPOST /api/send已实现跨 session 发送(仅纯文本,要求channel:type:idtarget)OrchestrationPolicy.can_send_session()存根已存在
需要补齐的是 LLM 面向的工具链 和 WebAPI 的富消息能力。
2. 能力分层
跨会话消息涉及三个不同层次的工具,各有明确边界:
┌────────────────────────────────────────────────────────────┐
│ Layer 3: sessions_send(A2A 事件接口) │
│ 语义: Agent → Agent / Session 间事件传递 │
│ 模式: record_only | enqueue │
│ 安全: 必须标记 source,不伪装用户消息 │
├────────────────────────────────────────────────────────────┤
│ Layer 2: message(跨 Channel 发消息) │
│ 语义: Agent → 用户,直接走 Channel 投递 │
│ 能力: 文本 + 附件(图片/文件/音频/视频)+ 回复引用 │
│ 场景: 主动通知、跨平台转发、工具已发回复 │
├────────────────────────────────────────────────────────────┤
│ Layer 1: WebAPI POST /api/send(HTTP 外部接口) │
│ 语义: 外部脚本/CLI → Channel 投递 │
│ 当前: 仅纯文本 │
│ 增强: 支持附件、reply_to │
└────────────────────────────────────────────────────────────┘三者的关键区别:
| 维度 | message 工具 | sessions_send | WebAPI send |
|---|---|---|---|
| 调用方 | LLM Agent | LLM Agent | 外部脚本/CLI |
| 消息形式 | OutboundMessage(完整) | agent/system 事件 | HTTP request |
| 是否触发目标 run | 否(直接投递) | record_only: 否, enqueue: 是 | 否(直接投递) |
| 前置条件 | sessions_list 发现目标 | sessions_list 发现目标 | 知道 typed target |
3. 已有基础设施
| 组件 | 状态 | 位置 |
|---|---|---|
ChannelRegistry | 已实现 | core/channel_registry.py — 按 platform 名查找 ChannelService |
BotAPI.send_message | 已实现 | plugins/api_bridge.py — 支持任意 channel + target |
ChannelService.send_message | 已实现 | Telegram/Milky 各自实现了文本+附件投递 |
POST /api/send | 已实现(纯文本) | gateway/routes/messages.py |
OrchestrationPolicy.can_send_session | 存根 | agent/orchestration/policy.py — 从未被调用 |
sessions_send denylist 引用 | 已存在 | agent/orchestration/service.py、policy.py — 工具未注册 |
OutboundMessage 模型 | 已实现 | plugins/base.py — text/reply_to/reasoning/attachments/extra |
Attachment 模型 | 已实现 | plugins/base.py — type/path/filename/mime_type/caption |
4. 设计详情
4.1 message 工具(Layer 2)
来源:ROADMAP.md Phase 3.6,优先级 P1
让 Agent 能通过 channel service 向任意已注册平台发送消息。这是跨会话消息的最常用形式。
工具契约:
{
"target": "string, required — channel:type:id,例如 milky:group:20001",
"text": "string, required — 消息文本",
"delivery": "notify | record, optional — 默认 notify",
"attachments": [
{
"type": "photo | document | audio | video",
"path": "string — 本地文件路径(工作空间内)",
"caption": "string, optional"
}
]
}实现要点:
- 在
plugins/builtin/commands.py注册message工具 target解析为ChatAddress- 调用
ctx.bot_api.send_message(address.target_id, OutboundMessage(..., extra={"chat_address": address.chat_key}), channel=address.channel) reply_signals的NO_REPLY与本工具协同:Agent 用message工具已发送回复后,主回复可用NO_REPLY避免重复- 附件路径必须在 workspace sandbox 内,防止路径穿越
- 需要权限检查:
check_network_outbound已在BotAPI.send_message中存在
路由:
- Agent 不需要知道 channel 的具体实现,只需提供 typed
target BotAPI.send_message从ChannelRegistry查找对应 ChannelService 并委托投递- 不存在的 channel 返回错误,不存在的 target id 由 channel 层报错
安全:
OrchestrationPolicy增加can_send_message(requester_session_id, target)检查- 子 Agent 默认禁用
message工具(加入 denylist) - 附件路径必须在 workspace 内(复用 workspace sandbox 校验)
4.2 sessions_send 工具(Layer 3)
A2A 最小跨会话事件接口,用于 Agent 间结构化通信。
工具契约:
{
"target_session_id": "string, required",
"message": "string, required",
"mode": "record_only | enqueue"
}编排层自动补充 source="agent:<run_id>"。
模式说明:
| mode | 行为 | 场景 |
|---|---|---|
record_only | 向目标 session 写入 agent/system 事件,不触发 run | 留言、状态同步 |
enqueue | 写入事件 + 排入目标 session lane 触发一个 run | 跨会话任务触发、通知 |
前置依赖:
sessions_list— Agent 需要知道有哪些 session 可以发session_status— 查询目标 session 的运行状态OrchestrationPolicy.can_send_session()— 从存根升级为实际调用
实现要点:
- 在
agent/orchestration/session_tools.py中注册(目前文件不存在,需新建) record_only:向目标 session 的 history 写入一条role=system的消息,metadata 标记event_type=agent_message+sourceenqueue:record_only的行为 + 通过AgentOrchestrator排入目标 session lane- 目标 session 的事件必须标记
source,不能伪装成用户消息
安全:
- 只能发送到自己创建的 child session 或当前 session(首版限制)
can_send_session()复用现有的can_read_session()策略:target 必须是 requester session 或其 child- 子 Agent 默认禁用
sessions_send(已在 denylist 中)
4.3 sessions_list / session_status / sessions_history
sessions_send 和 message 的前置工具。
sessions_list:
// 输入:无参数(列出当前 session 可见的所有 session)
// 输出:
{
"sessions": [
{
"session_id": "telegram:private:12345",
"target": "telegram:private:12345",
"platform": "telegram",
"chat_id": "12345",
"has_active_run": false,
"last_active_at": "2026-05-20T10:00:00Z"
}
]
}首版只返回当前 session + 自己创建的 child session。
session_status:
// 输入
{ "session_id": "string, optional — 默认当前 session" }
// 输出
{
"session_id": "...",
"active_run": { "run_id": "...", "kind": "main", "status": "running" } | null,
"recent_tasks": [{ "task_id": "...", "status": "succeeded", "summary": "..." }],
"queue_depth": 0
}sessions_history(安全过滤版):
// 输入
{
"session_id": "string — 只能读自己或 child session",
"limit": 20,
"offset": 0
}
// 输出
{
"messages": [
{ "role": "user", "content": "...(截断)...", "timestamp": "..." },
{ "role": "assistant", "content": "...(截断)..." }
],
"total": 42,
"truncated": true
}过滤规则:
- 单条消息截断到 2000 字符
- 移除 base64、临时 URL、raw_event、raw provider payload
- 不返回 reasoning 原文,只保留 metadata
- 只允许读当前 session 和自己创建的 child session
4.4 WebAPI POST /api/send 增强(Layer 1)
当前只支持纯文本。增强为支持完整 OutboundMessage。
增强后的请求 Schema:
class SendMessageRequest(BaseModel):
target: str # channel:type:id
text: str
session_id: str | None = None
# 未来增强字段
reply_to: str = ""
attachments: list[AttachmentSchema] = Field(default_factory=list)
class AttachmentSchema(BaseModel):
type: str # "photo" | "document" | "audio" | "video"
path: str # 本地文件路径
filename: str = ""
caption: str = ""实现要点:
- 构造完整的
OutboundMessage而非只传 text - 附件路径需要安全校验:限制在 workspace 或指定目录内
platform/chat_id不再作为写入 schema;旧格式只用于历史查询类接口
4.5 Cron 工具跨 session 参数
当前 _tool_cron_create 读取当前 session 的 typed ChatAddress。如果后续要支持跨 session cron,应增加 typed target 参数。
增强:
cron_create工具增加可选参数target- 不提供时默认使用当前 session 的 typed
ChatAddress - 底层
SchedulerService已支持,只需工具层透传
5. 实施路线
Phase 1:message 工具(Layer 2)
最小可用,让 Agent 能发消息。
Phase 2:Session 发现工具
sessions_send 和 message 都需要先发现目标 session。
Phase 3:sessions_send(Layer 3)
A2A 最小事件接口。
Phase 4:WebAPI 增强(Layer 1)
Phase 5:Cron 跨 session(可选)
6. 不做的事
- 不做多轮 A2A ping-pong — 首版
sessions_send只支持单向事件,不实现 agent 间来回对话 - 不做任意 agent 自主发现 — 首版 session 可见范围限制在当前 session + child session
- 不做复杂 announce/reply 协议 —
ANNOUNCE_SKIP/REPLY_SKIP留给 Phase 3.8 之后 - 不做 WebSocket 消息推送 — WebAPI 只做 REST,实时推送是后续需求
- 不做
sessions_send的 delivery 参数 — 首版只有record_only | enqueue两种模式
7. 与其他系统的协同
| 系统 | 协同方式 |
|---|---|
| 回复信号协议 | message 工具发送后,Agent 主回复用 NO_REPLY 避免重复(ROADMAP §2.10) |
| Subagent 编排 | 子 Agent 完成事件通过 sessions_send(record_only) 投递回父 session(agent-orchestration.md §6.3) |
| Cron 系统 | message 工具让 Agent 可在 cron turn 中向其他 chat 发通知 |
| WebAPI | POST /api/send 增强后,脚本和 CLI 可发送富消息(cron-and-webapi-optimization.md) |
8. 参考源码
- OpenClaw sessions tools:
openclaw\src\agents\tools\sessions-send-tool.ts、sessions-yield-tool.ts、sessions-spawn-tool.ts - OpenClaw A2A send flow:
openclaw\src\agents\tools\sessions-send-tool.a2a.ts - nahida-bot 现有 channel:
nahida_bot/channels/telegram/plugin.py、nahida_bot/channels/milky/plugin.py - nahida-bot BotAPI:
nahida_bot/plugins/api_bridge.py - nahida-bot 编排策略:
nahida_bot/agent/orchestration/policy.py