怎么读这份源码
从源码构建与运行、14 道验证门与测试登记表、类型路径怎样由包清单推导、AGENTS.md 的约定、两代运行时各自的角色,以及一条沿命令行主线的阅读路线。
MiniMax Code 的源码难读,不在算法,而在“杂”:它是内部 monorepo 的投影,同一批包同时服务命令行、桌面端、云端和 IM 渠道;两代运行时一起跑;Agent 循环又在 pi-mono 里。这一篇讲怎么把它跑起来、怎么确认改动没有弄坏东西,以及怎样只沿着命令行这条主线读。
从源码构建与运行
需要 Git、上一篇说过的 Node.js 版本,以及 pnpm 9.12.0(根 package.json:7)。README 的步骤(README_ZH.md:171):
git clone https://github.com/MiniMax-AI/minimax-code.git
cd minimax-code
pnpm install --frozen-lockfile
pnpm build
pnpm mcodepnpm 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):
// 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):
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 遗产。
几个读码的小窍门
- 名字:同一个产品在代码里有好几个名字。包名前缀和一部分环境变量用 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 定义,见存储。 - 沿着接口读:除了启动和退出,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 是什么 |
| 2 | packages/tui/src/runtime/lifecycle.ts、embedded-host.ts、adapter.ts | 进程内入口 |
| 3 | packages/local-runtime-v2/src/runtime.ts、services.ts、local/cli-service.ts、application/ | 进程内入口、会话系统 |
| 4 | service/turn-system/ 下的 turn-submission.service.ts、queue.dispatcher.ts、execution/ | 回合系统 |
| 5 | service/turn-system/agent-host/、packages/agent-core/src/pi-turn-runner/ | pi 的 Agent 循环与回合执行器 |
| 6 | third_party/pi-mono/packages/agent/src/agent-loop.ts、third_party/pi-mono/packages/ai/src/ | pi 的 Agent 循环与回合执行器、模型协议层 pi-ai |
| 7 | packages/agent-tools/src/desktop/、packages/agent-modules/permission/ | 工具体系、权限模式与规则 |
| 8 | packages/tui/src/tui/ 下的 controller/、transcript/、engine/ | TUI 应用、TUI 引擎 |
第 3 到第 6 步正好是下一篇要走的路:一条消息从 TUI 出发,经过 V2 的应用层与回合系统,进入 pi 的 Agent 循环,再把事件带回屏幕。
下一篇:一条消息的旅程——从回车到转录区,一条用户输入穿过 TUI、CliService、回合系统与 pi 的 Agent 循环,事件再原路流回屏幕。