zotero 与 obsidian:文献和笔记这两条线

2026-08-10

这两个 harness 在仓库里是被放在一起的。我们采集时(2026-08-10,对应仓库快照 39634a6)读到 matrix_registry.json 的 S2 knowledge-research 矩阵引用了 13 个 CLI,zoteroobsidian 都在其中。从用途上说它们也确实是一条线上的:一个管文献条目,一个管笔记。

但把两个 harness 的目录摊开看,它们像是两套完全不同的东西。下面这些数都来自我们对本地 clone 的逐目录清点,你 clone 下来能自己数一遍。

维度zoteroobsidian
非测试代码行50981450
测试代码行2340590
CLI 主入口行数1085(zotero_cli.py466(obsidian_cli.py
core/ 模块数85
utils/ 模块数52
组内命令数45(9 个组)14(6 个组)
test_ 函数数8859
驱动宿主的通道三条并存一条
工程状态session.pysession.py

非测试代码相差 3.5 倍,命令面相差 3 倍多。差在哪,得往下看驱动方式。

zotero:三条通道并存

zoterocore/ 下是 8 个模块:analysis.pycatalog.pydiscovery.pyexperimental.pyimports.pynotes.pyrendering.pysession.pyutils/ 下 5 个:openai_api.pyrepl_skin.pyzotero_http.pyzotero_paths.pyzotero_sqlite.py。光看 utils/ 就能看出它有三条不同的路子去够宿主软件。

第一条是只读 SQLite。Zotero 的本机数据是一个数据库,utils/zotero_sqlite.py 第 25 行的入口函数叫 connect_readonly()——名字本身就是约束。

第二条是Zotero 自带的本地 HTTP APIutils/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.dataDirextensions.zotero.useDataDirextensions.zotero.httpServer.localAPI.enabledextensions.zotero.httpServer.port。这四个键是排查这个 harness 连不上宿主时最直接的核对起点。

命令面上,9 个组分别是 app(5)、collection(7)、item(14)、search(3)、tag(2)、style(1)、import(2)、note(2)、session(9),再加一条顶层 replzotero_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),加一条顶层 replobsidian_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-zoterocli-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.1obsidian 归在 knowledge、version 1.1.0。整个 clis 数组我们数出来是 79 条。

但注册表条目和”能用的能力”是两回事,zotero 正好能拿来说明这一点。三件可以各自核对的事实并排放着:

  1. zotero 是 79 条里 skill_md 指向远程 URL 的 11 条之一,不是仓库内相对路径;
  2. 仓库 skills/ 下确实有 cli-anything-zotero 目录,而它属于我们比对出来的、没有被任何 registry 条目引用的 14 个目录之一;
  3. 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.jsondocs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。 本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness, 也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。 这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。 注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

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