Files
Cli-Proxy-API-Management-Ce…/app.js

1284 lines
47 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 模块导入
import { themeModule } from './src/modules/theme.js';
import { navigationModule } from './src/modules/navigation.js';
import { languageModule } from './src/modules/language.js';
import { loginModule } from './src/modules/login.js';
import { configEditorModule } from './src/modules/config-editor.js';
import { logsModule } from './src/modules/logs.js';
import { apiKeysModule } from './src/modules/api-keys.js';
import { authFilesModule } from './src/modules/auth-files.js';
import { oauthModule } from './src/modules/oauth.js';
import { usageModule } from './src/modules/usage.js';
import { settingsModule } from './src/modules/settings.js';
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 {
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
} from './src/utils/constants.js';
// 核心服务导入
import { createErrorHandler } from './src/core/error-handler.js';
// CLI Proxy API 管理界面 JavaScript
class CLIProxyManager {
constructor() {
// 仅保存基础地址(不含 /v0/management请求时自动补齐
const detectedBase = this.detectApiBaseFromLocation();
this.apiBase = detectedBase;
this.apiUrl = this.computeApiUrl(this.apiBase);
this.managementKey = '';
this.isConnected = false;
this.isLoggedIn = false;
// 配置缓存 - 改为分段缓存
this.configCache = {}; // 改为对象,按配置段缓存
this.cacheTimestamps = {}; // 每个配置段的时间戳
this.cacheExpiry = CACHE_EXPIRY_MS;
// 状态更新定时器
this.statusUpdateTimer = null;
// 日志自动刷新定时器
this.logsRefreshTimer = null;
// 当前展示的日志行
this.displayedLogLines = [];
this.maxDisplayLogLines = MAX_LOG_LINES;
// 日志时间戳(用于增量加载)
this.latestLogTimestamp = null;
// Auth file filter state cache
this.currentAuthFileFilter = 'all';
this.cachedAuthFiles = [];
this.authFilesPagination = {
pageSize: DEFAULT_AUTH_FILES_PAGE_SIZE,
currentPage: 1,
totalPages: 1
};
this.authFileStatsCache = {};
this.authFileSearchQuery = '';
this.authFilesPageSizeKey = STORAGE_KEY_AUTH_FILES_PAGE_SIZE;
this.loadAuthFilePreferences();
// Vertex AI credential import state
this.vertexImportState = {
file: null,
loading: false,
result: null
};
// 主题管理
this.currentTheme = 'light';
// 配置文件编辑器状态
this.configYamlCache = '';
this.isConfigEditorDirty = false;
this.configEditorElements = {
textarea: null,
editorInstance: null,
saveBtn: null,
reloadBtn: null,
statusEl: null
};
this.lastConfigFetchUrl = null;
this.lastEditorConnectionState = null;
// 初始化错误处理器
this.errorHandler = createErrorHandler((message, type) => this.showNotification(message, type));
this.init();
}
loadAuthFilePreferences() {
try {
if (typeof localStorage === 'undefined') {
return;
}
const savedPageSize = parseInt(localStorage.getItem(this.authFilesPageSizeKey), 10);
if (Number.isFinite(savedPageSize)) {
this.authFilesPagination.pageSize = this.normalizeAuthFilesPageSize(savedPageSize);
}
} catch (error) {
console.warn('Failed to restore auth file preferences:', error);
}
}
normalizeAuthFilesPageSize(value) {
const defaultSize = DEFAULT_AUTH_FILES_PAGE_SIZE;
const minSize = MIN_AUTH_FILES_PAGE_SIZE;
const maxSize = MAX_AUTH_FILES_PAGE_SIZE;
const parsed = parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
return defaultSize;
}
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();
this.bindEvents();
this.setupNavigation();
this.setupLanguageSwitcher();
this.setupThemeSwitcher();
this.setupConfigEditor();
this.updateConfigEditorAvailability();
// loadSettings 将在登录成功后调用
this.updateLoginConnectionInfo();
// 检查主机名,如果不是 localhost 或 127.0.0.1,则隐藏 OAuth 登录框
this.checkHostAndHideOAuth();
}
// 检查主机名并隐藏 OAuth 登录框
checkHostAndHideOAuth() {
const hostname = window.location.hostname;
const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
if (!isLocalhost) {
// 隐藏所有 OAuth 登录卡片
OAUTH_CARD_IDS.forEach(cardId => {
const card = document.getElementById(cardId);
if (card) {
card.style.display = 'none';
}
});
// 如果找不到具体的卡片 ID尝试通过类名查找
const oauthCardElements = document.querySelectorAll('.card');
oauthCardElements.forEach(card => {
const cardText = card.textContent || '';
if (cardText.includes('Codex OAuth') ||
cardText.includes('Anthropic OAuth') ||
cardText.includes('Gemini CLI OAuth') ||
cardText.includes('Qwen OAuth') ||
cardText.includes('iFlow OAuth')) {
card.style.display = 'none';
}
});
console.log(`当前主机名: ${hostname},已隐藏 OAuth 登录框`);
}
}
// 检查登录状态
// 处理登录表单提交
// 事件绑定
bindEvents() {
// 登录相关(安全绑定)
const loginSubmit = document.getElementById('login-submit');
const logoutBtn = document.getElementById('logout-btn');
if (loginSubmit) {
loginSubmit.addEventListener('click', () => this.handleLogin());
}
if (logoutBtn) {
logoutBtn.addEventListener('click', () => this.logout());
}
// 密钥可见性切换事件
this.setupKeyVisibilityToggle();
// 主页面元素(延迟绑定,在显示主页面时绑定)
this.bindMainPageEvents();
}
// 设置密钥可见性切换
setupKeyVisibilityToggle() {
const toggleButtons = document.querySelectorAll('.toggle-key-visibility');
toggleButtons.forEach(button => {
button.addEventListener('click', () => this.toggleLoginKeyVisibility(button));
});
}
// 绑定主页面事件
bindMainPageEvents() {
// 连接状态检查
const connectionStatus = document.getElementById('connection-status');
const refreshAll = document.getElementById('refresh-all');
if (connectionStatus) {
connectionStatus.addEventListener('click', () => this.checkConnectionStatus());
}
if (refreshAll) {
refreshAll.addEventListener('click', () => this.refreshAllData());
}
// 基础设置
const debugToggle = document.getElementById('debug-toggle');
const updateProxy = document.getElementById('update-proxy');
const clearProxy = document.getElementById('clear-proxy');
const updateRetry = document.getElementById('update-retry');
const switchProjectToggle = document.getElementById('switch-project-toggle');
const switchPreviewToggle = document.getElementById('switch-preview-model-toggle');
const usageStatisticsToggle = document.getElementById('usage-statistics-enabled-toggle');
const requestLogToggle = document.getElementById('request-log-toggle');
const wsAuthToggle = document.getElementById('ws-auth-toggle');
if (debugToggle) {
debugToggle.addEventListener('change', (e) => this.updateDebug(e.target.checked));
}
if (updateProxy) {
updateProxy.addEventListener('click', () => this.updateProxyUrl());
}
if (clearProxy) {
clearProxy.addEventListener('click', () => this.clearProxyUrl());
}
if (updateRetry) {
updateRetry.addEventListener('click', () => this.updateRequestRetry());
}
if (switchProjectToggle) {
switchProjectToggle.addEventListener('change', (e) => this.updateSwitchProject(e.target.checked));
}
if (switchPreviewToggle) {
switchPreviewToggle.addEventListener('change', (e) => this.updateSwitchPreviewModel(e.target.checked));
}
if (usageStatisticsToggle) {
usageStatisticsToggle.addEventListener('change', (e) => this.updateUsageStatisticsEnabled(e.target.checked));
}
if (requestLogToggle) {
requestLogToggle.addEventListener('change', (e) => this.updateRequestLog(e.target.checked));
}
if (wsAuthToggle) {
wsAuthToggle.addEventListener('change', (e) => this.updateWsAuth(e.target.checked));
}
// 日志记录设置
const loggingToFileToggle = document.getElementById('logging-to-file-toggle');
if (loggingToFileToggle) {
loggingToFileToggle.addEventListener('change', (e) => this.updateLoggingToFile(e.target.checked));
}
// 日志查看
const refreshLogs = document.getElementById('refresh-logs');
const downloadLogs = document.getElementById('download-logs');
const clearLogs = document.getElementById('clear-logs');
const logsAutoRefreshToggle = document.getElementById('logs-auto-refresh-toggle');
if (refreshLogs) {
refreshLogs.addEventListener('click', () => this.refreshLogs());
}
if (downloadLogs) {
downloadLogs.addEventListener('click', () => this.downloadLogs());
}
if (clearLogs) {
clearLogs.addEventListener('click', () => this.clearLogs());
}
if (logsAutoRefreshToggle) {
logsAutoRefreshToggle.addEventListener('change', (e) => this.toggleLogsAutoRefresh(e.target.checked));
}
// API 密钥管理
const addApiKey = document.getElementById('add-api-key');
const addGeminiKey = document.getElementById('add-gemini-key');
const addCodexKey = document.getElementById('add-codex-key');
const addClaudeKey = document.getElementById('add-claude-key');
const addOpenaiProvider = document.getElementById('add-openai-provider');
if (addApiKey) {
addApiKey.addEventListener('click', () => this.showAddApiKeyModal());
}
if (addGeminiKey) {
addGeminiKey.addEventListener('click', () => this.showAddGeminiKeyModal());
}
if (addCodexKey) {
addCodexKey.addEventListener('click', () => this.showAddCodexKeyModal());
}
if (addClaudeKey) {
addClaudeKey.addEventListener('click', () => this.showAddClaudeKeyModal());
}
if (addOpenaiProvider) {
addOpenaiProvider.addEventListener('click', () => this.showAddOpenAIProviderModal());
}
// 认证文件管理
const uploadAuthFile = document.getElementById('upload-auth-file');
const deleteAllAuthFiles = document.getElementById('delete-all-auth-files');
const authFileInput = document.getElementById('auth-file-input');
if (uploadAuthFile) {
uploadAuthFile.addEventListener('click', () => this.uploadAuthFile());
}
if (deleteAllAuthFiles) {
deleteAllAuthFiles.addEventListener('click', () => this.deleteAllAuthFiles());
}
if (authFileInput) {
authFileInput.addEventListener('change', (e) => this.handleFileUpload(e));
}
this.bindAuthFilesPaginationEvents();
this.bindAuthFilesSearchControl();
this.bindAuthFilesPageSizeControl();
this.syncAuthFileControls();
// Vertex AI credential import
const vertexSelectFile = document.getElementById('vertex-select-file');
const vertexFileInput = document.getElementById('vertex-file-input');
const vertexImportBtn = document.getElementById('vertex-import-btn');
if (vertexSelectFile) {
vertexSelectFile.addEventListener('click', () => this.openVertexFilePicker());
}
if (vertexFileInput) {
vertexFileInput.addEventListener('change', (e) => this.handleVertexFileSelection(e));
}
if (vertexImportBtn) {
vertexImportBtn.addEventListener('click', () => this.importVertexCredential());
}
this.updateVertexFileDisplay();
this.updateVertexImportButtonState();
this.renderVertexImportResult(this.vertexImportState.result);
// Codex OAuth
const codexOauthBtn = document.getElementById('codex-oauth-btn');
const codexOpenLink = document.getElementById('codex-open-link');
const codexCopyLink = document.getElementById('codex-copy-link');
if (codexOauthBtn) {
codexOauthBtn.addEventListener('click', () => this.startCodexOAuth());
}
if (codexOpenLink) {
codexOpenLink.addEventListener('click', () => this.openCodexLink());
}
if (codexCopyLink) {
codexCopyLink.addEventListener('click', () => this.copyCodexLink());
}
// Anthropic OAuth
const anthropicOauthBtn = document.getElementById('anthropic-oauth-btn');
const anthropicOpenLink = document.getElementById('anthropic-open-link');
const anthropicCopyLink = document.getElementById('anthropic-copy-link');
if (anthropicOauthBtn) {
anthropicOauthBtn.addEventListener('click', () => this.startAnthropicOAuth());
}
if (anthropicOpenLink) {
anthropicOpenLink.addEventListener('click', () => this.openAnthropicLink());
}
if (anthropicCopyLink) {
anthropicCopyLink.addEventListener('click', () => this.copyAnthropicLink());
}
// Gemini CLI OAuth
const geminiCliOauthBtn = document.getElementById('gemini-cli-oauth-btn');
const geminiCliOpenLink = document.getElementById('gemini-cli-open-link');
const geminiCliCopyLink = document.getElementById('gemini-cli-copy-link');
if (geminiCliOauthBtn) {
geminiCliOauthBtn.addEventListener('click', () => this.startGeminiCliOAuth());
}
if (geminiCliOpenLink) {
geminiCliOpenLink.addEventListener('click', () => this.openGeminiCliLink());
}
if (geminiCliCopyLink) {
geminiCliCopyLink.addEventListener('click', () => this.copyGeminiCliLink());
}
// Qwen OAuth
const qwenOauthBtn = document.getElementById('qwen-oauth-btn');
const qwenOpenLink = document.getElementById('qwen-open-link');
const qwenCopyLink = document.getElementById('qwen-copy-link');
if (qwenOauthBtn) {
qwenOauthBtn.addEventListener('click', () => this.startQwenOAuth());
}
if (qwenOpenLink) {
qwenOpenLink.addEventListener('click', () => this.openQwenLink());
}
if (qwenCopyLink) {
qwenCopyLink.addEventListener('click', () => this.copyQwenLink());
}
// iFlow OAuth
const iflowOauthBtn = document.getElementById('iflow-oauth-btn');
const iflowOpenLink = document.getElementById('iflow-open-link');
const iflowCopyLink = document.getElementById('iflow-copy-link');
if (iflowOauthBtn) {
iflowOauthBtn.addEventListener('click', () => this.startIflowOAuth());
}
if (iflowOpenLink) {
iflowOpenLink.addEventListener('click', () => this.openIflowLink());
}
if (iflowCopyLink) {
iflowCopyLink.addEventListener('click', () => this.copyIflowLink());
}
// 使用统计
const refreshUsageStats = document.getElementById('refresh-usage-stats');
const requestsHourBtn = document.getElementById('requests-hour-btn');
const requestsDayBtn = document.getElementById('requests-day-btn');
const tokensHourBtn = document.getElementById('tokens-hour-btn');
const tokensDayBtn = document.getElementById('tokens-day-btn');
const chartLineSelects = document.querySelectorAll('.chart-line-select');
if (refreshUsageStats) {
refreshUsageStats.addEventListener('click', () => this.loadUsageStats());
}
if (requestsHourBtn) {
requestsHourBtn.addEventListener('click', () => this.switchRequestsPeriod('hour'));
}
if (requestsDayBtn) {
requestsDayBtn.addEventListener('click', () => this.switchRequestsPeriod('day'));
}
if (tokensHourBtn) {
tokensHourBtn.addEventListener('click', () => this.switchTokensPeriod('hour'));
}
if (tokensDayBtn) {
tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day'));
}
if (chartLineSelects.length) {
chartLineSelects.forEach(select => {
select.addEventListener('change', (event) => {
const index = Number.parseInt(select.getAttribute('data-line-index'), 10);
this.handleChartLineSelectionChange(Number.isNaN(index) ? -1 : index, event.target.value);
});
});
}
// 模态框
const closeBtn = document.querySelector('.close');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.closeModal());
}
// 移动端菜单按钮
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
const sidebarOverlay = document.getElementById('sidebar-overlay');
const sidebar = document.getElementById('sidebar');
if (mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', () => this.toggleMobileSidebar());
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', () => this.closeMobileSidebar());
}
// 侧边栏收起/展开按钮(桌面端)
const sidebarToggleBtnDesktop = document.getElementById('sidebar-toggle-btn-desktop');
if (sidebarToggleBtnDesktop) {
sidebarToggleBtnDesktop.addEventListener('click', () => this.toggleSidebar());
}
// 从本地存储恢复侧边栏状态
this.restoreSidebarState();
// 监听窗口大小变化
window.addEventListener('resize', () => {
const sidebar = document.getElementById('sidebar');
const layout = document.getElementById('layout-container');
if (window.innerWidth <= 1024) {
// 移动端:移除收起状态
if (sidebar && layout) {
sidebar.classList.remove('collapsed');
layout.classList.remove('sidebar-collapsed');
}
} else {
// 桌面端:恢复保存的状态
this.restoreSidebarState();
}
});
// 点击侧边栏导航项时在移动端关闭侧边栏
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', () => {
if (window.innerWidth <= 1024) {
this.closeMobileSidebar();
}
});
});
}
// 初始化配置文件编辑器
// 规范化基础地址,移除尾部斜杠与 /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');
notification.textContent = message;
notification.className = `notification ${type}`;
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
// 密钥可见性切换
toggleKeyVisibility() {
const keyInput = document.getElementById('management-key');
const toggleButton = document.getElementById('toggle-key-visibility');
if (keyInput.type === 'password') {
keyInput.type = 'text';
toggleButton.innerHTML = '<i class="fas fa-eye-slash"></i>';
} else {
keyInput.type = 'password';
toggleButton.innerHTML = '<i class="fas fa-eye"></i>';
}
}
// 测试连接(简化版,用于内部调用)
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 = `<i class="fas fa-circle connection-indicator connected"></i> ${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 = `<i class="fas fa-circle connection-indicator disconnected"></i> ${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 = `<div class="loading"></div> ${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 = `
<h3>${i18n.t('api_keys.add_modal_title')}</h3>
<div class="form-group">
<label for="new-api-key">${i18n.t('api_keys.add_modal_key_label')}</label>
<input type="text" id="new-api-key" placeholder="${i18n.t('api_keys.add_modal_key_placeholder')}">
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.addApiKey()">${i18n.t('common.add')}</button>
</div>
`;
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 = `
<h3>${i18n.t('api_keys.edit_modal_title')}</h3>
<div class="form-group">
<label for="edit-api-key">${i18n.t('api_keys.edit_modal_key_label')}</label>
<input type="text" id="edit-api-key" value="${currentKey}">
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
<button class="btn btn-primary" onclick="manager.updateApiKey(${index})">${i18n.t('common.update')}</button>
</div>
`;
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');
}
}
// ===== 使用统计相关方法 =====
// 使用统计状态
requestsChart = null;
tokensChart = null;
currentUsageData = null;
chartLineSelections = ['none', 'none', 'none'];
chartLineSelectIds = ['chart-line-select-0', 'chart-line-select-1', 'chart-line-select-2'];
chartLineStyles = [
{ borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.15)' },
{ borderColor: '#a855f7', backgroundColor: 'rgba(168, 85, 247, 0.15)' },
{ borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.15)' }
];
showModal() {
const modal = document.getElementById('modal');
if (modal) {
modal.style.display = 'block';
}
}
// 关闭模态框
closeModal() {
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 = `
<div class="input-group">
<input type="text" class="model-name-input" placeholder="${i18n.t('common.model_name_placeholder')}" value="${model.name ? this.escapeHtml(model.name) : ''}">
<input type="text" class="model-alias-input" placeholder="${i18n.t('common.model_alias_placeholder')}" value="${model.alias ? this.escapeHtml(model.alias) : ''}">
<button type="button" class="btn btn-small btn-danger model-remove-btn"><i class="fas fa-trash"></i></button>
</div>
`;
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 `
<div class="provider-models">
${models.map(model => `
<span class="provider-model-tag">
<span class="model-name">${this.escapeHtml(model.name || '')}</span>
${model.alias ? `<span class="model-alias">${this.escapeHtml(model.alias)}</span>` : ''}
</span>
`).join('')}
</div>
`;
}
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(
CLIProxyManager.prototype,
themeModule,
navigationModule,
languageModule,
loginModule,
configEditorModule,
logsModule,
apiKeysModule,
authFilesModule,
oauthModule,
usageModule,
settingsModule,
aiProvidersModule
);
// 将工具函数绑定到原型上,供模块使用
CLIProxyManager.prototype.escapeHtml = escapeHtml;
CLIProxyManager.prototype.maskApiKey = maskApiKey;
CLIProxyManager.prototype.normalizeArrayResponse = normalizeArrayResponse;
// 全局管理器实例
let manager;
// 尝试自动加载根目录 Logo支持多种常见文件名/扩展名)
function setupSiteLogo() {
const img = document.getElementById('site-logo');
const loginImg = document.getElementById('login-logo');
if (!img && !loginImg) return;
const inlineLogo = typeof window !== 'undefined' ? window.__INLINE_LOGO__ : null;
if (inlineLogo) {
if (img) {
img.src = inlineLogo;
img.style.display = 'inline-block';
}
if (loginImg) {
loginImg.src = inlineLogo;
loginImg.style.display = 'inline-block';
}
return;
}
const candidates = [
'../logo.svg', '../logo.png', '../logo.jpg', '../logo.jpeg', '../logo.webp', '../logo.gif',
'logo.svg', 'logo.png', 'logo.jpg', 'logo.jpeg', 'logo.webp', 'logo.gif',
'/logo.svg', '/logo.png', '/logo.jpg', '/logo.jpeg', '/logo.webp', '/logo.gif'
];
let idx = 0;
const tryNext = () => {
if (idx >= candidates.length) return;
const test = new Image();
test.onload = () => {
if (img) {
img.src = test.src;
img.style.display = 'inline-block';
}
if (loginImg) {
loginImg.src = test.src;
loginImg.style.display = 'inline-block';
}
};
test.onerror = () => {
idx++;
tryNext();
};
test.src = candidates[idx];
};
tryNext();
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
// 初始化国际化
i18n.init();
setupSiteLogo();
manager = new CLIProxyManager();
});