# 怎么读这份源码

> 从源码构建与运行、14 道验证门与测试登记表、类型路径怎样由包清单推导、AGENTS.md 的约定、两代运行时各自的角色，以及一条沿命令行主线的阅读路线。

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

MiniMax Code 的源码难读，不在算法，而在“杂”：它是内部 monorepo 的投影，同一批包同时服务命令行、桌面端、云端和 IM 渠道；两代运行时一起跑；Agent 循环又在 pi-mono 里。这一篇讲怎么把它跑起来、怎么确认改动没有弄坏东西，以及怎样只沿着命令行这条主线读。

## 从源码构建与运行

需要 Git、上一篇说过的 Node.js 版本，以及 pnpm 9.12.0（根 `package.json:7`）。README 的步骤（`README_ZH.md:171`）：

```bash
git clone https://github.com/MiniMax-AI/minimax-code.git
cd minimax-code
pnpm install --frozen-lockfile
pnpm build
pnpm mcode
```

`pnpm mcode` 就是 `node dist/cli.js`（`package.json:14`）。要在别的项目里试源码构建的版本，就进入那个项目目录，用绝对路径运行 `dist/cli.js`；调试时最好把 `MINIMAX_DATA_DIR` 指到临时目录，免得和日常会话混在一起（`docs/installation.md:42`）。首次构建必须联网：除了 npm 依赖，构建脚本还会从公共 npm 下载 `@minimax-ai/code` 0.3.11 的包，从中取出内嵌的 mcode-tools（版本 0.0.4），并校验包与文件的哈希（`scripts/lib/mcode-tools-artifact.mjs:8`）。

根 `package.json` 的脚本（`package.json:11`）大致分三类：

| 类别 | 脚本 |
| --- | --- |
| 构建与运行 | `build`、`mcode` 与 `start`、`typecheck` |
| 检查与生成 | `check:source`、`check:tsconfig`、`check:standalone`、`gen:tsconfig` |
| 测试 | `test:smoke`、`test:byok`、`test:artifact`、`test:release-tools`（node:test）；`test:capabilities`、`test:status-contract`、`test:policy`、`test:sandbox`（Vitest）；总入口 `verify` |

`scripts/build.mjs` 用 esbuild 打包，入口有四个（`scripts/build.mjs:78`）：CLI 本体 `packages/tui/src/index.ts`、图片预览的 worker、mcode-tools 的启动入口，以及 Matrix 的 MCP stdio 服务。原生模块与可选的平台集成不打包，保留安装时的位置（`build.mjs:37`），具体是 `better-sqlite3`、剪贴板、ripgrep 和飞书 SDK 这几个依赖（`build.mjs:84`）；其余全部打进 `dist/`，工作区包按上一篇说的办法直接解析源码。打包之后再拷资源：第一代运行时目录下的内置技能与提示词、V2 的 Agent 定义、模型目录快照 `models-dev-catalog.json.gz`、沙箱的原生辅助程序和 mcode-tools。

对读源码最有用的，是构建顺手写出的 `dist/metafile.json`（`build.mjs:135`）。它列出了真正进入 CLI 产物的每一个源文件，构建结束时那行输出也会报出这个数目（`build.mjs:139`）。仓库里不少代码只服务于桌面端或云端，拿不准一个模块在不在命令行路径上时，查它在不在 metafile 里最直接；上一篇讲的 `check:standalone` 用的也正是这份文件。

## 验证门

`pnpm verify` 按与 CI 相同的顺序跑完全部关卡，定义在 `scripts/verify.mjs`，一共 14 道（`scripts/verify.mjs:36`）：

| 关卡 | 做什么 | 限制 |
| --- | --- | --- |
| `check:source` | 源码清单与内容检查 | |
| `check:tsconfig` | 类型路径是否与包清单一致 | |
| `export source preview` | 从已提交的 HEAD 导出源码归档并校验 | 需要 git，`archive` 档跳过 |
| `test:release-tools` | 发布与源码同步工具的回归测试 | |
| `typecheck` | `tsc -p tsconfig.standalone.json` | `platform` 档跳过 |
| `build` | 构建 `dist/` | |
| `check:standalone` | 构建图的边界 | |
| `test:artifact` | 内嵌的 mcode-tools 与公开包逐字节一致 | |
| `test:capabilities` | 136 个 Vitest 文件的能力测试 | |
| `test:status-contract` | 构建模式的契约 | |
| `test:smoke` | 冒烟测试，并断言没有意外的外网请求 | |
| `test:byok` | 用本地协议服务器驱动一次 BYOK 会话，再恢复它 | |
| `test:policy` | 权限门面 | 仅 macOS、Linux |
| `test:sandbox` | Seatbelt 沙箱后端 | 仅 macOS |

档位有四个（`verify.mjs:28`）：`full` 是本地默认；`platform` 只省掉重复的类型检查，CI 在 macOS 和 Windows 上用它；`docs` 只跑前四道，供纯文档改动使用；`archive` 用来校验已经导出的源码归档，跳过需要 git 的那一道。`pnpm verify --list` 列出当前平台要跑哪些关卡，设了 `MCODE_VERIFY_REPORT_DIR` 还会写出 JSON 与 Markdown 格式的报告（`verify.mjs:148`）。

Vitest 文件不写在各包的 `package.json` 里，而是统一登记在 `test/vitest-suites.json`，按关卡分成 `capability`（136 个）、`status-contract`、`policy`、`sandbox` 四组，后三组合计 4 个。`vitest.oss.config.mjs` 用包清单和各包的 `exports` 生成别名，测试同样直接跑源码（`vitest.oss.config.mjs:10`）。反过来，`packages/` 下每个测试文件都必须登记，漏了源码检查就会失败（`scripts/lib/vitest-suites.mjs:30`）；`third_party/` 自带的测试则不在本发行版的验证范围内（`AGENTS.md:8`）。

## 类型路径怎么来的

`tsconfig.standalone.json` 的 `paths` 有 120 条，把每个包名及其导出子路径映射到源码文件，例如 `@earendil-works/pi-ai/anthropic` 指向 pi-ai 的 `providers/anthropic.ts`。这一大段不是手写的，生成脚本开头的注释说得清楚（`scripts/gen-tsconfig-paths.mjs:7`）：

```js
// The standalone type-check path map is derived from the package scope in
// `release/extraction.json` plus each package's own `exports`, so adding a package
// or an export subpath never requires hand-editing 100+ path entries. Run with
// `--write` after changing package scope or exports; the default check mode fails
// when the committed map has drifted.
```

推导只有一处实现：`scripts/lib/package-exports.mjs` 把 `exports` 里指向产物的路径换回源码路径，类型检查和 Vitest 别名共用它，免得两边对同一个包解析出不同的文件（`scripts/lib/package-exports.mjs:4`）。`pnpm gen:tsconfig` 写入，`pnpm check:tsconfig` 只比对，漂移了就报错（`gen-tsconfig-paths.mjs:49`）。

还有一个容易忽略的细节：`tsconfig.standalone.json` 的 `include` 只有 CLI 入口 `packages/tui/src/index.ts` 和两组声明文件（`tsconfig.standalone.json:377`）。`tsc` 会顺着 import 把走得到的文件都检查一遍，走不到的——比如只给桌面端用的模块——就不在这次类型检查里。它和 metafile 一样，可以用来判断一段代码在不在命令行路径上。

## AGENTS.md 的约定

根 `AGENTS.md` 写给在这个仓库里干活的编码 Agent，也是现成的读者指南：

- **生成文件不手改**：`release/public-source.json` 与 `tsconfig.standalone.json` 的 `paths` 都用脚本重新生成（`AGENTS.md:16`）。
- **单一事实来源**：包范围、导出映射、测试登记、退役路径、验证流水线等各有唯一的声明处，别处只读不抄（`AGENTS.md:33`）。
- **常见改动有固定步骤**：加包、改导出、加测试、加验证关卡各怎么做都写明了；加关卡只改 `scripts/verify.mjs`，不动 CI 工作流文件（`AGENTS.md:53`）。
- **边界**：不引用内部主机、生成的 IDL 与私有服务；不恢复退役路径，也不靠删能力通过检查；不提交账号、会话、日志与凭据；文档与提交信息用英文，产品里的本地化字串和内置提示词保留原来的语言（`AGENTS.md:65`）。

仓库还在 `.agents/skills/` 下带了五个给贡献者用的技能：`cli-guide`、`cross-layer-drift-sweep`、`testing-workflow`、`verify-all-runtime-sinks`、`retro`（`CONTRIBUTING.md:21`）。它们和产品内置的技能是两回事。其中 `cli-guide` 列了命令行各入口文件的职责，和本书前几篇的路线基本一致。

## 两代运行时

`packages/local-runtime`（第一代，下称 V1）与 `packages/local-runtime-v2`（V2）不是“旧的弃用、新的替代”，而是一起跑：

- **V2 是主人**。TUI 用 `createEmbeddedRuntimeHost` 创建运行时，背后调用的是 V2 的 `createLocalRuntimeHostV2`；把 `MAVIS_LOCAL_RUNTIME_V2_FORCE_LEGACY` 设成 `1` 想退回旧实现，会直接报错（`packages/tui/src/runtime/embedded-host.ts:65`）。会话、队列、回合、Agent 宿主和 `CliService` 都在 V2（`docs/architecture.md:6`）。
- **V1 当宿主设施用**。V2 启动时先打开 SQLite 并执行迁移、组装 Agent 服务，然后创建一个 V1 宿主并等它就绪（`packages/local-runtime-v2/src/runtime.ts:254`），再在它之上组装自己的服务（`createRuntimeServices`，`packages/local-runtime-v2/src/services.ts:395`），最后才生成 `CliService`（`runtime.ts:298`）。按架构文档的说法，V1 提供数据库、文件工具、权限、存储等被复用的设施，旧会话读取器只读旧文件，不会退回旧的执行器（`docs/architecture.md:9`）。
- **Agent 资源只有一份**。内置 Agent 的定义与提示词只放在 `packages/local-runtime-v2/assets/agents`，构建时从这里拷，V1 不再带第二份（`scripts/lib/local-runtime-assets.mjs:5`）。

TUI 这一侧会明确告诉共享代码“我是命令行”（`embedded-host.ts:84`）：

```ts
    runtimeOwnerKind: 'tui',
    runtimeMode: 'clean',
    legacyOpencodeEnabled: false,
    enableLiveMcp: true,
    capabilityProfile: 'cli',
    isContextWindowUsageEnabled: () => true,
    capabilities: {
      ...runtimeOptions.capabilities,
      cliEmbedded: true,
    },
```

读共享代码时碰到 `capabilityProfile`、`cliEmbedded`、`runtimeOwnerKind` 这类判断，就是在区分产品形态。配置也会被收窄，例如桌面端的记忆功能在这里被直接关掉（`embedded-host.ts:56`）。同一个运行时在 TUI、exec、ACP 下还各带一个 `surface`（`tui`、`headless`、`acp`），比如 headless 下不会弹出权限询问（`packages/tui/src/runtime/lifecycle.ts:450`）。V1 本身以及它和 OpenCode 旧会话的关系，见[第一代运行时与 OpenCode 遗产](https://daiw.net/manual/minimax-code/runtime-v1)。

## 几个读码的小窍门

- **名字**：同一个产品在代码里有好几个名字。包名前缀和一部分环境变量用 Mavis（如 `MAVIS_DATA_DIR`），类名与文档里常写 MCode（如 `McodeProviderApplication`），运行时事件的 schema 名则是 `archon.runtime.event.v1`（`packages/protocol/src/runtime.ts:3`）。不必猜来历，知道指的是同一个东西就行。
- **数据在哪**：V2 的数据库是数据目录下 v2/sqlite 里的 `runtime-state.sqlite`（`packages/local-runtime-v2/src/infra/db/client.ts:49`），表结构用 Drizzle 定义，见[存储](https://daiw.net/manual/minimax-code/storage)。
- **沿着接口读**：除了启动和退出，TUI 与运行时之间只有 `CliService` 一个口子，类注释写明是进程内直接调用应用层，没有 HTTP 或 RPC 封套（`packages/local-runtime-v2/src/local/cli-service.ts:106`）；TUI 一侧的 `packages/tui/src/runtime/adapter.ts` 再把它拆成会话、对话、交互等几个访问器。想知道 TUI 能让运行时做什么，看这两个文件就够了。

## 推荐阅读路线

| 顺序 | 从哪读起 | 对应篇目 |
| --- | --- | --- |
| 1 | `packages/tui/src/index.ts`、`cli/main.ts`、`cli/program.ts`、`cli/contract.ts` | [MiniMax Code 是什么](https://daiw.net/manual/minimax-code/what-is-minimax-code) |
| 2 | `packages/tui/src/runtime/lifecycle.ts`、`embedded-host.ts`、`adapter.ts` | [进程内入口](https://daiw.net/manual/minimax-code/runtime-entry) |
| 3 | `packages/local-runtime-v2/src/runtime.ts`、`services.ts`、`local/cli-service.ts`、`application/` | [进程内入口](https://daiw.net/manual/minimax-code/runtime-entry)、[会话系统](https://daiw.net/manual/minimax-code/session-system) |
| 4 | `service/turn-system/` 下的 `turn-submission.service.ts`、`queue.dispatcher.ts`、`execution/` | [回合系统](https://daiw.net/manual/minimax-code/turn-system) |
| 5 | `service/turn-system/agent-host/`、`packages/agent-core/src/pi-turn-runner/` | [pi 的 Agent 循环与回合执行器](https://daiw.net/manual/minimax-code/pi-agent) |
| 6 | `third_party/pi-mono/packages/agent/src/agent-loop.ts`、`third_party/pi-mono/packages/ai/src/` | [pi 的 Agent 循环与回合执行器](https://daiw.net/manual/minimax-code/pi-agent)、[模型协议层 pi-ai](https://daiw.net/manual/minimax-code/pi-ai) |
| 7 | `packages/agent-tools/src/desktop/`、`packages/agent-modules/permission/` | [工具体系](https://daiw.net/manual/minimax-code/tool-system)、[权限模式与规则](https://daiw.net/manual/minimax-code/permission) |
| 8 | `packages/tui/src/tui/` 下的 `controller/`、`transcript/`、`engine/` | [TUI 应用](https://daiw.net/manual/minimax-code/tui-app)、[TUI 引擎](https://daiw.net/manual/minimax-code/tui-engine) |

第 3 到第 6 步正好是下一篇要走的路：一条消息从 TUI 出发，经过 V2 的应用层与回合系统，进入 pi 的 Agent 循环，再把事件带回屏幕。

下一篇：[一条消息的旅程](https://daiw.net/manual/minimax-code/message-lifecycle)——从回车到转录区，一条用户输入穿过 TUI、CliService、回合系统与 pi 的 Agent 循环，事件再原路流回屏幕。
