@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,5 +1,6 @@
|
|
|
1
1
|
const ScriptDefinition = require('../models/ScriptDefinition');
|
|
2
2
|
const ScriptRun = require('../models/ScriptRun');
|
|
3
|
+
const { basicAuth } = require('../middleware/auth');
|
|
3
4
|
const { startRun, getRunBus } = require('../services/scriptsRunner.service');
|
|
4
5
|
const { logAuditSync } = require('../services/auditLogger');
|
|
5
6
|
|
|
@@ -12,19 +13,6 @@ function toSafeJsonError(error) {
|
|
|
12
13
|
return { status: 500, body: { error: msg } };
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function audit(req, event) {
|
|
16
|
-
logAuditSync({
|
|
17
|
-
req,
|
|
18
|
-
action: event.action,
|
|
19
|
-
outcome: event.outcome,
|
|
20
|
-
entityType: 'ScriptDefinition',
|
|
21
|
-
entityId: event.entityId ? String(event.entityId) : null,
|
|
22
|
-
before: event.before || null,
|
|
23
|
-
after: event.after || null,
|
|
24
|
-
details: event.details || undefined,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
16
|
function normalizeEnv(env) {
|
|
29
17
|
const items = Array.isArray(env) ? env : [];
|
|
30
18
|
const out = [];
|
|
@@ -37,6 +25,35 @@ function normalizeEnv(env) {
|
|
|
37
25
|
return out;
|
|
38
26
|
}
|
|
39
27
|
|
|
28
|
+
// Helper functions for base64 handling
|
|
29
|
+
function isBase64(str) {
|
|
30
|
+
try {
|
|
31
|
+
return Buffer.from(str, 'base64').toString('base64') === str;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isValidBase64(str) {
|
|
38
|
+
try {
|
|
39
|
+
Buffer.from(str, 'base64');
|
|
40
|
+
return true;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function decodeScriptContent(script, format) {
|
|
47
|
+
if (format === 'base64') {
|
|
48
|
+
try {
|
|
49
|
+
return Buffer.from(script, 'base64').toString('utf8');
|
|
50
|
+
} catch (err) {
|
|
51
|
+
throw new Error('Failed to decode base64 script content');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return script;
|
|
55
|
+
}
|
|
56
|
+
|
|
40
57
|
exports.listScripts = async (req, res) => {
|
|
41
58
|
try {
|
|
42
59
|
const items = await ScriptDefinition.find().sort({ updatedAt: -1 }).lean();
|
|
@@ -61,41 +78,74 @@ exports.getScript = async (req, res) => {
|
|
|
61
78
|
exports.createScript = async (req, res) => {
|
|
62
79
|
let created = null;
|
|
63
80
|
try {
|
|
81
|
+
console.log('[createScript] Starting script creation...');
|
|
82
|
+
console.log('[createScript] Request body keys:', Object.keys(req.body || {}));
|
|
83
|
+
|
|
64
84
|
const payload = req.body || {};
|
|
85
|
+
console.log('[createScript] Payload name:', payload.name);
|
|
86
|
+
console.log('[createScript] Payload type:', payload.type);
|
|
87
|
+
console.log('[createScript] Payload runner:', payload.runner);
|
|
88
|
+
console.log('[createScript] Script length:', (payload.script || '').length);
|
|
89
|
+
console.log('[createScript] Script format:', payload.scriptFormat);
|
|
90
|
+
|
|
91
|
+
// Handle script content encoding
|
|
92
|
+
let scriptContent = String(payload.script || '');
|
|
93
|
+
let scriptFormat = payload.scriptFormat || 'string';
|
|
94
|
+
|
|
95
|
+
// Auto-detect base64 if not specified and content looks like base64
|
|
96
|
+
if (scriptFormat === 'string' && isBase64(scriptContent)) {
|
|
97
|
+
scriptFormat = 'base64';
|
|
98
|
+
console.log('[createScript] Auto-detected base64 format');
|
|
99
|
+
}
|
|
65
100
|
|
|
101
|
+
// Validate base64 content if format is base64
|
|
102
|
+
if (scriptFormat === 'base64' && !isValidBase64(scriptContent)) {
|
|
103
|
+
console.log('[createScript] Invalid base64 content detected');
|
|
104
|
+
throw new Error('Invalid base64 script content');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log('[createScript] About to create ScriptDefinition...');
|
|
66
108
|
const doc = await ScriptDefinition.create({
|
|
67
109
|
name: String(payload.name || '').trim(),
|
|
68
110
|
codeIdentifier: String(payload.codeIdentifier || '').trim(),
|
|
69
111
|
description: String(payload.description || ''),
|
|
70
112
|
type: String(payload.type || '').trim(),
|
|
71
113
|
runner: String(payload.runner || '').trim(),
|
|
72
|
-
script:
|
|
114
|
+
script: scriptContent,
|
|
115
|
+
scriptFormat: scriptFormat,
|
|
73
116
|
defaultWorkingDirectory: String(payload.defaultWorkingDirectory || ''),
|
|
74
117
|
env: normalizeEnv(payload.env),
|
|
75
118
|
timeoutMs: payload.timeoutMs === undefined ? undefined : Number(payload.timeoutMs),
|
|
76
119
|
enabled: payload.enabled === undefined ? true : Boolean(payload.enabled),
|
|
77
120
|
});
|
|
121
|
+
|
|
122
|
+
console.log('[createScript] ScriptDefinition created successfully');
|
|
78
123
|
|
|
79
124
|
created = doc.toObject();
|
|
80
|
-
|
|
125
|
+
console.log('[createScript] About to create audit entry...');
|
|
126
|
+
console.log('[createScript] Script created successfully:', { name: created.name, id: created._id });
|
|
127
|
+
|
|
128
|
+
logAuditSync({
|
|
129
|
+
req,
|
|
81
130
|
action: 'scripts.create',
|
|
82
131
|
outcome: 'success',
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
132
|
+
entityType: 'ScriptDefinition',
|
|
133
|
+
entityId: created._id,
|
|
134
|
+
data: { name: created.name },
|
|
86
135
|
});
|
|
87
|
-
|
|
136
|
+
|
|
137
|
+
console.log('[createScript] About to send response...');
|
|
88
138
|
res.status(201).json({ item: doc.toObject() });
|
|
139
|
+
console.log('[createScript] Response sent successfully');
|
|
89
140
|
} catch (err) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
details: { error: err?.message || 'Operation failed' },
|
|
97
|
-
});
|
|
141
|
+
console.log('[createScript] ERROR occurred:', err);
|
|
142
|
+
console.log('[createScript] ERROR stack:', err.stack);
|
|
143
|
+
console.log('[createScript] ERROR message:', err.message);
|
|
144
|
+
console.log('[createScript] ERROR code:', err.code);
|
|
145
|
+
|
|
146
|
+
console.log('[createScript] Script creation failed:', { error: err?.message || 'Operation failed' });
|
|
98
147
|
const safe = toSafeJsonError(err);
|
|
148
|
+
console.log('[createScript] Safe error:', safe);
|
|
99
149
|
res.status(safe.status).json(safe.body);
|
|
100
150
|
}
|
|
101
151
|
};
|
|
@@ -111,12 +161,30 @@ exports.updateScript = async (req, res) => {
|
|
|
111
161
|
|
|
112
162
|
before = doc.toObject();
|
|
113
163
|
|
|
164
|
+
// Handle script content encoding
|
|
165
|
+
if (payload.script !== undefined) {
|
|
166
|
+
let scriptContent = String(payload.script || '');
|
|
167
|
+
let scriptFormat = payload.scriptFormat || doc.scriptFormat || 'string';
|
|
168
|
+
|
|
169
|
+
// Auto-detect base64 if not specified and content looks like base64
|
|
170
|
+
if (scriptFormat === 'string' && isBase64(scriptContent)) {
|
|
171
|
+
scriptFormat = 'base64';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Validate base64 content if format is base64
|
|
175
|
+
if (scriptFormat === 'base64' && !isValidBase64(scriptContent)) {
|
|
176
|
+
throw new Error('Invalid base64 script content');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
doc.script = scriptContent;
|
|
180
|
+
doc.scriptFormat = scriptFormat;
|
|
181
|
+
}
|
|
182
|
+
|
|
114
183
|
if (payload.name !== undefined) doc.name = String(payload.name || '').trim();
|
|
115
184
|
if (payload.codeIdentifier !== undefined) doc.codeIdentifier = String(payload.codeIdentifier || '').trim();
|
|
116
185
|
if (payload.description !== undefined) doc.description = String(payload.description || '');
|
|
117
186
|
if (payload.type !== undefined) doc.type = String(payload.type || '').trim();
|
|
118
187
|
if (payload.runner !== undefined) doc.runner = String(payload.runner || '').trim();
|
|
119
|
-
if (payload.script !== undefined) doc.script = String(payload.script || '');
|
|
120
188
|
if (payload.defaultWorkingDirectory !== undefined) {
|
|
121
189
|
doc.defaultWorkingDirectory = String(payload.defaultWorkingDirectory || '');
|
|
122
190
|
}
|
|
@@ -126,23 +194,11 @@ exports.updateScript = async (req, res) => {
|
|
|
126
194
|
|
|
127
195
|
await doc.save();
|
|
128
196
|
after = doc.toObject();
|
|
129
|
-
|
|
130
|
-
action: 'scripts.update',
|
|
131
|
-
outcome: 'success',
|
|
132
|
-
entityId: doc._id,
|
|
133
|
-
before,
|
|
134
|
-
after,
|
|
135
|
-
});
|
|
197
|
+
console.log('[updateScript] Script updated successfully:', { name: after.name, id: after._id });
|
|
136
198
|
res.json({ item: doc.toObject() });
|
|
137
199
|
} catch (err) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
outcome: 'failure',
|
|
141
|
-
entityId: req.params?.id,
|
|
142
|
-
before,
|
|
143
|
-
after,
|
|
144
|
-
details: { error: err?.message || 'Operation failed' },
|
|
145
|
-
});
|
|
200
|
+
console.log('[updateScript] ERROR occurred:', err);
|
|
201
|
+
console.log('[updateScript] ERROR message:', err.message);
|
|
146
202
|
const safe = toSafeJsonError(err);
|
|
147
203
|
res.status(safe.status).json(safe.body);
|
|
148
204
|
}
|
|
@@ -156,23 +212,11 @@ exports.deleteScript = async (req, res) => {
|
|
|
156
212
|
before = doc.toObject();
|
|
157
213
|
await doc.deleteOne();
|
|
158
214
|
|
|
159
|
-
|
|
160
|
-
action: 'scripts.delete',
|
|
161
|
-
outcome: 'success',
|
|
162
|
-
entityId: doc._id,
|
|
163
|
-
before,
|
|
164
|
-
after: null,
|
|
165
|
-
});
|
|
215
|
+
console.log('[deleteScript] Script deleted successfully:', { name: before.name, id: before._id });
|
|
166
216
|
res.json({ ok: true });
|
|
167
217
|
} catch (err) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
outcome: 'failure',
|
|
171
|
-
entityId: req.params?.id,
|
|
172
|
-
before,
|
|
173
|
-
after: null,
|
|
174
|
-
details: { error: err?.message || 'Operation failed' },
|
|
175
|
-
});
|
|
218
|
+
console.log('[deleteScript] ERROR occurred:', err);
|
|
219
|
+
console.log('[deleteScript] ERROR message:', err.message);
|
|
176
220
|
const safe = toSafeJsonError(err);
|
|
177
221
|
res.status(safe.status).json(safe.body);
|
|
178
222
|
}
|
|
@@ -189,25 +233,12 @@ exports.runScript = async (req, res) => {
|
|
|
189
233
|
|
|
190
234
|
const runDoc = await startRun(doc, { trigger: 'manual', meta: { actorType: 'basicAuth' } });
|
|
191
235
|
|
|
192
|
-
|
|
193
|
-
action: 'scripts.run',
|
|
194
|
-
outcome: 'success',
|
|
195
|
-
entityId: doc._id,
|
|
196
|
-
before: null,
|
|
197
|
-
after: null,
|
|
198
|
-
details: { runId: String(runDoc._id) },
|
|
199
|
-
});
|
|
236
|
+
console.log('[runScript] Script executed successfully:', { name: script.name, runId: runDoc._id });
|
|
200
237
|
|
|
201
238
|
res.json({ runId: String(runDoc._id) });
|
|
202
239
|
} catch (err) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
outcome: 'failure',
|
|
206
|
-
entityId: req.params?.id,
|
|
207
|
-
before: script,
|
|
208
|
-
after: null,
|
|
209
|
-
details: { error: err?.message || 'Operation failed' },
|
|
210
|
-
});
|
|
240
|
+
console.log('[runScript] ERROR occurred:', err);
|
|
241
|
+
console.log('[runScript] ERROR message:', err.message);
|
|
211
242
|
const safe = toSafeJsonError(err);
|
|
212
243
|
res.status(safe.status).json(safe.body);
|
|
213
244
|
}
|
|
@@ -318,3 +349,141 @@ exports.streamRun = async (req, res) => {
|
|
|
318
349
|
return res.end();
|
|
319
350
|
}
|
|
320
351
|
};
|
|
352
|
+
|
|
353
|
+
// Get programmatic output (clean result for API consumption)
|
|
354
|
+
async function getProgrammaticOutput(req, res) {
|
|
355
|
+
try {
|
|
356
|
+
const { runId } = req.params;
|
|
357
|
+
if (!runId) {
|
|
358
|
+
return res.status(400).json({ error: 'runId is required' });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const run = await ScriptRun.findById(runId).lean();
|
|
362
|
+
if (!run) {
|
|
363
|
+
return res.status(404).json({ error: 'Script run not found' });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Parse programmatic output if it's JSON
|
|
367
|
+
let parsedResult = null;
|
|
368
|
+
let isJson = false;
|
|
369
|
+
|
|
370
|
+
if (run.programmaticOutput) {
|
|
371
|
+
try {
|
|
372
|
+
parsedResult = JSON.parse(run.programmaticOutput);
|
|
373
|
+
isJson = true;
|
|
374
|
+
} catch {
|
|
375
|
+
// Not JSON, keep as string
|
|
376
|
+
parsedResult = null;
|
|
377
|
+
isJson = false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
res.json({
|
|
382
|
+
runId: run._id,
|
|
383
|
+
status: run.status,
|
|
384
|
+
exitCode: run.exitCode,
|
|
385
|
+
programmaticOutput: run.programmaticOutput || 'No output',
|
|
386
|
+
outputType: run.outputType || 'none',
|
|
387
|
+
isJson: isJson,
|
|
388
|
+
parsedResult: parsedResult,
|
|
389
|
+
returnResult: run.returnResult,
|
|
390
|
+
lastConsoleLog: run.lastConsoleLog,
|
|
391
|
+
createdAt: run.createdAt,
|
|
392
|
+
updatedAt: run.updatedAt,
|
|
393
|
+
startedAt: run.startedAt,
|
|
394
|
+
finishedAt: run.finishedAt
|
|
395
|
+
});
|
|
396
|
+
} catch (err) {
|
|
397
|
+
console.error('Error getting programmatic output:', err);
|
|
398
|
+
res.status(500).json({ error: err?.message || 'Internal server error' });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Get full script output
|
|
403
|
+
async function getFullOutput(req, res) {
|
|
404
|
+
try {
|
|
405
|
+
const { runId } = req.params;
|
|
406
|
+
if (!runId) {
|
|
407
|
+
return res.status(400).json({ error: 'runId is required' });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const run = await ScriptRun.findById(runId).lean();
|
|
411
|
+
if (!run) {
|
|
412
|
+
return res.status(404).json({ error: 'Script run not found' });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
res.json({
|
|
416
|
+
runId: run._id,
|
|
417
|
+
status: run.status,
|
|
418
|
+
exitCode: run.exitCode,
|
|
419
|
+
fullOutput: run.fullOutput || '',
|
|
420
|
+
outputSize: run.outputSize || 0,
|
|
421
|
+
lineCount: run.lineCount || 0,
|
|
422
|
+
lastOutputUpdate: run.lastOutputUpdate,
|
|
423
|
+
createdAt: run.createdAt,
|
|
424
|
+
updatedAt: run.updatedAt,
|
|
425
|
+
startedAt: run.startedAt,
|
|
426
|
+
finishedAt: run.finishedAt
|
|
427
|
+
});
|
|
428
|
+
} catch (err) {
|
|
429
|
+
console.error('Error getting full output:', err);
|
|
430
|
+
res.status(500).json({ error: err?.message || 'Internal server error' });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Download script output as file
|
|
435
|
+
async function downloadOutput(req, res) {
|
|
436
|
+
try {
|
|
437
|
+
const { runId } = req.params;
|
|
438
|
+
if (!runId) {
|
|
439
|
+
return res.status(400).json({ error: 'runId is required' });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const run = await ScriptRun.findById(runId).lean();
|
|
443
|
+
if (!run) {
|
|
444
|
+
return res.status(404).json({ error: 'Script run not found' });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const filename = `script-output-${runId}-${run.createdAt.toISOString().slice(0, 19).replace(/:/g, '-')}.txt`;
|
|
448
|
+
|
|
449
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
450
|
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
451
|
+
|
|
452
|
+
// Include metadata at the top
|
|
453
|
+
const metadata = [
|
|
454
|
+
`Script Run ID: ${runId}`,
|
|
455
|
+
`Status: ${run.status}`,
|
|
456
|
+
`Exit Code: ${run.exitCode || 'N/A'}`,
|
|
457
|
+
`Started: ${run.startedAt || 'N/A'}`,
|
|
458
|
+
`Finished: ${run.finishedAt || 'N/A'}`,
|
|
459
|
+
`Output Size: ${run.outputSize || 0} characters`,
|
|
460
|
+
`Line Count: ${run.lineCount || 0}`,
|
|
461
|
+
`Created: ${run.createdAt}`,
|
|
462
|
+
'=' .repeat(50),
|
|
463
|
+
''
|
|
464
|
+
].join('\n');
|
|
465
|
+
|
|
466
|
+
res.send(metadata + (run.fullOutput || run.outputTail || 'No output available'));
|
|
467
|
+
} catch (err) {
|
|
468
|
+
console.error('Error downloading output:', err);
|
|
469
|
+
res.status(500).json({ error: err?.message || 'Internal server error' });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
exports.getFullOutput = getFullOutput;
|
|
474
|
+
exports.downloadOutput = downloadOutput;
|
|
475
|
+
|
|
476
|
+
module.exports = {
|
|
477
|
+
listScripts: exports.listScripts,
|
|
478
|
+
getScript: exports.getScript,
|
|
479
|
+
createScript: exports.createScript,
|
|
480
|
+
updateScript: exports.updateScript,
|
|
481
|
+
deleteScript: exports.deleteScript,
|
|
482
|
+
runScript: exports.runScript,
|
|
483
|
+
listRuns: exports.listRuns,
|
|
484
|
+
getRun: exports.getRun,
|
|
485
|
+
streamRunLogs: exports.streamRun,
|
|
486
|
+
getProgrammaticOutput: getProgrammaticOutput,
|
|
487
|
+
getFullOutput: exports.getFullOutput,
|
|
488
|
+
downloadOutput: exports.downloadOutput,
|
|
489
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const TelegramBot = require('../models/TelegramBot');
|
|
2
|
+
const Agent = require('../models/Agent');
|
|
3
|
+
const telegramService = require('../services/telegram.service');
|
|
4
|
+
|
|
5
|
+
exports.listBots = async (req, res) => {
|
|
6
|
+
try {
|
|
7
|
+
const bots = await TelegramBot.find().populate('defaultAgentId', 'name').lean();
|
|
8
|
+
return res.json({ items: bots });
|
|
9
|
+
} catch (error) {
|
|
10
|
+
return res.status(500).json({ error: error.message });
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
exports.createBot = async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const data = { ...req.body };
|
|
17
|
+
if (data.defaultAgentId === '') delete data.defaultAgentId;
|
|
18
|
+
|
|
19
|
+
const bot = await TelegramBot.create(data);
|
|
20
|
+
if (bot.isActive) {
|
|
21
|
+
await telegramService.startBot(bot._id);
|
|
22
|
+
}
|
|
23
|
+
return res.json(bot);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return res.status(500).json({ error: error.message });
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
exports.updateBot = async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const data = { ...req.body };
|
|
32
|
+
if (data.defaultAgentId === '') data.defaultAgentId = null;
|
|
33
|
+
|
|
34
|
+
const bot = await TelegramBot.findByIdAndUpdate(req.params.id, data, { new: true });
|
|
35
|
+
if (bot.isActive) {
|
|
36
|
+
await telegramService.startBot(bot._id);
|
|
37
|
+
} else {
|
|
38
|
+
await telegramService.stopBot(bot._id);
|
|
39
|
+
}
|
|
40
|
+
return res.json(bot);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return res.status(500).json({ error: error.message });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
exports.deleteBot = async (req, res) => {
|
|
47
|
+
try {
|
|
48
|
+
await telegramService.stopBot(req.params.id);
|
|
49
|
+
await TelegramBot.findByIdAndDelete(req.params.id);
|
|
50
|
+
return res.json({ success: true });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return res.status(500).json({ error: error.message });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
exports.toggleBot = async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const bot = await TelegramBot.findById(req.params.id);
|
|
59
|
+
bot.isActive = !bot.isActive;
|
|
60
|
+
await bot.save();
|
|
61
|
+
|
|
62
|
+
if (bot.isActive) {
|
|
63
|
+
await telegramService.startBot(bot._id);
|
|
64
|
+
} else {
|
|
65
|
+
await telegramService.stopBot(bot._id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return res.json(bot);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return res.status(500).json({ error: error.message });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const experimentsService = require('../services/experiments.service');
|
|
2
|
+
|
|
3
|
+
function toSafeJsonError(error) {
|
|
4
|
+
const msg = error?.message || 'Operation failed';
|
|
5
|
+
const code = error?.code;
|
|
6
|
+
if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
|
|
7
|
+
if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
|
|
8
|
+
if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
|
|
9
|
+
return { status: 500, body: { error: msg } };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
exports.getAssignment = async (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const orgId = req.headers['x-org-id'] || req.query.orgId || req.body?.orgId;
|
|
15
|
+
const subjectId = req.query.subjectId || req.body?.subjectId;
|
|
16
|
+
|
|
17
|
+
const context = req.body?.context && typeof req.body.context === 'object' ? req.body.context : {};
|
|
18
|
+
|
|
19
|
+
const { experiment, assignment } = await experimentsService.getOrCreateAssignment({
|
|
20
|
+
orgId,
|
|
21
|
+
experimentCode: req.params.code,
|
|
22
|
+
subjectId,
|
|
23
|
+
context,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const variant = (experiment.variants || []).find((v) => String(v?.key || '') === String(assignment.variantKey));
|
|
27
|
+
const config = await experimentsService.resolveVariantConfig(variant);
|
|
28
|
+
|
|
29
|
+
const { snapshot } = await experimentsService.getWinnerSnapshot({ orgId, experimentCode: req.params.code });
|
|
30
|
+
|
|
31
|
+
return res.json({
|
|
32
|
+
experimentCode: experiment.code,
|
|
33
|
+
variantKey: assignment.variantKey,
|
|
34
|
+
assignedAt: assignment.assignedAt,
|
|
35
|
+
config,
|
|
36
|
+
winner: {
|
|
37
|
+
winnerVariantKey: snapshot.winnerVariantKey,
|
|
38
|
+
decidedAt: snapshot.winnerDecidedAt,
|
|
39
|
+
reason: snapshot.winnerReason,
|
|
40
|
+
status: snapshot.status,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const safe = toSafeJsonError(err);
|
|
45
|
+
return res.status(safe.status).json(safe.body);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
exports.postEvents = async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const orgId = req.headers['x-org-id'] || req.query.orgId || req.body?.orgId;
|
|
52
|
+
const subjectId = req.query.subjectId || req.body?.subjectId;
|
|
53
|
+
|
|
54
|
+
const payload = req.body || {};
|
|
55
|
+
const events = Array.isArray(payload.events) ? payload.events : [payload];
|
|
56
|
+
|
|
57
|
+
const result = await experimentsService.ingestEvents({
|
|
58
|
+
orgId,
|
|
59
|
+
experimentCode: req.params.code,
|
|
60
|
+
subjectId,
|
|
61
|
+
events,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return res.status(201).json(result);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const safe = toSafeJsonError(err);
|
|
67
|
+
return res.status(safe.status).json(safe.body);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
exports.getWinner = async (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const orgId = req.headers['x-org-id'] || req.query.orgId || req.body?.orgId;
|
|
74
|
+
const { snapshot } = await experimentsService.getWinnerSnapshot({ orgId, experimentCode: req.params.code });
|
|
75
|
+
return res.json({
|
|
76
|
+
status: snapshot.status,
|
|
77
|
+
winnerVariantKey: snapshot.winnerVariantKey,
|
|
78
|
+
decidedAt: snapshot.winnerDecidedAt,
|
|
79
|
+
reason: snapshot.winnerReason,
|
|
80
|
+
});
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const safe = toSafeJsonError(err);
|
|
83
|
+
return res.status(safe.status).json(safe.body);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const experimentsAggregation = require('../services/experimentsAggregation.service');
|
|
2
|
+
const experimentsRetention = require('../services/experimentsRetention.service');
|
|
3
|
+
|
|
4
|
+
exports.runAggregation = async (req, res) => {
|
|
5
|
+
const body = req.body || {};
|
|
6
|
+
const bucketMs = body.bucketMs;
|
|
7
|
+
const start = body.start;
|
|
8
|
+
const end = body.end;
|
|
9
|
+
|
|
10
|
+
const data = await experimentsAggregation.runAggregationAndWinner({ bucketMs, start, end });
|
|
11
|
+
return res.json(data);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
exports.runRetention = async (_req, res) => {
|
|
15
|
+
const data = await experimentsRetention.runRetentionCleanup();
|
|
16
|
+
return res.json(data);
|
|
17
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const { getMarkdownByPath, searchMarkdowns } = require('../services/markdowns.service');
|
|
2
|
+
|
|
3
|
+
exports.getByPath = async (req, res) => {
|
|
4
|
+
try {
|
|
5
|
+
const { category, group_code, slug } = req.params;
|
|
6
|
+
// Check if JSON is requested via query or if we are on a .json route
|
|
7
|
+
const isJson = req.query?.json === 'true' || req.query?.json === '1' || req.path.endsWith('/json');
|
|
8
|
+
|
|
9
|
+
const doc = await getMarkdownByPath(category, group_code, slug);
|
|
10
|
+
|
|
11
|
+
if (isJson) {
|
|
12
|
+
return res.json({ item: doc });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Serve raw markdown with correct MIME type
|
|
16
|
+
return res.type('text/markdown').send(doc.markdownRaw);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const code = error?.code;
|
|
19
|
+
if (code === 'NOT_FOUND') {
|
|
20
|
+
return res.status(404).json({ error: 'Markdown not found' });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.error('Error fetching markdown:', error);
|
|
24
|
+
return res.status(500).json({ error: error?.message || 'Failed to fetch markdown' });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
exports.search = async (req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const { q: query, category, group_code, limit = 50 } = req.query;
|
|
31
|
+
|
|
32
|
+
if (!query) {
|
|
33
|
+
return res.status(400).json({ error: 'Search query (q) is required' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const results = await searchMarkdowns(query, { category, group_code, limit: Number(limit) });
|
|
37
|
+
return res.json({ results });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Error searching markdowns:', error);
|
|
40
|
+
return res.status(500).json({ error: error?.message || 'Failed to search markdowns' });
|
|
41
|
+
}
|
|
42
|
+
};
|