仓库全景:两层 workspace 与依赖方向
ZCode 仓库由产品一侧的 packages 与 Agent 一侧的 apps/zcode-cli 两层 workspace 组成:三十多个包各管什么、规模多大、依赖怎样单向流动,两层之间为何只靠一条进程边界相连,以及把边界写成规则的架构治理工具。
打开 ZCode 仓库,第一眼看到的是两套目录:packages/ 下是桌面端、Web、服务端和共享界面,apps/zcode-cli/ 下是 Agent CLI 与运行时。它们在同一个 pnpm workspace 里,却几乎是两个项目——产品一侧的包没有一个依赖 Agent 一侧,两边靠一条进程边界相连。这一篇先把各包的分工和依赖方向理清楚,后面各部分的篇目都以这张图为地图。
两层 workspace
根目录的 pnpm-workspace.yaml 同时收录两层(pnpm-workspace.yaml:1):
packages:
- packages/*
- apps/zcode-cli
- apps/zcode-cli/packages/*
- apps/zcode-cli/tools/*apps/zcode-cli 自己也是一个完整的 workspace:有自己的 pnpm-workspace.yaml、pnpm-lock.yaml 和 turbo.json,根包名叫 zcode-cli,版本 0.16.9。README 特意说明它“作为普通目录随本仓库一起克隆,无需单独拉取或初始化 Git submodule”(README.md:32),从这些痕迹看,它原先是一个独立仓库,公开时才并进来。两层对运行环境的要求也不一样:根 package.json 要求 node >=24.0.0(package.json:78),CLI 则钉死 24.14.0(apps/zcode-cli/package.json:46)。
版本号同样分两套:产品版本是根 package.json 的 3.14.0,Agent CLI 是 0.16.9,大多数内部包写的是 0.1.0 或干脆没有版本号,只有 @zcode/rpc(1.0.0)、@zcode/zcode-cua(0.6.3)、@zcode/node-repl-host(0.6.0)、@zcode/browser-use-plugin(0.5.1)这几个带了独立版本。
Agent 一侧:apps/zcode-cli
规模口径同封面:非测试的 .ts、.tsx,wc -l 物理行。
| 包 | 职责 | 文件 | 行数 |
|---|---|---|---|
packages/core | Agent 内核:AgentRuntime、回合循环、工具执行器与全部内置工具、权限、钩子、压缩、记忆、子 Agent、专家工作流 | 495 | 95557 |
packages/bootstrap | 组装层:读配置、造 adapter、创建会话运行时;ZCode Protocol 的服务端实现也在这里 | 222 | 63533 |
packages/adapters | I/O 实现:模型客户端、文件系统、子进程、HTTP、MCP、插件、技能、SQLite 会话库、登录凭据 | 202 | 51637 |
packages/contracts | 端口接口、事件与配置的 schema(zod)、工具与工作流契约 | 111 | 21427 |
packages/dynamic-workflow | 动态工作流的门面、编译器与纯执行引擎,不做任何 I/O | 90 | 19878 |
packages/tui | 终端界面,基于 OpenTUI(@mbears/opentui-* 发布的构建)的 React 渲染 | 91 | 13723 |
packages/cli | 命令行入口、子命令路由、无头模式、SEA 打包脚本 | 81 | 10609 |
packages/debug | 本地调试台:抓模型请求的 MITM 代理加时间线界面 | 12 | 4779 |
packages/telemetry | OpenTelemetry 指标与链路 | 10 | 3874 |
packages/node-repl-host | node_repl 的 MCP 宿主,Browser Use 与 Computer Use 的 bridge | 16 | 1519 |
packages/dynamic-workflow-runtime | 动态工作流的沙箱 harness:子进程加 vm 上下文 | 5 | 1294 |
packages/i18n | 中英文界面文案 | 5 | 1139 |
tools/prompt-trajectory | 录制与回看提示词轨迹的开发工具 | 11 | 2401 |
packages/shared-types、swift-bridge、browser-use-plugin、tools/typescript | 共享类型、Swift 互操作占位、Browser Use 内置插件、共享 tsconfig | 3 | 88 |
合计 1354 个文件、291458 行。几个小包的实情:swift-bridge 注释写明“Swift bridge placeholder”,两个函数都直接返回“不可用”(apps/zcode-cli/packages/swift-bridge/src/index.ts:1);superpowers-plugin 目录里只有一份 MIT 的 LICENSE,没有代码;browser-use-plugin 的源码只有一个 17 行的文件,内容主要是技能与文档(见 node_repl、Browser Use 与 Computer Use)。
这一侧的分层很清楚,名字就是职责:
core 与 adapters 互不依赖,都只认 contracts;把两者接起来的是 bootstrap,它读配置、创建各个 adapter,再注入 AgentRuntime(bootstrap:把运行时拼起来)。这是端口与适配器的写法,CLI 的 AGENTS.md 把它写成了硬规矩:业务模块“不得直接调用 fetch、http、fs、child_process、process.env 等底层 I/O API”(apps/zcode-cli/AGENTS.md:53)。实际执行得相当彻底,但并非没有例外:core/src 里仍有 15 个文件直接引用了 node:fs、node:http 或 node:net,只是没有一个直接起子进程(例如 tool/handlers/webfetch.ts、runtime/methods/bash-shell-snapshot.ts),3 处直接读 process.env(如 tool/handlers/task-output.ts:202)。
产品一侧:packages
| 包 | 职责 | 文件 | 行数 |
|---|---|---|---|
ui | 桌面端与 Web 共用的 React 界面:会话面板、工具调用卡片、设置页、Zustand 状态、中英文文案 | 1476 | 322555 |
services | 业务服务:Agent 子进程管理、会话与任务索引、Git、OAuth、插件与技能同步、用量统计等 | 298 | 87320 |
desktop | Electron 的 Main、Host、Preload、Renderer 与定时调度器,内嵌浏览器 | 267 | 61295 |
shared | 共享契约层:ZCode Protocol 的类型与校验、各类领域类型 | 216 | 38370 |
server | Web 模式的 HTTP 与 WebSocket 服务、stdio 服务、远程连接 | 51 | 10924 |
zcode-server-cli | 独立服务端的启动、守护与进程管理 | 43 | 5766 |
provider | Provider 与模型选择的纯逻辑 | 23 | 4827 |
rpc | 仿 VS Code 的 IPC 框架:channel、代理、序列化、可重连协议 | 20 | 4058 |
web | Web 客户端入口、登录与会话分享页 | 17 | 2976 |
provider-node | 内置 Provider 配置的下载、缓存与个人配置文件读写 | 17 | 1895 |
formal-proof | 产品行为的状态空间枚举器(带可视化) | 3 | 1835 |
model-option-map | 把统一的模型选项翻译成各家请求参数的小语言 | 8 | 867 |
client | Agent 客户端 SDK:MessagePort 与 WebSocket 两种连接 | 6 | 723 |
zcode-cua | Computer Use 的占位包 | 12 | 585 |
合计 2457 个文件、543996 行。packages/ui 一个包占了全仓近四成,其中 settings/ 就有 5.4 万行、v4/ 会话界面 5.2 万行。zcode-cua 的描述直说这是个“API-compatible placeholder”,开源版不带 Computer Use,所有入口一律返回不可用(packages/zcode-cua/package.json 的 description,以及根 NOTICE.md 第一节)。formal-proof 是一件很少见的工程工具,下一篇会单独介绍。
这一侧的依赖同样单向:
两层之间:一条进程边界
用各包 package.json 里的 workspace: 依赖算一遍,结论很干脆:packages/ 下没有任何包依赖 apps/zcode-cli 的包;反过来,Agent 一侧只用了产品一侧的五个基础包——shared、provider、provider-node、model-option-map 和 zcode-cua。
那么桌面端和 Web 服务端怎样用上 Agent?答案是进程。services 里的 Agent 进程管理器把构建好的 CLI 当子进程拉起,参数是 app-server --stdio(packages/services/src/zcode-agent/zcodeAgentProcessManager.ts:369),之后双方在标准输入输出上说 ZCode Protocol;协议的类型与运行时校验放在两边都依赖的 packages/shared/src/zcode-protocol-v4/。根 AGENTS.md 把这条边界写成了规则:“Desktop app 通过 stdio 与 Agent 通信。协议改动同步更新 packages/shared/src/zcode-protocol/index.ts,提供严格类型与运行时校验”(AGENTS.md:59)。
终端里的 zcode 则不经过这条边界:TUI 与运行时在同一个进程里,TUI 直接调用 bootstrap 导出的 createZCodeApp 创建会话(apps/zcode-cli/packages/cli/src/tui-prompt-handler-runtime.ts:50)。同一个运行时因此有两种接法,一条消息的旅程会把两条路都走一遍。
其他目录
| 目录 | 内容 |
|---|---|
config/ | 随客户端发布的默认配置 default.json,以及内置 Provider 规则集 provider/zcode-builtin.json(schemaVersion 1、revision 30,约 18 万字节,见 Provider 规则) |
third-party/ | 第三方声明材料:复制进仓库的组件清单、嵌入组件清单、原生搜索工具的许可证 |
apps/zcode-cli/dependencies/native-search/ | 随桌面端与 SEA 分发的 bfs、ugrep、ripgrep 预编译包,共 18 个归档,带 SHA256SUMS |
patches/ | 三个 pnpm 补丁:@ai-sdk/anthropic、@ai-sdk/openai-compatible 与桌面端的前端监控 SDK @arms/rum-electron |
scripts/ | 构建、开发、打包、第三方声明生成、架构检查与原生搜索工具准备 |
harness/remote/ | 本地起一个 SSH 容器,用来测试远程工作区 |
.agents/skills/ | 给在这个仓库里干活的编码 Agent 用的 8 个技能,和产品内置的技能是两回事 |
third-party/copied-components.json 值得翻一下,它记录了哪些代码是从别处复制进来的:packages/rpc 与协议 V4 的编解码文件 packages/shared/src/zcode-protocol-v4/wire-codec.ts 源自 VS Code 的 IPC 与通用工具代码;Bash 工具用来判断命令语义的命令注册表 core/src/tool/handlers/generated/bash-command-registry.ts,由 Fig 的 autocomplete 规格生成;界面组件用了 shadcn/ui 与 Vercel 的 ai-elements;另有从 obra/superpowers 改写的技能描述。这些线索在后面讲 RPC、Bash、技能时会再碰到。
架构治理:把边界写成规则
仓库根目录的 architecture-policy.yaml 把代码划成 15 个模块,每个模块声明根目录、依赖、公开入口,受管模块还要声明分层(architecture-policy.yaml:4)。全局规则只有六条(architecture-policy.yaml:59):
global:
maxFileLines: 400
maxContractLines: 300
maxPublicMethods: 12
forbidCycles: true
forbidDeepImports: true
managedOnly: true单文件不超过 400 行、契约文件不超过 300 行、契约公开方法不超过 12 个、禁止循环依赖、禁止绕过公开入口的深层导入。关键在最后一条 managedOnly:检查器遇到非受管模块的文件直接跳过(scripts/architecture/index.mjs:132),而 15 个模块里只有 packages/services/src/storage 一个标了 managed: true(architecture-policy.yaml:28),这个模块只有 13 个文件、1121 行,其余模块的注释写着“存量模块先标记为 legacy”(architecture-policy.yaml:3)。基线文件 .architecture-baseline.json 里的违规列表是空的。
所以“400 行”在今天更像目标而不是现状。CLI 的 AGENTS.md 写的是“单个源文件默认不能超过 400 行”(apps/zcode-cli/AGENTS.md:12),根目录的 oxlint 也配了 max-lines 400(跳过空行与注释,.oxlintrc.json:6),但 oxlint 的 ignorePatterns 把整个 apps/zcode-cli 排除在外(.oxlintrc.json:63),另有 201 个文件用注释关掉了这条规则。按物理行数,全仓超过 400 行的非测试文件有 478 个,最大的 packages/services/src/zcode-agent/zcodeTaskServiceAdapter.ts 有 5737 行,文件头的豁免理由是“迁移期需要在一个门面里集中维护旧 task projection 到 ZCode session 的协议适配”(zcodeTaskServiceAdapter.ts:1)。反过来也看得出团队的方向:CLI 的核心代码确实被拆得很碎,core/src/runtime/methods 一个目录就有 97 个文件。
治理工具不只用来卡提交,也用来喂 Agent:pnpm architecture:context <module-id> 为指定模块生成一份“受控上下文”,列出它的契约与公开入口(scripts/architecture/architecture-check.mjs:15);.agents/skills/architecture-governance/SKILL.md 要求编码 Agent 动手前先跑检查、读这份上下文、为每块可变状态指定唯一的所有者。这个仓库从一开始就是写给人和 Agent 一起读的,下一篇接着说这一点。
下一篇:怎么读这份源码——怎样把它跑起来、仓库里的 AGENTS.md 立了哪些规矩、测试去了哪里,以及一条沿 Agent 主线的阅读路线。