12 个 harness 对照表:驱动方式决定了它能做多少
CLI-Anything 仓库里的 harness 高度同构:目录树一样、入口都是 click 的 @click.group(invoke_without_command=True)、setup.py 里 console_scripts 都叫 cli-anything-<app>。看多了容易产生一个错觉,觉得这些 harness 是一批同样能力的东西,挑哪个都差不多。
把它们的驱动方式这一列拉出来对着看,这个错觉马上就破了。我们采集时(2026-08-10,仓库快照 39634a6)逐个读了 12 个代表应用的源码,命令面从 14 条到 276 条,差了近 20 倍;而这个差距跟作者写了多少行代码的关系,远不如跟「它是怎么碰到宿主软件的」关系大。
先分清五种碰宿主的姿势
12 个 harness 的 backend 大致落在五类里,每一类的能力上限完全不同。
一,压根不碰宿主。 obs-studio 是最纯粹的一个:整个包(排除 tests)里 grep -rl subprocess 命中 0 个文件,也没有任何文件 import requests 或 urllib。它只读写 JSON 场景集合,skills/SKILL.md 第 11 行自己写的是 “Uses a JSON scene collection format. No OBS installation required for editing.”。你以为在遥控 OBS,实际上它连 OBS 装没装都不关心。
二,自己解析并改宿主的文件格式,只在出片时喊一嗓子宿主。 inkscape 直接操作 SVG 这棵 XML 树,只有导出才拼 cmd = [inkscape, svg_path, f"--export-filename={output_path}", f"--export-dpi={dpi}"](utils/inkscape_backend.py:52)。kdenlive 走的是 JSON → MLT XML → melt 子进程,XML 生成器在 utils/mlt_xml.py,命令行拼装在 utils/melt_backend.py:130-134。
三,生成脚本交给宿主无头执行。 blender 把场景 JSON 翻成 bpy 脚本,utils/bpy_gen.py:1-4 的模块注释原文就是 “translates a scene JSON into a complete bpy script that can be run with: blender —background —python script.py”,真正拼装在 utils/blender_backend.py:54:cmd = [blender, "--background", "--python", script_path]。freecad 是同一套思路的放大版,用 tempfile.mkstemp(suffix=".py", prefix="freecad_macro_") 落宏文件(utils/freecad_backend.py:183)再交给 FreeCADCmd。gimp 则是 Script-Fu 批处理,utils/gimp_backend.py:47-63 的 batch_script_fu() 起 subprocess.run。
四,纯网络客户端。 comfyui 走 HTTP REST,默认 DEFAULT_BASE_URL = "http://localhost:8188"(utils/comfyui_backend.py:14);obsidian 走 Obsidian Local REST API 插件,默认 https://localhost:27124(utils/obsidian_backend.py:17);n8n 走它的 Public REST API,API_VERSION = "v1"、MIN_N8N_VERSION = "1.0.0"(utils/n8n_backend.py:18-20)。
五,多通道并存。 zotero 一个包里塞了三条路:只读 SQLite(utils/zotero_sqlite.py:25 的 connect_readonly())、本地 HTTP API(utils/zotero_http.py:183)、以及直接把 Zotero 进程拉起来(core/discovery.py:58 的 subprocess.Popen)。QGIS 是双通道,一边 from qgis.core import QgsApplication(utils/qgis_backend.py:105),一边起 qgis_process --json 子进程(:171)。
对照表
下面这张表是本篇的原始数据,12 行对应 12 个代表应用。“组内命令”不含各自的顶层 repl 等命令,core / utils 模块数不含 __init__.py。
| 应用 | 驱动宿主的方式(证据) | core | utils | 组内命令 | 非测试行 | 测试行 | test 函数 | e2e 是否需要真宿主 |
|---|---|---|---|---|---|---|---|---|
| blender | 生成 bpy 脚本 + blender --background --python(utils/blender_backend.py:54) | 9 | 4 | 53 | 5958 | 2729 | 234 | 否(tests/test_full_e2e.py:5) |
| obs-studio | 无(纯 JSON 场景集合,全包 0 处 subprocess/HTTP) | 8 | 2 | 42 | 2986 | 1554 | 173 | 否(skills/SKILL.md:11) |
| krita | krita --export --export-filename …(utils/krita_backend.py:206) | 3 | 3 | 19 | 2822 | 585 | 47 | 是,明说不降级(tests/test_full_e2e.py:3-5) |
| gimp | Script-Fu 批处理,缺 GIMP 回落 Pillow(utils/gimp_backend.py:47) | 7 | 2 | 40 | 4010 | 1187 | 115 | 部分(真实图片 I/O) |
| inkscape | 直接改 SVG + inkscape --export-filename=…(utils/inkscape_backend.py:52) | 10 | 3 | 60 | 4726 | 2010 | 207 | 否(tests/test_full_e2e.py:5) |
| QGIS | import PyQGIS + qgis_process --json(utils/qgis_backend.py:105/171) | 7 | 2 | 25 | 2610 | 821 | 22 | 是(需 QGIS 运行时) |
| zotero | 只读 SQLite + 本地 HTTP + 拉起进程(utils/zotero_sqlite.py:25 等三处) | 8 | 5 | 45 | 5098 | 2340 | 88 | 是(tests/TEST.md 标 Live validation) |
| obsidian | HTTPS REST,靠 Local REST API 插件(utils/obsidian_backend.py:17) | 5 | 2 | 14 | 1450 | 590 | 59 | 是(tests/test_full_e2e.py:1) |
| comfyui | HTTP REST(utils/comfyui_backend.py:14) | 4 | 1 | 19 | 1293 | 1075 | 73 | 否,全 mock(tests/test_full_e2e.py:3-4) |
| kdenlive | JSON → MLT XML → melt 子进程(utils/melt_backend.py:130) | 8 | 3 | 36 | 3467 | 1664 | 179 | 否(tests/test_full_e2e.py:3-4) |
| freecad | 生成 Python 宏 + FreeCADCmd 无头执行(utils/freecad_backend.py:183/209) | 19 | 4 | 276 | 22450 | 2670 | 107 | 分层,未装则 skip(tests/test_full_e2e.py:5-7) |
| n8n | REST API + 本地 SQLite 版本库(utils/n8n_backend.py:1、core/versions.py) | 12 | 2 | 57 | 3906 | 1340 | 127 | 是,需实例与 env(tests/test_full_e2e.py:1-3) |
反直觉的那一处:命令面被宿主的接口面卡死,不是被作者卡死
表里最扎眼的一组对比是 obsidian 的 14 条和 freecad 的 276 条。
obsidian 的 core 里五个模块小得离谱:vault.py 62 行、search.py 59 行、note.py 14 行、command.py 14 行、server.py 12 行。命令面是 vault 6 条、search 2 条、note 2 条、command 2 条、server 1 条、session 1 条。这不是没写完,而是它的能力全部来自 Obsidian Local REST API 插件暴露出来的那几个端点——插件给什么,它就只能包什么。comfyui 同理,1293 行是 12 个里最少的,5 个组 19 条命令,全都对着 ComfyUI 的 REST 端点,图片下载走 /view(core/images.py:1-7)。
freecad 反过来。它自己生成宏文本(utils/freecad_macro_gen.py 627 行),宏能调的 FreeCAD Python API 有多宽,它的命令面就能铺多宽,于是 19 个顶层组、276 条组内命令、22450 行非测试代码,freecad_cli.py 单文件 4911 行。blender 也在这一类,只是模块划分收得更紧,9 个 core 模块 53 条命令。
再看不碰宿主的那一类。obs-studio 一个 subprocess 都没起,却做出了 42 条命令和 12 种源类型、13 种滤镜类型、8 个编码预设(core/sources.py:17、core/filters.py:8、core/output.py:7)——因为文件格式本身就是它的全部世界,不需要任何人给它接口。inkscape 更极端,把 SVG 当自家数据结构改,做到了 60 条命令、10 个 core 模块。
所以判断一个 harness 能帮你做多少事,先看它是哪一类驱动方式,比看 star 数或代码行数都准。纯 API 客户端类的上限就是宿主 API 的边界;自己解析文件格式或生成脚本的那类,上限在作者愿意铺多少命令。
第二处反直觉:e2e 是否需要真宿主,和驱动方式并不对应
这是我们比对时最容易先入为主的一栏。直觉是「起子进程的必须装宿主,纯改 JSON 的不用装」,但表里的实际情况把这条直觉打散了:
- kdenlive 会真的起
melt子进程,可它的 e2e 文件头写的是 “No Kdenlive or melt installation required.”(tests/test_full_e2e.py:1-5)。 - blender 会真的调 blender 可执行文件,e2e 文件头同样写 “No actual Blender installation is required”(
tests/test_full_e2e.py:1-6)。 - comfyui 是纯 HTTP 客户端,e2e 却”全 mock”——文件头原文 “all tests use mocked HTTP responses. They do NOT require ComfyUI to be installed or running.”(
tests/test_full_e2e.py:1-8),tests/TEST.md:5也写 “No ComfyUI installation required”。 - 反过来,krita 只是调可执行文件的批处理导出参数,它的 e2e 文件头却明说 “These tests invoke the REAL Krita application for export operations… No graceful degradation — Krita must be installed.”(
tests/test_full_e2e.py:1-6)。
这条对你的实际影响很直接:「这个 harness 有几百个测试函数」不能拿来当「它真能驱动宿主」的证据。blender 那 234 个 test 函数、kdenlive 那 179 个,按文件头的说法都不需要宿主在场。注册表里有这一条、包里测试很齐全,跟你装上之后它能不能真的操控你机器上那个软件,是两回事。要判断后者,得去看 e2e 文件头那几行 docstring,以及 backend 模块里对可执行文件的探测逻辑——比如 gimp 探三种名字 gimp / gimp-2.10 / gimp-2.99(utils/gimp_backend.py:27),freecad 探 _FREECAD_CMD_NAMES = ["freecadcmd", "FreeCADCmd", "freecad", "FreeCAD"](utils/freecad_backend.py:50)。
顺带一提,12 个里明确写了双后端回落路径的只有 gimp:core/export.py:6-8 列了 GIMP 与 Pillow 两条路径,GIMP 缺席时回落 Pillow,第 100 到 102 行的报错文案提示 pip install Pillow。
状态与撤销:另一条把 12 个劈成两半的线
驱动方式之外还有一条分界线,是有没有工程文件与撤销栈。blender、obs-studio、gimp、kdenlive 用的是同一款 Session,core/session.py:36-39 都带 MAX_UNDO = 50,落盘用 fcntl.flock 加排他锁。krita 的 Session 换了个形状,存 (时间戳, label, dict) 三元组快照,撤销时丢弃 redo 分支(core/session.py:17、:29-40)。QGIS 有 session.py 但只有 status/history 两条命令,没有 undo/redo。
而 obsidian 和 comfyui 的 core 里根本没有 session.py——obsidian 的 session 组只有 1 条命令,comfyui 连 __main__.py 都没有。n8n 走了第三条路:它不做 undo 栈,改成用本地 SQLite 存每次写操作前的工作流快照(core/versions.py:1-5),对应 workflow versions 组的 list/rollback/show/diff/prune/stats 六条命令,自动清理保留 keep: int = 50(:87)。
Windows 用户要多看一眼这里:那把 fcntl.flock 的锁在 Windows 上是没有的。freecad 的 core/session.py:20-23 把话说明白了——在缺少 fcntl 的平台(Windows)上,写入不加锁照常进行。也就是说同一份工程 JSON 被两个进程同时写的场景,在 Windows 上没有这层保护。
仓库自己标出来的未完成处
选型时这几处值得先看,都是仓库自己写在代码里的:
- inkscape 的布尔路径运算(union / intersection / difference / exclusion)在
core/paths.py:171带注释# Placeholder,结果的d字段取的是obj_a的原值。 - freecad 的
core/mesh.py:263写 “volume/area placeholders”,:307写 “Diagnostic placeholder with mesh metadata.” - zotero 把实验能力隔在
core/experimental.py里,:11抛错要求 Zotero 必须先关闭,:27与:45抛错说明只支持本地用户库,返回体带"experimental": True字段;README 第 86 行重复了这条限制,并单列一节 ”### 10. Experimental Collection Refactoring”(README.md:331-338),命令带--experimental开关。
另外文档数字与代码对不上的地方,freecad 那边尤其集中:skills/SKILL.md:3 与 agent-harness/FREECAD.md:8 都写 “258 commands”,SKILL.md 正文写 “across 17 groups”,FREECAD.md 写 “18 workbench groups”,而代码里 freecad_cli.py 有 277 条 @x.command(、19 个顶层组加 1 个嵌套组;同包 README.md:97-107 的命令组表只列 7 组。TEST.md 与我们数出来的 def test_ 条数对不上的还有 blender、gimp、inkscape、kdenlive 四家;obs-studio 的合计数(153)恰好一致,但 test_core / e2e 的分项(TEST.md 写 117/36,我们数出来是 116/37)与类数对不上。这些是可以各自核对的事实,我们只陈述到这里;至于哪一边是”对的”,我们没有依据,不做判断。
安全这一栏,表里没有但你必须自己加
这 12 个 harness 里有 8 个会在你本机执行外部程序:blender、krita、gimp、inkscape、QGIS、zotero、kdenlive、freecad,形式是起子进程、跑生成出来的脚本或宏。其中 freecad 和 blender 是把临时生成的 Python 文本交给宿主执行的。这件事本身没有被淡化的余地:你在跑的是一条”由 agent 拼参数、在你本机落地执行”的链路。
仓库里至少有一处把这个风险写在了注释里。kdenlive 的 utils/melt_backend.py:17-21 原文说明,编解码参数是由 AI agent 控制的,接受任意字符串会让一个被攻陷或被提示注入的 agent 把构造过的值传进 melt 子进程,所以改用 ALLOWED_VCODECS / ALLOWED_ACODECS 白名单(:22-30)。在我们这次读到的范围里,只有 kdenlive 把提示注入风险写进了注释;其余各家的参数校验我们没有逐命令核实,不能据此断定别家没有类似约束。
网络那一侧也有两处值得知道。obsidian 的 utils/obsidian_backend.py:11 调了 urllib3.disable_warnings(...),:34、:65、:103 三处请求都传 verify=False,因为对接的是自签证书的本地插件;鉴权取值来自 --api-key 或环境变量 OBSIDIAN_API_KEY(obsidian_cli.py:126)。comfyui 的 backend 注释里则注明默认不需要鉴权(utils/comfyui_backend.py:8)。这两条都意味着相关端口的可达范围要你自己控制。
你可以自己复现的核查动作
这张表里的每一列都能自己数一遍,clone 仓库后在根目录执行:
find <app> -name '*.py' -not -path '*/tests/*' | xargs wc -l | tail -1
find <app> -path '*/tests/*' -name '*.py' | xargs wc -l | tail -1
grep -h 'def test_' <app>/agent-harness/cli_anything/*/tests/*.py | wc -l
grep -c 'dry.run\|dry_run' <cli>.py
以上为按仓库中的参数语义组合的示例,未经实测,以官方文档与 --help 的实际输出为准。
还有一个动作没写成命令,但它是本篇里性价比最高的一步:直接打开各 harness 的 tests/test_full_e2e.py,读开头那几行 docstring。判断”需不需要真装宿主”,那几行就是最快的入口——表里最后一列的依据全在那儿。
另外补一个跟驱动方式相关的数:--dry-run 只在部分 harness 出现,blender / obs / gimp / kdenlive 各 4 处、inkscape 7 处、n8n 5 处、krita 3 处、freecad 3 处,而 QGIS / zotero / obsidian / comfyui 是 0 处。要在动真格之前先看一眼会发生什么,这四家给不了。
拿这张表怎么做决定
按你的处境倒推:
- 宿主软件你机器上没装、也不打算装:只有不碰宿主那一类对你有意义,obs-studio 是纯的,inkscape 与 kdenlive 的编辑部分也基本在本地文件层完成,出片才需要宿主。
- 宿主是服务型的(自带 API 或插件):obsidian、comfyui、n8n 这类要先把服务端立起来并配好环境变量,n8n 缺
N8N_BASE_URL时会直接报 “Set N8N_BASE_URL env var or pass —url.”(utils/n8n_backend.py:42)。这类 harness 的能力面小不是缺点,是接口面就那么大。 - 你要的是批量出片 / 参数化建模:blender、freecad 这类生成脚本再交给宿主无头执行的,能做的事最多,代价是宿主必须真装,且执行的是本机生成的脚本文本。
- 你在 Windows 上并且会并发跑:先看一眼上面那条 fcntl 的说明。
- 你需要”能撤回”:认
MAX_UNDO = 50那一族,或者 n8n 那套 SQLite 版本库;obsidian、comfyui 没有这层。
至于每家该怎么配、参数该调成多少,项目没有给通用值,取决于你自己的用法。我们也只读了这 12 个,仓库根目录下还有约 70 个同构目录没读,它们各自属于哪一类驱动方式,得按上面那几条命令自己数。
本文依据 CLI-Anything 官方仓库(github.com/HKUDS/CLI-Anything)的 README、registry.json、
docs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。
本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness,
也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。
这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。
注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。