refactor: centralize API client and config caching

This commit is contained in:
hkfires
2025-11-21 09:42:16 +08:00
parent 93eb7f4717
commit a8b8bdc11c
6 changed files with 208 additions and 119 deletions

26
app.js
View File

@@ -31,14 +31,24 @@ import {
// 核心服务导入 // 核心服务导入
import { createErrorHandler } from './src/core/error-handler.js'; import { createErrorHandler } from './src/core/error-handler.js';
import { connectionModule } from './src/core/connection.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 // CLI Proxy API 管理界面 JavaScript
class CLIProxyManager { class CLIProxyManager {
constructor() { constructor() {
// 仅保存基础地址(不含 /v0/management请求时自动补齐 // 事件总线
this.events = createEventBus();
// API 客户端(规范化基础地址、封装请求)
this.apiClient = new ApiClient({
onVersionUpdate: (headers) => this.updateVersionFromHeaders(headers)
});
const detectedBase = this.detectApiBaseFromLocation(); const detectedBase = this.detectApiBaseFromLocation();
this.apiBase = detectedBase; this.apiClient.setApiBase(detectedBase);
this.apiUrl = this.computeApiUrl(this.apiBase); this.apiBase = this.apiClient.apiBase;
this.apiUrl = this.apiClient.apiUrl;
this.managementKey = ''; this.managementKey = '';
this.isConnected = false; this.isConnected = false;
this.isLoggedIn = false; this.isLoggedIn = false;
@@ -46,10 +56,14 @@ class CLIProxyManager {
this.serverVersion = null; this.serverVersion = null;
this.serverBuildDate = null; this.serverBuildDate = null;
// 配置缓存 - 改为分段缓存 // 配置缓存 - 改为分段缓存(交由 ConfigService 管理)
this.configCache = {}; // 改为对象,按配置段缓存
this.cacheTimestamps = {}; // 每个配置段的时间戳
this.cacheExpiry = CACHE_EXPIRY_MS; 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; this.statusUpdateTimer = null;

62
src/core/api-client.js Normal file
View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -7,33 +7,31 @@ import { secureStorage } from '../utils/secure-storage.js';
export const connectionModule = { export const connectionModule = {
// 规范化基础地址,移除尾部斜杠与 /v0/management // 规范化基础地址,移除尾部斜杠与 /v0/management
normalizeBase(input) { normalizeBase(input) {
let base = (input || '').trim(); return this.apiClient.normalizeBase(input);
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 地址 // 由基础地址生成完整管理 API 地址
computeApiUrl(base) { computeApiUrl(base) {
const b = this.normalizeBase(base); return this.apiClient.computeApiUrl(base);
if (!b) return '';
return b.replace(/\/$/, '') + '/v0/management';
}, },
setApiBase(newBase) { setApiBase(newBase) {
this.apiBase = this.normalizeBase(newBase); this.apiClient.setApiBase(newBase);
this.apiUrl = this.computeApiUrl(this.apiBase); this.apiBase = this.apiClient.apiBase;
this.apiUrl = this.apiClient.apiUrl;
secureStorage.setItem('apiBase', this.apiBase); secureStorage.setItem('apiBase', this.apiBase);
secureStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段 secureStorage.setItem('apiUrl', this.apiUrl); // 兼容旧字段
this.updateLoginConnectionInfo(); this.updateLoginConnectionInfo();
}, },
setManagementKey(key, { persist = true } = {}) {
this.managementKey = key || '';
this.apiClient.setManagementKey(this.managementKey);
if (persist) {
secureStorage.setItem('managementKey', this.managementKey);
}
},
// 加载设置(简化版,仅加载内部状态) // 加载设置(简化版,仅加载内部状态)
loadSettings() { loadSettings() {
secureStorage.migratePlaintextKeys(['apiBase', 'apiUrl', 'managementKey']); secureStorage.migratePlaintextKeys(['apiBase', 'apiUrl', 'managementKey']);
@@ -51,9 +49,7 @@ export const connectionModule = {
this.setApiBase(this.detectApiBaseFromLocation()); this.setApiBase(this.detectApiBaseFromLocation());
} }
if (savedKey) { this.setManagementKey(savedKey || '', { persist: false });
this.managementKey = savedKey;
}
this.updateLoginConnectionInfo(); this.updateLoginConnectionInfo();
}, },
@@ -149,27 +145,8 @@ export const connectionModule = {
// API 请求方法 // API 请求方法
async makeRequest(endpoint, options = {}) { async makeRequest(endpoint, options = {}) {
const url = `${this.apiUrl}${endpoint}`;
const headers = {
'Authorization': `Bearer ${this.managementKey}`,
'Content-Type': 'application/json',
...options.headers
};
try { try {
const response = await fetch(url, { return await this.apiClient.request(endpoint, options);
...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();
} catch (error) { } catch (error) {
console.error('API请求失败:', error); console.error('API请求失败:', error);
throw error; throw error;
@@ -236,6 +213,13 @@ export const connectionModule = {
// 更新连接信息显示 // 更新连接信息显示
this.updateConnectionInfo(); 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) { isCacheValid(section = null) {
if (section) { return this.configService.isCacheValid(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) { 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 { try {
const config = await this.makeRequest('/config'); const config = await this.configService.getConfig(section, forceRefresh);
this.configCache = this.configService.cache;
if (section) { this.cacheTimestamps = this.configService.cacheTimestamps;
// 缓存特定配置段
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(); this.updateConnectionStatus();
return config; return config;
} catch (error) { } catch (error) {
@@ -340,18 +273,10 @@ export const connectionModule = {
// 清除缓存(支持清除特定配置段) // 清除缓存(支持清除特定配置段)
clearCache(section = null) { clearCache(section = null) {
if (section) { this.configService.clearCache(section);
// 清除特定配置段的缓存 this.configCache = this.configService.cache;
delete this.configCache[section]; this.cacheTimestamps = this.configService.cacheTimestamps;
delete this.cacheTimestamps[section]; if (!section) {
// 同时清除全局缓存中的这一段
if (this.configCache['__full__']) {
delete this.configCache['__full__'][section];
}
} else {
// 清除所有缓存
this.configCache = {};
this.cacheTimestamps = {};
this.configYamlCache = ''; this.configYamlCache = '';
} }
}, },
@@ -410,6 +335,14 @@ export const connectionModule = {
await this.loadConfigFileEditor(forceRefresh); await this.loadConfigFileEditor(forceRefresh);
this.refreshConfigEditor(); this.refreshConfigEditor();
if (this.events && typeof this.events.emit === 'function') {
this.events.emit('data:config-loaded', {
config,
usageData,
keyStats
});
}
console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid()); console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid());
} catch (error) { } catch (error) {
console.error('加载配置失败:', error); console.error('加载配置失败:', error);

10
src/core/event-bus.js Normal file
View File

@@ -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 };
}

View File

@@ -39,7 +39,7 @@ export const loginModule = {
async attemptAutoLogin(apiBase, managementKey) { async attemptAutoLogin(apiBase, managementKey) {
try { try {
this.setApiBase(apiBase); this.setApiBase(apiBase);
this.managementKey = managementKey; this.setManagementKey(managementKey);
const savedProxy = localStorage.getItem('proxyUrl'); const savedProxy = localStorage.getItem('proxyUrl');
if (savedProxy) { if (savedProxy) {
@@ -79,8 +79,7 @@ export const loginModule = {
async login(apiBase, managementKey) { async login(apiBase, managementKey) {
try { try {
this.setApiBase(apiBase); this.setApiBase(apiBase);
this.managementKey = managementKey; this.setManagementKey(managementKey);
secureStorage.setItem('managementKey', this.managementKey);
await this.testConnection(); await this.testConnection();
@@ -101,6 +100,7 @@ export const loginModule = {
this.clearCache(); this.clearCache();
this.stopStatusUpdateTimer(); this.stopStatusUpdateTimer();
this.resetVersionInfo(); this.resetVersionInfo();
this.setManagementKey('', { persist: false });
localStorage.removeItem('isLoggedIn'); localStorage.removeItem('isLoggedIn');
secureStorage.removeItem('managementKey'); secureStorage.removeItem('managementKey');
@@ -132,8 +132,7 @@ export const loginModule = {
} }
this.hideLoginError(); this.hideLoginError();
this.managementKey = managementKey; this.setManagementKey(managementKey);
secureStorage.setItem('managementKey', this.managementKey);
await this.login(this.apiBase, this.managementKey); await this.login(this.apiBase, this.managementKey);
} catch (error) { } catch (error) {
@@ -210,6 +209,7 @@ export const loginModule = {
if (loginKeyInput && savedKey) { if (loginKeyInput && savedKey) {
loginKeyInput.value = savedKey; loginKeyInput.value = savedKey;
} }
this.setManagementKey(savedKey || '', { persist: false });
this.setupLoginAutoSave(); this.setupLoginAutoSave();
}, },
@@ -220,9 +220,9 @@ export const loginModule = {
const resetButton = document.getElementById('login-reset-api-base'); const resetButton = document.getElementById('login-reset-api-base');
const saveKey = (val) => { const saveKey = (val) => {
if (val.trim()) { const trimmed = val.trim();
this.managementKey = val; if (trimmed) {
secureStorage.setItem('managementKey', this.managementKey); this.setManagementKey(trimmed);
} }
}; };
const saveKeyDebounced = this.debounce(saveKey, 500); const saveKeyDebounced = this.debounce(saveKey, 500);