架構
Control Center 採用「功能優先」的整潔架構(Clean Architecture),並配以埠與配接器模式。程式碼庫圍繞業務領域(功能)組織,嚴格的依賴規則由自動化架構測試強制執行。
無介面伺服器之上的精簡用戶端
標題為「無介面伺服器之上的精簡用戶端」的章節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(伺服器端 DB)、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只有 Provider。它的設定與狀態介面位於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/ 存放由三個或以上功能共用的實體與儲存庫。它是純 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 統一的對等行為者。工作區成員是一個 User,以某個 WorkspaceRole(owner/admin/member/viewer/guest)繫結至一個 Workspace。成員資格就是存取邊界——而不是配對金鑰——而按儲存庫授予的權限,可確保工作區成員資格不會令人擁有高於程式碼託管平台的權限。在全新伺服器上首位建立的使用者擁有整個安裝;其他人經由邀請或 OIDC 加入。建立工作區時,會把該使用者標記為 owner。
即時協作採用權威伺服器 + 逐欄位最後寫入者勝(LWW),而非 CRDT。線上狀態(誰在線、身處何處、游標、正在輸入,以及代理的即時狀態與執行中成本)是一條獨立、永不持久化的短暫通道;持久狀態則走在樂觀變更 + 伺服器重定基底 + LWW 的主幹之上。人類與代理共用同一份名冊。只有單一人類操作者時,線上狀態通道會閒置,也不會出現任何名冊介面——多人協作對單人使用者零成本。
埠與配接器
標題為「埠與配接器」的章節埠是領域層中的抽象介面。配接器是基礎設施中的具體實作。由於無介面伺服器持有資料,而且必須在不含 Flutter 的情況下建置,幾乎所有配接器都僅限 VM,並按關注點組織,存放於 packages/cc_infra/lib/src/:
| 埠 | 配接器位置 |
|---|---|
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 連線、全安裝層級的伺服器設定、受管理動作政策(一個全安裝層級的限制,只能收緊工作區護欄)、每位使用者的資訊動態、機群佇列(工作節點/工作/放置紀錄)、驗證前的workspace_routes索引,以及安裝中繼資料。開機時只會開啟這個檔案,所以無論工作區累積多少歷史,啟動成本都維持平穩。在此加入資料表是一項隔離決定,棘輪測試會強迫你為此提出理據。WorkspaceDatabase(<dataDir>/<workspaceId>/workspace.db)——每個工作區一個_目錄_,內含一個 SQLite 檔案,以及所有其他只屬於該工作區的東西,因此對話的 worktree 與代理檔案就放在資料庫旁邊,並隨它一併刪除。幾乎所有東西都住在這裏:代理、空間、工單、記憶、管線、會議、程式碼圖、審查、儲存庫。檔案會在首次存取時經由WorkspaceDatabaseManager.of(workspaceId)延後開啟,並在當時支付自己的quick_check、FTS/觸發器安裝與vector_init。
兩半都是從一個壓平的 v1 基線開始——onCreate 會建立目前的結構描述——只有在已部署的資料庫必須沿用下去時,才會附加遷移步驟。
由於一個工作區就是一個檔案,匯出工作區只需一句 VACUUM INTO(workspace.export),而不是逐表傾印;workspace.import 會接收這樣的檔案,並利用內嵌的 workspace_meta,分辨「同一個檔案被重新接收」與「來自另一個安裝的檔案」。
兩個資料庫都由 cc_persistence 擁有,且只由 cc_server 開啟;沒有用戶端會開啟 DB——每個用戶端都經由 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中一個按帳戶的 Google OAuth 攔截器,會在收到 401 時重新整理權杖 - 所有錯誤都經由
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 驗證;一旦依賴規則被違反,它就會失敗。