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.
- package/.claude/settings.local.json +4 -1
- package/.cli_default_systemPrompt.md +291 -0
- package/CLAUDE.md +3 -0
- package/README.md +20 -3
- package/docs/architecture.md +34 -2
- package/docs/extensions.md +199 -0
- package/docs/migration.md +100 -0
- package/docs/public-api.md +280 -24
- package/docs/usage.md +122 -30
- package/package.json +1 -1
- package/plugins/core/audit/index.js +1 -1
- package/plugins/core/default/bootstrap.js +44 -19
- package/plugins/core/python-loader/index.js +43 -25
- package/plugins/core/scheduler/index.js +1 -0
- package/plugins/core/skill-manager/PROMPT.md +6 -0
- package/plugins/core/skill-manager/index.js +402 -115
- package/plugins/core/sub-agent/PROMPT.md +10 -0
- package/plugins/core/sub-agent/index.js +36 -3
- package/plugins/core/think/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 +204 -78
- 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/PROMPT.md +13 -0
- package/plugins/executors/data-splitter/index.js +8 -6
- package/plugins/executors/extension/extension-registry.js +145 -0
- package/plugins/executors/extension/index.js +405 -437
- package/plugins/executors/extension/prompt-builder.js +359 -0
- package/plugins/executors/extension/skill-helper.js +143 -0
- package/plugins/messaging/feishu/index.js +5 -3
- package/plugins/messaging/qq/index.js +6 -4
- package/plugins/messaging/telegram/index.js +6 -3
- package/plugins/messaging/weixin/index.js +5 -3
- package/plugins/tools/PROMPT.md +26 -0
- package/plugins/tools/index.js +6 -5
- 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/foliko/AGENTS.md +196 -43
- package/skills/foliko/SKILL.md +157 -28
- package/skills/mcp/SKILL.md +77 -118
- package/skills/plugins/SKILL.md +89 -3
- package/skills/python/SKILL.md +57 -39
- package/skills/skill-guide/SKILL.md +42 -34
- package/skills/workflows/SKILL.md +753 -436
- package/src/agent/chat.js +56 -28
- package/src/agent/main.js +39 -14
- package/src/agent/prompt-registry.js +56 -16
- package/src/agent/prompts/PROMPT.md +3 -0
- package/src/agent/sub.js +1 -1
- package/src/cli/ui/chat-ui-old.js +5 -2
- package/src/cli/ui/chat-ui.js +9 -5
- package/src/common/constants.js +12 -0
- package/src/common/error-capture.js +91 -0
- package/src/common/json-safe.js +20 -0
- package/src/common/logger.js +2 -2
- package/src/context/compressor.js +6 -2
- package/src/executors/mcp-executor.js +105 -125
- package/src/framework/framework.js +78 -12
- package/src/index.js +4 -0
- package/src/plugin/base.js +908 -5
- package/src/plugin/manager.js +124 -9
- package/src/tool/schema.js +32 -9
- package/src/utils/sandbox.js +1 -1
- package/website/index.html +821 -0
- package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
- 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
|
-
|
|
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,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
|
-
"
|
|
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
|
+
## 阶段类型
|
|
58
61
|
|
|
62
|
+
### action - 执行工具或脚本
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
308
|
+
## $.storage - 键值存储
|
|
148
309
|
|
|
149
|
-
|
|
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
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"tool": "
|
|
347
|
+
"name": "get-balance",
|
|
348
|
+
"extension": "gate",
|
|
349
|
+
"tool": "get_balance",
|
|
156
350
|
"args": {
|
|
157
|
-
"
|
|
158
|
-
"message": "当前公网IP: {{currentIp}}"
|
|
351
|
+
"currency": "USDT"
|
|
159
352
|
}
|
|
160
353
|
}
|
|
161
354
|
```
|
|
162
355
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
上一步骤的结果自动保存在 `{{result}}` 中,可以直接提取字段:
|
|
356
|
+
## 调用 skill
|
|
166
357
|
|
|
167
358
|
```json
|
|
168
359
|
{
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
371
|
+
# 完整示例
|
|
203
372
|
|
|
204
|
-
|
|
373
|
+
## 示例 1: 获取 IP 并发送通知
|
|
205
374
|
|
|
206
375
|
```json
|
|
207
376
|
{
|
|
208
|
-
"
|
|
209
|
-
"
|
|
210
|
-
"
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
217
|
-
- `loopVariable`: 循环计数器变量名(从 0 开始)
|
|
218
|
-
|
|
219
|
-
### 4. condition - 条件分支
|
|
220
|
-
|
|
221
|
-
根据条件选择执行分支:
|
|
397
|
+
## 示例 2: 条件分支
|
|
222
398
|
|
|
223
399
|
```json
|
|
224
400
|
{
|
|
225
|
-
"
|
|
226
|
-
"
|
|
227
|
-
"
|
|
401
|
+
"flow": "user-check",
|
|
402
|
+
"version": "2.0",
|
|
403
|
+
"input": { "userId": 1 },
|
|
404
|
+
"stages": [
|
|
228
405
|
{
|
|
229
|
-
"name": "
|
|
230
|
-
"
|
|
231
|
-
"
|
|
406
|
+
"name": "fetchUser",
|
|
407
|
+
"tool": "fetch",
|
|
408
|
+
"args": { "url": "https://api.example.com/users/{{input.userId}}" }
|
|
232
409
|
},
|
|
233
410
|
{
|
|
234
|
-
"name": "
|
|
235
|
-
"
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
454
|
+
## 示例 4: 调用 Skill
|
|
245
455
|
|
|
246
|
-
```
|
|
247
|
-
{
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
260
|
-
- `branches`: 分支数组,匹配第一个 `case` 等于 `value` 的分支
|
|
261
|
-
- `default`: 默认分支(可选)
|
|
474
|
+
## 示例 5: 调用 Gate 交易 API
|
|
262
475
|
|
|
263
|
-
|
|
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
|
-
|
|
486
|
+
$.push('done', function($) {
|
|
487
|
+
$.destroy();
|
|
488
|
+
});
|
|
266
489
|
|
|
267
|
-
|
|
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
|
-
|
|
285
|
-
- `catch`: 捕获异常后执行的步骤
|
|
494
|
+
---
|
|
286
495
|
|
|
287
|
-
|
|
496
|
+
# 常见问题
|
|
288
497
|
|
|
289
|
-
|
|
498
|
+
## Q: 如何调用 Skill?
|
|
290
499
|
|
|
500
|
+
**JSON:**
|
|
291
501
|
```json
|
|
292
502
|
{
|
|
293
|
-
"
|
|
294
|
-
"
|
|
295
|
-
"
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
312
|
-
"
|
|
313
|
-
"
|
|
314
|
-
|
|
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
|
-
|
|
536
|
+
## Q: 如何引用上一个阶段的结果?
|
|
326
537
|
|
|
327
|
-
|
|
538
|
+
使用 `{{stageName}}` 格式:
|
|
328
539
|
|
|
329
540
|
```json
|
|
330
541
|
{
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
"
|
|
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
|
-
|
|
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
|
-
"
|
|
344
|
-
"
|
|
345
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
639
|
+
## Q: parallel 的输出结果如何访问?
|
|
359
640
|
|
|
360
|
-
|
|
641
|
+
parallel 返回的是按 stage name 索引的对象(非数组):
|
|
361
642
|
|
|
362
643
|
```json
|
|
363
644
|
{
|
|
364
|
-
"name": "
|
|
365
|
-
"
|
|
366
|
-
|
|
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": "
|
|
394
|
-
"
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
"
|
|
451
|
-
"
|
|
452
|
-
"
|
|
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
|
-
"
|
|
479
|
-
"
|
|
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
|
-
|
|
489
|
-
|
|
825
|
+
**调用方式:**
|
|
490
826
|
```json
|
|
491
827
|
{
|
|
492
|
-
"name": "
|
|
493
|
-
"
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
###
|
|
837
|
+
### 模板 B:父工作流并行启动多个子工作流
|
|
525
838
|
|
|
526
839
|
```json
|
|
527
840
|
{
|
|
528
|
-
"
|
|
529
|
-
"
|
|
530
|
-
"
|
|
841
|
+
"flow": "orchestrator",
|
|
842
|
+
"version": "2.0",
|
|
843
|
+
"stages": [
|
|
531
844
|
{
|
|
532
|
-
"
|
|
533
|
-
"
|
|
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
|
-
"
|
|
538
|
-
"
|
|
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
|
-
|
|
601
|
-
// ❌ 错误 - script 没有 return
|
|
602
|
-
{ "type": "script", "script": "context.variables.count + 1;" }
|
|
858
|
+
---
|
|
603
859
|
|
|
604
|
-
|
|
605
|
-
{
|
|
606
|
-
"script": "
|
|
607
|
-
const a = 10;
|
|
608
|
-
return a + 1;
|
|
609
|
-
"
|
|
610
|
-
}
|
|
860
|
+
## 子工作流最佳实践
|
|
611
861
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
## 完整实战示例:6 个子工作流并行 orchestrator
|
|
927
|
+
|
|
928
|
+
```json
|
|
618
929
|
{
|
|
619
|
-
"
|
|
620
|
-
"
|
|
621
|
-
"
|
|
622
|
-
{
|
|
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
|
-
"
|
|
640
|
-
"
|
|
641
|
-
"
|
|
642
|
-
|
|
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]+` 解析正则,所有数据正确解析。
|