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');
+ }
}
};
-