foliko 2.0.5 → 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/.claude/settings.local.json +4 -1
- package/.cli_default_systemPrompt.md +291 -0
- package/CLAUDE.md +3 -0
- package/README.md +20 -3
- package/docs/architecture.md +34 -2
- package/docs/extensions.md +199 -0
- package/docs/migration.md +100 -0
- package/docs/public-api.md +280 -24
- package/docs/usage.md +122 -30
- package/package.json +1 -1
- package/plugins/core/audit/index.js +1 -1
- package/plugins/core/default/bootstrap.js +44 -19
- package/plugins/core/python-loader/index.js +43 -25
- package/plugins/core/scheduler/index.js +1 -0
- package/plugins/core/skill-manager/PROMPT.md +6 -0
- package/plugins/core/skill-manager/index.js +402 -115
- package/plugins/core/sub-agent/PROMPT.md +10 -0
- package/plugins/core/sub-agent/index.js +36 -3
- package/plugins/core/think/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 +204 -78
- 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/PROMPT.md +13 -0
- package/plugins/executors/data-splitter/index.js +8 -6
- package/plugins/executors/extension/extension-registry.js +145 -0
- package/plugins/executors/extension/index.js +405 -437
- package/plugins/executors/extension/prompt-builder.js +359 -0
- package/plugins/executors/extension/skill-helper.js +143 -0
- package/plugins/messaging/feishu/index.js +5 -3
- package/plugins/messaging/qq/index.js +6 -4
- package/plugins/messaging/telegram/index.js +6 -3
- package/plugins/messaging/weixin/index.js +5 -3
- package/plugins/tools/PROMPT.md +26 -0
- package/plugins/tools/index.js +6 -5
- 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/foliko/AGENTS.md +196 -43
- package/skills/foliko/SKILL.md +157 -28
- package/skills/mcp/SKILL.md +77 -118
- package/skills/plugins/SKILL.md +89 -3
- package/skills/python/SKILL.md +57 -39
- package/skills/skill-guide/SKILL.md +42 -34
- package/skills/workflows/SKILL.md +753 -436
- package/src/agent/chat.js +56 -28
- package/src/agent/main.js +39 -14
- package/src/agent/prompt-registry.js +56 -16
- package/src/agent/prompts/PROMPT.md +3 -0
- package/src/agent/sub.js +1 -1
- package/src/cli/ui/chat-ui-old.js +5 -2
- package/src/cli/ui/chat-ui.js +9 -5
- package/src/common/constants.js +12 -0
- package/src/common/error-capture.js +91 -0
- package/src/common/json-safe.js +20 -0
- package/src/common/logger.js +2 -2
- package/src/context/compressor.js +6 -2
- package/src/executors/mcp-executor.js +105 -125
- package/src/framework/framework.js +78 -12
- package/src/index.js +4 -0
- package/src/plugin/base.js +908 -5
- package/src/plugin/manager.js +124 -9
- package/src/tool/schema.js +32 -9
- package/src/utils/sandbox.js +1 -1
- package/website/index.html +821 -0
- package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
- package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -391
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* action stage executor
|
|
3
|
+
* Executes a tool, script, or chain-style call (extension, skill, workflow, agent, prompt)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { runScriptSafely } = require('../../../../src/utils/sandbox');
|
|
7
|
+
|
|
8
|
+
class ActionStage {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.name = config.name;
|
|
11
|
+
this.tool = config.tool;
|
|
12
|
+
this.args = config.args || {};
|
|
13
|
+
this.script = config.script;
|
|
14
|
+
this.action = config.action; // alias for tool
|
|
15
|
+
|
|
16
|
+
// Chain-style call types for JSON format
|
|
17
|
+
this.extension = config.extension; // extension name
|
|
18
|
+
this.skill = config.skill; // skill name
|
|
19
|
+
this.workflow = config.workflow; // workflow name
|
|
20
|
+
this.agent = config.agent; // agent action
|
|
21
|
+
this.prompt = config.prompt; // prompt action
|
|
22
|
+
this.command = config.command; // command for skill
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Execute the action stage
|
|
27
|
+
* @param {FlowContext} context
|
|
28
|
+
* @returns {Promise<any>} result
|
|
29
|
+
*/
|
|
30
|
+
async execute(context) {
|
|
31
|
+
const log = context._log;
|
|
32
|
+
|
|
33
|
+
// Handle chain-style calls in JSON format
|
|
34
|
+
if (this.extension) {
|
|
35
|
+
return await this._executeExtension(context, log);
|
|
36
|
+
}
|
|
37
|
+
if (this.skill) {
|
|
38
|
+
return await this._executeSkill(context, log);
|
|
39
|
+
}
|
|
40
|
+
if (this.workflow) {
|
|
41
|
+
return await this._executeWorkflow(context, log);
|
|
42
|
+
}
|
|
43
|
+
if (this.agent) {
|
|
44
|
+
return await this._executeAgent(context, log);
|
|
45
|
+
}
|
|
46
|
+
if (this.prompt) {
|
|
47
|
+
return await this._executePrompt(context, log);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle script execution first - "action": "script" with inline script code
|
|
51
|
+
if (this.script) {
|
|
52
|
+
log.debug(`Executing script in stage: ${this.name}`);
|
|
53
|
+
|
|
54
|
+
// 解析 script 模板字符串(支持 {{stageName}} 和 {{input.xxx}} 等)
|
|
55
|
+
const resolvedScript = context.resolveValue(this.script, { raw: true });
|
|
56
|
+
if (resolvedScript !== this.script) {
|
|
57
|
+
log.debug(`Script template resolved: ${this.script} -> ${resolvedScript}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 暴露 $ 链式 API(与 JS 工作流格式保持一致)
|
|
61
|
+
const scriptContext = {
|
|
62
|
+
$: context,
|
|
63
|
+
input: context.input,
|
|
64
|
+
data: context.data,
|
|
65
|
+
variables: Object.fromEntries(context._results),
|
|
66
|
+
previousResult: context._sentData,
|
|
67
|
+
console: {
|
|
68
|
+
log: (...args) => {
|
|
69
|
+
log.debug('[Script]', ...args);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const result = await runScriptSafely(resolvedScript, scriptContext, { timeout: 10000 });
|
|
75
|
+
|
|
76
|
+
// Store result
|
|
77
|
+
if (this.name) {
|
|
78
|
+
context.setResult(this.name, result);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Tool execution: if tool or action (non-"script") is specified
|
|
85
|
+
if (this.tool || (this.action && this.action !== 'script')) {
|
|
86
|
+
const toolName = this.tool || this.action;
|
|
87
|
+
log.debug(`Executing tool: ${toolName}`);
|
|
88
|
+
|
|
89
|
+
// Resolve args with variable interpolation
|
|
90
|
+
const resolvedArgs = context.resolveArgs(this.args);
|
|
91
|
+
|
|
92
|
+
// Execute tool
|
|
93
|
+
let result;
|
|
94
|
+
let sessionId = context._sessionId;
|
|
95
|
+
if (!sessionId && context._framework?.getExecutionContext) {
|
|
96
|
+
const ctx = context._framework.getExecutionContext();
|
|
97
|
+
sessionId = ctx?.sessionId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (sessionId && context._framework?.runWithContext) {
|
|
101
|
+
result = await context._framework.runWithContext(
|
|
102
|
+
{ sessionId },
|
|
103
|
+
async () => await context._framework.executeTool(toolName, resolvedArgs)
|
|
104
|
+
);
|
|
105
|
+
} else {
|
|
106
|
+
result = await context._framework.executeTool(toolName, resolvedArgs);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Store result
|
|
110
|
+
if (this.name) {
|
|
111
|
+
context.setResult(this.name, result);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw new Error(`Action stage "${this.name}" has neither tool nor script`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Execute extension call (JSON format: { extension: "name", tool: "toolName", args: {...} })
|
|
122
|
+
*/
|
|
123
|
+
async _executeExtension(context, log) {
|
|
124
|
+
const toolName = this.tool || this.command;
|
|
125
|
+
if (!toolName) {
|
|
126
|
+
throw new Error(`Extension stage "${this.name}" requires a tool name`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
log.debug(`Executing extension: ${this.extension}/${toolName}`);
|
|
130
|
+
const result = await context.extension.execute(this.extension, toolName, this.args);
|
|
131
|
+
|
|
132
|
+
if (this.name) {
|
|
133
|
+
context.setResult(this.name, result);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Execute skill call (JSON format: { skill: "name", command: "cmd", args: {...} })
|
|
140
|
+
*/
|
|
141
|
+
async _executeSkill(context, log) {
|
|
142
|
+
const command = this.command || this.tool;
|
|
143
|
+
if (!command) {
|
|
144
|
+
throw new Error(`Skill stage "${this.name}" requires a command`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
log.debug(`Executing skill: ${this.skill}/${command}`);
|
|
148
|
+
const result = await context.skill.execute(this.skill, command, this.args);
|
|
149
|
+
|
|
150
|
+
if (this.name) {
|
|
151
|
+
context.setResult(this.name, result);
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Execute workflow call (JSON format: { workflow: "name", args: { input: {...} } })
|
|
158
|
+
*/
|
|
159
|
+
async _executeWorkflow(context, log) {
|
|
160
|
+
log.debug(`Executing workflow: ${this.workflow}`);
|
|
161
|
+
const result = await context.workflow.execute(this.workflow, this.args.input || this.args);
|
|
162
|
+
|
|
163
|
+
if (this.name) {
|
|
164
|
+
context.setResult(this.name, result);
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Execute agent call (JSON format: { agent: "action", args: {...} })
|
|
171
|
+
*/
|
|
172
|
+
async _executeAgent(context, log) {
|
|
173
|
+
log.debug(`Executing agent: ${this.agent}`);
|
|
174
|
+
let result;
|
|
175
|
+
|
|
176
|
+
if (this.agent === 'create') {
|
|
177
|
+
const agent = await context.agent.create(this.args);
|
|
178
|
+
result = { success: true, agentId: agent.id };
|
|
179
|
+
} else if (this.agent === 'chat') {
|
|
180
|
+
result = await context.agent.chat(this.args);
|
|
181
|
+
} else {
|
|
182
|
+
throw new Error(`Unknown agent action: ${this.agent}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (this.name) {
|
|
186
|
+
context.setResult(this.name, result);
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Execute prompt call (JSON format: { prompt: "action", args: {...} })
|
|
193
|
+
*/
|
|
194
|
+
async _executePrompt(context, log) {
|
|
195
|
+
log.debug(`Executing prompt: ${this.prompt}`);
|
|
196
|
+
let result;
|
|
197
|
+
|
|
198
|
+
if (this.prompt === 'send' || this.prompt === 'chat') {
|
|
199
|
+
result = await context.prompt.send(this.args);
|
|
200
|
+
} else {
|
|
201
|
+
throw new Error(`Unknown prompt action: ${this.prompt}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (this.name) {
|
|
205
|
+
context.setResult(this.name, result);
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { ActionStage };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* choice stage executor
|
|
3
|
+
* Conditional branching (if/else/switch)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { evaluateInSandbox } = require('../../../../src/utils/sandbox');
|
|
7
|
+
|
|
8
|
+
class ChoiceStage {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.name = config.name;
|
|
11
|
+
this.choice = config.choice || []; // Array of { when, do } or { case, do }
|
|
12
|
+
this.default = config.default; // Default stage name
|
|
13
|
+
this._log = require('../../../../src/common/logger').logger.child('ChoiceStage');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Execute the choice stage
|
|
18
|
+
* @param {FlowContext} context
|
|
19
|
+
* @returns {Promise<string|null>} next stage name
|
|
20
|
+
*/
|
|
21
|
+
async execute(context) {
|
|
22
|
+
this._log.debug(`Evaluating choice: ${this.name}`);
|
|
23
|
+
|
|
24
|
+
// Try each when/case condition
|
|
25
|
+
for (const branch of this.choice) {
|
|
26
|
+
const condition = branch.when || branch.case;
|
|
27
|
+
if (!condition) continue;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Resolve condition with variable interpolation
|
|
31
|
+
const resolvedCondition = context.resolveValue(condition);
|
|
32
|
+
|
|
33
|
+
// Evaluate condition
|
|
34
|
+
let matched = false;
|
|
35
|
+
if (typeof resolvedCondition === 'boolean') {
|
|
36
|
+
matched = resolvedCondition;
|
|
37
|
+
} else if (typeof resolvedCondition === 'string') {
|
|
38
|
+
// Use sandbox to evaluate the condition expression
|
|
39
|
+
// Extract stage results to top level so they can be accessed directly
|
|
40
|
+
// e.g., "init.value > 100" can access init from stage results
|
|
41
|
+
// Also spread context.data so each-loop variables (e.g. "fruit") work
|
|
42
|
+
// in choice conditions.
|
|
43
|
+
const stageResults = Object.fromEntries(context._results);
|
|
44
|
+
const evalContext = {
|
|
45
|
+
...stageResults, // Spread stage results to top level
|
|
46
|
+
...context.data, // Spread data so loop variables are reachable
|
|
47
|
+
input: context.input,
|
|
48
|
+
data: context.data,
|
|
49
|
+
previousResult: context._sentData,
|
|
50
|
+
};
|
|
51
|
+
matched = evaluateInSandbox(resolvedCondition, evalContext, { timeout: 5000 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (matched) {
|
|
55
|
+
this._log.debug(`Choice matched: ${condition} -> ${branch.do}`);
|
|
56
|
+
return branch.do; // Return next stage name
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
this._log.warn(`Condition evaluation error for "${condition}": ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// No condition matched, use default
|
|
64
|
+
if (this.default) {
|
|
65
|
+
this._log.debug(`No choice matched, using default: ${this.default}`);
|
|
66
|
+
return this.default;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this._log.debug(`No choice matched and no default for: ${this.name}`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { ChoiceStage };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delay stage executor
|
|
3
|
+
*
|
|
4
|
+
* Pauses the workflow for a given number of milliseconds.
|
|
5
|
+
*
|
|
6
|
+
* Configuration:
|
|
7
|
+
* {
|
|
8
|
+
* "name": "wait-step", // optional
|
|
9
|
+
* "ms": 1000, // milliseconds to wait (alias: delay, wait)
|
|
10
|
+
* "result": { ... } // optional value to record as the stage's output
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* Useful for rate-limiting, pacing retries, or signaling that an
|
|
14
|
+
* external dependency is allowed time to settle.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
class DelayStage {
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.name = config.name;
|
|
20
|
+
// Accept several common field names so declarative authoring stays ergonomic.
|
|
21
|
+
this.msRaw = config.ms ?? config.delay ?? config.wait ?? 0;
|
|
22
|
+
this.result = config.result; // optional value to surface as the stage output
|
|
23
|
+
this._log = require('../../../../src/common/logger').logger.child('DelayStage');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the resolved delay in ms.
|
|
28
|
+
* Supports string templates (e.g. "{{poll-interval}}") so the wait time
|
|
29
|
+
* can be derived from prior stage output.
|
|
30
|
+
*/
|
|
31
|
+
_resolveMs(context) {
|
|
32
|
+
const resolveOptions = { raw: true };
|
|
33
|
+
if (typeof this.msRaw === 'string') {
|
|
34
|
+
const resolved = context.resolveValue(this.msRaw, resolveOptions);
|
|
35
|
+
const num = Number(resolved);
|
|
36
|
+
if (Number.isFinite(num) && num >= 0) {
|
|
37
|
+
return num;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
const num = Number(this.msRaw);
|
|
42
|
+
return Number.isFinite(num) && num >= 0 ? num : 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async execute(context) {
|
|
46
|
+
const ms = this._resolveMs(context);
|
|
47
|
+
this._log.debug(`Delay stage "${this.name || '(unnamed)'}" waiting ${ms}ms`);
|
|
48
|
+
|
|
49
|
+
// Hard cap: avoid accidental multi-hour sleeps via bad input.
|
|
50
|
+
const MAX_DELAY_MS = 10 * 60 * 1000; // 10 minutes
|
|
51
|
+
const safeMs = Math.min(ms, MAX_DELAY_MS);
|
|
52
|
+
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, safeMs));
|
|
54
|
+
|
|
55
|
+
// Optional result payload (used when the stage is named and downstream
|
|
56
|
+
// stages need to reference the resolution time, e.g. "{{wait.done}}").
|
|
57
|
+
const payload = typeof this.result === 'string'
|
|
58
|
+
? context.resolveValue(this.result, { raw: true })
|
|
59
|
+
: this.result;
|
|
60
|
+
|
|
61
|
+
const output = payload !== undefined
|
|
62
|
+
? payload
|
|
63
|
+
: { waited: safeMs, completedAt: new Date().toISOString() };
|
|
64
|
+
|
|
65
|
+
if (this.name) {
|
|
66
|
+
context.setResult(this.name, output);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return output;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { DelayStage };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* each stage executor
|
|
3
|
+
* Loop over items
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class EachStage {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.name = config.name;
|
|
9
|
+
this.each = config.each; // Array to iterate over
|
|
10
|
+
this.as = config.as || 'item'; // Item variable name
|
|
11
|
+
this.index = config.index || 'index'; // Index variable name
|
|
12
|
+
this.stages = config.stages || []; // Child stages to execute for each item
|
|
13
|
+
this.maxIterations = config.maxIterations || 100;
|
|
14
|
+
this.until = config.until; // Early exit condition
|
|
15
|
+
this._log = require('../../../../src/common/logger').logger.child('EachStage');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute the each stage
|
|
20
|
+
* @param {FlowContext} context
|
|
21
|
+
* @returns {Promise<Array>} results for each iteration
|
|
22
|
+
*/
|
|
23
|
+
async execute(context) {
|
|
24
|
+
this._log.debug(`Executing each loop: ${this.name}`);
|
|
25
|
+
|
|
26
|
+
// Resolve the array to iterate
|
|
27
|
+
let array;
|
|
28
|
+
if (typeof this.each === 'string') {
|
|
29
|
+
// Strip optional surrounding {{ }} so users can write either
|
|
30
|
+
// "each": "items" (bare identifier)
|
|
31
|
+
// "each": "{{items}}" (templated)
|
|
32
|
+
// "each": "{{init.items}}" (stage.field)
|
|
33
|
+
let path = this.each.trim();
|
|
34
|
+
const m = path.match(/^\{\{([^}]+)\}\}$/);
|
|
35
|
+
if (m) path = m[1].trim();
|
|
36
|
+
|
|
37
|
+
// Use the low-level _resolvePath so we get the actual array/object back
|
|
38
|
+
// (String.prototype.replace in resolveValue would coerce non-strings.)
|
|
39
|
+
let resolved = context._resolvePath(path);
|
|
40
|
+
if (resolved === undefined && path.startsWith('[')) {
|
|
41
|
+
// Support inline array literal: each: "['a','b']"
|
|
42
|
+
try { resolved = JSON.parse(path); } catch {}
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(resolved)) {
|
|
45
|
+
array = resolved;
|
|
46
|
+
} else if (resolved && typeof resolved === 'object') {
|
|
47
|
+
// Convert object to entries (so {{k, v}} style iteration works)
|
|
48
|
+
array = Object.entries(resolved);
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error(`each "${this.each}" resolved to non-iterable: ${typeof resolved}`);
|
|
51
|
+
}
|
|
52
|
+
} else if (Array.isArray(this.each)) {
|
|
53
|
+
array = this.each;
|
|
54
|
+
} else {
|
|
55
|
+
throw new Error(`each stage "${this.name}" requires an array to iterate`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const results = [];
|
|
59
|
+
const maxIter = Math.min(this.maxIterations, array.length);
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < maxIter; i++) {
|
|
62
|
+
// Set loop variables
|
|
63
|
+
context.data[this.as] = array[i];
|
|
64
|
+
context.data[this.index] = i;
|
|
65
|
+
context.data.loopIndex = i; // Legacy alias
|
|
66
|
+
|
|
67
|
+
this._log.debug(`Each iteration ${i}: ${this.as}=`, array[i]);
|
|
68
|
+
|
|
69
|
+
// Execute child stages sequentially
|
|
70
|
+
let stageResults = [];
|
|
71
|
+
for (const stageConfig of this.stages) {
|
|
72
|
+
const stageExecutor = context._stageExecutor;
|
|
73
|
+
if (stageExecutor) {
|
|
74
|
+
const result = await stageExecutor.executeStage(stageConfig, context);
|
|
75
|
+
stageResults.push(result);
|
|
76
|
+
|
|
77
|
+
// Check for early exit
|
|
78
|
+
if (context._destroyed) {
|
|
79
|
+
this._log.debug('Flow destroyed, stopping each loop');
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
results.push(stageResults);
|
|
85
|
+
|
|
86
|
+
// Check until condition
|
|
87
|
+
if (this.until) {
|
|
88
|
+
try {
|
|
89
|
+
const { evaluateInSandbox } = require('../../../../../src/utils/sandbox');
|
|
90
|
+
const evalContext = {
|
|
91
|
+
input: context.input,
|
|
92
|
+
data: context.data,
|
|
93
|
+
variables: Object.fromEntries(context._results),
|
|
94
|
+
previousResult: context._sentData,
|
|
95
|
+
item: array[i],
|
|
96
|
+
index: i,
|
|
97
|
+
};
|
|
98
|
+
const shouldStop = evaluateInSandbox(this.until, evalContext, { timeout: 5000 });
|
|
99
|
+
if (shouldStop) {
|
|
100
|
+
this._log.debug(`Each loop terminated at iteration ${i} by until condition`);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
this._log.warn(`until condition error: ${err.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Cleanup loop variables
|
|
110
|
+
delete context.data[this.as];
|
|
111
|
+
delete context.data[this.index];
|
|
112
|
+
delete context.data.loopIndex;
|
|
113
|
+
|
|
114
|
+
// Store results by name if this stage has a name
|
|
115
|
+
if (this.name) {
|
|
116
|
+
context.setResult(this.name, results);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { EachStage };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parallel stage executor
|
|
3
|
+
* Run stages concurrently
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class ParallelStage {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.name = config.name;
|
|
9
|
+
this.parallel = config.parallel || []; // Array of stages to run in parallel
|
|
10
|
+
this._log = require('../../../../src/common/logger').logger.child('ParallelStage');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute the parallel stage
|
|
15
|
+
* @param {FlowContext} context
|
|
16
|
+
* @returns {Promise<Array>} results from all parallel stages
|
|
17
|
+
*/
|
|
18
|
+
async execute(context) {
|
|
19
|
+
this._log.debug(`Executing parallel: ${this.name}`);
|
|
20
|
+
|
|
21
|
+
if (!this.parallel || this.parallel.length === 0) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const stageExecutor = context._stageExecutor;
|
|
26
|
+
if (!stageExecutor) {
|
|
27
|
+
throw new Error('Stage executor not set in context');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Execute all stages concurrently
|
|
31
|
+
const promises = this.parallel.map(async (stageConfig) => {
|
|
32
|
+
try {
|
|
33
|
+
return await stageExecutor.executeStage(stageConfig, context);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
this._log.warn(`Parallel stage error: ${err.message}`);
|
|
36
|
+
return { error: err.message };
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const results = await Promise.all(promises);
|
|
41
|
+
|
|
42
|
+
// Build result object keyed by stage name (not plain array)
|
|
43
|
+
const resultByName = {};
|
|
44
|
+
for (let i = 0; i < this.parallel.length; i++) {
|
|
45
|
+
const stageConfig = this.parallel[i];
|
|
46
|
+
const name = stageConfig.name || String(i);
|
|
47
|
+
resultByName[name] = results[i];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Store named results in context
|
|
51
|
+
// If the parallel block itself has a name, store the keyed object
|
|
52
|
+
if (this.name) {
|
|
53
|
+
context.setResult(this.name, resultByName);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Also store individual named results for access via {{stageName}}
|
|
57
|
+
for (let i = 0; i < this.parallel.length; i++) {
|
|
58
|
+
const stageConfig = this.parallel[i];
|
|
59
|
+
if (stageConfig.name) {
|
|
60
|
+
context.setResult(stageConfig.name, results[i]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this._log.debug(`Parallel completed: ${results.length} stages`);
|
|
65
|
+
return resultByName;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { ParallelStage };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* try stage executor
|
|
3
|
+
*
|
|
4
|
+
* Wraps a sequence of child stages in a try/catch so a single failure
|
|
5
|
+
* doesn't tear down the entire workflow. Optional `finally` stages run
|
|
6
|
+
* regardless of outcome.
|
|
7
|
+
*
|
|
8
|
+
* Configuration:
|
|
9
|
+
* {
|
|
10
|
+
* "name": "safe-fetch", // optional, allows {{safe-fetch.success}} access
|
|
11
|
+
* "try": {
|
|
12
|
+
* "stages": [ ... ] // required
|
|
13
|
+
* },
|
|
14
|
+
* "catch": { // optional
|
|
15
|
+
* "stages": [ ... ],
|
|
16
|
+
* "as": "err" // optional name to inject error.message into context.data
|
|
17
|
+
* },
|
|
18
|
+
* "finally": { // optional
|
|
19
|
+
* "stages": [ ... ]
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Result shape (stored on the named stage and returned):
|
|
24
|
+
* {
|
|
25
|
+
* success: true | false,
|
|
26
|
+
* result: <last try stage output>, // when success=true
|
|
27
|
+
* error: "<message>", // when success=false (optional stack)
|
|
28
|
+
* caught: [ ...catch stage results... ], // when catch ran
|
|
29
|
+
* finally: [ ...finally stage results... ] // when finally ran
|
|
30
|
+
* }
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
class TryStage {
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.name = config.name;
|
|
36
|
+
this.tryConfig = config.try || { stages: [] };
|
|
37
|
+
this.catchConfig = config.catch || null;
|
|
38
|
+
this.finallyConfig = config.finally || null;
|
|
39
|
+
this._log = require('../../../../src/common/logger').logger.child('TryStage');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Execute a list of sub-stages using the shared StageExecutor.
|
|
44
|
+
* Wraps each individual stage in its own try/catch so a single stage failure
|
|
45
|
+
* doesn't break the chain unless it's the top-level try.
|
|
46
|
+
*/
|
|
47
|
+
async _runStages(stages, context, { stopOnError = false } = {}) {
|
|
48
|
+
if (!stages || !Array.isArray(stages) || stages.length === 0) return [];
|
|
49
|
+
const stageExecutor = context._stageExecutor;
|
|
50
|
+
if (!stageExecutor) {
|
|
51
|
+
throw new Error('Stage executor not set in context');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const stage of stages) {
|
|
56
|
+
try {
|
|
57
|
+
const result = await stageExecutor.executeStage(stage, context);
|
|
58
|
+
out.push(result);
|
|
59
|
+
if (context._destroyed) break;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (stopOnError) {
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
this._log.warn(`Stage "${stage.name || '(unnamed)'}" failed inside try: ${err.message}`);
|
|
65
|
+
out.push({ error: err.message, stage: stage.name });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async execute(context) {
|
|
72
|
+
this._log.debug(`Executing try stage: ${this.name || '(unnamed)'}`);
|
|
73
|
+
|
|
74
|
+
let success = true;
|
|
75
|
+
let error = null;
|
|
76
|
+
let tryResult = null;
|
|
77
|
+
let caughtResult = null;
|
|
78
|
+
let finallyResult = null;
|
|
79
|
+
|
|
80
|
+
// === try block ===
|
|
81
|
+
try {
|
|
82
|
+
tryResult = await this._runStages(this.tryConfig.stages || [], context, { stopOnError: true });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
success = false;
|
|
85
|
+
error = err;
|
|
86
|
+
this._log.debug(`Try block failed: ${err.message}`);
|
|
87
|
+
|
|
88
|
+
// === catch block ===
|
|
89
|
+
if (this.catchConfig) {
|
|
90
|
+
// Optionally expose the error under a chosen key in context.data
|
|
91
|
+
if (this.catchConfig.as) {
|
|
92
|
+
context.data[this.catchConfig.as] = err;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
caughtResult = await this._runStages(this.catchConfig.stages || [], context);
|
|
96
|
+
} catch (catchErr) {
|
|
97
|
+
this._log.warn(`catch block itself failed: ${catchErr.message}`);
|
|
98
|
+
caughtResult = [{ error: catchErr.message }];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// === finally block (always runs) ===
|
|
104
|
+
if (this.finallyConfig) {
|
|
105
|
+
try {
|
|
106
|
+
finallyResult = await this._runStages(this.finallyConfig.stages || [], context);
|
|
107
|
+
} catch (finallyErr) {
|
|
108
|
+
this._log.warn(`finally block failed: ${finallyErr.message}`);
|
|
109
|
+
finallyResult = [{ error: finallyErr.message }];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Cleanup injected error reference so it doesn't leak past the try stage
|
|
114
|
+
if (this.catchConfig && this.catchConfig.as) {
|
|
115
|
+
delete context.data[this.catchConfig.as];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const output = success
|
|
119
|
+
? {
|
|
120
|
+
success: true,
|
|
121
|
+
result: tryResult && tryResult.length > 0 ? tryResult[tryResult.length - 1] : null,
|
|
122
|
+
stages: tryResult,
|
|
123
|
+
finally: finallyResult,
|
|
124
|
+
}
|
|
125
|
+
: {
|
|
126
|
+
success: false,
|
|
127
|
+
error: error.message,
|
|
128
|
+
stack: error.stack,
|
|
129
|
+
caught: caughtResult,
|
|
130
|
+
stages: tryResult,
|
|
131
|
+
finally: finallyResult,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (this.name) {
|
|
135
|
+
context.setResult(this.name, output);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return output;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { TryStage };
|