grok-cli-to-openai-compatible 1.2.0
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 +41 -0
- package/LICENSE +21 -0
- package/README.md +484 -0
- package/dist/app.d.ts +3 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +67 -0
- package/dist/app.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +5 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +77 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/migrate.d.ts +5 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -0
- package/dist/cli/commands/migrate.js +25 -0
- package/dist/cli/commands/migrate.js.map +1 -0
- package/dist/cli/commands/open.d.ts +6 -0
- package/dist/cli/commands/open.d.ts.map +1 -0
- package/dist/cli/commands/open.js +24 -0
- package/dist/cli/commands/open.js.map +1 -0
- package/dist/cli/commands/restart.d.ts +7 -0
- package/dist/cli/commands/restart.d.ts.map +1 -0
- package/dist/cli/commands/restart.js +12 -0
- package/dist/cli/commands/restart.js.map +1 -0
- package/dist/cli/commands/seed.d.ts +5 -0
- package/dist/cli/commands/seed.d.ts.map +1 -0
- package/dist/cli/commands/seed.js +38 -0
- package/dist/cli/commands/seed.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +6 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +73 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/start.d.ts +7 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +57 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.d.ts +5 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +42 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/stop.d.ts +5 -0
- package/dist/cli/commands/stop.d.ts.map +1 -0
- package/dist/cli/commands/stop.js +21 -0
- package/dist/cli/commands/stop.js.map +1 -0
- package/dist/cli/commands/update.d.ts +8 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +58 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/commands/version.d.ts +2 -0
- package/dist/cli/commands/version.d.ts.map +1 -0
- package/dist/cli/commands/version.js +16 -0
- package/dist/cli/commands/version.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +128 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/lib/env-file.d.ts +7 -0
- package/dist/cli/lib/env-file.d.ts.map +1 -0
- package/dist/cli/lib/env-file.js +99 -0
- package/dist/cli/lib/env-file.js.map +1 -0
- package/dist/cli/lib/paths.d.ts +27 -0
- package/dist/cli/lib/paths.d.ts.map +1 -0
- package/dist/cli/lib/paths.js +90 -0
- package/dist/cli/lib/paths.js.map +1 -0
- package/dist/cli/lib/print.d.ts +10 -0
- package/dist/cli/lib/print.d.ts.map +1 -0
- package/dist/cli/lib/print.js +28 -0
- package/dist/cli/lib/print.js.map +1 -0
- package/dist/cli/lib/process-mgr.d.ts +8 -0
- package/dist/cli/lib/process-mgr.d.ts.map +1 -0
- package/dist/cli/lib/process-mgr.js +105 -0
- package/dist/cli/lib/process-mgr.js.map +1 -0
- package/dist/config/constants.d.ts +60 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +127 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/cors.d.ts +3 -0
- package/dist/config/cors.d.ts.map +1 -0
- package/dist/config/cors.js +22 -0
- package/dist/config/cors.js.map +1 -0
- package/dist/config/database.d.ts +4 -0
- package/dist/config/database.d.ts.map +1 -0
- package/dist/config/database.js +18 -0
- package/dist/config/database.js.map +1 -0
- package/dist/config/env.d.ts +38 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +97 -0
- package/dist/config/env.js.map +1 -0
- package/dist/controllers/admin.controller.d.ts +21 -0
- package/dist/controllers/admin.controller.d.ts.map +1 -0
- package/dist/controllers/admin.controller.js +315 -0
- package/dist/controllers/admin.controller.js.map +1 -0
- package/dist/controllers/api-key.controller.d.ts +7 -0
- package/dist/controllers/api-key.controller.d.ts.map +1 -0
- package/dist/controllers/api-key.controller.js +48 -0
- package/dist/controllers/api-key.controller.js.map +1 -0
- package/dist/controllers/chat.controller.d.ts +5 -0
- package/dist/controllers/chat.controller.d.ts.map +1 -0
- package/dist/controllers/chat.controller.js +27 -0
- package/dist/controllers/chat.controller.js.map +1 -0
- package/dist/controllers/document.controller.d.ts +8 -0
- package/dist/controllers/document.controller.d.ts.map +1 -0
- package/dist/controllers/document.controller.js +53 -0
- package/dist/controllers/document.controller.js.map +1 -0
- package/dist/controllers/health.controller.d.ts +6 -0
- package/dist/controllers/health.controller.d.ts.map +1 -0
- package/dist/controllers/health.controller.js +39 -0
- package/dist/controllers/health.controller.js.map +1 -0
- package/dist/controllers/models.controller.d.ts +6 -0
- package/dist/controllers/models.controller.d.ts.map +1 -0
- package/dist/controllers/models.controller.js +18 -0
- package/dist/controllers/models.controller.js.map +1 -0
- package/dist/dto/admin.dto.d.ts +104 -0
- package/dist/dto/admin.dto.d.ts.map +1 -0
- package/dist/dto/admin.dto.js +45 -0
- package/dist/dto/admin.dto.js.map +1 -0
- package/dist/dto/api-key.dto.d.ts +27 -0
- package/dist/dto/api-key.dto.d.ts.map +1 -0
- package/dist/dto/api-key.dto.js +15 -0
- package/dist/dto/api-key.dto.js.map +1 -0
- package/dist/dto/chat.dto.d.ts +70 -0
- package/dist/dto/chat.dto.d.ts.map +1 -0
- package/dist/dto/chat.dto.js +49 -0
- package/dist/dto/chat.dto.js.map +1 -0
- package/dist/dto/document.dto.d.ts +21 -0
- package/dist/dto/document.dto.d.ts.map +1 -0
- package/dist/dto/document.dto.js +12 -0
- package/dist/dto/document.dto.js.map +1 -0
- package/dist/entities/api-key.entity.d.ts +33 -0
- package/dist/entities/api-key.entity.d.ts.map +1 -0
- package/dist/entities/api-key.entity.js +3 -0
- package/dist/entities/api-key.entity.js.map +1 -0
- package/dist/entities/audit-log.entity.d.ts +19 -0
- package/dist/entities/audit-log.entity.d.ts.map +1 -0
- package/dist/entities/audit-log.entity.js +3 -0
- package/dist/entities/audit-log.entity.js.map +1 -0
- package/dist/entities/chat-request.entity.d.ts +21 -0
- package/dist/entities/chat-request.entity.d.ts.map +1 -0
- package/dist/entities/chat-request.entity.js +3 -0
- package/dist/entities/chat-request.entity.js.map +1 -0
- package/dist/entities/document.entity.d.ts +22 -0
- package/dist/entities/document.entity.d.ts.map +1 -0
- package/dist/entities/document.entity.js +3 -0
- package/dist/entities/document.entity.js.map +1 -0
- package/dist/exceptions/error-codes.d.ts +18 -0
- package/dist/exceptions/error-codes.d.ts.map +1 -0
- package/dist/exceptions/error-codes.js +20 -0
- package/dist/exceptions/error-codes.js.map +1 -0
- package/dist/exceptions/exception.factory.d.ts +18 -0
- package/dist/exceptions/exception.factory.d.ts.map +1 -0
- package/dist/exceptions/exception.factory.js +50 -0
- package/dist/exceptions/exception.factory.js.map +1 -0
- package/dist/exceptions/http.exception.d.ts +18 -0
- package/dist/exceptions/http.exception.d.ts.map +1 -0
- package/dist/exceptions/http.exception.js +45 -0
- package/dist/exceptions/http.exception.js.map +1 -0
- package/dist/interfaces/auth.interface.d.ts +14 -0
- package/dist/interfaces/auth.interface.d.ts.map +1 -0
- package/dist/interfaces/auth.interface.js +3 -0
- package/dist/interfaces/auth.interface.js.map +1 -0
- package/dist/interfaces/express.interface.d.ts +11 -0
- package/dist/interfaces/express.interface.d.ts.map +1 -0
- package/dist/interfaces/express.interface.js +3 -0
- package/dist/interfaces/express.interface.js.map +1 -0
- package/dist/interfaces/grok.interface.d.ts +41 -0
- package/dist/interfaces/grok.interface.d.ts.map +1 -0
- package/dist/interfaces/grok.interface.js +3 -0
- package/dist/interfaces/grok.interface.js.map +1 -0
- package/dist/interfaces/openai.interface.d.ts +97 -0
- package/dist/interfaces/openai.interface.d.ts.map +1 -0
- package/dist/interfaces/openai.interface.js +3 -0
- package/dist/interfaces/openai.interface.js.map +1 -0
- package/dist/middlewares/auth.middleware.d.ts +5 -0
- package/dist/middlewares/auth.middleware.d.ts.map +1 -0
- package/dist/middlewares/auth.middleware.js +40 -0
- package/dist/middlewares/auth.middleware.js.map +1 -0
- package/dist/middlewares/concurrency.middleware.d.ts +6 -0
- package/dist/middlewares/concurrency.middleware.d.ts.map +1 -0
- package/dist/middlewares/concurrency.middleware.js +16 -0
- package/dist/middlewares/concurrency.middleware.js.map +1 -0
- package/dist/middlewares/error.middleware.d.ts +4 -0
- package/dist/middlewares/error.middleware.d.ts.map +1 -0
- package/dist/middlewares/error.middleware.js +38 -0
- package/dist/middlewares/error.middleware.js.map +1 -0
- package/dist/middlewares/rate-limit.middleware.d.ts +3 -0
- package/dist/middlewares/rate-limit.middleware.d.ts.map +1 -0
- package/dist/middlewares/rate-limit.middleware.js +40 -0
- package/dist/middlewares/rate-limit.middleware.js.map +1 -0
- package/dist/middlewares/request-id.middleware.d.ts +3 -0
- package/dist/middlewares/request-id.middleware.d.ts.map +1 -0
- package/dist/middlewares/request-id.middleware.js +12 -0
- package/dist/middlewares/request-id.middleware.js.map +1 -0
- package/dist/middlewares/upload.middleware.d.ts +2 -0
- package/dist/middlewares/upload.middleware.d.ts.map +1 -0
- package/dist/middlewares/upload.middleware.js +26 -0
- package/dist/middlewares/upload.middleware.js.map +1 -0
- package/dist/middlewares/validate.middleware.d.ts +6 -0
- package/dist/middlewares/validate.middleware.d.ts.map +1 -0
- package/dist/middlewares/validate.middleware.js +21 -0
- package/dist/middlewares/validate.middleware.js.map +1 -0
- package/dist/routes/admin.routes.d.ts +3 -0
- package/dist/routes/admin.routes.d.ts.map +1 -0
- package/dist/routes/admin.routes.js +44 -0
- package/dist/routes/admin.routes.js.map +1 -0
- package/dist/routes/index.d.ts +3 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +20 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/v1/api-keys.routes.d.ts +3 -0
- package/dist/routes/v1/api-keys.routes.d.ts.map +1 -0
- package/dist/routes/v1/api-keys.routes.js +13 -0
- package/dist/routes/v1/api-keys.routes.js.map +1 -0
- package/dist/routes/v1/chat.routes.d.ts +3 -0
- package/dist/routes/v1/chat.routes.d.ts.map +1 -0
- package/dist/routes/v1/chat.routes.js +13 -0
- package/dist/routes/v1/chat.routes.js.map +1 -0
- package/dist/routes/v1/documents.routes.d.ts +3 -0
- package/dist/routes/v1/documents.routes.d.ts.map +1 -0
- package/dist/routes/v1/documents.routes.js +15 -0
- package/dist/routes/v1/documents.routes.js.map +1 -0
- package/dist/routes/v1/models.routes.d.ts +3 -0
- package/dist/routes/v1/models.routes.d.ts.map +1 -0
- package/dist/routes/v1/models.routes.js +10 -0
- package/dist/routes/v1/models.routes.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +42 -0
- package/dist/server.js.map +1 -0
- package/dist/services/api-key.service.d.ts +33 -0
- package/dist/services/api-key.service.d.ts.map +1 -0
- package/dist/services/api-key.service.js +168 -0
- package/dist/services/api-key.service.js.map +1 -0
- package/dist/services/audit.service.d.ts +6 -0
- package/dist/services/audit.service.d.ts.map +1 -0
- package/dist/services/audit.service.js +21 -0
- package/dist/services/audit.service.js.map +1 -0
- package/dist/services/chat-admin.service.d.ts +76 -0
- package/dist/services/chat-admin.service.d.ts.map +1 -0
- package/dist/services/chat-admin.service.js +160 -0
- package/dist/services/chat-admin.service.js.map +1 -0
- package/dist/services/chat.service.d.ts +19 -0
- package/dist/services/chat.service.d.ts.map +1 -0
- package/dist/services/chat.service.js +335 -0
- package/dist/services/chat.service.js.map +1 -0
- package/dist/services/document.service.d.ts +37 -0
- package/dist/services/document.service.d.ts.map +1 -0
- package/dist/services/document.service.js +206 -0
- package/dist/services/document.service.js.map +1 -0
- package/dist/services/encryption.service.d.ts +8 -0
- package/dist/services/encryption.service.d.ts.map +1 -0
- package/dist/services/encryption.service.js +34 -0
- package/dist/services/encryption.service.js.map +1 -0
- package/dist/services/grok-cli.service.d.ts +18 -0
- package/dist/services/grok-cli.service.d.ts.map +1 -0
- package/dist/services/grok-cli.service.js +213 -0
- package/dist/services/grok-cli.service.js.map +1 -0
- package/dist/services/models.service.d.ts +10 -0
- package/dist/services/models.service.d.ts.map +1 -0
- package/dist/services/models.service.js +43 -0
- package/dist/services/models.service.js.map +1 -0
- package/dist/services/policy.service.d.ts +20 -0
- package/dist/services/policy.service.d.ts.map +1 -0
- package/dist/services/policy.service.js +66 -0
- package/dist/services/policy.service.js.map +1 -0
- package/dist/services/settings.service.d.ts +14 -0
- package/dist/services/settings.service.d.ts.map +1 -0
- package/dist/services/settings.service.js +86 -0
- package/dist/services/settings.service.js.map +1 -0
- package/dist/services/stats.service.d.ts +35 -0
- package/dist/services/stats.service.d.ts.map +1 -0
- package/dist/services/stats.service.js +59 -0
- package/dist/services/stats.service.js.map +1 -0
- package/dist/services/update.service.d.ts +44 -0
- package/dist/services/update.service.d.ts.map +1 -0
- package/dist/services/update.service.js +278 -0
- package/dist/services/update.service.js.map +1 -0
- package/dist/utils/async-handler.d.ts +5 -0
- package/dist/utils/async-handler.d.ts.map +1 -0
- package/dist/utils/async-handler.js +9 -0
- package/dist/utils/async-handler.js.map +1 -0
- package/dist/utils/id.d.ts +6 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +24 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +34 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/openai-mapper.d.ts +30 -0
- package/dist/utils/openai-mapper.d.ts.map +1 -0
- package/dist/utils/openai-mapper.js +176 -0
- package/dist/utils/openai-mapper.js.map +1 -0
- package/dist/utils/path-safe.d.ts +4 -0
- package/dist/utils/path-safe.d.ts.map +1 -0
- package/dist/utils/path-safe.js +37 -0
- package/dist/utils/path-safe.js.map +1 -0
- package/dist/utils/stream.d.ts +5 -0
- package/dist/utils/stream.d.ts.map +1 -0
- package/dist/utils/stream.js +22 -0
- package/dist/utils/stream.js.map +1 -0
- package/package.json +87 -0
- package/prisma/migrations/20260714084159_init_sqlite/migration.sql +106 -0
- package/prisma/migrations/20260714090338_safe_mode_admin/migration.sql +36 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +122 -0
- package/prisma/seed.ts +79 -0
- package/public/admin/app.js +734 -0
- package/public/admin/index.html +13 -0
- package/public/admin/styles.css +296 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
const API = '/admin/api';
|
|
2
|
+
const KEY_STORAGE = 'gog_admin_key';
|
|
3
|
+
|
|
4
|
+
const state = {
|
|
5
|
+
key: sessionStorage.getItem(KEY_STORAGE) || '',
|
|
6
|
+
page: 'dashboard',
|
|
7
|
+
me: null,
|
|
8
|
+
error: '',
|
|
9
|
+
modal: null,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async function api(path, options = {}) {
|
|
13
|
+
const headers = {
|
|
14
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
15
|
+
...(state.key ? { Authorization: `Bearer ${state.key}` } : {}),
|
|
16
|
+
...(options.headers || {}),
|
|
17
|
+
};
|
|
18
|
+
const res = await fetch(`${API}${path}`, { ...options, headers });
|
|
19
|
+
const text = await res.text();
|
|
20
|
+
let data = null;
|
|
21
|
+
try {
|
|
22
|
+
data = text ? JSON.parse(text) : null;
|
|
23
|
+
} catch {
|
|
24
|
+
data = { error: { message: text } };
|
|
25
|
+
}
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
const msg = data?.error?.message || res.statusText || 'Request failed';
|
|
28
|
+
if (res.status === 401 || res.status === 403) {
|
|
29
|
+
if (state.page !== 'login') {
|
|
30
|
+
logout(false);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw new Error(msg);
|
|
34
|
+
}
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function logout(clear = true) {
|
|
39
|
+
if (clear) sessionStorage.removeItem(KEY_STORAGE);
|
|
40
|
+
state.key = '';
|
|
41
|
+
state.me = null;
|
|
42
|
+
state.page = 'login';
|
|
43
|
+
render();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setPage(page) {
|
|
47
|
+
state.page = page;
|
|
48
|
+
state.modal = null;
|
|
49
|
+
state.error = '';
|
|
50
|
+
render();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function el(html) {
|
|
54
|
+
const t = document.createElement('template');
|
|
55
|
+
t.innerHTML = html.trim();
|
|
56
|
+
return t.content.firstElementChild;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function badgeStatus(s) {
|
|
60
|
+
const c =
|
|
61
|
+
s === 'success' ? 'success' : s === 'error' || s === 'timeout' ? 'error' : 'pending';
|
|
62
|
+
return `<span class="badge ${c}">${escapeHtml(s || '-')}</span>`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function badgeMode(m) {
|
|
66
|
+
return `<span class="badge ${m === 'agent' ? 'agent' : 'safe'}">${escapeHtml(m || 'safe')}</span>`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function escapeHtml(s) {
|
|
70
|
+
return String(s ?? '')
|
|
71
|
+
.replaceAll('&', '&')
|
|
72
|
+
.replaceAll('<', '<')
|
|
73
|
+
.replaceAll('>', '>')
|
|
74
|
+
.replaceAll('"', '"');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fmtTime(iso) {
|
|
78
|
+
if (!iso) return '-';
|
|
79
|
+
try {
|
|
80
|
+
return new Date(iso).toLocaleString();
|
|
81
|
+
} catch {
|
|
82
|
+
return iso;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function showError(msg) {
|
|
87
|
+
state.error = msg;
|
|
88
|
+
const box = document.querySelector('#flash-error');
|
|
89
|
+
if (box) {
|
|
90
|
+
box.hidden = !msg;
|
|
91
|
+
box.textContent = msg;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function ensureMe() {
|
|
96
|
+
if (!state.key) return false;
|
|
97
|
+
const data = await api('/me');
|
|
98
|
+
state.me = data.data;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function shell(content) {
|
|
103
|
+
return `
|
|
104
|
+
<div class="layout">
|
|
105
|
+
<aside class="sidebar">
|
|
106
|
+
<div class="brand">Grok Gateway<small>Admin Panel</small></div>
|
|
107
|
+
${nav('dashboard', '儀表板')}
|
|
108
|
+
${nav('chats', 'Chat 記錄')}
|
|
109
|
+
${nav('keys', 'API Keys')}
|
|
110
|
+
${nav('documents', '文件')}
|
|
111
|
+
${nav('audit', 'Audit Logs')}
|
|
112
|
+
${nav('settings', '安全設定')}
|
|
113
|
+
${nav('system', '系統狀態')}
|
|
114
|
+
<div class="sidebar-foot">
|
|
115
|
+
<div>${escapeHtml(state.me?.name || '')}</div>
|
|
116
|
+
<div>${escapeHtml(state.me?.keyPrefix || '')}…</div>
|
|
117
|
+
<button class="btn secondary sm" style="margin-top:10px;width:100%" id="btn-logout">登出</button>
|
|
118
|
+
</div>
|
|
119
|
+
</aside>
|
|
120
|
+
<main class="main">
|
|
121
|
+
<div id="flash-error" class="error-box" ${state.error ? '' : 'hidden'}>${escapeHtml(state.error)}</div>
|
|
122
|
+
${content}
|
|
123
|
+
</main>
|
|
124
|
+
</div>
|
|
125
|
+
${state.modal || ''}
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function nav(id, label) {
|
|
130
|
+
return `<button class="nav-btn ${state.page === id ? 'active' : ''}" data-nav="${id}">${label}</button>`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function renderLogin() {
|
|
134
|
+
document.getElementById('app').innerHTML = `
|
|
135
|
+
<div class="login-wrap">
|
|
136
|
+
<div class="login-card">
|
|
137
|
+
<h1>Admin 登入</h1>
|
|
138
|
+
<p>貼上 <strong>admin API key</strong>(seed 產生、只顯示一次)。資料會存放在本瀏覽器 sessionStorage。</p>
|
|
139
|
+
<div id="flash-error" class="error-box" ${state.error ? '' : 'hidden'}>${escapeHtml(state.error)}</div>
|
|
140
|
+
<label>Admin API Key</label>
|
|
141
|
+
<input id="login-key" type="password" placeholder="gk_live_..." autocomplete="off" />
|
|
142
|
+
<button class="btn" id="btn-login" style="width:100%">進入控制台</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
`;
|
|
146
|
+
document.getElementById('btn-login').onclick = async () => {
|
|
147
|
+
const key = document.getElementById('login-key').value.trim();
|
|
148
|
+
if (!key) return showError('請輸入 API key');
|
|
149
|
+
state.key = key;
|
|
150
|
+
try {
|
|
151
|
+
await ensureMe();
|
|
152
|
+
sessionStorage.setItem(KEY_STORAGE, key);
|
|
153
|
+
state.page = 'dashboard';
|
|
154
|
+
state.error = '';
|
|
155
|
+
render();
|
|
156
|
+
} catch (e) {
|
|
157
|
+
state.key = '';
|
|
158
|
+
showError(e.message);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function renderDashboard() {
|
|
164
|
+
const { data } = await api('/stats');
|
|
165
|
+
const t = data.totals;
|
|
166
|
+
const rows = (data.recentChats || [])
|
|
167
|
+
.map(
|
|
168
|
+
(c) => `
|
|
169
|
+
<tr>
|
|
170
|
+
<td><button class="linkish" data-chat="${c.id}">${escapeHtml(c.requestId)}</button></td>
|
|
171
|
+
<td>${escapeHtml(c.model)}</td>
|
|
172
|
+
<td>${badgeStatus(c.status)}</td>
|
|
173
|
+
<td>${badgeMode(c.policyMode || '-')}</td>
|
|
174
|
+
<td>${c.durationMs ?? '-'} ms</td>
|
|
175
|
+
<td>${fmtTime(c.createdAt)}</td>
|
|
176
|
+
</tr>`,
|
|
177
|
+
)
|
|
178
|
+
.join('');
|
|
179
|
+
|
|
180
|
+
document.getElementById('app').innerHTML = shell(`
|
|
181
|
+
<div class="topbar"><h2>儀表板</h2><span class="muted">最近 24h 請求:${t.chats24h}</span></div>
|
|
182
|
+
<div class="grid">
|
|
183
|
+
<div class="card"><div class="label">總 Chat</div><div class="value">${t.chats}</div></div>
|
|
184
|
+
<div class="card"><div class="label">成功</div><div class="value">${t.success}</div></div>
|
|
185
|
+
<div class="card"><div class="label">錯誤/逾時</div><div class="value">${t.errors}</div></div>
|
|
186
|
+
<div class="card"><div class="label">文件</div><div class="value">${t.documents}</div></div>
|
|
187
|
+
<div class="card"><div class="label">活躍 Keys</div><div class="value">${t.activeKeys}/${t.totalKeys}</div></div>
|
|
188
|
+
<div class="card"><div class="label">Grok 併發</div><div class="value">${data.concurrency.active}/${data.concurrency.max}</div></div>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="panel">
|
|
191
|
+
<div class="panel-h"><strong>最近 Chat</strong></div>
|
|
192
|
+
<table>
|
|
193
|
+
<thead><tr><th>Request</th><th>Model</th><th>Status</th><th>Mode</th><th>耗時</th><th>時間</th></tr></thead>
|
|
194
|
+
<tbody>${rows || `<tr><td colspan="6" class="empty">暫無資料</td></tr>`}</tbody>
|
|
195
|
+
</table>
|
|
196
|
+
</div>
|
|
197
|
+
`);
|
|
198
|
+
bindShell();
|
|
199
|
+
document.querySelectorAll('[data-chat]').forEach((b) => {
|
|
200
|
+
b.onclick = () => openChat(b.dataset.chat);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function renderChats() {
|
|
205
|
+
const q = new URLSearchParams({ limit: '50' });
|
|
206
|
+
const status = document.getElementById('f-status')?.value;
|
|
207
|
+
const model = document.getElementById('f-model')?.value;
|
|
208
|
+
const search = document.getElementById('f-q')?.value;
|
|
209
|
+
if (status) q.set('status', status);
|
|
210
|
+
if (model) q.set('model', model);
|
|
211
|
+
if (search) q.set('q', search);
|
|
212
|
+
|
|
213
|
+
const data = await api(`/chats?${q}`);
|
|
214
|
+
const rows = (data.items || [])
|
|
215
|
+
.map(
|
|
216
|
+
(c) => `
|
|
217
|
+
<tr>
|
|
218
|
+
<td><button class="linkish" data-chat="${c.id}">${escapeHtml(c.requestId)}</button></td>
|
|
219
|
+
<td>${escapeHtml(c.apiKey?.name || '')}<div class="muted">${escapeHtml(c.apiKey?.keyPrefix || '')}</div></td>
|
|
220
|
+
<td>${escapeHtml(c.model)}</td>
|
|
221
|
+
<td>${badgeStatus(c.status)} ${badgeMode(c.policyMode || '-')}</td>
|
|
222
|
+
<td><div class="muted">${escapeHtml(c.promptPreview)}</div></td>
|
|
223
|
+
<td><div class="muted">${escapeHtml(c.contentPreview)}</div></td>
|
|
224
|
+
<td>${fmtTime(c.createdAt)}</td>
|
|
225
|
+
</tr>`,
|
|
226
|
+
)
|
|
227
|
+
.join('');
|
|
228
|
+
|
|
229
|
+
document.getElementById('app').innerHTML = shell(`
|
|
230
|
+
<div class="topbar"><h2>Chat 記錄</h2><span class="muted">共 ${data.total} 筆(可解密查看完整 in/out)</span></div>
|
|
231
|
+
<div class="panel">
|
|
232
|
+
<div class="panel-h">
|
|
233
|
+
<div class="toolbar">
|
|
234
|
+
<select id="f-status">
|
|
235
|
+
<option value="">全部狀態</option>
|
|
236
|
+
<option value="success">success</option>
|
|
237
|
+
<option value="error">error</option>
|
|
238
|
+
<option value="timeout">timeout</option>
|
|
239
|
+
<option value="pending">pending</option>
|
|
240
|
+
</select>
|
|
241
|
+
<input id="f-model" placeholder="model 篩選" />
|
|
242
|
+
<input id="f-q" placeholder="搜尋 prompt/response 預覽" style="min-width:220px" />
|
|
243
|
+
<button class="btn secondary sm" id="btn-filter">篩選</button>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
<table>
|
|
247
|
+
<thead>
|
|
248
|
+
<tr>
|
|
249
|
+
<th>Request</th><th>API Key</th><th>Model</th><th>狀態</th>
|
|
250
|
+
<th>Prompt 預覽</th><th>Response 預覽</th><th>時間</th>
|
|
251
|
+
</tr>
|
|
252
|
+
</thead>
|
|
253
|
+
<tbody>${rows || `<tr><td colspan="7" class="empty">暫無資料</td></tr>`}</tbody>
|
|
254
|
+
</table>
|
|
255
|
+
</div>
|
|
256
|
+
`);
|
|
257
|
+
bindShell();
|
|
258
|
+
if (status) document.getElementById('f-status').value = status;
|
|
259
|
+
if (model) document.getElementById('f-model').value = model;
|
|
260
|
+
if (search) document.getElementById('f-q').value = search;
|
|
261
|
+
document.getElementById('btn-filter').onclick = () => renderChats().catch(onErr);
|
|
262
|
+
document.querySelectorAll('[data-chat]').forEach((b) => {
|
|
263
|
+
b.onclick = () => openChat(b.dataset.chat);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function openChat(id) {
|
|
268
|
+
const { data } = await api(`/chats/${id}`);
|
|
269
|
+
const r = data.response || {};
|
|
270
|
+
state.modal = `
|
|
271
|
+
<div class="modal-back" id="modal-back">
|
|
272
|
+
<div class="modal">
|
|
273
|
+
<div class="modal-h">
|
|
274
|
+
<div>
|
|
275
|
+
<strong>Chat 詳情</strong>
|
|
276
|
+
<div class="muted">${escapeHtml(data.requestId)} · ${badgeStatus(data.status)} ${badgeMode(data.policyMode || '-')}</div>
|
|
277
|
+
</div>
|
|
278
|
+
<button class="btn secondary sm" id="modal-close">關閉</button>
|
|
279
|
+
</div>
|
|
280
|
+
<div class="modal-b">
|
|
281
|
+
<div class="grid" style="margin-bottom:14px">
|
|
282
|
+
<div class="card"><div class="label">Model</div><div class="value" style="font-size:1rem">${escapeHtml(data.model)}</div></div>
|
|
283
|
+
<div class="card"><div class="label">耗時</div><div class="value" style="font-size:1rem">${data.durationMs ?? '-'} ms</div></div>
|
|
284
|
+
<div class="card"><div class="label">API Key</div><div class="value" style="font-size:1rem">${escapeHtml(data.apiKey?.name || '')}</div></div>
|
|
285
|
+
<div class="card"><div class="label">Stream</div><div class="value" style="font-size:1rem">${data.stream ? 'yes' : 'no'}</div></div>
|
|
286
|
+
</div>
|
|
287
|
+
${data.errorMessage ? `<div class="error-box">${escapeHtml(data.errorMessage)}</div>` : ''}
|
|
288
|
+
<div class="block">
|
|
289
|
+
<h4>Prompt(輸入 · 完整明文)</h4>
|
|
290
|
+
<div class="pre">${escapeHtml(data.prompt)}</div>
|
|
291
|
+
<button class="btn secondary sm" data-copy="prompt">複製 Prompt</button>
|
|
292
|
+
</div>
|
|
293
|
+
<div class="block">
|
|
294
|
+
<h4>Reasoning / Thought</h4>
|
|
295
|
+
<div class="pre">${escapeHtml(r.reasoning_content || '(無)')}</div>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="block">
|
|
298
|
+
<h4>Content(輸出 · 完整明文)</h4>
|
|
299
|
+
<div class="pre">${escapeHtml(r.content || '(無)')}</div>
|
|
300
|
+
<button class="btn secondary sm" data-copy="content">複製 Content</button>
|
|
301
|
+
</div>
|
|
302
|
+
<div class="block">
|
|
303
|
+
<h4>Raw Response 儲存值</h4>
|
|
304
|
+
<div class="pre">${escapeHtml(r.raw || '')}</div>
|
|
305
|
+
</div>
|
|
306
|
+
<div class="muted">IP: ${escapeHtml(data.ip || '-')} · UA: ${escapeHtml(data.userAgent || '-')} · ${fmtTime(data.createdAt)}</div>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
`;
|
|
311
|
+
// re-render current page body with modal - simpler: inject modal
|
|
312
|
+
const app = document.getElementById('app');
|
|
313
|
+
const existing = document.getElementById('modal-back');
|
|
314
|
+
if (existing) existing.remove();
|
|
315
|
+
app.insertAdjacentHTML('beforeend', state.modal);
|
|
316
|
+
document.getElementById('modal-close').onclick = () => {
|
|
317
|
+
state.modal = null;
|
|
318
|
+
document.getElementById('modal-back')?.remove();
|
|
319
|
+
};
|
|
320
|
+
document.getElementById('modal-back').onclick = (e) => {
|
|
321
|
+
if (e.target.id === 'modal-back') {
|
|
322
|
+
state.modal = null;
|
|
323
|
+
document.getElementById('modal-back')?.remove();
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
document.querySelector('[data-copy="prompt"]')?.addEventListener('click', () => {
|
|
327
|
+
navigator.clipboard.writeText(data.prompt || '');
|
|
328
|
+
});
|
|
329
|
+
document.querySelector('[data-copy="content"]')?.addEventListener('click', () => {
|
|
330
|
+
navigator.clipboard.writeText(r.content || '');
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function renderKeys() {
|
|
335
|
+
const { data } = await api('/keys');
|
|
336
|
+
const rows = data
|
|
337
|
+
.map(
|
|
338
|
+
(k) => `
|
|
339
|
+
<tr>
|
|
340
|
+
<td>${escapeHtml(k.name)}<div class="muted">${escapeHtml(k.keyPrefix)}…</div></td>
|
|
341
|
+
<td>${escapeHtml(k.role)}</td>
|
|
342
|
+
<td>${badgeMode(k.mode)}</td>
|
|
343
|
+
<td>${k.rateLimit}/min</td>
|
|
344
|
+
<td>${k.isActive ? '<span class="badge success">active</span>' : '<span class="badge error">revoked</span>'}</td>
|
|
345
|
+
<td>${fmtTime(k.createdAt)}</td>
|
|
346
|
+
<td>
|
|
347
|
+
<button class="btn secondary sm" data-edit="${k.id}">編輯</button>
|
|
348
|
+
${k.isActive ? `<button class="btn danger sm" data-revoke="${k.id}">撤銷</button>` : ''}
|
|
349
|
+
</td>
|
|
350
|
+
</tr>`,
|
|
351
|
+
)
|
|
352
|
+
.join('');
|
|
353
|
+
|
|
354
|
+
document.getElementById('app').innerHTML = shell(`
|
|
355
|
+
<div class="topbar">
|
|
356
|
+
<h2>API Keys</h2>
|
|
357
|
+
<button class="btn" id="btn-new-key">新增 Key</button>
|
|
358
|
+
</div>
|
|
359
|
+
<div class="panel">
|
|
360
|
+
<table>
|
|
361
|
+
<thead><tr><th>名稱</th><th>Role</th><th>Mode</th><th>Rate</th><th>狀態</th><th>建立</th><th></th></tr></thead>
|
|
362
|
+
<tbody>${rows || `<tr><td colspan="7" class="empty">暫無</td></tr>`}</tbody>
|
|
363
|
+
</table>
|
|
364
|
+
</div>
|
|
365
|
+
`);
|
|
366
|
+
bindShell();
|
|
367
|
+
document.getElementById('btn-new-key').onclick = () => showKeyForm();
|
|
368
|
+
document.querySelectorAll('[data-edit]').forEach((b) => {
|
|
369
|
+
const key = data.find((x) => x.id === b.dataset.edit);
|
|
370
|
+
b.onclick = () => showKeyForm(key);
|
|
371
|
+
});
|
|
372
|
+
document.querySelectorAll('[data-revoke]').forEach((b) => {
|
|
373
|
+
b.onclick = async () => {
|
|
374
|
+
if (!confirm('確定撤銷此 key?')) return;
|
|
375
|
+
await api(`/keys/${b.dataset.revoke}`, { method: 'DELETE' });
|
|
376
|
+
renderKeys().catch(onErr);
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function showKeyForm(existing) {
|
|
382
|
+
const isEdit = Boolean(existing);
|
|
383
|
+
state.modal = `
|
|
384
|
+
<div class="modal-back" id="modal-back">
|
|
385
|
+
<div class="modal" style="max-width:560px">
|
|
386
|
+
<div class="modal-h">
|
|
387
|
+
<strong>${isEdit ? '編輯 API Key' : '新增 API Key'}</strong>
|
|
388
|
+
<button class="btn secondary sm" id="modal-close">關閉</button>
|
|
389
|
+
</div>
|
|
390
|
+
<div class="modal-b">
|
|
391
|
+
<div class="form-grid">
|
|
392
|
+
<label class="full">名稱<input id="k-name" value="${escapeHtml(existing?.name || '')}" /></label>
|
|
393
|
+
<label>Role
|
|
394
|
+
<select id="k-role">
|
|
395
|
+
<option value="client">client</option>
|
|
396
|
+
<option value="admin">admin</option>
|
|
397
|
+
</select>
|
|
398
|
+
</label>
|
|
399
|
+
<label>Mode
|
|
400
|
+
<select id="k-mode">
|
|
401
|
+
<option value="safe">safe(對外安全)</option>
|
|
402
|
+
<option value="agent">agent(全能力)</option>
|
|
403
|
+
</select>
|
|
404
|
+
</label>
|
|
405
|
+
<label>Rate limit / min<input id="k-rate" type="number" value="${existing?.rateLimit ?? 60}" /></label>
|
|
406
|
+
<label>Max turns(可空)<input id="k-turns" type="number" value="${existing?.maxTurns ?? ''}" /></label>
|
|
407
|
+
<label>Timeout ms(可空)<input id="k-timeout" type="number" value="${existing?.timeoutMs ?? ''}" /></label>
|
|
408
|
+
${isEdit ? `<label class="full">啟用
|
|
409
|
+
<select id="k-active"><option value="true">active</option><option value="false">revoked</option></select>
|
|
410
|
+
</label>` : ''}
|
|
411
|
+
</div>
|
|
412
|
+
<div style="margin-top:14px;display:flex;gap:8px">
|
|
413
|
+
<button class="btn" id="k-save">儲存</button>
|
|
414
|
+
</div>
|
|
415
|
+
<pre id="k-created" class="pre" style="display:none;margin-top:12px"></pre>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
</div>`;
|
|
419
|
+
document.getElementById('modal-back')?.remove();
|
|
420
|
+
document.getElementById('app').insertAdjacentHTML('beforeend', state.modal);
|
|
421
|
+
document.getElementById('k-role').value = existing?.role || 'client';
|
|
422
|
+
document.getElementById('k-mode').value = existing?.mode || 'safe';
|
|
423
|
+
if (isEdit) document.getElementById('k-active').value = String(existing.isActive);
|
|
424
|
+
document.getElementById('modal-close').onclick = () => document.getElementById('modal-back')?.remove();
|
|
425
|
+
document.getElementById('k-save').onclick = async () => {
|
|
426
|
+
const body = {
|
|
427
|
+
name: document.getElementById('k-name').value.trim(),
|
|
428
|
+
role: document.getElementById('k-role').value,
|
|
429
|
+
mode: document.getElementById('k-mode').value,
|
|
430
|
+
rateLimit: Number(document.getElementById('k-rate').value || 60),
|
|
431
|
+
maxTurns: document.getElementById('k-turns').value
|
|
432
|
+
? Number(document.getElementById('k-turns').value)
|
|
433
|
+
: null,
|
|
434
|
+
timeoutMs: document.getElementById('k-timeout').value
|
|
435
|
+
? Number(document.getElementById('k-timeout').value)
|
|
436
|
+
: null,
|
|
437
|
+
};
|
|
438
|
+
if (isEdit) {
|
|
439
|
+
body.isActive = document.getElementById('k-active').value === 'true';
|
|
440
|
+
await api(`/keys/${existing.id}`, { method: 'PATCH', body: JSON.stringify(body) });
|
|
441
|
+
document.getElementById('modal-back')?.remove();
|
|
442
|
+
renderKeys().catch(onErr);
|
|
443
|
+
} else {
|
|
444
|
+
const res = await api('/keys', { method: 'POST', body: JSON.stringify(body) });
|
|
445
|
+
const box = document.getElementById('k-created');
|
|
446
|
+
box.style.display = 'block';
|
|
447
|
+
box.textContent = `已建立(明文 key 只顯示一次):\n${res.data.key}`;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function renderDocuments() {
|
|
453
|
+
const data = await api('/documents?limit=50');
|
|
454
|
+
const rows = (data.data || [])
|
|
455
|
+
.map(
|
|
456
|
+
(d) => `
|
|
457
|
+
<tr>
|
|
458
|
+
<td><button class="linkish" data-doc="${d.id}">${escapeHtml(d.originalName)}</button></td>
|
|
459
|
+
<td>${escapeHtml(d.apiKey?.name || '')}</td>
|
|
460
|
+
<td>${escapeHtml(d.mimeType)}</td>
|
|
461
|
+
<td>${d.sizeBytes}</td>
|
|
462
|
+
<td>${fmtTime(d.createdAt)}</td>
|
|
463
|
+
<td><button class="btn danger sm" data-del="${d.id}">刪除</button></td>
|
|
464
|
+
</tr>`,
|
|
465
|
+
)
|
|
466
|
+
.join('');
|
|
467
|
+
document.getElementById('app').innerHTML = shell(`
|
|
468
|
+
<div class="topbar"><h2>文件</h2><span class="muted">共 ${data.total} 個</span></div>
|
|
469
|
+
<div class="panel">
|
|
470
|
+
<table>
|
|
471
|
+
<thead><tr><th>檔名</th><th>API Key</th><th>MIME</th><th>大小</th><th>時間</th><th></th></tr></thead>
|
|
472
|
+
<tbody>${rows || `<tr><td colspan="6" class="empty">暫無</td></tr>`}</tbody>
|
|
473
|
+
</table>
|
|
474
|
+
</div>
|
|
475
|
+
`);
|
|
476
|
+
bindShell();
|
|
477
|
+
document.querySelectorAll('[data-doc]').forEach((b) => {
|
|
478
|
+
b.onclick = () => openDocument(b.dataset.doc);
|
|
479
|
+
});
|
|
480
|
+
document.querySelectorAll('[data-del]').forEach((b) => {
|
|
481
|
+
b.onclick = async () => {
|
|
482
|
+
if (!confirm('刪除此文件?')) return;
|
|
483
|
+
await api(`/documents/${b.dataset.del}`, { method: 'DELETE' });
|
|
484
|
+
renderDocuments().catch(onErr);
|
|
485
|
+
};
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function openDocument(id) {
|
|
490
|
+
const { data: doc } = await api(`/documents/${id}`);
|
|
491
|
+
document.getElementById('modal-back')?.remove();
|
|
492
|
+
document.getElementById('app').insertAdjacentHTML(
|
|
493
|
+
'beforeend',
|
|
494
|
+
`
|
|
495
|
+
<div class="modal-back" id="modal-back">
|
|
496
|
+
<div class="modal">
|
|
497
|
+
<div class="modal-h">
|
|
498
|
+
<div>
|
|
499
|
+
<strong>文件詳情</strong>
|
|
500
|
+
<div class="muted">${escapeHtml(doc.originalName)} · ${escapeHtml(doc.mimeType)} · ${doc.sizeBytes} bytes</div>
|
|
501
|
+
</div>
|
|
502
|
+
<button class="btn secondary sm" id="modal-close">關閉</button>
|
|
503
|
+
</div>
|
|
504
|
+
<div class="modal-b">
|
|
505
|
+
<div class="grid" style="margin-bottom:14px">
|
|
506
|
+
<div class="card"><div class="label">API Key</div><div class="value" style="font-size:1rem">${escapeHtml(doc.apiKey?.name || '')}</div></div>
|
|
507
|
+
<div class="card"><div class="label">Checksum</div><div class="value" style="font-size:.75rem;word-break:break-all">${escapeHtml(doc.checksumSha256 || '')}</div></div>
|
|
508
|
+
<div class="card"><div class="label">建立時間</div><div class="value" style="font-size:1rem">${fmtTime(doc.createdAt)}</div></div>
|
|
509
|
+
</div>
|
|
510
|
+
<div class="block">
|
|
511
|
+
<h4>解密內容預覽</h4>
|
|
512
|
+
<div class="pre" id="doc-content">${escapeHtml(doc.content || '(無)')}</div>
|
|
513
|
+
<button class="btn secondary sm" id="doc-copy">複製內容</button>
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
</div>`,
|
|
518
|
+
);
|
|
519
|
+
document.getElementById('modal-close').onclick = () => document.getElementById('modal-back')?.remove();
|
|
520
|
+
document.getElementById('modal-back').onclick = (e) => {
|
|
521
|
+
if (e.target.id === 'modal-back') document.getElementById('modal-back')?.remove();
|
|
522
|
+
};
|
|
523
|
+
document.getElementById('doc-copy').onclick = () => {
|
|
524
|
+
navigator.clipboard.writeText(doc.content || '');
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function renderAudit() {
|
|
529
|
+
const data = await api('/audit-logs?limit=100');
|
|
530
|
+
const rows = (data.data || [])
|
|
531
|
+
.map(
|
|
532
|
+
(a) => `
|
|
533
|
+
<tr>
|
|
534
|
+
<td>${fmtTime(a.createdAt)}</td>
|
|
535
|
+
<td>${escapeHtml(a.action)}</td>
|
|
536
|
+
<td>${escapeHtml(a.resource || '')} ${escapeHtml(a.resourceId || '')}</td>
|
|
537
|
+
<td>${escapeHtml(a.apiKey?.name || '-')}</td>
|
|
538
|
+
<td class="muted">${escapeHtml(a.metaJson || '')}</td>
|
|
539
|
+
</tr>`,
|
|
540
|
+
)
|
|
541
|
+
.join('');
|
|
542
|
+
document.getElementById('app').innerHTML = shell(`
|
|
543
|
+
<div class="topbar"><h2>Audit Logs</h2></div>
|
|
544
|
+
<div class="panel">
|
|
545
|
+
<table>
|
|
546
|
+
<thead><tr><th>時間</th><th>Action</th><th>Resource</th><th>Key</th><th>Meta</th></tr></thead>
|
|
547
|
+
<tbody>${rows || `<tr><td colspan="5" class="empty">暫無</td></tr>`}</tbody>
|
|
548
|
+
</table>
|
|
549
|
+
</div>
|
|
550
|
+
`);
|
|
551
|
+
bindShell();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function renderSettings() {
|
|
555
|
+
const { data } = await api('/settings');
|
|
556
|
+
document.getElementById('app').innerHTML = shell(`
|
|
557
|
+
<div class="topbar"><h2>安全設定</h2></div>
|
|
558
|
+
<div class="panel">
|
|
559
|
+
<div class="modal-b">
|
|
560
|
+
<p class="muted">對外安全模式會強制 sandbox cwd、停 always-approve、限制 tools。全域開啟時即使 key 設 agent 也會降級為 safe。</p>
|
|
561
|
+
<div class="switch-row">
|
|
562
|
+
<input type="checkbox" id="s-global" ${data.globalSafeMode ? 'checked' : ''} />
|
|
563
|
+
<label for="s-global">全域 Safe Mode(強制所有 key)</label>
|
|
564
|
+
</div>
|
|
565
|
+
<div class="form-grid">
|
|
566
|
+
<label>Safe tools 模式
|
|
567
|
+
<select id="s-tools">
|
|
568
|
+
<option value="none">none(禁危險 tools)</option>
|
|
569
|
+
<option value="readonly">readonly(只讀)</option>
|
|
570
|
+
</select>
|
|
571
|
+
</label>
|
|
572
|
+
<label>Safe max turns<input id="s-turns" type="number" value="${data.safeMaxTurns}" /></label>
|
|
573
|
+
<label>Safe timeout (ms)<input id="s-timeout" type="number" value="${data.safeTimeoutMs}" /></label>
|
|
574
|
+
<label>預設 model<input id="s-model" value="${escapeHtml(data.defaultModel)}" /></label>
|
|
575
|
+
<label class="full"><span class="switch-row" style="margin:0">
|
|
576
|
+
<input type="checkbox" id="s-panel" ${data.adminPanelEnabled ? 'checked' : ''} />
|
|
577
|
+
啟用 Admin Panel API
|
|
578
|
+
</span></label>
|
|
579
|
+
</div>
|
|
580
|
+
<div style="margin-top:16px">
|
|
581
|
+
<button class="btn" id="s-save">儲存設定</button>
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
`);
|
|
586
|
+
bindShell();
|
|
587
|
+
document.getElementById('s-tools').value = data.safeToolsMode || 'none';
|
|
588
|
+
document.getElementById('s-save').onclick = async () => {
|
|
589
|
+
try {
|
|
590
|
+
await api('/settings', {
|
|
591
|
+
method: 'PUT',
|
|
592
|
+
body: JSON.stringify({
|
|
593
|
+
globalSafeMode: document.getElementById('s-global').checked,
|
|
594
|
+
safeToolsMode: document.getElementById('s-tools').value,
|
|
595
|
+
safeMaxTurns: Number(document.getElementById('s-turns').value),
|
|
596
|
+
safeTimeoutMs: Number(document.getElementById('s-timeout').value),
|
|
597
|
+
defaultModel: document.getElementById('s-model').value.trim(),
|
|
598
|
+
adminPanelEnabled: document.getElementById('s-panel').checked,
|
|
599
|
+
}),
|
|
600
|
+
});
|
|
601
|
+
state.error = '';
|
|
602
|
+
const bar = document.querySelector('#flash-error');
|
|
603
|
+
if (bar) {
|
|
604
|
+
bar.hidden = false;
|
|
605
|
+
bar.style.background = 'rgba(34,197,94,.15)';
|
|
606
|
+
bar.style.borderColor = 'rgba(34,197,94,.4)';
|
|
607
|
+
bar.style.color = '#bbf7d0';
|
|
608
|
+
bar.textContent = '設定已儲存';
|
|
609
|
+
setTimeout(() => {
|
|
610
|
+
bar.hidden = true;
|
|
611
|
+
bar.removeAttribute('style');
|
|
612
|
+
}, 2000);
|
|
613
|
+
}
|
|
614
|
+
} catch (e) {
|
|
615
|
+
onErr(e);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function renderSystem() {
|
|
621
|
+
const { data } = await api('/system');
|
|
622
|
+
const v = data.version || {};
|
|
623
|
+
const updateBadge = v.updateAvailable
|
|
624
|
+
? '<span class="badge warn">有更新</span>'
|
|
625
|
+
: '<span class="badge success">最新 / 未知</span>';
|
|
626
|
+
document.getElementById('app').innerHTML = shell(`
|
|
627
|
+
<div class="topbar">
|
|
628
|
+
<h2>系統狀態</h2>
|
|
629
|
+
<div class="toolbar">
|
|
630
|
+
<button class="btn secondary sm" id="btn-check-update">檢查更新</button>
|
|
631
|
+
<button class="btn sm" id="btn-one-click-update">一鍵更新並重啟</button>
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
634
|
+
<div class="grid">
|
|
635
|
+
<div class="card"><div class="label">Database</div><div class="value" style="font-size:1.2rem">${escapeHtml(data.database)}</div></div>
|
|
636
|
+
<div class="card"><div class="label">Grok CLI</div><div class="value" style="font-size:1.2rem">${escapeHtml(data.grokCli)}</div></div>
|
|
637
|
+
<div class="card"><div class="label">Concurrency</div><div class="value" style="font-size:1.2rem">${data.concurrency.active}/${data.concurrency.max}</div></div>
|
|
638
|
+
<div class="card"><div class="label">版本</div><div class="value" style="font-size:1.1rem">${escapeHtml(v.current || '?')} ${updateBadge}</div></div>
|
|
639
|
+
</div>
|
|
640
|
+
<div class="panel" style="margin-bottom:14px">
|
|
641
|
+
<div class="panel-h"><strong>自我更新</strong><span class="muted">等同 CLI:gctoac update</span></div>
|
|
642
|
+
<div class="modal-b">
|
|
643
|
+
<p class="muted">會按安裝方式自動選擇:git pull、npm global、或 GitHub 安裝。更新後會重啟 gateway(約 10–30 秒),之後請重新整理本頁。</p>
|
|
644
|
+
<div class="grid">
|
|
645
|
+
<div class="card"><div class="label">目前版本</div><div class="value" style="font-size:1rem">${escapeHtml(v.current || '-')}</div></div>
|
|
646
|
+
<div class="card"><div class="label">npm latest</div><div class="value" style="font-size:1rem">${escapeHtml(v.latestNpm || 'n/a')}</div></div>
|
|
647
|
+
<div class="card"><div class="label">GitHub latest</div><div class="value" style="font-size:1rem">${escapeHtml(v.latestGithub || 'n/a')}</div></div>
|
|
648
|
+
<div class="card"><div class="label">安裝方式</div><div class="value" style="font-size:.9rem">${escapeHtml(v.channel || '-')} · ${escapeHtml(v.installSource || '')}</div></div>
|
|
649
|
+
</div>
|
|
650
|
+
<pre id="update-log" class="pre" style="display:none;margin-top:12px"></pre>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
<div class="panel"><div class="modal-b"><pre class="pre">${escapeHtml(JSON.stringify({ env: data.env, version: v }, null, 2))}</pre></div></div>
|
|
654
|
+
`);
|
|
655
|
+
bindShell();
|
|
656
|
+
document.getElementById('btn-check-update').onclick = async () => {
|
|
657
|
+
try {
|
|
658
|
+
const res = await api('/system/update-check');
|
|
659
|
+
const d = res.data || {};
|
|
660
|
+
alert(
|
|
661
|
+
`目前: ${d.current}\nnpm: ${d.latestNpm || 'n/a'}\nGitHub: ${d.latestGithub || 'n/a'}\n可更新: ${d.updateAvailable ? '是' : '否'}`,
|
|
662
|
+
);
|
|
663
|
+
renderSystem().catch(onErr);
|
|
664
|
+
} catch (e) {
|
|
665
|
+
onErr(e);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
document.getElementById('btn-one-click-update').onclick = async () => {
|
|
669
|
+
if (
|
|
670
|
+
!confirm(
|
|
671
|
+
'確定要一鍵更新並重啟 gateway?\n過程約 10–30 秒,期間 API 會短暫中斷。',
|
|
672
|
+
)
|
|
673
|
+
) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const logEl = document.getElementById('update-log');
|
|
677
|
+
try {
|
|
678
|
+
const btn = document.getElementById('btn-one-click-update');
|
|
679
|
+
if (btn) btn.disabled = true;
|
|
680
|
+
const res = await api('/system/update', {
|
|
681
|
+
method: 'POST',
|
|
682
|
+
body: JSON.stringify({ restart: true }),
|
|
683
|
+
});
|
|
684
|
+
if (logEl) {
|
|
685
|
+
logEl.style.display = 'block';
|
|
686
|
+
logEl.textContent =
|
|
687
|
+
(res.data && (res.data.message || JSON.stringify(res.data, null, 2))) ||
|
|
688
|
+
'Update scheduled';
|
|
689
|
+
}
|
|
690
|
+
alert(
|
|
691
|
+
(res.data && res.data.message) ||
|
|
692
|
+
'已排程更新,請約 30 秒後重新整理頁面。',
|
|
693
|
+
);
|
|
694
|
+
} catch (e) {
|
|
695
|
+
onErr(e);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function bindShell() {
|
|
701
|
+
document.querySelectorAll('[data-nav]').forEach((b) => {
|
|
702
|
+
b.onclick = () => setPage(b.dataset.nav);
|
|
703
|
+
});
|
|
704
|
+
document.getElementById('btn-logout')?.addEventListener('click', () => logout(true));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function onErr(e) {
|
|
708
|
+
console.error(e);
|
|
709
|
+
showError(e.message || String(e));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function render() {
|
|
713
|
+
const app = document.getElementById('app');
|
|
714
|
+
try {
|
|
715
|
+
if (!state.key) {
|
|
716
|
+
await renderLogin();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (!state.me) await ensureMe();
|
|
720
|
+
if (state.page === 'dashboard') await renderDashboard();
|
|
721
|
+
else if (state.page === 'chats') await renderChats();
|
|
722
|
+
else if (state.page === 'keys') await renderKeys();
|
|
723
|
+
else if (state.page === 'documents') await renderDocuments();
|
|
724
|
+
else if (state.page === 'audit') await renderAudit();
|
|
725
|
+
else if (state.page === 'settings') await renderSettings();
|
|
726
|
+
else if (state.page === 'system') await renderSystem();
|
|
727
|
+
else await renderDashboard();
|
|
728
|
+
} catch (e) {
|
|
729
|
+
app.innerHTML = shell(`<div class="error-box">${escapeHtml(e.message)}</div>`);
|
|
730
|
+
bindShell();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
render();
|