这是一套完整的工程规范,核心思想只有一句话:CLI 是软件的命令行接口,不是软件的替代品------生成中间文件,调用真实软件,验证真实输出,这三步缺一不可。
核心目标
让 AI Agent(比如 Claude、Codex)能够操控那些原本只能用鼠标点击的 GUI 软件,比如 LibreOffice、Blender、GIMP、Shotcut 等。方法是为每个软件构建一套命令行接口(CLI),Agent 通过调用这些命令来完成任务,完全不需要显示器和鼠标。
Phase 1:代码库分析
在动手写任何代码之前,先把目标软件研究透彻。
识别后端引擎:大多数 GUI 软件的界面和逻辑是分离的。界面只是一层皮,真正干活的是底层引擎。比如 Shotcut 的界面是 Qt 写的,但视频处理全靠 MLT 框架;GIMP 的界面背后是 GEGL 图像处理库。找到这个引擎,你就找到了真正的能力所在。
映射 GUI 操作到函数调用 :用户在界面上的每一个操作,背后都对应一个函数调用。点击"导出为 PDF",背后是 libreoffice --headless --convert-to pdf;拖动时间轴上的片段,背后是修改 MLT XML 文件里某个节点的 in 和 out 属性。把这张"操作 → 函数"的对照表建立起来,你就知道用代码怎么复现同样的效果。
识别数据模型 :搞清楚软件用什么格式存储项目。LibreOffice 用 ODF(本质是 ZIP 压缩的 XML);Shotcut 用 .mlt(XML 文件);Blender 用 .blend(二进制)。读懂这个格式,你就能直接用代码生成或修改项目文件,绕过 GUI。
找现有 CLI 工具 :很多软件的后端引擎自带命令行工具,这些是现成的积木。libreoffice --headless 可以无界面转换文档;melt 可以渲染 MLT 项目;ffmpeg 处理视频;sox 处理音频;inkscape --actions 处理 SVG。有了这些,很多事情一行命令就能搞定。
梳理撤销/重做系统:如果软件支持撤销,它内部几乎一定用了"命令模式"------每个操作都被封装成一个独立的对象,存在一个栈里。这些命令对象就是你 CLI 操作的原型,照着它们设计命令接口会非常自然。
Phase 2:CLI 架构设计
研究完软件之后,在动手写代码之前,先把架构设计好。
选择交互模式 :有三种选择。第一种是 REPL(交互式终端),像 Python 解释器一样,进入一个会话,它记住上下文,适合 Agent 连续操作。第二种是子命令 CLI,像 git commit 这样,每次执行一条独立命令,无状态,适合脚本和流水线。文档推荐两者兼备------同一套 CLI,cli-anything-shotcut 直接回车进入 REPL,cli-anything-shotcut export --format mp4 则是单次执行。
定义命令分组:按照软件的逻辑领域来划分,通常分五组:项目管理(new/open/save/close)、核心操作(软件的主要功能)、导入导出(文件读写和格式转换)、配置(设置和偏好)、会话状态管理(undo/redo/status)。这样分组的好处是结构清晰,用户和 Agent 一看命令名就知道它属于哪个领域。
设计状态模型 :要想清楚三个问题:命令之间需要保留什么状态(比如"当前打开的项目");状态存在哪里(REPL 模式放内存,纯 CLI 模式写到 .session.json 文件);状态怎么序列化(推荐 JSON 格式)。
规划输出格式 :输出要服务两类受众。人类看的用表格和颜色,直观易读;机器读的输出纯 JSON,方便 Agent 解析。通过 --json 标志来切换,这是业界标准做法,GitHub CLI 和 AWS CLI 都是这么设计的。
Phase 3:具体实现(8个步骤,由内到外)
第一步,从数据层开始:先写最基础的部分------读写项目文件。能把 XML/JSON 格式的项目文件解析出来、修改、保存回去,这是一切操作的基础。不要急着调用真实软件。
第二步,加探查命令 :在能修改之前,先让 Agent 能"看"。inspect、info、status 这类只读命令,让 Agent 在动手之前先了解当前状态。这是为 AI Agent 设计 CLI 的重要原则:先观察,再行动。
第三步,加修改命令:每个逻辑操作对应一条命令,保持原子性。"插入片段"是一条命令,"调整音量"是另一条命令,不要把多个操作混在一起。
第四步,集成后端 :写一个 utils/<软件名>_backend.py 模块,专门负责调用真实软件的 CLI。这个模块做三件事:用 shutil.which() 找到软件可执行文件的路径;用 subprocess.run() 带着正确参数去调用它;如果找不到软件,给出清晰的安装提示而不是报一个莫名其妙的错误。文档给出了 LibreOffice 的例子:convert_odf_to() 函数先找到 LibreOffice 路径,然后用 --headless 无界面模式做格式转换,最后返回一个结构化的结果字典。
第五步,加渲染导出:导出流水线分两步------先生成合法的中间文件(ODF、MLT XML 等),再调用 backend 模块让真实软件做最终渲染。两步走,职责分离。
第六步,加会话管理 :实现状态持久化和撤销/重做。这里有一个重要的技术细节------文件锁。直接用 open("w") 打开文件再写 JSON 是危险的,因为文件在打开瞬间就被清空了,如果这时候另一个进程也在写,数据就会损坏。正确做法是用 open("r+") 打开(不清空),拿到文件锁之后再 truncate()(清空),最后写入数据,写完释放锁。文档提供了完整的 _locked_save_json() 函数实现,还兼容了 Windows(Windows 不支持 fcntl,就跳过加锁降级处理)。
第七步,加 REPL 交互模式 :把 repl_skin.py 复制到项目里,用 ReplSkin 类统一管理交互界面。它提供了一整套现成的 UI 组件:启动横幅、带历史记录的输入框、帮助列表、成功/错误/警告/信息提示、状态行、表格、进度条、退出提示。ReplSkin 还会自动检测包目录里有没有 skills/SKILL.md,有的话在启动横幅里显示路径,方便 AI Agent 读取技能文件。
第八步,让 REPL 成为默认行为 :在 Click 的主命令组上加 invoke_without_command=True,判断如果用户没有输入任何子命令,就自动进入 REPL 模式。这样直接回车进交互模式,加子命令就是单次执行,两种用法无缝兼容。
Phase 4:测试规划(先写计划)
在动手写任何测试代码之前,先在 tests/ 目录下创建 TEST.md 作为测试计划书。这个文件要包含:计划写哪些测试文件、每个文件大概多少个测试、每个核心模块要测什么函数和边界条件、端到端测试要模拟哪些真实工作流。
这个"先写计划"的做法强迫你在写代码之前就想清楚测试覆盖范围,避免写完代码才发现漏测了关键场景。
Phase 5:测试实现(四层体系)
文档规定了四种测试,层层递进,缺一不可。
单元测试 (test_core.py):用合成数据测每个函数,不依赖任何外部软件,快速且稳定,适合 CI 持续集成。
E2E 中间文件测试 (test_full_e2e.py 第一部分):验证 CLI 生成的项目文件格式是否正确,比如 ODF 的 ZIP 结构是否合法、XML 内容是否符合规范。这一层不需要真实软件,只验证中间产物。
E2E 真实后端测试 (test_full_e2e.py 第二部分):必须调用真实软件生成真实输出文件,然后验证文件存在、大小合理、格式正确。验证方式很具体:检查 PDF 文件的前 5 个字节是否是 %PDF-;检查 DOCX/XLSX/PPTX 是否是合法的 ZIP/OOXML 结构;对视频文件用 ffmpeg 探测特定帧的亮度来验证淡入淡出效果是否真的生效。还要打印输出文件的路径和大小,方便人工检查。
CLI 子进程测试 :像真实用户一样,通过 subprocess.run() 调用已安装的 cli-anything-<软件名> 命令来测试完整流程。文档提供了 _resolve_cli() 辅助函数,优先用已安装的命令,找不到才回退到 Python 模块。在 CI 环境里可以设置 CLI_ANYTHING_FORCE_INSTALLED=1 环境变量,强制要求必须用已安装的命令,不允许回退。
最重要的原则 :测试不能"优雅降级"。如果 LibreOffice 没装,测试必须报错,而不是跳过或用 Python 的 reportlab 库假装生成了 PDF。软件是硬依赖,不是可选项。
Phase 6:测试文档 + SKILL.md 生成
测试全部通过后,把完整的 pytest -v 输出追加到 TEST.md,让这个文件同时承担"测试计划"和"测试结果"两个角色,形成完整的测试记录。
然后生成 SKILL.md 文件。这是让 AI Agent 能发现和使用这个 CLI 的关键文件,包含 YAML 元数据(用于技能发现)和 Markdown 正文(安装说明、命令结构、使用示例、Agent 专用指引)。文档提供了 skill_generator.py 脚本,能自动从 setup.py、CLI 文件的 Click 装饰器、README.md 中提取信息来生成这个文件。SKILL.md 要放在 Python 包内部(cli_anything/<软件名>/skills/SKILL.md),这样 pip install 之后它也会被安装,ReplSkin 启动时会自动找到它并在横幅里显示路径。
Phase 7:发布到 PyPI(命名空间包)
这一阶段解决多个 CLI 工具共存的问题。
核心技术是 PEP 420 命名空间包 。cli_anything/ 目录不能有 __init__.py ,这样它就成了一个命名空间包。cli-anything-gimp 安装后贡献 cli_anything/gimp/,cli-anything-blender 安装后贡献 cli_anything/blender/,两者在同一个 Python 环境里完全不冲突,都挂在 cli_anything.* 命名空间下。
setup.py 里要用 find_namespace_packages 而不是 find_packages,并且用 include=["cli_anything.*"] 来限定范围。系统软件(LibreOffice、Blender 等)是硬依赖,无法写进 install_requires,必须在 README.md 里说明,并在 backend 模块里给出清晰的安装提示。
文档中最有价值的"踩坑经验"
坑一:不要重新实现软件功能。 这是文档列为第一条的最重要原则。用 Python 自己写一个"类 LibreOffice"的文档渲染器是错误的,这样产出的是一个玩具,无法处理真实工作负载,而且行为会和真实软件不一致。正确做法是生成合法的中间文件,然后交给真实软件渲染。
坑二:渲染缺口问题。 你在项目文件里加了滤镜和特效,但渲染时如果用了简单工具(比如 ffmpeg 的 concat demuxer),它只读原始媒体文件,完全忽略项目里的特效,输出和输入看起来一模一样,用户完全察觉不到出了问题。解决方案是优先用软件原生渲染器(比如 melt 读 MLT 项目),其次构建一个滤镜转换层把项目格式的特效翻译成渲染工具的原生语法。
坑三:滤镜翻译的细节陷阱。 比如 ffmpeg 不允许同一个滤镜(如 eq=)在一个滤镜链里出现两次,如果项目里同时有亮度和饱和度调整,必须合并成 eq=brightness=X:saturation=Y 一条。另外 ffmpeg 的 concat 滤镜要求流的顺序是 [v0][a0][v1][a1](视频音频交替),而不是 [v0][v1][a0][a1](先所有视频再所有音频),搞错了报错信息是"media type mismatch",非常难以定位。不同工具的参数范围也不同,MLT 的亮度 1.15 表示 +15%,但 ffmpeg 的 eq=brightness= 是 -1 到 1 的范围,每个映射都要明确文档化。
坑四:时间码精度问题。 29.97fps 这种非整数帧率(实际是 30000/1001)会产生累积误差。必须用 round() 而不是 int() 来做浮点数到帧数的转换,int(9000 * 29.97) 会截断丢帧,round() 才能得到正确结果。在非整数帧率下做往返测试时,要允许 ±1 帧的误差,精确相等在数学上是不可能的。
坑五:输出验证不能只看退出码。 命令成功退出不代表输出正确。必须用程序化的方式验证:检查文件的魔数字节(PDF 是 %PDF-)、验证 ZIP 结构(DOCX/XLSX/PPTX 都是 ZIP)、用 ffmpeg 探测视频帧的像素值来验证颜色效果、检查音频的 RMS 电平来验证淡入淡出。
原文:https://github.com/HKUDS/CLI-Anything/blob/main/cli-anything-plugin/HARNESS.md