# 怎么读这份源码

> 怎样把 ZCode 的桌面端、Web 与 Agent CLI 分别跑起来，各层有哪些检查命令，开源仓库为何几乎不带测试、formal-proof 又是什么，两份 AGENTS.md 立了哪些规矩，以及一条沿 Agent 主线的阅读路线。

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

ZCode 的源码难读，不在算法，而在“两个项目叠在一起”：84 万行里有一半多是桌面端与 Web 的界面代码，Agent 运行时又拆成了上百个小文件；仓库以一个提交整体公开，没有开发历史可翻，测试也几乎没有带出来。好在它是一份写给 Agent 读的源码——约定写在 `AGENTS.md` 里，原因写在中文注释里。这一篇讲怎么把它跑起来、有哪些检查可做，以及怎样只沿着 Agent 这条主线读。

## 跑起来

工具版本以 `mise.toml` 为准：Node.js `24.14.0`、pnpm `10.33.2`。所有命令在仓库根目录执行，第一步是（`README.md:27`）：

```bash
pnpm bootstrap
```

它安装依赖、准备桌面端的本地运行资源，再串行构建各个包（`scripts/bootstrap.mjs`）。之后按要看的形态选入口：

| 目标 | 命令 | 说明 |
| --- | --- | --- |
| 桌面应用 | `pnpm dev:desktop` | 默认等同 `dev:desktop:prod`，连生产服务；`dev:desktop:test` 连测试环境 |
| Web 与后端 | `pnpm dev:web` | 同时起 Web 开发服务器（`5173`）与后端（`3030`），见 `package.json:8` |
| Agent CLI（源码） | `pnpm --filter @zcode/cli dev` | 即 `tsx src/main.ts`，直接跑 TypeScript 源码 |
| Agent CLI（构建产物） | `pnpm --filter @zcode/cli... build` | 产出 `apps/zcode-cli/packages/cli/dist/zcode.cjs`，用 `node` 运行 |
| 命令行发行包 | `pnpm build:zcode` | 打出含 TUI、Web 与 Agent 的整包，需要先配 `ZCODE_DIST_BASE_URL` |

读 Agent 的代码，最快的反馈回路是第三行：改完直接 `pnpm --filter @zcode/cli dev`，不必构建。注意 `--filter @zcode/cli...` 末尾的三个点表示“连同它依赖的 workspace 包一起构建”，桌面端与 Web 用的 Agent 就是这份构建产物，改了 Agent 代码要重新构建再重启服务（`README.md:82`）。

数据目录要当心混用。桌面端可以用 `ZCODE_DATA_BASE_DIR` 换一个独立的数据基目录（`README.md:59`）；独立 CLI 的会话库、配置与日志则直接放在用户主目录下的 `~/.zcode/cli`，会话库路径写死为 `join(homedir(), ".zcode", "cli", "db", "db.sqlite")`（`apps/zcode-cli/packages/adapters/src/storage/session-store/paths.ts:7`）。想让调试会话和日常会话互不干扰，最省事的办法是临时换一个 `HOME` 再运行。跑之前可以先 `zcode doctor` 看一眼运行时假设：版本、Node、平台、是否是 SEA 单文件（`apps/zcode-cli/packages/cli/src/run.ts:188`）。

## 检查命令

根目录与 CLI 各有一套检查，而且**互不覆盖**：

| 范围 | 命令 | 做什么 |
| --- | --- | --- |
| 产品一侧 | `pnpm typecheck` | `tsc -b` 逐个检查 `packages/` 下的包，**不含** `apps/zcode-cli`（`package.json:29`） |
| 产品一侧 | `pnpm lint`、`pnpm fmt:check` | oxlint 与 oxfmt；oxlint 的忽略列表里有整个 `apps/zcode-cli`（`.oxlintrc.json:63`） |
| 全仓 | `pnpm architecture:check --changed` | 按 `architecture-policy.yaml` 检查模块边界，见[上一篇](https://daiw.net/manual/zcode/monorepo-map) |
| 全仓 | `pnpm knip`、`pnpm dep:refs --list-exports <file>` | 找未使用的依赖与导出、查一个导出被谁引用 |
| 全仓 | `pnpm verify:pre-push` | lint 加变更范围内的架构检查（`package.json:19`） |
| Agent 一侧 | `pnpm --dir apps/zcode-cli typecheck`、`lint` | 经 turbo 在各子包里跑 `tsc --noEmit` 与 oxlint |
| Agent 一侧 | `pnpm --dir apps/zcode-cli check` | 先确认 Bash 命令注册表与生成脚本一致，再做类型检查（`apps/zcode-cli/package.json:10`） |

根 `AGENTS.md` 还要求开工前先跑 `node scripts/check-workspace-freshness.mjs`（`AGENTS.md:10`）：本地分支落后远端就直接失败。脚本开头的注释交代了来由——本地的 zcode-cua 仓库曾落后主线 140 个提交，还有人在上面开工（`scripts/check-workspace-freshness.mjs:5`）。对只读源码的人，这一步可以跳过。

## 测试去哪了

`AGENTS.md` 把测试看得很重：“有行为改动时先补充对应测试；交互改动需要 E2E 场景”（`AGENTS.md:42`），CLI 那份也说“测试 case 很关键”（`apps/zcode-cli/AGENTS.md:8`）。可开源仓库里，全仓只有 4 个测试文件、631 行，分别在 `packages/services/test/` 与 `packages/ui/test/`，所有 `package.json` 里也找不到一个测试脚本。

测试是在公开时被拿掉的，痕迹还在：`dynamic-workflow` 的 README 用了两节介绍 `tests/workflows/` 与 `tests/graphs/` 的夹具测试（`apps/zcode-cli/packages/dynamic-workflow/README.md:82`），目录本身却不存在；桌面端还留着 E2E 覆盖率采集（`packages/desktop/src/main/e2eCoverage.ts`）和只在 E2E 运行时才打开的测试桥（`packages/shared/src/e2e-test-bridge.ts:10`）；AGENTS.md 反复要求的 spec 文档也一样没有公开，`config/README.md:28` 指向的 `docs/ui/…` 在仓库里并不存在。读代码时要记住这一点：你无法靠跑测试来验证理解，只能靠读。

### formal-proof：把产品行为画成状态空间

有一件测试相关的工具倒是留了下来，而且相当少见。`packages/formal-proof` 的 README 说它是“产品行为状态空间枚举器”，聚焦对话场景里压缩、分叉、目标、消息队列与编辑提问的组合（`packages/formal-proof/README.md:3`）。它把会话抽象成几个维度（`packages/formal-proof/src/model.ts:1`）：

```ts
export type RunPhase = "idle" | "running" | "completed" | "compacting" | "goalVerifying";
export type QueueState = "empty" | "text" | "goal" | "compact" | "mixed";
export type CompactMemory = "never" | "compactable" | "justCompacted" | "notNeeded";
export type GoalState = "none" | "active" | "verifying" | "verified" | "failed";
export type TurnTarget = "latest" | "old" | "none";
export type CandidateKind = "user" | "system";
// held 状态输入不静默入队，
// 由用户选择「清空 queue 后发送 / 保留 queue 立即发送」。
export type DecisionKind = "allow" | "reject" | "enqueue" | "choice" | "system" | "undefined";
```

再枚举“在某个状态下来了某种输入”的全部组合，给每个组合一个裁决：放行、拒绝、入队、让用户选、交给系统，或者“未定义”——最后一类正是要人去补规则的地方。`pnpm --filter @zcode/formal-proof dev` 会在本机 `4176` 端口起一个用 d3 画的可视化界面。这张表不只是文档：ZCode Protocol 的受理规则声称与它“逐条对齐”（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/projection-state.ts:115`），注释里提到的黄金测试 `formal-proof-consistency` 同样没有公开。协议怎样用这张表受理输入，见 [ZCode Protocol V4](https://daiw.net/manual/zcode/zcode-protocol)。

## AGENTS.md 立的规矩

仓库有两份 `AGENTS.md`：根目录那份管全仓，`apps/zcode-cli/AGENTS.md` 补充 CLI 的规则。它们写给在这里干活的编码 Agent，也是最好的读者指南。挑对读代码最有用的几条：

| 规矩 | 出处 | 读代码时的体现 |
| --- | --- | --- |
| 先写 spec，明确状态所有者、接口与验收场景 | `AGENTS.md:3` | 注释里常见“唯一所有者”“单一写入路径”的说法 |
| 避免重复状态和多条写入路径，不用超时掩盖同步问题 | `AGENTS.md:41` | 队列、快照、租约都有明确的所有者 |
| 长程任务优先，不以工具调用次数硬停 | `apps/zcode-cli/AGENTS.md:11` | [回合循环](https://daiw.net/manual/zcode/turn-loop)里没有步数上限 |
| 单文件默认不超过 400 行 | `apps/zcode-cli/AGENTS.md:12` | CLI 代码拆得很碎，但全仓仍有 478 个文件超标 |
| 常量提取为命名常量 | `apps/zcode-cli/AGENTS.md:13` | 超时、上限几乎都是具名常量，搜常量名就能找到数字 |
| 外部 I/O 收敛到 adapter | `apps/zcode-cli/AGENTS.md:53` | `core` 依赖端口，`adapters` 做实现 |
| 每个工具声明只读、破坏性、并发安全、超时等 | `apps/zcode-cli/AGENTS.md:62` | 见[工具契约](https://daiw.net/manual/zcode/tool-contract) |
| 大体积工具结果落盘，只回灌摘要与引用 | `apps/zcode-cli/AGENTS.md:65` | 见[执行器](https://daiw.net/manual/zcode/tool-executor) |
| TUI 不保存业务状态 | `apps/zcode-cli/AGENTS.md:71` | 见[终端界面](https://daiw.net/manual/zcode/tui) |
| 所有任务携带可传播的 `traceId` | `apps/zcode-cli/AGENTS.md:74` | 见[遥测与调试](https://daiw.net/manual/zcode/telemetry-debug) |
| 兼容 `.agents` 协议与 `AGENTS.md` | `apps/zcode-cli/AGENTS.md:80` | 技能目录同时认 `.zcode/skills` 与 `.agents/skills` |
| 错误默认向上冒泡，不靠错误文本做判断 | `apps/zcode-cli/AGENTS.md:87` | 错误多带结构化的 code，而不是字符串匹配 |
| 修 bug 时用中文注释写明原因和依据 | `AGENTS.md:43` | 见下一节 |

## 几个读码的小窍门

**先读注释。** “修 bug 要写原因”这条执行得很认真：`core/src` 里 5426 行注释，有 4023 行是中文，大多在解释“为什么”而不是“做什么”——为什么某个判断放在这一步之前、这里曾经出过什么问题。比如入口文件里的一句（`apps/zcode-cli/packages/cli/src/main.ts:30`）：

```ts
  // app-server/agent-server 的 stdout 是严格的 ZCode Protocol 帧通道，三方 SDK 的
  // console.debug 等普通输出不能直接写入 stdout。必须在加载 run/bootstrap 之前将
  // 进程级 console 统一引导到 stderr，否则任意依赖的一行普通日志都会触发传输层 JSON 解析崩溃。
```

**认名字。** 同一个概念常有几种写法：

| 名字 | 指什么 |
| --- | --- |
| V4、legacy | ZCode Protocol 的第四版与旧版，两套实现分别在 `bootstrap/src/zcode-protocol-v4` 与 `zcode-protocol` |
| dwf | dynamic workflow，动态工作流 |
| expert | 专家工作流，`/expert` 命令 |
| target、goal | 目标模式，CLI 参数叫 `--target`，斜杠命令叫 `/goal`，两者是别名 |
| mcs | mid-conversation system，会话中途插入的系统消息（`--force-mcs`） |
| cua | Computer Use，开源版是占位实现 |
| surface | 呈现面：`terminal` 或 `zcode_desktop`，决定提示词与交互的差异（`run.ts:146`） |
| off-peak | 闲时：闲时计划与闲时任务 |

**顺着端口读。** `core` 需要的外部能力都声明在 `apps/zcode-cli/packages/contracts/src/interfaces/` 下，`AgentRuntime` 的构造依赖集中在 `AgentRuntimeDeps` 里。想知道运行时能碰外部世界的哪些东西，看这两处就够了；想知道某个端口怎么实现，再去 `adapters` 找同名文件。

**先分清两条路。** TUI 在进程内调用 `createZCodeApp`（`apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:146`）；桌面端与 Web 走 `app-server`，入口是 `runZCodeProtocolAgent`（`apps/zcode-cli/packages/bootstrap/src/index.ts:68`）。两条路最终都落到同一个 `AgentRuntime`，但前者经过的是 `bootstrap/src/app/`，后者经过的是 `bootstrap/src/zcode-protocol-v4/`，读错了路就会在别人的分支里打转。

## 推荐阅读路线

| 顺序 | 从哪读起 | 对应篇目 |
| --- | --- | --- |
| 1 | `apps/zcode-cli/packages/cli/src/main.ts`、`run.ts`、`arguments.ts` | [命令行入口](https://daiw.net/manual/zcode/cli-surface) |
| 2 | `bootstrap/src/app/create-app.ts`、`adapters/src/config/` | [bootstrap](https://daiw.net/manual/zcode/bootstrap-assembly) |
| 3 | `core/src/runtime/agent-runtime.ts`、`runtime/methods/index.ts` | [AgentRuntime](https://daiw.net/manual/zcode/agent-runtime) |
| 4 | `runtime/methods/prompt-admission.ts`、`runtime/command-queue.ts` | [输入受理](https://daiw.net/manual/zcode/prompt-admission) |
| 5 | `runtime/methods/turn.ts`、`turn-loop.ts`、`turn-model-step.ts` | [回合循环](https://daiw.net/manual/zcode/turn-loop)、[一次模型请求](https://daiw.net/manual/zcode/model-step) |
| 6 | `core/src/tool/executor/`、`tool/handlers/index.ts` | [工具契约](https://daiw.net/manual/zcode/tool-contract)、[执行器](https://daiw.net/manual/zcode/tool-executor) |
| 7 | `adapters/src/model/` | [模型适配层](https://daiw.net/manual/zcode/model-adapters) |
| 8 | `bootstrap/src/zcode-protocol-v4/`、`packages/shared/src/zcode-protocol-v4/` | [ZCode Protocol V4](https://daiw.net/manual/zcode/zcode-protocol) |
| 9 | `packages/services/src/zcode-agent/`、`packages/desktop/src/` | [桌面应用](https://daiw.net/manual/zcode/desktop) |

第 1 到第 7 步正是下一篇要走的路：一条用户输入从终端出发，进入运行时，经过模型与工具，再以事件的形式回到屏幕；桌面端的那条路则在第 8、9 步汇合。

下一篇：[一条消息的旅程](https://daiw.net/manual/zcode/message-lifecycle)——从回车到屏幕，一条输入在 TUI 与桌面端两条路上各经过哪些层，模型与工具的结果又怎样以事件流回界面。
