ACP 持久绑定:Discord 频道与 Telegram 话题
状态:草案
摘要
引入持久 ACP 绑定,将以下对象映射到长期存活的 ACP 会话:
- Discord 频道(以及需要时的已有线程)
- Telegram 群/超级群中的论坛话题(
chatId:topic:topicId)
绑定状态存储在顶层 bindings[] 条目中,使用显式的绑定类型。
这让高流量消息频道中的 ACP 使用变得可预测、持久。用户可以创建专用频道/话题,比如 codex、claude-1 或 claude-myrepo。
为什么需要
当前线程绑定的 ACP 行为是为 Discord 临时线程工作流设计的。Telegram 没有同样的线程模型——它有群/超级群中的论坛话题。用户需要的是稳定的、常驻的 ACP”工作空间”,而不仅是临时线程会话。
目标
- 支持持久 ACP 绑定:
- Discord 频道/线程
- Telegram 论坛话题(群/超级群)
- 让绑定的权威来源由配置驱动
- 保持
/acp、/new、/reset、/focus和消息投递行为在 Discord 和 Telegram 之间一致 - 保留现有的临时绑定流程用于临时使用
不做的事
- ACP 运行时/会话内部的全面重设计
- 移除现有的临时绑定流程
- 第一次迭代就扩展到所有频道
- 本阶段不实现 Telegram 频道的直接消息话题(
direct_messages_topic_id) - 本阶段不实现 Telegram 私聊话题变体
用户体验方向
1) 两种绑定类型
- 持久绑定:保存在配置中,启动时协调,用于”命名工作空间”频道/话题。
- 临时绑定:仅运行时存在,按空闲/最大存活策略过期。
2) 命令行为
/acp spawn ... --thread here|auto|off继续可用。- 添加显式的绑定生命周期控制:
/acp bind [session|agent] [--persist]/acp unbind [--persist]/acp status包含绑定是persistent还是temporary的信息。
- 在绑定会话中,
/new和/reset会原地重置绑定的 ACP 会话,保持绑定关系不变。
3) 会话身份
- 使用规范化会话 ID:
- Discord:频道/线程 ID。
- Telegram 话题:
chatId:topic:topicId。
- Telegram 绑定绝不能单独用裸话题 ID 做 key。
配置模型(提案)
在顶层 bindings[] 中统一路由和持久 ACP 绑定配置,使用显式的 type 区分:
{
"agents": {
"list": [
{
"id": "main",
"default": true,
"workspace": "~/.openclaw/workspace-main",
"runtime": { "type": "embedded" },
},
{
"id": "codex",
"workspace": "~/.openclaw/workspace-codex",
"runtime": {
"type": "acp",
"acp": {
"agent": "codex",
"backend": "acpx",
"mode": "persistent",
"cwd": "/workspace/repo-a",
},
},
},
{
"id": "claude",
"workspace": "~/.openclaw/workspace-claude",
"runtime": {
"type": "acp",
"acp": {
"agent": "claude",
"backend": "acpx",
"mode": "persistent",
"cwd": "/workspace/repo-b",
},
},
},
],
},
"acp": {
"enabled": true,
"backend": "acpx",
"allowedAgents": ["codex", "claude"],
},
"bindings": [
// 路由绑定(现有行为)
{
"type": "route",
"agentId": "main",
"match": { "channel": "discord", "accountId": "default" },
},
{
"type": "route",
"agentId": "main",
"match": { "channel": "telegram", "accountId": "default" },
},
// 持久 ACP 会话绑定
{
"type": "acp",
"agentId": "codex",
"match": {
"channel": "discord",
"accountId": "default",
"peer": { "kind": "channel", "id": "222222222222222222" },
},
"acp": {
"label": "codex-main",
"mode": "persistent",
"cwd": "/workspace/repo-a",
"backend": "acpx",
},
},
{
"type": "acp",
"agentId": "claude",
"match": {
"channel": "discord",
"accountId": "default",
"peer": { "kind": "channel", "id": "333333333333333333" },
},
"acp": {
"label": "claude-repo-b",
"mode": "persistent",
"cwd": "/workspace/repo-b",
},
},
{
"type": "acp",
"agentId": "codex",
"match": {
"channel": "telegram",
"accountId": "default",
"peer": { "kind": "group", "id": "-1001234567890:topic:42" },
},
"acp": {
"label": "tg-codex-42",
"mode": "persistent",
},
},
],
"channels": {
"discord": {
"guilds": {
"111111111111111111": {
"channels": {
"222222222222222222": {
"enabled": true,
"requireMention": false,
},
"333333333333333333": {
"enabled": true,
"requireMention": false,
},
},
},
},
},
"telegram": {
"groups": {
"-1001234567890": {
"topics": {
"42": {
"requireMention": false,
},
},
},
},
},
},
}
最简示例(无按绑定的 ACP 覆盖)
{
"agents": {
"list": [
{ "id": "main", "default": true, "runtime": { "type": "embedded" } },
{
"id": "codex",
"runtime": {
"type": "acp",
"acp": { "agent": "codex", "backend": "acpx", "mode": "persistent" },
},
},
{
"id": "claude",
"runtime": {
"type": "acp",
"acp": { "agent": "claude", "backend": "acpx", "mode": "persistent" },
},
},
],
},
"acp": { "enabled": true, "backend": "acpx" },
"bindings": [
{
"type": "route",
"agentId": "main",
"match": { "channel": "discord", "accountId": "default" },
},
{
"type": "route",
"agentId": "main",
"match": { "channel": "telegram", "accountId": "default" },
},
{
"type": "acp",
"agentId": "codex",
"match": {
"channel": "discord",
"accountId": "default",
"peer": { "kind": "channel", "id": "222222222222222222" },
},
},
{
"type": "acp",
"agentId": "claude",
"match": {
"channel": "discord",
"accountId": "default",
"peer": { "kind": "channel", "id": "333333333333333333" },
},
},
{
"type": "acp",
"agentId": "codex",
"match": {
"channel": "telegram",
"accountId": "default",
"peer": { "kind": "group", "id": "-1009876543210:topic:5" },
},
},
],
}
说明:
bindings[].type是显式的:route:普通的 agent 路由。acp:匹配会话的持久 ACP harness 绑定。
- 对于
type: "acp",match.peer.id是规范化的会话 key:- Discord 频道/线程:原始频道/线程 ID。
- Telegram 话题:
chatId:topic:topicId。
bindings[].acp.backend是可选的。Backend 回退顺序:bindings[].acp.backendagents.list[].runtime.acp.backend- 全局
acp.backend
mode、cwd和label遵循相同的覆盖模式(绑定覆盖 -> agent 运行时默认 -> 全局/默认行为)。- 保留现有的
session.threadBindings.*和channels.discord.threadBindings.*用于临时绑定策略。 - 持久条目声明期望状态;运行时协调到实际的 ACP 会话/绑定。
- 每个会话节点同时只有一个活跃的 ACP 绑定。
- 向后兼容:缺少
type的条目被当作route处理。
Backend 选择
- ACP 会话初始化已经在 spawn 时使用配置的 backend 选择(当前是
acp.backend)。 - 本提案扩展 spawn/协调逻辑,优先使用类型化的 ACP 绑定覆盖:
bindings[].acp.backend用于按会话的本地覆盖。agents.list[].runtime.acp.backend用于按 agent 的默认值。
- 如果没有覆盖,保持当前行为(
acp.backend默认值)。
在现有系统中的架构适配
复用已有组件
SessionBindingService已经支持频道无关的会话引用。- ACP spawn/bind 流程已经支持通过服务 API 进行绑定。
- Telegram 已经通过
MessageThreadId和chatId携带话题/线程上下文。
新增/扩展组件
- Telegram 绑定适配器(与 Discord 适配器并行):
- 按 Telegram 账号注册适配器,
- 通过规范化会话 ID 进行解析/列举/绑定/解绑/touch。
- 类型化绑定解析器/索引:
- 将
bindings[]分为route和acp视图, resolveAgentRoute只处理route绑定,- 持久 ACP 意图只从
acp绑定中解析。
- 将
- Telegram 入站绑定解析:
- 在路由最终确定之前解析绑定会话(Discord 已经这样做了)。
- 持久绑定协调器:
- 启动时:加载配置中的顶层
type: "acp"绑定,确保 ACP 会话存在,确保绑定存在。 - 配置变更时:安全地应用差异。
- 启动时:加载配置中的顶层
- 切换模型:
- 不再读取频道本地的 ACP 绑定回退,
- 持久 ACP 绑定仅从顶层
bindings[].type="acp"条目获取。
分阶段交付
第一阶段:类型化绑定 schema 基础
- 扩展配置 schema 支持
bindings[].type区分器:route,acp,带可选的acp覆盖对象(mode、backend、cwd、label)。
- 扩展 agent schema,用运行时描述符标记 ACP 原生 agent(
agents.list[].runtime.type)。 - 添加路由 vs ACP 绑定的解析器/索引器拆分。
第二阶段:运行时解析 + Discord/Telegram 对等
- 从顶层
type: "acp"条目中解析持久 ACP 绑定:- Discord 频道/线程,
- Telegram 论坛话题(
chatId:topic:topicId规范化 ID)。
- 实现 Telegram 绑定适配器和入站绑定会话覆盖,达到与 Discord 对等。
- 本阶段不包含 Telegram 直接/私有话题变体。
第三阶段:命令对等和重置
- 对齐绑定的 Telegram/Discord 会话中
/acp、/new、/reset和/focus的行为。 - 确保绑定在配置的重置流程中保持存活。
第四阶段:加固
- 更好的诊断(
/acp status、启动协调日志)。 - 冲突处理和健康检查。
边界和策略
- 严格遵守当前的 ACP 启用和沙箱限制。
- 保持显式的账号作用域(
accountId)以避免跨账号泄露。 - 路由模糊时闭合失败。
- 每个频道配置的 mention/访问策略行为保持显式。
测试计划
- 单元测试:
- 会话 ID 规范化(尤其是 Telegram 话题 ID),
- 协调器的创建/更新/删除路径,
/acp bind --persist和 unbind 流程。
- 集成测试:
- 入站 Telegram 话题 -> 绑定的 ACP 会话解析,
- 入站 Discord 频道/线程 -> 持久绑定优先级。
- 回归测试:
- 临时绑定继续工作,
- 未绑定的频道/话题保持当前路由行为。
待定问题
- 在 Telegram 话题中
/acp spawn --thread auto是否应该默认为here? - 持久绑定是否应该在绑定会话中总是绕过 mention 门控,还是需要显式设置
requireMention=false? /focus是否应该增加--persist作为/acp bind --persist的别名?
上线策略
- 按会话 opt-in 发布(需要有
bindings[].type="acp"条目)。 - 先只支持 Discord + Telegram。
- 添加文档,包含以下使用示例:
- “每个 agent 一个频道/话题”
- “同一个 agent 多个频道/话题,使用不同的
cwd” - “团队命名模式(
codex-1、claude-repo-x)”