ACP 持久绑定:Discord 频道与 Telegram 话题

状态:草案

摘要

引入持久 ACP 绑定,将以下对象映射到长期存活的 ACP 会话:

  • Discord 频道(以及需要时的已有线程)
  • Telegram 群/超级群中的论坛话题(chatId:topic:topicId

绑定状态存储在顶层 bindings[] 条目中,使用显式的绑定类型。

这让高流量消息频道中的 ACP 使用变得可预测、持久。用户可以创建专用频道/话题,比如 codexclaude-1claude-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 回退顺序:
    1. bindings[].acp.backend
    2. agents.list[].runtime.acp.backend
    3. 全局 acp.backend
  • modecwdlabel 遵循相同的覆盖模式(绑定覆盖 -> 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 已经通过 MessageThreadIdchatId 携带话题/线程上下文。

新增/扩展组件

  • Telegram 绑定适配器(与 Discord 适配器并行):
    • 按 Telegram 账号注册适配器,
    • 通过规范化会话 ID 进行解析/列举/绑定/解绑/touch。
  • 类型化绑定解析器/索引
    • bindings[] 分为 routeacp 视图,
    • resolveAgentRoute 只处理 route 绑定,
    • 持久 ACP 意图只从 acp 绑定中解析。
  • Telegram 入站绑定解析
    • 在路由最终确定之前解析绑定会话(Discord 已经这样做了)。
  • 持久绑定协调器
    • 启动时:加载配置中的顶层 type: "acp" 绑定,确保 ACP 会话存在,确保绑定存在。
    • 配置变更时:安全地应用差异。
  • 切换模型
    • 不再读取频道本地的 ACP 绑定回退,
    • 持久 ACP 绑定仅从顶层 bindings[].type="acp" 条目获取。

分阶段交付

第一阶段:类型化绑定 schema 基础

  • 扩展配置 schema 支持 bindings[].type 区分器:
    • route
    • acp,带可选的 acp 覆盖对象(modebackendcwdlabel)。
  • 扩展 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-1claude-repo-x)”