@intranefr/superbackend 1.5.0 → 1.5.2
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 +15 -0
- package/README.md +11 -0
- package/analysis-only.skill +0 -0
- package/index.js +23 -0
- package/package.json +8 -2
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +90 -6
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminExperiments.controller.js +200 -0
- package/src/controllers/adminHeadless.controller.js +9 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +126 -4
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/experiments.controller.js +85 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/internalExperiments.controller.js +17 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +80 -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/rbac.js +62 -0
- package/src/middleware.js +810 -48
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- 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/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/GlobalSetting.js +1 -2
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +1 -0
- package/src/models/Webhook.js +2 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminExperiments.routes.js +29 -0
- package/src/routes/adminHeadless.routes.js +2 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminUiComponents.routes.js +2 -1
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/experiments.routes.js +30 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/internalExperiments.routes.js +15 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +1 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +185 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +738 -0
- package/src/services/consoleOverride.service.js +7 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- 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/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/globalSettings.service.js +15 -0
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/jsonConfigs.service.js +2 -2
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +215 -15
- package/src/services/uiComponentsAi.service.js +6 -19
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +33 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-dashboard.ejs +28 -8
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-experiments.ejs +91 -0
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +163 -1
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +597 -3
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-ui-components.ejs +57 -25
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +12 -0
- package/views/partials/dashboard/palette.ejs +5 -3
- package/views/partials/llm-provider-model-picker.ejs +183 -0
- package/src/routes/llmUi.routes.js +0 -26
|
@@ -1,6 +1,8 @@
|
|
|
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');
|
|
5
|
+
const { logAuditSync } = require('../services/auditLogger');
|
|
4
6
|
|
|
5
7
|
function toSafeJsonError(error) {
|
|
6
8
|
const msg = error?.message || 'Operation failed';
|
|
@@ -23,6 +25,35 @@ function normalizeEnv(env) {
|
|
|
23
25
|
return out;
|
|
24
26
|
}
|
|
25
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
|
+
|
|
26
57
|
exports.listScripts = async (req, res) => {
|
|
27
58
|
try {
|
|
28
59
|
const items = await ScriptDefinition.find().sort({ updatedAt: -1 }).lean();
|
|
@@ -45,42 +76,115 @@ exports.getScript = async (req, res) => {
|
|
|
45
76
|
};
|
|
46
77
|
|
|
47
78
|
exports.createScript = async (req, res) => {
|
|
79
|
+
let created = null;
|
|
48
80
|
try {
|
|
81
|
+
console.log('[createScript] Starting script creation...');
|
|
82
|
+
console.log('[createScript] Request body keys:', Object.keys(req.body || {}));
|
|
83
|
+
|
|
49
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
|
+
}
|
|
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
|
+
}
|
|
50
106
|
|
|
107
|
+
console.log('[createScript] About to create ScriptDefinition...');
|
|
51
108
|
const doc = await ScriptDefinition.create({
|
|
52
109
|
name: String(payload.name || '').trim(),
|
|
53
110
|
codeIdentifier: String(payload.codeIdentifier || '').trim(),
|
|
54
111
|
description: String(payload.description || ''),
|
|
55
112
|
type: String(payload.type || '').trim(),
|
|
56
113
|
runner: String(payload.runner || '').trim(),
|
|
57
|
-
script:
|
|
114
|
+
script: scriptContent,
|
|
115
|
+
scriptFormat: scriptFormat,
|
|
58
116
|
defaultWorkingDirectory: String(payload.defaultWorkingDirectory || ''),
|
|
59
117
|
env: normalizeEnv(payload.env),
|
|
60
118
|
timeoutMs: payload.timeoutMs === undefined ? undefined : Number(payload.timeoutMs),
|
|
61
119
|
enabled: payload.enabled === undefined ? true : Boolean(payload.enabled),
|
|
62
120
|
});
|
|
121
|
+
|
|
122
|
+
console.log('[createScript] ScriptDefinition created successfully');
|
|
63
123
|
|
|
124
|
+
created = doc.toObject();
|
|
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,
|
|
130
|
+
action: 'scripts.create',
|
|
131
|
+
outcome: 'success',
|
|
132
|
+
entityType: 'ScriptDefinition',
|
|
133
|
+
entityId: created._id,
|
|
134
|
+
data: { name: created.name },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
console.log('[createScript] About to send response...');
|
|
64
138
|
res.status(201).json({ item: doc.toObject() });
|
|
139
|
+
console.log('[createScript] Response sent successfully');
|
|
65
140
|
} catch (err) {
|
|
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' });
|
|
66
147
|
const safe = toSafeJsonError(err);
|
|
148
|
+
console.log('[createScript] Safe error:', safe);
|
|
67
149
|
res.status(safe.status).json(safe.body);
|
|
68
150
|
}
|
|
69
151
|
};
|
|
70
152
|
|
|
71
153
|
exports.updateScript = async (req, res) => {
|
|
154
|
+
let before = null;
|
|
155
|
+
let after = null;
|
|
72
156
|
try {
|
|
73
157
|
const payload = req.body || {};
|
|
74
158
|
|
|
75
159
|
const doc = await ScriptDefinition.findById(req.params.id);
|
|
76
160
|
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
77
161
|
|
|
162
|
+
before = doc.toObject();
|
|
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
|
+
|
|
78
183
|
if (payload.name !== undefined) doc.name = String(payload.name || '').trim();
|
|
79
184
|
if (payload.codeIdentifier !== undefined) doc.codeIdentifier = String(payload.codeIdentifier || '').trim();
|
|
80
185
|
if (payload.description !== undefined) doc.description = String(payload.description || '');
|
|
81
186
|
if (payload.type !== undefined) doc.type = String(payload.type || '').trim();
|
|
82
187
|
if (payload.runner !== undefined) doc.runner = String(payload.runner || '').trim();
|
|
83
|
-
if (payload.script !== undefined) doc.script = String(payload.script || '');
|
|
84
188
|
if (payload.defaultWorkingDirectory !== undefined) {
|
|
85
189
|
doc.defaultWorkingDirectory = String(payload.defaultWorkingDirectory || '');
|
|
86
190
|
}
|
|
@@ -89,34 +193,52 @@ exports.updateScript = async (req, res) => {
|
|
|
89
193
|
if (payload.enabled !== undefined) doc.enabled = Boolean(payload.enabled);
|
|
90
194
|
|
|
91
195
|
await doc.save();
|
|
196
|
+
after = doc.toObject();
|
|
197
|
+
console.log('[updateScript] Script updated successfully:', { name: after.name, id: after._id });
|
|
92
198
|
res.json({ item: doc.toObject() });
|
|
93
199
|
} catch (err) {
|
|
200
|
+
console.log('[updateScript] ERROR occurred:', err);
|
|
201
|
+
console.log('[updateScript] ERROR message:', err.message);
|
|
94
202
|
const safe = toSafeJsonError(err);
|
|
95
203
|
res.status(safe.status).json(safe.body);
|
|
96
204
|
}
|
|
97
205
|
};
|
|
98
206
|
|
|
99
207
|
exports.deleteScript = async (req, res) => {
|
|
208
|
+
let before = null;
|
|
100
209
|
try {
|
|
101
210
|
const doc = await ScriptDefinition.findById(req.params.id);
|
|
102
211
|
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
212
|
+
before = doc.toObject();
|
|
103
213
|
await doc.deleteOne();
|
|
214
|
+
|
|
215
|
+
console.log('[deleteScript] Script deleted successfully:', { name: before.name, id: before._id });
|
|
104
216
|
res.json({ ok: true });
|
|
105
217
|
} catch (err) {
|
|
218
|
+
console.log('[deleteScript] ERROR occurred:', err);
|
|
219
|
+
console.log('[deleteScript] ERROR message:', err.message);
|
|
106
220
|
const safe = toSafeJsonError(err);
|
|
107
221
|
res.status(safe.status).json(safe.body);
|
|
108
222
|
}
|
|
109
223
|
};
|
|
110
224
|
|
|
111
225
|
exports.runScript = async (req, res) => {
|
|
226
|
+
let script = null;
|
|
112
227
|
try {
|
|
113
228
|
const doc = await ScriptDefinition.findById(req.params.id);
|
|
114
229
|
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
115
230
|
if (!doc.enabled) return res.status(400).json({ error: 'Script is disabled' });
|
|
116
231
|
|
|
117
|
-
|
|
118
|
-
|
|
232
|
+
script = doc.toObject();
|
|
233
|
+
|
|
234
|
+
const runDoc = await startRun(doc, { trigger: 'manual', meta: { actorType: 'basicAuth' } });
|
|
235
|
+
|
|
236
|
+
console.log('[runScript] Script executed successfully:', { name: script.name, runId: runDoc._id });
|
|
237
|
+
|
|
238
|
+
res.json({ runId: String(runDoc._id) });
|
|
119
239
|
} catch (err) {
|
|
240
|
+
console.log('[runScript] ERROR occurred:', err);
|
|
241
|
+
console.log('[runScript] ERROR message:', err.message);
|
|
120
242
|
const safe = toSafeJsonError(err);
|
|
121
243
|
res.status(safe.status).json(safe.body);
|
|
122
244
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const OpenAI = require('openai');
|
|
2
1
|
const fs = require('fs');
|
|
3
2
|
const path = require('path');
|
|
4
3
|
|
|
@@ -15,6 +14,9 @@ const {
|
|
|
15
14
|
DEFAULT_OG_PNG_OUTPUT_PATH,
|
|
16
15
|
} = require('../services/seoConfig.service');
|
|
17
16
|
|
|
17
|
+
const llmService = require('../services/llm.service');
|
|
18
|
+
const { resolveLlmProviderModel } = require('../services/llmDefaults.service');
|
|
19
|
+
|
|
18
20
|
function handleServiceError(res, error) {
|
|
19
21
|
const msg = error?.message || 'Operation failed';
|
|
20
22
|
const code = error?.code;
|
|
@@ -231,6 +233,7 @@ exports.seoConfigAiGenerateEntry = async (req, res) => {
|
|
|
231
233
|
const viewPath = String(req.body?.viewPath || '').trim();
|
|
232
234
|
const routePath = validateRoutePathOrThrow(req.body?.routePath);
|
|
233
235
|
const modelOverride = req.body?.model;
|
|
236
|
+
const providerKeyOverride = req.body?.providerKey;
|
|
234
237
|
|
|
235
238
|
if (!viewPath || !viewPath.endsWith('.ejs')) {
|
|
236
239
|
return res.status(400).json({ error: 'viewPath is required and must end with .ejs' });
|
|
@@ -250,12 +253,18 @@ exports.seoConfigAiGenerateEntry = async (req, res) => {
|
|
|
250
253
|
return res.status(400).json({ error: 'view file is too large' });
|
|
251
254
|
}
|
|
252
255
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
const resolved = await resolveLlmProviderModel({
|
|
257
|
+
systemKey: 'seoConfig.entry.generate',
|
|
258
|
+
providerKey: providerKeyOverride,
|
|
259
|
+
model: modelOverride,
|
|
260
|
+
});
|
|
257
261
|
|
|
258
|
-
const
|
|
262
|
+
const legacyApiKey = await getSeoconfigOpenRouterApiKey();
|
|
263
|
+
const runtimeOptions = (resolved.providerKey === 'openrouter' && legacyApiKey)
|
|
264
|
+
? { apiKey: legacyApiKey, baseUrl: 'https://openrouter.ai/api/v1' }
|
|
265
|
+
: {};
|
|
266
|
+
|
|
267
|
+
const model = resolved.model || (await getSeoconfigOpenRouterModel());
|
|
259
268
|
|
|
260
269
|
const { data } = await getSeoConfigData();
|
|
261
270
|
const siteName = data?.siteName || '';
|
|
@@ -263,11 +272,6 @@ exports.seoConfigAiGenerateEntry = async (req, res) => {
|
|
|
263
272
|
|
|
264
273
|
const ejsSource = await fs.promises.readFile(abs, 'utf8');
|
|
265
274
|
|
|
266
|
-
const client = new OpenAI({
|
|
267
|
-
apiKey,
|
|
268
|
-
baseURL: 'https://openrouter.ai/api/v1',
|
|
269
|
-
});
|
|
270
|
-
|
|
271
275
|
const prompt = buildSeoEntryPromptFromEjs({
|
|
272
276
|
routePath,
|
|
273
277
|
viewRelPath: viewPath,
|
|
@@ -276,15 +280,20 @@ exports.seoConfigAiGenerateEntry = async (req, res) => {
|
|
|
276
280
|
baseUrl,
|
|
277
281
|
});
|
|
278
282
|
|
|
279
|
-
const resp = await
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
const resp = await llmService.callAdhoc(
|
|
284
|
+
{
|
|
285
|
+
providerKey: resolved.providerKey,
|
|
286
|
+
model,
|
|
287
|
+
messages: [{ role: 'user', content: prompt }],
|
|
288
|
+
promptKeyForAudit: 'seoConfig.entry.generate',
|
|
289
|
+
},
|
|
290
|
+
runtimeOptions,
|
|
291
|
+
);
|
|
283
292
|
|
|
284
|
-
const out = resp.
|
|
293
|
+
const out = resp.content || '';
|
|
285
294
|
const entry = parseAiJsonObjectOrThrow(out);
|
|
286
295
|
|
|
287
|
-
return res.json({ routePath, entry, model });
|
|
296
|
+
return res.json({ routePath, entry, model, providerKey: resolved.providerKey });
|
|
288
297
|
} catch (error) {
|
|
289
298
|
const code = error?.code;
|
|
290
299
|
if (code === 'VALIDATION') {
|
|
@@ -323,6 +332,7 @@ exports.seoConfigAiImproveEntry = async (req, res) => {
|
|
|
323
332
|
const routePath = validateRoutePathOrThrow(req.body?.routePath);
|
|
324
333
|
const instruction = String(req.body?.instruction || '').trim();
|
|
325
334
|
const modelOverride = req.body?.model;
|
|
335
|
+
const providerKeyOverride = req.body?.providerKey;
|
|
326
336
|
|
|
327
337
|
if (!instruction) {
|
|
328
338
|
return res.status(400).json({ error: 'instruction is required' });
|
|
@@ -331,12 +341,18 @@ exports.seoConfigAiImproveEntry = async (req, res) => {
|
|
|
331
341
|
return res.status(400).json({ error: 'instruction is too large' });
|
|
332
342
|
}
|
|
333
343
|
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
344
|
+
const resolved = await resolveLlmProviderModel({
|
|
345
|
+
systemKey: 'seoConfig.entry.improve',
|
|
346
|
+
providerKey: providerKeyOverride,
|
|
347
|
+
model: modelOverride,
|
|
348
|
+
});
|
|
338
349
|
|
|
339
|
-
const
|
|
350
|
+
const legacyApiKey = await getSeoconfigOpenRouterApiKey();
|
|
351
|
+
const runtimeOptions = (resolved.providerKey === 'openrouter' && legacyApiKey)
|
|
352
|
+
? { apiKey: legacyApiKey, baseUrl: 'https://openrouter.ai/api/v1' }
|
|
353
|
+
: {};
|
|
354
|
+
|
|
355
|
+
const model = resolved.model || (await getSeoconfigOpenRouterModel());
|
|
340
356
|
|
|
341
357
|
const { data } = await getSeoConfigData();
|
|
342
358
|
const siteName = data?.siteName || '';
|
|
@@ -346,11 +362,6 @@ exports.seoConfigAiImproveEntry = async (req, res) => {
|
|
|
346
362
|
return res.status(404).json({ error: `No existing entry for ${routePath}` });
|
|
347
363
|
}
|
|
348
364
|
|
|
349
|
-
const client = new OpenAI({
|
|
350
|
-
apiKey,
|
|
351
|
-
baseURL: 'https://openrouter.ai/api/v1',
|
|
352
|
-
});
|
|
353
|
-
|
|
354
365
|
const prompt = buildSeoEntryPromptImprove({
|
|
355
366
|
routePath,
|
|
356
367
|
existingEntry,
|
|
@@ -359,15 +370,20 @@ exports.seoConfigAiImproveEntry = async (req, res) => {
|
|
|
359
370
|
baseUrl,
|
|
360
371
|
});
|
|
361
372
|
|
|
362
|
-
const resp = await
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
373
|
+
const resp = await llmService.callAdhoc(
|
|
374
|
+
{
|
|
375
|
+
providerKey: resolved.providerKey,
|
|
376
|
+
model,
|
|
377
|
+
messages: [{ role: 'user', content: prompt }],
|
|
378
|
+
promptKeyForAudit: 'seoConfig.entry.improve',
|
|
379
|
+
},
|
|
380
|
+
runtimeOptions,
|
|
381
|
+
);
|
|
366
382
|
|
|
367
|
-
const out = resp.
|
|
383
|
+
const out = resp.content || '';
|
|
368
384
|
const entry = parseAiJsonObjectOrThrow(out);
|
|
369
385
|
|
|
370
|
-
return res.json({ routePath, entry, model });
|
|
386
|
+
return res.json({ routePath, entry, model, providerKey: resolved.providerKey });
|
|
371
387
|
} catch (error) {
|
|
372
388
|
const code = error?.code;
|
|
373
389
|
if (code === 'VALIDATION') {
|
|
@@ -469,6 +485,7 @@ exports.aiEditSvg = async (req, res) => {
|
|
|
469
485
|
const svgRaw = req.body?.svgRaw;
|
|
470
486
|
const instruction = req.body?.instruction;
|
|
471
487
|
const modelOverride = req.body?.model;
|
|
488
|
+
const providerKeyOverride = req.body?.providerKey;
|
|
472
489
|
|
|
473
490
|
if (typeof svgRaw !== 'string' || svgRaw.trim() === '') {
|
|
474
491
|
return res.status(400).json({ error: 'svgRaw is required' });
|
|
@@ -484,30 +501,36 @@ exports.aiEditSvg = async (req, res) => {
|
|
|
484
501
|
return res.status(400).json({ error: 'instruction is too large' });
|
|
485
502
|
}
|
|
486
503
|
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
504
|
+
const resolved = await resolveLlmProviderModel({
|
|
505
|
+
systemKey: 'seoConfig.ogSvg.edit',
|
|
506
|
+
providerKey: providerKeyOverride,
|
|
507
|
+
model: modelOverride,
|
|
508
|
+
});
|
|
491
509
|
|
|
492
|
-
const
|
|
510
|
+
const legacyApiKey = await getSeoconfigOpenRouterApiKey();
|
|
511
|
+
const runtimeOptions = (resolved.providerKey === 'openrouter' && legacyApiKey)
|
|
512
|
+
? { apiKey: legacyApiKey, baseUrl: 'https://openrouter.ai/api/v1' }
|
|
513
|
+
: {};
|
|
493
514
|
|
|
494
|
-
const
|
|
495
|
-
apiKey,
|
|
496
|
-
baseURL: 'https://openrouter.ai/api/v1',
|
|
497
|
-
});
|
|
515
|
+
const model = resolved.model || (await getSeoconfigOpenRouterModel());
|
|
498
516
|
|
|
499
517
|
const prompt = buildSvgAiPrompt({ svg: svgRaw, instruction });
|
|
500
|
-
const resp = await
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
518
|
+
const resp = await llmService.callAdhoc(
|
|
519
|
+
{
|
|
520
|
+
providerKey: resolved.providerKey,
|
|
521
|
+
model,
|
|
522
|
+
messages: [{ role: 'user', content: prompt }],
|
|
523
|
+
promptKeyForAudit: 'seoConfig.ogSvg.edit',
|
|
524
|
+
},
|
|
525
|
+
runtimeOptions,
|
|
526
|
+
);
|
|
504
527
|
|
|
505
|
-
const out = resp.
|
|
528
|
+
const out = String(resp.content || '').trim();
|
|
506
529
|
if (!out.startsWith('<svg') || !out.includes('</svg>')) {
|
|
507
530
|
return res.status(500).json({ error: 'AI returned invalid SVG' });
|
|
508
531
|
}
|
|
509
532
|
|
|
510
|
-
return res.json({ svgRaw: out, model });
|
|
533
|
+
return res.json({ svgRaw: out, model, providerKey: resolved.providerKey });
|
|
511
534
|
} catch (error) {
|
|
512
535
|
console.error('Error editing SVG with AI:', error);
|
|
513
536
|
return res.status(500).json({ error: error?.message || 'Failed to edit SVG' });
|