首页
/ rtk 命令过滤器模块详解:四种 Filter 模式、执行流水线与新增命令过滤器的完整流程

rtk 命令过滤器模块详解:四种 Filter 模式、执行流水线与新增命令过滤器的完整流程

2026-09-06 13:48:52作者:沈韬淼Beryl

rtk 是一个用单个 Rust 二进制实现的 CLI 代理,通过对外部开发命令的 stdout/stderr 做压缩过滤来减少 LLM 读入的 token。本文以仓库文档 src/cmds/README.md 为主体,结合 src/core/runner.rssrc/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 个:clouddotnetgitgojsjvmphppythonrubyrustscalasystem。从源码结构看,jvm/php/scala/ 是 README 生态清单之后陆续加入的生态(分别覆盖 Maven/Gradle、PHP 工具链、sbt),其余各子目录都带有各自的 README(如 git/README.mdrust/README.mdjs/README.md),描述文件职责、解析策略与跨命令依赖。

Rust 模块 vs TOML 过滤器:什么时候必须写 Rust

这是文档中最重要的架构决策之一。TOML 过滤器(src/filters/ 目录下大量 .toml 文件)擅长纯行级过滤;而 Rust 模块存在于这里,是因为它们需要 TOML 引擎不具备的能力:

  1. 解析结构化输出(JSON、NDJSON);
  2. 跨阶段的状态机解析(state machine parsing across phases);
  3. 向子命令注入 CLI 参数(如 --format json);
  4. 跨命令路由(cross-command routing);
  5. 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()

  1. Spawnrun_streaming() 以管道化 stdout/stderr 启动子进程(Passthrough 模式下则继承父进程 TTY)。源码中可以看到 run_streaming 对 Passthrough 分支直接设置 Stdio::inherit()cmd.status() 同步等待;其余模式则 spawn() 后用 ChildGuard(RAII 包装)防止僵尸进程,原始输出受 RAW_CAP(10 MiB)上限保护(见 stream.rs 的 RAW_CAP 定义)。
  2. Filter:stdout 按所选 FilterMode 处理;stderr 由专用读取线程实时转发到终端。
  3. Print:过滤后的输出写入 stdout(Streaming 模式实时输出,CaptureOnly/Buffered 事后输出);若启用 tee 且命令失败,则追加恢复提示。
  4. Tracktimer.track() 记录 raw 与 filtered 的字节差,即 token 节省量。实现位于 tracking.rs 的 TimedExecution
  5. Exit code:向调用方返回 Ok(exit_code),由 main.rs 在唯一点调用 process::exit(code) 一次。

四种 FilterMode

run_streaming() 接受 FilterMode 的四种变体(定义见 stream.rs L241-L247),runner 入口函数(run_filteredrun_streamedrun_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_labelfilter_stdout_onlyskip_filter_on_failureno_trailing_newlineinherit_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")都由它处理。RegexBlockFilterBlockStreamFilter 均定义在 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 中的 CargoBuildHandlercmds/js/tsc_cmd.rs 中的 TscHandler

Level 3:StreamFilter trait —— 逐行完全控制。当块式解析不适配(状态机、多阶段输出、行变换)时直接实现 StreamFiltertrait 定义见 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_cmdruff_cmd
  • format_cmd 依据检测到的格式化器路由到 prettier_cmdruff_cmd
  • gh_cmdgit 模块导入 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 listdocker 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 模块(结构化输出、参数注入、状态机)

  1. 创建模块 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)。
  2. 注册模块
    • 生态 mod.rs 使用 automod::dir!()——目录内任何 .rs 文件自动成为公共模块,无需手写 pub mod。注意副作用:WIP 或辅助文件也会被暴露,因此只提交"命令就绪"的模块;
    • main.rsCommands 枚举中添加变体,附 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]main.rs 中的既有变体均使用该属性,使命令参数能透传给底层工具);
    • main.rs 添加路由匹配臂:Commands::Mycmd { args } => mycmd_cmd::run(&args, cli.verbose)?,
  3. 添加重写模式:在 src/discover/rules.rs 的 PATTERNS + RULES 数组(同索引位置)加入条目,使 hook 能自动重写命令。
  4. 编写测试:真实 fixture、快照测试、bash 输出减少 ≥ 20%(用 rtk 的 token 估算器度量,测试规范见 .claude/rules/cli-testing.md)。
  5. 更新文档:更新所在生态的 README(CHANGELOG.md 由 release-please 自动生成,无需手写)。

路径二:TOML 过滤器(简单行级过滤)

  1. src/filters/ 下创建过滤器 TOML;
  2. src/discover/rules.rs 添加重写模式;
  3. 编写测试、更新文档。

七、小结

src/cmds/ 的价值在于把"过滤一个命令"这件看似琐碎的事固化成了可复用、可审计的工程体系:core/runner.rs 的四个入口函数统一了执行骨架,FilterMode 四态决定了缓冲策略,RunOptions 构建器收敛了 tee/stdout-only 等横切开关,而退出码传播、过滤失败直通、tee 恢复、截断上限(CAP_* + reduced)、stderr 计入统计与全路径跟踪这组契约保证了所有生态模块行为一致。新增一个过滤器时,开发者只需按"模块 → 枚举注册 → 路由 → discover 规则 → 测试 → 文档"的清单逐项落地,底层执行、跟踪与恢复机制均由框架自动完成——这正是单二进制 rtk 能持续覆盖 git、cargo、npm、pytest、dotnet、aws 等十余种生态命令而不失一致性的结构性原因。

登录后查看全文
热门项目推荐
相关项目推荐