# 收尾复盘

> 全书要点串联：一条消息穿过的各层、二十条值得记住的设计、与另外三本的四方对照，以及可迁移的工程经验和这份源码的短板。

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

读到这里，OpenCode 的 V1 现役链路与 V2 迁移方向都已拆完。这一篇把散落在各章的要点收拢起来。

## 全景回顾

默认 `opencode` 命令下，一条消息从终端到模型再回到屏幕，经过的层：

```text
TUI（packages/tui：OpenTUI + SolidJS，主线程）
  ↓ SDK 请求：默认经 RPC 隧道进 Worker；带 --port 时走真实 HTTP
服务端（Effect HttpApi：路由分组 → 实例定位 → handler）
  ↓
SessionPrompt（createUserMessage → loop → runLoop，一个会话一个 Runner）
  ├── system     ← 模型家族模板 + 环境信息 + 指令文件 + Skills + MCP 说明
  ├── LLM.stream ← AI SDK（默认）或 @opencode-ai/llm（实验开关）
  └── SessionProcessor ← LLMEvent → Part（text / reasoning / tool / step / patch）
        └── 工具 execute ← 插件钩子 ← Permission.ask（Deferred 挂起等用户）
  ↓
Session.updatePart → durable 事件：SQLite 事务里分配序号、跑投影
  ↓ GlobalBus → global.event（RPC）或 SSE
TUI 的 sync store（16ms 攒批）→ 渲染
```

V2 的链路已在旁边成形：`/api/session` → `session_input` 收件箱 → `SessionRunCoordinator` → `SessionRunner`（每轮一次 `llm.stream`、工具急切执行）→ `session.next.*` 持久事件。两代共用 `session` 表，由同一个服务端进程同时提供。

## 二十条值得记住的设计

**边界与契约**

1. **连 TUI 也是客户端**。服务端跑在 Worker 里，TUI 只发 SDK 请求、订阅事件；同一棵路由树既能进程内调用，也能对外监听（[启动链路](https://daiw.net/manual/opencode/startup)）。
2. **handler 只认 Request 与 Response**。Worker 隧道、`opencode run` 的本地模式、`sdk-next` 都只是把网络换成一次函数调用（[SDK、协议与客户端](https://daiw.net/manual/opencode/sdk-and-protocol)）。
3. **依赖方向写进规则**。schema → core 与 protocol → server，client 永不依赖 core；protocol 决定中间件放在哪，server 注入具体的键（[SDK、协议与客户端](https://daiw.net/manual/opencode/sdk-and-protocol)）。
4. **界面动作也是事件**。`/tui/append-prompt` 这类端点只发布事件，由正在运行的 TUI 接收，VS Code 扩展因此不需要自己的界面（[ACP、GitHub 与其他入口](https://daiw.net/manual/opencode/frontends)）。

**会话引擎**

5. **一个会话一个 Runner**。连发消息不需要队列：新消息先落库，循环下一步重读历史自然看到（[主循环](https://daiw.net/manual/opencode/prompt-loop)）。
6. **@ 文件伪装成已发生的 Read 调用**。模型看到的是它熟悉的工具结果，而不是一段来路不明的文本（[主循环](https://daiw.net/manual/opencode/prompt-loop)）。
7. **压缩请求本身是一条消息**。它可见、可回看，还充当历史视图的锚点；数据库里的旧消息一条不删，只是模型看不到（[上下文压缩](https://daiw.net/manual/opencode/compaction)）。
8. **撤销先暂存、后清理**。revert 只写边界，删除推迟到下一条消息，`/redo` 与 unrevert 几乎零成本（[快照与撤销](https://daiw.net/manual/opencode/snapshot-revert)）。
9. **影子 git 仓库借用对象库**。经 `objects/info/alternates` 复用用户仓库的对象，巨型仓库也不必重算哈希（[快照与撤销](https://daiw.net/manual/opencode/snapshot-revert)）。
10. **快照要在流开始前拍**。AI SDK 可能先于 start-step 执行工具，等事件到了再拍就晚了（[流处理器](https://daiw.net/manual/opencode/processor)）。

**工具与权限**

11. **模糊编辑配一道比例闸**。九级匹配级联省掉大量“空白不一致”的重试，`isDisproportionateMatch` 防止锚点匹配吞掉半个文件（[文件工具](https://daiw.net/manual/opencode/file-tools)）。
12. **按命令元数记住“总是允许”**。tree-sitter 解析出每条子命令，记下 `git checkout *` 这样的有效前缀，而不是整条命令或整个工具（[Shell 工具](https://daiw.net/manual/opencode/shell-tool)）。
13. **看不见的工具**。某个权限的最后一条相关规则若是模式为 `*` 的 deny，对应工具就从请求里整个消失（[权限系统](https://daiw.net/manual/opencode/permission)）。
14. **工具 ID 即权限键**。所以在 PowerShell 上 Shell 工具也叫 bash，改名要等 2.0（[Shell 工具](https://daiw.net/manual/opencode/shell-tool)）。

**模型与扩展**

15. **模型目录不进仓库**。models.dev 按磁盘缓存、编译期快照、网络三级获取，新增提供方原则上只需给目录提 PR（[提供方目录与 models.dev](https://daiw.net/manual/opencode/provider-catalog)）。
16. **把“取凭据的函数”交给 loader**。自定义 `fetch` 每次请求都重读凭据，token 刷新对 AI SDK 透明（[认证与提供方插件](https://daiw.net/manual/opencode/auth-and-plugins)）。
17. **插件串行、共享可变的 output**。加载顺序就是语义，内置插件总在外部插件之前（[插件系统](https://daiw.net/manual/opencode/plugins)）。

**V2 的方向**

18. **受理与执行分离**。提示先落进持久收件箱拿到回执，模型失败、进程重启都不丢输入（[V2 会话内核](https://daiw.net/manual/opencode/session-v2)）。
19. **事件、序号与投影同在一个事务**。读模型永远与事件一致；“可回放”与“实时渲染”拆成两个通道（[存储：SQLite 与事件投影](https://daiw.net/manual/opencode/storage)、[事件与同步](https://daiw.net/manual/opencode/events-sync)）。
20. **单一执行体与陈旧调用拒绝**。V2 工具的名字属于注册而非工具本身，本轮开始后被替换的注册只会得到 `Stale tool call`；输出只在结算边界截断一次（[V2 工具与权限](https://daiw.net/manual/opencode/tools-v2)）。

## 四方对照

| 维度 | [llm-to-agent](https://daiw.net/manual/llm-to-agent) | [Grok Build](https://daiw.net/manual/grok-build) | [Kimi Code](https://daiw.net/manual/kimi-code) | OpenCode |
| --- | --- | --- | --- | --- |
| 定位 | 教学最小实现 | 生产实现 | 生产实现 | 生产实现 |
| 语言 | 教学用 | **Rust** | **TypeScript** | **TypeScript + Effect** |
| 规模 | 小 | 约 85 万行（非测试）/ 96 crate | 38 万行 / 19 个单元（引擎约 13.6 万行） | 近 50 万行 / 36 包（agent 相关约 17 万行） |
| 前端 | 1 | TUI 为主，另有 headless 与 ACP | 7（TUI、`-p`、Web、Remote Control、VS Code、ACP、SDK） | TUI、Web、桌面、VS Code、ACP、GitHub、`run`、SDK |
| 依赖组织 | 无 | 显式传参 | VS Code 式 DI × Scope | Effect 的 Service、Layer 与 LayerNode |
| TUI | — | 自研 | pi-tui（vendored 自 pi-mono） | OpenTUI + SolidJS |
| 存储 | — | — | wire.jsonl 追加日志 + minidb 索引 | SQLite + Drizzle + 事件投影 |
| 模型接入 | 单一 | 分层 catalog | 引擎自带 `human/llm`（协议基座 + trait） | models.dev + AI SDK，另有自研 `packages/llm` |
| 引擎代数 | 1 | 1 | 2 代 → 1 代（v1 已删除） | 2 代并存（V1 现役、V2 迁移中） |

最大的结构差异来自一个选择：**把服务端当成产品的中心**。Grok Build 以 TUI 为中心，agent 核心与界面可以贴得很近；Kimi Code 要喂七种形态，于是有了 klient 契约层与独立的 transcript 包；OpenCode 走得更远，连默认的 TUI 都是 HTTP 客户端，服务端、协议、SDK、事件同步因此全都成了一等公民。代价是哪怕只在终端里用，也要跑一套完整的服务端（藏在 Worker 线程里）；回报是任何能发 HTTP 请求的东西都能驱动它。

从 OpenCode v1.14.19 分叉出来的 MiMo Code 保留了这套服务端架构，改动集中在会话引擎之上，见[《MiMo Code 源码解读》](https://daiw.net/manual/mimo-code)。

**这不是谁更好，是约束不同。**

## 可迁移的经验

**给 AI 助手的文档要和代码一起评审。** 根 `AGENTS.md` 写清了依赖方向、Effect 用法与测试约定，是现成的读者指南；但本书也记下了几处文档与代码不符的地方——原生 LLM 开关、TUI 抽包后仍依赖 core、`CONTRIBUTING.md` 里的旧路径。文档一旦被当成指令，漂移的代价就更高。

**工具说明是给模型的契约，写了就要兑现。** `edit.txt`、`write.txt` 说“没先读就会失败”，`webfetch.txt` 说会自动升级 HTTPS、会摘要大页面，代码里都没有。模型会照着说明行事，说明与实现不一致，等于给了它错误的前提。

**把数字从说明里拿出来。** Shell 工具的说明按 shell 种类渲染，默认超时与截断阈值在渲染时插入，说明与代码不会漂移——恰好是上一条的解药。

**用最小 API 守住不变量。** V2 的 `Tool.make` 只有一个执行体，`AGENTS.md` 明令禁止第二种入口。与其事后审查，不如让错误的写法根本写不出来。

**规格里标清完成度。** V2 会话规格的对齐清单逐项标注 complete、partial、missing（21 项里分别是 3、8、10），迁移到了哪一步一目了然，本书第 6 部分的许多判断都靠它。

**兼容负担要关进边界。** 插件工具仍用 Zod 描述参数，注册表只在边界上转换；V1 读懂部分 V2 配置写法时，产出 invalid、unsupported、conflict 三类诊断，而不是悄悄吞掉。

## 这份源码的短板

- **两代并存的认知成本**：一个 `opencode serve` 进程同时提供 `/session/...` 与 `/api/...`，读代码时得时刻分清走的是哪条链路。
- **声明了却没接上**：插件的 `permission.ask` 钩子没有触发点，`SessionContextEpoch.reset` 没有调用方，`snapshot`、`retry` 两种 Part 不再产生，`copilot-gpt-5.txt` 等提示词文件无人引用，`effect-sqlite-node` 只出现在依赖清单里。
- **默认宽松**：内置 agent 以“全部放行”打底（V1 与 V2 相同），编辑与 Shell 都不询问，只对越界目录、`.env` 读取（V1 还有疑似死循环）提问；想收紧得自己写规则，而 Shell 从来不是沙箱。
- **Effect 的门槛**：`Context.Service`、`Layer`、`Effect.fn`、`InstanceState` 贯穿全仓，不熟 Effect 的贡献者起步要先补一课。
- **大文件不少**：`provider.ts` 与 `transform.ts` 各约两千行，会话视图 `routes/session/index.tsx` 超过 2700 行。

## 继续读什么

- **想对照 TypeScript 的另一种架构**：[《Kimi Code 源码解读》](https://daiw.net/manual/kimi-code)。
- **想看 OpenCode 被二次开发成什么样**：[《MiMo Code 源码解读》](https://daiw.net/manual/mimo-code)。
- **想看 Rust 生产实现**：[《Grok Build 源码解读》](https://daiw.net/manual/grok-build)。
- **想从零理解 agent 该做什么**：[《从 LLM 到 Coding Agent》](https://daiw.net/manual/llm-to-agent)。
- **想深入模型侧**：[《AI 大模型解构》](https://daiw.net/manual/llm-anatomy)。

回到[专栏首页](https://daiw.net/manual/opencode)可查任意章节。源码基准 `3104c142`（v1.18.30，2026-09-09）——仓库更新很快，读时以当前 HEAD 为准；V2 迁移完成后，第 6 部分的“迁移中”会变成“现役”，但本专栏梳理的结构与契约一般稳定。
