RTK OpenCode 插件实现解析:在 OpenCode 中自动改写命令以节省 LLM Token
本文基于 RTK 仓库的 hooks/opencode/ 模块文档及其 TypeScript 插件源码,详解 RTK 如何以 OpenCode 原生插件形式拦截 shell 命令、通过 rtk rewrite 子进程将其透明改写为 RTK 等价命令,从而在不改变 OpenCode 工作流的前提下削减进入 LLM 上下文的输出 token。读完本文,你将掌握该插件的安装方式、事件拦截机制、容错设计,以及底层 rtk rewrite 命令的退出码契约与单一事实来源架构。
一、OpenCode 集成在 RTK Hook 体系中的定位
RTK 是一个用 Rust 编写的 CLI 代理,目标是在常见开发命令上减少 60-90% 的 LLM token 消耗,且只需单一二进制、零外部依赖。其 Agent 集成层(hooks/ 目录下的已部署 hook 产物)覆盖 10 个受支持的 Agent,包括 Claude Code、Cursor、Copilot、Codex、OpenCode、Pi、Hermes 等。所有集成遵循统一的 thin delegate(薄委托) 架构:
Hook 脚本本身零过滤逻辑——只负责解析各自 Agent 特有的 JSON/事件格式,把命令交给
rtk rewrite子进程做决策,再把结果按 Agent 约定的格式回传。全部 70+ 条改写规则集中在 Rust 二进制内(src/discover/registry.rs),确保所有 Agent 共享同一份事实来源。
按 hooks/README.md 的 Supported Agents 表,OpenCode 的集成方式在 10 个 Agent 中独树一帜:
| Agent | 机制 | 修改方式 |
|---|---|---|
| Claude Code / Cursor / Copilot | Shell 脚本或 Rust 二进制 hook(JSON stdin/stdout 协议) | 通过 updatedInput 等字段回传改写命令 |
| Cline / Windsurf / Codex | 规则文件(prompt 级提示) | 不可改命令,仅引导 |
| OpenCode | TypeScript 插件(tool.execute.before 事件) |
In-place mutation(原地修改 args.command) |
| Hermes | Python 插件(pre_tool_call) |
In-place mutation |
| Pi | TypeScript 扩展(tool_call 事件) |
In-place mutation |
OpenCode 文档(hooks/opencode/README.md)对其特性做了五点概括,这五点恰好覆盖了该插件全部技术决策:
- 这是一个 TypeScript 插件(基于 zx 库执行 shell),而不是 shell hook;
- 拦截
tool.execute.before事件,以子进程方式调用rtk rewrite; - 使用
.quiet().nothrow()静默忽略一切失败; - 若改写结果与原命令不同,则原地修改
args.command; - 由
rtk init -g --opencode安装到~/.config/opencode/plugins/rtk.ts。
下文逐条结合源码展开。
二、安装:rtk init -g --opencode 的完整链路
2.1 安装命令与目标路径
在 docs/guide/getting-started/supported-agents.md 中,OpenCode 的安装方式为:
rtk init --global --opencode
该命令会创建 ~/.config/opencode/plugins/rtk.ts,并注册 tool.execute.before hook。OpenCode 启动时会自动加载 ~/.config/opencode/plugins/ 下的插件,无需额外配置。
需要注意两个源码级约束(见 src/hooks/init.rs):
- 全局安装是唯一模式:若未带
-g,rtk init会直接报错OpenCode plugin is global-only. Use: rtk init -g --opencode; - 互斥约束:
--opencode不能与--codex组合使用。
安装后终端会提示重启 OpenCode 并用 git status 验证(src/hooks/init.rs 中的 run_opencode_only_mode):
OpenCode plugin installed (global).
OpenCode: /home/<user>/.config/opencode/plugins/rtk.ts
Restart OpenCode. Test with: git status
2.2 安装逻辑的实现细节
插件内容并非在运行时从外部拷贝,而是通过 include_str! 在编译期嵌入 Rust 二进制(src/hooks/init.rs):
const OPENCODE_PLUGIN: &str = include_str!("../../hooks/opencode/rtk.ts");
安装路径由一组常量拼出(src/hooks/constants.rs):
pub const CONFIG_DIR: &str = ".config";
pub const OPENCODE_SUBDIR: &str = "opencode";
pub const PLUGIN_SUBDIR: &str = "plugins";
pub const OPENCODE_PLUGIN_FILE: &str = "rtk.ts";
即 resolve_opencode_dir() 解析出 ~/.config/opencode,再拼上 plugins/rtk.ts(src/hooks/init.rs)。写入由 ensure_opencode_plugin_installed 完成,语义是“缺失或过期才写”(write_if_changed),因此重复执行 rtk init 是幂等安全的;配合 dry-run 模式可只打印“将创建/将写入”而不落盘(src/hooks/init.rs)。卸载路径同样有独立函数 remove_opencode_plugin,在 --uninstall 流程中调用(src/hooks/init.rs)。安装与更新、移除行为均有对应单元测试覆盖(test_opencode_plugin_install_and_update、test_opencode_plugin_remove)。
三、插件实现逐行解析
hooks/opencode/rtk.ts 全文仅 39 行,是典型的“薄委托”实现。完整源码如下:
import type { Plugin } from "@opencode-ai/plugin"
// RTK OpenCode plugin — rewrites commands to use rtk for token savings.
// Requires: rtk >= 0.23.0 in PATH.
//
// This is a thin delegating plugin: all rewrite logic lives in `rtk rewrite`,
// which is the single source of truth (src/discover/registry.rs).
// To add or change rewrite rules, edit the Rust registry — not this file.
export const RtkOpenCodePlugin: Plugin = async ({ $ }) => {
try {
await $`which rtk`.quiet()
} catch {
console.warn("[rtk] rtk binary not found in PATH — plugin disabled")
return {}
}
return {
"tool.execute.before": async (input, output) => {
const tool = String(input?.tool ?? "").toLowerCase()
if (tool !== "bash" && tool !== "shell") return
const args = output?.args
if (!args || typeof args !== "object") return
const command = (args as Record<string, unknown>).command
if (typeof command !== "string" || !command) return
try {
const result = await $`rtk rewrite ${command}`.quiet().nothrow()
const rewritten = String(result.stdout).trim()
if (rewritten && rewritten !== command) {
;(args as Record<string, unknown>).command = rewritten
}
} catch {
// rtk rewrite failed — pass through unchanged
}
},
}
}
3.1 插件签名与启动自检
插件导出 RtkOpenCodePlugin,类型是 @opencode-ai/plugin 的 Plugin——一个返回事件处理器对象的 async 工厂函数,参数中注入了 zx 的 $ shell 执行器(这正是文档所说“使用 zx 库而非 shell hook”的体现)。
插件激活的第一步是 await $which rtk.quiet():若 rtk 不在 PATH 中,打印一条警告并返回空对象 {},插件即整体禁用。文件头部注释明确了前提:要求 rtk >= 0.23.0 在 PATH 中。注意这里与 shell hook 不同——shell hook 会检查版本并警告“rtk version too old”,而 TS 插件只依赖 rtk rewrite 子命令本身是否可用。
3.2 tool.execute.before 事件与工具过滤
插件注册的唯一事件是 tool.execute.before:在 OpenCode 每次执行工具之前触发,回调签名为 (input, output)。文档第 2、4 点描述的核心行为即在此处:
- 工具过滤:
input.tool小写化后必须等于bash或shell才继续,其余工具(如文件读写类)直接跳过,零开销; - 参数形状防御:
output.args必须是对象、其中command必须是非空字符串,否则原样放行。这类防御保证了即便 OpenCode 事件 payload 结构变化,插件也不会抛错; - 原地修改(in-place mutation):这是 OpenCode/Pi/Hermes 这类“插件型”集成与 shell/JSON 协议型集成(Claude Code、Cursor 等通过
updatedInput回传新命令)的本质区别——OpenCode 插件直接改写output.args.command字段,框架随后执行的即是改写后的命令。
3.3 调用 rtk rewrite 与容错语义
核心改写逻辑只有四行(hooks/opencode/rtk.ts):
const result = await $`rtk rewrite ${command}`.quiet().nothrow()
const rewritten = String(result.stdout).trim()
if (rewritten && rewritten !== command) {
;(args as Record<string, unknown>).command = rewritten
}
文档第 3 点提到的 .quiet().nothrow() 两个 zx 链式调用是关键设计:
.quiet():抑制 zx 把命令输出回显到终端/日志;.nothrow():子进程以非零退出码结束时不抛异常。
结合外层 try/catch,插件对任何 rtk rewrite 故障(二进制崩溃、超时、异常退出)都选择“pass through unchanged”——原命令照常执行。这与 hooks/README.md 中定义的退出码契约一脉相承:hook 绝不能阻塞命令执行,所有错误路径都让命令原样跑。rtk rewrite 的完整退出码语义见 src/hooks/rewrite_cmd.rs:
| 退出码 | stdout | 含义 | 插件的实际反应 |
|---|---|---|---|
| 0 | 改写后命令 | 允许改写(allow 权限判定) | command 原地替换 |
| 1 | 空 | 无 RTK 等价命令 | rewritten 为空串,不修改 |
| 2 | 空 | 命中 Deny 规则 | rewritten 为空串,原命令放行给宿主工具处理拒绝 |
| 3 | 改写后命令 | 命中 Ask 规则,需宿主提示用户 | 与 0 相同,命令被替换;权限提示由 OpenCode 自身的工具审批机制接管 |
值得注意:TS 插件不解析退出码,只依据“stdout 非空且与原文不同”决定是否改写。nothrow() 保证了退出码 1/2/3 不会中断流程,而空 stdout 天然等价于“不改写”,因此无需显式分支。这也是为什么源码注释强调它是 thin delegate:权限判定(Deny/Ask/Allow/Default)的完整实现留在 src/hooks/rewrite_cmd.rs 中,由 permissions.rs 读取宿主设置文件并映射为退出码,OpenCode 插件只是消费其 stdout。
3.4 单一事实来源:改写规则改哪里
源码头注释写得很明确:要增改改写规则,编辑 Rust 注册表(src/discover/registry.rs),而不是这个 TS 文件。改写决策链为:
OpenCode 执行 bash 命令(如 "cargo test --nocapture")
→ "tool.execute.before" 事件
→ 插件提取 args.command
→ 子进程 `rtk rewrite "cargo test --nocapture"`
→ src/discover/registry.rs 匹配 70+ 条模式,返回 "rtk cargo test --nocapture"
→ 插件原地替换 args.command
→ OpenCode 实际执行 "rtk cargo test --nocapture"
→ 过滤后的输出进入 LLM 上下文(bash 输出最多减少 90%)
rtk rewrite 的判定逻辑还包含若干安全防护,从 src/hooks/rewrite_cmd.rs 的 evaluate_with_verdict 可以看到:
- 含“不可证明构造”的命令(反引号/
$()命令替换、文件重定向等,由discover/lexer.rs::contains_unattestable_construct检测)一律 passthrough,绝不改写; - 已是 RTK 前缀的命令(如
rtk git status)原样返回,不会出现rtk rtk git; - 支持
RTK_DISABLED=1环境变量按命令禁用、~/.config/rtk/config.toml的exclude_commands排除清单、以及复合命令(&&、||、;、|等)的分段改写,例如cargo fmt --all && cargo test变为rtk cargo fmt --all && rtk cargo test。
这些机制对 OpenCode 用户透明生效:无论在哪种 Agent 下,rtk rewrite 的行为完全一致。
四、与其他集成方式的对照与维护建议
从 src/hooks/README.md 的集成分级看,OpenCode 属于 Plugin 档(TypeScript/JS/Python 插件,维护成本中等——由 Agent 负责加载,RTK 只需保持插件文件本身不腐化):
- Full hook(Shell 脚本或 Rust 二进制,需跟踪 Agent hook API 变更):Claude Code、Cursor、Copilot、Gemini;
- Plugin(Agent 插件系统内加载):OpenCode、Hermes、Pi;
- Rules file(prompt 级指令,无代码可坏):Cline、Windsurf、Codex。
对照 hooks/pi/rtk.ts 与 hooks/hermes/rtk-rewrite/plugin.yaml 可见,Pi 用 tool_call 事件、Hermes 用 Python 的 pre_tool_call(subprocess.run + 2 秒超时),三者都实现同一份委托协议,仅事件名、命令提取方式和 in-place 修改入口不同。这印证了文档中“thin delegate”的架构原则:Agent 差异被隔离在几十个 TS/Python 行里,规则演进全部发生在 Rust 侧。
排障时可参考 docs/guide/resources/troubleshooting.md:若 OpenCode 中命令未被改写,按序检查 ① which rtk 是否可用;② 插件文件是否存在于 ~/.config/opencode/plugins/rtk.ts(可用 rtk init -g --opencode 重新写入);③ 重启 OpenCode 使插件生效。由于插件全程静默容错,最常见的“无效果”根因是 rtk 不在 PATH 或版本过旧(< 0.23.0 无 rtk rewrite 子命令)。
五、小结
- 形态:OpenCode 集成是一个嵌入
@opencode-ai/plugin接口的 39 行 TypeScript 插件,基于 zx 执行 shell,拦截tool.execute.before事件并原地修改args.command,区别于 Claude Code/Cursor 的 JSON 协议 hook 与 Cline/Windsurf 的规则文件集成; - 安装:
rtk init -g --opencode全局安装至~/.config/opencode/plugins/rtk.ts,内容由include_str!编译期嵌入 Rust 二进制,写入幂等、支持 dry-run、可--uninstall移除; - 容错:
.quiet().nothrow()+ 启动自检 + 全路径 passthrough,保证插件任何故障都不会阻塞 OpenCode 的命令执行,满足 RTK 全局的“never block”退出码契约; - 规则:零逻辑在插件内,70+ 条改写模式与权限判定统一由
rtk rewrite子进程(src/discover/registry.rs/src/hooks/rewrite_cmd.rs)提供,单一事实来源,跨 Agent 行为一致。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00