krita / gimp / inkscape:三个画图工具的 harness 怎么分工
在 CLI-Anything 仓库里,krita、gimp、inkscape 是三个挨着的目录,包路径也长得一模一样,都是 <app>/agent-harness/cli_anything/<pkg>/<pkg>_cli.py。三个都是「画图软件」,你很容易预期它们的 harness 是一个模子刻出来的三份拷贝。
我们采集时(2026-08-10,对应仓库快照 39634a6)把这三个包逐个读了一遍,结论是:外壳同构,里子不同。而且差异的方向和直觉是反的——命令面最小的 krita,恰恰是三个里最离不开你本机那份宿主软件的一个。
这里先声明一件事,后面反复要用:这类 harness 是要在你本机起外部进程、跑外部程序的。三个包里的 backend 模块都有 subprocess.run,krita 在 utils/krita_backend.py:108,gimp 在 utils/gimp_backend.py:47-63 的 batch_script_fu() 里,inkscape 在 utils/inkscape_backend.py 的 :58、:92、:124 三处。宿主软件也得你自己装。这不是细节,是这三个 harness 分工差异的根因。
三行对照
| 驱动宿主的方式 | core 模块 | 组内命令 | 非测试行 / 测试行 | test 函数 | e2e 要不要真宿主 | |
|---|---|---|---|---|---|---|
| krita | krita --export --export-filename …(utils/krita_backend.py:206) | 3 | 19 | 2822 / 585 | 47 | 要,且明说不降级 |
| gimp | Script-Fu 批处理,缺 GIMP 回落 Pillow(utils/gimp_backend.py:47、core/export.py:97) | 7 | 40 | 4010 / 1187 | 115 | 部分(真实图片 I/O) |
| inkscape | 自己改 SVG,导出时调 inkscape --export-filename=…(utils/inkscape_backend.py:52) | 10 | 60 | 4726 / 2010 | 207 | 不要 |
(组内命令不含各自的顶层 repl;core 模块数不含 __init__.py。这三行是从 12 个代表应用的对照里摘出来的,完整那张表另有一篇专门讲。)
这张表最好按最后两列反过来读:一个 harness 能自己读写宿主的文件格式到什么程度,决定了它的命令面能长多大、以及它的测试离不离得开真软件。
inkscape:因为格式是开的,所以命令能长到 60 条
inkscape 的 SKILL.md 里那句自述说得很直白——它「直接操作 SVG(XML)文档,用一个 JSON 工程格式做状态跟踪」(skills/SKILL.md:11)。SVG 就是一份 XML,Python 自己就能改。于是这个 harness 根本不需要为了「加一个矩形」去启动 Inkscape。
结果就是 core 下摊出了 10 个模块:document.py、export.py、gradients.py、layers.py、paths.py、session.py、shapes.py、styles.py、text.py、transforms.py,另有一份 231 行的 utils/svg_utils.py。命令面 10 个组 60 条:document(8)、shape(11)、text(3)、style(6)、transform(7)、layer(7)、path(6)、gradient(4)、export(4)、session(4)。内置形状类型 10 种(core/shapes.py:15 的 SHAPE_TYPES)。
真正调 Inkscape 可执行文件的只有导出这一步,utils/inkscape_backend.py:52 拼的是 cmd = [inkscape, svg_path, f"--export-filename={output_path}", f"--export-dpi={dpi}"],导出预设 6 个(core/export.py:17:png_web、png_print、png_hires、svg、pdf、eps)。所以它的 e2e 测试文件头才敢写「No actual Inkscape installation is required.」(tests/test_full_e2e.py:1-6)。
但命令存在 ≠ 这条命令做完了它字面上的事
这是本篇最该记住的一处,也是整个注册表体系最容易被读错的地方:注册表里有这个条目、--help 里有这条命令,不等于这个能力是齐的。
inkscape 的 path 组里有一整套布尔路径运算,union / intersection / difference / exclusion / convert / list-operations,从 inkscape_cli.py:769 起。看命令名,这是矢量编辑里最硬的一块功能。但翻到 core/paths.py:171:布尔运算结果对象的 d(路径数据)字段,取的是 obj_a 的原 d 值,那一行的注释里自己标了 # Placeholder。
从代码路径看,这条命令会走完并往 session 里落一个新对象,但真正的路径求交求并在这一行是占位的。仓库里把它标出来了,我们照实记;至于调用它会得到什么样的文件,我们没有运行过,不作判断。
你要评估任何一个 harness 的时候,这就是那个可复现的核查动作:别只数命令条数,去 core/ 下 grep -rn 'Placeholder\|TODO\|NotImplemented' 一遍。
gimp:三个里唯一一个宿主不在也能出图的
gimp 走的是 Script-Fu 批处理这条路。utils/gimp_backend.py:1-6 的模块说明就写着用 GIMP 的 Script-Fu batch mode,:47-63 的 batch_script_fu() 起子进程;:14-22 有个 _script_fu_escape() 专门对传进去的字符串做转义;可执行名探测三种:gimp、gimp-2.10、gimp-2.99(:27)。
它跟另外两个最不一样的地方是双后端。core/export.py:6-8 把渲染后端按顺序列了两个:1. GIMP(真引擎);2. Pillow(PIL),纯 Python 的回落路径,用于 GIMP 缺席时。:77 有一句注释 fall through to Pillow,:97 处 from PIL import Image, ImageDraw, ImageFont,:100-102 的报错文案会提示你 pip install Pillow。
这条回落路径带来一个直接后果:三个里只有 gimp 在宿主没装的情况下仍有一条能出图的通道。它的滤镜注册表 24 项(core/filters.py:7 的 FILTER_REGISTRY),导出预设 13 个(core/export.py:16),命令面 8 个组 40 条,其中 layer 组一家就占 9 条。
顺带记一处口径差异:gimp 的 skills/SKILL.md(:5 与 :11)把自己描述成「built on Pillow」的有状态命令行图像编辑器,而代码里 Pillow 是 GIMP 缺席时的回落路径,主路径是 Script-Fu 批处理(core/export.py:6-8、utils/gimp_backend.py:1-3)。两处写的不是一回事,以我们读到的代码为准;为什么会这样,我们不推断。
krita:3 个模块、19 条命令,然后把活全交给 Krita 自己
krita 的 core 只有 3 个模块——export.py(504 行)、project.py(470 行)、session.py(117 行),是 12 个代表应用里 core 最少的之一。命令面 6 个组 19 条:project(4)、layer(4)、filter(2)、canvas(2)、export(4)、session(3),另有 2 条顶层命令 status(L509)与 repl(L538)——顶层带 status 这一点,另外两个包没有。
它没有像 inkscape 那样自己去解 .kra,而是把导出这件事整个转成 Krita 命令行参数。utils/krita_backend.py:206 拼的是 args = [krita, "--export", "--export-filename", output_path],:209 按 key/value 追加 --export-option,:253-259 用 --export-sequence / --export-sequence-start / --export-sequence-end 导动画序列。导出预设 13 个(core/export.py:29:png、png-web、jpeg、jpeg-web、jpeg-low、tiff、tiff-lzw、psd、pdf、svg、webp、gif、bmp)——预设数跟 gimp 一样多,但每一个都要靠真 Krita 才能落地。
于是就有了那个反直觉的组合:三个里命令最少(19 条)、代码最少(2822 行)、测试也最少(47 个 test 函数),而对宿主的硬依赖是三个里最绕不开的。它的 e2e 文件头写得毫不含糊:这些测试会调用真实的 Krita 应用做导出,「No graceful degradation — Krita must be installed.」(tests/test_full_e2e.py:1-6)。
同一句话在 inkscape 那边是「不需要装」,在 krita 这边是「必须装、且不降级」。你判断某个 harness 能不能进你的流水线,先读这六行 docstring,比读 README 快。
状态模型:三家的撤销不是同一套
三个包 core 下都有 session.py,但实现不一样。
gimp 用的是与 blender、obs-studio 同款的 Session,core/session.py:36-39 带 MAX_UNDO = 50,落盘用 fcntl.flock 加排他锁。这里有一处 Windows 读者要留意:blender 的 core/session.py:10-33 的 _locked_save_json 用 fcntl.flock 加排他锁,Windows 无 fcntl 时降级为不加锁继续写,这是代码层面的行为;把这句话直接写进 docstring 的是 freecad 的 core/session.py:20-23。gimp 这份是同款实现。
krita 的不一样。core/session.py:17 的 Session 保存的是 (时间戳, label, dict) 三元组快照列表,撤销时会丢弃 redo 分支(:29-40),落盘走 utils/io.py 里的 locked_save_json。inkscape 的 session 组是 4 条命令,session.py 在 core 的 10 个模块之列。
差别落到用法上很实际:如果你的编排里有「撤到某一步再往另一个方向改」这种回溯,krita 这边撤销之后 redo 分支是丢掉的。具体该怎么组织你的调用顺序,取决于你自己的流程,项目没有给通用建议。
dry-run 与测试口径
--dry-run 这个开关三家都有,但密度不同:inkscape 7 处、gimp 4 处、krita 3 处(grep -c 'dry.run\|dry_run' <cli>.py)。同一批 12 个应用里,QGIS、zotero、obsidian、comfyui 是 0 处。要在真动文件之前先看一眼命令会做什么,这个计数值得先扫。
测试这边,三家的 TEST.md 与我们数出来的 def test_ 条数都对不上,方向各不相同:
| TEST.md 写的 | 我们数到的 | |
|---|---|---|
| gimp | test_core 5 类 66 条、e2e 9 类 37 条,合计 103(tests/TEST.md:5-9) | 71 与 44,合计 115;类数 7 与 12 |
| inkscape | test_core 11 类 150 条、e2e 5 类 47 条,合计 197(tests/TEST.md:5-9) | 153 与 54,合计 207;类数 12 与 7 |
| krita | 「~40 unit tests planned」「~20 E2E tests planned」(tests/TEST.md:7-8) | 36 与 11 |
krita 这份和另外两份还不是一类东西:它写的是「planned」,是计划不是结果。我们的计数用的是 grep -h 'def test_' <app>/agent-harness/cli_anything/*/tests/*.py | wc -l,没有排除嵌套在测试内部的辅助函数,也没有展开 pytest.mark.parametrize。所以这些差异只能说明两边口径或时点不同,不能断言哪一边写错了。我们说到这里为止。
按你的处境选
三条路径,倒着推:
- 你手上就是 SVG,或者产出物要进版本库做 diff:inkscape 那条最合脚,它自己改 XML,本机没装 Inkscape 也能把编辑跑完,只有导出栅格图那一步需要可执行文件。前提是先确认你要用的命令不在占位实现的名单里。
- 你要批量做位图处理,而机器上不方便装 GIMP:gimp 是三个里唯一留了 Pillow 回落的,代价是回落路径与 GIMP 引擎不是同一套渲染实现,两者是否等价我们没有依据,不比。
- 你的产出必须是 Krita 的真实导出(比如
.kra转 psd、导动画序列):krita 那条是唯一给了通道的,但它的前提写死了——Krita 必须装在本机、必须能被命令行调起来,harness 不做降级。 - 你只是想先看看这三个能不能用:别从 README 开始,先跑下面这几条。
# 1. 各自的驱动方式:backend 模块头六行就写清楚了
sed -n '1,10p' krita/agent-harness/cli_anything/krita/utils/krita_backend.py
sed -n '1,10p' gimp/agent-harness/cli_anything/gimp/utils/gimp_backend.py
sed -n '1,10p' inkscape/agent-harness/cli_anything/inkscape/utils/inkscape_backend.py
# 2. e2e 到底要不要装宿主:读这六行
sed -n '1,6p' krita/agent-harness/cli_anything/krita/tests/test_full_e2e.py
sed -n '1,6p' inkscape/agent-harness/cli_anything/inkscape/tests/test_full_e2e.py
# 3. 命令是不是占位实现
grep -rn 'Placeholder' inkscape/agent-harness/cli_anything/inkscape/core/
# 4. 有多少条命令支持先空跑
grep -c 'dry.run\|dry_run' */agent-harness/cli_anything/*/*_cli.py
以上为按仓库中的文件路径与统计口径组合的示例,未经实测,以官方文档与 --help 的实际输出为准。
最后回到开头那句:三个画图 harness 的差别不在「谁的命令多」,而在「谁自己会读写宿主的格式」。inkscape 会,所以它长成 60 条命令、测试不用装软件;krita 不碰 .kra,所以它只有 19 条命令、却必须有真 Krita;gimp 在中间,多了一条 Pillow 回落。你要给自己的软件套一层 harness 时,这个取舍是第一个要先想清楚的,它会一路决定你的模块划分、命令面大小和测试能不能在 CI 里跑。
本文依据 CLI-Anything 官方仓库(github.com/HKUDS/CLI-Anything)的 README、registry.json、
docs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。
本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness,
也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。
这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。
注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。