From cefb934a2c1bcacd03d9d98f5916d02b5fd59eb8 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sun, 11 Jan 2026 23:14:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BA=20skill-generator=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=84=9A=E6=9C=AC=E6=89=A7=E8=A1=8C=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 默认创建 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 --- .../phases/02-structure-generation.md | 15 +- .../specs/scripting-integration.md | 265 +++++++++++++++++ .../templates/autonomous-action.md | 11 + .../skill-generator/templates/script-bash.md | 277 ++++++++++++++++++ .../templates/script-python.md | 198 +++++++++++++ .../templates/sequential-phase.md | 52 +++- 6 files changed, 808 insertions(+), 10 deletions(-) create mode 100644 .claude/skills/skill-generator/specs/scripting-integration.md create mode 100644 .claude/skills/skill-generator/templates/script-bash.md create mode 100644 .claude/skills/skill-generator/templates/script-python.md diff --git a/.claude/skills/skill-generator/phases/02-structure-generation.md b/.claude/skills/skill-generator/phases/02-structure-generation.md index f846fdfa..dd7058dd 100644 --- a/.claude/skills/skill-generator/phases/02-structure-generation.md +++ b/.claude/skills/skill-generator/phases/02-structure-generation.md @@ -34,10 +34,8 @@ if (config.execution_mode === 'autonomous' || config.execution_mode === 'hybrid' Bash(`mkdir -p "${skillDir}/phases/actions"`); } -// 可选: scripts 目录 -if (config.needs_scripts) { - Bash(`mkdir -p "${skillDir}/scripts"`); -} +// scripts 目录(默认创建,用于存放确定性脚本) +Bash(`mkdir -p "${skillDir}/scripts"`); ``` ### Step 3: 生成 SKILL.md @@ -197,11 +195,12 @@ function generateReferenceTable(config) { ## Output - **Directory**: `.claude/skills/{skill-name}/` -- **Files**: +- **Files**: - `SKILL.md` (入口文件) - - `phases/` (空目录) - - `specs/` (空目录) - - `templates/` (空目录) + - `phases/` (执行阶段目录) + - `specs/` (规范文档目录) + - `templates/` (模板目录) + - `scripts/` (脚本目录,存放 Python/Bash 确定性脚本) ## Next Phase diff --git a/.claude/skills/skill-generator/specs/scripting-integration.md b/.claude/skills/skill-generator/specs/scripting-integration.md new file mode 100644 index 00000000..e1642801 --- /dev/null +++ b/.claude/skills/skill-generator/specs/scripting-integration.md @@ -0,0 +1,265 @@ +# Scripting Integration Spec + +技能脚本集成规范,定义如何在技能中使用外部脚本执行确定性任务。 + +## 核心原则 + +1. **约定优于配置**:命名即 ID,扩展名即运行时 +2. **极简调用**:一行完成脚本调用 +3. **标准输入输出**:命令行参数输入,JSON 标准输出 + +## 目录结构 + +``` +.claude/skills// +├── 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.md 所在位置) +├── SKILL.md +├── scripts/ # 脚本目录 +│ └── process-data.py # 相对路径: scripts/process-data.py +└── phases/ +``` + +`ExecuteScript` 自动从技能根目录查找脚本: +```javascript +// 实际执行: python .claude/skills//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. **路径参数化**:输出路径由调用方指定,不硬编码 diff --git a/.claude/skills/skill-generator/templates/autonomous-action.md b/.claude/skills/skill-generator/templates/autonomous-action.md index 1c85ef84..83cae3c7 100644 --- a/.claude/skills/skill-generator/templates/autonomous-action.md +++ b/.claude/skills/skill-generator/templates/autonomous-action.md @@ -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); } \`\`\` diff --git a/.claude/skills/skill-generator/templates/script-bash.md b/.claude/skills/skill-generator/templates/script-bash.md new file mode 100644 index 00000000..9b90fa65 --- /dev/null +++ b/.claude/skills/skill-generator/templates/script-bash.md @@ -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 --output-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(', ')}}" +`; +} +``` diff --git a/.claude/skills/skill-generator/templates/script-python.md b/.claude/skills/skill-generator/templates/script-python.md new file mode 100644 index 00000000..87ebb934 --- /dev/null +++ b/.claude/skills/skill-generator/templates/script-python.md @@ -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() +`; +} +``` diff --git a/.claude/skills/skill-generator/templates/sequential-phase.md b/.claude/skills/skill-generator/templates/sequential-phase.md index 3a19649f..f8f6b284 100644 --- a/.claude/skills/skill-generator/templates/sequential-phase.md +++ b/.claude/skills/skill-generator/templates/sequential-phase.md @@ -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)