From b61155d2157f3ca487e389ab47b5fd7f1e4e91e0 Mon Sep 17 00:00:00 2001 From: Supra4E8C <69194597+LTbinglingfeng@users.noreply.github.com> Date: Thu, 2 Oct 2025 17:34:26 +0800 Subject: [PATCH] v0.0.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加代理 URL 支持,更新 API 配置模态框,增强 XSS 防护,优化界面样式,修复若干 UI 问题,版本更新至 0.0.6 --- app.js | 5446 +++++++++++++++++++++++++++------------------------- i18n.js | 1353 ++++++------- index.html | 1158 +++++------ styles.css | 3545 +++++++++++++++++----------------- 4 files changed, 5876 insertions(+), 5626 deletions(-) diff --git a/app.js b/app.js index 7a96db2..0741144 100644 --- a/app.js +++ b/app.js @@ -1,2635 +1,2811 @@ -// CLI Proxy API 管理界面 JavaScript -class CLIProxyManager { - constructor() { - // 仅保存基础地址(不含 /v0/management),请求时自动补齐 - const detectedBase = this.detectApiBaseFromLocation(); - this.apiBase = detectedBase; - this.apiUrl = this.computeApiUrl(this.apiBase); - this.managementKey = ''; - this.isConnected = false; - this.isLoggedIn = false; - - // 配置缓存 - this.configCache = null; - this.cacheTimestamp = null; - this.cacheExpiry = 30000; // 30秒缓存过期时间 - - // 状态更新定时器 - this.statusUpdateTimer = null; - - // 主题管理 - this.currentTheme = 'light'; - - this.init(); - } - - // 简易防抖,减少频繁写 localStorage - debounce(fn, delay = 400) { - let timer; - return (...args) => { - clearTimeout(timer); - timer = setTimeout(() => fn.apply(this, args), delay); - }; - } - - // 初始化主题 - initializeTheme() { - // 从本地存储获取用户偏好主题 - const savedTheme = localStorage.getItem('preferredTheme'); - if (savedTheme && ['light', 'dark'].includes(savedTheme)) { - this.currentTheme = savedTheme; - } else { - if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { - this.currentTheme = 'dark'; - } else { - this.currentTheme = 'light'; - } - } - - this.applyTheme(this.currentTheme); - this.updateThemeButtons(); - - // 监听系统主题变化 - if (window.matchMedia) { - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { - if (!localStorage.getItem('preferredTheme')) { - this.currentTheme = e.matches ? 'dark' : 'light'; - this.applyTheme(this.currentTheme); - this.updateThemeButtons(); - } - }); - } - } - - // 应用主题 - applyTheme(theme) { - if (theme === 'dark') { - document.documentElement.setAttribute('data-theme', 'dark'); - } else { - document.documentElement.removeAttribute('data-theme'); - } - this.currentTheme = theme; - } - - // 切换主题 - toggleTheme() { - const newTheme = this.currentTheme === 'light' ? 'dark' : 'light'; - this.applyTheme(newTheme); - this.updateThemeButtons(); - localStorage.setItem('preferredTheme', newTheme); - } - - // 更新主题按钮状态 - updateThemeButtons() { - const loginThemeBtn = document.getElementById('theme-toggle'); - const mainThemeBtn = document.getElementById('theme-toggle-main'); - - const updateButton = (btn) => { - if (!btn) return; - const icon = btn.querySelector('i'); - if (this.currentTheme === 'dark') { - icon.className = 'fas fa-sun'; - btn.title = i18n.t('theme.switch_to_light'); - } else { - icon.className = 'fas fa-moon'; - btn.title = i18n.t('theme.switch_to_dark'); - } - }; - - updateButton(loginThemeBtn); - updateButton(mainThemeBtn); - } - - init() { - this.initializeTheme(); - this.checkLoginStatus(); - this.bindEvents(); - this.setupNavigation(); - this.setupLanguageSwitcher(); - this.setupThemeSwitcher(); - // loadSettings 将在登录成功后调用 - this.updateLoginConnectionInfo(); - } - - // 检查登录状态 - async checkLoginStatus() { - // 检查是否有保存的连接信息 - const savedBase = localStorage.getItem('apiBase'); - const savedKey = localStorage.getItem('managementKey'); - const wasLoggedIn = localStorage.getItem('isLoggedIn') === 'true'; - - // 如果有完整的连接信息且之前已登录,尝试自动登录 - if (savedBase && savedKey && wasLoggedIn) { - try { - console.log('检测到本地连接数据,尝试自动登录...'); - this.showAutoLoginLoading(); - await this.attemptAutoLogin(savedBase, savedKey); - return; // 自动登录成功,不显示登录页面 - } catch (error) { - console.log('自动登录失败:', error.message); - // 清除无效的登录状态 - localStorage.removeItem('isLoggedIn'); - this.hideAutoLoginLoading(); - } - } - - // 如果没有连接信息或自动登录失败,显示登录页面 - this.showLoginPage(); - this.loadLoginSettings(); - } - - // 显示自动登录加载页面 - showAutoLoginLoading() { - document.getElementById('auto-login-loading').style.display = 'flex'; - document.getElementById('login-page').style.display = 'none'; - document.getElementById('main-page').style.display = 'none'; - } - - // 隐藏自动登录加载页面 - hideAutoLoginLoading() { - document.getElementById('auto-login-loading').style.display = 'none'; - } - - // 尝试自动登录 - async attemptAutoLogin(apiBase, managementKey) { - try { - // 设置API基础地址和密钥 - this.setApiBase(apiBase); - this.managementKey = managementKey; - - // 恢复代理设置(如果有) - const savedProxy = localStorage.getItem('proxyUrl'); - if (savedProxy) { - // 代理设置会在后续的API请求中自动使用 - } - - // 测试连接 - await this.testConnection(); - - // 自动登录成功 - this.isLoggedIn = true; - this.hideAutoLoginLoading(); - this.showMainPage(); - - console.log('自动登录成功'); - return true; - } catch (error) { - console.error('自动登录失败:', error); - // 重置状态 - this.isLoggedIn = false; - this.isConnected = false; - throw error; - } - } - - // 显示登录页面 - showLoginPage() { - document.getElementById('login-page').style.display = 'flex'; - document.getElementById('main-page').style.display = 'none'; - this.isLoggedIn = false; - this.updateLoginConnectionInfo(); - } - - // 显示主页面 - showMainPage() { - document.getElementById('login-page').style.display = 'none'; - document.getElementById('main-page').style.display = 'block'; - this.isLoggedIn = true; - this.updateConnectionInfo(); - } - - // 登录验证 - async login(apiBase, managementKey) { - try { - // 设置API基础地址和密钥 - this.setApiBase(apiBase); - this.managementKey = managementKey; - localStorage.setItem('managementKey', this.managementKey); - - // 测试连接并加载所有数据 - await this.testConnection(); - - // 登录成功 - this.isLoggedIn = true; - localStorage.setItem('isLoggedIn', 'true'); - - this.showMainPage(); - // 不需要再调用loadSettings,因为内部状态已经在上面设置了 - - return true; - } catch (error) { - console.error('登录失败:', error); - throw error; - } - } - - // 登出 - logout() { - this.isLoggedIn = false; - this.isConnected = false; - this.clearCache(); - this.stopStatusUpdateTimer(); - - // 清除本地存储 - localStorage.removeItem('isLoggedIn'); - localStorage.removeItem('managementKey'); - - this.showLoginPage(); - } - - // 处理登录表单提交 - async handleLogin() { - const apiBaseInput = document.getElementById('login-api-base'); - const managementKeyInput = document.getElementById('login-management-key'); - const managementKey = managementKeyInput ? managementKeyInput.value.trim() : ''; - - if (!managementKey) { - this.showLoginError(i18n.t('login.error_required')); - return; - } - - if (apiBaseInput && apiBaseInput.value.trim()) { - this.setApiBase(apiBaseInput.value.trim()); - } - - const submitBtn = document.getElementById('login-submit'); - const originalText = submitBtn ? submitBtn.innerHTML : ''; - - try { - if (submitBtn) { - submitBtn.innerHTML = `
${i18n.t('login.submitting')}`; - submitBtn.disabled = true; - } - this.hideLoginError(); - - this.managementKey = managementKey; - localStorage.setItem('managementKey', this.managementKey); - - await this.login(this.apiBase, this.managementKey); - } catch (error) { - this.showLoginError(`${i18n.t('login.error_title')}: ${error.message}`); - } finally { - if (submitBtn) { - submitBtn.innerHTML = originalText; - submitBtn.disabled = false; - } - } - } - - // 切换登录页面密钥可见性 - toggleLoginKeyVisibility(button) { - const inputGroup = button.closest('.input-group'); - const keyInput = inputGroup.querySelector('input[type="password"], input[type="text"]'); - - if (keyInput.type === 'password') { - keyInput.type = 'text'; - button.innerHTML = ''; - } else { - keyInput.type = 'password'; - button.innerHTML = ''; - } - } - - // 显示登录错误 - showLoginError(message) { - const errorDiv = document.getElementById('login-error'); - const errorMessage = document.getElementById('login-error-message'); - - errorMessage.textContent = message; - errorDiv.style.display = 'flex'; - } - - // 隐藏登录错误 - hideLoginError() { - const errorDiv = document.getElementById('login-error'); - errorDiv.style.display = 'none'; - } - - // 更新连接信息显示 - updateConnectionInfo() { - const apiUrlElement = document.getElementById('display-api-url'); - const keyElement = document.getElementById('display-management-key'); - const statusElement = document.getElementById('display-connection-status'); - - // 显示API地址 - if (apiUrlElement) { - apiUrlElement.textContent = this.apiBase || '-'; - } - - // 显示密钥(遮蔽显示) - if (keyElement) { - if (this.managementKey) { - const maskedKey = this.maskApiKey(this.managementKey); - keyElement.textContent = maskedKey; - } else { - keyElement.textContent = '-'; - } - } - - // 显示连接状态 - if (statusElement) { - let statusHtml = ''; - if (this.isConnected) { - statusHtml = ` ${i18n.t('common.connected')}`; - } else { - statusHtml = ` ${i18n.t('common.disconnected')}`; - } - statusElement.innerHTML = statusHtml; - } - } - - - // 加载登录页面设置 - loadLoginSettings() { - const savedBase = localStorage.getItem('apiBase'); - const savedKey = localStorage.getItem('managementKey'); - const loginKeyInput = document.getElementById('login-management-key'); - const apiBaseInput = document.getElementById('login-api-base'); - - if (savedBase) { - this.setApiBase(savedBase); - } else { - this.setApiBase(this.detectApiBaseFromLocation()); - } - - if (apiBaseInput) { - apiBaseInput.value = this.apiBase || ''; - } - - if (loginKeyInput && savedKey) { - loginKeyInput.value = savedKey; - } - - this.setupLoginAutoSave(); - } - - setupLoginAutoSave() { - const loginKeyInput = document.getElementById('login-management-key'); - const apiBaseInput = document.getElementById('login-api-base'); - const resetButton = document.getElementById('login-reset-api-base'); - - const saveKey = (val) => { - if (val.trim()) { - this.managementKey = val; - localStorage.setItem('managementKey', this.managementKey); - } - }; - const saveKeyDebounced = this.debounce(saveKey, 500); - - if (loginKeyInput) { - loginKeyInput.addEventListener('change', (e) => saveKey(e.target.value)); - loginKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value)); - } - - if (apiBaseInput) { - const persistBase = (val) => { - const normalized = this.normalizeBase(val); - if (normalized) { - this.setApiBase(normalized); - } - }; - const persistBaseDebounced = this.debounce(persistBase, 500); - - apiBaseInput.addEventListener('change', (e) => persistBase(e.target.value)); - apiBaseInput.addEventListener('input', (e) => persistBaseDebounced(e.target.value)); - } - - if (resetButton) { - resetButton.addEventListener('click', () => { - const detected = this.detectApiBaseFromLocation(); - this.setApiBase(detected); - if (apiBaseInput) { - apiBaseInput.value = detected; - } - }); - } - } - - // 事件绑定 - bindEvents() { - // 登录相关(安全绑定) - const loginSubmit = document.getElementById('login-submit'); - const logoutBtn = document.getElementById('logout-btn'); - - if (loginSubmit) { - loginSubmit.addEventListener('click', () => this.handleLogin()); - } - if (logoutBtn) { - logoutBtn.addEventListener('click', () => this.logout()); - } - - // 密钥可见性切换事件 - this.setupKeyVisibilityToggle(); - - // 主页面元素(延迟绑定,在显示主页面时绑定) - this.bindMainPageEvents(); - } - - // 设置密钥可见性切换 - setupKeyVisibilityToggle() { - const toggleButtons = document.querySelectorAll('.toggle-key-visibility'); - toggleButtons.forEach(button => { - button.addEventListener('click', () => this.toggleLoginKeyVisibility(button)); - }); - } - - // 绑定主页面事件 - bindMainPageEvents() { - // 连接状态检查 - const connectionStatus = document.getElementById('connection-status'); - const refreshAll = document.getElementById('refresh-all'); - - if (connectionStatus) { - connectionStatus.addEventListener('click', () => this.checkConnectionStatus()); - } - if (refreshAll) { - refreshAll.addEventListener('click', () => this.refreshAllData()); - } - - // 基础设置 - const debugToggle = document.getElementById('debug-toggle'); - const updateProxy = document.getElementById('update-proxy'); - const clearProxy = document.getElementById('clear-proxy'); - const updateRetry = document.getElementById('update-retry'); - const switchProjectToggle = document.getElementById('switch-project-toggle'); - const switchPreviewToggle = document.getElementById('switch-preview-model-toggle'); - - if (debugToggle) { - debugToggle.addEventListener('change', (e) => this.updateDebug(e.target.checked)); - } - if (updateProxy) { - updateProxy.addEventListener('click', () => this.updateProxyUrl()); - } - if (clearProxy) { - clearProxy.addEventListener('click', () => this.clearProxyUrl()); - } - if (updateRetry) { - updateRetry.addEventListener('click', () => this.updateRequestRetry()); - } - if (switchProjectToggle) { - switchProjectToggle.addEventListener('change', (e) => this.updateSwitchProject(e.target.checked)); - } - if (switchPreviewToggle) { - switchPreviewToggle.addEventListener('change', (e) => this.updateSwitchPreviewModel(e.target.checked)); - } - - // API 密钥管理 - const addApiKey = document.getElementById('add-api-key'); - const addGeminiKey = document.getElementById('add-gemini-key'); - const addCodexKey = document.getElementById('add-codex-key'); - const addClaudeKey = document.getElementById('add-claude-key'); - const addOpenaiProvider = document.getElementById('add-openai-provider'); - - if (addApiKey) { - addApiKey.addEventListener('click', () => this.showAddApiKeyModal()); - } - if (addGeminiKey) { - addGeminiKey.addEventListener('click', () => this.showAddGeminiKeyModal()); - } - if (addCodexKey) { - addCodexKey.addEventListener('click', () => this.showAddCodexKeyModal()); - } - if (addClaudeKey) { - addClaudeKey.addEventListener('click', () => this.showAddClaudeKeyModal()); - } - if (addOpenaiProvider) { - addOpenaiProvider.addEventListener('click', () => this.showAddOpenAIProviderModal()); - } - - - // Gemini Web Token - const geminiWebTokenBtn = document.getElementById('gemini-web-token-btn'); - if (geminiWebTokenBtn) { - geminiWebTokenBtn.addEventListener('click', () => this.showGeminiWebTokenModal()); - } - - // 认证文件管理 - const uploadAuthFile = document.getElementById('upload-auth-file'); - const deleteAllAuthFiles = document.getElementById('delete-all-auth-files'); - const authFileInput = document.getElementById('auth-file-input'); - - if (uploadAuthFile) { - uploadAuthFile.addEventListener('click', () => this.uploadAuthFile()); - } - if (deleteAllAuthFiles) { - deleteAllAuthFiles.addEventListener('click', () => this.deleteAllAuthFiles()); - } - if (authFileInput) { - authFileInput.addEventListener('change', (e) => this.handleFileUpload(e)); - } - - // 使用统计 - const refreshUsageStats = document.getElementById('refresh-usage-stats'); - const requestsHourBtn = document.getElementById('requests-hour-btn'); - const requestsDayBtn = document.getElementById('requests-day-btn'); - const tokensHourBtn = document.getElementById('tokens-hour-btn'); - const tokensDayBtn = document.getElementById('tokens-day-btn'); - - if (refreshUsageStats) { - refreshUsageStats.addEventListener('click', () => this.loadUsageStats()); - } - if (requestsHourBtn) { - requestsHourBtn.addEventListener('click', () => this.switchRequestsPeriod('hour')); - } - if (requestsDayBtn) { - requestsDayBtn.addEventListener('click', () => this.switchRequestsPeriod('day')); - } - if (tokensHourBtn) { - tokensHourBtn.addEventListener('click', () => this.switchTokensPeriod('hour')); - } - if (tokensDayBtn) { - tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day')); - } - - // 模态框 - const closeBtn = document.querySelector('.close'); - if (closeBtn) { - closeBtn.addEventListener('click', () => this.closeModal()); - } - - window.addEventListener('click', (e) => { - const modal = document.getElementById('modal'); - if (modal && e.target === modal) { - this.closeModal(); - } - }); - } - - // 设置导航 - setupNavigation() { - const navItems = document.querySelectorAll('.nav-item'); - navItems.forEach(item => { - item.addEventListener('click', (e) => { - e.preventDefault(); - - // 移除所有活动状态 - navItems.forEach(nav => nav.classList.remove('active')); - document.querySelectorAll('.content-section').forEach(section => section.classList.remove('active')); - - // 添加活动状态 - item.classList.add('active'); - const sectionId = item.getAttribute('data-section'); - document.getElementById(sectionId).classList.add('active'); - }); - }); - } - - // 设置语言切换 - setupLanguageSwitcher() { - const loginToggle = document.getElementById('language-toggle'); - const mainToggle = document.getElementById('language-toggle-main'); - - if (loginToggle) { - loginToggle.addEventListener('click', () => this.toggleLanguage()); - } - if (mainToggle) { - mainToggle.addEventListener('click', () => this.toggleLanguage()); - } - } - - // 设置主题切换 - setupThemeSwitcher() { - const loginToggle = document.getElementById('theme-toggle'); - const mainToggle = document.getElementById('theme-toggle-main'); - - if (loginToggle) { - loginToggle.addEventListener('click', () => this.toggleTheme()); - } - if (mainToggle) { - mainToggle.addEventListener('click', () => this.toggleTheme()); - } - } - - // 切换语言 - toggleLanguage() { - const currentLang = i18n.currentLanguage; - const newLang = currentLang === 'zh-CN' ? 'en-US' : 'zh-CN'; - i18n.setLanguage(newLang); - - // 更新主题按钮文本 - this.updateThemeButtons(); - - // 更新连接状态显示 - this.updateConnectionStatus(); - - // 重新加载所有数据以更新动态内容 - if (this.isLoggedIn && this.isConnected) { - this.loadAllData(true); - } - } - - // 规范化基础地址,移除尾部斜杠与 /v0/management - normalizeBase(input) { - let base = (input || '').trim(); - if (!base) return ''; - // 若用户粘贴了完整地址,剥离后缀 - base = base.replace(/\/?v0\/management\/?$/i, ''); - base = base.replace(/\/+$/i, ''); - // 自动补 http:// - if (!/^https?:\/\//i.test(base)) { - base = 'http://' + base; - } - return base; - } - - // 由基础地址生成完整管理 API 地址 - computeApiUrl(base) { - const b = this.normalizeBase(base); - if (!b) return ''; - return b.replace(/\/$/, '') + '/v0/management'; - } - - setApiBase(newBase) { - this.apiBase = this.normalizeBase(newBase); - this.apiUrl = this.computeApiUrl(this.apiBase); - localStorage.setItem('apiBase', this.apiBase); - localStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段 - this.updateLoginConnectionInfo(); - } - - // 加载设置(简化版,仅加载内部状态) - loadSettings() { - const savedBase = localStorage.getItem('apiBase'); - const savedUrl = localStorage.getItem('apiUrl'); - const savedKey = localStorage.getItem('managementKey'); - - if (savedBase) { - this.setApiBase(savedBase); - } else if (savedUrl) { - const base = (savedUrl || '').replace(/\/?v0\/management\/?$/i, ''); - this.setApiBase(base); - } else { - this.setApiBase(this.detectApiBaseFromLocation()); - } - - if (savedKey) { - this.managementKey = savedKey; - } - - this.updateLoginConnectionInfo(); - } - - // API 请求方法 - async makeRequest(endpoint, options = {}) { - const url = `${this.apiUrl}${endpoint}`; - const headers = { - 'Authorization': `Bearer ${this.managementKey}`, - 'Content-Type': 'application/json', - ...options.headers - }; - - try { - const response = await fetch(url, { - ...options, - headers - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error || `HTTP ${response.status}`); - } - - return await response.json(); - } catch (error) { - console.error('API请求失败:', error); - throw error; - } - } - - // 显示通知 - showNotification(message, type = 'info') { - const notification = document.getElementById('notification'); - notification.textContent = message; - notification.className = `notification ${type}`; - notification.classList.add('show'); - - setTimeout(() => { - notification.classList.remove('show'); - }, 3000); - } - - // 密钥可见性切换 - toggleKeyVisibility() { - const keyInput = document.getElementById('management-key'); - const toggleButton = document.getElementById('toggle-key-visibility'); - - if (keyInput.type === 'password') { - keyInput.type = 'text'; - toggleButton.innerHTML = ''; - } else { - keyInput.type = 'password'; - toggleButton.innerHTML = ''; - } - } - - // 测试连接(简化版,用于内部调用) - async testConnection() { - try { - await this.makeRequest('/debug'); - this.isConnected = true; - this.updateConnectionStatus(); - this.startStatusUpdateTimer(); - await this.loadAllData(); - return true; - } catch (error) { - this.isConnected = false; - this.updateConnectionStatus(); - this.stopStatusUpdateTimer(); - throw error; - } - } - - // 更新连接状态 - updateConnectionStatus() { - const statusButton = document.getElementById('connection-status'); - const apiStatus = document.getElementById('api-status'); - const configStatus = document.getElementById('config-status'); - const lastUpdate = document.getElementById('last-update'); - - if (this.isConnected) { - statusButton.innerHTML = ` ${i18n.t('common.connected')}`; - statusButton.className = 'btn btn-success'; - apiStatus.textContent = i18n.t('common.connected'); - - // 更新配置状态 - if (this.isCacheValid()) { - const cacheAge = Math.floor((Date.now() - this.cacheTimestamp) / 1000); - configStatus.textContent = `${i18n.t('system_info.cache_data')} (${cacheAge}${i18n.t('system_info.seconds_ago')})`; - configStatus.style.color = '#f59e0b'; // 橙色表示缓存 - } else if (this.configCache) { - configStatus.textContent = i18n.t('system_info.real_time_data'); - configStatus.style.color = '#10b981'; // 绿色表示实时 - } else { - configStatus.textContent = i18n.t('system_info.not_loaded'); - configStatus.style.color = '#6b7280'; // 灰色表示未加载 - } - } else { - statusButton.innerHTML = ` ${i18n.t('common.disconnected')}`; - statusButton.className = 'btn btn-danger'; - apiStatus.textContent = i18n.t('common.disconnected'); - configStatus.textContent = i18n.t('system_info.not_loaded'); - configStatus.style.color = '#6b7280'; - } - - lastUpdate.textContent = new Date().toLocaleString('zh-CN'); - - // 更新连接信息显示 - this.updateConnectionInfo(); - } - - // 检查连接状态 - async checkConnectionStatus() { - await this.testConnection(); - } - - // 刷新所有数据 - async refreshAllData() { - if (!this.isConnected) { - this.showNotification(i18n.t('notification.connection_required'), 'error'); - return; - } - - const button = document.getElementById('refresh-all'); - const originalText = button.innerHTML; - - button.innerHTML = `
${i18n.t('common.loading')}`; - button.disabled = true; - - try { - // 强制刷新,清除缓存 - await this.loadAllData(true); - this.showNotification(i18n.t('notification.data_refreshed'), 'success'); - } catch (error) { - this.showNotification(`${i18n.t('notification.refresh_failed')}: ${error.message}`, 'error'); - } finally { - button.innerHTML = originalText; - button.disabled = false; - } - } - - // 检查缓存是否有效 - isCacheValid() { - if (!this.configCache || !this.cacheTimestamp) { - return false; - } - return (Date.now() - this.cacheTimestamp) < this.cacheExpiry; - } - - // 获取配置(优先使用缓存) - async getConfig(forceRefresh = false) { - if (!forceRefresh && this.isCacheValid()) { - this.updateConnectionStatus(); // 更新状态显示 - return this.configCache; - } - - try { - const config = await this.makeRequest('/config'); - this.configCache = config; - this.cacheTimestamp = Date.now(); - this.updateConnectionStatus(); // 更新状态显示 - return config; - } catch (error) { - console.error('获取配置失败:', error); - throw error; - } - } - - // 清除缓存 - clearCache() { - this.configCache = null; - this.cacheTimestamp = null; - } - - // 启动状态更新定时器 - startStatusUpdateTimer() { - if (this.statusUpdateTimer) { - clearInterval(this.statusUpdateTimer); - } - this.statusUpdateTimer = setInterval(() => { - if (this.isConnected) { - this.updateConnectionStatus(); - } - }, 1000); // 每秒更新一次 - } - - // 停止状态更新定时器 - stopStatusUpdateTimer() { - if (this.statusUpdateTimer) { - clearInterval(this.statusUpdateTimer); - this.statusUpdateTimer = null; - } - } - - // 加载所有数据 - 使用新的 /config 端点一次性获取所有配置 - async loadAllData(forceRefresh = false) { - try { - console.log('使用新的 /config 端点加载所有配置...'); - // 使用新的 /config 端点一次性获取所有配置 - const config = await this.getConfig(forceRefresh); - - // 从配置中提取并设置各个设置项 - this.updateSettingsFromConfig(config); - - // 认证文件需要单独加载,因为不在配置中 - await this.loadAuthFiles(); - - // 使用统计需要单独加载 - await this.loadUsageStats(); - - console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid()); - } catch (error) { - console.error('加载配置失败:', error); - console.log('回退到逐个加载方式...'); - // 如果新方法失败,回退到原来的逐个加载方式 - await this.loadAllDataLegacy(); - } - } - - // 从配置对象更新所有设置 - updateSettingsFromConfig(config) { - // 调试设置 - if (config.debug !== undefined) { - document.getElementById('debug-toggle').checked = config.debug; - } - - // 代理设置 - if (config['proxy-url'] !== undefined) { - document.getElementById('proxy-url').value = config['proxy-url'] || ''; - } - - // 请求重试设置 - if (config['request-retry'] !== undefined) { - document.getElementById('request-retry').value = config['request-retry']; - } - - // 配额超出行为 - if (config['quota-exceeded']) { - if (config['quota-exceeded']['switch-project'] !== undefined) { - document.getElementById('switch-project-toggle').checked = config['quota-exceeded']['switch-project']; - } - if (config['quota-exceeded']['switch-preview-model'] !== undefined) { - document.getElementById('switch-preview-model-toggle').checked = config['quota-exceeded']['switch-preview-model']; - } - } - - - // API 密钥 - if (config['api-keys']) { - this.renderApiKeys(config['api-keys']); - } - - // Gemini 密钥 - if (config['generative-language-api-key']) { - this.renderGeminiKeys(config['generative-language-api-key']); - } - - // Codex 密钥 - if (config['codex-api-key']) { - this.renderCodexKeys(config['codex-api-key']); - } - - // Claude 密钥 - if (config['claude-api-key']) { - this.renderClaudeKeys(config['claude-api-key']); - } - - // OpenAI 兼容提供商 - if (config['openai-compatibility']) { - this.renderOpenAIProviders(config['openai-compatibility']); - } - } - - // 回退方法:原来的逐个加载方式 - async loadAllDataLegacy() { - await Promise.all([ - this.loadDebugSettings(), - this.loadProxySettings(), - this.loadRetrySettings(), - this.loadQuotaSettings(), - this.loadApiKeys(), - this.loadGeminiKeys(), - this.loadCodexKeys(), - this.loadClaudeKeys(), - this.loadOpenAIProviders(), - this.loadAuthFiles() - ]); - } - - // 加载调试设置 - async loadDebugSettings() { - try { - const config = await this.getConfig(); - if (config.debug !== undefined) { - document.getElementById('debug-toggle').checked = config.debug; - } - } catch (error) { - console.error('加载调试设置失败:', error); - } - } - - // 更新调试设置 - async updateDebug(enabled) { - try { - await this.makeRequest('/debug', { - method: 'PUT', - body: JSON.stringify({ value: enabled }) - }); - this.clearCache(); // 清除缓存 - this.showNotification(i18n.t('notification.debug_updated'), 'success'); - } catch (error) { - this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); - // 恢复原状态 - document.getElementById('debug-toggle').checked = !enabled; - } - } - - // 加载代理设置 - async loadProxySettings() { - try { - const config = await this.getConfig(); - if (config['proxy-url'] !== undefined) { - document.getElementById('proxy-url').value = config['proxy-url'] || ''; - } - } catch (error) { - console.error('加载代理设置失败:', error); - } - } - - // 更新代理URL - async updateProxyUrl() { - const proxyUrl = document.getElementById('proxy-url').value.trim(); - - try { - await this.makeRequest('/proxy-url', { - method: 'PUT', - body: JSON.stringify({ value: proxyUrl }) - }); - this.clearCache(); // 清除缓存 - this.showNotification(i18n.t('notification.proxy_updated'), 'success'); - } catch (error) { - this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); - } - } - - // 清空代理URL - async clearProxyUrl() { - try { - await this.makeRequest('/proxy-url', { method: 'DELETE' }); - document.getElementById('proxy-url').value = ''; - this.clearCache(); // 清除缓存 - this.showNotification(i18n.t('notification.proxy_cleared'), 'success'); - } catch (error) { - this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); - } - } - - // 加载重试设置 - async loadRetrySettings() { - try { - const config = await this.getConfig(); - if (config['request-retry'] !== undefined) { - document.getElementById('request-retry').value = config['request-retry']; - } - } catch (error) { - console.error('加载重试设置失败:', error); - } - } - - // 更新请求重试 - async updateRequestRetry() { - const retryCount = parseInt(document.getElementById('request-retry').value); - - try { - await this.makeRequest('/request-retry', { - method: 'PUT', - body: JSON.stringify({ value: retryCount }) - }); - this.clearCache(); // 清除缓存 - this.showNotification(i18n.t('notification.retry_updated'), 'success'); - } catch (error) { - this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); - } - } - - // 加载配额设置 - async loadQuotaSettings() { - try { - const config = await this.getConfig(); - if (config['quota-exceeded']) { - if (config['quota-exceeded']['switch-project'] !== undefined) { - document.getElementById('switch-project-toggle').checked = config['quota-exceeded']['switch-project']; - } - if (config['quota-exceeded']['switch-preview-model'] !== undefined) { - document.getElementById('switch-preview-model-toggle').checked = config['quota-exceeded']['switch-preview-model']; - } - } - } catch (error) { - console.error('加载配额设置失败:', error); - } - } - - // 更新项目切换设置 - async updateSwitchProject(enabled) { - try { - await this.makeRequest('/quota-exceeded/switch-project', { - method: 'PUT', - body: JSON.stringify({ value: enabled }) - }); - this.clearCache(); // 清除缓存 - this.showNotification(i18n.t('notification.quota_switch_project_updated'), 'success'); - } catch (error) { - this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); - document.getElementById('switch-project-toggle').checked = !enabled; - } - } - - // 更新预览模型切换设置 - async updateSwitchPreviewModel(enabled) { - try { - await this.makeRequest('/quota-exceeded/switch-preview-model', { - method: 'PUT', - body: JSON.stringify({ value: enabled }) - }); - this.clearCache(); // 清除缓存 - this.showNotification(i18n.t('notification.quota_switch_preview_updated'), 'success'); - } catch (error) { - this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); - document.getElementById('switch-preview-model-toggle').checked = !enabled; - } - } - - - // 加载API密钥 - async loadApiKeys() { - try { - const config = await this.getConfig(); - if (config['api-keys']) { - this.renderApiKeys(config['api-keys']); - } - } catch (error) { - console.error('加载API密钥失败:', error); - } - } - - // 渲染API密钥列表 - renderApiKeys(keys) { - const container = document.getElementById('api-keys-list'); - - if (keys.length === 0) { - container.innerHTML = ` -
- -

${i18n.t('api_keys.empty_title')}

-

${i18n.t('api_keys.empty_desc')}

-
- `; - return; - } - - container.innerHTML = keys.map((key, index) => ` -
-
-
${i18n.t('api_keys.item_title')} #${index + 1}
-
${this.maskApiKey(key)}
-
-
- - -
-
- `).join(''); - } - - // 遮蔽API密钥显示 - maskApiKey(key) { - if (key.length <= 8) return key; - return key.substring(0, 4) + '...' + key.substring(key.length - 4); - } - - // 显示添加API密钥模态框 - showAddApiKeyModal() { - const modal = document.getElementById('modal'); - const modalBody = document.getElementById('modal-body'); - - modalBody.innerHTML = ` -

${i18n.t('api_keys.add_modal_title')}

-
- - -
- - `; - - modal.style.display = 'block'; - } - - // 添加API密钥 - async addApiKey() { - const newKey = document.getElementById('new-api-key').value.trim(); - - if (!newKey) { - this.showNotification(`${i18n.t('notification.please_enter')} ${i18n.t('notification.api_key')}`, 'error'); - return; - } - - try { - const data = await this.makeRequest('/api-keys'); - const currentKeys = data['api-keys'] || []; - currentKeys.push(newKey); - - await this.makeRequest('/api-keys', { - method: 'PUT', - body: JSON.stringify(currentKeys) - }); - - this.clearCache(); // 清除缓存 - this.closeModal(); - this.loadApiKeys(); - this.showNotification(i18n.t('notification.api_key_added'), 'success'); - } catch (error) { - this.showNotification(`${i18n.t('notification.add_failed')}: ${error.message}`, 'error'); - } - } - - // 编辑API密钥 - editApiKey(index, currentKey) { - const modal = document.getElementById('modal'); - const modalBody = document.getElementById('modal-body'); - - modalBody.innerHTML = ` -

${i18n.t('api_keys.edit_modal_title')}

-
- - -
- - `; - - modal.style.display = 'block'; - } - - // 更新API密钥 - async updateApiKey(index) { - const newKey = document.getElementById('edit-api-key').value.trim(); - - if (!newKey) { - this.showNotification(`${i18n.t('notification.please_enter')} ${i18n.t('notification.api_key')}`, 'error'); - return; - } - - try { - await this.makeRequest('/api-keys', { - method: 'PATCH', - body: JSON.stringify({ index, value: newKey }) - }); - - this.clearCache(); // 清除缓存 - this.closeModal(); - this.loadApiKeys(); - this.showNotification(i18n.t('notification.api_key_updated'), 'success'); - } catch (error) { - this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); - } - } - - // 删除API密钥 - async deleteApiKey(index) { - if (!confirm(i18n.t('api_keys.delete_confirm'))) return; - - try { - await this.makeRequest(`/api-keys?index=${index}`, { method: 'DELETE' }); - this.clearCache(); // 清除缓存 - this.loadApiKeys(); - this.showNotification(i18n.t('notification.api_key_deleted'), 'success'); - } catch (error) { - this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error'); - } - } - - // 加载Gemini密钥 - async loadGeminiKeys() { - try { - const config = await this.getConfig(); - if (config['generative-language-api-key']) { - this.renderGeminiKeys(config['generative-language-api-key']); - } - } catch (error) { - console.error('加载Gemini密钥失败:', error); - } - } - - // 渲染Gemini密钥列表 - renderGeminiKeys(keys) { - const container = document.getElementById('gemini-keys-list'); - - if (keys.length === 0) { - container.innerHTML = ` -
- -

${i18n.t('ai_providers.gemini_empty_title')}

-

${i18n.t('ai_providers.gemini_empty_desc')}

-
- `; - return; - } - - container.innerHTML = keys.map((key, index) => ` -
-
-
${i18n.t('ai_providers.gemini_item_title')} #${index + 1}
-
${this.maskApiKey(key)}
-
-
- - -
-
- `).join(''); - } - - // 显示添加Gemini密钥模态框 - showAddGeminiKeyModal() { - const modal = document.getElementById('modal'); - const modalBody = document.getElementById('modal-body'); - - modalBody.innerHTML = ` -

添加Gemini API密钥

-
- - -
- - `; - - modal.style.display = 'block'; - } - - // 添加Gemini密钥 - async addGeminiKey() { - const newKey = document.getElementById('new-gemini-key').value.trim(); - - if (!newKey) { - this.showNotification('请输入Gemini API密钥', 'error'); - return; - } - - try { - const data = await this.makeRequest('/generative-language-api-key'); - const currentKeys = data['generative-language-api-key'] || []; - currentKeys.push(newKey); - - await this.makeRequest('/generative-language-api-key', { - method: 'PUT', - body: JSON.stringify(currentKeys) - }); - - this.clearCache(); // 清除缓存 - this.closeModal(); - this.loadGeminiKeys(); - this.showNotification('Gemini密钥添加成功', 'success'); - } catch (error) { - this.showNotification(`添加Gemini密钥失败: ${error.message}`, 'error'); - } - } - - // 编辑Gemini密钥 - editGeminiKey(index, currentKey) { - const modal = document.getElementById('modal'); - const modalBody = document.getElementById('modal-body'); - - modalBody.innerHTML = ` -

编辑Gemini API密钥

-
- - -
- - `; - - modal.style.display = 'block'; - } - - // 更新Gemini密钥 - async updateGeminiKey(oldKey) { - const newKey = document.getElementById('edit-gemini-key').value.trim(); - - if (!newKey) { - this.showNotification('请输入Gemini API密钥', 'error'); - return; - } - - try { - await this.makeRequest('/generative-language-api-key', { - method: 'PATCH', - body: JSON.stringify({ old: oldKey, new: newKey }) - }); - - this.clearCache(); // 清除缓存 - this.closeModal(); - this.loadGeminiKeys(); - this.showNotification('Gemini密钥更新成功', 'success'); - } catch (error) { - this.showNotification(`更新Gemini密钥失败: ${error.message}`, 'error'); - } - } - - // 删除Gemini密钥 - async deleteGeminiKey(key) { - if (!confirm(i18n.t('ai_providers.gemini_delete_confirm'))) return; - - try { - await this.makeRequest(`/generative-language-api-key?value=${encodeURIComponent(key)}`, { method: 'DELETE' }); - this.clearCache(); // 清除缓存 - this.loadGeminiKeys(); - this.showNotification('Gemini密钥删除成功', 'success'); - } catch (error) { - this.showNotification(`删除Gemini密钥失败: ${error.message}`, 'error'); - } - } - - // 加载Codex密钥 - async loadCodexKeys() { - try { - const config = await this.getConfig(); - if (config['codex-api-key']) { - this.renderCodexKeys(config['codex-api-key']); - } - } catch (error) { - console.error('加载Codex密钥失败:', error); - } - } - - // 渲染Codex密钥列表 - renderCodexKeys(keys) { - const container = document.getElementById('codex-keys-list'); - - if (keys.length === 0) { - container.innerHTML = ` -
- -

${i18n.t('ai_providers.codex_empty_title')}

-

${i18n.t('ai_providers.codex_empty_desc')}

-
- `; - return; - } - - container.innerHTML = keys.map((config, index) => ` -
-
-
${i18n.t('ai_providers.codex_item_title')} #${index + 1}
-
${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}
- ${config['base-url'] ? `
${i18n.t('common.base_url')}: ${config['base-url']}
` : ''} -
-
- - -
-
- `).join(''); - } - - // 显示添加Codex密钥模态框 - showAddCodexKeyModal() { - const modal = document.getElementById('modal'); - const modalBody = document.getElementById('modal-body'); - - modalBody.innerHTML = ` -

添加Codex API配置

-
- - -
-
- - -
- - `; - - modal.style.display = 'block'; - } - - // 添加Codex密钥 - async addCodexKey() { - const apiKey = document.getElementById('new-codex-key').value.trim(); - const baseUrl = document.getElementById('new-codex-url').value.trim(); - - if (!apiKey) { - this.showNotification('请输入API密钥', 'error'); - return; - } - - try { - const data = await this.makeRequest('/codex-api-key'); - const currentKeys = data['codex-api-key'] || []; - - const newConfig = { 'api-key': apiKey }; - if (baseUrl) { - newConfig['base-url'] = baseUrl; - } - - currentKeys.push(newConfig); - - await this.makeRequest('/codex-api-key', { - method: 'PUT', - body: JSON.stringify(currentKeys) - }); - - this.clearCache(); // 清除缓存 - this.closeModal(); - this.loadCodexKeys(); - this.showNotification('Codex配置添加成功', 'success'); - } catch (error) { - this.showNotification(`添加Codex配置失败: ${error.message}`, 'error'); - } - } - - // 编辑Codex密钥 - editCodexKey(index, config) { - const modal = document.getElementById('modal'); - const modalBody = document.getElementById('modal-body'); - - modalBody.innerHTML = ` -

编辑Codex API配置

-
- - -
-
- - -
- - `; - - modal.style.display = 'block'; - } - - // 更新Codex密钥 - async updateCodexKey(index) { - const apiKey = document.getElementById('edit-codex-key').value.trim(); - const baseUrl = document.getElementById('edit-codex-url').value.trim(); - - if (!apiKey) { - this.showNotification('请输入API密钥', 'error'); - return; - } - - try { - const newConfig = { 'api-key': apiKey }; - if (baseUrl) { - newConfig['base-url'] = baseUrl; - } - - await this.makeRequest('/codex-api-key', { - method: 'PATCH', - body: JSON.stringify({ index, value: newConfig }) - }); - - this.clearCache(); // 清除缓存 - this.closeModal(); - this.loadCodexKeys(); - this.showNotification('Codex配置更新成功', 'success'); - } catch (error) { - this.showNotification(`更新Codex配置失败: ${error.message}`, 'error'); - } - } - - // 删除Codex密钥 - async deleteCodexKey(apiKey) { - if (!confirm(i18n.t('ai_providers.codex_delete_confirm'))) return; - - try { - await this.makeRequest(`/codex-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' }); - this.clearCache(); // 清除缓存 - this.loadCodexKeys(); - this.showNotification('Codex配置删除成功', 'success'); - } catch (error) { - this.showNotification(`删除Codex配置失败: ${error.message}`, 'error'); - } - } - - // 加载Claude密钥 - async loadClaudeKeys() { - try { - const config = await this.getConfig(); - if (config['claude-api-key']) { - this.renderClaudeKeys(config['claude-api-key']); - } - } catch (error) { - console.error('加载Claude密钥失败:', error); - } - } - - // 渲染Claude密钥列表 - renderClaudeKeys(keys) { - const container = document.getElementById('claude-keys-list'); - - if (keys.length === 0) { - container.innerHTML = ` -
- -

${i18n.t('ai_providers.claude_empty_title')}

-

${i18n.t('ai_providers.claude_empty_desc')}

-
- `; - return; - } - - container.innerHTML = keys.map((config, index) => ` -
-
-
${i18n.t('ai_providers.claude_item_title')} #${index + 1}
-
${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}
- ${config['base-url'] ? `
${i18n.t('common.base_url')}: ${config['base-url']}
` : ''} -
-
- - -
-
- `).join(''); - } - - // 显示添加Claude密钥模态框 - showAddClaudeKeyModal() { - const modal = document.getElementById('modal'); - const modalBody = document.getElementById('modal-body'); - - modalBody.innerHTML = ` -

添加Claude API配置

-
- - -
-
- - -
- - `; - - modal.style.display = 'block'; - } - - // 添加Claude密钥 - async addClaudeKey() { - const apiKey = document.getElementById('new-claude-key').value.trim(); - const baseUrl = document.getElementById('new-claude-url').value.trim(); - - if (!apiKey) { - this.showNotification('请输入API密钥', 'error'); - return; - } - - try { - const data = await this.makeRequest('/claude-api-key'); - const currentKeys = data['claude-api-key'] || []; - - const newConfig = { 'api-key': apiKey }; - if (baseUrl) { - newConfig['base-url'] = baseUrl; - } - - currentKeys.push(newConfig); - - await this.makeRequest('/claude-api-key', { - method: 'PUT', - body: JSON.stringify(currentKeys) - }); - - this.clearCache(); // 清除缓存 - this.closeModal(); - this.loadClaudeKeys(); - this.showNotification('Claude配置添加成功', 'success'); - } catch (error) { - this.showNotification(`添加Claude配置失败: ${error.message}`, 'error'); - } - } - - // 编辑Claude密钥 - editClaudeKey(index, config) { - const modal = document.getElementById('modal'); - const modalBody = document.getElementById('modal-body'); - - modalBody.innerHTML = ` -

编辑Claude API配置

-
- - -
-
- - -
- - `; - - modal.style.display = 'block'; - } - - // 更新Claude密钥 - async updateClaudeKey(index) { - const apiKey = document.getElementById('edit-claude-key').value.trim(); - const baseUrl = document.getElementById('edit-claude-url').value.trim(); - - if (!apiKey) { - this.showNotification('请输入API密钥', 'error'); - return; - } - - try { - const newConfig = { 'api-key': apiKey }; - if (baseUrl) { - newConfig['base-url'] = baseUrl; - } - - await this.makeRequest('/claude-api-key', { - method: 'PATCH', - body: JSON.stringify({ index, value: newConfig }) - }); - - this.clearCache(); // 清除缓存 - this.closeModal(); - this.loadClaudeKeys(); - this.showNotification('Claude配置更新成功', 'success'); - } catch (error) { - this.showNotification(`更新Claude配置失败: ${error.message}`, 'error'); - } - } - - // 删除Claude密钥 - async deleteClaudeKey(apiKey) { - if (!confirm(i18n.t('ai_providers.claude_delete_confirm'))) return; - - try { - await this.makeRequest(`/claude-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' }); - this.clearCache(); // 清除缓存 - this.loadClaudeKeys(); - this.showNotification('Claude配置删除成功', 'success'); - } catch (error) { - this.showNotification(`删除Claude配置失败: ${error.message}`, 'error'); - } - } - - // 加载OpenAI提供商 - async loadOpenAIProviders() { - try { - const config = await this.getConfig(); - if (config['openai-compatibility']) { - this.renderOpenAIProviders(config['openai-compatibility']); - } - } catch (error) { - console.error('加载OpenAI提供商失败:', error); - } - } - - // 渲染OpenAI提供商列表 - renderOpenAIProviders(providers) { - const container = document.getElementById('openai-providers-list'); - - if (providers.length === 0) { - container.innerHTML = ` -
- -

${i18n.t('ai_providers.openai_empty_title')}

-

${i18n.t('ai_providers.openai_empty_desc')}

-
- `; - return; - } - - container.innerHTML = providers.map((provider, index) => ` -
-
-
${provider.name}
-
${i18n.t('common.base_url')}: ${provider['base-url']}
-
${i18n.t('ai_providers.openai_keys_count')}: ${(provider['api-keys'] || []).length}
-
${i18n.t('ai_providers.openai_models_count')}: ${(provider.models || []).length}
-
-
- - -
-
- `).join(''); - } - - // 显示添加OpenAI提供商模态框 - showAddOpenAIProviderModal() { - const modal = document.getElementById('modal'); - const modalBody = document.getElementById('modal-body'); - - modalBody.innerHTML = ` -

添加OpenAI兼容提供商

-
- - -
-
- - -
-
- - -
- - `; - - modal.style.display = 'block'; - } - - // 添加OpenAI提供商 - async addOpenAIProvider() { - const name = document.getElementById('new-provider-name').value.trim(); - const baseUrl = document.getElementById('new-provider-url').value.trim(); - const keysText = document.getElementById('new-provider-keys').value.trim(); - - if (!name || !baseUrl) { - this.showNotification('请填写提供商名称和Base URL', 'error'); - return; - } - - try { - const data = await this.makeRequest('/openai-compatibility'); - const currentProviders = data['openai-compatibility'] || []; - - const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : []; - - const newProvider = { - name, - 'base-url': baseUrl, - 'api-keys': apiKeys, - models: [] - }; - - currentProviders.push(newProvider); - - await this.makeRequest('/openai-compatibility', { - method: 'PUT', - body: JSON.stringify(currentProviders) - }); - - this.clearCache(); // 清除缓存 - this.closeModal(); - this.loadOpenAIProviders(); - this.showNotification('OpenAI提供商添加成功', 'success'); - } catch (error) { - this.showNotification(`添加OpenAI提供商失败: ${error.message}`, 'error'); - } - } - - // 编辑OpenAI提供商 - editOpenAIProvider(index, provider) { - const modal = document.getElementById('modal'); - const modalBody = document.getElementById('modal-body'); - - const apiKeysText = (provider['api-keys'] || []).join('\n'); - - modalBody.innerHTML = ` -

编辑OpenAI兼容提供商

-
- - -
-
- - -
-
- - -
- - `; - - modal.style.display = 'block'; - } - - // 更新OpenAI提供商 - async updateOpenAIProvider(index) { - const name = document.getElementById('edit-provider-name').value.trim(); - const baseUrl = document.getElementById('edit-provider-url').value.trim(); - const keysText = document.getElementById('edit-provider-keys').value.trim(); - - if (!name || !baseUrl) { - this.showNotification('请填写提供商名称和Base URL', 'error'); - return; - } - - try { - const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : []; - - const updatedProvider = { - name, - 'base-url': baseUrl, - 'api-keys': apiKeys, - models: [] - }; - - await this.makeRequest('/openai-compatibility', { - method: 'PATCH', - body: JSON.stringify({ index, value: updatedProvider }) - }); - - this.clearCache(); // 清除缓存 - this.closeModal(); - this.loadOpenAIProviders(); - this.showNotification('OpenAI提供商更新成功', 'success'); - } catch (error) { - this.showNotification(`更新OpenAI提供商失败: ${error.message}`, 'error'); - } - } - - // 删除OpenAI提供商 - async deleteOpenAIProvider(name) { - if (!confirm(i18n.t('ai_providers.openai_delete_confirm'))) return; - - try { - await this.makeRequest(`/openai-compatibility?name=${encodeURIComponent(name)}`, { method: 'DELETE' }); - this.clearCache(); // 清除缓存 - this.loadOpenAIProviders(); - this.showNotification('OpenAI提供商删除成功', 'success'); - } catch (error) { - this.showNotification(`删除OpenAI提供商失败: ${error.message}`, 'error'); - } - } - - // 加载认证文件 - async loadAuthFiles() { - try { - const data = await this.makeRequest('/auth-files'); - this.renderAuthFiles(data.files || []); - } catch (error) { - console.error('加载认证文件失败:', error); - } - } - - // 渲染认证文件列表 - renderAuthFiles(files) { - const container = document.getElementById('auth-files-list'); - - if (files.length === 0) { - container.innerHTML = ` -
- -

${i18n.t('auth_files.empty_title')}

-

${i18n.t('auth_files.empty_desc')}

-
- `; - return; - } - - container.innerHTML = files.map(file => ` -
-
-
${file.name}
-
${i18n.t('auth_files.file_size')}: ${this.formatFileSize(file.size)}
-
${i18n.t('auth_files.file_modified')}: ${new Date(file.modtime).toLocaleString(i18n.currentLanguage === 'zh-CN' ? 'zh-CN' : 'en-US')}
-
-
- - -
-
- `).join(''); - } - - // 格式化文件大小 - formatFileSize(bytes) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - } - - // 上传认证文件 - uploadAuthFile() { - document.getElementById('auth-file-input').click(); - } - - // 处理文件上传 - async handleFileUpload(event) { - const file = event.target.files[0]; - if (!file) return; - - if (!file.name.endsWith('.json')) { - this.showNotification(i18n.t('auth_files.upload_error_json'), 'error'); - return; - } - - try { - const formData = new FormData(); - formData.append('file', file); - - const response = await fetch(`${this.apiUrl}/auth-files`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${this.managementKey}` - }, - body: formData - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error || `HTTP ${response.status}`); - } - - this.clearCache(); // 清除缓存 - this.loadAuthFiles(); - this.showNotification(i18n.t('auth_files.upload_success'), 'success'); - } catch (error) { - this.showNotification(`文件上传失败: ${error.message}`, 'error'); - } - - // 清空文件输入 - event.target.value = ''; - } - - // 下载认证文件 - async downloadAuthFile(filename) { - try { - const response = await fetch(`${this.apiUrl}/auth-files/download?name=${encodeURIComponent(filename)}`, { - headers: { - 'Authorization': `Bearer ${this.managementKey}` - } - }); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.click(); - window.URL.revokeObjectURL(url); - - this.showNotification(i18n.t('auth_files.download_success'), 'success'); - } catch (error) { - this.showNotification(`文件下载失败: ${error.message}`, 'error'); - } - } - - // 删除认证文件 - async deleteAuthFile(filename) { - if (!confirm(`${i18n.t('auth_files.delete_confirm')} "${filename}" 吗?`)) return; - - try { - await this.makeRequest(`/auth-files?name=${encodeURIComponent(filename)}`, { method: 'DELETE' }); - this.clearCache(); // 清除缓存 - this.loadAuthFiles(); - this.showNotification(i18n.t('auth_files.delete_success'), 'success'); - } catch (error) { - this.showNotification(`文件删除失败: ${error.message}`, 'error'); - } - } - - // 删除所有认证文件 - async deleteAllAuthFiles() { - if (!confirm(i18n.t('auth_files.delete_all_confirm'))) return; - - try { - const response = await this.makeRequest('/auth-files?all=true', { method: 'DELETE' }); - this.clearCache(); // 清除缓存 - this.loadAuthFiles(); - this.showNotification(`${i18n.t('auth_files.delete_all_success')} ${response.deleted} ${i18n.t('auth_files.files_count')}`, 'success'); - } catch (error) { - this.showNotification(`删除文件失败: ${error.message}`, 'error'); - } - } - - - - - - // 显示 Gemini Web Token 模态框 - showGeminiWebTokenModal() { - const inlineSecure1psid = document.getElementById('secure-1psid-input'); - const inlineSecure1psidts = document.getElementById('secure-1psidts-input'); - const inlineLabel = document.getElementById('gemini-web-label-input'); - const modalBody = document.getElementById('modal-body'); - modalBody.innerHTML = ` -

${i18n.t('auth_login.gemini_web_button')}

-
-
- - -
从浏览器开发者工具 → Application → Cookies 中获取
-
-
- - -
从浏览器开发者工具 → Application → Cookies 中获取
-
-
- - -
为此认证文件设置一个标签名称(可选)
-
- -
- `; - this.showModal(); - - const modalSecure1psid = document.getElementById('modal-secure-1psid'); - const modalSecure1psidts = document.getElementById('modal-secure-1psidts'); - const modalLabel = document.getElementById('modal-gemini-web-label'); - - if (modalSecure1psid && inlineSecure1psid) { - modalSecure1psid.value = inlineSecure1psid.value.trim(); - } - if (modalSecure1psidts && inlineSecure1psidts) { - modalSecure1psidts.value = inlineSecure1psidts.value.trim(); - } - if (modalLabel && inlineLabel) { - modalLabel.value = inlineLabel.value.trim(); - } - - if (modalSecure1psid) { - modalSecure1psid.focus(); - } - } - - // 保存 Gemini Web Token - async saveGeminiWebToken() { - const secure1psid = document.getElementById('modal-secure-1psid').value.trim(); - const secure1psidts = document.getElementById('modal-secure-1psidts').value.trim(); - const label = document.getElementById('modal-gemini-web-label').value.trim(); - - if (!secure1psid || !secure1psidts) { - this.showNotification('请填写完整的 Cookie 信息', 'error'); - return; - } - - try { - const requestBody = { - secure_1psid: secure1psid, - secure_1psidts: secure1psidts - }; - - // 如果提供了 label,则添加到请求体中 - if (label) { - requestBody.label = label; - } - - const response = await this.makeRequest('/gemini-web-token', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(requestBody) - }); - - this.closeModal(); - this.loadAuthFiles(); // 刷新认证文件列表 - const inlineSecure1psid = document.getElementById('secure-1psid-input'); - const inlineSecure1psidts = document.getElementById('secure-1psidts-input'); - const inlineLabel = document.getElementById('gemini-web-label-input'); - if (inlineSecure1psid) { - inlineSecure1psid.value = secure1psid; - } - if (inlineSecure1psidts) { - inlineSecure1psidts.value = secure1psidts; - } - if (inlineLabel) { - inlineLabel.value = label; - } - this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success'); - } catch (error) { - this.showNotification(`保存失败: ${error.message}`, 'error'); - } - } - - // ===== 使用统计相关方法 ===== - - // 初始化图表变量 - requestsChart = null; - tokensChart = null; - currentUsageData = null; - - // 加载使用统计 - async loadUsageStats() { - try { - const response = await this.makeRequest('/usage'); - const usage = response?.usage || null; - this.currentUsageData = usage; - - if (!usage) { - throw new Error('usage payload missing'); - } - - // 更新概览卡片 - this.updateUsageOverview(usage); - - // 读取当前图表周期 - const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active'); - const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active'); - const requestsPeriod = requestsHourActive ? 'hour' : 'day'; - const tokensPeriod = tokensHourActive ? 'hour' : 'day'; - - // 初始化图表(使用当前周期) - this.initializeRequestsChart(requestsPeriod); - this.initializeTokensChart(tokensPeriod); - - // 更新API详细统计表格 - this.updateApiStatsTable(usage); - - } catch (error) { - console.error('加载使用统计失败:', error); - this.currentUsageData = null; - - // 清空概览数据 - ['total-requests', 'success-requests', 'failed-requests', 'total-tokens'].forEach(id => { - const el = document.getElementById(id); - if (el) el.textContent = '-'; - }); - - // 清空图表 - if (this.requestsChart) { - this.requestsChart.destroy(); - this.requestsChart = null; - } - if (this.tokensChart) { - this.tokensChart.destroy(); - this.tokensChart = null; - } - - const tableElement = document.getElementById('api-stats-table'); - if (tableElement) { - tableElement.innerHTML = `
${i18n.t('usage_stats.loading_error')}: ${error.message}
`; - } - } - } - - // 更新使用统计概览 - updateUsageOverview(data) { - const safeData = data || {}; - document.getElementById('total-requests').textContent = safeData.total_requests ?? 0; - document.getElementById('success-requests').textContent = safeData.success_count ?? 0; - document.getElementById('failed-requests').textContent = safeData.failure_count ?? 0; - document.getElementById('total-tokens').textContent = safeData.total_tokens ?? 0; - } - - // 初始化图表 - initializeCharts() { - const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active'); - const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active'); - this.initializeRequestsChart(requestsHourActive ? 'hour' : 'day'); - this.initializeTokensChart(tokensHourActive ? 'hour' : 'day'); - } - - // 初始化请求趋势图表 - initializeRequestsChart(period = 'day') { - const ctx = document.getElementById('requests-chart'); - if (!ctx) return; - - // 销毁现有图表 - if (this.requestsChart) { - this.requestsChart.destroy(); - } - - const data = this.getRequestsChartData(period); - - this.requestsChart = new Chart(ctx, { - type: 'line', - data: data, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: false - } - }, - scales: { - x: { - title: { - display: true, - text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day') - } - }, - y: { - beginAtZero: true, - title: { - display: true, - text: i18n.t('usage_stats.requests_count') - } - } - }, - elements: { - line: { - borderColor: '#3b82f6', - backgroundColor: 'rgba(59, 130, 246, 0.1)', - fill: true, - tension: 0.4 - }, - point: { - backgroundColor: '#3b82f6', - borderColor: '#ffffff', - borderWidth: 2, - radius: 4 - } - } - } - }); - } - - // 初始化Token使用趋势图表 - initializeTokensChart(period = 'day') { - const ctx = document.getElementById('tokens-chart'); - if (!ctx) return; - - // 销毁现有图表 - if (this.tokensChart) { - this.tokensChart.destroy(); - } - - const data = this.getTokensChartData(period); - - this.tokensChart = new Chart(ctx, { - type: 'line', - data: data, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - display: false - } - }, - scales: { - x: { - title: { - display: true, - text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day') - } - }, - y: { - beginAtZero: true, - title: { - display: true, - text: i18n.t('usage_stats.tokens_count') - } - } - }, - elements: { - line: { - borderColor: '#10b981', - backgroundColor: 'rgba(16, 185, 129, 0.1)', - fill: true, - tension: 0.4 - }, - point: { - backgroundColor: '#10b981', - borderColor: '#ffffff', - borderWidth: 2, - radius: 4 - } - } - } - }); - } - - // 获取请求图表数据 - getRequestsChartData(period) { - if (!this.currentUsageData) { - return { labels: [], datasets: [{ data: [] }] }; - } - - let dataSource, labels, values; - - if (period === 'hour') { - dataSource = this.currentUsageData.requests_by_hour || {}; - labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')); - values = labels.map(hour => dataSource[hour] || 0); - } else { - dataSource = this.currentUsageData.requests_by_day || {}; - labels = Object.keys(dataSource).sort(); - values = labels.map(day => dataSource[day] || 0); - } - - return { - labels: labels, - datasets: [{ - data: values - }] - }; - } - - // 获取Token图表数据 - getTokensChartData(period) { - if (!this.currentUsageData) { - return { labels: [], datasets: [{ data: [] }] }; - } - - let dataSource, labels, values; - - if (period === 'hour') { - dataSource = this.currentUsageData.tokens_by_hour || {}; - labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')); - values = labels.map(hour => dataSource[hour] || 0); - } else { - dataSource = this.currentUsageData.tokens_by_day || {}; - labels = Object.keys(dataSource).sort(); - values = labels.map(day => dataSource[day] || 0); - } - - return { - labels: labels, - datasets: [{ - data: values - }] - }; - } - - // 切换请求图表时间周期 - switchRequestsPeriod(period) { - // 更新按钮状态 - document.getElementById('requests-hour-btn').classList.toggle('active', period === 'hour'); - document.getElementById('requests-day-btn').classList.toggle('active', period === 'day'); - - // 更新图表数据 - if (this.requestsChart) { - const newData = this.getRequestsChartData(period); - this.requestsChart.data = newData; - this.requestsChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day'); - this.requestsChart.update(); - } - } - - // 切换Token图表时间周期 - switchTokensPeriod(period) { - // 更新按钮状态 - document.getElementById('tokens-hour-btn').classList.toggle('active', period === 'hour'); - document.getElementById('tokens-day-btn').classList.toggle('active', period === 'day'); - - // 更新图表数据 - if (this.tokensChart) { - const newData = this.getTokensChartData(period); - this.tokensChart.data = newData; - this.tokensChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day'); - this.tokensChart.update(); - } - } - - // 更新API详细统计表格 - updateApiStatsTable(data) { - const container = document.getElementById('api-stats-table'); - if (!container) return; - - const apis = data.apis || {}; - - if (Object.keys(apis).length === 0) { - container.innerHTML = `
${i18n.t('usage_stats.no_data')}
`; - return; - } - - let tableHtml = ` - - - - - - - - - - - - `; - - Object.entries(apis).forEach(([endpoint, apiData]) => { - const totalRequests = apiData.total_requests || 0; - const successCount = apiData.success_count ?? null; - const successRate = successCount !== null && totalRequests > 0 - ? Math.round((successCount / totalRequests) * 100) - : null; - - // 构建模型详情 - let modelsHtml = ''; - if (apiData.models && Object.keys(apiData.models).length > 0) { - modelsHtml = '
'; - Object.entries(apiData.models).forEach(([modelName, modelData]) => { - const modelRequests = modelData.total_requests ?? 0; - const modelTokens = modelData.total_tokens ?? 0; - modelsHtml += ` -
- ${modelName} - ${modelRequests} 请求 / ${modelTokens} tokens -
- `; - }); - modelsHtml += '
'; - } - - tableHtml += ` - - - - - - - - `; - }); - - tableHtml += '
${i18n.t('usage_stats.api_endpoint')}${i18n.t('usage_stats.requests_count')}${i18n.t('usage_stats.tokens_count')}${i18n.t('usage_stats.success_rate')}${i18n.t('usage_stats.models')}
${endpoint}${totalRequests}${apiData.total_tokens || 0}${successRate !== null ? successRate + '%' : '-'}${modelsHtml || '-'}
'; - container.innerHTML = tableHtml; - } - - showModal() { - const modal = document.getElementById('modal'); - if (modal) { - modal.style.display = 'block'; - } - } - - // 关闭模态框 - closeModal() { - document.getElementById('modal').style.display = 'none'; - } - - detectApiBaseFromLocation() { - try { - const { protocol, hostname, port } = window.location; - const normalizedPort = port ? `:${port}` : ''; - return this.normalizeBase(`${protocol}//${hostname}${normalizedPort}`); - } catch (error) { - console.warn('无法从当前地址检测 API 基础地址,使用默认设置', error); - return this.normalizeBase(this.apiBase || 'http://localhost:8317'); - } - } - - updateLoginConnectionInfo() { - const connectionUrlElement = document.getElementById('login-connection-url'); - const customInput = document.getElementById('login-api-base'); - if (connectionUrlElement) { - connectionUrlElement.textContent = this.apiBase || '-'; - } - if (customInput && customInput !== document.activeElement) { - customInput.value = this.apiBase || ''; - } - } -} - -// 全局管理器实例 -let manager; - -// 尝试自动加载根目录 Logo(支持多种常见文件名/扩展名) -function setupSiteLogo() { - const img = document.getElementById('site-logo'); - const loginImg = document.getElementById('login-logo'); - if (!img && !loginImg) return; - - const inlineLogo = typeof window !== 'undefined' ? window.__INLINE_LOGO__ : null; - if (inlineLogo) { - if (img) { - img.src = inlineLogo; - img.style.display = 'inline-block'; - } - if (loginImg) { - loginImg.src = inlineLogo; - loginImg.style.display = 'inline-block'; - } - return; - } - - const candidates = [ - '../logo.svg', '../logo.png', '../logo.jpg', '../logo.jpeg', '../logo.webp', '../logo.gif', - 'logo.svg', 'logo.png', 'logo.jpg', 'logo.jpeg', 'logo.webp', 'logo.gif', - '/logo.svg', '/logo.png', '/logo.jpg', '/logo.jpeg', '/logo.webp', '/logo.gif' - ]; - let idx = 0; - const tryNext = () => { - if (idx >= candidates.length) return; - const test = new Image(); - test.onload = () => { - if (img) { - img.src = test.src; - img.style.display = 'inline-block'; - } - if (loginImg) { - loginImg.src = test.src; - loginImg.style.display = 'inline-block'; - } - }; - test.onerror = () => { - idx++; - tryNext(); - }; - test.src = candidates[idx]; - }; - tryNext(); -} - -// 页面加载完成后初始化 -document.addEventListener('DOMContentLoaded', () => { - // 初始化国际化 - i18n.init(); - - setupSiteLogo(); - manager = new CLIProxyManager(); -}); +// CLI Proxy API 管理界面 JavaScript +class CLIProxyManager { + constructor() { + // 仅保存基础地址(不含 /v0/management),请求时自动补齐 + const detectedBase = this.detectApiBaseFromLocation(); + this.apiBase = detectedBase; + this.apiUrl = this.computeApiUrl(this.apiBase); + this.managementKey = ''; + this.isConnected = false; + this.isLoggedIn = false; + + // 配置缓存 + this.configCache = null; + this.cacheTimestamp = null; + this.cacheExpiry = 30000; // 30秒缓存过期时间 + + // 状态更新定时器 + this.statusUpdateTimer = null; + + // 主题管理 + this.currentTheme = 'light'; + + this.init(); + } + + // 简易防抖,减少频繁写 localStorage + debounce(fn, delay = 400) { + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => fn.apply(this, args), delay); + }; + } + + // 初始化主题 + initializeTheme() { + // 从本地存储获取用户偏好主题 + const savedTheme = localStorage.getItem('preferredTheme'); + if (savedTheme && ['light', 'dark'].includes(savedTheme)) { + this.currentTheme = savedTheme; + } else { + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + this.currentTheme = 'dark'; + } else { + this.currentTheme = 'light'; + } + } + + this.applyTheme(this.currentTheme); + this.updateThemeButtons(); + + // 监听系统主题变化 + if (window.matchMedia) { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + if (!localStorage.getItem('preferredTheme')) { + this.currentTheme = e.matches ? 'dark' : 'light'; + this.applyTheme(this.currentTheme); + this.updateThemeButtons(); + } + }); + } + } + + // 应用主题 + applyTheme(theme) { + if (theme === 'dark') { + document.documentElement.setAttribute('data-theme', 'dark'); + } else { + document.documentElement.removeAttribute('data-theme'); + } + this.currentTheme = theme; + } + + // 切换主题 + toggleTheme() { + const newTheme = this.currentTheme === 'light' ? 'dark' : 'light'; + this.applyTheme(newTheme); + this.updateThemeButtons(); + localStorage.setItem('preferredTheme', newTheme); + } + + // 更新主题按钮状态 + updateThemeButtons() { + const loginThemeBtn = document.getElementById('theme-toggle'); + const mainThemeBtn = document.getElementById('theme-toggle-main'); + + const updateButton = (btn) => { + if (!btn) return; + const icon = btn.querySelector('i'); + if (this.currentTheme === 'dark') { + icon.className = 'fas fa-sun'; + btn.title = i18n.t('theme.switch_to_light'); + } else { + icon.className = 'fas fa-moon'; + btn.title = i18n.t('theme.switch_to_dark'); + } + }; + + updateButton(loginThemeBtn); + updateButton(mainThemeBtn); + } + + init() { + this.initializeTheme(); + this.checkLoginStatus(); + this.bindEvents(); + this.setupNavigation(); + this.setupLanguageSwitcher(); + this.setupThemeSwitcher(); + // loadSettings 将在登录成功后调用 + this.updateLoginConnectionInfo(); + } + + // 检查登录状态 + async checkLoginStatus() { + // 检查是否有保存的连接信息 + const savedBase = localStorage.getItem('apiBase'); + const savedKey = localStorage.getItem('managementKey'); + const wasLoggedIn = localStorage.getItem('isLoggedIn') === 'true'; + + // 如果有完整的连接信息且之前已登录,尝试自动登录 + if (savedBase && savedKey && wasLoggedIn) { + try { + console.log('检测到本地连接数据,尝试自动登录...'); + this.showAutoLoginLoading(); + await this.attemptAutoLogin(savedBase, savedKey); + return; // 自动登录成功,不显示登录页面 + } catch (error) { + console.log('自动登录失败:', error.message); + // 清除无效的登录状态 + localStorage.removeItem('isLoggedIn'); + this.hideAutoLoginLoading(); + } + } + + // 如果没有连接信息或自动登录失败,显示登录页面 + this.showLoginPage(); + this.loadLoginSettings(); + } + + // 显示自动登录加载页面 + showAutoLoginLoading() { + document.getElementById('auto-login-loading').style.display = 'flex'; + document.getElementById('login-page').style.display = 'none'; + document.getElementById('main-page').style.display = 'none'; + } + + // 隐藏自动登录加载页面 + hideAutoLoginLoading() { + document.getElementById('auto-login-loading').style.display = 'none'; + } + + // 尝试自动登录 + async attemptAutoLogin(apiBase, managementKey) { + try { + // 设置API基础地址和密钥 + this.setApiBase(apiBase); + this.managementKey = managementKey; + + // 恢复代理设置(如果有) + const savedProxy = localStorage.getItem('proxyUrl'); + if (savedProxy) { + // 代理设置会在后续的API请求中自动使用 + } + + // 测试连接 + await this.testConnection(); + + // 自动登录成功 + this.isLoggedIn = true; + this.hideAutoLoginLoading(); + this.showMainPage(); + + console.log('自动登录成功'); + return true; + } catch (error) { + console.error('自动登录失败:', error); + // 重置状态 + this.isLoggedIn = false; + this.isConnected = false; + throw error; + } + } + + // 显示登录页面 + showLoginPage() { + document.getElementById('login-page').style.display = 'flex'; + document.getElementById('main-page').style.display = 'none'; + this.isLoggedIn = false; + this.updateLoginConnectionInfo(); + } + + // 显示主页面 + showMainPage() { + document.getElementById('login-page').style.display = 'none'; + document.getElementById('main-page').style.display = 'block'; + this.isLoggedIn = true; + this.updateConnectionInfo(); + } + + // 登录验证 + async login(apiBase, managementKey) { + try { + // 设置API基础地址和密钥 + this.setApiBase(apiBase); + this.managementKey = managementKey; + localStorage.setItem('managementKey', this.managementKey); + + // 测试连接并加载所有数据 + await this.testConnection(); + + // 登录成功 + this.isLoggedIn = true; + localStorage.setItem('isLoggedIn', 'true'); + + this.showMainPage(); + // 不需要再调用loadSettings,因为内部状态已经在上面设置了 + + return true; + } catch (error) { + console.error('登录失败:', error); + throw error; + } + } + + // 登出 + logout() { + this.isLoggedIn = false; + this.isConnected = false; + this.clearCache(); + this.stopStatusUpdateTimer(); + + // 清除本地存储 + localStorage.removeItem('isLoggedIn'); + localStorage.removeItem('managementKey'); + + this.showLoginPage(); + } + + // 处理登录表单提交 + async handleLogin() { + const apiBaseInput = document.getElementById('login-api-base'); + const managementKeyInput = document.getElementById('login-management-key'); + const managementKey = managementKeyInput ? managementKeyInput.value.trim() : ''; + + if (!managementKey) { + this.showLoginError(i18n.t('login.error_required')); + return; + } + + if (apiBaseInput && apiBaseInput.value.trim()) { + this.setApiBase(apiBaseInput.value.trim()); + } + + const submitBtn = document.getElementById('login-submit'); + const originalText = submitBtn ? submitBtn.innerHTML : ''; + + try { + if (submitBtn) { + submitBtn.innerHTML = `
${i18n.t('login.submitting')}`; + submitBtn.disabled = true; + } + this.hideLoginError(); + + this.managementKey = managementKey; + localStorage.setItem('managementKey', this.managementKey); + + await this.login(this.apiBase, this.managementKey); + } catch (error) { + this.showLoginError(`${i18n.t('login.error_title')}: ${error.message}`); + } finally { + if (submitBtn) { + submitBtn.innerHTML = originalText; + submitBtn.disabled = false; + } + } + } + + // 切换登录页面密钥可见性 + toggleLoginKeyVisibility(button) { + const inputGroup = button.closest('.input-group'); + const keyInput = inputGroup.querySelector('input[type="password"], input[type="text"]'); + + if (keyInput.type === 'password') { + keyInput.type = 'text'; + button.innerHTML = ''; + } else { + keyInput.type = 'password'; + button.innerHTML = ''; + } + } + + // 显示登录错误 + showLoginError(message) { + const errorDiv = document.getElementById('login-error'); + const errorMessage = document.getElementById('login-error-message'); + + errorMessage.textContent = message; + errorDiv.style.display = 'flex'; + } + + // 隐藏登录错误 + hideLoginError() { + const errorDiv = document.getElementById('login-error'); + errorDiv.style.display = 'none'; + } + + // 更新连接信息显示 + updateConnectionInfo() { + const apiUrlElement = document.getElementById('display-api-url'); + const keyElement = document.getElementById('display-management-key'); + const statusElement = document.getElementById('display-connection-status'); + + // 显示API地址 + if (apiUrlElement) { + apiUrlElement.textContent = this.apiBase || '-'; + } + + // 显示密钥(遮蔽显示) + if (keyElement) { + if (this.managementKey) { + const maskedKey = this.maskApiKey(this.managementKey); + keyElement.textContent = maskedKey; + } else { + keyElement.textContent = '-'; + } + } + + // 显示连接状态 + if (statusElement) { + let statusHtml = ''; + if (this.isConnected) { + statusHtml = ` ${i18n.t('common.connected')}`; + } else { + statusHtml = ` ${i18n.t('common.disconnected')}`; + } + statusElement.innerHTML = statusHtml; + } + } + + + // 加载登录页面设置 + loadLoginSettings() { + const savedBase = localStorage.getItem('apiBase'); + const savedKey = localStorage.getItem('managementKey'); + const loginKeyInput = document.getElementById('login-management-key'); + const apiBaseInput = document.getElementById('login-api-base'); + + if (savedBase) { + this.setApiBase(savedBase); + } else { + this.setApiBase(this.detectApiBaseFromLocation()); + } + + if (apiBaseInput) { + apiBaseInput.value = this.apiBase || ''; + } + + if (loginKeyInput && savedKey) { + loginKeyInput.value = savedKey; + } + + this.setupLoginAutoSave(); + } + + setupLoginAutoSave() { + const loginKeyInput = document.getElementById('login-management-key'); + const apiBaseInput = document.getElementById('login-api-base'); + const resetButton = document.getElementById('login-reset-api-base'); + + const saveKey = (val) => { + if (val.trim()) { + this.managementKey = val; + localStorage.setItem('managementKey', this.managementKey); + } + }; + const saveKeyDebounced = this.debounce(saveKey, 500); + + if (loginKeyInput) { + loginKeyInput.addEventListener('change', (e) => saveKey(e.target.value)); + loginKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value)); + } + + if (apiBaseInput) { + const persistBase = (val) => { + const normalized = this.normalizeBase(val); + if (normalized) { + this.setApiBase(normalized); + } + }; + const persistBaseDebounced = this.debounce(persistBase, 500); + + apiBaseInput.addEventListener('change', (e) => persistBase(e.target.value)); + apiBaseInput.addEventListener('input', (e) => persistBaseDebounced(e.target.value)); + } + + if (resetButton) { + resetButton.addEventListener('click', () => { + const detected = this.detectApiBaseFromLocation(); + this.setApiBase(detected); + if (apiBaseInput) { + apiBaseInput.value = detected; + } + }); + } + } + + // 事件绑定 + bindEvents() { + // 登录相关(安全绑定) + const loginSubmit = document.getElementById('login-submit'); + const logoutBtn = document.getElementById('logout-btn'); + + if (loginSubmit) { + loginSubmit.addEventListener('click', () => this.handleLogin()); + } + if (logoutBtn) { + logoutBtn.addEventListener('click', () => this.logout()); + } + + // 密钥可见性切换事件 + this.setupKeyVisibilityToggle(); + + // 主页面元素(延迟绑定,在显示主页面时绑定) + this.bindMainPageEvents(); + } + + // 设置密钥可见性切换 + setupKeyVisibilityToggle() { + const toggleButtons = document.querySelectorAll('.toggle-key-visibility'); + toggleButtons.forEach(button => { + button.addEventListener('click', () => this.toggleLoginKeyVisibility(button)); + }); + } + + // 绑定主页面事件 + bindMainPageEvents() { + // 连接状态检查 + const connectionStatus = document.getElementById('connection-status'); + const refreshAll = document.getElementById('refresh-all'); + + if (connectionStatus) { + connectionStatus.addEventListener('click', () => this.checkConnectionStatus()); + } + if (refreshAll) { + refreshAll.addEventListener('click', () => this.refreshAllData()); + } + + // 基础设置 + const debugToggle = document.getElementById('debug-toggle'); + const updateProxy = document.getElementById('update-proxy'); + const clearProxy = document.getElementById('clear-proxy'); + const updateRetry = document.getElementById('update-retry'); + const switchProjectToggle = document.getElementById('switch-project-toggle'); + const switchPreviewToggle = document.getElementById('switch-preview-model-toggle'); + + if (debugToggle) { + debugToggle.addEventListener('change', (e) => this.updateDebug(e.target.checked)); + } + if (updateProxy) { + updateProxy.addEventListener('click', () => this.updateProxyUrl()); + } + if (clearProxy) { + clearProxy.addEventListener('click', () => this.clearProxyUrl()); + } + if (updateRetry) { + updateRetry.addEventListener('click', () => this.updateRequestRetry()); + } + if (switchProjectToggle) { + switchProjectToggle.addEventListener('change', (e) => this.updateSwitchProject(e.target.checked)); + } + if (switchPreviewToggle) { + switchPreviewToggle.addEventListener('change', (e) => this.updateSwitchPreviewModel(e.target.checked)); + } + + // API 密钥管理 + const addApiKey = document.getElementById('add-api-key'); + const addGeminiKey = document.getElementById('add-gemini-key'); + const addCodexKey = document.getElementById('add-codex-key'); + const addClaudeKey = document.getElementById('add-claude-key'); + const addOpenaiProvider = document.getElementById('add-openai-provider'); + + if (addApiKey) { + addApiKey.addEventListener('click', () => this.showAddApiKeyModal()); + } + if (addGeminiKey) { + addGeminiKey.addEventListener('click', () => this.showAddGeminiKeyModal()); + } + if (addCodexKey) { + addCodexKey.addEventListener('click', () => this.showAddCodexKeyModal()); + } + if (addClaudeKey) { + addClaudeKey.addEventListener('click', () => this.showAddClaudeKeyModal()); + } + if (addOpenaiProvider) { + addOpenaiProvider.addEventListener('click', () => this.showAddOpenAIProviderModal()); + } + + + // Gemini Web Token + const geminiWebTokenBtn = document.getElementById('gemini-web-token-btn'); + if (geminiWebTokenBtn) { + geminiWebTokenBtn.addEventListener('click', () => this.showGeminiWebTokenModal()); + } + + // 认证文件管理 + const uploadAuthFile = document.getElementById('upload-auth-file'); + const deleteAllAuthFiles = document.getElementById('delete-all-auth-files'); + const authFileInput = document.getElementById('auth-file-input'); + + if (uploadAuthFile) { + uploadAuthFile.addEventListener('click', () => this.uploadAuthFile()); + } + if (deleteAllAuthFiles) { + deleteAllAuthFiles.addEventListener('click', () => this.deleteAllAuthFiles()); + } + if (authFileInput) { + authFileInput.addEventListener('change', (e) => this.handleFileUpload(e)); + } + + // 使用统计 + const refreshUsageStats = document.getElementById('refresh-usage-stats'); + const requestsHourBtn = document.getElementById('requests-hour-btn'); + const requestsDayBtn = document.getElementById('requests-day-btn'); + const tokensHourBtn = document.getElementById('tokens-hour-btn'); + const tokensDayBtn = document.getElementById('tokens-day-btn'); + + if (refreshUsageStats) { + refreshUsageStats.addEventListener('click', () => this.loadUsageStats()); + } + if (requestsHourBtn) { + requestsHourBtn.addEventListener('click', () => this.switchRequestsPeriod('hour')); + } + if (requestsDayBtn) { + requestsDayBtn.addEventListener('click', () => this.switchRequestsPeriod('day')); + } + if (tokensHourBtn) { + tokensHourBtn.addEventListener('click', () => this.switchTokensPeriod('hour')); + } + if (tokensDayBtn) { + tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day')); + } + + // 模态框 + const closeBtn = document.querySelector('.close'); + if (closeBtn) { + closeBtn.addEventListener('click', () => this.closeModal()); + } + + window.addEventListener('click', (e) => { + const modal = document.getElementById('modal'); + if (modal && e.target === modal) { + this.closeModal(); + } + }); + } + + // 设置导航 + setupNavigation() { + const navItems = document.querySelectorAll('.nav-item'); + navItems.forEach(item => { + item.addEventListener('click', (e) => { + e.preventDefault(); + + // 移除所有活动状态 + navItems.forEach(nav => nav.classList.remove('active')); + document.querySelectorAll('.content-section').forEach(section => section.classList.remove('active')); + + // 添加活动状态 + item.classList.add('active'); + const sectionId = item.getAttribute('data-section'); + document.getElementById(sectionId).classList.add('active'); + }); + }); + } + + // 设置语言切换 + setupLanguageSwitcher() { + const loginToggle = document.getElementById('language-toggle'); + const mainToggle = document.getElementById('language-toggle-main'); + + if (loginToggle) { + loginToggle.addEventListener('click', () => this.toggleLanguage()); + } + if (mainToggle) { + mainToggle.addEventListener('click', () => this.toggleLanguage()); + } + } + + // 设置主题切换 + setupThemeSwitcher() { + const loginToggle = document.getElementById('theme-toggle'); + const mainToggle = document.getElementById('theme-toggle-main'); + + if (loginToggle) { + loginToggle.addEventListener('click', () => this.toggleTheme()); + } + if (mainToggle) { + mainToggle.addEventListener('click', () => this.toggleTheme()); + } + } + + // 切换语言 + toggleLanguage() { + const currentLang = i18n.currentLanguage; + const newLang = currentLang === 'zh-CN' ? 'en-US' : 'zh-CN'; + i18n.setLanguage(newLang); + + // 更新主题按钮文本 + this.updateThemeButtons(); + + // 更新连接状态显示 + this.updateConnectionStatus(); + + // 重新加载所有数据以更新动态内容 + if (this.isLoggedIn && this.isConnected) { + this.loadAllData(true); + } + } + + // 规范化基础地址,移除尾部斜杠与 /v0/management + normalizeBase(input) { + let base = (input || '').trim(); + if (!base) return ''; + // 若用户粘贴了完整地址,剥离后缀 + base = base.replace(/\/?v0\/management\/?$/i, ''); + base = base.replace(/\/+$/i, ''); + // 自动补 http:// + if (!/^https?:\/\//i.test(base)) { + base = 'http://' + base; + } + return base; + } + + // 由基础地址生成完整管理 API 地址 + computeApiUrl(base) { + const b = this.normalizeBase(base); + if (!b) return ''; + return b.replace(/\/$/, '') + '/v0/management'; + } + + setApiBase(newBase) { + this.apiBase = this.normalizeBase(newBase); + this.apiUrl = this.computeApiUrl(this.apiBase); + localStorage.setItem('apiBase', this.apiBase); + localStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段 + this.updateLoginConnectionInfo(); + } + + // 加载设置(简化版,仅加载内部状态) + loadSettings() { + const savedBase = localStorage.getItem('apiBase'); + const savedUrl = localStorage.getItem('apiUrl'); + const savedKey = localStorage.getItem('managementKey'); + + if (savedBase) { + this.setApiBase(savedBase); + } else if (savedUrl) { + const base = (savedUrl || '').replace(/\/?v0\/management\/?$/i, ''); + this.setApiBase(base); + } else { + this.setApiBase(this.detectApiBaseFromLocation()); + } + + if (savedKey) { + this.managementKey = savedKey; + } + + this.updateLoginConnectionInfo(); + } + + // API 请求方法 + async makeRequest(endpoint, options = {}) { + const url = `${this.apiUrl}${endpoint}`; + const headers = { + 'Authorization': `Bearer ${this.managementKey}`, + 'Content-Type': 'application/json', + ...options.headers + }; + + try { + const response = await fetch(url, { + ...options, + headers + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `HTTP ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error('API请求失败:', error); + throw error; + } + } + + // 显示通知 + showNotification(message, type = 'info') { + const notification = document.getElementById('notification'); + notification.textContent = message; + notification.className = `notification ${type}`; + notification.classList.add('show'); + + setTimeout(() => { + notification.classList.remove('show'); + }, 3000); + } + + // 密钥可见性切换 + toggleKeyVisibility() { + const keyInput = document.getElementById('management-key'); + const toggleButton = document.getElementById('toggle-key-visibility'); + + if (keyInput.type === 'password') { + keyInput.type = 'text'; + toggleButton.innerHTML = ''; + } else { + keyInput.type = 'password'; + toggleButton.innerHTML = ''; + } + } + + // 测试连接(简化版,用于内部调用) + async testConnection() { + try { + await this.makeRequest('/debug'); + this.isConnected = true; + this.updateConnectionStatus(); + this.startStatusUpdateTimer(); + await this.loadAllData(); + return true; + } catch (error) { + this.isConnected = false; + this.updateConnectionStatus(); + this.stopStatusUpdateTimer(); + throw error; + } + } + + // 更新连接状态 + updateConnectionStatus() { + const statusButton = document.getElementById('connection-status'); + const apiStatus = document.getElementById('api-status'); + const configStatus = document.getElementById('config-status'); + const lastUpdate = document.getElementById('last-update'); + + if (this.isConnected) { + statusButton.innerHTML = ` ${i18n.t('common.connected')}`; + statusButton.className = 'btn btn-success'; + apiStatus.textContent = i18n.t('common.connected'); + + // 更新配置状态 + if (this.isCacheValid()) { + const cacheAge = Math.floor((Date.now() - this.cacheTimestamp) / 1000); + configStatus.textContent = `${i18n.t('system_info.cache_data')} (${cacheAge}${i18n.t('system_info.seconds_ago')})`; + configStatus.style.color = '#f59e0b'; // 橙色表示缓存 + } else if (this.configCache) { + configStatus.textContent = i18n.t('system_info.real_time_data'); + configStatus.style.color = '#10b981'; // 绿色表示实时 + } else { + configStatus.textContent = i18n.t('system_info.not_loaded'); + configStatus.style.color = '#6b7280'; // 灰色表示未加载 + } + } else { + statusButton.innerHTML = ` ${i18n.t('common.disconnected')}`; + statusButton.className = 'btn btn-danger'; + apiStatus.textContent = i18n.t('common.disconnected'); + configStatus.textContent = i18n.t('system_info.not_loaded'); + configStatus.style.color = '#6b7280'; + } + + lastUpdate.textContent = new Date().toLocaleString('zh-CN'); + + // 更新连接信息显示 + this.updateConnectionInfo(); + } + + // 检查连接状态 + async checkConnectionStatus() { + await this.testConnection(); + } + + // 刷新所有数据 + async refreshAllData() { + if (!this.isConnected) { + this.showNotification(i18n.t('notification.connection_required'), 'error'); + return; + } + + const button = document.getElementById('refresh-all'); + const originalText = button.innerHTML; + + button.innerHTML = `
${i18n.t('common.loading')}`; + button.disabled = true; + + try { + // 强制刷新,清除缓存 + await this.loadAllData(true); + this.showNotification(i18n.t('notification.data_refreshed'), 'success'); + } catch (error) { + this.showNotification(`${i18n.t('notification.refresh_failed')}: ${error.message}`, 'error'); + } finally { + button.innerHTML = originalText; + button.disabled = false; + } + } + + // 检查缓存是否有效 + isCacheValid() { + if (!this.configCache || !this.cacheTimestamp) { + return false; + } + return (Date.now() - this.cacheTimestamp) < this.cacheExpiry; + } + + // 获取配置(优先使用缓存) + async getConfig(forceRefresh = false) { + if (!forceRefresh && this.isCacheValid()) { + this.updateConnectionStatus(); // 更新状态显示 + return this.configCache; + } + + try { + const config = await this.makeRequest('/config'); + this.configCache = config; + this.cacheTimestamp = Date.now(); + this.updateConnectionStatus(); // 更新状态显示 + return config; + } catch (error) { + console.error('获取配置失败:', error); + throw error; + } + } + + // 清除缓存 + clearCache() { + this.configCache = null; + this.cacheTimestamp = null; + } + + // 启动状态更新定时器 + startStatusUpdateTimer() { + if (this.statusUpdateTimer) { + clearInterval(this.statusUpdateTimer); + } + this.statusUpdateTimer = setInterval(() => { + if (this.isConnected) { + this.updateConnectionStatus(); + } + }, 1000); // 每秒更新一次 + } + + // 停止状态更新定时器 + stopStatusUpdateTimer() { + if (this.statusUpdateTimer) { + clearInterval(this.statusUpdateTimer); + this.statusUpdateTimer = null; + } + } + + // 加载所有数据 - 使用新的 /config 端点一次性获取所有配置 + async loadAllData(forceRefresh = false) { + try { + console.log('使用新的 /config 端点加载所有配置...'); + // 使用新的 /config 端点一次性获取所有配置 + const config = await this.getConfig(forceRefresh); + + // 从配置中提取并设置各个设置项 + this.updateSettingsFromConfig(config); + + // 认证文件需要单独加载,因为不在配置中 + await this.loadAuthFiles(); + + // 使用统计需要单独加载 + await this.loadUsageStats(); + + console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid()); + } catch (error) { + console.error('加载配置失败:', error); + console.log('回退到逐个加载方式...'); + // 如果新方法失败,回退到原来的逐个加载方式 + await this.loadAllDataLegacy(); + } + } + + // 从配置对象更新所有设置 + updateSettingsFromConfig(config) { + // 调试设置 + if (config.debug !== undefined) { + document.getElementById('debug-toggle').checked = config.debug; + } + + // 代理设置 + if (config['proxy-url'] !== undefined) { + document.getElementById('proxy-url').value = config['proxy-url'] || ''; + } + + // 请求重试设置 + if (config['request-retry'] !== undefined) { + document.getElementById('request-retry').value = config['request-retry']; + } + + // 配额超出行为 + if (config['quota-exceeded']) { + if (config['quota-exceeded']['switch-project'] !== undefined) { + document.getElementById('switch-project-toggle').checked = config['quota-exceeded']['switch-project']; + } + if (config['quota-exceeded']['switch-preview-model'] !== undefined) { + document.getElementById('switch-preview-model-toggle').checked = config['quota-exceeded']['switch-preview-model']; + } + } + + + // API 密钥 + if (config['api-keys']) { + this.renderApiKeys(config['api-keys']); + } + + // Gemini 密钥 + if (config['generative-language-api-key']) { + this.renderGeminiKeys(config['generative-language-api-key']); + } + + // Codex 密钥 + if (config['codex-api-key']) { + this.renderCodexKeys(config['codex-api-key']); + } + + // Claude 密钥 + if (config['claude-api-key']) { + this.renderClaudeKeys(config['claude-api-key']); + } + + // OpenAI 兼容提供商 + if (config['openai-compatibility']) { + this.renderOpenAIProviders(config['openai-compatibility']); + } + } + + // 回退方法:原来的逐个加载方式 + async loadAllDataLegacy() { + await Promise.all([ + this.loadDebugSettings(), + this.loadProxySettings(), + this.loadRetrySettings(), + this.loadQuotaSettings(), + this.loadApiKeys(), + this.loadGeminiKeys(), + this.loadCodexKeys(), + this.loadClaudeKeys(), + this.loadOpenAIProviders(), + this.loadAuthFiles() + ]); + } + + // 加载调试设置 + async loadDebugSettings() { + try { + const config = await this.getConfig(); + if (config.debug !== undefined) { + document.getElementById('debug-toggle').checked = config.debug; + } + } catch (error) { + console.error('加载调试设置失败:', error); + } + } + + // 更新调试设置 + async updateDebug(enabled) { + try { + await this.makeRequest('/debug', { + method: 'PUT', + body: JSON.stringify({ value: enabled }) + }); + this.clearCache(); // 清除缓存 + this.showNotification(i18n.t('notification.debug_updated'), 'success'); + } catch (error) { + this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); + // 恢复原状态 + document.getElementById('debug-toggle').checked = !enabled; + } + } + + // 加载代理设置 + async loadProxySettings() { + try { + const config = await this.getConfig(); + if (config['proxy-url'] !== undefined) { + document.getElementById('proxy-url').value = config['proxy-url'] || ''; + } + } catch (error) { + console.error('加载代理设置失败:', error); + } + } + + // 更新代理URL + async updateProxyUrl() { + const proxyUrl = document.getElementById('proxy-url').value.trim(); + + try { + await this.makeRequest('/proxy-url', { + method: 'PUT', + body: JSON.stringify({ value: proxyUrl }) + }); + this.clearCache(); // 清除缓存 + this.showNotification(i18n.t('notification.proxy_updated'), 'success'); + } catch (error) { + this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); + } + } + + // 清空代理URL + async clearProxyUrl() { + try { + await this.makeRequest('/proxy-url', { method: 'DELETE' }); + document.getElementById('proxy-url').value = ''; + this.clearCache(); // 清除缓存 + this.showNotification(i18n.t('notification.proxy_cleared'), 'success'); + } catch (error) { + this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); + } + } + + // 加载重试设置 + async loadRetrySettings() { + try { + const config = await this.getConfig(); + if (config['request-retry'] !== undefined) { + document.getElementById('request-retry').value = config['request-retry']; + } + } catch (error) { + console.error('加载重试设置失败:', error); + } + } + + // 更新请求重试 + async updateRequestRetry() { + const retryCount = parseInt(document.getElementById('request-retry').value); + + try { + await this.makeRequest('/request-retry', { + method: 'PUT', + body: JSON.stringify({ value: retryCount }) + }); + this.clearCache(); // 清除缓存 + this.showNotification(i18n.t('notification.retry_updated'), 'success'); + } catch (error) { + this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); + } + } + + // 加载配额设置 + async loadQuotaSettings() { + try { + const config = await this.getConfig(); + if (config['quota-exceeded']) { + if (config['quota-exceeded']['switch-project'] !== undefined) { + document.getElementById('switch-project-toggle').checked = config['quota-exceeded']['switch-project']; + } + if (config['quota-exceeded']['switch-preview-model'] !== undefined) { + document.getElementById('switch-preview-model-toggle').checked = config['quota-exceeded']['switch-preview-model']; + } + } + } catch (error) { + console.error('加载配额设置失败:', error); + } + } + + // 更新项目切换设置 + async updateSwitchProject(enabled) { + try { + await this.makeRequest('/quota-exceeded/switch-project', { + method: 'PUT', + body: JSON.stringify({ value: enabled }) + }); + this.clearCache(); // 清除缓存 + this.showNotification(i18n.t('notification.quota_switch_project_updated'), 'success'); + } catch (error) { + this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); + document.getElementById('switch-project-toggle').checked = !enabled; + } + } + + // 更新预览模型切换设置 + async updateSwitchPreviewModel(enabled) { + try { + await this.makeRequest('/quota-exceeded/switch-preview-model', { + method: 'PUT', + body: JSON.stringify({ value: enabled }) + }); + this.clearCache(); // 清除缓存 + this.showNotification(i18n.t('notification.quota_switch_preview_updated'), 'success'); + } catch (error) { + this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); + document.getElementById('switch-preview-model-toggle').checked = !enabled; + } + } + + + // 加载API密钥 + async loadApiKeys() { + try { + const config = await this.getConfig(); + if (config['api-keys']) { + this.renderApiKeys(config['api-keys']); + } + } catch (error) { + console.error('加载API密钥失败:', error); + } + } + + // 渲染API密钥列表 + renderApiKeys(keys) { + const container = document.getElementById('api-keys-list'); + + if (keys.length === 0) { + container.innerHTML = ` +
+ +

${i18n.t('api_keys.empty_title')}

+

${i18n.t('api_keys.empty_desc')}

+
+ `; + return; + } + + container.innerHTML = keys.map((key, index) => ` +
+
+
${i18n.t('api_keys.item_title')} #${index + 1}
+
${this.maskApiKey(key)}
+
+
+ + +
+
+ `).join(''); + } + + // 遮蔽API密钥显示 + maskApiKey(key) { + if (key.length <= 8) return key; + return key.substring(0, 4) + '...' + key.substring(key.length - 4); + } + + // HTML 转义,防止 XSS + escapeHtml(value) { + if (value === null || value === undefined) return ''; + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + // 显示添加API密钥模态框 + showAddApiKeyModal() { + const modal = document.getElementById('modal'); + const modalBody = document.getElementById('modal-body'); + + modalBody.innerHTML = ` +

${i18n.t('api_keys.add_modal_title')}

+
+ + +
+ + `; + + modal.style.display = 'block'; + } + + // 添加API密钥 + async addApiKey() { + const newKey = document.getElementById('new-api-key').value.trim(); + + if (!newKey) { + this.showNotification(`${i18n.t('notification.please_enter')} ${i18n.t('notification.api_key')}`, 'error'); + return; + } + + try { + const data = await this.makeRequest('/api-keys'); + const currentKeys = data['api-keys'] || []; + currentKeys.push(newKey); + + await this.makeRequest('/api-keys', { + method: 'PUT', + body: JSON.stringify(currentKeys) + }); + + this.clearCache(); // 清除缓存 + this.closeModal(); + this.loadApiKeys(); + this.showNotification(i18n.t('notification.api_key_added'), 'success'); + } catch (error) { + this.showNotification(`${i18n.t('notification.add_failed')}: ${error.message}`, 'error'); + } + } + + // 编辑API密钥 + editApiKey(index, currentKey) { + const modal = document.getElementById('modal'); + const modalBody = document.getElementById('modal-body'); + + modalBody.innerHTML = ` +

${i18n.t('api_keys.edit_modal_title')}

+
+ + +
+ + `; + + modal.style.display = 'block'; + } + + // 更新API密钥 + async updateApiKey(index) { + const newKey = document.getElementById('edit-api-key').value.trim(); + + if (!newKey) { + this.showNotification(`${i18n.t('notification.please_enter')} ${i18n.t('notification.api_key')}`, 'error'); + return; + } + + try { + await this.makeRequest('/api-keys', { + method: 'PATCH', + body: JSON.stringify({ index, value: newKey }) + }); + + this.clearCache(); // 清除缓存 + this.closeModal(); + this.loadApiKeys(); + this.showNotification(i18n.t('notification.api_key_updated'), 'success'); + } catch (error) { + this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error'); + } + } + + // 删除API密钥 + async deleteApiKey(index) { + if (!confirm(i18n.t('api_keys.delete_confirm'))) return; + + try { + await this.makeRequest(`/api-keys?index=${index}`, { method: 'DELETE' }); + this.clearCache(); // 清除缓存 + this.loadApiKeys(); + this.showNotification(i18n.t('notification.api_key_deleted'), 'success'); + } catch (error) { + this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error'); + } + } + + // 加载Gemini密钥 + async loadGeminiKeys() { + try { + const config = await this.getConfig(); + if (config['generative-language-api-key']) { + this.renderGeminiKeys(config['generative-language-api-key']); + } + } catch (error) { + console.error('加载Gemini密钥失败:', error); + } + } + + // 渲染Gemini密钥列表 + renderGeminiKeys(keys) { + const container = document.getElementById('gemini-keys-list'); + + if (keys.length === 0) { + container.innerHTML = ` +
+ +

${i18n.t('ai_providers.gemini_empty_title')}

+

${i18n.t('ai_providers.gemini_empty_desc')}

+
+ `; + return; + } + + container.innerHTML = keys.map((key, index) => ` +
+
+
${i18n.t('ai_providers.gemini_item_title')} #${index + 1}
+
${this.maskApiKey(key)}
+
+
+ + +
+
+ `).join(''); + } + + // 显示添加Gemini密钥模态框 + showAddGeminiKeyModal() { + const modal = document.getElementById('modal'); + const modalBody = document.getElementById('modal-body'); + + modalBody.innerHTML = ` +

添加Gemini API密钥

+
+ + +
+ + `; + + modal.style.display = 'block'; + } + + // 添加Gemini密钥 + async addGeminiKey() { + const newKey = document.getElementById('new-gemini-key').value.trim(); + + if (!newKey) { + this.showNotification('请输入Gemini API密钥', 'error'); + return; + } + + try { + const data = await this.makeRequest('/generative-language-api-key'); + const currentKeys = data['generative-language-api-key'] || []; + currentKeys.push(newKey); + + await this.makeRequest('/generative-language-api-key', { + method: 'PUT', + body: JSON.stringify(currentKeys) + }); + + this.clearCache(); // 清除缓存 + this.closeModal(); + this.loadGeminiKeys(); + this.showNotification('Gemini密钥添加成功', 'success'); + } catch (error) { + this.showNotification(`添加Gemini密钥失败: ${error.message}`, 'error'); + } + } + + // 编辑Gemini密钥 + editGeminiKey(index, currentKey) { + const modal = document.getElementById('modal'); + const modalBody = document.getElementById('modal-body'); + + modalBody.innerHTML = ` +

编辑Gemini API密钥

+
+ + +
+ + `; + + modal.style.display = 'block'; + } + + // 更新Gemini密钥 + async updateGeminiKey(oldKey) { + const newKey = document.getElementById('edit-gemini-key').value.trim(); + + if (!newKey) { + this.showNotification('请输入Gemini API密钥', 'error'); + return; + } + + try { + await this.makeRequest('/generative-language-api-key', { + method: 'PATCH', + body: JSON.stringify({ old: oldKey, new: newKey }) + }); + + this.clearCache(); // 清除缓存 + this.closeModal(); + this.loadGeminiKeys(); + this.showNotification('Gemini密钥更新成功', 'success'); + } catch (error) { + this.showNotification(`更新Gemini密钥失败: ${error.message}`, 'error'); + } + } + + // 删除Gemini密钥 + async deleteGeminiKey(key) { + if (!confirm(i18n.t('ai_providers.gemini_delete_confirm'))) return; + + try { + await this.makeRequest(`/generative-language-api-key?value=${encodeURIComponent(key)}`, { method: 'DELETE' }); + this.clearCache(); // 清除缓存 + this.loadGeminiKeys(); + this.showNotification('Gemini密钥删除成功', 'success'); + } catch (error) { + this.showNotification(`删除Gemini密钥失败: ${error.message}`, 'error'); + } + } + + // 加载Codex密钥 + async loadCodexKeys() { + try { + const config = await this.getConfig(); + if (config['codex-api-key']) { + this.renderCodexKeys(config['codex-api-key']); + } + } catch (error) { + console.error('加载Codex密钥失败:', error); + } + } + + // 渲染Codex密钥列表 + renderCodexKeys(keys) { + const container = document.getElementById('codex-keys-list'); + + if (keys.length === 0) { + container.innerHTML = ` +
+ +

${i18n.t('ai_providers.codex_empty_title')}

+

${i18n.t('ai_providers.codex_empty_desc')}

+
+ `; + return; + } + + container.innerHTML = keys.map((config, index) => ` +
+
+
${i18n.t('ai_providers.codex_item_title')} #${index + 1}
+
${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}
+ ${config['base-url'] ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}
` : ''} + ${config['proxy-url'] ? `
${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}
` : ''} +
+
+ + +
+
+ `).join(''); + } + + // 显示添加Codex密钥模态框 + showAddCodexKeyModal() { + const modal = document.getElementById('modal'); + const modalBody = document.getElementById('modal-body'); + + modalBody.innerHTML = ` +

${i18n.t('ai_providers.codex_add_modal_title')}

+
+ + +
+
+ + +
+
+ + +
+ + `; + + modal.style.display = 'block'; + } + + // 添加Codex密钥 + async addCodexKey() { + const apiKey = document.getElementById('new-codex-key').value.trim(); + const baseUrl = document.getElementById('new-codex-url').value.trim(); + const proxyUrl = document.getElementById('new-codex-proxy').value.trim(); + + if (!apiKey) { + this.showNotification(i18n.t('notification.field_required'), 'error'); + return; + } + + try { + const data = await this.makeRequest('/codex-api-key'); + const currentKeys = data['codex-api-key'] || []; + + const newConfig = { 'api-key': apiKey }; + if (baseUrl) { + newConfig['base-url'] = baseUrl; + } + if (proxyUrl) { + newConfig['proxy-url'] = proxyUrl; + } + + currentKeys.push(newConfig); + + await this.makeRequest('/codex-api-key', { + method: 'PUT', + body: JSON.stringify(currentKeys) + }); + + this.clearCache(); // 清除缓存 + this.closeModal(); + this.loadCodexKeys(); + this.showNotification('Codex配置添加成功', 'success'); + } catch (error) { + this.showNotification(`添加Codex配置失败: ${error.message}`, 'error'); + } + } + + // 编辑Codex密钥 + editCodexKey(index, config) { + const modal = document.getElementById('modal'); + const modalBody = document.getElementById('modal-body'); + + modalBody.innerHTML = ` +

${i18n.t('ai_providers.codex_edit_modal_title')}

+
+ + +
+
+ + +
+
+ + +
+ + `; + + modal.style.display = 'block'; + } + + // 更新Codex密钥 + async updateCodexKey(index) { + const apiKey = document.getElementById('edit-codex-key').value.trim(); + const baseUrl = document.getElementById('edit-codex-url').value.trim(); + const proxyUrl = document.getElementById('edit-codex-proxy').value.trim(); + + if (!apiKey) { + this.showNotification(i18n.t('notification.field_required'), 'error'); + return; + } + + try { + const newConfig = { 'api-key': apiKey }; + if (baseUrl) { + newConfig['base-url'] = baseUrl; + } + if (proxyUrl) { + newConfig['proxy-url'] = proxyUrl; + } + + await this.makeRequest('/codex-api-key', { + method: 'PATCH', + body: JSON.stringify({ index, value: newConfig }) + }); + + this.clearCache(); // 清除缓存 + this.closeModal(); + this.loadCodexKeys(); + this.showNotification('Codex配置更新成功', 'success'); + } catch (error) { + this.showNotification(`更新Codex配置失败: ${error.message}`, 'error'); + } + } + + // 删除Codex密钥 + async deleteCodexKey(apiKey) { + if (!confirm(i18n.t('ai_providers.codex_delete_confirm'))) return; + + try { + await this.makeRequest(`/codex-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' }); + this.clearCache(); // 清除缓存 + this.loadCodexKeys(); + this.showNotification('Codex配置删除成功', 'success'); + } catch (error) { + this.showNotification(`删除Codex配置失败: ${error.message}`, 'error'); + } + } + + // 加载Claude密钥 + async loadClaudeKeys() { + try { + const config = await this.getConfig(); + if (config['claude-api-key']) { + this.renderClaudeKeys(config['claude-api-key']); + } + } catch (error) { + console.error('加载Claude密钥失败:', error); + } + } + + // 渲染Claude密钥列表 + renderClaudeKeys(keys) { + const container = document.getElementById('claude-keys-list'); + + if (keys.length === 0) { + container.innerHTML = ` +
+ +

${i18n.t('ai_providers.claude_empty_title')}

+

${i18n.t('ai_providers.claude_empty_desc')}

+
+ `; + return; + } + + container.innerHTML = keys.map((config, index) => ` +
+
+
${i18n.t('ai_providers.claude_item_title')} #${index + 1}
+
${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}
+ ${config['base-url'] ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}
` : ''} + ${config['proxy-url'] ? `
${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}
` : ''} +
+
+ + +
+
+ `).join(''); + } + + // 显示添加Claude密钥模态框 + showAddClaudeKeyModal() { + const modal = document.getElementById('modal'); + const modalBody = document.getElementById('modal-body'); + + modalBody.innerHTML = ` +

${i18n.t('ai_providers.claude_add_modal_title')}

+
+ + +
+
+ + +
+
+ + +
+ + `; + + modal.style.display = 'block'; + } + + // 添加Claude密钥 + async addClaudeKey() { + const apiKey = document.getElementById('new-claude-key').value.trim(); + const baseUrl = document.getElementById('new-claude-url').value.trim(); + const proxyUrl = document.getElementById('new-claude-proxy').value.trim(); + + if (!apiKey) { + this.showNotification(i18n.t('notification.field_required'), 'error'); + return; + } + + try { + const data = await this.makeRequest('/claude-api-key'); + const currentKeys = data['claude-api-key'] || []; + + const newConfig = { 'api-key': apiKey }; + if (baseUrl) { + newConfig['base-url'] = baseUrl; + } + if (proxyUrl) { + newConfig['proxy-url'] = proxyUrl; + } + + currentKeys.push(newConfig); + + await this.makeRequest('/claude-api-key', { + method: 'PUT', + body: JSON.stringify(currentKeys) + }); + + this.clearCache(); // 清除缓存 + this.closeModal(); + this.loadClaudeKeys(); + this.showNotification('Claude配置添加成功', 'success'); + } catch (error) { + this.showNotification(`添加Claude配置失败: ${error.message}`, 'error'); + } + } + + // 编辑Claude密钥 + editClaudeKey(index, config) { + const modal = document.getElementById('modal'); + const modalBody = document.getElementById('modal-body'); + + modalBody.innerHTML = ` +

${i18n.t('ai_providers.claude_edit_modal_title')}

+
+ + +
+
+ + +
+
+ + +
+ + `; + + modal.style.display = 'block'; + } + + // 更新Claude密钥 + async updateClaudeKey(index) { + const apiKey = document.getElementById('edit-claude-key').value.trim(); + const baseUrl = document.getElementById('edit-claude-url').value.trim(); + const proxyUrl = document.getElementById('edit-claude-proxy').value.trim(); + + if (!apiKey) { + this.showNotification(i18n.t('notification.field_required'), 'error'); + return; + } + + try { + const newConfig = { 'api-key': apiKey }; + if (baseUrl) { + newConfig['base-url'] = baseUrl; + } + if (proxyUrl) { + newConfig['proxy-url'] = proxyUrl; + } + + await this.makeRequest('/claude-api-key', { + method: 'PATCH', + body: JSON.stringify({ index, value: newConfig }) + }); + + this.clearCache(); // 清除缓存 + this.closeModal(); + this.loadClaudeKeys(); + this.showNotification('Claude配置更新成功', 'success'); + } catch (error) { + this.showNotification(`更新Claude配置失败: ${error.message}`, 'error'); + } + } + + // 删除Claude密钥 + async deleteClaudeKey(apiKey) { + if (!confirm(i18n.t('ai_providers.claude_delete_confirm'))) return; + + try { + await this.makeRequest(`/claude-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' }); + this.clearCache(); // 清除缓存 + this.loadClaudeKeys(); + this.showNotification('Claude配置删除成功', 'success'); + } catch (error) { + this.showNotification(`删除Claude配置失败: ${error.message}`, 'error'); + } + } + + // 加载OpenAI提供商 + async loadOpenAIProviders() { + try { + const config = await this.getConfig(); + if (config['openai-compatibility']) { + this.renderOpenAIProviders(config['openai-compatibility']); + } + } catch (error) { + console.error('加载OpenAI提供商失败:', error); + } + } + + // 渲染OpenAI提供商列表 + renderOpenAIProviders(providers) { + const container = document.getElementById('openai-providers-list'); + + if (providers.length === 0) { + container.innerHTML = ` +
+ +

${i18n.t('ai_providers.openai_empty_title')}

+

${i18n.t('ai_providers.openai_empty_desc')}

+
+ `; + return; + } + + container.innerHTML = providers.map((provider, index) => ` +
+
+
${this.escapeHtml(provider.name)}
+
${i18n.t('common.base_url')}: ${this.escapeHtml(provider['base-url'])}
+
${i18n.t('ai_providers.openai_keys_count')}: ${(provider['api-key-entries'] || []).length}
+
${i18n.t('ai_providers.openai_models_count')}: ${(provider.models || []).length}
+ ${this.renderOpenAIModelBadges(provider.models || [])} +
+
+ + +
+
+ `).join(''); + } + + // 显示添加OpenAI提供商模态框 + showAddOpenAIProviderModal() { + const modal = document.getElementById('modal'); + const modalBody = document.getElementById('modal-body'); + + modalBody.innerHTML = ` +

${i18n.t('ai_providers.openai_add_modal_title')}

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +

${i18n.t('ai_providers.openai_models_hint')}

+
+ +
+ + `; + + modal.style.display = 'block'; + this.populateModelFields('new-provider-models-wrapper', []); + } + + // 添加OpenAI提供商 + async addOpenAIProvider() { + const name = document.getElementById('new-provider-name').value.trim(); + const baseUrl = document.getElementById('new-provider-url').value.trim(); + const keysText = document.getElementById('new-provider-keys').value.trim(); + const proxiesText = document.getElementById('new-provider-proxies').value.trim(); + const models = this.collectModelInputs('new-provider-models-wrapper'); + + if (!this.validateOpenAIProviderInput(name, baseUrl, models)) { + return; + } + + try { + const data = await this.makeRequest('/openai-compatibility'); + const currentProviders = data['openai-compatibility'] || []; + + const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : []; + const proxies = proxiesText ? proxiesText.split('\n').map(p => p.trim()).filter(p => p) : []; + const apiKeyEntries = apiKeys.map((key, idx) => ({ + 'api-key': key, + 'proxy-url': proxies[idx] || '' + })); + + const newProvider = { + name, + 'base-url': baseUrl, + 'api-key-entries': apiKeyEntries, + models + }; + + currentProviders.push(newProvider); + + await this.makeRequest('/openai-compatibility', { + method: 'PUT', + body: JSON.stringify(currentProviders) + }); + + this.clearCache(); // 清除缓存 + this.closeModal(); + this.loadOpenAIProviders(); + this.showNotification('OpenAI提供商添加成功', 'success'); + } catch (error) { + this.showNotification(`添加OpenAI提供商失败: ${error.message}`, 'error'); + } + } + + // 编辑OpenAI提供商 + editOpenAIProvider(index, provider) { + const modal = document.getElementById('modal'); + const modalBody = document.getElementById('modal-body'); + + const apiKeyEntries = provider['api-key-entries'] || []; + const apiKeysText = apiKeyEntries.map(entry => entry['api-key'] || '').join('\n'); + const proxiesText = apiKeyEntries.map(entry => entry['proxy-url'] || '').join('\n'); + + modalBody.innerHTML = ` +

${i18n.t('ai_providers.openai_edit_modal_title')}

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +

${i18n.t('ai_providers.openai_models_hint')}

+
+ +
+ + `; + + modal.style.display = 'block'; + this.populateModelFields('edit-provider-models-wrapper', provider.models || []); + } + + // 更新OpenAI提供商 + async updateOpenAIProvider(index) { + const name = document.getElementById('edit-provider-name').value.trim(); + const baseUrl = document.getElementById('edit-provider-url').value.trim(); + const keysText = document.getElementById('edit-provider-keys').value.trim(); + const proxiesText = document.getElementById('edit-provider-proxies').value.trim(); + const models = this.collectModelInputs('edit-provider-models-wrapper'); + + if (!this.validateOpenAIProviderInput(name, baseUrl, models)) { + return; + } + + try { + const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : []; + const proxies = proxiesText ? proxiesText.split('\n').map(p => p.trim()).filter(p => p) : []; + const apiKeyEntries = apiKeys.map((key, idx) => ({ + 'api-key': key, + 'proxy-url': proxies[idx] || '' + })); + + const updatedProvider = { + name, + 'base-url': baseUrl, + 'api-key-entries': apiKeyEntries, + models + }; + + await this.makeRequest('/openai-compatibility', { + method: 'PATCH', + body: JSON.stringify({ index, value: updatedProvider }) + }); + + this.clearCache(); // 清除缓存 + this.closeModal(); + this.loadOpenAIProviders(); + this.showNotification('OpenAI提供商更新成功', 'success'); + } catch (error) { + this.showNotification(`更新OpenAI提供商失败: ${error.message}`, 'error'); + } + } + + // 删除OpenAI提供商 + async deleteOpenAIProvider(name) { + if (!confirm(i18n.t('ai_providers.openai_delete_confirm'))) return; + + try { + await this.makeRequest(`/openai-compatibility?name=${encodeURIComponent(name)}`, { method: 'DELETE' }); + this.clearCache(); // 清除缓存 + this.loadOpenAIProviders(); + this.showNotification('OpenAI提供商删除成功', 'success'); + } catch (error) { + this.showNotification(`删除OpenAI提供商失败: ${error.message}`, 'error'); + } + } + + // 加载认证文件 + async loadAuthFiles() { + try { + const data = await this.makeRequest('/auth-files'); + this.renderAuthFiles(data.files || []); + } catch (error) { + console.error('加载认证文件失败:', error); + } + } + + // 渲染认证文件列表 + renderAuthFiles(files) { + const container = document.getElementById('auth-files-list'); + + if (files.length === 0) { + container.innerHTML = ` +
+ +

${i18n.t('auth_files.empty_title')}

+

${i18n.t('auth_files.empty_desc')}

+
+ `; + return; + } + + container.innerHTML = files.map(file => ` +
+
+
${file.name}
+
${i18n.t('auth_files.file_size')}: ${this.formatFileSize(file.size)}
+
${i18n.t('auth_files.file_modified')}: ${new Date(file.modtime).toLocaleString(i18n.currentLanguage === 'zh-CN' ? 'zh-CN' : 'en-US')}
+
+
+ + +
+
+ `).join(''); + } + + // 格式化文件大小 + formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + // 上传认证文件 + uploadAuthFile() { + document.getElementById('auth-file-input').click(); + } + + // 处理文件上传 + async handleFileUpload(event) { + const file = event.target.files[0]; + if (!file) return; + + if (!file.name.endsWith('.json')) { + this.showNotification(i18n.t('auth_files.upload_error_json'), 'error'); + return; + } + + try { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch(`${this.apiUrl}/auth-files`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.managementKey}` + }, + body: formData + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `HTTP ${response.status}`); + } + + this.clearCache(); // 清除缓存 + this.loadAuthFiles(); + this.showNotification(i18n.t('auth_files.upload_success'), 'success'); + } catch (error) { + this.showNotification(`文件上传失败: ${error.message}`, 'error'); + } + + // 清空文件输入 + event.target.value = ''; + } + + // 下载认证文件 + async downloadAuthFile(filename) { + try { + const response = await fetch(`${this.apiUrl}/auth-files/download?name=${encodeURIComponent(filename)}`, { + headers: { + 'Authorization': `Bearer ${this.managementKey}` + } + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + window.URL.revokeObjectURL(url); + + this.showNotification(i18n.t('auth_files.download_success'), 'success'); + } catch (error) { + this.showNotification(`文件下载失败: ${error.message}`, 'error'); + } + } + + // 删除认证文件 + async deleteAuthFile(filename) { + if (!confirm(`${i18n.t('auth_files.delete_confirm')} "${filename}" 吗?`)) return; + + try { + await this.makeRequest(`/auth-files?name=${encodeURIComponent(filename)}`, { method: 'DELETE' }); + this.clearCache(); // 清除缓存 + this.loadAuthFiles(); + this.showNotification(i18n.t('auth_files.delete_success'), 'success'); + } catch (error) { + this.showNotification(`文件删除失败: ${error.message}`, 'error'); + } + } + + // 删除所有认证文件 + async deleteAllAuthFiles() { + if (!confirm(i18n.t('auth_files.delete_all_confirm'))) return; + + try { + const response = await this.makeRequest('/auth-files?all=true', { method: 'DELETE' }); + this.clearCache(); // 清除缓存 + this.loadAuthFiles(); + this.showNotification(`${i18n.t('auth_files.delete_all_success')} ${response.deleted} ${i18n.t('auth_files.files_count')}`, 'success'); + } catch (error) { + this.showNotification(`删除文件失败: ${error.message}`, 'error'); + } + } + + + + + + // 显示 Gemini Web Token 模态框 + showGeminiWebTokenModal() { + const inlineSecure1psid = document.getElementById('secure-1psid-input'); + const inlineSecure1psidts = document.getElementById('secure-1psidts-input'); + const inlineLabel = document.getElementById('gemini-web-label-input'); + const modalBody = document.getElementById('modal-body'); + modalBody.innerHTML = ` +

${i18n.t('auth_login.gemini_web_button')}

+
+
+ + +
从浏览器开发者工具 → Application → Cookies 中获取
+
+
+ + +
从浏览器开发者工具 → Application → Cookies 中获取
+
+
+ + +
为此认证文件设置一个标签名称(可选)
+
+ +
+ `; + this.showModal(); + + const modalSecure1psid = document.getElementById('modal-secure-1psid'); + const modalSecure1psidts = document.getElementById('modal-secure-1psidts'); + const modalLabel = document.getElementById('modal-gemini-web-label'); + + if (modalSecure1psid && inlineSecure1psid) { + modalSecure1psid.value = inlineSecure1psid.value.trim(); + } + if (modalSecure1psidts && inlineSecure1psidts) { + modalSecure1psidts.value = inlineSecure1psidts.value.trim(); + } + if (modalLabel && inlineLabel) { + modalLabel.value = inlineLabel.value.trim(); + } + + if (modalSecure1psid) { + modalSecure1psid.focus(); + } + } + + // 保存 Gemini Web Token + async saveGeminiWebToken() { + const secure1psid = document.getElementById('modal-secure-1psid').value.trim(); + const secure1psidts = document.getElementById('modal-secure-1psidts').value.trim(); + const label = document.getElementById('modal-gemini-web-label').value.trim(); + + if (!secure1psid || !secure1psidts) { + this.showNotification('请填写完整的 Cookie 信息', 'error'); + return; + } + + try { + const requestBody = { + secure_1psid: secure1psid, + secure_1psidts: secure1psidts + }; + + // 如果提供了 label,则添加到请求体中 + if (label) { + requestBody.label = label; + } + + const response = await this.makeRequest('/gemini-web-token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }); + + this.closeModal(); + this.loadAuthFiles(); // 刷新认证文件列表 + const inlineSecure1psid = document.getElementById('secure-1psid-input'); + const inlineSecure1psidts = document.getElementById('secure-1psidts-input'); + const inlineLabel = document.getElementById('gemini-web-label-input'); + if (inlineSecure1psid) { + inlineSecure1psid.value = secure1psid; + } + if (inlineSecure1psidts) { + inlineSecure1psidts.value = secure1psidts; + } + if (inlineLabel) { + inlineLabel.value = label; + } + this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success'); + } catch (error) { + this.showNotification(`保存失败: ${error.message}`, 'error'); + } + } + + // ===== 使用统计相关方法 ===== + + // 初始化图表变量 + requestsChart = null; + tokensChart = null; + currentUsageData = null; + + // 加载使用统计 + async loadUsageStats() { + try { + const response = await this.makeRequest('/usage'); + const usage = response?.usage || null; + this.currentUsageData = usage; + + if (!usage) { + throw new Error('usage payload missing'); + } + + // 更新概览卡片 + this.updateUsageOverview(usage); + + // 读取当前图表周期 + const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active'); + const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active'); + const requestsPeriod = requestsHourActive ? 'hour' : 'day'; + const tokensPeriod = tokensHourActive ? 'hour' : 'day'; + + // 初始化图表(使用当前周期) + this.initializeRequestsChart(requestsPeriod); + this.initializeTokensChart(tokensPeriod); + + // 更新API详细统计表格 + this.updateApiStatsTable(usage); + + } catch (error) { + console.error('加载使用统计失败:', error); + this.currentUsageData = null; + + // 清空概览数据 + ['total-requests', 'success-requests', 'failed-requests', 'total-tokens'].forEach(id => { + const el = document.getElementById(id); + if (el) el.textContent = '-'; + }); + + // 清空图表 + if (this.requestsChart) { + this.requestsChart.destroy(); + this.requestsChart = null; + } + if (this.tokensChart) { + this.tokensChart.destroy(); + this.tokensChart = null; + } + + const tableElement = document.getElementById('api-stats-table'); + if (tableElement) { + tableElement.innerHTML = `
${i18n.t('usage_stats.loading_error')}: ${error.message}
`; + } + } + } + + // 更新使用统计概览 + updateUsageOverview(data) { + const safeData = data || {}; + document.getElementById('total-requests').textContent = safeData.total_requests ?? 0; + document.getElementById('success-requests').textContent = safeData.success_count ?? 0; + document.getElementById('failed-requests').textContent = safeData.failure_count ?? 0; + document.getElementById('total-tokens').textContent = safeData.total_tokens ?? 0; + } + + // 初始化图表 + initializeCharts() { + const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active'); + const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active'); + this.initializeRequestsChart(requestsHourActive ? 'hour' : 'day'); + this.initializeTokensChart(tokensHourActive ? 'hour' : 'day'); + } + + // 初始化请求趋势图表 + initializeRequestsChart(period = 'day') { + const ctx = document.getElementById('requests-chart'); + if (!ctx) return; + + // 销毁现有图表 + if (this.requestsChart) { + this.requestsChart.destroy(); + } + + const data = this.getRequestsChartData(period); + + this.requestsChart = new Chart(ctx, { + type: 'line', + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + } + }, + scales: { + x: { + title: { + display: true, + text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day') + } + }, + y: { + beginAtZero: true, + title: { + display: true, + text: i18n.t('usage_stats.requests_count') + } + } + }, + elements: { + line: { + borderColor: '#3b82f6', + backgroundColor: 'rgba(59, 130, 246, 0.1)', + fill: true, + tension: 0.4 + }, + point: { + backgroundColor: '#3b82f6', + borderColor: '#ffffff', + borderWidth: 2, + radius: 4 + } + } + } + }); + } + + // 初始化Token使用趋势图表 + initializeTokensChart(period = 'day') { + const ctx = document.getElementById('tokens-chart'); + if (!ctx) return; + + // 销毁现有图表 + if (this.tokensChart) { + this.tokensChart.destroy(); + } + + const data = this.getTokensChartData(period); + + this.tokensChart = new Chart(ctx, { + type: 'line', + data: data, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + } + }, + scales: { + x: { + title: { + display: true, + text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day') + } + }, + y: { + beginAtZero: true, + title: { + display: true, + text: i18n.t('usage_stats.tokens_count') + } + } + }, + elements: { + line: { + borderColor: '#10b981', + backgroundColor: 'rgba(16, 185, 129, 0.1)', + fill: true, + tension: 0.4 + }, + point: { + backgroundColor: '#10b981', + borderColor: '#ffffff', + borderWidth: 2, + radius: 4 + } + } + } + }); + } + + // 获取请求图表数据 + getRequestsChartData(period) { + if (!this.currentUsageData) { + return { labels: [], datasets: [{ data: [] }] }; + } + + let dataSource, labels, values; + + if (period === 'hour') { + dataSource = this.currentUsageData.requests_by_hour || {}; + labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')); + values = labels.map(hour => dataSource[hour] || 0); + } else { + dataSource = this.currentUsageData.requests_by_day || {}; + labels = Object.keys(dataSource).sort(); + values = labels.map(day => dataSource[day] || 0); + } + + return { + labels: labels, + datasets: [{ + data: values + }] + }; + } + + // 获取Token图表数据 + getTokensChartData(period) { + if (!this.currentUsageData) { + return { labels: [], datasets: [{ data: [] }] }; + } + + let dataSource, labels, values; + + if (period === 'hour') { + dataSource = this.currentUsageData.tokens_by_hour || {}; + labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0')); + values = labels.map(hour => dataSource[hour] || 0); + } else { + dataSource = this.currentUsageData.tokens_by_day || {}; + labels = Object.keys(dataSource).sort(); + values = labels.map(day => dataSource[day] || 0); + } + + return { + labels: labels, + datasets: [{ + data: values + }] + }; + } + + // 切换请求图表时间周期 + switchRequestsPeriod(period) { + // 更新按钮状态 + document.getElementById('requests-hour-btn').classList.toggle('active', period === 'hour'); + document.getElementById('requests-day-btn').classList.toggle('active', period === 'day'); + + // 更新图表数据 + if (this.requestsChart) { + const newData = this.getRequestsChartData(period); + this.requestsChart.data = newData; + this.requestsChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day'); + this.requestsChart.update(); + } + } + + // 切换Token图表时间周期 + switchTokensPeriod(period) { + // 更新按钮状态 + document.getElementById('tokens-hour-btn').classList.toggle('active', period === 'hour'); + document.getElementById('tokens-day-btn').classList.toggle('active', period === 'day'); + + // 更新图表数据 + if (this.tokensChart) { + const newData = this.getTokensChartData(period); + this.tokensChart.data = newData; + this.tokensChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day'); + this.tokensChart.update(); + } + } + + // 更新API详细统计表格 + updateApiStatsTable(data) { + const container = document.getElementById('api-stats-table'); + if (!container) return; + + const apis = data.apis || {}; + + if (Object.keys(apis).length === 0) { + container.innerHTML = `
${i18n.t('usage_stats.no_data')}
`; + return; + } + + let tableHtml = ` + + + + + + + + + + + + `; + + Object.entries(apis).forEach(([endpoint, apiData]) => { + const totalRequests = apiData.total_requests || 0; + const successCount = apiData.success_count ?? null; + const successRate = successCount !== null && totalRequests > 0 + ? Math.round((successCount / totalRequests) * 100) + : null; + + // 构建模型详情 + let modelsHtml = ''; + if (apiData.models && Object.keys(apiData.models).length > 0) { + modelsHtml = '
'; + Object.entries(apiData.models).forEach(([modelName, modelData]) => { + const modelRequests = modelData.total_requests ?? 0; + const modelTokens = modelData.total_tokens ?? 0; + modelsHtml += ` +
+ ${modelName} + ${modelRequests} 请求 / ${modelTokens} tokens +
+ `; + }); + modelsHtml += '
'; + } + + tableHtml += ` + + + + + + + + `; + }); + + tableHtml += '
${i18n.t('usage_stats.api_endpoint')}${i18n.t('usage_stats.requests_count')}${i18n.t('usage_stats.tokens_count')}${i18n.t('usage_stats.success_rate')}${i18n.t('usage_stats.models')}
${endpoint}${totalRequests}${apiData.total_tokens || 0}${successRate !== null ? successRate + '%' : '-'}${modelsHtml || '-'}
'; + container.innerHTML = tableHtml; + } + + showModal() { + const modal = document.getElementById('modal'); + if (modal) { + modal.style.display = 'block'; + } + } + + // 关闭模态框 + closeModal() { + document.getElementById('modal').style.display = 'none'; + } + + detectApiBaseFromLocation() { + try { + const { protocol, hostname, port } = window.location; + const normalizedPort = port ? `:${port}` : ''; + return this.normalizeBase(`${protocol}//${hostname}${normalizedPort}`); + } catch (error) { + console.warn('无法从当前地址检测 API 基础地址,使用默认设置', error); + return this.normalizeBase(this.apiBase || 'http://localhost:8317'); + } + } + + updateLoginConnectionInfo() { + const connectionUrlElement = document.getElementById('login-connection-url'); + const customInput = document.getElementById('login-api-base'); + if (connectionUrlElement) { + connectionUrlElement.textContent = this.apiBase || '-'; + } + if (customInput && customInput !== document.activeElement) { + customInput.value = this.apiBase || ''; + } + } + + addModelField(wrapperId, model = {}) { + const wrapper = document.getElementById(wrapperId); + if (!wrapper) return; + + const row = document.createElement('div'); + row.className = 'model-input-row'; + row.innerHTML = ` +
+ + + +
+ `; + + const removeBtn = row.querySelector('.model-remove-btn'); + if (removeBtn) { + removeBtn.addEventListener('click', () => { + wrapper.removeChild(row); + }); + } + + wrapper.appendChild(row); + } + + populateModelFields(wrapperId, models = []) { + const wrapper = document.getElementById(wrapperId); + if (!wrapper) return; + wrapper.innerHTML = ''; + + if (!models.length) { + this.addModelField(wrapperId); + return; + } + + models.forEach(model => this.addModelField(wrapperId, model)); + } + + collectModelInputs(wrapperId) { + const wrapper = document.getElementById(wrapperId); + if (!wrapper) return []; + + const rows = Array.from(wrapper.querySelectorAll('.model-input-row')); + const models = []; + + rows.forEach(row => { + const nameInput = row.querySelector('.model-name-input'); + const aliasInput = row.querySelector('.model-alias-input'); + const name = nameInput ? nameInput.value.trim() : ''; + const alias = aliasInput ? aliasInput.value.trim() : ''; + + if (name) { + const model = { name }; + if (alias) { + model.alias = alias; + } + models.push(model); + } + }); + + return models; + } + + renderOpenAIModelBadges(models) { + if (!models || models.length === 0) { + return ''; + } + + return ` +
+ ${models.map(model => ` + + ${this.escapeHtml(model.name || '')} + ${model.alias ? `${this.escapeHtml(model.alias)}` : ''} + + `).join('')} +
+ `; + } + + validateOpenAIProviderInput(name, baseUrl, models) { + if (!name || !baseUrl) { + this.showNotification(i18n.t('notification.openai_provider_required'), 'error'); + return false; + } + + const invalidModel = models.find(model => !model.name); + if (invalidModel) { + this.showNotification(i18n.t('notification.openai_model_name_required'), 'error'); + return false; + } + + return true; + } +} + +// 全局管理器实例 +let manager; + +// 尝试自动加载根目录 Logo(支持多种常见文件名/扩展名) +function setupSiteLogo() { + const img = document.getElementById('site-logo'); + const loginImg = document.getElementById('login-logo'); + if (!img && !loginImg) return; + + const inlineLogo = typeof window !== 'undefined' ? window.__INLINE_LOGO__ : null; + if (inlineLogo) { + if (img) { + img.src = inlineLogo; + img.style.display = 'inline-block'; + } + if (loginImg) { + loginImg.src = inlineLogo; + loginImg.style.display = 'inline-block'; + } + return; + } + + const candidates = [ + '../logo.svg', '../logo.png', '../logo.jpg', '../logo.jpeg', '../logo.webp', '../logo.gif', + 'logo.svg', 'logo.png', 'logo.jpg', 'logo.jpeg', 'logo.webp', 'logo.gif', + '/logo.svg', '/logo.png', '/logo.jpg', '/logo.jpeg', '/logo.webp', '/logo.gif' + ]; + let idx = 0; + const tryNext = () => { + if (idx >= candidates.length) return; + const test = new Image(); + test.onload = () => { + if (img) { + img.src = test.src; + img.style.display = 'inline-block'; + } + if (loginImg) { + loginImg.src = test.src; + loginImg.style.display = 'inline-block'; + } + }; + test.onerror = () => { + idx++; + tryNext(); + }; + test.src = candidates[idx]; + }; + tryNext(); +} + +// 页面加载完成后初始化 +document.addEventListener('DOMContentLoaded', () => { + // 初始化国际化 + i18n.init(); + + setupSiteLogo(); + manager = new CLIProxyManager(); +}); diff --git a/i18n.js b/i18n.js index 0d500b2..28a9dcc 100644 --- a/i18n.js +++ b/i18n.js @@ -1,666 +1,687 @@ -// 国际化语言包 -const i18n = { - // 语言配置 - currentLanguage: 'zh-CN', - fallbackLanguage: 'zh-CN', - - // 语言包 - translations: { - 'zh-CN': { - // 通用 - 'common.login': '登录', - 'common.logout': '登出', - 'common.cancel': '取消', - 'common.confirm': '确认', - 'common.save': '保存', - 'common.delete': '删除', - 'common.edit': '编辑', - 'common.add': '添加', - 'common.update': '更新', - 'common.refresh': '刷新', - 'common.close': '关闭', - 'common.success': '成功', - 'common.error': '错误', - 'common.info': '信息', - 'common.warning': '警告', - 'common.loading': '加载中...', - 'common.connecting': '连接中...', - 'common.connected': '已连接', - 'common.disconnected': '未连接', - 'common.connecting_status': '连接中', - 'common.connected_status': '已连接', - 'common.disconnected_status': '未连接', - 'common.yes': '是', - 'common.no': '否', - 'common.optional': '可选', - 'common.required': '必填', - 'common.api_key': '密钥', - 'common.base_url': '地址', - - // 页面标题 - 'title.main': 'CLI Proxy API Management Center', - 'title.login': 'CLI Proxy API Management Center', - - // 自动登录 - 'auto_login.title': '正在自动登录...', - 'auto_login.message': '正在使用本地保存的连接信息尝试连接服务器', - - // 登录页面 - 'login.subtitle': '请输入连接信息以访问管理界面', - 'login.connection_title': '连接地址', - 'login.connection_current': '当前地址', - 'login.connection_auto_hint': '系统将自动使用当前访问地址进行连接', - 'login.custom_connection_label': '自定义连接地址:', - 'login.custom_connection_placeholder': '例如: https://example.com:8317', - 'login.custom_connection_hint': '默认使用当前访问地址,若需要可手动输入其他地址。', - 'login.use_current_address': '使用当前地址', - 'login.management_key_label': '管理密钥:', - 'login.management_key_placeholder': '请输入管理密钥', - 'login.connect_button': '连接', - 'login.submit_button': '登录', - 'login.submitting': '连接中...', - 'login.error_title': '登录失败', - 'login.error_required': '请填写完整的连接信息', - 'login.error_invalid': '连接失败,请检查地址和密钥', - - // 头部导航 - 'header.check_connection': '检查连接', - 'header.refresh_all': '刷新全部', - 'header.logout': '登出', - - // 连接信息 - 'connection.title': '连接信息', - 'connection.server_address': '服务器地址:', - 'connection.management_key': '管理密钥:', - 'connection.status': '连接状态:', - - // 侧边栏导航 - 'nav.basic_settings': '基础设置', - 'nav.api_keys': 'API 密钥', - 'nav.ai_providers': 'AI 提供商', - 'nav.auth_files': '认证文件', - 'nav.usage_stats': '使用统计', - 'nav.system_info': '系统信息', - - // 基础设置 - 'basic_settings.title': '基础设置', - 'basic_settings.debug_title': '调试模式', - 'basic_settings.debug_enable': '启用调试模式', - 'basic_settings.proxy_title': '代理设置', - 'basic_settings.proxy_url_label': '代理 URL:', - 'basic_settings.proxy_url_placeholder': '例如: socks5://user:pass@127.0.0.1:1080/', - 'basic_settings.proxy_update': '更新', - 'basic_settings.proxy_clear': '清空', - 'basic_settings.retry_title': '请求重试', - 'basic_settings.retry_count_label': '重试次数:', - 'basic_settings.retry_update': '更新', - 'basic_settings.quota_title': '配额超出行为', - 'basic_settings.quota_switch_project': '自动切换项目', - 'basic_settings.quota_switch_preview': '切换到预览模型', - - // API 密钥管理 - 'api_keys.title': 'API 密钥管理', - 'api_keys.proxy_auth_title': '代理服务认证密钥', - 'api_keys.add_button': '添加密钥', - 'api_keys.empty_title': '暂无API密钥', - 'api_keys.empty_desc': '点击上方按钮添加第一个密钥', - 'api_keys.item_title': 'API密钥', - 'api_keys.add_modal_title': '添加API密钥', - 'api_keys.add_modal_key_label': 'API密钥:', - 'api_keys.add_modal_key_placeholder': '请输入API密钥', - 'api_keys.edit_modal_title': '编辑API密钥', - 'api_keys.edit_modal_key_label': 'API密钥:', - 'api_keys.delete_confirm': '确定要删除这个API密钥吗?', - - // AI 提供商 - 'ai_providers.title': 'AI 提供商配置', - 'ai_providers.gemini_title': 'Gemini API 密钥', - 'ai_providers.gemini_add_button': '添加密钥', - 'ai_providers.gemini_empty_title': '暂无Gemini密钥', - 'ai_providers.gemini_empty_desc': '点击上方按钮添加第一个密钥', - 'ai_providers.gemini_item_title': 'Gemini密钥', - 'ai_providers.gemini_add_modal_title': '添加Gemini API密钥', - 'ai_providers.gemini_add_modal_key_label': 'API密钥:', - 'ai_providers.gemini_add_modal_key_placeholder': '请输入Gemini API密钥', - 'ai_providers.gemini_edit_modal_title': '编辑Gemini API密钥', - 'ai_providers.gemini_edit_modal_key_label': 'API密钥:', - 'ai_providers.gemini_delete_confirm': '确定要删除这个Gemini密钥吗?', - - 'ai_providers.codex_title': 'Codex API 配置', - 'ai_providers.codex_add_button': '添加配置', - 'ai_providers.codex_empty_title': '暂无Codex配置', - 'ai_providers.codex_empty_desc': '点击上方按钮添加第一个配置', - 'ai_providers.codex_item_title': 'Codex配置', - 'ai_providers.codex_add_modal_title': '添加Codex API配置', - 'ai_providers.codex_add_modal_key_label': 'API密钥:', - 'ai_providers.codex_add_modal_key_placeholder': '请输入Codex API密钥', - 'ai_providers.codex_add_modal_url_label': 'Base URL (可选):', - 'ai_providers.codex_add_modal_url_placeholder': '例如: https://api.example.com', - 'ai_providers.codex_edit_modal_title': '编辑Codex API配置', - 'ai_providers.codex_edit_modal_key_label': 'API密钥:', - 'ai_providers.codex_edit_modal_url_label': 'Base URL (可选):', - 'ai_providers.codex_delete_confirm': '确定要删除这个Codex配置吗?', - - 'ai_providers.claude_title': 'Claude API 配置', - 'ai_providers.claude_add_button': '添加配置', - 'ai_providers.claude_empty_title': '暂无Claude配置', - 'ai_providers.claude_empty_desc': '点击上方按钮添加第一个配置', - 'ai_providers.claude_item_title': 'Claude配置', - 'ai_providers.claude_add_modal_title': '添加Claude API配置', - 'ai_providers.claude_add_modal_key_label': 'API密钥:', - 'ai_providers.claude_add_modal_key_placeholder': '请输入Claude API密钥', - 'ai_providers.claude_add_modal_url_label': 'Base URL (可选):', - 'ai_providers.claude_add_modal_url_placeholder': '例如: https://api.anthropic.com', - 'ai_providers.claude_edit_modal_title': '编辑Claude API配置', - 'ai_providers.claude_edit_modal_key_label': 'API密钥:', - 'ai_providers.claude_edit_modal_url_label': 'Base URL (可选):', - 'ai_providers.claude_delete_confirm': '确定要删除这个Claude配置吗?', - - 'ai_providers.openai_title': 'OpenAI 兼容提供商', - 'ai_providers.openai_add_button': '添加提供商', - 'ai_providers.openai_empty_title': '暂无OpenAI兼容提供商', - 'ai_providers.openai_empty_desc': '点击上方按钮添加第一个提供商', - 'ai_providers.openai_add_modal_title': '添加OpenAI兼容提供商', - 'ai_providers.openai_add_modal_name_label': '提供商名称:', - 'ai_providers.openai_add_modal_name_placeholder': '例如: openrouter', - 'ai_providers.openai_add_modal_url_label': 'Base URL:', - 'ai_providers.openai_add_modal_url_placeholder': '例如: https://openrouter.ai/api/v1', - 'ai_providers.openai_add_modal_keys_label': 'API密钥 (每行一个):', - 'ai_providers.openai_add_modal_keys_placeholder': 'sk-key1\nsk-key2', - 'ai_providers.openai_edit_modal_title': '编辑OpenAI兼容提供商', - 'ai_providers.openai_edit_modal_name_label': '提供商名称:', - 'ai_providers.openai_edit_modal_url_label': 'Base URL:', - 'ai_providers.openai_edit_modal_keys_label': 'API密钥 (每行一个):', - 'ai_providers.openai_delete_confirm': '确定要删除这个OpenAI提供商吗?', - 'ai_providers.openai_keys_count': '密钥数量', - 'ai_providers.openai_models_count': '模型数量', - - - // 认证文件管理 - 'auth_files.title': '认证文件管理', - 'auth_files.title_section': '认证文件', - 'auth_files.description': '这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。', - 'auth_files.upload_button': '上传文件', - 'auth_files.delete_all_button': '删除全部', - 'auth_files.empty_title': '暂无认证文件', - 'auth_files.empty_desc': '点击上方按钮上传第一个文件', - 'auth_files.file_size': '大小', - 'auth_files.file_modified': '修改时间', - 'auth_files.download_button': '下载', - 'auth_files.delete_button': '删除', - 'auth_files.delete_confirm': '确定要删除文件', - 'auth_files.delete_all_confirm': '确定要删除所有认证文件吗?此操作不可恢复!', - 'auth_files.upload_error_json': '只能上传JSON文件', - 'auth_files.upload_success': '文件上传成功', - 'auth_files.download_success': '文件下载成功', - 'auth_files.delete_success': '文件删除成功', - 'auth_files.delete_all_success': '成功删除', - 'auth_files.files_count': '个文件', - - // Gemini Web Token - 'auth_login.gemini_web_title': 'Gemini Web Token', - 'auth_login.gemini_web_button': '保存 Gemini Web Token', - 'auth_login.gemini_web_hint': '从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。', - 'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:', - 'auth_login.secure_1psid_placeholder': '输入 __Secure-1PSID cookie 值', - 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:', - 'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值', - 'auth_login.gemini_web_label_label': '标签 (可选):', - 'auth_login.gemini_web_label_placeholder': '输入标签名称 (可选)', - 'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功', - - // 使用统计 - 'usage_stats.title': '使用统计', - 'usage_stats.total_requests': '总请求数', - 'usage_stats.success_requests': '成功请求', - 'usage_stats.failed_requests': '失败请求', - 'usage_stats.total_tokens': '总Token数', - 'usage_stats.requests_trend': '请求趋势', - 'usage_stats.tokens_trend': 'Token 使用趋势', - 'usage_stats.api_details': 'API 详细统计', - 'usage_stats.by_hour': '按小时', - 'usage_stats.by_day': '按天', - 'usage_stats.refresh': '刷新', - 'usage_stats.no_data': '暂无数据', - 'usage_stats.loading_error': '加载失败', - 'usage_stats.api_endpoint': 'API端点', - 'usage_stats.requests_count': '请求次数', - 'usage_stats.tokens_count': 'Token数量', - 'usage_stats.models': '模型统计', - 'usage_stats.success_rate': '成功率', - - // 系统信息 - 'system_info.title': '系统信息', - 'system_info.connection_status_title': '连接状态', - 'system_info.api_status_label': 'API 状态:', - 'system_info.config_status_label': '配置状态:', - 'system_info.last_update_label': '最后更新:', - 'system_info.cache_data': '缓存数据', - 'system_info.real_time_data': '实时数据', - 'system_info.not_loaded': '未加载', - 'system_info.seconds_ago': '秒前', - - // 通知消息 - 'notification.debug_updated': '调试设置已更新', - 'notification.proxy_updated': '代理设置已更新', - 'notification.proxy_cleared': '代理设置已清空', - 'notification.retry_updated': '重试设置已更新', - 'notification.quota_switch_project_updated': '项目切换设置已更新', - 'notification.quota_switch_preview_updated': '预览模型切换设置已更新', - 'notification.api_key_added': 'API密钥添加成功', - 'notification.api_key_updated': 'API密钥更新成功', - 'notification.api_key_deleted': 'API密钥删除成功', - 'notification.gemini_key_added': 'Gemini密钥添加成功', - 'notification.gemini_key_updated': 'Gemini密钥更新成功', - 'notification.gemini_key_deleted': 'Gemini密钥删除成功', - 'notification.codex_config_added': 'Codex配置添加成功', - 'notification.codex_config_updated': 'Codex配置更新成功', - 'notification.codex_config_deleted': 'Codex配置删除成功', - 'notification.claude_config_added': 'Claude配置添加成功', - 'notification.claude_config_updated': 'Claude配置更新成功', - 'notification.claude_config_deleted': 'Claude配置删除成功', - 'notification.openai_provider_added': 'OpenAI提供商添加成功', - 'notification.openai_provider_updated': 'OpenAI提供商更新成功', - 'notification.openai_provider_deleted': 'OpenAI提供商删除成功', - 'notification.data_refreshed': '数据刷新成功', - 'notification.connection_required': '请先建立连接', - 'notification.refresh_failed': '刷新失败', - 'notification.update_failed': '更新失败', - 'notification.add_failed': '添加失败', - 'notification.delete_failed': '删除失败', - 'notification.upload_failed': '上传失败', - 'notification.download_failed': '下载失败', - 'notification.login_failed': '登录失败', - 'notification.please_enter': '请输入', - 'notification.please_fill': '请填写', - 'notification.provider_name_url': '提供商名称和Base URL', - 'notification.api_key': 'API密钥', - 'notification.gemini_api_key': 'Gemini API密钥', - 'notification.codex_api_key': 'Codex API密钥', - 'notification.claude_api_key': 'Claude API密钥', - - // 语言切换 - 'language.switch': '语言', - 'language.chinese': '中文', - 'language.english': 'English', - - // 主题切换 - 'theme.switch': '主题', - 'theme.light': '亮色', - 'theme.dark': '暗色', - 'theme.switch_to_light': '切换到亮色模式', - 'theme.switch_to_dark': '切换到暗色模式', - 'theme.auto': '跟随系统', - - // 页脚 - 'footer.version': '版本', - 'footer.author': '作者' - }, - - 'en-US': { - // Common - 'common.login': 'Login', - 'common.logout': 'Logout', - 'common.cancel': 'Cancel', - 'common.confirm': 'Confirm', - 'common.save': 'Save', - 'common.delete': 'Delete', - 'common.edit': 'Edit', - 'common.add': 'Add', - 'common.update': 'Update', - 'common.refresh': 'Refresh', - 'common.close': 'Close', - 'common.success': 'Success', - 'common.error': 'Error', - 'common.info': 'Info', - 'common.warning': 'Warning', - 'common.loading': 'Loading...', - 'common.connecting': 'Connecting...', - 'common.connected': 'Connected', - 'common.disconnected': 'Disconnected', - 'common.connecting_status': 'Connecting', - 'common.connected_status': 'Connected', - 'common.disconnected_status': 'Disconnected', - 'common.yes': 'Yes', - 'common.no': 'No', - 'common.optional': 'Optional', - 'common.required': 'Required', - 'common.api_key': 'Key', - 'common.base_url': 'Address', - - // Page titles - 'title.main': 'CLI Proxy API Management Center', - 'title.login': 'CLI Proxy API Management Center', - - // Auto login - 'auto_login.title': 'Auto Login in Progress...', - 'auto_login.message': 'Attempting to connect to server using locally saved connection information', - - // Login page - 'login.subtitle': 'Please enter connection information to access the management interface', - 'login.connection_title': 'Connection Address', - 'login.connection_current': 'Current URL', - 'login.connection_auto_hint': 'The system will automatically use the current URL for connection', - 'login.custom_connection_label': 'Custom Connection URL:', - 'login.custom_connection_placeholder': 'Eg: https://example.com:8317', - 'login.custom_connection_hint': 'By default the current URL is used. Override it here if needed.', - 'login.use_current_address': 'Use Current URL', - 'login.management_key_label': 'Management Key:', - 'login.management_key_placeholder': 'Enter the management key', - 'login.connect_button': 'Connect', - 'login.submit_button': 'Login', - 'login.submitting': 'Connecting...', - 'login.error_title': 'Login Failed', - 'login.error_required': 'Please fill in complete connection information', - 'login.error_invalid': 'Connection failed, please check address and key', - - // Header navigation - 'header.check_connection': 'Check Connection', - 'header.refresh_all': 'Refresh All', - 'header.logout': 'Logout', - - // Connection info - 'connection.title': 'Connection Information', - 'connection.server_address': 'Server Address:', - 'connection.management_key': 'Management Key:', - 'connection.status': 'Connection Status:', - - // Sidebar navigation - 'nav.basic_settings': 'Basic Settings', - 'nav.api_keys': 'API Keys', - 'nav.ai_providers': 'AI Providers', - 'nav.auth_files': 'Auth Files', - 'nav.usage_stats': 'Usage Statistics', - 'nav.system_info': 'System Info', - - // Basic settings - 'basic_settings.title': 'Basic Settings', - 'basic_settings.debug_title': 'Debug Mode', - 'basic_settings.debug_enable': 'Enable Debug Mode', - 'basic_settings.proxy_title': 'Proxy Settings', - 'basic_settings.proxy_url_label': 'Proxy URL:', - 'basic_settings.proxy_url_placeholder': 'e.g.: socks5://user:pass@127.0.0.1:1080/', - 'basic_settings.proxy_update': 'Update', - 'basic_settings.proxy_clear': 'Clear', - 'basic_settings.retry_title': 'Request Retry', - 'basic_settings.retry_count_label': 'Retry Count:', - 'basic_settings.retry_update': 'Update', - 'basic_settings.quota_title': 'Quota Exceeded Behavior', - 'basic_settings.quota_switch_project': 'Auto Switch Project', - 'basic_settings.quota_switch_preview': 'Switch to Preview Model', - - // API Keys management - 'api_keys.title': 'API Keys Management', - 'api_keys.proxy_auth_title': 'Proxy Service Authentication Keys', - 'api_keys.add_button': 'Add Key', - 'api_keys.empty_title': 'No API Keys', - 'api_keys.empty_desc': 'Click the button above to add the first key', - 'api_keys.item_title': 'API Key', - 'api_keys.add_modal_title': 'Add API Key', - 'api_keys.add_modal_key_label': 'API Key:', - 'api_keys.add_modal_key_placeholder': 'Please enter API key', - 'api_keys.edit_modal_title': 'Edit API Key', - 'api_keys.edit_modal_key_label': 'API Key:', - 'api_keys.delete_confirm': 'Are you sure you want to delete this API key?', - - // AI Providers - 'ai_providers.title': 'AI Providers Configuration', - 'ai_providers.gemini_title': 'Gemini API Keys', - 'ai_providers.gemini_add_button': 'Add Key', - 'ai_providers.gemini_empty_title': 'No Gemini Keys', - 'ai_providers.gemini_empty_desc': 'Click the button above to add the first key', - 'ai_providers.gemini_item_title': 'Gemini Key', - 'ai_providers.gemini_add_modal_title': 'Add Gemini API Key', - 'ai_providers.gemini_add_modal_key_label': 'API Key:', - 'ai_providers.gemini_add_modal_key_placeholder': 'Please enter Gemini API key', - 'ai_providers.gemini_edit_modal_title': 'Edit Gemini API Key', - 'ai_providers.gemini_edit_modal_key_label': 'API Key:', - 'ai_providers.gemini_delete_confirm': 'Are you sure you want to delete this Gemini key?', - - 'ai_providers.codex_title': 'Codex API Configuration', - 'ai_providers.codex_add_button': 'Add Configuration', - 'ai_providers.codex_empty_title': 'No Codex Configuration', - 'ai_providers.codex_empty_desc': 'Click the button above to add the first configuration', - 'ai_providers.codex_item_title': 'Codex Configuration', - 'ai_providers.codex_add_modal_title': 'Add Codex API Configuration', - 'ai_providers.codex_add_modal_key_label': 'API Key:', - 'ai_providers.codex_add_modal_key_placeholder': 'Please enter Codex API key', - 'ai_providers.codex_add_modal_url_label': 'Base URL (Optional):', - 'ai_providers.codex_add_modal_url_placeholder': 'e.g.: https://api.example.com', - 'ai_providers.codex_edit_modal_title': 'Edit Codex API Configuration', - 'ai_providers.codex_edit_modal_key_label': 'API Key:', - 'ai_providers.codex_edit_modal_url_label': 'Base URL (Optional):', - 'ai_providers.codex_delete_confirm': 'Are you sure you want to delete this Codex configuration?', - - 'ai_providers.claude_title': 'Claude API Configuration', - 'ai_providers.claude_add_button': 'Add Configuration', - 'ai_providers.claude_empty_title': 'No Claude Configuration', - 'ai_providers.claude_empty_desc': 'Click the button above to add the first configuration', - 'ai_providers.claude_item_title': 'Claude Configuration', - 'ai_providers.claude_add_modal_title': 'Add Claude API Configuration', - 'ai_providers.claude_add_modal_key_label': 'API Key:', - 'ai_providers.claude_add_modal_key_placeholder': 'Please enter Claude API key', - 'ai_providers.claude_add_modal_url_label': 'Base URL (Optional):', - 'ai_providers.claude_add_modal_url_placeholder': 'e.g.: https://api.anthropic.com', - 'ai_providers.claude_edit_modal_title': 'Edit Claude API Configuration', - 'ai_providers.claude_edit_modal_key_label': 'API Key:', - 'ai_providers.claude_edit_modal_url_label': 'Base URL (Optional):', - 'ai_providers.claude_delete_confirm': 'Are you sure you want to delete this Claude configuration?', - - 'ai_providers.openai_title': 'OpenAI Compatible Providers', - 'ai_providers.openai_add_button': 'Add Provider', - 'ai_providers.openai_empty_title': 'No OpenAI Compatible Providers', - 'ai_providers.openai_empty_desc': 'Click the button above to add the first provider', - 'ai_providers.openai_add_modal_title': 'Add OpenAI Compatible Provider', - 'ai_providers.openai_add_modal_name_label': 'Provider Name:', - 'ai_providers.openai_add_modal_name_placeholder': 'e.g.: openrouter', - 'ai_providers.openai_add_modal_url_label': 'Base URL:', - 'ai_providers.openai_add_modal_url_placeholder': 'e.g.: https://openrouter.ai/api/v1', - 'ai_providers.openai_add_modal_keys_label': 'API Keys (one per line):', - 'ai_providers.openai_add_modal_keys_placeholder': 'sk-key1\nsk-key2', - 'ai_providers.openai_edit_modal_title': 'Edit OpenAI Compatible Provider', - 'ai_providers.openai_edit_modal_name_label': 'Provider Name:', - 'ai_providers.openai_edit_modal_url_label': 'Base URL:', - 'ai_providers.openai_edit_modal_keys_label': 'API Keys (one per line):', - 'ai_providers.openai_delete_confirm': 'Are you sure you want to delete this OpenAI provider?', - 'ai_providers.openai_keys_count': 'Keys Count', - 'ai_providers.openai_models_count': 'Models Count', - - - // Auth files management - 'auth_files.title': 'Auth Files Management', - 'auth_files.title_section': 'Auth Files', - 'auth_files.description': 'Here you can manage authentication configuration files for Qwen and Gemini. Upload JSON format authentication files to enable the corresponding AI services.', - 'auth_files.upload_button': 'Upload File', - 'auth_files.delete_all_button': 'Delete All', - 'auth_files.empty_title': 'No Auth Files', - 'auth_files.empty_desc': 'Click the button above to upload the first file', - 'auth_files.file_size': 'Size', - 'auth_files.file_modified': 'Modified', - 'auth_files.download_button': 'Download', - 'auth_files.delete_button': 'Delete', - 'auth_files.delete_confirm': 'Are you sure you want to delete file', - 'auth_files.delete_all_confirm': 'Are you sure you want to delete all auth files? This operation cannot be undone!', - 'auth_files.upload_error_json': 'Only JSON files are allowed', - 'auth_files.upload_success': 'File uploaded successfully', - 'auth_files.download_success': 'File downloaded successfully', - 'auth_files.delete_success': 'File deleted successfully', - 'auth_files.delete_all_success': 'Successfully deleted', - 'auth_files.files_count': 'files', - - // Gemini Web Token - 'auth_login.gemini_web_title': 'Gemini Web Token', - 'auth_login.gemini_web_button': 'Save Gemini Web Token', - 'auth_login.gemini_web_hint': 'Obtain the Cookie value of the Gemini web version from the browser\'s developer tools, used for direct authentication to access Gemini.', - 'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:', - 'auth_login.secure_1psid_placeholder': 'Enter __Secure-1PSID cookie value', - 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:', - 'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value', - 'auth_login.gemini_web_label_label': 'Label (Optional):', - 'auth_login.gemini_web_label_placeholder': 'Enter label name (optional)', - 'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully', - - // Usage Statistics - 'usage_stats.title': 'Usage Statistics', - 'usage_stats.total_requests': 'Total Requests', - 'usage_stats.success_requests': 'Success Requests', - 'usage_stats.failed_requests': 'Failed Requests', - 'usage_stats.total_tokens': 'Total Tokens', - 'usage_stats.requests_trend': 'Request Trends', - 'usage_stats.tokens_trend': 'Token Usage Trends', - 'usage_stats.api_details': 'API Details', - 'usage_stats.by_hour': 'By Hour', - 'usage_stats.by_day': 'By Day', - 'usage_stats.refresh': 'Refresh', - 'usage_stats.no_data': 'No Data Available', - 'usage_stats.loading_error': 'Loading Failed', - 'usage_stats.api_endpoint': 'API Endpoint', - 'usage_stats.requests_count': 'Request Count', - 'usage_stats.tokens_count': 'Token Count', - 'usage_stats.models': 'Model Statistics', - 'usage_stats.success_rate': 'Success Rate', - - // System info - 'system_info.title': 'System Information', - 'system_info.connection_status_title': 'Connection Status', - 'system_info.api_status_label': 'API Status:', - 'system_info.config_status_label': 'Config Status:', - 'system_info.last_update_label': 'Last Update:', - 'system_info.cache_data': 'Cache Data', - 'system_info.real_time_data': 'Real-time Data', - 'system_info.not_loaded': 'Not Loaded', - 'system_info.seconds_ago': 'seconds ago', - - // Notification messages - 'notification.debug_updated': 'Debug settings updated', - 'notification.proxy_updated': 'Proxy settings updated', - 'notification.proxy_cleared': 'Proxy settings cleared', - 'notification.retry_updated': 'Retry settings updated', - 'notification.quota_switch_project_updated': 'Project switch settings updated', - 'notification.quota_switch_preview_updated': 'Preview model switch settings updated', - 'notification.api_key_added': 'API key added successfully', - 'notification.api_key_updated': 'API key updated successfully', - 'notification.api_key_deleted': 'API key deleted successfully', - 'notification.gemini_key_added': 'Gemini key added successfully', - 'notification.gemini_key_updated': 'Gemini key updated successfully', - 'notification.gemini_key_deleted': 'Gemini key deleted successfully', - 'notification.codex_config_added': 'Codex configuration added successfully', - 'notification.codex_config_updated': 'Codex configuration updated successfully', - 'notification.codex_config_deleted': 'Codex configuration deleted successfully', - 'notification.claude_config_added': 'Claude configuration added successfully', - 'notification.claude_config_updated': 'Claude configuration updated successfully', - 'notification.claude_config_deleted': 'Claude configuration deleted successfully', - 'notification.openai_provider_added': 'OpenAI provider added successfully', - 'notification.openai_provider_updated': 'OpenAI provider updated successfully', - 'notification.openai_provider_deleted': 'OpenAI provider deleted successfully', - 'notification.data_refreshed': 'Data refreshed successfully', - 'notification.connection_required': 'Please establish connection first', - 'notification.refresh_failed': 'Refresh failed', - 'notification.update_failed': 'Update failed', - 'notification.add_failed': 'Add failed', - 'notification.delete_failed': 'Delete failed', - 'notification.upload_failed': 'Upload failed', - 'notification.download_failed': 'Download failed', - 'notification.login_failed': 'Login failed', - 'notification.please_enter': 'Please enter', - 'notification.please_fill': 'Please fill', - 'notification.provider_name_url': 'provider name and Base URL', - 'notification.api_key': 'API key', - 'notification.gemini_api_key': 'Gemini API key', - 'notification.codex_api_key': 'Codex API key', - 'notification.claude_api_key': 'Claude API key', - - // Language switch - 'language.switch': 'Language', - 'language.chinese': '中文', - 'language.english': 'English', - - // Theme switch - 'theme.switch': 'Theme', - 'theme.light': 'Light', - 'theme.dark': 'Dark', - 'theme.switch_to_light': 'Switch to light mode', - 'theme.switch_to_dark': 'Switch to dark mode', - 'theme.auto': 'Follow system', - - // Footer - 'footer.version': 'Version', - 'footer.author': 'Author' - } - }, - - // 获取翻译文本 - t(key, params = {}) { - const translation = this.translations[this.currentLanguage]?.[key] || - this.translations[this.fallbackLanguage]?.[key] || - key; - - // 简单的参数替换 - return translation.replace(/\{(\w+)\}/g, (match, param) => { - return params[param] || match; - }); - }, - - // 设置语言 - setLanguage(lang) { - if (this.translations[lang]) { - this.currentLanguage = lang; - localStorage.setItem('preferredLanguage', lang); - this.updatePageLanguage(); - this.updateAllTexts(); - } - }, - - // 更新页面语言属性 - updatePageLanguage() { - document.documentElement.lang = this.currentLanguage; - }, - - // 更新所有文本 - updateAllTexts() { - // 更新所有带有 data-i18n 属性的元素 - document.querySelectorAll('[data-i18n]').forEach(element => { - const key = element.getAttribute('data-i18n'); - const text = this.t(key); - - if (element.tagName === 'INPUT' && (element.type === 'text' || element.type === 'password')) { - element.placeholder = text; - } else if (element.tagName === 'TITLE') { - element.textContent = text; - } else { - element.textContent = text; - } - }); - - // 更新所有带有 data-i18n-html 属性的元素(支持HTML) - document.querySelectorAll('[data-i18n-html]').forEach(element => { - const key = element.getAttribute('data-i18n-html'); - const html = this.t(key); - element.innerHTML = html; - }); - }, - - // 初始化 - init() { - // 从本地存储获取用户偏好语言 - const savedLanguage = localStorage.getItem('preferredLanguage'); - if (savedLanguage && this.translations[savedLanguage]) { - this.currentLanguage = savedLanguage; - } else { - // 根据浏览器语言自动选择 - const browserLang = navigator.language || navigator.userLanguage; - if (browserLang.startsWith('zh')) { - this.currentLanguage = 'zh-CN'; - } else { - this.currentLanguage = 'en-US'; - } - } - - this.updatePageLanguage(); - this.updateAllTexts(); - } -}; - -// 全局函数,供HTML调用 -window.t = (key, params) => i18n.t(key, params); -window.setLanguage = (lang) => i18n.setLanguage(lang); +// 国际化语言包 +const i18n = { + // 语言配置 + currentLanguage: 'zh-CN', + fallbackLanguage: 'zh-CN', + + // 语言包 + translations: { + 'zh-CN': { + // 通用 + 'common.login': '登录', + 'common.logout': '登出', + 'common.cancel': '取消', + 'common.confirm': '确认', + 'common.save': '保存', + 'common.delete': '删除', + 'common.edit': '编辑', + 'common.add': '添加', + 'common.update': '更新', + 'common.refresh': '刷新', + 'common.close': '关闭', + 'common.success': '成功', + 'common.error': '错误', + 'common.info': '信息', + 'common.warning': '警告', + 'common.loading': '加载中...', + 'common.connecting': '连接中...', + 'common.connected': '已连接', + 'common.disconnected': '未连接', + 'common.connecting_status': '连接中', + 'common.connected_status': '已连接', + 'common.disconnected_status': '未连接', + 'common.yes': '是', + 'common.no': '否', + 'common.optional': '可选', + 'common.required': '必填', + 'common.api_key': '密钥', + 'common.base_url': '地址', + 'common.proxy_url': '代理', + 'common.alias': '别名', + + // 页面标题 + 'title.main': 'CLI Proxy API Management Center', + 'title.login': 'CLI Proxy API Management Center', + + // 自动登录 + 'auto_login.title': '正在自动登录...', + 'auto_login.message': '正在使用本地保存的连接信息尝试连接服务器', + + // 登录页面 + 'login.subtitle': '请输入连接信息以访问管理界面', + 'login.connection_title': '连接地址', + 'login.connection_current': '当前地址', + 'login.connection_auto_hint': '系统将自动使用当前访问地址进行连接', + 'login.custom_connection_label': '自定义连接地址:', + 'login.custom_connection_placeholder': '例如: https://example.com:8317', + 'login.custom_connection_hint': '默认使用当前访问地址,若需要可手动输入其他地址。', + 'login.use_current_address': '使用当前地址', + 'login.management_key_label': '管理密钥:', + 'login.management_key_placeholder': '请输入管理密钥', + 'login.connect_button': '连接', + 'login.submit_button': '登录', + 'login.submitting': '连接中...', + 'login.error_title': '登录失败', + 'login.error_required': '请填写完整的连接信息', + 'login.error_invalid': '连接失败,请检查地址和密钥', + + // 头部导航 + 'header.check_connection': '检查连接', + 'header.refresh_all': '刷新全部', + 'header.logout': '登出', + + // 连接信息 + 'connection.title': '连接信息', + 'connection.server_address': '服务器地址:', + 'connection.management_key': '管理密钥:', + 'connection.status': '连接状态:', + + // 侧边栏导航 + 'nav.basic_settings': '基础设置', + 'nav.api_keys': 'API 密钥', + 'nav.ai_providers': 'AI 提供商', + 'nav.auth_files': '认证文件', + 'nav.usage_stats': '使用统计', + 'nav.system_info': '系统信息', + + // 基础设置 + 'basic_settings.title': '基础设置', + 'basic_settings.debug_title': '调试模式', + 'basic_settings.debug_enable': '启用调试模式', + 'basic_settings.proxy_title': '代理设置', + 'basic_settings.proxy_url_label': '代理 URL:', + 'basic_settings.proxy_url_placeholder': '例如: socks5://user:pass@127.0.0.1:1080/', + 'basic_settings.proxy_update': '更新', + 'basic_settings.proxy_clear': '清空', + 'basic_settings.retry_title': '请求重试', + 'basic_settings.retry_count_label': '重试次数:', + 'basic_settings.retry_update': '更新', + 'basic_settings.quota_title': '配额超出行为', + 'basic_settings.quota_switch_project': '自动切换项目', + 'basic_settings.quota_switch_preview': '切换到预览模型', + + // API 密钥管理 + 'api_keys.title': 'API 密钥管理', + 'api_keys.proxy_auth_title': '代理服务认证密钥', + 'api_keys.add_button': '添加密钥', + 'api_keys.empty_title': '暂无API密钥', + 'api_keys.empty_desc': '点击上方按钮添加第一个密钥', + 'api_keys.item_title': 'API密钥', + 'api_keys.add_modal_title': '添加API密钥', + 'api_keys.add_modal_key_label': 'API密钥:', + 'api_keys.add_modal_key_placeholder': '请输入API密钥', + 'api_keys.edit_modal_title': '编辑API密钥', + 'api_keys.edit_modal_key_label': 'API密钥:', + 'api_keys.delete_confirm': '确定要删除这个API密钥吗?', + + // AI 提供商 + 'ai_providers.title': 'AI 提供商配置', + 'ai_providers.gemini_title': 'Gemini API 密钥', + 'ai_providers.gemini_add_button': '添加密钥', + 'ai_providers.gemini_empty_title': '暂无Gemini密钥', + 'ai_providers.gemini_empty_desc': '点击上方按钮添加第一个密钥', + 'ai_providers.gemini_item_title': 'Gemini密钥', + 'ai_providers.gemini_add_modal_title': '添加Gemini API密钥', + 'ai_providers.gemini_add_modal_key_label': 'API密钥:', + 'ai_providers.gemini_add_modal_key_placeholder': '请输入Gemini API密钥', + 'ai_providers.gemini_edit_modal_title': '编辑Gemini API密钥', + 'ai_providers.gemini_edit_modal_key_label': 'API密钥:', + 'ai_providers.gemini_delete_confirm': '确定要删除这个Gemini密钥吗?', + + 'ai_providers.codex_title': 'Codex API 配置', + 'ai_providers.codex_add_button': '添加配置', + 'ai_providers.codex_empty_title': '暂无Codex配置', + 'ai_providers.codex_empty_desc': '点击上方按钮添加第一个配置', + 'ai_providers.codex_item_title': 'Codex配置', + 'ai_providers.codex_add_modal_title': '添加Codex API配置', + 'ai_providers.codex_add_modal_key_label': 'API密钥:', + 'ai_providers.codex_add_modal_key_placeholder': '请输入Codex API密钥', + 'ai_providers.codex_add_modal_url_label': 'Base URL (可选):', + 'ai_providers.codex_add_modal_url_placeholder': '例如: https://api.example.com', + 'ai_providers.codex_add_modal_proxy_label': '代理 URL (可选):', + 'ai_providers.codex_add_modal_proxy_placeholder': '例如: socks5://proxy.example.com:1080', + 'ai_providers.codex_edit_modal_title': '编辑Codex API配置', + 'ai_providers.codex_edit_modal_key_label': 'API密钥:', + 'ai_providers.codex_edit_modal_url_label': 'Base URL (可选):', + 'ai_providers.codex_edit_modal_proxy_label': '代理 URL (可选):', + 'ai_providers.codex_delete_confirm': '确定要删除这个Codex配置吗?', + + 'ai_providers.claude_title': 'Claude API 配置', + 'ai_providers.claude_add_button': '添加配置', + 'ai_providers.claude_empty_title': '暂无Claude配置', + 'ai_providers.claude_empty_desc': '点击上方按钮添加第一个配置', + 'ai_providers.claude_item_title': 'Claude配置', + 'ai_providers.claude_add_modal_title': '添加Claude API配置', + 'ai_providers.claude_add_modal_key_label': 'API密钥:', + 'ai_providers.claude_add_modal_key_placeholder': '请输入Claude API密钥', + 'ai_providers.claude_add_modal_url_label': 'Base URL (可选):', + 'ai_providers.claude_add_modal_url_placeholder': '例如: https://api.anthropic.com', + 'ai_providers.claude_add_modal_proxy_label': '代理 URL (可选):', + 'ai_providers.claude_add_modal_proxy_placeholder': '例如: socks5://proxy.example.com:1080', + 'ai_providers.claude_edit_modal_title': '编辑Claude API配置', + 'ai_providers.claude_edit_modal_key_label': 'API密钥:', + 'ai_providers.claude_edit_modal_url_label': 'Base URL (可选):', + 'ai_providers.claude_edit_modal_proxy_label': '代理 URL (可选):', + 'ai_providers.claude_delete_confirm': '确定要删除这个Claude配置吗?', + + 'ai_providers.openai_title': 'OpenAI 兼容提供商', + 'ai_providers.openai_add_button': '添加提供商', + 'ai_providers.openai_empty_title': '暂无OpenAI兼容提供商', + 'ai_providers.openai_empty_desc': '点击上方按钮添加第一个提供商', + 'ai_providers.openai_add_modal_title': '添加OpenAI兼容提供商', + 'ai_providers.openai_add_modal_name_label': '提供商名称:', + 'ai_providers.openai_add_modal_name_placeholder': '例如: openrouter', + 'ai_providers.openai_add_modal_url_label': 'Base URL:', + 'ai_providers.openai_add_modal_url_placeholder': '例如: https://openrouter.ai/api/v1', + 'ai_providers.openai_add_modal_keys_label': 'API密钥 (每行一个):', + 'ai_providers.openai_add_modal_keys_placeholder': 'sk-key1\nsk-key2', + 'ai_providers.openai_add_modal_keys_proxy_label': '代理 URL (按行对应,可选):', + 'ai_providers.openai_add_modal_keys_proxy_placeholder': 'socks5://proxy.example.com:1080\n', + 'ai_providers.openai_add_modal_models_label': '模型列表 (name[, alias] 每行一个):', + 'ai_providers.openai_models_hint': '示例:gpt-4o-mini 或 moonshotai/kimi-k2:free, kimi-k2', + 'ai_providers.openai_model_name_placeholder': '模型名称,如 moonshotai/kimi-k2:free', + 'ai_providers.openai_model_alias_placeholder': '模型别名 (可选)', + 'ai_providers.openai_models_add_btn': '添加模型', + 'ai_providers.openai_edit_modal_title': '编辑OpenAI兼容提供商', + 'ai_providers.openai_edit_modal_name_label': '提供商名称:', + 'ai_providers.openai_edit_modal_url_label': 'Base URL:', + 'ai_providers.openai_edit_modal_keys_label': 'API密钥 (每行一个):', + 'ai_providers.openai_edit_modal_keys_proxy_label': '代理 URL (按行对应,可选):', + 'ai_providers.openai_edit_modal_models_label': '模型列表 (name[, alias] 每行一个):', + 'ai_providers.openai_delete_confirm': '确定要删除这个OpenAI提供商吗?', + 'ai_providers.openai_keys_count': '密钥数量', + 'ai_providers.openai_models_count': '模型数量', + + + // 认证文件管理 + 'auth_files.title': '认证文件管理', + 'auth_files.title_section': '认证文件', + 'auth_files.description': '这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。', + 'auth_files.upload_button': '上传文件', + 'auth_files.delete_all_button': '删除全部', + 'auth_files.empty_title': '暂无认证文件', + 'auth_files.empty_desc': '点击上方按钮上传第一个文件', + 'auth_files.file_size': '大小', + 'auth_files.file_modified': '修改时间', + 'auth_files.download_button': '下载', + 'auth_files.delete_button': '删除', + 'auth_files.delete_confirm': '确定要删除文件', + 'auth_files.delete_all_confirm': '确定要删除所有认证文件吗?此操作不可恢复!', + 'auth_files.upload_error_json': '只能上传JSON文件', + 'auth_files.upload_success': '文件上传成功', + 'auth_files.download_success': '文件下载成功', + 'auth_files.delete_success': '文件删除成功', + 'auth_files.delete_all_success': '成功删除', + 'auth_files.files_count': '个文件', + + // Gemini Web Token + 'auth_login.gemini_web_title': 'Gemini Web Token', + 'auth_login.gemini_web_button': '保存 Gemini Web Token', + 'auth_login.gemini_web_hint': '从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。', + 'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:', + 'auth_login.secure_1psid_placeholder': '输入 __Secure-1PSID cookie 值', + 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:', + 'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值', + 'auth_login.gemini_web_label_label': '标签 (可选):', + 'auth_login.gemini_web_label_placeholder': '输入标签名称 (可选)', + 'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功', + + // 使用统计 + 'usage_stats.title': '使用统计', + 'usage_stats.total_requests': '总请求数', + 'usage_stats.success_requests': '成功请求', + 'usage_stats.failed_requests': '失败请求', + 'usage_stats.total_tokens': '总Token数', + 'usage_stats.requests_trend': '请求趋势', + 'usage_stats.tokens_trend': 'Token 使用趋势', + 'usage_stats.api_details': 'API 详细统计', + 'usage_stats.by_hour': '按小时', + 'usage_stats.by_day': '按天', + 'usage_stats.refresh': '刷新', + 'usage_stats.no_data': '暂无数据', + 'usage_stats.loading_error': '加载失败', + 'usage_stats.api_endpoint': 'API端点', + 'usage_stats.requests_count': '请求次数', + 'usage_stats.tokens_count': 'Token数量', + 'usage_stats.models': '模型统计', + 'usage_stats.success_rate': '成功率', + + // 系统信息 + 'system_info.title': '系统信息', + 'system_info.connection_status_title': '连接状态', + 'system_info.api_status_label': 'API 状态:', + 'system_info.config_status_label': '配置状态:', + 'system_info.last_update_label': '最后更新:', + 'system_info.cache_data': '缓存数据', + 'system_info.real_time_data': '实时数据', + 'system_info.not_loaded': '未加载', + 'system_info.seconds_ago': '秒前', + + // 通知消息 + 'notification.debug_updated': '调试设置已更新', + 'notification.proxy_updated': '代理设置已更新', + 'notification.proxy_cleared': '代理设置已清空', + 'notification.retry_updated': '重试设置已更新', + 'notification.quota_switch_project_updated': '项目切换设置已更新', + 'notification.quota_switch_preview_updated': '预览模型切换设置已更新', + 'notification.api_key_added': 'API密钥添加成功', + 'notification.api_key_updated': 'API密钥更新成功', + 'notification.api_key_deleted': 'API密钥删除成功', + 'notification.gemini_key_added': 'Gemini密钥添加成功', + 'notification.gemini_key_updated': 'Gemini密钥更新成功', + 'notification.gemini_key_deleted': 'Gemini密钥删除成功', + 'notification.codex_config_added': 'Codex配置添加成功', + 'notification.codex_config_updated': 'Codex配置更新成功', + 'notification.codex_config_deleted': 'Codex配置删除成功', + 'notification.claude_config_added': 'Claude配置添加成功', + 'notification.claude_config_updated': 'Claude配置更新成功', + 'notification.claude_config_deleted': 'Claude配置删除成功', + 'notification.field_required': '必填字段不能为空', + 'notification.openai_provider_required': '请填写提供商名称和Base URL', + 'notification.openai_provider_added': 'OpenAI提供商添加成功', + 'notification.openai_provider_updated': 'OpenAI提供商更新成功', + 'notification.openai_provider_deleted': 'OpenAI提供商删除成功', + 'notification.openai_model_name_required': '请填写模型名称', + 'notification.data_refreshed': '数据刷新成功', + 'notification.connection_required': '请先建立连接', + 'notification.refresh_failed': '刷新失败', + 'notification.update_failed': '更新失败', + 'notification.add_failed': '添加失败', + 'notification.delete_failed': '删除失败', + 'notification.upload_failed': '上传失败', + 'notification.download_failed': '下载失败', + 'notification.login_failed': '登录失败', + 'notification.please_enter': '请输入', + 'notification.please_fill': '请填写', + 'notification.provider_name_url': '提供商名称和Base URL', + 'notification.api_key': 'API密钥', + 'notification.gemini_api_key': 'Gemini API密钥', + 'notification.codex_api_key': 'Codex API密钥', + 'notification.claude_api_key': 'Claude API密钥', + + // 语言切换 + 'language.switch': '语言', + 'language.chinese': '中文', + 'language.english': 'English', + + // 主题切换 + 'theme.switch': '主题', + 'theme.light': '亮色', + 'theme.dark': '暗色', + 'theme.switch_to_light': '切换到亮色模式', + 'theme.switch_to_dark': '切换到暗色模式', + 'theme.auto': '跟随系统', + + // 页脚 + 'footer.version': '版本', + 'footer.author': '作者' + }, + + 'en-US': { + // Common + 'common.login': 'Login', + 'common.logout': 'Logout', + 'common.cancel': 'Cancel', + 'common.confirm': 'Confirm', + 'common.save': 'Save', + 'common.delete': 'Delete', + 'common.edit': 'Edit', + 'common.add': 'Add', + 'common.update': 'Update', + 'common.refresh': 'Refresh', + 'common.close': 'Close', + 'common.success': 'Success', + 'common.error': 'Error', + 'common.info': 'Info', + 'common.warning': 'Warning', + 'common.loading': 'Loading...', + 'common.connecting': 'Connecting...', + 'common.connected': 'Connected', + 'common.disconnected': 'Disconnected', + 'common.connecting_status': 'Connecting', + 'common.connected_status': 'Connected', + 'common.disconnected_status': 'Disconnected', + 'common.yes': 'Yes', + 'common.no': 'No', + 'common.optional': 'Optional', + 'common.required': 'Required', + 'common.api_key': 'Key', + 'common.base_url': 'Address', + + // Page titles + 'title.main': 'CLI Proxy API Management Center', + 'title.login': 'CLI Proxy API Management Center', + + // Auto login + 'auto_login.title': 'Auto Login in Progress...', + 'auto_login.message': 'Attempting to connect to server using locally saved connection information', + + // Login page + 'login.subtitle': 'Please enter connection information to access the management interface', + 'login.connection_title': 'Connection Address', + 'login.connection_current': 'Current URL', + 'login.connection_auto_hint': 'The system will automatically use the current URL for connection', + 'login.custom_connection_label': 'Custom Connection URL:', + 'login.custom_connection_placeholder': 'Eg: https://example.com:8317', + 'login.custom_connection_hint': 'By default the current URL is used. Override it here if needed.', + 'login.use_current_address': 'Use Current URL', + 'login.management_key_label': 'Management Key:', + 'login.management_key_placeholder': 'Enter the management key', + 'login.connect_button': 'Connect', + 'login.submit_button': 'Login', + 'login.submitting': 'Connecting...', + 'login.error_title': 'Login Failed', + 'login.error_required': 'Please fill in complete connection information', + 'login.error_invalid': 'Connection failed, please check address and key', + + // Header navigation + 'header.check_connection': 'Check Connection', + 'header.refresh_all': 'Refresh All', + 'header.logout': 'Logout', + + // Connection info + 'connection.title': 'Connection Information', + 'connection.server_address': 'Server Address:', + 'connection.management_key': 'Management Key:', + 'connection.status': 'Connection Status:', + + // Sidebar navigation + 'nav.basic_settings': 'Basic Settings', + 'nav.api_keys': 'API Keys', + 'nav.ai_providers': 'AI Providers', + 'nav.auth_files': 'Auth Files', + 'nav.usage_stats': 'Usage Statistics', + 'nav.system_info': 'System Info', + + // Basic settings + 'basic_settings.title': 'Basic Settings', + 'basic_settings.debug_title': 'Debug Mode', + 'basic_settings.debug_enable': 'Enable Debug Mode', + 'basic_settings.proxy_title': 'Proxy Settings', + 'basic_settings.proxy_url_label': 'Proxy URL:', + 'basic_settings.proxy_url_placeholder': 'e.g.: socks5://user:pass@127.0.0.1:1080/', + 'basic_settings.proxy_update': 'Update', + 'basic_settings.proxy_clear': 'Clear', + 'basic_settings.retry_title': 'Request Retry', + 'basic_settings.retry_count_label': 'Retry Count:', + 'basic_settings.retry_update': 'Update', + 'basic_settings.quota_title': 'Quota Exceeded Behavior', + 'basic_settings.quota_switch_project': 'Auto Switch Project', + 'basic_settings.quota_switch_preview': 'Switch to Preview Model', + + // API Keys management + 'api_keys.title': 'API Keys Management', + 'api_keys.proxy_auth_title': 'Proxy Service Authentication Keys', + 'api_keys.add_button': 'Add Key', + 'api_keys.empty_title': 'No API Keys', + 'api_keys.empty_desc': 'Click the button above to add the first key', + 'api_keys.item_title': 'API Key', + 'api_keys.add_modal_title': 'Add API Key', + 'api_keys.add_modal_key_label': 'API Key:', + 'api_keys.add_modal_key_placeholder': 'Please enter API key', + 'api_keys.edit_modal_title': 'Edit API Key', + 'api_keys.edit_modal_key_label': 'API Key:', + 'api_keys.delete_confirm': 'Are you sure you want to delete this API key?', + + // AI Providers + 'ai_providers.title': 'AI Providers Configuration', + 'ai_providers.gemini_title': 'Gemini API Keys', + 'ai_providers.gemini_add_button': 'Add Key', + 'ai_providers.gemini_empty_title': 'No Gemini Keys', + 'ai_providers.gemini_empty_desc': 'Click the button above to add the first key', + 'ai_providers.gemini_item_title': 'Gemini Key', + 'ai_providers.gemini_add_modal_title': 'Add Gemini API Key', + 'ai_providers.gemini_add_modal_key_label': 'API Key:', + 'ai_providers.gemini_add_modal_key_placeholder': 'Please enter Gemini API key', + 'ai_providers.gemini_edit_modal_title': 'Edit Gemini API Key', + 'ai_providers.gemini_edit_modal_key_label': 'API Key:', + 'ai_providers.gemini_delete_confirm': 'Are you sure you want to delete this Gemini key?', + + 'ai_providers.codex_title': 'Codex API Configuration', + 'ai_providers.codex_add_button': 'Add Configuration', + 'ai_providers.codex_empty_title': 'No Codex Configuration', + 'ai_providers.codex_empty_desc': 'Click the button above to add the first configuration', + 'ai_providers.codex_item_title': 'Codex Configuration', + 'ai_providers.codex_add_modal_title': 'Add Codex API Configuration', + 'ai_providers.codex_add_modal_key_label': 'API Key:', + 'ai_providers.codex_add_modal_key_placeholder': 'Please enter Codex API key', + 'ai_providers.codex_add_modal_url_label': 'Base URL (Optional):', + 'ai_providers.codex_add_modal_url_placeholder': 'e.g.: https://api.example.com', + 'ai_providers.codex_edit_modal_title': 'Edit Codex API Configuration', + 'ai_providers.codex_edit_modal_key_label': 'API Key:', + 'ai_providers.codex_edit_modal_url_label': 'Base URL (Optional):', + 'ai_providers.codex_delete_confirm': 'Are you sure you want to delete this Codex configuration?', + + 'ai_providers.claude_title': 'Claude API Configuration', + 'ai_providers.claude_add_button': 'Add Configuration', + 'ai_providers.claude_empty_title': 'No Claude Configuration', + 'ai_providers.claude_empty_desc': 'Click the button above to add the first configuration', + 'ai_providers.claude_item_title': 'Claude Configuration', + 'ai_providers.claude_add_modal_title': 'Add Claude API Configuration', + 'ai_providers.claude_add_modal_key_label': 'API Key:', + 'ai_providers.claude_add_modal_key_placeholder': 'Please enter Claude API key', + 'ai_providers.claude_add_modal_url_label': 'Base URL (Optional):', + 'ai_providers.claude_add_modal_url_placeholder': 'e.g.: https://api.anthropic.com', + 'ai_providers.claude_edit_modal_title': 'Edit Claude API Configuration', + 'ai_providers.claude_edit_modal_key_label': 'API Key:', + 'ai_providers.claude_edit_modal_url_label': 'Base URL (Optional):', + 'ai_providers.claude_delete_confirm': 'Are you sure you want to delete this Claude configuration?', + + 'ai_providers.openai_title': 'OpenAI Compatible Providers', + 'ai_providers.openai_add_button': 'Add Provider', + 'ai_providers.openai_empty_title': 'No OpenAI Compatible Providers', + 'ai_providers.openai_empty_desc': 'Click the button above to add the first provider', + 'ai_providers.openai_add_modal_title': 'Add OpenAI Compatible Provider', + 'ai_providers.openai_add_modal_name_label': 'Provider Name:', + 'ai_providers.openai_add_modal_name_placeholder': 'e.g.: openrouter', + 'ai_providers.openai_add_modal_url_label': 'Base URL:', + 'ai_providers.openai_add_modal_url_placeholder': 'e.g.: https://openrouter.ai/api/v1', + 'ai_providers.openai_add_modal_keys_label': 'API Keys (one per line):', + 'ai_providers.openai_add_modal_keys_placeholder': 'sk-key1\nsk-key2', + 'ai_providers.openai_edit_modal_title': 'Edit OpenAI Compatible Provider', + 'ai_providers.openai_edit_modal_name_label': 'Provider Name:', + 'ai_providers.openai_edit_modal_url_label': 'Base URL:', + 'ai_providers.openai_edit_modal_keys_label': 'API Keys (one per line):', + 'ai_providers.openai_delete_confirm': 'Are you sure you want to delete this OpenAI provider?', + 'ai_providers.openai_keys_count': 'Keys Count', + 'ai_providers.openai_models_count': 'Models Count', + + + // Auth files management + 'auth_files.title': 'Auth Files Management', + 'auth_files.title_section': 'Auth Files', + 'auth_files.description': 'Here you can manage authentication configuration files for Qwen and Gemini. Upload JSON format authentication files to enable the corresponding AI services.', + 'auth_files.upload_button': 'Upload File', + 'auth_files.delete_all_button': 'Delete All', + 'auth_files.empty_title': 'No Auth Files', + 'auth_files.empty_desc': 'Click the button above to upload the first file', + 'auth_files.file_size': 'Size', + 'auth_files.file_modified': 'Modified', + 'auth_files.download_button': 'Download', + 'auth_files.delete_button': 'Delete', + 'auth_files.delete_confirm': 'Are you sure you want to delete file', + 'auth_files.delete_all_confirm': 'Are you sure you want to delete all auth files? This operation cannot be undone!', + 'auth_files.upload_error_json': 'Only JSON files are allowed', + 'auth_files.upload_success': 'File uploaded successfully', + 'auth_files.download_success': 'File downloaded successfully', + 'auth_files.delete_success': 'File deleted successfully', + 'auth_files.delete_all_success': 'Successfully deleted', + 'auth_files.files_count': 'files', + + // Gemini Web Token + 'auth_login.gemini_web_title': 'Gemini Web Token', + 'auth_login.gemini_web_button': 'Save Gemini Web Token', + 'auth_login.gemini_web_hint': 'Obtain the Cookie value of the Gemini web version from the browser\'s developer tools, used for direct authentication to access Gemini.', + 'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:', + 'auth_login.secure_1psid_placeholder': 'Enter __Secure-1PSID cookie value', + 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:', + 'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value', + 'auth_login.gemini_web_label_label': 'Label (Optional):', + 'auth_login.gemini_web_label_placeholder': 'Enter label name (optional)', + 'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully', + + // Usage Statistics + 'usage_stats.title': 'Usage Statistics', + 'usage_stats.total_requests': 'Total Requests', + 'usage_stats.success_requests': 'Success Requests', + 'usage_stats.failed_requests': 'Failed Requests', + 'usage_stats.total_tokens': 'Total Tokens', + 'usage_stats.requests_trend': 'Request Trends', + 'usage_stats.tokens_trend': 'Token Usage Trends', + 'usage_stats.api_details': 'API Details', + 'usage_stats.by_hour': 'By Hour', + 'usage_stats.by_day': 'By Day', + 'usage_stats.refresh': 'Refresh', + 'usage_stats.no_data': 'No Data Available', + 'usage_stats.loading_error': 'Loading Failed', + 'usage_stats.api_endpoint': 'API Endpoint', + 'usage_stats.requests_count': 'Request Count', + 'usage_stats.tokens_count': 'Token Count', + 'usage_stats.models': 'Model Statistics', + 'usage_stats.success_rate': 'Success Rate', + + // System info + 'system_info.title': 'System Information', + 'system_info.connection_status_title': 'Connection Status', + 'system_info.api_status_label': 'API Status:', + 'system_info.config_status_label': 'Config Status:', + 'system_info.last_update_label': 'Last Update:', + 'system_info.cache_data': 'Cache Data', + 'system_info.real_time_data': 'Real-time Data', + 'system_info.not_loaded': 'Not Loaded', + 'system_info.seconds_ago': 'seconds ago', + + // Notification messages + 'notification.debug_updated': 'Debug settings updated', + 'notification.proxy_updated': 'Proxy settings updated', + 'notification.proxy_cleared': 'Proxy settings cleared', + 'notification.retry_updated': 'Retry settings updated', + 'notification.quota_switch_project_updated': 'Project switch settings updated', + 'notification.quota_switch_preview_updated': 'Preview model switch settings updated', + 'notification.api_key_added': 'API key added successfully', + 'notification.api_key_updated': 'API key updated successfully', + 'notification.api_key_deleted': 'API key deleted successfully', + 'notification.gemini_key_added': 'Gemini key added successfully', + 'notification.gemini_key_updated': 'Gemini key updated successfully', + 'notification.gemini_key_deleted': 'Gemini key deleted successfully', + 'notification.codex_config_added': 'Codex configuration added successfully', + 'notification.codex_config_updated': 'Codex configuration updated successfully', + 'notification.codex_config_deleted': 'Codex configuration deleted successfully', + 'notification.claude_config_added': 'Claude configuration added successfully', + 'notification.claude_config_updated': 'Claude configuration updated successfully', + 'notification.claude_config_deleted': 'Claude configuration deleted successfully', + 'notification.openai_provider_added': 'OpenAI provider added successfully', + 'notification.openai_provider_updated': 'OpenAI provider updated successfully', + 'notification.openai_provider_deleted': 'OpenAI provider deleted successfully', + 'notification.openai_model_name_required': 'Model name is required', + 'notification.data_refreshed': 'Data refreshed successfully', + 'notification.connection_required': 'Please establish connection first', + 'notification.refresh_failed': 'Refresh failed', + 'notification.update_failed': 'Update failed', + 'notification.add_failed': 'Add failed', + 'notification.delete_failed': 'Delete failed', + 'notification.upload_failed': 'Upload failed', + 'notification.download_failed': 'Download failed', + 'notification.login_failed': 'Login failed', + 'notification.please_enter': 'Please enter', + 'notification.please_fill': 'Please fill', + 'notification.provider_name_url': 'provider name and Base URL', + 'notification.api_key': 'API key', + 'notification.gemini_api_key': 'Gemini API key', + 'notification.codex_api_key': 'Codex API key', + 'notification.claude_api_key': 'Claude API key', + + // Language switch + 'language.switch': 'Language', + 'language.chinese': '中文', + 'language.english': 'English', + + // Theme switch + 'theme.switch': 'Theme', + 'theme.light': 'Light', + 'theme.dark': 'Dark', + 'theme.switch_to_light': 'Switch to light mode', + 'theme.switch_to_dark': 'Switch to dark mode', + 'theme.auto': 'Follow system', + + // Footer + 'footer.version': 'Version', + 'footer.author': 'Author' + } + }, + + // 获取翻译文本 + t(key, params = {}) { + const translation = this.translations[this.currentLanguage]?.[key] || + this.translations[this.fallbackLanguage]?.[key] || + key; + + // 简单的参数替换 + return translation.replace(/\{(\w+)\}/g, (match, param) => { + return params[param] || match; + }); + }, + + // 设置语言 + setLanguage(lang) { + if (this.translations[lang]) { + this.currentLanguage = lang; + localStorage.setItem('preferredLanguage', lang); + this.updatePageLanguage(); + this.updateAllTexts(); + } + }, + + // 更新页面语言属性 + updatePageLanguage() { + document.documentElement.lang = this.currentLanguage; + }, + + // 更新所有文本 + updateAllTexts() { + // 更新所有带有 data-i18n 属性的元素 + document.querySelectorAll('[data-i18n]').forEach(element => { + const key = element.getAttribute('data-i18n'); + const text = this.t(key); + + if (element.tagName === 'INPUT' && (element.type === 'text' || element.type === 'password')) { + element.placeholder = text; + } else if (element.tagName === 'TITLE') { + element.textContent = text; + } else { + element.textContent = text; + } + }); + + // 更新所有带有 data-i18n-html 属性的元素(支持HTML) + document.querySelectorAll('[data-i18n-html]').forEach(element => { + const key = element.getAttribute('data-i18n-html'); + const html = this.t(key); + element.innerHTML = html; + }); + }, + + // 初始化 + init() { + // 从本地存储获取用户偏好语言 + const savedLanguage = localStorage.getItem('preferredLanguage'); + if (savedLanguage && this.translations[savedLanguage]) { + this.currentLanguage = savedLanguage; + } else { + // 根据浏览器语言自动选择 + const browserLang = navigator.language || navigator.userLanguage; + if (browserLang.startsWith('zh')) { + this.currentLanguage = 'zh-CN'; + } else { + this.currentLanguage = 'en-US'; + } + } + + this.updatePageLanguage(); + this.updateAllTexts(); + } +}; + +// 全局函数,供HTML调用 +window.t = (key, params) => i18n.t(key, params); +window.setLanguage = (lang) => i18n.setLanguage(lang); diff --git a/index.html b/index.html index 06ee2fd..817bb93 100644 --- a/index.html +++ b/index.html @@ -1,579 +1,579 @@ - - - - - - CLI Proxy API Management Center - - - - - - - - - - -
-
- - - - - -
- -
- - -
-
- - - - - - - - -
- - - - - - + + + + + + CLI Proxy API Management Center + + + + + + + + + + +
+
+ + + + + +
+ +
+ + +
+
+ + + + + + + + +
+ + + + + + diff --git a/styles.css b/styles.css index bb96c59..9de6236 100644 --- a/styles.css +++ b/styles.css @@ -1,1746 +1,1799 @@ -/* 全局样式 */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -/* CSS变量主题系统 */ -:root { - /* 亮色主题(默认) */ - --bg-primary: #f3f4f6; - --bg-secondary: rgba(255, 255, 255, 0.95); - --bg-tertiary: #f8fafc; - --bg-quaternary: #f7fafc; - --bg-modal: rgba(0, 0, 0, 0.5); - - --text-primary: #333; - --text-secondary: #4a5568; - --text-tertiary: #64748b; - --text-quaternary: #6b7280; - --text-inverse: white; - - --border-primary: #e2e8f0; - --border-secondary: #cbd5e0; - --border-focus: #475569; - - --accent-primary: linear-gradient(135deg, #475569, #334155); - --accent-secondary: #e2e8f0; - --accent-tertiary: #f8fafc; - --primary-color: #3b82f6; - --card-bg: #ffffff; - --border-color: #e2e8f0; - - --success-bg: linear-gradient(135deg, #dcfce7, #bbf7d0); - --success-text: #166534; - --success-border: #86efac; - - --error-bg: linear-gradient(135deg, #fef2f2, #fecaca); - --error-text: #dc2626; - --error-border: #fca5a5; - - --warning-bg: linear-gradient(135deg, #fef3c7, #fed7aa); - --warning-text: #d97706; - --warning-border: #fdba74; - - --shadow-primary: 0 8px 32px rgba(0, 0, 0, 0.1); - --shadow-secondary: 0 20px 60px rgba(0, 0, 0, 0.15); - --shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.3); - - --glass-bg: rgba(255, 255, 255, 0.95); - --glass-border: rgba(255, 255, 255, 0.2); -} - -/* 暗色主题 */ -[data-theme="dark"] { - --bg-primary: #0f172a; - --bg-secondary: rgba(30, 41, 59, 0.95); - --bg-tertiary: #1e293b; - --bg-quaternary: #334155; - --bg-modal: rgba(0, 0, 0, 0.7); - - --text-primary: #f1f5f9; - --text-secondary: #cbd5e1; - --text-tertiary: #94a3b8; - --text-quaternary: #64748b; - --text-inverse: #ffffff; - - --border-primary: #334155; - --border-secondary: #475569; - --border-focus: #64748b; - - --accent-primary: linear-gradient(135deg, #64748b, #475569); - --accent-secondary: #334155; - --accent-tertiary: #1e293b; - --primary-color: #38bdf8; - --card-bg: #1e293b; - --border-color: #334155; - - --success-bg: linear-gradient(135deg, #064e3b, #047857); - --success-text: #bbf7d0; - --success-border: #059669; - - --error-bg: linear-gradient(135deg, #7f1d1d, #b91c1c); - --error-text: #fecaca; - --error-border: #dc2626; - - --warning-bg: linear-gradient(135deg, #78350f, #d97706); - --warning-text: #fed7aa; - --warning-border: #f59e0b; - - --shadow-primary: 0 8px 32px rgba(0, 0, 0, 0.3); - --shadow-secondary: 0 20px 60px rgba(0, 0, 0, 0.4); - --shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.8); - - --glass-bg: rgba(30, 41, 59, 0.95); - --glass-border: rgba(100, 116, 139, 0.2); -} - -/* 登录页面样式 */ -.login-container { - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - background: var(--bg-primary); - padding: 20px; -} - -/* 自动登录加载页面样式 */ -.auto-login-content { - text-align: center; - padding: 20px; -} - -.loading-spinner { - margin: 0 auto 20px; - width: 60px; - height: 60px; -} - -.spinner { - width: 100%; - height: 100%; - border: 4px solid #e5e7eb; - border-top: 4px solid #3b82f6; - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -.auto-login-content h2 { - color: var(--text-secondary); - margin-bottom: 10px; - font-size: 24px; - font-weight: 600; -} - -.auto-login-content p { - color: var(--text-tertiary); - font-size: 16px; - line-height: 1.5; -} - -/* 登录页面头部布局 */ -.login-header-top { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 30px; - position: relative; -} - -.login-title { - width: 100%; - text-align: center; - margin-bottom: 30px; -} - -/* 头部控制按钮组 */ -.header-controls { - display: flex; - gap: 8px; - align-items: center; - height: 100%; -} - -/* 登录页面的控制按钮组样式 */ -.login-header-top .header-controls { - background: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - border-radius: 12px; - padding: 8px; - border: 1px solid rgba(255, 255, 255, 0.2); - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); - transition: all 0.3s ease; - margin-top: 8px; /* 与标题拉开距离,避免遮挡 */ - margin-bottom: 20px; -} - -.login-header-top .header-controls:hover { - background: rgba(255, 255, 255, 0.15); - border-color: rgba(255, 255, 255, 0.3); - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); - transform: translateY(-1px); -} - -[data-theme="dark"] .login-header-top .header-controls { - background: rgba(30, 41, 59, 0.8); - border: 1px solid rgba(100, 116, 139, 0.3); - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); -} - -[data-theme="dark"] .login-header-top .header-controls:hover { - background: rgba(30, 41, 59, 0.9); - border-color: rgba(100, 116, 139, 0.5); - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); -} - -.language-switcher, -.theme-switcher { - position: relative; - flex-shrink: 0; -} - -.language-btn, -.theme-btn { - padding: 10px 20px; - font-size: 14px; - font-weight: 500; - border-radius: 8px; - background: var(--bg-tertiary); - border: 1px solid var(--border-primary); - color: var(--text-tertiary); - transition: all 0.3s ease; - white-space: nowrap; - backdrop-filter: blur(5px); - height: 40px; - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; -} - -/* 登录页面的按钮样式优化 */ -.login-header-top .language-btn, -.login-header-top .theme-btn { - padding: 8px 12px; - font-size: 13px; - height: 36px; - min-width: 36px; - border-radius: 8px; - background: rgba(255, 255, 255, 0.9); - border: 1px solid rgba(255, 255, 255, 0.3); - color: #64748b; - backdrop-filter: blur(10px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} - -.login-header-top .language-btn:hover, -.login-header-top .theme-btn:hover { - background: rgba(255, 255, 255, 1); - border-color: rgba(100, 116, 139, 0.3); - color: #475569; - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} - -[data-theme="dark"] .login-header-top .language-btn, -[data-theme="dark"] .login-header-top .theme-btn { - background: rgba(30, 41, 59, 0.9); - border: 1px solid rgba(100, 116, 139, 0.3); - color: #94a3b8; -} - -[data-theme="dark"] .login-header-top .language-btn:hover, -[data-theme="dark"] .login-header-top .theme-btn:hover { - background: rgba(51, 65, 85, 0.95); - border-color: rgba(100, 116, 139, 0.5); - color: #cbd5e1; -} - -.language-btn:hover, -.theme-btn:hover { - background: var(--accent-secondary); - border-color: var(--border-secondary); - color: var(--text-secondary); -} - -.language-btn i, -.theme-btn i { - margin-right: 6px; -} - -.theme-btn.active { - background: var(--accent-primary); - color: var(--text-inverse); -} - -.login-card { - background: var(--glass-bg); - backdrop-filter: blur(10px); - border-radius: 20px; - padding: 40px; - width: 100%; - max-width: 500px; - box-shadow: var(--shadow-secondary); - border: 1px solid var(--glass-border); -} - -.login-header { - text-align: center; - margin-bottom: 25px; -} - -.login-title { - display: flex; - align-items: center; - justify-content: center; - gap: 15px; - color: var(--text-secondary); - font-size: 1.8rem; - font-weight: 600; - margin-bottom: 0; - line-height: 1.2; -} - -#login-logo { - height: 60px; - width: auto; - object-fit: contain; - border-radius: 6px; - padding: 2px; - background: #fff; - border: 1px solid rgba(15, 23, 42, 0.06); - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06); -} - -.login-subtitle { - color: #64748b; - font-size: 1rem; - margin: 0; - text-align: center; -} - -.login-body { - display: flex; - flex-direction: column; - gap: 24px; -} - -.login-connection-info { - background: var(--bg-tertiary); - border: 1px solid var(--border-primary); - border-radius: 16px; - padding: 20px; - display: flex; - flex-direction: column; - gap: 12px; -} - -[data-theme="dark"] .login-connection-info { - background: rgba(30, 41, 59, 0.7); - border-color: rgba(100, 116, 139, 0.3); -} - -.connection-summary { - display: flex; - align-items: center; - gap: 16px; - color: var(--text-secondary); -} - -.connection-summary i { - font-size: 24px; - color: var(--primary-color); -} - -.connection-url { - font-size: 16px; - color: var(--text-secondary); -} - -.connection-url-separator { - margin: 0 8px; - color: var(--text-tertiary); -} - -#login-connection-url { - font-family: "Fira Code", "Consolas", "Courier New", monospace; - color: var(--text-primary); - word-break: break-all; -} - -[data-theme="dark"] #login-connection-url { - color: var(--text-secondary); -} - -.login-form { - display: flex; - flex-direction: column; - gap: 20px; -} - -.login-form .form-group { - margin-bottom: 25px; -} - -.login-form .form-group label { - display: block; - margin-bottom: 8px; - color: var(--text-secondary); - font-weight: 600; - font-size: 0.95rem; -} - -.login-form .form-hint { - margin-top: 6px; - color: #64748b; - font-size: 12px; -} - -.login-form .input-group { - display: flex; - gap: 10px; - align-items: center; -} - -.login-form input[type="text"], -.login-form input[type="password"] { - flex: 1; - padding: 14px 16px; - border: 2px solid var(--border-primary); - border-radius: 10px; - font-size: 15px; - transition: all 0.3s ease; - background: var(--bg-secondary); - color: var(--text-primary); -} - -.login-form input:focus { - outline: none; - border-color: var(--border-focus); - box-shadow: 0 0 0 3px var(--border-primary); -} - -.login-form .btn-secondary { - padding: 14px 16px; - border: 2px solid #e2e8f0; - background: #f8fafc; - color: #64748b; - border-radius: 10px; - transition: all 0.3s ease; -} - -.login-form .btn-secondary:hover { - background: #e2e8f0; - border-color: #cbd5e0; -} - -.form-actions { - margin-top: 30px; - text-align: center; -} - -.login-btn { - width: 100%; - padding: 16px 24px; - font-size: 16px; - font-weight: 600; - border-radius: 12px; - background: linear-gradient(135deg, #475569, #334155); - color: white; - border: none; - cursor: pointer; - transition: all 0.3s ease; - display: flex; - align-items: center; - justify-content: center; - gap: 10px; -} - -.login-btn:hover { - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(51, 65, 85, 0.4); -} - -.login-btn:disabled { - opacity: 0.7; - cursor: not-allowed; - transform: none; -} - -.login-error { - background: var(--error-bg); - border: 1px solid var(--error-border); - color: var(--error-text); - padding: 12px 16px; - border-radius: 8px; - margin-top: 20px; - display: flex; - align-items: center; - gap: 8px; - font-size: 14px; - font-weight: 500; -} - -.login-error i { - color: var(--error-text); -} - -/* 响应式设计 - 登录页面 */ -@media (max-width: 640px) { - .login-card { - padding: 30px 20px; - margin: 10px; - max-width: 100%; - } - - .login-header-top { - flex-direction: column; - gap: 20px; - align-items: center; - margin-bottom: 30px; - } - - .header-controls { - flex-direction: row; - justify-content: center; - gap: 8px; - margin-bottom: 10px; /* 在小屏幕上给控制按钮组添加下边距 */ - } - - /* 登录页面小屏幕优化 */ - .login-header-top .header-controls { - margin: 8px auto 0 auto; /* 顶部留白,避免与标题拥挤 */ - background: rgba(255, 255, 255, 0.08); - padding: 6px; - border-radius: 10px; - } - - .login-header-top .language-btn, - .login-header-top .theme-btn { - padding: 6px 10px; - font-size: 12px; - height: 32px; - min-width: 32px; - } - - .language-btn, - .theme-btn { - padding: 8px 16px; - font-size: 13px; - height: 36px; /* 在小屏幕上稍微减小高度 */ - } - - .login-title { - font-size: 1.5rem; - flex-direction: column; - gap: 10px; - text-align: center; - justify-content: center; - margin-bottom: 25px; - } - - #login-logo { - height: 50px; - } - - .login-form .input-group { - flex-direction: column; - align-items: stretch; - } - - .login-form .btn-secondary { - width: 100%; - margin-top: 10px; - } -} - -body { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background: var(--bg-primary); - min-height: 100vh; - color: var(--text-primary); - transition: background-color 0.3s ease, color 0.3s ease; -} - -.container { - max-width: 1400px; - margin: 0 auto; - padding: 20px; -} - -/* 头部样式 */ -.header { - background: var(--glass-bg); - backdrop-filter: blur(10px); - border-radius: 15px; - padding: 48px 28px; /* 再次拉长,适配更大 Logo */ - margin-bottom: 20px; - box-shadow: var(--shadow-primary); -} - -.header-content { - display: flex; - justify-content: space-between; - align-items: center; -} - -.header h1 { - color: var(--text-secondary); - font-size: 1.6rem; /* 字体小一号 */ - font-weight: 600; -} - -.header h1.brand { - display: flex; - align-items: flex-end; /* 文字在 Logo 右下角(底对齐) */ - gap: 18px; - line-height: 1; /* 减少标题内上下空白 */ -} - -.header h1 i { - color: #64748b; /* slate-500 */ - margin-right: 10px; -} - -/* 站点 Logo(嵌入头部) */ -#site-logo { - height: 88px; - width: auto; - display: inline-block; - object-fit: contain; - border-radius: 6px; - padding: 2px; - background: #fff; - border: 1px solid rgba(15, 23, 42, 0.06); - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06); -} - -.brand-title { - display: inline-block; - line-height: 1.1; /* 保持原字号不变,同时更紧凑 */ - margin-bottom: 4px; /* 让文字更贴近更高的 Logo 底部 */ -} - -/* 小屏幕适配:避免 Logo 过大占位 */ -@media (max-width: 640px) { - #site-logo { height: 64px; } -} - -.header-actions { - display: flex; - gap: 12px; - align-items: center; - height: 40px; /* 统一高度 */ -} - - -/* 认证区域 */ -.auth-section { - margin-bottom: 20px; -} - -/* 连接信息样式 */ -.connection-info { - background: var(--bg-tertiary); - border-radius: 12px; - padding: 20px; - margin-bottom: 20px; -} - -.info-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 0; - border-bottom: 1px solid var(--border-primary); -} - -.info-item:last-child { - border-bottom: none; - padding-bottom: 0; -} - -.info-label { - display: flex; - align-items: center; - gap: 8px; - color: var(--text-secondary); - font-weight: 600; - font-size: 14px; -} - -.info-label i { - color: var(--text-tertiary); - width: 16px; -} - -.info-value { - color: var(--text-primary); - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 13px; - background: var(--bg-secondary); - padding: 6px 12px; - border-radius: 6px; - border: 1px solid var(--border-primary); - max-width: 300px; - word-break: break-all; -} - -.status-indicator { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 12px; - border-radius: 20px; - font-size: 12px; - font-weight: 600; -} - -.status-indicator.connected { - background: var(--success-bg); - color: var(--success-text); - border: 1px solid var(--success-border); -} - -.status-indicator.disconnected { - background: var(--error-bg); - color: var(--error-text); - border: 1px solid var(--error-border); -} - -.status-indicator.connecting { - background: var(--warning-bg); - color: var(--warning-text); - border: 1px solid var(--warning-border); -} - -.connection-actions { - display: flex; - gap: 12px; - justify-content: center; -} - -.connection-actions .btn { - flex: 1; - max-width: 150px; -} - -/* 响应式设计 - 连接信息 */ -@media (max-width: 768px) { - .info-item { - flex-direction: column; - align-items: flex-start; - gap: 8px; - } - - .info-value { - max-width: 100%; - width: 100%; - } - - .connection-actions { - flex-direction: column; - } - - .connection-actions .btn { - max-width: 100%; - } -} - -/* 主要内容区域 */ -.main-content { - display: flex; - gap: 20px; -} - -/* 侧边栏 */ -.sidebar { - width: 250px; - background: var(--glass-bg); - backdrop-filter: blur(10px); - border-radius: 15px; - padding: 20px; - height: fit-content; - box-shadow: var(--shadow-primary); -} - -.nav-menu { - list-style: none; -} - -.nav-menu li { - margin-bottom: 8px; -} - -.nav-item { - display: flex; - align-items: center; - padding: 12px 16px; - color: var(--text-secondary); - text-decoration: none; - border-radius: 10px; - transition: all 0.3s ease; - border: 2px solid transparent; -} - -.nav-item:hover { - background: var(--accent-tertiary); - color: var(--text-secondary); -} - -.nav-item.active { - background: var(--accent-primary); - color: var(--text-inverse); - box-shadow: var(--shadow-primary); -} - -.nav-item i { - margin-right: 10px; - width: 20px; -} - -/* 内容区域 */ -.content-area { - flex: 1; -} - -.content-section { - display: none; -} - -.content-section.active { - display: block; -} - -.content-section h2 { - color: var(--text-secondary); - margin-bottom: 20px; - font-size: 1.5rem; - font-weight: 600; -} - -/* 卡片样式 */ -.card { - background: var(--glass-bg); - backdrop-filter: blur(10px); - border-radius: 15px; - margin-bottom: 20px; - box-shadow: var(--shadow-primary); - overflow: hidden; -} - -.card-header { - background: var(--bg-tertiary); - padding: 20px; - border-bottom: 1px solid var(--border-primary); - display: flex; - justify-content: space-between; - align-items: center; -} - -.card-header h3 { - color: var(--text-secondary); - font-size: 1.2rem; - font-weight: 600; -} - -.card-header h3 i { - color: var(--text-tertiary); - margin-right: 10px; -} - -.card-content { - padding: 20px; -} - -/* 按钮样式 */ -.btn { - padding: 10px 20px; - border: none; - border-radius: 8px; - cursor: pointer; - font-size: 14px; - font-weight: 500; - transition: all 0.3s ease; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - text-decoration: none; - height: 40px; - white-space: nowrap; -} - -.btn-primary { - background: var(--accent-primary); - color: var(--text-inverse); -} - -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-primary); -} - -.btn-secondary { - background: var(--accent-secondary); - color: var(--text-secondary); -} - -.btn-secondary:hover { - background: var(--border-secondary); - transform: translateY(-1px); -} - -.btn-danger { - background: linear-gradient(135deg, #ef4444, #b91c1c); - color: white; -} - -.btn-danger:hover { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(185, 28, 28, 0.45); -} - -.btn-success { - background: linear-gradient(135deg, #22c55e, #15803d); - color: white; -} - -.btn-success:hover { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(21, 128, 61, 0.45); -} - -.btn:disabled { - opacity: 0.6; - cursor: not-allowed; - transform: none !important; -} - -/* 表单元素 */ -.form-group { - margin-bottom: 20px; -} - -.form-group label { - display: block; - margin-bottom: 8px; - color: var(--text-secondary); - font-weight: 500; -} - -.form-hint { - margin-top: 6px; - color: var(--text-tertiary); - font-size: 12px; -} - -.input-group { - display: flex; - gap: 10px; - align-items: center; -} - -input[type="text"], -input[type="password"], -input[type="number"], -input[type="url"], -textarea, -select { - flex: 1; - padding: 12px 16px; - border: 2px solid var(--border-primary); - border-radius: 8px; - font-size: 14px; - transition: all 0.3s ease; - background: var(--bg-secondary); - color: var(--text-primary); -} - -input:focus, -textarea:focus, -select:focus { - outline: none; - border-color: var(--border-focus); - box-shadow: 0 0 0 3px var(--border-primary); -} - -/* 切换开关 */ -.toggle-group { - display: flex; - align-items: center; - gap: 15px; - margin-bottom: 15px; -} - -.toggle-switch { - position: relative; - display: inline-block; - width: 50px; - height: 25px; -} - -.toggle-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--accent-secondary); - transition: .4s; - border-radius: 25px; -} - -.slider:before { - position: absolute; - content: ""; - height: 19px; - width: 19px; - left: 3px; - bottom: 3px; - background-color: var(--text-inverse); - transition: .4s; - border-radius: 50%; -} - -input:checked + .slider { - background: var(--accent-primary); -} - -input:checked + .slider:before { - transform: translateX(25px); -} - -.toggle-label { - color: var(--text-secondary); - font-weight: 500; -} - -/* 列表样式 */ -.key-list, -.provider-list, -.file-list { - max-height: 400px; - overflow-y: auto; -} - -.key-item, -.provider-item, -.file-item { - background: var(--bg-quaternary); - border: 1px solid var(--border-primary); - border-radius: 8px; - padding: 15px; - margin-bottom: 10px; - display: flex; - justify-content: space-between; - align-items: center; - transition: all 0.3s ease; -} - -.key-item:hover, -.provider-item:hover, -.file-item:hover { - background: var(--bg-tertiary); - border-color: var(--border-secondary); - transform: translateY(-1px); -} - -.item-content { - flex: 1; -} - -.item-actions { - display: flex; - gap: 8px; -} - -.item-title { - font-weight: 600; - color: var(--text-secondary); - margin-bottom: 4px; -} - -.item-subtitle { - color: var(--text-tertiary); - font-size: 0.9rem; -} - -.item-value { - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - background: var(--bg-tertiary); - padding: 4px 8px; - border-radius: 4px; - font-size: 0.85rem; - color: var(--text-secondary); - word-break: break-all; -} - -/* 状态信息 */ -.status-info { - background: var(--bg-quaternary); - border-radius: 8px; - padding: 20px; -} - -.status-item { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 15px; - padding-bottom: 15px; - border-bottom: 1px solid var(--border-primary); -} - -.status-item:last-child { - margin-bottom: 0; - padding-bottom: 0; - border-bottom: none; -} - -.status-label { - font-weight: 600; - color: var(--text-secondary); -} - -.status-value { - color: var(--text-tertiary); -} - -/* 模态框 */ -.modal { - display: none; - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: var(--bg-modal); - backdrop-filter: blur(5px); -} - -.modal-content { - background-color: var(--bg-secondary); - margin: 8% auto; - padding: 0; - border-radius: 15px; - width: 90%; - max-width: 550px; - box-shadow: var(--shadow-modal); - animation: modalSlideIn 0.3s ease; - position: relative; -} - -@keyframes modalSlideIn { - from { - opacity: 0; - transform: translateY(-50px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.close { - color: var(--text-tertiary); - position: absolute; - top: 15px; - right: 20px; - font-size: 28px; - font-weight: bold; - cursor: pointer; - z-index: 1001; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - transition: all 0.3s ease; -} - -.close:hover, -.close:focus { - color: var(--text-secondary); - background-color: var(--bg-tertiary); -} - -#modal-body { - padding: 50px 35px 35px 35px; -} - -/* 模态框标题样式 */ -#modal-body h3 { - color: var(--text-secondary); - font-size: 1.3rem; - font-weight: 600; - margin: 0 0 25px 0; - text-align: center; - border-bottom: 2px solid var(--border-primary); - padding-bottom: 15px; -} - -/* 模态框表单组 */ -#modal-body .form-group { - margin-bottom: 20px; -} - -#modal-body .form-group label { - display: block; - margin-bottom: 8px; - color: var(--text-secondary); - font-weight: 600; - font-size: 14px; -} - -#modal-body .form-group input, -#modal-body .form-group textarea { - width: 100%; - padding: 12px 16px; - border: 2px solid var(--border-primary); - border-radius: 8px; - font-size: 14px; - transition: all 0.3s ease; - background: var(--bg-tertiary); - color: var(--text-primary); - font-family: inherit; -} - -#modal-body .form-group input:focus, -#modal-body .form-group textarea:focus { - outline: none; - border-color: var(--border-focus); - box-shadow: 0 0 0 3px var(--border-primary); -} - -#modal-body .form-group textarea { - resize: vertical; - min-height: 80px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 13px; -} - -/* 模态框按钮组 */ -#modal-body .modal-actions { - display: flex; - justify-content: flex-end; - gap: 12px; - margin-top: 30px; - padding-top: 20px; - border-top: 1px solid var(--border-primary); -} - -#modal-body .modal-actions .btn { - min-width: 80px; - padding: 10px 20px; -} - -/* 通知 */ -.notification { - position: fixed; - top: 20px; - right: 20px; - padding: 15px 20px; - border-radius: 8px; - color: white; - font-weight: 500; - z-index: 1001; - transform: translateX(400px); - transition: all 0.3s ease; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); -} - -.notification.show { - transform: translateX(0); -} - -.notification.success { - background: linear-gradient(135deg, #68d391, #38a169); -} - -.notification.error { - background: linear-gradient(135deg, #fc8181, #e53e3e); -} - -.notification.info { - background: linear-gradient(135deg, #63b3ed, #3182ce); -} - -/* 响应式设计 */ -@media (max-width: 768px) { - .main-content { - flex-direction: column; - } - - .sidebar { - width: 100%; - order: 2; - } - - .content-area { - order: 1; - } - - .nav-menu { - display: flex; - overflow-x: auto; - gap: 10px; - } - - .nav-menu li { - margin-bottom: 0; - flex-shrink: 0; - } - - .header-content { - flex-direction: column; - gap: 15px; - text-align: center; - } - - .input-group { - flex-direction: column; - align-items: stretch; - } - - .card-header { - flex-direction: column; - gap: 15px; - align-items: flex-start; - } - - .header-actions { - width: 100%; - justify-content: center; - flex-wrap: wrap; - gap: 8px; - } - - .header-controls { - order: -1; /* 确保控制按钮在小屏幕上排在前面 */ - } - - /* 模态框响应式 */ - .modal-content { - margin: 5% auto; - width: 95%; - max-width: none; - } - - #modal-body { - padding: 40px 25px 25px 25px; - } - - #modal-body h3 { - font-size: 1.2rem; - margin-bottom: 20px; - } - - #modal-body .modal-actions { - flex-direction: column-reverse; - gap: 10px; - } - - #modal-body .modal-actions .btn { - width: 100%; - margin: 0; - } -} - -/* 加载动画 */ -.loading { - display: inline-block; - width: 20px; - height: 20px; - border: 3px solid rgba(255, 255, 255, 0.3); - border-radius: 50%; - border-top-color: #fff; - animation: spin 1s ease-in-out infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* 空状态 */ -.empty-state { - text-align: center; - padding: 40px 20px; - color: var(--text-tertiary); -} - -.empty-state i { - font-size: 48px; - margin-bottom: 16px; - color: var(--border-secondary); -} - -.empty-state h3 { - margin-bottom: 8px; - color: var(--text-secondary); -} - -/* 滚动条样式 */ -::-webkit-scrollbar { - width: 8px; -} - -::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 4px; -} - -::-webkit-scrollbar-thumb { - background: linear-gradient(135deg, #475569, #334155); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: linear-gradient(135deg, #334155, #1f2937); -} - -/* 连接状态指示器 */ -.connection-indicator { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 50%; - margin-right: 8px; -} - -.connection-indicator.connected { - background-color: #68d391; - box-shadow: 0 0 8px rgba(104, 211, 145, 0.6); -} - -.connection-indicator.disconnected { - background-color: #fc8181; - box-shadow: 0 0 8px rgba(252, 129, 129, 0.6); -} - -.connection-indicator.connecting { - background-color: #fbb040; - animation: pulse 1.5s infinite; -} - -@keyframes pulse { - 0% { - opacity: 1; - } - 50% { - opacity: 0.5; - } - 100% { - opacity: 1; - } -} - - -/* Gemini Web Token 模态框样式 */ -.gemini-web-form .form-group { - margin-bottom: 20px; -} - -.gemini-web-form .form-group label { - display: block; - margin-bottom: 8px; - color: var(--text-secondary); - font-weight: 600; - font-size: 14px; -} - -.gemini-web-form .form-group input { - width: 100%; - padding: 12px 16px; - border: 2px solid var(--border-primary); - border-radius: 8px; - font-size: 14px; - transition: all 0.3s ease; - background: var(--bg-tertiary); - color: var(--text-primary); - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; -} - -.gemini-web-form .form-group input:focus { - outline: none; - border-color: var(--border-focus); - box-shadow: 0 0 0 3px var(--border-primary); -} - -.gemini-web-form .form-hint { - margin-top: 6px; - color: var(--text-tertiary); - font-size: 12px; - line-height: 1.4; -} - -/* 使用统计样式 */ -.stats-overview { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 20px; - margin-bottom: 30px; -} - -.stat-card { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 12px; - padding: 24px; - display: flex; - align-items: center; - gap: 16px; - transition: all 0.2s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.stat-card:hover { - border-color: var(--border-primary); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - transform: translateY(-2px); -} - -.stat-icon { - width: 50px; - height: 50px; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - background: var(--primary-color); - color: white; - font-size: 20px; - flex-shrink: 0; -} - -.stat-icon.success { - background: #10b981; -} - -.stat-icon.error { - background: #ef4444; -} - -.stat-content { - flex: 1; -} - -.stat-number { - font-size: 28px; - font-weight: 700; - color: var(--text-primary); - line-height: 1; - margin-bottom: 4px; -} - -.stat-label { - font-size: 14px; - color: var(--text-secondary); - font-weight: 500; -} - -.charts-container { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - margin-bottom: 30px; -} - -@media (max-width: 1200px) { - .charts-container { - grid-template-columns: 1fr; - } -} - -.chart-card { - min-height: 400px; -} - -.chart-container { - position: relative; - height: 300px; - width: 100%; -} - -.chart-controls { - display: flex; - gap: 8px; -} - -.btn.btn-small { - padding: 6px 12px; - font-size: 12px; - border-radius: 6px; - border: 1px solid var(--border-color); - background: var(--card-bg); - color: var(--text-secondary); - cursor: pointer; - transition: all 0.2s ease; -} - -.btn.btn-small:hover { - border-color: var(--border-primary); - color: var(--text-primary); -} - -.btn.btn-small.active { - background: var(--primary-color); - border-color: var(--primary-color); - color: white; -} - -.api-stats-table { - overflow-x: auto; -} - -.stats-table { - width: 100%; - border-collapse: collapse; - margin-top: 16px; -} - -.stats-table th, -.stats-table td { - padding: 12px 16px; - text-align: left; - border-bottom: 1px solid var(--border-color); -} - -.stats-table th { - background: var(--bg-secondary); - font-weight: 600; - color: var(--text-primary); - font-size: 14px; -} - -.stats-table td { - color: var(--text-secondary); - font-size: 14px; -} - -.stats-table tr:hover { - background: var(--bg-secondary); -} - -.model-details { - margin-top: 8px; - padding: 8px 12px; - background: var(--bg-tertiary); - border-radius: 6px; - font-size: 12px; -} - -.model-item { - display: flex; - justify-content: space-between; - margin-bottom: 4px; - color: var(--text-tertiary); -} - -.model-name { - font-weight: 500; - color: var(--text-secondary); -} - -.loading-placeholder { - display: flex; - align-items: center; - justify-content: center; - height: 100px; - color: var(--text-tertiary); - font-size: 14px; -} - -.no-data-message { - text-align: center; - color: var(--text-tertiary); - font-style: italic; - padding: 40px; -} - -/* 暗色主题适配 */ -[data-theme="dark"] .stat-card { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); -} - -[data-theme="dark"] .stat-card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); -} - -[data-theme="dark"] .btn.btn-small { - background: var(--bg-tertiary); -} - -/* 版本信息样式 */ -.version-footer { - margin-top: 2rem; - padding: 1rem 0; - border-top: 1px solid var(--border); - background: var(--bg-secondary); -} - -.version-info { - text-align: center; - font-size: 0.875rem; - color: var(--text-secondary); - opacity: 0.8; -} - -.version-info .separator { - margin: 0 0.75rem; - color: var(--text-secondary); - opacity: 0.6; -} - -/* 暗黑主题下的版本信息 */ -[data-theme="dark"] .version-footer { - border-top-color: var(--border); - background: var(--bg-secondary); -} - -[data-theme="dark"] .version-info { - color: var(--text-secondary); -} - -.connection-reset-btn { - display: inline-flex; - align-items: center; - gap: 6px; - white-space: nowrap; -} - -.connection-reset-btn i { - margin: 0; -} - -.connection-reset-btn span { - font-size: 13px; -} - -[data-theme="dark"] .connection-reset-btn { - background: rgba(30, 41, 59, 0.9); - border-color: rgba(100, 116, 139, 0.4); - color: #cbd5e1; -} - -[data-theme="dark"] .connection-reset-btn:hover { - background: rgba(51, 65, 85, 0.95); - border-color: rgba(100, 116, 139, 0.6); -} - +/* 全局样式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* CSS变量主题系统 */ +:root { + /* 亮色主题(默认) */ + --bg-primary: #f3f4f6; + --bg-secondary: rgba(255, 255, 255, 0.95); + --bg-tertiary: #f8fafc; + --bg-quaternary: #f7fafc; + --bg-modal: rgba(0, 0, 0, 0.5); + + --text-primary: #333; + --text-secondary: #4a5568; + --text-tertiary: #64748b; + --text-quaternary: #6b7280; + --text-inverse: white; + + --border-primary: #e2e8f0; + --border-secondary: #cbd5e0; + --border-focus: #475569; + + --accent-primary: linear-gradient(135deg, #475569, #334155); + --accent-secondary: #e2e8f0; + --accent-tertiary: #f8fafc; + --primary-color: #3b82f6; + --card-bg: #ffffff; + --border-color: #e2e8f0; + + --success-bg: linear-gradient(135deg, #dcfce7, #bbf7d0); + --success-text: #166534; + --success-border: #86efac; + + --error-bg: linear-gradient(135deg, #fef2f2, #fecaca); + --error-text: #dc2626; + --error-border: #fca5a5; + + --warning-bg: linear-gradient(135deg, #fef3c7, #fed7aa); + --warning-text: #d97706; + --warning-border: #fdba74; + + --shadow-primary: 0 8px 32px rgba(0, 0, 0, 0.1); + --shadow-secondary: 0 20px 60px rgba(0, 0, 0, 0.15); + --shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.3); + + --glass-bg: rgba(255, 255, 255, 0.95); + --glass-border: rgba(255, 255, 255, 0.2); +} + +/* 暗色主题 */ +[data-theme="dark"] { + --bg-primary: #0f172a; + --bg-secondary: rgba(30, 41, 59, 0.95); + --bg-tertiary: #1e293b; + --bg-quaternary: #334155; + --bg-modal: rgba(0, 0, 0, 0.7); + + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-tertiary: #94a3b8; + --text-quaternary: #64748b; + --text-inverse: #ffffff; + + --border-primary: #334155; + --border-secondary: #475569; + --border-focus: #64748b; + + --accent-primary: linear-gradient(135deg, #64748b, #475569); + --accent-secondary: #334155; + --accent-tertiary: #1e293b; + --primary-color: #38bdf8; + --card-bg: #1e293b; + --border-color: #334155; + + --success-bg: linear-gradient(135deg, #064e3b, #047857); + --success-text: #bbf7d0; + --success-border: #059669; + + --error-bg: linear-gradient(135deg, #7f1d1d, #b91c1c); + --error-text: #fecaca; + --error-border: #dc2626; + + --warning-bg: linear-gradient(135deg, #78350f, #d97706); + --warning-text: #fed7aa; + --warning-border: #f59e0b; + + --shadow-primary: 0 8px 32px rgba(0, 0, 0, 0.3); + --shadow-secondary: 0 20px 60px rgba(0, 0, 0, 0.4); + --shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.8); + + --glass-bg: rgba(30, 41, 59, 0.95); + --glass-border: rgba(100, 116, 139, 0.2); +} + +/* 登录页面样式 */ +.login-container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-primary); + padding: 20px; +} + +/* 自动登录加载页面样式 */ +.auto-login-content { + text-align: center; + padding: 20px; +} + +.loading-spinner { + margin: 0 auto 20px; + width: 60px; + height: 60px; +} + +.spinner { + width: 100%; + height: 100%; + border: 4px solid #e5e7eb; + border-top: 4px solid #3b82f6; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.auto-login-content h2 { + color: var(--text-secondary); + margin-bottom: 10px; + font-size: 24px; + font-weight: 600; +} + +.auto-login-content p { + color: var(--text-tertiary); + font-size: 16px; + line-height: 1.5; +} + +/* 登录页面头部布局 */ +.login-header-top { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 30px; + position: relative; +} + +.login-title { + width: 100%; + text-align: center; + margin-bottom: 30px; +} + +/* 头部控制按钮组 */ +.header-controls { + display: flex; + gap: 8px; + align-items: center; + height: 100%; +} + +/* 登录页面的控制按钮组样式 */ +.login-header-top .header-controls { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border-radius: 12px; + padding: 8px; + border: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + margin-top: 8px; /* 与标题拉开距离,避免遮挡 */ + margin-bottom: 20px; +} + +.login-header-top .header-controls:hover { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.3); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); + transform: translateY(-1px); +} + +[data-theme="dark"] .login-header-top .header-controls { + background: rgba(30, 41, 59, 0.8); + border: 1px solid rgba(100, 116, 139, 0.3); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); +} + +[data-theme="dark"] .login-header-top .header-controls:hover { + background: rgba(30, 41, 59, 0.9); + border-color: rgba(100, 116, 139, 0.5); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); +} + +.language-switcher, +.theme-switcher { + position: relative; + flex-shrink: 0; +} + +.language-btn, +.theme-btn { + padding: 10px 20px; + font-size: 14px; + font-weight: 500; + border-radius: 8px; + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + color: var(--text-tertiary); + transition: all 0.3s ease; + white-space: nowrap; + backdrop-filter: blur(5px); + height: 40px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +/* 登录页面的按钮样式优化 */ +.login-header-top .language-btn, +.login-header-top .theme-btn { + padding: 8px 12px; + font-size: 13px; + height: 36px; + min-width: 36px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(255, 255, 255, 0.3); + color: #64748b; + backdrop-filter: blur(10px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.login-header-top .language-btn:hover, +.login-header-top .theme-btn:hover { + background: rgba(255, 255, 255, 1); + border-color: rgba(100, 116, 139, 0.3); + color: #475569; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +[data-theme="dark"] .login-header-top .language-btn, +[data-theme="dark"] .login-header-top .theme-btn { + background: rgba(30, 41, 59, 0.9); + border: 1px solid rgba(100, 116, 139, 0.3); + color: #94a3b8; +} + +[data-theme="dark"] .login-header-top .language-btn:hover, +[data-theme="dark"] .login-header-top .theme-btn:hover { + background: rgba(51, 65, 85, 0.95); + border-color: rgba(100, 116, 139, 0.5); + color: #cbd5e1; +} + +.language-btn:hover, +.theme-btn:hover { + background: var(--accent-secondary); + border-color: var(--border-secondary); + color: var(--text-secondary); +} + +.language-btn i, +.theme-btn i { + margin-right: 6px; +} + +.theme-btn.active { + background: var(--accent-primary); + color: var(--text-inverse); +} + +.login-card { + background: var(--glass-bg); + backdrop-filter: blur(10px); + border-radius: 20px; + padding: 40px; + width: 100%; + max-width: 500px; + box-shadow: var(--shadow-secondary); + border: 1px solid var(--glass-border); +} + +.login-header { + text-align: center; + margin-bottom: 25px; +} + +.login-title { + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + color: var(--text-secondary); + font-size: 1.8rem; + font-weight: 600; + margin-bottom: 0; + line-height: 1.2; +} + +#login-logo { + height: 60px; + width: auto; + object-fit: contain; + border-radius: 6px; + padding: 2px; + background: #fff; + border: 1px solid rgba(15, 23, 42, 0.06); + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06); +} + +.login-subtitle { + color: #64748b; + font-size: 1rem; + margin: 0; + text-align: center; +} + +.login-body { + display: flex; + flex-direction: column; + gap: 24px; +} + +.login-connection-info { + background: var(--bg-tertiary); + border: 1px solid var(--border-primary); + border-radius: 16px; + padding: 20px; + display: flex; + flex-direction: column; + gap: 12px; +} + +[data-theme="dark"] .login-connection-info { + background: rgba(30, 41, 59, 0.7); + border-color: rgba(100, 116, 139, 0.3); +} + +.connection-summary { + display: flex; + align-items: center; + gap: 16px; + color: var(--text-secondary); +} + +.connection-summary i { + font-size: 24px; + color: var(--primary-color); +} + +.connection-url { + font-size: 16px; + color: var(--text-secondary); +} + +.connection-url-separator { + margin: 0 8px; + color: var(--text-tertiary); +} + +#login-connection-url { + font-family: "Fira Code", "Consolas", "Courier New", monospace; + color: var(--text-primary); + word-break: break-all; +} + +[data-theme="dark"] #login-connection-url { + color: var(--text-secondary); +} + +.login-form { + display: flex; + flex-direction: column; + gap: 20px; +} + +.login-form .form-group { + margin-bottom: 25px; +} + +.login-form .form-group label { + display: block; + margin-bottom: 8px; + color: var(--text-secondary); + font-weight: 600; + font-size: 0.95rem; +} + +.login-form .form-hint { + margin-top: 6px; + color: #64748b; + font-size: 12px; +} + +.login-form .input-group { + display: flex; + gap: 10px; + align-items: center; +} + +.login-form input[type="text"], +.login-form input[type="password"] { + flex: 1; + padding: 14px 16px; + border: 2px solid var(--border-primary); + border-radius: 10px; + font-size: 15px; + transition: all 0.3s ease; + background: var(--bg-secondary); + color: var(--text-primary); +} + +.login-form input:focus { + outline: none; + border-color: var(--border-focus); + box-shadow: 0 0 0 3px var(--border-primary); +} + +.login-form .btn-secondary { + padding: 14px 16px; + border: 2px solid #e2e8f0; + background: #f8fafc; + color: #64748b; + border-radius: 10px; + transition: all 0.3s ease; +} + +.login-form .btn-secondary:hover { + background: #e2e8f0; + border-color: #cbd5e0; +} + +.form-actions { + margin-top: 30px; + text-align: center; +} + +.login-btn { + width: 100%; + padding: 16px 24px; + font-size: 16px; + font-weight: 600; + border-radius: 12px; + background: linear-gradient(135deg, #475569, #334155); + color: white; + border: none; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} + +.login-btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(51, 65, 85, 0.4); +} + +.login-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +.login-error { + background: var(--error-bg); + border: 1px solid var(--error-border); + color: var(--error-text); + padding: 12px 16px; + border-radius: 8px; + margin-top: 20px; + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 500; +} + +.login-error i { + color: var(--error-text); +} + +/* 响应式设计 - 登录页面 */ +@media (max-width: 640px) { + .login-card { + padding: 30px 20px; + margin: 10px; + max-width: 100%; + } + + .login-header-top { + flex-direction: column; + gap: 20px; + align-items: center; + margin-bottom: 30px; + } + + .header-controls { + flex-direction: row; + justify-content: center; + gap: 8px; + margin-bottom: 10px; /* 在小屏幕上给控制按钮组添加下边距 */ + } + + /* 登录页面小屏幕优化 */ + .login-header-top .header-controls { + margin: 8px auto 0 auto; /* 顶部留白,避免与标题拥挤 */ + background: rgba(255, 255, 255, 0.08); + padding: 6px; + border-radius: 10px; + } + + .login-header-top .language-btn, + .login-header-top .theme-btn { + padding: 6px 10px; + font-size: 12px; + height: 32px; + min-width: 32px; + } + + .language-btn, + .theme-btn { + padding: 8px 16px; + font-size: 13px; + height: 36px; /* 在小屏幕上稍微减小高度 */ + } + + .login-title { + font-size: 1.5rem; + flex-direction: column; + gap: 10px; + text-align: center; + justify-content: center; + margin-bottom: 25px; + } + + #login-logo { + height: 50px; + } + + .login-form .input-group { + flex-direction: column; + align-items: stretch; + } + + .login-form .btn-secondary { + width: 100%; + margin-top: 10px; + } +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: var(--bg-primary); + min-height: 100vh; + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +/* 头部样式 */ +.header { + background: var(--glass-bg); + backdrop-filter: blur(10px); + border-radius: 15px; + padding: 48px 28px; /* 再次拉长,适配更大 Logo */ + margin-bottom: 20px; + box-shadow: var(--shadow-primary); +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; +} + +.header h1 { + color: var(--text-secondary); + font-size: 1.6rem; /* 字体小一号 */ + font-weight: 600; +} + +.header h1.brand { + display: flex; + align-items: flex-end; /* 文字在 Logo 右下角(底对齐) */ + gap: 18px; + line-height: 1; /* 减少标题内上下空白 */ +} + +.header h1 i { + color: #64748b; /* slate-500 */ + margin-right: 10px; +} + +/* 站点 Logo(嵌入头部) */ +#site-logo { + height: 88px; + width: auto; + display: inline-block; + object-fit: contain; + border-radius: 6px; + padding: 2px; + background: #fff; + border: 1px solid rgba(15, 23, 42, 0.06); + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06); +} + +.brand-title { + display: inline-block; + line-height: 1.1; /* 保持原字号不变,同时更紧凑 */ + margin-bottom: 4px; /* 让文字更贴近更高的 Logo 底部 */ +} + +/* 小屏幕适配:避免 Logo 过大占位 */ +@media (max-width: 640px) { + #site-logo { height: 64px; } +} + +.header-actions { + display: flex; + gap: 12px; + align-items: center; + height: 40px; /* 统一高度 */ +} + + +/* 认证区域 */ +.auth-section { + margin-bottom: 20px; +} + +/* 连接信息样式 */ +.connection-info { + background: var(--bg-tertiary); + border-radius: 12px; + padding: 20px; + margin-bottom: 20px; +} + +.info-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid var(--border-primary); +} + +.info-item:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.info-label { + display: flex; + align-items: center; + gap: 8px; + color: var(--text-secondary); + font-weight: 600; + font-size: 14px; +} + +.info-label i { + color: var(--text-tertiary); + width: 16px; +} + +.info-value { + color: var(--text-primary); + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + background: var(--bg-secondary); + padding: 6px 12px; + border-radius: 6px; + border: 1px solid var(--border-primary); + max-width: 300px; + word-break: break-all; +} + +.status-indicator { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; +} + +.status-indicator.connected { + background: var(--success-bg); + color: var(--success-text); + border: 1px solid var(--success-border); +} + +.status-indicator.disconnected { + background: var(--error-bg); + color: var(--error-text); + border: 1px solid var(--error-border); +} + +.status-indicator.connecting { + background: var(--warning-bg); + color: var(--warning-text); + border: 1px solid var(--warning-border); +} + +.connection-actions { + display: flex; + gap: 12px; + justify-content: center; +} + +.connection-actions .btn { + flex: 1; + max-width: 150px; +} + +/* 响应式设计 - 连接信息 */ +@media (max-width: 768px) { + .info-item { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .info-value { + max-width: 100%; + width: 100%; + } + + .connection-actions { + flex-direction: column; + } + + .connection-actions .btn { + max-width: 100%; + } +} + +/* 主要内容区域 */ +.main-content { + display: flex; + gap: 20px; +} + +/* 侧边栏 */ +.sidebar { + width: 250px; + background: var(--glass-bg); + backdrop-filter: blur(10px); + border-radius: 15px; + padding: 20px; + height: fit-content; + box-shadow: var(--shadow-primary); +} + +.nav-menu { + list-style: none; +} + +.nav-menu li { + margin-bottom: 8px; +} + +.nav-item { + display: flex; + align-items: center; + padding: 12px 16px; + color: var(--text-secondary); + text-decoration: none; + border-radius: 10px; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.nav-item:hover { + background: var(--accent-tertiary); + color: var(--text-secondary); +} + +.nav-item.active { + background: var(--accent-primary); + color: var(--text-inverse); + box-shadow: var(--shadow-primary); +} + +.nav-item i { + margin-right: 10px; + width: 20px; +} + +/* 内容区域 */ +.content-area { + flex: 1; +} + +.content-section { + display: none; +} + +.content-section.active { + display: block; +} + +.content-section h2 { + color: var(--text-secondary); + margin-bottom: 20px; + font-size: 1.5rem; + font-weight: 600; +} + +/* 卡片样式 */ +.card { + background: var(--glass-bg); + backdrop-filter: blur(10px); + border-radius: 15px; + margin-bottom: 20px; + box-shadow: var(--shadow-primary); + overflow: hidden; +} + +.card-header { + background: var(--bg-tertiary); + padding: 20px; + border-bottom: 1px solid var(--border-primary); + display: flex; + justify-content: space-between; + align-items: center; +} + +.card-header h3 { + color: var(--text-secondary); + font-size: 1.2rem; + font-weight: 600; +} + +.card-header h3 i { + color: var(--text-tertiary); + margin-right: 10px; +} + +.card-content { + padding: 20px; +} + +/* 按钮样式 */ +.btn { + padding: 10px 20px; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.3s ease; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + text-decoration: none; + height: 40px; + white-space: nowrap; +} + +.btn-primary { + background: var(--accent-primary); + color: var(--text-inverse); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-primary); +} + +.btn-secondary { + background: var(--accent-secondary); + color: var(--text-secondary); +} + +.btn-secondary:hover { + background: var(--border-secondary); + transform: translateY(-1px); +} + +.btn-danger { + background: linear-gradient(135deg, #ef4444, #b91c1c); + color: white; +} + +.btn-danger:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(185, 28, 28, 0.45); +} + +.btn-success { + background: linear-gradient(135deg, #22c55e, #15803d); + color: white; +} + +.btn-success:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(21, 128, 61, 0.45); +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; +} + +/* 表单元素 */ +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: block; + margin-bottom: 6px; + color: var(--text-secondary); + font-weight: 500; +} + +.form-hint { + font-size: 0.85rem; + color: var(--text-tertiary); + margin: 4px 0 8px; +} + +.input-group { + display: flex; + gap: 10px; + align-items: center; +} + +input[type="text"], +input[type="password"], +input[type="number"], +input[type="url"], +textarea, +select { + flex: 1; + padding: 12px 16px; + border: 2px solid var(--border-primary); + border-radius: 8px; + font-size: 14px; + transition: all 0.3s ease; + background: var(--bg-secondary); + color: var(--text-primary); +} + +input:focus, +textarea:focus, +select:focus { + outline: none; + border-color: var(--border-focus); + box-shadow: 0 0 0 3px var(--border-primary); +} + +/* 切换开关 */ +.toggle-group { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; +} + +.toggle-switch { + position: relative; + display: inline-block; + width: 50px; + height: 25px; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--accent-secondary); + transition: .4s; + border-radius: 25px; +} + +.slider:before { + position: absolute; + content: ""; + height: 19px; + width: 19px; + left: 3px; + bottom: 3px; + background-color: var(--text-inverse); + transition: .4s; + border-radius: 50%; +} + +input:checked + .slider { + background: var(--accent-primary); +} + +input:checked + .slider:before { + transform: translateX(25px); +} + +.toggle-label { + color: var(--text-secondary); + font-weight: 500; +} + +/* 列表样式 */ +.key-list, +.provider-list, +.file-list { + max-height: 400px; + overflow-y: auto; +} + +.key-item, +.provider-item, +.file-item { + background: var(--bg-quaternary); + border: 1px solid var(--border-primary); + border-radius: 8px; + padding: 15px; + margin-bottom: 10px; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.3s ease; +} + +.key-item:hover, +.provider-item:hover, +.file-item:hover { + background: var(--bg-tertiary); + border-color: var(--border-secondary); + transform: translateY(-1px); +} + +.item-content { + flex: 1; +} + +.item-actions { + display: flex; + gap: 8px; +} + +.item-title { + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 4px; +} + +.item-subtitle { + color: var(--text-tertiary); + font-size: 0.9rem; +} + +.provider-item .provider-models { + margin-top: 8px; + display: flex; + flex-wrap: wrap; +} + +.provider-model-tag { + display: inline-flex; + align-items: center; + gap: 4px; + background: var(--bg-quinary); + color: var(--text-secondary); + border: 1px solid var(--border-secondary); + border-radius: 14px; + padding: 4px 10px; + margin-right: 6px; + margin-top: 6px; + font-size: 0.85rem; +} + +.provider-model-tag .model-name { + font-weight: 600; +} + +.provider-model-tag .model-alias { + color: var(--text-tertiary); + font-style: italic; +} + +.item-value { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + background: var(--bg-tertiary); + padding: 4px 8px; + border-radius: 4px; + font-size: 0.85rem; + color: var(--text-secondary); + word-break: break-all; +} + +/* 状态信息 */ +.status-info { + background: var(--bg-quaternary); + border-radius: 8px; + padding: 20px; +} + +.status-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px solid var(--border-primary); +} + +.status-item:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.status-label { + font-weight: 600; + color: var(--text-secondary); +} + +.status-value { + color: var(--text-tertiary); +} + +/* 模态框 */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: var(--bg-modal); + backdrop-filter: blur(5px); +} + +.modal-content { + background-color: var(--bg-secondary); + margin: 4% auto; + padding: 0; + border-radius: 15px; + width: 90%; + max-width: 550px; + box-shadow: var(--shadow-modal); + animation: modalSlideIn 0.3s ease; + position: relative; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.close { + color: var(--text-tertiary); + position: absolute; + top: 15px; + right: 20px; + font-size: 28px; + font-weight: bold; + cursor: pointer; + z-index: 1001; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.3s ease; +} + +.close:hover, +.close:focus { + color: var(--text-secondary); + background-color: var(--bg-tertiary); +} + +#modal-body { + padding: 35px 30px 30px 30px; +} + +/* 模态框标题样式 */ +#modal-body h3 { + color: var(--text-secondary); + font-size: 1.25rem; + font-weight: 600; + margin: 0 0 20px 0; + text-align: center; + border-bottom: 2px solid var(--border-primary); + padding-bottom: 12px; +} + +/* 模态框表单组 */ +#modal-body .form-group { + margin-bottom: 16px; +} + +#modal-body .form-group label { + display: block; + margin-bottom: 6px; + color: var(--text-secondary); + font-weight: 600; + font-size: 14px; +} + +#modal-body .form-group input, +#modal-body .form-group textarea { + width: 100%; + padding: 12px 16px; + border: 2px solid var(--border-primary); + border-radius: 8px; + font-size: 14px; + transition: all 0.3s ease; + background: var(--bg-tertiary); + color: var(--text-primary); + font-family: inherit; +} + +#modal-body .form-group input:focus, +#modal-body .form-group textarea:focus { + outline: none; + border-color: var(--border-focus); + box-shadow: 0 0 0 3px var(--border-primary); +} + +#modal-body .form-group textarea { + resize: vertical; + min-height: 80px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; +} + +/* 模态框按钮组 */ +#modal-body .modal-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 24px; + padding-top: 18px; + border-top: 1px solid var(--border-primary); +} + +#modal-body .modal-actions .btn { + min-width: 80px; + padding: 10px 20px; +} + +/* 通知 */ +.notification { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 20px; + border-radius: 8px; + color: white; + font-weight: 500; + z-index: 1001; + transform: translateX(400px); + transition: all 0.3s ease; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); +} + +.notification.show { + transform: translateX(0); +} + +.notification.success { + background: linear-gradient(135deg, #68d391, #38a169); +} + +.notification.error { + background: linear-gradient(135deg, #fc8181, #e53e3e); +} + +.notification.info { + background: linear-gradient(135deg, #63b3ed, #3182ce); +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .main-content { + flex-direction: column; + } + + .sidebar { + width: 100%; + order: 2; + } + + .content-area { + order: 1; + } + + .nav-menu { + display: flex; + overflow-x: auto; + gap: 10px; + } + + .nav-menu li { + margin-bottom: 0; + flex-shrink: 0; + } + + .header-content { + flex-direction: column; + gap: 15px; + text-align: center; + } + + .input-group { + flex-direction: column; + align-items: stretch; + } + + .card-header { + flex-direction: column; + gap: 15px; + align-items: flex-start; + } + + .header-actions { + width: 100%; + justify-content: center; + flex-wrap: wrap; + gap: 8px; + } + + .header-controls { + order: -1; /* 确保控制按钮在小屏幕上排在前面 */ + } + + /* 模态框响应式 */ + .modal-content { + margin: 5% auto; + width: 95%; + max-width: none; + } + + #modal-body { + padding: 40px 25px 25px 25px; + } + + #modal-body h3 { + font-size: 1.2rem; + margin-bottom: 20px; + } + + #modal-body .modal-actions { + flex-direction: column-reverse; + gap: 10px; + } + + #modal-body .modal-actions .btn { + width: 100%; + margin: 0; + } +} + +/* 加载动画 */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* 空状态 */ +.empty-state { + text-align: center; + padding: 40px 20px; + color: var(--text-tertiary); +} + +.empty-state i { + font-size: 48px; + margin-bottom: 16px; + color: var(--border-secondary); +} + +.empty-state h3 { + margin-bottom: 8px; + color: var(--text-secondary); +} + +/* 滚动条样式 */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, #475569, #334155); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(135deg, #334155, #1f2937); +} + +/* 连接状态指示器 */ +.connection-indicator { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 8px; +} + +.connection-indicator.connected { + background-color: #68d391; + box-shadow: 0 0 8px rgba(104, 211, 145, 0.6); +} + +.connection-indicator.disconnected { + background-color: #fc8181; + box-shadow: 0 0 8px rgba(252, 129, 129, 0.6); +} + +.connection-indicator.connecting { + background-color: #fbb040; + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } +} + + +/* Gemini Web Token 模态框样式 */ +.gemini-web-form .form-group { + margin-bottom: 20px; +} + +.gemini-web-form .form-group label { + display: block; + margin-bottom: 8px; + color: var(--text-secondary); + font-weight: 600; + font-size: 14px; +} + +.gemini-web-form .form-group input { + width: 100%; + padding: 12px 16px; + border: 2px solid var(--border-primary); + border-radius: 8px; + font-size: 14px; + transition: all 0.3s ease; + background: var(--bg-tertiary); + color: var(--text-primary); + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; +} + +.gemini-web-form .form-group input:focus { + outline: none; + border-color: var(--border-focus); + box-shadow: 0 0 0 3px var(--border-primary); +} + +.gemini-web-form .form-hint { + margin-top: 6px; + color: var(--text-tertiary); + font-size: 12px; + line-height: 1.4; +} + +/* 使用统计样式 */ +.stats-overview { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.stat-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 24px; + display: flex; + align-items: center; + gap: 16px; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.stat-card:hover { + border-color: var(--border-primary); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); +} + +.stat-icon { + width: 50px; + height: 50px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + background: var(--primary-color); + color: white; + font-size: 20px; + flex-shrink: 0; +} + +.stat-icon.success { + background: #10b981; +} + +.stat-icon.error { + background: #ef4444; +} + +.stat-content { + flex: 1; +} + +.stat-number { + font-size: 28px; + font-weight: 700; + color: var(--text-primary); + line-height: 1; + margin-bottom: 4px; +} + +.stat-label { + font-size: 14px; + color: var(--text-secondary); + font-weight: 500; +} + +.charts-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 30px; +} + +@media (max-width: 1200px) { + .charts-container { + grid-template-columns: 1fr; + } +} + +.chart-card { + min-height: 400px; +} + +.chart-container { + position: relative; + height: 300px; + width: 100%; +} + +.chart-controls { + display: flex; + gap: 8px; +} + +.btn.btn-small { + padding: 6px 12px; + font-size: 12px; + border-radius: 6px; + border: 1px solid var(--border-color); + background: var(--card-bg); + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s ease; +} + +.btn.btn-small:hover { + border-color: var(--border-primary); + color: var(--text-primary); +} + +.btn.btn-small.active { + background: var(--primary-color); + border-color: var(--primary-color); + color: white; +} + +.api-stats-table { + overflow-x: auto; +} + +.stats-table { + width: 100%; + border-collapse: collapse; + margin-top: 16px; +} + +.stats-table th, +.stats-table td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.stats-table th { + background: var(--bg-secondary); + font-weight: 600; + color: var(--text-primary); + font-size: 14px; +} + +.stats-table td { + color: var(--text-secondary); + font-size: 14px; +} + +.stats-table tr:hover { + background: var(--bg-secondary); +} + +.model-details { + margin-top: 8px; + padding: 8px 12px; + background: var(--bg-tertiary); + border-radius: 6px; + font-size: 12px; +} + +.model-item { + display: flex; + justify-content: space-between; + margin-bottom: 4px; + color: var(--text-tertiary); +} + +.model-name { + font-weight: 500; + color: var(--text-secondary); +} + +.loading-placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 100px; + color: var(--text-tertiary); + font-size: 14px; +} + +.no-data-message { + text-align: center; + color: var(--text-tertiary); + font-style: italic; + padding: 40px; +} + +/* 暗色主题适配 */ +[data-theme="dark"] .stat-card { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +[data-theme="dark"] .stat-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +[data-theme="dark"] .btn.btn-small { + background: var(--bg-tertiary); +} + +/* 版本信息样式 */ +.version-footer { + margin-top: 2rem; + padding: 1rem 0; + border-top: 1px solid var(--border); + background: var(--bg-secondary); +} + +.version-info { + text-align: center; + font-size: 0.875rem; + color: var(--text-secondary); + opacity: 0.8; +} + +.version-info .separator { + margin: 0 0.75rem; + color: var(--text-secondary); + opacity: 0.6; +} + +/* 暗黑主题下的版本信息 */ +[data-theme="dark"] .version-footer { + border-top-color: var(--border); + background: var(--bg-secondary); +} + +[data-theme="dark"] .version-info { + color: var(--text-secondary); +} + +.connection-reset-btn { + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; +} + +.connection-reset-btn i { + margin: 0; +} + +.connection-reset-btn span { + font-size: 13px; +} + +[data-theme="dark"] .connection-reset-btn { + background: rgba(30, 41, 59, 0.9); + border-color: rgba(100, 116, 139, 0.4); + color: #cbd5e1; +} + +[data-theme="dark"] .connection-reset-btn:hover { + background: rgba(51, 65, 85, 0.95); + border-color: rgba(100, 116, 139, 0.6); +} + +.model-input-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.model-input-row .input-group { + display: flex; + gap: 8px; +} + +.model-input-row .model-name-input, +.model-input-row .model-alias-input { + flex: 1; +} + +.model-input-row .model-alias-input { + max-width: 220px; +} + +.model-input-row .model-remove-btn { + align-self: center; +} +