rtk 命令过滤器模块详解:四种 Filter 模式、执行流水线与新增命令过滤器的完整流程
rtk 是一个用单个 Rust 二进制实现的 CLI 代理,通过对外部开发命令的 stdout/stderr 做压缩过滤来减少 LLM 读入的 token。本文以仓库文档 src/cmds/README.md 为主体,结合 src/core/runner.rs、src/core/stream.rs 等实现源码,完整讲解 src/cmds/ 目录的职责边界、按生态组织的命令过滤器模块、Spawn→Filter→Print→Track 五阶段执行流水线、四种 FilterMode 的选型依据、tee 恢复与截断上限等行为契约,以及从写 Rust 模块到注册路由、补充重写规则的端到端新增过滤器流程。
一、模块定位:谁负责什么
src/cmds/ 是 rtk 中所有"命令过滤"逻辑的集合地。文档给出的边界规则非常明确,这也是理解整个目录的第一把钥匙:
- 负责(Owns):命令执行与输出过滤。这里的每个模块都调用一个外部 CLI 工具(
Command::new("some_tool")),把它的 stdout/stderr 转换为更小的字节流供 agent 读取,并通过 src/core/tracking.rs 记录节省量。按生态组织为 git、rust、js、python、go、dotnet、cloud、system 等子目录;跨生态路由(例如lint_cmd检测到 Python 项目后转交ruff_cmd)属于组件内部事务。 - 不负责(Does not own):TOML DSL 过滤引擎(在 src/core/toml_filter.rs)、hook 拦截(在
hooks/)、分析看板(在analytics/)。cmds/是 tracking 数据库的写入方,analytics/是读取方。 - 边界判据:一个模块当且仅当"执行外部命令并过滤其输出"时才属于这里;服务于多个模块且不调用外部命令的基础设施应放在
core/。
当前 src/cmds/mod.rs 中实际注册的生态模块共 12 个:cloud、dotnet、git、go、js、jvm、php、python、ruby、rust、scala、system。从源码结构看,jvm/、php/、scala/ 是 README 生态清单之后陆续加入的生态(分别覆盖 Maven/Gradle、PHP 工具链、sbt),其余各子目录都带有各自的 README(如 git/README.md、rust/README.md、js/README.md),描述文件职责、解析策略与跨命令依赖。
Rust 模块 vs TOML 过滤器:什么时候必须写 Rust
这是文档中最重要的架构决策之一。TOML 过滤器(src/filters/ 目录下大量 .toml 文件)擅长纯行级过滤;而 Rust 模块存在于这里,是因为它们需要 TOML 引擎不具备的能力:
- 解析结构化输出(JSON、NDJSON);
- 跨阶段的状态机解析(state machine parsing across phases);
- 向子命令注入 CLI 参数(如
--format json); - 跨命令路由(cross-command routing);
- flag-aware filtering——检测用户显式请求的冗长参数(例如
--nocapture)并相应调整压缩策略。
对应的决策表与贡献哲学见 CONTRIBUTING.md。生态归属规则也很具体:命令归入其语言/工具链对应的目录;语言无关命令放 system/;当有 3 个以上相关命令时才新建一个生态目录。
二、执行流水线:从 Spawn 到 Exit Code
所有模块遵循同一个模式:执行底层命令 → 过滤输出 → 记录 token 节省 → 传播退出码。执行骨架封装在共享包装器 src/core/runner.rs 中——模块只负责构建 Command(自定义参数逻辑),然后把执行、跟踪、tee 恢复和退出码传播全部交给 runner 入口函数。
文档给出的数据流图如下(完整保留):
run_streaming() Filter applied tee_and_hint()
| (per-line or post-hoc) |
v | v
+---------+ stdout +-------+-------+ filtered +-------+
| Spawn |--------->| filter |----------->| Print |
+---------+ stderr +---------------+ +-------+
| (live) |
v v
+----------+ +---------+
| raw = | | Track |
| stdout + | | savings |
| stderr | +---------+
+----------+ |
v
+-----------+
| Ok(code) |
| returned |
+-----------+
整个流程分为 5 个阶段,且所有执行都经过 src/core/stream.rs 中的 run_streaming():
- Spawn:
run_streaming()以管道化 stdout/stderr 启动子进程(Passthrough 模式下则继承父进程 TTY)。源码中可以看到run_streaming对 Passthrough 分支直接设置Stdio::inherit()并cmd.status()同步等待;其余模式则spawn()后用ChildGuard(RAII 包装)防止僵尸进程,原始输出受RAW_CAP(10 MiB)上限保护(见 stream.rs 的 RAW_CAP 定义)。 - Filter:stdout 按所选
FilterMode处理;stderr 由专用读取线程实时转发到终端。 - Print:过滤后的输出写入 stdout(Streaming 模式实时输出,CaptureOnly/Buffered 事后输出);若启用 tee 且命令失败,则追加恢复提示。
- Track:
timer.track()记录 raw 与 filtered 的字节差,即 token 节省量。实现位于 tracking.rs 的 TimedExecution。 - Exit code:向调用方返回
Ok(exit_code),由main.rs在唯一点调用process::exit(code)一次。
四种 FilterMode
run_streaming() 接受 FilterMode 的四种变体(定义见 stream.rs L241-L247),runner 入口函数(run_filtered、run_streamed、run_passthrough)会自动选择合适模式——模块作者无需直接接触 FilterMode:
| FilterMode | 工作方式 | 使用方 |
|---|---|---|
CaptureOnly |
静默缓冲全部 stdout,事后把完整字符串交给 filter_fn;stderr 实时流到终端 |
run_filtered()(默认路径) |
Buffered |
缓冲全部 stdout,应用过滤器后再打印;stderr 实时流。文档称由 run_filtered() 在 filter_stdout_only 置位时自动选择 |
run_filtered()(stdout-only 路径) |
Streaming |
每行 stdout 到达即送入 StreamFilter::feed_line(),输出的行立即打印;进程退出后调用 flush() 收尾 |
run_streamed() |
Passthrough |
直接继承父 TTY——无管道、无缓冲,raw/filtered 为空 |
run_passthrough() |
一个源码层面的细节值得指出:从 stream.rs 的 FilterMode 定义 看,Buffered 变体目前带有 #[allow(dead_code)] 标注,而 run_captured_filter 实际始终传入 FilterMode::CaptureOnly,并在 filter_stdout_only 置位时仅以 raw_stdout 而非合并文本作为过滤输入。也就是说,当前实现中"stdout-only"语义是通过输入源选择而非独立的 Buffered 模式完成的,Buffered 更像是保留的接口变体。
场景选型:该用哪个 runner
文档给出了一张选型表,覆盖了绝大多数实际场景:
| 场景 | Runner | FilterMode | 原因 |
|---|---|---|---|
| 解析结构化输出(JSON、表格) | run_filtered() |
CaptureOnly/Buffered | 过滤器需要完整文本才能解析结构 |
| 长时间运行、可按行解析的输出 | run_streamed() |
Streaming | 内存占用低,输出实时 |
| 不过滤、只记录用量 | run_passthrough() |
Passthrough | 零开销,直接继承 TTY |
| 自定义逻辑(多命令、文件 I/O) | 手工 exec_capture() |
CaptureOnly | 对执行过程完全可控 |
三、RunOptions 配置构建器
runner.rs 中的 RunOptions 是一个 #[derive(Default)] 的构建器,文档列出的四个构造/链式方法及其行为如下:
| 构造方式 | 行为 |
|---|---|
RunOptions::default() |
stdout+stderr 合并后交给过滤器,无 tee |
RunOptions::with_tee("label") |
合并过滤 + tee 恢复 |
RunOptions::stdout_only() |
只过滤 stdout,stderr 直通,无 tee |
RunOptions::stdout_only().tee("label") |
只过滤 stdout + tee 恢复 |
对照 runner.rs 的 RunOptions 结构体,实际字段为 tee_label、filter_stdout_only、skip_filter_on_failure、no_trailing_newline、inherit_stdin 五个,因此除文档所列方法外,源码还提供三个文档未列出的链式开关,可作为进阶用法参考:
early_exit_on_failure():置位skip_filter_on_failure。从 run_captured_filter 的实现 看,一旦子进程退出码非 0,就原样打印 raw stdout/stderr、跳过过滤并直接跟踪原始数据——适合"失败时宁可多输出也不丢信息"的命令。no_trailing_newline():打印时不追加末尾换行,用于精确拼接输出。inherit_stdin():把 rtk 自身的 stdin 转发给子进程。源码注释明确说明动机:对于能从管道读入的命令(如cat file | rtk wc),不开启它时子进程拿到空 stdin 会统计为零。
四、实现模板:从文档代码示例到生产代码
以下是文档给出的四类实现模板。第一个是推荐的默认写法——过滤型命令:
pub fn run(args: &[String], verbose: u8) -> Result<i32> {
let mut cmd = resolved_command("mycmd");
for arg in args { cmd.arg(arg); }
if verbose > 0 { eprintln!("Running: mycmd {}", args.join(" ")); }
runner::run_filtered(
cmd, "mycmd", &args.join(" "),
filter_mycmd_output,
runner::RunOptions::stdout_only().tee("mycmd"),
)
}
使用 run_filtered() 时退出码处理是全自动的——包装器提取退出码(包括 Unix 信号处理,按 128+signal 约定)、跟踪节省量并返回 Ok(exit_code),模块作者直接返回其结果即可。这里有两个源码印证:resolved_command 定义于 core/utils.rs,而 run_filtered 本体 只是把 filter_fn 包进 RunMode::Filtered 后转调统一的 run()。此外 runner 还暴露了 run_filtered_with_exit()(过滤器可感知退出码,见 runner.rs L237)与 run_passthrough()、run_streamed() 两个入口。
流式过滤器的三层抽象
当命令长时间运行或产生无界输出、需要逐行过滤时,应使用 runner::run_streamed()。文档给出从最简到最灵活的三个抽象层级:
Level 1:RegexBlockFilter —— 正则起始模式 + 缩进续行(3~5 行即可配置)。
use crate::core::stream::{BlockStreamFilter, RegexBlockFilter};
pub fn run(args: &[String], verbose: u8) -> Result<i32> {
let mut cmd = resolved_command("mycmd");
for arg in args { cmd.arg(arg); }
let filter = RegexBlockFilter::new("mycmd", r"^error\[")
.skip_prefixes(&["warning:", "note:"]);
runner::run_streamed(
cmd, "mycmd", &args.join(" "),
Box::new(BlockStreamFilter::new(filter)),
runner::RunOptions::with_tee("mycmd"),
)
}
适用于"块式错误":块以正则匹配的行开始、以缩进行继续。块计数、按前缀跳过、自动摘要("mycmd: 3 blocks in output" 或 "mycmd: no errors found")都由它处理。RegexBlockFilter 与 BlockStreamFilter 均定义在 stream.rs 中。
Level 2:BlockHandler trait —— 自定义块检测 + 状态跟踪。当正则+缩进不够用、需要状态化解析时实现该 trait 并包进 BlockStreamFilter:
use crate::core::stream::{BlockHandler, BlockStreamFilter};
struct MyHandler { error_count: usize }
impl BlockHandler for MyHandler {
fn should_skip(&mut self, line: &str) -> bool { line.is_empty() }
fn is_block_start(&mut self, line: &str) -> bool {
if line.starts_with("FAIL") { self.error_count += 1; true } else { false }
}
fn is_block_continuation(&mut self, line: &str, _block: &[String]) -> bool {
line.starts_with(" ") || line.starts_with("at ")
}
fn format_summary(&self, _exit_code: i32, _raw: &str) -> Option<String> {
Some(format!("{} failures\n", self.error_count))
}
}
文档指名的生产级示例:cmds/rust/cargo_cmd.rs 中的 CargoBuildHandler 与 cmds/js/tsc_cmd.rs 中的 TscHandler。
Level 3:StreamFilter trait —— 逐行完全控制。当块式解析不适配(状态机、多阶段输出、行变换)时直接实现 StreamFilter(trait 定义见 stream.rs L42):
use crate::core::stream::StreamFilter;
struct MyFilter { state: State }
impl StreamFilter for MyFilter {
fn feed_line(&mut self, line: &str) -> Option<String> {
// 返回 Some(text) 即输出该行,返回 None 即抑制
if line.contains("error") { Some(format!("{}\n", line)) } else { None }
}
fn flush(&mut self) -> String { String::new() }
fn on_exit(&mut self, exit_code: i32, raw: &str) -> Option<String> { None }
}
完整参考实现是 cmds/rust/runner.rs 中的 ErrorStreamFilter——一个跨行跟踪错误块的状态机。
直通与手工执行
不需要过滤的命令(仅记录用量):
pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<i32> {
runner::run_passthrough("mycmd", args, verbose)
}
需要自定义逻辑(多命令编排、文件 I/O)时手工执行:
pub fn run(args: &[String], verbose: u8) -> Result<i32> {
let output = resolved_command("mycmd").args(args)
.output().context("Failed to run mycmd")?;
let exit_code = exit_code_from_output(&output, "mycmd");
// ... 自定义过滤、跟踪 ...
Ok(exit_code)
}
五、跨命令依赖与横切行为契约
跨命令依赖
文档列出了三处组件内部的路由/复用关系:
lint_cmd检测到 Python 项目时路由到mypy_cmd或ruff_cmd;format_cmd依据检测到的格式化器路由到prettier_cmd或ruff_cmd;gh_cmd从git模块导入compact_diff()做 diff 格式化(markdown 辅助函数则定义在gh_cmd自身)。
退出码传播
所有模块的 run() 都返回 Result<i32>,其中 i32 是底层命令的退出码;main.rs 在唯一点调用 std::process::exit(code),模块绝不直接调用 process::exit():
| 返回值 | 含义 | 谁负责退出 |
|---|---|---|
Ok(0) |
命令成功 | main.rs 以 0 退出 |
Ok(N) |
命令以 N 失败 | main.rs 以 N 退出 |
Err(e) |
rtk 本身失败(非命令失败) | main.rs 打印错误并以 1 退出 |
退出码的提取方式按执行风格分三种:
| 执行风格 | 辅助函数 | 信号处理 |
|---|---|---|
cmd.output()(过滤式) |
exit_code_from_output(&output, "tool") |
Unix 下 128+signal |
cmd.status()(直通式) |
exit_code_from_status(&status, "tool") |
Unix 下 128+signal |
run_filtered()(包装器) |
自动,无需手写 | 内置 |
这两个辅助函数的实现见 core/utils.rs L215 与 L236:当 status.code() 为 None(进程被信号终止)时,在 Unix 平台读取 signal() 并按 128+signal 返回,同时向 stderr 打印诊断;非 Unix 平台回退为 1。手工执行时必须使用这些辅助函数返回 Ok(exit_code),绝不能调用 process::exit(),也绝不能用 .code().unwrap_or(1)——那会丢失信号信息。
过滤失败直通
过滤失败时回退到原始输出并向 stderr 发出警告,永远不阻塞用户。
tee 恢复
解析结构化输出(JSON、NDJSON、状态机)的模块必须调用 tee::tee_and_hint(),使用户在命令失败时能取回完整原始输出。该函数定义于 core/tee.rs L270,在 runner 中由 print_with_hint 组合进打印流程——过滤结果先与恢复提示拼接,再经 guard::never_worse() 保证输出体积不超过原始数据("never worse" 保证:过滤后若比原文还长,则直接输出原文)。
内部截断恢复(Truncation Caps)
当过滤器把列表截断到 N 项(如 take(20))时,剩余项必须能通过 tee 提示取回。绝不允许在没有恢复路径的情况下显示 "… +N more"——agent 将无法获取被隐藏的内容。
| 内容类型 | 函数 | 适用条件 |
|---|---|---|
| 扁平列表(一项=tee 中一行) | force_tee_tail_hint(content, slug, MAX + 1) |
PR 列表、错误行、文件路径等单行项 |
| 多行块 | force_tee_hint(content, slug) |
测试失败、构建错误块等跨多项的行——此时行偏移无意义 |
截断上限统一来自 src/core/truncate.rs,当前定义了四个数据类常量:
| 常量 | 值 | 适用数据 |
|---|---|---|
CAP_ERRORS |
20 | 错误——可操作性最强,展示最多 |
CAP_WARNINGS |
10 | 警告与测试失败——信号密度低于错误 |
CAP_LIST |
20 | 扁平列表(PR、服务、包),每项一行 |
CAP_INVENTORY |
50 | 清单类(pip list、docker images),需要尽量穷尽 |
文档给出的使用规范:按数据类挑选 CAP_* 并绑定到本地 const MAX_XXX: usize = CAP_Y;,take(MAX_XXX)、> MAX_XXX 与偏移量 MAX_XXX + 1 全部从该本地常量推导。这样做的原因是这些 CAP 将成为未来"按过滤器调上限"的配置面(用户可通过配置覆盖)——把截断值全部路由经过 CAP,配置落地时就是一次开关切换,而不是全代码库搜索替换。确需偏离时,必须用 truncate.rs 的 reduced()(例如 reduced(CAP_WARNINGS, 5)),使偏离值仍能跟随全局配置联动;禁止裸字面量,禁止 cap - n(上限一旦可运行时配置就会下溢),禁止 *// 缩放(会无界放大)。reduced 在缩减量会清空列表时回退到完整上限(其不变量由 truncate.rs 内的测试矩阵 全量覆盖)。每次偏离需附一行注释说明原因;没有真实理由就直接用普通 CAP。
另外两条配套规则:
- tee 内容必须与
tail的产出一致。对force_tee_tail_hint,tee 要用与显示相同的格式化值构建,而不是原始/中间数据。若过滤器在展示前重排了项,应预先构建一个格式化行的Vec<String>,同时用于显示循环与 tee。 - stderr 必须计入统计:模块须捕获 stderr 并合并进传给
timer.track()的 raw 字符串,使 token 节省量反映总输出。
跟踪完整性与 verbose 参数
所有模块必须在每条路径(成功、失败、回退)上调用 timer.track()。由于模块返回 Ok(exit_code) 而非自行退出,跟踪一定在程序退出前执行。所有模块还接受 verbose: u8 参数用于打印调试信息(执行的命令、节省百分比、过滤器层级),不允许接受后无视。
六、新增一个命令过滤器:完整清单
文档强调新增过滤器/命令需要多处改动,并给出了两条路径的清单。
路径一:Rust 模块(结构化输出、参数注入、状态机)
- 创建模块
src/cmds/<ecosystem>/mycmd_cmd.rs:- 编写
filter_mycmd()(纯函数:&str -> String,无副作用); - 用
runner::run_filtered()编写pub fn run(...) -> Result<i32>——构建Command、选择RunOptions、委托执行; - 过滤器解析结构化 stdout(JSON、NDJSON)时用
RunOptions::stdout_only()——stderr 会污染解析; - 过滤合并文本输出时用
RunOptions::default(); - 解析结构化输出的过滤器应加
.tee("label")(命令失败时可恢复原始输出); - 退出码:由
run_filtered()自动处理,直接返回其结果; - 截断:若过滤器把任何列表截断到 N 项,必须输出
force_tee_tail_hint(扁平列表)或force_tee_hint(多行块),让 agent 能取回被隐藏项;上限使用命名常量,偏移量由它推导(MAX_XXX + 1)。
- 编写
- 注册模块:
- 生态
mod.rs使用automod::dir!()——目录内任何.rs文件自动成为公共模块,无需手写pub mod。注意副作用:WIP 或辅助文件也会被暴露,因此只提交"命令就绪"的模块; - 在 main.rs 的
Commands枚举中添加变体,附#[arg(trailing_var_arg = true, allow_hyphen_values = true)](main.rs 中的既有变体均使用该属性,使命令参数能透传给底层工具); - 在
main.rs添加路由匹配臂:Commands::Mycmd { args } => mycmd_cmd::run(&args, cli.verbose)?,。
- 生态
- 添加重写模式:在 src/discover/rules.rs 的 PATTERNS + RULES 数组(同索引位置)加入条目,使 hook 能自动重写命令。
- 编写测试:真实 fixture、快照测试、bash 输出减少 ≥ 20%(用 rtk 的 token 估算器度量,测试规范见 .claude/rules/cli-testing.md)。
- 更新文档:更新所在生态的 README(
CHANGELOG.md由 release-please 自动生成,无需手写)。
路径二:TOML 过滤器(简单行级过滤)
- 在 src/filters/ 下创建过滤器 TOML;
- 在
src/discover/rules.rs添加重写模式; - 编写测试、更新文档。
七、小结
src/cmds/ 的价值在于把"过滤一个命令"这件看似琐碎的事固化成了可复用、可审计的工程体系:core/runner.rs 的四个入口函数统一了执行骨架,FilterMode 四态决定了缓冲策略,RunOptions 构建器收敛了 tee/stdout-only 等横切开关,而退出码传播、过滤失败直通、tee 恢复、截断上限(CAP_* + reduced)、stderr 计入统计与全路径跟踪这组契约保证了所有生态模块行为一致。新增一个过滤器时,开发者只需按"模块 → 枚举注册 → 路由 → discover 规则 → 测试 → 文档"的清单逐项落地,底层执行、跟踪与恢复机制均由框架自动完成——这正是单二进制 rtk 能持续覆盖 git、cargo、npm、pytest、dotnet、aws 等十余种生态命令而不失一致性的结构性原因。
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 StartedRust0625
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