架构
Control Center 采用以功能为先的整洁架构,并运用端口与适配器模式。代码库围绕业务领域(功能)组织,并通过自动化架构测试强制执行严格的依赖规则。
无界面服务器之上的瘦客户端
标题为“无界面服务器之上的瘦客户端”的部分Control Center 采用瘦客户端架构。没有任何客户端会直接打开数据库——数据由一个无界面的 cc_server 进程持有(一个很小的 global.db,加上每个工作区一个 SQLite 文件,按需延迟打开),并通过 WebSocket RPC 对外提供。桌面应用、Web 构建产物和手机伴侣端都只是这一个 RPC 连接之上的渲染层。
桌面端在首次启动时选择连接服务器的方式:本地(在本机启动一个 cc_server 并通过回环地址通信——这是默认选项)或远程(通过安全 WebSocket 连接别处的 cc_server)。Web 客户端始终是远程的。启动解析器(bootstrap/server_backend.dart)在 Riverpod 存在之前就确定路径,然后用已连接的客户端覆盖 rpcClientProvider——因此每个功能都通过服务器而非本地数据库进行读写。
RPC 层本身是无状态的:会话经过身份验证,但不会绑定到某个工作区。每个以工作区为作用域的调用都会在自己的参数中携带 workspace_id,缺少该参数时调度器会拒绝此类调用。正因如此,同一服务器上的两个客户端可以各自持有不同的活动工作区,而无需让服务器保存一个会引发分歧的“当前工作区”——这也意味着隔离是按调用而非按连接强制执行的。
这是一个使用单一已解析锁文件的 Dart 工作区,包含二十一个成员——十五个包加六个应用——并以桌面端 + Web 应用(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(密封的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。在线状态(谁在场、他们在哪里、光标、正在输入、智能体的实时状态与运行费用)是一条独立的从不持久化的临时通道;持久状态则依托“乐观修改 + 服务器 rebase + 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> - 列表和实时数据行通过 RPC 订阅以
AsyncValue<List<T>>的形式到达——服务器内部使用 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 会构建当前 schema——只有在已部署的数据库需要向前迁移时才会追加迁移步骤。
由于一个工作区就是一个文件,导出工作区只需一条 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中一个按账号生效的 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 校验,一旦依赖规则被违反,该测试就会失败。