# 仓库全景：两层 workspace 与依赖方向

> ZCode 仓库由产品一侧的 packages 与 Agent 一侧的 apps/zcode-cli 两层 workspace 组成：三十多个包各管什么、规模多大、依赖怎样单向流动，两层之间为何只靠一条进程边界相连，以及把边界写成规则的架构治理工具。

- 作者：David（道雾轩）
- 专栏：ZCode 源码解读（https://daiw.net/manual/zcode.md）
- 最后更新：2026-09-21
- 原文：https://daiw.net/manual/zcode/monorepo-map
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

打开 ZCode 仓库，第一眼看到的是两套目录：`packages/` 下是桌面端、Web、服务端和共享界面，`apps/zcode-cli/` 下是 Agent CLI 与运行时。它们在同一个 pnpm workspace 里，却几乎是两个项目——产品一侧的包没有一个依赖 Agent 一侧，两边靠一条进程边界相连。这一篇先把各包的分工和依赖方向理清楚，后面各部分的篇目都以这张图为地图。

## 两层 workspace

根目录的 `pnpm-workspace.yaml` 同时收录两层（`pnpm-workspace.yaml:1`）：

```yaml
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

规模口径同[封面](https://daiw.net/manual/zcode)：非测试的 `.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](https://daiw.net/manual/zcode/node-repl-browser)）。

这一侧的分层很清楚，名字就是职责：

```mermaid
flowchart TD
  CLI["cli<br/>入口与子命令"] --> BOOT["bootstrap<br/>组装与协议服务端"]
  CLI --> TUI["tui<br/>终端界面"]
  BOOT --> CORE["core<br/>Agent 内核"]
  BOOT --> ADP["adapters<br/>I/O 实现"]
  BOOT --> TEL["telemetry"]
  CORE --> CON["contracts<br/>端口与 schema"]
  ADP --> CON
  TUI --> CON
  CORE --> DWF["dynamic-workflow<br/>编译器与纯引擎"]
  BOOT --> DWR["dynamic-workflow-runtime<br/>沙箱 harness"]
  DWR --> DWF
  CON --> SH["packages/shared<br/>（产品一侧）"]
```

`core` 与 `adapters` 互不依赖，都只认 `contracts`；把两者接起来的是 `bootstrap`，它读配置、创建各个 adapter，再注入 `AgentRuntime`（[bootstrap：把运行时拼起来](https://daiw.net/manual/zcode/bootstrap-assembly)）。这是端口与适配器的写法，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` 是一件很少见的工程工具，[下一篇](https://daiw.net/manual/zcode/reading-the-source)会单独介绍。

这一侧的依赖同样单向：

```mermaid
flowchart TD
  DESK["desktop"] --> UI["ui"]
  WEB["web"] --> UI
  DESK --> SRV["server"]
  UI --> SVC["services"]
  SRV --> SVC
  SCLI["zcode-server-cli"] --> SVC
  DESK --> CLIENT["client"]
  WEB --> CLIENT
  CLIENT --> RPC["rpc"]
  SVC --> RPC
  SVC --> PN["provider-node"]
  PN --> PROV["provider"]
  PROV --> SHR["shared"]
  SHR --> MOM["model-option-map"]
```

## 两层之间：一条进程边界

用各包 `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`）。

```mermaid
flowchart LR
  subgraph PRODUCT["packages（产品一侧）"]
    D["desktop / server"] --> S["services<br/>zcodeAgentProcessManager"]
  end
  subgraph AGENT["apps/zcode-cli（Agent 一侧）"]
    A["cli app-server"] --> B["bootstrap<br/>协议服务端"] --> C["core<br/>AgentRuntime"]
  end
  S -- "子进程 + stdio<br/>ZCode Protocol V4" --> A
  SH["packages/shared<br/>协议类型与校验"] -.-> S
  SH -.-> B
```

终端里的 `zcode` 则不经过这条边界：TUI 与运行时在同一个进程里，TUI 直接调用 bootstrap 导出的 `createZCodeApp` 创建会话（`apps/zcode-cli/packages/cli/src/tui-prompt-handler-runtime.ts:50`）。同一个运行时因此有两种接法，[一条消息的旅程](https://daiw.net/manual/zcode/message-lifecycle)会把两条路都走一遍。

## 其他目录

| 目录 | 内容 |
| --- | --- |
| `config/` | 随客户端发布的默认配置 `default.json`，以及内置 Provider 规则集 `provider/zcode-builtin.json`（`schemaVersion` 1、`revision` 30，约 18 万字节，见 [Provider 规则](https://daiw.net/manual/zcode/provider-config)） |
| `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](https://daiw.net/manual/zcode/server-web)、[Bash](https://daiw.net/manual/zcode/bash)、[技能](https://daiw.net/manual/zcode/skills-commands)时会再碰到。

## 架构治理：把边界写成规则

仓库根目录的 `architecture-policy.yaml` 把代码划成 15 个模块，每个模块声明根目录、依赖、公开入口，受管模块还要声明分层（`architecture-policy.yaml:4`）。全局规则只有六条（`architecture-policy.yaml:59`）：

```yaml
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 一起读的，[下一篇](https://daiw.net/manual/zcode/reading-the-source)接着说这一点。

下一篇：[怎么读这份源码](https://daiw.net/manual/zcode/reading-the-source)——怎样把它跑起来、仓库里的 AGENTS.md 立了哪些规矩、测试去了哪里，以及一条沿 Agent 主线的阅读路线。
