@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
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intranefr/superbackend",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"description": "Node.js middleware that gives your project backend superpowers",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "node server.js",
|
|
8
|
-
"dev": "nodemon --verbose --ignore uploads --ignore '*.log' server.js",
|
|
8
|
+
"dev": "nodemon --verbose --ignore uploads --ignore stdout.log --ignore '*.log' server.js",
|
|
9
9
|
"start:minio": "docker compose -f compose.standalone.yml --profile minio-only up -d minio",
|
|
10
10
|
"minio:envs": "node -e \"console.log(['S3_ENDPOINT=http://localhost:9000','S3_REGION=us-east-1','S3_ACCESS_KEY_ID=minioadmin','S3_SECRET_ACCESS_KEY=minioadmin','S3_BUCKET=saasbackend','S3_FORCE_PATH_STYLE=true'].join('\\n'))\"",
|
|
11
11
|
"build:sdk:error-tracking:browser": "esbuild sdk/error-tracking/browser/src/embed.js --bundle --format=iife --global-name=saasbackendErrorTrackingEmbed --outfile=sdk/error-tracking/browser/dist/embed.iife.js",
|
|
@@ -47,11 +47,13 @@
|
|
|
47
47
|
"mysql2": "^3.16.1",
|
|
48
48
|
"node-cron": "^4.2.1",
|
|
49
49
|
"node-pty": "^1.1.0",
|
|
50
|
+
"node-telegram-bot-api": "^0.67.0",
|
|
50
51
|
"openai": "^4.0.0",
|
|
51
52
|
"redis": "^4.7.1",
|
|
52
53
|
"resend": "^6.4.0",
|
|
53
54
|
"ssh2-sftp-client": "^12.0.1",
|
|
54
55
|
"stripe": "^14.0.0",
|
|
56
|
+
"terminal-kit": "^3.1.2",
|
|
55
57
|
"vm2": "^3.10.0",
|
|
56
58
|
"ws": "^8.18.0"
|
|
57
59
|
},
|
|
@@ -63,6 +65,7 @@
|
|
|
63
65
|
},
|
|
64
66
|
"jest": {
|
|
65
67
|
"testEnvironment": "node",
|
|
68
|
+
"testTimeout": 15000,
|
|
66
69
|
"collectCoverageFrom": [
|
|
67
70
|
"src/**/*.js",
|
|
68
71
|
"!src/**/*.test.js"
|
|
@@ -10,6 +10,7 @@ const FormSubmission = require('../models/FormSubmission');
|
|
|
10
10
|
const asyncHandler = require('../utils/asyncHandler');
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
+
const crypto = require('crypto');
|
|
13
14
|
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|
14
15
|
const { generateAccessToken, generateRefreshToken } = require('../utils/jwt');
|
|
15
16
|
const { retryFailedWebhooks, processWebhookEvent } = require('../utils/webhookRetry');
|
|
@@ -351,7 +352,7 @@ const getWebhookStats = asyncHandler(async (req, res) => {
|
|
|
351
352
|
const provisionCoolifyDeploy = asyncHandler(async (req, res) => {
|
|
352
353
|
try {
|
|
353
354
|
const { overwrite } = req.body;
|
|
354
|
-
const managePath = path.join(process.cwd(), "manage.
|
|
355
|
+
const managePath = path.join(process.cwd(), "manage.js");
|
|
355
356
|
const exists = fs.existsSync(managePath);
|
|
356
357
|
|
|
357
358
|
if (exists && !overwrite) {
|
|
@@ -362,13 +363,19 @@ const provisionCoolifyDeploy = asyncHandler(async (req, res) => {
|
|
|
362
363
|
});
|
|
363
364
|
}
|
|
364
365
|
|
|
365
|
-
//
|
|
366
|
-
|
|
366
|
+
// Copy the improved manage.js from polybot
|
|
367
|
+
const sourceManageJs = path.join(__dirname, "../../../manage.js");
|
|
368
|
+
if (fs.existsSync(sourceManageJs)) {
|
|
369
|
+
fs.copyFileSync(sourceManageJs, managePath);
|
|
370
|
+
// Make it executable
|
|
371
|
+
fs.chmodSync(managePath, '755');
|
|
372
|
+
}
|
|
373
|
+
|
|
367
374
|
res.json({
|
|
368
375
|
success: true,
|
|
369
376
|
message: exists
|
|
370
|
-
? "Coolify Headless Deploy script (manage.
|
|
371
|
-
: "Coolify Headless Deploy script (manage.
|
|
377
|
+
? "Coolify Headless Deploy script (manage.js) was updated."
|
|
378
|
+
: "Coolify Headless Deploy script (manage.js) is ready in the root directory.",
|
|
372
379
|
path: managePath,
|
|
373
380
|
});
|
|
374
381
|
} catch (error) {
|
|
@@ -458,6 +465,71 @@ async function cleanupUserData(userId) {
|
|
|
458
465
|
}
|
|
459
466
|
}
|
|
460
467
|
|
|
468
|
+
const generateTokenForEmail = asyncHandler(async (req, res) => {
|
|
469
|
+
const { email } = req.body;
|
|
470
|
+
|
|
471
|
+
if (!email) {
|
|
472
|
+
return res.status(400).json({ error: 'Email is required' });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Find or create user
|
|
476
|
+
let user = await User.findOne({ email: email.toLowerCase() });
|
|
477
|
+
|
|
478
|
+
if (!user) {
|
|
479
|
+
// Generate random password
|
|
480
|
+
const randomPassword = crypto.randomBytes(16).toString('hex');
|
|
481
|
+
|
|
482
|
+
// Create user with admin role
|
|
483
|
+
user = new User({
|
|
484
|
+
email: email.toLowerCase(),
|
|
485
|
+
passwordHash: randomPassword,
|
|
486
|
+
name: email,
|
|
487
|
+
role: 'admin' // All auto-created users get admin role
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
await user.save();
|
|
491
|
+
|
|
492
|
+
// Create default organization automatically
|
|
493
|
+
const defaultOrgSlug = process.env.POLYBOT_DEFAULT_ORG_SLUG || 'polybot';
|
|
494
|
+
const defaultOrgName = process.env.POLYBOT_DEFAULT_ORG_NAME || 'Polybot';
|
|
495
|
+
|
|
496
|
+
let org = await Organization.findOne({ slug: defaultOrgSlug });
|
|
497
|
+
if (!org) {
|
|
498
|
+
org = new Organization({
|
|
499
|
+
name: defaultOrgName,
|
|
500
|
+
slug: defaultOrgSlug,
|
|
501
|
+
ownerUserId: user._id
|
|
502
|
+
});
|
|
503
|
+
await org.save();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Add user to organization
|
|
507
|
+
const existingMember = await OrganizationMember.findOne({
|
|
508
|
+
userId: user._id,
|
|
509
|
+
orgId: org._id
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
if (!existingMember) {
|
|
513
|
+
const member = new OrganizationMember({
|
|
514
|
+
userId: user._id,
|
|
515
|
+
orgId: org._id,
|
|
516
|
+
role: 'admin'
|
|
517
|
+
});
|
|
518
|
+
await member.save();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Generate tokens with 1 second expiry for access, 2 hours for refresh
|
|
523
|
+
const token = generateAccessToken(user._id, user.role);
|
|
524
|
+
const refreshToken = generateRefreshToken(user._id);
|
|
525
|
+
|
|
526
|
+
res.json({
|
|
527
|
+
token,
|
|
528
|
+
refreshToken,
|
|
529
|
+
user: user.toJSON()
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
461
533
|
module.exports = {
|
|
462
534
|
getUsers,
|
|
463
535
|
registerUser,
|
|
@@ -467,10 +539,11 @@ module.exports = {
|
|
|
467
539
|
deleteUser,
|
|
468
540
|
reconcileUser,
|
|
469
541
|
generateToken,
|
|
542
|
+
generateTokenForEmail,
|
|
470
543
|
getWebhookEvents,
|
|
471
544
|
getWebhookEvent,
|
|
472
545
|
retryFailedWebhookEvents,
|
|
473
546
|
retrySingleWebhookEvent,
|
|
474
547
|
getWebhookStats,
|
|
475
|
-
provisionCoolifyDeploy
|
|
548
|
+
provisionCoolifyDeploy,
|
|
476
549
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const Agent = require('../models/Agent');
|
|
2
|
+
|
|
3
|
+
exports.listAgents = async (req, res) => {
|
|
4
|
+
try {
|
|
5
|
+
const agents = await Agent.find().lean();
|
|
6
|
+
return res.json({ items: agents });
|
|
7
|
+
} catch (error) {
|
|
8
|
+
return res.status(500).json({ error: error.message });
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
exports.createAgent = async (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const agent = await Agent.create(req.body);
|
|
15
|
+
return res.json(agent);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return res.status(500).json({ error: error.message });
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
exports.updateAgent = async (req, res) => {
|
|
22
|
+
try {
|
|
23
|
+
const agent = await Agent.findByIdAndUpdate(req.params.id, req.body, { new: true });
|
|
24
|
+
return res.json(agent);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return res.status(500).json({ error: error.message });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
exports.deleteAgent = async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
await Agent.findByIdAndDelete(req.params.id);
|
|
33
|
+
return res.json({ success: true });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return res.status(500).json({ error: error.message });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const Experiment = require('../models/Experiment');
|
|
4
|
+
const ExperimentMetricBucket = require('../models/ExperimentMetricBucket');
|
|
5
|
+
|
|
6
|
+
const experimentsService = require('../services/experiments.service');
|
|
7
|
+
|
|
8
|
+
function toSafeJsonError(error) {
|
|
9
|
+
const msg = error?.message || 'Operation failed';
|
|
10
|
+
const code = error?.code;
|
|
11
|
+
if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
|
|
12
|
+
if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
|
|
13
|
+
if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
|
|
14
|
+
return { status: 500, body: { error: msg } };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isValidObjectId(id) {
|
|
18
|
+
return id && mongoose.Types.ObjectId.isValid(String(id));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeVariant(v) {
|
|
22
|
+
const key = String(v?.key || '').trim();
|
|
23
|
+
const weight = Number(v?.weight || 0) || 0;
|
|
24
|
+
const configSlug = String(v?.configSlug || '').trim();
|
|
25
|
+
if (!key) return null;
|
|
26
|
+
return { key, weight, configSlug };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeMetric(d) {
|
|
30
|
+
const key = String(d?.key || '').trim();
|
|
31
|
+
const kind = String(d?.kind || '').trim() || 'count';
|
|
32
|
+
if (!key) return null;
|
|
33
|
+
return {
|
|
34
|
+
key,
|
|
35
|
+
kind,
|
|
36
|
+
numeratorEventKey: String(d?.numeratorEventKey || '').trim(),
|
|
37
|
+
denominatorEventKey: String(d?.denominatorEventKey || '').trim(),
|
|
38
|
+
objective: String(d?.objective || 'maximize').trim() === 'minimize' ? 'minimize' : 'maximize',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
exports.list = async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
const orgId = req.query.orgId || null;
|
|
45
|
+
const q = {};
|
|
46
|
+
if (orgId) q.organizationId = orgId;
|
|
47
|
+
|
|
48
|
+
const items = await Experiment.find(q).sort({ updatedAt: -1 }).lean();
|
|
49
|
+
return res.json({ items });
|
|
50
|
+
} catch (err) {
|
|
51
|
+
const safe = toSafeJsonError(err);
|
|
52
|
+
return res.status(safe.status).json(safe.body);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
exports.get = async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const id = req.params.id;
|
|
59
|
+
const doc = await Experiment.findById(id).lean();
|
|
60
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
61
|
+
return res.json({ item: doc });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const safe = toSafeJsonError(err);
|
|
64
|
+
return res.status(safe.status).json(safe.body);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
exports.create = async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const p = req.body || {};
|
|
71
|
+
|
|
72
|
+
const orgId = p.organizationId === null || p.organizationId === '' ? null : p.organizationId;
|
|
73
|
+
if (orgId && !isValidObjectId(orgId)) {
|
|
74
|
+
return res.status(400).json({ error: 'Invalid organizationId' });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const code = String(p.code || '').trim();
|
|
78
|
+
if (!code) return res.status(400).json({ error: 'code is required' });
|
|
79
|
+
|
|
80
|
+
const variants = (Array.isArray(p.variants) ? p.variants : []).map(normalizeVariant).filter(Boolean);
|
|
81
|
+
const primaryMetric = normalizeMetric(p.primaryMetric);
|
|
82
|
+
if (!primaryMetric) return res.status(400).json({ error: 'primaryMetric is required' });
|
|
83
|
+
|
|
84
|
+
const doc = await Experiment.create({
|
|
85
|
+
organizationId: orgId || null,
|
|
86
|
+
code,
|
|
87
|
+
name: String(p.name || '').trim(),
|
|
88
|
+
description: String(p.description || '').trim(),
|
|
89
|
+
status: String(p.status || 'draft'),
|
|
90
|
+
startedAt: p.startedAt ? new Date(p.startedAt) : null,
|
|
91
|
+
endsAt: p.endsAt ? new Date(p.endsAt) : null,
|
|
92
|
+
assignment: { unit: 'subjectId', sticky: p.assignment?.sticky !== false, salt: String(p.assignment?.salt || '').trim() },
|
|
93
|
+
variants,
|
|
94
|
+
primaryMetric,
|
|
95
|
+
secondaryMetrics: (Array.isArray(p.secondaryMetrics) ? p.secondaryMetrics : []).map(normalizeMetric).filter(Boolean),
|
|
96
|
+
winnerPolicy: {
|
|
97
|
+
mode: String(p.winnerPolicy?.mode || 'manual') === 'automatic' ? 'automatic' : 'manual',
|
|
98
|
+
pickAfterMs: Number(p.winnerPolicy?.pickAfterMs || 0) || 0,
|
|
99
|
+
minAssignments: Number(p.winnerPolicy?.minAssignments || 0) || 0,
|
|
100
|
+
minExposures: Number(p.winnerPolicy?.minExposures || 0) || 0,
|
|
101
|
+
minConversions: Number(p.winnerPolicy?.minConversions || 0) || 0,
|
|
102
|
+
statMethod: String(p.winnerPolicy?.statMethod || 'simple_rate'),
|
|
103
|
+
overrideWinnerVariantKey: String(p.winnerPolicy?.overrideWinnerVariantKey || '').trim(),
|
|
104
|
+
},
|
|
105
|
+
createdByUserId: req.user?._id || null,
|
|
106
|
+
updatedByUserId: req.user?._id || null,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await experimentsService.clearExperimentCaches(doc._id);
|
|
110
|
+
|
|
111
|
+
return res.status(201).json({ item: doc.toObject() });
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const safe = toSafeJsonError(err);
|
|
114
|
+
return res.status(safe.status).json(safe.body);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
exports.update = async (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
const id = req.params.id;
|
|
121
|
+
const p = req.body || {};
|
|
122
|
+
|
|
123
|
+
const doc = await Experiment.findById(id);
|
|
124
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
125
|
+
|
|
126
|
+
if (p.name !== undefined) doc.name = String(p.name || '').trim();
|
|
127
|
+
if (p.description !== undefined) doc.description = String(p.description || '').trim();
|
|
128
|
+
if (p.status !== undefined) doc.status = String(p.status);
|
|
129
|
+
if (p.startedAt !== undefined) doc.startedAt = p.startedAt ? new Date(p.startedAt) : null;
|
|
130
|
+
if (p.endsAt !== undefined) doc.endsAt = p.endsAt ? new Date(p.endsAt) : null;
|
|
131
|
+
|
|
132
|
+
if (p.variants !== undefined) {
|
|
133
|
+
doc.variants = (Array.isArray(p.variants) ? p.variants : []).map(normalizeVariant).filter(Boolean);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (p.primaryMetric !== undefined) {
|
|
137
|
+
const m = normalizeMetric(p.primaryMetric);
|
|
138
|
+
if (!m) return res.status(400).json({ error: 'primaryMetric is required' });
|
|
139
|
+
doc.primaryMetric = m;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (p.secondaryMetrics !== undefined) {
|
|
143
|
+
doc.secondaryMetrics = (Array.isArray(p.secondaryMetrics) ? p.secondaryMetrics : []).map(normalizeMetric).filter(Boolean);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (p.winnerPolicy !== undefined) {
|
|
147
|
+
doc.winnerPolicy = {
|
|
148
|
+
...(doc.winnerPolicy?.toObject ? doc.winnerPolicy.toObject() : doc.winnerPolicy),
|
|
149
|
+
...(p.winnerPolicy || {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
doc.updatedByUserId = req.user?._id || null;
|
|
154
|
+
|
|
155
|
+
await doc.save();
|
|
156
|
+
await experimentsService.clearExperimentCaches(doc._id);
|
|
157
|
+
|
|
158
|
+
return res.json({ item: doc.toObject() });
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const safe = toSafeJsonError(err);
|
|
161
|
+
return res.status(safe.status).json(safe.body);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
exports.remove = async (req, res) => {
|
|
166
|
+
try {
|
|
167
|
+
const id = req.params.id;
|
|
168
|
+
const doc = await Experiment.findById(id);
|
|
169
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
170
|
+
|
|
171
|
+
await doc.deleteOne();
|
|
172
|
+
await experimentsService.clearExperimentCaches(id);
|
|
173
|
+
|
|
174
|
+
return res.json({ success: true });
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const safe = toSafeJsonError(err);
|
|
177
|
+
return res.status(safe.status).json(safe.body);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
exports.getMetrics = async (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const experimentId = req.params.id;
|
|
184
|
+
const start = req.query.start ? new Date(req.query.start) : null;
|
|
185
|
+
const end = req.query.end ? new Date(req.query.end) : null;
|
|
186
|
+
|
|
187
|
+
const q = { experimentId };
|
|
188
|
+
if (start || end) {
|
|
189
|
+
q.bucketStart = {};
|
|
190
|
+
if (start) q.bucketStart.$gte = start;
|
|
191
|
+
if (end) q.bucketStart.$lte = end;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const buckets = await ExperimentMetricBucket.find(q).sort({ bucketStart: 1 }).lean();
|
|
195
|
+
return res.json({ buckets });
|
|
196
|
+
} catch (err) {
|
|
197
|
+
const safe = toSafeJsonError(err);
|
|
198
|
+
return res.status(safe.status).json(safe.body);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
@@ -387,6 +387,24 @@ async function listCosts(req, res) {
|
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
async function listProviders(req, res) {
|
|
391
|
+
try {
|
|
392
|
+
const providers = await getJsonSetting(PROVIDERS_KEY, {});
|
|
393
|
+
const safeProviders = {};
|
|
394
|
+
if (providers && typeof providers === "object") {
|
|
395
|
+
for (const [key, value] of Object.entries(providers)) {
|
|
396
|
+
if (!value || typeof value !== "object") continue;
|
|
397
|
+
const { apiKey, ...rest } = value;
|
|
398
|
+
safeProviders[key] = rest;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
res.json({ providers: safeProviders });
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error("[adminLlm] listProviders error", error);
|
|
404
|
+
res.status(500).json({ error: "Failed to load providers" });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
390
408
|
module.exports = {
|
|
391
409
|
getConfig,
|
|
392
410
|
saveConfig,
|
|
@@ -394,4 +412,5 @@ module.exports = {
|
|
|
394
412
|
listAudit,
|
|
395
413
|
listCosts,
|
|
396
414
|
listOpenRouterModels,
|
|
415
|
+
listProviders,
|
|
397
416
|
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const {
|
|
2
|
+
ERROR_CODES,
|
|
3
|
+
listMarkdowns,
|
|
4
|
+
getMarkdownById,
|
|
5
|
+
createMarkdown,
|
|
6
|
+
updateMarkdown,
|
|
7
|
+
deleteMarkdown,
|
|
8
|
+
getFolderContents,
|
|
9
|
+
getUniqueGroupCodes,
|
|
10
|
+
validatePathUniqueness,
|
|
11
|
+
} = require('../services/markdowns.service');
|
|
12
|
+
|
|
13
|
+
function handleServiceError(res, error) {
|
|
14
|
+
const msg = error?.message || 'Operation failed';
|
|
15
|
+
const code = error?.code;
|
|
16
|
+
|
|
17
|
+
if (code === 'VALIDATION' || code === 'INVALID_MARKDOWN' || code === 'INVALID_GROUP_CODE') {
|
|
18
|
+
return res.status(400).json({ error: msg });
|
|
19
|
+
}
|
|
20
|
+
if (code === 'NOT_FOUND') {
|
|
21
|
+
return res.status(404).json({ error: msg });
|
|
22
|
+
}
|
|
23
|
+
if (code === 'PATH_NOT_UNIQUE') {
|
|
24
|
+
return res.status(409).json({ error: msg });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return res.status(500).json({ error: msg });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseJsonMaybe(value) {
|
|
31
|
+
if (value === undefined || value === null) return null;
|
|
32
|
+
if (typeof value !== 'string') return value;
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
if (!trimmed) return null;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(trimmed);
|
|
37
|
+
} catch (_) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
exports.list = async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
const filters = {
|
|
45
|
+
category: req.query.category,
|
|
46
|
+
group_code: req.query.group_code,
|
|
47
|
+
status: req.query.status,
|
|
48
|
+
ownerUserId: req.query.ownerUserId,
|
|
49
|
+
orgId: req.query.orgId,
|
|
50
|
+
search: req.query.search,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const pagination = {
|
|
54
|
+
page: Number(req.query.page) || 1,
|
|
55
|
+
limit: Number(req.query.limit) || 50,
|
|
56
|
+
sort: parseJsonMaybe(req.query.sort) || { updatedAt: -1 },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const result = await listMarkdowns(filters, pagination, { isAdmin: true });
|
|
60
|
+
return res.json(result);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Error listing markdowns:', error);
|
|
63
|
+
return handleServiceError(res, error);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
exports.get = async (req, res) => {
|
|
68
|
+
try {
|
|
69
|
+
const item = await getMarkdownById(req.params.id);
|
|
70
|
+
if (!item) return res.status(404).json({ error: 'Markdown not found' });
|
|
71
|
+
return res.json({ item });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('Error fetching markdown:', error);
|
|
74
|
+
return handleServiceError(res, error);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
exports.create = async (req, res) => {
|
|
79
|
+
try {
|
|
80
|
+
const item = await createMarkdown(req.body || {});
|
|
81
|
+
return res.status(201).json({ item });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Error creating markdown:', error);
|
|
84
|
+
return handleServiceError(res, error);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
exports.update = async (req, res) => {
|
|
89
|
+
try {
|
|
90
|
+
const item = await updateMarkdown(req.params.id, req.body || {});
|
|
91
|
+
return res.json({ item });
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Error updating markdown:', error);
|
|
94
|
+
return handleServiceError(res, error);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
exports.remove = async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const result = await deleteMarkdown(req.params.id);
|
|
101
|
+
return res.json(result);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Error deleting markdown:', error);
|
|
104
|
+
return handleServiceError(res, error);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
exports.getFolderContents = async (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const { category } = req.params;
|
|
111
|
+
const { group_code } = req.params;
|
|
112
|
+
|
|
113
|
+
const pagination = {
|
|
114
|
+
page: Number(req.query.page) || 1,
|
|
115
|
+
limit: Number(req.query.limit) || 100,
|
|
116
|
+
sort: parseJsonMaybe(req.query.sort) || { title: 1 },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const result = await getFolderContents(category, group_code, pagination, { isAdmin: true });
|
|
120
|
+
return res.json(result);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Error getting folder contents:', error);
|
|
123
|
+
return handleServiceError(res, error);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
exports.validatePath = async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const { category, group_code, slug, excludeId } = req.body;
|
|
130
|
+
|
|
131
|
+
if (!category || !slug) {
|
|
132
|
+
return res.status(400).json({ error: 'category and slug are required' });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const isUnique = await validatePathUniqueness(category, group_code, slug, excludeId);
|
|
136
|
+
return res.json({ unique: isUnique });
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('Error validating path:', error);
|
|
139
|
+
return handleServiceError(res, error);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
exports.getGroupCodes = async (req, res) => {
|
|
144
|
+
try {
|
|
145
|
+
const { category } = req.params;
|
|
146
|
+
|
|
147
|
+
if (!category) {
|
|
148
|
+
return res.status(400).json({ error: 'category is required' });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const groupCodes = await getUniqueGroupCodes(category, { isAdmin: true });
|
|
152
|
+
return res.json(groupCodes);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('Error getting group codes:', error);
|
|
155
|
+
return handleServiceError(res, error);
|
|
156
|
+
}
|
|
157
|
+
};
|