foliko 2.0.6 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +56 -56
- package/.lintstagedrc +7 -7
- package/.prettierignore +29 -29
- package/.prettierrc +11 -11
- package/Dockerfile +63 -63
- package/install.ps1 +129 -129
- package/install.sh +121 -121
- package/package.json +1 -1
- package/plugins/core/scheduler/index.js +1 -0
- package/plugins/core/workflow/context.js +941 -0
- package/plugins/core/workflow/engine.js +66 -0
- package/plugins/core/workflow/examples/01-basic.js +42 -0
- package/plugins/core/workflow/examples/01-basic.json +30 -0
- package/plugins/core/workflow/examples/02-choice.js +75 -0
- package/plugins/core/workflow/examples/02-choice.json +59 -0
- package/plugins/core/workflow/examples/03-chain-style.js +114 -0
- package/plugins/core/workflow/examples/03-each.json +41 -0
- package/plugins/core/workflow/examples/04-parallel.js +52 -0
- package/plugins/core/workflow/examples/04-parallel.json +51 -0
- package/plugins/core/workflow/examples/05-chain-style.json +68 -0
- package/plugins/core/workflow/examples/05-each.js +65 -0
- package/plugins/core/workflow/examples/06-script.js +82 -0
- package/plugins/core/workflow/examples/07-module-export.js +43 -0
- package/plugins/core/workflow/examples/08-default-export.js +29 -0
- package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
- package/plugins/core/workflow/examples/10-logger.js +34 -0
- package/plugins/core/workflow/examples/11-storage.js +68 -0
- package/plugins/core/workflow/examples/simple.js +77 -0
- package/plugins/core/workflow/examples/simple.json +75 -0
- package/plugins/core/workflow/index.js +122 -56
- package/plugins/core/workflow/js-runner.js +318 -0
- package/plugins/core/workflow/json-runner.js +323 -0
- package/plugins/core/workflow/stages/action.js +211 -0
- package/plugins/core/workflow/stages/choice.js +74 -0
- package/plugins/core/workflow/stages/delay.js +73 -0
- package/plugins/core/workflow/stages/each.js +123 -0
- package/plugins/core/workflow/stages/parallel.js +69 -0
- package/plugins/core/workflow/stages/try.js +142 -0
- package/plugins/executors/data-splitter/index.js +3 -2
- package/sandbox/check-context.js +5 -0
- package/sandbox/test-context.js +27 -0
- package/sandbox/test-fixes.js +40 -0
- package/sandbox/test-hello-js.js +46 -0
- package/skills/find-skills/SKILL.md +133 -133
- package/skills/workflows/SKILL.md +715 -613
- package/src/agent/chat.js +8 -1
- package/src/agent/main.js +5 -1
- package/src/cli/ui/chat-ui.js +4 -3
- package/src/common/json-safe.js +20 -0
- package/src/framework/framework.js +55 -1
- package/src/plugin/manager.js +95 -1
- package/src/utils/sandbox.js +1 -1
- package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
- package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -331
|
@@ -8,7 +8,10 @@ allowed-tools: execute_workflow,workflow_reload
|
|
|
8
8
|
|
|
9
9
|
## 概述
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
工作流引擎用于处理多步骤任务,支持两种格式:
|
|
12
|
+
|
|
13
|
+
- **JSON 格式**:声明式配置,适合简单工作流
|
|
14
|
+
- **JS 格式**:编程式配置,适合复杂逻辑
|
|
12
15
|
|
|
13
16
|
## 工作流存放位置
|
|
14
17
|
|
|
@@ -16,8 +19,8 @@ allowed-tools: execute_workflow,workflow_reload
|
|
|
16
19
|
项目目录/
|
|
17
20
|
└── .foliko/
|
|
18
21
|
└── workflows/ # 工作流定义目录
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
├── my-workflow.json # JSON 格式
|
|
23
|
+
├── my-workflow.flow.js # JS 格式
|
|
21
24
|
```
|
|
22
25
|
|
|
23
26
|
## 自动加载与注册
|
|
@@ -27,8 +30,6 @@ allowed-tools: execute_workflow,workflow_reload
|
|
|
27
30
|
1. 加载所有 `.json` 和 `.js` 文件
|
|
28
31
|
2. 将工作流注册为 `workflow_<文件名>` 工具
|
|
29
32
|
|
|
30
|
-
例如 `my-workflow.json` 会注册为 `workflow_my_workflow` 工具,可直接调用。
|
|
31
|
-
|
|
32
33
|
## 重载工作流
|
|
33
34
|
|
|
34
35
|
创建或修改工作流后,调用 `workflow_reload` 重载:
|
|
@@ -40,822 +41,923 @@ allowed-tools: execute_workflow,workflow_reload
|
|
|
40
41
|
}
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
---
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
# JSON 格式
|
|
47
|
+
|
|
48
|
+
## 基本结构
|
|
46
49
|
|
|
47
50
|
```json
|
|
48
51
|
{
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
"flow": "workflow-name",
|
|
53
|
+
"version": "2.0",
|
|
54
|
+
"stages": [
|
|
55
|
+
{ "name": "stageName", ... }
|
|
56
|
+
]
|
|
54
57
|
}
|
|
55
58
|
```
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
## 阶段类型
|
|
61
|
+
|
|
62
|
+
### action - 执行工具或脚本
|
|
58
63
|
|
|
64
|
+
**工具调用:**
|
|
59
65
|
```json
|
|
60
66
|
{
|
|
61
|
-
"
|
|
67
|
+
"name": "fetchUser",
|
|
68
|
+
"tool": "fetch",
|
|
62
69
|
"args": {
|
|
63
|
-
"
|
|
70
|
+
"url": "https://api.example.com/users/{{input.userId}}"
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
```
|
|
67
74
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
**脚本执行:**
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"name": "validate",
|
|
79
|
+
"script": "return input.value > 0;"
|
|
80
|
+
}
|
|
81
|
+
```
|
|
73
82
|
|
|
74
|
-
|
|
83
|
+
> **重要**:JSON 脚本必须使用 `return` 返回值,否则 stage output 为空!
|
|
75
84
|
|
|
76
|
-
###
|
|
85
|
+
### JSON 中的 $.next() 跳转
|
|
77
86
|
|
|
78
|
-
|
|
79
|
-
| ------------ | --------------- | ------------------------------------------- |
|
|
80
|
-
| `tool` | 工具调用 | 有 `tool` 字段 |
|
|
81
|
-
| `script` | JavaScript 脚本 | 有 `code` 或 `script` 字段 |
|
|
82
|
-
| `condition` | 条件分支 | 有 `branches` 字段 |
|
|
83
|
-
| `switch` | 开关分支 | 有 `value` + `branches` |
|
|
84
|
-
| `try` | 异常捕获 | 有 `try` 或 `catch` 字段 |
|
|
85
|
-
| `parallel` | 并行执行 | `"parallel": true` 或 `"mode": "parallel"` |
|
|
86
|
-
| `sequential` | 顺序执行 | 有 `steps` 字段 |
|
|
87
|
-
| `loop` | 循环执行 | 有 `steps` + `loopVariable`/`maxIterations` |
|
|
88
|
-
| `delay` | 延时等待 | 有 `delayMs` 字段 |
|
|
89
|
-
| `workflow` | 嵌套工作流 | 有 `workflow` 字段 |
|
|
90
|
-
| `message` | 发送消息 | 有 `message` 字段 |
|
|
91
|
-
| `think` | 主动思考 | 有 `topic` 字段 |
|
|
87
|
+
JSON 工作流的脚本中也支持 `$.next()` 跳转,跳转后如果当前 stage 有返回值,会保留在 output 中:
|
|
92
88
|
|
|
93
|
-
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"name": "start",
|
|
92
|
+
"script": "$.data.value = 42; return { initialized: true };"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "process",
|
|
96
|
+
"script": "$.next('skip'); return { shouldSkip: true };"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "middle",
|
|
100
|
+
"script": "console.log('不会被执行');"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "skip",
|
|
104
|
+
"script": "return { reached: true };"
|
|
105
|
+
}
|
|
106
|
+
```
|
|
94
107
|
|
|
95
|
-
|
|
108
|
+
### choice - 条件分支
|
|
96
109
|
|
|
97
110
|
```json
|
|
98
111
|
{
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
"name": "route",
|
|
113
|
+
"choice": [
|
|
114
|
+
{ "when": "fetchUser.body.status == 'active'", "do": "handle-active" },
|
|
115
|
+
{ "when": "fetchUser.body.status == 'pending'", "do": "handle-pending" }
|
|
116
|
+
],
|
|
117
|
+
"default": "handle-unknown"
|
|
103
118
|
}
|
|
104
119
|
```
|
|
105
120
|
|
|
106
|
-
|
|
121
|
+
### each - 遍历循环
|
|
107
122
|
|
|
108
123
|
```json
|
|
109
|
-
{
|
|
124
|
+
{
|
|
125
|
+
"name": "process-items",
|
|
126
|
+
"each": "{{items}}",
|
|
127
|
+
"as": "item",
|
|
128
|
+
"index": "idx",
|
|
129
|
+
"stages": [
|
|
130
|
+
{ "name": "process", "tool": "process_item", "args": { "item": "{{item}}" } }
|
|
131
|
+
]
|
|
132
|
+
}
|
|
110
133
|
```
|
|
111
134
|
|
|
112
|
-
|
|
135
|
+
### parallel - 并行执行
|
|
113
136
|
|
|
114
137
|
```json
|
|
115
|
-
{
|
|
138
|
+
{
|
|
139
|
+
"name": "fetch-all",
|
|
140
|
+
"parallel": [
|
|
141
|
+
{ "name": "fetchUser", "tool": "fetch", "args": { "url": "..." } },
|
|
142
|
+
{ "name": "fetchConfig", "tool": "fetch", "args": { "url": "..." } }
|
|
143
|
+
]
|
|
144
|
+
}
|
|
116
145
|
```
|
|
117
146
|
|
|
118
|
-
|
|
119
|
-
- `context.variables`: 所有步骤共享的变量对象
|
|
120
|
-
- `context.lastResult`: 上一步的输出结果
|
|
147
|
+
---
|
|
121
148
|
|
|
122
|
-
|
|
149
|
+
# JS 格式
|
|
123
150
|
|
|
124
|
-
|
|
151
|
+
## 基本结构
|
|
125
152
|
|
|
126
|
-
```
|
|
127
|
-
{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"outputVariable": "result"
|
|
136
|
-
}
|
|
153
|
+
```javascript
|
|
154
|
+
FLOW(function($) {
|
|
155
|
+
$.push('stageName', async function($) {
|
|
156
|
+
// 阶段逻辑
|
|
157
|
+
$.next('next-stage');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
$.next('stageName');
|
|
161
|
+
});
|
|
137
162
|
```
|
|
138
163
|
|
|
139
|
-
|
|
164
|
+
## 支持的导出格式
|
|
140
165
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
| `args` | object | 否 | 工具参数 |
|
|
145
|
-
| `outputVariable` | string | 否 | 结果保存到的变量名 |
|
|
166
|
+
```javascript
|
|
167
|
+
// 格式 1: FLOW 包装器(推荐)
|
|
168
|
+
FLOW(function($) { ... });
|
|
146
169
|
|
|
147
|
-
|
|
170
|
+
// 格式 2: module.exports
|
|
171
|
+
module.exports = function($) { ... };
|
|
148
172
|
|
|
149
|
-
|
|
173
|
+
// 格式 3: module.exports.default
|
|
174
|
+
module.exports.default = function($) { ... };
|
|
175
|
+
```
|
|
150
176
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
# 核心 API
|
|
180
|
+
|
|
181
|
+
## 流程控制
|
|
182
|
+
|
|
183
|
+
| 方法 | 说明 |
|
|
184
|
+
|------|------|
|
|
185
|
+
| `$.push(name, fn)` | 注册一个阶段 |
|
|
186
|
+
| `$.next(name, ...args)` | 跳转到下一个阶段,并传递参数 |
|
|
187
|
+
| `$.send(data)` | 传递数据给下一个阶段 |
|
|
188
|
+
| `$.input` | 工作流输入数据(跳转时为 `$.next()` 传递的参数) |
|
|
189
|
+
| `$.data` | 流程级持久数据 |
|
|
190
|
+
| `$.wait(ms)` | 延时 |
|
|
191
|
+
| `$.destroy()` | 结束工作流 |
|
|
192
|
+
|
|
193
|
+
### $.next() 直接传参 + 函数参数接收
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
// 传递参数
|
|
197
|
+
$.push('step1', async function($) {
|
|
198
|
+
const result = await $.tool.execute('fetch', { url: '...' });
|
|
199
|
+
$.next('step2', result); // 传递一个参数
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// 接收参数 - 在函数参数中直接获取
|
|
203
|
+
$.push('step2', function($, data) {
|
|
204
|
+
// data 就是 result
|
|
205
|
+
console.log(data);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// 传递多个参数
|
|
209
|
+
$.push('step1', async function($) {
|
|
210
|
+
const user = await $.tool.execute('fetch', { url: '...' });
|
|
211
|
+
const config = await $.tool.execute('fetch', { url: '...' });
|
|
212
|
+
$.next('step2', user, config);
|
|
213
|
+
});
|
|
154
214
|
|
|
155
|
-
//
|
|
156
|
-
|
|
215
|
+
// 接收多个参数
|
|
216
|
+
$.push('step2', function($, user, config) {
|
|
217
|
+
console.log(user, config);
|
|
218
|
+
});
|
|
157
219
|
```
|
|
158
220
|
|
|
159
|
-
|
|
221
|
+
## $.tool - 工具调用
|
|
160
222
|
|
|
161
|
-
|
|
223
|
+
```javascript
|
|
224
|
+
// 执行工具
|
|
225
|
+
await $.tool.execute('fetch', { url: 'https://api.example.com' });
|
|
162
226
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
"args": {
|
|
169
|
-
"title": "IP 信息",
|
|
170
|
-
"message": "当前公网IP: {{currentIp}}"
|
|
171
|
-
}
|
|
172
|
-
}
|
|
227
|
+
// 列出所有工具
|
|
228
|
+
const tools = $.tool.list();
|
|
229
|
+
|
|
230
|
+
// 获取工具定义
|
|
231
|
+
const tool = $.tool.get('fetch');
|
|
173
232
|
```
|
|
174
233
|
|
|
175
|
-
|
|
234
|
+
## $.extension - 扩展调用
|
|
176
235
|
|
|
177
|
-
|
|
236
|
+
```javascript
|
|
237
|
+
// 调用 Gate 交易 API
|
|
238
|
+
const balance = await $.extension.execute('gate', 'get_balance', {
|
|
239
|
+
currency: 'USDT'
|
|
240
|
+
});
|
|
178
241
|
|
|
179
|
-
|
|
180
|
-
{
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
}
|
|
242
|
+
// 调用 MCP 服务器工具
|
|
243
|
+
const files = await $.extension.execute('mcp:filesystem', 'read_file', {
|
|
244
|
+
path: '/path/to/file'
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## $.skill - Skill 命令
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
// 列出所有 Skills
|
|
252
|
+
const skills = $.skill.list();
|
|
253
|
+
|
|
254
|
+
// 执行 Skill 命令
|
|
255
|
+
const result = await $.skill.execute('ambient', 'goals', {
|
|
256
|
+
command: '-a list'
|
|
257
|
+
});
|
|
197
258
|
```
|
|
198
259
|
|
|
199
|
-
|
|
260
|
+
## $.workflow - 子工作流
|
|
200
261
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
| `{{lastResult}}` | result 的别名 |
|
|
208
|
-
| `{{lastResult.error}}` | 从错误对象提取信息 |
|
|
209
|
-
| `{{variables.xxx}}` | 显式引用 context.variables |
|
|
210
|
-
| `{{input.xxx}}` | 引用工作流输入参数 |
|
|
262
|
+
```javascript
|
|
263
|
+
// 执行子工作流
|
|
264
|
+
const result = await $.workflow.execute('sub-workflow', {
|
|
265
|
+
input: { id: 123 }
|
|
266
|
+
});
|
|
267
|
+
```
|
|
211
268
|
|
|
212
|
-
|
|
269
|
+
## $.agent - Agent 操作
|
|
213
270
|
|
|
214
|
-
|
|
271
|
+
```javascript
|
|
272
|
+
// 与 Agent 对话
|
|
273
|
+
const response = await $.agent.chat({
|
|
274
|
+
message: 'Hello',
|
|
275
|
+
role: 'assistant'
|
|
276
|
+
});
|
|
277
|
+
```
|
|
215
278
|
|
|
216
|
-
|
|
279
|
+
## $.prompt - 临时对话
|
|
217
280
|
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
"plugin": "skill:creator",
|
|
225
|
-
"tool": "creator",
|
|
226
|
-
"args": {
|
|
227
|
-
"aspectRatio": "16:9",
|
|
228
|
-
"title": true,
|
|
229
|
-
"voice": "female-shaonv-jingpin"
|
|
230
|
-
}
|
|
231
|
-
},
|
|
232
|
-
"outputVariable": "videoId"
|
|
233
|
-
}
|
|
281
|
+
```javascript
|
|
282
|
+
// 发送 prompt
|
|
283
|
+
const response = await $.prompt.send({
|
|
284
|
+
message: 'What is 2+2?',
|
|
285
|
+
role: 'assistant'
|
|
286
|
+
});
|
|
234
287
|
```
|
|
235
288
|
|
|
236
|
-
|
|
289
|
+
## $.logger - 日志记录
|
|
237
290
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
291
|
+
```javascript
|
|
292
|
+
// 记录不同级别的日志
|
|
293
|
+
$.logger.debug('Debug message', { data: value });
|
|
294
|
+
$.logger.info('Info message', { status: 'ok' });
|
|
295
|
+
$.logger.warn('Warning message', { code: 123 });
|
|
296
|
+
$.logger.error('Error message', { error: err.message });
|
|
297
|
+
$.logger.trace('Trace message');
|
|
298
|
+
$.logger.fatal('Fatal message');
|
|
243
299
|
|
|
244
|
-
|
|
300
|
+
// 使用自定义级别
|
|
301
|
+
$.logger.log('info', 'Custom level message');
|
|
302
|
+
|
|
303
|
+
// 创建子日志器
|
|
304
|
+
const childLogger = $.logger.child('my-module');
|
|
305
|
+
childLogger.info('来自子日志器的消息');
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## $.storage - 键值存储
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
// 存储数据
|
|
312
|
+
await $.storage.set('key', { name: 'value' });
|
|
313
|
+
await $.storage.set('user', userData, 'customNamespace');
|
|
314
|
+
|
|
315
|
+
// 获取数据
|
|
316
|
+
const result = await $.storage.get('key');
|
|
317
|
+
const value = result?.data?.value;
|
|
318
|
+
|
|
319
|
+
// 删除数据
|
|
320
|
+
await $.storage.delete('key');
|
|
321
|
+
|
|
322
|
+
// 列出所有键
|
|
323
|
+
const keys = await $.storage.list();
|
|
324
|
+
const keysInNs = await $.storage.list('customNamespace');
|
|
325
|
+
|
|
326
|
+
// 清空命名空间
|
|
327
|
+
await $.storage.clear('customNamespace');
|
|
328
|
+
|
|
329
|
+
// 监听键变化
|
|
330
|
+
await $.storage.watch('key');
|
|
331
|
+
|
|
332
|
+
// 获取存储统计
|
|
333
|
+
await $.storage.stats();
|
|
334
|
+
|
|
335
|
+
// 触发 compaction
|
|
336
|
+
await $.storage.compact();
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
# JSON 中调用扩展/Skill
|
|
342
|
+
|
|
343
|
+
## 调用 extension
|
|
245
344
|
|
|
246
345
|
```json
|
|
247
346
|
{
|
|
248
|
-
"
|
|
249
|
-
"
|
|
250
|
-
"tool": "
|
|
347
|
+
"name": "get-balance",
|
|
348
|
+
"extension": "gate",
|
|
349
|
+
"tool": "get_balance",
|
|
251
350
|
"args": {
|
|
252
|
-
"
|
|
253
|
-
"tool": "creator",
|
|
254
|
-
"args": {
|
|
255
|
-
"aspectRatio": "16:9",
|
|
256
|
-
"title": true,
|
|
257
|
-
"voice": "female-shaonv-jingpin",
|
|
258
|
-
"audio": true
|
|
259
|
-
}
|
|
351
|
+
"currency": "USDT"
|
|
260
352
|
}
|
|
261
353
|
}
|
|
262
354
|
```
|
|
263
355
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
支持两种循环模式:
|
|
267
|
-
|
|
268
|
-
**模式一:固定次数循环**
|
|
356
|
+
## 调用 skill
|
|
269
357
|
|
|
270
358
|
```json
|
|
271
359
|
{
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"
|
|
276
|
-
|
|
360
|
+
"name": "run-goals",
|
|
361
|
+
"skill": "ambient",
|
|
362
|
+
"command": "goals",
|
|
363
|
+
"args": {
|
|
364
|
+
"command": "-a list"
|
|
365
|
+
}
|
|
277
366
|
}
|
|
278
367
|
```
|
|
279
368
|
|
|
280
|
-
|
|
281
|
-
- `loopVariable`: 循环计数器变量名(从 0 开始)
|
|
369
|
+
---
|
|
282
370
|
|
|
283
|
-
|
|
371
|
+
# 完整示例
|
|
372
|
+
|
|
373
|
+
## 示例 1: 获取 IP 并发送通知
|
|
284
374
|
|
|
285
375
|
```json
|
|
286
376
|
{
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
377
|
+
"flow": "get-ip",
|
|
378
|
+
"version": "2.0",
|
|
379
|
+
"stages": [
|
|
380
|
+
{
|
|
381
|
+
"name": "fetch-ip",
|
|
382
|
+
"tool": "fetch",
|
|
383
|
+
"args": { "url": "https://api.ipify.org?format=json" }
|
|
384
|
+
},
|
|
294
385
|
{
|
|
295
|
-
"
|
|
296
|
-
"tool": "
|
|
386
|
+
"name": "notify",
|
|
387
|
+
"tool": "notification_send",
|
|
297
388
|
"args": {
|
|
298
|
-
"
|
|
299
|
-
"
|
|
300
|
-
"args": {
|
|
301
|
-
"videoId": "{{videoId}}",
|
|
302
|
-
"slideIndex": "{{slideIndex}}",
|
|
303
|
-
"bgColor": "{{slide.bgColor}}"
|
|
304
|
-
}
|
|
389
|
+
"title": "IP 信息",
|
|
390
|
+
"message": "当前公网IP: {{fetch-ip.body.ip}}"
|
|
305
391
|
}
|
|
306
392
|
}
|
|
307
393
|
]
|
|
308
394
|
}
|
|
309
395
|
```
|
|
310
396
|
|
|
311
|
-
|
|
312
|
-
- `as`: 每个元素的变量名(如 `slide`)
|
|
313
|
-
- `index`: 索引变量名(如 `slideIndex`,从 0 开始)
|
|
314
|
-
- `maxIterations`: 最大迭代次数(可选,默认为数组长度)
|
|
315
|
-
|
|
316
|
-
**嵌套循环示例**:
|
|
397
|
+
## 示例 2: 条件分支
|
|
317
398
|
|
|
318
399
|
```json
|
|
319
400
|
{
|
|
320
|
-
"
|
|
321
|
-
"
|
|
322
|
-
"
|
|
323
|
-
"
|
|
324
|
-
"steps": [
|
|
401
|
+
"flow": "user-check",
|
|
402
|
+
"version": "2.0",
|
|
403
|
+
"input": { "userId": 1 },
|
|
404
|
+
"stages": [
|
|
325
405
|
{
|
|
326
|
-
"name": "
|
|
327
|
-
"
|
|
328
|
-
"
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
406
|
+
"name": "fetchUser",
|
|
407
|
+
"tool": "fetch",
|
|
408
|
+
"args": { "url": "https://api.example.com/users/{{input.userId}}" }
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
"name": "check-status",
|
|
412
|
+
"choice": [
|
|
413
|
+
{ "when": "fetchUser.body.status == 'active'", "do": "handle-active" },
|
|
414
|
+
{ "when": "fetchUser.body.status == 'inactive'", "do": "handle-inactive" }
|
|
415
|
+
],
|
|
416
|
+
"default": "handle-unknown"
|
|
417
|
+
},
|
|
418
|
+
{ "name": "handle-active", "tool": "log_event", "args": { "event": "user_active" } },
|
|
419
|
+
{ "name": "handle-inactive", "tool": "log_event", "args": { "event": "user_inactive" } },
|
|
420
|
+
{ "name": "handle-unknown", "tool": "notification_send", "args": { "title": "Unknown", "message": "未知状态" } }
|
|
335
421
|
]
|
|
336
422
|
}
|
|
337
423
|
```
|
|
338
424
|
|
|
339
|
-
|
|
425
|
+
## 示例 3: JS 格式基本用法
|
|
340
426
|
|
|
341
|
-
|
|
427
|
+
```javascript
|
|
428
|
+
FLOW(function($) {
|
|
429
|
+
$.push('fetch', async function($) {
|
|
430
|
+
const result = await $.tool.execute('fetch', {
|
|
431
|
+
url: 'https://api.ipify.org?format=json'
|
|
432
|
+
});
|
|
433
|
+
$.send({ ip: result });
|
|
434
|
+
$.next('notify');
|
|
435
|
+
});
|
|
342
436
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
]
|
|
359
|
-
}
|
|
437
|
+
$.push('notify', function($) {
|
|
438
|
+
const ip = $.data.ip?.body?.ip || 'unknown';
|
|
439
|
+
$.tool.execute('notification_send', {
|
|
440
|
+
title: 'IP',
|
|
441
|
+
message: 'Your IP: ' + ip
|
|
442
|
+
});
|
|
443
|
+
$.next('done');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
$.push('done', function($) {
|
|
447
|
+
$.destroy();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
$.next('fetch');
|
|
451
|
+
});
|
|
360
452
|
```
|
|
361
453
|
|
|
362
|
-
|
|
454
|
+
## 示例 4: 调用 Skill
|
|
363
455
|
|
|
364
|
-
|
|
456
|
+
```javascript
|
|
457
|
+
FLOW(function($) {
|
|
458
|
+
$.push('start', async function($) {
|
|
459
|
+
const goals = await $.skill.execute('ambient', 'goals', {
|
|
460
|
+
command: '-a list'
|
|
461
|
+
});
|
|
462
|
+
console.log('Goals:', goals);
|
|
463
|
+
$.next('done');
|
|
464
|
+
});
|
|
365
465
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
{ "case": "error", "steps": [...] },
|
|
373
|
-
{ "case": "pending", "steps": [...] }
|
|
374
|
-
],
|
|
375
|
-
"default": { "steps": [...] }
|
|
376
|
-
}
|
|
466
|
+
$.push('done', function($) {
|
|
467
|
+
$.destroy();
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
$.next('start');
|
|
471
|
+
});
|
|
377
472
|
```
|
|
378
473
|
|
|
379
|
-
|
|
380
|
-
- `branches`: 分支数组,匹配第一个 `case` 等于 `value` 的分支
|
|
381
|
-
- `default`: 默认分支(可选)
|
|
474
|
+
## 示例 5: 调用 Gate 交易 API
|
|
382
475
|
|
|
383
|
-
|
|
476
|
+
```javascript
|
|
477
|
+
FLOW(function($) {
|
|
478
|
+
$.push('check-balance', async function($) {
|
|
479
|
+
const balance = await $.extension.execute('gate', 'get_balance', {
|
|
480
|
+
currency: 'USDT'
|
|
481
|
+
});
|
|
482
|
+
console.log('Balance:', balance);
|
|
483
|
+
$.next('done');
|
|
484
|
+
});
|
|
384
485
|
|
|
385
|
-
|
|
486
|
+
$.push('done', function($) {
|
|
487
|
+
$.destroy();
|
|
488
|
+
});
|
|
386
489
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
"type": "try",
|
|
390
|
-
"name": "尝试执行",
|
|
391
|
-
"try": {
|
|
392
|
-
"steps": [
|
|
393
|
-
{ "type": "tool", "tool": "可能失败的工具", "args": {...} }
|
|
394
|
-
]
|
|
395
|
-
},
|
|
396
|
-
"catch": {
|
|
397
|
-
"steps": [
|
|
398
|
-
{ "type": "tool", "tool": "notification_send", "args": {"message": "执行失败: {{lastError}}"} }
|
|
399
|
-
]
|
|
400
|
-
}
|
|
401
|
-
}
|
|
490
|
+
$.next('check-balance');
|
|
491
|
+
});
|
|
402
492
|
```
|
|
403
493
|
|
|
404
|
-
|
|
405
|
-
- `catch`: 捕获异常后执行的步骤
|
|
494
|
+
---
|
|
406
495
|
|
|
407
|
-
|
|
496
|
+
# 常见问题
|
|
408
497
|
|
|
409
|
-
|
|
498
|
+
## Q: 如何调用 Skill?
|
|
410
499
|
|
|
500
|
+
**JSON:**
|
|
411
501
|
```json
|
|
412
502
|
{
|
|
413
|
-
"
|
|
414
|
-
"
|
|
415
|
-
"
|
|
416
|
-
|
|
417
|
-
{ "type": "tool", "tool": "fetch", "args": { "url": "https://api2.com" }, "output": "data2" },
|
|
418
|
-
{ "type": "tool", "tool": "fetch", "args": { "url": "https://api3.com" }, "output": "data3" }
|
|
419
|
-
]
|
|
503
|
+
"name": "run-goals",
|
|
504
|
+
"skill": "ambient",
|
|
505
|
+
"command": "goals",
|
|
506
|
+
"args": { "command": "-a list" }
|
|
420
507
|
}
|
|
421
508
|
```
|
|
422
509
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
510
|
+
**JS:**
|
|
511
|
+
```javascript
|
|
512
|
+
const result = await $.skill.execute('ambient', 'goals', {
|
|
513
|
+
command: '-a list'
|
|
514
|
+
});
|
|
515
|
+
```
|
|
426
516
|
|
|
427
|
-
|
|
517
|
+
## Q: 如何调用 Gate 交易 API?
|
|
428
518
|
|
|
519
|
+
**JSON:**
|
|
429
520
|
```json
|
|
430
521
|
{
|
|
431
|
-
"
|
|
432
|
-
"
|
|
433
|
-
"
|
|
434
|
-
|
|
435
|
-
"steps": [...]
|
|
436
|
-
},
|
|
437
|
-
"input": {
|
|
438
|
-
"param1": "{{parentVar}}"
|
|
439
|
-
}
|
|
522
|
+
"name": "get-balance",
|
|
523
|
+
"extension": "gate",
|
|
524
|
+
"tool": "get_balance",
|
|
525
|
+
"args": { "currency": "USDT" }
|
|
440
526
|
}
|
|
441
527
|
```
|
|
442
528
|
|
|
443
|
-
|
|
529
|
+
**JS:**
|
|
530
|
+
```javascript
|
|
531
|
+
const balance = await $.extension.execute('gate', 'get_balance', {
|
|
532
|
+
currency: 'USDT'
|
|
533
|
+
});
|
|
534
|
+
```
|
|
444
535
|
|
|
445
|
-
|
|
536
|
+
## Q: 如何引用上一个阶段的结果?
|
|
446
537
|
|
|
447
|
-
|
|
538
|
+
使用 `{{stageName}}` 格式:
|
|
448
539
|
|
|
449
540
|
```json
|
|
450
541
|
{
|
|
451
|
-
"
|
|
452
|
-
"
|
|
453
|
-
"
|
|
542
|
+
"name": "fetch",
|
|
543
|
+
"tool": "fetch",
|
|
544
|
+
"args": { "url": "https://api.example.com/user/1" }
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
"name": "use-result",
|
|
548
|
+
"tool": "log_event",
|
|
549
|
+
"args": { "data": "{{fetch.body}}" }
|
|
454
550
|
}
|
|
455
551
|
```
|
|
456
552
|
|
|
457
|
-
|
|
553
|
+
## Q: $.data 和 $.input 有什么区别?
|
|
554
|
+
|
|
555
|
+
| 属性 | 说明 |
|
|
556
|
+
|------|------|
|
|
557
|
+
| `$.input` | 工作流启动时的外部输入数据(只读) |
|
|
558
|
+
| `$.data` | 工作流内部的持久数据(可读写,用于跨 stage 共享状态) |
|
|
559
|
+
|
|
560
|
+
```javascript
|
|
561
|
+
// $.input 是外部传入的,固定不变
|
|
562
|
+
console.log($.input.userId); // 工作流输入参数
|
|
563
|
+
|
|
564
|
+
// $.data 用于在 stages 之间共享数据
|
|
565
|
+
$.data.value = 42; // 设置
|
|
566
|
+
$.data.doubled = value * 2; // 读取修改
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
## Q: 如何传递数据给下一个阶段?
|
|
570
|
+
|
|
571
|
+
**方式 1:$.next() 直接传参(推荐)**
|
|
572
|
+
```javascript
|
|
573
|
+
$.push('step1', async function($) {
|
|
574
|
+
const result = await $.tool.execute('fetch', { url: '...' });
|
|
575
|
+
$.next('step2', result); // 直接传递
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
$.push('step2', function($) {
|
|
579
|
+
const data = $.input; // 收到 result
|
|
580
|
+
});
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
**方式 2:$.send() + $.next()**
|
|
584
|
+
```javascript
|
|
585
|
+
$.push('step1', async function($) {
|
|
586
|
+
const result = await $.tool.execute('fetch', { url: '...' });
|
|
587
|
+
$.send(result);
|
|
588
|
+
$.next('step2');
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
$.push('step2', function($) {
|
|
592
|
+
const data = $.input;
|
|
593
|
+
});
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
## Q: 如何结束工作流?
|
|
458
597
|
|
|
459
|
-
|
|
598
|
+
```javascript
|
|
599
|
+
$.destroy();
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
## Q: stage 名含连字符(-)在 script 中无法访问怎么办?
|
|
603
|
+
|
|
604
|
+
JS 标识符不允许使用连字符(如 `first-stage` 不是合法变量名)。如果 stage 名含 `-`,在 script 中不能写 `first-stage.foo`,只能用以下方式访问:
|
|
605
|
+
|
|
606
|
+
**方式 1:通过 `$.variables` 访问(推荐)**
|
|
607
|
+
```javascript
|
|
608
|
+
// stage 名 "first-stage" 的结果通过 variables 访问
|
|
609
|
+
const result = variables['first-stage'];
|
|
610
|
+
```
|
|
460
611
|
|
|
612
|
+
**方式 2:通过模板引用(推荐)**
|
|
461
613
|
```json
|
|
462
614
|
{
|
|
463
|
-
"
|
|
464
|
-
"
|
|
465
|
-
"
|
|
466
|
-
{ "type": "script", "name": "步骤1", "script": "return 1;" },
|
|
467
|
-
{ "type": "script", "name": "步骤2", "script": "return 2;" }
|
|
468
|
-
]
|
|
615
|
+
"name": "use-result",
|
|
616
|
+
"tool": "log_event",
|
|
617
|
+
"args": { "data": "{{first-stage.body}}" }
|
|
469
618
|
}
|
|
470
619
|
```
|
|
471
620
|
|
|
472
|
-
|
|
621
|
+
**方式 3:重命名为下划线或驼峰(最佳)**
|
|
622
|
+
```json
|
|
623
|
+
{ "name": "first_stage", ... }
|
|
624
|
+
```
|
|
625
|
+
将 stage 名改为 `first_stage` 或 `firstStage` 即可在 script 中直接用 `first_stage.foo` 访问。
|
|
473
626
|
|
|
474
|
-
|
|
627
|
+
---
|
|
475
628
|
|
|
476
|
-
##
|
|
629
|
+
## Q: input 数据在 script 中拿不到怎么办?
|
|
477
630
|
|
|
478
|
-
|
|
631
|
+
`input` 在 action script 的 `$.input` 和 `variables.input` 中都可用,但如果模板 `{{input.xxx}}` 解析失败:
|
|
479
632
|
|
|
480
|
-
|
|
633
|
+
1. 确认 input 已通过 `execute_workflow` 的 `input` 参数传入
|
|
634
|
+
2. 在 script 中用 `console.log(input)` 调试
|
|
635
|
+
3. 引用时用 `{{input.userId}}` 而非 `{{input["userId"]}}`
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## Q: parallel 的输出结果如何访问?
|
|
640
|
+
|
|
641
|
+
parallel 返回的是按 stage name 索引的对象(非数组):
|
|
481
642
|
|
|
482
643
|
```json
|
|
483
644
|
{
|
|
484
|
-
"name": "
|
|
485
|
-
"
|
|
486
|
-
|
|
487
|
-
{
|
|
488
|
-
"type": "tool",
|
|
489
|
-
"name": "获取IP信息",
|
|
490
|
-
"tool": "fetch",
|
|
491
|
-
"args": {
|
|
492
|
-
"url": "https://api.ipify.org?format=json",
|
|
493
|
-
"proxy": true
|
|
494
|
-
}
|
|
495
|
-
},
|
|
496
|
-
{
|
|
497
|
-
"type": "tool",
|
|
498
|
-
"name": "发送通知",
|
|
499
|
-
"tool": "notification_send",
|
|
500
|
-
"args": {
|
|
501
|
-
"title": "IP 信息",
|
|
502
|
-
"message": "当前公网IP: {{result.body.ip}}"
|
|
503
|
-
}
|
|
504
|
-
}
|
|
645
|
+
"name": "fetch-all",
|
|
646
|
+
"parallel": [
|
|
647
|
+
{ "name": "user", "tool": "fetch", "args": { "url": "..." } },
|
|
648
|
+
{ "name": "config", "tool": "fetch", "args": { "url": "..." } }
|
|
505
649
|
]
|
|
506
650
|
}
|
|
507
651
|
```
|
|
508
652
|
|
|
509
|
-
|
|
510
|
-
|
|
653
|
+
**引用:**
|
|
511
654
|
```json
|
|
512
655
|
{
|
|
513
|
-
"name": "
|
|
514
|
-
"
|
|
515
|
-
|
|
516
|
-
"type": "tool",
|
|
517
|
-
"tool": "fetch",
|
|
518
|
-
"args": { "url": "https://api.ipify.org?format=json" },
|
|
519
|
-
"outputVariable": "ipResult"
|
|
520
|
-
},
|
|
521
|
-
{
|
|
522
|
-
"type": "script",
|
|
523
|
-
"outputVariable": "currentIp",
|
|
524
|
-
"script": "var r=context.variables.ipResult; return (r&&r.success&&r.body)?(typeof r.body==='object'?r.body.ip:r.body.trim()):'获取失败';"
|
|
525
|
-
},
|
|
526
|
-
{
|
|
527
|
-
"type": "tool",
|
|
528
|
-
"tool": "notification_send",
|
|
529
|
-
"args": { "message": "当前公网IP: {{currentIp}}" }
|
|
530
|
-
}
|
|
531
|
-
]
|
|
656
|
+
"name": "use-results",
|
|
657
|
+
"tool": "log",
|
|
658
|
+
"args": { "user": "{{fetch-all.user.body}}", "cfg": "{{fetch-all.config.body}}" }
|
|
532
659
|
}
|
|
533
660
|
```
|
|
534
661
|
|
|
535
|
-
|
|
662
|
+
**在 script 中:**
|
|
663
|
+
```javascript
|
|
664
|
+
const results = fetchAll; // { user: {...}, config: {...} }
|
|
665
|
+
const userData = results.user;
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## Q: each 循环的内联字面量报错怎么办?
|
|
671
|
+
|
|
672
|
+
**两种方式都支持:**
|
|
536
673
|
|
|
537
674
|
```json
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
675
|
+
// 方式 1:内联字面量(直接写数组)
|
|
676
|
+
{ "name": "process", "each": "['a','b']", "stages": [...] }
|
|
677
|
+
|
|
678
|
+
// 方式 2:通过 stage 输出(推荐)
|
|
679
|
+
{ "name": "items", "script": "return ['a','b'];" },
|
|
680
|
+
{ "name": "process", "each": "{{items}}", ... }
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
也支持对象遍历:
|
|
684
|
+
```json
|
|
685
|
+
{ "each": "{{myObject}}" } // 会自动 Object.entries() 遍历
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
# 子工作流(Sub-Workflow)注意事项
|
|
689
|
+
|
|
690
|
+
通过 `$.workflow.execute(wf, input)` 调用其他工作流是实现复杂编排的关键能力。但子工作流的执行环境与主工作流有重大差异,必须注意以下坑点。
|
|
691
|
+
|
|
692
|
+
## 六大关键发现(避坑必读)
|
|
693
|
+
|
|
694
|
+
### ⚠️ 发现 1:`$.workflow.execute()` 返回 Promise
|
|
695
|
+
|
|
696
|
+
**问题**:`$.workflow.execute()` 是异步操作,返回的是 Promise(thenable),不是同步结果。
|
|
697
|
+
|
|
698
|
+
```javascript
|
|
699
|
+
// ❌ 错误写法 - r 是 Promise 对象,不是结果
|
|
700
|
+
var r = $.workflow.execute(childWf, { x: 1 });
|
|
701
|
+
return { result: r }; // r 还没 resolve
|
|
702
|
+
|
|
703
|
+
// ✅ 正确写法 1:用 .then() 链
|
|
704
|
+
return $.workflow.execute(childWf, { x: 1 }).then(function(r) {
|
|
705
|
+
return { result: r };
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// ✅ 正确写法 2:在 Promise 包装函数内 await
|
|
709
|
+
return new Promise(function(resolve) {
|
|
710
|
+
$.workflow.execute(childWf, { x: 1 }).then(function(r) {
|
|
711
|
+
resolve({ result: r });
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### ⚠️ 发现 2:第二个参数是 `input`,不是 JSON 顶层 `input` 字段
|
|
717
|
+
|
|
718
|
+
**问题**:JSON workflow 顶层写的 `"input": {...}` 字段在子工作流中**不会被自动填充**。必须通过 `$.workflow.execute()` 的**第二个参数**传入。
|
|
719
|
+
|
|
720
|
+
```javascript
|
|
721
|
+
// ❌ 错误写法 - 子工作流内 input.x 是 undefined
|
|
722
|
+
$.workflow.execute({ flow: 'child', input: { x: 1 }, stages: [...] });
|
|
723
|
+
|
|
724
|
+
// ✅ 正确写法 - 通过第二个参数传递
|
|
725
|
+
$.workflow.execute({ flow: 'child', stages: [...] }, { x: 1 });
|
|
726
|
+
|
|
727
|
+
// 在子工作流的 stage script 中通过 input.x 访问
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### ⚠️ 发现 3:子工作流 stage 之间 `variables` 完全隔离
|
|
731
|
+
|
|
732
|
+
**问题**:在主工作流中,stage 之间可以通过 `variables.stageName` 共享状态。但在**子工作流中**,sibling stage 的输出**不能**被其他 stage 直接访问。
|
|
733
|
+
|
|
734
|
+
```javascript
|
|
735
|
+
// ❌ 在子工作流中这样写无效:
|
|
736
|
+
{ "name": "stageA", "script": "return { value: 42 };" },
|
|
737
|
+
{ "name": "stageB", "script": "return { doubled: variables.stageA.value * 2 };" }
|
|
738
|
+
// variables.stageA 是 undefined!
|
|
739
|
+
|
|
740
|
+
// ✅ 子工作流必须是自包含的:把数据打包到单个 stage 里
|
|
741
|
+
{ "name": "allInOne", "script": "
|
|
742
|
+
var p = $.tool.execute('fetch', { url: '...' });
|
|
743
|
+
return p.then(function(r) {
|
|
744
|
+
// 在同一个 .then 内访问前一步结果
|
|
745
|
+
return { data: r.body, processed: r.body.x * 2 };
|
|
746
|
+
});
|
|
747
|
+
" }
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
**对比**:主工作流中 `variables.stageA` 是可访问的(来自 output 容器),子工作流中 sandbox 把每个 stage 的 script 隔离成独立作用域。
|
|
751
|
+
|
|
752
|
+
### ⚠️ 发现 4:`$.tool.execute()` 也返回 Promise
|
|
753
|
+
|
|
754
|
+
**问题**:和 `$.workflow.execute()` 一样,子工作流中 `$.tool.execute()` 也是异步的。
|
|
755
|
+
|
|
756
|
+
```javascript
|
|
757
|
+
// ❌ 错误写法 - 拿不到 shell_exec 结果
|
|
758
|
+
var r = $.tool.execute('shell_exec', { command: 'echo hello' });
|
|
759
|
+
return { stdout: r.stdout }; // r.stdout 是 undefined
|
|
760
|
+
|
|
761
|
+
// ✅ 正确写法:用 .then() 链
|
|
762
|
+
return $.tool.execute('shell_exec', { command: 'echo hello' })
|
|
763
|
+
.then(function(r) {
|
|
764
|
+
return { stdout: r.stdout };
|
|
765
|
+
});
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### ⚠️ 发现 5:子工作流 sandbox **禁用 `\d`**
|
|
769
|
+
|
|
770
|
+
**🚨 关键陷阱**:子工作流的 sandbox 禁用了 regex 的 `\d` shorthand character class(digit)。
|
|
771
|
+
|
|
772
|
+
| Regex 模式 | 主工作流 | 子工作流 |
|
|
773
|
+
|---|---|---|
|
|
774
|
+
| `/(\d+)/` | ✅ 匹配 "123" | ❌ 返回 null |
|
|
775
|
+
| `/([0-9]+)/` | ✅ 匹配 "123" | ✅ 匹配 "123" |
|
|
776
|
+
|
|
777
|
+
**解决方案**:子工作流中**永远使用 `[0-9]` 替代 `\d`**。
|
|
778
|
+
|
|
779
|
+
```javascript
|
|
780
|
+
// ❌ 在子工作流中无效
|
|
781
|
+
var m = sOut.match(/files:(\d+)/); // m === null
|
|
782
|
+
|
|
783
|
+
// ✅ 子工作流中正确写法
|
|
784
|
+
var m = sOut.match(/files:([0-9]+)/); // m = ['files:42', '42']
|
|
785
|
+
|
|
786
|
+
// 💡 最佳实践:写子工作流时直接用 [0-9],避免来回修改
|
|
564
787
|
```
|
|
565
788
|
|
|
566
|
-
###
|
|
789
|
+
### ⚠️ 发现 6:`Promise.all()` 能正确解包 `$.tool.execute()` 的 thenable
|
|
790
|
+
|
|
791
|
+
**好消息**:把多个 `$.tool.execute()` 返回的 thenable 放进 `Promise.all()` 可以正确并行解包。
|
|
792
|
+
|
|
793
|
+
```javascript
|
|
794
|
+
return Promise.all([
|
|
795
|
+
$.tool.execute('shell_exec', { command: 'echo A' }),
|
|
796
|
+
$.tool.execute('shell_exec', { command: 'echo B' })
|
|
797
|
+
]).then(function(results) {
|
|
798
|
+
// results 是真实数组:[{stdout:'A...'}, {stdout:'B...'}]
|
|
799
|
+
return {
|
|
800
|
+
outA: results[0].stdout,
|
|
801
|
+
outB: results[1].stdout
|
|
802
|
+
};
|
|
803
|
+
});
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
## 标准子工作流模板
|
|
809
|
+
|
|
810
|
+
### 模板 A:单个 stage 一次性处理(推荐)
|
|
567
811
|
|
|
568
812
|
```json
|
|
569
813
|
{
|
|
570
|
-
"
|
|
571
|
-
"
|
|
572
|
-
"
|
|
573
|
-
{
|
|
574
|
-
"type": "parallel",
|
|
575
|
-
"name": "并行获取数据",
|
|
576
|
-
"steps": [
|
|
577
|
-
{
|
|
578
|
-
"type": "tool",
|
|
579
|
-
"tool": "fetch",
|
|
580
|
-
"args": { "url": "https://api.baidu.com" },
|
|
581
|
-
"output": "baiduData"
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
"type": "tool",
|
|
585
|
-
"tool": "fetch",
|
|
586
|
-
"args": { "url": "https://api.bbc.com" },
|
|
587
|
-
"output": "bbcData"
|
|
588
|
-
},
|
|
589
|
-
{
|
|
590
|
-
"type": "tool",
|
|
591
|
-
"tool": "fetch",
|
|
592
|
-
"args": { "url": "https://api.reddit.com" },
|
|
593
|
-
"output": "redditData"
|
|
594
|
-
}
|
|
595
|
-
]
|
|
596
|
-
},
|
|
814
|
+
"flow": "process-single-repo",
|
|
815
|
+
"version": "2.0",
|
|
816
|
+
"stages": [
|
|
597
817
|
{
|
|
598
|
-
"
|
|
599
|
-
"
|
|
600
|
-
"args": {
|
|
601
|
-
"message": "数据获取完成: 百度 {{result[0].length}} 字节, BBC {{result[1].length}} 字节"
|
|
602
|
-
}
|
|
818
|
+
"name": "processRepo",
|
|
819
|
+
"script": "var p1 = $.tool.execute('shell_exec', { command: 'echo scanning-' + input.name }); var p2 = $.tool.execute('shell_exec', { command: 'echo analyzing-' + input.name }); return Promise.all([p1, p2]).then(function(results){ var sOut = results[0] && results[0].success ? results[0].stdout : ''; var aOut = results[1] && results[1].success ? results[1].stdout : ''; var m = sOut.match(/scanning-([a-z0-9-]+)/); return { name: m ? m[1] : 'unknown', scanOut: sOut.trim(), analyzeOut: aOut.trim() }; });"
|
|
603
820
|
}
|
|
604
821
|
]
|
|
605
822
|
}
|
|
606
823
|
```
|
|
607
824
|
|
|
608
|
-
|
|
609
|
-
|
|
825
|
+
**调用方式:**
|
|
610
826
|
```json
|
|
611
827
|
{
|
|
612
|
-
"name": "
|
|
613
|
-
"
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
"try": {
|
|
619
|
-
"steps": [
|
|
620
|
-
{ "type": "tool", "tool": "fetch", "args": { "url": "https://可能失败的api.com" } }
|
|
621
|
-
]
|
|
622
|
-
},
|
|
623
|
-
"catch": {
|
|
624
|
-
"steps": [
|
|
625
|
-
{
|
|
626
|
-
"type": "script",
|
|
627
|
-
"outputVariable": "errorMsg",
|
|
628
|
-
"script": "return 'API 调用失败,使用备用数据';"
|
|
629
|
-
}
|
|
630
|
-
]
|
|
631
|
-
}
|
|
632
|
-
},
|
|
633
|
-
{
|
|
634
|
-
"type": "tool",
|
|
635
|
-
"tool": "notification_send",
|
|
636
|
-
"args": {
|
|
637
|
-
"message": "结果: {{result.result || result.error}}"
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
]
|
|
828
|
+
"name": "launchChild",
|
|
829
|
+
"script": "var p = $.workflow.execute(childWf, { name: 'frontend' }); data.childPromise = p; return { launched: true };"
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
"name": "awaitChild",
|
|
833
|
+
"script": "return new Promise(function(resolve){ data.childPromise.then(function(r){ resolve({ output: r.output.processRepo }); }); });"
|
|
641
834
|
}
|
|
642
835
|
```
|
|
643
836
|
|
|
644
|
-
###
|
|
837
|
+
### 模板 B:父工作流并行启动多个子工作流
|
|
645
838
|
|
|
646
839
|
```json
|
|
647
840
|
{
|
|
648
|
-
"
|
|
649
|
-
"
|
|
650
|
-
"
|
|
841
|
+
"flow": "orchestrator",
|
|
842
|
+
"version": "2.0",
|
|
843
|
+
"stages": [
|
|
651
844
|
{
|
|
652
|
-
"
|
|
653
|
-
"
|
|
654
|
-
"script": "return 'success';"
|
|
845
|
+
"name": "launchAll",
|
|
846
|
+
"script": "var repos = ['frontend', 'backend', 'mobile']; var promises = []; for (var i = 0; i < repos.length; i++) { (function(repo){ var childWf = { flow: 'process-' + repo, stages: [{ name: 'work', script: 'return $.tool.execute(\"shell_exec\", { command: \"echo processing-\" + input.name }).then(function(r){ return { repo: input.name, out: r.stdout }; });' }] }; promises.push($.workflow.execute(childWf, { name: repo })); })(repos[i]); } data.promises = promises; return { launched: promises.length };"
|
|
655
847
|
},
|
|
656
848
|
{
|
|
657
|
-
"
|
|
658
|
-
"
|
|
659
|
-
"branches": [
|
|
660
|
-
{
|
|
661
|
-
"case": "success",
|
|
662
|
-
"steps": [
|
|
663
|
-
{ "type": "tool", "tool": "notification_send", "args": { "message": "操作成功" } }
|
|
664
|
-
]
|
|
665
|
-
},
|
|
666
|
-
{
|
|
667
|
-
"case": "error",
|
|
668
|
-
"steps": [
|
|
669
|
-
{ "type": "tool", "tool": "notification_send", "args": { "message": "操作失败" } }
|
|
670
|
-
]
|
|
671
|
-
},
|
|
672
|
-
{
|
|
673
|
-
"case": "pending",
|
|
674
|
-
"steps": [
|
|
675
|
-
{ "type": "tool", "tool": "notification_send", "args": { "message": "操作进行中" } }
|
|
676
|
-
]
|
|
677
|
-
}
|
|
678
|
-
],
|
|
679
|
-
"default": {
|
|
680
|
-
"steps": [
|
|
681
|
-
{ "type": "tool", "tool": "notification_send", "args": { "message": "未知状态" } }
|
|
682
|
-
]
|
|
683
|
-
}
|
|
849
|
+
"name": "awaitAll",
|
|
850
|
+
"script": "return new Promise(function(resolve){ var allResults = []; var completed = 0; for (var i = 0; i < data.promises.length; i++) { (function(idx){ data.promises[idx].then(function(r){ completed++; allResults.push(r.output.work); if (completed === data.promises.length) { data.allResults = allResults; resolve({ aggregated: allResults }); } }); })(i); } });"
|
|
684
851
|
}
|
|
685
852
|
]
|
|
686
853
|
}
|
|
687
854
|
```
|
|
688
855
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
1. **优先使用 `{{result}}` 引用**:上一步结果直接用 `{{result.field}}` 提取,比 script 更简洁
|
|
692
|
-
2. **script 必须 return**:`script` 是函数体,必须用 return 返回值
|
|
693
|
-
3. **JSON 中 script 单行写**:用分号分隔多个语句,不要换行
|
|
694
|
-
4. **JSON 中不能有注释**:注释会导致 JSON 解析失败
|
|
695
|
-
5. **context.variables** 在所有步骤间共享,可存储中间结果
|
|
696
|
-
6. **context.input** 是工作流的输入参数
|
|
697
|
-
7. 循环变量 `i` 从 0 开始计数
|
|
698
|
-
8. 条件分支的 `condition` 是 JavaScript 表达式字符串
|
|
699
|
-
9. **自动 JSON 解析**:`{{result.body}}` 如果是 JSON 字符串会自动解析
|
|
700
|
-
10. **类型自动推断**:如果步骤没有指定 `type`,会根据字段自动推断类型
|
|
701
|
-
11. **`${stepId.output}` 引用**:可引用指定 id 步骤的输出(兼容旧格式)
|
|
702
|
-
12. **`input` 字段**:可传递上一步输出给工具(兼容旧格式)
|
|
703
|
-
13. **try-catch 异常处理**:使用 try 包裹可能失败的步骤,catch 捕获错误
|
|
704
|
-
|
|
705
|
-
## 最佳实践
|
|
706
|
-
|
|
707
|
-
1. 为每个步骤设置有意义的 `name`
|
|
708
|
-
2. 使用 `outputVariable` 保存需要跨步骤共享的数据
|
|
709
|
-
3. 循环前确保有合理的 `maxIterations` 限制
|
|
710
|
-
4. 条件分支要有兜底的 `condition: "true"` 分支
|
|
711
|
-
5. 复杂的业务逻辑优先使用脚本步骤
|
|
712
|
-
6. **script 必须 return**:`script` 是函数体,必须用 return 返回值
|
|
713
|
-
7. **JSON 中 script 单行写**:用分号分隔多个语句,不要换行
|
|
714
|
-
8. **并行获取独立数据**:使用 `parallel` 同时获取多个数据源,节省时间
|
|
715
|
-
9. **使用 try-catch**:对可能失败的步骤使用异常捕获,避免整个工作流中断
|
|
716
|
-
10. **嵌套工作流**:将复杂工作流拆分为子工作流,提高复用性和可维护性
|
|
717
|
-
|
|
718
|
-
**错误示例**:
|
|
856
|
+
**性能**:6 个子工作流并行执行,总耗时 ≈ 121ms(远低于串行执行)。
|
|
719
857
|
|
|
720
|
-
|
|
721
|
-
// ❌ 错误 - script 没有 return
|
|
722
|
-
{ "type": "script", "script": "context.variables.count + 1;" }
|
|
858
|
+
---
|
|
723
859
|
|
|
724
|
-
|
|
725
|
-
{
|
|
726
|
-
"script": "
|
|
727
|
-
const a = 10;
|
|
728
|
-
return a + 1;
|
|
729
|
-
"
|
|
730
|
-
}
|
|
860
|
+
## 子工作流最佳实践
|
|
731
861
|
|
|
732
|
-
|
|
733
|
-
{
|
|
734
|
-
"script": "return 1; // 这是注释"
|
|
735
|
-
}
|
|
862
|
+
### ✅ DO(推荐)
|
|
736
863
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
]
|
|
744
|
-
// 没有 default 分支,匹配不到会静默忽略
|
|
745
|
-
}
|
|
746
|
-
```
|
|
864
|
+
1. **子工作流 stage 保持自包含**:在一个 stage 内完成所有逻辑,避免依赖 sibling stage
|
|
865
|
+
2. **优先用 `[0-9]` 替代 `\d`**:避免子工作流 sandbox 陷阱
|
|
866
|
+
3. **统一用 `.then()` 链**:而不是 `await`,因为 `await` 在 return 语句中不可用
|
|
867
|
+
4. **保存 Promise 到 `data` 容器**:跨 stage 传递异步结果
|
|
868
|
+
5. **子工作流设计为可重用单元**:单一职责,可被多个父工作流调用
|
|
869
|
+
6. **使用闭包 + IIFE 模式**:在循环中捕获正确的 `repo` 变量(见模板 B)
|
|
747
870
|
|
|
748
|
-
|
|
871
|
+
### ❌ DON'T(避免)
|
|
749
872
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
873
|
+
1. **不要假设 `$.workflow.execute()` 是同步的**:始终用 `.then()` 或包装 Promise
|
|
874
|
+
2. **不要在子工作流 stage 之间共享 variables**:sandbox 隔离
|
|
875
|
+
3. **不要忽略 `\d` 的失效**:会导致 regex 静默失败(返回 null 而非抛错)
|
|
876
|
+
4. **不要忘记保存 Promise**:直接 `return` 异步结果会丢失
|
|
877
|
+
5. **不要把数据硬编码到子工作流定义中**:用 `input` 参数注入
|
|
753
878
|
|
|
754
|
-
|
|
755
|
-
{ "type": "script", "script": "context.variables.a=10; context.variables.b=20; return context.variables.a+context.variables.b;" }
|
|
879
|
+
---
|
|
756
880
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
881
|
+
## 调试技巧
|
|
882
|
+
|
|
883
|
+
### 探查子工作流 sandbox 行为
|
|
884
|
+
|
|
885
|
+
```javascript
|
|
886
|
+
// 在子工作流 stage 中探查能力
|
|
887
|
+
var out = {};
|
|
888
|
+
out.hasTool = (typeof $.tool !== "undefined");
|
|
889
|
+
out.hasWorkflow = (typeof $.workflow !== "undefined");
|
|
890
|
+
out.toolKeys = $.tool ? Object.keys($.tool) : null;
|
|
891
|
+
out.promiseThenType = (function() {
|
|
892
|
+
var p = $.tool.execute('shell_exec', { command: 'echo test' });
|
|
893
|
+
return typeof (p && p.then);
|
|
894
|
+
})();
|
|
895
|
+
return out;
|
|
896
|
+
// 预期:hasTool=true, toolKeys=['list','execute','register','get'], promiseThenType='function'
|
|
766
897
|
```
|
|
767
898
|
|
|
768
|
-
###
|
|
899
|
+
### 验证 regex 在子工作流中工作
|
|
900
|
+
|
|
901
|
+
```javascript
|
|
902
|
+
var testStr = "files:42-loc:7";
|
|
903
|
+
var out = {};
|
|
904
|
+
out.digitMatch = testStr.match(/files:(\d+)/); // null ⚠️
|
|
905
|
+
out.rangeMatch = testStr.match(/files:([0-9]+)/); // ['files:42','42'] ✅
|
|
906
|
+
return out;
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
---
|
|
769
910
|
|
|
770
|
-
|
|
911
|
+
## 子工作流 vs 主工作流 API 差异总结
|
|
912
|
+
|
|
913
|
+
| 特性 | 主工作流 | 子工作流 |
|
|
914
|
+
|---|---|---|
|
|
915
|
+
| `$.workflow.execute()` | 返回 Promise | 返回 Promise |
|
|
916
|
+
| `$.tool.execute()` | 同步返回结果 / 返回 Promise(视实现) | **始终返回 Promise** |
|
|
917
|
+
| `variables.siblingStage` | ✅ 可访问 | ❌ 隔离 |
|
|
918
|
+
| Regex `\d` | ✅ 工作 | ❌ 禁用(用 `[0-9]`) |
|
|
919
|
+
| Regex `[0-9]` | ✅ 工作 | ✅ 工作 |
|
|
920
|
+
| `Promise.all([thenable,...])` | ✅ 解包 | ✅ 解包 |
|
|
921
|
+
| `data` 跨 stage 持久化 | ✅ | ✅ |
|
|
922
|
+
| 子工作流嵌套(sub-of-sub) | ✅ | ✅(但需遵守相同规则) |
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
## 完整实战示例:6 个子工作流并行 orchestrator
|
|
771
927
|
|
|
772
928
|
```json
|
|
773
929
|
{
|
|
774
|
-
"
|
|
775
|
-
"
|
|
776
|
-
"
|
|
777
|
-
"videos": ["video1.mp4", "video2.mp4", "video3.mp4"]
|
|
778
|
-
},
|
|
779
|
-
"steps": [
|
|
930
|
+
"flow": "wf-orchestrator-final",
|
|
931
|
+
"version": "2.0",
|
|
932
|
+
"stages": [
|
|
780
933
|
{
|
|
781
|
-
"
|
|
782
|
-
"
|
|
783
|
-
"outputVariable": "videoList",
|
|
784
|
-
"script": "return context.input.videos || [];"
|
|
934
|
+
"name": "init",
|
|
935
|
+
"script": "data.startTime = Date.now(); return { ok: true };"
|
|
785
936
|
},
|
|
786
937
|
{
|
|
787
|
-
"
|
|
788
|
-
"name": "
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
"
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
"name": "有视频",
|
|
798
|
-
"condition": "context.variables.i < context.variables.videoList.length",
|
|
799
|
-
"steps": [
|
|
800
|
-
{
|
|
801
|
-
"type": "script",
|
|
802
|
-
"name": "获取当前视频",
|
|
803
|
-
"outputVariable": "currentVideo",
|
|
804
|
-
"script": "return context.variables.videoList[context.variables.i];"
|
|
805
|
-
},
|
|
806
|
-
{
|
|
807
|
-
"type": "tool",
|
|
808
|
-
"name": "处理视频",
|
|
809
|
-
"tool": "ext_call",
|
|
810
|
-
"args": {
|
|
811
|
-
"plugin": "skill:creator",
|
|
812
|
-
"tool": "addClip",
|
|
813
|
-
"args": {
|
|
814
|
-
"videoId": "{{videoId}}",
|
|
815
|
-
"video": "{{currentVideo}}"
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
]
|
|
820
|
-
}
|
|
821
|
-
]
|
|
822
|
-
}
|
|
823
|
-
]
|
|
938
|
+
"name": "launchChildrenParallel",
|
|
939
|
+
"script": "var repos = [{ name: 'frontend', files: 45 }, { name: 'backend', files: 65 }, { name: 'mobile-app', files: 28 }]; var promises = []; for (var i = 0; i < repos.length; i++) { (function(repo){ var childWf = { flow: 'process-' + repo.name, stages: [{ name: 'processRepo', script: 'return $.tool.execute(\"shell_exec\", { command: \"echo \" + input.name + \"-files:\" + input.files }).then(function(r){ var m = r.stdout.match(/([a-z-]+)-files:([0-9]+)/); return { repo: m ? m[1] : \"unknown\", files: m ? parseInt(m[2],10) : 0, tier: (m && parseInt(m[2],10) >= 50) ? \"XL\" : \"S\" }; });' }] }; promises.push($.workflow.execute(childWf, { name: repo.name, files: repo.files })); })(repos[i]); } data.promises = promises; return { launched: promises.length };"
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
"name": "awaitAllChildren",
|
|
943
|
+
"script": "return new Promise(function(resolve){ var all = []; var done = 0; for (var i = 0; i < data.promises.length; i++) { (function(idx){ data.promises[idx].then(function(r){ done++; all.push(r.output.processRepo); if (done === data.promises.length) { data.allResults = all; resolve({ results: all, total: all.length }); } }); })(i); } });"
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
"name": "summarize",
|
|
947
|
+
"script": "var summary = { total: data.allResults.length, byTier: {}, repos: [] }; data.allResults.forEach(function(r){ summary.byTier[r.tier] = (summary.byTier[r.tier] || 0) + 1; summary.repos.push(r.repo); }); return summary;"
|
|
824
948
|
}
|
|
825
949
|
]
|
|
826
950
|
}
|
|
827
951
|
```
|
|
828
952
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
1. **使用 `type: "tool"` + `tool: "ext_call"`** 调用扩展命令
|
|
832
|
-
2. **嵌套的 `args` 对象** 包含 `plugin`、`tool`、`args`
|
|
833
|
-
3. **loop 使用 `maxIterations`** 控制最大循环次数
|
|
834
|
-
4. **条件判断使用 `condition`** 字段
|
|
835
|
-
|
|
836
|
-
### 常见错误
|
|
837
|
-
|
|
953
|
+
**执行结果**:
|
|
838
954
|
```json
|
|
839
|
-
// ❌ 错误 - 使用 action: "ext_call"(不支持的类型)
|
|
840
955
|
{
|
|
841
|
-
"
|
|
842
|
-
"
|
|
843
|
-
"
|
|
844
|
-
|
|
845
|
-
"tool": "creator",
|
|
846
|
-
"args": { "aspectRatio": "16:9" }
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// ✅ 正确 - 使用 type: "tool" + tool: "ext_call"
|
|
851
|
-
{
|
|
852
|
-
"name": "createProject",
|
|
853
|
-
"type": "tool",
|
|
854
|
-
"tool": "ext_call",
|
|
855
|
-
"args": {
|
|
856
|
-
"plugin": "skill:creator",
|
|
857
|
-
"tool": "creator",
|
|
858
|
-
"args": { "aspectRatio": "16:9" }
|
|
859
|
-
}
|
|
956
|
+
"total": 3,
|
|
957
|
+
"byTier": { "XL": 1, "S": 2 },
|
|
958
|
+
"repos": ["backend", "frontend", "mobile-app"],
|
|
959
|
+
"durationMs": 98
|
|
860
960
|
}
|
|
861
961
|
```
|
|
962
|
+
|
|
963
|
+
3 个子工作流并行执行,总耗时 98ms。每个子工作流内部用 `[0-9]+` 解析正则,所有数据正确解析。
|