跳至內容

聊天橋接

提供方 傳輸值 傳輸方式 空間類型
Slack slack Socket Mode(對外 WSS) slack

Slack 是唯一的提供方。 ChatProvider 是單值枚舉,而 cc_server 只註冊一個外掛。結構描述、每一次查詢,以及每一個唯一索引都以 provider 為鍵,因此 id 永遠不會跨提供方,而新增一個提供方只是伺服器端的變更。

每個工作區 × 提供方對應一個提供方端的應用。

指令名稱是每個應用各自的(預設為 cc,即 /cc)。管理員可以為它改名;此後橋接會沿用最後一次叫用時所用的名稱。

指令 效果
/cc help(或沒有動詞的 /cc) 列出機械人可以做什麼
/cc link <code> 兌換一組一次性代碼,並連結發送者的聊天身分
/cc ticket <title> 為已連結成員回報的問題開立一張本機工單
/cc ticket <title> | <description> 同上,並以第一個 | 之後的全部內容作為描述
未知動詞 回報該未知動詞,並附上說明文字

觸達代理的方式:

手勢 行為
在頻道中使用 @bot … 開啟一個 Slack 討論串和一個對應的 Control Center 空間;代理會在該 Slack 討論串中回覆
在已橋接的 Slack 討論串中回覆 延續同一個 Control Center 空間——毋須提及
頻道中的普通訊息 忽略(既非向機械人發送,也不在已橋接的 Slack 討論串中)
私訊機械人 每個私訊對話對應一個 Control Center 空間;在代理工作期間設定 Slack 討論串標題和「正在思考…」狀態

由提供方描述符宣告;連接對話框正是由它產生,提交內容也由它驗證。

欄位 id 標籤 必填 前置詞 所在位置
botToken Bot user OAuth token 是 xoxb- OAuth & Permissions
appToken App-level token 是 xapp- Basic Information → App-Level Tokens,權限範圍 connections:write
configRefreshToken App configuration refresh token 否 xoxe api.slack.com/apps 上的 App configuration tokens

configRefreshToken 是應用管理憑證:沒有它,連接仍然可用,但無法從 Control Center 建立或編輯機械人。它每次使用都會輪換,而輪換後的值會立即儲存。

值會去除首尾空白,未宣告的鍵會被捨棄,而前置詞不符會被拒絕,且客戶端與伺服器會回傳同一句話。每次連接都必須提供必填欄位;選填欄位留空會保留已儲存的值。

機械人權杖權限範圍:

app_mentions:read
assistant:write
channels:history
chat:write
commands
groups:history
im:history
users:read
users:read.email

機械人事件訂閱:

app_home_opened
app_mention
message.channels
message.groups
message.im

Control Center 在建立或編輯應用時強制施加的結構性設定:已啟用 Socket Mode、沒有請求 URL、Messages 分頁已啟用且可寫入、機械人 always_online、把所需的權限範圍和事件合併進應用已有的內容(絕不減去),以及一條斜線指令,其描述為 File a ticket or link your Control Center account——這是在匯出的 manifest 中識別 Control Center 自有指令的標記。

狀態 傳輸值 含義
未連接 disconnected 此工作區 × 提供方未儲存任何憑證
連接中… connecting 憑證已儲存,傳輸正在撥號或重新撥號
運作中 connected 傳輸已建立並正在接收事件
連接錯誤 error 已儲存但失敗;提供方自身的錯誤會一併顯示

重連採用帶抖動的指數退避,上限為 30 秒。由提供方發起的通訊端輪換會立即重連,不會推高退避。被拒絕的憑證(invalid_auth、not_authed、token_revoked、account_inactive、token_expired、invalid_app_id)或缺少權限範圍屬於終局:橋接會停止重試,直到有新憑證到達。

橋接是照着最豐富的功能面編寫,並逐項能力降級。

能力 Slack 缺失時的效果
streaming 是 整段回覆會在結束時一次性發出
streamingRequiresThread 是 沒有討論串錨點的回覆目標只會發出完整回覆
ephemeralMessages 是 沒有可用回覆句柄的斜線指令應答會被丟棄(拒絕一律在討論串內發出)
threadStatus 是 沒有短暫的「正在思考…」狀態行
threadTitle 是 聊天討論串會保留其預設標題
slashCommands 是 沒有 /cc 介面
taskCards 是 只有文字——回覆上方沒有卡片,也沒有「View in Control Center」連結
maxMessageLength 39,000 較長的回覆會被截斷,並附上指向 Control Center 的提示
maxStreamChunkLength 3,800 追加內容會按段落、行或空格邊界切分

宣告串流並不等於承諾串流:Slack 的串流需要付費方案中的 Agents & AI Apps 功能。呼叫時被拒絕(streaming_not_supported、feature_not_enabled、paid_only、……)會在整條連線的生命週期內被記住,並以 streamingAvailable: false 呈現。Slack 的任務卡片與同一次呼叫同行,因此一個拒絕卡片顯示模式的 Slack 會保留自己的串流,只在整條連線的生命週期內失去一次卡片。

一條串流還必須指明它是為哪位讀者開啟的。Slack 要求在頻道中開啟串流時同時提供接收者用戶及其團隊(私訊則不需要),否則會以 missing_recipient_user_id / missing_recipient_team_id 拒絕——這是每次呼叫都會發生的拒絕,因此橋接會繼續嘗試,而該回合在此期間會以一條已發出的回覆形式到達。接收者就是發問的那個人,取自入站訊息(取其團隊,而非應用程式的團隊,這樣 Slack Connect 頻道才能正確解析)。對於沒有人從聊天發起的回合——即在 Control Center 中就一個已橋接對話啟動的工作——接收者就是其連結建立了該橋接的成員;如果此人之後已解除其聊天帳戶的連結,該回合就會整體發出,而不是以串流傳送。

每個代理回合一張分組卡片(task_display_mode: plan),每個步驟一行。每一行是一次 task_update,由 id + title + status 組成,並在思考就緒時附上一次性的 details。相同 id 的 details 與 output 會無分隔符地串接,因此 details 只發送一次,而 output 從不以串流傳送。計劃的標題就是該請求;回答則是之後單獨追加的一段內容,攜帶一個 markdown_text 分塊。

部分 內容
計劃標題 所提出的請求
初始化行 Working on it…,然後 Cloning …,再然後 Starting the agent…——同一行,標題被取代
思考行 Thinking…,在代理開始推理時加入;思考內容是這一行的 details,在推理結束時發送一次
工具行 每個工具一行(Bash which swift),id 唯一,因此彼此並排
狀態 該行執行期間為進行中;當下一行開始或回合結束時為完成;失敗、中斷及達到回合上限時為錯誤
行動呼籲 回合結束時作為末尾一行出現的 View in Control Center——工作仍在進行時不出現,伺服器沒有可達公網 URL 時也不出現

卡片絕不承載:工具輸出、該允許清單以外的引數,或任何從資料庫讀取的內容。推理文字只是 Thinking… 行的 details(上限 256 個字元),而不是訊息本文。不會重發一張說不出新內容的卡片。提及會立即開啟串流,以免 Slack 討論串陷入沉默;若隨後的回合完全沒有產生文字,就會用一條簡短說明結束同一張卡片,而不是再發一條訊息。在一個從未開啟過串流的空間(沒有卡片,或串流已被拒絕)上,沒有文字的回合仍會發出一條訊息,攜帶已完成的卡片和那條說明。

以串流傳送的卡片與發出的卡片是兩種不同的載荷:

形式 形態 文字
以串流傳送 plan_update 加上以 id 為鍵的 task_update 分塊 純字串,每塊上限 256 個字元
發出 一個 plan 區塊,包裹以 task_id 為鍵的 task_card 富文字

失敗處理:

情況 行為
提供方在追加時拒絕卡片 該追加會立即在不帶卡片的情況下重試;文字仍會即時到達,而在整條連線的生命週期內卡片都被放棄
提供方也拒絕了文字 串流會被關閉,回覆整體發出
串流在部分回覆已到達後中斷 串流會被關閉,且不會重新發出任何內容

關於載荷為何這樣拆分,參見聊天橋接。

卡片在提及被接受時開啟,此時尚未複製程式庫,也尚未產生第一個權杖;隨後的回合沿用同一張卡片和同一條串流。之後的敘述來自領域事件匯流排上的 SpaceProvisioningChanged——與桌面端佈建橫幅從空間列上讀到的進展完全相同。

佈建狀態 敘述
提及已被接受 Working on it…
步驟 repo Cloning <repo>…
步驟 prCheckout Checking out the pull request in <repo>…
步驟 script Running the setup script for <repo>…
步驟 agent Setting up <agent>…
provisioning,尚無步驟 Preparing the workspace…
ready Starting the agent…
代理正在推理 Thinking…
failed Workspace setup failed — details are in Control Center.
cancelled Workspace setup was stopped — details are in Control Center.

初始化敘述需要同時滿足以下四個條件:

條件
提供方能夠渲染卡片
此連線上可使用串流
該空間有一個正在進行中的聊天請求
該處尚未有卡片正在報告某個回合

請求會一直處於進行中,直到某個回合開始或設定失敗。若 ready 事件發生時尚未有卡片開啟,代表這是一個已預熱的空間,會被保持靜默;而聊天中無人等待的佈建——在應用程式中開啟的對話,或在啟動時恢復的滯留對話——完全不產生卡片。

聊天產品在連結中只接受 http(s),其他一概不行,因此行動呼籲無法直接寫出桌面應用程式的 scheme。

屬性 值
路由 GET /open/workspaces/<workspaceId>/(spaces|tickets)/<id>
來源 由伺服器的公網 URL 推導(ws → http,wss → https);回環位址會保留
回應 一個很小的 no-store HTML 頁面,會導覽至 control-center://workspaces/<workspaceId>/(spaces|tickets)/<id>,並顯示一個手動按鈕
驗證 id 必須匹配 [A-Za-z0-9_.-]{1,128};其他任何情況都是 404
存取 公開且惰性——它不讀取任何資料庫,因此不是存在性判定的依據;授權由它開啟的應用程式負責
欄位 限制 說明
appName 1–35 個字元 預設為 Control Center · <workspace name>
botDisplayName 1–80 個字元 預設為 control-center
description 不超過 140 個字元
agentDescription 不超過 300 個字元 顯示在與機械人的私訊上方
command 不超過 32 個字元 正規化:移除開頭的斜線、轉為小寫、移除 [^a-z0-9_-]、留空則回退為 cc
agentEnabled — 是否開放私訊/代理體驗

空白的輸入框代表「保持原樣」,而不是「清空」。這些限制是 Slack 的,也是所有被建模產品中最嚴格的一套。

屬性 值
長度 6 個字元
字母表 23456789ABCDEFGHJKLMNPQRSTVWXYZ(不含 0/O、1/I、U)
有效期 15 分鐘
使用次數 一次
儲存 記憶體中——伺服器重啟會使尚未使用的代碼失效
範圍 一個工作區和一個提供方;簽發新代碼會使該成員之前的一組失效

兌換時以不區分大小寫的方式比對。未知、已過期、屬於其他工作區和來自錯誤提供方的代碼,都會故意以完全相同的方式失敗。

方式 傳輸值 如何
代碼 code 成員兌換了 /cc link <code>
電郵 email 提供方已驗證的電郵匹配到一位已是工作區成員的用戶

全部以工作區為範圍。伺服器是無狀態的,因此每次呼叫都會在 args 中攜帶自己的 workspace_id;分派器會在開啟任何資料庫之前,拒絕省略它的工作區範圍操作。憑證只會_進去_——狀態讀取從不回傳它們。

操作 種類 最低角色 操作類別
chat.providers read guest —
chat.status read guest —
chat.connect mutate admin secretAccess, networkEgress
chat.disconnect destructive admin secretAccess
chat.listUserLinks read guest —
chat.beginUserLink mutate member —
chat.unlinkUser mutate 對自己為 member,對任何其他人則為 admin —
chat.createApp mutate admin secretAccess, networkEgress
chat.setupLink read admin —
chat.botProfile read admin networkEgress
chat.updateBotProfile mutate admin networkEgress

讀取操作會從其種類繼承 guest 下限;沒有明確下限的變更操作需要 member。

chat.setupLink 會根據機械人資料組裝出提供方的應用建立 URL。它不宣告任何操作類別,因為它不讀取任何密鑰,也不撥打任何連線——唯一的呼叫由用戶自己的瀏覽器發出。

一個回應式查詢補全了整個介面:

查詢 種類 發出
chat.watchUserLinks watch 名冊,每當有連結建立或移除時重新推送

它之所以存在,是因為連結發生在_另一_端:成員把一次性代碼輸入到機械人裏,因此客戶端的任何回應都無法把結果帶回來。設定中的名冊和已開啟的「連結我的帳戶」對話框都會跟隨它,一旦成員自己的連結建立,對話框就會切換為確認狀態。

chat.beginUserLink 一律為呼叫者自己的身分簽發一組代碼。chat.updateBotProfile 僅在仍有剩餘步驟時(通常是重新安裝)才回傳 remaining_step;沒有該欄位代表這次編輯已生效。

chat.providers 為每個提供的提供方回傳一個項目——描述符,以及在有連線狀態時的連線狀態。設定介面渲染的一切都來自該載荷,這正是新增一個提供方只是伺服器端變更的原因。

內容 位置
連接憑證 <dataDir>/<workspaceId>/chat_credentials/<provider>.json
尚未完成的引導式設定 <dataDir>/<workspaceId>/chat_credentials/<provider>_setup.json
舊版 Slack 檔案 <dataDir>/<workspaceId>/slack_credentials.json 和 slack_app_setup.json,讀取一次並原地遷移

憑證存放在工作區資料庫旁邊,而不是裏面:刪除工作區會帶走它的聊天權杖,而 workspace.export 或備份只會複製 workspace.db——因此交給別人的工作區不會夾帶仍然有效的機械人權杖。寫入是原子的(暫存檔加上重新命名),並收緊為僅限擁有者存取。

兩者都存放在工作區資料庫中,而該資料庫帶有一份壓平的 v1 基線——因此跨工作區的讀取根本無法編譯。

chat_space_links——一個外部對話 ↔ 一個 Control Center 空間:

欄 備註
id UUID v4
workspace_id 工作區範圍
provider ChatProvider.wire
external_team_id Slack 團隊 T…,對沒有此概念的提供方為空
external_channel_id Slack 頻道 C…、群組 G…、私訊 D…
external_thread_id Slack 討論串錨點;當整個對話就是錨點時(機械人私訊)為 null
cc_space_id REFERENCES spaces(id) ON DELETE CASCADE
created_by_user_id、created_at、last_activity_at

在 (workspace_id, provider, external_channel_id, external_thread_id) 上唯一,並以一個部分索引對 null 討論串 id 施加同樣的約束(SQLite 把 NULL 視為彼此不同)。

chat_user_links——一個外部成員 ↔ 一個 Control Center 用戶:

欄 備註
id UUID v4
workspace_id 工作區範圍
provider ChatProvider.wire
external_team_id、external_user_id Slack 成員 U…
user_id Control Center 用戶(全域,因此沒有外鍵)
method code 或 email
linked_at

在 (workspace_id, provider, external_team_id, external_user_id) 和 (workspace_id, provider, user_id) 上唯一——兩個方向的歸屬都是單值的,而同一個人可以在多個提供方上建立連結。

連結是身分解析,而不是存取授權:成員身分會在每個入站事件上重新檢查,因此指向一個後來已被移除之人的連結會解析為一次拒絕。

橋接空間在建立時會使用提供方的 SpaceKind(slack)。它的行為與面向普通人的空間一樣——像 topic 一樣發出通知和分區——而 isExternalChat 是每種行為(鏡像、徽章、保留)都會經過的檢查。

橋接帶進來的每條訊息都帶有一個 metadata['chat'] 戳記:

{
"chat": {
"provider": "slack",
"teamId": "T024BE7LD",
"channelId": "C0G9QF9GW",
"threadId": "1700000000.000100",
"messageId": "1700000000.000200",
"userId": "U024BE7LH",
"userLabel": "sam"
}
}

這個戳記同時也是回聲防護:出站鏡像會跳過任何帶有它的訊息,正是這一點阻止了入站聊天訊息被直接原樣發回去。

行為 細節
向外轉發的內容 只有代理的文字,加上任務卡片的狀態行。推理文字和工具輸出留在 Control Center 中
串流節奏 回合執行期間節流為每秒刷新一次;卡片和它所解釋的文字在同一次呼叫中傳輸
在 Control Center 中輸入的人類訊息 鏡像到已連結的討論串,並以作者的顯示名稱作為前置詞
空白回合 以一條簡短說明回報(執行失敗、被中斷,或在其回合上限處停止),而不是沉默
去重 以提供方自己的事件 id 為鍵;記住最近 512 條
入口代理 新的已橋接對話會喚醒工作區的 CEO 代理,否則喚醒其第一個代理,再沒有則誰也不喚醒
空間命名 Slack 頻道討論串為 #channel · <request>,私訊為 Slack DM · <request>,標題上限為 48 個字元
故障隔離 單一壞事件絕不會令橋接停擺;連線會繼續服務其他所有對話