Skip to content

Commit cefb934

Browse files
catlog22claude
andcommitted
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>
1 parent 37614a3 commit cefb934

6 files changed

Lines changed: 808 additions & 10 deletions

File tree

.claude/skills/skill-generator/phases/02-structure-generation.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,8 @@ if (config.execution_mode === 'autonomous' || config.execution_mode === 'hybrid'
3434
Bash(`mkdir -p "${skillDir}/phases/actions"`);
3535
}
3636

37-
// 可选: scripts 目录
38-
if (config.needs_scripts) {
39-
Bash(`mkdir -p "${skillDir}/scripts"`);
40-
}
37+
// scripts 目录(默认创建,用于存放确定性脚本)
38+
Bash(`mkdir -p "${skillDir}/scripts"`);
4139
```
4240

4341
### Step 3: 生成 SKILL.md
@@ -197,11 +195,12 @@ function generateReferenceTable(config) {
197195
## Output
198196

199197
- **Directory**: `.claude/skills/{skill-name}/`
200-
- **Files**:
198+
- **Files**:
201199
- `SKILL.md` (入口文件)
202-
- `phases/` (空目录)
203-
- `specs/` (空目录)
204-
- `templates/` (空目录)
200+
- `phases/` (执行阶段目录)
201+
- `specs/` (规范文档目录)
202+
- `templates/` (模板目录)
203+
- `scripts/` (脚本目录,存放 Python/Bash 确定性脚本)
205204

206205
## Next Phase
207206

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
# Scripting Integration Spec
2+
3+
技能脚本集成规范,定义如何在技能中使用外部脚本执行确定性任务。
4+
5+
## 核心原则
6+
7+
1. **约定优于配置**:命名即 ID,扩展名即运行时
8+
2. **极简调用**:一行完成脚本调用
9+
3. **标准输入输出**:命令行参数输入,JSON 标准输出
10+
11+
## 目录结构
12+
13+
```
14+
.claude/skills/<skill-name>/
15+
├── scripts/ # 脚本专用目录
16+
│ ├── process-data.py # id: process-data
17+
│ ├── validate-output.sh # id: validate-output
18+
│ └── transform-json.js # id: transform-json
19+
├── phases/
20+
└── specs/
21+
```
22+
23+
## 命名约定
24+
25+
| 扩展名 | 运行时 | 执行命令 |
26+
|--------|--------|----------|
27+
| `.py` | python | `python scripts/{id}.py` |
28+
| `.sh` | bash | `bash scripts/{id}.sh` |
29+
| `.js` | node | `node scripts/{id}.js` |
30+
31+
## 声明格式
32+
33+
在 Phase 或 Action 文件的 `## Scripts` 部分声明:
34+
35+
```yaml
36+
## Scripts
37+
38+
- process-data
39+
- validate-output
40+
```
41+
42+
## 调用语法
43+
44+
### 基础调用
45+
46+
```javascript
47+
const result = await ExecuteScript('script-id', { key: value });
48+
```
49+
50+
### 参数命名转换
51+
52+
调用时 JS 对象中的键会**自动转换**为 `kebab-case` 命令行参数:
53+
54+
| JS 键名 | 转换后参数 |
55+
|---------|-----------|
56+
| `input_path` | `--input-path` |
57+
| `output_dir` | `--output-dir` |
58+
| `max_count` | `--max-count` |
59+
60+
脚本中使用 `--input-path` 接收,调用时使用 `input_path` 传入。
61+
62+
### 完整调用(含错误处理)
63+
64+
```javascript
65+
const result = await ExecuteScript('process-data', {
66+
input_path: `${workDir}/data.json`,
67+
threshold: 0.9
68+
});
69+
70+
if (!result.success) {
71+
throw new Error(`脚本执行失败: ${result.stderr}`);
72+
}
73+
74+
const { output_file, count } = result.outputs;
75+
```
76+
77+
## 返回格式
78+
79+
```typescript
80+
interface ScriptResult {
81+
success: boolean; // exit code === 0
82+
stdout: string; // 完整标准输出
83+
stderr: string; // 完整标准错误
84+
outputs: { // 从 stdout 最后一行解析的 JSON
85+
[key: string]: any;
86+
};
87+
}
88+
```
89+
90+
## 脚本编写规范
91+
92+
### 输入:命令行参数
93+
94+
```bash
95+
# Python: argparse
96+
--input-path /path/to/file --threshold 0.9
97+
98+
# Bash: 手动解析
99+
--input-path /path/to/file
100+
```
101+
102+
### 输出:标准输出 JSON
103+
104+
脚本最后一行必须打印单行 JSON:
105+
106+
```json
107+
{"output_file": "/tmp/result.json", "count": 42}
108+
```
109+
110+
### Python 模板
111+
112+
```python
113+
import argparse
114+
import json
115+
116+
def main():
117+
parser = argparse.ArgumentParser()
118+
parser.add_argument('--input-path', required=True)
119+
parser.add_argument('--threshold', type=float, default=0.9)
120+
args = parser.parse_args()
121+
122+
# 执行逻辑...
123+
result_path = "/tmp/result.json"
124+
125+
# 输出 JSON
126+
print(json.dumps({
127+
"output_file": result_path,
128+
"items_processed": 100
129+
}))
130+
131+
if __name__ == '__main__':
132+
main()
133+
```
134+
135+
### Bash 模板
136+
137+
```bash
138+
#!/bin/bash
139+
140+
# 解析参数
141+
while [[ "$#" -gt 0 ]]; do
142+
case $1 in
143+
--input-path) INPUT_PATH="$2"; shift ;;
144+
*) echo "Unknown: $1" >&2; exit 1 ;;
145+
esac
146+
shift
147+
done
148+
149+
# 执行逻辑...
150+
LOG_FILE="/tmp/process.log"
151+
echo "Processing $INPUT_PATH" > "$LOG_FILE"
152+
153+
# 输出 JSON
154+
echo "{\"log_file\": \"$LOG_FILE\", \"status\": \"done\"}"
155+
```
156+
157+
## ExecuteScript 实现
158+
159+
```javascript
160+
async function ExecuteScript(scriptId, inputs = {}) {
161+
const skillDir = GetSkillDir();
162+
163+
// 查找脚本文件
164+
const extensions = ['.py', '.sh', '.js'];
165+
let scriptPath, runtime;
166+
167+
for (const ext of extensions) {
168+
const path = `${skillDir}/scripts/${scriptId}${ext}`;
169+
if (FileExists(path)) {
170+
scriptPath = path;
171+
runtime = ext === '.py' ? 'python' : ext === '.sh' ? 'bash' : 'node';
172+
break;
173+
}
174+
}
175+
176+
if (!scriptPath) {
177+
throw new Error(`Script not found: ${scriptId}`);
178+
}
179+
180+
// 构建命令行参数
181+
const args = Object.entries(inputs)
182+
.map(([k, v]) => `--${k.replace(/_/g, '-')} "${v}"`)
183+
.join(' ');
184+
185+
// 执行脚本
186+
const cmd = `${runtime} "${scriptPath}" ${args}`;
187+
const { stdout, stderr, exitCode } = await Bash(cmd);
188+
189+
// 解析输出
190+
let outputs = {};
191+
try {
192+
const lastLine = stdout.trim().split('\n').pop();
193+
outputs = JSON.parse(lastLine);
194+
} catch (e) {
195+
// 无法解析 JSON,保持空对象
196+
}
197+
198+
return {
199+
success: exitCode === 0,
200+
stdout,
201+
stderr,
202+
outputs
203+
};
204+
}
205+
```
206+
207+
## 使用场景
208+
209+
### 适合脚本化的任务
210+
211+
- 数据处理和转换
212+
- 文件格式转换
213+
- 批量文件操作
214+
- 复杂计算逻辑
215+
- 调用外部工具/库
216+
217+
### 不适合脚本化的任务
218+
219+
- 需要用户交互的任务
220+
- 需要访问 Claude 工具的任务
221+
- 简单的文件读写
222+
- 需要动态决策的任务
223+
224+
## 路径约定
225+
226+
### 脚本路径
227+
228+
脚本路径相对于 `SKILL.md` 所在目录(技能根目录):
229+
230+
```
231+
.claude/skills/<skill-name>/ # 技能根目录(SKILL.md 所在位置)
232+
├── SKILL.md
233+
├── scripts/ # 脚本目录
234+
│ └── process-data.py # 相对路径: scripts/process-data.py
235+
└── phases/
236+
```
237+
238+
`ExecuteScript` 自动从技能根目录查找脚本:
239+
```javascript
240+
// 实际执行: python .claude/skills/<skill-name>/scripts/process-data.py
241+
await ExecuteScript('process-data', { ... });
242+
```
243+
244+
### 输出目录
245+
246+
**推荐**:由调用方传递输出目录,而非脚本默认 `/tmp`
247+
248+
```javascript
249+
// 调用时指定输出目录(在工作流工作目录内)
250+
const result = await ExecuteScript('process-data', {
251+
input_path: `${workDir}/data.json`,
252+
output_dir: `${workDir}/output` // 明确指定输出位置
253+
});
254+
```
255+
256+
脚本应接受 `--output-dir` 参数,而非硬编码输出路径。
257+
258+
## 最佳实践
259+
260+
1. **单一职责**:每个脚本只做一件事
261+
2. **无副作用**:脚本不应修改全局状态
262+
3. **幂等性**:相同输入产生相同输出
263+
4. **错误明确**:错误信息写入 stderr,正常输出写入 stdout
264+
5. **快速失败**:参数验证失败立即退出
265+
6. **路径参数化**:输出路径由调用方指定,不硬编码

.claude/skills/skill-generator/templates/autonomous-action.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,22 @@
1717

1818
{{preconditions_list}}
1919

20+
## Scripts
21+
22+
\`\`\`yaml
23+
# 声明本动作使用的脚本(可选)
24+
# - script-id # 对应 scripts/script-id.py 或 .sh
25+
\`\`\`
26+
2027
## Execution
2128

2229
\`\`\`javascript
2330
async function execute(state) {
2431
{{execution_code}}
32+
33+
// 调用脚本示例
34+
// const result = await ExecuteScript('script-id', { input: state.context.data });
35+
// if (!result.success) throw new Error(result.stderr);
2536
}
2637
\`\`\`
2738

0 commit comments

Comments
 (0)