foliko 2.0.6 → 2.0.8

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.
Files changed (53) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/docs/public-api.md +3 -3
  3. package/examples/workflow.js +62 -57
  4. package/package.json +2 -3
  5. package/plugins/core/scheduler/CHANGELOG.md +31 -0
  6. package/plugins/core/scheduler/index.js +576 -638
  7. package/plugins/core/workflow/context.js +941 -0
  8. package/plugins/core/workflow/engine.js +66 -0
  9. package/plugins/core/workflow/examples/01-basic.js +42 -0
  10. package/plugins/core/workflow/examples/01-basic.json +30 -0
  11. package/plugins/core/workflow/examples/02-choice.js +75 -0
  12. package/plugins/core/workflow/examples/02-choice.json +59 -0
  13. package/plugins/core/workflow/examples/03-chain-style.js +114 -0
  14. package/plugins/core/workflow/examples/03-each.json +41 -0
  15. package/plugins/core/workflow/examples/04-parallel.js +52 -0
  16. package/plugins/core/workflow/examples/04-parallel.json +51 -0
  17. package/plugins/core/workflow/examples/05-chain-style.json +68 -0
  18. package/plugins/core/workflow/examples/05-each.js +65 -0
  19. package/plugins/core/workflow/examples/06-script.js +82 -0
  20. package/plugins/core/workflow/examples/07-module-export.js +43 -0
  21. package/plugins/core/workflow/examples/08-default-export.js +29 -0
  22. package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
  23. package/plugins/core/workflow/examples/10-logger.js +34 -0
  24. package/plugins/core/workflow/examples/11-storage.js +68 -0
  25. package/plugins/core/workflow/examples/simple.js +77 -0
  26. package/plugins/core/workflow/examples/simple.json +75 -0
  27. package/plugins/core/workflow/index.js +124 -58
  28. package/plugins/core/workflow/js-runner.js +318 -0
  29. package/plugins/core/workflow/json-runner.js +323 -0
  30. package/plugins/core/workflow/stages/action.js +211 -0
  31. package/plugins/core/workflow/stages/choice.js +74 -0
  32. package/plugins/core/workflow/stages/delay.js +73 -0
  33. package/plugins/core/workflow/stages/each.js +123 -0
  34. package/plugins/core/workflow/stages/parallel.js +69 -0
  35. package/plugins/core/workflow/stages/try.js +142 -0
  36. package/plugins/executors/data-splitter/index.js +3 -2
  37. package/plugins/memory/index.js +10 -3
  38. package/sandbox/check-context.js +5 -0
  39. package/sandbox/test-context.js +27 -0
  40. package/sandbox/test-fixes.js +40 -0
  41. package/sandbox/test-hello-js.js +46 -0
  42. package/skills/workflows/SKILL.md +715 -613
  43. package/src/agent/chat.js +8 -1
  44. package/src/agent/main.js +5 -1
  45. package/src/cli/ui/chat-ui.js +4 -3
  46. package/src/common/json-safe.js +20 -0
  47. package/src/framework/framework.js +58 -2
  48. package/src/index.js +10 -0
  49. package/src/plugin/base.js +7 -4
  50. package/src/plugin/manager.js +95 -1
  51. package/src/utils/sandbox.js +1 -1
  52. package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
  53. 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
- 工作流引擎用于处理多步骤任务,通过 `execute_workflow` 工具执行。工作流由多个步骤组成,支持脚本、条件分支、循环、延时、工具调用等类型。
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
- └── my-workflow.json # 工作流 JSON 定义
20
- └── another.js # JS 文件(导出 default 工作流)
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
- "tool": "execute_workflow",
50
- "args": {
51
- "workflow": "my-workflow", // 工作流名称(自动从 .foliko/workflows/ 加载)
52
- "input": { "变量名": "" }
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
- "tool": "workflow_my_workflow",
67
+ "name": "fetchUser",
68
+ "tool": "fetch",
62
69
  "args": {
63
- "input": { "变量名": "值" }
70
+ "url": "https://api.example.com/users/{{input.userId}}"
64
71
  }
65
72
  }
66
73
  ```
67
74
 
68
- ## 重要注意事项
69
-
70
- 1. **script 必须用 return 返回值**:`script` 是函数体,不是表达式
71
- 2. **JSON 中不能使用多行字符串**:所有 script 内容写成单行,用分号分隔
72
- 3. **JSON 中不能有注释**:注释会导致 JSON 解析失败
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
- ### 1. script - 脚本步骤
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
- 执行 JavaScript 脚本,**必须用 return 返回值**:
108
+ ### choice - 条件分支
96
109
 
97
110
  ```json
98
111
  {
99
- "type": "script",
100
- "name": "步骤名称",
101
- "outputVariable": "result",
102
- "script": "var x=10; context.variables.count=x+1; return context.variables.count;"
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
- { "script": "return context.variables.value + 1;" }
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
- **错误写法**(缺少 return):
135
+ ### parallel - 并行执行
113
136
 
114
137
  ```json
115
- { "script": "context.variables.value + 1;" }
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
- - `outputVariable`: 可选,将返回值存入 `context.variables`
119
- - `context.variables`: 所有步骤共享的变量对象
120
- - `context.lastResult`: 上一步的输出结果
147
+ ---
121
148
 
122
- ### 2. tool - 工具调用步骤
149
+ # JS 格式
123
150
 
124
- 在工作流中调用框架注册的工具:
151
+ ## 基本结构
125
152
 
126
- ```json
127
- {
128
- "type": "tool",
129
- "name": "发送通知",
130
- "tool": "notification_send",
131
- "args": {
132
- "title": "标题",
133
- "message": "内容"
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
- | `tool` | string | 是 | 工具名称 |
144
- | `args` | object | 否 | 工具参数 |
145
- | `outputVariable` | string | 否 | 结果保存到的变量名 |
166
+ ```javascript
167
+ // 格式 1: FLOW 包装器(推荐)
168
+ FLOW(function($) { ... });
146
169
 
147
- **重要:`args` vs `params` 字段**:
170
+ // 格式 2: module.exports
171
+ module.exports = function($) { ... };
148
172
 
149
- 工作流引擎使用 `args` 字段传递工具参数,不是 `params`:
173
+ // 格式 3: module.exports.default
174
+ module.exports.default = function($) { ... };
175
+ ```
150
176
 
151
- ```json
152
- // ✅ 正确
153
- { "type": "tool", "tool": "fetch", "args": { "url": "https://..." } }
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
- { "type": "tool", "tool": "fetch", "params": { "url": "https://..." } }
215
+ // 接收多个参数
216
+ $.push('step2', function($, user, config) {
217
+ console.log(user, config);
218
+ });
157
219
  ```
158
220
 
159
- **args 中支持变量引用**:
221
+ ## $.tool - 工具调用
160
222
 
161
- 使用 `{{variableName}}` 语法引用 context.variables 中的变量:
223
+ ```javascript
224
+ // 执行工具
225
+ await $.tool.execute('fetch', { url: 'https://api.example.com' });
162
226
 
163
- ```json
164
- {
165
- "type": "tool",
166
- "name": "发送通知",
167
- "tool": "notification_send",
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
- **重要:使用 `{{result}}` 引用上一步结果(推荐)**
234
+ ## $.extension - 扩展调用
176
235
 
177
- 上一步骤的结果自动保存在 `{{result}}` 中,可以直接提取字段:
236
+ ```javascript
237
+ // 调用 Gate 交易 API
238
+ const balance = await $.extension.execute('gate', 'get_balance', {
239
+ currency: 'USDT'
240
+ });
178
241
 
179
- ```json
180
- {
181
- "type": "tool",
182
- "name": "获取IP",
183
- "tool": "fetch",
184
- "args": {
185
- "url": "https://api.ipify.org?format=json"
186
- }
187
- },
188
- {
189
- "type": "tool",
190
- "name": "发送通知",
191
- "tool": "notification_send",
192
- "args": {
193
- "title": "IP 信息",
194
- "message": "当前公网IP: {{result.body.ip}}"
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
- | `{{result}}` | 上一步完整结果 |
204
- | `{{result.body}}` | 提取 body 字段 |
205
- | `{{result.body.ip}}` | 从 body 提取嵌套字段 |
206
- | `{{result.body.data[0].name}}` | 支持数组索引 |
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
- **自动 JSON 解析**:如果字段值是 JSON 字符串,会自动解析为对象后再提取嵌套字段。
269
+ ## $.agent - Agent 操作
213
270
 
214
- ### 3. 调用扩展工具(ext_call)
271
+ ```javascript
272
+ // 与 Agent 对话
273
+ const response = await $.agent.chat({
274
+ message: 'Hello',
275
+ role: 'assistant'
276
+ });
277
+ ```
215
278
 
216
- 使用 `ext_call` 工具调用插件扩展的命令(如 skill 插件的命令):
279
+ ## $.prompt - 临时对话
217
280
 
218
- ```json
219
- {
220
- "type": "tool",
221
- "name": "调用视频创作命令",
222
- "tool": "ext_call",
223
- "args": {
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
- | `plugin` | string | 是 | 扩展名(格式:`skill:<技能名>` / `mcp:<server>` / `python:<插件名>`) |
241
- | `tool` | string | 是 | 工具名(不带扩展名前缀) |
242
- | `args` | object | 是 | Zod 结构化参数(无需拼装命令字符串) |
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
- **调用 skill 命令示例**:
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
- "type": "tool",
249
- "name": "创建项目",
250
- "tool": "ext_call",
347
+ "name": "get-balance",
348
+ "extension": "gate",
349
+ "tool": "get_balance",
251
350
  "args": {
252
- "plugin": "skill:creator",
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
- ### 4. loop - 循环步骤
265
-
266
- 支持两种循环模式:
267
-
268
- **模式一:固定次数循环**
356
+ ## 调用 skill
269
357
 
270
358
  ```json
271
359
  {
272
- "type": "loop",
273
- "name": "循环3次",
274
- "maxIterations": 3,
275
- "loopVariable": "i",
276
- "steps": [{ "type": "script", "name": "执行内容", "script": "..." }]
360
+ "name": "run-goals",
361
+ "skill": "ambient",
362
+ "command": "goals",
363
+ "args": {
364
+ "command": "-a list"
365
+ }
277
366
  }
278
367
  ```
279
368
 
280
- - `maxIterations`: 最大迭代次数
281
- - `loopVariable`: 循环计数器变量名(从 0 开始)
369
+ ---
282
370
 
283
- **模式二:数组迭代循环(over)**
371
+ # 完整示例
372
+
373
+ ## 示例 1: 获取 IP 并发送通知
284
374
 
285
375
  ```json
286
376
  {
287
- "type": "loop",
288
- "name": "处理幻灯片",
289
- "over": "input.slides",
290
- "as": "slide",
291
- "index": "slideIndex",
292
- "maxIterations": 10,
293
- "steps": [
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
- "type": "tool",
296
- "tool": "ext_call",
386
+ "name": "notify",
387
+ "tool": "notification_send",
297
388
  "args": {
298
- "plugin": "skill:creator",
299
- "tool": "addSlide",
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
- - `over`: 要迭代的数组路径(如 `input.slides`、`slide.cards`)
312
- - `as`: 每个元素的变量名(如 `slide`)
313
- - `index`: 索引变量名(如 `slideIndex`,从 0 开始)
314
- - `maxIterations`: 最大迭代次数(可选,默认为数组长度)
315
-
316
- **嵌套循环示例**:
397
+ ## 示例 2: 条件分支
317
398
 
318
399
  ```json
319
400
  {
320
- "type": "loop",
321
- "over": "input.slides",
322
- "as": "slide",
323
- "index": "slideIndex",
324
- "steps": [
401
+ "flow": "user-check",
402
+ "version": "2.0",
403
+ "input": { "userId": 1 },
404
+ "stages": [
325
405
  {
326
- "name": "处理数据卡片",
327
- "type": "loop",
328
- "over": "slide.cards",
329
- "as": "card",
330
- "index": "cardIndex",
331
- "steps": [
332
- { "type": "tool", "tool": "addCard", "args": { ... } }
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
- ### 5. condition - 条件分支
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
- ```json
344
- {
345
- "type": "condition",
346
- "name": "判断条件",
347
- "branches": [
348
- {
349
- "name": "条件1",
350
- "condition": "context.variables.value > 10",
351
- "steps": [...]
352
- },
353
- {
354
- "name": "条件2",
355
- "condition": "context.variables.value <= 10",
356
- "steps": [...]
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
- ### 6. switch - 开关分支
454
+ ## 示例 4: 调用 Skill
363
455
 
364
- 根据值匹配执行对应分支(类似编程语言的 switch-case):
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
- ```json
367
- {
368
- "type": "switch",
369
- "value": "{{status}}",
370
- "branches": [
371
- { "case": "success", "steps": [...] },
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
- - `value`: 要匹配的值,支持 `{{variable}}` 引用
380
- - `branches`: 分支数组,匹配第一个 `case` 等于 `value` 的分支
381
- - `default`: 默认分支(可选)
474
+ ## 示例 5: 调用 Gate 交易 API
382
475
 
383
- ### 7. try - 异常捕获
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
- 尝试执行,失败时执行 catch 分支:
486
+ $.push('done', function($) {
487
+ $.destroy();
488
+ });
386
489
 
387
- ```json
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
- - `try`: 尝试执行的步骤
405
- - `catch`: 捕获异常后执行的步骤
494
+ ---
406
495
 
407
- ### 8. parallel - 并行执行
496
+ # 常见问题
408
497
 
409
- 多个步骤同时并行执行:
498
+ ## Q: 如何调用 Skill?
410
499
 
500
+ **JSON:**
411
501
  ```json
412
502
  {
413
- "type": "parallel",
414
- "name": "并行获取数据",
415
- "steps": [
416
- { "type": "tool", "tool": "fetch", "args": { "url": "https://api1.com" }, "output": "data1" },
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
- ### 9. workflow - 嵌套工作流
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
- "type": "workflow",
432
- "name": "调用子工作流",
433
- "workflow": {
434
- "name": "子工作流名称",
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
- ### 10. delay - 延时步骤
536
+ ## Q: 如何引用上一个阶段的结果?
446
537
 
447
- 等待指定毫秒数:
538
+ 使用 `{{stageName}}` 格式:
448
539
 
449
540
  ```json
450
541
  {
451
- "type": "delay",
452
- "name": "等待1秒",
453
- "delayMs": 1000
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
- ### 11. sequential - 顺序步骤
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
- "type": "sequential",
464
- "name": "顺序执行",
465
- "steps": [
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
- ## sessionId 传递
621
+ **方式 3:重命名为下划线或驼峰(最佳)**
622
+ ```json
623
+ { "name": "first_stage", ... }
624
+ ```
625
+ 将 stage 名改为 `first_stage` 或 `firstStage` 即可在 script 中直接用 `first_stage.foo` 访问。
473
626
 
474
- 工作流执行时会自动获取当前 sessionId,所有 tool 调用都会使用该 sessionId,确保通知发送到当前会话。
627
+ ---
475
628
 
476
- ## 完整示例
629
+ ## Q: input 数据在 script 中拿不到怎么办?
477
630
 
478
- ### 示例:获取IP并发送通知
631
+ `input` 在 action script 的 `$.input` 和 `variables.input` 中都可用,但如果模板 `{{input.xxx}}` 解析失败:
479
632
 
480
- **推荐写法**(使用 `{{result}}` 直接引用上一步结果):
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": "get-ip-notify",
485
- "description": "获取本机IP并发送通知",
486
- "steps": [
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": "get-ip-notify-old",
514
- "steps": [
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
- "name": "process-items",
540
- "description": "循环处理列表中的每个项目",
541
- "steps": [
542
- {
543
- "type": "script",
544
- "name": "初始化列表",
545
- "outputVariable": "items",
546
- "script": "context.variables.items=['任务A','任务B','任务C']; return context.variables.items;"
547
- },
548
- {
549
- "type": "loop",
550
- "name": "处理每个任务",
551
- "maxIterations": 3,
552
- "loopVariable": "i",
553
- "steps": [
554
- {
555
- "type": "script",
556
- "name": "处理任务",
557
- "outputVariable": "processed",
558
- "script": "var item=context.variables.items[context.variables.i]; context.variables.processed=(context.variables.processed||[]); context.variables.processed.push(item+'_完成'); return item;"
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
- "name": "multi-source-fetch",
571
- "description": "并行获取多个数据源",
572
- "steps": [
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
- "type": "tool",
599
- "tool": "notification_send",
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
- ### 示例:try-catch 异常处理
609
-
825
+ **调用方式:**
610
826
  ```json
611
827
  {
612
- "name": "safe-execute",
613
- "description": "带异常处理的工作流",
614
- "steps": [
615
- {
616
- "type": "try",
617
- "name": "尝试执行",
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
- ### 示例:switch 分支处理
837
+ ### 模板 B:父工作流并行启动多个子工作流
645
838
 
646
839
  ```json
647
840
  {
648
- "name": "status-handler",
649
- "description": "根据状态分发处理",
650
- "steps": [
841
+ "flow": "orchestrator",
842
+ "version": "2.0",
843
+ "stages": [
651
844
  {
652
- "type": "script",
653
- "outputVariable": "status",
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
- "type": "switch",
658
- "value": "{{status}}",
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
- ```json
721
- // ❌ 错误 - script 没有 return
722
- { "type": "script", "script": "context.variables.count + 1;" }
858
+ ---
723
859
 
724
- // ❌ 错误 - JSON 多行字符串
725
- {
726
- "script": "
727
- const a = 10;
728
- return a + 1;
729
- "
730
- }
860
+ ## 子工作流最佳实践
731
861
 
732
- // 错误 - JSON 中有注释
733
- {
734
- "script": "return 1; // 这是注释"
735
- }
862
+ ### DO(推荐)
736
863
 
737
- // 错误 - switch 缺少 default 兜底
738
- {
739
- "type": "switch",
740
- "value": "{{status}}",
741
- "branches": [
742
- { "case": "success", "steps": [...] }
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
- ```json
751
- // 正确
752
- { "type": "script", "outputVariable": "count", "script": "return (context.variables.count||0) + 1;" }
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
- // ✅ 正确 - switch 有 default 兜底
758
- {
759
- "type": "switch",
760
- "value": "{{status}}",
761
- "branches": [
762
- { "case": "success", "steps": [...] }
763
- ],
764
- "default": { "steps": [{ "type": "tool", "tool": "notification_send", "args": {"message": "未知状态"} }] }
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
- ### 示例:ext_call + loop 调用 skill 命令处理列表
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
- **调用 skill 插件命令的正确方式**:
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
- "name": "process-videos",
775
- "description": "使用 ext_call 调用 skill 命令处理多个视频",
776
- "input": {
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
- "type": "script",
782
- "name": "初始化",
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
- "type": "loop",
788
- "name": "处理每个视频",
789
- "maxIterations": 10,
790
- "loopVariable": "i",
791
- "steps": [
792
- {
793
- "type": "condition",
794
- "name": "检查是否还有视频",
795
- "branches": [
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
- "name": "createProject",
842
- "action": "ext_call",
843
- "params": {
844
- "plugin": "skill:creator",
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]+` 解析正则,所有数据正确解析。