12 个 harness 对照表:驱动方式决定了它能做多少

2026-08-10

CLI-Anything 仓库里的 harness 高度同构:目录树一样、入口都是 click@click.group(invoke_without_command=True)setup.pyconsole_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:54cmd = [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-63batch_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:27124utils/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:25connect_readonly())、本地 HTTP API(utils/zotero_http.py:183)、以及直接把 Zotero 进程拉起来(core/discovery.py:58subprocess.Popen)。QGIS 是双通道,一边 from qgis.core import QgsApplicationutils/qgis_backend.py:105),一边起 qgis_process --json 子进程(:171)。

对照表

下面这张表是本篇的原始数据,12 行对应 12 个代表应用。“组内命令”不含各自的顶层 repl 等命令,core / utils 模块数不含 __init__.py

应用驱动宿主的方式(证据)coreutils组内命令非测试行测试行test 函数e2e 是否需要真宿主
blender生成 bpy 脚本 + blender --background --pythonutils/blender_backend.py:54945359582729234否(tests/test_full_e2e.py:5
obs-studio无(纯 JSON 场景集合,全包 0 处 subprocess/HTTP)824229861554173否(skills/SKILL.md:11
kritakrita --export --export-filename …utils/krita_backend.py:2063319282258547是,明说不降级(tests/test_full_e2e.py:3-5
gimpScript-Fu 批处理,缺 GIMP 回落 Pillow(utils/gimp_backend.py:47724040101187115部分(真实图片 I/O)
inkscape直接改 SVG + inkscape --export-filename=…utils/inkscape_backend.py:521036047262010207否(tests/test_full_e2e.py:5
QGISimport PyQGIS + qgis_process --jsonutils/qgis_backend.py:105/1717225261082122是(需 QGIS 运行时)
zotero只读 SQLite + 本地 HTTP + 拉起进程(utils/zotero_sqlite.py:25 等三处)85455098234088是(tests/TEST.md 标 Live validation)
obsidianHTTPS REST,靠 Local REST API 插件(utils/obsidian_backend.py:175214145059059是(tests/test_full_e2e.py:1
comfyuiHTTP REST(utils/comfyui_backend.py:1441191293107573否,全 mock(tests/test_full_e2e.py:3-4
kdenliveJSON → MLT XML → melt 子进程(utils/melt_backend.py:130833634671664179否(tests/test_full_e2e.py:3-4
freecad生成 Python 宏 + FreeCADCmd 无头执行(utils/freecad_backend.py:183/209194276224502670107分层,未装则 skip(tests/test_full_e2e.py:5-7
n8nREST API + 本地 SQLite 版本库(utils/n8n_backend.py:1core/versions.py1225739061340127是,需实例与 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 端点,图片下载走 /viewcore/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:17core/filters.py:8core/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.99utils/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 用的是同一款 Sessioncore/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:3agent-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_KEYobsidian_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.jsondocs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。 本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness, 也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。 这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。 注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。