|
|
const API_URL = window.location.origin; |
|
|
let apiKey = null; |
|
|
let currentRole = null; |
|
|
let ws = null; |
|
|
let statsIntervalId = null; |
|
|
let wsPingIntervalId = null; |
|
|
|
|
|
const loginScreen = document.getElementById('login-screen'); |
|
|
const adminScreen = document.getElementById('admin-screen'); |
|
|
const userScreen = document.getElementById('user-screen'); |
|
|
const loginForm = document.getElementById('login-form'); |
|
|
const loginError = document.getElementById('login-error'); |
|
|
const apiKeyInput = document.getElementById('api-key-input'); |
|
|
const logoutButtons = document.querySelectorAll('.logout-btn'); |
|
|
const adminUserInfo = document.getElementById('admin-user-info'); |
|
|
const userInfo = document.getElementById('user-info'); |
|
|
const createKeyBtn = document.getElementById('create-key-btn'); |
|
|
const createKeyModal = document.getElementById('create-key-modal'); |
|
|
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'); |
|
|
const commandInput = document.getElementById('command-input'); |
|
|
const commandHistory = document.getElementById('command-history'); |
|
|
const userEventsList = document.getElementById('user-events-list'); |
|
|
const aiConfigForm = document.getElementById('ai-config-form'); |
|
|
const aiApiUrlInput = document.getElementById('ai-api-url'); |
|
|
const aiModelIdInput = document.getElementById('ai-model-id'); |
|
|
const aiApiKeyInput = document.getElementById('ai-api-key'); |
|
|
const aiApiKeyHint = document.getElementById('ai-api-key-hint'); |
|
|
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'); |
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
|
|
|
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; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
initTheme(); |
|
|
|
|
|
themeToggleBtns.forEach(btn => { |
|
|
btn.addEventListener('click', toggleTheme); |
|
|
}); |
|
|
|
|
|
function authHeaders(key) { |
|
|
return { Authorization: `Bearer ${key}` }; |
|
|
} |
|
|
|
|
|
function resetSession() { |
|
|
apiKey = null; |
|
|
currentRole = null; |
|
|
|
|
|
if (statsIntervalId) { |
|
|
clearInterval(statsIntervalId); |
|
|
statsIntervalId = null; |
|
|
} |
|
|
|
|
|
if (wsPingIntervalId) { |
|
|
clearInterval(wsPingIntervalId); |
|
|
wsPingIntervalId = null; |
|
|
} |
|
|
|
|
|
if (ws) { |
|
|
ws.close(); |
|
|
ws = null; |
|
|
} |
|
|
|
|
|
if (loginScreen) { |
|
|
loginScreen.classList.remove('hidden'); |
|
|
} |
|
|
if (adminScreen) { |
|
|
adminScreen.classList.add('hidden'); |
|
|
} |
|
|
if (userScreen) { |
|
|
userScreen.classList.add('hidden'); |
|
|
} |
|
|
} |
|
|
|
|
|
async function detectRole(key) { |
|
|
const adminResponse = await fetch(`${API_URL}/manage/keys`, { |
|
|
headers: authHeaders(key) |
|
|
}); |
|
|
|
|
|
if (adminResponse.ok) { |
|
|
return 'admin'; |
|
|
} |
|
|
|
|
|
const regularResponse = await fetch(`${API_URL}/manage/keys/server-keys`, { |
|
|
headers: authHeaders(key) |
|
|
}); |
|
|
|
|
|
if (regularResponse.ok) { |
|
|
return 'regular'; |
|
|
} |
|
|
|
|
|
return null; |
|
|
} |
|
|
|
|
|
function showAdminPanel() { |
|
|
currentRole = 'admin'; |
|
|
loginScreen.classList.add('hidden'); |
|
|
userScreen.classList.add('hidden'); |
|
|
adminScreen.classList.remove('hidden'); |
|
|
|
|
|
if (adminUserInfo) { |
|
|
adminUserInfo.textContent = 'Admin Key'; |
|
|
} |
|
|
|
|
|
loadAdminKeys(); |
|
|
loadAIConfig(); |
|
|
} |
|
|
|
|
|
function showUserPanel() { |
|
|
currentRole = 'regular'; |
|
|
loginScreen.classList.add('hidden'); |
|
|
adminScreen.classList.add('hidden'); |
|
|
userScreen.classList.remove('hidden'); |
|
|
|
|
|
if (userInfo) { |
|
|
userInfo.textContent = 'Regular Key'; |
|
|
} |
|
|
|
|
|
loadUserServerKeys(); |
|
|
loadStats(); |
|
|
connectWebSocket(); |
|
|
|
|
|
statsIntervalId = setInterval(loadStats, 5000); |
|
|
} |
|
|
|
|
|
async function loadAdminKeys() { |
|
|
if (!adminKeysList || !apiKey) { |
|
|
return; |
|
|
} |
|
|
try { |
|
|
const response = await fetch(`${API_URL}/manage/keys`, { |
|
|
headers: authHeaders(apiKey) |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const keys = await response.json(); |
|
|
renderAdminKeys(keys); |
|
|
} else { |
|
|
adminKeysList.innerHTML = '<p>Failed to load keys.</p>'; |
|
|
} |
|
|
} catch (error) { |
|
|
adminKeysList.innerHTML = `<p>Failed to load keys: ${error.message}</p>`; |
|
|
} |
|
|
} |
|
|
|
|
|
async function loadUserServerKeys() { |
|
|
if (!userKeysList || !apiKey) { |
|
|
return; |
|
|
} |
|
|
try { |
|
|
const response = await fetch(`${API_URL}/manage/keys/server-keys`, { |
|
|
headers: authHeaders(apiKey) |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const keys = await response.json(); |
|
|
renderUserServerKeys(keys); |
|
|
} else { |
|
|
userKeysList.innerHTML = '<p>Failed to load server keys.</p>'; |
|
|
} |
|
|
} catch (error) { |
|
|
userKeysList.innerHTML = `<p>Failed to load server keys: ${error.message}</p>`; |
|
|
} |
|
|
} |
|
|
|
|
|
function renderAdminKeys(keys) { |
|
|
if (!adminKeysList) { |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!keys.length) { |
|
|
adminKeysList.innerHTML = '<p>No API keys found.</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
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 = ''; |
|
|
|
|
|
if (adminKeys.length > 0) { |
|
|
html += '<h3 class="group-title">管理员密钥 (Admin Keys)</h3>'; |
|
|
html += adminKeys.map(key => renderKeyCard(key)).join(''); |
|
|
} |
|
|
|
|
|
if (regularKeys.length > 0) { |
|
|
html += '<h3 class="group-title">用户密钥 (Regular Keys)</h3>'; |
|
|
html += regularKeys.map(regularKey => { |
|
|
const childServerKeys = serverKeysMap[regularKey.id] || []; |
|
|
return renderRegularKeyCard(regularKey, childServerKeys); |
|
|
}).join(''); |
|
|
} |
|
|
|
|
|
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 += '<h3 class="group-title">独立服务器密钥 (Orphan Server Keys)</h3>'; |
|
|
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 ` |
|
|
<div class="key-card ${key.keyType === 'admin' ? 'admin' : ''} ${isNested ? 'nested' : ''}"> |
|
|
<div class="key-info"> |
|
|
<h3> |
|
|
<span class="key-badge ${key.keyType}"> |
|
|
${key.keyType === 'admin' ? 'Admin' : key.keyType === 'server' ? 'Server' : 'Regular'} |
|
|
</span> |
|
|
<span class="key-badge ${key.isActive ? 'active' : 'inactive'}"> |
|
|
${key.isActive ? '已启用' : '已停用'} |
|
|
</span> |
|
|
${key.name} |
|
|
</h3> |
|
|
<p>ID: ${key.id}</p> |
|
|
<p>Prefix: ${key.keyPrefix}</p> |
|
|
${key.serverId ? `<p>Server ID: ${key.serverId}</p>` : ''} |
|
|
<p>创建时间: ${new Date(key.createdAt).toLocaleString()}</p> |
|
|
<p>最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : '从未'}</p> |
|
|
</div> |
|
|
<div class="key-actions"> |
|
|
${key.isActive |
|
|
? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>` |
|
|
: `<button class="btn-success" onclick="activateKey('${key.id}')">启用</button>` |
|
|
} |
|
|
<button class="btn-secondary" onclick="showRefreshKeyModal('${key.id}', '${key.name}')">刷新</button> |
|
|
<button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">删除</button> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderRegularKeyCard(regularKey, childServerKeys = []) { |
|
|
return ` |
|
|
<div class="regular-key-group"> |
|
|
<div class="regular-key-header" onclick="toggleGroup(this)"> |
|
|
<div class="key-card"> |
|
|
<div class="key-info"> |
|
|
<h3> |
|
|
<span class="key-badge regular">Regular</span> |
|
|
<span class="key-badge ${regularKey.isActive ? 'active' : 'inactive'}"> |
|
|
${regularKey.isActive ? '已启用' : '已停用'} |
|
|
</span> |
|
|
${regularKey.name} |
|
|
</h3> |
|
|
<p>ID: ${regularKey.id}</p> |
|
|
<p>Prefix: ${regularKey.keyPrefix}</p> |
|
|
${regularKey.serverId ? `<p>Server ID: ${regularKey.serverId}</p>` : ''} |
|
|
<p>创建时间: ${new Date(regularKey.createdAt).toLocaleString()}</p> |
|
|
<p>最后使用: ${regularKey.lastUsed ? new Date(regularKey.lastUsed).toLocaleString() : '从未'}</p> |
|
|
</div> |
|
|
<div class="key-actions"> |
|
|
${regularKey.isActive |
|
|
? `<button class="btn-danger" onclick="deactivateKey('${regularKey.id}')">停用</button>` |
|
|
: `<button class="btn-success" onclick="activateKey('${regularKey.id}')">启用</button>` |
|
|
} |
|
|
<button class="btn-secondary" onclick="showRefreshKeyModal('${regularKey.id}', '${regularKey.name}')">刷新</button> |
|
|
<button class="btn-danger" onclick="deleteKey('${regularKey.id}', '${regularKey.name}')">删除</button> |
|
|
<button class="btn-primary" onclick="showCreateServerKeyModal('${regularKey.id}', '${regularKey.name}')">添加 Server Key</button> |
|
|
</div> |
|
|
</div> |
|
|
${childServerKeys.length > 0 ? `<span class="toggle-icon">▼</span>` : ''} |
|
|
</div> |
|
|
${childServerKeys.length > 0 ? ` |
|
|
<div class="nested-server-keys hidden"> |
|
|
<h4>关联的服务器密钥 (Linked Server Keys)</h4> |
|
|
${childServerKeys.map(serverKey => renderKeyCard(serverKey, true)).join('')} |
|
|
</div> |
|
|
` : ''} |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
function renderUserServerKeys(keys) { |
|
|
if (!userKeysList) { |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!keys.length) { |
|
|
userKeysList.innerHTML = '<p>No server keys available.</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
userKeysList.innerHTML = keys.map((key) => ` |
|
|
<div class="key-card"> |
|
|
<div class="key-info"> |
|
|
<h3> |
|
|
<span class="key-badge server">Server</span> |
|
|
<span class="key-badge ${key.isActive ? 'active' : 'inactive'}"> |
|
|
${key.isActive ? '已启用' : '已停用'} |
|
|
</span> |
|
|
${key.name} |
|
|
</h3> |
|
|
<p>ID: ${key.id}</p> |
|
|
<p>Prefix: ${key.keyPrefix}</p> |
|
|
${key.serverId ? `<p>Server ID: ${key.serverId}</p>` : ''} |
|
|
<p>创建时间: ${new Date(key.createdAt).toLocaleString()}</p> |
|
|
<p>最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : '从未'}</p> |
|
|
</div> |
|
|
<div class="key-actions"> |
|
|
${key.isActive |
|
|
? `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>` |
|
|
: `<button class="btn-success" onclick="activateKey('${key.id}')">启用</button>` |
|
|
} |
|
|
<button class="btn-secondary" onclick="showRefreshKeyModal('${key.id}', '${key.name}')">刷新</button> |
|
|
</div> |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
async function activateKey(keyId) { |
|
|
if (!apiKey) { |
|
|
return; |
|
|
} |
|
|
try { |
|
|
const response = await fetch(`${API_URL}/manage/keys/${keyId}/activate`, { |
|
|
method: 'PATCH', |
|
|
headers: authHeaders(apiKey) |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
const error = await response.json(); |
|
|
alert(`Failed to activate key: ${error.detail}`); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (currentRole === 'admin') { |
|
|
loadAdminKeys(); |
|
|
} else if (currentRole === 'regular') { |
|
|
loadUserServerKeys(); |
|
|
} |
|
|
} catch (error) { |
|
|
alert(`Failed to activate key: ${error.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
async function deactivateKey(keyId) { |
|
|
if (!apiKey) { |
|
|
return; |
|
|
} |
|
|
try { |
|
|
const response = await fetch(`${API_URL}/manage/keys/${keyId}/deactivate`, { |
|
|
method: 'PATCH', |
|
|
headers: authHeaders(apiKey) |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
const error = await response.json(); |
|
|
alert(`Failed to deactivate key: ${error.detail}`); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (currentRole === 'admin') { |
|
|
loadAdminKeys(); |
|
|
} else if (currentRole === 'regular') { |
|
|
loadUserServerKeys(); |
|
|
} |
|
|
} catch (error) { |
|
|
alert(`Failed to deactivate key: ${error.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
async function deleteKey(keyId, keyName) { |
|
|
if (currentRole !== 'admin') { |
|
|
return; |
|
|
} |
|
|
if (!confirm(`确定要删除密钥 "${keyName}" 吗? 此操作无法撤销。`)) { |
|
|
return; |
|
|
} |
|
|
try { |
|
|
const response = await fetch(`${API_URL}/manage/keys/${keyId}`, { |
|
|
method: 'DELETE', |
|
|
headers: authHeaders(apiKey) |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
const error = await response.json(); |
|
|
alert(`Failed to delete key: ${error.detail}`); |
|
|
return; |
|
|
} |
|
|
|
|
|
loadAdminKeys(); |
|
|
} catch (error) { |
|
|
alert(`Failed to delete key: ${error.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
function showKeyCreatedModal(payload) { |
|
|
if (!keyDetailsModal) { |
|
|
return; |
|
|
} |
|
|
|
|
|
let content = ''; |
|
|
if (payload.regularKey && payload.serverKey) { |
|
|
content = ` |
|
|
<p><strong>Regular Key</strong></p> |
|
|
<p>Name: ${payload.regularKey.name}</p> |
|
|
<p>Type: ${payload.regularKey.keyType}</p> |
|
|
<p>ID: ${payload.regularKey.id}</p> |
|
|
<p>Key: ${payload.regularKey.key}</p> |
|
|
<hr> |
|
|
<p><strong>Server Key</strong></p> |
|
|
<p>Name: ${payload.serverKey.name}</p> |
|
|
<p>Type: ${payload.serverKey.keyType}</p> |
|
|
<p>ID: ${payload.serverKey.id}</p> |
|
|
<p>Key: ${payload.serverKey.key}</p> |
|
|
`; |
|
|
} else { |
|
|
content = ` |
|
|
<p><strong>Key Created</strong></p> |
|
|
<p>Name: ${payload.name}</p> |
|
|
<p>Type: ${payload.keyType}</p> |
|
|
<p>ID: ${payload.id}</p> |
|
|
<p>Key: ${payload.key}</p> |
|
|
`; |
|
|
} |
|
|
|
|
|
const detailsContent = document.getElementById('key-details-content'); |
|
|
if (detailsContent) { |
|
|
detailsContent.innerHTML = content; |
|
|
} |
|
|
|
|
|
keyDetailsModal.classList.remove('hidden'); |
|
|
} |
|
|
|
|
|
async function loadStats() { |
|
|
try { |
|
|
const response = await fetch(`${API_URL}/health`); |
|
|
if (!response.ok) { |
|
|
return; |
|
|
} |
|
|
const data = await response.json(); |
|
|
|
|
|
const connections = document.getElementById('user-stat-connections'); |
|
|
if (connections) { |
|
|
connections.textContent = data.active_ws || 0; |
|
|
} |
|
|
|
|
|
const serverKeys = document.getElementById('user-stat-server-keys'); |
|
|
if (serverKeys) { |
|
|
serverKeys.textContent = data.server_active || 0; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to load stats:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
function connectWebSocket() { |
|
|
if (!apiKey) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; |
|
|
const wsUrl = `${protocol}://${window.location.host}/ws?api_key=${apiKey}`; |
|
|
ws = new WebSocket(wsUrl); |
|
|
|
|
|
ws.onopen = () => { |
|
|
console.log('WebSocket connected'); |
|
|
}; |
|
|
|
|
|
ws.onmessage = (event) => { |
|
|
try { |
|
|
const message = JSON.parse(event.data); |
|
|
if (message.type === 'minecraft_event') { |
|
|
addEventToList(message.event); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to parse WebSocket message:', error); |
|
|
} |
|
|
}; |
|
|
|
|
|
ws.onerror = (error) => { |
|
|
console.error('WebSocket error:', error); |
|
|
}; |
|
|
|
|
|
ws.onclose = () => { |
|
|
console.log('WebSocket disconnected'); |
|
|
if (wsPingIntervalId) { |
|
|
clearInterval(wsPingIntervalId); |
|
|
wsPingIntervalId = null; |
|
|
} |
|
|
if (apiKey && currentRole === 'regular') { |
|
|
setTimeout(() => connectWebSocket(), 5000); |
|
|
} |
|
|
}; |
|
|
|
|
|
wsPingIntervalId = setInterval(() => { |
|
|
if (ws && ws.readyState === WebSocket.OPEN) { |
|
|
ws.send(JSON.stringify({ type: 'ping' })); |
|
|
} |
|
|
}, 30000); |
|
|
} |
|
|
|
|
|
function addEventToList(event) { |
|
|
if (!userEventsList) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const eventItem = document.createElement('div'); |
|
|
eventItem.className = 'event-item'; |
|
|
eventItem.innerHTML = ` |
|
|
<strong>${event.event_type}</strong> - ${event.server_name}<br> |
|
|
<small>${new Date(event.timestamp).toLocaleString()}</small><br> |
|
|
<pre>${JSON.stringify(event.data, null, 2)}</pre> |
|
|
`; |
|
|
|
|
|
userEventsList.insertBefore(eventItem, userEventsList.firstChild); |
|
|
|
|
|
while (userEventsList.children.length > 50) { |
|
|
userEventsList.removeChild(userEventsList.lastChild); |
|
|
} |
|
|
} |
|
|
|
|
|
function appendCommandHistory(command, status, detail) { |
|
|
if (!commandHistory) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const entry = document.createElement('div'); |
|
|
entry.className = 'command-item'; |
|
|
entry.innerHTML = ` |
|
|
<div>${status}: ${command}</div> |
|
|
${detail ? `<div class="timestamp">${detail}</div>` : ''} |
|
|
`; |
|
|
commandHistory.insertBefore(entry, commandHistory.firstChild); |
|
|
|
|
|
while (commandHistory.children.length > 20) { |
|
|
commandHistory.removeChild(commandHistory.lastChild); |
|
|
} |
|
|
} |
|
|
|
|
|
if (loginForm) { |
|
|
loginForm.addEventListener('submit', async (event) => { |
|
|
event.preventDefault(); |
|
|
const key = apiKeyInput.value.trim(); |
|
|
loginError.textContent = ''; |
|
|
|
|
|
if (!key) { |
|
|
loginError.textContent = 'API key is required.'; |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const role = await detectRole(key); |
|
|
if (!role) { |
|
|
loginError.textContent = 'Invalid key or insufficient permissions.'; |
|
|
return; |
|
|
} |
|
|
|
|
|
apiKey = key; |
|
|
|
|
|
if (role === 'admin') { |
|
|
showAdminPanel(); |
|
|
} else if (role === 'regular') { |
|
|
showUserPanel(); |
|
|
} |
|
|
} catch (error) { |
|
|
loginError.textContent = error.message || 'Unable to connect to server.'; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
logoutButtons.forEach((button) => { |
|
|
button.addEventListener('click', () => { |
|
|
resetSession(); |
|
|
}); |
|
|
}); |
|
|
|
|
|
if (createKeyBtn) { |
|
|
createKeyBtn.addEventListener('click', () => { |
|
|
createKeyModal.classList.remove('hidden'); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (cancelCreateBtn) { |
|
|
cancelCreateBtn.addEventListener('click', () => { |
|
|
createKeyModal.classList.add('hidden'); |
|
|
createKeyForm.reset(); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (closeDetailsBtn) { |
|
|
closeDetailsBtn.addEventListener('click', () => { |
|
|
keyDetailsModal.classList.add('hidden'); |
|
|
}); |
|
|
} |
|
|
|
|
|
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(); |
|
|
|
|
|
const name = document.getElementById('key-name').value.trim(); |
|
|
const description = document.getElementById('key-description').value.trim(); |
|
|
const keyType = document.getElementById('key-type').value; |
|
|
const serverId = document.getElementById('key-server-id').value.trim(); |
|
|
|
|
|
if (!name) { |
|
|
alert('Name is required.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const payload = { |
|
|
name, |
|
|
description, |
|
|
key_type: keyType |
|
|
}; |
|
|
|
|
|
if (serverId) { |
|
|
payload.server_id = serverId; |
|
|
} |
|
|
|
|
|
const response = await fetch(`${API_URL}/manage/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 key: ${error.detail}`); |
|
|
return; |
|
|
} |
|
|
|
|
|
const result = await response.json(); |
|
|
createKeyModal.classList.add('hidden'); |
|
|
createKeyForm.reset(); |
|
|
showKeyCreatedModal(result); |
|
|
loadAdminKeys(); |
|
|
} catch (error) { |
|
|
alert(`Failed to create key: ${error.message}`); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
if (commandForm) { |
|
|
commandForm.addEventListener('submit', async (event) => { |
|
|
event.preventDefault(); |
|
|
|
|
|
const command = commandInput.value.trim(); |
|
|
if (!command) { |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch(`${API_URL}/api/server/command`, { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
...authHeaders(apiKey), |
|
|
'Content-Type': 'application/json' |
|
|
}, |
|
|
body: JSON.stringify({ command }) |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
const error = await response.json(); |
|
|
appendCommandHistory(command, 'Rejected', error.detail); |
|
|
return; |
|
|
} |
|
|
|
|
|
appendCommandHistory(command, 'Sent', new Date().toLocaleString()); |
|
|
commandInput.value = ''; |
|
|
} catch (error) { |
|
|
appendCommandHistory(command, 'Error', error.message); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
window.activateKey = activateKey; |
|
|
window.deactivateKey = deactivateKey; |
|
|
window.deleteKey = deleteKey; |
|
|
|
|
|
async function loadAIConfig() { |
|
|
if (!apiKey) { |
|
|
return; |
|
|
} |
|
|
try { |
|
|
const response = await fetch(`${API_URL}/api/ai/config`, { |
|
|
headers: authHeaders(apiKey) |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const config = await response.json(); |
|
|
if (aiApiUrlInput) aiApiUrlInput.value = config.apiUrl || ''; |
|
|
if (aiModelIdInput) aiModelIdInput.value = config.modelId || ''; |
|
|
if (aiApiKeyInput) aiApiKeyInput.value = ''; |
|
|
if (aiApiKeyHint) aiApiKeyHint.textContent = config.apiKey ? `Current: ${config.apiKey}` : ''; |
|
|
if (aiSystemPromptInput) aiSystemPromptInput.value = config.systemPrompt || ''; |
|
|
if (aiEnabledCheckbox) aiEnabledCheckbox.checked = config.enabled; |
|
|
showAIStatus('Configuration loaded', 'success'); |
|
|
} else if (response.status === 404) { |
|
|
if (aiApiUrlInput) aiApiUrlInput.value = ''; |
|
|
if (aiModelIdInput) aiModelIdInput.value = ''; |
|
|
if (aiApiKeyInput) aiApiKeyInput.value = ''; |
|
|
if (aiApiKeyHint) aiApiKeyHint.textContent = ''; |
|
|
if (aiSystemPromptInput) aiSystemPromptInput.value = ''; |
|
|
if (aiEnabledCheckbox) aiEnabledCheckbox.checked = true; |
|
|
showAIStatus('No configuration found. Please set up AI configuration.', 'info'); |
|
|
} |
|
|
} catch (error) { |
|
|
showAIStatus(`Failed to load AI config: ${error.message}`, 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
async function saveAIConfig(event) { |
|
|
event.preventDefault(); |
|
|
if (!apiKey) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const apiUrl = aiApiUrlInput?.value?.trim(); |
|
|
const modelId = aiModelIdInput?.value?.trim(); |
|
|
const apiKeyValue = aiApiKeyInput?.value?.trim(); |
|
|
const systemPrompt = aiSystemPromptInput?.value?.trim() || null; |
|
|
const enabled = aiEnabledCheckbox?.checked ?? true; |
|
|
|
|
|
if (!apiUrl || !modelId) { |
|
|
showAIStatus('API URL and Model ID are required', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const existingResponse = await fetch(`${API_URL}/api/ai/config`, { |
|
|
headers: authHeaders(apiKey) |
|
|
}); |
|
|
const isUpdate = existingResponse.ok; |
|
|
|
|
|
const payload = { |
|
|
api_url: apiUrl, |
|
|
model_id: modelId, |
|
|
enabled: enabled, |
|
|
system_prompt: systemPrompt |
|
|
}; |
|
|
|
|
|
if (apiKeyValue) { |
|
|
payload.api_key = apiKeyValue; |
|
|
} else if (!isUpdate) { |
|
|
showAIStatus('API Key is required for new configuration', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const method = isUpdate ? 'PATCH' : 'POST'; |
|
|
const response = await fetch(`${API_URL}/api/ai/config`, { |
|
|
method: method, |
|
|
headers: { |
|
|
...authHeaders(apiKey), |
|
|
'Content-Type': 'application/json' |
|
|
}, |
|
|
body: JSON.stringify(payload) |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const config = await response.json(); |
|
|
if (aiApiKeyInput) aiApiKeyInput.value = ''; |
|
|
if (aiApiKeyHint) aiApiKeyHint.textContent = config.apiKey ? `Current: ${config.apiKey}` : ''; |
|
|
showAIStatus('Configuration saved successfully', 'success'); |
|
|
} else { |
|
|
const error = await response.json(); |
|
|
showAIStatus(`Failed to save: ${error.detail}`, 'error'); |
|
|
} |
|
|
} catch (error) { |
|
|
showAIStatus(`Failed to save AI config: ${error.message}`, 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
async function testAIConnection() { |
|
|
if (!apiKey) { |
|
|
return; |
|
|
} |
|
|
showAIStatus('Testing connection...', 'info'); |
|
|
|
|
|
try { |
|
|
const response = await fetch(`${API_URL}/api/ai/config/test`, { |
|
|
method: 'POST', |
|
|
headers: authHeaders(apiKey) |
|
|
}); |
|
|
|
|
|
const result = await response.json(); |
|
|
if (result.success) { |
|
|
showAIStatus(`Connection successful! Model: ${result.model}, Response: ${result.response}`, 'success'); |
|
|
} else { |
|
|
showAIStatus(`Connection failed: ${result.error}`, 'error'); |
|
|
} |
|
|
} catch (error) { |
|
|
showAIStatus(`Test failed: ${error.message}`, 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
async function deleteAIConfig() { |
|
|
if (!apiKey) { |
|
|
return; |
|
|
} |
|
|
if (!confirm('确定要删除 AI 配置吗?')) { |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch(`${API_URL}/api/ai/config`, { |
|
|
method: 'DELETE', |
|
|
headers: authHeaders(apiKey) |
|
|
}); |
|
|
|
|
|
if (response.ok || response.status === 204) { |
|
|
if (aiApiUrlInput) aiApiUrlInput.value = ''; |
|
|
if (aiModelIdInput) aiModelIdInput.value = ''; |
|
|
if (aiApiKeyInput) aiApiKeyInput.value = ''; |
|
|
if (aiApiKeyHint) aiApiKeyHint.textContent = ''; |
|
|
if (aiSystemPromptInput) aiSystemPromptInput.value = ''; |
|
|
if (aiEnabledCheckbox) aiEnabledCheckbox.checked = true; |
|
|
showAIStatus('Configuration deleted', 'success'); |
|
|
} else { |
|
|
const error = await response.json(); |
|
|
showAIStatus(`Failed to delete: ${error.detail}`, 'error'); |
|
|
} |
|
|
} catch (error) { |
|
|
showAIStatus(`Failed to delete AI config: ${error.message}`, 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
function showAIStatus(message, type) { |
|
|
if (!aiStatus) { |
|
|
return; |
|
|
} |
|
|
aiStatus.textContent = message; |
|
|
aiStatus.className = `ai-status ${type}`; |
|
|
} |
|
|
|
|
|
if (aiConfigForm) { |
|
|
aiConfigForm.addEventListener('submit', saveAIConfig); |
|
|
} |
|
|
|
|
|
if (aiTestBtn) { |
|
|
aiTestBtn.addEventListener('click', testAIConnection); |
|
|
} |
|
|
|
|
|
if (aiDeleteBtn) { |
|
|
aiDeleteBtn.addEventListener('click', deleteAIConfig); |
|
|
} |
|
|
|