聊天橋接
提供方
標題為「提供方」的章節| 提供方 | 傳輸值 | 傳輸方式 | 空間類型 |
|---|---|---|---|
| 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 討論串標題和「正在思考…」狀態 |
憑證欄位(Slack)
標題為「憑證欄位(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 建立或編輯機械人。它每次使用都會輪換,而輪換後的值會立即儲存。
值會去除首尾空白,未宣告的鍵會被捨棄,而前置詞不符會被拒絕,且客戶端與伺服器會回傳同一句話。每次連接都必須提供必填欄位;選填欄位留空會保留已儲存的值。
Slack 應用要求
標題為「Slack 應用要求」的章節機械人權杖權限範圍:
app_mentions:readassistant:writechannels:historychat:writecommandsgroups:historyim:historyusers:readusers:read.email機械人事件訂閱:
app_home_openedapp_mentionmessage.channelsmessage.groupsmessage.imControl 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)
標題為「提供方能力(Slack)」的章節橋接是照着最豐富的功能面編寫,並逐項能力降級。
| 能力 | 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 事件發生時尚未有卡片開啟,代表這是一個已預熱的空間,會被保持靜默;而聊天中無人等待的佈建——在應用程式中開啟的對話,或在啟動時恢復的滯留對話——完全不產生卡片。
/open 跳轉頁
標題為「/open 跳轉頁」的章節聊天產品在連結中只接受 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 |
提供方已驗證的電郵匹配到一位已是工作區成員的用戶 |
RPC 操作
標題為「RPC 操作」的章節全部以工作區為範圍。伺服器是無狀態的,因此每次呼叫都會在 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 個字元 |
| 故障隔離 | 單一壞事件絕不會令橋接停擺;連線會繼續服務其他所有對話 |
另請參閱
標題為「另請參閱」的章節- 設定 Slack 整合
- 連結你的 Slack 帳戶
- 自訂聊天機械人
- 聊天橋接
- 護欄——了解這些操作所宣告的操作類別