From a8b8bdc11c78a656cc6fdc5754a1ce90201d3ddc Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:42:16 +0800 Subject: [PATCH] refactor: centralize API client and config caching --- app.js | 26 +++++-- src/core/api-client.js | 62 ++++++++++++++++ src/core/config-service.js | 70 ++++++++++++++++++ src/core/connection.js | 143 ++++++++++--------------------------- src/core/event-bus.js | 10 +++ src/modules/login.js | 16 ++--- 6 files changed, 208 insertions(+), 119 deletions(-) create mode 100644 src/core/api-client.js create mode 100644 src/core/config-service.js create mode 100644 src/core/event-bus.js diff --git a/app.js b/app.js index 6d36e7b..6d19123 100644 --- a/app.js +++ b/app.js @@ -31,14 +31,24 @@ import { // 核心服务导入 import { createErrorHandler } from './src/core/error-handler.js'; import { connectionModule } from './src/core/connection.js'; +import { ApiClient } from './src/core/api-client.js'; +import { ConfigService } from './src/core/config-service.js'; +import { createEventBus } from './src/core/event-bus.js'; // CLI Proxy API 管理界面 JavaScript class CLIProxyManager { constructor() { - // 仅保存基础地址(不含 /v0/management),请求时自动补齐 + // 事件总线 + this.events = createEventBus(); + + // API 客户端(规范化基础地址、封装请求) + this.apiClient = new ApiClient({ + onVersionUpdate: (headers) => this.updateVersionFromHeaders(headers) + }); const detectedBase = this.detectApiBaseFromLocation(); - this.apiBase = detectedBase; - this.apiUrl = this.computeApiUrl(this.apiBase); + this.apiClient.setApiBase(detectedBase); + this.apiBase = this.apiClient.apiBase; + this.apiUrl = this.apiClient.apiUrl; this.managementKey = ''; this.isConnected = false; this.isLoggedIn = false; @@ -46,10 +56,14 @@ class CLIProxyManager { this.serverVersion = null; this.serverBuildDate = null; - // 配置缓存 - 改为分段缓存 - this.configCache = {}; // 改为对象,按配置段缓存 - this.cacheTimestamps = {}; // 每个配置段的时间戳 + // 配置缓存 - 改为分段缓存(交由 ConfigService 管理) this.cacheExpiry = CACHE_EXPIRY_MS; + this.configService = new ConfigService({ + apiClient: this.apiClient, + cacheExpiry: this.cacheExpiry + }); + this.configCache = this.configService.cache; + this.cacheTimestamps = this.configService.cacheTimestamps; // 状态更新定时器 this.statusUpdateTimer = null; diff --git a/src/core/api-client.js b/src/core/api-client.js new file mode 100644 index 0000000..20e82bc --- /dev/null +++ b/src/core/api-client.js @@ -0,0 +1,62 @@ +// API 客户端:负责规范化基础地址、构造完整 URL、发送请求并回传版本信息 +export class ApiClient { + constructor({ apiBase = '', managementKey = '', onVersionUpdate = null } = {}) { + this.apiBase = ''; + this.apiUrl = ''; + this.managementKey = managementKey || ''; + this.onVersionUpdate = onVersionUpdate; + this.setApiBase(apiBase); + } + + normalizeBase(input) { + let base = (input || '').trim(); + if (!base) return ''; + base = base.replace(/\/?v0\/management\/?$/i, ''); + base = base.replace(/\/+$/i, ''); + if (!/^https?:\/\//i.test(base)) { + base = 'http://' + base; + } + return base; + } + + computeApiUrl(base) { + const normalized = this.normalizeBase(base); + if (!normalized) return ''; + return normalized.replace(/\/$/, '') + '/v0/management'; + } + + setApiBase(newBase) { + this.apiBase = this.normalizeBase(newBase); + this.apiUrl = this.computeApiUrl(this.apiBase); + return this.apiUrl; + } + + setManagementKey(key) { + this.managementKey = key || ''; + } + + async request(endpoint, options = {}) { + const url = `${this.apiUrl}${endpoint}`; + const headers = { + 'Authorization': `Bearer ${this.managementKey}`, + 'Content-Type': 'application/json', + ...options.headers + }; + + const response = await fetch(url, { + ...options, + headers + }); + + if (typeof this.onVersionUpdate === 'function') { + this.onVersionUpdate(response.headers); + } + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `HTTP ${response.status}`); + } + + return await response.json(); + } +} diff --git a/src/core/config-service.js b/src/core/config-service.js new file mode 100644 index 0000000..b9771ba --- /dev/null +++ b/src/core/config-service.js @@ -0,0 +1,70 @@ +// 配置缓存服务:负责分段/全量读取配置与缓存控制,不涉及任何 DOM +export class ConfigService { + constructor({ apiClient, cacheExpiry }) { + this.apiClient = apiClient; + this.cacheExpiry = cacheExpiry; + this.cache = {}; + this.cacheTimestamps = {}; + } + + isCacheValid(section = null) { + if (section) { + if (!(section in this.cache) || !(section in this.cacheTimestamps)) { + return false; + } + return (Date.now() - this.cacheTimestamps[section]) < this.cacheExpiry; + } + if (!this.cache['__full__'] || !this.cacheTimestamps['__full__']) { + return false; + } + return (Date.now() - this.cacheTimestamps['__full__']) < this.cacheExpiry; + } + + clearCache(section = null) { + if (section) { + delete this.cache[section]; + delete this.cacheTimestamps[section]; + if (this.cache['__full__']) { + delete this.cache['__full__'][section]; + } + return; + } + Object.keys(this.cache).forEach(key => delete this.cache[key]); + Object.keys(this.cacheTimestamps).forEach(key => delete this.cacheTimestamps[key]); + } + + async getConfig(section = null, forceRefresh = false) { + const now = Date.now(); + + if (section && !forceRefresh && this.isCacheValid(section)) { + return this.cache[section]; + } + + if (!section && !forceRefresh && this.isCacheValid()) { + return this.cache['__full__']; + } + + const config = await this.apiClient.request('/config'); + + if (section) { + this.cache[section] = config[section]; + this.cacheTimestamps[section] = now; + if (this.cache['__full__']) { + this.cache['__full__'][section] = config[section]; + } else { + this.cache['__full__'] = config; + this.cacheTimestamps['__full__'] = now; + } + return config[section]; + } + + this.cache['__full__'] = config; + this.cacheTimestamps['__full__'] = now; + Object.keys(config).forEach(key => { + this.cache[key] = config[key]; + this.cacheTimestamps[key] = now; + }); + + return config; + } +} diff --git a/src/core/connection.js b/src/core/connection.js index 40ed899..b5e22d4 100644 --- a/src/core/connection.js +++ b/src/core/connection.js @@ -7,33 +7,31 @@ import { secureStorage } from '../utils/secure-storage.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; + return this.apiClient.normalizeBase(input); }, // 由基础地址生成完整管理 API 地址 computeApiUrl(base) { - const b = this.normalizeBase(base); - if (!b) return ''; - return b.replace(/\/$/, '') + '/v0/management'; + return this.apiClient.computeApiUrl(base); }, setApiBase(newBase) { - this.apiBase = this.normalizeBase(newBase); - this.apiUrl = this.computeApiUrl(this.apiBase); + this.apiClient.setApiBase(newBase); + this.apiBase = this.apiClient.apiBase; + this.apiUrl = this.apiClient.apiUrl; secureStorage.setItem('apiBase', this.apiBase); secureStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段 this.updateLoginConnectionInfo(); }, + setManagementKey(key, { persist = true } = {}) { + this.managementKey = key || ''; + this.apiClient.setManagementKey(this.managementKey); + if (persist) { + secureStorage.setItem('managementKey', this.managementKey); + } + }, + // 加载设置(简化版,仅加载内部状态) loadSettings() { secureStorage.migratePlaintextKeys(['apiBase', 'apiUrl', 'managementKey']); @@ -51,9 +49,7 @@ export const connectionModule = { this.setApiBase(this.detectApiBaseFromLocation()); } - if (savedKey) { - this.managementKey = savedKey; - } + this.setManagementKey(savedKey || '', { persist: false }); this.updateLoginConnectionInfo(); }, @@ -149,27 +145,8 @@ export const connectionModule = { // 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 - }); - - this.updateVersionFromHeaders(response.headers); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error || `HTTP ${response.status}`); - } - - return await response.json(); + return await this.apiClient.request(endpoint, options); } catch (error) { console.error('API请求失败:', error); throw error; @@ -236,6 +213,13 @@ export const connectionModule = { // 更新连接信息显示 this.updateConnectionInfo(); + + if (this.events && typeof this.events.emit === 'function') { + this.events.emit('connection:status-changed', { + isConnected: this.isConnected, + apiBase: this.apiBase + }); + } }, // 检查连接状态 @@ -270,66 +254,15 @@ export const connectionModule = { // 检查缓存是否有效 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; + return this.configService.isCacheValid(section); }, // 获取配置(优先使用缓存,支持按段获取) 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; - }); - + const config = await this.configService.getConfig(section, forceRefresh); + this.configCache = this.configService.cache; + this.cacheTimestamps = this.configService.cacheTimestamps; this.updateConnectionStatus(); return config; } catch (error) { @@ -340,18 +273,10 @@ export const connectionModule = { // 清除缓存(支持清除特定配置段) 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.configService.clearCache(section); + this.configCache = this.configService.cache; + this.cacheTimestamps = this.configService.cacheTimestamps; + if (!section) { this.configYamlCache = ''; } }, @@ -410,6 +335,14 @@ export const connectionModule = { await this.loadConfigFileEditor(forceRefresh); this.refreshConfigEditor(); + if (this.events && typeof this.events.emit === 'function') { + this.events.emit('data:config-loaded', { + config, + usageData, + keyStats + }); + } + console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid()); } catch (error) { console.error('加载配置失败:', error); diff --git a/src/core/event-bus.js b/src/core/event-bus.js new file mode 100644 index 0000000..ddf02cc --- /dev/null +++ b/src/core/event-bus.js @@ -0,0 +1,10 @@ +// 轻量事件总线,避免模块之间的直接耦合 +export function createEventBus() { + const target = new EventTarget(); + + const on = (type, listener) => target.addEventListener(type, listener); + const off = (type, listener) => target.removeEventListener(type, listener); + const emit = (type, detail = {}) => target.dispatchEvent(new CustomEvent(type, { detail })); + + return { on, off, emit }; +} diff --git a/src/modules/login.js b/src/modules/login.js index 7878e90..c3db73e 100644 --- a/src/modules/login.js +++ b/src/modules/login.js @@ -39,7 +39,7 @@ export const loginModule = { async attemptAutoLogin(apiBase, managementKey) { try { this.setApiBase(apiBase); - this.managementKey = managementKey; + this.setManagementKey(managementKey); const savedProxy = localStorage.getItem('proxyUrl'); if (savedProxy) { @@ -79,8 +79,7 @@ export const loginModule = { async login(apiBase, managementKey) { try { this.setApiBase(apiBase); - this.managementKey = managementKey; - secureStorage.setItem('managementKey', this.managementKey); + this.setManagementKey(managementKey); await this.testConnection(); @@ -101,6 +100,7 @@ export const loginModule = { this.clearCache(); this.stopStatusUpdateTimer(); this.resetVersionInfo(); + this.setManagementKey('', { persist: false }); localStorage.removeItem('isLoggedIn'); secureStorage.removeItem('managementKey'); @@ -132,8 +132,7 @@ export const loginModule = { } this.hideLoginError(); - this.managementKey = managementKey; - secureStorage.setItem('managementKey', this.managementKey); + this.setManagementKey(managementKey); await this.login(this.apiBase, this.managementKey); } catch (error) { @@ -210,6 +209,7 @@ export const loginModule = { if (loginKeyInput && savedKey) { loginKeyInput.value = savedKey; } + this.setManagementKey(savedKey || '', { persist: false }); this.setupLoginAutoSave(); }, @@ -220,9 +220,9 @@ export const loginModule = { const resetButton = document.getElementById('login-reset-api-base'); const saveKey = (val) => { - if (val.trim()) { - this.managementKey = val; - secureStorage.setItem('managementKey', this.managementKey); + const trimmed = val.trim(); + if (trimmed) { + this.setManagementKey(trimmed); } }; const saveKeyDebounced = this.debounce(saveKey, 500);