diff --git a/app.js b/app.js index d8be85d..06c5a29 100644 --- a/app.js +++ b/app.js @@ -1,2856 +1,2941 @@ -// 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(); - } - }); - - // 移动端菜单按钮 - const mobileMenuBtn = document.getElementById('mobile-menu-btn'); - const sidebarOverlay = document.getElementById('sidebar-overlay'); - const sidebar = document.getElementById('sidebar'); - - if (mobileMenuBtn) { - mobileMenuBtn.addEventListener('click', () => this.toggleMobileSidebar()); - } - - if (sidebarOverlay) { - sidebarOverlay.addEventListener('click', () => this.closeMobileSidebar()); - } - - // 点击侧边栏导航项时在移动端关闭侧边栏 - const navItems = document.querySelectorAll('.nav-item'); - navItems.forEach(item => { - item.addEventListener('click', () => { - if (window.innerWidth <= 1024) { - this.closeMobileSidebar(); - } - }); - }); - } - - // 切换移动端侧边栏 - toggleMobileSidebar() { - const sidebar = document.getElementById('sidebar'); - const overlay = document.getElementById('sidebar-overlay'); - - if (sidebar && overlay) { - sidebar.classList.toggle('mobile-open'); - overlay.classList.toggle('active'); - } - } - - // 关闭移动端侧边栏 - closeMobileSidebar() { - const sidebar = document.getElementById('sidebar'); - const overlay = document.getElementById('sidebar-overlay'); - - if (sidebar && overlay) { - sidebar.classList.remove('mobile-open'); - overlay.classList.remove('active'); - } - } - - // 设置导航 - 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(); -}); +// 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(); + } + }); + + // 移动端菜单按钮 + const mobileMenuBtn = document.getElementById('mobile-menu-btn'); + const sidebarOverlay = document.getElementById('sidebar-overlay'); + const sidebar = document.getElementById('sidebar'); + + if (mobileMenuBtn) { + mobileMenuBtn.addEventListener('click', () => this.toggleMobileSidebar()); + } + + if (sidebarOverlay) { + sidebarOverlay.addEventListener('click', () => this.closeMobileSidebar()); + } + + // 侧边栏收起/展开按钮(桌面端) + const sidebarToggleBtnDesktop = document.getElementById('sidebar-toggle-btn-desktop'); + if (sidebarToggleBtnDesktop) { + sidebarToggleBtnDesktop.addEventListener('click', () => this.toggleSidebar()); + } + + // 从本地存储恢复侧边栏状态 + this.restoreSidebarState(); + + // 监听窗口大小变化 + window.addEventListener('resize', () => { + const sidebar = document.getElementById('sidebar'); + const layout = document.getElementById('layout-container'); + + if (window.innerWidth <= 1024) { + // 移动端:移除收起状态 + if (sidebar && layout) { + sidebar.classList.remove('collapsed'); + layout.classList.remove('sidebar-collapsed'); + } + } else { + // 桌面端:恢复保存的状态 + this.restoreSidebarState(); + } + }); + + // 点击侧边栏导航项时在移动端关闭侧边栏 + const navItems = document.querySelectorAll('.nav-item'); + navItems.forEach(item => { + item.addEventListener('click', () => { + if (window.innerWidth <= 1024) { + this.closeMobileSidebar(); + } + }); + }); + } + + // 切换移动端侧边栏 + toggleMobileSidebar() { + const sidebar = document.getElementById('sidebar'); + const overlay = document.getElementById('sidebar-overlay'); + const layout = document.getElementById('layout-container'); + const mainWrapper = document.getElementById('main-wrapper'); + + if (sidebar && overlay) { + const isOpen = sidebar.classList.toggle('mobile-open'); + overlay.classList.toggle('active'); + if (layout) { + layout.classList.toggle('sidebar-open', isOpen); + } + if (mainWrapper) { + mainWrapper.classList.toggle('sidebar-open', isOpen); + } + } + } + + // 关闭移动端侧边栏 + closeMobileSidebar() { + const sidebar = document.getElementById('sidebar'); + const overlay = document.getElementById('sidebar-overlay'); + const layout = document.getElementById('layout-container'); + const mainWrapper = document.getElementById('main-wrapper'); + + if (sidebar && overlay) { + sidebar.classList.remove('mobile-open'); + overlay.classList.remove('active'); + if (layout) { + layout.classList.remove('sidebar-open'); + } + if (mainWrapper) { + mainWrapper.classList.remove('sidebar-open'); + } + } + } + + // 切换侧边栏收起/展开状态 + toggleSidebar() { + const sidebar = document.getElementById('sidebar'); + const layout = document.getElementById('layout-container'); + + if (sidebar && layout) { + const isCollapsed = sidebar.classList.toggle('collapsed'); + layout.classList.toggle('sidebar-collapsed', isCollapsed); + + // 保存状态到本地存储 + localStorage.setItem('sidebarCollapsed', isCollapsed ? 'true' : 'false'); + + // 更新按钮提示文本 + const toggleBtn = document.getElementById('sidebar-toggle-btn-desktop'); + if (toggleBtn) { + toggleBtn.setAttribute('title', isCollapsed ? '展开侧边栏' : '收起侧边栏'); + } + } + } + + // 恢复侧边栏状态 + restoreSidebarState() { + // 只在桌面端恢复侧栏状态 + if (window.innerWidth > 1024) { + const savedState = localStorage.getItem('sidebarCollapsed'); + if (savedState === 'true') { + const sidebar = document.getElementById('sidebar'); + const layout = document.getElementById('layout-container'); + + if (sidebar && layout) { + sidebar.classList.add('collapsed'); + layout.classList.add('sidebar-collapsed'); + + // 更新按钮提示文本 + const toggleBtn = document.getElementById('sidebar-toggle-btn-desktop'); + if (toggleBtn) { + toggleBtn.setAttribute('title', '展开侧边栏'); + } + } + } + } + } + + // 设置导航 + 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/index.html b/index.html index 45beafd..2dc01c8 100644 --- a/index.html +++ b/index.html @@ -1,591 +1,593 @@ - - - - - - CLI Proxy API Management Center - - - - - - - - - - -
-
- - - - - -
- -
- - -
-
- - - - - - - - - -
- - - - - - + + + + + + CLI Proxy API Management Center + + + + + + + + + + +
+
+ + + + + +
+ +
+ + +
+
+ + + + + + + diff --git a/styles.css b/styles.css index ee54b32..fe73e38 100644 --- a/styles.css +++ b/styles.css @@ -7,6 +7,9 @@ /* CSS变量主题系统 */ :root { + /* 布局尺寸 */ + --navbar-height: 69px; + /* 亮色主题(默认) */ --bg-primary: #f5f7fa; --bg-secondary: #ffffff; @@ -605,30 +608,92 @@ body { overflow-x: hidden; } -.container { - display: flex; - min-height: 100vh; - position: relative; +/* 侧边栏样式 */ +.layout { + display: grid; + grid-template-columns: 240px 1fr; + min-height: calc(100vh - var(--navbar-height, 69px)); + width: 100%; + transition: grid-template-columns 0.3s ease; +} + +.layout.sidebar-collapsed { + grid-template-columns: 64px 1fr; } -/* 侧边栏样式 */ .sidebar { width: 240px; background: var(--sidebar-bg); border-right: 1px solid var(--sidebar-border); - height: 100vh; - position: fixed; - left: 0; - top: 0; + height: calc(100vh - var(--navbar-height, 69px)); + position: sticky; + top: var(--navbar-height, 69px); overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column; - z-index: 100; + z-index: 5; box-shadow: var(--shadow-sm); transition: all 0.3s ease; } +.sidebar.collapsed { + width: 64px; +} + +.sidebar.collapsed .nav-item span { + opacity: 0; + width: 0; + overflow: hidden; + white-space: nowrap; +} + +.sidebar.collapsed .nav-item { + justify-content: center; + padding: 10px 0; +} + +.sidebar.collapsed .nav-item i { + margin-right: 0; +} + +/* 侧边栏切换按钮(桌面端-在顶栏) */ +.sidebar-toggle-btn-desktop { + display: none; + width: 40px; + height: 40px; + border: none; + background: transparent; + color: var(--text-primary); + border-radius: 8px; + cursor: pointer; + align-items: center; + justify-content: center; + font-size: 18px; + transition: all 0.2s ease; + padding: 0; +} + +.sidebar-toggle-btn-desktop:hover { + background: var(--bg-tertiary); + color: var(--primary-color); +} + +.sidebar-toggle-btn-desktop i { + transition: transform 0.3s ease; +} + +.layout.sidebar-collapsed .sidebar-toggle-btn-desktop i { + transform: rotate(90deg); +} + +/* 在大屏幕上显示桌面端切换按钮 */ +@media (min-width: 1025px) { + .sidebar-toggle-btn-desktop { + display: flex; + } +} + /* 侧边栏品牌区域 */ .sidebar-brand { padding: 24px 20px; @@ -662,6 +727,7 @@ body { .sidebar .nav-menu li { margin-bottom: 4px; + position: relative; } .sidebar .nav-item { @@ -675,6 +741,7 @@ body { font-size: 14px; font-weight: 500; cursor: pointer; + position: relative; } .sidebar .nav-item:hover { @@ -692,16 +759,43 @@ body { width: 18px; font-size: 16px; text-align: center; + transition: margin 0.3s ease; +} + +/* 收起状态时的工具提示 */ +.sidebar.collapsed .nav-menu li:hover::after { + content: attr(data-tooltip); + position: absolute; + left: 68px; + top: 50%; + transform: translateY(-50%); + background: var(--bg-secondary); + color: var(--text-primary); + padding: 8px 12px; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + white-space: nowrap; + box-shadow: var(--shadow-md); + border: 1px solid var(--border-primary); + z-index: 1000; + pointer-events: none; + opacity: 0; + animation: tooltipFadeIn 0.2s ease forwards; +} + +@keyframes tooltipFadeIn { + to { + opacity: 1; + } } /* 主内容区域包装器 */ .main-wrapper { - margin-left: 240px; - min-height: 100vh; - width: calc(100% - 240px); display: flex; flex-direction: column; background: var(--bg-primary); + min-height: 100%; } /* 顶部导航栏 */ @@ -714,27 +808,47 @@ body { align-items: center; position: sticky; top: 0; - z-index: 90; + z-index: 200; box-shadow: var(--shadow-sm); + height: var(--navbar-height, 69px); + min-height: var(--navbar-height, 69px); + box-sizing: border-box; } .top-navbar-left { display: flex; align-items: center; - gap: 16px; + gap: 12px; + flex: 1; } -.top-navbar-title { - font-size: 20px; +.top-navbar-brand { + display: flex; + align-items: center; + gap: 12px; + max-width: max-content; +} + +.top-navbar-brand-logo { + width: 32px; + height: 32px; + object-fit: contain; + border-radius: 8px; +} + +.top-navbar-brand-text { + font-size: 18px; font-weight: 600; color: var(--text-primary); - margin: 0; + overflow: hidden; + text-overflow: ellipsis; } .top-navbar-actions { display: flex; align-items: center; gap: 10px; + margin-left: auto; } .top-navbar-actions > * { @@ -1476,17 +1590,54 @@ input:checked + .slider:before { /* 响应式设计 */ @media (max-width: 1024px) { - .sidebar { - transform: translateX(-100%); + .layout { + position: relative; + grid-template-columns: 1fr; + min-height: calc(100vh - var(--navbar-height, 69px)); } - + + .sidebar { + position: fixed; + left: 0; + top: var(--navbar-height, 69px); + height: calc(100vh - var(--navbar-height, 69px)); + transform: translateX(-100%); + z-index: 150; + box-shadow: var(--shadow-sm); + background: var(--sidebar-bg); + width: 240px !important; + } + .sidebar.mobile-open { transform: translateX(0); + box-shadow: var(--shadow-secondary); + } + + /* 移动端强制恢复侧栏展开状态 */ + .sidebar.collapsed { + width: 240px !important; + } + + .sidebar.collapsed .nav-item span { + opacity: 1 !important; + width: auto !important; + overflow: visible !important; + } + + .sidebar.collapsed .nav-item { + justify-content: flex-start !important; + padding: 10px 14px !important; + } + + .sidebar.collapsed .nav-item i { + margin-right: 12px !important; } .main-wrapper { margin-left: 0; width: 100%; + min-height: calc(100vh - var(--navbar-height, 69px)); + overflow: hidden; } .main-content { @@ -1605,29 +1756,38 @@ input:checked + .slider:before { width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); - z-index: 99; + z-index: 120; } -@media (max-width: 1024px) { - .sidebar-overlay.active { - display: block; - } +.sidebar-overlay.active { + display: block; } /* 移动端菜单按钮 */ .mobile-menu-btn { display: none; - background: none; + width: 40px; + height: 40px; + background: transparent; border: none; - padding: 8px; + padding: 0; cursor: pointer; color: var(--text-primary); font-size: 20px; + border-radius: 8px; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.mobile-menu-btn:hover { + background: var(--bg-tertiary); + color: var(--primary-color); } @media (max-width: 1024px) { .mobile-menu-btn { - display: block; + display: flex; } }