@intranefr/superbackend 1.5.1 → 1.5.3
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/.env.example +10 -0
- package/index.js +2 -0
- package/manage.js +745 -0
- package/package.json +5 -2
- package/src/controllers/admin.controller.js +79 -6
- package/src/controllers/adminAgents.controller.js +37 -0
- package/src/controllers/adminExperiments.controller.js +200 -0
- package/src/controllers/adminLlm.controller.js +19 -0
- package/src/controllers/adminMarkdowns.controller.js +157 -0
- package/src/controllers/adminScripts.controller.js +243 -74
- package/src/controllers/adminTelegram.controller.js +72 -0
- package/src/controllers/experiments.controller.js +85 -0
- package/src/controllers/internalExperiments.controller.js +17 -0
- package/src/controllers/markdowns.controller.js +42 -0
- package/src/helpers/mongooseHelper.js +258 -0
- package/src/helpers/scriptBase.js +230 -0
- package/src/helpers/scriptRunner.js +335 -0
- package/src/middleware.js +195 -34
- package/src/models/Agent.js +105 -0
- package/src/models/AgentMessage.js +82 -0
- package/src/models/CacheEntry.js +1 -1
- package/src/models/ConsoleLog.js +1 -1
- package/src/models/Experiment.js +75 -0
- package/src/models/ExperimentAssignment.js +23 -0
- package/src/models/ExperimentEvent.js +26 -0
- package/src/models/ExperimentMetricBucket.js +30 -0
- package/src/models/GlobalSetting.js +1 -2
- package/src/models/Markdown.js +75 -0
- package/src/models/RateLimitCounter.js +1 -1
- package/src/models/ScriptDefinition.js +1 -0
- package/src/models/ScriptRun.js +8 -0
- package/src/models/TelegramBot.js +42 -0
- package/src/models/Webhook.js +2 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminAgents.routes.js +13 -0
- package/src/routes/adminConsoleManager.routes.js +1 -1
- package/src/routes/adminExperiments.routes.js +29 -0
- package/src/routes/adminLlm.routes.js +1 -0
- package/src/routes/adminMarkdowns.routes.js +16 -0
- package/src/routes/adminScripts.routes.js +4 -1
- package/src/routes/adminTelegram.routes.js +14 -0
- package/src/routes/blogInternal.routes.js +2 -2
- package/src/routes/experiments.routes.js +30 -0
- package/src/routes/internalExperiments.routes.js +15 -0
- package/src/routes/markdowns.routes.js +16 -0
- package/src/services/agent.service.js +546 -0
- package/src/services/agentHistory.service.js +345 -0
- package/src/services/agentTools.service.js +578 -0
- package/src/services/blogCronsBootstrap.service.js +7 -6
- package/src/services/consoleManager.service.js +56 -18
- package/src/services/consoleOverride.service.js +1 -0
- package/src/services/experiments.service.js +273 -0
- package/src/services/experimentsAggregation.service.js +308 -0
- package/src/services/experimentsCronsBootstrap.service.js +118 -0
- package/src/services/experimentsRetention.service.js +43 -0
- package/src/services/experimentsWs.service.js +134 -0
- package/src/services/globalSettings.service.js +15 -0
- package/src/services/jsonConfigs.service.js +24 -12
- package/src/services/llm.service.js +219 -6
- package/src/services/markdowns.service.js +522 -0
- package/src/services/scriptsRunner.service.js +514 -23
- package/src/services/telegram.service.js +130 -0
- package/src/utils/rbac/rightsRegistry.js +4 -0
- package/views/admin-agents.ejs +273 -0
- package/views/admin-coolify-deploy.ejs +8 -8
- package/views/admin-dashboard.ejs +63 -12
- package/views/admin-experiments.ejs +91 -0
- package/views/admin-markdowns.ejs +905 -0
- package/views/admin-scripts.ejs +817 -6
- package/views/admin-telegram.ejs +269 -0
- package/views/partials/dashboard/nav-items.ejs +4 -0
- package/views/partials/dashboard/palette.ejs +5 -3
- package/src/middleware/internalCronAuth.js +0 -29
|
@@ -1,20 +1,187 @@
|
|
|
1
1
|
const { EventEmitter } = require('events');
|
|
2
2
|
const { spawn } = require('child_process');
|
|
3
3
|
const { NodeVM } = require('vm2');
|
|
4
|
+
const mongoose = require('mongoose');
|
|
4
5
|
|
|
5
6
|
const ScriptRun = require('../models/ScriptRun');
|
|
7
|
+
const { mongooseHelper } = require('../helpers/mongooseHelper');
|
|
6
8
|
|
|
7
9
|
const MAX_TAIL_BYTES = 64 * 1024;
|
|
8
10
|
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
+
function isTruthyEnv(v) {
|
|
12
|
+
const s = String(v || '').trim().toLowerCase();
|
|
13
|
+
return s === '1' || s === 'true' || s === 'yes' || s === 'y' || s === 'on';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function shouldAutoWrapAsyncScripts() {
|
|
17
|
+
if (process.env.SCRIPT_AUTO_ASYNC_WRAP === undefined) return true;
|
|
18
|
+
return isTruthyEnv(process.env.SCRIPT_AUTO_ASYNC_WRAP);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function detectTopLevelAwait(code) {
|
|
22
|
+
const s = String(code || '');
|
|
23
|
+
if (!/\bawait\b/.test(s)) return false;
|
|
24
|
+
if (/^\s*\(\s*async\s*\(/.test(s)) return false;
|
|
25
|
+
if (/^\s*async\s+function\b/.test(s)) return false;
|
|
26
|
+
if (/\bmodule\.exports\b/.test(s) || /\bexports\./.test(s)) return false;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function wrapInAsyncIife(code) {
|
|
31
|
+
const body = String(code || '');
|
|
32
|
+
return [
|
|
33
|
+
'(async () => {',
|
|
34
|
+
body,
|
|
35
|
+
'})().then((result) => {',
|
|
36
|
+
' // Store result globally for VM2 to capture',
|
|
37
|
+
' global.__scriptResult = result;',
|
|
38
|
+
'}).catch((err) => {',
|
|
39
|
+
' try { console.error(err && err.stack ? err.stack : err); } catch {}',
|
|
40
|
+
' global.__scriptResult = { error: err.message || String(err) };',
|
|
41
|
+
'});',
|
|
42
|
+
'',
|
|
43
|
+
].join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function wrapExistingAsyncIife(code) {
|
|
47
|
+
const body = String(code || '');
|
|
48
|
+
// Remove the final closing parenthesis and semicolon, then add our result capture
|
|
49
|
+
const codeWithoutEnding = body.replace(/\)\s*;?\s*$/, '');
|
|
50
|
+
return [
|
|
51
|
+
'// Wrapped to capture return value',
|
|
52
|
+
codeWithoutEnding,
|
|
53
|
+
').then((result) => {',
|
|
54
|
+
' // Store result globally for VM2 to capture',
|
|
55
|
+
' global.__scriptResult = result;',
|
|
56
|
+
'}).catch((err) => {',
|
|
57
|
+
' try { console.error(err && err.stack ? err.stack : err); } catch {}',
|
|
58
|
+
' global.__scriptResult = { error: err.message || String(err) };',
|
|
59
|
+
'});',
|
|
60
|
+
'',
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function prepareVmCodeForExecution(code) {
|
|
65
|
+
const raw = String(code || '');
|
|
66
|
+
if (!shouldAutoWrapAsyncScripts()) return { code: raw, wrapped: false };
|
|
67
|
+
if (!detectTopLevelAwait(raw)) return { code: raw, wrapped: false };
|
|
68
|
+
|
|
69
|
+
// Check if it's already an async IIFE that doesn't expose its result
|
|
70
|
+
if (/^\s*\(\s*async\s+function\s*\(/.test(raw) && !/global\.__scriptResult\s*=/.test(raw)) {
|
|
71
|
+
// Wrap the existing async IIFE to capture its result
|
|
72
|
+
return { code: wrapExistingAsyncIife(raw), wrapped: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { code: wrapInAsyncIife(raw), wrapped: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildAwaitSyntaxHelpMessage() {
|
|
79
|
+
return [
|
|
80
|
+
'Your script uses `await` at top-level.',
|
|
81
|
+
'Wrap it in an async IIFE, or rely on auto-wrapping:',
|
|
82
|
+
'',
|
|
83
|
+
'(async () => {',
|
|
84
|
+
' const count = await countCollectionDocuments("users");',
|
|
85
|
+
' console.log("count:", count);',
|
|
86
|
+
'})();',
|
|
87
|
+
'',
|
|
88
|
+
].join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Helper function to decode script content
|
|
92
|
+
function decodeScriptContent(script, format) {
|
|
93
|
+
if (format === 'base64') {
|
|
94
|
+
try {
|
|
95
|
+
return Buffer.from(script, 'base64').toString('utf8');
|
|
96
|
+
} catch (err) {
|
|
97
|
+
throw new Error('Failed to decode base64 script content');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return script;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const nowIso = () => new Date().toISOString();
|
|
104
|
+
|
|
105
|
+
const appendTail = (tail, more) => {
|
|
106
|
+
const max = 20000; // keep last 20k chars
|
|
107
|
+
tail = (tail + more).slice(-max);
|
|
108
|
+
return tail;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Infrastructure log patterns to filter out
|
|
112
|
+
const infrastructurePatterns = [
|
|
113
|
+
'Using existing app database connection',
|
|
114
|
+
'No existing connection found',
|
|
115
|
+
'Auto-wrapping script in async function',
|
|
116
|
+
'=== SCRIPT START ===',
|
|
117
|
+
'=== SCRIPT END ===',
|
|
118
|
+
'Executing script',
|
|
119
|
+
'Script preview',
|
|
120
|
+
'Database connection established',
|
|
121
|
+
'chars)',
|
|
122
|
+
'Infrastructure logs'
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
// Utility functions for output processing
|
|
126
|
+
function isInfrastructureLog(line) {
|
|
127
|
+
return infrastructurePatterns.some(pattern => line.includes(pattern));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isMeaningfulConsoleLog(line) {
|
|
131
|
+
return !isInfrastructureLog(line) &&
|
|
132
|
+
line.trim().length > 0 &&
|
|
133
|
+
!line.startsWith('[') &&
|
|
134
|
+
!line.includes('===');
|
|
11
135
|
}
|
|
12
136
|
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return
|
|
137
|
+
function formatOutput(value) {
|
|
138
|
+
if (typeof value === 'object') {
|
|
139
|
+
return JSON.stringify(value, null, 2);
|
|
140
|
+
}
|
|
141
|
+
return String(value);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function tryParseJson(str) {
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(str);
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isJsonString(str) {
|
|
153
|
+
return tryParseJson(str) !== null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function determineProgrammaticOutput(returnValue, lastConsoleLog) {
|
|
157
|
+
// Priority 1: Return value
|
|
158
|
+
if (returnValue !== undefined && returnValue !== null) {
|
|
159
|
+
const formatted = formatOutput(returnValue);
|
|
160
|
+
return {
|
|
161
|
+
programmaticOutput: formatted,
|
|
162
|
+
outputType: 'return',
|
|
163
|
+
isJson: isJsonString(formatted),
|
|
164
|
+
parsedResult: tryParseJson(formatted)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Priority 2: Last meaningful console.log
|
|
169
|
+
if (lastConsoleLog && !isInfrastructureLog(lastConsoleLog)) {
|
|
170
|
+
return {
|
|
171
|
+
programmaticOutput: lastConsoleLog,
|
|
172
|
+
outputType: 'console',
|
|
173
|
+
isJson: isJsonString(lastConsoleLog),
|
|
174
|
+
parsedResult: tryParseJson(lastConsoleLog)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Priority 3: No output
|
|
179
|
+
return {
|
|
180
|
+
programmaticOutput: 'No output',
|
|
181
|
+
outputType: 'none',
|
|
182
|
+
isJson: false,
|
|
183
|
+
parsedResult: null
|
|
184
|
+
};
|
|
18
185
|
}
|
|
19
186
|
|
|
20
187
|
function safeJsonParse(str) {
|
|
@@ -114,24 +281,16 @@ async function startRun(scriptDef, options) {
|
|
|
114
281
|
runId: runDoc._id,
|
|
115
282
|
bus,
|
|
116
283
|
command: 'bash',
|
|
117
|
-
args: ['-lc', scriptDef.script],
|
|
284
|
+
args: ['-lc', decodeScriptContent(scriptDef.script, scriptDef.scriptFormat)],
|
|
118
285
|
env,
|
|
119
286
|
cwd,
|
|
120
287
|
timeoutMs,
|
|
121
288
|
});
|
|
122
289
|
} else if (scriptDef.type === 'node') {
|
|
123
290
|
if (scriptDef.runner === 'vm2') {
|
|
124
|
-
exitCode = await runVm2({ runId: runDoc._id, bus, code: scriptDef.script, timeoutMs });
|
|
291
|
+
exitCode = await runVm2({ runId: runDoc._id, bus, code: decodeScriptContent(scriptDef.script, scriptDef.scriptFormat), timeoutMs });
|
|
125
292
|
} else if (scriptDef.runner === 'host') {
|
|
126
|
-
exitCode = await
|
|
127
|
-
runId: runDoc._id,
|
|
128
|
-
bus,
|
|
129
|
-
command: 'node',
|
|
130
|
-
args: ['-e', scriptDef.script],
|
|
131
|
-
env,
|
|
132
|
-
cwd,
|
|
133
|
-
timeoutMs,
|
|
134
|
-
});
|
|
293
|
+
exitCode = await runHostWithDatabase({ runId: runDoc._id, bus, code: decodeScriptContent(scriptDef.script, scriptDef.scriptFormat), env, cwd, timeoutMs });
|
|
135
294
|
} else {
|
|
136
295
|
throw Object.assign(new Error('Invalid runner for node script'), { code: 'VALIDATION' });
|
|
137
296
|
}
|
|
@@ -213,19 +372,316 @@ async function runSpawned({ runId, bus, command, args, env, cwd, timeoutMs }) {
|
|
|
213
372
|
});
|
|
214
373
|
}
|
|
215
374
|
|
|
375
|
+
async function runHostWithDatabase({ runId, bus, code, env, cwd, timeoutMs }) {
|
|
376
|
+
let tail = '';
|
|
377
|
+
let scriptResult = null;
|
|
378
|
+
let lastConsoleLog = null;
|
|
379
|
+
|
|
380
|
+
function pushLog(stream, line) {
|
|
381
|
+
const s = String(line || '');
|
|
382
|
+
tail = appendTail(tail, s);
|
|
383
|
+
bus.push({ type: 'log', ts: nowIso(), stream, line: s });
|
|
384
|
+
|
|
385
|
+
// Track last console.log for programmatic output
|
|
386
|
+
if (stream === 'stdout' && isMeaningfulConsoleLog(s)) {
|
|
387
|
+
lastConsoleLog = s.trim();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Update both outputTail and fullOutput using string concatenation
|
|
391
|
+
return ScriptRun.findById(runId).then(run => {
|
|
392
|
+
if (run) {
|
|
393
|
+
run.outputTail = tail;
|
|
394
|
+
run.fullOutput = (run.fullOutput || '') + s;
|
|
395
|
+
run.lastConsoleLog = lastConsoleLog;
|
|
396
|
+
run.lastOutputUpdate = new Date();
|
|
397
|
+
run.outputSize = (run.outputSize || 0) + s.length;
|
|
398
|
+
run.lineCount = (run.lineCount || 0) + (s.split('\n').length - 1);
|
|
399
|
+
return run.save();
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
// Use existing app connection if available, otherwise create new one
|
|
406
|
+
if (mongoose.connection.readyState !== 1) {
|
|
407
|
+
await pushLog('stdout', 'No existing connection found, establishing new connection...\n');
|
|
408
|
+
await mongooseHelper.connect();
|
|
409
|
+
|
|
410
|
+
// Wait for connection to be fully ready
|
|
411
|
+
await mongooseHelper.waitForConnection(5000);
|
|
412
|
+
} else {
|
|
413
|
+
await pushLog('stdout', 'Using existing app database connection\n');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Validate connection is ready
|
|
417
|
+
if (mongoose.connection.readyState !== 1) {
|
|
418
|
+
throw new Error('Database connection is not ready');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const prepared = prepareVmCodeForExecution(code);
|
|
422
|
+
if (prepared.wrapped) {
|
|
423
|
+
await pushLog('stdout', 'Auto-wrapping script to capture return value\n');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Create a VM with database context
|
|
427
|
+
const vm = new NodeVM({
|
|
428
|
+
console: 'inherit',
|
|
429
|
+
sandbox: {
|
|
430
|
+
// Expose pre-connected mongoose instance
|
|
431
|
+
mongoose: mongoose,
|
|
432
|
+
db: mongoose.connection.db,
|
|
433
|
+
|
|
434
|
+
// Expose helper functions
|
|
435
|
+
countCollectionDocuments: async (collectionName, query = {}) => {
|
|
436
|
+
try {
|
|
437
|
+
// Ensure connection is still valid
|
|
438
|
+
if (mongoose.connection.readyState !== 1) {
|
|
439
|
+
throw new Error('Database connection lost during operation');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const db = mongoose.connection.db;
|
|
443
|
+
if (!db) {
|
|
444
|
+
throw new Error('Database instance not available');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const collection = db.collection(collectionName);
|
|
448
|
+
const count = await collection.countDocuments(query);
|
|
449
|
+
return count;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
throw new Error(`Failed to count documents in ${collectionName}: ${error.message}`);
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
// Expose connection status helper
|
|
456
|
+
getConnectionStatus: () => {
|
|
457
|
+
const readyStateMap = {
|
|
458
|
+
0: 'disconnected',
|
|
459
|
+
1: 'connected',
|
|
460
|
+
2: 'connecting',
|
|
461
|
+
3: 'disconnecting'
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
readyState: mongoose.connection.readyState,
|
|
466
|
+
readyStateText: readyStateMap[mongoose.connection.readyState] || 'unknown',
|
|
467
|
+
host: mongoose.connection.host,
|
|
468
|
+
name: mongoose.connection.name,
|
|
469
|
+
hasActiveConnection: mongoose.connection.readyState === 1
|
|
470
|
+
};
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
// Expose models if available
|
|
474
|
+
models: mongoose.models || {},
|
|
475
|
+
|
|
476
|
+
// Global objects
|
|
477
|
+
JSON,
|
|
478
|
+
Date,
|
|
479
|
+
Math,
|
|
480
|
+
parseInt,
|
|
481
|
+
parseFloat,
|
|
482
|
+
String,
|
|
483
|
+
Number,
|
|
484
|
+
Object,
|
|
485
|
+
Array,
|
|
486
|
+
|
|
487
|
+
// Process environment
|
|
488
|
+
process: {
|
|
489
|
+
env: { ...process.env, ...env }
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
// Debug: Log available models
|
|
493
|
+
debugModels: () => {
|
|
494
|
+
console.log('Available models:', Object.keys(mongoose.models || {}));
|
|
495
|
+
return mongoose.models || {};
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
// Global variables for async result capture
|
|
499
|
+
global: {},
|
|
500
|
+
__scriptResult: undefined
|
|
501
|
+
},
|
|
502
|
+
require: {
|
|
503
|
+
external: false,
|
|
504
|
+
builtin: ['util', 'path', 'os', 'mongoose'], // Allow mongoose for parent app model access
|
|
505
|
+
},
|
|
506
|
+
timeout: timeoutMs,
|
|
507
|
+
eval: false,
|
|
508
|
+
wasm: false,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Set up console redirection
|
|
512
|
+
vm.on('console.log', (...args) => {
|
|
513
|
+
const message = args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ') + '\n';
|
|
514
|
+
|
|
515
|
+
// Send to UI output
|
|
516
|
+
pushLog('stdout', message);
|
|
517
|
+
|
|
518
|
+
// Also send to parent process (backend logs)
|
|
519
|
+
console.log('[Script]', message.trim());
|
|
520
|
+
});
|
|
521
|
+
vm.on('console.error', (...args) => {
|
|
522
|
+
const message = args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ') + '\n';
|
|
523
|
+
|
|
524
|
+
// Send to UI output
|
|
525
|
+
pushLog('stderr', message);
|
|
526
|
+
|
|
527
|
+
// Also send to parent process (backend logs)
|
|
528
|
+
console.error('[Script]', message.trim());
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Handle unhandled promise rejections within the VM
|
|
532
|
+
vm.on('unhandledRejection', (reason, promise) => {
|
|
533
|
+
let errorMsg = 'Unhandled Promise Rejection: ';
|
|
534
|
+
|
|
535
|
+
if (reason instanceof Error) {
|
|
536
|
+
errorMsg += reason.message;
|
|
537
|
+
if (reason.stack) {
|
|
538
|
+
errorMsg += '\n' + reason.stack;
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
errorMsg += JSON.stringify(reason);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Send to UI output
|
|
545
|
+
pushLog('stderr', errorMsg + '\n');
|
|
546
|
+
|
|
547
|
+
// Also send to parent process (backend logs)
|
|
548
|
+
console.error('[Script]', errorMsg);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Handle uncaught exceptions within the VM
|
|
552
|
+
vm.on('error', (error) => {
|
|
553
|
+
let errorMsg = 'VM Error: ';
|
|
554
|
+
|
|
555
|
+
if (error instanceof Error) {
|
|
556
|
+
errorMsg += error.message;
|
|
557
|
+
if (error.stack) {
|
|
558
|
+
errorMsg += '\n' + error.stack;
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
errorMsg += JSON.stringify(error);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Send to UI output
|
|
565
|
+
pushLog('stderr', errorMsg + '\n');
|
|
566
|
+
|
|
567
|
+
// Also send to parent process (backend logs)
|
|
568
|
+
console.error('[Script]', errorMsg);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Run the script code with better error handling
|
|
572
|
+
try {
|
|
573
|
+
await pushLog('stdout', `=== SCRIPT START ===\n`);
|
|
574
|
+
await pushLog('stdout', `Executing script (${prepared.code.length} chars)...\n`);
|
|
575
|
+
|
|
576
|
+
// Show first few lines of script for debugging
|
|
577
|
+
const scriptPreview = prepared.code.split('\n').slice(0, 5).join('\n');
|
|
578
|
+
await pushLog('stdout', `Script preview:\n${scriptPreview}\n...\n`);
|
|
579
|
+
|
|
580
|
+
// Run the script
|
|
581
|
+
vm.run(prepared.code, 'script.host.js');
|
|
582
|
+
|
|
583
|
+
// Capture result based on whether the script was wrapped
|
|
584
|
+
if (prepared.wrapped) {
|
|
585
|
+
// For wrapped scripts, wait and capture from global variable
|
|
586
|
+
await new Promise(resolve => setTimeout(resolve, 100)); // Wait for async completion
|
|
587
|
+
scriptResult = vm.sandbox.__scriptResult;
|
|
588
|
+
} else {
|
|
589
|
+
// For non-wrapped scripts, try to capture direct return or use global variable
|
|
590
|
+
scriptResult = vm.sandbox.__scriptResult;
|
|
591
|
+
// Also try to get the last meaningful console.log as fallback
|
|
592
|
+
if (!scriptResult && lastConsoleLog) {
|
|
593
|
+
scriptResult = lastConsoleLog;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
await pushLog('stdout', `=== SCRIPT END ===\n`);
|
|
598
|
+
|
|
599
|
+
// Determine and save programmatic output
|
|
600
|
+
const programmaticOutput = determineProgrammaticOutput(scriptResult, lastConsoleLog);
|
|
601
|
+
await ScriptRun.updateOne(
|
|
602
|
+
{ _id: runId },
|
|
603
|
+
{
|
|
604
|
+
$set: {
|
|
605
|
+
programmaticOutput: programmaticOutput.programmaticOutput,
|
|
606
|
+
returnResult: scriptResult !== undefined && scriptResult !== null ? formatOutput(scriptResult) : '',
|
|
607
|
+
outputType: programmaticOutput.outputType
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
);
|
|
611
|
+
} catch (vmError) {
|
|
612
|
+
let errorMsg = 'VM execution error: ';
|
|
613
|
+
|
|
614
|
+
if (vmError instanceof Error) {
|
|
615
|
+
errorMsg += vmError.message;
|
|
616
|
+
if (vmError.stack) {
|
|
617
|
+
errorMsg += '\n' + vmError.stack;
|
|
618
|
+
}
|
|
619
|
+
} else {
|
|
620
|
+
errorMsg += JSON.stringify(vmError);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const help = vmError?.message?.includes('await is only valid in async functions') ? `\n\n${buildAwaitSyntaxHelpMessage()}` : '';
|
|
624
|
+
await pushLog('stderr', errorMsg + help + '\n');
|
|
625
|
+
return 1;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return 0;
|
|
629
|
+
|
|
630
|
+
} catch (err) {
|
|
631
|
+
let errorMsg = 'Host script error: ';
|
|
632
|
+
|
|
633
|
+
if (err instanceof Error) {
|
|
634
|
+
errorMsg += err.message;
|
|
635
|
+
if (err.stack) {
|
|
636
|
+
errorMsg += '\n' + err.stack;
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
errorMsg += JSON.stringify(err);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
await pushLog('stderr', errorMsg + '\n');
|
|
643
|
+
return 1;
|
|
644
|
+
} finally {
|
|
645
|
+
// Don't disconnect here - let mongooseHelper manage connection pooling
|
|
646
|
+
// The connection will be cleaned up when the helper decides
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
216
650
|
async function runVm2({ runId, bus, code, timeoutMs }) {
|
|
217
651
|
let tail = '';
|
|
652
|
+
let scriptResult = null;
|
|
653
|
+
let lastConsoleLog = null;
|
|
218
654
|
|
|
219
655
|
function pushLog(stream, line) {
|
|
220
656
|
const s = String(line || '');
|
|
221
657
|
tail = appendTail(tail, s);
|
|
222
658
|
bus.push({ type: 'log', ts: nowIso(), stream, line: s });
|
|
223
|
-
|
|
659
|
+
|
|
660
|
+
// Track last console.log for programmatic output
|
|
661
|
+
if (stream === 'stdout' && isMeaningfulConsoleLog(s)) {
|
|
662
|
+
lastConsoleLog = s.trim();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Update both outputTail and fullOutput using string concatenation
|
|
666
|
+
return ScriptRun.findById(runId).then(run => {
|
|
667
|
+
if (run) {
|
|
668
|
+
run.outputTail = tail;
|
|
669
|
+
run.fullOutput = (run.fullOutput || '') + s;
|
|
670
|
+
run.lastConsoleLog = lastConsoleLog;
|
|
671
|
+
run.lastOutputUpdate = new Date();
|
|
672
|
+
run.outputSize = (run.outputSize || 0) + s.length;
|
|
673
|
+
run.lineCount = (run.lineCount || 0) + (s.split('\n').length - 1);
|
|
674
|
+
return run.save();
|
|
675
|
+
}
|
|
676
|
+
});
|
|
224
677
|
}
|
|
225
678
|
|
|
226
679
|
const vm = new NodeVM({
|
|
227
680
|
console: 'redirect',
|
|
228
|
-
sandbox: {
|
|
681
|
+
sandbox: {
|
|
682
|
+
global: {},
|
|
683
|
+
__scriptResult: undefined
|
|
684
|
+
},
|
|
229
685
|
require: {
|
|
230
686
|
external: false,
|
|
231
687
|
builtin: [],
|
|
@@ -243,11 +699,46 @@ async function runVm2({ runId, bus, code, timeoutMs }) {
|
|
|
243
699
|
});
|
|
244
700
|
|
|
245
701
|
try {
|
|
246
|
-
|
|
702
|
+
const prepared = prepareVmCodeForExecution(code);
|
|
703
|
+
if (prepared.wrapped) {
|
|
704
|
+
await pushLog('stdout', 'Auto-wrapping script to capture return value\n');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Run the script
|
|
708
|
+
vm.run(prepared.code, 'script.vm2.js');
|
|
709
|
+
|
|
710
|
+
// Capture result based on whether the script was wrapped
|
|
711
|
+
if (prepared.wrapped) {
|
|
712
|
+
// For wrapped scripts, wait and capture from global variable
|
|
713
|
+
await new Promise(resolve => setTimeout(resolve, 100)); // Wait for async completion
|
|
714
|
+
scriptResult = vm.sandbox.__scriptResult;
|
|
715
|
+
} else {
|
|
716
|
+
// For non-wrapped scripts, try to capture direct return or use global variable
|
|
717
|
+
scriptResult = vm.sandbox.__scriptResult;
|
|
718
|
+
// Also try to get the last meaningful console.log as fallback
|
|
719
|
+
if (!scriptResult && lastConsoleLog) {
|
|
720
|
+
scriptResult = lastConsoleLog;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Determine and save programmatic output
|
|
725
|
+
const programmaticOutput = determineProgrammaticOutput(scriptResult, lastConsoleLog);
|
|
726
|
+
await ScriptRun.updateOne(
|
|
727
|
+
{ _id: runId },
|
|
728
|
+
{
|
|
729
|
+
$set: {
|
|
730
|
+
programmaticOutput: programmaticOutput.programmaticOutput,
|
|
731
|
+
returnResult: scriptResult !== undefined && scriptResult !== null ? formatOutput(scriptResult) : '',
|
|
732
|
+
outputType: programmaticOutput.outputType
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
);
|
|
736
|
+
|
|
247
737
|
return 0;
|
|
248
738
|
} catch (err) {
|
|
249
|
-
const
|
|
250
|
-
|
|
739
|
+
const baseMsg = err?.message || 'vm2 error';
|
|
740
|
+
const help = baseMsg.includes('await is only valid in async functions') ? `\n\n${buildAwaitSyntaxHelpMessage()}` : '';
|
|
741
|
+
await pushLog('stderr', baseMsg + help + '\n');
|
|
251
742
|
return 1;
|
|
252
743
|
}
|
|
253
744
|
}
|