foliko 2.0.30 → 2.0.32

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.
@@ -1,941 +1,941 @@
1
- /**
2
- * FlowContext - Shared context for v2 workflow execution
3
- * Provides $.push(), $.next(), $.send() API for JS format
4
- * and manages stage results for JSON format
5
- *
6
- * Chain-style API support:
7
- * - $.tool - Direct tool registry/execution (list, execute, register)
8
- * - $.extension - Extension tools (list, register, remove, execute)
9
- * - $.workflow - Nested workflow calls (list, execute)
10
- * - $.agent - Agent creation/calls (create, chat, list)
11
- * - $.prompt - Prompt/chat operations
12
- * - $.skill - Skill commands (list, execute)
13
- * - $.logger - Logging operations (debug, info, warn, error, trace, fatal)
14
- * - $.storage - Key-value storage operations (set, get, delete, list, clear)
15
- */
16
-
17
- const { runScriptSafely } = require('../../../src/utils/sandbox');
18
-
19
- class FlowContext {
20
- constructor(input = {}, sessionId = null) {
21
- // Input from workflow caller
22
- this.input = input;
23
-
24
- // Flow-level persistent data (like $.data)
25
- this.data = {};
26
-
27
- // Stage results storage (stageName -> result)
28
- this._results = new Map();
29
-
30
- // Current stage info
31
- this.current = null;
32
- this.prev = null;
33
-
34
- // Session ID for tool execution
35
- this._sessionId = sessionId;
36
-
37
- // Framework reference for tool execution
38
- this._framework = null;
39
-
40
- // Destruction flag
41
- this._destroyed = false;
42
-
43
- // Pending next stage (for async transitions)
44
- this._pendingNext = null;
45
-
46
- // Log prefix
47
- this._log = require('../../../src/common/logger').logger.child('FlowContext');
48
-
49
- // Lazy-loaded chain-style API objects
50
- this._toolAccessor = null;
51
- this._extensionAccessor = null;
52
- this._workflowAccessor = null;
53
- this._agentAccessor = null;
54
- this._promptAccessor = null;
55
- this._skillAccessor = null;
56
- this._loggerAccessor = null;
57
- this._storageAccessor = null;
58
- }
59
-
60
- /**
61
- * Set framework reference (called by engine)
62
- */
63
- setFramework(framework) {
64
- this._framework = framework;
65
- }
66
-
67
- // ============================================================
68
- // $.tool - Direct tool registry and execution
69
- // ============================================================
70
- get tool() {
71
- if (!this._toolAccessor) {
72
- this._toolAccessor = this._createToolAccessor();
73
- }
74
- return this._toolAccessor;
75
- }
76
-
77
- _createToolAccessor() {
78
- const context = this;
79
- const log = this._log;
80
-
81
- return {
82
- /**
83
- * List all registered tools
84
- */
85
- list: () => {
86
- if (!context._framework) return [];
87
- return context._framework.toolRegistry ? context._framework.toolRegistry.listTools() : [];
88
- },
89
-
90
- /**
91
- * Execute a tool directly
92
- */
93
- execute: async (name, args = {}) => {
94
- return await context._executeTool(name, args);
95
- },
96
-
97
- /**
98
- * Register a new tool (temporary, workflow-scoped)
99
- */
100
- register: (toolDef) => {
101
- log.warn('tool.register() is not implemented in workflow context - tools must be registered via plugin');
102
- },
103
-
104
- /**
105
- * Get tool definition
106
- */
107
- get: (name) => {
108
- if (!context._framework?.toolRegistry) return null;
109
- return context._framework.toolRegistry.getTool(name);
110
- },
111
- };
112
- }
113
-
114
- // ============================================================
115
- // $.extension - Extension tools (like Plugin.extension accessor)
116
- // ============================================================
117
- get extension() {
118
- if (!this._extensionAccessor) {
119
- this._extensionAccessor = this._createExtensionAccessor();
120
- }
121
- return this._extensionAccessor;
122
- }
123
-
124
- _createExtensionAccessor() {
125
- const context = this;
126
- const log = this._log;
127
-
128
- return {
129
- /**
130
- * List all registered extensions
131
- */
132
- list: () => {
133
- if (!context._framework) return [];
134
- const extExec = context._framework.pluginManager?.get('extension-executor');
135
- if (!extExec?._registry) return [];
136
- return extExec._registry.listExtensions();
137
- },
138
-
139
- /**
140
- * Register an extension tool
141
- */
142
- register: (toolDef) => {
143
- if (!context._framework) {
144
- log.warn('extension.register() called before framework is set');
145
- return;
146
- }
147
- if (!toolDef || !toolDef.name) {
148
- log.warn('extension.register() missing tool name');
149
- return;
150
- }
151
-
152
- const extExec = context._framework.pluginManager?.get('extension-executor');
153
- if (!extExec) {
154
- log.warn('extension-executor plugin not found');
155
- return;
156
- }
157
-
158
- extExec.registerTool('workflow', {
159
- name: 'workflow',
160
- description: 'Workflow scoped extensions',
161
- version: '1.0.0',
162
- }, toolDef);
163
-
164
- extExec._invalidatePromptCaches(context._framework);
165
- log.debug(`[Workflow] Registered extension tool: workflow/${toolDef.name}`);
166
- },
167
-
168
- /**
169
- * Remove an extension tool
170
- */
171
- remove: (toolName) => {
172
- if (!context._framework) {
173
- log.warn('extension.remove() called before framework is set');
174
- return;
175
- }
176
-
177
- const extExec = context._framework.pluginManager?.get('extension-executor');
178
- if (extExec) {
179
- extExec._unregisterTool('workflow', toolName);
180
- }
181
- log.debug(`[Workflow] Removed extension tool: workflow/${toolName}`);
182
- },
183
-
184
- /**
185
- * Execute an extension tool
186
- * Supports: extension.execute('tool-name', args) or extension.execute('plugin', 'tool-name', args)
187
- */
188
- execute: async (pluginNameOrToolName, toolNameOrArgs, args = {}) => {
189
- if (!context._framework) {
190
- throw new Error('extension.execute() called before framework is set');
191
- }
192
-
193
- const extExec = context._framework.pluginManager?.get('extension-executor');
194
- if (!extExec) {
195
- return { success: false, error: 'extension-executor not found' };
196
- }
197
-
198
- let pluginName, toolName;
199
- if (typeof toolNameOrArgs === 'object') {
200
- // extension.execute('tool-name', args)
201
- pluginName = 'workflow';
202
- toolName = pluginNameOrToolName;
203
- args = toolNameOrArgs;
204
- } else {
205
- // extension.execute('plugin', 'tool-name', args)
206
- pluginName = pluginNameOrToolName;
207
- toolName = toolNameOrArgs;
208
- }
209
-
210
- const result = await extExec._executeTool(pluginName, toolName, args, context._framework);
211
- return result;
212
- },
213
-
214
- /**
215
- * Get extension info
216
- */
217
- get: (pluginName) => {
218
- if (!context._framework) return null;
219
- const extExec = context._framework.pluginManager?.get('extension-executor');
220
- if (!extExec?._registry) return null;
221
- return extExec._registry.getExtension(pluginName);
222
- },
223
- };
224
- }
225
-
226
- // ============================================================
227
- // $.workflow - Nested workflow operations
228
- // ============================================================
229
- get workflow() {
230
- if (!this._workflowAccessor) {
231
- this._workflowAccessor = this._createWorkflowAccessor();
232
- }
233
- return this._workflowAccessor;
234
- }
235
-
236
- _createWorkflowAccessor() {
237
- const context = this;
238
- const log = this._log;
239
-
240
- return {
241
- /**
242
- * List all available workflows
243
- */
244
- list: () => {
245
- if (!context._framework) return [];
246
- const workflowPlugin = context._framework.pluginManager?.get('workflow');
247
- if (!workflowPlugin?._workflows) return [];
248
- return Array.from(workflowPlugin._workflows.keys());
249
- },
250
-
251
- /**
252
- * Execute a workflow by name
253
- */
254
- execute: async (workflowName, input = {}, sessionId = null) => {
255
- if (!context._framework) {
256
- throw new Error('workflow.execute() called before framework is set');
257
- }
258
-
259
- const workflowPlugin = context._framework.pluginManager?.get('workflow');
260
- if (!workflowPlugin) {
261
- return { success: false, error: 'workflow plugin not found' };
262
- }
263
-
264
- const sid = sessionId || context._sessionId;
265
- return await workflowPlugin.executeWorkflow(workflowName, input, sid);
266
- },
267
-
268
- /**
269
- * Get workflow definition
270
- */
271
- get: (workflowName) => {
272
- if (!context._framework) return null;
273
- const workflowPlugin = context._framework.pluginManager?.get('workflow');
274
- if (!workflowPlugin?._workflows) return null;
275
- return workflowPlugin._workflows.get(workflowName);
276
- },
277
- };
278
- }
279
-
280
- // ============================================================
281
- // $.agent - Agent operations
282
- // ============================================================
283
- get agent() {
284
- if (!this._agentAccessor) {
285
- this._agentAccessor = this._createAgentAccessor();
286
- }
287
- return this._agentAccessor;
288
- }
289
-
290
- _createAgentAccessor() {
291
- const context = this;
292
- const log = this._log;
293
-
294
- return {
295
- /**
296
- * Create a new agent
297
- */
298
- create: (config = {}) => {
299
- if (!context._framework) {
300
- throw new Error('agent.create() called before framework is set');
301
- }
302
-
303
- const resolvedConfig = context.resolveArgs(config);
304
- let sessionId = context._sessionId;
305
-
306
- if (sessionId && context._framework.runWithContext) {
307
- return context._framework.runWithContext(
308
- { sessionId },
309
- () => context._framework.createAgent(resolvedConfig)
310
- );
311
- }
312
- return context._framework.createAgent(resolvedConfig);
313
- },
314
-
315
- /**
316
- * Chat with an agent
317
- */
318
- chat: async (config = {}) => {
319
- if (!context._framework) {
320
- throw new Error('agent.chat() called before framework is set');
321
- }
322
-
323
- const resolvedConfig = context.resolveArgs(config);
324
- const { agentId, message, ...options } = resolvedConfig;
325
-
326
- if (!message) {
327
- throw new Error('agent.chat() requires message');
328
- }
329
-
330
- let sessionId = context._sessionId;
331
-
332
- if (sessionId && context._framework.runWithContext) {
333
- return await context._framework.runWithContext(
334
- { sessionId },
335
- async () => {
336
- const agent = agentId
337
- ? context._framework.createAgent({ id: agentId, hidden: true, ...options })
338
- : context._framework.createSubAgent({ hidden: true, ...options });
339
- return await agent.chat(message, options);
340
- }
341
- );
342
- }
343
-
344
- const agent = agentId
345
- ? context._framework.createAgent({ id: agentId, hidden: true, ...options })
346
- : context._framework.createSubAgent({ hidden: true, ...options });
347
- return await agent.chat(message, options);
348
- },
349
-
350
- /**
351
- * List active agents
352
- */
353
- list: () => {
354
- if (!context._framework) return [];
355
- return context._framework._agents || [];
356
- },
357
- };
358
- }
359
-
360
- // ============================================================
361
- // $.prompt - Prompt operations
362
- // ============================================================
363
- get prompt() {
364
- if (!this._promptAccessor) {
365
- this._promptAccessor = this._createPromptAccessor();
366
- }
367
- return this._promptAccessor;
368
- }
369
-
370
- _createPromptAccessor() {
371
- const context = this;
372
- const log = this._log;
373
-
374
- return {
375
- /**
376
- * Send a prompt and get response (creates temporary agent)
377
- */
378
- send: async (config = {}) => {
379
- if (!context._framework) {
380
- throw new Error('prompt.send() called before framework is set');
381
- }
382
-
383
- const resolvedConfig = context.resolveArgs(config);
384
- const { message, role, systemPrompt, ...options } = resolvedConfig;
385
-
386
- if (!message) {
387
- throw new Error('prompt.send() requires message');
388
- }
389
-
390
- let sessionId = context._sessionId;
391
-
392
- if (sessionId && context._framework.runWithContext) {
393
- return await context._framework.runWithContext(
394
- { sessionId },
395
- async () => {
396
- const agent = context._framework.createSubAgent({
397
- role: role || 'Assistant',
398
- systemPrompt: systemPrompt || '',
399
- hidden: true,
400
- });
401
- return await agent.chat(message, options);
402
- }
403
- );
404
- }
405
-
406
- const agent = context._framework.createSubAgent({
407
- role: role || 'Assistant',
408
- systemPrompt: systemPrompt || '',
409
- hidden: true,
410
- });
411
- return await agent.chat(message, options);
412
- },
413
-
414
- /**
415
- * Chat with a prompt (alias for send)
416
- */
417
- chat: async (config = {}) => {
418
- return this.send(config);
419
- },
420
- };
421
- }
422
-
423
- // ============================================================
424
- // $.skill - Skill commands
425
- // ============================================================
426
- get skill() {
427
- if (!this._skillAccessor) {
428
- this._skillAccessor = this._createSkillAccessor();
429
- }
430
- return this._skillAccessor;
431
- }
432
-
433
- _createSkillAccessor() {
434
- const context = this;
435
- const log = this._log;
436
-
437
- return {
438
- /**
439
- * List available skills
440
- */
441
- list: () => {
442
- if (!context._framework) return [];
443
- const extExec = context._framework.pluginManager?.get('extension-executor');
444
- if (!extExec?._registry) return [];
445
- const extensions = extExec._registry.listExtensions();
446
- return extensions
447
- .filter(e => e.name.startsWith('skill:'))
448
- .map(e => e.name.replace('skill:', ''));
449
- },
450
-
451
- /**
452
- * Execute a skill command
453
- * Supports: skill.execute('skill-name', 'command', args)
454
- */
455
- execute: async (skillName, commandOrArgs, args = {}) => {
456
- if (!context._framework) {
457
- throw new Error('skill.execute() called before framework is set');
458
- }
459
-
460
- let command, toolArgs;
461
- if (typeof commandOrArgs === 'object') {
462
- command = commandOrArgs.command || commandOrArgs.tool;
463
- toolArgs = commandOrArgs;
464
- } else {
465
- command = commandOrArgs;
466
- toolArgs = args;
467
- }
468
-
469
- if (!command) {
470
- throw new Error('skill.execute() requires command');
471
- }
472
-
473
- const plugin = `skill:${skillName}`;
474
- const resolvedArgs = context.resolveArgs(toolArgs);
475
-
476
- if (context._sessionId && context._framework.runWithContext) {
477
- return await context._framework.runWithContext(
478
- { sessionId: context._sessionId },
479
- async () => await context._framework.executeTool('ext_call', {
480
- plugin,
481
- tool: command,
482
- args: resolvedArgs,
483
- })
484
- );
485
- }
486
- return await context._framework.executeTool('ext_call', {
487
- plugin,
488
- tool: command,
489
- args: resolvedArgs,
490
- });
491
- },
492
-
493
- /**
494
- * Get skill info
495
- */
496
- get: (skillName) => {
497
- if (!context._framework) return null;
498
- const extExec = context._framework.pluginManager?.get('extension-executor');
499
- if (!extExec?._registry) return null;
500
- return extExec._registry.getExtension(`skill:${skillName}`);
501
- },
502
- };
503
- }
504
-
505
- // ============================================================
506
- // $.logger - Logging operations
507
- // ============================================================
508
- get logger() {
509
- if (!this._loggerAccessor) {
510
- this._loggerAccessor = this._createLoggerAccessor();
511
- }
512
- return this._loggerAccessor;
513
- }
514
-
515
- _createLoggerAccessor() {
516
- const context = this;
517
- const flowLog = this._log;
518
-
519
- // Get the underlying logger methods
520
- const LEVELS = ['debug', 'info', 'warn', 'error', 'trace', 'fatal'];
521
-
522
- const loggerObj = {
523
- /**
524
- * Log debug message
525
- */
526
- debug: (...args) => {
527
- flowLog.debug(...args);
528
- },
529
-
530
- /**
531
- * Log info message
532
- */
533
- info: (...args) => {
534
- flowLog.info(...args);
535
- },
536
-
537
- /**
538
- * Log warning message
539
- */
540
- warn: (...args) => {
541
- flowLog.warn(...args);
542
- },
543
-
544
- /**
545
- * Log error message
546
- */
547
- error: (...args) => {
548
- flowLog.error(...args);
549
- },
550
-
551
- /**
552
- * Log trace message
553
- */
554
- trace: (...args) => {
555
- flowLog.trace(...args);
556
- },
557
-
558
- /**
559
- * Log fatal message
560
- */
561
- fatal: (...args) => {
562
- flowLog.fatal(...args);
563
- },
564
-
565
- /**
566
- * Create a child logger with namespace
567
- */
568
- child: (namespace) => {
569
- return flowLog.child(namespace);
570
- },
571
-
572
- /**
573
- * Log with custom level
574
- */
575
- log: (level, ...args) => {
576
- if (typeof flowLog[level] === 'function') {
577
- flowLog[level](...args);
578
- } else {
579
- flowLog.info(...args);
580
- }
581
- },
582
- };
583
-
584
- return loggerObj;
585
- }
586
-
587
- // ============================================================
588
- // $.storage - Storage operations
589
- // ============================================================
590
- get storage() {
591
- if (!this._storageAccessor) {
592
- this._storageAccessor = this._createStorageAccessor();
593
- }
594
- return this._storageAccessor;
595
- }
596
-
597
- _createStorageAccessor() {
598
- const context = this;
599
-
600
- return {
601
- /**
602
- * Set a value in storage
603
- */
604
- set: async (key, value, namespace) => {
605
- return await context._executeTool('storage_set', { key, value, namespace });
606
- },
607
-
608
- /**
609
- * Get a value from storage
610
- */
611
- get: async (key, namespace) => {
612
- return await context._executeTool('storage_get', { key, namespace });
613
- },
614
-
615
- /**
616
- * Delete a value from storage
617
- */
618
- delete: async (key, namespace) => {
619
- return await context._executeTool('storage_delete', { key, namespace });
620
- },
621
-
622
- /**
623
- * List all keys in a namespace
624
- */
625
- list: async (namespace) => {
626
- return await context._executeTool('storage_list', { namespace });
627
- },
628
-
629
- /**
630
- * Clear all values in a namespace
631
- */
632
- clear: async (namespace) => {
633
- return await context._executeTool('storage_clear', { namespace });
634
- },
635
-
636
- /**
637
- * Watch a key for changes
638
- */
639
- watch: async (key, namespace) => {
640
- return await context._executeTool('storage_watch', { key, namespace });
641
- },
642
-
643
- /**
644
- * Get storage statistics
645
- */
646
- stats: async (namespace) => {
647
- return await context._executeTool('storage_stats', { namespace });
648
- },
649
-
650
- /**
651
- * Trigger storage compaction
652
- */
653
- compact: async (namespace) => {
654
- return await context._executeTool('storage_compact', { namespace });
655
- },
656
- };
657
- }
658
-
659
- // ============================================================
660
- // NOTE: 之前这里有 `async tool(name, args)` legacy method,
661
- // 但与上方的 `get tool()` 冲突 (同名),JavaScript class
662
- // 让后定义的方法覆盖 getter,导致 $.tool 返回的永远是
663
- // async function 而非 accessor object。
664
- // 已删除此 legacy method;新 API 用 `$.tool.execute(name, args)`
665
- // 走 get tool() 返回的 accessor 对象。
666
- // ============================================================
667
-
668
- /**
669
- * Execute a tool directly
670
- */
671
- async _executeTool(toolName, args) {
672
- if (!this._framework) {
673
- throw new Error('Framework not set');
674
- }
675
-
676
- // Resolve args with variable interpolation
677
- const resolvedArgs = this.resolveArgs(args || {});
678
-
679
- let sessionId = this._sessionId;
680
- if (!sessionId && this._framework.getExecutionContext) {
681
- const ctx = this._framework.getExecutionContext();
682
- sessionId = ctx?.sessionId;
683
- }
684
-
685
- if (sessionId && this._framework.runWithContext) {
686
- return await this._framework.runWithContext(
687
- { sessionId },
688
- async () => await this._framework.executeTool(toolName, resolvedArgs)
689
- );
690
- }
691
- return await this._framework.executeTool(toolName, resolvedArgs);
692
- }
693
-
694
- // ============================================================
695
- // Basic Flow API
696
- // ============================================================
697
-
698
- /**
699
- * Register a stage (JS format - $.push)
700
- */
701
- push(name, fn) {
702
- if (!this._stages) {
703
- this._stages = new Map();
704
- }
705
- this._stages.set(name, fn);
706
- return this;
707
- }
708
-
709
- /**
710
- * Go to next stage (sync) - $.next
711
- * 支持直接传参: $.next('stage-name', arg1, arg2, ...)
712
- * 等同�?$.send({ arg1, arg2, ... }) 后再 $.next('stage-name')
713
- */
714
- next(name, ...args) {
715
- this._pendingNext = name;
716
- if (args.length > 0) {
717
- this._sentData = args.length === 1 ? args[0] : args;
718
- }
719
- return this;
720
- }
721
-
722
- /**
723
- * Go to next stage (async) - $.next2
724
- * Returns a function for deferred transition
725
- */
726
- next2(name) {
727
- return () => {
728
- this.next(name);
729
- };
730
- }
731
-
732
- /**
733
- * Pass data to next stage - $.send
734
- */
735
- send(data) {
736
- if (data !== undefined) {
737
- this._sentData = data;
738
- }
739
- return this;
740
- }
741
-
742
- /**
743
- * Built-in delay helper - $.wait
744
- */
745
- wait(ms) {
746
- return new Promise((resolve) => setTimeout(resolve, ms));
747
- }
748
-
749
- /**
750
- * Execute a script - $.script
751
- */
752
- async script(code) {
753
- const scriptContext = {
754
- input: this.input,
755
- data: this.data,
756
- variables: Object.fromEntries(this._results),
757
- previousResult: this._sentData,
758
- console: {
759
- log: (...args) => {
760
- this._log.debug('[Script]', ...args);
761
- },
762
- },
763
- };
764
- return await runScriptSafely(code, scriptContext, { timeout: 10000 });
765
- }
766
-
767
- /**
768
- * End workflow - $.destroy
769
- */
770
- destroy() {
771
- this._destroyed = true;
772
- this._pendingNext = null;
773
- }
774
-
775
- /**
776
- * Get result from a named stage
777
- */
778
- getResult(stageName) {
779
- return this._results.get(stageName);
780
- }
781
-
782
- /**
783
- * Set result for a stage
784
- */
785
- setResult(stageName, result) {
786
- this._results.set(stageName, result);
787
- }
788
-
789
- // ============================================================
790
- // Variable Resolution
791
- // ============================================================
792
-
793
- /**
794
- * Resolve a value with variable interpolation
795
- * Supports {{stageName}}, {{stage.field}}, {{input.field}}, {{data.field}}
796
- *
797
- * @param {*} value - Value to resolve (string templates only)
798
- * @param {Object} [options]
799
- * @param {boolean} [options.raw=false] - If true, return raw resolved value (preserve type).
800
- * Used by stages like `each` that need actual arrays/objects
801
- * instead of stringified versions.
802
- * @returns {*} Resolved value
803
- */
804
- resolveValue(value, options = {}) {
805
- if (typeof value !== 'string') {
806
- return value;
807
- }
808
-
809
- const { raw = false } = options;
810
-
811
- // Replace {{stageName}} or {{stage.field}} with stage result
812
- return value.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
813
- const resolved = this._resolvePath(path);
814
-
815
- // Path not found - keep the original placeholder
816
- if (resolved === undefined) {
817
- return match;
818
- }
819
-
820
- // Raw mode: return the value as-is (caller handles serialization)
821
- if (raw) {
822
- return resolved;
823
- }
824
-
825
- // String mode: serialize for interpolation
826
- if (resolved === null) {
827
- return 'null';
828
- }
829
- // For arrays and objects, JSON-encode so the template works as a string
830
- // (Raw arrays/objects can't be inlined into strings via String.replace without coercion)
831
- if (typeof resolved === 'object') {
832
- try {
833
- return JSON.stringify(resolved);
834
- } catch {
835
- return String(resolved);
836
- }
837
- }
838
-
839
- return resolved;
840
- });
841
- }
842
-
843
- /**
844
- * Resolve a template path to its value.
845
- * Internal helper used by resolveValue and other template consumers.
846
- * Supports: input.field, data.field, stageName, stage.field
847
- * Also searches context.data when a simple path is not found in _results.
848
- *
849
- * @param {string} path - Template path (without {{ }} markers)
850
- * @returns {*} Resolved value or undefined if not found
851
- */
852
- _resolvePath(path) {
853
- // input.field access
854
- if (path.startsWith('input.')) {
855
- const val = this._getNestedValue(this.input, path.substring(6));
856
- return val !== undefined ? val : undefined;
857
- }
858
-
859
- // data.field access
860
- if (path.startsWith('data.')) {
861
- const val = this._getNestedValue(this.data, path.substring(5));
862
- return val !== undefined ? val : undefined;
863
- }
864
-
865
- // stage.field or stageName access
866
- const dotIndex = path.indexOf('.');
867
- if (dotIndex > 0) {
868
- const stageName = path.substring(0, dotIndex);
869
- const fieldPath = path.substring(dotIndex + 1);
870
- // Look up the root value (try stage results first, then loop/data context)
871
- // so that nested property access works for each-loop variables too,
872
- // e.g. inside an each body: {{u.name}} where u is the current item.
873
- let stageResult = this._results.get(stageName);
874
- if (stageResult === undefined) {
875
- stageResult = this.data[stageName];
876
- }
877
- if (stageResult !== undefined) {
878
- const val = this._getNestedValue(stageResult, fieldPath);
879
- if (val !== undefined) return val;
880
- }
881
- return undefined;
882
- }
883
-
884
- // Simple stageName reference: try _results first, then data
885
- // (data lookup supports each-loop variables, e.g., {{fruit}} inside an each body)
886
- let val = this._results.get(path);
887
- if (val === undefined) {
888
- val = this.data[path];
889
- }
890
- return val !== undefined ? val : undefined;
891
- }
892
-
893
- /**
894
- * Resolve args object with variable interpolation
895
- */
896
- resolveArgs(args) {
897
- if (!args || typeof args !== 'object') {
898
- return args;
899
- }
900
- const resolved = {};
901
- for (const [key, value] of Object.entries(args)) {
902
- if (typeof value === 'string') {
903
- resolved[key] = this.resolveValue(value);
904
- } else if (Array.isArray(value)) {
905
- // Resolve each element as a template (raw mode preserves types)
906
- resolved[key] = value.map((v) =>
907
- typeof v === 'string' ? this.resolveValue(v, { raw: true }) : v
908
- );
909
- } else if (typeof value === 'object' && value !== null) {
910
- resolved[key] = this.resolveArgs(value);
911
- } else {
912
- resolved[key] = value;
913
- }
914
- }
915
- return resolved;
916
- }
917
-
918
- /**
919
- * Get nested value from object by dot notation path
920
- */
921
- _getNestedValue(obj, path) {
922
- if (!obj) return undefined;
923
- const parts = path.split('.');
924
- let current = obj;
925
- for (const part of parts) {
926
- if (current === null || current === undefined) return undefined;
927
- // Handle array index in path like "items.0.name"
928
- if (typeof current === 'string') {
929
- try {
930
- current = JSON.parse(current);
931
- } catch {
932
- // Not JSON, continue
933
- }
934
- }
935
- current = current[part];
936
- }
937
- return current;
938
- }
939
- }
940
-
941
- module.exports = { FlowContext };
1
+ /**
2
+ * FlowContext - Shared context for v2 workflow execution
3
+ * Provides $.push(), $.next(), $.send() API for JS format
4
+ * and manages stage results for JSON format
5
+ *
6
+ * Chain-style API support:
7
+ * - $.tool - Direct tool registry/execution (list, execute, register)
8
+ * - $.extension - Extension tools (list, register, remove, execute)
9
+ * - $.workflow - Nested workflow calls (list, execute)
10
+ * - $.agent - Agent creation/calls (create, chat, list)
11
+ * - $.prompt - Prompt/chat operations
12
+ * - $.skill - Skill commands (list, execute)
13
+ * - $.logger - Logging operations (debug, info, warn, error, trace, fatal)
14
+ * - $.storage - Key-value storage operations (set, get, delete, list, clear)
15
+ */
16
+
17
+ const { runScriptSafely } = require('../../../src/utils/sandbox');
18
+
19
+ class FlowContext {
20
+ constructor(input = {}, sessionId = null) {
21
+ // Input from workflow caller
22
+ this.input = input;
23
+
24
+ // Flow-level persistent data (like $.data)
25
+ this.data = {};
26
+
27
+ // Stage results storage (stageName -> result)
28
+ this._results = new Map();
29
+
30
+ // Current stage info
31
+ this.current = null;
32
+ this.prev = null;
33
+
34
+ // Session ID for tool execution
35
+ this._sessionId = sessionId;
36
+
37
+ // Framework reference for tool execution
38
+ this._framework = null;
39
+
40
+ // Destruction flag
41
+ this._destroyed = false;
42
+
43
+ // Pending next stage (for async transitions)
44
+ this._pendingNext = null;
45
+
46
+ // Log prefix
47
+ this._log = require('../../../src/common/logger').logger.child('FlowContext');
48
+
49
+ // Lazy-loaded chain-style API objects
50
+ this._toolAccessor = null;
51
+ this._extensionAccessor = null;
52
+ this._workflowAccessor = null;
53
+ this._agentAccessor = null;
54
+ this._promptAccessor = null;
55
+ this._skillAccessor = null;
56
+ this._loggerAccessor = null;
57
+ this._storageAccessor = null;
58
+ }
59
+
60
+ /**
61
+ * Set framework reference (called by engine)
62
+ */
63
+ setFramework(framework) {
64
+ this._framework = framework;
65
+ }
66
+
67
+ // ============================================================
68
+ // $.tool - Direct tool registry and execution
69
+ // ============================================================
70
+ get tool() {
71
+ if (!this._toolAccessor) {
72
+ this._toolAccessor = this._createToolAccessor();
73
+ }
74
+ return this._toolAccessor;
75
+ }
76
+
77
+ _createToolAccessor() {
78
+ const context = this;
79
+ const log = this._log;
80
+
81
+ return {
82
+ /**
83
+ * List all registered tools
84
+ */
85
+ list: () => {
86
+ if (!context._framework) return [];
87
+ return context._framework.toolRegistry ? context._framework.toolRegistry.listTools() : [];
88
+ },
89
+
90
+ /**
91
+ * Execute a tool directly
92
+ */
93
+ execute: async (name, args = {}) => {
94
+ return await context._executeTool(name, args);
95
+ },
96
+
97
+ /**
98
+ * Register a new tool (temporary, workflow-scoped)
99
+ */
100
+ register: (toolDef) => {
101
+ log.warn('tool.register() is not implemented in workflow context - tools must be registered via plugin');
102
+ },
103
+
104
+ /**
105
+ * Get tool definition
106
+ */
107
+ get: (name) => {
108
+ if (!context._framework?.toolRegistry) return null;
109
+ return context._framework.toolRegistry.getTool(name);
110
+ },
111
+ };
112
+ }
113
+
114
+ // ============================================================
115
+ // $.extension - Extension tools (like Plugin.extension accessor)
116
+ // ============================================================
117
+ get extension() {
118
+ if (!this._extensionAccessor) {
119
+ this._extensionAccessor = this._createExtensionAccessor();
120
+ }
121
+ return this._extensionAccessor;
122
+ }
123
+
124
+ _createExtensionAccessor() {
125
+ const context = this;
126
+ const log = this._log;
127
+
128
+ return {
129
+ /**
130
+ * List all registered extensions
131
+ */
132
+ list: () => {
133
+ if (!context._framework) return [];
134
+ const extExec = context._framework.pluginManager?.get('extension-executor');
135
+ if (!extExec?._registry) return [];
136
+ return extExec._registry.listExtensions();
137
+ },
138
+
139
+ /**
140
+ * Register an extension tool
141
+ */
142
+ register: (toolDef) => {
143
+ if (!context._framework) {
144
+ log.warn('extension.register() called before framework is set');
145
+ return;
146
+ }
147
+ if (!toolDef || !toolDef.name) {
148
+ log.warn('extension.register() missing tool name');
149
+ return;
150
+ }
151
+
152
+ const extExec = context._framework.pluginManager?.get('extension-executor');
153
+ if (!extExec) {
154
+ log.warn('extension-executor plugin not found');
155
+ return;
156
+ }
157
+
158
+ extExec.registerTool('workflow', {
159
+ name: 'workflow',
160
+ description: 'Workflow scoped extensions',
161
+ version: '1.0.0',
162
+ }, toolDef);
163
+
164
+ extExec._invalidatePromptCaches(context._framework);
165
+ log.debug(`[Workflow] Registered extension tool: workflow/${toolDef.name}`);
166
+ },
167
+
168
+ /**
169
+ * Remove an extension tool
170
+ */
171
+ remove: (toolName) => {
172
+ if (!context._framework) {
173
+ log.warn('extension.remove() called before framework is set');
174
+ return;
175
+ }
176
+
177
+ const extExec = context._framework.pluginManager?.get('extension-executor');
178
+ if (extExec) {
179
+ extExec._unregisterTool('workflow', toolName);
180
+ }
181
+ log.debug(`[Workflow] Removed extension tool: workflow/${toolName}`);
182
+ },
183
+
184
+ /**
185
+ * Execute an extension tool
186
+ * Supports: extension.execute('tool-name', args) or extension.execute('plugin', 'tool-name', args)
187
+ */
188
+ execute: async (pluginNameOrToolName, toolNameOrArgs, args = {}) => {
189
+ if (!context._framework) {
190
+ throw new Error('extension.execute() called before framework is set');
191
+ }
192
+
193
+ const extExec = context._framework.pluginManager?.get('extension-executor');
194
+ if (!extExec) {
195
+ return { success: false, error: 'extension-executor not found' };
196
+ }
197
+
198
+ let pluginName, toolName;
199
+ if (typeof toolNameOrArgs === 'object') {
200
+ // extension.execute('tool-name', args)
201
+ pluginName = 'workflow';
202
+ toolName = pluginNameOrToolName;
203
+ args = toolNameOrArgs;
204
+ } else {
205
+ // extension.execute('plugin', 'tool-name', args)
206
+ pluginName = pluginNameOrToolName;
207
+ toolName = toolNameOrArgs;
208
+ }
209
+
210
+ const result = await extExec._executeTool(pluginName, toolName, args, context._framework);
211
+ return result;
212
+ },
213
+
214
+ /**
215
+ * Get extension info
216
+ */
217
+ get: (pluginName) => {
218
+ if (!context._framework) return null;
219
+ const extExec = context._framework.pluginManager?.get('extension-executor');
220
+ if (!extExec?._registry) return null;
221
+ return extExec._registry.getExtension(pluginName);
222
+ },
223
+ };
224
+ }
225
+
226
+ // ============================================================
227
+ // $.workflow - Nested workflow operations
228
+ // ============================================================
229
+ get workflow() {
230
+ if (!this._workflowAccessor) {
231
+ this._workflowAccessor = this._createWorkflowAccessor();
232
+ }
233
+ return this._workflowAccessor;
234
+ }
235
+
236
+ _createWorkflowAccessor() {
237
+ const context = this;
238
+ const log = this._log;
239
+
240
+ return {
241
+ /**
242
+ * List all available workflows
243
+ */
244
+ list: () => {
245
+ if (!context._framework) return [];
246
+ const workflowPlugin = context._framework.pluginManager?.get('workflow');
247
+ if (!workflowPlugin?._workflows) return [];
248
+ return Array.from(workflowPlugin._workflows.keys());
249
+ },
250
+
251
+ /**
252
+ * Execute a workflow by name
253
+ */
254
+ execute: async (workflowName, input = {}, sessionId = null) => {
255
+ if (!context._framework) {
256
+ throw new Error('workflow.execute() called before framework is set');
257
+ }
258
+
259
+ const workflowPlugin = context._framework.pluginManager?.get('workflow');
260
+ if (!workflowPlugin) {
261
+ return { success: false, error: 'workflow plugin not found' };
262
+ }
263
+
264
+ const sid = sessionId || context._sessionId;
265
+ return await workflowPlugin.executeWorkflow(workflowName, input, sid);
266
+ },
267
+
268
+ /**
269
+ * Get workflow definition
270
+ */
271
+ get: (workflowName) => {
272
+ if (!context._framework) return null;
273
+ const workflowPlugin = context._framework.pluginManager?.get('workflow');
274
+ if (!workflowPlugin?._workflows) return null;
275
+ return workflowPlugin._workflows.get(workflowName);
276
+ },
277
+ };
278
+ }
279
+
280
+ // ============================================================
281
+ // $.agent - Agent operations
282
+ // ============================================================
283
+ get agent() {
284
+ if (!this._agentAccessor) {
285
+ this._agentAccessor = this._createAgentAccessor();
286
+ }
287
+ return this._agentAccessor;
288
+ }
289
+
290
+ _createAgentAccessor() {
291
+ const context = this;
292
+ const log = this._log;
293
+
294
+ return {
295
+ /**
296
+ * Create a new agent
297
+ */
298
+ create: (config = {}) => {
299
+ if (!context._framework) {
300
+ throw new Error('agent.create() called before framework is set');
301
+ }
302
+
303
+ const resolvedConfig = context.resolveArgs(config);
304
+ let sessionId = context._sessionId;
305
+
306
+ if (sessionId && context._framework.runWithContext) {
307
+ return context._framework.runWithContext(
308
+ { sessionId },
309
+ () => context._framework.createAgent(resolvedConfig)
310
+ );
311
+ }
312
+ return context._framework.createAgent(resolvedConfig);
313
+ },
314
+
315
+ /**
316
+ * Chat with an agent
317
+ */
318
+ chat: async (config = {}) => {
319
+ if (!context._framework) {
320
+ throw new Error('agent.chat() called before framework is set');
321
+ }
322
+
323
+ const resolvedConfig = context.resolveArgs(config);
324
+ const { agentId, message, ...options } = resolvedConfig;
325
+
326
+ if (!message) {
327
+ throw new Error('agent.chat() requires message');
328
+ }
329
+
330
+ let sessionId = context._sessionId;
331
+
332
+ if (sessionId && context._framework.runWithContext) {
333
+ return await context._framework.runWithContext(
334
+ { sessionId },
335
+ async () => {
336
+ const agent = agentId
337
+ ? context._framework.createAgent({ id: agentId, hidden: true, ...options })
338
+ : context._framework.createSubAgent({ hidden: true, ...options });
339
+ return await agent.chat(message, options);
340
+ }
341
+ );
342
+ }
343
+
344
+ const agent = agentId
345
+ ? context._framework.createAgent({ id: agentId, hidden: true, ...options })
346
+ : context._framework.createSubAgent({ hidden: true, ...options });
347
+ return await agent.chat(message, options);
348
+ },
349
+
350
+ /**
351
+ * List active agents
352
+ */
353
+ list: () => {
354
+ if (!context._framework) return [];
355
+ return context._framework._agents || [];
356
+ },
357
+ };
358
+ }
359
+
360
+ // ============================================================
361
+ // $.prompt - Prompt operations
362
+ // ============================================================
363
+ get prompt() {
364
+ if (!this._promptAccessor) {
365
+ this._promptAccessor = this._createPromptAccessor();
366
+ }
367
+ return this._promptAccessor;
368
+ }
369
+
370
+ _createPromptAccessor() {
371
+ const context = this;
372
+ const log = this._log;
373
+
374
+ return {
375
+ /**
376
+ * Send a prompt and get response (creates temporary agent)
377
+ */
378
+ send: async (config = {}) => {
379
+ if (!context._framework) {
380
+ throw new Error('prompt.send() called before framework is set');
381
+ }
382
+
383
+ const resolvedConfig = context.resolveArgs(config);
384
+ const { message, role, systemPrompt, ...options } = resolvedConfig;
385
+
386
+ if (!message) {
387
+ throw new Error('prompt.send() requires message');
388
+ }
389
+
390
+ let sessionId = context._sessionId;
391
+
392
+ if (sessionId && context._framework.runWithContext) {
393
+ return await context._framework.runWithContext(
394
+ { sessionId },
395
+ async () => {
396
+ const agent = context._framework.createSubAgent({
397
+ role: role || 'Assistant',
398
+ systemPrompt: systemPrompt || '',
399
+ hidden: true,
400
+ });
401
+ return await agent.chat(message, options);
402
+ }
403
+ );
404
+ }
405
+
406
+ const agent = context._framework.createSubAgent({
407
+ role: role || 'Assistant',
408
+ systemPrompt: systemPrompt || '',
409
+ hidden: true,
410
+ });
411
+ return await agent.chat(message, options);
412
+ },
413
+
414
+ /**
415
+ * Chat with a prompt (alias for send)
416
+ */
417
+ chat: async (config = {}) => {
418
+ return this.send(config);
419
+ },
420
+ };
421
+ }
422
+
423
+ // ============================================================
424
+ // $.skill - Skill commands
425
+ // ============================================================
426
+ get skill() {
427
+ if (!this._skillAccessor) {
428
+ this._skillAccessor = this._createSkillAccessor();
429
+ }
430
+ return this._skillAccessor;
431
+ }
432
+
433
+ _createSkillAccessor() {
434
+ const context = this;
435
+ const log = this._log;
436
+
437
+ return {
438
+ /**
439
+ * List available skills
440
+ */
441
+ list: () => {
442
+ if (!context._framework) return [];
443
+ const extExec = context._framework.pluginManager?.get('extension-executor');
444
+ if (!extExec?._registry) return [];
445
+ const extensions = extExec._registry.listExtensions();
446
+ return extensions
447
+ .filter(e => e.name.startsWith('skill:'))
448
+ .map(e => e.name.replace('skill:', ''));
449
+ },
450
+
451
+ /**
452
+ * Execute a skill command
453
+ * Supports: skill.execute('skill-name', 'command', args)
454
+ */
455
+ execute: async (skillName, commandOrArgs, args = {}) => {
456
+ if (!context._framework) {
457
+ throw new Error('skill.execute() called before framework is set');
458
+ }
459
+
460
+ let command, toolArgs;
461
+ if (typeof commandOrArgs === 'object') {
462
+ command = commandOrArgs.command || commandOrArgs.tool;
463
+ toolArgs = commandOrArgs;
464
+ } else {
465
+ command = commandOrArgs;
466
+ toolArgs = args;
467
+ }
468
+
469
+ if (!command) {
470
+ throw new Error('skill.execute() requires command');
471
+ }
472
+
473
+ const plugin = `skill:${skillName}`;
474
+ const resolvedArgs = context.resolveArgs(toolArgs);
475
+
476
+ if (context._sessionId && context._framework.runWithContext) {
477
+ return await context._framework.runWithContext(
478
+ { sessionId: context._sessionId },
479
+ async () => await context._framework.executeTool('ext_call', {
480
+ plugin,
481
+ tool: command,
482
+ args: resolvedArgs,
483
+ })
484
+ );
485
+ }
486
+ return await context._framework.executeTool('ext_call', {
487
+ plugin,
488
+ tool: command,
489
+ args: resolvedArgs,
490
+ });
491
+ },
492
+
493
+ /**
494
+ * Get skill info
495
+ */
496
+ get: (skillName) => {
497
+ if (!context._framework) return null;
498
+ const extExec = context._framework.pluginManager?.get('extension-executor');
499
+ if (!extExec?._registry) return null;
500
+ return extExec._registry.getExtension(`skill:${skillName}`);
501
+ },
502
+ };
503
+ }
504
+
505
+ // ============================================================
506
+ // $.logger - Logging operations
507
+ // ============================================================
508
+ get logger() {
509
+ if (!this._loggerAccessor) {
510
+ this._loggerAccessor = this._createLoggerAccessor();
511
+ }
512
+ return this._loggerAccessor;
513
+ }
514
+
515
+ _createLoggerAccessor() {
516
+ const context = this;
517
+ const flowLog = this._log;
518
+
519
+ // Get the underlying logger methods
520
+ const LEVELS = ['debug', 'info', 'warn', 'error', 'trace', 'fatal'];
521
+
522
+ const loggerObj = {
523
+ /**
524
+ * Log debug message
525
+ */
526
+ debug: (...args) => {
527
+ flowLog.debug(...args);
528
+ },
529
+
530
+ /**
531
+ * Log info message
532
+ */
533
+ info: (...args) => {
534
+ flowLog.info(...args);
535
+ },
536
+
537
+ /**
538
+ * Log warning message
539
+ */
540
+ warn: (...args) => {
541
+ flowLog.warn(...args);
542
+ },
543
+
544
+ /**
545
+ * Log error message
546
+ */
547
+ error: (...args) => {
548
+ flowLog.error(...args);
549
+ },
550
+
551
+ /**
552
+ * Log trace message
553
+ */
554
+ trace: (...args) => {
555
+ flowLog.trace(...args);
556
+ },
557
+
558
+ /**
559
+ * Log fatal message
560
+ */
561
+ fatal: (...args) => {
562
+ flowLog.fatal(...args);
563
+ },
564
+
565
+ /**
566
+ * Create a child logger with namespace
567
+ */
568
+ child: (namespace) => {
569
+ return flowLog.child(namespace);
570
+ },
571
+
572
+ /**
573
+ * Log with custom level
574
+ */
575
+ log: (level, ...args) => {
576
+ if (typeof flowLog[level] === 'function') {
577
+ flowLog[level](...args);
578
+ } else {
579
+ flowLog.info(...args);
580
+ }
581
+ },
582
+ };
583
+
584
+ return loggerObj;
585
+ }
586
+
587
+ // ============================================================
588
+ // $.storage - Storage operations
589
+ // ============================================================
590
+ get storage() {
591
+ if (!this._storageAccessor) {
592
+ this._storageAccessor = this._createStorageAccessor();
593
+ }
594
+ return this._storageAccessor;
595
+ }
596
+
597
+ _createStorageAccessor() {
598
+ const context = this;
599
+
600
+ return {
601
+ /**
602
+ * Set a value in storage
603
+ */
604
+ set: async (key, value, namespace) => {
605
+ return await context._executeTool('storage_set', { key, value, namespace });
606
+ },
607
+
608
+ /**
609
+ * Get a value from storage
610
+ */
611
+ get: async (key, namespace) => {
612
+ return await context._executeTool('storage_get', { key, namespace });
613
+ },
614
+
615
+ /**
616
+ * Delete a value from storage
617
+ */
618
+ delete: async (key, namespace) => {
619
+ return await context._executeTool('storage_delete', { key, namespace });
620
+ },
621
+
622
+ /**
623
+ * List all keys in a namespace
624
+ */
625
+ list: async (namespace) => {
626
+ return await context._executeTool('storage_list', { namespace });
627
+ },
628
+
629
+ /**
630
+ * Clear all values in a namespace
631
+ */
632
+ clear: async (namespace) => {
633
+ return await context._executeTool('storage_clear', { namespace });
634
+ },
635
+
636
+ /**
637
+ * Watch a key for changes
638
+ */
639
+ watch: async (key, namespace) => {
640
+ return await context._executeTool('storage_watch', { key, namespace });
641
+ },
642
+
643
+ /**
644
+ * Get storage statistics
645
+ */
646
+ stats: async (namespace) => {
647
+ return await context._executeTool('storage_stats', { namespace });
648
+ },
649
+
650
+ /**
651
+ * Trigger storage compaction
652
+ */
653
+ compact: async (namespace) => {
654
+ return await context._executeTool('storage_compact', { namespace });
655
+ },
656
+ };
657
+ }
658
+
659
+ // ============================================================
660
+ // NOTE: 之前这里有 `async tool(name, args)` legacy method,
661
+ // 但与上方的 `get tool()` 冲突 (同名),JavaScript class
662
+ // 让后定义的方法覆盖 getter,导致 $.tool 返回的永远是
663
+ // async function 而非 accessor object。
664
+ // 已删除此 legacy method;新 API 用 `$.tool.execute(name, args)`
665
+ // 走 get tool() 返回的 accessor 对象。
666
+ // ============================================================
667
+
668
+ /**
669
+ * Execute a tool directly
670
+ */
671
+ async _executeTool(toolName, args) {
672
+ if (!this._framework) {
673
+ throw new Error('Framework not set');
674
+ }
675
+
676
+ // Resolve args with variable interpolation
677
+ const resolvedArgs = this.resolveArgs(args || {});
678
+
679
+ let sessionId = this._sessionId;
680
+ if (!sessionId && this._framework.getExecutionContext) {
681
+ const ctx = this._framework.getExecutionContext();
682
+ sessionId = ctx?.sessionId;
683
+ }
684
+
685
+ if (sessionId && this._framework.runWithContext) {
686
+ return await this._framework.runWithContext(
687
+ { sessionId },
688
+ async () => await this._framework.executeTool(toolName, resolvedArgs)
689
+ );
690
+ }
691
+ return await this._framework.executeTool(toolName, resolvedArgs);
692
+ }
693
+
694
+ // ============================================================
695
+ // Basic Flow API
696
+ // ============================================================
697
+
698
+ /**
699
+ * Register a stage (JS format - $.push)
700
+ */
701
+ push(name, fn) {
702
+ if (!this._stages) {
703
+ this._stages = new Map();
704
+ }
705
+ this._stages.set(name, fn);
706
+ return this;
707
+ }
708
+
709
+ /**
710
+ * Go to next stage (sync) - $.next
711
+ * 支持直接传参: $.next('stage-name', arg1, arg2, ...)
712
+ * 等同�?$.send({ arg1, arg2, ... }) 后再 $.next('stage-name')
713
+ */
714
+ next(name, ...args) {
715
+ this._pendingNext = name;
716
+ if (args.length > 0) {
717
+ this._sentData = args.length === 1 ? args[0] : args;
718
+ }
719
+ return this;
720
+ }
721
+
722
+ /**
723
+ * Go to next stage (async) - $.next2
724
+ * Returns a function for deferred transition
725
+ */
726
+ next2(name) {
727
+ return () => {
728
+ this.next(name);
729
+ };
730
+ }
731
+
732
+ /**
733
+ * Pass data to next stage - $.send
734
+ */
735
+ send(data) {
736
+ if (data !== undefined) {
737
+ this._sentData = data;
738
+ }
739
+ return this;
740
+ }
741
+
742
+ /**
743
+ * Built-in delay helper - $.wait
744
+ */
745
+ wait(ms) {
746
+ return new Promise((resolve) => setTimeout(resolve, ms));
747
+ }
748
+
749
+ /**
750
+ * Execute a script - $.script
751
+ */
752
+ async script(code) {
753
+ const scriptContext = {
754
+ input: this.input,
755
+ data: this.data,
756
+ variables: Object.fromEntries(this._results),
757
+ previousResult: this._sentData,
758
+ console: {
759
+ log: (...args) => {
760
+ this._log.debug('[Script]', ...args);
761
+ },
762
+ },
763
+ };
764
+ return await runScriptSafely(code, scriptContext, { timeout: 10000 });
765
+ }
766
+
767
+ /**
768
+ * End workflow - $.destroy
769
+ */
770
+ destroy() {
771
+ this._destroyed = true;
772
+ this._pendingNext = null;
773
+ }
774
+
775
+ /**
776
+ * Get result from a named stage
777
+ */
778
+ getResult(stageName) {
779
+ return this._results.get(stageName);
780
+ }
781
+
782
+ /**
783
+ * Set result for a stage
784
+ */
785
+ setResult(stageName, result) {
786
+ this._results.set(stageName, result);
787
+ }
788
+
789
+ // ============================================================
790
+ // Variable Resolution
791
+ // ============================================================
792
+
793
+ /**
794
+ * Resolve a value with variable interpolation
795
+ * Supports {{stageName}}, {{stage.field}}, {{input.field}}, {{data.field}}
796
+ *
797
+ * @param {*} value - Value to resolve (string templates only)
798
+ * @param {Object} [options]
799
+ * @param {boolean} [options.raw=false] - If true, return raw resolved value (preserve type).
800
+ * Used by stages like `each` that need actual arrays/objects
801
+ * instead of stringified versions.
802
+ * @returns {*} Resolved value
803
+ */
804
+ resolveValue(value, options = {}) {
805
+ if (typeof value !== 'string') {
806
+ return value;
807
+ }
808
+
809
+ const { raw = false } = options;
810
+
811
+ // Replace {{stageName}} or {{stage.field}} with stage result
812
+ return value.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
813
+ const resolved = this._resolvePath(path);
814
+
815
+ // Path not found - keep the original placeholder
816
+ if (resolved === undefined) {
817
+ return match;
818
+ }
819
+
820
+ // Raw mode: return the value as-is (caller handles serialization)
821
+ if (raw) {
822
+ return resolved;
823
+ }
824
+
825
+ // String mode: serialize for interpolation
826
+ if (resolved === null) {
827
+ return 'null';
828
+ }
829
+ // For arrays and objects, JSON-encode so the template works as a string
830
+ // (Raw arrays/objects can't be inlined into strings via String.replace without coercion)
831
+ if (typeof resolved === 'object') {
832
+ try {
833
+ return JSON.stringify(resolved);
834
+ } catch {
835
+ return String(resolved);
836
+ }
837
+ }
838
+
839
+ return resolved;
840
+ });
841
+ }
842
+
843
+ /**
844
+ * Resolve a template path to its value.
845
+ * Internal helper used by resolveValue and other template consumers.
846
+ * Supports: input.field, data.field, stageName, stage.field
847
+ * Also searches context.data when a simple path is not found in _results.
848
+ *
849
+ * @param {string} path - Template path (without {{ }} markers)
850
+ * @returns {*} Resolved value or undefined if not found
851
+ */
852
+ _resolvePath(path) {
853
+ // input.field access
854
+ if (path.startsWith('input.')) {
855
+ const val = this._getNestedValue(this.input, path.substring(6));
856
+ return val !== undefined ? val : undefined;
857
+ }
858
+
859
+ // data.field access
860
+ if (path.startsWith('data.')) {
861
+ const val = this._getNestedValue(this.data, path.substring(5));
862
+ return val !== undefined ? val : undefined;
863
+ }
864
+
865
+ // stage.field or stageName access
866
+ const dotIndex = path.indexOf('.');
867
+ if (dotIndex > 0) {
868
+ const stageName = path.substring(0, dotIndex);
869
+ const fieldPath = path.substring(dotIndex + 1);
870
+ // Look up the root value (try stage results first, then loop/data context)
871
+ // so that nested property access works for each-loop variables too,
872
+ // e.g. inside an each body: {{u.name}} where u is the current item.
873
+ let stageResult = this._results.get(stageName);
874
+ if (stageResult === undefined) {
875
+ stageResult = this.data[stageName];
876
+ }
877
+ if (stageResult !== undefined) {
878
+ const val = this._getNestedValue(stageResult, fieldPath);
879
+ if (val !== undefined) return val;
880
+ }
881
+ return undefined;
882
+ }
883
+
884
+ // Simple stageName reference: try _results first, then data
885
+ // (data lookup supports each-loop variables, e.g., {{fruit}} inside an each body)
886
+ let val = this._results.get(path);
887
+ if (val === undefined) {
888
+ val = this.data[path];
889
+ }
890
+ return val !== undefined ? val : undefined;
891
+ }
892
+
893
+ /**
894
+ * Resolve args object with variable interpolation
895
+ */
896
+ resolveArgs(args) {
897
+ if (!args || typeof args !== 'object') {
898
+ return args;
899
+ }
900
+ const resolved = {};
901
+ for (const [key, value] of Object.entries(args)) {
902
+ if (typeof value === 'string') {
903
+ resolved[key] = this.resolveValue(value);
904
+ } else if (Array.isArray(value)) {
905
+ // Resolve each element as a template (raw mode preserves types)
906
+ resolved[key] = value.map((v) =>
907
+ typeof v === 'string' ? this.resolveValue(v, { raw: true }) : v
908
+ );
909
+ } else if (typeof value === 'object' && value !== null) {
910
+ resolved[key] = this.resolveArgs(value);
911
+ } else {
912
+ resolved[key] = value;
913
+ }
914
+ }
915
+ return resolved;
916
+ }
917
+
918
+ /**
919
+ * Get nested value from object by dot notation path
920
+ */
921
+ _getNestedValue(obj, path) {
922
+ if (!obj) return undefined;
923
+ const parts = path.split('.');
924
+ let current = obj;
925
+ for (const part of parts) {
926
+ if (current === null || current === undefined) return undefined;
927
+ // Handle array index in path like "items.0.name"
928
+ if (typeof current === 'string') {
929
+ try {
930
+ current = JSON.parse(current);
931
+ } catch {
932
+ // Not JSON, continue
933
+ }
934
+ }
935
+ current = current[part];
936
+ }
937
+ return current;
938
+ }
939
+ }
940
+
941
+ module.exports = { FlowContext };