// 连接与配置缓存核心模块 // 提供 API 基础地址规范化、请求封装、配置缓存以及统一数据加载能力 import { STATUS_UPDATE_INTERVAL_MS, DEFAULT_API_PORT } from '../utils/constants.js'; import { secureStorage } from '../utils/secure-storage.js'; import { normalizeModelList, classifyModels } from '../utils/models.js'; const buildModelsEndpoint = (baseUrl) => { if (!baseUrl) return ''; const trimmed = String(baseUrl).trim().replace(/\/+$/g, ''); if (!trimmed) return ''; return trimmed.endsWith('/v1') ? `${trimmed}/models` : `${trimmed}/v1/models`; }; const normalizeApiKeyList = (input) => { if (!Array.isArray(input)) return []; const seen = new Set(); const keys = []; input.forEach(item => { const value = typeof item === 'string' ? item : (item && item['api-key'] ? item['api-key'] : ''); const trimmed = String(value || '').trim(); if (!trimmed || seen.has(trimmed)) { return; } seen.add(trimmed); keys.push(trimmed); }); return keys; }; export const connectionModule = { // 规范化基础地址,移除尾部斜杠与 /v0/management normalizeBase(input) { return this.apiClient.normalizeBase(input); }, // 由基础地址生成完整管理 API 地址 computeApiUrl(base) { return this.apiClient.computeApiUrl(base); }, setApiBase(newBase) { 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']); const savedBase = secureStorage.getItem('apiBase'); const savedUrl = secureStorage.getItem('apiUrl'); const savedKey = secureStorage.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()); } this.setManagementKey(savedKey || '', { persist: false }); this.updateLoginConnectionInfo(); }, // 读取并填充管理中心版本号(可能来自构建时注入或占位符) initUiVersion() { const uiVersion = this.readUiVersionFromDom(); this.uiVersion = uiVersion; this.renderVersionInfo(); }, // 从 DOM 获取版本占位符,并处理空值、引号或未替换的占位符 readUiVersionFromDom() { const el = document.getElementById('ui-version'); if (!el) return null; const raw = (el.dataset && el.dataset.uiVersion) ? el.dataset.uiVersion : el.textContent; if (typeof raw !== 'string') return null; const cleaned = raw.replace(/^"+|"+$/g, '').trim(); if (!cleaned || cleaned === '__VERSION__') { return null; } return cleaned; }, // 根据响应头更新版本与构建时间 updateVersionFromHeaders(headers) { if (!headers || typeof headers.get !== 'function') { return; } const version = headers.get('X-CPA-VERSION'); const buildDate = headers.get('X-CPA-BUILD-DATE'); let updated = false; if (version && version !== this.serverVersion) { this.serverVersion = version; updated = true; } if (buildDate && buildDate !== this.serverBuildDate) { this.serverBuildDate = buildDate; updated = true; } if (updated) { this.renderVersionInfo(); } }, renderVersionCheckStatus({ currentVersion, latestVersion, message, status } = {}) { const resolvedCurrent = (typeof currentVersion === 'undefined' || currentVersion === null) ? this.serverVersion : currentVersion; const resolvedLatest = (typeof latestVersion === 'undefined' || latestVersion === null) ? this.latestVersion : latestVersion; const resolvedMessage = (typeof message === 'undefined' || message === null) ? (this.versionCheckMessage || i18n.t('system_info.version_check_idle')) : message; const resolvedStatus = status || this.versionCheckStatus || 'muted'; this.latestVersion = resolvedLatest || null; this.versionCheckMessage = resolvedMessage; this.versionCheckStatus = resolvedStatus; const currentEl = document.getElementById('version-check-current'); if (currentEl) { currentEl.textContent = resolvedCurrent || i18n.t('system_info.version_unknown'); } const latestEl = document.getElementById('version-check-latest'); if (latestEl) { latestEl.textContent = resolvedLatest || '-'; } const resultEl = document.getElementById('version-check-result'); if (resultEl) { resultEl.textContent = resolvedMessage; resultEl.className = `version-check-result ${resolvedStatus}`.trim(); } }, resetVersionCheckStatus() { this.latestVersion = null; this.versionCheckMessage = i18n.t('system_info.version_check_idle'); this.versionCheckStatus = 'muted'; this.renderVersionCheckStatus({ currentVersion: this.serverVersion, latestVersion: this.latestVersion, message: this.versionCheckMessage, status: this.versionCheckStatus }); }, // 渲染底栏的版本与构建时间 renderVersionInfo() { const versionEl = document.getElementById('api-version'); const buildDateEl = document.getElementById('api-build-date'); const uiVersionEl = document.getElementById('ui-version'); if (versionEl) { versionEl.textContent = this.serverVersion || '-'; } if (buildDateEl) { buildDateEl.textContent = this.serverBuildDate ? this.formatBuildDate(this.serverBuildDate) : '-'; } if (uiVersionEl) { const domVersion = this.readUiVersionFromDom(); uiVersionEl.textContent = this.uiVersion || domVersion || 'v0.0.0-dev'; } this.renderVersionCheckStatus({ currentVersion: this.serverVersion, latestVersion: this.latestVersion, message: this.versionCheckMessage, status: this.versionCheckStatus }); }, // 清空版本信息(例如登出时) resetVersionInfo() { this.serverVersion = null; this.serverBuildDate = null; this.resetVersionCheckStatus(); this.renderVersionInfo(); }, // 格式化构建时间,优先使用界面语言对应的本地格式 formatBuildDate(buildDate) { if (!buildDate) return '-'; const parsed = Date.parse(buildDate); if (!Number.isNaN(parsed)) { const locale = i18n?.currentLanguage || undefined; return new Date(parsed).toLocaleString(locale); } return buildDate; }, parseVersionSegments(version) { if (!version || typeof version !== 'string') return null; const cleaned = version.trim().replace(/^v/i, ''); if (!cleaned) return null; const parts = cleaned.split(/[^0-9]+/).filter(Boolean).map(segment => { const parsed = parseInt(segment, 10); return Number.isFinite(parsed) ? parsed : 0; }); return parts.length ? parts : null; }, compareVersions(latestVersion, currentVersion) { const latestParts = this.parseVersionSegments(latestVersion); const currentParts = this.parseVersionSegments(currentVersion); if (!latestParts || !currentParts) { return null; } const length = Math.max(latestParts.length, currentParts.length); for (let i = 0; i < length; i++) { const latest = latestParts[i] || 0; const current = currentParts[i] || 0; if (latest > current) return 1; if (latest < current) return -1; } return 0; }, async checkLatestVersion() { if (!this.isConnected) { const message = i18n.t('notification.connection_required'); this.renderVersionCheckStatus({ currentVersion: this.serverVersion, latestVersion: this.latestVersion, message, status: 'warning' }); this.showNotification(message, 'error'); return; } const button = document.getElementById('version-check-btn'); const originalLabel = button ? button.innerHTML : ''; if (button) { button.disabled = true; button.innerHTML = `
${i18n.t('system_info.version_checking')}`; } this.renderVersionCheckStatus({ currentVersion: this.serverVersion, latestVersion: this.latestVersion, message: i18n.t('system_info.version_checking'), status: 'info' }); try { const data = await this.makeRequest('/latest-version'); const latestVersion = data?.['latest-version'] || data?.latest_version || ''; const latestParts = this.parseVersionSegments(latestVersion); const currentParts = this.parseVersionSegments(this.serverVersion); const comparison = (latestParts && currentParts) ? this.compareVersions(latestVersion, this.serverVersion) : null; let messageKey = 'system_info.version_check_error'; let statusClass = 'error'; if (!latestParts) { messageKey = 'system_info.version_check_error'; } else if (!currentParts) { messageKey = 'system_info.version_current_missing'; statusClass = 'warning'; } else if (comparison > 0) { messageKey = 'system_info.version_update_available'; statusClass = 'warning'; } else { messageKey = 'system_info.version_is_latest'; statusClass = 'success'; } const message = i18n.t(messageKey, latestVersion ? { version: latestVersion } : undefined); this.renderVersionCheckStatus({ currentVersion: this.serverVersion, latestVersion, message, status: statusClass }); if (latestVersion && comparison !== null) { const notifyKey = comparison > 0 ? 'system_info.version_update_available' : 'system_info.version_is_latest'; const notifyType = comparison > 0 ? 'warning' : 'success'; this.showNotification(i18n.t(notifyKey, { version: latestVersion }), notifyType); } } catch (error) { const message = `${i18n.t('system_info.version_check_error')}: ${error.message}`; this.renderVersionCheckStatus({ currentVersion: this.serverVersion, latestVersion: this.latestVersion, message, status: 'error' }); this.showNotification(message, 'error'); } finally { if (button) { button.disabled = false; button.innerHTML = originalLabel; } } }, // API 请求方法 async makeRequest(endpoint, options = {}) { try { return await this.apiClient.request(endpoint, options); } catch (error) { console.error('API请求失败:', error); throw error; } }, buildAvailableModelsEndpoint() { return buildModelsEndpoint(this.apiBase || this.apiClient?.apiBase || ''); }, setAvailableModelsStatus(message = '', type = 'info') { const statusEl = document.getElementById('available-models-status'); if (!statusEl) return; statusEl.textContent = message || ''; statusEl.className = `available-models-status ${type}`; }, renderAvailableModels(models = []) { const listEl = document.getElementById('available-models-list'); if (!listEl) return; if (!models.length) { listEl.innerHTML = `