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.
- package/.claude/settings.local.json +2 -1
- package/docs/public-api.md +3 -3
- package/examples/workflow.js +62 -57
- package/package.json +2 -3
- package/plugins/core/scheduler/CHANGELOG.md +31 -0
- package/plugins/core/scheduler/index.js +576 -638
- 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 +124 -58
- 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/plugins/memory/index.js +10 -3
- 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/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 +58 -2
- package/src/index.js +10 -0
- package/src/plugin/base.js +7 -4
- 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 };
|
|
@@ -174,7 +174,9 @@ class Framework extends EventEmitter {
|
|
|
174
174
|
agent._systemPromptDirty = true;
|
|
175
175
|
}
|
|
176
176
|
};
|
|
177
|
-
|
|
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) {
|
|
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');
|
package/src/plugin/base.js
CHANGED
|
@@ -254,13 +254,16 @@ class Plugin {
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/**
|
|
257
|
-
* 注册工作流到
|
|
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
|
-
*
|
|
263
|
-
*
|
|
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() {
|
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
|
-
```
|