zotero 与 obsidian:文献和笔记这两条线
这两个 harness 在仓库里是被放在一起的。我们采集时(2026-08-10,对应仓库快照 39634a6)读到 matrix_registry.json 的 S2 knowledge-research 矩阵引用了 13 个 CLI,zotero 和 obsidian 都在其中。从用途上说它们也确实是一条线上的:一个管文献条目,一个管笔记。
但把两个 harness 的目录摊开看,它们像是两套完全不同的东西。下面这些数都来自我们对本地 clone 的逐目录清点,你 clone 下来能自己数一遍。
| 维度 | zotero | obsidian |
|---|---|---|
| 非测试代码行 | 5098 | 1450 |
| 测试代码行 | 2340 | 590 |
| CLI 主入口行数 | 1085(zotero_cli.py) | 466(obsidian_cli.py) |
core/ 模块数 | 8 | 5 |
utils/ 模块数 | 5 | 2 |
| 组内命令数 | 45(9 个组) | 14(6 个组) |
test_ 函数数 | 88 | 59 |
| 驱动宿主的通道 | 三条并存 | 一条 |
| 工程状态 | 有 session.py | 无 session.py |
非测试代码相差 3.5 倍,命令面相差 3 倍多。差在哪,得往下看驱动方式。
zotero:三条通道并存
zotero 的 core/ 下是 8 个模块:analysis.py、catalog.py、discovery.py、experimental.py、imports.py、notes.py、rendering.py、session.py;utils/ 下 5 个:openai_api.py、repl_skin.py、zotero_http.py、zotero_paths.py、zotero_sqlite.py。光看 utils/ 就能看出它有三条不同的路子去够宿主软件。
第一条是只读 SQLite。Zotero 的本机数据是一个数据库,utils/zotero_sqlite.py 第 25 行的入口函数叫 connect_readonly()——名字本身就是约束。
第二条是Zotero 自带的本地 HTTP API。utils/zotero_http.py 第 12 行写死 LOCAL_API_VERSION = "3",第 183 行的 local_api_root(port) 拼地址,第 220 行的 local_api_get_json 发请求。
第三条是直接拉起 Zotero 进程:core/discovery.py 第 58 行是 subprocess.Popen([str(executable)], …)。这是本机执行外部程序,读者要清楚这件事。
三条通道要能对上同一份数据,前提是 harness 知道 Zotero 装在哪、数据目录在哪、本地 API 有没有开、端口是几号。utils/zotero_paths.py 第 12 到 15 行给出了它认的四个 Zotero 偏好设置键名:extensions.zotero.dataDir、extensions.zotero.useDataDir、extensions.zotero.httpServer.localAPI.enabled、extensions.zotero.httpServer.port。这四个键是排查这个 harness 连不上宿主时最直接的核对起点。
命令面上,9 个组分别是 app(5)、collection(7)、item(14)、search(3)、tag(2)、style(1)、import(2)、note(2)、session(9),再加一条顶层 repl(zotero_cli.py 第 1065 行)。导出这一块的能力边界写在 core/rendering.py 第 11 行的 SUPPORTED_EXPORT_FORMATS,7 种:ris、bibtex、biblatex、csljson、csv、mods、refer。
还有一处要单独点出来:core/analysis.py 引用了 utils/openai_api.py(70 行),也就是 item analyze 这条路会调外部 LLM API。这跟其它命令不是一个性质——它会把内容发出本机。
zotero 把写操作单独关起来了
Zotero 的数据既然就在本机 SQLite 里,直觉上 harness 应该可以直接改。它没有。写操作被单独放进 core/experimental.py,并且带三重限制:
- 第 11 行抛错,原文是 “Experimental SQLite write commands require Zotero to be closed”——要求宿主先关掉;
- 第 27 行和第 45 行两处抛错,都写 “…currently support only the local user library”——只支持本地用户库;
- 返回体里会带一个
"experimental": True字段(第 79、113、169 行三处)。
仓库文档也重复了这条限制:README.md 第 86 行写了同一条约束,第 331 到 338 行单列一节 ”### 10. Experimental Collection Refactoring”,对应命令带 --experimental 开关。仓库自己把这一块标成 experimental,我们照实标出来,不做任何”能不能用”的判断。
状态这一层,core/session.py 第 9 到 23 行把会话写到 ~/.config/cli-anything-zotero/session.json,可以用环境变量 CLI_ANYTHING_ZOTERO_STATE_DIR 覆盖落点;命令历史条数上限写在第 8 行的 COMMAND_HISTORY_LIMIT = 50。这是代码里的默认配置,不是对实际运行结果的保证。
obsidian:本地 vault 也走一次 HTTPS
obsidian 这边的形态几乎是反过来的。它的 core/ 有 5 个模块,但都极小:vault.py(62)、search.py(59)、note.py(14)、command.py(14)、server.py(12)——五个加起来 161 行。utils/ 只有 2 个模块。
原因在 utils/obsidian_backend.py。它第 1 到 8 行的模块说明自述是 “the single module that makes network requests”,第 17 行 DEFAULT_BASE_URL = "https://localhost:27124",第 22 行走 Bearer 鉴权。也就是说,vault 组那 6 条命令(list / read / create / update / delete / append)是经由这个模块,发 HTTPS 请求给 Obsidian 的 Local REST API 插件来完成的。
这就是这两条线里最反直觉的一处,而且方向刚好对调:zotero 那边数据在一个本机数据库里,harness 却坚持只读、把写操作关进 experimental;obsidian 这边 vault 本来就是一堆本机 md 文件,harness 却把“够到 vault”这件事交给了唯一一个发网络请求的模块 utils/obsidian_backend.py,vault 组走的是 localhost 上的一次 HTTPS 往返。这个模块自述自己是仓库里唯一发网络请求的地方,但这句话管的是网络请求,不等于说 harness 别处一个本机文件都不碰——它是否还在别的模块里直接读写 md 文件,我们没有逐模块核实过,这里不替它下结论。
这带来两件必须交代清楚的事。
一是前置条件多了一层。README 已经提醒过包装真实桌面软件的 CLI 需要用户自行安装上游应用(README.md 第 236 行);obsidian 这条线在”装 Obsidian”之外,还要在 Obsidian 里装上 Local REST API 插件、把它跑起来,并拿到 API key。key 的取法写在 obsidian_cli.py 第 126 行:从 --api-key 参数或环境变量 OBSIDIAN_API_KEY 取,缺失时第 107 行抛 “API key required.”。
二是自签证书这一处。utils/obsidian_backend.py 第 11 行调了 urllib3.disable_warnings(...),第 34、65、103 行三处请求都传 verify=False。这是本机 HTTPS 端点跳过证书校验、并把告警静音的写法,我们只陈述它在代码里的位置,不做评价。要不要在你自己的机器上这么用,请结合环境自行评估。
命令面也是照着这个形态收窄的。obsidian 的 6 个组是 vault(6)、search(2: query / simple)、note(2: active / open)、command(2: list / execute)、server(1: status)、session(1),加一条顶层 repl(obsidian_cli.py 第 402 行)。对比 zotero 的 item 一个组就有 14 条,两边的命令密度不在一个量级上。
状态这一层 obsidian 是空的:core/ 里没有 session.py,session 组只有 1 条命令,没有 undo/redo。而 zotero 那边 session 组是 9 条。
至于两边像的地方:两个 harness 的 utils/ 里都有 repl_skin.py,也都有一条顶层 repl 命令;两个 setup.py 注册的 console_scripts 命名遵循同一套规则,即 cli-anything-zotero 与 cli-anything-obsidian。这类骨架层面的共性是整个仓库的统一约定,不是这两条线特有的,我们另有一篇专门讲那套目录规范,这里不展开。
还有一处两边一致、但值得放在一起看:我们按 grep -c 'dry.run\|dry_run' <cli>.py 数过,zotero 与 obsidian 的 CLI 主入口里 --dry-run 都是 0 处。obsidian 的 vault 组里有 delete 和 update,这两件事并列摆着,够你判断要不要先在一个可丢弃的 vault 上过一遍——需要说明的是,“先拿可丢弃的目标试一遍”属于通用运维做法,不是这个仓库的文档给出的建议,仓库里没有关于这一点的任何说明。
测试这一层,两边都要真宿主
这两个是 12 个代表应用里少数几个 e2e 明确要求宿主在场的。obsidian 的 tests/test_full_e2e.py 文件头第 1 到 7 行写 “requires Obsidian running with Local REST API plugin… Skip if not available.”;zotero 的 tests/TEST.md 把 e2e 标为 Live validation。作为对照,同一批里 comfyui 的 e2e 文件头(第 3 到 4 行)写的是全部用 mock 的 HTTP 响应、不需要装也不需要运行 ComfyUI。
zotero 的测试还有一处结构上的不同:它一共 4 个测试文件、88 个 test_ 函数,分别是 test_core.py(701 行)、test_cli_entrypoint.py(523 行)、test_full_e2e.py(351 行)、test_agent_harness.py(59 行),另有一份 _helpers.py(705 行)。obsidian 则是 test_core 52 + e2e 7 = 59。
需要说明的是,这些”要不要真宿主”的结论都来自测试文件的 docstring 与 import 关系,不是运行结果——我们没有跑过任何一个测试,也没有安装 Zotero 或 Obsidian。
注册表里有这条,不等于装上就能用
这两个应用在 registry.json 里各占一条:zotero 归在 category office、version 0.4.1;obsidian 归在 knowledge、version 1.1.0。整个 clis 数组我们数出来是 79 条。
但注册表条目和”能用的能力”是两回事,zotero 正好能拿来说明这一点。三件可以各自核对的事实并排放着:
zotero是 79 条里skill_md指向远程 URL 的 11 条之一,不是仓库内相对路径;- 仓库
skills/下确实有cli-anything-zotero目录,而它属于我们比对出来的、没有被任何 registry 条目引用的 14 个目录之一; zotero的 harness 目录里带着agent-harness/skill_generator.py(297 行) 和agent-harness/templates/SKILL.md.template——12 个代表应用里只有它有这两样。
obsidian 那边则是另一种情形:除了 registry.json 里那条,public_registry.json 的 22 条里还有 obsidian-cli(knowledge,bundled)与 obsidian-agent-cli(productivity)两条。两份注册表加起来,“obsidian”相关的条目有三条。你按名字去搜,得先分清搜到的是哪一条。
你可以自己跑一遍的核对动作
上面每个论断都能落到文件上,核对起来不需要装宿主软件,只需要 clone 仓库后读文件:
grep -n 'connect_readonly' zotero/agent-harness/cli_anything/zotero/utils/zotero_sqlite.py
grep -n 'LOCAL_API_VERSION\|local_api_root' zotero/agent-harness/cli_anything/zotero/utils/zotero_http.py
grep -n 'subprocess.Popen' zotero/agent-harness/cli_anything/zotero/core/discovery.py
grep -n 'experimental' zotero/agent-harness/cli_anything/zotero/core/experimental.py
grep -n 'DEFAULT_BASE_URL\|verify=False\|disable_warnings' obsidian/agent-harness/cli_anything/obsidian/utils/obsidian_backend.py
grep -c 'dry.run\|dry_run' obsidian/agent-harness/cli_anything/obsidian/obsidian_cli.py
这几条路径是按仓库统一的 <app>/agent-harness/cli_anything/<pkg>/ 布局拼出来的,我们没有实跑过这几条命令,请以你 clone 到本机的实际目录为准。它们只读文件,不驱动任何宿主软件。数字会随上游更新而变,重要的不是记住 5098 和 1450,而是记住这两条线各自把”够到宿主”这件事放在了哪个模块里:zotero 在 utils/ 的三个文件加一个 core/experimental.py,obsidian 全部在 utils/obsidian_backend.py 一个文件里。你要判断某条命令会不会动你的本机数据、会不会发出网络请求,先看这几个文件就够了。
本文依据 CLI-Anything 官方仓库(github.com/HKUDS/CLI-Anything)的 README、registry.json、
docs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。
本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness,
也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。
这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。
注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。