如何为 CLI-Anything 会话型 CLI 实现一次性命令自动保存与 --dry-run 跳过写入
【免费下载链接】CLI-Anything"CLI-Anything: Making ALL Software Agent-Native" -- CLI-Hub: https://clianything.cc/项目地址: https://gitcode.com/GitHub_Trending/cl/CLI-Anything
按 HARNESS.md 搭建 CLI-Anything 会话型 CLI 时,会碰到一个具体缺陷:形如cli-anything-kdenlive --project p.json bin import video.mp4的一次性(one-shot)命令只修改了内存中的项目对象,退出前从未调用save_session()。进程结束后磁盘上的项目文件没有任何变化,修改被静默丢失。
本文解决的就是这个问题:在<software>_cli.py中做两处改动,让一次性命令执行完成后自动把项目写回磁盘,同时新增--dry-run标志让调用方可以执行命令、查看输出但不落盘。仓库中的 auto-save-dry-run.md 给出完整方案,kdenlive_cli.py 是一份可对照的现成实现。
适用条件:先确认你的 harness 属于会话型
官方指南给出了明确的适用边界。这一套模式仅适用于同时满足以下条件的 harness:
core/session.py存在,且带有save_session()方法和_modified修改标记;- CLI 接受
--project参数来加载文件支撑的项目; - 命令在修改项目前调用
sess.snapshot()记录 undo 快照。
无状态的 API 客户端、服务包装器或不带持久化项目文件的 harness 不适用——它们没有"修改后需要保存"的概念,不要硬套。
前置步骤:确认 save_session 使用带锁的 JSON 写入
自动保存最终会调用sess.save_session(),而它底层的文件写入必须使用 _locked_save_json 模式。直接用open("w") + json.dump()是不行的:open("w")在任何锁获取之前就会把文件截断,并发写入会损坏数据。
正确做法是先用"r+"打开(打开时不截断),拿到排他锁后再在锁内截断:
def _locked_save_json(path, data, **dump_kwargs) -> None: """Atomically write JSON with exclusive file locking.""" try: f = open(path, "r+") # no truncation on open except FileNotFoundError: os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) f = open(path, "w") # first save — file doesn't exist yet with f: _locked = False try: import fcntl fcntl.flock(f.fileno(), fcntl.LOCK_EX) _locked = True except (ImportError, OSError): pass # Windows / unsupported FS — proceed unlocked try: f.seek(0) f.truncate() # truncate INSIDE the lock json.dump(data, f, **dump_kwargs) f.flush() finally: if _locked: fcntl.flock(f.fileno(), fcntl.LOCK_UN)两点行为说明来自指南:fcntl不可用或文件系统不支持时(如 Windows),代码按无锁方式继续写;文件不存在时首次保存会先创建目录再用"w"打开。kdenlive harness 中的实际实现在 core/session.py,save_session()在写入后把self._modified重置为False(L125-L139)。
如果现有 harness 的core/session.py里已经是这个模式,可以直接跳到下一步。
改动一:在 main CLI group 上增加 --dry-run
指南要求在<software>_cli.py的主 Click group 上添加--dry-run选项:
@click.group(invoke_without_command=True) @click.option("--json", "use_json", is_flag=True, help="Output as JSON") @click.option("--project", "project_path", type=str, default=None, help="Path to project file") @click.option("--dry-run", "dry_run", is_flag=True, default=False, help="Run command without saving changes to disk") @click.pass_context def cli(ctx, use_json, project_path, dry_run): ...click会把 group 上的 option 值透传给后面的result_callback,所以下一步的回调函数里能直接拿到dry_run。kdenlive 的对应代码见 kdenlive_cli.py 第 124-L131 行。
改动二:用 @cli.result_callback() 实现退出时自动保存
在 group 定义之后追加一个 result callback:
@cli.result_callback() def auto_save_on_exit(result, use_json, project_path, dry_run, **kwargs): """Auto-save project after one-shot commands if state was modified.""" if _repl_mode: return if dry_run: return sess = get_session() if sess.has_project() and sess._modified and sess.project_path: try: sess.save_session() except Exception as e: click.echo(f"Warning: Auto-save failed: {e}", err=True)result_callback在 CLI group 的整条命令链执行完之后触发一次。回调里做三道检查,任何一条不满足就跳过保存:
_repl_mode为真时跳过——REPL 模式下用户手动保存,不自动写盘;dry_run为真时跳过——这是--dry-run的语义来源;sess._modified为假时跳过——没有任何修改发生就没有东西可存(snapshot()在每次变更前置位这个标记)。
保存失败不会抛异常中断,而是向 stderr 输出一行Warning: Auto-save failed: ...。另外指南明确指出:如果错误路径中handle_error调用了sys.exit(1),result callback 不会触发——这是预期行为,错误退出时不做保存。
如果上面的get_session()、_repl_mode等符号在你的 harness 里名字不同,按自己的会话模块替换即可;kdenlive 的实现与指南模板逐行一致,见 kdenlive_cli.py 第 149-L161 行。
可选替代:ctx.call_on_close 闭包模式
如果你的 harness 不是在 group 入口通过全局单例拿会话,而是在 group 内部内联打开会话,指南给出了替代写法——用闭包把会话和dry_run捕获进ctx.call_on_close回调:
def cli(ctx, use_json, project_path, dry_run): ... if project_path: sess = get_session() proj = proj_mod.open_project(project_path) sess.set_project(proj, project_path) def _auto_save(): if dry_run: return if sess._modified and sess.project_path and not _repl_mode: sess.save_session() ctx.call_on_close(_auto_save)两种方式二选一,不要把两套都挂上,否则同一次修改会被保存两遍。
--dry-run 的完整语义
指南用一个表格定义了三种模式下的行为,实现时以此为准:
| 模式 | 行为 |
|---|---|
| One-shot(默认) | 命令执行,输出打印,项目自动保存 |
One-shot +--dry-run | 命令执行,输出打印,项目不保存 |
| REPL | --dry-run被接受但忽略(REPL 从不自动保存) |
注意第三行:在 REPL 中传--dry-run不会报错,也不会产生任何额外效果,因为 REPL 本来就不自动保存。
验证改动是否生效
改动完成后按两条命令核对行为,判断依据是项目文件本身:
- 默认路径:对磁盘上的项目文件执行一次有修改的一次性命令(kdenlive 示例中是
cli-anything-kdenlive --project p.json bin import video.mp4)。执行完毕后检查项目文件——命令执行、输出正常打印,且文件内容已更新为修改后的状态,说明自动保存生效。这正是修复前"进程退出时文件无变化"问题的反面验证。 - 跳过写入路径:对同一文件加上
--dry-run重跑同一条命令。命令照常执行并打印输出,但项目文件内容与运行前一致,说明写入被正确抑制。 - 如果保存过程出错(如路径不可写),命令行会输出
Warning: Auto-save failed: <错误信息>而不是静默吞掉异常,可以据此判断写入环节的问题。
仓库里可以直接读源码对照的参考实现:kdenlive_cli.py(group 定义与auto_save_on_exit回调)和 core/session.py(_locked_save_json与save_session())。
边界与限制
- 该模式只覆盖"文件支撑的项目 + 一次性命令"这一条链路;REPL 会话的保存责任仍在用户(或驱动 REPL 的 agent)手上。
fcntl锁依赖 POSIX 文件系统;在 Windows 或不支持锁的文件系统上,_locked_save_json会退化为无锁写入,并发写入的防护随之失效,指南对此的说明是"proceed unlocked"。- 无状态 harness 不需要也不应该引入这套逻辑。
完整的方案文档见 guides/auto-save-dry-run.md,锁写入模式见 guides/session-locking.md,harness 整体开发流程(Phase 3 的会话管理要求)见 HARNESS.md。
【免费下载链接】CLI-Anything"CLI-Anything: Making ALL Software Agent-Native" -- CLI-Hub: https://clianything.cc/项目地址: https://gitcode.com/GitHub_Trending/cl/CLI-Anything
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考