From ad520b7b26538b6ee14ec0882d723f4df06cd4c4 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Mon, 17 Nov 2025 14:45:42 +0800 Subject: [PATCH] refactor(app): reuse debounce util and connection module --- app.js | 653 +--------------------------------------- src/core/connection.js | 441 +++++++++++++++++++++++++++ src/modules/api-keys.js | 107 ++++++- 3 files changed, 553 insertions(+), 648 deletions(-) create mode 100644 src/core/connection.js diff --git a/app.js b/app.js index 293b1d1..9894649 100644 --- a/app.js +++ b/app.js @@ -16,20 +16,20 @@ import { aiProvidersModule } from './src/modules/ai-providers.js'; import { escapeHtml } from './src/utils/html.js'; import { maskApiKey } from './src/utils/string.js'; import { normalizeArrayResponse } from './src/utils/array.js'; +import { debounce } from './src/utils/dom.js'; import { CACHE_EXPIRY_MS, MAX_LOG_LINES, - DEFAULT_API_PORT, DEFAULT_AUTH_FILES_PAGE_SIZE, MIN_AUTH_FILES_PAGE_SIZE, MAX_AUTH_FILES_PAGE_SIZE, OAUTH_CARD_IDS, - STORAGE_KEY_AUTH_FILES_PAGE_SIZE, - STATUS_UPDATE_INTERVAL_MS + STORAGE_KEY_AUTH_FILES_PAGE_SIZE } from './src/utils/constants.js'; // 核心服务导入 import { createErrorHandler } from './src/core/error-handler.js'; +import { connectionModule } from './src/core/connection.js'; // CLI Proxy API 管理界面 JavaScript class CLIProxyManager { @@ -127,15 +127,6 @@ class CLIProxyManager { return Math.min(maxSize, Math.max(minSize, parsed)); } - // 简易防抖,减少频繁写 localStorage - debounce(fn, delay = 400) { - let timer; - return (...args) => { - clearTimeout(timer); - timer = setTimeout(() => fn.apply(this, args), delay); - }; - } - init() { this.initializeTheme(); this.checkLoginStatus(); @@ -514,85 +505,6 @@ class CLIProxyManager { } - // 初始化配置文件编辑器 - // 规范化基础地址,移除尾部斜杠与 /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'); @@ -619,457 +531,6 @@ class CLIProxyManager { } } - // 测试连接(简化版,用于内部调用) - 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'); - - if (this.lastEditorConnectionState !== this.isConnected) { - this.updateConfigEditorAvailability(); - } - - // 更新连接信息显示 - 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(section = null) { - if (section) { - // 检查特定配置段的缓存 - // 注意:配置值可能是 false、0、'' 等 falsy 值,不能用 ! 判断 - if (!(section in this.configCache) || !(section in this.cacheTimestamps)) { - return false; - } - return (Date.now() - this.cacheTimestamps[section]) < this.cacheExpiry; - } - // 检查全局缓存(兼容旧代码) - if (!this.configCache['__full__'] || !this.cacheTimestamps['__full__']) { - return false; - } - return (Date.now() - this.cacheTimestamps['__full__']) < this.cacheExpiry; - } - - // 获取配置(优先使用缓存,支持按段获取) - async getConfig(section = null, forceRefresh = false) { - const now = Date.now(); - - // 如果请求特定配置段且该段缓存有效 - if (section && !forceRefresh && this.isCacheValid(section)) { - this.updateConnectionStatus(); - return this.configCache[section]; - } - - // 如果请求全部配置且全局缓存有效 - if (!section && !forceRefresh && this.isCacheValid()) { - this.updateConnectionStatus(); - return this.configCache['__full__']; - } - - try { - const config = await this.makeRequest('/config'); - - if (section) { - // 缓存特定配置段 - this.configCache[section] = config[section]; - this.cacheTimestamps[section] = now; - // 同时更新全局缓存中的这一段 - if (this.configCache['__full__']) { - this.configCache['__full__'][section] = config[section]; - } else { - // 如果全局缓存不存在,也创建它 - this.configCache['__full__'] = config; - this.cacheTimestamps['__full__'] = now; - } - this.updateConnectionStatus(); - return config[section]; - } else { - // 缓存全部配置 - this.configCache['__full__'] = config; - this.cacheTimestamps['__full__'] = now; - - // 同时缓存各个配置段 - Object.keys(config).forEach(key => { - this.configCache[key] = config[key]; - this.cacheTimestamps[key] = now; - }); - - this.updateConnectionStatus(); - return config; - } - } catch (error) { - console.error('获取配置失败:', error); - throw error; - } - } - - // 清除缓存(支持清除特定配置段) - clearCache(section = null) { - if (section) { - // 清除特定配置段的缓存 - delete this.configCache[section]; - delete this.cacheTimestamps[section]; - // 同时清除全局缓存中的这一段 - if (this.configCache['__full__']) { - delete this.configCache['__full__'][section]; - } - } else { - // 清除所有缓存 - this.configCache = {}; - this.cacheTimestamps = {}; - this.configYamlCache = ''; - } - } - - // 启动状态更新定时器 - startStatusUpdateTimer() { - if (this.statusUpdateTimer) { - clearInterval(this.statusUpdateTimer); - } - this.statusUpdateTimer = setInterval(() => { - if (this.isConnected) { - this.updateConnectionStatus(); - } - }, STATUS_UPDATE_INTERVAL_MS); - } - - // 停止状态更新定时器 - stopStatusUpdateTimer() { - if (this.statusUpdateTimer) { - clearInterval(this.statusUpdateTimer); - this.statusUpdateTimer = null; - } - } - - // 加载所有数据 - 使用新的 /config 端点一次性获取所有配置 - async loadAllData(forceRefresh = false) { - try { - console.log(i18n.t('system_info.real_time_data')); - // 使用新的 /config 端点一次性获取所有配置 - // 注意:getConfig(section, forceRefresh),不传 section 表示获取全部 - const config = await this.getConfig(null, forceRefresh); - - // 获取一次usage统计数据,供渲染函数和loadUsageStats复用 - let usageData = null; - let keyStats = null; - try { - const response = await this.makeRequest('/usage'); - usageData = response?.usage || null; - if (usageData) { - // 从usage数据中提取keyStats - const sourceStats = {}; - const apis = usageData.apis || {}; - - Object.values(apis).forEach(apiEntry => { - const models = apiEntry.models || {}; - - Object.values(models).forEach(modelEntry => { - const details = modelEntry.details || []; - - details.forEach(detail => { - const source = detail.source; - if (!source) return; - - if (!sourceStats[source]) { - sourceStats[source] = { - success: 0, - failure: 0 - }; - } - - const isFailed = detail.failed === true; - if (isFailed) { - sourceStats[source].failure += 1; - } else { - sourceStats[source].success += 1; - } - }); - }); - }); - - keyStats = sourceStats; - } - } catch (error) { - console.warn('获取usage统计失败:', error); - } - - // 从配置中提取并设置各个设置项(现在传递keyStats) - await this.updateSettingsFromConfig(config, keyStats); - - // 认证文件需要单独加载,因为不在配置中 - await this.loadAuthFiles(keyStats); - - // 使用统计需要单独加载,复用已获取的usage数据 - await this.loadUsageStats(usageData); - - // 加载配置文件编辑器内容 - await this.loadConfigFileEditor(forceRefresh); - this.refreshConfigEditor(); - - console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid()); - } catch (error) { - console.error('加载配置失败:', error); - console.log('回退到逐个加载方式...'); - // 如果新方法失败,回退到原来的逐个加载方式 - await this.loadAllDataLegacy(); - } - } - - // 从配置对象更新所有设置 - async updateSettingsFromConfig(config, keyStats = null) { - // 调试设置 - 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']; - } - } - - if (config['usage-statistics-enabled'] !== undefined) { - const usageToggle = document.getElementById('usage-statistics-enabled-toggle'); - if (usageToggle) { - usageToggle.checked = config['usage-statistics-enabled']; - } - } - - // 日志记录设置 - if (config['logging-to-file'] !== undefined) { - const loggingToggle = document.getElementById('logging-to-file-toggle'); - if (loggingToggle) { - loggingToggle.checked = config['logging-to-file']; - } - // 显示或隐藏日志查看栏目 - this.toggleLogsNavItem(config['logging-to-file']); - } - if (config['request-log'] !== undefined) { - const requestLogToggle = document.getElementById('request-log-toggle'); - if (requestLogToggle) { - requestLogToggle.checked = config['request-log']; - } - } - if (config['ws-auth'] !== undefined) { - const wsAuthToggle = document.getElementById('ws-auth-toggle'); - if (wsAuthToggle) { - wsAuthToggle.checked = config['ws-auth']; - } - } - - // API 密钥 - if (config['api-keys']) { - this.renderApiKeys(config['api-keys']); - } - - // Gemini keys - await this.renderGeminiKeys(this.getGeminiKeysFromConfig(config), keyStats); - - // Codex 密钥 - await this.renderCodexKeys(Array.isArray(config['codex-api-key']) ? config['codex-api-key'] : [], keyStats); - - // Claude 密钥 - await this.renderClaudeKeys(Array.isArray(config['claude-api-key']) ? config['claude-api-key'] : [], keyStats); - - // OpenAI 兼容提供商 - await this.renderOpenAIProviders(Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : [], keyStats); - } - - // 显示添加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'); - } - } - // ===== 使用统计相关方法 ===== // 使用统计状态 @@ -1096,110 +557,6 @@ class CLIProxyManager { 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:${DEFAULT_API_PORT}`); - } - } - - 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; - } - - renderModelBadges(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; - } } Object.assign( @@ -1215,13 +572,15 @@ Object.assign( oauthModule, usageModule, settingsModule, - aiProvidersModule + aiProvidersModule, + connectionModule ); // 将工具函数绑定到原型上,供模块使用 CLIProxyManager.prototype.escapeHtml = escapeHtml; CLIProxyManager.prototype.maskApiKey = maskApiKey; CLIProxyManager.prototype.normalizeArrayResponse = normalizeArrayResponse; +CLIProxyManager.prototype.debounce = debounce; // 全局管理器实例 let manager; diff --git a/src/core/connection.js b/src/core/connection.js new file mode 100644 index 0000000..2426f14 --- /dev/null +++ b/src/core/connection.js @@ -0,0 +1,441 @@ +// 连接与配置缓存核心模块 +// 提供 API 基础地址规范化、请求封装、配置缓存以及统一数据加载能力 + +import { STATUS_UPDATE_INTERVAL_MS, DEFAULT_API_PORT } from '../utils/constants.js'; + +export const connectionModule = { + // 规范化基础地址,移除尾部斜杠与 /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; + } + }, + + // 测试连接(简化版,用于内部调用) + 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 fullTimestamp = this.cacheTimestamps && this.cacheTimestamps['__full__']; + const cacheAge = fullTimestamp + ? Math.floor((Date.now() - fullTimestamp) / 1000) + : 0; + configStatus.textContent = `${i18n.t('system_info.cache_data')} (${cacheAge}${i18n.t('system_info.seconds_ago')})`; + configStatus.style.color = '#f59e0b'; // 橙色表示缓存 + } else if (this.configCache && this.configCache['__full__']) { + 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'); + + if (this.lastEditorConnectionState !== this.isConnected) { + this.updateConfigEditorAvailability(); + } + + // 更新连接信息显示 + 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(section = null) { + if (section) { + // 检查特定配置段的缓存 + // 注意:配置值可能是 false、0、'' 等 falsy 值,不能用 ! 判断 + if (!(section in this.configCache) || !(section in this.cacheTimestamps)) { + return false; + } + return (Date.now() - this.cacheTimestamps[section]) < this.cacheExpiry; + } + // 检查全局缓存(兼容旧代码) + if (!this.configCache['__full__'] || !this.cacheTimestamps['__full__']) { + return false; + } + return (Date.now() - this.cacheTimestamps['__full__']) < this.cacheExpiry; + }, + + // 获取配置(优先使用缓存,支持按段获取) + async getConfig(section = null, forceRefresh = false) { + const now = Date.now(); + + // 如果请求特定配置段且该段缓存有效 + if (section && !forceRefresh && this.isCacheValid(section)) { + this.updateConnectionStatus(); + return this.configCache[section]; + } + + // 如果请求全部配置且全局缓存有效 + if (!section && !forceRefresh && this.isCacheValid()) { + this.updateConnectionStatus(); + return this.configCache['__full__']; + } + + try { + const config = await this.makeRequest('/config'); + + if (section) { + // 缓存特定配置段 + this.configCache[section] = config[section]; + this.cacheTimestamps[section] = now; + // 同时更新全局缓存中的这一段 + if (this.configCache['__full__']) { + this.configCache['__full__'][section] = config[section]; + } else { + // 如果全局缓存不存在,也创建它 + this.configCache['__full__'] = config; + this.cacheTimestamps['__full__'] = now; + } + this.updateConnectionStatus(); + return config[section]; + } + + // 缓存全部配置 + this.configCache['__full__'] = config; + this.cacheTimestamps['__full__'] = now; + + // 同时缓存各个配置段 + Object.keys(config).forEach(key => { + this.configCache[key] = config[key]; + this.cacheTimestamps[key] = now; + }); + + this.updateConnectionStatus(); + return config; + } catch (error) { + console.error('获取配置失败:', error); + throw error; + } + }, + + // 清除缓存(支持清除特定配置段) + clearCache(section = null) { + if (section) { + // 清除特定配置段的缓存 + delete this.configCache[section]; + delete this.cacheTimestamps[section]; + // 同时清除全局缓存中的这一段 + if (this.configCache['__full__']) { + delete this.configCache['__full__'][section]; + } + } else { + // 清除所有缓存 + this.configCache = {}; + this.cacheTimestamps = {}; + this.configYamlCache = ''; + } + }, + + // 启动状态更新定时器 + startStatusUpdateTimer() { + if (this.statusUpdateTimer) { + clearInterval(this.statusUpdateTimer); + } + this.statusUpdateTimer = setInterval(() => { + if (this.isConnected) { + this.updateConnectionStatus(); + } + }, STATUS_UPDATE_INTERVAL_MS); + }, + + // 停止状态更新定时器 + stopStatusUpdateTimer() { + if (this.statusUpdateTimer) { + clearInterval(this.statusUpdateTimer); + this.statusUpdateTimer = null; + } + }, + + // 加载所有数据 - 使用新的 /config 端点一次性获取所有配置 + async loadAllData(forceRefresh = false) { + try { + console.log(i18n.t('system_info.real_time_data')); + // 使用新的 /config 端点一次性获取所有配置 + // 注意:getConfig(section, forceRefresh),不传 section 表示获取全部 + const config = await this.getConfig(null, forceRefresh); + + // 获取一次usage统计数据,供渲染函数和loadUsageStats复用 + let usageData = null; + let keyStats = null; + try { + const response = await this.makeRequest('/usage'); + usageData = response?.usage || null; + if (usageData) { + // 从usage数据中提取keyStats + const sourceStats = {}; + const apis = usageData.apis || {}; + + Object.values(apis).forEach(apiEntry => { + const models = apiEntry.models || {}; + + Object.values(models).forEach(modelEntry => { + const details = modelEntry.details || []; + + details.forEach(detail => { + const source = detail.source; + if (!source) return; + + if (!sourceStats[source]) { + sourceStats[source] = { + success: 0, + failure: 0 + }; + } + + const isFailed = detail.failed === true; + if (isFailed) { + sourceStats[source].failure += 1; + } else { + sourceStats[source].success += 1; + } + }); + }); + }); + + keyStats = sourceStats; + } + } catch (error) { + console.warn('获取usage统计失败:', error); + } + + // 从配置中提取并设置各个设置项(现在传递keyStats) + await this.updateSettingsFromConfig(config, keyStats); + + // 认证文件需要单独加载,因为不在配置中 + await this.loadAuthFiles(keyStats); + + // 使用统计需要单独加载,复用已获取的usage数据 + await this.loadUsageStats(usageData); + + // 加载配置文件编辑器内容 + await this.loadConfigFileEditor(forceRefresh); + this.refreshConfigEditor(); + + console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid()); + } catch (error) { + console.error('加载配置失败:', error); + } + }, + + // 从配置对象更新所有设置 + async updateSettingsFromConfig(config, keyStats = null) { + // 调试设置 + 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']; + } + } + + if (config['usage-statistics-enabled'] !== undefined) { + const usageToggle = document.getElementById('usage-statistics-enabled-toggle'); + if (usageToggle) { + usageToggle.checked = config['usage-statistics-enabled']; + } + } + + // 日志记录设置 + if (config['logging-to-file'] !== undefined) { + const loggingToggle = document.getElementById('logging-to-file-toggle'); + if (loggingToggle) { + loggingToggle.checked = config['logging-to-file']; + } + // 显示或隐藏日志查看栏目 + this.toggleLogsNavItem(config['logging-to-file']); + } + if (config['request-log'] !== undefined) { + const requestLogToggle = document.getElementById('request-log-toggle'); + if (requestLogToggle) { + requestLogToggle.checked = config['request-log']; + } + } + if (config['ws-auth'] !== undefined) { + const wsAuthToggle = document.getElementById('ws-auth-toggle'); + if (wsAuthToggle) { + wsAuthToggle.checked = config['ws-auth']; + } + } + + // API 密钥 + if (config['api-keys']) { + this.renderApiKeys(config['api-keys']); + } + + // Gemini keys + await this.renderGeminiKeys(this.getGeminiKeysFromConfig(config), keyStats); + + // Codex 密钥 + await this.renderCodexKeys(Array.isArray(config['codex-api-key']) ? config['codex-api-key'] : [], keyStats); + + // Claude 密钥 + await this.renderClaudeKeys(Array.isArray(config['claude-api-key']) ? config['claude-api-key'] : [], keyStats); + + // OpenAI 兼容提供商 + await this.renderOpenAIProviders(Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : [], keyStats); + }, + + 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:${DEFAULT_API_PORT}`); + } + } +}; + diff --git a/src/modules/api-keys.js b/src/modules/api-keys.js index 69ac4fc..3b172a6 100644 --- a/src/modules/api-keys.js +++ b/src/modules/api-keys.js @@ -230,6 +230,111 @@ export const apiKeysModule = { }; this.applyHeadersToConfig(result, headers); return result; + }, + + // 显示添加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'); + } } }; -