ACP 持久綁定:Discord 頻道與 Telegram 主題

狀態:草案

摘要

引入持久 ACP 綁定,將以下對象對應至長期存在的 ACP session:

  • Discord 頻道(以及需要時的現有執行緒),以及
  • Telegram 群組/超級群組中的論壇主題(chatId:topic:topicId

綁定狀態儲存在最上層的 bindings[] 項目中,使用明確的綁定型別。

這讓高流量訊息頻道中的 ACP 使用變得可預測且持久,使用者可建立如 codexclaude-1claude-myrepo 等專屬頻道/主題。

動機

目前以執行緒為基礎的 ACP 行為是針對短暫的 Discord 執行緒工作流最佳化的。Telegram 沒有相同的執行緒模型;它在群組/超級群組中有論壇主題。使用者想要穩定、常駐的 ACP「工作區」聊天介面,而不僅是臨時的執行緒 session。

目標

  • 支援以下對象的持久 ACP 綁定:
    • Discord 頻道/執行緒
    • Telegram 論壇主題(群組/超級群組)
  • 以設定檔驅動綁定,作為唯一的事實來源。
  • 保持 /acp/new/reset/focus 及訊息投遞行為在 Discord 和 Telegram 之間的一致性。
  • 保留既有的臨時綁定流程供隨意使用。

非目標

  • 全面重新設計 ACP 執行時/session 內部機制。
  • 移除既有的短暫綁定流程。
  • 第一次迭代就擴展到所有頻道。
  • 本階段實作 Telegram 頻道直接訊息主題(direct_messages_topic_id)。
  • 本階段實作 Telegram 私人聊天主題變體。

UX 方向

1) 兩種綁定型別

  • 持久綁定:儲存在設定檔中,啟動時進行調和,用於「具名工作區」頻道/主題。
  • 臨時綁定:僅存在於執行時,依閒置/最大存活時間策略過期。

2) 指令行為

  • /acp spawn ... --thread here|auto|off 仍然可用。
  • 新增明確的綁定生命週期控制:
    • /acp bind [session|agent] [--persist]
    • /acp unbind [--persist]
    • /acp status 顯示綁定屬於 persistenttemporary
  • 在已綁定的對話中,/new/reset 會就地重置綁定的 ACP session 並保持綁定連結。

3) 對話識別碼

  • 使用正規化的對話 ID:
    • Discord:頻道/執行緒 ID。
    • Telegram 主題:chatId:topic:topicId
  • Telegram 綁定絕不單獨以主題 ID 作為鍵值。

設定模型(提議)

在最上層的 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:一般代理路由。
    • acp:針對匹配對話的持久 ACP harness 綁定。
  • type: "acp"match.peer.id 為正規化的對話鍵:
    • Discord 頻道/執行緒:原始頻道/執行緒 ID。
    • Telegram 主題:chatId:topic:topicId
  • bindings[].acp.backend 為選用。後端回退順序:
    1. bindings[].acp.backend
    2. agents.list[].runtime.acp.backend
    3. 全域 acp.backend
  • modecwdlabel 遵循相同的覆寫模式(綁定覆寫 -> 代理執行時預設 -> 全域/預設行為)。
  • 保留既有的 session.threadBindings.*channels.discord.threadBindings.* 用於臨時綁定策略。
  • 持久項目宣告期望狀態;執行時調和為實際的 ACP session/綁定。
  • 每個對話節點只能有一個活躍的 ACP 綁定。
  • 向後相容:缺少 type 的項目會被視為 route

後端選擇

  • ACP session 初始化在 spawn 時已使用設定好的後端選擇(目前的 acp.backend)。
  • 本提案擴展 spawn/調和邏輯,優先使用帶型別的 ACP 綁定覆寫:
    • bindings[].acp.backend 用於逐對話覆寫。
    • agents.list[].runtime.acp.backend 用於逐代理預設。
  • 若無覆寫,維持現行行為(acp.backend 預設值)。

與現有系統的架構適配

重用既有元件

  • SessionBindingService 已支援與頻道無關的對話參照。
  • ACP spawn/bind 流程已透過服務 API 支援綁定。
  • Telegram 已透過 MessageThreadIdchatId 攜帶主題/執行緒上下文。

新增/擴展的元件

  • Telegram 綁定轉接器(與 Discord 轉接器並行):
    • 為每個 Telegram 帳號註冊轉接器,
    • 透過正規化的對話 ID 進行解析/列表/綁定/解綁/觸碰。
  • 帶型別的綁定解析器/索引
    • bindings[] 分為 routeacp 視圖,
    • resolveAgentRoute 僅作用於 route 綁定,
    • 持久 ACP 意圖僅從 acp 綁定解析。
  • Telegram 入站綁定解析
    • 在路由最終確定前解析已綁定的 session(Discord 已實作此功能)。
  • 持久綁定調和器
    • 啟動時:載入設定的最上層 type: "acp" 綁定,確保 ACP session 存在,確保綁定存在。
    • 設定變更時:安全套用差異。
  • 轉換模型
    • 不讀取頻道層級的 ACP 綁定回退,
    • 持久 ACP 綁定僅來源於最上層 bindings[].type="acp" 項目。

分階段交付

第一階段:帶型別的綁定 schema 基礎

  • 擴展設定 schema 以支援 bindings[].type 辨別器:
    • route
    • acp 搭配選用的 acp 覆寫物件(modebackendcwdlabel)。
  • 擴展代理 schema,透過執行時描述器標記 ACP 原生代理(agents.list[].runtime.type)。
  • 新增路由 vs ACP 綁定的解析器/索引分離。

第二階段:執行時解析 + Discord/Telegram 平衡

  • 從最上層 type: "acp" 項目解析持久 ACP 綁定,適用於:
    • Discord 頻道/執行緒,
    • Telegram 論壇主題(chatId:topic:topicId 正規化 ID)。
  • 實作 Telegram 綁定轉接器,以及與 Discord 對等的入站已綁定 session 覆寫。
  • 本階段不包含 Telegram 直接/私人主題變體。

第三階段:指令對等與重置

  • 統一 /acp/new/reset/focus 在已綁定 Telegram/Discord 對話中的行為。
  • 確保綁定在重置流程中依設定保留。

第四階段:穩固化

  • 更完善的診斷(/acp status、啟動調和日誌)。
  • 衝突處理與健康檢查。

安全機制與策略

  • 完全遵循現有 ACP 啟用與沙箱限制。
  • 保持明確的帳號範圍限定(accountId)以避免跨帳號洩漏。
  • 路由模糊時預設拒絕。
  • 每個頻道設定維持明確的提及/存取策略行為。

測試計畫

  • 單元:
    • 對話 ID 正規化(特別是 Telegram 主題 ID),
    • 調和器的建立/更新/刪除路徑,
    • /acp bind --persist 及解綁流程。
  • 整合:
    • 入站 Telegram 主題 -> 已綁定 ACP session 解析,
    • 入站 Discord 頻道/執行緒 -> 持久綁定優先順序。
  • 回歸:
    • 臨時綁定持續正常運作,
    • 未綁定的頻道/主題維持現行路由行為。

待決問題

  • 在 Telegram 主題中使用 /acp spawn --thread auto 時,是否應預設為 here
  • 持久綁定是否應在已綁定的對話中一律跳過提及限制,或需要明確設定 requireMention=false
  • /focus 是否應加入 --persist 作為 /acp bind --persist 的別名?

上線策略

  • 以逐對話選擇加入的方式發布(存在 bindings[].type="acp" 項目即啟用)。
  • 初期僅支援 Discord + Telegram。
  • 附上範例文件:
    • 「每個代理一個頻道/主題」
    • 「同一代理多個頻道/主題搭配不同 cwd
    • 「團隊命名模式(codex-1claude-repo-x)」。