foliko 1.0.66 → 1.0.67
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/package.json +1 -1
- package/plugins/email.js +13 -13
- package/skills/workflow-guide/SKILL.md +74 -14
- package/src/capabilities/workflow-engine.js +25 -0
package/package.json
CHANGED
package/plugins/email.js
CHANGED
|
@@ -44,19 +44,19 @@ class EmailPlugin extends Plugin {
|
|
|
44
44
|
return this
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
if (process.env.IMAP_USER && process.env.IMAP_PASS) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
} else {
|
|
58
|
-
|
|
59
|
-
}
|
|
47
|
+
// if (process.env.IMAP_USER && process.env.IMAP_PASS) {
|
|
48
|
+
// console.log('[Email] Auto-starting email watch...')
|
|
49
|
+
// this._startEmailWatch({
|
|
50
|
+
// interval: 60,
|
|
51
|
+
// host: process.env.IMAP_HOST,
|
|
52
|
+
// port: parseInt(process.env.IMAP_PORT) || 993,
|
|
53
|
+
// user: process.env.IMAP_USER,
|
|
54
|
+
// password: process.env.IMAP_PASS,
|
|
55
|
+
// box: 'INBOX'
|
|
56
|
+
// })
|
|
57
|
+
// } else {
|
|
58
|
+
// console.log('[Email] IMAP credentials not configured, skipping auto-start')
|
|
59
|
+
// }
|
|
60
60
|
return this
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -138,6 +138,45 @@ allowed-tools: execute_workflow,reloadWorkflows
|
|
|
138
138
|
}
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
+
**重要:使用 `{{result}}` 引用上一步结果(推荐)**
|
|
142
|
+
|
|
143
|
+
上一步骤的结果自动保存在 `{{result}}` 中,可以直接提取字段:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"type": "tool",
|
|
148
|
+
"name": "获取IP",
|
|
149
|
+
"tool": "fetch",
|
|
150
|
+
"args": {
|
|
151
|
+
"url": "https://api.ipify.org?format=json"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"type": "tool",
|
|
156
|
+
"name": "发送通知",
|
|
157
|
+
"tool": "notification_send",
|
|
158
|
+
"args": {
|
|
159
|
+
"title": "IP 信息",
|
|
160
|
+
"message": "当前公网IP: {{result.body.ip}}"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
支持的引用格式:
|
|
166
|
+
|
|
167
|
+
| 格式 | 说明 |
|
|
168
|
+
|------|------|
|
|
169
|
+
| `{{result}}` | 上一步完整结果 |
|
|
170
|
+
| `{{result.body}}` | 提取 body 字段 |
|
|
171
|
+
| `{{result.body.ip}}` | 从 body 提取嵌套字段 |
|
|
172
|
+
| `{{result.body.data[0].name}}` | 支持数组索引 |
|
|
173
|
+
| `{{lastResult}}` | result 的别名 |
|
|
174
|
+
| `{{lastResult.error}}` | 从错误对象提取信息 |
|
|
175
|
+
| `{{variables.xxx}}` | 显式引用 context.variables |
|
|
176
|
+
| `{{input.xxx}}` | 引用工作流输入参数 |
|
|
177
|
+
|
|
178
|
+
**自动 JSON 解析**:如果字段值是 JSON 字符串,会自动解析为对象后再提取嵌套字段。
|
|
179
|
+
|
|
141
180
|
### 3. loop - 循环步骤
|
|
142
181
|
|
|
143
182
|
重复执行一组步骤:
|
|
@@ -215,6 +254,7 @@ allowed-tools: execute_workflow,reloadWorkflows
|
|
|
215
254
|
|
|
216
255
|
### 示例:获取IP并发送通知
|
|
217
256
|
|
|
257
|
+
**推荐写法**(使用 `{{result}}` 直接引用上一步结果):
|
|
218
258
|
```json
|
|
219
259
|
{
|
|
220
260
|
"name": "get-ip-notify",
|
|
@@ -227,23 +267,41 @@ allowed-tools: execute_workflow,reloadWorkflows
|
|
|
227
267
|
"args": {
|
|
228
268
|
"url": "https://api.ipify.org?format=json",
|
|
229
269
|
"proxy": true
|
|
230
|
-
}
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"type": "tool",
|
|
274
|
+
"name": "发送通知",
|
|
275
|
+
"tool": "notification_send",
|
|
276
|
+
"args": {
|
|
277
|
+
"title": "IP 信息",
|
|
278
|
+
"message": "当前公网IP: {{result.body.ip}}"
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**旧写法**(仍支持,但不推荐):
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"name": "get-ip-notify-old",
|
|
289
|
+
"steps": [
|
|
290
|
+
{
|
|
291
|
+
"type": "tool",
|
|
292
|
+
"tool": "fetch",
|
|
293
|
+
"args": { "url": "https://api.ipify.org?format=json" },
|
|
231
294
|
"outputVariable": "ipResult"
|
|
232
295
|
},
|
|
233
296
|
{
|
|
234
297
|
"type": "script",
|
|
235
|
-
"name": "提取IP",
|
|
236
298
|
"outputVariable": "currentIp",
|
|
237
299
|
"script": "var r=context.variables.ipResult; return (r&&r.success&&r.body)?(typeof r.body==='object'?r.body.ip:r.body.trim()):'获取失败';"
|
|
238
300
|
},
|
|
239
301
|
{
|
|
240
302
|
"type": "tool",
|
|
241
|
-
"name": "发送通知",
|
|
242
303
|
"tool": "notification_send",
|
|
243
|
-
"args": {
|
|
244
|
-
"title": "IP 信息",
|
|
245
|
-
"message": "当前公网IP: {{currentIp}}"
|
|
246
|
-
}
|
|
304
|
+
"args": { "message": "当前公网IP: {{currentIp}}" }
|
|
247
305
|
}
|
|
248
306
|
]
|
|
249
307
|
}
|
|
@@ -282,13 +340,15 @@ allowed-tools: execute_workflow,reloadWorkflows
|
|
|
282
340
|
|
|
283
341
|
## 注意事项
|
|
284
342
|
|
|
285
|
-
1.
|
|
286
|
-
2. **
|
|
287
|
-
3. **JSON
|
|
288
|
-
4. **
|
|
289
|
-
5. **context.
|
|
290
|
-
6.
|
|
291
|
-
7.
|
|
343
|
+
1. **优先使用 `{{result}}` 引用**:上一步结果直接用 `{{result.field}}` 提取,比 script 更简洁
|
|
344
|
+
2. **script 必须 return**:`script` 是函数体,必须用 return 返回值
|
|
345
|
+
3. **JSON 中 script 单行写**:用分号分隔多个语句,不要换行
|
|
346
|
+
4. **JSON 中不能有注释**:注释会导致 JSON 解析失败
|
|
347
|
+
5. **context.variables** 在所有步骤间共享,可存储中间结果
|
|
348
|
+
6. **context.input** 是工作流的输入参数
|
|
349
|
+
7. 循环变量 `i` 从 0 开始计数
|
|
350
|
+
8. 条件分支的 `condition` 是 JavaScript 表达式字符串
|
|
351
|
+
9. **自动 JSON 解析**:`{{result.body}}` 如果是 JSON 字符串会自动解析
|
|
292
352
|
|
|
293
353
|
## 最佳实践
|
|
294
354
|
|
|
@@ -328,12 +328,29 @@ class StepExecutor {
|
|
|
328
328
|
|
|
329
329
|
_resolveValue(value, context) {
|
|
330
330
|
if (typeof value === 'string') {
|
|
331
|
+
// 支持多种引用格式:
|
|
332
|
+
// {{result}} - 上一步结果 (lastResult)
|
|
333
|
+
// {{result.body.ip}} - 从上一步结果提取嵌套字段
|
|
334
|
+
// {{variables.xxx}} - 显式引用 context.variables
|
|
335
|
+
// {{context.xxx}} - 显式引用 context 根属性
|
|
336
|
+
// {{lastResult}} - 上一步结果(result 的别名)
|
|
331
337
|
return value.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (match, path) => {
|
|
338
|
+
// result 和 lastResult 都指向 context.lastResult
|
|
339
|
+
if (path === 'result' || path === 'lastResult') {
|
|
340
|
+
return context.lastResult ?? match
|
|
341
|
+
}
|
|
342
|
+
if (path.startsWith('result.') || path.startsWith('lastResult.')) {
|
|
343
|
+
const nestedPath = path.replace(/^(result|lastResult)\./, '')
|
|
344
|
+
const val = this._getNestedValue(context.lastResult, nestedPath)
|
|
345
|
+
if (val !== undefined) return val
|
|
346
|
+
return match
|
|
347
|
+
}
|
|
332
348
|
if (path.startsWith('variables.')) {
|
|
333
349
|
return this._getNestedValue(context, path) ?? match
|
|
334
350
|
} else if (path.startsWith('context.')) {
|
|
335
351
|
return this._getNestedValue(context, path) ?? match
|
|
336
352
|
} else {
|
|
353
|
+
// 先从 variables 查找,再从 context 根属性查找
|
|
337
354
|
const val = this._getNestedValue(context.variables, path)
|
|
338
355
|
if (val !== undefined) return val
|
|
339
356
|
return this._getNestedValue(context, path) ?? match
|
|
@@ -357,6 +374,14 @@ class StepExecutor {
|
|
|
357
374
|
let current = obj
|
|
358
375
|
for (const part of parts) {
|
|
359
376
|
if (current === null || current === undefined) return undefined
|
|
377
|
+
// 如果当前值是字符串,尝试解析为 JSON
|
|
378
|
+
if (typeof current === 'string' && part !== '0' && part !== 'length') {
|
|
379
|
+
try {
|
|
380
|
+
current = JSON.parse(current)
|
|
381
|
+
} catch {
|
|
382
|
+
// 解析失败,继续用原值
|
|
383
|
+
}
|
|
384
|
+
}
|
|
360
385
|
current = current[part]
|
|
361
386
|
}
|
|
362
387
|
return current
|