QGIS harness:地理信息这类专业软件怎么接
CLI-Anything 里的 harness 是高度同构的:目录树、四份文档、Click 入口,几乎每个都一个模子。所以看单个 harness 时,真正值得花时间的不是把那棵树再复述一遍(那是 harness 目录结构那一篇的事),而是找出这一个跟其它的不一样在哪。
QGIS 这一份的不一样很集中,就一条:它是我们读过的 12 个代表应用里,唯一一个把宿主软件的运行时直接 import 进自己 Python 进程的。 这一条会往下牵动前置条件、测试方式,甚至牵动它为什么没有撤销栈。
先声明边界:本文全部结论来自读源码文本,我们采集时(2026-08-10,对应仓库快照 39634a6)没有安装过 QGIS,也没有跑过这个 harness 的任何一条命令。
先把坐标钉住
按仓库的统一命名,这份 harness 落在 <app>/agent-harness/cli_anything/<pkg>/ 下,入口是 qgis_cli.py。想自己找的话,在仓库根跑:
find QGIS -type f -name 'qgis_cli.py'
以上是在我们采集时的统计口径(find <app> -type f)基础上改写出的定位命令,未经实测,实际输出以你 clone 到的仓库状态为准。
我们采集时的规模:非测试代码 2610 行、测试代码 821 行,qgis_cli.py 750 行。core/ 下 7 个模块:export.py、features.py、layers.py、layouts.py、processing.py、project.py、session.py;utils/ 2 个。
命令面是 7 个组、25 条组内命令,外加一条顶层 repl(qgis_cli.py:682):
| 组 | 组内命令数 |
|---|---|
| project | 5 |
| layer | 4 |
| feature | 2 |
| layout | 6 |
| export | 3 |
| process | 3 |
| session | 2 |
这张表有两处值得先记一下:命令最多的组是 layout(制图版面)6 条,而 feature(要素)只有 2 条;session 也只有 2 条。后面会回到这两处。
横向放一下量级:同批读的 inkscape 是 60 条组内命令、freecad 是 276 条,obsidian 是 14 条。QGIS 的 25 条处在中间偏下。
反直觉的那一处:门槛不在「装没装」,在「用哪个解释器」
12 个代表应用里,驱动宿主的做法大致三类:起子进程调宿主的可执行文件(blender 生成 bpy 脚本后调 blender --background --python、krita 调 --export、freecad 交给 FreeCADCmd)、发 HTTP 请求(comfyui、obsidian、n8n)、以及干脆不驱动宿主(obs-studio 全包 0 处 subprocess 与 HTTP,只读写 JSON 场景集合)。
QGIS 是双通道,两条都占,而且第一条是别人没有的。utils/qgis_backend.py 的第 1 行模块 docstring 就把这件事写在门口:
Low-level backend helpers for PyQGIS and qgis_process.
通道一,进程内加载。 utils/qgis_backend.py:105 是 from qgis.core import QgsApplication;:126-128 拿到这个类之后构造 QgsApplication([], False)。也就是说,QGIS 的运行时是被加载进运行这个 harness 的那个 Python 进程里的,不是另起一个进程。
通道二,子进程。 utils/qgis_backend.py:171 拼 command = [find_qgis_process(), "--json", *arguments],:179 交给 subprocess.run。这一条和 blender 那类做法是同一个性质:在本机启动一个外部程序。
反直觉就出在通道一上。用子进程那一路的 harness,前置条件是「宿主的可执行文件能被找到」——blender 那份的 find_blender() 用 shutil.which("blender") 定位,找不到就抛错并附上安装指令。这类条件是跟你用哪个 Python 跑没关系的:shutil.which 找的是 PATH 上的可执行文件,与当前解释器的 import 路径无关。
QGIS 这一条不是。import qgis.core 能不能成功,取决于当前这个解释器的搜索路径里有没有 PyQGIS 绑定。仓库自己也把话说到了这个份上——utils/qgis_backend.py:109,缺 PyQGIS 时的报错文案要求读者 “use a Python interpreter that can import qgis.core”。注意这句的落点:它不是让你去装 QGIS,是让你换一个解释器。
这一处最容易踩空的地方在于:你完全可能已经把 QGIS 装好了、qgis_process 也在 PATH 上(于是通道二畅通),但通道一仍然抛错,因为你 pip install 的那个 venv 并不是能 import 到 PyQGIS 的那个解释器。这时候「装没装 QGIS」这个问题问下去是没有结论的,得改问「我现在这个解释器能不能 import qgis.core」。
具体怎么让某个解释器满足这一条,那句报错文案没有展开,我们也没有安装过 QGIS,给不出步骤——只能提醒你:判断 QGIS harness 装没装成,别只看 qgis_process 在不在 PATH 上。SKILL.md :11-13 那两条前置条件(QGIS 已装、qgis_process 在 PATH)满足的是通道二,通道一还得另看当前解释器能不能 import qgis.core。
顺带把 SKILL.md 那一层的口径也对一下。这份 SKILL.md 只有 140 行,触发语写在正文首句(skills/SKILL.md:8):
Use this skill when you need to inspect or modify QGIS projects from the terminal through the real QGIS runtime.
“through the real QGIS runtime” 这半句和上面两条通道是对得上的。前置条件写在 :11-13:要求 QGIS 已装,且 qgis_process 在 PATH。
这里必须说清楚一件事:这类 harness 会在你本机把 QGIS 的运行时加载进 Python 进程、并启动 qgis_process 子进程执行操作,宿主软件也需要你自己先装。要不要在你的环境里跑,请自行评估。
三个「别人有、它没有」
同构度高的好处是差异特别好找。QGIS 这份少了三样东西,都能一条命令数出来。
一、没有 --dry-run。 我们采集时按 grep -c 'dry.run\|dry_run' 数各家 CLI 主文件:inkscape 7 处、n8n 5 处,blender / obs / gimp / kdenlive 各 4 处,krita 与 freecad 各 3 处,而 QGIS 是 0 处(zotero、obsidian、comfyui 同样是 0)。
二、没有撤销栈。 blender、obs-studio、gimp、kdenlive 四家共用同一款 Session,带类常量 MAX_UNDO = 50(各自 core/session.py:36-39)。QGIS 的 core/session.py 是另一种写法:dataclass 加 _locked_save_json(:12-38),只有 status 与 history 两条命令,没有 undo/redo。这正是上面那张表里 session 组只有 2 条的原因。
把一和二放一起看:在文档层面能找到的两个兜底——「先看看这条命令会做什么」和「做错了退回上一步」——QGIS 这份 harness 都没有提供对应的命令。这是两条缺失事实的陈述,不是对它的评价;换个角度说,你在为它编排 agent 工作流时,得自己想清楚回退怎么做。
三、REPL 皮肤是漂移版。 规范要求 utils/repl_skin.py 从 plugin 目录复制。12 个代表应用里 11 个有这份文件,其中 10 个 md5 完全相同(623dc43e9c2421a97231d398ea303f0c,567 行),QGIS 是不同的那个(2754dcbd…,500 行),n8n 也不同(703 行)。核对命令就是卡口径里那条:
md5sum */agent-harness/cli_anything/*/utils/repl_skin.py
规范写的是复制,仓库里存在多个版本,这是两个可以各自核对的事实,我们只陈述到这里。
测试这一面:22 个,且需要真宿主
QGIS 的测试函数数是 12 个代表应用里最少的:test_core.py 13 个 + test_full_e2e.py 9 个 = 22。对照一下同批的数:blender 234、inkscape 207、kdenlive 179、krita 47。25 条组内命令配 22 个测试函数。
更值得注意的是 e2e 的性质。同批 12 个里,e2e 不需要真宿主的有 5 个:blender(e2e 文件头明写 “No actual Blender installation is required.”)、obs-studio、inkscape、kdenlive,以及全程 mock HTTP 的 comfyui(TEST.md 里也写 “No ComfyUI installation required”)。需要真宿主的同样是 5 个:krita(e2e 文件头明说不做优雅降级,Krita 必须装)、QGIS、zotero、obsidian、n8n。剩下两个居中:gimp 属于「部分」(做真实图片 I/O),freecad 是分层的,未装 FreeCAD 的那一层直接 skip。
QGIS 落在需要真宿主的那一边:它的 e2e 需要 QGIS 运行时。
这个差别的实际含义是:QGIS 这 9 个 e2e,在没装 QGIS 的机器上是跑不出结果的。你想靠「跑一遍测试」来判断这份 harness 在你机器上是否可用,前提本身就是那台机器已经满足了它的运行时条件。
还有一处小差异:QGIS 是 12 个里唯一带 agent-harness/pytest.ini 的。这个文件里配了什么,我们没有逐项核实。
顺带提醒一句关于测试数字的读法:本批我们统计 def test_ 的方式没有排除参数化展开,也没排除会被 skip 的用例,所以这些数字之间只能横向比较口径一致的相对量级,不能当成「实际跑过多少条」。
注册表里有它,不等于你能直接用它
这一点在 CLI-Anything 上尤其要说清楚:注册表条目不等于可用能力。 我们采集时 registry.json 的 clis 数组是 79 条,其中有 13 条在本仓找不到同名的 <name>/agent-harness 目录。QGIS 正好是这 13 条之一,但原因和其它 11 条不同:另外那 11 条带的是外部 source_url 或外部安装串,属于外部仓托管;QGIS 与 3mf 这两条纯粹是大小写差异——registry 记录里的 name 是小写 qgis,本地目录是大写 QGIS。
所以如果你写脚本按 registry 里的 name 直接拼路径去仓库里找目录,这一条会对不上。cli-hub 那一层有没有做大小写归一,我们没有核实,那是分发层那几篇的话题。
对读者来说,判断「这一条能不能用」的顺序应该是:注册表里有 → 仓库里能找到对应目录 → 目录里四件套齐不齐 → 它驱动宿主的方式你的环境满不满足。QGIS 的最后一环与别家不同——它要求的不是「装了就行」,而是当前解释器能否 import qgis.core。
你可以自己跑一遍的核查动作
上面每个数都是从文件系统数出来的,clone 下来就能复现:
find QGIS -type f -name 'qgis_cli.py'
find QGIS -name '*.py' -not -path '*/tests/*' | xargs wc -l | tail -1
grep -h 'def test_' QGIS/agent-harness/cli_anything/*/tests/*.py | wc -l
grep -c 'dry.run\|dry_run' QGIS/agent-harness/cli_anything/qgis/qgis_cli.py
以上为按我们采集时所用的统计口径组合出的命令,未经实测,实际输出以你 clone 到的仓库状态与 --help 为准。要看那条双通道,直接翻 utils/qgis_backend.py 的第 1 行、105 行、126 到 128 行、171 行、179 行;要看那句把门槛挪到解释器上的报错文案,看第 109 行。
数字会随上游更新变动。真正值得记住的不是 2610 行或者 22 个测试,而是这条判断路径:看一个 harness 之前,先看它的 backend 是起子进程、发 HTTP,还是像 QGIS 这样把宿主运行时 import 进自己的进程——这一条决定了你要满足的前置条件长什么样,也决定了「我装好了」这句话到底指的是哪件事装好了。
本文依据 CLI-Anything 官方仓库(github.com/HKUDS/CLI-Anything)的 README、registry.json、
docs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。
本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness,
也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。
这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。
注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。