foliko 2.0.5 → 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.
Files changed (93) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/.cli_default_systemPrompt.md +291 -0
  3. package/CLAUDE.md +3 -0
  4. package/README.md +20 -3
  5. package/docs/architecture.md +34 -2
  6. package/docs/extensions.md +199 -0
  7. package/docs/migration.md +100 -0
  8. package/docs/public-api.md +280 -24
  9. package/docs/usage.md +122 -30
  10. package/package.json +1 -1
  11. package/plugins/core/audit/index.js +1 -1
  12. package/plugins/core/default/bootstrap.js +44 -19
  13. package/plugins/core/python-loader/index.js +43 -25
  14. package/plugins/core/scheduler/index.js +1 -0
  15. package/plugins/core/skill-manager/PROMPT.md +6 -0
  16. package/plugins/core/skill-manager/index.js +402 -115
  17. package/plugins/core/sub-agent/PROMPT.md +10 -0
  18. package/plugins/core/sub-agent/index.js +36 -3
  19. package/plugins/core/think/index.js +1 -0
  20. package/plugins/core/workflow/context.js +941 -0
  21. package/plugins/core/workflow/engine.js +66 -0
  22. package/plugins/core/workflow/examples/01-basic.js +42 -0
  23. package/plugins/core/workflow/examples/01-basic.json +30 -0
  24. package/plugins/core/workflow/examples/02-choice.js +75 -0
  25. package/plugins/core/workflow/examples/02-choice.json +59 -0
  26. package/plugins/core/workflow/examples/03-chain-style.js +114 -0
  27. package/plugins/core/workflow/examples/03-each.json +41 -0
  28. package/plugins/core/workflow/examples/04-parallel.js +52 -0
  29. package/plugins/core/workflow/examples/04-parallel.json +51 -0
  30. package/plugins/core/workflow/examples/05-chain-style.json +68 -0
  31. package/plugins/core/workflow/examples/05-each.js +65 -0
  32. package/plugins/core/workflow/examples/06-script.js +82 -0
  33. package/plugins/core/workflow/examples/07-module-export.js +43 -0
  34. package/plugins/core/workflow/examples/08-default-export.js +29 -0
  35. package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
  36. package/plugins/core/workflow/examples/10-logger.js +34 -0
  37. package/plugins/core/workflow/examples/11-storage.js +68 -0
  38. package/plugins/core/workflow/examples/simple.js +77 -0
  39. package/plugins/core/workflow/examples/simple.json +75 -0
  40. package/plugins/core/workflow/index.js +204 -78
  41. package/plugins/core/workflow/js-runner.js +318 -0
  42. package/plugins/core/workflow/json-runner.js +323 -0
  43. package/plugins/core/workflow/stages/action.js +211 -0
  44. package/plugins/core/workflow/stages/choice.js +74 -0
  45. package/plugins/core/workflow/stages/delay.js +73 -0
  46. package/plugins/core/workflow/stages/each.js +123 -0
  47. package/plugins/core/workflow/stages/parallel.js +69 -0
  48. package/plugins/core/workflow/stages/try.js +142 -0
  49. package/plugins/executors/data-splitter/PROMPT.md +13 -0
  50. package/plugins/executors/data-splitter/index.js +8 -6
  51. package/plugins/executors/extension/extension-registry.js +145 -0
  52. package/plugins/executors/extension/index.js +405 -437
  53. package/plugins/executors/extension/prompt-builder.js +359 -0
  54. package/plugins/executors/extension/skill-helper.js +143 -0
  55. package/plugins/messaging/feishu/index.js +5 -3
  56. package/plugins/messaging/qq/index.js +6 -4
  57. package/plugins/messaging/telegram/index.js +6 -3
  58. package/plugins/messaging/weixin/index.js +5 -3
  59. package/plugins/tools/PROMPT.md +26 -0
  60. package/plugins/tools/index.js +6 -5
  61. package/sandbox/check-context.js +5 -0
  62. package/sandbox/test-context.js +27 -0
  63. package/sandbox/test-fixes.js +40 -0
  64. package/sandbox/test-hello-js.js +46 -0
  65. package/skills/foliko/AGENTS.md +196 -43
  66. package/skills/foliko/SKILL.md +157 -28
  67. package/skills/mcp/SKILL.md +77 -118
  68. package/skills/plugins/SKILL.md +89 -3
  69. package/skills/python/SKILL.md +57 -39
  70. package/skills/skill-guide/SKILL.md +42 -34
  71. package/skills/workflows/SKILL.md +753 -436
  72. package/src/agent/chat.js +56 -28
  73. package/src/agent/main.js +39 -14
  74. package/src/agent/prompt-registry.js +56 -16
  75. package/src/agent/prompts/PROMPT.md +3 -0
  76. package/src/agent/sub.js +1 -1
  77. package/src/cli/ui/chat-ui-old.js +5 -2
  78. package/src/cli/ui/chat-ui.js +9 -5
  79. package/src/common/constants.js +12 -0
  80. package/src/common/error-capture.js +91 -0
  81. package/src/common/json-safe.js +20 -0
  82. package/src/common/logger.js +2 -2
  83. package/src/context/compressor.js +6 -2
  84. package/src/executors/mcp-executor.js +105 -125
  85. package/src/framework/framework.js +78 -12
  86. package/src/index.js +4 -0
  87. package/src/plugin/base.js +908 -5
  88. package/src/plugin/manager.js +124 -9
  89. package/src/tool/schema.js +32 -9
  90. package/src/utils/sandbox.js +1 -1
  91. package/website/index.html +821 -0
  92. package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
  93. package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -391
@@ -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,607 +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
+ ## 阶段类型
58
61
 
62
+ ### action - 执行工具或脚本
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
+ });
162
+ ```
163
+
164
+ ## 支持的导出格式
165
+
166
+ ```javascript
167
+ // 格式 1: FLOW 包装器(推荐)
168
+ FLOW(function($) { ... });
169
+
170
+ // 格式 2: module.exports
171
+ module.exports = function($) { ... };
172
+
173
+ // 格式 3: module.exports.default
174
+ module.exports.default = function($) { ... };
175
+ ```
176
+
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
+ });
214
+
215
+ // 接收多个参数
216
+ $.push('step2', function($, user, config) {
217
+ console.log(user, config);
218
+ });
219
+ ```
220
+
221
+ ## $.tool - 工具调用
222
+
223
+ ```javascript
224
+ // 执行工具
225
+ await $.tool.execute('fetch', { url: 'https://api.example.com' });
226
+
227
+ // 列出所有工具
228
+ const tools = $.tool.list();
229
+
230
+ // 获取工具定义
231
+ const tool = $.tool.get('fetch');
232
+ ```
233
+
234
+ ## $.extension - 扩展调用
235
+
236
+ ```javascript
237
+ // 调用 Gate 交易 API
238
+ const balance = await $.extension.execute('gate', 'get_balance', {
239
+ currency: 'USDT'
240
+ });
241
+
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
+ });
258
+ ```
259
+
260
+ ## $.workflow - 子工作流
261
+
262
+ ```javascript
263
+ // 执行子工作流
264
+ const result = await $.workflow.execute('sub-workflow', {
265
+ input: { id: 123 }
266
+ });
137
267
  ```
138
268
 
139
- **参数说明**:
269
+ ## $.agent - Agent 操作
140
270
 
141
- | 字段 | 类型 | 必填 | 说明 |
142
- | ---------------- | ------ | ---- | ------------------ |
143
- | `tool` | string | 是 | 工具名称 |
144
- | `args` | object | 否 | 工具参数 |
145
- | `outputVariable` | string | 否 | 结果保存到的变量名 |
271
+ ```javascript
272
+ // Agent 对话
273
+ const response = await $.agent.chat({
274
+ message: 'Hello',
275
+ role: 'assistant'
276
+ });
277
+ ```
278
+
279
+ ## $.prompt - 临时对话
280
+
281
+ ```javascript
282
+ // 发送 prompt
283
+ const response = await $.prompt.send({
284
+ message: 'What is 2+2?',
285
+ role: 'assistant'
286
+ });
287
+ ```
288
+
289
+ ## $.logger - 日志记录
290
+
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');
299
+
300
+ // 使用自定义级别
301
+ $.logger.log('info', 'Custom level message');
302
+
303
+ // 创建子日志器
304
+ const childLogger = $.logger.child('my-module');
305
+ childLogger.info('来自子日志器的消息');
306
+ ```
146
307
 
147
- **args 中支持变量引用**:
308
+ ## $.storage - 键值存储
148
309
 
149
- 使用 `{{variableName}}` 语法引用 context.variables 中的变量:
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
150
344
 
151
345
  ```json
152
346
  {
153
- "type": "tool",
154
- "name": "发送通知",
155
- "tool": "notification_send",
347
+ "name": "get-balance",
348
+ "extension": "gate",
349
+ "tool": "get_balance",
156
350
  "args": {
157
- "title": "IP 信息",
158
- "message": "当前公网IP: {{currentIp}}"
351
+ "currency": "USDT"
159
352
  }
160
353
  }
161
354
  ```
162
355
 
163
- **重要:使用 `{{result}}` 引用上一步结果(推荐)**
164
-
165
- 上一步骤的结果自动保存在 `{{result}}` 中,可以直接提取字段:
356
+ ## 调用 skill
166
357
 
167
358
  ```json
168
359
  {
169
- "type": "tool",
170
- "name": "获取IP",
171
- "tool": "fetch",
172
- "args": {
173
- "url": "https://api.ipify.org?format=json"
174
- }
175
- },
176
- {
177
- "type": "tool",
178
- "name": "发送通知",
179
- "tool": "notification_send",
360
+ "name": "run-goals",
361
+ "skill": "ambient",
362
+ "command": "goals",
180
363
  "args": {
181
- "title": "IP 信息",
182
- "message": "当前公网IP: {{result.body.ip}}"
364
+ "command": "-a list"
183
365
  }
184
366
  }
185
367
  ```
186
368
 
187
- 支持的引用格式:
188
-
189
- | 格式 | 说明 |
190
- | ------------------------------ | -------------------------- |
191
- | `{{result}}` | 上一步完整结果 |
192
- | `{{result.body}}` | 提取 body 字段 |
193
- | `{{result.body.ip}}` | 从 body 提取嵌套字段 |
194
- | `{{result.body.data[0].name}}` | 支持数组索引 |
195
- | `{{lastResult}}` | result 的别名 |
196
- | `{{lastResult.error}}` | 从错误对象提取信息 |
197
- | `{{variables.xxx}}` | 显式引用 context.variables |
198
- | `{{input.xxx}}` | 引用工作流输入参数 |
199
-
200
- **自动 JSON 解析**:如果字段值是 JSON 字符串,会自动解析为对象后再提取嵌套字段。
369
+ ---
201
370
 
202
- ### 3. loop - 循环步骤
371
+ # 完整示例
203
372
 
204
- 重复执行一组步骤:
373
+ ## 示例 1: 获取 IP 并发送通知
205
374
 
206
375
  ```json
207
376
  {
208
- "type": "loop",
209
- "name": "循环3次",
210
- "maxIterations": 3,
211
- "loopVariable": "i",
212
- "steps": [{ "type": "script", "name": "执行内容", "script": "..." }]
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
+ },
385
+ {
386
+ "name": "notify",
387
+ "tool": "notification_send",
388
+ "args": {
389
+ "title": "IP 信息",
390
+ "message": "当前公网IP: {{fetch-ip.body.ip}}"
391
+ }
392
+ }
393
+ ]
213
394
  }
214
395
  ```
215
396
 
216
- - `maxIterations`: 最大迭代次数
217
- - `loopVariable`: 循环计数器变量名(从 0 开始)
218
-
219
- ### 4. condition - 条件分支
220
-
221
- 根据条件选择执行分支:
397
+ ## 示例 2: 条件分支
222
398
 
223
399
  ```json
224
400
  {
225
- "type": "condition",
226
- "name": "判断条件",
227
- "branches": [
401
+ "flow": "user-check",
402
+ "version": "2.0",
403
+ "input": { "userId": 1 },
404
+ "stages": [
228
405
  {
229
- "name": "条件1",
230
- "condition": "context.variables.value > 10",
231
- "steps": [...]
406
+ "name": "fetchUser",
407
+ "tool": "fetch",
408
+ "args": { "url": "https://api.example.com/users/{{input.userId}}" }
232
409
  },
233
410
  {
234
- "name": "条件2",
235
- "condition": "context.variables.value <= 10",
236
- "steps": [...]
237
- }
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": "未知状态" } }
238
421
  ]
239
422
  }
240
423
  ```
241
424
 
242
- ### 5. switch - 开关分支
425
+ ## 示例 3: JS 格式基本用法
426
+
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
+ });
436
+
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
+ });
452
+ ```
243
453
 
244
- 根据值匹配执行对应分支(类似编程语言的 switch-case):
454
+ ## 示例 4: 调用 Skill
245
455
 
246
- ```json
247
- {
248
- "type": "switch",
249
- "value": "{{status}}",
250
- "branches": [
251
- { "case": "success", "steps": [...] },
252
- { "case": "error", "steps": [...] },
253
- { "case": "pending", "steps": [...] }
254
- ],
255
- "default": { "steps": [...] }
256
- }
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
+ });
465
+
466
+ $.push('done', function($) {
467
+ $.destroy();
468
+ });
469
+
470
+ $.next('start');
471
+ });
257
472
  ```
258
473
 
259
- - `value`: 要匹配的值,支持 `{{variable}}` 引用
260
- - `branches`: 分支数组,匹配第一个 `case` 等于 `value` 的分支
261
- - `default`: 默认分支(可选)
474
+ ## 示例 5: 调用 Gate 交易 API
262
475
 
263
- ### 6. 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
+ });
264
485
 
265
- 尝试执行,失败时执行 catch 分支:
486
+ $.push('done', function($) {
487
+ $.destroy();
488
+ });
266
489
 
267
- ```json
268
- {
269
- "type": "try",
270
- "name": "尝试执行",
271
- "try": {
272
- "steps": [
273
- { "type": "tool", "tool": "可能失败的工具", "args": {...} }
274
- ]
275
- },
276
- "catch": {
277
- "steps": [
278
- { "type": "tool", "tool": "notification_send", "args": {"message": "执行失败: {{lastError}}"} }
279
- ]
280
- }
281
- }
490
+ $.next('check-balance');
491
+ });
282
492
  ```
283
493
 
284
- - `try`: 尝试执行的步骤
285
- - `catch`: 捕获异常后执行的步骤
494
+ ---
286
495
 
287
- ### 7. parallel - 并行执行
496
+ # 常见问题
288
497
 
289
- 多个步骤同时并行执行:
498
+ ## Q: 如何调用 Skill?
290
499
 
500
+ **JSON:**
291
501
  ```json
292
502
  {
293
- "type": "parallel",
294
- "name": "并行获取数据",
295
- "steps": [
296
- { "type": "tool", "tool": "fetch", "args": { "url": "https://api1.com" }, "output": "data1" },
297
- { "type": "tool", "tool": "fetch", "args": { "url": "https://api2.com" }, "output": "data2" },
298
- { "type": "tool", "tool": "fetch", "args": { "url": "https://api3.com" }, "output": "data3" }
299
- ]
503
+ "name": "run-goals",
504
+ "skill": "ambient",
505
+ "command": "goals",
506
+ "args": { "command": "-a list" }
300
507
  }
301
508
  ```
302
509
 
303
- 返回数组形式的执行结果。
304
-
305
- ### 8. workflow - 嵌套工作流
510
+ **JS:**
511
+ ```javascript
512
+ const result = await $.skill.execute('ambient', 'goals', {
513
+ command: '-a list'
514
+ });
515
+ ```
306
516
 
307
- 在一个工作流中调用另一个工作流:
517
+ ## Q: 如何调用 Gate 交易 API?
308
518
 
519
+ **JSON:**
309
520
  ```json
310
521
  {
311
- "type": "workflow",
312
- "name": "调用子工作流",
313
- "workflow": {
314
- "name": "子工作流名称",
315
- "steps": [...]
316
- },
317
- "input": {
318
- "param1": "{{parentVar}}"
319
- }
522
+ "name": "get-balance",
523
+ "extension": "gate",
524
+ "tool": "get_balance",
525
+ "args": { "currency": "USDT" }
320
526
  }
321
527
  ```
322
528
 
323
- 子工作流的输出会合并到父工作流的上下文变量中。
529
+ **JS:**
530
+ ```javascript
531
+ const balance = await $.extension.execute('gate', 'get_balance', {
532
+ currency: 'USDT'
533
+ });
534
+ ```
324
535
 
325
- ### 9. delay - 延时步骤
536
+ ## Q: 如何引用上一个阶段的结果?
326
537
 
327
- 等待指定毫秒数:
538
+ 使用 `{{stageName}}` 格式:
328
539
 
329
540
  ```json
330
541
  {
331
- "type": "delay",
332
- "name": "等待1秒",
333
- "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}}" }
334
550
  }
335
551
  ```
336
552
 
337
- ### 10. 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: 如何结束工作流?
597
+
598
+ ```javascript
599
+ $.destroy();
600
+ ```
601
+
602
+ ## Q: stage 名含连字符(-)在 script 中无法访问怎么办?
338
603
 
339
- 将多个步骤组合为顺序执行(可嵌套使用):
604
+ JS 标识符不允许使用连字符(如 `first-stage` 不是合法变量名)。如果 stage 名含 `-`,在 script 中不能写 `first-stage.foo`,只能用以下方式访问:
340
605
 
606
+ **方式 1:通过 `$.variables` 访问(推荐)**
607
+ ```javascript
608
+ // stage 名 "first-stage" 的结果通过 variables 访问
609
+ const result = variables['first-stage'];
610
+ ```
611
+
612
+ **方式 2:通过模板引用(推荐)**
341
613
  ```json
342
614
  {
343
- "type": "sequential",
344
- "name": "顺序执行",
345
- "steps": [
346
- { "type": "script", "name": "步骤1", "script": "return 1;" },
347
- { "type": "script", "name": "步骤2", "script": "return 2;" }
348
- ]
615
+ "name": "use-result",
616
+ "tool": "log_event",
617
+ "args": { "data": "{{first-stage.body}}" }
349
618
  }
350
619
  ```
351
620
 
352
- ## sessionId 传递
621
+ **方式 3:重命名为下划线或驼峰(最佳)**
622
+ ```json
623
+ { "name": "first_stage", ... }
624
+ ```
625
+ 将 stage 名改为 `first_stage` 或 `firstStage` 即可在 script 中直接用 `first_stage.foo` 访问。
626
+
627
+ ---
628
+
629
+ ## Q: input 数据在 script 中拿不到怎么办?
630
+
631
+ `input` 在 action script 的 `$.input` 和 `variables.input` 中都可用,但如果模板 `{{input.xxx}}` 解析失败:
353
632
 
354
- 工作流执行时会自动获取当前 sessionId,所有 tool 调用都会使用该 sessionId,确保通知发送到当前会话。
633
+ 1. 确认 input 已通过 `execute_workflow` 的 `input` 参数传入
634
+ 2. 在 script 中用 `console.log(input)` 调试
635
+ 3. 引用时用 `{{input.userId}}` 而非 `{{input["userId"]}}`
355
636
 
356
- ## 完整示例
637
+ ---
357
638
 
358
- ### 示例:获取IP并发送通知
639
+ ## Q: parallel 的输出结果如何访问?
359
640
 
360
- **推荐写法**(使用 `{{result}}` 直接引用上一步结果):
641
+ parallel 返回的是按 stage name 索引的对象(非数组):
361
642
 
362
643
  ```json
363
644
  {
364
- "name": "get-ip-notify",
365
- "description": "获取本机IP并发送通知",
366
- "steps": [
367
- {
368
- "type": "tool",
369
- "name": "获取IP信息",
370
- "tool": "fetch",
371
- "args": {
372
- "url": "https://api.ipify.org?format=json",
373
- "proxy": true
374
- }
375
- },
376
- {
377
- "type": "tool",
378
- "name": "发送通知",
379
- "tool": "notification_send",
380
- "args": {
381
- "title": "IP 信息",
382
- "message": "当前公网IP: {{result.body.ip}}"
383
- }
384
- }
645
+ "name": "fetch-all",
646
+ "parallel": [
647
+ { "name": "user", "tool": "fetch", "args": { "url": "..." } },
648
+ { "name": "config", "tool": "fetch", "args": { "url": "..." } }
385
649
  ]
386
650
  }
387
651
  ```
388
652
 
389
- **旧写法**(仍支持,但不推荐):
390
-
653
+ **引用:**
391
654
  ```json
392
655
  {
393
- "name": "get-ip-notify-old",
394
- "steps": [
395
- {
396
- "type": "tool",
397
- "tool": "fetch",
398
- "args": { "url": "https://api.ipify.org?format=json" },
399
- "outputVariable": "ipResult"
400
- },
401
- {
402
- "type": "script",
403
- "outputVariable": "currentIp",
404
- "script": "var r=context.variables.ipResult; return (r&&r.success&&r.body)?(typeof r.body==='object'?r.body.ip:r.body.trim()):'获取失败';"
405
- },
406
- {
407
- "type": "tool",
408
- "tool": "notification_send",
409
- "args": { "message": "当前公网IP: {{currentIp}}" }
410
- }
411
- ]
656
+ "name": "use-results",
657
+ "tool": "log",
658
+ "args": { "user": "{{fetch-all.user.body}}", "cfg": "{{fetch-all.config.body}}" }
412
659
  }
413
660
  ```
414
661
 
415
- ### 示例:循环处理任务列表
662
+ **在 script 中:**
663
+ ```javascript
664
+ const results = fetchAll; // { user: {...}, config: {...} }
665
+ const userData = results.user;
666
+ ```
667
+
668
+ ---
669
+
670
+ ## Q: each 循环的内联字面量报错怎么办?
671
+
672
+ **两种方式都支持:**
416
673
 
417
674
  ```json
418
- {
419
- "name": "process-items",
420
- "description": "循环处理列表中的每个项目",
421
- "steps": [
422
- {
423
- "type": "script",
424
- "name": "初始化列表",
425
- "outputVariable": "items",
426
- "script": "context.variables.items=['任务A','任务B','任务C']; return context.variables.items;"
427
- },
428
- {
429
- "type": "loop",
430
- "name": "处理每个任务",
431
- "maxIterations": 3,
432
- "loopVariable": "i",
433
- "steps": [
434
- {
435
- "type": "script",
436
- "name": "处理任务",
437
- "outputVariable": "processed",
438
- "script": "var item=context.variables.items[context.variables.i]; context.variables.processed=(context.variables.processed||[]); context.variables.processed.push(item+'_完成'); return item;"
439
- }
440
- ]
441
- }
442
- ]
443
- }
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 访问
444
728
  ```
445
729
 
446
- ### 示例:并行获取多个数据源
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],避免来回修改
787
+ ```
788
+
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 一次性处理(推荐)
447
811
 
448
812
  ```json
449
813
  {
450
- "name": "multi-source-fetch",
451
- "description": "并行获取多个数据源",
452
- "steps": [
453
- {
454
- "type": "parallel",
455
- "name": "并行获取数据",
456
- "steps": [
457
- {
458
- "type": "tool",
459
- "tool": "fetch",
460
- "args": { "url": "https://api.baidu.com" },
461
- "output": "baiduData"
462
- },
463
- {
464
- "type": "tool",
465
- "tool": "fetch",
466
- "args": { "url": "https://api.bbc.com" },
467
- "output": "bbcData"
468
- },
469
- {
470
- "type": "tool",
471
- "tool": "fetch",
472
- "args": { "url": "https://api.reddit.com" },
473
- "output": "redditData"
474
- }
475
- ]
476
- },
814
+ "flow": "process-single-repo",
815
+ "version": "2.0",
816
+ "stages": [
477
817
  {
478
- "type": "tool",
479
- "tool": "notification_send",
480
- "args": {
481
- "message": "数据获取完成: 百度 {{result[0].length}} 字节, BBC {{result[1].length}} 字节"
482
- }
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() }; });"
483
820
  }
484
821
  ]
485
822
  }
486
823
  ```
487
824
 
488
- ### 示例:try-catch 异常处理
489
-
825
+ **调用方式:**
490
826
  ```json
491
827
  {
492
- "name": "safe-execute",
493
- "description": "带异常处理的工作流",
494
- "steps": [
495
- {
496
- "type": "try",
497
- "name": "尝试执行",
498
- "try": {
499
- "steps": [
500
- { "type": "tool", "tool": "fetch", "args": { "url": "https://可能失败的api.com" } }
501
- ]
502
- },
503
- "catch": {
504
- "steps": [
505
- {
506
- "type": "script",
507
- "outputVariable": "errorMsg",
508
- "script": "return 'API 调用失败,使用备用数据';"
509
- }
510
- ]
511
- }
512
- },
513
- {
514
- "type": "tool",
515
- "tool": "notification_send",
516
- "args": {
517
- "message": "结果: {{result.result || result.error}}"
518
- }
519
- }
520
- ]
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 }); }); });"
521
834
  }
522
835
  ```
523
836
 
524
- ### 示例:switch 分支处理
837
+ ### 模板 B:父工作流并行启动多个子工作流
525
838
 
526
839
  ```json
527
840
  {
528
- "name": "status-handler",
529
- "description": "根据状态分发处理",
530
- "steps": [
841
+ "flow": "orchestrator",
842
+ "version": "2.0",
843
+ "stages": [
531
844
  {
532
- "type": "script",
533
- "outputVariable": "status",
534
- "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 };"
535
847
  },
536
848
  {
537
- "type": "switch",
538
- "value": "{{status}}",
539
- "branches": [
540
- {
541
- "case": "success",
542
- "steps": [
543
- { "type": "tool", "tool": "notification_send", "args": { "message": "操作成功" } }
544
- ]
545
- },
546
- {
547
- "case": "error",
548
- "steps": [
549
- { "type": "tool", "tool": "notification_send", "args": { "message": "操作失败" } }
550
- ]
551
- },
552
- {
553
- "case": "pending",
554
- "steps": [
555
- { "type": "tool", "tool": "notification_send", "args": { "message": "操作进行中" } }
556
- ]
557
- }
558
- ],
559
- "default": {
560
- "steps": [
561
- { "type": "tool", "tool": "notification_send", "args": { "message": "未知状态" } }
562
- ]
563
- }
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); } });"
564
851
  }
565
852
  ]
566
853
  }
567
854
  ```
568
855
 
569
- ## 注意事项
570
-
571
- 1. **优先使用 `{{result}}` 引用**:上一步结果直接用 `{{result.field}}` 提取,比 script 更简洁
572
- 2. **script 必须 return**:`script` 是函数体,必须用 return 返回值
573
- 3. **JSON 中 script 单行写**:用分号分隔多个语句,不要换行
574
- 4. **JSON 中不能有注释**:注释会导致 JSON 解析失败
575
- 5. **context.variables** 在所有步骤间共享,可存储中间结果
576
- 6. **context.input** 是工作流的输入参数
577
- 7. 循环变量 `i` 从 0 开始计数
578
- 8. 条件分支的 `condition` 是 JavaScript 表达式字符串
579
- 9. **自动 JSON 解析**:`{{result.body}}` 如果是 JSON 字符串会自动解析
580
- 10. **类型自动推断**:如果步骤没有指定 `type`,会根据字段自动推断类型
581
- 11. **`${stepId.output}` 引用**:可引用指定 id 步骤的输出(兼容旧格式)
582
- 12. **`input` 字段**:可传递上一步输出给工具(兼容旧格式)
583
- 13. **try-catch 异常处理**:使用 try 包裹可能失败的步骤,catch 捕获错误
584
-
585
- ## 最佳实践
586
-
587
- 1. 为每个步骤设置有意义的 `name`
588
- 2. 使用 `outputVariable` 保存需要跨步骤共享的数据
589
- 3. 循环前确保有合理的 `maxIterations` 限制
590
- 4. 条件分支要有兜底的 `condition: "true"` 分支
591
- 5. 复杂的业务逻辑优先使用脚本步骤
592
- 6. **script 必须 return**:`script` 是函数体,必须用 return 返回值
593
- 7. **JSON 中 script 单行写**:用分号分隔多个语句,不要换行
594
- 8. **并行获取独立数据**:使用 `parallel` 同时获取多个数据源,节省时间
595
- 9. **使用 try-catch**:对可能失败的步骤使用异常捕获,避免整个工作流中断
596
- 10. **嵌套工作流**:将复杂工作流拆分为子工作流,提高复用性和可维护性
597
-
598
- **错误示例**:
856
+ **性能**:6 个子工作流并行执行,总耗时 ≈ 121ms(远低于串行执行)。
599
857
 
600
- ```json
601
- // ❌ 错误 - script 没有 return
602
- { "type": "script", "script": "context.variables.count + 1;" }
858
+ ---
603
859
 
604
- // ❌ 错误 - JSON 多行字符串
605
- {
606
- "script": "
607
- const a = 10;
608
- return a + 1;
609
- "
610
- }
860
+ ## 子工作流最佳实践
611
861
 
612
- // 错误 - JSON 中有注释
613
- {
614
- "script": "return 1; // 这是注释"
615
- }
862
+ ### DO(推荐)
863
+
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)
870
+
871
+ ### ❌ DON'T(避免)
872
+
873
+ 1. **不要假设 `$.workflow.execute()` 是同步的**:始终用 `.then()` 或包装 Promise
874
+ 2. **不要在子工作流 stage 之间共享 variables**:sandbox 隔离
875
+ 3. **不要忽略 `\d` 的失效**:会导致 regex 静默失败(返回 null 而非抛错)
876
+ 4. **不要忘记保存 Promise**:直接 `return` 异步结果会丢失
877
+ 5. **不要把数据硬编码到子工作流定义中**:用 `input` 参数注入
878
+
879
+ ---
880
+
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'
897
+ ```
898
+
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
+ ---
910
+
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) | ✅ | ✅(但需遵守相同规则) |
616
923
 
617
- // ❌ 错误 - switch 缺少 default 兜底
924
+ ---
925
+
926
+ ## 完整实战示例:6 个子工作流并行 orchestrator
927
+
928
+ ```json
618
929
  {
619
- "type": "switch",
620
- "value": "{{status}}",
621
- "branches": [
622
- { "case": "success", "steps": [...] }
930
+ "flow": "wf-orchestrator-final",
931
+ "version": "2.0",
932
+ "stages": [
933
+ {
934
+ "name": "init",
935
+ "script": "data.startTime = Date.now(); return { ok: true };"
936
+ },
937
+ {
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;"
948
+ }
623
949
  ]
624
- // 没有 default 分支,匹配不到会静默忽略
625
950
  }
626
951
  ```
627
952
 
628
- **正确示例**:
629
-
953
+ **执行结果**:
630
954
  ```json
631
- // ✅ 正确
632
- { "type": "script", "outputVariable": "count", "script": "return (context.variables.count||0) + 1;" }
633
-
634
- // ✅ 正确 - 复杂逻辑拆成多步
635
- { "type": "script", "script": "context.variables.a=10; context.variables.b=20; return context.variables.a+context.variables.b;" }
636
-
637
- // ✅ 正确 - switch 有 default 兜底
638
955
  {
639
- "type": "switch",
640
- "value": "{{status}}",
641
- "branches": [
642
- { "case": "success", "steps": [...] }
643
- ],
644
- "default": { "steps": [{ "type": "tool", "tool": "notification_send", "args": {"message": "未知状态"} }] }
956
+ "total": 3,
957
+ "byTier": { "XL": 1, "S": 2 },
958
+ "repos": ["backend", "frontend", "mobile-app"],
959
+ "durationMs": 98
645
960
  }
646
961
  ```
962
+
963
+ 3 个子工作流并行执行,总耗时 98ms。每个子工作流内部用 `[0-9]+` 解析正则,所有数据正确解析。