foliko 2.0.6 → 2.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/docs/public-api.md +3 -3
  3. package/examples/workflow.js +62 -57
  4. package/package.json +2 -3
  5. package/plugins/core/scheduler/CHANGELOG.md +31 -0
  6. package/plugins/core/scheduler/index.js +576 -638
  7. package/plugins/core/workflow/context.js +941 -0
  8. package/plugins/core/workflow/engine.js +66 -0
  9. package/plugins/core/workflow/examples/01-basic.js +42 -0
  10. package/plugins/core/workflow/examples/01-basic.json +30 -0
  11. package/plugins/core/workflow/examples/02-choice.js +75 -0
  12. package/plugins/core/workflow/examples/02-choice.json +59 -0
  13. package/plugins/core/workflow/examples/03-chain-style.js +114 -0
  14. package/plugins/core/workflow/examples/03-each.json +41 -0
  15. package/plugins/core/workflow/examples/04-parallel.js +52 -0
  16. package/plugins/core/workflow/examples/04-parallel.json +51 -0
  17. package/plugins/core/workflow/examples/05-chain-style.json +68 -0
  18. package/plugins/core/workflow/examples/05-each.js +65 -0
  19. package/plugins/core/workflow/examples/06-script.js +82 -0
  20. package/plugins/core/workflow/examples/07-module-export.js +43 -0
  21. package/plugins/core/workflow/examples/08-default-export.js +29 -0
  22. package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
  23. package/plugins/core/workflow/examples/10-logger.js +34 -0
  24. package/plugins/core/workflow/examples/11-storage.js +68 -0
  25. package/plugins/core/workflow/examples/simple.js +77 -0
  26. package/plugins/core/workflow/examples/simple.json +75 -0
  27. package/plugins/core/workflow/index.js +124 -58
  28. package/plugins/core/workflow/js-runner.js +318 -0
  29. package/plugins/core/workflow/json-runner.js +323 -0
  30. package/plugins/core/workflow/stages/action.js +211 -0
  31. package/plugins/core/workflow/stages/choice.js +74 -0
  32. package/plugins/core/workflow/stages/delay.js +73 -0
  33. package/plugins/core/workflow/stages/each.js +123 -0
  34. package/plugins/core/workflow/stages/parallel.js +69 -0
  35. package/plugins/core/workflow/stages/try.js +142 -0
  36. package/plugins/executors/data-splitter/index.js +3 -2
  37. package/plugins/memory/index.js +10 -3
  38. package/sandbox/check-context.js +5 -0
  39. package/sandbox/test-context.js +27 -0
  40. package/sandbox/test-fixes.js +40 -0
  41. package/sandbox/test-hello-js.js +46 -0
  42. package/skills/workflows/SKILL.md +715 -613
  43. package/src/agent/chat.js +8 -1
  44. package/src/agent/main.js +5 -1
  45. package/src/cli/ui/chat-ui.js +4 -3
  46. package/src/common/json-safe.js +20 -0
  47. package/src/framework/framework.js +58 -2
  48. package/src/index.js +10 -0
  49. package/src/plugin/base.js +7 -4
  50. package/src/plugin/manager.js +95 -1
  51. package/src/utils/sandbox.js +1 -1
  52. package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
  53. package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -331
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
- return allTools.map((t) => `${t.name}:${t.description ? t.description.length : 0}`).join('|');
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?.startsWith('['))?.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
  }
@@ -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 ? JSON.stringify(chunk.input).slice(0, 30) : '';
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) : JSON.stringify(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 };
@@ -174,7 +174,9 @@ class Framework extends EventEmitter {
174
174
  agent._systemPromptDirty = true;
175
175
  }
176
176
  };
177
- const REFRESH_EVENTS = ['register', 'unregister', 'clear-owner'];
177
+ // 注意:监听 invalidate 不会形成循环,因为回调内只设 dirty,
178
+ // 不会反过来调用 promptRegistry.invalidate*()
179
+ const REFRESH_EVENTS = ['register', 'unregister', 'clear-owner', 'invalidate', 'invalidate-all'];
178
180
  for (const event of REFRESH_EVENTS) {
179
181
  this.promptRegistry.on(event, invalidate);
180
182
  }
@@ -206,16 +208,70 @@ class Framework extends EventEmitter {
206
208
  async disablePlugin(name) { await this.pluginManager.disable(name); return this; }
207
209
  updatePluginConfig(name, config) { return this.pluginManager.updatePluginConfig(name, config); }
208
210
 
209
- registerTool(tool) { this.toolRegistry.register(tool); return this; }
211
+ registerTool(tool) {
212
+ this.toolRegistry.register(tool);
213
+ // 通知所有 Agent 使 ChatHandler._aiToolsCache 失效。
214
+ // 🔴 修复:热重载插件时,插件调 framework.registerTool 注册新工具,
215
+ // 但 ChatHandler 的 _aiToolsCache 只在 agent.registerTool 被调用时清,
216
+ // 所以 reload 后 LLM 仍使用旧的 execute 闭包。
217
+ if (Array.isArray(this._agents)) {
218
+ for (const agent of this._agents) {
219
+ const ch = agent && agent._chatHandler;
220
+ if (ch) {
221
+ ch._aiToolsCache = null;
222
+ ch._aiToolsCacheKey = '';
223
+ ch._toolsTokensCacheVersion = 0;
224
+ if (ch._toolExecutor && ch._toolExecutor._tools) {
225
+ ch._toolExecutor._tools.set(tool.name, tool);
226
+ }
227
+ }
228
+ }
229
+ }
230
+ this.emit('tool:registered-changed', tool);
231
+ return this;
232
+ }
210
233
  tool(toolDef) {
211
234
  const tool = { ...toolDef };
212
235
  if (tool.input && !tool.inputSchema) tool.inputSchema = tool.input;
213
236
  delete tool.input;
214
237
  this.toolRegistry.register(tool);
238
+ // 同步使所有 Agent 的 ChatHandler 缓存失效并同步工具。
239
+ if (Array.isArray(this._agents)) {
240
+ for (const agent of this._agents) {
241
+ const ch = agent && agent._chatHandler;
242
+ if (ch) {
243
+ ch._aiToolsCache = null;
244
+ ch._aiToolsCacheKey = '';
245
+ ch._toolsTokensCacheVersion = 0;
246
+ if (ch._toolExecutor && ch._toolExecutor._tools) {
247
+ ch._toolExecutor._tools.set(tool.name, tool);
248
+ }
249
+ }
250
+ }
251
+ }
215
252
  return this;
216
253
  }
217
254
  getTools() { return this.toolRegistry.getAll(); }
218
255
 
256
+ unregisterTool(name) {
257
+ if (this.toolRegistry.unregister) this.toolRegistry.unregister(name);
258
+ if (Array.isArray(this._agents)) {
259
+ for (const agent of this._agents) {
260
+ const ch = agent && agent._chatHandler;
261
+ if (ch) {
262
+ ch._aiToolsCache = null;
263
+ ch._aiToolsCacheKey = '';
264
+ ch._toolsTokensCacheVersion = 0;
265
+ if (ch._toolExecutor && ch._toolExecutor._tools) {
266
+ ch._toolExecutor._tools.delete(name);
267
+ }
268
+ }
269
+ }
270
+ }
271
+ this.emit('tool:unregistered-changed', { name });
272
+ return this;
273
+ }
274
+
219
275
  // tools 对象别名
220
276
  get tools() {
221
277
  const self = this;
package/src/index.js CHANGED
@@ -143,11 +143,21 @@ exports.Logger = Logger;
143
143
 
144
144
  const { SkillManagerPlugin, Skill, SkillMetadata } = require('../plugins/core/skill-manager');
145
145
  const { WorkflowPlugin, WorkflowEngine, StepExecutor } = require('../plugins/core/workflow');
146
+ // v2 引擎(实际执行路径走这里)。仍同时导出 v1 名字以保持向后兼容。
147
+ const { FlowEngine } = require('../plugins/core/workflow/engine');
148
+ const { JsonRunner, StageExecutor: V2StageExecutor } = require('../plugins/core/workflow/json-runner');
149
+ const { JsRunner } = require('../plugins/core/workflow/js-runner');
150
+ const { FlowContext } = require('../plugins/core/workflow/context');
146
151
  exports.SkillManagerPlugin = SkillManagerPlugin;
147
152
  exports.Skill = Skill;
148
153
  exports.SkillMetadata = SkillMetadata;
149
154
  exports.WorkflowPlugin = WorkflowPlugin;
150
155
  exports.WorkflowEngine = WorkflowEngine;
151
156
  exports.StepExecutor = StepExecutor;
157
+ exports.FlowEngine = FlowEngine;
158
+ exports.JsonRunner = JsonRunner;
159
+ exports.JsRunner = JsRunner;
160
+ exports.FlowContext = FlowContext;
161
+ exports.V2StageExecutor = V2StageExecutor;
152
162
  exports.Agent = MainAgent;
153
163
  exports.MCPExecutorPlugin = require('./executors/mcp-executor');
@@ -254,13 +254,16 @@ class Plugin {
254
254
  }
255
255
 
256
256
  /**
257
- * 注册工作流到 WorkflowEngine
257
+ * 注册工作流到 FlowEngine (v2)
258
258
  * 支持 register/remove/execute 方法
259
259
  *
260
- * 用法:
260
+ * 用法(v2 schema:flow + version + stages):
261
261
  * this.workflow.register('my-workflow', {
262
- * name: 'my-workflow',
263
- * steps: [...]
262
+ * flow: 'my-workflow',
263
+ * version: '2.0',
264
+ * stages: [
265
+ * { name: 'step1', action: 'script', script: '...' },
266
+ * ],
264
267
  * });
265
268
  */
266
269
  get workflow() {
@@ -78,7 +78,7 @@ class PluginManager {
78
78
  instance: plugin,
79
79
  status: 'registered',
80
80
  enabled,
81
- sourcePath: plugin.sourcePath || null,
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 {
@@ -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: { now: Date.now },
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
- ```