架構
Control Center 遵循以功能為先的整潔架構,並採用埠與配接器模式。程式碼庫圍繞業務領域(功能)組織,並由自動化架構測試強制執行嚴格的相依性規則。
無介面伺服器之上的精簡用戶端
標題為「無介面伺服器之上的精簡用戶端」的章節Control Center 採用精簡用戶端架構。沒有任何用戶端會開啟資料庫——資料由無介面的 cc_server 程序持有(一個很小的 global.db,加上每個工作區一個 SQLite 檔案,採延遲開啟),並透過 WebSocket RPC 對外提供。桌面應用程式、網頁版與手機伴侶端,全都只是那條單一 RPC 連線之上的呈現端。
桌面端在首次啟動時選擇要以何種方式連上伺服器:本機(在這台機器上啟動 cc_server 並透過回送介面通訊——這是預設)或遠端(透過安全的 WebSocket 撥接別處的 cc_server)。網頁用戶端一律為遠端。啟動解析器(bootstrap/server_backend.dart)會在 Riverpod 存在之前決定走哪條路徑,接著以已連線的用戶端覆寫 rpcClientProvider——因此每個功能都透過伺服器讀寫,而不是本機資料庫。
RPC 層本身是無狀態的:工作階段經過驗證,但不會繫結至任何工作區。每個以工作區為範圍的呼叫都會在自己的引數中攜帶 workspace_id,缺少它時派送器會拒絕該呼叫。這正是同一個伺服器上的兩個用戶端能各自持有不同使用中工作區的原因,而不需要由伺服器保存一個會產生歧見的「目前工作區」——也意味著隔離是逐呼叫強制執行,而不是逐連線。
這是一個採單一已解析鎖定檔的 Dart 工作區,有二十一個成員——十五個套件加六個應用程式——並以桌面加網頁的應用程式(control_center)作為工作區根。這六個應用程式分別是無介面伺服器二進位檔(cc_server)、無介面機群執行器(cc_worker)、手機 PWA(cc_remote)、WebSocket 中繼代理(cc_signaling_server)、Widgetbook 展示庫(cc_gallery)以及公開示範主機(cc_demo_server)。這十五個套件是 cc_ui(設計系統)、cc_domain(純 Dart 共享核心:實體、值物件、埠、事件,以及每個功能的領域層)、cc_harness(內建代理核心:一個純 Dart 代理迴圈——訊息、供應商埠、工具、壓縮、引導、勾點、子代理——不依賴 dart:io,也沒有其他 cc_* 相依性)、cc_harness_runtime(內建 harness 執行階段配接器:僅限 VM 的串流供應商、OAuth/PKCE 憑證仲介、憑證儲存區與通用工具集——在未安裝任何外部 CLI 的情況下也能執行代理)、cc_rpc(用戶端傳輸)、cc_host(伺服器核心)、cc_data(遠端儲存庫)、cc_persistence(伺服器端資料庫)、cc_server_core(應用程式伺服器組合 + 身分/線上狀態/機群/評測執行階段)、cc_infra(僅限 VM 的配接器)、cc_mcp(MCP 工具)、cc_mcp_client(外部 MCP 伺服器的用戶端,將其工具橋接進本機登錄)、cc_markdown(儲存庫內建的 Markdown 引擎)、cc_natives(FFI 葉節點)以及 system_audio_capture。完整的用戶端矩陣與各元件執行位置,請參閱部署與用戶端。
相依性規則
標題為「相依性規則」的章節Presentation → Application/Providers → Domain ← Infrastructure- 領域層:純 Dart 的實體、值物件、儲存庫介面、埠與領域服務。對基礎設施的匯入為零(不使用 dio、drift 或網路模型)。
- 呈現層:畫面、widget、notifier。不得直接存取 drift/DAO/資料層;一切都經過 Riverpod provider → 儲存庫。
- 基礎設施層:領域埠與儲存庫介面的具體實作。相依於領域層,絕不反向相依。
功能結構
標題為「功能結構」的章節功能的完整形態如下:
feature_name/├── data/ # Repository implementations, data sources, services, DTOs, mappers├── domain/ # Entities, repository interfaces, ports, use cases├── presentation/ # Screens, widgets, notifiers└── providers/ # Riverpod providerslib/features/ 底下真正同時具備這四個目錄的功能極少,而這些缺口多半帶有資訊,並非偶然。共享核心已將每個功能的 domain/ 收進 cc_domain,而精簡用戶端不持有任何儲存庫實作,因此 lib/ 底下多數功能只有 presentation/ 加 providers/,別無其他。若用戶端中仍留有 data/ 或 domain/ 目錄,代表那確實是屬於用戶端的東西。
有兩個具名例外值得一提:
mcp只有 providers。它的設定與狀態介面位於settings/底下,而工具介面本身是一個套件(cc_mcp),不是功能資料夾。orchestration與plan_studio只有presentation/與providers/。
guardrails 功能將其領域層放在 cc_domain/features/guardrails/,而非根應用程式,因為護欄是由伺服器強制執行:規則會在某個範圍內把 ActionClass(或 shell 的 commandPrefix)對應到一項決策——allow、prompt 或 deny;規則依工作區儲存,而每個受控管的代理動作在執行前,都會先經過政策鏈解析。解析順序為 space > agent > workspace > mode preset > built-in default,而且由第一個具有相符規則的範圍做出決定;最嚴格只在單一範圍內,以及合併某個動作所宣告的多個類別時,才作為決勝依據。操作者面對的介面是設定 → 工作區 → 代理權限,位於 /workspaces/<id>/settings/workspace/permissions。模型說明請參閱護欄。
共享核心
標題為「共享核心」的章節packages/cc_domain/lib/core/domain/ 放置由 3 個以上功能共用的實體與儲存庫。它是零基礎設施相依性的純 Dart,這正是讓 Flutter 用戶端與不含 Flutter 的伺服器二進位檔都能匯入同一組定義的原因:
- 核心實體:
Agent、AgentRunLog、Workspace、Repo、ReviewSpaceAssociation - 身分與多人協作:
User、Principal(sealedUserPrincipal|AgentPrincipal)、WorkspaceMember、WorkspaceRole - 記憶實體:
MemoryFact、MemoryPolicy、AgentWorkingMemory、MemoryAccessGrant - 共用的值物件:
AgentSkills、AgentRole、Mode、SandboxBackend、SandboxSpec、RunCost - 共用的埠:
SandboxPort、CredentialBrokerPort、WorkspaceFilesystemPort、GitRepoInspectorPort、EmbeddingPort - 領域服務:
MemoryAccessPolicy、ActivityLogger、MentionResolver、RunLivenessClassifier DomainEventBus+ 事件類型(工作區/代理、PR/審查、訊息、工單、管線、編排、記憶、日曆/會議、身分與成員資格、可觀測性)
身分與多人協作
標題為「身分與多人協作」的章節Control Center 是多人使用的:人類與代理是地位對等的行為者,由 Principal 統一代表。工作區成員是在某個 WorkspaceRole(owner/admin/member/viewer/guest)上繫結至某個 Workspace 的 User。成員資格才是存取邊界——不是配對金鑰——而依儲存庫授予的權限,可避免工作區成員資格帶來超出程式碼代管的權限。在全新伺服器上建立的第一位使用者擁有整個安裝;其他人透過邀請或 OIDC 加入。建立工作區時,會將該使用者標記為 owner。
即時協作採用權威伺服器 + 逐欄位後寫入者勝(LWW),而不是 CRDT。線上狀態(誰在線上、人在哪裡、游標、輸入中、代理的即時狀態與執行成本)是一條獨立的暫時性通道,永不持久化;持久狀態則由樂觀變更 + 伺服器重定基底 + LWW 的主幹承載。人類與代理共用同一份名冊。只有一位人類操作者時,線上狀態通道會閒置,也不會出現任何名冊介面元素——多人功能對單人使用者毫無成本。
埠與配接器
標題為「埠與配接器」的章節埠是領域層中的抽象介面。配接器是基礎設施中的具體實作。由於無介面伺服器擁有資料,而且必須能在不含 Flutter 的情況下建置,幾乎所有配接器都僅適用於 VM,並依關注點整理在 packages/cc_infra/lib/src/ 底下:
| Port | Adapter location |
|---|---|
SandboxPort |
cc_infra/lib/src/sandboxing/ (native_sandbox_adapter.dart) |
CredentialBrokerPort |
cc_infra/lib/src/sandboxing/ (env_credential_broker.dart, github_fine_grained_broker.dart) |
GitRepoInspectorPort |
cc_infra/lib/src/git/ (git_repo_inspector.dart) |
EmbeddingPort |
cc_infra/lib/src/embedding/ (embedding_service.dart) |
NotificationPort |
lib/core/notifications/(用戶端:在本機呈現伺服器事件) |
AgentBackend |
cc_infra/lib/src/dispatch/backends/ (acp_backend.dart, harness_backend.dart, cli_backends.dart) |
TicketProviderPort |
cc_infra/lib/src/tickets/linear/ (LinearTicketAdapter)。Jira、ClickUp 與 GitHub Issues 是 cc_infra/lib/src/tickets/sync/ 中的工單同步配接器;本機工單則存在工作區資料庫中,沒有 TicketProviderPort。 |
PipelineEnginePort |
cc_domain features/pipelines/domain/services/(純 Dart 領域服務——不需要任何基礎設施) |
應用程式的組合根(di/providers.dart)會透過 Riverpod provider 將用戶端埠繫結到實作;伺服器則在 cc_server_core 中組合其配接器。
狀態管理
標題為「狀態管理」的章節所有用戶端狀態都使用 Riverpod:
Notifier<T>與AsyncNotifier<T>用於可變狀態FutureProvider<T>用於非同步資料- 清單與即時資料列會以
AsyncValue<List<T>>的形式透過 RPC 訂閱送達——伺服器內部使用 Drift 的.watch();沒有任何用戶端會開啟 Drift - MCP 工具以具型別的建構函式參數接收相依項目,絕不接收
Ref
資料庫
標題為「資料庫」的章節Drift(SQLite)分成兩個資料庫,都位於 cc_persistence:
GlobalDatabase(<dataDir>/global.db)——一份真正屬於整個伺服器的資料表清單:工作區登錄、使用者/偏好設定/已配對裝置、SSO 連線、安裝範圍的伺服器設定、受管理的動作原則(安裝範圍的夾制,只能收緊工作區護欄)、每位使用者的新聞動態、機群佇列(worker/工作/放置記錄)、驗證前的workspace_routes索引,以及安裝中介資料。開機時只會開啟這個檔案,因此無論工作區累積多少歷史,啟動成本都維持不變。在這裡新增資料表是一項隔離決策,棘輪測試會強迫你為此提出理由。WorkspaceDatabase(<dataDir>/<workspaceId>/workspace.db)——每個工作區一個_目錄_,裡面放一個 SQLite 檔案,以及所有只屬於該工作區的其他內容,因此對話的工作樹與代理檔案會與資料庫並存,並隨它一起刪除。幾乎所有東西都住在這裡:代理、空間、工單、記憶、管線、會議、程式碼圖、審查、儲存庫。檔案會在第一次接觸時透過WorkspaceDatabaseManager.of(workspaceId)延遲開啟,並於此時自行支付quick_check、FTS/觸發程序安裝與vector_init的成本。
這兩半都是從壓平的 v1 基準開始——由 onCreate 建出目前的結構定義——只有在已部署的資料庫必須往前帶時,才會附加移轉步驟。
由於一個工作區就是一個檔案,匯出工作區只需單一 VACUUM INTO 陳述式(workspace.export),而不是逐表傾印;workspace.import 會採用這樣的檔案,並利用內嵌的 workspace_meta 分辨這是重新採用自己的檔案,還是來自另一次安裝的檔案。
兩個資料庫都歸 cc_persistence 所有,且只由 cc_server 開啟;沒有任何用戶端會開啟資料庫——每個用戶端都透過 RPC 讀寫。資料表定義在 packages/cc_persistence/lib/database/tables/,DAO 則在 .../daos/;領域實體是純 Dart,與 Drift 資料表類別分開,對應轉換則位於資料層。FTS5 負責全文檢索,sqlite_vector 負責嵌入向量,兩者都僅限伺服器端;兩個資料庫都在 WAL 日誌模式下執行 PRAGMA foreign_keys=ON。
go_router,而有一項事實主導其餘一切:應用程式內的每個目的地都帶有工作區前置路徑,形式為 /workspaces/:workspaceId/…。URL 中的工作區 id 是使用中工作區的唯一真實來源——activeWorkspaceIdProvider 是由路由驅動,而不是反過來——這也是為什麼在前置路徑之外讀取以工作區為範圍的資料時,無從取得其 id。
- 路由建構_函式_位於
router/routes.dart,每個都以工作區 id 作為第一個引數,而不是字串常數 - 四個情境之前的路由不帶前置路徑:
/splash、/onboarding、/signed-out與/workspaces(選擇器) ShellRoute包住應用程式外殼;那四個介面則在其外部以全螢幕呈現- 驗證守衛:未完成設定的會被導向
/onboarding;設定已完成、但程式碼代管憑證已消失的,則會被導向/signed-out。完成是指已登入的使用者至少連接了一個程式碼代管服務,且至少存在一個工作區——不特定是 GitHub,而且ghCLI 不是一種驗證方式 - 入門引導的閘門位於
features/auth/providers/
網路(伺服器端)
標題為「網路(伺服器端)」的章節所有對外的 HTTP 都位於伺服器。用戶端絕不直接連上 GitHub、Linear 或 Google——它們呼叫伺服器 RPC 操作,甚至遠端媒體也是透過伺服器的 /proxy/media 端點取得,而不是向上游來源取得。
packages/cc_infra/lib/src/中的 dio 用戶端:GitHubApiClient、GitHubPrClient、GitHubContentClient、GitHubGraphQLClient、GitLabApiClient、BitbucketApiClient、LinearGraphQlClient與GoogleCalendarApiClient- 透過攔截器注入驗證權杖,其中包括
cc_server_core中會於 401 時重新整理權杖的個別帳號 Google OAuth 攔截器 - 所有錯誤都會經過
packages/cc_infra/lib/src/network/error_mapper.dart對應 → 對應到cc_domain中定義的具型別AppException子類別
安全性
標題為「安全性」的章節伺服器持有真正重要的憑證:用於啟動代理的 GitHub 與工單服務權杖、LLM 供應商金鑰,以及 OAuth 權杖,全都位於伺服器資料目錄或其環境中。用戶端鑰匙圈仍保管的,是它自己的裝置憑證,以及用來連線遠端伺服器的配對金鑰。
- 機密存放於
flutter_secure_storage(macOS 鑰匙圈、Windows 認證管理員、Linux libsecret)——即用戶端的裝置憑證與遠端配對金鑰,而非供應商權杖 shared_preferences僅用於非敏感的偏好設定(佈景主題、字型、版面配置)- 較舊安裝留下的供應商機密,會由
features/auth/providers/credential_migration.dart一次性交給伺服器,然後從用戶端鑰匙圈刪除
架構強制執行
標題為「架構強制執行」的章節架構限制由 test/core/architecture_constraints_test.dart 驗證,若違反相依性規則就會失敗。