部署與用戶端
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——因此工作區成員身分絕不會默默賦予 超出程式碼代管的權限。
關鍵的一句話是:持有配對金鑰並不是存取邊界;成員身分才是。
為什麼要這樣拆分
標題為「為什麼要這樣拆分」的章節有三股壓力推動了走向伺服器架構:
- 單一事實來源。 一台桌面和一部手機,或一台筆電和一台桌面, 應該看到同一套機群狀態。如果各自擁有自己的資料庫,它們就會逐漸產生落差。 單一伺服器意味著每個用戶端讀取的都是同一份即時資料。
- Web 無法自給自足。 瀏覽器無法啟動子處理程序或開啟本機檔案,因此 Web 版本永遠無法擁有資料庫或執行代理。它必須與做得到的伺服器通訊。與其另建 一條僅限 Web 的資料路徑,不如把桌面也帶到同一條路徑上。
- 無介面運作。 一台可以跑在小機器上的伺服器——在火車上用電話就能觸及, 或從任何地方的瀏覽器觸及——比一個你必須一直開著的圖形介面更有用。
取捨在於,伺服器現在成了一項相依性:如果它沒有在執行,什麼都無法運作。桌面 對此的預設答案,就是自行啟動一個。
用戶端
標題為「用戶端」的章節共有五種用戶端會連上伺服器,各自的形態與信任特性都不同。
桌面應用程式
標題為「桌面應用程式」的章節Flutter 桌面應用程式(macOS、Windows、Linux)是完整用戶端。首次啟動時,它會 詢問要以何種方式執行,然後記住這個選擇:
- 本機:在這台機器上啟動並監督一個
cc_server——這是預設、自成一體、 單一使用者的設定。桌面會啟動該伺服器,由它擁有應用程式支援根目錄下的 資料庫,並透過回送與其通訊。 - 遠端:透過安全的 WebSocket 連線到在其他位置執行的
cc_server。資料 存放在那台伺服器上;這台桌面純粹只是渲染器。
在兩種情況下,產生的 RPC 用戶端都會覆寫應用程式的資料提供者,因此整個 UI——每個畫面、每項功能——都透過伺服器讀寫。桌面絕不會直接接觸資料庫檔案。
Web 用戶端
標題為「Web 用戶端」的章節Web 版本是同一個 Flutter 應用程式編譯成可在瀏覽器中執行的形式,代管於
app.usectrl.dev,因此無需安裝任何東西。瀏覽器
無法啟動子處理程序,所以 Web 用戶端一律是遠端的:它透過 wss:// 撥接
一個 cc_server,並渲染完整的桌面介面。連線之後,它會針對同一套機群顯示
相同的畫面,因為那是同一份程式碼、走同一條 RPC。缺口在於少數幾處需要瀏覽器
所不具備的原生能力——下面提到的會議擷取品質層是主要的一處,作業系統通知則是
另一處。
這就是為什麼「無需下載」和「無需伺服器」是兩種不同的說法。Web 用戶端省去了
安裝;它並沒有省去伺服器。它撥接的對象可以是同一台筆電上的
cc_server——ws://localhost:9030,這不需要憑證,因為瀏覽器把回送視為
可信任——也可以是另一台機器上的,而那台機器必須能以 wss:// 觸及,並使用
瀏覽器信任的憑證。cc_server 拒絕在沒有 TLS 的情況下繫結非回送位址,因此
遠端伺服器意味著它自己的憑證、一個終止 TLS 的代理,或是一條提供憑證的通道
或 VPN。
套件組合本身與它所通訊的伺服器分開代管。獨立的 cc_server 會提供 RPC
端點、MCP 介面及其代理端點,但不會提供 Web 套件組合——伺服器中確實有
靜態檔案路徑,但獨立二進位檔上並未設定 Web 根目錄,因此它在 / 沒有任何
東西可以提供。已發佈的 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 Web 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) |
桌面用戶端 | Web/手機用戶端 |
|---|---|---|---|---|
| 資料庫(Drift/SQLite) | 擁有它們 | — | — | — |
| 驗證、核准、預算、身分 | 擁有它 | — | — | — |
| 管線、協調器、編排監聽器 | 執行它 | — | — | — |
| MCP 工具介面 | 提供它 | — | — | — |
| 代理執行 | 執行它(或將其租借出去) | 執行租約工作 | — | — |
| UI 渲染 | — | — | 完整 | 完整(Web)/以讀取為主(手機) |
| 會議擷取(麥克風 + 會議音訊) | — | — | 原生擷取 | 僅限 Web,透過螢幕分享音訊 |
| 日曆 OAuth(Google) | 儲存權杖 | — | — | — |
這兩個層級實施沙箱的方式並不相同。cc_server 在後端存在時,會把它自己執行
的運行包裝在作業系統的沙箱中——macOS 上的 Seatbelt、Linux 與 WSL2 上的
bubblewrap——並在 Windows 上或缺少 bwrap 與 socat 時退回其他邊界機制。
cc_worker 完全不實施沙箱。 它會宣告 sandbox 能力,讓伺服器能依平台
配置工作,但工作程序中沒有任何東西會實際套用沙箱:租約工作會被直接啟動,
僅受租約環境與分配給它的工作樹所約束。因此,即使主機本身有沙箱,把工作租借
出去也會讓執行移到沙箱之外。各種情況下執行受什麼約束,請參閱
沙箱與資安。
會議擷取之所以在用戶端上進行,是因為它需要麥克風和會議音訊——放在機櫃裡的 伺服器無法錄製你的通話。接著伺服器的管線會摘要這段錄音,筆記則存放在每個 用戶端都能讀取的地方。
Web 用戶端也會錄製,這一點可能被表格的呈現方式所掩蓋:它在瀏覽器中擷取 麥克風外加一條螢幕分享音軌,並把 16 kHz PCM 串流至伺服器。它_要求_必須有 這條音軌,因此當瀏覽器無法提供時,它會明確失敗。真正僅限桌面的,是擷取周遭 的品質層——透過原生處理器進行的訊號層級回音消除、輸入音量表與麥克風失效 警告、重新執行摘要、取消處理,以及日曆的「錄製並連結」動作。
配對與信任
標題為「配對與信任」的章節每個用戶端都透過一組一次性配置的預先共用金鑰來證明自己的身分:
- 本機模式下的桌面應用程式每次開機都會產生一組新金鑰,並透過環境變數把它 交給所啟動的伺服器,因此桌面上不會保存任何機密。
- 遠端用戶端(遠端桌面、Web、手機)使用由伺服器
pair命令簽發的金鑰, 並存放在作業系統鑰匙圈中(手機上則存放在本機)。
撤銷是即時的,而不是消極被動的。伺服器會監看配對裝置表,並在數秒內 中斷任何裝置已離開作用中集合的已開啟工作階段——無論直接 WebSocket 路徑或 中繼路徑都一樣。你不必等到裝置重新連線才能把它切斷。
金鑰絕不會經過靜態主機看得到的 URL。當它透過連結送達時——無論在手機上或
瀏覽器中——它都夾帶在 URL 片段中,也就是 # 之後的部分,瀏覽器絕不會
把這個部分傳送給代管該頁面的伺服器。這是值得記住的不變條件,而且兩者皆
適用。Web 用戶端還有另外兩個進入點:連線表單,讓你直接貼上伺服器與金鑰;
以及邀請碼,它會用邀請碼換取一份僅為該瀏覽器簽發的認證資訊。