@intranefr/superbackend 1.4.4 → 1.5.1
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 +5 -0
- package/README.md +11 -0
- package/index.js +39 -1
- package/package.json +11 -3
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +111 -5
- 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/adminHeadless.controller.js +91 -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 +320 -0
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- 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/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +366 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware/internalCronAuth.js +29 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +879 -56
- 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/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- 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 +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +1 -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/adminHeadless.routes.js +8 -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/adminScripts.routes.js +21 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +30 -0
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -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/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +6 -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/uiComponentsPublic.routes.js +9 -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 +184 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +700 -0
- package/src/services/consoleOverride.service.js +6 -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/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- 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/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 +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +299 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- 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 +29 -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-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-headless.ejs +294 -24
- 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 +528 -10
- 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 +497 -0
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +741 -0
- package/views/admin-users.ejs +261 -4
- 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 +14 -0
- package/views/partials/llm-provider-model-picker.ejs +183 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
const ScriptDefinition = require('../models/ScriptDefinition');
|
|
2
|
+
const ScriptRun = require('../models/ScriptRun');
|
|
3
|
+
const { startRun, getRunBus } = require('../services/scriptsRunner.service');
|
|
4
|
+
const { logAuditSync } = require('../services/auditLogger');
|
|
5
|
+
|
|
6
|
+
function toSafeJsonError(error) {
|
|
7
|
+
const msg = error?.message || 'Operation failed';
|
|
8
|
+
const code = error?.code;
|
|
9
|
+
if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
|
|
10
|
+
if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
|
|
11
|
+
if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
|
|
12
|
+
return { status: 500, body: { error: msg } };
|
|
13
|
+
}
|
|
14
|
+
|
|
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
|
+
function normalizeEnv(env) {
|
|
29
|
+
const items = Array.isArray(env) ? env : [];
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const it of items) {
|
|
32
|
+
if (!it || typeof it !== 'object') continue;
|
|
33
|
+
const key = String(it.key || '').trim();
|
|
34
|
+
if (!key) continue;
|
|
35
|
+
out.push({ key, value: String(it.value || '') });
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
exports.listScripts = async (req, res) => {
|
|
41
|
+
try {
|
|
42
|
+
const items = await ScriptDefinition.find().sort({ updatedAt: -1 }).lean();
|
|
43
|
+
res.json({ items });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const safe = toSafeJsonError(err);
|
|
46
|
+
res.status(safe.status).json(safe.body);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
exports.getScript = async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const doc = await ScriptDefinition.findById(req.params.id).lean();
|
|
53
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
54
|
+
res.json({ item: doc });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const safe = toSafeJsonError(err);
|
|
57
|
+
res.status(safe.status).json(safe.body);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
exports.createScript = async (req, res) => {
|
|
62
|
+
let created = null;
|
|
63
|
+
try {
|
|
64
|
+
const payload = req.body || {};
|
|
65
|
+
|
|
66
|
+
const doc = await ScriptDefinition.create({
|
|
67
|
+
name: String(payload.name || '').trim(),
|
|
68
|
+
codeIdentifier: String(payload.codeIdentifier || '').trim(),
|
|
69
|
+
description: String(payload.description || ''),
|
|
70
|
+
type: String(payload.type || '').trim(),
|
|
71
|
+
runner: String(payload.runner || '').trim(),
|
|
72
|
+
script: String(payload.script || ''),
|
|
73
|
+
defaultWorkingDirectory: String(payload.defaultWorkingDirectory || ''),
|
|
74
|
+
env: normalizeEnv(payload.env),
|
|
75
|
+
timeoutMs: payload.timeoutMs === undefined ? undefined : Number(payload.timeoutMs),
|
|
76
|
+
enabled: payload.enabled === undefined ? true : Boolean(payload.enabled),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
created = doc.toObject();
|
|
80
|
+
audit(req, {
|
|
81
|
+
action: 'scripts.create',
|
|
82
|
+
outcome: 'success',
|
|
83
|
+
entityId: doc._id,
|
|
84
|
+
before: null,
|
|
85
|
+
after: created,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
res.status(201).json({ item: doc.toObject() });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
audit(req, {
|
|
91
|
+
action: 'scripts.create',
|
|
92
|
+
outcome: 'failure',
|
|
93
|
+
entityId: created?._id,
|
|
94
|
+
before: null,
|
|
95
|
+
after: created,
|
|
96
|
+
details: { error: err?.message || 'Operation failed' },
|
|
97
|
+
});
|
|
98
|
+
const safe = toSafeJsonError(err);
|
|
99
|
+
res.status(safe.status).json(safe.body);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
exports.updateScript = async (req, res) => {
|
|
104
|
+
let before = null;
|
|
105
|
+
let after = null;
|
|
106
|
+
try {
|
|
107
|
+
const payload = req.body || {};
|
|
108
|
+
|
|
109
|
+
const doc = await ScriptDefinition.findById(req.params.id);
|
|
110
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
111
|
+
|
|
112
|
+
before = doc.toObject();
|
|
113
|
+
|
|
114
|
+
if (payload.name !== undefined) doc.name = String(payload.name || '').trim();
|
|
115
|
+
if (payload.codeIdentifier !== undefined) doc.codeIdentifier = String(payload.codeIdentifier || '').trim();
|
|
116
|
+
if (payload.description !== undefined) doc.description = String(payload.description || '');
|
|
117
|
+
if (payload.type !== undefined) doc.type = String(payload.type || '').trim();
|
|
118
|
+
if (payload.runner !== undefined) doc.runner = String(payload.runner || '').trim();
|
|
119
|
+
if (payload.script !== undefined) doc.script = String(payload.script || '');
|
|
120
|
+
if (payload.defaultWorkingDirectory !== undefined) {
|
|
121
|
+
doc.defaultWorkingDirectory = String(payload.defaultWorkingDirectory || '');
|
|
122
|
+
}
|
|
123
|
+
if (payload.env !== undefined) doc.env = normalizeEnv(payload.env);
|
|
124
|
+
if (payload.timeoutMs !== undefined) doc.timeoutMs = Number(payload.timeoutMs || 0);
|
|
125
|
+
if (payload.enabled !== undefined) doc.enabled = Boolean(payload.enabled);
|
|
126
|
+
|
|
127
|
+
await doc.save();
|
|
128
|
+
after = doc.toObject();
|
|
129
|
+
audit(req, {
|
|
130
|
+
action: 'scripts.update',
|
|
131
|
+
outcome: 'success',
|
|
132
|
+
entityId: doc._id,
|
|
133
|
+
before,
|
|
134
|
+
after,
|
|
135
|
+
});
|
|
136
|
+
res.json({ item: doc.toObject() });
|
|
137
|
+
} catch (err) {
|
|
138
|
+
audit(req, {
|
|
139
|
+
action: 'scripts.update',
|
|
140
|
+
outcome: 'failure',
|
|
141
|
+
entityId: req.params?.id,
|
|
142
|
+
before,
|
|
143
|
+
after,
|
|
144
|
+
details: { error: err?.message || 'Operation failed' },
|
|
145
|
+
});
|
|
146
|
+
const safe = toSafeJsonError(err);
|
|
147
|
+
res.status(safe.status).json(safe.body);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
exports.deleteScript = async (req, res) => {
|
|
152
|
+
let before = null;
|
|
153
|
+
try {
|
|
154
|
+
const doc = await ScriptDefinition.findById(req.params.id);
|
|
155
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
156
|
+
before = doc.toObject();
|
|
157
|
+
await doc.deleteOne();
|
|
158
|
+
|
|
159
|
+
audit(req, {
|
|
160
|
+
action: 'scripts.delete',
|
|
161
|
+
outcome: 'success',
|
|
162
|
+
entityId: doc._id,
|
|
163
|
+
before,
|
|
164
|
+
after: null,
|
|
165
|
+
});
|
|
166
|
+
res.json({ ok: true });
|
|
167
|
+
} catch (err) {
|
|
168
|
+
audit(req, {
|
|
169
|
+
action: 'scripts.delete',
|
|
170
|
+
outcome: 'failure',
|
|
171
|
+
entityId: req.params?.id,
|
|
172
|
+
before,
|
|
173
|
+
after: null,
|
|
174
|
+
details: { error: err?.message || 'Operation failed' },
|
|
175
|
+
});
|
|
176
|
+
const safe = toSafeJsonError(err);
|
|
177
|
+
res.status(safe.status).json(safe.body);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
exports.runScript = async (req, res) => {
|
|
182
|
+
let script = null;
|
|
183
|
+
try {
|
|
184
|
+
const doc = await ScriptDefinition.findById(req.params.id);
|
|
185
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
186
|
+
if (!doc.enabled) return res.status(400).json({ error: 'Script is disabled' });
|
|
187
|
+
|
|
188
|
+
script = doc.toObject();
|
|
189
|
+
|
|
190
|
+
const runDoc = await startRun(doc, { trigger: 'manual', meta: { actorType: 'basicAuth' } });
|
|
191
|
+
|
|
192
|
+
audit(req, {
|
|
193
|
+
action: 'scripts.run',
|
|
194
|
+
outcome: 'success',
|
|
195
|
+
entityId: doc._id,
|
|
196
|
+
before: null,
|
|
197
|
+
after: null,
|
|
198
|
+
details: { runId: String(runDoc._id) },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
res.json({ runId: String(runDoc._id) });
|
|
202
|
+
} catch (err) {
|
|
203
|
+
audit(req, {
|
|
204
|
+
action: 'scripts.run',
|
|
205
|
+
outcome: 'failure',
|
|
206
|
+
entityId: req.params?.id,
|
|
207
|
+
before: script,
|
|
208
|
+
after: null,
|
|
209
|
+
details: { error: err?.message || 'Operation failed' },
|
|
210
|
+
});
|
|
211
|
+
const safe = toSafeJsonError(err);
|
|
212
|
+
res.status(safe.status).json(safe.body);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
exports.getRun = async (req, res) => {
|
|
217
|
+
try {
|
|
218
|
+
const run = await ScriptRun.findById(req.params.runId).lean();
|
|
219
|
+
if (!run) return res.status(404).json({ error: 'Not found' });
|
|
220
|
+
res.json({ item: run });
|
|
221
|
+
} catch (err) {
|
|
222
|
+
const safe = toSafeJsonError(err);
|
|
223
|
+
res.status(safe.status).json(safe.body);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
exports.listRuns = async (req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
const filter = {};
|
|
230
|
+
if (req.query.scriptId) filter.scriptId = req.query.scriptId;
|
|
231
|
+
|
|
232
|
+
const items = await ScriptRun.find(filter)
|
|
233
|
+
.sort({ createdAt: -1 })
|
|
234
|
+
.limit(50)
|
|
235
|
+
.lean();
|
|
236
|
+
|
|
237
|
+
res.json({ items });
|
|
238
|
+
} catch (err) {
|
|
239
|
+
const safe = toSafeJsonError(err);
|
|
240
|
+
res.status(safe.status).json(safe.body);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
exports.streamRun = async (req, res) => {
|
|
245
|
+
try {
|
|
246
|
+
const runId = String(req.params.runId);
|
|
247
|
+
|
|
248
|
+
res.status(200);
|
|
249
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
250
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
251
|
+
res.setHeader('Connection', 'keep-alive');
|
|
252
|
+
|
|
253
|
+
const bus = getRunBus(runId);
|
|
254
|
+
|
|
255
|
+
const since = Number(req.query.since || 0);
|
|
256
|
+
if (bus) {
|
|
257
|
+
const existing = bus.snapshot(since);
|
|
258
|
+
for (const e of existing) {
|
|
259
|
+
res.write(`event: ${e.type}\n`);
|
|
260
|
+
res.write(`data: ${JSON.stringify(e)}\n\n`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const onEvent = (e) => {
|
|
264
|
+
res.write(`event: ${e.type}\n`);
|
|
265
|
+
res.write(`data: ${JSON.stringify(e)}\n\n`);
|
|
266
|
+
};
|
|
267
|
+
const cleanup = () => {
|
|
268
|
+
clearInterval(heartbeat);
|
|
269
|
+
bus.emitter.off('event', onEvent);
|
|
270
|
+
bus.emitter.off('close', onClose);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const onClose = () => {
|
|
274
|
+
cleanup();
|
|
275
|
+
res.end();
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const heartbeat = setInterval(() => {
|
|
279
|
+
res.write(`: ping\n\n`);
|
|
280
|
+
}, 15000);
|
|
281
|
+
heartbeat.unref();
|
|
282
|
+
|
|
283
|
+
bus.emitter.on('event', onEvent);
|
|
284
|
+
bus.emitter.once('close', onClose);
|
|
285
|
+
|
|
286
|
+
req.on('close', () => {
|
|
287
|
+
cleanup();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const run = await ScriptRun.findById(runId).lean();
|
|
294
|
+
if (!run) {
|
|
295
|
+
res.write(`event: error\n`);
|
|
296
|
+
res.write(`data: ${JSON.stringify({ error: 'Not found' })}\n\n`);
|
|
297
|
+
return res.end();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (run.outputTail) {
|
|
301
|
+
res.write(`event: log\n`);
|
|
302
|
+
res.write(
|
|
303
|
+
`data: ${JSON.stringify({ seq: 1, type: 'log', ts: new Date().toISOString(), stream: 'stdout', line: run.outputTail })}\n\n`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
res.write(`event: status\n`);
|
|
307
|
+
res.write(
|
|
308
|
+
`data: ${JSON.stringify({ seq: 2, type: 'status', ts: new Date().toISOString(), status: run.status, exitCode: run.exitCode })}\n\n`,
|
|
309
|
+
);
|
|
310
|
+
res.write(`event: done\n`);
|
|
311
|
+
res.write(
|
|
312
|
+
`data: ${JSON.stringify({ seq: 3, type: 'done', ts: new Date().toISOString(), status: run.status, exitCode: run.exitCode })}\n\n`,
|
|
313
|
+
);
|
|
314
|
+
return res.end();
|
|
315
|
+
} catch (err) {
|
|
316
|
+
res.write(`event: error\n`);
|
|
317
|
+
res.write(`data: ${JSON.stringify({ error: err?.message || 'Stream error' })}\n\n`);
|
|
318
|
+
return res.end();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
@@ -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' });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createSession,
|
|
3
|
+
listSessions,
|
|
4
|
+
killSession,
|
|
5
|
+
} = require('../services/terminals.service');
|
|
6
|
+
|
|
7
|
+
function handleError(res, err) {
|
|
8
|
+
const msg = err?.message || 'Operation failed';
|
|
9
|
+
const code = err?.code;
|
|
10
|
+
if (code === 'NOT_FOUND') return res.status(404).json({ error: msg });
|
|
11
|
+
if (code === 'LIMIT') return res.status(429).json({ error: msg });
|
|
12
|
+
return res.status(500).json({ error: msg });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
exports.createSession = async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
const { cols, rows } = req.body || {};
|
|
18
|
+
const result = createSession({ cols, rows });
|
|
19
|
+
res.json(result);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
handleError(res, err);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
exports.listSessions = async (req, res) => {
|
|
26
|
+
try {
|
|
27
|
+
res.json({ items: listSessions() });
|
|
28
|
+
} catch (err) {
|
|
29
|
+
handleError(res, err);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
exports.killSession = async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
res.json(killSession(req.params.sessionId));
|
|
36
|
+
} catch (err) {
|
|
37
|
+
handleError(res, err);
|
|
38
|
+
}
|
|
39
|
+
};
|