Agent Harness: GUI-to-CLI for Open Source Software
以下内容翻译自 https://github.com/HKUDS/CLI-Anything/blob/main/cli-anything-plugin/QUICKSTART.md
Agent Harness:为开源软件构建 GUI 到 CLI 的适配层
目的
本适配层为编码代理(Claude Code、Codex 等)提供了一个标准操作流程(SOP)和工具包,用于为开源的 GUI 应用程序构建功能强大、有状态的命令行界面(CLI)。目标是:让 AI 代理能够操作那些为人类设计的软件,而无需显示器或鼠标。
通用 SOP:将任何 GUI 应用程序转化为代理可用的 CLI
第一阶段:代码库分析
- 识别后端引擎 — 大多数 GUI 应用程序将表示层与逻辑层分离。找到核心库/框架(例如,Shotcut 的 MLT,GIMP 的 ImageMagick)。
- 将 GUI 操作映射到 API 调用 — 每一次按钮点击、拖拽和菜单项都对应一个函数调用。整理这些映射关系。
- 识别数据模型 — 它使用什么文件格式?项目状态如何表示?(XML、JSON、二进制、数据库?)
- 寻找现有的 CLI 工具 — 许多后端自带 CLI(
melt、ffmpeg、convert)。这些都是构建模块。 - 整理命令/撤销系统 — 如果应用程序有撤销/重做功能,它可能使用了命令模式。这些命令就是你的 CLI 操作。
第二阶段:CLI 架构设计
-
选择交互模式:
- 有状态的 REPL 用于交互式会话(代理需要维护上下文)
- 子命令 CLI 用于一次性操作(脚本编写、管道处理)
- 两者兼有(推荐)—— 一个能同时工作在两种模式下的 CLI
-
定义命令组,与应用程序的逻辑领域相匹配:
- 项目管理(新建、打开、保存、关闭)
- 核心操作(应用程序的主要目的)
- 导入/导出(文件 I/O、格式转换)
- 配置(设置、偏好、配置文件)
- 会话/状态管理(撤销、重做、历史、状态)
-
设计状态模型:
- 哪些内容需要在命令之间持久化?(打开的项目、光标位置、选中区域)
- 状态存储在哪里?(REPL 中存储在内存,CLI 中基于文件)
- 状态如何序列化?(JSON 会话文件)
-
规划输出格式:
- 人类可读的(表格、颜色)用于交互式使用
- 机器可读的(JSON)供代理消费
- 两者兼有,通过
--json标志控制
第三阶段:实现
-
从数据层开始 — 项目文件的 XML/JSON 操作
-
添加探测/信息命令 — 让代理在修改前先进行检查
-
添加变更命令 — 每个逻辑操作对应一个命令
-
添加后端集成 — 一个
utils/<software>_backend.py模块,用于封装真实软件的 CLI。该模块处理:- 查找软件可执行文件(
shutil.which()) - 使用正确的参数调用它(
subprocess.run()) - 错误处理,如果未找到则给出清晰的安装说明
- 示例(LibreOffice):
# utils/lo_backend.py def convert_odf_to(odf_path, output_format, output_path=None, overwrite=False): lo = find_libreoffice() # 引发 RuntimeError,附带安装说明 subprocess.run([lo, "--headless", "--convert-to", output_format, ...]) return {"output": final_path, "format": output_format, "method": "libreoffice-headless"}
- 查找软件可执行文件(
-
添加渲染/导出 — 导出管道调用后端模块。生成有效的中间文件,然后调用真实软件进行转换。
-
添加会话管理 — 状态持久化、撤销/重做
会话文件锁定 — 保存会话 JSON 时,使用独占文件锁定防止并发写入导致数据损坏。切勿使用裸的
open("w") + json.dump()—open("w")会在获取锁之前截断文件。相反,使用"r+"打开,然后锁定,在锁定内部进行截断:def _locked_save_json(path, data, **dump_kwargs) -> None: """使用独占文件锁定原子地写入 JSON。""" try: f = open(path, "r+") # 打开时不截断 except FileNotFoundError: os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) f = open(path, "w") # 首次保存——文件尚不存在 with f: _locked = False try: import fcntl fcntl.flock(f.fileno(), fcntl.LOCK_EX) _locked = True except (ImportError, OSError): pass # Windows / 不支持的文件系统——继续,不加锁 try: f.seek(0) f.truncate() # 在锁内部截断 json.dump(data, f, **dump_kwargs) f.flush() finally: if _locked: fcntl.flock(f.fileno(), fcntl.LOCK_UN) -
添加具有统一外观的 REPL — 交互式模式,包装子命令。
- 从插件(
cli-anything-plugin/repl_skin.py)复制repl_skin.py到 CLI 包的utils/repl_skin.py - 导入并使用
ReplSkin作为 REPL 界面:from cli_anything.<software>.utils.repl_skin import ReplSkin skin = ReplSkin("<software>", version="1.0.0") skin.print_banner() # 品牌化的启动框(自动检测 skills/SKILL.md) pt_session = skin.create_prompt_session() # 带有历史记录和样式的 prompt_toolkit line = skin.get_input(pt_session, project_name="my_project", modified=True) skin.help(commands_dict) # 格式化的帮助列表 skin.success("Saved") # ✓ 绿色消息 skin.error("Not found") # ✗ 红色消息 skin.warning("Unsaved") # ⚠ 黄色消息 skin.info("Processing...") # ● 蓝色消息 skin.status("Key", "value") # 键值对状态行 skin.table(headers, rows) # 格式化的表格 skin.progress(3, 10, "...") # 进度条 skin.print_goodbye() # 样式化的退出消息 - ReplSkin 会自动检测包目录内的
skills/SKILL.md并在横幅中显示它。AI 代理可以读取显示的绝对路径处的技能文件。 - 使 REPL 成为默认行为:在主 Click 组上使用
invoke_without_command=True,并在没有给定子命令时调用repl命令:@click.group(invoke_without_command=True) @click.pass_context def cli(ctx, ...): ... if ctx.invoked_subcommand is None: ctx.invoke(repl, project_path=None) - 这确保了不带参数的
cli-anything-<software>会进入 REPL
- 从插件(
第四阶段:测试计划(TEST.md - 第一部分)
在编写任何测试代码之前,在 agent-harness/cli_anything/<software>/tests/ 目录中创建一个 TEST.md 文件。此文件作为测试计划,必须包含:
-
测试清单计划 — 列出计划的测试文件和估计的测试数量:
test_core.py:计划 XX 个单元测试test_full_e2e.py:计划 XX 个端到端测试
-
单元测试计划 — 对于每个核心模块,描述将要测试的内容:
- 模块名称(例如
project.py) - 要测试的函数
- 要覆盖的边缘情况(无效输入、边界条件、错误处理)
- 预期的测试数量
- 模块名称(例如
-
端到端测试计划 — 描述要测试的真实场景:
- 将模拟哪些工作流程?
- 将生成/处理哪些真实文件?
- 将验证哪些输出属性?
- 将执行哪些格式验证?
-
真实工作流程场景 — 详细说明每个多步骤工作流程:
- 工作流程名称:简短标题
- 模拟:什么真实任务(例如,“照片编辑流水线”、“播客制作”、“产品渲染设置”)
- 操作链:逐步操作
- 验证:将检查哪些输出属性
此计划文档确保在编写代码之前有全面的测试覆盖。
第五阶段:测试实现
现在根据 TEST.md 计划编写实际的测试代码:
-
单元测试(
test_core.py)— 每个核心函数独立测试,使用合成数据。无外部依赖。 -
端到端测试 — 中间文件(
test_full_e2e.py)— 验证你的 CLI 生成的项目文件结构是否正确(有效的 XML、正确的 ZIP 结构等)。 -
端到端测试 — 真实后端(
test_full_e2e.py)— 必须调用真实软件。 创建一个项目,通过实际软件后端导出,并验证输出:- 文件存在且大小 > 0
- 正确的格式(PDF 魔数
%PDF-,DOCX/XLSX/PPTX 是有效的 ZIP/OOXML 等) - 尽可能验证内容(CSV 包含预期数据等)
- 打印工件路径 以便用户手动检查:
print(f"\n PDF: {path} ({size:,} bytes)") - 没有优雅降级 — 如果软件未安装,测试应失败,而不是跳过
-
输出验证 — 不要仅仅因为导出成功退出就相信它有效。 以编程方式验证输出:
- 魔数 / 文件格式验证
- OOXML 格式(DOCX、XLSX、PPTX)的 ZIP 结构
- 视频/图像的像素级分析(探测帧,比较亮度)
- 音频分析(RMS 电平,频谱比较)
- 针对预期值的时长/格式检查
-
CLI 子进程测试 — 像真实用户/代理那样测试已安装的 CLI 命令。子进程测试也必须产生真实的最终输出(不仅仅是 ODF 中间文件)。使用
_resolve_cli助手运行已安装的cli-anything-<software>命令:def _resolve_cli(name): """解析已安装的 CLI 命令;在开发环境中回退到 python -m。 设置环境变量 CLI_ANYTHING_FORCE_INSTALLED=1 以要求使用已安装的命令。 """ import shutil force = os.environ.get("CLI_ANYTHING_FORCE_INSTALLED", "").strip() == "1" path = shutil.which(name) if path: print(f"[_resolve_cli] 使用已安装的命令:{path}") return [path] if force: raise RuntimeError(f"{name} 在 PATH 中未找到。使用以下命令安装:pip install -e .") module = name.replace("cli-anything-", "cli_anything.") + "." + name.split("-")[-1] + "_cli" print(f"[_resolve_cli] 回退到:{sys.executable} -m {module}") return [sys.executable, "-m", module] class TestCLISubprocess: CLI_BASE = _resolve_cli("cli-anything-<software>") def _run(self, args, check=True): return subprocess.run( self.CLI_BASE + args, capture_output=True, text=True, check=check, ) def test_help(self): result = self._run(["--help"]) assert result.returncode == 0 def test_project_new_json(self, tmp_dir): out = os.path.join(tmp_dir, "test.json") result = self._run(["--json", "project", "new", "-o", out]) assert result.returncode == 0 data = json.loads(result.stdout) # ... 验证结构子进程测试的关键规则:
- 始终使用
_resolve_cli("cli-anything-<software>")— 切勿直接硬编码sys.executable或模块路径 - 不要设置
cwd— 已安装的命令必须能从任何目录工作 - 在 CI/发布测试中使用
CLI_ANYTHING_FORCE_INSTALLED=1以确保测试的是已安装的命令(而不是回退方式) - 测试
--help、--json、项目创建、关键命令和完整工作流程
- 始终使用
-
往返测试 — 通过 CLI 创建项目,在 GUI 中打开,验证正确性
-
代理测试 — 让 AI 代理仅使用 CLI 完成一个真实任务
第六阶段:测试文档(TEST.md - 第二部分)
在所有测试成功运行后,附加到现有的 TEST.md:
- 测试结果 — 粘贴完整的
pytest -v --tb=no输出,显示所有通过的测试及其名称和状态 - 摘要统计 — 总测试数、通过率、执行时间
- 覆盖说明 — 测试未覆盖的任何缺口或领域
TEST.md 现在既作为测试计划(在实现之前编写)又作为测试结果文档(在执行后附加),提供测试过程的完整记录。
第六阶段半:SKILL.md 生成
生成一个 SKILL.md 文件,使 CLI 能够通过技能创建者方法被 AI 代理发现和使用。此文件作为一个自包含的技能定义,可以被 Claude Code 或其他 AI 助手加载。
目的: SKILL.md 文件遵循标准格式,使 AI 代理能够:
- 发现 CLI 的能力
- 理解命令结构和用法
- 生成正确的命令调用
- 以编程方式处理输出
SKILL.md 结构:
-
YAML 前置元数据 — 用于技能发现的触发元数据:
--- name: "cli-anything-<software>" description: "对 CLI 功能的简要描述" --- -
Markdown 正文 — 使用说明,包括:
- 安装先决条件
- 基本命令语法
- 命令组及其功能
- 使用示例
- 代理特定指导(JSON 输出、错误处理)
生成过程:
-
使用
skill_generator.py提取 CLI 元数据:from skill_generator import generate_skill_file skill_path = generate_skill_file( harness_path="/path/to/agent-harness" ) # 默认输出:cli_anything/<software>/skills/SKILL.md -
生成器自动提取:
- 软件名称和版本(来自 setup.py)
- 命令组(来自 CLI 文件的 Click 装饰器)
- README.md 中的文档
- 系统包要求
-
自定义模板(可选):
- 默认模板:
templates/SKILL.md.template - 使用 Jinja2 占位符填充动态内容
- 可以为特定软件部分进行扩展
- 默认模板:
输出位置:
SKILL.md 生成在 Python 包内部,以便与 pip install 一起安装:
<software>/
└── agent-harness/
└── cli_anything/
└── <software>/
└── skills/
└── SKILL.md
手动生成:
cd cli-anything-plugin
python skill_generator.py /path/to/software/agent-harness
与 CLI 构建的集成:
SKILL.md 的生成应在第六阶段(测试文档)成功完成后运行,确保在创建技能定义之前 CLI 已被完整记录和测试。
关键原则:
- SKILL.md 必须是自包含的(理解它不需要外部依赖)
- 包含针对程序化使用的代理特定指导
- 记录
--json标志的用法以获得机器可读的输出 - 列出所有命令组及其简要描述
- 提供演示常见工作流程的真实示例
CLI 横幅中的技能路径:
ReplSkin 自动检测包内的 skills/SKILL.md 并在启动横幅中显示绝对路径。AI 代理可以读取显示的路径处的文件:
# 在 REPL 初始化中(例如 shotcut_cli.py)
from cli_anything.<software>.utils.repl_skin import ReplSkin
skin = ReplSkin("<software>", version="1.0.0")
skin.print_banner() # 自动检测并显示:◇ 技能:/path/to/cli_anything/<software>/skills/SKILL.md
包数据:
确保 setup.py 将技能文件包含为包数据,以便随 pip 安装:
package_data={
"cli_anything.<software>": ["skills/*.md"],
},
关键经验教训
使用真实软件 — 不要重新实现它
这是第一条规则。 CLI 必须调用实际的软件进行渲染和导出 — 而不是用 Python 重新实现软件的功能。
反模式: 构建基于 Pillow 的图像合成器来替代 GIMP,或者生成 bpy 脚本却从不调用 Blender。这会产生一个玩具,无法处理真实的工作负载,并且与真实软件的行为相偏离。
正确的方法:
-
使用软件的 CLI/脚本接口作为后端:
- LibreOffice:
libreoffice --headless --convert-to pdf/docx/xlsx/pptx - Blender:
blender --background --python script.py - GIMP:
gimp -i -b '(script-fu-console-eval ...)' - Inkscape:
inkscape --actions="..." --export-filename=... - Shotcut/Kdenlive:
melt project.mlt -consumer avformat:output.mp4 - Audacity:
sox用于效果处理 - OBS:
obs-websocket协议
- LibreOffice:
-
软件是必需的依赖项,而不是可选的。将其添加到安装说明中。没有实际软件,CLI 是无用的。
-
生成有效的项目/中间文件(ODF、MLT XML、.blend、SVG 等),然后将它们交给真实软件进行渲染。你的 CLI 是该软件的结构化命令行界面,而不是它的替代品。
示例 — LibreOffice CLI 导出管道:
# 1. 将文档构建为有效的 ODF 文件(我们的 XML 构建器)
odf_path = write_odf(tmp_path, doc_type, project)
# 2. 通过真实的 LibreOffice 进行转换(而不是重新实现)
subprocess.run([
"libreoffice", "--headless",
"--convert-to", "pdf",
"--outdir", output_dir,
odf_path,
])
# 结果:由 LibreOffice 完整引擎渲染的真实 PDF
渲染差距
这是第二大陷阱。 大多数 GUI 应用程序在渲染时通过其引擎应用效果。当你构建一个直接操作项目文件的 CLI 时,你还必须处理渲染——而简单的方法会静默地丢弃效果。
问题: 你的 CLI 将滤镜/效果添加到项目文件格式中。但在渲染时,如果你使用一个简单的工具(例如 ffmpeg concat demuxer),它读取原始媒体文件并忽略所有项目级效果。输出看起来与输入相同。用户看不出发生了什么。
解决方案 — 滤镜转换层:
- 最佳情况: 使用应用程序的原生渲染器(MLT 项目的
melt)。它读取项目文件并应用所有内容。 - 后备方案: 构建一个转换层,将项目格式的效果转换为渲染工具的原生语法(例如 MLT 滤镜 → ffmpeg
-filter_complex)。 - 最后的手段: 生成一个用户可以手动运行的渲染脚本。
渲染的优先级顺序: 原生引擎 → 转换后的滤镜图 → 脚本。
滤镜转换陷阱
在格式之间转换效果时(例如 MLT → ffmpeg),注意:
- 重复的滤镜类型: 有些工具(ffmpeg)不允许在同一个链中出现两次相同的滤镜。如果你的项目同时有
brightness和saturation滤镜,并且两者都映射到 ffmpeg 的eq=,你必须合并它们为一个单独的eq=brightness=X:saturation=Y。 - 顺序约束: ffmpeg 的
concat滤镜要求交错的流顺序:[v0][a0][v1][a1][v2][a2],而不是分组的[v0][v1][v2][a0][a1][a2]。如果你不知道这一点,错误信息(“媒体类型不匹配”)会很隐晦。 - 参数空间差异: 效果参数通常使用不同的尺度。MLT 亮度
1.15= +15%,但 ffmpegeq=brightness=0.06在 -1..1 的尺度上。明确记录每一个映射。 - 无法映射的效果: 有些效果在渲染工具中没有等价物。优雅地处理(警告、跳过)而不是崩溃。
时间码精度
非整数帧率(29.97fps = 30000/1001)会导致累积舍入误差:
- 使用
round(),而不是int()进行浮点到帧的转换。int(9000 * 29.97)会截断并丢失帧;round()得到正确答案。 - 对时间码显示使用整数算术。 通过
round(frames * fps_den * 1000 / fps_num)将帧转换为总毫秒数,然后用整数除法分解。避免在长时间内漂移的中间浮点数。 - 在非整数 FPS 的往返测试中接受 ±1 帧的容差。 数学上的精确相等是不可能的。
输出验证方法
永远不要仅仅因为导出没有错误运行就认为它是正确的。验证:
# 视频:使用 ffmpeg 探测特定帧
# 第 0 帧用于淡入(应接近全黑)
# 中间帧用于颜色效果(比较亮度/饱和度与源)
# 最后一帧用于淡出(应接近全黑)
# 比较不同分辨率之间的像素值时,
# 排除信箱模式/柱箱模式(黑色填充条)。
# 水平帧中的垂直视频将有大约 40% 的黑色像素。
# 音频:检查开始/结束时的 RMS 电平以检查淡入淡出
# 将频谱特性与源进行比较
测试策略
四个测试层,各有其目的:
- 单元测试(
test_core.py):合成数据,无外部依赖。独立测试每个函数。快速、确定性强,适合 CI。 - 端到端测试 — 原生(
test_full_e2e.py):测试项目文件生成管道(ODF 结构、XML 内容、格式验证)。验证你的 CLI 生成的中间文件是否正确。 - 端到端测试 — 真实后端(
test_full_e2e.py):调用真实软件(LibreOffice、Blender、melt 等)生成最终输出文件(PDF、DOCX、渲染图像、视频)。验证输出文件:- 存在且大小 > 0
- 具有正确的格式(魔数、ZIP 结构等)
- 在可验证的情况下包含预期内容
- 打印工件路径 以便用户可以手动检查结果
- CLI 子进程测试(在
test_full_e2e.py中):通过subprocess.run调用已安装的cli-anything-<software>命令,端到端运行完整工作流程:创建项目 → 添加内容 → 通过真实软件导出 → 验证输出。
没有优雅降级。 必须安装真实软件。当软件缺失时,测试绝不能跳过或伪造结果——没有该软件,CLI 是无用的。软件是硬性依赖,而不是可选的。
示例 — LibreOffice 的真实端到端测试:
class TestWriterToPDF:
def test_rich_writer_to_pdf(self, tmp_dir):
proj = create_document(doc_type="writer", name="Report")
add_heading(proj, text="Quarterly Report", level=1)
add_table(proj, rows=3, cols=3, data=[...])
pdf_path = os.path.join(tmp_dir, "report.pdf")
result = export(proj, pdf_path, preset="pdf", overwrite=True)
# 验证真实的输出文件
assert os.path.exists(result["output"])
assert result["file_size"] > 1000 # 不是可疑的小文件
with open(result["output"], "rb") as f:
assert f.read(5) == b"%PDF-" # 验证格式魔数
print(f"\n PDF:{result['output']} ({result['file_size']:,} bytes)")
class TestCLISubprocessE2E:
CLI_BASE = _resolve_cli("cli-anything-libreoffice")
def test_full_writer_pdf_workflow(self, tmp_dir):
proj_path = os.path.join(tmp_dir, "test.json")
pdf_path = os.path.join(tmp_dir, "output.pdf")
self._run(["document", "new", "-o", proj_path, "--type", "writer"])
self._run(["--project", proj_path, "writer", "add-heading", "-t", "Title"])
self._run(["--project", proj_path, "export", "render", pdf_path, "-p", "pdf", "--overwrite"])
assert os.path.exists(pdf_path)
with open(pdf_path, "rb") as f:
assert f.read(5) == b"%PDF-"
在强制安装模式下运行测试以确保使用真实命令:
CLI_ANYTHING_FORCE_INSTALLED=1 python3 -m pytest cli_anything/<software>/tests/ -v -s
-s 标志会显示 [_resolve_cli] 的打印输出,确认使用的是哪个后端,并打印工件路径供手动检查。
真实工作流程测试场景应包括:
- 多段编辑(类似 YouTube 的剪切/修剪)
- 蒙太奇组合(许多短片段)
- 画中画合成
- 调色流水线
- 音频混音(类似播客)
- 繁重的撤销/重做压力测试
- 复杂项目的保存/加载往返
- 迭代细化(添加、修改、删除、重新添加)
关键原则
- 使用真实软件 — CLI 必须调用实际应用程序进行渲染和导出。生成有效的中间文件(ODF、MLT XML、.blend、SVG),然后将它们交给真实软件。切勿用 Python 重新实现渲染引擎。
- 软件是硬性依赖 — 不是可选的,也不是优雅降级的。如果未安装 LibreOffice,
cli-anything-libreoffice必须清晰地报错,而不是用回退库静默地产生劣质输出。 - 直接操作原生格式 — 解析和修改应用程序的原生项目文件(MLT XML、ODF、SVG 等)作为数据层。
- 利用现有的 CLI 工具 — 使用
libreoffice --headless、blender --background、melt、ffmpeg、inkscape --actions、sox作为子进程进行渲染。 - 验证渲染产生正确的输出 — 参见上面的“渲染差距”。
- 端到端测试必须产生真实工件 — PDF、DOCX、渲染图像、视频。打印输出路径以便用户检查。切勿仅测试中间格式。
- 失败时要响亮而清晰 — 代理需要明确的错误信息来自我纠正。
- 尽可能幂等 — 两次运行同一命令应该是安全的。
- 提供内省能力 —
info、list、status命令对于代理在执行操作前了解当前状态至关重要。 - JSON 输出模式 — 每个命令都应支持
--json以便机器解析。
规则
- 真实软件必须是硬性依赖。 CLI 必须调用实际软件(LibreOffice、Blender、GIMP 等)进行渲染和导出。不要在 Python 中重新实现渲染。不要优雅地降级到回退库。如果未安装该软件,CLI 必须报错并给出清晰的安装说明。
- 每个
cli_anything/<software>/目录必须包含一个README.md,说明如何安装软件依赖、如何安装 CLI、如何运行测试,并显示基本用法。 - 端到端测试必须调用真实软件 并产生真实的输出文件(PDF、DOCX、渲染图像、视频)。测试必须验证输出存在、格式正确,并打印工件路径以便用户检查结果。切勿仅测试中间文件。
- 每个导出/渲染函数必须经过验证,在标记为正常工作之前,必须以编程方式分析输出。“它运行没有错误”是不够的。
- 注册表中的每个滤镜/效果必须有对应的渲染映射,或者明确记录为“仅限项目(不渲染)”。
- 测试套件必须包含真实文件的端到端测试,而不仅仅是带有合成数据的单元测试。格式假设在真实媒体中不断被打破。
- 端到端测试必须包含子进程测试,通过
_resolve_cli()调用已安装的cli-anything-<software>命令。测试必须针对实际安装的包,而不仅仅是源导入。 - 每个
cli_anything/<software>/tests/目录必须包含一个TEST.md,记录测试覆盖的内容、测试了哪些真实工作流程以及完整的测试结果输出。 - 每个 CLI 必须使用统一的 REPL 皮肤(
repl_skin.py)用于交互模式。将cli-anything-plugin/repl_skin.py复制到utils/repl_skin.py,并使用ReplSkin处理横幅、提示、帮助、消息和告别。当 CLI 在没有子命令的情况下调用时(invoke_without_command=True),REPL 必须是默认行为。
目录结构
<software>/
└── agent-harness/
├── <SOFTWARE>.md # 特定项目的分析和 SOP
├── setup.py # PyPI 包配置(第七阶段)
├── cli_anything/ # 命名空间包(此处没有 __init__.py)
│ └── <software>/ # 此 CLI 的子包
│ ├── __init__.py
│ ├── __main__.py # python3 -m cli_anything.<software>
│ ├── README.md # 如何运行 — 必需
│ ├── <software>_cli.py # 主 CLI 入口点(Click + REPL)
│ ├── core/ # 核心模块(每个领域一个)
│ │ ├── __init__.py
│ │ ├── project.py # 项目创建/打开/保存/信息
│ │ ├── ... # 特定领域的模块
│ │ ├── export.py # 渲染管道 + 滤镜转换
│ │ └── session.py # 有状态会话、撤销/重做
│ ├── utils/ # 共享工具
│ │ ├── __init__.py
│ │ ├── <software>_backend.py # 后端:调用真实软件
│ │ └── repl_skin.py # 统一的 REPL 皮肤(从插件复制)
│ └── tests/ # 测试套件
│ ├── TEST.md # 测试文档和结果 — 必需
│ ├── test_core.py # 单元测试(合成数据)
│ └── test_full_e2e.py # 端到端测试(真实文件)
└── examples/ # 示例脚本和工作流程
关键点: cli_anything/ 目录不得包含 __init__.py。这使其成为 PEP 420 命名空间包——多个单独安装的 PyPI 包可以各自在 cli_anything/ 下贡献一个子包而不会冲突。例如,cli-anything-gimp 添加 cli_anything/gimp/,cli-anything-blender 添加 cli_anything/blender/,两者共存于同一个 Python 环境中。
注意:此 HARNESS.md 是 cli-anything-plugin 的一部分。各个软件目录都引用此文件 — 请勿重复复制。
将此应用于其他软件
同样的 SOP 适用于任何 GUI 应用程序:
| 软件 | 后端 CLI | 原生格式 | 系统包 | CLI 如何使用它 |
|---|---|---|---|---|
| LibreOffice | libreoffice --headless |
.odt/.ods/.odp (ODF ZIP) | apt install libreoffice |
生成 ODF → 转换为 PDF/DOCX/XLSX/PPTX |
| Blender | blender --background --python |
.blend-cli.json | apt install blender |
生成 bpy 脚本 → Blender 渲染为 PNG/MP4 |
| GIMP | gimp -i -b '(script-fu ...)' |
.xcf | apt install gimp |
Script-Fu 命令 → GIMP 处理并导出 |
| Inkscape | inkscape --actions="..." |
.svg (XML) | apt install inkscape |
操作 SVG → Inkscape 导出为 PNG/PDF |
| Shotcut/Kdenlive | melt 或 ffmpeg |
.mlt (XML) | apt install melt ffmpeg |
构建 MLT XML → melt/ffmpeg 渲染视频 |
| Audacity | sox |
.aup3 | apt install sox |
生成 sox 命令 → sox 处理音频 |
| OBS Studio | obs-websocket |
scene.json | apt install obs-studio |
WebSocket API → OBS 捕获/录制 |
软件是必需的依赖项,而不是可选的。 CLI 生成有效的中间文件(ODF、MLT XML、bpy 脚本、SVG)并将它们交给真实软件进行渲染。这正是使 CLI 真正有用的原因——它是该软件的命令行界面,而不是替代品。
模式总是相同的:构建数据 → 调用真实软件 → 验证输出。
第七阶段:PyPI 发布和安装
在构建和测试 CLI 之后,使其可安装和可发现。
所有 cli-anything CLI 在共享的 cli_anything 命名空间下使用 PEP 420 命名空间包。这允许多个 CLI 包在同一 Python 环境中并排安装而不会冲突。
-
将包结构化为命名空间包:
agent-harness/ ├── setup.py └── cli_anything/ # 此处没有 __init__.py(命名空间包) └── <software>/ # 例如 gimp、blender、audacity ├── __init__.py # 有 __init__.py(常规子包) ├── <software>_cli.py ├── core/ ├── utils/ └── tests/关键规则:
cli_anything/有没有__init__.py。每个子包(gimp/、blender/等)确实有__init__.py。这使得多个包可以贡献于同一个命名空间。 -
在
agent-harness/目录中创建 setup.py:from setuptools import setup, find_namespace_packages setup( name="cli-anything-<software>", version="1.0.0", packages=find_namespace_packages(include=["cli_anything.*"]), install_requires=[ "click>=8.0.0", "prompt-toolkit>=3.0.0", # 在此添加 Python 库依赖项 ], entry_points={ "console_scripts": [ "cli-anything-<software>=cli_anything.<software>.<software>_cli:main", ], }, python_requires=">=3.10", )重要细节:
- 使用
find_namespace_packages,而不是find_packages - 使用
include=["cli_anything.*"]限定发现范围 - 入口点格式:
cli_anything.<software>.<software>_cli:main - 系统包(LibreOffice、Blender 等)是硬性依赖,无法在
install_requires中表达。在 README.md 中记录,并让后端模块引发一个带有安装说明的清晰错误:# 在 utils/<software>_backend.py 中 def find_<software>(): path = shutil.which("<software>") if path: return path raise RuntimeError( "<Software> 未安装。请使用以下命令安装:\n" " apt install <software> # Debian/Ubuntu\n" " brew install <software> # macOS" )
- 使用
-
所有导入都使用
cli_anything.<software>前缀:from cli_anything.gimp.core.project import create_project from cli_anything.gimp.core.session import Session from cli_anything.blender.core.scene import create_scene -
测试本地安装:
cd /root/cli-anything/<software>/agent-harness pip install -e . -
验证 PATH 安装:
which cli-anything-<software> cli-anything-<software> --help -
针对已安装的命令运行测试:
cd /root/cli-anything/<software>/agent-harness CLI_ANYTHING_FORCE_INSTALLED=1 python3 -m pytest cli_anything/<software>/tests/ -v -s输出必须显示
[_resolve_cli] 使用已安装的命令:/path/to/cli-anything-<software>,确认子进程测试针对真实安装的二进制文件运行,而不是模块回退。 -
验证命名空间跨包工作(当安装了多个 CLI 时):
import cli_anything.gimp import cli_anything.blender # 两者都解析到各自的源目录
为什么使用命名空间包:
- 多个 CLI 在同一 Python 环境中共存而不冲突
- 在单个
cli_anything命名空间下进行干净、有组织的导入 - 每个 CLI 可通过 pip 独立安装/卸载
- 代理可以通过
cli_anything.*发现所有已安装的 CLI - 标准的 Python 打包方式——无需黑客或变通方法
浙公网安备 33010602011771号