foliko 2.0.6 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +56 -56
- package/.lintstagedrc +7 -7
- package/.prettierignore +29 -29
- package/.prettierrc +11 -11
- package/Dockerfile +63 -63
- package/install.ps1 +129 -129
- package/install.sh +121 -121
- package/package.json +1 -1
- package/plugins/core/scheduler/index.js +1 -0
- package/plugins/core/workflow/context.js +941 -0
- package/plugins/core/workflow/engine.js +66 -0
- package/plugins/core/workflow/examples/01-basic.js +42 -0
- package/plugins/core/workflow/examples/01-basic.json +30 -0
- package/plugins/core/workflow/examples/02-choice.js +75 -0
- package/plugins/core/workflow/examples/02-choice.json +59 -0
- package/plugins/core/workflow/examples/03-chain-style.js +114 -0
- package/plugins/core/workflow/examples/03-each.json +41 -0
- package/plugins/core/workflow/examples/04-parallel.js +52 -0
- package/plugins/core/workflow/examples/04-parallel.json +51 -0
- package/plugins/core/workflow/examples/05-chain-style.json +68 -0
- package/plugins/core/workflow/examples/05-each.js +65 -0
- package/plugins/core/workflow/examples/06-script.js +82 -0
- package/plugins/core/workflow/examples/07-module-export.js +43 -0
- package/plugins/core/workflow/examples/08-default-export.js +29 -0
- package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
- package/plugins/core/workflow/examples/10-logger.js +34 -0
- package/plugins/core/workflow/examples/11-storage.js +68 -0
- package/plugins/core/workflow/examples/simple.js +77 -0
- package/plugins/core/workflow/examples/simple.json +75 -0
- package/plugins/core/workflow/index.js +122 -56
- package/plugins/core/workflow/js-runner.js +318 -0
- package/plugins/core/workflow/json-runner.js +323 -0
- package/plugins/core/workflow/stages/action.js +211 -0
- package/plugins/core/workflow/stages/choice.js +74 -0
- package/plugins/core/workflow/stages/delay.js +73 -0
- package/plugins/core/workflow/stages/each.js +123 -0
- package/plugins/core/workflow/stages/parallel.js +69 -0
- package/plugins/core/workflow/stages/try.js +142 -0
- package/plugins/executors/data-splitter/index.js +3 -2
- package/sandbox/check-context.js +5 -0
- package/sandbox/test-context.js +27 -0
- package/sandbox/test-fixes.js +40 -0
- package/sandbox/test-hello-js.js +46 -0
- package/skills/find-skills/SKILL.md +133 -133
- package/skills/workflows/SKILL.md +715 -613
- package/src/agent/chat.js +8 -1
- package/src/agent/main.js +5 -1
- package/src/cli/ui/chat-ui.js +4 -3
- package/src/common/json-safe.js +20 -0
- package/src/framework/framework.js +55 -1
- package/src/plugin/manager.js +95 -1
- package/src/utils/sandbox.js +1 -1
- package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
- package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -331
package/src/agent/chat.js
CHANGED
|
@@ -907,7 +907,14 @@ class AgentChatHandler extends EventEmitter {
|
|
|
907
907
|
*/
|
|
908
908
|
_computeToolsCacheKey() {
|
|
909
909
|
const allTools = this.agent.framework.getTools();
|
|
910
|
-
|
|
910
|
+
// Use name + inputSchema hash for stable cache key
|
|
911
|
+
// description.length is unstable across calls (changes affect cache incorrectly)
|
|
912
|
+
const { createHash } = require('crypto');
|
|
913
|
+
const toolDescriptors = allTools
|
|
914
|
+
.map((t) => `${t.name}::${t.inputSchema ? JSON.stringify(t.inputSchema) : ''}`)
|
|
915
|
+
.sort()
|
|
916
|
+
.join('|');
|
|
917
|
+
return createHash('sha256').update(toolDescriptors).digest('hex').slice(0, 32);
|
|
911
918
|
}
|
|
912
919
|
|
|
913
920
|
/**
|
package/src/agent/main.js
CHANGED
|
@@ -215,6 +215,10 @@ class MainAgent extends BaseAgent {
|
|
|
215
215
|
|
|
216
216
|
_initChatHandler() {
|
|
217
217
|
const { AgentChatHandler } = require('./chat');
|
|
218
|
+
// Remove listeners from previous chatHandler to prevent accumulation
|
|
219
|
+
if (this._chatHandler) {
|
|
220
|
+
this._chatHandler.removeAllListeners();
|
|
221
|
+
}
|
|
218
222
|
let aiClient = null;
|
|
219
223
|
const aiPlugin = this.framework.pluginManager?.get('ai');
|
|
220
224
|
if (aiPlugin) aiClient = aiPlugin.getAIClient();
|
|
@@ -410,7 +414,7 @@ class MainAgent extends BaseAgent {
|
|
|
410
414
|
const { generateEntryId, EntryTypes } = require('../session/entry');
|
|
411
415
|
sessionManager._appendEntry({
|
|
412
416
|
id: generateEntryId(sessionManager.byId), type: EntryTypes.COMPACTION,
|
|
413
|
-
summary: messages.find(m => m.role === 'assistant' && m.content
|
|
417
|
+
summary: messages.find(m => m.role === 'assistant' && typeof m.content === 'string' && m.content.startsWith('['))?.content || '',
|
|
414
418
|
firstKeptEntryId: null, tokensBefore: before, timestamp: Date.now(),
|
|
415
419
|
});
|
|
416
420
|
}
|
package/src/cli/ui/chat-ui.js
CHANGED
|
@@ -8,6 +8,7 @@ const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../utils
|
|
|
8
8
|
const { TUI, ProcessTerminal, Editor, Text, CombinedAutocompleteProvider, matchesKey, Container } = require('@earendil-works/pi-tui');
|
|
9
9
|
const { cleanResponse, logger } = require('../../../src/utils');
|
|
10
10
|
const { logEmitter } = require('../../../src/common/logger');
|
|
11
|
+
const { safeStringify } = require('../../../src/common/json-safe');
|
|
11
12
|
const { renderLine } = require('../utils/markdown');
|
|
12
13
|
const { MessageBubble } = require('./components/message-bubble');
|
|
13
14
|
const Queue=require('js-queue');
|
|
@@ -596,11 +597,11 @@ class ChatUI {
|
|
|
596
597
|
this._streamBufferTimer = setTimeout(() => this._flushStreamBuffer(), 0);
|
|
597
598
|
}
|
|
598
599
|
} else if (chunk.type === 'tool-call') {
|
|
599
|
-
const args = chunk.input ?
|
|
600
|
+
const args = chunk.input ? safeStringify(chunk.input).slice(0, 30) : '';
|
|
600
601
|
this.statusBar.tooler.show(`${folikoGold(chunk.toolName.toLocaleUpperCase())} ${chalk.gray(args+'...')}`)
|
|
601
602
|
} else if(chunk.type==='tool-result'){
|
|
602
603
|
const result = chunk.result;
|
|
603
|
-
const shortResult = typeof result === 'string' ? result.slice(0, 30) :
|
|
604
|
+
const shortResult = typeof result === 'string' ? result.slice(0, 30) : safeStringify(result).slice(0, 30);
|
|
604
605
|
this.statusBar.tooler.show(`${folikoGold(chunk.toolName.toLocaleUpperCase())} ${chalk.gray(shortResult+'...')}`)
|
|
605
606
|
|
|
606
607
|
// 如果是 edit/edit_file 的结果并且包含 diff,渲染彩色 diff
|
|
@@ -613,7 +614,7 @@ class ChatUI {
|
|
|
613
614
|
if (resultObj && resultObj.diff) {
|
|
614
615
|
const diffContent = renderDiffWithHeader(resultObj.diff, resultObj.filePath);
|
|
615
616
|
const bgColor = chalk.bgHex('#1a1a2e');
|
|
616
|
-
this._currentBotMessage.appendContent(['\n',diffContent,'\n\n'].join(''));
|
|
617
|
+
this._currentBotMessage&&this._currentBotMessage.appendContent(['\n',diffContent,'\n\n'].join(''));
|
|
617
618
|
// this.create_message(diffContent, chalk.yellow('▸ '), true, (line) => bgColor(line));
|
|
618
619
|
this.statusBar.tooler.show(`${chalk.green('[差异]')} ${folikoGold(resultObj.filePath)}`);
|
|
619
620
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safely stringify objects that may contain circular references.
|
|
5
|
+
* Replaces circular values with '[Circular]' string.
|
|
6
|
+
*/
|
|
7
|
+
function safeStringify(obj, space) {
|
|
8
|
+
const seen = new WeakSet();
|
|
9
|
+
return JSON.stringify(obj, (key, value) => {
|
|
10
|
+
if (typeof value === 'object' && value !== null) {
|
|
11
|
+
if (seen.has(value)) {
|
|
12
|
+
return '[Circular]';
|
|
13
|
+
}
|
|
14
|
+
seen.add(value);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}, space);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { safeStringify };
|
|
@@ -206,16 +206,70 @@ class Framework extends EventEmitter {
|
|
|
206
206
|
async disablePlugin(name) { await this.pluginManager.disable(name); return this; }
|
|
207
207
|
updatePluginConfig(name, config) { return this.pluginManager.updatePluginConfig(name, config); }
|
|
208
208
|
|
|
209
|
-
registerTool(tool) {
|
|
209
|
+
registerTool(tool) {
|
|
210
|
+
this.toolRegistry.register(tool);
|
|
211
|
+
// 通知所有 Agent 使 ChatHandler._aiToolsCache 失效。
|
|
212
|
+
// 🔴 修复:热重载插件时,插件调 framework.registerTool 注册新工具,
|
|
213
|
+
// 但 ChatHandler 的 _aiToolsCache 只在 agent.registerTool 被调用时清,
|
|
214
|
+
// 所以 reload 后 LLM 仍使用旧的 execute 闭包。
|
|
215
|
+
if (Array.isArray(this._agents)) {
|
|
216
|
+
for (const agent of this._agents) {
|
|
217
|
+
const ch = agent && agent._chatHandler;
|
|
218
|
+
if (ch) {
|
|
219
|
+
ch._aiToolsCache = null;
|
|
220
|
+
ch._aiToolsCacheKey = '';
|
|
221
|
+
ch._toolsTokensCacheVersion = 0;
|
|
222
|
+
if (ch._toolExecutor && ch._toolExecutor._tools) {
|
|
223
|
+
ch._toolExecutor._tools.set(tool.name, tool);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
this.emit('tool:registered-changed', tool);
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
210
231
|
tool(toolDef) {
|
|
211
232
|
const tool = { ...toolDef };
|
|
212
233
|
if (tool.input && !tool.inputSchema) tool.inputSchema = tool.input;
|
|
213
234
|
delete tool.input;
|
|
214
235
|
this.toolRegistry.register(tool);
|
|
236
|
+
// 同步使所有 Agent 的 ChatHandler 缓存失效并同步工具。
|
|
237
|
+
if (Array.isArray(this._agents)) {
|
|
238
|
+
for (const agent of this._agents) {
|
|
239
|
+
const ch = agent && agent._chatHandler;
|
|
240
|
+
if (ch) {
|
|
241
|
+
ch._aiToolsCache = null;
|
|
242
|
+
ch._aiToolsCacheKey = '';
|
|
243
|
+
ch._toolsTokensCacheVersion = 0;
|
|
244
|
+
if (ch._toolExecutor && ch._toolExecutor._tools) {
|
|
245
|
+
ch._toolExecutor._tools.set(tool.name, tool);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
215
250
|
return this;
|
|
216
251
|
}
|
|
217
252
|
getTools() { return this.toolRegistry.getAll(); }
|
|
218
253
|
|
|
254
|
+
unregisterTool(name) {
|
|
255
|
+
if (this.toolRegistry.unregister) this.toolRegistry.unregister(name);
|
|
256
|
+
if (Array.isArray(this._agents)) {
|
|
257
|
+
for (const agent of this._agents) {
|
|
258
|
+
const ch = agent && agent._chatHandler;
|
|
259
|
+
if (ch) {
|
|
260
|
+
ch._aiToolsCache = null;
|
|
261
|
+
ch._aiToolsCacheKey = '';
|
|
262
|
+
ch._toolsTokensCacheVersion = 0;
|
|
263
|
+
if (ch._toolExecutor && ch._toolExecutor._tools) {
|
|
264
|
+
ch._toolExecutor._tools.delete(name);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
this.emit('tool:unregistered-changed', { name });
|
|
270
|
+
return this;
|
|
271
|
+
}
|
|
272
|
+
|
|
219
273
|
// tools 对象别名
|
|
220
274
|
get tools() {
|
|
221
275
|
const self = this;
|
package/src/plugin/manager.js
CHANGED
|
@@ -78,7 +78,7 @@ class PluginManager {
|
|
|
78
78
|
instance: plugin,
|
|
79
79
|
status: 'registered',
|
|
80
80
|
enabled,
|
|
81
|
-
sourcePath: plugin.sourcePath
|
|
81
|
+
sourcePath: this._detectPluginSourcePath(plugin, plugin.sourcePath),
|
|
82
82
|
});
|
|
83
83
|
this._knownPlugins.add(plugin.name);
|
|
84
84
|
this._log.debug(`Plugin registered: ${plugin.name} (enabled: ${enabled})`);
|
|
@@ -89,6 +89,71 @@ class PluginManager {
|
|
|
89
89
|
return this._plugins.has(name);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Find the source file path for a plugin instance by scanning plugin
|
|
94
|
+
* class names in candidate directories (used as a fallback when the
|
|
95
|
+
* plugin was created directly via `new SomePlugin(...)` instead of going
|
|
96
|
+
* through `loadPluginFromPath`).
|
|
97
|
+
*/
|
|
98
|
+
_detectPluginSourcePath(plugin, declared) {
|
|
99
|
+
if (declared) return declared;
|
|
100
|
+
|
|
101
|
+
// For system plugins registered in plugins/core/*, the class name often
|
|
102
|
+
// matches the on-disk filename (e.g. `WorkflowPlugin` => `index.js` of
|
|
103
|
+
// `plugins/core/workflow`, `SubAgentManagerPlugin` => `.../sub-agent/index.js`).
|
|
104
|
+
const candidates = [];
|
|
105
|
+
const fs = require('fs');
|
|
106
|
+
const path = require('path');
|
|
107
|
+
try {
|
|
108
|
+
const projectRoot = path.resolve(__dirname, '..', '..');
|
|
109
|
+
candidates.push(path.resolve(projectRoot, 'plugins'));
|
|
110
|
+
} catch (_) {}
|
|
111
|
+
|
|
112
|
+
const className =
|
|
113
|
+
(plugin.constructor && plugin.constructor.name) || plugin.name;
|
|
114
|
+
if (!className) return null;
|
|
115
|
+
|
|
116
|
+
for (const base of candidates) {
|
|
117
|
+
// exact-match: <base>/<dir>/index.js with class declared inside
|
|
118
|
+
// heuristic: walk top-level plugin dirs and look for index.js exports
|
|
119
|
+
// a class named `className`.
|
|
120
|
+
let dirs = [];
|
|
121
|
+
try {
|
|
122
|
+
dirs = fs.readdirSync(base);
|
|
123
|
+
} catch (_) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
for (const sub of dirs) {
|
|
127
|
+
const subDir = path.join(base, sub);
|
|
128
|
+
let files = [];
|
|
129
|
+
try {
|
|
130
|
+
files = fs.statSync(subDir).isDirectory()
|
|
131
|
+
? fs.readdirSync(subDir)
|
|
132
|
+
: [path.basename(subDir)];
|
|
133
|
+
subDir = fs.statSync(subDir).isDirectory()
|
|
134
|
+
? subDir
|
|
135
|
+
: path.dirname(subDir);
|
|
136
|
+
} catch (_) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
if (!file.endsWith('.js')) continue;
|
|
141
|
+
const full = path.join(subDir, file);
|
|
142
|
+
try {
|
|
143
|
+
const src = fs.readFileSync(full, 'utf-8');
|
|
144
|
+
// Heuristic class declaration
|
|
145
|
+
if (
|
|
146
|
+
new RegExp(`class\\s+${className}\\s+extends\\s+Plugin`).test(src)
|
|
147
|
+
) {
|
|
148
|
+
return full;
|
|
149
|
+
}
|
|
150
|
+
} catch (_) {}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
92
157
|
get(name) {
|
|
93
158
|
const entry = this._plugins.get(name);
|
|
94
159
|
return entry ? entry.instance : null;
|
|
@@ -264,6 +329,35 @@ class PluginManager {
|
|
|
264
329
|
this._log.info(`Reloading plugin: ${name}`);
|
|
265
330
|
const plugin = entry.instance;
|
|
266
331
|
|
|
332
|
+
// Refresh the plugin's prototype with the latest source code BEFORE invoking
|
|
333
|
+
// its reload hook. Without this, plugin.reload() still runs against the
|
|
334
|
+
// prototype cached at construction time (and Node.js' require cache means
|
|
335
|
+
// the same source file is returned on disk), so changes to plugin code
|
|
336
|
+
// (e.g., workflow/_loadWorkflows) won't take effect until the process
|
|
337
|
+
// restarts.
|
|
338
|
+
if (entry.sourcePath) {
|
|
339
|
+
try {
|
|
340
|
+
delete require.cache[require.resolve(entry.sourcePath)];
|
|
341
|
+
const mod = require(entry.sourcePath);
|
|
342
|
+
// The module may export a single class or an object map.
|
|
343
|
+
const PluginClass =
|
|
344
|
+
(mod && (mod.default || mod.WorkflowPlugin || mod)) ||
|
|
345
|
+
null;
|
|
346
|
+
if (
|
|
347
|
+
PluginClass &&
|
|
348
|
+
PluginClass.prototype &&
|
|
349
|
+
Object.getPrototypeOf(plugin) !== PluginClass.prototype
|
|
350
|
+
) {
|
|
351
|
+
Object.setPrototypeOf(plugin, PluginClass.prototype);
|
|
352
|
+
}
|
|
353
|
+
} catch (err) {
|
|
354
|
+
this._log.warn(
|
|
355
|
+
`Failed to refresh prototype for plugin '${name}' from ${entry.sourcePath}:`,
|
|
356
|
+
err.message
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
267
361
|
if (typeof plugin.reload === 'function') {
|
|
268
362
|
await plugin.reload(this.framework);
|
|
269
363
|
} else {
|
package/src/utils/sandbox.js
CHANGED
|
@@ -29,7 +29,7 @@ function createSandboxContext(customContext = {}, options = {}) {
|
|
|
29
29
|
Number: { isNaN: Number.isNaN, isFinite: Number.isFinite, parseInt: Number.parseInt, parseFloat: Number.parseFloat },
|
|
30
30
|
Boolean,
|
|
31
31
|
Object: { keys: Object.keys, values: Object.values, entries: Object.entries, assign: Object.assign, create: Object.create, freeze: Object.freeze },
|
|
32
|
-
Date
|
|
32
|
+
Date,
|
|
33
33
|
RegExp,
|
|
34
34
|
Error,
|
|
35
35
|
Promise,
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
# 工作流开发调试指南
|
|
2
|
-
|
|
3
|
-
## 快速测试工作流
|
|
4
|
-
|
|
5
|
-
### 1. 使用 execute_workflow 直接测试
|
|
6
|
-
|
|
7
|
-
```javascript
|
|
8
|
-
execute_workflow({
|
|
9
|
-
workflow: {
|
|
10
|
-
name: 'test-workflow',
|
|
11
|
-
description: '测试工作流',
|
|
12
|
-
steps: [
|
|
13
|
-
{
|
|
14
|
-
id: 'step1',
|
|
15
|
-
type: 'tool',
|
|
16
|
-
tool: 'py_execute',
|
|
17
|
-
args: {
|
|
18
|
-
code: "print('Hello from Python!')",
|
|
19
|
-
},
|
|
20
|
-
outputVariable: 'result',
|
|
21
|
-
},
|
|
22
|
-
],
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### 2. JSON 格式注意事项
|
|
28
|
-
|
|
29
|
-
- ✅ 使用双引号包裹字符串
|
|
30
|
-
- ✅ 确保 JSON 语法正确
|
|
31
|
-
- ❌ 避免在 JSON 中使用未转义的特殊字符
|
|
32
|
-
- ❌ 不要在 code 中混用单引号和双引号
|
|
33
|
-
|
|
34
|
-
### 3. 常见错误
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
Unexpected identifier 'xxx'
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
- 原因:JSON 格式错误,可能是引号或逗号问题
|
|
41
|
-
- 解决:检查 JSON 语法
|
|
42
|
-
|
|
43
|
-
```
|
|
44
|
-
Unexpected end of JSON input
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
- 原因:模板字符串 `${}` 在 JSON 中导致解析错误
|
|
48
|
-
- 解决:不要在 JSON 中使用模板变量,改用纯 Python 代码
|
|
49
|
-
|
|
50
|
-
## 工作流重载问题
|
|
51
|
-
|
|
52
|
-
### 问题
|
|
53
|
-
|
|
54
|
-
添加新工作流后,`workflow_reload` 不会重新加载同名工作流。
|
|
55
|
-
|
|
56
|
-
### 解决
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
# 方法1:使用不同的文件名
|
|
60
|
-
news-dashboard-v3.json # 不会被 v2 覆盖
|
|
61
|
-
|
|
62
|
-
# 方法2:重启插件
|
|
63
|
-
reload_plugins() # 重载所有插件
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Python 执行最佳实践
|
|
67
|
-
|
|
68
|
-
### 1. 错误处理
|
|
69
|
-
|
|
70
|
-
```python
|
|
71
|
-
try:
|
|
72
|
-
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
|
73
|
-
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
74
|
-
data = resp.read().decode('utf-8')
|
|
75
|
-
print('SUCCESS')
|
|
76
|
-
except Exception as e:
|
|
77
|
-
print(f'ERROR: {e}')
|
|
78
|
-
# fallback 逻辑
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### 2. 输出格式
|
|
82
|
-
|
|
83
|
-
```python
|
|
84
|
-
# 简单输出
|
|
85
|
-
print('Status: OK')
|
|
86
|
-
print(f'Count: {len(items)}')
|
|
87
|
-
|
|
88
|
-
# JSON 输出
|
|
89
|
-
import json
|
|
90
|
-
print('---JSON_START---')
|
|
91
|
-
print(json.dumps(data, ensure_ascii=False))
|
|
92
|
-
print('---JSON_END---')
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### 3. 超时设置
|
|
96
|
-
|
|
97
|
-
```python
|
|
98
|
-
with urllib.request.urlopen(req, timeout=30) as resp: # 30秒超时
|
|
99
|
-
...
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
## 工作流步骤类型
|
|
103
|
-
|
|
104
|
-
| 类型 | 用途 | 示例 |
|
|
105
|
-
| ----------- | --------------- | ------------------------------------- |
|
|
106
|
-
| `tool` | 调用工具 | `py_execute`, `shell_exec`, `get_time` |
|
|
107
|
-
| `script` | 执行 JavaScript | 数据处理、变量操作 |
|
|
108
|
-
| `condition` | 条件分支 | if/else 逻辑 |
|
|
109
|
-
| `loop` | 循环执行 | 遍历列表 |
|
|
110
|
-
| `delay` | 延时等待 | 等待指定时间 |
|
|
111
|
-
| `message` | 消息通知 | 发送通知 |
|
|
112
|
-
| `think` | LLM 思考 | 反思/分析 |
|
|
113
|
-
|
|
114
|
-
## 工具输出变量
|
|
115
|
-
|
|
116
|
-
### 正确用法
|
|
117
|
-
|
|
118
|
-
```json
|
|
119
|
-
{
|
|
120
|
-
"id": "step1",
|
|
121
|
-
"type": "tool",
|
|
122
|
-
"tool": "get_time",
|
|
123
|
-
"outputVariable": "current_time"
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
### 访问输出
|
|
128
|
-
|
|
129
|
-
```javascript
|
|
130
|
-
// 在 script 步骤中
|
|
131
|
-
context.input.current_time
|
|
132
|
-
|
|
133
|
-
// 在下一个 tool 步骤中
|
|
134
|
-
${step1.current_time}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## 调试技巧
|
|
138
|
-
|
|
139
|
-
### 1. 打印中间变量
|
|
140
|
-
|
|
141
|
-
```python
|
|
142
|
-
print('Debug - data:', data)
|
|
143
|
-
print('Debug - length:', len(data))
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
### 2. 保存到文件
|
|
147
|
-
|
|
148
|
-
```python
|
|
149
|
-
with open('debug.log', 'w') as f:
|
|
150
|
-
f.write(str(data))
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### 3. 逐步执行
|
|
154
|
-
|
|
155
|
-
```javascript
|
|
156
|
-
{
|
|
157
|
-
"steps": [
|
|
158
|
-
{ "id": "step1", ... }, // 先测试 step1
|
|
159
|
-
{ "id": "step2", ... } // 确认 step1 成功后添加
|
|
160
|
-
]
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
## 性能优化
|
|
165
|
-
|
|
166
|
-
### 1. 合并步骤
|
|
167
|
-
|
|
168
|
-
```javascript
|
|
169
|
-
// ❌ 多个小步骤
|
|
170
|
-
{ "id": "step1", "tool": "py_execute", "code": "a = 1" }
|
|
171
|
-
{ "id": "step2", "tool": "py_execute", "code": "b = 2" }
|
|
172
|
-
|
|
173
|
-
// ✅ 单个大步骤
|
|
174
|
-
{ "id": "step1", "tool": "py_execute", "code": "a = 1; b = 2; print(a + b)" }
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### 2. 减少变量传递
|
|
178
|
-
|
|
179
|
-
```javascript
|
|
180
|
-
// ❌ 多步骤传递
|
|
181
|
-
{ "outputVariable": "data1" }
|
|
182
|
-
{ "outputVariable": "data2" }
|
|
183
|
-
|
|
184
|
-
// ✅ 单步骤完成
|
|
185
|
-
{ "outputVariable": "final_result" }
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
### 3. 超时设置
|
|
189
|
-
|
|
190
|
-
```json
|
|
191
|
-
{
|
|
192
|
-
"args": {
|
|
193
|
-
"code": "...",
|
|
194
|
-
"timeout": 60000 // 60秒超时
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
```
|