聊天橋接
供應商
標題為「供應商」的章節| 供應商 | 傳輸值 | 傳輸方式 | 空間類型 |
|---|---|---|---|
| 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 上的應用程式設定權杖 |
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),其他一概不行,因此行動呼籲無法直接寫出桌面應用程式的自訂連結通訊協定。
| 屬性 | 值 |
|---|---|
| 路由 | 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 操作」的章節全部以工作區為範圍。伺服器是無狀態的,因此每次呼叫都會在自己的引數中攜帶 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 帳號
- 自訂聊天機器人
- 聊天橋接
- 護欄:說明這些操作所宣告的動作類別