跳至內容

架構

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 providers

lib/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(sealed UserPrincipal | 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,而 gh CLI 也不是一種驗證方式
  • 新手導引閘門位於 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 驗證;一旦依賴規則被違反,它就會失敗。