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
|
@@ -876,8 +876,8 @@ class WorkflowPlugin extends Plugin {
|
|
|
876
876
|
constructor(config = {}) {
|
|
877
877
|
super();
|
|
878
878
|
this.name = 'workflow';
|
|
879
|
-
this.version = '
|
|
880
|
-
this.description = '
|
|
879
|
+
this.version = '2.0.0';
|
|
880
|
+
this.description = '工作流引擎 v2(FlowEngine + JsonRunner + JsRunner + FlowContext)';
|
|
881
881
|
this.priority = 6;
|
|
882
882
|
this.system = true;
|
|
883
883
|
this._framework = null;
|
|
@@ -907,7 +907,11 @@ class WorkflowPlugin extends Plugin {
|
|
|
907
907
|
description: '执行指定的工作流',
|
|
908
908
|
inputSchema: z.object({
|
|
909
909
|
workflow: z.string().describe('工作流定义(JSON 或 JavaScript 代码)'),
|
|
910
|
-
|
|
910
|
+
// Use record(z.any()) rather than object({}) so callers can pass an
|
|
911
|
+
// object with arbitrary fields; default z.object({}) strips unknown
|
|
912
|
+
// keys (and here the schema is empty, so input would always end up
|
|
913
|
+
// as `{}` regardless of what was sent).
|
|
914
|
+
input: z.record(z.any()).optional().describe('工作流输入参数'),
|
|
911
915
|
}),
|
|
912
916
|
execute: async (args, framework) => {
|
|
913
917
|
// 获取当前 sessionId
|
|
@@ -930,7 +934,16 @@ class WorkflowPlugin extends Plugin {
|
|
|
930
934
|
description: '重载所有工作流,当用户添加或修改工作流后调用此工具',
|
|
931
935
|
inputSchema: z.object({}),
|
|
932
936
|
execute: async () => {
|
|
933
|
-
|
|
937
|
+
// Forward to pluginManager.reload so the framework has a chance to
|
|
938
|
+
// refresh the plugin prototype (workflow_reload used to call
|
|
939
|
+
// this.reload() directly, but instance.reload() always runs against
|
|
940
|
+
// the prototype captured at construction time, ignoring code changes
|
|
941
|
+
// until the process restarts).
|
|
942
|
+
if (typeof framework.reloadPlugin === 'function') {
|
|
943
|
+
await framework.reloadPlugin('workflow');
|
|
944
|
+
} else {
|
|
945
|
+
this.reload(this._framework);
|
|
946
|
+
}
|
|
934
947
|
return {
|
|
935
948
|
success: true,
|
|
936
949
|
data: `Workflows reloaded. Total: ${this._workflows.size}`,
|
|
@@ -994,15 +1007,48 @@ class WorkflowPlugin extends Plugin {
|
|
|
994
1007
|
let workflowDef;
|
|
995
1008
|
if (file.endsWith('.js')) {
|
|
996
1009
|
workflowDef = runWorkflowFileSafely(content, { timeout: 10000 });
|
|
1010
|
+
if (workflowDef && typeof workflowDef === 'object' && typeof workflowDef.default === 'function') {
|
|
1011
|
+
// ES module-style default export: { default: function($){...} }
|
|
1012
|
+
workflowDef = workflowDef.default;
|
|
1013
|
+
}
|
|
997
1014
|
if (typeof workflowDef === 'function') {
|
|
998
|
-
|
|
1015
|
+
// Two JS workflow patterns are supported:
|
|
1016
|
+
// 1. FLOW(function($) { $.push(...); $.next('start'); })
|
|
1017
|
+
// -> the exported value is a *factory* that must be called with engine
|
|
1018
|
+
// 2. module.exports = function($) { $.push(...); $.next('start'); }
|
|
1019
|
+
// -> the exported value is the workflow function itself (not a factory)
|
|
1020
|
+
// -> it must NOT be called here; v2 JsRunner will invoke it with $ directly.
|
|
1021
|
+
// -> FLOW() can also export a factory directly if there's no surrounding
|
|
1022
|
+
// function body (raw expression form).
|
|
1023
|
+
// We detect the factory form by looking for the literal `FLOW(` token
|
|
1024
|
+
// in the file's source. When FLOW() is detected, we invoke it as factory.
|
|
1025
|
+
if (/\bFLOW\s*\(/.test(content)) {
|
|
1026
|
+
try {
|
|
1027
|
+
workflowDef = workflowDef(this._engine || {});
|
|
1028
|
+
if (workflowDef && typeof workflowDef === 'object' && typeof workflowDef.default === 'function') {
|
|
1029
|
+
workflowDef = workflowDef.default;
|
|
1030
|
+
}
|
|
1031
|
+
} catch (err) {
|
|
1032
|
+
log.error(` Failed to call FLOW() factory for ${file}:`, err.message);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
// else: leave workflowDef as-is (it's already the workflow function)
|
|
999
1036
|
}
|
|
1000
|
-
workflowDef = workflowDef.default || workflowDef;
|
|
1001
1037
|
} else {
|
|
1002
1038
|
workflowDef = JSON.parse(content);
|
|
1003
1039
|
}
|
|
1004
|
-
|
|
1040
|
+
// Accept either:
|
|
1041
|
+
// - JSON-style workflow with stages/flow field
|
|
1042
|
+
// - JS workflow exported as a function (Total.js style: FLOW(function($){...})
|
|
1043
|
+
// or module.exports = function($){...})
|
|
1044
|
+
if (workflowDef && (workflowDef.stages || workflowDef.flow)) {
|
|
1045
|
+
this._workflows.set(workflowName, workflowDef);
|
|
1046
|
+
} else if (typeof workflowDef === 'function') {
|
|
1047
|
+
// Store the JS function directly; v2 FlowEngine detects function type
|
|
1048
|
+
// and dispatches to JsRunner.
|
|
1005
1049
|
this._workflows.set(workflowName, workflowDef);
|
|
1050
|
+
} else {
|
|
1051
|
+
log.warn(`Skipping ${file}: not a recognized workflow definition (no stages/flow, and not a function)`);
|
|
1006
1052
|
}
|
|
1007
1053
|
} catch (err) {
|
|
1008
1054
|
log.error(` Failed to load ${file}:`, err.message);
|
|
@@ -1018,9 +1064,17 @@ class WorkflowPlugin extends Plugin {
|
|
|
1018
1064
|
*/
|
|
1019
1065
|
_registerWorkflowTools() {
|
|
1020
1066
|
for (const [name, workflow] of this._workflows) {
|
|
1021
|
-
if (this._workflowTools.has(name)) continue;
|
|
1022
1067
|
const toolName = `workflow_${name}`;
|
|
1023
1068
|
const description = workflow.description || `执行工作流: ${name}`;
|
|
1069
|
+
// 注意:framework.registerTool 不做去重,导致 reload 后会有两个同名
|
|
1070
|
+
// `workflow_<name>` 工具。getTool 返回第一个(旧的)。每次注册前先清掉。
|
|
1071
|
+
if (this._framework?.toolRegistry?.unregisterTool) {
|
|
1072
|
+
try {
|
|
1073
|
+
this._framework.toolRegistry.unregisterTool(toolName);
|
|
1074
|
+
} catch (e) {
|
|
1075
|
+
// noop
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1024
1078
|
this._framework.registerTool({
|
|
1025
1079
|
name: toolName,
|
|
1026
1080
|
description,
|
|
@@ -1028,13 +1082,24 @@ class WorkflowPlugin extends Plugin {
|
|
|
1028
1082
|
input: z.object({}).optional().describe('工作流输入参数'),
|
|
1029
1083
|
}),
|
|
1030
1084
|
execute: async (args, framework) => {
|
|
1031
|
-
//
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1085
|
+
// 每次执行都从 this._workflows 重新查最新处理函数。
|
|
1086
|
+
// 这样即使 ChatHandler 缓存了上个版本的 execute,我们仍能闭包到本对象上。
|
|
1087
|
+
// 注意:ToolRegistry._tools.set 被调用后都是一个新的 execute 对象(闭包重新生成),
|
|
1088
|
+
// 旧 execute 会因为 ChatHandler._aiToolsCache 不清而一直残留。
|
|
1089
|
+
// 修复点:framework.registerTool现在会同步重设 ChatHandler 相关缓存。
|
|
1090
|
+
let liveWorkflow;
|
|
1091
|
+
try {
|
|
1092
|
+
liveWorkflow = this._workflows.get(name);
|
|
1093
|
+
if (!liveWorkflow) {
|
|
1094
|
+
return { success: false, error: `Workflow '${name}' not loaded` };
|
|
1095
|
+
}
|
|
1096
|
+
let sessionId = null;
|
|
1097
|
+
const ctx = framework?.getExecutionContext?.();
|
|
1098
|
+
if (ctx?.sessionId) sessionId = ctx.sessionId;
|
|
1099
|
+
return await this.executeWorkflow(liveWorkflow, args.input || {}, sessionId);
|
|
1100
|
+
} catch (e) {
|
|
1101
|
+
return { success: false, error: e.message, stack: e.stack };
|
|
1036
1102
|
}
|
|
1037
|
-
return await this.executeWorkflow(workflow, args.input || {}, sessionId);
|
|
1038
1103
|
},
|
|
1039
1104
|
});
|
|
1040
1105
|
this._workflowTools.set(name, toolName);
|
|
@@ -1069,10 +1134,35 @@ class WorkflowPlugin extends Plugin {
|
|
|
1069
1134
|
* 重载工作流
|
|
1070
1135
|
*/
|
|
1071
1136
|
reload(framework) {
|
|
1072
|
-
//
|
|
1073
|
-
|
|
1137
|
+
// 清除所有 workflow 相关模块的 require 缓存(action.js / engine.js / json-runner.js / context.js)
|
|
1138
|
+
// 否则即使代码已修改,Node.js 仍会使用旧的模块引用
|
|
1139
|
+
const workflowDir = path.dirname(__filename);
|
|
1140
|
+
// framework 根目录(用于清除 src/utils 等跨插件共享模块)
|
|
1141
|
+
const frameworkRoot = path.resolve(workflowDir, '..', '..', '..');
|
|
1142
|
+
const modulesToClear = [
|
|
1143
|
+
__filename,
|
|
1144
|
+
path.join(workflowDir, 'engine.js'),
|
|
1145
|
+
path.join(workflowDir, 'json-runner.js'),
|
|
1146
|
+
path.join(workflowDir, 'js-runner.js'),
|
|
1147
|
+
path.join(workflowDir, 'context.js'),
|
|
1148
|
+
path.join(workflowDir, 'stages', 'action.js'),
|
|
1149
|
+
path.join(workflowDir, 'stages', 'choice.js'),
|
|
1150
|
+
path.join(workflowDir, 'stages', 'each.js'),
|
|
1151
|
+
path.join(workflowDir, 'stages', 'parallel.js'),
|
|
1152
|
+
path.join(frameworkRoot, 'src', 'utils', 'sandbox.js'),
|
|
1153
|
+
];
|
|
1154
|
+
for (const modPath of modulesToClear) {
|
|
1155
|
+
delete require.cache[modPath];
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// 销毁 v2 引擎实例(懒加载缓存),强制下次执行时重建
|
|
1159
|
+
this._flowEngineV2 = null;
|
|
1160
|
+
|
|
1074
1161
|
this._framework = framework;
|
|
1075
1162
|
const fresh = require(__filename);
|
|
1163
|
+
// 重现现有实例的 prototype,让后续方法调用走最新代码
|
|
1164
|
+
// (实例属性如 _workflows/_workflowTools/_engine 保持,prototype 方法切换为 fresh)
|
|
1165
|
+
Object.setPrototypeOf(this, fresh.WorkflowPlugin.prototype);
|
|
1076
1166
|
// 重新创建 engine(确保使用最新代码)
|
|
1077
1167
|
this._engine = new fresh.WorkflowEngine(framework);
|
|
1078
1168
|
// 注册内置步骤类型
|
|
@@ -1087,8 +1177,12 @@ class WorkflowPlugin extends Plugin {
|
|
|
1087
1177
|
this._engine.registerStepType(fresh.StepType.DELAY, fresh.DelayStep);
|
|
1088
1178
|
this._engine.registerStepType(fresh.StepType.WORKFLOW, fresh.NestedWorkflowStep);
|
|
1089
1179
|
// 清除已注册的工具
|
|
1180
|
+
// framework.registerTool/unregisterTool 会同步重置 ChatHandler._aiToolsCache。
|
|
1090
1181
|
for (const toolName of this._workflowTools.values()) {
|
|
1091
|
-
|
|
1182
|
+
try {
|
|
1183
|
+
if (this._framework?.unregisterTool) this._framework.unregisterTool(toolName);
|
|
1184
|
+
else this._framework?.toolRegistry?.unregister?.(toolName);
|
|
1185
|
+
} catch (_) {}
|
|
1092
1186
|
}
|
|
1093
1187
|
this._workflowTools.clear();
|
|
1094
1188
|
this._workflows.clear();
|
|
@@ -1104,8 +1198,15 @@ class WorkflowPlugin extends Plugin {
|
|
|
1104
1198
|
}
|
|
1105
1199
|
/**
|
|
1106
1200
|
* 执行工作流
|
|
1201
|
+
* 支持 v2 JSON 和 JS 格式
|
|
1107
1202
|
*/
|
|
1108
1203
|
async executeWorkflow(workflowDef, input = {}, sessionId = null) {
|
|
1204
|
+
// Lazy load v2 engine to avoid circular require
|
|
1205
|
+
if (!this._flowEngineV2) {
|
|
1206
|
+
const { FlowEngine } = require('./engine');
|
|
1207
|
+
this._flowEngineV2 = new FlowEngine(this._framework);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1109
1210
|
try {
|
|
1110
1211
|
let workflow;
|
|
1111
1212
|
if (typeof workflowDef === 'string') {
|
|
@@ -1114,55 +1215,20 @@ class WorkflowPlugin extends Plugin {
|
|
|
1114
1215
|
if (this._workflows.has(workflowName)) {
|
|
1115
1216
|
workflow = this._workflows.get(workflowName);
|
|
1116
1217
|
} else {
|
|
1117
|
-
// 尝试解析 JSON
|
|
1218
|
+
// 尝试解析 JSON
|
|
1118
1219
|
try {
|
|
1119
1220
|
workflow = JSON.parse(workflowDef);
|
|
1120
1221
|
} catch {
|
|
1121
|
-
//
|
|
1122
|
-
workflow =
|
|
1222
|
+
// JSON 解析失败,视为 JS 代码
|
|
1223
|
+
workflow = workflowDef;
|
|
1123
1224
|
}
|
|
1124
1225
|
}
|
|
1125
1226
|
} else {
|
|
1126
1227
|
workflow = workflowDef;
|
|
1127
1228
|
}
|
|
1128
|
-
|
|
1129
|
-
//
|
|
1130
|
-
|
|
1131
|
-
context.variables._sessionId = sessionId;
|
|
1132
|
-
}
|
|
1133
|
-
// 执行工作流步骤
|
|
1134
|
-
if (workflow.steps && Array.isArray(workflow.steps)) {
|
|
1135
|
-
const results = [];
|
|
1136
|
-
// 按 id 存储步骤输出,供 ${id.output} 引用
|
|
1137
|
-
context.variables._stepOutputs = {};
|
|
1138
|
-
for (const stepConfig of workflow.steps) {
|
|
1139
|
-
const step = this._engine.createStep(stepConfig);
|
|
1140
|
-
const result = await step.execute(context, this._engine);
|
|
1141
|
-
results.push(result);
|
|
1142
|
-
// 如果步骤有 id,存储其输出
|
|
1143
|
-
if (stepConfig.id) {
|
|
1144
|
-
context.variables._stepOutputs[stepConfig.id] = result;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
// 只返回最后一步的结果(包含最终输出)和成功状态
|
|
1148
|
-
// 不返回所有中间结果,避免上下文超限
|
|
1149
|
-
const lastResult = results.length > 0 ? results[results.length - 1] : null;
|
|
1150
|
-
// 从 context.variables 中提取非下划线开头的用户变量
|
|
1151
|
-
// 下划线开头的是内部变量(如 _stepOutputs),不需要返回
|
|
1152
|
-
const userVariables = {};
|
|
1153
|
-
for (const [key, value] of Object.entries(context.variables)) {
|
|
1154
|
-
if (!key.startsWith('_')) {
|
|
1155
|
-
userVariables[key] = value;
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
return {
|
|
1159
|
-
success: true,
|
|
1160
|
-
stepCount: workflow.steps.length,
|
|
1161
|
-
result: lastResult,
|
|
1162
|
-
output: userVariables,
|
|
1163
|
-
};
|
|
1164
|
-
}
|
|
1165
|
-
return { success: true, output: {} };
|
|
1229
|
+
|
|
1230
|
+
// 委托给 v2 引擎执行
|
|
1231
|
+
return await this._flowEngineV2.execute(workflow, input, sessionId);
|
|
1166
1232
|
} catch (err) {
|
|
1167
1233
|
return { success: false, error: err.message };
|
|
1168
1234
|
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JS Workflow Runner - Total.js Style (Secondary)
|
|
3
|
+
* Executes workflows defined in JavaScript format using $.push(), $.next(), $.send() API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { runScriptSafely } = require('../../../src/utils/sandbox');
|
|
7
|
+
|
|
8
|
+
class JsRunner {
|
|
9
|
+
constructor(framework) {
|
|
10
|
+
this.framework = framework;
|
|
11
|
+
this._log = require('../../../src/common/logger').logger.child('JsRunner');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Execute a JS workflow
|
|
16
|
+
* @param {string|Function} workflowCode - Workflow code or exported function
|
|
17
|
+
* @param {Object} input - Input data
|
|
18
|
+
* @param {string} sessionId - Session ID for tool execution
|
|
19
|
+
* @returns {Promise<Object>} execution result
|
|
20
|
+
*/
|
|
21
|
+
async execute(workflowCode, input = {}, sessionId = null) {
|
|
22
|
+
this._log.debug('Executing JS workflow');
|
|
23
|
+
|
|
24
|
+
const { FlowContext } = require('./context');
|
|
25
|
+
const context = new FlowContext(input, sessionId);
|
|
26
|
+
context.setFramework(this.framework);
|
|
27
|
+
|
|
28
|
+
// Create stage executor and attach to context
|
|
29
|
+
const stageExecutor = new JsStageExecutor(this.framework);
|
|
30
|
+
context._stageExecutor = stageExecutor;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Get the workflow function
|
|
34
|
+
let workflowFn;
|
|
35
|
+
if (typeof workflowCode === 'function') {
|
|
36
|
+
workflowFn = workflowCode;
|
|
37
|
+
} else if (typeof workflowCode === 'string') {
|
|
38
|
+
workflowFn = this._parseWorkflow(workflowCode);
|
|
39
|
+
} else {
|
|
40
|
+
throw new Error('Workflow must be a function or string');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create the $ object for Total.js style API
|
|
44
|
+
const $ = this._createFlowApi(context, stageExecutor);
|
|
45
|
+
|
|
46
|
+
// Execute the workflow function
|
|
47
|
+
const flowPromise = workflowFn($);
|
|
48
|
+
|
|
49
|
+
// If it returns a promise, wait for it
|
|
50
|
+
if (flowPromise && typeof flowPromise.then === 'function') {
|
|
51
|
+
await flowPromise;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Execute stages until destroyed or no more pending next
|
|
55
|
+
await this._runStages(context, stageExecutor);
|
|
56
|
+
|
|
57
|
+
// Build output
|
|
58
|
+
const output = {};
|
|
59
|
+
for (const [key, value] of context._results) {
|
|
60
|
+
output[key] = value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
success: !context._destroyed,
|
|
65
|
+
output,
|
|
66
|
+
result: context._sentData || output,
|
|
67
|
+
};
|
|
68
|
+
} catch (err) {
|
|
69
|
+
this._log.error(`JS Workflow execution error: ${err.message}`);
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: err.message,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse workflow code string into a function
|
|
79
|
+
* Supports:
|
|
80
|
+
* FLOW(function($) { ... }) - Direct function
|
|
81
|
+
* module.exports = function($) { ... } - CommonJS export
|
|
82
|
+
* module.exports.default = function($) { ... } - Default export
|
|
83
|
+
*/
|
|
84
|
+
_parseWorkflow(code) {
|
|
85
|
+
const sandboxContext = {
|
|
86
|
+
console: { log() {}, error() {}, warn() {}, info() {} },
|
|
87
|
+
Math,
|
|
88
|
+
JSON,
|
|
89
|
+
Promise,
|
|
90
|
+
Array,
|
|
91
|
+
Object,
|
|
92
|
+
Date,
|
|
93
|
+
RegExp,
|
|
94
|
+
Error,
|
|
95
|
+
Symbol,
|
|
96
|
+
Map,
|
|
97
|
+
Set,
|
|
98
|
+
module: { exports: {} },
|
|
99
|
+
exports: {},
|
|
100
|
+
FlowContext: require('./context').FlowContext,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const ctx = require('vm').createContext(sandboxContext);
|
|
105
|
+
|
|
106
|
+
// Check which format the code uses
|
|
107
|
+
const isModuleExport = code.includes('module.exports');
|
|
108
|
+
let fn;
|
|
109
|
+
|
|
110
|
+
if (isModuleExport) {
|
|
111
|
+
// CommonJS export format: module.exports = function($) { ... }
|
|
112
|
+
const wrapped = `
|
|
113
|
+
(function(module, exports, FlowContext) {
|
|
114
|
+
${code}
|
|
115
|
+
return module.exports;
|
|
116
|
+
})
|
|
117
|
+
`;
|
|
118
|
+
const factory = require('vm').runInContext(wrapped, ctx, { timeout: 5000 });
|
|
119
|
+
fn = factory(ctx.module, ctx.exports, require('./context').FlowContext);
|
|
120
|
+
} else if (code.includes('FLOW(')) {
|
|
121
|
+
// FLOW wrapper format: FLOW(function($) { ... })
|
|
122
|
+
const wrapped = `(function(FlowContext) { return (${code}) })`;
|
|
123
|
+
fn = require('vm').runInContext(wrapped, ctx, { timeout: 5000 });
|
|
124
|
+
fn = fn(require('./context').FlowContext);
|
|
125
|
+
} else {
|
|
126
|
+
// Assume it's a function directly
|
|
127
|
+
const wrapped = `(function(FlowContext) { return (${code}) })`;
|
|
128
|
+
fn = require('vm').runInContext(wrapped, ctx, { timeout: 5000 });
|
|
129
|
+
fn = fn(require('./context').FlowContext);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handle module.exports.default
|
|
133
|
+
if (fn && fn.default) {
|
|
134
|
+
fn = fn.default;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return fn;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
throw new Error(`Failed to parse workflow: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create the $ API object for Total.js style
|
|
145
|
+
*/
|
|
146
|
+
_createFlowApi(context, stageExecutor) {
|
|
147
|
+
const $ = {
|
|
148
|
+
// Register a stage
|
|
149
|
+
push: (name, fn) => {
|
|
150
|
+
context.push(name, fn);
|
|
151
|
+
return $;
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// Go to next stage (sync) - 支持直接传参
|
|
155
|
+
next: (name, ...args) => {
|
|
156
|
+
context.next(name, ...args);
|
|
157
|
+
return $;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
// Go to next stage (async helper)
|
|
161
|
+
next2: (name, ...args) => {
|
|
162
|
+
return () => {
|
|
163
|
+
context.next(name, ...args);
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// Send data to next stage
|
|
168
|
+
send: (data) => {
|
|
169
|
+
context.send(data);
|
|
170
|
+
return $;
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
// Input from previous stage (返回 sentData �?workflow input)
|
|
174
|
+
get input() {
|
|
175
|
+
return context._sentData !== undefined ? context._sentData : context.input;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// Flow-level persistent data
|
|
179
|
+
data: context.data,
|
|
180
|
+
|
|
181
|
+
// Built-in delay
|
|
182
|
+
wait: (ms) => context.wait(ms),
|
|
183
|
+
|
|
184
|
+
// $.tool - Tool accessor with list(), execute(), register(), get()
|
|
185
|
+
get tool() { return context.tool; },
|
|
186
|
+
|
|
187
|
+
// $.extension - Extension accessor with list(), register(), remove(), execute(), get()
|
|
188
|
+
get extension() { return context.extension; },
|
|
189
|
+
|
|
190
|
+
// $.workflow - Workflow accessor with list(), execute(), get()
|
|
191
|
+
get workflow() { return context.workflow; },
|
|
192
|
+
|
|
193
|
+
// $.agent - Agent accessor with create(), chat(), list()
|
|
194
|
+
get agent() { return context.agent; },
|
|
195
|
+
|
|
196
|
+
// $.prompt - Prompt accessor with send(), chat()
|
|
197
|
+
get prompt() { return context.prompt; },
|
|
198
|
+
|
|
199
|
+
// $.skill - Skill accessor with list(), execute(), get()
|
|
200
|
+
get skill() { return context.skill; },
|
|
201
|
+
|
|
202
|
+
// Execute script
|
|
203
|
+
script: (code) => context.script(code),
|
|
204
|
+
|
|
205
|
+
// End workflow
|
|
206
|
+
destroy: () => context.destroy(),
|
|
207
|
+
|
|
208
|
+
// Current stage info
|
|
209
|
+
get current() { return context.current; },
|
|
210
|
+
get prev() { return context.prev; },
|
|
211
|
+
|
|
212
|
+
// Stage registry
|
|
213
|
+
_stages: context._stages,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return $;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Run stages sequentially based on $.next() calls
|
|
221
|
+
*/
|
|
222
|
+
async _runStages(context, stageExecutor) {
|
|
223
|
+
// Create $ API object for this run
|
|
224
|
+
const $ = this._createFlowApi(context, stageExecutor);
|
|
225
|
+
|
|
226
|
+
while (!context._destroyed && context._pendingNext) {
|
|
227
|
+
const nextName = context._pendingNext;
|
|
228
|
+
|
|
229
|
+
// Get args that were passed via $.next(name, ...args)
|
|
230
|
+
const args = context._sentData;
|
|
231
|
+
context._sentData = null;
|
|
232
|
+
context._pendingNext = null;
|
|
233
|
+
|
|
234
|
+
const fn = context._stages?.get(nextName);
|
|
235
|
+
if (!fn) {
|
|
236
|
+
this._log.warn(`Stage "${nextName}" not found`);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
context.prev = context.current;
|
|
241
|
+
context.current = nextName;
|
|
242
|
+
|
|
243
|
+
this._log.debug(`Executing JS stage: ${nextName}`);
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
// Call fn with $ as first arg, then unpack args
|
|
247
|
+
// Supports: fn($) or fn($, a, b, c)
|
|
248
|
+
const fnArgs = args !== undefined
|
|
249
|
+
? (Array.isArray(args) ? [$, ...args] : [$, args])
|
|
250
|
+
: [$];
|
|
251
|
+
|
|
252
|
+
const result = fn(...fnArgs);
|
|
253
|
+
if (result && typeof result.then === 'function') {
|
|
254
|
+
await result;
|
|
255
|
+
}
|
|
256
|
+
} catch (err) {
|
|
257
|
+
this._log.error(`Stage "${nextName}" error: ${err.message}`);
|
|
258
|
+
context.destroy();
|
|
259
|
+
throw err;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* JS Stage Executor - handles stage execution in JS workflows
|
|
267
|
+
*/
|
|
268
|
+
class JsStageExecutor {
|
|
269
|
+
constructor(framework) {
|
|
270
|
+
this.framework = framework;
|
|
271
|
+
this._log = require('../../../src/common/logger').logger.child('JsStageExecutor');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Execute a stage from JSON config within a JS workflow
|
|
276
|
+
*/
|
|
277
|
+
async executeStage(config, context) {
|
|
278
|
+
const { ActionStage } = require('./stages/action');
|
|
279
|
+
const { ChoiceStage } = require('./stages/choice');
|
|
280
|
+
const { EachStage } = require('./stages/each');
|
|
281
|
+
const { ParallelStage } = require('./stages/parallel');
|
|
282
|
+
const type = this._getStageType(config);
|
|
283
|
+
|
|
284
|
+
switch (type) {
|
|
285
|
+
case 'action': {
|
|
286
|
+
const stage = new ActionStage(config);
|
|
287
|
+
return await stage.execute(context);
|
|
288
|
+
}
|
|
289
|
+
case 'choice': {
|
|
290
|
+
const stage = new ChoiceStage(config);
|
|
291
|
+
const nextStage = await stage.execute(context);
|
|
292
|
+
if (nextStage) {
|
|
293
|
+
context._pendingNext = nextStage;
|
|
294
|
+
}
|
|
295
|
+
return { nextStage };
|
|
296
|
+
}
|
|
297
|
+
case 'each': {
|
|
298
|
+
const stage = new EachStage(config);
|
|
299
|
+
return await stage.execute(context);
|
|
300
|
+
}
|
|
301
|
+
case 'parallel': {
|
|
302
|
+
const stage = new ParallelStage(config);
|
|
303
|
+
return await stage.execute(context);
|
|
304
|
+
}
|
|
305
|
+
default:
|
|
306
|
+
throw new Error(`Unknown stage type: ${type}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
_getStageType(config) {
|
|
311
|
+
if (config.choice || config.when || config.default) return 'choice';
|
|
312
|
+
if (config.each || config.over) return 'each';
|
|
313
|
+
if (config.parallel) return 'parallel';
|
|
314
|
+
return 'action';
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = { JsRunner };
|