部署與用戶端
Control Center 不是單一程式。它是一種精簡用戶端架構:一個擁有你資料的無介面伺服器,加上一組用戶端,透過一條指向該伺服器的 RPC 連線呈現介面。理解這種拆分,就能解釋為何某些東西會在其所在的位置執行,以及每個用戶端能觸及和無法觸及甚麼。
伺服器擁有資料
標題為「伺服器擁有資料」的章節一個名為 cc_server 的行程持有資料庫——用於全伺服器狀態的一個小型 global.db,加上每個工作區一個延後開啟的 SQLite 檔案——並執行長時間存在的工作:管線引擎、協調器、MCP 工具介面與編排監聽器。凡需要比一個視窗存活更久、或兩個用戶端應當看到完全相同內容的東西,都住在那裏。
沒有任何用戶端會自行開啟資料庫檔案。相反,每個用戶端都持有一個 RemoteRpcClient,並透過它發出儲存庫操作與訂閱。伺服器依據已配對裝置的預共享金鑰驗證該連線——然後就不再理會這條連線。工作階段不會繫結至任何工作區。 派送器是無狀態的:每個以工作區為範圍的呼叫,都會在引數中帶上自己的 workspace_id,而省略它的呼叫會遭拒絕。
這是刻意為之,也正是它讓同一部伺服器上的兩個用戶端可以各自處於不同的工作區,而無需一個由伺服器持有、供它們產生分歧的「目前工作區」。隔離改為按每次呼叫執行,分為四步:
- 操作會宣告自己是否以工作區為範圍;若是,而呼叫中又沒有傳來
workspace_id,該呼叫就會遭拒絕。 - 所指名的 id 必須是已註冊的工作區,並須在任何東西開啟資料庫之前檢查——否則未知的 id 會在每個請求上憑空產生一個空的幽靈
workspace.db。未命中就是找不到。 - 呼叫者的
WorkspaceRole必須達到該操作的下限,下限由其類型推導:讀取需要 guest,變更需要 member,破壞性操作需要 admin。 - 會暴露儲存庫內容的操作,還額外需要在它所宣告的級別上取得逐儲存庫授權——
none/read/review/write——因此工作區成員資格絕不會悄然賦予超出程式碼託管平台的權限。
關鍵的一句話是:持有配對金鑰並不是存取邊界;成員資格才是。
為何要這樣拆分
標題為「為何要這樣拆分」的章節有三股壓力推動了朝向伺服器的設計:
- 唯一真實來源。 一部桌面與一部手機,或一部筆記型電腦與一部桌面,應當看到同一套機群狀態。如果各自擁有自己的資料庫,它們就會逐漸偏離。單一伺服器意味着每個用戶端讀取的都是同一份即時資料。
- 網頁無法自給自足。 瀏覽器無法衍生子行程或開啟本機檔案,因此網頁版本永遠無法擁有資料庫或執行代理。它必須與做得到這些的伺服器通訊。與其另建一條僅限網頁的資料路徑,不如把桌面也帶到同一條路徑上。
- 無介面運作。 一部可以跑在小機器上的伺服器——在火車上用電話就能連接,或在任何地方的瀏覽器上連接——比你必須一直開着的圖形介面更有用。
代價是伺服器現在成了一項依賴:如果它沒有執行,甚麼都辦不到。桌面對此的預設答案是自己衍生一個。
用戶端
標題為「用戶端」的章節共有五類用戶端會連上伺服器,各自形態與信任概況都不同。
桌面應用程式
標題為「桌面應用程式」的章節Flutter 桌面應用程式(macOS、Windows、Linux)是完整用戶端。首次啟動時,它會詢問應以何種方式執行,然後記住這個選擇:
- 本機:在本機衍生並監管一個
cc_server——這是預設、自足、單一使用者的設定。桌面會啟動伺服器,由它擁有應用程式支援根目錄下的資料庫,並透過回送與它通訊。 - 遠端:透過安全的 WebSocket 連接到在別處執行的
cc_server。資料位於那部伺服器上;這部桌面純粹是呈現端。
兩種情況下,最終得到的 RPC 用戶端都會覆寫應用程式的資料提供者,因此整個介面——每個畫面、每項功能——都經由伺服器讀寫。桌面從不直接接觸資料庫檔案。
網頁用戶端
標題為「網頁用戶端」的章節網頁版本是同一套 Flutter 應用程式編譯成可在瀏覽器中執行的版本,託管於 app.usectrl.dev,因此無需安裝任何東西。瀏覽器無法衍生子行程,所以網頁用戶端一律是遠端:它透過 wss:// 撥接一個 cc_server,並呈現完整的桌面介面。連上之後,它會針對同一套機群顯示相同的畫面,因為那是同一份程式碼、走同一條 RPC。缺口在於少數幾個需要瀏覽器所不具備之原生能力的地方——下方所述的會議擷取品質層是主要一處,作業系統通知則是另一處。
這就是為何「無需下載」與「無需伺服器」是兩種不同的說法。網頁用戶端省去了安裝;它並沒有省去伺服器。它撥接的對象可以是同一部筆記型電腦上的 cc_server——ws://localhost:9030,這無需憑證,因為瀏覽器視回送為可信——也可以是另一部機器上的,那部機器必須能透過 wss:// 連接,並使用瀏覽器信任的憑證。cc_server 拒絕在沒有 TLS 的情況下繫結非回送位址,因此遠端伺服器意味着它自己的憑證、一個終止 TLS 的代理,或一條提供憑證的通道或 VPN。
組合包本身與它所通訊的伺服器分開託管。獨立的 cc_server 提供 RPC 端點、MCP 介面及其代理,但不提供網頁組合包——伺服器內存在靜態檔案路徑,但獨立二進位檔上沒有設定網頁根目錄,所以它在 / 處沒有任何東西可提供。已發佈的 cc-webapp 容器映像才是託管該組合包的東西,而 scripts/build_web.sh 才是建置它的東西。伺服器只接受其 --allowed-origins 清單中的瀏覽器來源,該清單預設為 https://app.usectrl.dev,因此自行託管該組合包意味着要加入它自己的來源。
媒體(頭像、資訊動態圖片、PR 截圖)經由伺服器的代理端點路由,而非直接觸及上游主機,因此瀏覽器絕不會抓取任意來源。已部署的用戶端會依據一個非敏感的 Cookie,為每個請求蓋上 Content-Security Policy,令只有已連接伺服器的來源獲准。
手機伴侶程式 (cc_remote)
標題為「手機伴侶程式 (cc_remote)」的章節Remote 應用程式是一個獨立、更輕量的用戶端——一個位於 remote.usectrl.dev 的 Flutter 網頁 PWA。它刻意不是完整應用程式。它經由一個由中介撮合、端對端密封的 WebSocket 中繼進行配對,並使用工具介面中以讀取為主的子集:讀取工單、訊息與資訊動態;送出回覆或更新工單。
手機是比本機代理權限更低的主體。一套預設拒絕的工具政策、為每個訊框密封的預共享金鑰、一項把特權操作(例如配對)擋在伴侶用戶端之外的工作階段能力、上文所述的逐呼叫角色與儲存庫授權檢查,以及速率限制,共同阻止一次已批准(或已洩漏)的配對演變成完整的遙控。完整的安全模型見遙控與流動裝置。
MCP 用戶端
標題為「MCP 用戶端」的章節會說 Model Context Protocol 的外部工具——編輯器、其他代理、CLI——連上伺服器的 MCP 介面,並取用具型別的工具註冊表。這是程式化的整合路徑;它並非圖形介面。參閱使用 MCP 伺服器與 MCP 工具參考。
機群執行器 (cc_worker)
標題為「機群執行器 (cc_worker)」的章節cc_worker 是一個無介面的純 Dart 二進位檔,它與一個 cc_server 配對、宣告其主機能力,並拉取租約工作來執行。它透過同一條 RPC 把行程事件串流傳回,並且不持有任何持久狀態:沒有資料庫、沒有驗證、沒有審批、沒有預算——這些永遠不會離開 cc_server。工作節點是肢體,不是第二個大腦。一個權威伺服器,N 個笨肢體;沒有共識,也沒有工作節點之間的流量。
租約執行仍然只是租約管道。agentRun 是唯一會執行真實指令的類型(CC_JOB_COMMAND,或在未提供任何指令時回顯的提示詞)。pipelineStep、codeIndex、goldenRender、benchmark 與 evalBatch 目前以 git --version / rev-parse HEAD 進行探測,以便在不假裝那些工作真的執行過的情況下,演練串流路徑。
它對外宣告的是作業系統、CPU 架構、核心數、RAM、是否可觸及 Flutter SDK,以及主機具備哪些沙盒後端(Windows 上沒有)。專用工作節點亦一律宣告 alwaysOn 與 acceptsParallel,pin → prefer → spill 排程器正是據此進行放置。沒有 GPU 或 ML 探測。
單機桌面對此毫無察覺,因為一般派送根本不經過機群。伺服器主機確實註冊了一個隱含的行程內工作節點,但它只是一個註冊接縫,其中沒有接入任何執行器——真正放到本機工作節點上的工作會失敗,而不是執行。時至今日,只有真正的 cc_worker 會執行租約工作。
甚麼在哪裏執行
標題為「甚麼在哪裏執行」的章節| 關注點 | 伺服器(cc_server) |
機群執行器(cc_worker) |
桌面用戶端 | 網頁/手機用戶端 |
|---|---|---|---|---|
| 資料庫(Drift/SQLite) | 擁有它們 | — | — | — |
| 驗證、審批、預算、身分 | 擁有它們 | — | — | — |
| 管線、協調器、編排監聽器 | 執行它們 | — | — | — |
| MCP 工具介面 | 提供它 | — | — | — |
| 代理執行 | 執行它(或把它租借出去) | 執行租約工作 | — | — |
| 介面呈現 | — | — | 完整 | 完整(網頁)/以讀取為主(手機) |
| 會議擷取(麥克風 + 會議音訊) | — | — | 原生擷取 | 僅網頁,經由螢幕分享音訊 |
| 日曆 OAuth(Google) | 儲存權杖 | — | — | — |
這兩個層級實施沙盒的方式並不相同。cc_server 在存在後端的情況下,把它自己執行的運行包裹在主機作業系統的沙盒之中——macOS 上的 Seatbelt、Linux 與 WSL2 上的 bubblewrap——並在 Windows 上、或在缺少 bwrap 與 socat 時退回其他邊界機制。cc_worker 完全不實施沙盒。 它宣告一項 sandbox 能力,好讓伺服器按平台放置工作,但工作節點中沒有任何東西會加以實施:租約工作會被直接衍生,只受租約環境與分配給它的 worktree 約束。因此,即使主機本身有沙盒,把工作租借出去也會把執行移到沙盒之外。各種情況下運行受甚麼約束,見沙箱與保安。
會議擷取之所以在用戶端上進行,是因為它需要麥克風與會議的音訊——放在機櫃裏的伺服器無法錄製你的通話。錄音隨後由伺服器的管線摘要,筆記則儲存在每個用戶端都能讀到的地方。
網頁用戶端也會錄製,這一點可能會被表格的結構所掩蓋:它在瀏覽器中擷取麥克風加上一條螢幕分享音訊軌,並把 16 kHz PCM 串流傳送到伺服器。它_要求_必須有那條音訊軌,因此在瀏覽器無法提供時會明確失敗。真正僅限桌面的是擷取周邊的品質層——透過原生處理器進行的訊號層級回音消除、輸入電平表與麥克風失效警告、重新執行摘要、取消處理,以及日曆的錄製並連結動作。
配對與信任
標題為「配對與信任」的章節每個用戶端都以一個一次性配置的預共享金鑰來證明自己是誰:
- 本機模式下的桌面每次開機都會產生一個新金鑰,並經由環境變數把它交給所衍生的伺服器,因此桌面上不會持久保存任何機密。
- 遠端用戶端(遠端桌面、網頁、手機)使用由伺服器的
pair指令簽發、並存放在作業系統鑰匙圈中的金鑰(手機上則存放在本機)。
撤銷是即時的,而非惰性。伺服器會監視其已配對裝置表,一旦某部裝置離開了作用中集合,就在數秒內中斷該裝置的任何已開啟工作階段——無論是直連 WebSocket 路徑還是中繼路徑,都一樣。你不必等裝置重新連接才能把它切斷。
金鑰絕不會經過靜態託管方看得到的 URL。當它經由連結送達時——無論在手機上還是瀏覽器裏——它都搭乘在 URL 片段中,也就是 # 之後的部分,而瀏覽器從不把這部分傳送給託管頁面的伺服器。這是值得記住的不變式,而且對兩者都適用。網頁用戶端另有兩個入口:連接表單,你可以在其中直接貼上伺服器與金鑰;以及邀請碼,它會用邀請碼換取一份僅為該瀏覽器簽發的憑證。