clak 业务脚本开发
使用 clak 构建固定流程业务工具。业务代码创建自己的 Clak 实例并注册 Script 子类;全局 clak 命令只包含内置命令,不能运行业务脚本。
安装方式
运行及测试本 skill 的示例均需要以下依赖:
python -m pip install esclak pytest
生产项目若不运行 pytest,可以只安装 esclak;但本文“测试”和“提交前检查”章节的命令要求安装 pytest。
Skill 内置参考实现
此 skill 自包含可复制的参考代码。按需求读取相对于本文件的路径:
references/dev_tools/README.md:完整用法和运行命令references/dev_tools/app.py:应用、状态栏和 footer 注册references/dev_tools/__main__.py:项目级 CLI 入口references/dev_tools/scripts/select_res_server.py:过滤选择和共享状态references/dev_tools/scripts/pack_upload.py:多步编排、子进程、条件步骤和资源清理references/dev_tools/scripts/verify_release.py:业务失败与退出码references/testing.py:ScriptTester与ScriptRun测试夹具
不要将参考项目作为依赖导入。复制其结构和模式到业务项目;目标项目只需安装 esclak 包即可使用本 skill。
最小项目结构
my_tools/
├── __init__.py
├── __main__.py
├── app.py
└── scripts/
├── __init__.py
└── deploy.py
# app.py
from clak import Clak
from .scripts.deploy import DeployScript
app = Clak(status="发布工具 · staging")
app.register(DeployScript)
# __main__.py
from .app import app
app.run_cli(prog="my-tools")
业务入口必须是:
python -m my_tools
python -m my_tools exec /deploy
不要执行 clak exec /deploy,它不会加载业务项目注册的脚本。
Script 约定
脚本每次运行会创建新实例。run 无参数;框架在 on_enter(session) 中绑定会话,再在结束、失败或取消时调用 on_exit()。
from clak import Script
class DeployScript(Script):
name = "/deploy"
description = "发布服务"
def run(self) -> None:
def deploy() -> int:
self.output.push("发布完成")
return 0
self.add_step(True, deploy, "deploy")
self.run_steps()
name必须是 slash 命令,如/deployrun(self)内定义并编排本次执行的步骤self.add_step(can_do, task, name)添加步骤,can_do为布尔值或无参判断函数- 步骤返回
0继续,非0表示等待输入后恢复,不是业务失败 - 以
self.run_steps()结束run,不要直接操作Session的执行队列 self.output.push()自动加[script-name]前缀- 预期业务失败抛
ScriptError(message, exit_code=1);参数格式不合法抛ValueError
收集输入
输入步骤返回 self.ask_tool(request, on_answer),答案回调保存业务状态;下一步骤会在输入提交后继续。
from clak import AskAnswer, AskChoice, AskMode, AskRequest
def ask_environment() -> int:
def save(answer: AskAnswer) -> None:
state.environment = answer.primary
self.output.push(f"环境:{state.environment}")
return self.ask_tool(
AskRequest(
field_id="environment",
prompt="选择环境",
default="staging",
choices=(
AskChoice(label="预发", value="staging"),
AskChoice(label="生产", value="production"),
),
mode=AskMode.SINGLE,
),
save,
)
AskRequest.default 是 esclak 当前 API 的必填构造参数,即使业务不希望提供可用默认值也必须显式传入,例如 default=""。只有非空默认值才能被无头 CLI 自动采用;空默认值与缺失输入一样会失败。
无头 CLI 的真实输入规则如下:
| 命令形态 | 未提供的 ask 字段行为 |
|---|---|
不带任何 --set,如 python -m my_tools exec /deploy |
自动采用每个字段的非空 AskRequest.default。没有可用默认值时退出码为 2。 |
带 --set environment=staging |
该字段采用显式值。若还有未提供字段,默认值不会自动采用;缺失字段退出码为 2。 |
带 --set ... --use-defaults |
显式 --set 优先;其余字段采用各自的非空默认值。 |
因此,设计无头批处理时应为安全的默认路径声明默认值;对必须由调用者确认的字段传 default="",并用 --set 提供它。--use-defaults 不是“跳过校验”:框架仍会校验 choice,答案回调仍应校验业务格式。
field_id是无头模式的--set field_id=value键AskMode.SINGLE单选,MULTI多选,FILTER可过滤选择- 仅
AskChoice(value=None)允许单选字段输入候选外文本,回调仍须校验业务格式 - 无头运行:
python -m my_tools exec /deploy --set environment=staging
资源、状态与错误边界
- 临时目录、文件句柄和外部资源在
on_enter创建,在on_exit清理;清理必须覆盖完成、失败和取消 - 跨脚本的进程内状态放入显式
context.py,并提供reset()供测试隔离 - 调外部命令使用
subprocess.run(..., check=False, timeout=...),将启动异常和非零退出码转换成ScriptError - 路径、用户输入和权限参数先校验;不要把失败退出码误写成步骤挂起
测试
优先用 ScriptTester 验证脚本生命周期、等待输入、成功和失败路径。
from clak import ScriptTester
from my_tools.scripts.deploy import DeployScript
def test_deploy_asks_then_completes():
run = ScriptTester(DeployScript).run()
assert run.is_awaiting("environment")
run.provide("environment", "staging")
assert run.succeeded
assert run.output.contains("发布完成")
每个脚本至少覆盖:
- 默认或完整输入的成功路径
- 每个必填输入缺失时的行为
- 非法候选或格式错误返回退出码
2 ScriptError业务失败返回预期退出码- 有资源时,完成、失败、取消均执行清理
- 条件步骤的执行与跳过
最后运行:
python -m pytest
python -m my_tools exec /deploy --set environment=staging
提交前检查
- 业务入口而非全局
clak命令可运行 run(self)已调用一次self.run_steps()- 输出具备可定位的业务信息
- 无头成功、业务失败、参数失败的退出码分别正确
- TUI 手测搜索、输入、取消以及动态状态或 footer