feat: 为 skill-generator 添加脚本执行能力

- 默认创建 scripts/ 目录用于存放确定性脚本
- 新增 specs/scripting-integration.md 脚本集成规范
- 新增 templates/script-python.md 和 script-bash.md 脚本模板
- 模板中添加 ## Scripts 声明和 ExecuteScript 调用示例
- 支持命名即ID、扩展名即运行时的约定
- 参数自动转换: snake_case → kebab-case
- Bash 模板使用 jq 构建 JSON 输出

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
catlog22
2026-01-11 23:14:00 +08:00
parent 37614a3362
commit cefb934a2c
6 changed files with 808 additions and 10 deletions

View File

@@ -34,10 +34,8 @@ if (config.execution_mode === 'autonomous' || config.execution_mode === 'hybrid'
Bash(`mkdir -p "${skillDir}/phases/actions"`);
}
// 可选: scripts 目录
if (config.needs_scripts) {
// scripts 目录(默认创建,用于存放确定性脚本)
Bash(`mkdir -p "${skillDir}/scripts"`);
}
```
### Step 3: 生成 SKILL.md
@@ -199,9 +197,10 @@ function generateReferenceTable(config) {
- **Directory**: `.claude/skills/{skill-name}/`
- **Files**:
- `SKILL.md` (入口文件)
- `phases/` (目录)
- `specs/` (目录)
- `templates/` (目录)
- `phases/` (执行阶段目录)
- `specs/` (规范文档目录)
- `templates/` (模板目录)
- `scripts/` (脚本目录,存放 Python/Bash 确定性脚本)
## Next Phase

View File

@@ -0,0 +1,265 @@
# Scripting Integration Spec
技能脚本集成规范,定义如何在技能中使用外部脚本执行确定性任务。
## 核心原则
1. **约定优于配置**:命名即 ID扩展名即运行时
2. **极简调用**:一行完成脚本调用
3. **标准输入输出**命令行参数输入JSON 标准输出
## 目录结构
```
.claude/skills/<skill-name>/
├── scripts/ # 脚本专用目录
│ ├── process-data.py # id: process-data
│ ├── validate-output.sh # id: validate-output
│ └── transform-json.js # id: transform-json
├── phases/
└── specs/
```
## 命名约定
| 扩展名 | 运行时 | 执行命令 |
|--------|--------|----------|
| `.py` | python | `python scripts/{id}.py` |
| `.sh` | bash | `bash scripts/{id}.sh` |
| `.js` | node | `node scripts/{id}.js` |
## 声明格式
在 Phase 或 Action 文件的 `## Scripts` 部分声明:
```yaml
## Scripts
- process-data
- validate-output
```
## 调用语法
### 基础调用
```javascript
const result = await ExecuteScript('script-id', { key: value });
```
### 参数命名转换
调用时 JS 对象中的键会**自动转换**为 `kebab-case` 命令行参数:
| JS 键名 | 转换后参数 |
|---------|-----------|
| `input_path` | `--input-path` |
| `output_dir` | `--output-dir` |
| `max_count` | `--max-count` |
脚本中使用 `--input-path` 接收,调用时使用 `input_path` 传入。
### 完整调用(含错误处理)
```javascript
const result = await ExecuteScript('process-data', {
input_path: `${workDir}/data.json`,
threshold: 0.9
});
if (!result.success) {
throw new Error(`脚本执行失败: ${result.stderr}`);
}
const { output_file, count } = result.outputs;
```
## 返回格式
```typescript
interface ScriptResult {
success: boolean; // exit code === 0
stdout: string; // 完整标准输出
stderr: string; // 完整标准错误
outputs: { // 从 stdout 最后一行解析的 JSON
[key: string]: any;
};
}
```
## 脚本编写规范
### 输入:命令行参数
```bash
# Python: argparse
--input-path /path/to/file --threshold 0.9
# Bash: 手动解析
--input-path /path/to/file
```
### 输出:标准输出 JSON
脚本最后一行必须打印单行 JSON
```json
{"output_file": "/tmp/result.json", "count": 42}
```
### Python 模板
```python
import argparse
import json
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--input-path', required=True)
parser.add_argument('--threshold', type=float, default=0.9)
args = parser.parse_args()
# 执行逻辑...
result_path = "/tmp/result.json"
# 输出 JSON
print(json.dumps({
"output_file": result_path,
"items_processed": 100
}))
if __name__ == '__main__':
main()
```
### Bash 模板
```bash
#!/bin/bash
# 解析参数
while [[ "$#" -gt 0 ]]; do
case $1 in
--input-path) INPUT_PATH="$2"; shift ;;
*) echo "Unknown: $1" >&2; exit 1 ;;
esac
shift
done
# 执行逻辑...
LOG_FILE="/tmp/process.log"
echo "Processing $INPUT_PATH" > "$LOG_FILE"
# 输出 JSON
echo "{\"log_file\": \"$LOG_FILE\", \"status\": \"done\"}"
```
## ExecuteScript 实现
```javascript
async function ExecuteScript(scriptId, inputs = {}) {
const skillDir = GetSkillDir();
// 查找脚本文件
const extensions = ['.py', '.sh', '.js'];
let scriptPath, runtime;
for (const ext of extensions) {
const path = `${skillDir}/scripts/${scriptId}${ext}`;
if (FileExists(path)) {
scriptPath = path;
runtime = ext === '.py' ? 'python' : ext === '.sh' ? 'bash' : 'node';
break;
}
}
if (!scriptPath) {
throw new Error(`Script not found: ${scriptId}`);
}
// 构建命令行参数
const args = Object.entries(inputs)
.map(([k, v]) => `--${k.replace(/_/g, '-')} "${v}"`)
.join(' ');
// 执行脚本
const cmd = `${runtime} "${scriptPath}" ${args}`;
const { stdout, stderr, exitCode } = await Bash(cmd);
// 解析输出
let outputs = {};
try {
const lastLine = stdout.trim().split('\n').pop();
outputs = JSON.parse(lastLine);
} catch (e) {
// 无法解析 JSON保持空对象
}
return {
success: exitCode === 0,
stdout,
stderr,
outputs
};
}
```
## 使用场景
### 适合脚本化的任务
- 数据处理和转换
- 文件格式转换
- 批量文件操作
- 复杂计算逻辑
- 调用外部工具/库
### 不适合脚本化的任务
- 需要用户交互的任务
- 需要访问 Claude 工具的任务
- 简单的文件读写
- 需要动态决策的任务
## 路径约定
### 脚本路径
脚本路径相对于 `SKILL.md` 所在目录(技能根目录):
```
.claude/skills/<skill-name>/ # 技能根目录SKILL.md 所在位置)
├── SKILL.md
├── scripts/ # 脚本目录
│ └── process-data.py # 相对路径: scripts/process-data.py
└── phases/
```
`ExecuteScript` 自动从技能根目录查找脚本:
```javascript
// 实际执行: python .claude/skills/<skill-name>/scripts/process-data.py
await ExecuteScript('process-data', { ... });
```
### 输出目录
**推荐**:由调用方传递输出目录,而非脚本默认 `/tmp`
```javascript
// 调用时指定输出目录(在工作流工作目录内)
const result = await ExecuteScript('process-data', {
input_path: `${workDir}/data.json`,
output_dir: `${workDir}/output` // 明确指定输出位置
});
```
脚本应接受 `--output-dir` 参数,而非硬编码输出路径。
## 最佳实践
1. **单一职责**:每个脚本只做一件事
2. **无副作用**:脚本不应修改全局状态
3. **幂等性**:相同输入产生相同输出
4. **错误明确**:错误信息写入 stderr正常输出写入 stdout
5. **快速失败**:参数验证失败立即退出
6. **路径参数化**:输出路径由调用方指定,不硬编码

View File

@@ -17,11 +17,22 @@
{{preconditions_list}}
## Scripts
\`\`\`yaml
# 声明本动作使用的脚本(可选)
# - script-id # 对应 scripts/script-id.py 或 .sh
\`\`\`
## Execution
\`\`\`javascript
async function execute(state) {
{{execution_code}}
// 调用脚本示例
// const result = await ExecuteScript('script-id', { input: state.context.data });
// if (!result.success) throw new Error(result.stderr);
}
\`\`\`

View File

@@ -0,0 +1,277 @@
# Bash Script Template
Bash 脚本模板,用于生成技能中的确定性脚本。
## 模板代码
```bash
#!/bin/bash
# {{script_description}}
set -euo pipefail
# ============================================================
# 参数解析
# ============================================================
INPUT_PATH=""
OUTPUT_DIR="" # 由调用方指定,不设默认值
show_help() {
echo "用法: $0 --input-path <path> --output-dir <dir>"
echo ""
echo "参数:"
echo " --input-path 输入文件路径 (必需)"
echo " --output-dir 输出目录 (必需,由调用方指定)"
echo " --help 显示帮助信息"
}
while [[ "$#" -gt 0 ]]; do
case $1 in
--input-path)
INPUT_PATH="$2"
shift
;;
--output-dir)
OUTPUT_DIR="$2"
shift
;;
--help)
show_help
exit 0
;;
*)
echo "错误: 未知参数 $1" >&2
show_help >&2
exit 1
;;
esac
shift
done
# ============================================================
# 参数验证
# ============================================================
if [[ -z "$INPUT_PATH" ]]; then
echo "错误: --input-path 是必需参数" >&2
exit 1
fi
if [[ -z "$OUTPUT_DIR" ]]; then
echo "错误: --output-dir 是必需参数" >&2
exit 1
fi
if [[ ! -f "$INPUT_PATH" ]]; then
echo "错误: 输入文件不存在: $INPUT_PATH" >&2
exit 1
fi
# 检查 jq 是否可用(用于 JSON 输出)
if ! command -v jq &> /dev/null; then
echo "错误: 需要安装 jq" >&2
exit 1
fi
mkdir -p "$OUTPUT_DIR"
# ============================================================
# 核心逻辑
# ============================================================
OUTPUT_FILE="$OUTPUT_DIR/result.txt"
ITEMS_COUNT=0
# TODO: 实现处理逻辑
# 示例:处理输入文件
while IFS= read -r line; do
echo "$line" >> "$OUTPUT_FILE"
((ITEMS_COUNT++))
done < "$INPUT_PATH"
# ============================================================
# 输出 JSON 结果(使用 jq 构建,避免特殊字符问题)
# ============================================================
jq -n \
--arg output_file "$OUTPUT_FILE" \
--argjson items_processed "$ITEMS_COUNT" \
'{output_file: $output_file, items_processed: $items_processed, status: "success"}'
```
## 变量说明
| 变量 | 说明 |
|------|------|
| `{{script_description}}` | 脚本功能描述 |
## 使用规范
### 脚本头部
```bash
#!/bin/bash
set -euo pipefail # 严格模式:出错退出、未定义变量报错、管道错误传递
```
### 参数解析模式
```bash
while [[ "$#" -gt 0 ]]; do
case $1 in
--param-name)
PARAM_VAR="$2"
shift
;;
--flag)
FLAG_VAR=true
;;
*)
echo "Unknown: $1" >&2
exit 1
;;
esac
shift
done
```
### 输出格式
- 最后一行打印单行 JSON
- **强烈推荐使用 `jq`**:自动处理转义和类型
```bash
# 推荐:使用 jq 构建(安全、可靠)
jq -n \
--arg file "$FILE" \
--argjson count "$COUNT" \
'{output_file: $file, items_processed: $count}'
# 备选:简单场景手动拼接(注意特殊字符转义)
echo "{\"file\": \"$FILE\", \"count\": $COUNT}"
```
**jq 参数类型**
- `--arg name value`:字符串类型
- `--argjson name value`:数字/布尔/null 类型
### 错误处理
```bash
# 验证错误
if [[ -z "$PARAM" ]]; then
echo "错误: 参数不能为空" >&2
exit 1
fi
# 命令错误
if ! command -v jq &> /dev/null; then
echo "错误: 需要安装 jq" >&2
exit 1
fi
# 运行时错误
if ! some_command; then
echo "错误: 命令执行失败" >&2
exit 1
fi
```
## 常用模式
### 文件遍历
```bash
for file in "$INPUT_DIR"/*.json; do
[[ -f "$file" ]] || continue
echo "处理: $file"
# 处理逻辑...
done
```
### 临时文件
```bash
TEMP_FILE=$(mktemp)
trap "rm -f $TEMP_FILE" EXIT
echo "data" > "$TEMP_FILE"
```
### 调用其他工具
```bash
# 检查工具存在
require_command() {
if ! command -v "$1" &> /dev/null; then
echo "错误: 需要 $1" >&2
exit 1
fi
}
require_command jq
require_command curl
```
### JSON 处理(使用 jq
```bash
# 读取 JSON 字段
VALUE=$(jq -r '.field' "$INPUT_PATH")
# 修改 JSON
jq '.field = "new_value"' "$INPUT_PATH" > "$OUTPUT_FILE"
# 合并 JSON 文件
jq -s 'add' file1.json file2.json > merged.json
```
## 生成函数
```javascript
function generateBashScript(scriptConfig) {
return `#!/bin/bash
# ${scriptConfig.description}
set -euo pipefail
# 参数定义
${scriptConfig.inputs.map(i =>
`${i.name.toUpperCase().replace(/-/g, '_')}="${i.default || ''}"`
).join('\n')}
# 参数解析
while [[ "$#" -gt 0 ]]; do
case $1 in
${scriptConfig.inputs.map(i =>
` --${i.name})
${i.name.toUpperCase().replace(/-/g, '_')}="$2"
shift
;;`
).join('\n')}
*)
echo "未知参数: $1" >&2
exit 1
;;
esac
shift
done
# 参数验证
${scriptConfig.inputs.filter(i => i.required).map(i =>
`if [[ -z "$${i.name.toUpperCase().replace(/-/g, '_')}" ]]; then
echo "错误: --${i.name} 是必需参数" >&2
exit 1
fi`
).join('\n\n')}
# TODO: 实现处理逻辑
# 输出结果
echo "{${scriptConfig.outputs.map(o =>
`\\"${o.name}\\": \\"\\$${o.name.toUpperCase().replace(/-/g, '_')}\\"`
).join(', ')}}"
`;
}
```

View File

@@ -0,0 +1,198 @@
# Python Script Template
Python 脚本模板,用于生成技能中的确定性脚本。
## 模板代码
```python
#!/usr/bin/env python3
"""
{{script_description}}
"""
import argparse
import json
import sys
from pathlib import Path
def main():
# 1. 定义参数
parser = argparse.ArgumentParser(description='{{script_description}}')
parser.add_argument('--input-path', type=str, required=True,
help='输入文件路径')
parser.add_argument('--output-dir', type=str, required=True,
help='输出目录(由调用方指定)')
# 添加更多参数...
args = parser.parse_args()
# 2. 验证输入
input_path = Path(args.input_path)
if not input_path.exists():
print(f"错误: 输入文件不存在: {input_path}", file=sys.stderr)
sys.exit(1)
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# 3. 执行核心逻辑
try:
result = process(input_path, output_dir)
except Exception as e:
print(f"错误: {e}", file=sys.stderr)
sys.exit(1)
# 4. 输出 JSON 结果
print(json.dumps(result))
def process(input_path: Path, output_dir: Path) -> dict:
"""
核心处理逻辑
Args:
input_path: 输入文件路径
output_dir: 输出目录
Returns:
dict: 包含输出结果的字典
"""
# TODO: 实现处理逻辑
output_file = output_dir / 'result.json'
# 示例:读取并处理数据
with open(input_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 处理数据...
processed_count = len(data) if isinstance(data, list) else 1
# 写入输出
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return {
'output_file': str(output_file),
'items_processed': processed_count,
'status': 'success'
}
if __name__ == '__main__':
main()
```
## 变量说明
| 变量 | 说明 |
|------|------|
| `{{script_description}}` | 脚本功能描述 |
## 使用规范
### 输入参数
- 使用 `argparse` 定义参数
- 参数名使用 kebab-case`--input-path`
- 必需参数设置 `required=True`
- 可选参数提供 `default`
### 输出格式
- 最后一行打印单行 JSON
- 包含所有输出文件路径和关键数据
- 错误信息输出到 stderr
### 错误处理
```python
# 验证错误 - 直接退出
if not valid:
print("错误信息", file=sys.stderr)
sys.exit(1)
# 运行时错误 - 捕获并退出
try:
result = process()
except Exception as e:
print(f"错误: {e}", file=sys.stderr)
sys.exit(1)
```
## 常用模式
### 文件处理
```python
def process_files(input_dir: Path, pattern: str = '*.json') -> list:
results = []
for file in input_dir.glob(pattern):
with open(file, 'r') as f:
data = json.load(f)
results.append({'file': str(file), 'data': data})
return results
```
### 数据转换
```python
def transform_data(data: dict) -> dict:
return {
'id': data.get('id'),
'name': data.get('name', '').strip(),
'timestamp': datetime.now().isoformat()
}
```
### 调用外部命令
```python
import subprocess
def run_command(cmd: list) -> str:
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(result.stderr)
return result.stdout
```
## 生成函数
```javascript
function generatePythonScript(scriptConfig) {
return `#!/usr/bin/env python3
"""
${scriptConfig.description}
"""
import argparse
import json
import sys
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description='${scriptConfig.description}')
${scriptConfig.inputs.map(i =>
` parser.add_argument('--${i.name}', type=${i.type || 'str'}, ${i.required ? 'required=True' : `default='${i.default}'`},
help='${i.description}')`
).join('\n')}
args = parser.parse_args()
# TODO: 实现处理逻辑
result = {
${scriptConfig.outputs.map(o =>
` '${o.name}': None # ${o.description}`
).join(',\n')}
}
print(json.dumps(result))
if __name__ == '__main__':
main()
`;
}
```

View File

@@ -18,6 +18,13 @@
- 依赖: `{{input_dependency}}`
- 配置: `{workDir}/skill-config.json`
## Scripts
\`\`\`yaml
# 声明本阶段使用的脚本(可选)
# - script-id # 对应 scripts/script-id.py 或 .sh
\`\`\`
## Execution Steps
### Step 1: {{step_1_name}}
@@ -32,10 +39,13 @@
{{step_2_code}}
\`\`\`
### Step 3: {{step_3_name}}
### Step 3: 执行脚本(可选)
\`\`\`javascript
{{step_3_code}}
// 调用脚本示例
// const result = await ExecuteScript('script-id', { input_path: `${workDir}/data.json` });
// if (!result.success) throw new Error(result.stderr);
// console.log(result.outputs.output_file);
\`\`\`
## Output
@@ -68,6 +78,44 @@
| `{{quality_checklist}}` | 质量检查项 |
| `{{next_phase_link}}` | 下一阶段链接 |
## 脚本调用说明
### 目录约定
```
scripts/
├── process-data.py # id: process-data, runtime: python
├── validate.sh # id: validate, runtime: bash
└── transform.js # id: transform, runtime: node
```
- **命名即 ID**:文件名(不含扩展名)= 脚本 ID
- **扩展名即运行时**`.py` → python, `.sh` → bash, `.js` → node
### 调用语法
```javascript
// 一行调用
const result = await ExecuteScript('script-id', { key: value });
// 检查结果
if (!result.success) throw new Error(result.stderr);
// 获取输出
const { output_file } = result.outputs;
```
### 返回格式
```typescript
interface ScriptResult {
success: boolean; // exit code === 0
stdout: string; // 标准输出
stderr: string; // 标准错误
outputs: object; // 从 stdout 解析的 JSON 输出
}
```
## Phase 类型模板
### 1. 收集型 Phase (Collection)