@intranefr/superbackend 1.5.3 → 1.6.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/cookies.txt +6 -0
- package/cookies1.txt +6 -0
- package/cookies2.txt +6 -0
- package/cookies3.txt +6 -0
- package/cookies4.txt +5 -0
- package/cookies_old.txt +5 -0
- package/cookies_old_test.txt +6 -0
- package/cookies_super.txt +5 -0
- package/cookies_super_test.txt +6 -0
- package/cookies_test.txt +6 -0
- package/index.js +7 -0
- package/package.json +3 -1
- package/plugins/core-waiting-list-migration/README.md +118 -0
- package/plugins/core-waiting-list-migration/index.js +438 -0
- package/plugins/global-settings-presets/index.js +20 -0
- package/plugins/hello-cli/index.js +17 -0
- package/plugins/ui-components-seeder/components/suiAlert.js +212 -0
- package/plugins/ui-components-seeder/components/suiToast.js +186 -0
- package/plugins/ui-components-seeder/index.js +31 -0
- package/public/js/admin-ui-components-preview.js +281 -0
- package/public/js/admin-ui-components.js +408 -0
- package/public/js/llm-provider-model-picker.js +193 -0
- package/public/test-iframe-fix.html +63 -0
- package/public/test-iframe.html +14 -0
- package/src/admin/endpointRegistry.js +68 -0
- package/src/controllers/admin.controller.js +25 -5
- package/src/controllers/adminDataCleanup.controller.js +45 -0
- package/src/controllers/adminLlm.controller.js +0 -8
- package/src/controllers/adminLogin.controller.js +269 -0
- package/src/controllers/adminPlugins.controller.js +55 -0
- package/src/controllers/adminRegistry.controller.js +106 -0
- package/src/controllers/adminStats.controller.js +4 -4
- package/src/controllers/registry.controller.js +32 -0
- package/src/controllers/waitingList.controller.js +52 -74
- package/src/middleware/auth.js +71 -1
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +454 -153
- package/src/models/GlobalSetting.js +11 -1
- package/src/models/UiComponent.js +2 -0
- package/src/models/User.js +1 -1
- package/src/routes/admin.routes.js +3 -3
- package/src/routes/adminAgents.routes.js +2 -2
- package/src/routes/adminAssets.routes.js +11 -11
- package/src/routes/adminBlog.routes.js +2 -2
- package/src/routes/adminBlogAi.routes.js +2 -2
- package/src/routes/adminBlogAutomation.routes.js +2 -2
- package/src/routes/adminCache.routes.js +2 -2
- package/src/routes/adminConsoleManager.routes.js +2 -2
- package/src/routes/adminCrons.routes.js +2 -2
- package/src/routes/adminDataCleanup.routes.js +26 -0
- package/src/routes/adminDbBrowser.routes.js +2 -2
- package/src/routes/adminEjsVirtual.routes.js +2 -2
- package/src/routes/adminFeatureFlags.routes.js +6 -6
- package/src/routes/adminHeadless.routes.js +2 -2
- package/src/routes/adminHealthChecks.routes.js +2 -2
- package/src/routes/adminI18n.routes.js +2 -2
- package/src/routes/adminJsonConfigs.routes.js +8 -8
- package/src/routes/adminLlm.routes.js +8 -8
- package/src/routes/adminLogin.routes.js +23 -0
- package/src/routes/adminMarkdowns.routes.js +3 -9
- package/src/routes/adminMigration.routes.js +12 -12
- package/src/routes/adminPages.routes.js +2 -2
- package/src/routes/adminPlugins.routes.js +15 -0
- package/src/routes/adminProxy.routes.js +2 -2
- package/src/routes/adminRateLimits.routes.js +8 -8
- package/src/routes/adminRbac.routes.js +2 -2
- package/src/routes/adminRegistry.routes.js +24 -0
- package/src/routes/adminScripts.routes.js +2 -2
- package/src/routes/adminSeoConfig.routes.js +10 -10
- package/src/routes/adminTelegram.routes.js +2 -2
- package/src/routes/adminTerminals.routes.js +2 -2
- package/src/routes/adminUiComponents.routes.js +2 -2
- package/src/routes/adminUploadNamespaces.routes.js +7 -7
- package/src/routes/blogInternal.routes.js +2 -2
- package/src/routes/experiments.routes.js +2 -2
- package/src/routes/formsAdmin.routes.js +6 -6
- package/src/routes/globalSettings.routes.js +8 -8
- package/src/routes/internalExperiments.routes.js +2 -2
- package/src/routes/notificationAdmin.routes.js +7 -7
- package/src/routes/orgAdmin.routes.js +16 -16
- package/src/routes/pages.routes.js +3 -3
- package/src/routes/registry.routes.js +11 -0
- package/src/routes/stripeAdmin.routes.js +12 -12
- package/src/routes/userAdmin.routes.js +7 -7
- package/src/routes/waitingListAdmin.routes.js +2 -2
- package/src/routes/workflows.routes.js +3 -3
- package/src/services/dataCleanup.service.js +286 -0
- package/src/services/jsonConfigs.service.js +262 -0
- package/src/services/plugins.service.js +348 -0
- package/src/services/registry.service.js +452 -0
- package/src/services/uiComponents.service.js +180 -0
- package/src/services/waitingListJson.service.js +401 -0
- package/src/utils/rbac/rightsRegistry.js +118 -0
- package/test-access.js +63 -0
- package/test-iframe-fix.html +63 -0
- package/test-iframe.html +14 -0
- package/views/admin-403.ejs +92 -0
- package/views/admin-dashboard-home.ejs +52 -2
- package/views/admin-dashboard.ejs +143 -2
- package/views/admin-data-cleanup.ejs +357 -0
- package/views/admin-login.ejs +286 -0
- package/views/admin-plugins-system.ejs +223 -0
- package/views/admin-ui-components.ejs +82 -402
- package/views/admin-users.ejs +207 -11
- package/views/partials/dashboard/nav-items.ejs +2 -0
- package/views/partials/llm-provider-model-picker.ejs +0 -161
package/views/admin-users.ejs
CHANGED
|
@@ -135,9 +135,12 @@
|
|
|
135
135
|
<div>
|
|
136
136
|
<label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
|
137
137
|
<select id="edit-role" class="w-full border rounded px-3 py-2">
|
|
138
|
-
<option value="
|
|
139
|
-
<option value="admin">admin</option>
|
|
138
|
+
<option value="">Select a role</option>
|
|
140
139
|
</select>
|
|
140
|
+
<div id="edit-role-info" class="mt-2 p-2 bg-gray-50 rounded text-sm hidden">
|
|
141
|
+
<div id="edit-role-description" class="text-gray-600"></div>
|
|
142
|
+
<div id="edit-role-grants" class="text-gray-500 text-xs mt-1"></div>
|
|
143
|
+
</div>
|
|
141
144
|
</div>
|
|
142
145
|
<div>
|
|
143
146
|
<label class="block text-sm font-medium text-gray-700 mb-1">Plan</label>
|
|
@@ -257,10 +260,15 @@
|
|
|
257
260
|
</div>
|
|
258
261
|
<div>
|
|
259
262
|
<label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
|
260
|
-
<
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
<div class="space-y-2">
|
|
264
|
+
<select id="register-role" class="w-full border rounded px-3 py-2">
|
|
265
|
+
<option value="">Loading roles...</option>
|
|
266
|
+
</select>
|
|
267
|
+
<div id="role-info" class="hidden text-xs text-gray-600 bg-gray-50 p-2 rounded border">
|
|
268
|
+
<div id="role-description"></div>
|
|
269
|
+
<div id="role-grants" class="mt-1"></div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
264
272
|
</div>
|
|
265
273
|
<div id="register-error" class="hidden text-red-600 text-sm"></div>
|
|
266
274
|
</form>
|
|
@@ -301,10 +309,55 @@
|
|
|
301
309
|
}
|
|
302
310
|
|
|
303
311
|
const state = { offset: 0, limit: 50, total: 0 };
|
|
312
|
+
const isIframe = <%= typeof isIframe !== 'undefined' && isIframe %>;
|
|
313
|
+
|
|
314
|
+
// Enhanced fetch function for iframe communication
|
|
315
|
+
async function apiFetch(url, options = {}) {
|
|
316
|
+
if (isIframe) {
|
|
317
|
+
// In iframe mode, request data from parent window
|
|
318
|
+
return new Promise((resolve, reject) => {
|
|
319
|
+
const messageId = 'api-request-' + Date.now() + '-' + Math.random();
|
|
320
|
+
|
|
321
|
+
const handleMessage = (event) => {
|
|
322
|
+
if (event.data.type === 'api-response' && event.data.messageId === messageId) {
|
|
323
|
+
window.removeEventListener('message', handleMessage);
|
|
324
|
+
if (event.data.error) {
|
|
325
|
+
reject(new Error(event.data.error));
|
|
326
|
+
} else {
|
|
327
|
+
resolve({
|
|
328
|
+
ok: event.data.ok,
|
|
329
|
+
json: async () => event.data.data,
|
|
330
|
+
text: async () => JSON.stringify(event.data.data)
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
window.addEventListener('message', handleMessage);
|
|
337
|
+
|
|
338
|
+
// Request data from parent
|
|
339
|
+
window.parent.postMessage({
|
|
340
|
+
type: 'api-request',
|
|
341
|
+
messageId: messageId,
|
|
342
|
+
url: url,
|
|
343
|
+
options: options
|
|
344
|
+
}, '*');
|
|
345
|
+
|
|
346
|
+
// Timeout after 10 seconds
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
window.removeEventListener('message', handleMessage);
|
|
349
|
+
reject(new Error('Timeout waiting for parent response'));
|
|
350
|
+
}, 10000);
|
|
351
|
+
});
|
|
352
|
+
} else {
|
|
353
|
+
// Normal mode - direct fetch
|
|
354
|
+
return fetch(url, options);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
304
357
|
|
|
305
358
|
async function loadStats() {
|
|
306
359
|
try {
|
|
307
|
-
const res = await
|
|
360
|
+
const res = await apiFetch(`${API_BASE}/api/admin/users/stats`);
|
|
308
361
|
const data = await res.json();
|
|
309
362
|
if (res.ok) {
|
|
310
363
|
document.getElementById('stat-total').textContent = data.total ?? '-';
|
|
@@ -331,7 +384,7 @@
|
|
|
331
384
|
|
|
332
385
|
try {
|
|
333
386
|
const url = `${API_BASE}/api/admin/users${qs({ q, role, subscriptionStatus, currentPlan, limit: state.limit, offset: state.offset })}`;
|
|
334
|
-
const res = await
|
|
387
|
+
const res = await apiFetch(url);
|
|
335
388
|
const data = await res.json();
|
|
336
389
|
|
|
337
390
|
if (!res.ok) { showToast(data?.error || 'Failed to load users', 'error'); return; }
|
|
@@ -433,13 +486,19 @@
|
|
|
433
486
|
} catch (e) { showToast(e.message, 'error'); }
|
|
434
487
|
}
|
|
435
488
|
|
|
436
|
-
function openEditModal(userId, name, role, plan, subscription) {
|
|
489
|
+
async function openEditModal(userId, name, role, plan, subscription) {
|
|
437
490
|
document.getElementById('edit-user-id').value = userId;
|
|
438
491
|
document.getElementById('edit-name').value = name || '';
|
|
439
|
-
document.getElementById('edit-role').value = role || 'user';
|
|
440
492
|
document.getElementById('edit-plan').value = plan || 'free';
|
|
441
493
|
document.getElementById('edit-subscription').value = subscription || 'none';
|
|
442
494
|
|
|
495
|
+
// Load RBAC roles for edit form
|
|
496
|
+
await loadEditRbacRoles();
|
|
497
|
+
|
|
498
|
+
// Set the role after loading RBAC roles
|
|
499
|
+
document.getElementById('edit-role').value = role || 'user';
|
|
500
|
+
displayEditRoleInfo(role || 'user');
|
|
501
|
+
|
|
443
502
|
// Reset password fields
|
|
444
503
|
document.getElementById('edit-reset-password').checked = false;
|
|
445
504
|
document.getElementById('password-fields').classList.add('hidden');
|
|
@@ -625,13 +684,141 @@
|
|
|
625
684
|
} catch (e) { showToast(e.message, 'error'); }
|
|
626
685
|
}
|
|
627
686
|
|
|
687
|
+
// RBAC Role Management Functions
|
|
688
|
+
let rbacRoles = [];
|
|
689
|
+
|
|
690
|
+
async function loadRbacRoles() {
|
|
691
|
+
try {
|
|
692
|
+
const res = await fetch(`${API_BASE}/api/admin/rbac/roles`);
|
|
693
|
+
const data = await res.json();
|
|
694
|
+
if (res.ok) {
|
|
695
|
+
rbacRoles = data.roles || [];
|
|
696
|
+
populateRoleSelect();
|
|
697
|
+
} else {
|
|
698
|
+
console.error('Failed to load RBAC roles:', data.error);
|
|
699
|
+
// Fallback to hardcoded options
|
|
700
|
+
rbacRoles = [
|
|
701
|
+
{ key: 'user', name: 'User', description: 'Regular user with no admin access' },
|
|
702
|
+
{ key: 'admin', name: 'Admin', description: 'Admin with panel access' }
|
|
703
|
+
];
|
|
704
|
+
populateRoleSelect();
|
|
705
|
+
}
|
|
706
|
+
} catch (error) {
|
|
707
|
+
console.error('Error loading RBAC roles:', error);
|
|
708
|
+
// Fallback to hardcoded options
|
|
709
|
+
rbacRoles = [
|
|
710
|
+
{ key: 'user', name: 'User', description: 'Regular user with no admin access' },
|
|
711
|
+
{ key: 'admin', name: 'Admin', description: 'Admin with panel access' }
|
|
712
|
+
];
|
|
713
|
+
populateRoleSelect();
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function populateRoleSelect() {
|
|
718
|
+
const select = document.getElementById('register-role');
|
|
719
|
+
select.innerHTML = '<option value="">Select a role</option>';
|
|
720
|
+
|
|
721
|
+
rbacRoles.forEach(role => {
|
|
722
|
+
const option = document.createElement('option');
|
|
723
|
+
option.value = role.key;
|
|
724
|
+
option.textContent = `${role.name} - ${role.description}`;
|
|
725
|
+
select.appendChild(option);
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function displayRoleInfo(roleKey) {
|
|
730
|
+
const role = rbacRoles.find(r => r.key === roleKey);
|
|
731
|
+
const infoDiv = document.getElementById('role-info');
|
|
732
|
+
const descDiv = document.getElementById('role-description');
|
|
733
|
+
const grantsDiv = document.getElementById('role-grants');
|
|
734
|
+
|
|
735
|
+
if (!role) {
|
|
736
|
+
infoDiv.classList.add('hidden');
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
descDiv.textContent = role.description || '';
|
|
741
|
+
|
|
742
|
+
// Show grants information if available
|
|
743
|
+
if (role.grants && role.grants.length > 0) {
|
|
744
|
+
grantsDiv.innerHTML = `<strong>Permissions:</strong> ${role.grants.slice(0, 3).join(', ')}${role.grants.length > 3 ? '...' : ''}`;
|
|
745
|
+
} else {
|
|
746
|
+
grantsDiv.innerHTML = '<strong>Permissions:</strong> Admin panel access';
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
infoDiv.classList.remove('hidden');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Edit Form RBAC Functions
|
|
753
|
+
async function loadEditRbacRoles() {
|
|
754
|
+
try {
|
|
755
|
+
const res = await fetch(`${API_BASE}/api/admin/rbac/roles`);
|
|
756
|
+
const data = await res.json();
|
|
757
|
+
if (res.ok) {
|
|
758
|
+
rbacRoles = data.roles || [];
|
|
759
|
+
populateEditRoleSelect();
|
|
760
|
+
} else {
|
|
761
|
+
rbacRoles = [
|
|
762
|
+
{ key: 'user', name: 'User', description: 'Regular user with no admin access' },
|
|
763
|
+
{ key: 'admin', name: 'Admin', description: 'Admin Panel Access - Full administrative privileges' }
|
|
764
|
+
];
|
|
765
|
+
populateEditRoleSelect();
|
|
766
|
+
}
|
|
767
|
+
} catch (error) {
|
|
768
|
+
rbacRoles = [
|
|
769
|
+
{ key: 'user', name: 'User', description: 'Regular user with no admin access' },
|
|
770
|
+
{ key: 'admin', name: 'Admin', description: 'Admin Panel Access - Full administrative privileges' }
|
|
771
|
+
];
|
|
772
|
+
populateEditRoleSelect();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function populateEditRoleSelect() {
|
|
777
|
+
const select = document.getElementById('edit-role');
|
|
778
|
+
select.innerHTML = '<option value="">Select a role</option>';
|
|
779
|
+
|
|
780
|
+
rbacRoles.forEach(role => {
|
|
781
|
+
const option = document.createElement('option');
|
|
782
|
+
option.value = role.key;
|
|
783
|
+
option.textContent = `${role.name} - ${role.description}`;
|
|
784
|
+
select.appendChild(option);
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function displayEditRoleInfo(roleKey) {
|
|
789
|
+
const role = rbacRoles.find(r => r.key === roleKey);
|
|
790
|
+
const infoDiv = document.getElementById('edit-role-info');
|
|
791
|
+
const descDiv = document.getElementById('edit-role-description');
|
|
792
|
+
const grantsDiv = document.getElementById('edit-role-grants');
|
|
793
|
+
|
|
794
|
+
if (!role) {
|
|
795
|
+
infoDiv.classList.add('hidden');
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
descDiv.textContent = role.description || '';
|
|
800
|
+
|
|
801
|
+
// Show grants information if available
|
|
802
|
+
if (role.grants && role.grants.length > 0) {
|
|
803
|
+
grantsDiv.innerHTML = `<strong>Permissions:</strong> ${role.grants.slice(0, 3).join(', ')}${role.grants.length > 3 ? '...' : ''}`;
|
|
804
|
+
} else {
|
|
805
|
+
grantsDiv.innerHTML = '<strong>Permissions:</strong> Admin panel access';
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
infoDiv.classList.remove('hidden');
|
|
809
|
+
}
|
|
810
|
+
|
|
628
811
|
function openRegisterModal() {
|
|
629
812
|
document.getElementById('register-email').value = '';
|
|
630
813
|
document.getElementById('register-password').value = '';
|
|
631
814
|
document.getElementById('register-name').value = '';
|
|
632
|
-
document.getElementById('register-role').value = '
|
|
815
|
+
document.getElementById('register-role').value = '';
|
|
633
816
|
document.getElementById('register-error').classList.add('hidden');
|
|
817
|
+
document.getElementById('role-info').classList.add('hidden');
|
|
634
818
|
document.getElementById('modal-register').classList.remove('hidden');
|
|
819
|
+
|
|
820
|
+
// Load RBAC roles when modal opens
|
|
821
|
+
loadRbacRoles();
|
|
635
822
|
}
|
|
636
823
|
|
|
637
824
|
function closeRegisterModal() {
|
|
@@ -866,6 +1053,15 @@
|
|
|
866
1053
|
document.getElementById('btn-register-submit').onclick = registerUser;
|
|
867
1054
|
document.getElementById('toggle-password').onclick = togglePasswordVisibility;
|
|
868
1055
|
|
|
1056
|
+
// Role selection change events
|
|
1057
|
+
document.getElementById('register-role').onchange = (e) => {
|
|
1058
|
+
displayRoleInfo(e.target.value);
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
document.getElementById('edit-role').onchange = (e) => {
|
|
1062
|
+
displayEditRoleInfo(e.target.value);
|
|
1063
|
+
};
|
|
1064
|
+
|
|
869
1065
|
// Form submission
|
|
870
1066
|
document.getElementById('register-form').onsubmit = (e) => {
|
|
871
1067
|
e.preventDefault();
|
|
@@ -38,12 +38,14 @@
|
|
|
38
38
|
title: 'System & DevOps',
|
|
39
39
|
items: [
|
|
40
40
|
{ id: 'settings', label: 'Global Settings', path: adminPath + '/global-settings', icon: 'ti-settings' },
|
|
41
|
+
{ id: 'plugins-system', label: 'Plugins system', path: adminPath + '/plugins-system', icon: 'ti-puzzle' },
|
|
41
42
|
{ id: 'flags', label: 'Feature Flags', path: adminPath + '/feature-flags', icon: 'ti-flag' },
|
|
42
43
|
{ id: 'ejs', label: 'Virtual EJS', path: adminPath + '/ejs-virtual', icon: 'ti-code' },
|
|
43
44
|
{ id: 'rate-limiter', label: 'Rate Limiter', path: adminPath + '/rate-limiter', icon: 'ti-traffic-cone' },
|
|
44
45
|
{ id: 'proxy', label: 'Proxy system', path: adminPath + '/proxy', icon: 'ti-world' },
|
|
45
46
|
{ id: 'cache', label: 'Cache Layer', path: adminPath + '/cache', icon: 'ti-database' },
|
|
46
47
|
{ id: 'db-browser', label: 'DB Browser', path: adminPath + '/db-browser', icon: 'ti-database-search' },
|
|
48
|
+
{ id: 'data-cleanup', label: 'Data cleanup', path: adminPath + '/data-cleanup', icon: 'ti-broom' },
|
|
47
49
|
{ id: 'migration', label: 'Migration', path: adminPath + '/migration', icon: 'ti-database-export' },
|
|
48
50
|
{ id: 'webhooks', label: 'Webhooks', path: adminPath + '/webhooks', icon: 'ti-webhook' },
|
|
49
51
|
{ id: 'coolify', label: 'Coolify Deploy', path: adminPath + '/coolify-deploy', icon: 'ti-rocket' },
|
|
@@ -20,164 +20,3 @@
|
|
|
20
20
|
<input id="<%= modelInputId %>" class="w-full border rounded px-2 py-2 text-sm" placeholder="e.g. google/gemini-2.5-flash-lite" list="<%= modelInputId %>__datalist" />
|
|
21
21
|
<datalist id="<%= modelInputId %>__datalist"></datalist>
|
|
22
22
|
</div>
|
|
23
|
-
|
|
24
|
-
<script>
|
|
25
|
-
(function () {
|
|
26
|
-
if (!window.__llmProviderModelPicker) {
|
|
27
|
-
window.__llmProviderModelPicker = { instances: {} };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function safeJsonParse(raw, fallback) {
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(raw);
|
|
33
|
-
} catch (_) {
|
|
34
|
-
return fallback;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function fetchJson(url) {
|
|
39
|
-
const res = await fetch(url);
|
|
40
|
-
const data = await res.json();
|
|
41
|
-
if (!res.ok) {
|
|
42
|
-
throw new Error(data?.error || 'Request failed');
|
|
43
|
-
}
|
|
44
|
-
return data;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function setDatalistOptions(datalistEl, items) {
|
|
48
|
-
datalistEl.innerHTML = '';
|
|
49
|
-
const uniq = Array.from(new Set((items || []).filter(Boolean)));
|
|
50
|
-
for (const item of uniq) {
|
|
51
|
-
const opt = document.createElement('option');
|
|
52
|
-
opt.value = String(item);
|
|
53
|
-
datalistEl.appendChild(opt);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function trim(v) {
|
|
58
|
-
return String(v || '').trim();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isOpenRouterProvider({ providerKey, providerConfig }) {
|
|
62
|
-
const pk = String(providerKey || '').trim().toLowerCase();
|
|
63
|
-
if (pk === 'openrouter') return true;
|
|
64
|
-
|
|
65
|
-
const baseUrl = providerConfig && typeof providerConfig === 'object'
|
|
66
|
-
? String(providerConfig.baseUrl || providerConfig.baseURL || '').trim().toLowerCase()
|
|
67
|
-
: '';
|
|
68
|
-
|
|
69
|
-
return Boolean(baseUrl && baseUrl.includes('openrouter'));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function getInstanceKey({ providerInputId, modelInputId }) {
|
|
73
|
-
return `${String(providerInputId || '').trim()}::${String(modelInputId || '').trim()}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function getOrCreateInstance(opts) {
|
|
77
|
-
const key = getInstanceKey(opts);
|
|
78
|
-
const existing = window.__llmProviderModelPicker.instances[key];
|
|
79
|
-
if (existing) return existing;
|
|
80
|
-
|
|
81
|
-
const inst = {
|
|
82
|
-
apiBase: opts.apiBase,
|
|
83
|
-
providerInputId: opts.providerInputId,
|
|
84
|
-
modelInputId: opts.modelInputId,
|
|
85
|
-
providers: {},
|
|
86
|
-
providerModels: {},
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
window.__llmProviderModelPicker.instances[key] = inst;
|
|
90
|
-
return inst;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function loadConfig(inst) {
|
|
94
|
-
const data = await fetchJson(`${inst.apiBase}/api/admin/llm/config`);
|
|
95
|
-
inst.providers = data.providers || {};
|
|
96
|
-
inst.providerModels = data.providerModels || {};
|
|
97
|
-
return data;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function renderProviderOptions(inst) {
|
|
101
|
-
const providerInput = document.getElementById(inst.providerInputId);
|
|
102
|
-
const providerList = document.getElementById(`${inst.providerInputId}__datalist`);
|
|
103
|
-
if (!providerInput || !providerList) return;
|
|
104
|
-
|
|
105
|
-
const providerKeys = Object.keys(inst.providers || {}).sort();
|
|
106
|
-
setDatalistOptions(providerList, providerKeys);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function renderModelOptions(inst) {
|
|
110
|
-
const providerInput = document.getElementById(inst.providerInputId);
|
|
111
|
-
const modelList = document.getElementById(`${inst.modelInputId}__datalist`);
|
|
112
|
-
if (!providerInput || !modelList) return;
|
|
113
|
-
|
|
114
|
-
const providerKey = trim(providerInput.value);
|
|
115
|
-
const models = providerKey && inst.providerModels && typeof inst.providerModels === 'object'
|
|
116
|
-
? inst.providerModels[providerKey]
|
|
117
|
-
: null;
|
|
118
|
-
|
|
119
|
-
setDatalistOptions(modelList, Array.isArray(models) ? models : []);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function maybeAutoFetchOpenRouterModels(inst) {
|
|
123
|
-
try {
|
|
124
|
-
const providerInput = document.getElementById(inst.providerInputId);
|
|
125
|
-
if (!providerInput) return;
|
|
126
|
-
|
|
127
|
-
const providerKey = trim(providerInput.value);
|
|
128
|
-
const providerConfig = inst.providers && typeof inst.providers === 'object' ? inst.providers[providerKey] : null;
|
|
129
|
-
if (!isOpenRouterProvider({ providerKey, providerConfig })) return;
|
|
130
|
-
|
|
131
|
-
const existing = inst.providerModels && typeof inst.providerModels === 'object' ? inst.providerModels.openrouter : null;
|
|
132
|
-
if (Array.isArray(existing) && existing.length > 0) return;
|
|
133
|
-
|
|
134
|
-
await fetchOpenRouterModels({
|
|
135
|
-
apiBase: inst.apiBase,
|
|
136
|
-
providerInputId: inst.providerInputId,
|
|
137
|
-
modelInputId: inst.modelInputId,
|
|
138
|
-
});
|
|
139
|
-
} catch {
|
|
140
|
-
// ignore
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function fetchOpenRouterModels(opts) {
|
|
145
|
-
const inst = getOrCreateInstance(opts || {});
|
|
146
|
-
inst.apiBase = (opts && opts.apiBase) || inst.apiBase || window.__llmProviderModelPicker.defaultApiBase || null;
|
|
147
|
-
if (!inst.apiBase) return;
|
|
148
|
-
|
|
149
|
-
const data = await fetchJson(`${inst.apiBase}/api/admin/llm/openrouter/models`);
|
|
150
|
-
const models = Array.isArray(data?.models) ? data.models : [];
|
|
151
|
-
|
|
152
|
-
inst.providerModels = inst.providerModels && typeof inst.providerModels === 'object' ? inst.providerModels : {};
|
|
153
|
-
inst.providerModels.openrouter = models;
|
|
154
|
-
renderModelOptions(inst);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function init(opts) {
|
|
158
|
-
const inst = getOrCreateInstance(opts || {});
|
|
159
|
-
|
|
160
|
-
if (opts && opts.apiBase) {
|
|
161
|
-
window.__llmProviderModelPicker.defaultApiBase = opts.apiBase;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
await loadConfig(inst);
|
|
165
|
-
renderProviderOptions(inst);
|
|
166
|
-
renderModelOptions(inst);
|
|
167
|
-
await maybeAutoFetchOpenRouterModels(inst);
|
|
168
|
-
|
|
169
|
-
const providerInput = document.getElementById(inst.providerInputId);
|
|
170
|
-
if (providerInput) {
|
|
171
|
-
providerInput.addEventListener('change', async () => {
|
|
172
|
-
renderModelOptions(inst);
|
|
173
|
-
await maybeAutoFetchOpenRouterModels(inst);
|
|
174
|
-
});
|
|
175
|
-
providerInput.addEventListener('input', () => renderModelOptions(inst));
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
window.__llmProviderModelPicker.init = init;
|
|
180
|
-
window.__llmProviderModelPicker.fetchOpenRouterModels = fetchOpenRouterModels;
|
|
181
|
-
window.__llmProviderModelPicker._util = { safeJsonParse };
|
|
182
|
-
})();
|
|
183
|
-
</script>
|