diff --git a/dashboard/public/app.js b/dashboard/public/app.js index 7d598555193c2c907b24f3bfe922136cc80813fd..e5419d02a9c2e9471cd7656b10d3988a421e1a0a 100644 --- a/dashboard/public/app.js +++ b/dashboard/public/app.js @@ -20,6 +20,12 @@ const createKeyForm = document.getElementById('create-key-form'); const cancelCreateBtn = document.getElementById('cancel-create-btn'); const keyDetailsModal = document.getElementById('key-details-modal'); const closeDetailsBtn = document.getElementById('close-details-btn'); +const createServerKeyModal = document.getElementById('create-server-key-modal'); +const createServerKeyForm = document.getElementById('create-server-key-form'); +const cancelServerKeyBtn = document.getElementById('cancel-server-key-btn'); +const refreshKeyModal = document.getElementById('refresh-key-modal'); +const cancelRefreshBtn = document.getElementById('cancel-refresh-btn'); +const confirmRefreshBtn = document.getElementById('confirm-refresh-btn'); const adminKeysList = document.getElementById('admin-keys-list'); const userKeysList = document.getElementById('user-keys-list'); const commandForm = document.getElementById('command-form'); @@ -263,35 +269,19 @@ function renderAdminKeys(keys) { let html = ''; - // Render Admin Keys if (adminKeys.length > 0) { html += '

管理员密钥 (Admin Keys)

'; html += adminKeys.map(key => renderKeyCard(key)).join(''); } - // Render Regular Keys with nested Server Keys if (regularKeys.length > 0) { html += '

用户密钥 (Regular Keys)

'; html += regularKeys.map(regularKey => { const childServerKeys = serverKeysMap[regularKey.id] || []; - return ` -
-
- ${renderKeyCard(regularKey)} - ${childServerKeys.length > 0 ? `` : ''} -
- ${childServerKeys.length > 0 ? ` - - ` : ''} -
- `; + return renderRegularKeyCard(regularKey, childServerKeys); }).join(''); } - // Render Orphaned Server Keys (if any) const linkedServerKeyIds = new Set(Object.values(serverKeysMap).flat().map(k => k.id)); const orphanServerKeys = serverKeys.filter(k => !linkedServerKeyIds.has(k.id)); @@ -339,12 +329,54 @@ function renderKeyCard(key, isNested = false) { ? `` : `` } + `; } +function renderRegularKeyCard(regularKey, childServerKeys = []) { + return ` +
+
+
+
+

+ Regular + + ${regularKey.isActive ? '已启用' : '已停用'} + + ${regularKey.name} +

+

ID: ${regularKey.id}

+

Prefix: ${regularKey.keyPrefix}

+ ${regularKey.serverId ? `

Server ID: ${regularKey.serverId}

` : ''} +

创建时间: ${new Date(regularKey.createdAt).toLocaleString()}

+

最后使用: ${regularKey.lastUsed ? new Date(regularKey.lastUsed).toLocaleString() : '从未'}

+
+
+ ${regularKey.isActive + ? `` + : `` + } + + + +
+
+ ${childServerKeys.length > 0 ? `` : ''} +
+ ${childServerKeys.length > 0 ? ` + + ` : ''} +
+ `; +} + function renderUserServerKeys(keys) { if (!userKeysList) { return; @@ -376,6 +408,7 @@ function renderUserServerKeys(keys) { ? `` : `` } + `).join(''); @@ -658,6 +691,166 @@ if (closeDetailsBtn) { }); } +function showCreateServerKeyModal(regularKeyId, regularKeyName) { + if (!createServerKeyModal) { + return; + } + + const parentNameEl = document.getElementById('create-server-key-parent-name'); + if (parentNameEl) { + parentNameEl.textContent = `父级 Regular Key: ${regularKeyName}`; + } + + const regularIdInput = document.getElementById('server-key-regular-id'); + if (regularIdInput) { + regularIdInput.value = regularKeyId; + } + + createServerKeyModal.classList.remove('hidden'); +} + +function hideCreateServerKeyModal() { + if (!createServerKeyModal) { + return; + } + + createServerKeyModal.classList.add('hidden'); + + const nameInput = document.getElementById('server-key-name'); + const descInput = document.getElementById('server-key-description'); + const serverIdInput = document.getElementById('server-key-server-id'); + + if (nameInput) nameInput.value = ''; + if (descInput) descInput.value = ''; + if (serverIdInput) serverIdInput.value = ''; +} + +async function createServerKey(event) { + event.preventDefault(); + + const regularKeyId = document.getElementById('server-key-regular-id')?.value; + const name = document.getElementById('server-key-name')?.value.trim(); + const description = document.getElementById('server-key-description')?.value.trim(); + const serverId = document.getElementById('server-key-server-id')?.value.trim(); + + if (!regularKeyId || !name) { + alert('Regular Key ID and name are required.'); + return; + } + + try { + const payload = { + name, + description, + regular_key_id: regularKeyId + }; + + if (serverId) { + payload.server_id = serverId; + } + + const response = await fetch(`${API_URL}/manage/keys/server-keys`, { + method: 'POST', + headers: { + ...authHeaders(apiKey), + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + const error = await response.json(); + alert(`Failed to create server key: ${error.detail}`); + return; + } + + const result = await response.json(); + hideCreateServerKeyModal(); + showKeyCreatedModal(result); + loadAdminKeys(); + } catch (error) { + alert(`Failed to create server key: ${error.message}`); + } +} + +if (cancelServerKeyBtn) { + cancelServerKeyBtn.addEventListener('click', hideCreateServerKeyModal); +} + +if (createServerKeyForm) { + createServerKeyForm.addEventListener('submit', createServerKey); +} + +window.showCreateServerKeyModal = showCreateServerKeyModal; +window.hideCreateServerKeyModal = hideCreateServerKeyModal; +window.createServerKey = createServerKey; + +function showRefreshKeyModal(keyId, keyName) { + if (!refreshKeyModal) { + return; + } + + const keyNameEl = document.getElementById('refresh-key-name'); + const keyIdInput = document.getElementById('refresh-key-id'); + + if (keyNameEl) keyNameEl.textContent = keyName; + if (keyIdInput) keyIdInput.value = keyId; + + refreshKeyModal.classList.remove('hidden'); +} + +function hideRefreshKeyModal() { + if (!refreshKeyModal) { + return; + } + refreshKeyModal.classList.add('hidden'); +} + +async function refreshKey() { + const keyId = document.getElementById('refresh-key-id')?.value; + + if (!keyId || !apiKey) { + return; + } + + try { + const response = await fetch(`${API_URL}/manage/keys/${keyId}/refresh`, { + method: 'POST', + headers: authHeaders(apiKey) + }); + + if (!response.ok) { + const error = await response.json(); + alert(`刷新失败: ${error.detail}`); + return; + } + + const result = await response.json(); + hideRefreshKeyModal(); + showKeyCreatedModal(result); + + if (currentRole === 'admin') { + loadAdminKeys(); + } else if (currentRole === 'regular') { + loadUserServerKeys(); + } + } catch (error) { + alert(`刷新失败: ${error.message}`); + } +} + +if (cancelRefreshBtn) { + cancelRefreshBtn.addEventListener('click', hideRefreshKeyModal); +} + +if (confirmRefreshBtn) { + confirmRefreshBtn.addEventListener('click', refreshKey); +} + +window.showRefreshKeyModal = showRefreshKeyModal; +window.hideRefreshKeyModal = hideRefreshKeyModal; +window.refreshKey = refreshKey; + if (createKeyForm) { createKeyForm.addEventListener('submit', async (event) => { event.preventDefault(); diff --git a/dashboard/public/index.html b/dashboard/public/index.html index 0dff2f3dbfabe960d232125afddcfa0cfb060c76..0c9c913ac2f4bd994b917bf5da40315cefbc6a24 100644 --- a/dashboard/public/index.html +++ b/dashboard/public/index.html @@ -170,6 +170,47 @@ + + + + \ No newline at end of file diff --git a/dashboard/public/style.css b/dashboard/public/style.css index de33bfb52ed0257fb7a1134eaeb6d0cdb0cc62b7..4af92b96575a5d4944a08a18d24eacc99bfedeb1 100644 --- a/dashboard/public/style.css +++ b/dashboard/public/style.css @@ -1,29 +1,31 @@ :root { - /* Colors - Light Theme (Clean & Professional) */ - --bg-body: #f3f4f6; + /* Colors - Light Theme (Modern Minimalist) */ + --bg-body: #fafafa; --bg-card: #ffffff; - --bg-card-hover: #f9fafb; + --bg-card-hover: #f4f4f5; --bg-input: #ffffff; - --border-color: #e5e7eb; - --border-hover: #d1d5db; + --border-color: #e4e4e7; + --border-hover: #d4d4d8; - --primary: #4f46e5; /* Slightly darker indigo for better contrast on white */ - --primary-hover: #4338ca; - --primary-glow: rgba(79, 70, 229, 0.15); + /* Primary: Teal/Emerald (Modern & Elegant) */ + --primary: #0d9488; + --primary-hover: #0f766e; + --primary-glow: rgba(13, 148, 136, 0.15); - --danger: #dc2626; - --danger-hover: #b91c1c; + /* Accents */ + --danger: #ef4444; + --danger-hover: #dc2626; - --success: #059669; - --success-hover: #047857; + --success: #10b981; + --success-hover: #059669; - --text-main: #111827; - --text-muted: #6b7280; + --text-main: #18181b; + --text-muted: #71717a; /* Spacing & Radius */ --radius-sm: 6px; - --radius-md: 12px; + --radius-md: 10px; --radius-lg: 16px; --radius-xl: 24px; @@ -35,33 +37,34 @@ /* Effects */ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); - --glass-blur: blur(12px); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.02); + --glass-blur: blur(16px); --transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } [data-theme="dark"] { - /* Colors - Dark Theme */ + /* Colors - Dark Theme (Slate/Zinc) */ --bg-body: #09090b; - --bg-card: rgba(39, 39, 42, 0.6); - --bg-card-hover: rgba(63, 63, 70, 0.6); - --bg-input: rgba(24, 24, 27, 0.8); + --bg-card: rgba(24, 24, 27, 0.6); + --bg-card-hover: rgba(39, 39, 42, 0.6); + --bg-input: rgba(39, 39, 42, 0.5); --border-color: rgba(255, 255, 255, 0.08); - --border-hover: rgba(255, 255, 255, 0.15); + --border-hover: rgba(255, 255, 255, 0.12); - --primary: #6366f1; - --primary-hover: #4f46e5; - --primary-glow: rgba(99, 102, 241, 0.4); + /* Primary: Lighter Teal for Dark Mode */ + --primary: #14b8a6; + --primary-hover: #2dd4bf; + --primary-glow: rgba(20, 184, 166, 0.25); - --danger: #ef4444; - --danger-hover: #dc2626; + --danger: #f87171; + --danger-hover: #ef4444; - --success: #10b981; - --success-hover: #059669; + --success: #34d399; + --success-hover: #10b981; - --text-main: #fafafa; + --text-main: #f4f4f5; --text-muted: #a1a1aa; --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5); @@ -84,26 +87,41 @@ body { -webkit-font-smoothing: antialiased; } +/* Elegant Background Pattern */ +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + background-image: radial-gradient(circle at 1px 1px, var(--border-color) 1px, transparent 0); + background-size: 40px 40px; + opacity: 0.4; + z-index: -1; +} + [data-theme="dark"] body { background-image: - radial-gradient(circle at 15% 50%, rgba(99, 102, 241, 0.08), transparent 25%), - radial-gradient(circle at 85% 30%, rgba(139, 92, 246, 0.08), transparent 25%); + radial-gradient(circle at 20% 20%, rgba(20, 184, 166, 0.05), transparent 40%), + radial-gradient(circle at 80% 80%, rgba(20, 184, 166, 0.05), transparent 40%); } /* Scrollbar */ ::-webkit-scrollbar { - width: 8px; - height: 8px; + width: 6px; + height: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); - border-radius: 4px; + background: var(--border-color); + border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.2); + background: var(--text-muted); } /* Utilities */ @@ -115,11 +133,11 @@ body { min-height: 100vh; display: flex; flex-direction: column; - animation: fadeIn 0.5s ease-out; + animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1); } @keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } + from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } @@ -131,18 +149,16 @@ body { align-items: center; justify-content: center; padding: var(--space-xl); - background: radial-gradient(circle at center, rgba(99, 102, 241, 0.1) 0%, transparent 70%); } .login-container h1 { font-size: 2.5rem; font-weight: 800; - letter-spacing: -0.05em; + letter-spacing: -0.03em; margin-bottom: var(--space-xs); - background: linear-gradient(135deg, #fff 0%, #a1a1aa 100%); + background: linear-gradient(135deg, var(--text-main) 0%, var(--text-muted) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; - text-shadow: 0 0 30px rgba(99, 102, 241, 0.3); } .login-container h2 { @@ -155,23 +171,23 @@ body { #login-form { background: var(--bg-card); backdrop-filter: var(--glass-blur); - padding: 40px; + padding: 48px; border-radius: var(--radius-xl); border: 1px solid var(--border-color); box-shadow: var(--shadow-lg); width: 100%; - max-width: 420px; + max-width: 400px; transition: var(--transition); } #login-form input { width: 100%; - padding: 16px; + padding: 14px 16px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-md); color: var(--text-main); - font-size: 1rem; + font-size: 0.95rem; margin-bottom: var(--space-lg); transition: var(--transition); } @@ -179,17 +195,17 @@ body { #login-form input:focus { outline: none; border-color: var(--primary); - box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1); + box-shadow: 0 0 0 3px var(--primary-glow); } #login-form button { width: 100%; - padding: 16px; - background: linear-gradient(135deg, var(--primary) 0%, var(--primary-hover) 100%); + padding: 14px; + background: var(--primary); color: white; border: none; border-radius: var(--radius-md); - font-size: 1rem; + font-size: 0.95rem; font-weight: 600; cursor: pointer; transition: var(--transition); @@ -197,15 +213,15 @@ body { } #login-form button:hover { - transform: translateY(-2px); - box-shadow: 0 8px 20px var(--primary-glow); + background: var(--primary-hover); + transform: translateY(-1px); } .error-message { color: var(--danger); margin-top: var(--space-md); text-align: center; - font-size: 0.9rem; + font-size: 0.85rem; min-height: 20px; } @@ -213,17 +229,15 @@ body { margin-top: var(--space-xl); text-align: center; color: var(--text-muted); - font-size: 0.9rem; - background: rgba(255, 255, 255, 0.03); + font-size: 0.85rem; padding: var(--space-md) var(--space-lg); border-radius: var(--radius-lg); - border: 1px solid var(--border-color); } /* Navbar */ .navbar { - background: var(--bg-card); - backdrop-filter: blur(20px); + background: rgba(var(--bg-card), 0.8); + backdrop-filter: blur(12px); padding: 16px 32px; display: flex; justify-content: space-between; @@ -235,10 +249,23 @@ body { } .navbar h1 { - font-size: 1.25rem; + font-size: 1.1rem; font-weight: 700; - letter-spacing: -0.02em; + letter-spacing: -0.01em; color: var(--text-main); + display: flex; + align-items: center; + gap: 12px; +} + +.navbar h1::before { + content: ''; + display: block; + width: 10px; + height: 10px; + background: var(--primary); + border-radius: 50%; + box-shadow: 0 0 10px var(--primary); } .nav-info { @@ -249,7 +276,8 @@ body { #user-info, #admin-user-info { font-size: 0.85rem; - color: var(--text-muted); + font-weight: 500; + color: var(--text-main); background: var(--bg-card); padding: 6px 12px; border-radius: 20px; @@ -258,61 +286,44 @@ body { .logout-btn { padding: 8px 16px; - background: rgba(239, 68, 68, 0.1); - color: var(--danger); - border: 1px solid rgba(239, 68, 68, 0.2); - border-radius: var(--radius-sm); + background: transparent; + color: var(--text-muted); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); cursor: pointer; font-size: 0.85rem; - font-weight: 600; + font-weight: 500; transition: var(--transition); } .logout-btn:hover { - background: rgba(239, 68, 68, 0.2); + background: var(--bg-card-hover); + color: var(--danger); border-color: var(--danger); } /* Theme Toggle */ .theme-toggle { background: transparent; - border: 1px solid var(--border-color); + border: none; color: var(--text-muted); - padding: 6px 16px; - border-radius: var(--radius-md); + padding: 8px; + border-radius: 50%; cursor: pointer; - font-size: 0.85rem; - font-weight: 600; transition: var(--transition); display: flex; align-items: center; justify-content: center; - height: auto; - width: auto; - white-space: nowrap; } .theme-toggle:hover { - background: var(--bg-card-hover); color: var(--text-main); - border-color: var(--border-hover); -} - -[data-theme="light"] .theme-toggle { - background: var(--bg-input); - color: var(--text-main); - border-color: var(--border-color); -} - -[data-theme="light"] .theme-toggle:hover { background: var(--bg-card-hover); - border-color: var(--primary); - color: var(--primary); } /* Main Container */ .container { - max-width: 1200px; + max-width: 1000px; margin: 0 auto; padding: var(--space-xl); width: 100%; @@ -321,7 +332,7 @@ body { /* Stats Grid */ .stats-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-md); margin-bottom: var(--space-xl); } @@ -331,32 +342,29 @@ body { padding: var(--space-lg); border-radius: var(--radius-lg); border: 1px solid var(--border-color); - backdrop-filter: var(--glass-blur); - text-align: center; + text-align: left; transition: var(--transition); } .stat-card:hover { - border-color: var(--border-hover); - transform: translateY(-2px); - background: var(--bg-card-hover); + border-color: var(--primary); + box-shadow: var(--shadow-md); } .stat-card h3 { color: var(--text-muted); - font-size: 0.85rem; + font-size: 0.8rem; + font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; - margin-bottom: var(--space-sm); + margin-bottom: var(--space-xs); } .stat-value { - font-size: 2.5rem; + font-size: 2.2rem; font-weight: 700; color: var(--text-main); - background: linear-gradient(135deg, #fff 0%, #cbd5e1 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; + letter-spacing: -0.03em; } /* Section */ @@ -366,7 +374,7 @@ body { border-radius: var(--radius-lg); border: 1px solid var(--border-color); margin-bottom: var(--space-xl); - backdrop-filter: var(--glass-blur); + box-shadow: var(--shadow-sm); } .section-header { @@ -374,19 +382,19 @@ body { justify-content: space-between; align-items: center; margin-bottom: var(--space-lg); - border-bottom: 1px solid var(--border-color); padding-bottom: var(--space-md); + border-bottom: 1px solid var(--border-color); } .section h2 { - font-size: 1.25rem; + font-size: 1.1rem; font-weight: 600; color: var(--text-main); } /* Buttons */ .btn-primary, .btn-secondary, .btn-danger, .btn-success { - padding: 10px 20px; + padding: 10px 18px; border-radius: var(--radius-md); font-weight: 500; cursor: pointer; @@ -396,46 +404,46 @@ body { align-items: center; justify-content: center; gap: 8px; + border: 1px solid transparent; } .btn-primary { background: var(--primary); color: white; - border: 1px solid transparent; - box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); + box-shadow: 0 2px 8px var(--primary-glow); } .btn-primary:hover { background: var(--primary-hover); - box-shadow: 0 4px 12px rgba(99, 102, 241, 0.5); + transform: translateY(-1px); } .btn-secondary { - background: transparent; + background: var(--bg-input); color: var(--text-main); - border: 1px solid var(--border-color); + border-color: var(--border-color); } .btn-secondary:hover { - background: rgba(255, 255, 255, 0.05); + background: var(--bg-card-hover); border-color: var(--border-hover); } .btn-danger { - background: rgba(239, 68, 68, 0.1); + background: transparent; color: var(--danger); - border: 1px solid rgba(239, 68, 68, 0.2); + border-color: rgba(239, 68, 68, 0.3); } .btn-danger:hover { - background: rgba(239, 68, 68, 0.2); + background: rgba(239, 68, 68, 0.1); border-color: var(--danger); } .btn-success { - background: rgba(16, 185, 129, 0.1); + background: transparent; color: var(--success); - border: 1px solid rgba(16, 185, 129, 0.2); + border-color: rgba(16, 185, 129, 0.3); } .btn-success:hover { - background: rgba(16, 185, 129, 0.2); + background: rgba(16, 185, 129, 0.1); border-color: var(--success); } @@ -448,45 +456,41 @@ body { .group-title { color: var(--text-muted); - font-size: 0.9rem; + font-size: 0.8rem; + font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin: var(--space-lg) 0 var(--space-sm) 0; - padding-bottom: var(--space-xs); - border-bottom: 1px solid var(--border-color); } /* AI Provider Select */ #ai-provider-select { margin-bottom: var(--space-md); - background: var(--bg-input); - color: var(--text-main); - border: 1px solid var(--border-color); } + /* Key Cards */ .key-card { background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-md); - padding: var(--space-lg); + padding: var(--space-md) var(--space-lg); display: flex; align-items: center; transition: var(--transition); } -.key-card.admin { - border-color: rgba(99, 102, 241, 0.3); - background: rgba(99, 102, 241, 0.05); +.key-card:hover { + border-color: var(--border-hover); + background: var(--bg-card-hover); } -[data-theme="light"] .key-card.admin { - background: rgba(99, 102, 241, 0.05); - border-color: rgba(99, 102, 241, 0.2); +.key-card.admin { + border-left: 3px solid var(--primary); } .regular-key-group { border: 1px solid var(--border-color); - border-radius: var(--radius-lg); + border-radius: var(--radius-md); overflow: hidden; background: var(--bg-card); } @@ -505,26 +509,24 @@ body { } .nested-server-keys { - padding: 0 var(--space-lg) var(--space-lg) calc(var(--space-lg) + 30px); + padding: 0 var(--space-lg) var(--space-lg) calc(var(--space-lg) + 24px); border-top: 1px solid var(--border-color); - background: rgba(0, 0, 0, 0.1); -} - -[data-theme="light"] .nested-server-keys { - background: rgba(0, 0, 0, 0.02); + background: var(--bg-card-hover); } .nested-server-keys h4 { color: var(--text-muted); - font-size: 0.8rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; margin: var(--space-md) 0; - opacity: 0.7; + opacity: 0.8; } .key-card.nested { padding: var(--space-md); background: var(--bg-card); - border-left: 2px solid var(--primary); + border: 1px solid var(--border-color); } .toggle-icon-container { @@ -534,25 +536,15 @@ body { align-items: center; justify-content: center; margin-right: var(--space-md); - background: rgba(255, 255, 255, 0.05); + background: var(--bg-input); border-radius: 50%; -} - -[data-theme="light"] .toggle-icon-container { - background: rgba(0, 0, 0, 0.05); + border: 1px solid var(--border-color); } .toggle-icon { - font-size: 0.8rem; + font-size: 0.7rem; color: var(--text-muted); - transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); - /* Fix alignment */ - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - margin-top: 2px; /* Visual correction */ + transition: transform 0.2s; } .key-info { @@ -562,33 +554,35 @@ body { .key-info h3 { display: flex; align-items: center; - font-size: 1rem; - margin-bottom: var(--space-xs); + font-size: 0.95rem; + font-weight: 600; + margin-bottom: 4px; color: var(--text-main); } .key-info p { color: var(--text-muted); - font-size: 0.85rem; + font-size: 0.8rem; margin-bottom: 2px; font-family: 'JetBrains Mono', monospace; } .key-badge { padding: 2px 8px; - border-radius: 4px; + border-radius: 12px; font-size: 0.7rem; font-weight: 600; margin-right: var(--space-sm); text-transform: uppercase; + letter-spacing: 0.02em; } -.key-badge.admin { background: rgba(139, 92, 246, 0.2); color: #c4b5fd; border: 1px solid rgba(139, 92, 246, 0.3); } -.key-badge.regular { background: rgba(59, 130, 246, 0.2); color: #93c5fd; border: 1px solid rgba(59, 130, 246, 0.3); } -.key-badge.server { background: rgba(16, 185, 129, 0.2); color: #6ee7b7; border: 1px solid rgba(16, 185, 129, 0.3); } +.key-badge.admin { background: var(--primary); color: white; } +.key-badge.regular { background: var(--bg-input); color: var(--text-main); border: 1px solid var(--border-color); } +.key-badge.server { background: transparent; color: var(--text-muted); border: 1px solid var(--border-color); } -.key-badge.active { background: rgba(16, 185, 129, 0.2); color: #34d399; } -.key-badge.inactive { background: rgba(239, 68, 68, 0.2); color: #f87171; } +.key-badge.active { color: var(--success); background: rgba(16, 185, 129, 0.1); } +.key-badge.inactive { color: var(--text-muted); background: var(--bg-input); } .key-actions { display: flex; @@ -603,10 +597,10 @@ body { label { display: block; - margin-bottom: var(--space-xs); + margin-bottom: 6px; font-weight: 500; color: var(--text-main); - font-size: 0.9rem; + font-size: 0.85rem; } input[type="text"], @@ -614,25 +608,25 @@ input[type="password"], textarea, select { width: 100%; - padding: 12px; + padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-md); color: var(--text-main); font-family: inherit; - font-size: 0.95rem; + font-size: 0.9rem; transition: var(--transition); } input:focus, textarea:focus, select:focus { outline: none; border-color: var(--primary); - box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); + box-shadow: 0 0 0 3px var(--primary-glow); } .hint { display: block; - margin-top: var(--space-xs); + margin-top: 6px; font-size: 0.8rem; color: var(--text-muted); } @@ -641,39 +635,48 @@ input:focus, textarea:focus, select:focus { .events-list { max-height: 400px; overflow-y: auto; - background: rgba(0, 0, 0, 0.2); + background: var(--bg-input); + border: 1px solid var(--border-color); border-radius: var(--radius-md); - padding: var(--space-sm); + padding: 0; } .event-item { - padding: var(--space-md); - border-left: 3px solid var(--primary); - background: rgba(255, 255, 255, 0.02); - margin-bottom: var(--space-sm); - border-radius: 0 var(--radius-sm) var(--radius-sm) 0; - font-size: 0.9rem; + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); + border-left: 3px solid transparent; + transition: var(--transition); +} + +.event-item:last-child { + border-bottom: none; +} + +.event-item:hover { + background: var(--bg-card-hover); + border-left-color: var(--primary); } .event-item strong { - color: var(--primary); + color: var(--text-main); + font-weight: 600; } .event-item pre { - background: rgba(0, 0, 0, 0.3); - padding: var(--space-sm); + background: var(--bg-body); + padding: 10px; border-radius: var(--radius-sm); - margin-top: var(--space-xs); + margin-top: 8px; color: var(--text-muted); font-size: 0.8rem; - overflow-x: auto; + border: 1px solid var(--border-color); } /* Command Console */ .command-history { height: 300px; overflow-y: auto; - background: #000; + background: #0f172a; /* Always dark for terminal feel */ border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: var(--space-md); @@ -682,17 +685,17 @@ input:focus, textarea:focus, select:focus { } .command-item { - margin-bottom: 6px; - padding-bottom: 6px; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); color: #e2e8f0; - font-size: 0.9rem; + font-size: 0.85rem; } .command-item .timestamp { color: #64748b; font-size: 0.75rem; - margin-top: 2px; + margin-top: 4px; } .command-form { @@ -707,8 +710,8 @@ input:focus, textarea:focus, select:focus { left: 0; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(8px); + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(4px); display: flex; justify-content: center; align-items: center; @@ -718,29 +721,57 @@ input:focus, textarea:focus, select:focus { .modal-content { background: var(--bg-card); - padding: var(--space-xl); + padding: 32px; border-radius: var(--radius-xl); border: 1px solid var(--border-color); - width: 90%; - max-width: 500px; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5); - color: var(--text-main); + box-shadow: var(--shadow-lg); } .modal-actions { - display: flex; - justify-content: flex-end; - gap: var(--space-sm); - margin-top: var(--space-xl); + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid var(--border-color); } /* AI Status */ .ai-status { margin-top: var(--space-md); - padding: var(--space-md); + padding: 12px 16px; border-radius: var(--radius-md); - font-size: 0.9rem; + font-size: 0.85rem; + display: flex; + align-items: center; + gap: 8px; +} +.ai-status::before { + content: ''; + display: block; + width: 8px; + height: 8px; + border-radius: 50%; } .ai-status.success { background: rgba(16, 185, 129, 0.1); color: var(--success); border: 1px solid rgba(16, 185, 129, 0.2); } +.ai-status.success::before { background: var(--success); } + .ai-status.error { background: rgba(239, 68, 68, 0.1); color: var(--danger); border: 1px solid rgba(239, 68, 68, 0.2); } -.ai-status.info { background: rgba(99, 102, 241, 0.1); color: var(--primary); border: 1px solid rgba(99, 102, 241, 0.2); } +.ai-status.error::before { background: var(--danger); } + +.ai-status.info { background: rgba(13, 148, 136, 0.1); color: var(--primary); border: 1px solid rgba(13, 148, 136, 0.2); } +.ai-status.info::before { background: var(--primary); } + +.modal-parent-info { + color: var(--primary); + font-size: 0.85rem; + margin-bottom: var(--space-lg); + padding: 12px; + background: var(--primary-glow); + border: 1px solid transparent; + border-radius: var(--radius-md); + font-weight: 500; +} + +.modal-parent-info.danger-bg { + color: var(--danger); + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.2); +} \ No newline at end of file diff --git a/docs/API.md b/docs/API.md index a1d2f98f189da79fe2d88cfadfb40b9d59498b8f..1baa1a36000ae299c4aaf35cc1de9bbb54f64425 100644 --- a/docs/API.md +++ b/docs/API.md @@ -290,7 +290,7 @@ Authorization: Bearer 为指定的 Regular Key 创建新的 Server Key。 -**权限**: Admin Key +**权限**: Admin Key(仅限管理员,Regular Key 不能为自己创建 Server Key) **请求** @@ -336,6 +336,33 @@ Content-Type: application/json } ``` +**错误响应** `400 Bad Request` + +```json +{ + "detail": "Name is required" +} +``` + +```json +{ + "detail": "regular_key_id is required" +} +``` + +**错误响应** `404 Not Found` + +```json +{ + "detail": "Invalid Regular Key ID" +} +``` + +> ⚠️ **重要**: +> - 只有 Admin Key 可以为 Regular Key 创建额外的 Server Key +> - Regular Key 不能为自己创建或删除 Server Key +> - `key` 字段只在创建时返回一次,请妥善保存! + --- ### 激活 API Key diff --git a/docs/openapi.json b/docs/openapi.json new file mode 100644 index 0000000000000000000000000000000000000000..962ffcd7f52e9452015bf35bc6d35325a0c69ac4 --- /dev/null +++ b/docs/openapi.json @@ -0,0 +1,930 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "InterConnect-Server API", + "version": "1.0.0", + "description": "InterConnect-Server 是一个 Minecraft WebSocket API 服务器,提供 API Key 管理、事件广播、服务器命令等功能。\n\n权限说明:\n- **Admin Key**: `mc_admin_` 前缀,拥有完整管理权限。\n- **Regular Key**: `mc_key_` 前缀,可查看/管理关联的 Server Key,发送服务器命令。\n- **Server Key**: `mc_server_` 前缀,仅用于插件/Mod 配置,用于认证和事件上报。" + }, + "servers": [ + { + "url": "http://localhost:8000", + "description": "本地服务器" + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "API Key", + "description": "使用 API Key 进行认证 (Bearer Token)" + } + }, + "schemas": { + "ApiKey": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "keyPrefix": { + "type": "string", + "enum": ["mc_admin_", "mc_key_", "mc_server_"] + }, + "keyType": { + "type": "string", + "enum": ["admin", "regular", "server"] + }, + "serverId": { + "type": "string", + "nullable": true + }, + "regularKeyId": { + "type": "string", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "lastUsed": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "isActive": { + "type": "boolean" + } + } + }, + "ApiKeyWithSecret": { + "allOf": [ + { + "$ref": "#/components/schemas/ApiKey" + }, + { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "原始 API Key,仅在创建时返回一次" + } + } + } + ] + }, + "CreateKeyRequest": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "key_type": { + "type": "string", + "enum": ["regular", "admin"], + "default": "regular" + }, + "server_id": { + "type": "string" + } + } + }, + "CreateServerKeyRequest": { + "type": "object", + "required": ["name", "regular_key_id"], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "server_id": { + "type": "string" + }, + "regular_key_id": { + "type": "string", + "description": "关联的 Regular Key ID" + } + } + }, + "Event": { + "type": "object", + "required": ["event_type", "server_name", "timestamp", "data"], + "properties": { + "event_type": { + "type": "string", + "description": "事件类型 (e.g., player_join, player_leave, message, ai_chat)" + }, + "server_name": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "data": { + "type": "object", + "description": "事件具体数据" + } + } + }, + "CommandRequest": { + "type": "object", + "required": ["command"], + "properties": { + "command": { + "type": "string" + }, + "server_id": { + "type": "string", + "description": "仅 Admin Key 需要/可以指定" + } + } + }, + "AiConfig": { + "type": "object", + "properties": { + "apiUrl": { + "type": "string" + }, + "modelId": { + "type": "string" + }, + "apiKey": { + "type": "string", + "description": "部分隐藏的 API Key" + }, + "enabled": { + "type": "boolean" + }, + "systemPrompt": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "AiConfigUpdateRequest": { + "type": "object", + "properties": { + "api_url": { + "type": "string" + }, + "model_id": { + "type": "string" + }, + "api_key": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "system_prompt": { + "type": "string" + } + } + }, + "AiChatRequest": { + "type": "object", + "required": ["message"], + "properties": { + "message": { + "type": "string" + }, + "player_name": { + "type": "string" + }, + "server_id": { + "type": "string" + } + } + }, + "ErrorResponse": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + } + } + } + }, + "paths": { + "/health": { + "get": { + "summary": "健康检查", + "description": "获取服务器状态和统计信息", + "security": [], + "responses": { + "200": { + "description": "服务器正常", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "active_ws": { + "type": "integer" + }, + "keys_total": { + "type": "integer" + }, + "admin_active": { + "type": "integer" + }, + "server_active": { + "type": "integer" + }, + "regular_active": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "/manage/keys": { + "get": { + "summary": "获取所有 API Key", + "description": "获取所有 API Key 列表 (仅 Admin)", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKey" + } + } + } + } + }, + "403": { + "description": "权限不足", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "summary": "创建 API Key", + "description": "创建新的 Admin 或 Regular Key (创建 Regular Key 会自动附带一个 Server Key)", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateKeyRequest" + } + } + } + }, + "responses": { + "201": { + "description": "创建成功", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiKeyWithSecret" + }, + { + "type": "object", + "properties": { + "regularKey": { + "$ref": "#/components/schemas/ApiKeyWithSecret" + }, + "serverKey": { + "$ref": "#/components/schemas/ApiKeyWithSecret" + } + } + } + ] + } + } + } + } + } + } + }, + "/manage/keys/{key_id}": { + "get": { + "summary": "获取 API Key 详情", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "key_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKey" + } + } + } + }, + "404": { + "description": "未找到", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "summary": "删除 API Key", + "description": "仅 Admin 可操作,不能删除自己", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "key_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "删除成功" + }, + "400": { + "description": "无法删除(如试图删除自己)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/manage/keys/{key_id}/activate": { + "patch": { + "summary": "激活 API Key", + "description": "Admin 可激活任意 Key,Regular 仅可激活关联的 Server Key", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "key_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/manage/keys/{key_id}/deactivate": { + "patch": { + "summary": "停用 API Key", + "description": "Admin 可停用任意 Key,Regular 仅可停用关联的 Server Key", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "key_id", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/manage/keys/server-keys": { + "get": { + "summary": "获取 Server Key 列表", + "description": "Admin 获取所有 Server Key,Regular 获取关联的 Server Key", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKey" + } + } + } + } + } + } + }, + "post": { + "summary": "创建 Server Key", + "description": "为指定的 Regular Key 创建新的 Server Key (仅 Admin,Regular Key 不能为自己创建)", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateServerKeyRequest" + } + } + } + }, + "responses": { + "201": { + "description": "创建成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiKeyWithSecret" + } + } + } + }, + "400": { + "description": "参数错误", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Regular Key 不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/events": { + "post": { + "summary": "发送事件", + "description": "发送 Minecraft 事件并广播给所有 WebSocket 连接", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + } + }, + "responses": { + "200": { + "description": "成功广播", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/server/info": { + "get": { + "summary": "获取服务器信息", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "server_id", + "schema": { + "type": "string" + }, + "description": "仅 Admin 需要" + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "server_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "online_players": { + "type": "integer" + } + } + } + } + } + } + } + } + }, + "/api/server/command": { + "post": { + "summary": "发送服务器命令", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommandRequest" + } + } + } + }, + "responses": { + "200": { + "description": "命令发送成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "403": { + "description": "禁止的命令", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/ai/config": { + "get": { + "summary": "获取 AI 配置 (Admin)", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AiConfig" + } + } + } + }, + "404": { + "description": "配置不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "summary": "创建 AI 配置 (Admin)", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AiConfigUpdateRequest" + } + } + } + }, + "responses": { + "201": { + "description": "创建成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AiConfig" + } + } + } + } + } + }, + "patch": { + "summary": "更新 AI 配置 (Admin)", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AiConfigUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "更新成功", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AiConfig" + } + } + } + } + } + }, + "delete": { + "summary": "删除 AI 配置 (Admin)", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "204": { + "description": "删除成功" + } + } + } + }, + "/api/ai/config/test": { + "post": { + "summary": "测试 AI 连接 (Admin)", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "连接成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "model": { + "type": "string" + }, + "response": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/api/ai/chat": { + "post": { + "summary": "AI 聊天", + "description": "发送消息给 AI 并获取回复", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AiChatRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "reply": { + "type": "string" + }, + "model": { + "type": "string" + }, + "usage": { + "type": "object", + "properties": { + "prompt_tokens": { + "type": "integer" + }, + "completion_tokens": { + "type": "integer" + }, + "total_tokens": { + "type": "integer" + } + } + } + } + } + } + } + }, + "403": { + "description": "AI 功能已禁用", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "AI 配置不存在", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + } + } +} diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9eab028db9ae8878f13fb751f1e35cd47d9b4817 --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,512 @@ +openapi: 3.0.0 +info: + title: InterConnect-Server API + version: 1.0.0 + description: | + InterConnect-Server 是一个 Minecraft WebSocket API 服务器,提供 API Key 管理、事件广播、服务器命令等功能。 + + 权限说明: + - **Admin Key**: `mc_admin_` 前缀,拥有完整管理权限。 + - **Regular Key**: `mc_key_` 前缀,可查看/管理关联的 Server Key,发送服务器命令。 + - **Server Key**: `mc_server_` 前缀,仅用于插件/Mod 配置,用于认证和事件上报。 + +servers: + - url: http://localhost:8000 + description: 本地服务器 + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: API Key + description: 使用 API Key 进行认证 (Bearer Token) + + schemas: + ApiKey: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + description: + type: string + keyPrefix: + type: string + enum: [mc_admin_, mc_key_, mc_server_] + keyType: + type: string + enum: [admin, regular, server] + serverId: + type: string + nullable: true + regularKeyId: + type: string + nullable: true + createdAt: + type: string + format: date-time + lastUsed: + type: string + format: date-time + nullable: true + isActive: + type: boolean + + ApiKeyWithSecret: + allOf: + - $ref: '#/components/schemas/ApiKey' + - type: object + properties: + key: + type: string + description: 原始 API Key,仅在创建时返回一次 + + CreateKeyRequest: + type: object + required: [name] + properties: + name: + type: string + description: + type: string + key_type: + type: string + enum: [regular, admin] + default: regular + server_id: + type: string + + CreateServerKeyRequest: + type: object + required: [name, regular_key_id] + properties: + name: + type: string + description: + type: string + server_id: + type: string + regular_key_id: + type: string + + Event: + type: object + required: [event_type, server_name, timestamp, data] + properties: + event_type: + type: string + description: 事件类型 (e.g., player_join, player_leave, message, ai_chat) + server_name: + type: string + timestamp: + type: string + format: date-time + data: + type: object + description: 事件具体数据 + + CommandRequest: + type: object + required: [command] + properties: + command: + type: string + server_id: + type: string + description: 仅 Admin Key 需要/可以指定 + + AiConfig: + type: object + properties: + apiUrl: + type: string + modelId: + type: string + apiKey: + type: string + description: 部分隐藏的 API Key + enabled: + type: boolean + systemPrompt: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + + AiConfigUpdateRequest: + type: object + properties: + api_url: + type: string + model_id: + type: string + api_key: + type: string + enabled: + type: boolean + system_prompt: + type: string + + AiChatRequest: + type: object + required: [message] + properties: + message: + type: string + player_name: + type: string + server_id: + type: string + +paths: + /health: + get: + summary: 健康检查 + description: 获取服务器状态和统计信息 + security: [] + responses: + '200': + description: 服务器正常 + content: + application/json: + schema: + type: object + properties: + status: + type: string + timestamp: + type: string + active_ws: + type: integer + keys_total: + type: integer + admin_active: + type: integer + server_active: + type: integer + regular_active: + type: integer + + /manage/keys: + get: + summary: 获取所有 API Key + description: 获取所有 API Key 列表 (仅 Admin) + security: + - bearerAuth: [] + responses: + '200': + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ApiKey' + '403': + description: 权限不足 + + post: + summary: 创建 API Key + description: 创建新的 Admin 或 Regular Key (创建 Regular Key 会自动附带一个 Server Key) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateKeyRequest' + responses: + '201': + description: 创建成功 + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ApiKeyWithSecret' + - type: object + properties: + regularKey: + $ref: '#/components/schemas/ApiKeyWithSecret' + serverKey: + $ref: '#/components/schemas/ApiKeyWithSecret' + + /manage/keys/{key_id}: + get: + summary: 获取 API Key 详情 + security: + - bearerAuth: [] + parameters: + - in: path + name: key_id + schema: + type: string + required: true + responses: + '200': + description: 成功 + content: + application/json: + schema: + $ref: '#/components/schemas/ApiKey' + '404': + description: 未找到 + + delete: + summary: 删除 API Key + description: 仅 Admin 可操作,不能删除自己 + security: + - bearerAuth: [] + parameters: + - in: path + name: key_id + schema: + type: string + required: true + responses: + '204': + description: 删除成功 + '400': + description: 无法删除(如试图删除自己) + + /manage/keys/{key_id}/activate: + patch: + summary: 激活 API Key + description: Admin 可激活任意 Key,Regular 仅可激活关联的 Server Key + security: + - bearerAuth: [] + parameters: + - in: path + name: key_id + schema: + type: string + required: true + responses: + '200': + description: 成功 + content: + application/json: + schema: + type: object + properties: + message: + type: string + + /manage/keys/{key_id}/deactivate: + patch: + summary: 停用 API Key + description: Admin 可停用任意 Key,Regular 仅可停用关联的 Server Key + security: + - bearerAuth: [] + parameters: + - in: path + name: key_id + schema: + type: string + required: true + responses: + '200': + description: 成功 + content: + application/json: + schema: + type: object + properties: + message: + type: string + + /manage/keys/server-keys: + get: + summary: 获取 Server Key 列表 + description: Admin 获取所有 Server Key,Regular 获取关联的 Server Key + security: + - bearerAuth: [] + responses: + '200': + description: 成功 + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ApiKey' + + post: + summary: 创建 Server Key + description: 为指定的 Regular Key 创建新的 Server Key (仅 Admin) + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateServerKeyRequest' + responses: + '201': + description: 创建成功 + content: + application/json: + schema: + $ref: '#/components/schemas/ApiKeyWithSecret' + '400': + description: 参数错误 + '404': + description: Regular Key 不存在 + + /api/events: + post: + summary: 发送事件 + description: 发送 Minecraft 事件并广播给所有 WebSocket 连接 + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Event' + responses: + '200': + description: 成功广播 + + /api/server/info: + get: + summary: 获取服务器信息 + security: + - bearerAuth: [] + parameters: + - in: query + name: server_id + schema: + type: string + description: 仅 Admin 需要 + responses: + '200': + description: 成功 + content: + application/json: + schema: + type: object + properties: + server_id: + type: string + status: + type: string + online_players: + type: integer + + /api/server/command: + post: + summary: 发送服务器命令 + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CommandRequest' + responses: + '200': + description: 命令发送成功 + '403': + description: 禁止的命令 + + /api/ai/config: + get: + summary: 获取 AI 配置 (Admin) + security: + - bearerAuth: [] + responses: + '200': + description: 成功 + content: + application/json: + schema: + $ref: '#/components/schemas/AiConfig' + + post: + summary: 创建 AI 配置 (Admin) + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AiConfigUpdateRequest' + responses: + '201': + description: 创建成功 + + patch: + summary: 更新 AI 配置 (Admin) + security: + - bearerAuth: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AiConfigUpdateRequest' + responses: + '200': + description: 更新成功 + + delete: + summary: 删除 AI 配置 (Admin) + security: + - bearerAuth: [] + responses: + '204': + description: 删除成功 + + /api/ai/config/test: + post: + summary: 测试 AI 连接 (Admin) + security: + - bearerAuth: [] + responses: + '200': + description: 连接成功 + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + message: + type: string + + /api/ai/chat: + post: + summary: AI 聊天 + description: 发送消息给 AI 并获取回复 + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AiChatRequest' + responses: + '200': + description: 成功 + content: + application/json: + schema: + type: object + properties: + success: + type: boolean + reply: + type: string + model: + type: string diff --git a/hf_repo/docs/TEST_REPORT.md b/hf_repo/docs/TEST_REPORT.md new file mode 100644 index 0000000000000000000000000000000000000000..8b591c4c0bbdfdc6c4751ceb624c1c9af5d823b6 --- /dev/null +++ b/hf_repo/docs/TEST_REPORT.md @@ -0,0 +1,69 @@ +# InterConnect-Server 测试总结报告 + +## 1. 测试概览 + +本项目已完成全面的自动化测试套件构建,覆盖了核心业务逻辑、数据库操作、认证机制以及 API 接口。 + +- **测试框架**: Jest +- **API 测试工具**: Supertest +- **测试结果**: ✅ 全部通过 (30/30 测试用例) +- **测试时间**: 2026-01-23 + +## 2. 测试覆盖范围 + +测试套件分为单元测试 (Unit Tests) 和集成测试 (Integration Tests) 两部分。 + +### 2.1 单元测试 (Unit Tests) + +针对各个独立模块进行的功能验证。 + +#### 认证模块 (`src/auth.js`) +- **测试文件**: `tests/unit/auth.test.js` +- **覆盖内容**: + - `verifyApiKey`: 验证 API Key 的提取、校验和错误处理(401 Unauthorized)。 + - `requireAdminKey`: 验证管理员权限控制。 + - `requireRegularOrAdminKey`: 验证普通用户与管理员的权限兼容性。 + - `requireAnyKey`: 验证基础访问权限。 + +#### 数据库模块 (`src/database.js`) +- **测试文件**: `tests/unit/database.test.js` +- **覆盖内容**: + - **初始化**: 验证数据库表结构 (`api_keys`, `event_logs`, `ai_config`) 的自动创建。 + - **密钥管理**: 验证 Admin Key 的初始生成、Regular Key 和 Server Key 的创建与关联。 + - **验证逻辑**: 验证 `verifyApiKey` 的准确性。 + - **事件日志**: 验证 `logEvent` 和 `getRecentEvents` 的读写功能。 + - **AI 配置**: 验证 AI 配置信息的保存、读取和更新。 + +#### WebSocket 管理器 (`src/websocket.js`) +- **测试文件**: `tests/unit/websocket.test.js` +- **覆盖内容**: + - **连接管理**: 验证连接跟踪 (`connect`) 和断开处理 (`disconnect`)。 + - **广播功能**: 验证 `broadcastToAll` 能正确向所有活跃连接发送消息。 + - **异常处理**: 验证在广播过程中自动清理已关闭的连接。 + +### 2.2 集成测试 (Integration Tests) + +针对 HTTP API 接口的端到端测试。 + +#### API 接口 (`src/routes/*.js`) +- **测试文件**: `tests/integration/api.test.js` +- **覆盖内容**: + - **基础端点**: + - `GET /`: 验证服务器欢迎信息。 + - `GET /health`: 验证健康检查接口返回 `healthy` 状态。 + - **受保护路由**: + - `POST /api/events`: 验证无 Key、无效 Key 和有效 Key 的访问控制及业务逻辑。 + - **密钥管理 API**: + - `GET /manage/keys`: 验证 Admin 只有权访问。 + - `POST /manage/keys`: 验证密钥创建流程。 + +## 3. 测试环境配置 + +为了支持测试,对项目进行了以下配置(现已清理): +1. **依赖**: 安装了 `jest` 和 `supertest`。 +2. **配置**: 添加了 `jest.config.js` 和 `tests/setup.js`。 +3. **代码调整**: `src/server.js` 采用了条件启动模式,允许测试框架导入 App 实例而不自动监听端口。 + +## 4. 结论 + +当前代码库的核心功能稳定,权限控制逻辑严密,数据库操作符合预期。所有测试用例均已通过,可以放心地进行部署或后续开发。 diff --git a/hf_repo/hf_repo/.gitignore b/hf_repo/hf_repo/.gitignore index dc1638545f197128dcb850f5ed0791d2133e1af1..01699a3f6bf13d32c00b827f7a769ac49f205d54 100644 --- a/hf_repo/hf_repo/.gitignore +++ b/hf_repo/hf_repo/.gitignore @@ -57,3 +57,4 @@ cli/*.log *.dockerfile +ICS-测试数据.txt diff --git a/hf_repo/hf_repo/hf_repo/cli/cli.js b/hf_repo/hf_repo/hf_repo/cli/cli.js index cb79ee0985ac9181dee33ed26142fda018b0ea76..868be113d5a52cdebb09ca59005c3b7ac36b3261 100644 --- a/hf_repo/hf_repo/hf_repo/cli/cli.js +++ b/hf_repo/hf_repo/hf_repo/cli/cli.js @@ -111,8 +111,10 @@ class MinecraftWSCLIClient { return true; } - async healthCheck() { - return await this.request('GET', '/health'); + async resetAIConfig() { + this.ensureAdminKeyForManagement(); + await this.request('DELETE', '/api/ai/config'); + return true; } } @@ -125,6 +127,22 @@ program .option('-s, --server-url ', 'API服务器URL', process.env.MC_WS_API_URL || HARDCODED_API_URL) .option('-k, --admin-key ', '用于管理操作的Admin Key', process.env.ADMIN_KEY || HARDCODED_ADMIN_KEY); +program + .command('reset-ai-config') + .description('重置/清除 AI 配置 (使用 Admin Key)') + .action(async () => { + try { + const opts = program.opts(); + const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey); + await client.resetAIConfig(); + console.log(`\x1b[32m✅ AI 配置已成功重置/清除!\x1b[0m`); + console.log('现在您可以重新在 Dashboard 配置 AI 设置。'); + } catch (error) { + console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`); + process.exit(1); + } + }); + program .command('create-key ') .description('创建新的API密钥') diff --git a/hf_repo/hf_repo/hf_repo/dashboard/public/app.js b/hf_repo/hf_repo/hf_repo/dashboard/public/app.js index 4948be20bae648f050142e5d62f18c1cd66e8a37..7d598555193c2c907b24f3bfe922136cc80813fd 100644 --- a/hf_repo/hf_repo/hf_repo/dashboard/public/app.js +++ b/hf_repo/hf_repo/hf_repo/dashboard/public/app.js @@ -35,8 +35,83 @@ const aiSystemPromptInput = document.getElementById('ai-system-prompt'); const aiEnabledCheckbox = document.getElementById('ai-enabled'); const aiTestBtn = document.getElementById('ai-test-btn'); const aiDeleteBtn = document.getElementById('ai-delete-btn'); +const aiProviderSelect = document.getElementById('ai-provider-select'); +const systemLogs = document.getElementById('system-logs'); +const clearLogsBtn = document.getElementById('clear-logs-btn'); + +// AI Providers Configuration +const AI_PROVIDERS = { + openai: { + url: 'https://api.openai.com/v1/chat/completions', + model: 'gpt-3.5-turbo' + }, + siliconflow: { + url: 'https://api.siliconflow.cn/v1/chat/completions', + model: 'deepseek-ai/DeepSeek-R1' + }, + gemini: { + url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', + model: 'gemini-2.0-flash-exp' + }, + deepseek: { + url: 'https://api.deepseek.com/chat/completions', + model: 'deepseek-chat' + }, + moonshot: { + url: 'https://api.moonshot.cn/v1/chat/completions', + model: 'moonshot-v1-8k' + }, + custom: { + url: '', + model: '' + } +}; + +if (aiProviderSelect) { + aiProviderSelect.addEventListener('change', (e) => { + const provider = AI_PROVIDERS[e.target.value]; + if (provider && e.target.value !== 'custom') { + if (aiApiUrlInput) aiApiUrlInput.value = provider.url; + if (aiModelIdInput) aiModelIdInput.value = provider.model; + } + }); +} + const aiStatus = document.getElementById('ai-status'); +// Theme Management +const themeToggleBtns = document.querySelectorAll('.theme-toggle'); + +function initTheme() { + const savedTheme = localStorage.getItem('theme') || 'dark'; + document.documentElement.setAttribute('data-theme', savedTheme); + updateThemeIcons(savedTheme); +} + +function toggleTheme() { + const currentTheme = document.documentElement.getAttribute('data-theme'); + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + + document.documentElement.setAttribute('data-theme', newTheme); + localStorage.setItem('theme', newTheme); + updateThemeIcons(newTheme); +} + +function updateThemeIcons(theme) { + const text = theme === 'dark' ? '浅色模式' : '深色模式'; + + themeToggleBtns.forEach(btn => { + btn.textContent = text; + }); +} + +// Initialize Theme +initTheme(); + +themeToggleBtns.forEach(btn => { + btn.addEventListener('click', toggleTheme); +}); + function authHeaders(key) { return { Authorization: `Bearer ${key}` }; } @@ -172,33 +247,102 @@ function renderAdminKeys(keys) { return; } - adminKeysList.innerHTML = keys.map((key) => ` -
+ const adminKeys = keys.filter(k => k.keyType === 'admin'); + const regularKeys = keys.filter(k => k.keyType === 'regular'); + const serverKeys = keys.filter(k => k.keyType === 'server'); + const serverKeysMap = {}; + + serverKeys.forEach(key => { + if (key.regularKeyId) { + if (!serverKeysMap[key.regularKeyId]) { + serverKeysMap[key.regularKeyId] = []; + } + serverKeysMap[key.regularKeyId].push(key); + } + }); + + let html = ''; + + // Render Admin Keys + if (adminKeys.length > 0) { + html += '

管理员密钥 (Admin Keys)

'; + html += adminKeys.map(key => renderKeyCard(key)).join(''); + } + + // Render Regular Keys with nested Server Keys + if (regularKeys.length > 0) { + html += '

用户密钥 (Regular Keys)

'; + html += regularKeys.map(regularKey => { + const childServerKeys = serverKeysMap[regularKey.id] || []; + return ` +
+
+ ${renderKeyCard(regularKey)} + ${childServerKeys.length > 0 ? `` : ''} +
+ ${childServerKeys.length > 0 ? ` + + ` : ''} +
+ `; + }).join(''); + } + + // Render Orphaned Server Keys (if any) + const linkedServerKeyIds = new Set(Object.values(serverKeysMap).flat().map(k => k.id)); + const orphanServerKeys = serverKeys.filter(k => !linkedServerKeyIds.has(k.id)); + + if (orphanServerKeys.length > 0) { + html += '

独立服务器密钥 (Orphan Server Keys)

'; + html += orphanServerKeys.map(key => renderKeyCard(key)).join(''); + } + + adminKeysList.innerHTML = html; +} + +function toggleGroup(headerElement) { + const nestedContainer = headerElement.nextElementSibling; + const toggleIcon = headerElement.querySelector('.toggle-icon'); + + if (nestedContainer && nestedContainer.classList.contains('nested-server-keys')) { + nestedContainer.classList.toggle('hidden'); + if (toggleIcon) { + toggleIcon.style.transform = nestedContainer.classList.contains('hidden') ? 'rotate(0deg)' : 'rotate(180deg)'; + } + } +} + +function renderKeyCard(key, isNested = false) { + return ` +

${key.keyType === 'admin' ? 'Admin' : key.keyType === 'server' ? 'Server' : 'Regular'} - ${key.isActive ? 'Active' : 'Inactive'} + ${key.isActive ? '已启用' : '已停用'} ${key.name}

ID: ${key.id}

Prefix: ${key.keyPrefix}

${key.serverId ? `

Server ID: ${key.serverId}

` : ''} -

Created: ${new Date(key.createdAt).toLocaleString()}

-

Last Used: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : 'Never'}

+

创建时间: ${new Date(key.createdAt).toLocaleString()}

+

最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : '从未'}

${key.isActive - ? `` - : `` + ? `` + : `` } - +
- `).join(''); + `; } function renderUserServerKeys(keys) { @@ -217,20 +361,20 @@ function renderUserServerKeys(keys) {

Server - ${key.isActive ? 'Active' : 'Inactive'} + ${key.isActive ? '已启用' : '已停用'} ${key.name}

ID: ${key.id}

Prefix: ${key.keyPrefix}

${key.serverId ? `

Server ID: ${key.serverId}

` : ''} -

Created: ${new Date(key.createdAt).toLocaleString()}

-

Last Used: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : 'Never'}

+

创建时间: ${new Date(key.createdAt).toLocaleString()}

+

最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : '从未'}

${key.isActive - ? `` - : `` + ? `` + : `` }
@@ -293,7 +437,7 @@ async function deleteKey(keyId, keyName) { if (currentRole !== 'admin') { return; } - if (!confirm(`Delete key "${keyName}"? This action cannot be undone.`)) { + if (!confirm(`确定要删除密钥 "${keyName}" 吗? 此操作无法撤销。`)) { return; } try { @@ -365,21 +509,6 @@ async function loadStats() { connections.textContent = data.active_ws || 0; } - const totalKeys = document.getElementById('user-stat-total-keys'); - if (totalKeys) { - totalKeys.textContent = data.keys_total || 0; - } - - const adminKeys = document.getElementById('user-stat-admin-keys'); - if (adminKeys) { - adminKeys.textContent = data.admin_active || 0; - } - - const regularKeys = document.getElementById('user-stat-regular-keys'); - if (regularKeys) { - regularKeys.textContent = data.regular_active || 0; - } - const serverKeys = document.getElementById('user-stat-server-keys'); if (serverKeys) { serverKeys.textContent = data.server_active || 0; @@ -737,7 +866,7 @@ async function deleteAIConfig() { if (!apiKey) { return; } - if (!confirm('Are you sure you want to delete the AI configuration?')) { + if (!confirm('确定要删除 AI 配置吗?')) { return; } diff --git a/hf_repo/hf_repo/hf_repo/dashboard/public/index.html b/hf_repo/hf_repo/hf_repo/dashboard/public/index.html index ca06a8a7cbaf41bfd8bed0674ca4fea03c984cfe..0dff2f3dbfabe960d232125afddcfa0cfb060c76 100644 --- a/hf_repo/hf_repo/hf_repo/dashboard/public/index.html +++ b/hf_repo/hf_repo/hf_repo/dashboard/public/index.html @@ -3,7 +3,10 @@ - Minecraft WebSocket API - 控制台 + InterConnect Dashboard + + + @@ -28,6 +31,9 @@