Joplin 自动化 Harness 全指南:cli-anything-joplin 有状态 CLI 状态机与真实后端命令契约详解
【免费下载链接】CLI-Anything"CLI-Anything: Making ALL Software Agent-Native" -- CLI-Hub: https://clianything.cc/项目地址: https://gitcode.com/GitHub_Trending/cl/CLI-Anything
cli-anything-joplin是 CLI-Anything 仓库为笔记工具 Joplin 打造的"有状态"命令驾驶舱(harness):它以真实安装的joplin终端二进制作为后端,把笔记、笔记本、待办、标签、同步、导入导出、E2EE 等原生命令包装成一套带项目状态、undo/redo 与稳定 JSON 信封的 CLI。读完本文,你将掌握其安装方式、REPL/一次性两种运行模式、JSON 状态模型与自动保存语义、15 个命令分组的完整用法,以及它如何通过后端探测与防御性设计在 Agent 无人值守场景下保证可靠执行。
Overview:什么是 Joplin Harness
Joplin 是一款开源笔记应用,自带终端版 CLI。cli-anything-joplin并没有重新实现一套笔记数据库,而是直接调用真实joplin终端二进制,在其之上增加一层对 Agent 友好的状态与契约。正如 JOPLIN.md 所述,它的定位是:
A stateful CLI harness for Joplin automation backed by the real
joplinterminal binary, intended for command-driven workflows, real-backend validation, and agent demo runs.
从包元数据(setup.py)可知包名为cli-anything-joplin,版本 1.0.0,仅依赖click>=8.0.0(CLI 框架)与prompt-toolkit>=3.0.0(REPL 皮肤),并以 console script 注册入口cli-anything-joplin=cli_anything.joplin.joplin_cli:main。所有命令都从 joplin_cli.py 这一个 Click 根分组出发。
运行环境与前置要求
使用前需满足(Python 侧要求见 setup.py):
- Python 3.10+
- Joplin 终端 CLI 已安装,且以
joplin为名出现在PATH中 - 本地开发建议以可编辑模式安装 harness 包
文档建议先用下面两条命令确认后端可用:
where joplin # POSIX 上使用: which joplin joplin help后端查找逻辑位于 utils/joplin_backend.py:find_joplin通过shutil.which(binary)解析二进制路径,找不到时抛出RuntimeError("Joplin terminal binary not found in PATH ...")。也可以在启动时通过--binary、--profile覆盖,或把后端设置持久化进 harness 项目文件(见下文状态模型)。
安装与运行模式
在仓库内进入 harness 目录并执行可编辑安装:
cd joplin/agent-harness pip install -e .安装完成后获得控制台命令cli-anything-joplin,同时支持python -m cli_anything.joplin(包内提供main.py)。根分组cli(invoke_without_command=True)意味着不带任何子命令启动时直接进入 REPL(见 joplin_cli.py),这也解释了文档中"REPL mode (default)"的说法。
根级全局参数
根分组共定义 5 个全局参数(joplin_cli.py):
| 参数 | 说明 | 默认值 |
|---|---|---|
--json | 以稳定 JSON 信封输出所有命令结果 | 关闭(人类可读文本) |
--project <path> | 加载一个 harness 项目 JSON 文件 | 无 |
--binary <name/path> | 指定 Joplin CLI 二进制名或路径 | 项目保存值,其次joplin |
--profile <path> | 指定 Joplin profile 路径 | 项目保存值,其次无 |
--dry-run | 执行命令但不自动保存 harness 项目 | False |
其中--binary/--profile使用None作为哨兵默认值,以区分"用户没传"与"用户显式传了默认值",从而避免显式传参意外覆盖项目内保存的 backend 设置(_backend_config解析优先级:显式 CLI 旗标 > 项目 backend 段 > 内建默认)。
典型使用方式
# REPL 模式(默认,无自动保存) cli-anything-joplin # 一次性命令 + JSON 输出 cli-anything-joplin --json notebooks list # 新建有状态项目文件 cli-anything-joplin project new --name demo -o ./demo.joplin-harness.json # 加载项目文件并执行改动命令 cli-anything-joplin --project ./demo.joplin-harness.json notes create "hello" # 干跑:只执行、不写回项目文件 cli-anything-joplin --json --dry-run --project ./demo.joplin-harness.json notes create tempREPL 内部(joplin_cli.py)借助ReplSkin(utils/repl_skin.py)渲染 banner、help面板与状态提示符;用户输入经shlex.split后复用同一个 Clickcli.main(...),因此REPL 与一次性模式的命令语义完全一致。
命令分组总览
文档列出的 15 个命令分组全部注册于 joplin_cli.py,并与 README.md 保持一致:
| 分组 | 子命令 | 对应后端实现 |
|---|---|---|
project | new,open,save,info,json,status | core/project.py |
notebooks | list,create,use,remove | core/notebooks.py |
notes | list,create,set,get,remove,copy,move,rename | core/notes.py |
todos | list,create,toggle,clear,done,undone | core/todos.py |
tags | list,add,remove,notetags,tagnotes | core/tags.py |
search | run | core/search.py |
sync | run(--target,--upgrade,--use-lock) | core/sync.py |
interop | import,export | core/interop.py |
config | get,set,list,export,import-file | core/config.py |
attach | add | core/attach.py |
status | show,restore | core/status.py |
backend | version,dump,keymap,geoloc,export-sync-status | core/backend.py |
server | status,start,stop | 同上 |
e2ee | status,target-status,decrypt,decrypt-file | 同上 |
session | status,undo,redo,history | core/session.py |
子命令选项速查(来自各 Click 定义)
notebooks list:--limit、--sort、--reverse、--longnotes list:--pattern、--limit、--sort、--reverse、--type(Joplin 条目类型过滤,取n/t/nt)、--longnotes get/backend e2ee target-status/config get/config list/config export:--verbose/-vnotes remove/notebooks remove:--force/--no-force(默认--force)、--permanent(要求 Joplin >= 3.0,见"已知限制")sync run:--target、--upgrade、--use-lockinterop import:--notebook、--format、--force、--output-format;interop export:--format(默认jex)、--note、--notebookserver start:--exit-early/--wait(默认--exit-early)、--quiete2ee decrypt:--retry-failed-items、--forcee2ee decrypt-file:--output
list类命令在底层对应 Joplin 的ls并追加--format json;但并非所有源命令都支持 JSON——例如tag list就不支持(开发约定中明确:"Prefer Joplin's native--format jsonfor list-style commands when the source command supports it;lsdoes;tag listdoes not")。
有状态项目模型(State Model)
harness 的"有状态"由 JSON 项目文件承载。create_project 定义了项目初始结构:
{ "name": "joplin-project", "created_at": "<ISO-8601 UTC>", "updated_at": "<ISO-8601 UTC>", "backend": { "binary": "joplin", "profile": null }, "context": { "current_notebook": null }, "history": [] }其中history是操作日志,每次命令成功后由add_history追加一条{ "at": <utc iso>, "action": <动作名>, "payload": {...} }(project.py)。project info返回name / created_at / updated_at / backend / context / history_count,方便快速盘点状态。
会话层(core/session.py)在进程内存中维护项目副本外加两套栈:
snapshot(reason):把当前项目深拷贝压入_undo_stack并清空_redo_stack,置_modified=True,同时向 history 追加一条"snapshot"记录——每个可撤销的改动命令都会先打一个快照点;mark_dirty():只置_modified=True,不产生 undo 快照,供"无需撤销但需持久化"的命令使用(如sync run、interop export、config import_file、server start/stop、backend export_sync_status);undo()/redo():在两栈之间搬运项目副本,完成回退与重做;status():返回has_project / project_path / modified / undo_depth / redo_depth。
保存由save_session完成,写入采用"锁文件 + 临时文件 + 原子替换"协议(session.py):POSIX 用fcntl.flock(LOCK_EX),Windows 用msvcrt.locking配合指数退避重试,随后写<path>.tmp再os.replace,确保多个并发 Agent 进程写入同一项目文件不会互相践踏——这正是 Agent 并行场景下文件状态安全的关键设计。
保存行为(Save Behavior)
自动保存的语义贯穿三种使用形态(joplin_cli.py):
- 一次性改动命令:加载了项目文件时,命令成功即触发自动保存;实现上,每个改动命令在执行后调用
sess.snapshot(reason)(生成 undo 点)或sess.mark_dirty()(仅置脏),进程退出时由auto_save_on_exit回调检查has_project() and _modified and project_path,满足即调用sess.save_session()写盘;保存失败只告警、不阻断命令结果。 --dry-run:在auto_save_on_exit入口直接return,禁止任何写盘。- REPL 模式:
auto_save_on_exit同样直接return,REPL 会话全程不自动保存,用户显式执行project save才会落盘(此时可省略 path 参数,写入project_path)。 - 不产生 undo 快照的命令(
sync run、interop export等)仍通过mark_dirty()把项目标记为修改,因此自动保存依然会把其 history 条目持久化——保证"项目文件里的操作日志从不缺账"。
JSON 输出契约
--json开启后,所有命令返回统一信封(构造逻辑见 joplin_cli.py):
| 字段 | 成功 | 失败 |
|---|---|---|
ok | true | false |
command | 稳定命令标识,如notes.list、todos.toggle | 与成功时相同的字符串 |
data | 命令负载 | null |
error | null | { "type": <异常类名>, "message": <信息> } |
错误处理装饰器handle_error(joplin_cli.py)捕获RuntimeError / ValueError / FileNotFoundError / IndexError,并以func.__name__.replace("_", ".", 1)推导 command 标识——只替换第一个下划线,因此多词子命令得到的是config.import_file、backend.export_sync_status、e2ee.decrypt_file,而不是错误的config.import.file。非 REPL 模式下出错即sys.exit(1),REPL 中则打印错误后回到提示符。这保证了调用方无论成败都能用同一 command 标识对齐响应。
成功示例(--json notebooks list之类):
{ "ok": true, "command": "notebooks.list", "data": [ ... ], "error": null }后端封装与健壮性设计
原样透传 stdout / stderr
run_joplin_command(utils/joplin_backend.py)以subprocess.run执行[binary] + (["--profile", profile] if profile) + args,默认 120 秒超时(dump/export/e2ee decrypt-file等重命令在各自封装中放大到 300–600 秒)。返回结果携带command / returncode / stdout / stderr,其中stdout 保持逐字原样——因为多段落笔记正文、导出的 Markdown、配置导出都依赖空行与空白不被破坏。
良性 Node 警告过滤器
Joplin 基于 Node 运行,Node 20+ 会对punycode(DEP0040)、url.parse(DEP0169)等打 DeprecationWarning,几乎每次调用都出现在 stderr,但并非真实失败。过滤策略(_strip_benign_node_warnings)逐行识别警告头、警告后的 "(Usenode --trace-deprecation...)" 提示行以及贴邻空行并剔除。关键约束是:过滤器只在判断"非零退出码是否是真失败"时使用(外加一次 JSON 解析兜底),绝不改写返回给调用方的 stdout。判定规则如下:
- 清洗后仍有内容 → 真错误,抛
RuntimeError; - 原始流非空但清洗后被清空 → 纯 Node 噪音,按成功处理;
- 原始 stdout/stderr 均为空但退出码非零 → 静默失败(如删除不存在的笔记、进程被 kill),抛出通用错误以保证
ok=false而非"假成功"。
run_joplin_json(同文件 L169-L192)对以 JSON 为目标的命令统一补--format json,解析失败时先清洗 Node 警告前缀再重试一次,仍失败则退化为{ "text": <原文> }。
命令分层:core 模块 → joplin_cli 组装
每个分组对应一个core/*.py纯函数模块,例如笔记操作在 core/notes.py 中映射为:list→ls、create→mknote、set→set、get→cat、remove→rmnote、copy→cp、move→mv、rename→ren。CLI 层负责参数解析、调用 core、记录 history 与快照。config 映射见 core/config.py(config <key> [value]、config --export、config --import-file <path>),导入导出格式映射见 core/interop.py。
测试策略与验证基线
JOPLIN.md 定义了五层测试(用例文件为 tests/test_core.py 与 tests/test_full_e2e.py):
- 单元 / 命令测试(
test_core.py):不依赖任何后端,验证 harness 内部逻辑与 CLI 契约。 - CLI 子进程测试(
TestCLISubprocess):跑安装后的cli-anything-joplin(或python -m)入口,只对非改动命令断言外部可见的 JSON 信封。 - 真实后端命令测试(
TestBackendCommands):在全新 profile 上做单命令检查。 - 真实后端工作流测试(
TestBackendWorkflows):覆盖笔记生命周期、组织(copy/move/rename)、待办、标签、搜索(best-effort)、同步、导出、导入、附件与 history 的短流程脚本。 - 端到端集成测试(
TestBackendIntegration):完整演示流程,同时回归验证"保存后的项目 history 捕获了每一步动作"。
配套执行命令:
python -m pytest -q cli_anything/joplin/tests/test_core.py python -m pytest -q cli_anything/joplin/tests/test_full_e2e.py::TestCLISubprocess python -m pytest -v cli_anything/joplin/tests/test_full_e2e.py::TestBackendCommands python -m pytest -v cli_anything/joplin/tests/test_full_e2e.py::TestBackendWorkflows python -m pytest -v cli_anything/joplin/tests/test_full_e2e.py::TestBackendIntegration python -m pytest -v --tb=no cli_anything/joplin/tests真实后端用例要求joplin已安装且在PATH。文档记录的验证基线(Windows + Joplin CLI 3.6.2)为:
python -m pytest -q cli_anything/joplin/tests/test_core.py→107 passedpython -m pytest -q cli_anything/joplin/tests→134 passed, 1 skipped
更完整的用例清单(12 类工作流与各层用例数量)见 WORKFLOWS.md,完整测试计划见 tests/TEST.md。注意:上述测试命令中的相对路径cli_anything/...以joplin/agent-harness为当前目录,执行前需cd joplin/agent-harness。
开发约定(向 harness 添加新命令)
文档对贡献新命令给出明确约束:
- 先以小命令测试补覆盖,长用户旅程再提升为工作流测试;只保留一条完整集成流程用于演示与回归。
- 保持 JSON 信封与后端命令命名约定不变。
list类命令优先使用 Joplin 原生--format json(源命令支持时);ls支持、tag list不支持。- 新改动命令成功路径必须二选一:
sess.snapshot(reason)——创建可撤销点(undo 深度 +1);sess.mark_dirty()——仅标记修改,供不需要 undo 深度的命令使用,同时保证自动保存仍会持久化追加的 history 条目。
utils/joplin_backend.py原样返回 stdout/stderr;良性 Node 警告过滤器只在非零退出判定与run_joplin_json的 JSON 解析兜底中使用,绝不改写对外 stdout。- 错误信封必须与成功负载使用同一
command字符串。
已知限制与防御性设计
文档明确记录了三条真实限制,harness 均以"宁可明确报错,也不静默降级"的原则应对(详见 core/backend.py 与 core/notes.py):
search的 GUI 模式门禁:部分 Joplin CLI 构建在 REPL 外执行search会报"only available in GUI mode"。harness 将其作为普通ok=false信封返回,Agent 应把搜索视为 best-effort。- Windows 非 ASCII 参数截断:非 ASCII 进程参数经
joplin.cmd→cmd.exe会按活动代码页截断。harness 的 JSON 状态本身对 Unicode 处理正确(ensure_ascii=False+ UTF-8),仅 argv 转发的标题受影响,因此 Unicode 工作流测试在 Windows 上跳过。 - 版本敏感旗标的探测门禁:Joplin CLI 会静默忽略未知选项。为避免旧版本吞掉关键旗标造成假成功,harness 用
joplin help <command>探测:notes remove --permanent/notebooks remove --permanent要求 Joplin >= 3.0,按(binary, command)缓存探测结果;不支持时抛带指引的RuntimeError,防止"永久删除"退化成软删除进回收站。旗标一律用长形式--permanent/--force,绝不使用短-p(-p在mkbook中表示--parent,且--permanent自 3.x 才出现)。server start --exit-early/--quiet与e2ee decrypt --force通过共享助手_cli_supports_flag(按(binary, command, flag)缓存)探测。它们都是 Joplin 3.x 的真实选项,但旧版本静默忽略会导致:server start若收不到--exit-early,harness 会在前台服务器循环上无限阻塞;e2ee decrypt若收不到--force,会在交互式主密码提示处死锁。探测不通过时同样抛出清晰可执行的错误信息。
综合这些设计可以看出:该 harness 的可靠性并不只来自"把命令包一层",而是建立在对真实后端怪异行为的逐一实证与防御之上。所有流程清单、测试分层、实现保证与添加新工作流的步骤,都可以在 WORKFLOWS.md 与 README.md 中找到对照;面向 Agent 的能力描述则集中在 skills/SKILL.md,在集成到 Agent 时可直接引用该能力清单。
【免费下载链接】CLI-Anything"CLI-Anything: Making ALL Software Agent-Native" -- CLI-Hub: https://clianything.cc/项目地址: https://gitcode.com/GitHub_Trending/cl/CLI-Anything
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考