kdenlive 与 freecad:剪辑与 CAD 的两种接法
CLI-Anything 里的 harness 彼此高度同构,目录树、四份文档、click 入口都长得差不多。所以看具体某一个 harness 时,真正有信息量的不是「它也有 core/ 和 utils/」,而是这一个跟别的不一样在哪。kdenlive 和 freecad 放在一起看特别合适:一个是视频剪辑,一个是参数化 CAD,两者都属于「不生成中间产物就没法工作」的重型桌面软件,但它们把宿主接进来的方式完全是两条路。
先把两者的体量摆出来。我们采集时(2026-08-10,对应仓库快照 39634a6)读到的数是:kdenlive 非测试代码 3467 行、测试 1664 行,入口 kdenlive_cli.py 759 行;freecad 非测试代码 22450 行、测试 2670 行,入口 freecad_cli.py 一个文件就 4911 行。命令面上,kdenlive 是 8 个组、36 条组内命令加一条顶层 repl(kdenlive_cli.py:682);freecad 是 19 个顶层组加 1 个嵌套组,276 条组内命令加顶层 repl(freecad_cli.py:4823),全文件 277 处 @x.command(。差距不是一点半点。
kdenlive:中间隔了一层 MLT XML
kdenlive 的链路是三段:JSON 工程 → MLT XML → melt 子进程。
中间那一段是关键。utils/mlt_xml.py(448 行)用 xml.etree.ElementTree 把工程描述拼成 MLT XML;utils/melt_backend.py 的模块 docstring(第 1 到 7 行)写明 Kdenlive 和 Shotcut 共用 MLT 框架,melt 这个命令行工具可以把 MLT XML 工程渲染成视频文件,并注明需要系统包 melt。落到具体调用,第 69 行用 shutil.which("melt") 找可执行文件,第 130 到 134 行拼 melt 的命令行。
这个设计的直接后果是:harness 自己不碰任何视频编解码,它只负责把一份描述写成 MLT 能读的 XML。所以 core 那 8 个模块(bin.py、export.py、filters.py、guides.py、project.py、session.py、timeline.py、transitions.py)全都是在操作数据结构:timeline 组 8 条命令是 add-track / remove-track / add-clip / remove-clip / trim / split / move / list,全是对时间线这棵树的增删改查。滤镜注册表 13 项(core/filters.py:6,含 chroma_key,以及音视频各一对 fade_in / fade_out),渲染预设 8 个(core/export.py:11:h264_hq、h264_fast、h265_hq、webm_vp9、prores、lossless、gif、audio_only)。export 组只有 2 条命令:xml 和 presets——也就是说,「导出」在这个 harness 里的字面意思就是产出 XML 与列预设。
白名单那一段值得单独看
utils/melt_backend.py 第 17 到 21 行有一段注释,说明为什么要给编解码器加白名单:编解码参数是由 AI agent 控制的,如果接受任意字符串,一个被攻陷或者被提示注入的 agent 就可能把构造过的值传给 melt 子进程。因此第 22 到 30 行定义了 ALLOWED_VCODECS / ALLOWED_ACODECS 两个 frozenset,里面是 libx264、libx265、libvpx 一直到 hevc_vaapi 这些名字,不在名单里的直接抛 ValueError。仓库根的 SECURITY.md 把这条白名单单独标成 “Breaking Change”,并点名 kdenlive / shotcut 的 melt 后端(SECURITY.md:34-38)。
这一段之所以值得读,是因为它把这类 harness 的风险位置说得很直白:harness 会在你本机拉起外部程序,而拼命令行的那一头是模型。SECURITY.md 列了 5 类攻击面(子进程参数、Script-Fu 注入、XML/SVG 内容、路径穿越、凭据泄露)及对应缓解手段(SECURITY.md:14-22)。要不要在自己机器上跑这类东西,得按自己的环境判断,本文不给方案。
freecad:把宏落成临时 .py 再交出去
freecad 的链路只有两段,但那一段更重:生成 Python 宏文本,写成临时文件,交给 FreeCADCmd 无头执行。
utils/freecad_backend.py 的 docstring(第 2 到 5 行)自述它包的是真实的 FreeCAD 无头 CLI(FreeCADCmd),用于宏执行、导出与版本查询。可执行文件探测名列表在第 50 行:_FREECAD_CMD_NAMES = ["freecadcmd", "FreeCADCmd", "freecad", "FreeCAD"],按顺序找。第 183 行用 tempfile.mkstemp(suffix=".py", prefix="freecad_macro_") 把宏文本落成临时脚本,第 209 行的 _macro_command() 拼 argv,第 144 行 subprocess.run,对外的两个口子是第 257 行的 run_macro() 与第 290 行的 run_macro_content()。宏文本本身由 utils/freecad_macro_gen.py(627 行)生成。
跟 kdenlive 对照着看,差别就出来了:kdenlive 交出去的是一份声明式的 XML,melt 拿到它按自己的语义渲染;freecad 交出去的是一段要被执行的 Python 代码,FreeCADCmd 拿到就当宏跑。前者的注入面在参数(所以有编解码白名单),后者的注入面在脚本正文——freecad 侧是否有对应的校验,我们没有核实过,这里只说到我们读到的这几行为止。
模块划分上,freecad 的 core 有 19 个模块,最大的三个是 body.py(2151 行)、sketch.py(1708 行)、parts.py(1614 行)。逐组命令数从代码里数出来是:document 5、part 27、sketch 28、body 40、material 8、export 3、preview 3、preview live 5、motion 8、session 4、measure 12、spreadsheet 7、mesh 16、draft 33、surface 6、import 13、assembly 14、techdraw 15、fem 16、cam 13。内置数据表也是 CAD 那一路的规格:图元 11 种(core/parts.py:17 的 PRIMITIVES),导出预设 17 个(core/export.py:24),材质预设 21 个(core/materials.py:16 的 PRESETS)。另外 core/mesh.py 有两处自己标了占位:第 263 行写的是 volume/area 占位、实际数值分析在别处,第 307 行写的是带 mesh 元数据的诊断占位。
freecad 还有一处是跨 harness 共享的:它的 preview 与 blender 用的是同一套 preview bundle 协议——两边的 utils/preview_bundle.py 都是 468 行,都声明 PROTOCOL_VERSION = "preview-bundle/v1" 与 TRAJECTORY_PROTOCOL_VERSION = "preview-trajectory/v1"(blender/…/utils/preview_bundle.py:14-15),preview live 组两边同样都是 start / push / status / stop / monitor 五条。我们只比对了命令名、行数与协议常量,没有逐行 diff 这两个文件是否完全相同。
反直觉的那一处:命令面差近 8 倍,测试反而少了
把两边的测试数放在一起,结果是反的。
| 项 | kdenlive | freecad |
|---|---|---|
| 组内命令数 | 36 | 276 |
| 非测试代码行 | 3467 | 22450 |
| 测试代码行 | 1664 | 2670 |
| test 函数数 | 179(test_core 111 + e2e 68) | 107(test_core 83 + e2e 24) |
| e2e 是否需要真宿主 | 不需要 | 分层,未装则 skip |
kdenlive 用 36 条组内命令配了 179 个 test 函数;freecad 用 276 条组内命令配了 107 个。这不是「谁写得好」的评价——test 函数数是用 grep 'def test_' 数出来的,没有排除测试内部的辅助函数,也没有把 pytest.mark.parametrize 展开后的实际用例数算进去,两边口径一致但都不等于真实用例数。所以这里只能停在陈述上:两边的命令数与 test 函数数之比差得很远(36:179 与 276:107),至于具体某一条命令有没有被测到,我们没有逐条核对过。改动前后自己补一层验证,是用这类工具时的通用做法,跟上面这组比值没有因果关系。
e2e 的前提也是反过来的。kdenlive 的 tests/test_full_e2e.py 文件头(第 3 到 4 行)写明不需要安装 Kdenlive 或 melt——链路里那两段(JSON→XML)本来就是纯数据变换,可以离线验完。freecad 的 e2e 分三层(tests/test_full_e2e.py:1-9):TestIntermediateFiles 不需要 FreeCAD,TestFreeCADBackend 未安装则 skip,TestCLISubprocess 走子进程。也就是说,链路更短的那个反而更依赖真机。
两边都要提醒同一件事:README 明确写了,包装真实桌面软件的 CLI 需要用户自行安装上游应用(README.md:236)。melt 装没装、FreeCADCmd 在不在 PATH,直接决定你能走到哪一步——注册表里有这一条,不等于装上就能用。
文档口径对不上的地方(只陈述差异)
freecad 这边有好几处可以自己核对的数字差异:
- 命令总数三处互不相同。
skills/SKILL.md写 “258 commands”(:3、:8、:86),agent-harness/FREECAD.md写 “258 commands across 18 workbench groups”(:8),SKILL.md 正文又写 “across 17 groups”(:8);代码里数到的是 277 条@x.command(、19 个顶层组加 1 个嵌套组。 - SKILL.md 自己那张分组表加起来也不是 258:表里 17 个
### 组名 (数字)标题的括号数相加是 248。 - 那张表还漏了两个实际存在的组:
preview(freecad_cli.py:2222,含嵌套live共 8 条)与motion(:2372,8 条)。 - 单组数字对不上:SKILL.md 写 part(29)/sketch(26)/body(38)/assembly(11)/fem(12)/cam(10),代码侧是 part 27 / sketch 28 / body 40 / assembly 14 / fem 16 / cam 13。
- 包内
README.md的命令组表只列 7 组(:97-107),导出格式表只列 7 个(:113-121),图元清单只列 6 个(:109-111);代码里分别是 19 个顶层组、core/export.py:24的 17 个 key、core/parts.py:17的 11 项。SKILL.md 那边写的是 “Export (17 formats)”。
kdenlive 这边则是 TEST.md 与测试文件对不上:TEST.md(:5-9)写 test_core 8 个类 118 条、e2e 3 个类 33 条、合计 151;数出来是 111 与 68(合计 179),类数 8 与 6。freecad 的 TEST.md 性质又不一样,它表头写的是 “Estimated Tests”,列的是 test_core.py ~45 tests / test_full_e2e.py ~15 tests(:3-8),数出来是 83 与 24。
以上都是两处各自可以核对的事实,以我们实读的仓库状态为准。为什么会这样、哪一边该改,我们不作推断,也不拿它评价项目。
你可以自己跑的核查动作
clone 下来在仓库根执行,两边的数都能自己复现一遍:
find kdenlive -name '*.py' -not -path '*/tests/*' | xargs wc -l | tail -1
find freecad -name '*.py' -not -path '*/tests/*' | xargs wc -l | tail -1
grep -h 'def test_' kdenlive/agent-harness/cli_anything/*/tests/*.py | wc -l
grep -h 'def test_' freecad/agent-harness/cli_anything/*/tests/*.py | wc -l
(以上为按仓库中的统计口径组合的示例,未经实测,以官方文档与 --help 的实际输出为准。)
命令面那几个数不在上面这几行里:我们数命令用的不是 grep,而是用 Python 逐行扫 *_cli.py,匹配 @cli.group(...)、@<var>.group(...)、@<var>.command(...),@cli.command(...) 另计为顶层命令。想复现这一组数字,得照这个口径自己写一小段扫描,按行 grep -c 会把注释行一起算进去。
再看三处能一眼分辨两条链路的文件:kdenlive/agent-harness/cli_anything/kdenlive/utils/melt_backend.py 的第 17 到 30 行(注释加两个白名单),freecad/agent-harness/cli_anything/freecad/utils/freecad_backend.py 的第 50 行与第 183 行(探测名列表与临时宏文件),以及两边的 core/session.py。session 这一份两家都是同款:MAX_UNDO = 50、用 fcntl 加排他锁写 JSON;freecad 的那份 docstring(:20-23)明写在缺少 fcntl 的平台(Windows)上写入不加锁。Windows 用户尤其要记住这一条——同一份工程 JSON 被两个进程同时写的场景,在 Windows 上没有这层保护。
最后一句关于「选哪个」:这两个 harness 不构成替代关系,它们的差别是被宿主软件的形态推出来的——剪辑的产物天然可以先落成一份声明式工程文件,CAD 的建模步骤则更接近一段要被执行的过程。真正决定你能用到什么程度的,是你本机装没装 melt 和 FreeCADCmd,以及你打算让 agent 碰到链路的哪一段。这一点仓库没给通用建议,我们也不给。
本文依据 CLI-Anything 官方仓库(github.com/HKUDS/CLI-Anything)的 README、registry.json、
docs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。
本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness,
也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。
这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。
注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。