diff --git a/app.js b/app.js
index d8be85d..06c5a29 100644
--- a/app.js
+++ b/app.js
@@ -1,2856 +1,2941 @@
-// 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 = null;
- this.cacheTimestamp = null;
- this.cacheExpiry = 30000; // 30秒缓存过期时间
-
- // 状态更新定时器
- this.statusUpdateTimer = null;
-
- // 主题管理
- this.currentTheme = 'light';
-
- this.init();
- }
-
- // 简易防抖,减少频繁写 localStorage
- debounce(fn, delay = 400) {
- let timer;
- return (...args) => {
- clearTimeout(timer);
- timer = setTimeout(() => fn.apply(this, args), delay);
- };
- }
-
- // 初始化主题
- initializeTheme() {
- // 从本地存储获取用户偏好主题
- const savedTheme = localStorage.getItem('preferredTheme');
- if (savedTheme && ['light', 'dark'].includes(savedTheme)) {
- this.currentTheme = savedTheme;
- } else {
- if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
- this.currentTheme = 'dark';
- } else {
- this.currentTheme = 'light';
- }
- }
-
- this.applyTheme(this.currentTheme);
- this.updateThemeButtons();
-
- // 监听系统主题变化
- if (window.matchMedia) {
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
- if (!localStorage.getItem('preferredTheme')) {
- this.currentTheme = e.matches ? 'dark' : 'light';
- this.applyTheme(this.currentTheme);
- this.updateThemeButtons();
- }
- });
- }
- }
-
- // 应用主题
- applyTheme(theme) {
- if (theme === 'dark') {
- document.documentElement.setAttribute('data-theme', 'dark');
- } else {
- document.documentElement.removeAttribute('data-theme');
- }
- this.currentTheme = theme;
- }
-
- // 切换主题
- toggleTheme() {
- const newTheme = this.currentTheme === 'light' ? 'dark' : 'light';
- this.applyTheme(newTheme);
- this.updateThemeButtons();
- localStorage.setItem('preferredTheme', newTheme);
- }
-
- // 更新主题按钮状态
- updateThemeButtons() {
- const loginThemeBtn = document.getElementById('theme-toggle');
- const mainThemeBtn = document.getElementById('theme-toggle-main');
-
- const updateButton = (btn) => {
- if (!btn) return;
- const icon = btn.querySelector('i');
- if (this.currentTheme === 'dark') {
- icon.className = 'fas fa-sun';
- btn.title = i18n.t('theme.switch_to_light');
- } else {
- icon.className = 'fas fa-moon';
- btn.title = i18n.t('theme.switch_to_dark');
- }
- };
-
- updateButton(loginThemeBtn);
- updateButton(mainThemeBtn);
- }
-
- init() {
- this.initializeTheme();
- this.checkLoginStatus();
- this.bindEvents();
- this.setupNavigation();
- this.setupLanguageSwitcher();
- this.setupThemeSwitcher();
- // loadSettings 将在登录成功后调用
- this.updateLoginConnectionInfo();
- }
-
- // 检查登录状态
- async checkLoginStatus() {
- // 检查是否有保存的连接信息
- const savedBase = localStorage.getItem('apiBase');
- const savedKey = localStorage.getItem('managementKey');
- const wasLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
-
- // 如果有完整的连接信息且之前已登录,尝试自动登录
- if (savedBase && savedKey && wasLoggedIn) {
- try {
- console.log('检测到本地连接数据,尝试自动登录...');
- this.showAutoLoginLoading();
- await this.attemptAutoLogin(savedBase, savedKey);
- return; // 自动登录成功,不显示登录页面
- } catch (error) {
- console.log('自动登录失败:', error.message);
- // 清除无效的登录状态
- localStorage.removeItem('isLoggedIn');
- this.hideAutoLoginLoading();
- }
- }
-
- // 如果没有连接信息或自动登录失败,显示登录页面
- this.showLoginPage();
- this.loadLoginSettings();
- }
-
- // 显示自动登录加载页面
- showAutoLoginLoading() {
- document.getElementById('auto-login-loading').style.display = 'flex';
- document.getElementById('login-page').style.display = 'none';
- document.getElementById('main-page').style.display = 'none';
- }
-
- // 隐藏自动登录加载页面
- hideAutoLoginLoading() {
- document.getElementById('auto-login-loading').style.display = 'none';
- }
-
- // 尝试自动登录
- async attemptAutoLogin(apiBase, managementKey) {
- try {
- // 设置API基础地址和密钥
- this.setApiBase(apiBase);
- this.managementKey = managementKey;
-
- // 恢复代理设置(如果有)
- const savedProxy = localStorage.getItem('proxyUrl');
- if (savedProxy) {
- // 代理设置会在后续的API请求中自动使用
- }
-
- // 测试连接
- await this.testConnection();
-
- // 自动登录成功
- this.isLoggedIn = true;
- this.hideAutoLoginLoading();
- this.showMainPage();
-
- console.log('自动登录成功');
- return true;
- } catch (error) {
- console.error('自动登录失败:', error);
- // 重置状态
- this.isLoggedIn = false;
- this.isConnected = false;
- throw error;
- }
- }
-
- // 显示登录页面
- showLoginPage() {
- document.getElementById('login-page').style.display = 'flex';
- document.getElementById('main-page').style.display = 'none';
- this.isLoggedIn = false;
- this.updateLoginConnectionInfo();
- }
-
- // 显示主页面
- showMainPage() {
- document.getElementById('login-page').style.display = 'none';
- document.getElementById('main-page').style.display = 'block';
- this.isLoggedIn = true;
- this.updateConnectionInfo();
- }
-
- // 登录验证
- async login(apiBase, managementKey) {
- try {
- // 设置API基础地址和密钥
- this.setApiBase(apiBase);
- this.managementKey = managementKey;
- localStorage.setItem('managementKey', this.managementKey);
-
- // 测试连接并加载所有数据
- await this.testConnection();
-
- // 登录成功
- this.isLoggedIn = true;
- localStorage.setItem('isLoggedIn', 'true');
-
- this.showMainPage();
- // 不需要再调用loadSettings,因为内部状态已经在上面设置了
-
- return true;
- } catch (error) {
- console.error('登录失败:', error);
- throw error;
- }
- }
-
- // 登出
- logout() {
- this.isLoggedIn = false;
- this.isConnected = false;
- this.clearCache();
- this.stopStatusUpdateTimer();
-
- // 清除本地存储
- localStorage.removeItem('isLoggedIn');
- localStorage.removeItem('managementKey');
-
- this.showLoginPage();
- }
-
- // 处理登录表单提交
- async handleLogin() {
- const apiBaseInput = document.getElementById('login-api-base');
- const managementKeyInput = document.getElementById('login-management-key');
- const managementKey = managementKeyInput ? managementKeyInput.value.trim() : '';
-
- if (!managementKey) {
- this.showLoginError(i18n.t('login.error_required'));
- return;
- }
-
- if (apiBaseInput && apiBaseInput.value.trim()) {
- this.setApiBase(apiBaseInput.value.trim());
- }
-
- const submitBtn = document.getElementById('login-submit');
- const originalText = submitBtn ? submitBtn.innerHTML : '';
-
- try {
- if (submitBtn) {
- submitBtn.innerHTML = `
${i18n.t('login.submitting')}`;
- submitBtn.disabled = true;
- }
- this.hideLoginError();
-
- this.managementKey = managementKey;
- localStorage.setItem('managementKey', this.managementKey);
-
- await this.login(this.apiBase, this.managementKey);
- } catch (error) {
- this.showLoginError(`${i18n.t('login.error_title')}: ${error.message}`);
- } finally {
- if (submitBtn) {
- submitBtn.innerHTML = originalText;
- submitBtn.disabled = false;
- }
- }
- }
-
- // 切换登录页面密钥可见性
- toggleLoginKeyVisibility(button) {
- const inputGroup = button.closest('.input-group');
- const keyInput = inputGroup.querySelector('input[type="password"], input[type="text"]');
-
- if (keyInput.type === 'password') {
- keyInput.type = 'text';
- button.innerHTML = '';
- } else {
- keyInput.type = 'password';
- button.innerHTML = '';
- }
- }
-
- // 显示登录错误
- showLoginError(message) {
- const errorDiv = document.getElementById('login-error');
- const errorMessage = document.getElementById('login-error-message');
-
- errorMessage.textContent = message;
- errorDiv.style.display = 'flex';
- }
-
- // 隐藏登录错误
- hideLoginError() {
- const errorDiv = document.getElementById('login-error');
- errorDiv.style.display = 'none';
- }
-
- // 更新连接信息显示
- updateConnectionInfo() {
- const apiUrlElement = document.getElementById('display-api-url');
- const keyElement = document.getElementById('display-management-key');
- const statusElement = document.getElementById('display-connection-status');
-
- // 显示API地址
- if (apiUrlElement) {
- apiUrlElement.textContent = this.apiBase || '-';
- }
-
- // 显示密钥(遮蔽显示)
- if (keyElement) {
- if (this.managementKey) {
- const maskedKey = this.maskApiKey(this.managementKey);
- keyElement.textContent = maskedKey;
- } else {
- keyElement.textContent = '-';
- }
- }
-
- // 显示连接状态
- if (statusElement) {
- let statusHtml = '';
- if (this.isConnected) {
- statusHtml = ` ${i18n.t('common.connected')}`;
- } else {
- statusHtml = ` ${i18n.t('common.disconnected')}`;
- }
- statusElement.innerHTML = statusHtml;
- }
- }
-
-
- // 加载登录页面设置
- loadLoginSettings() {
- const savedBase = localStorage.getItem('apiBase');
- const savedKey = localStorage.getItem('managementKey');
- const loginKeyInput = document.getElementById('login-management-key');
- const apiBaseInput = document.getElementById('login-api-base');
-
- if (savedBase) {
- this.setApiBase(savedBase);
- } else {
- this.setApiBase(this.detectApiBaseFromLocation());
- }
-
- if (apiBaseInput) {
- apiBaseInput.value = this.apiBase || '';
- }
-
- if (loginKeyInput && savedKey) {
- loginKeyInput.value = savedKey;
- }
-
- this.setupLoginAutoSave();
- }
-
- setupLoginAutoSave() {
- const loginKeyInput = document.getElementById('login-management-key');
- const apiBaseInput = document.getElementById('login-api-base');
- const resetButton = document.getElementById('login-reset-api-base');
-
- const saveKey = (val) => {
- if (val.trim()) {
- this.managementKey = val;
- localStorage.setItem('managementKey', this.managementKey);
- }
- };
- const saveKeyDebounced = this.debounce(saveKey, 500);
-
- if (loginKeyInput) {
- loginKeyInput.addEventListener('change', (e) => saveKey(e.target.value));
- loginKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value));
- }
-
- if (apiBaseInput) {
- const persistBase = (val) => {
- const normalized = this.normalizeBase(val);
- if (normalized) {
- this.setApiBase(normalized);
- }
- };
- const persistBaseDebounced = this.debounce(persistBase, 500);
-
- apiBaseInput.addEventListener('change', (e) => persistBase(e.target.value));
- apiBaseInput.addEventListener('input', (e) => persistBaseDebounced(e.target.value));
- }
-
- if (resetButton) {
- resetButton.addEventListener('click', () => {
- const detected = this.detectApiBaseFromLocation();
- this.setApiBase(detected);
- if (apiBaseInput) {
- apiBaseInput.value = detected;
- }
- });
- }
- }
-
- // 事件绑定
- 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');
-
- 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));
- }
-
- // 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());
- }
-
-
- // Gemini Web Token
- const geminiWebTokenBtn = document.getElementById('gemini-web-token-btn');
- if (geminiWebTokenBtn) {
- geminiWebTokenBtn.addEventListener('click', () => this.showGeminiWebTokenModal());
- }
-
- // 认证文件管理
- 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));
- }
-
- // 使用统计
- 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');
-
- 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'));
- }
-
- // 模态框
- const closeBtn = document.querySelector('.close');
- if (closeBtn) {
- closeBtn.addEventListener('click', () => this.closeModal());
- }
-
- window.addEventListener('click', (e) => {
- const modal = document.getElementById('modal');
- if (modal && e.target === modal) {
- 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 navItems = document.querySelectorAll('.nav-item');
- navItems.forEach(item => {
- item.addEventListener('click', () => {
- if (window.innerWidth <= 1024) {
- this.closeMobileSidebar();
- }
- });
- });
- }
-
- // 切换移动端侧边栏
- toggleMobileSidebar() {
- const sidebar = document.getElementById('sidebar');
- const overlay = document.getElementById('sidebar-overlay');
-
- if (sidebar && overlay) {
- sidebar.classList.toggle('mobile-open');
- overlay.classList.toggle('active');
- }
- }
-
- // 关闭移动端侧边栏
- closeMobileSidebar() {
- const sidebar = document.getElementById('sidebar');
- const overlay = document.getElementById('sidebar-overlay');
-
- if (sidebar && overlay) {
- sidebar.classList.remove('mobile-open');
- overlay.classList.remove('active');
- }
- }
-
- // 设置导航
- setupNavigation() {
- const navItems = document.querySelectorAll('.nav-item');
- navItems.forEach(item => {
- item.addEventListener('click', (e) => {
- e.preventDefault();
-
- // 移除所有活动状态
- navItems.forEach(nav => nav.classList.remove('active'));
- document.querySelectorAll('.content-section').forEach(section => section.classList.remove('active'));
-
- // 添加活动状态
- item.classList.add('active');
- const sectionId = item.getAttribute('data-section');
- document.getElementById(sectionId).classList.add('active');
- });
- });
- }
-
- // 设置语言切换
- setupLanguageSwitcher() {
- const loginToggle = document.getElementById('language-toggle');
- const mainToggle = document.getElementById('language-toggle-main');
-
- if (loginToggle) {
- loginToggle.addEventListener('click', () => this.toggleLanguage());
- }
- if (mainToggle) {
- mainToggle.addEventListener('click', () => this.toggleLanguage());
- }
- }
-
- // 设置主题切换
- setupThemeSwitcher() {
- const loginToggle = document.getElementById('theme-toggle');
- const mainToggle = document.getElementById('theme-toggle-main');
-
- if (loginToggle) {
- loginToggle.addEventListener('click', () => this.toggleTheme());
- }
- if (mainToggle) {
- mainToggle.addEventListener('click', () => this.toggleTheme());
- }
- }
-
- // 切换语言
- toggleLanguage() {
- const currentLang = i18n.currentLanguage;
- const newLang = currentLang === 'zh-CN' ? 'en-US' : 'zh-CN';
- i18n.setLanguage(newLang);
-
- // 更新主题按钮文本
- this.updateThemeButtons();
-
- // 更新连接状态显示
- this.updateConnectionStatus();
-
- // 重新加载所有数据以更新动态内容
- if (this.isLoggedIn && this.isConnected) {
- this.loadAllData(true);
- }
- }
-
- // 规范化基础地址,移除尾部斜杠与 /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 = '';
- } else {
- keyInput.type = 'password';
- toggleButton.innerHTML = '';
- }
- }
-
- // 测试连接(简化版,用于内部调用)
- 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');
-
- // 更新连接信息显示
- 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() {
- if (!this.configCache || !this.cacheTimestamp) {
- return false;
- }
- return (Date.now() - this.cacheTimestamp) < this.cacheExpiry;
- }
-
- // 获取配置(优先使用缓存)
- async getConfig(forceRefresh = false) {
- if (!forceRefresh && this.isCacheValid()) {
- this.updateConnectionStatus(); // 更新状态显示
- return this.configCache;
- }
-
- try {
- const config = await this.makeRequest('/config');
- this.configCache = config;
- this.cacheTimestamp = Date.now();
- this.updateConnectionStatus(); // 更新状态显示
- return config;
- } catch (error) {
- console.error('获取配置失败:', error);
- throw error;
- }
- }
-
- // 清除缓存
- clearCache() {
- this.configCache = null;
- this.cacheTimestamp = null;
- }
-
- // 启动状态更新定时器
- startStatusUpdateTimer() {
- if (this.statusUpdateTimer) {
- clearInterval(this.statusUpdateTimer);
- }
- this.statusUpdateTimer = setInterval(() => {
- if (this.isConnected) {
- this.updateConnectionStatus();
- }
- }, 1000); // 每秒更新一次
- }
-
- // 停止状态更新定时器
- stopStatusUpdateTimer() {
- if (this.statusUpdateTimer) {
- clearInterval(this.statusUpdateTimer);
- this.statusUpdateTimer = null;
- }
- }
-
- // 加载所有数据 - 使用新的 /config 端点一次性获取所有配置
- async loadAllData(forceRefresh = false) {
- try {
- console.log('使用新的 /config 端点加载所有配置...');
- // 使用新的 /config 端点一次性获取所有配置
- const config = await this.getConfig(forceRefresh);
-
- // 从配置中提取并设置各个设置项
- this.updateSettingsFromConfig(config);
-
- // 认证文件需要单独加载,因为不在配置中
- await this.loadAuthFiles();
-
- // 使用统计需要单独加载
- await this.loadUsageStats();
-
- console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid());
- } catch (error) {
- console.error('加载配置失败:', error);
- console.log('回退到逐个加载方式...');
- // 如果新方法失败,回退到原来的逐个加载方式
- await this.loadAllDataLegacy();
- }
- }
-
- // 从配置对象更新所有设置
- updateSettingsFromConfig(config) {
- // 调试设置
- 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'];
- }
- }
-
-
- // API 密钥
- if (config['api-keys']) {
- this.renderApiKeys(config['api-keys']);
- }
-
- // Gemini 密钥
- if (config['generative-language-api-key']) {
- this.renderGeminiKeys(config['generative-language-api-key']);
- }
-
- // Codex 密钥
- if (config['codex-api-key']) {
- this.renderCodexKeys(config['codex-api-key']);
- }
-
- // Claude 密钥
- if (config['claude-api-key']) {
- this.renderClaudeKeys(config['claude-api-key']);
- }
-
- // OpenAI 兼容提供商
- if (config['openai-compatibility']) {
- this.renderOpenAIProviders(config['openai-compatibility']);
- }
- }
-
- // 回退方法:原来的逐个加载方式
- async loadAllDataLegacy() {
- await Promise.all([
- this.loadDebugSettings(),
- this.loadProxySettings(),
- this.loadRetrySettings(),
- this.loadQuotaSettings(),
- this.loadApiKeys(),
- this.loadGeminiKeys(),
- this.loadCodexKeys(),
- this.loadClaudeKeys(),
- this.loadOpenAIProviders(),
- this.loadAuthFiles()
- ]);
- }
-
- // 加载调试设置
- async loadDebugSettings() {
- try {
- const config = await this.getConfig();
- if (config.debug !== undefined) {
- document.getElementById('debug-toggle').checked = config.debug;
- }
- } catch (error) {
- console.error('加载调试设置失败:', error);
- }
- }
-
- // 更新调试设置
- async updateDebug(enabled) {
- try {
- await this.makeRequest('/debug', {
- method: 'PUT',
- body: JSON.stringify({ value: enabled })
- });
- this.clearCache(); // 清除缓存
- this.showNotification(i18n.t('notification.debug_updated'), 'success');
- } catch (error) {
- this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
- // 恢复原状态
- document.getElementById('debug-toggle').checked = !enabled;
- }
- }
-
- // 加载代理设置
- async loadProxySettings() {
- try {
- const config = await this.getConfig();
- if (config['proxy-url'] !== undefined) {
- document.getElementById('proxy-url').value = config['proxy-url'] || '';
- }
- } catch (error) {
- console.error('加载代理设置失败:', error);
- }
- }
-
- // 更新代理URL
- async updateProxyUrl() {
- const proxyUrl = document.getElementById('proxy-url').value.trim();
-
- try {
- await this.makeRequest('/proxy-url', {
- method: 'PUT',
- body: JSON.stringify({ value: proxyUrl })
- });
- this.clearCache(); // 清除缓存
- this.showNotification(i18n.t('notification.proxy_updated'), 'success');
- } catch (error) {
- this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
- }
- }
-
- // 清空代理URL
- async clearProxyUrl() {
- try {
- await this.makeRequest('/proxy-url', { method: 'DELETE' });
- document.getElementById('proxy-url').value = '';
- this.clearCache(); // 清除缓存
- this.showNotification(i18n.t('notification.proxy_cleared'), 'success');
- } catch (error) {
- this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
- }
- }
-
- // 加载重试设置
- async loadRetrySettings() {
- try {
- const config = await this.getConfig();
- if (config['request-retry'] !== undefined) {
- document.getElementById('request-retry').value = config['request-retry'];
- }
- } catch (error) {
- console.error('加载重试设置失败:', error);
- }
- }
-
- // 更新请求重试
- async updateRequestRetry() {
- const retryCount = parseInt(document.getElementById('request-retry').value);
-
- try {
- await this.makeRequest('/request-retry', {
- method: 'PUT',
- body: JSON.stringify({ value: retryCount })
- });
- this.clearCache(); // 清除缓存
- this.showNotification(i18n.t('notification.retry_updated'), 'success');
- } catch (error) {
- this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
- }
- }
-
- // 加载配额设置
- async loadQuotaSettings() {
- try {
- const config = await this.getConfig();
- 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'];
- }
- }
- } catch (error) {
- console.error('加载配额设置失败:', error);
- }
- }
-
- // 更新项目切换设置
- async updateSwitchProject(enabled) {
- try {
- await this.makeRequest('/quota-exceeded/switch-project', {
- method: 'PUT',
- body: JSON.stringify({ value: enabled })
- });
- this.clearCache(); // 清除缓存
- this.showNotification(i18n.t('notification.quota_switch_project_updated'), 'success');
- } catch (error) {
- this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
- document.getElementById('switch-project-toggle').checked = !enabled;
- }
- }
-
- // 更新预览模型切换设置
- async updateSwitchPreviewModel(enabled) {
- try {
- await this.makeRequest('/quota-exceeded/switch-preview-model', {
- method: 'PUT',
- body: JSON.stringify({ value: enabled })
- });
- this.clearCache(); // 清除缓存
- this.showNotification(i18n.t('notification.quota_switch_preview_updated'), 'success');
- } catch (error) {
- this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
- document.getElementById('switch-preview-model-toggle').checked = !enabled;
- }
- }
-
-
- // 加载API密钥
- async loadApiKeys() {
- try {
- const config = await this.getConfig();
- if (config['api-keys']) {
- this.renderApiKeys(config['api-keys']);
- }
- } catch (error) {
- console.error('加载API密钥失败:', error);
- }
- }
-
- // 渲染API密钥列表
- renderApiKeys(keys) {
- const container = document.getElementById('api-keys-list');
-
- if (keys.length === 0) {
- container.innerHTML = `
-
-
-
${i18n.t('api_keys.empty_title')}
-
${i18n.t('api_keys.empty_desc')}
-
- `;
- return;
- }
-
- container.innerHTML = keys.map((key, index) => `
-
-
-
${i18n.t('api_keys.item_title')} #${index + 1}
-
${this.maskApiKey(key)}
-
-
-
-
-
-
- `).join('');
- }
-
- // 遮蔽API密钥显示
- maskApiKey(key) {
- if (key.length <= 8) return key;
- return key.substring(0, 4) + '...' + key.substring(key.length - 4);
- }
-
- // HTML 转义,防止 XSS
- escapeHtml(value) {
- if (value === null || value === undefined) return '';
- return String(value)
- .replace(/&/g, '&')
- .replace(//g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
- }
-
- // 显示添加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');
- }
- }
-
- // 加载Gemini密钥
- async loadGeminiKeys() {
- try {
- const config = await this.getConfig();
- if (config['generative-language-api-key']) {
- this.renderGeminiKeys(config['generative-language-api-key']);
- }
- } catch (error) {
- console.error('加载Gemini密钥失败:', error);
- }
- }
-
- // 渲染Gemini密钥列表
- renderGeminiKeys(keys) {
- const container = document.getElementById('gemini-keys-list');
-
- if (keys.length === 0) {
- container.innerHTML = `
-
-
-
${i18n.t('ai_providers.gemini_empty_title')}
-
${i18n.t('ai_providers.gemini_empty_desc')}
-
- `;
- return;
- }
-
- container.innerHTML = keys.map((key, index) => `
-
-
-
${i18n.t('ai_providers.gemini_item_title')} #${index + 1}
-
${this.maskApiKey(key)}
-
-
-
-
-
-
- `).join('');
- }
-
- // 显示添加Gemini密钥模态框
- showAddGeminiKeyModal() {
- const modal = document.getElementById('modal');
- const modalBody = document.getElementById('modal-body');
-
- modalBody.innerHTML = `
- 添加Gemini API密钥
-
-
-
-
-
-
-
-
- `;
-
- modal.style.display = 'block';
- }
-
- // 添加Gemini密钥
- async addGeminiKey() {
- const newKey = document.getElementById('new-gemini-key').value.trim();
-
- if (!newKey) {
- this.showNotification('请输入Gemini API密钥', 'error');
- return;
- }
-
- try {
- const data = await this.makeRequest('/generative-language-api-key');
- const currentKeys = data['generative-language-api-key'] || [];
- currentKeys.push(newKey);
-
- await this.makeRequest('/generative-language-api-key', {
- method: 'PUT',
- body: JSON.stringify(currentKeys)
- });
-
- this.clearCache(); // 清除缓存
- this.closeModal();
- this.loadGeminiKeys();
- this.showNotification('Gemini密钥添加成功', 'success');
- } catch (error) {
- this.showNotification(`添加Gemini密钥失败: ${error.message}`, 'error');
- }
- }
-
- // 编辑Gemini密钥
- editGeminiKey(index, currentKey) {
- const modal = document.getElementById('modal');
- const modalBody = document.getElementById('modal-body');
-
- modalBody.innerHTML = `
- 编辑Gemini API密钥
-
-
-
-
-
-
-
-
- `;
-
- modal.style.display = 'block';
- }
-
- // 更新Gemini密钥
- async updateGeminiKey(oldKey) {
- const newKey = document.getElementById('edit-gemini-key').value.trim();
-
- if (!newKey) {
- this.showNotification('请输入Gemini API密钥', 'error');
- return;
- }
-
- try {
- await this.makeRequest('/generative-language-api-key', {
- method: 'PATCH',
- body: JSON.stringify({ old: oldKey, new: newKey })
- });
-
- this.clearCache(); // 清除缓存
- this.closeModal();
- this.loadGeminiKeys();
- this.showNotification('Gemini密钥更新成功', 'success');
- } catch (error) {
- this.showNotification(`更新Gemini密钥失败: ${error.message}`, 'error');
- }
- }
-
- // 删除Gemini密钥
- async deleteGeminiKey(key) {
- if (!confirm(i18n.t('ai_providers.gemini_delete_confirm'))) return;
-
- try {
- await this.makeRequest(`/generative-language-api-key?value=${encodeURIComponent(key)}`, { method: 'DELETE' });
- this.clearCache(); // 清除缓存
- this.loadGeminiKeys();
- this.showNotification('Gemini密钥删除成功', 'success');
- } catch (error) {
- this.showNotification(`删除Gemini密钥失败: ${error.message}`, 'error');
- }
- }
-
- // 加载Codex密钥
- async loadCodexKeys() {
- try {
- const config = await this.getConfig();
- if (config['codex-api-key']) {
- this.renderCodexKeys(config['codex-api-key']);
- }
- } catch (error) {
- console.error('加载Codex密钥失败:', error);
- }
- }
-
- // 渲染Codex密钥列表
- renderCodexKeys(keys) {
- const container = document.getElementById('codex-keys-list');
-
- if (keys.length === 0) {
- container.innerHTML = `
-
-
-
${i18n.t('ai_providers.codex_empty_title')}
-
${i18n.t('ai_providers.codex_empty_desc')}
-
- `;
- return;
- }
-
- container.innerHTML = keys.map((config, index) => `
-
-
-
${i18n.t('ai_providers.codex_item_title')} #${index + 1}
-
${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}
- ${config['base-url'] ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}
` : ''}
- ${config['proxy-url'] ? `
${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}
` : ''}
-
-
-
-
-
-
- `).join('');
- }
-
- // 显示添加Codex密钥模态框
- showAddCodexKeyModal() {
- const modal = document.getElementById('modal');
- const modalBody = document.getElementById('modal-body');
-
- modalBody.innerHTML = `
- ${i18n.t('ai_providers.codex_add_modal_title')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- modal.style.display = 'block';
- }
-
- // 添加Codex密钥
- async addCodexKey() {
- const apiKey = document.getElementById('new-codex-key').value.trim();
- const baseUrl = document.getElementById('new-codex-url').value.trim();
- const proxyUrl = document.getElementById('new-codex-proxy').value.trim();
-
- if (!apiKey) {
- this.showNotification(i18n.t('notification.field_required'), 'error');
- return;
- }
-
- try {
- const data = await this.makeRequest('/codex-api-key');
- const currentKeys = data['codex-api-key'] || [];
-
- const newConfig = { 'api-key': apiKey };
- if (baseUrl) {
- newConfig['base-url'] = baseUrl;
- }
- if (proxyUrl) {
- newConfig['proxy-url'] = proxyUrl;
- }
-
- currentKeys.push(newConfig);
-
- await this.makeRequest('/codex-api-key', {
- method: 'PUT',
- body: JSON.stringify(currentKeys)
- });
-
- this.clearCache(); // 清除缓存
- this.closeModal();
- this.loadCodexKeys();
- this.showNotification('Codex配置添加成功', 'success');
- } catch (error) {
- this.showNotification(`添加Codex配置失败: ${error.message}`, 'error');
- }
- }
-
- // 编辑Codex密钥
- editCodexKey(index, config) {
- const modal = document.getElementById('modal');
- const modalBody = document.getElementById('modal-body');
-
- modalBody.innerHTML = `
- ${i18n.t('ai_providers.codex_edit_modal_title')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- modal.style.display = 'block';
- }
-
- // 更新Codex密钥
- async updateCodexKey(index) {
- const apiKey = document.getElementById('edit-codex-key').value.trim();
- const baseUrl = document.getElementById('edit-codex-url').value.trim();
- const proxyUrl = document.getElementById('edit-codex-proxy').value.trim();
-
- if (!apiKey) {
- this.showNotification(i18n.t('notification.field_required'), 'error');
- return;
- }
-
- try {
- const newConfig = { 'api-key': apiKey };
- if (baseUrl) {
- newConfig['base-url'] = baseUrl;
- }
- if (proxyUrl) {
- newConfig['proxy-url'] = proxyUrl;
- }
-
- await this.makeRequest('/codex-api-key', {
- method: 'PATCH',
- body: JSON.stringify({ index, value: newConfig })
- });
-
- this.clearCache(); // 清除缓存
- this.closeModal();
- this.loadCodexKeys();
- this.showNotification('Codex配置更新成功', 'success');
- } catch (error) {
- this.showNotification(`更新Codex配置失败: ${error.message}`, 'error');
- }
- }
-
- // 删除Codex密钥
- async deleteCodexKey(apiKey) {
- if (!confirm(i18n.t('ai_providers.codex_delete_confirm'))) return;
-
- try {
- await this.makeRequest(`/codex-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' });
- this.clearCache(); // 清除缓存
- this.loadCodexKeys();
- this.showNotification('Codex配置删除成功', 'success');
- } catch (error) {
- this.showNotification(`删除Codex配置失败: ${error.message}`, 'error');
- }
- }
-
- // 加载Claude密钥
- async loadClaudeKeys() {
- try {
- const config = await this.getConfig();
- if (config['claude-api-key']) {
- this.renderClaudeKeys(config['claude-api-key']);
- }
- } catch (error) {
- console.error('加载Claude密钥失败:', error);
- }
- }
-
- // 渲染Claude密钥列表
- renderClaudeKeys(keys) {
- const container = document.getElementById('claude-keys-list');
-
- if (keys.length === 0) {
- container.innerHTML = `
-
-
-
${i18n.t('ai_providers.claude_empty_title')}
-
${i18n.t('ai_providers.claude_empty_desc')}
-
- `;
- return;
- }
-
- container.innerHTML = keys.map((config, index) => `
-
-
-
${i18n.t('ai_providers.claude_item_title')} #${index + 1}
-
${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}
- ${config['base-url'] ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}
` : ''}
- ${config['proxy-url'] ? `
${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}
` : ''}
-
-
-
-
-
-
- `).join('');
- }
-
- // 显示添加Claude密钥模态框
- showAddClaudeKeyModal() {
- const modal = document.getElementById('modal');
- const modalBody = document.getElementById('modal-body');
-
- modalBody.innerHTML = `
- ${i18n.t('ai_providers.claude_add_modal_title')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- modal.style.display = 'block';
- }
-
- // 添加Claude密钥
- async addClaudeKey() {
- const apiKey = document.getElementById('new-claude-key').value.trim();
- const baseUrl = document.getElementById('new-claude-url').value.trim();
- const proxyUrl = document.getElementById('new-claude-proxy').value.trim();
-
- if (!apiKey) {
- this.showNotification(i18n.t('notification.field_required'), 'error');
- return;
- }
-
- try {
- const data = await this.makeRequest('/claude-api-key');
- const currentKeys = data['claude-api-key'] || [];
-
- const newConfig = { 'api-key': apiKey };
- if (baseUrl) {
- newConfig['base-url'] = baseUrl;
- }
- if (proxyUrl) {
- newConfig['proxy-url'] = proxyUrl;
- }
-
- currentKeys.push(newConfig);
-
- await this.makeRequest('/claude-api-key', {
- method: 'PUT',
- body: JSON.stringify(currentKeys)
- });
-
- this.clearCache(); // 清除缓存
- this.closeModal();
- this.loadClaudeKeys();
- this.showNotification('Claude配置添加成功', 'success');
- } catch (error) {
- this.showNotification(`添加Claude配置失败: ${error.message}`, 'error');
- }
- }
-
- // 编辑Claude密钥
- editClaudeKey(index, config) {
- const modal = document.getElementById('modal');
- const modalBody = document.getElementById('modal-body');
-
- modalBody.innerHTML = `
- ${i18n.t('ai_providers.claude_edit_modal_title')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- modal.style.display = 'block';
- }
-
- // 更新Claude密钥
- async updateClaudeKey(index) {
- const apiKey = document.getElementById('edit-claude-key').value.trim();
- const baseUrl = document.getElementById('edit-claude-url').value.trim();
- const proxyUrl = document.getElementById('edit-claude-proxy').value.trim();
-
- if (!apiKey) {
- this.showNotification(i18n.t('notification.field_required'), 'error');
- return;
- }
-
- try {
- const newConfig = { 'api-key': apiKey };
- if (baseUrl) {
- newConfig['base-url'] = baseUrl;
- }
- if (proxyUrl) {
- newConfig['proxy-url'] = proxyUrl;
- }
-
- await this.makeRequest('/claude-api-key', {
- method: 'PATCH',
- body: JSON.stringify({ index, value: newConfig })
- });
-
- this.clearCache(); // 清除缓存
- this.closeModal();
- this.loadClaudeKeys();
- this.showNotification('Claude配置更新成功', 'success');
- } catch (error) {
- this.showNotification(`更新Claude配置失败: ${error.message}`, 'error');
- }
- }
-
- // 删除Claude密钥
- async deleteClaudeKey(apiKey) {
- if (!confirm(i18n.t('ai_providers.claude_delete_confirm'))) return;
-
- try {
- await this.makeRequest(`/claude-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' });
- this.clearCache(); // 清除缓存
- this.loadClaudeKeys();
- this.showNotification('Claude配置删除成功', 'success');
- } catch (error) {
- this.showNotification(`删除Claude配置失败: ${error.message}`, 'error');
- }
- }
-
- // 加载OpenAI提供商
- async loadOpenAIProviders() {
- try {
- const config = await this.getConfig();
- if (config['openai-compatibility']) {
- this.renderOpenAIProviders(config['openai-compatibility']);
- }
- } catch (error) {
- console.error('加载OpenAI提供商失败:', error);
- }
- }
-
- // 渲染OpenAI提供商列表
- renderOpenAIProviders(providers) {
- const container = document.getElementById('openai-providers-list');
-
- if (providers.length === 0) {
- container.innerHTML = `
-
-
-
${i18n.t('ai_providers.openai_empty_title')}
-
${i18n.t('ai_providers.openai_empty_desc')}
-
- `;
- return;
- }
-
- container.innerHTML = providers.map((provider, index) => `
-
-
-
${this.escapeHtml(provider.name)}
-
${i18n.t('common.base_url')}: ${this.escapeHtml(provider['base-url'])}
-
${i18n.t('ai_providers.openai_keys_count')}: ${(provider['api-key-entries'] || []).length}
-
${i18n.t('ai_providers.openai_models_count')}: ${(provider.models || []).length}
- ${this.renderOpenAIModelBadges(provider.models || [])}
-
-
-
-
-
-
- `).join('');
- }
-
- // 显示添加OpenAI提供商模态框
- showAddOpenAIProviderModal() {
- const modal = document.getElementById('modal');
- const modalBody = document.getElementById('modal-body');
-
- modalBody.innerHTML = `
- ${i18n.t('ai_providers.openai_add_modal_title')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- modal.style.display = 'block';
- this.populateModelFields('new-provider-models-wrapper', []);
- }
-
- // 添加OpenAI提供商
- async addOpenAIProvider() {
- const name = document.getElementById('new-provider-name').value.trim();
- const baseUrl = document.getElementById('new-provider-url').value.trim();
- const keysText = document.getElementById('new-provider-keys').value.trim();
- const proxiesText = document.getElementById('new-provider-proxies').value.trim();
- const models = this.collectModelInputs('new-provider-models-wrapper');
-
- if (!this.validateOpenAIProviderInput(name, baseUrl, models)) {
- return;
- }
-
- try {
- const data = await this.makeRequest('/openai-compatibility');
- const currentProviders = data['openai-compatibility'] || [];
-
- const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : [];
- const proxies = proxiesText ? proxiesText.split('\n').map(p => p.trim()).filter(p => p) : [];
- const apiKeyEntries = apiKeys.map((key, idx) => ({
- 'api-key': key,
- 'proxy-url': proxies[idx] || ''
- }));
-
- const newProvider = {
- name,
- 'base-url': baseUrl,
- 'api-key-entries': apiKeyEntries,
- models
- };
-
- currentProviders.push(newProvider);
-
- await this.makeRequest('/openai-compatibility', {
- method: 'PUT',
- body: JSON.stringify(currentProviders)
- });
-
- this.clearCache(); // 清除缓存
- this.closeModal();
- this.loadOpenAIProviders();
- this.showNotification('OpenAI提供商添加成功', 'success');
- } catch (error) {
- this.showNotification(`添加OpenAI提供商失败: ${error.message}`, 'error');
- }
- }
-
- // 编辑OpenAI提供商
- editOpenAIProvider(index, provider) {
- const modal = document.getElementById('modal');
- const modalBody = document.getElementById('modal-body');
-
- const apiKeyEntries = provider['api-key-entries'] || [];
- const apiKeysText = apiKeyEntries.map(entry => entry['api-key'] || '').join('\n');
- const proxiesText = apiKeyEntries.map(entry => entry['proxy-url'] || '').join('\n');
-
- modalBody.innerHTML = `
- ${i18n.t('ai_providers.openai_edit_modal_title')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- modal.style.display = 'block';
- this.populateModelFields('edit-provider-models-wrapper', provider.models || []);
- }
-
- // 更新OpenAI提供商
- async updateOpenAIProvider(index) {
- const name = document.getElementById('edit-provider-name').value.trim();
- const baseUrl = document.getElementById('edit-provider-url').value.trim();
- const keysText = document.getElementById('edit-provider-keys').value.trim();
- const proxiesText = document.getElementById('edit-provider-proxies').value.trim();
- const models = this.collectModelInputs('edit-provider-models-wrapper');
-
- if (!this.validateOpenAIProviderInput(name, baseUrl, models)) {
- return;
- }
-
- try {
- const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : [];
- const proxies = proxiesText ? proxiesText.split('\n').map(p => p.trim()).filter(p => p) : [];
- const apiKeyEntries = apiKeys.map((key, idx) => ({
- 'api-key': key,
- 'proxy-url': proxies[idx] || ''
- }));
-
- const updatedProvider = {
- name,
- 'base-url': baseUrl,
- 'api-key-entries': apiKeyEntries,
- models
- };
-
- await this.makeRequest('/openai-compatibility', {
- method: 'PATCH',
- body: JSON.stringify({ index, value: updatedProvider })
- });
-
- this.clearCache(); // 清除缓存
- this.closeModal();
- this.loadOpenAIProviders();
- this.showNotification('OpenAI提供商更新成功', 'success');
- } catch (error) {
- this.showNotification(`更新OpenAI提供商失败: ${error.message}`, 'error');
- }
- }
-
- // 删除OpenAI提供商
- async deleteOpenAIProvider(name) {
- if (!confirm(i18n.t('ai_providers.openai_delete_confirm'))) return;
-
- try {
- await this.makeRequest(`/openai-compatibility?name=${encodeURIComponent(name)}`, { method: 'DELETE' });
- this.clearCache(); // 清除缓存
- this.loadOpenAIProviders();
- this.showNotification('OpenAI提供商删除成功', 'success');
- } catch (error) {
- this.showNotification(`删除OpenAI提供商失败: ${error.message}`, 'error');
- }
- }
-
- // 加载认证文件
- async loadAuthFiles() {
- try {
- const data = await this.makeRequest('/auth-files');
- this.renderAuthFiles(data.files || []);
- } catch (error) {
- console.error('加载认证文件失败:', error);
- }
- }
-
- // 渲染认证文件列表
- renderAuthFiles(files) {
- const container = document.getElementById('auth-files-list');
-
- if (files.length === 0) {
- container.innerHTML = `
-
-
-
${i18n.t('auth_files.empty_title')}
-
${i18n.t('auth_files.empty_desc')}
-
- `;
- return;
- }
-
- container.innerHTML = files.map(file => `
-
-
-
${file.name}
-
${i18n.t('auth_files.file_size')}: ${this.formatFileSize(file.size)}
-
${i18n.t('auth_files.file_modified')}: ${new Date(file.modtime).toLocaleString(i18n.currentLanguage === 'zh-CN' ? 'zh-CN' : 'en-US')}
-
-
-
-
-
-
- `).join('');
- }
-
- // 格式化文件大小
- formatFileSize(bytes) {
- if (bytes === 0) return '0 Bytes';
- const k = 1024;
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
- }
-
- // 上传认证文件
- uploadAuthFile() {
- document.getElementById('auth-file-input').click();
- }
-
- // 处理文件上传
- async handleFileUpload(event) {
- const file = event.target.files[0];
- if (!file) return;
-
- if (!file.name.endsWith('.json')) {
- this.showNotification(i18n.t('auth_files.upload_error_json'), 'error');
- return;
- }
-
- try {
- const formData = new FormData();
- formData.append('file', file);
-
- const response = await fetch(`${this.apiUrl}/auth-files`, {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${this.managementKey}`
- },
- body: formData
- });
-
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}));
- throw new Error(errorData.error || `HTTP ${response.status}`);
- }
-
- this.clearCache(); // 清除缓存
- this.loadAuthFiles();
- this.showNotification(i18n.t('auth_files.upload_success'), 'success');
- } catch (error) {
- this.showNotification(`文件上传失败: ${error.message}`, 'error');
- }
-
- // 清空文件输入
- event.target.value = '';
- }
-
- // 下载认证文件
- async downloadAuthFile(filename) {
- try {
- const response = await fetch(`${this.apiUrl}/auth-files/download?name=${encodeURIComponent(filename)}`, {
- headers: {
- 'Authorization': `Bearer ${this.managementKey}`
- }
- });
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
-
- const blob = await response.blob();
- const url = window.URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- a.click();
- window.URL.revokeObjectURL(url);
-
- this.showNotification(i18n.t('auth_files.download_success'), 'success');
- } catch (error) {
- this.showNotification(`文件下载失败: ${error.message}`, 'error');
- }
- }
-
- // 删除认证文件
- async deleteAuthFile(filename) {
- if (!confirm(`${i18n.t('auth_files.delete_confirm')} "${filename}" 吗?`)) return;
-
- try {
- await this.makeRequest(`/auth-files?name=${encodeURIComponent(filename)}`, { method: 'DELETE' });
- this.clearCache(); // 清除缓存
- this.loadAuthFiles();
- this.showNotification(i18n.t('auth_files.delete_success'), 'success');
- } catch (error) {
- this.showNotification(`文件删除失败: ${error.message}`, 'error');
- }
- }
-
- // 删除所有认证文件
- async deleteAllAuthFiles() {
- if (!confirm(i18n.t('auth_files.delete_all_confirm'))) return;
-
- try {
- const response = await this.makeRequest('/auth-files?all=true', { method: 'DELETE' });
- this.clearCache(); // 清除缓存
- this.loadAuthFiles();
- this.showNotification(`${i18n.t('auth_files.delete_all_success')} ${response.deleted} ${i18n.t('auth_files.files_count')}`, 'success');
- } catch (error) {
- this.showNotification(`删除文件失败: ${error.message}`, 'error');
- }
- }
-
-
-
-
-
- // 显示 Gemini Web Token 模态框
- showGeminiWebTokenModal() {
- const inlineSecure1psid = document.getElementById('secure-1psid-input');
- const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
- const inlineLabel = document.getElementById('gemini-web-label-input');
- const modalBody = document.getElementById('modal-body');
- modalBody.innerHTML = `
- ${i18n.t('auth_login.gemini_web_button')}
-
- `;
- this.showModal();
-
- const modalSecure1psid = document.getElementById('modal-secure-1psid');
- const modalSecure1psidts = document.getElementById('modal-secure-1psidts');
- const modalLabel = document.getElementById('modal-gemini-web-label');
-
- if (modalSecure1psid && inlineSecure1psid) {
- modalSecure1psid.value = inlineSecure1psid.value.trim();
- }
- if (modalSecure1psidts && inlineSecure1psidts) {
- modalSecure1psidts.value = inlineSecure1psidts.value.trim();
- }
- if (modalLabel && inlineLabel) {
- modalLabel.value = inlineLabel.value.trim();
- }
-
- if (modalSecure1psid) {
- modalSecure1psid.focus();
- }
- }
-
- // 保存 Gemini Web Token
- async saveGeminiWebToken() {
- const secure1psid = document.getElementById('modal-secure-1psid').value.trim();
- const secure1psidts = document.getElementById('modal-secure-1psidts').value.trim();
- const label = document.getElementById('modal-gemini-web-label').value.trim();
-
- if (!secure1psid || !secure1psidts) {
- this.showNotification('请填写完整的 Cookie 信息', 'error');
- return;
- }
-
- try {
- const requestBody = {
- secure_1psid: secure1psid,
- secure_1psidts: secure1psidts
- };
-
- // 如果提供了 label,则添加到请求体中
- if (label) {
- requestBody.label = label;
- }
-
- const response = await this.makeRequest('/gemini-web-token', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(requestBody)
- });
-
- this.closeModal();
- this.loadAuthFiles(); // 刷新认证文件列表
- const inlineSecure1psid = document.getElementById('secure-1psid-input');
- const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
- const inlineLabel = document.getElementById('gemini-web-label-input');
- if (inlineSecure1psid) {
- inlineSecure1psid.value = secure1psid;
- }
- if (inlineSecure1psidts) {
- inlineSecure1psidts.value = secure1psidts;
- }
- if (inlineLabel) {
- inlineLabel.value = label;
- }
- this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success');
- } catch (error) {
- this.showNotification(`保存失败: ${error.message}`, 'error');
- }
- }
-
- // ===== 使用统计相关方法 =====
-
- // 初始化图表变量
- requestsChart = null;
- tokensChart = null;
- currentUsageData = null;
-
- // 加载使用统计
- async loadUsageStats() {
- try {
- const response = await this.makeRequest('/usage');
- const usage = response?.usage || null;
- this.currentUsageData = usage;
-
- if (!usage) {
- throw new Error('usage payload missing');
- }
-
- // 更新概览卡片
- this.updateUsageOverview(usage);
-
- // 读取当前图表周期
- const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
- const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
- const requestsPeriod = requestsHourActive ? 'hour' : 'day';
- const tokensPeriod = tokensHourActive ? 'hour' : 'day';
-
- // 初始化图表(使用当前周期)
- this.initializeRequestsChart(requestsPeriod);
- this.initializeTokensChart(tokensPeriod);
-
- // 更新API详细统计表格
- this.updateApiStatsTable(usage);
-
- } catch (error) {
- console.error('加载使用统计失败:', error);
- this.currentUsageData = null;
-
- // 清空概览数据
- ['total-requests', 'success-requests', 'failed-requests', 'total-tokens'].forEach(id => {
- const el = document.getElementById(id);
- if (el) el.textContent = '-';
- });
-
- // 清空图表
- if (this.requestsChart) {
- this.requestsChart.destroy();
- this.requestsChart = null;
- }
- if (this.tokensChart) {
- this.tokensChart.destroy();
- this.tokensChart = null;
- }
-
- const tableElement = document.getElementById('api-stats-table');
- if (tableElement) {
- tableElement.innerHTML = `${i18n.t('usage_stats.loading_error')}: ${error.message}
`;
- }
- }
- }
-
- // 更新使用统计概览
- updateUsageOverview(data) {
- const safeData = data || {};
- document.getElementById('total-requests').textContent = safeData.total_requests ?? 0;
- document.getElementById('success-requests').textContent = safeData.success_count ?? 0;
- document.getElementById('failed-requests').textContent = safeData.failure_count ?? 0;
- document.getElementById('total-tokens').textContent = safeData.total_tokens ?? 0;
- }
-
- // 初始化图表
- initializeCharts() {
- const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
- const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
- this.initializeRequestsChart(requestsHourActive ? 'hour' : 'day');
- this.initializeTokensChart(tokensHourActive ? 'hour' : 'day');
- }
-
- // 初始化请求趋势图表
- initializeRequestsChart(period = 'day') {
- const ctx = document.getElementById('requests-chart');
- if (!ctx) return;
-
- // 销毁现有图表
- if (this.requestsChart) {
- this.requestsChart.destroy();
- }
-
- const data = this.getRequestsChartData(period);
-
- this.requestsChart = new Chart(ctx, {
- type: 'line',
- data: data,
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- legend: {
- display: false
- }
- },
- scales: {
- x: {
- title: {
- display: true,
- text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day')
- }
- },
- y: {
- beginAtZero: true,
- title: {
- display: true,
- text: i18n.t('usage_stats.requests_count')
- }
- }
- },
- elements: {
- line: {
- borderColor: '#3b82f6',
- backgroundColor: 'rgba(59, 130, 246, 0.1)',
- fill: true,
- tension: 0.4
- },
- point: {
- backgroundColor: '#3b82f6',
- borderColor: '#ffffff',
- borderWidth: 2,
- radius: 4
- }
- }
- }
- });
- }
-
- // 初始化Token使用趋势图表
- initializeTokensChart(period = 'day') {
- const ctx = document.getElementById('tokens-chart');
- if (!ctx) return;
-
- // 销毁现有图表
- if (this.tokensChart) {
- this.tokensChart.destroy();
- }
-
- const data = this.getTokensChartData(period);
-
- this.tokensChart = new Chart(ctx, {
- type: 'line',
- data: data,
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- legend: {
- display: false
- }
- },
- scales: {
- x: {
- title: {
- display: true,
- text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day')
- }
- },
- y: {
- beginAtZero: true,
- title: {
- display: true,
- text: i18n.t('usage_stats.tokens_count')
- }
- }
- },
- elements: {
- line: {
- borderColor: '#10b981',
- backgroundColor: 'rgba(16, 185, 129, 0.1)',
- fill: true,
- tension: 0.4
- },
- point: {
- backgroundColor: '#10b981',
- borderColor: '#ffffff',
- borderWidth: 2,
- radius: 4
- }
- }
- }
- });
- }
-
- // 获取请求图表数据
- getRequestsChartData(period) {
- if (!this.currentUsageData) {
- return { labels: [], datasets: [{ data: [] }] };
- }
-
- let dataSource, labels, values;
-
- if (period === 'hour') {
- dataSource = this.currentUsageData.requests_by_hour || {};
- labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
- values = labels.map(hour => dataSource[hour] || 0);
- } else {
- dataSource = this.currentUsageData.requests_by_day || {};
- labels = Object.keys(dataSource).sort();
- values = labels.map(day => dataSource[day] || 0);
- }
-
- return {
- labels: labels,
- datasets: [{
- data: values
- }]
- };
- }
-
- // 获取Token图表数据
- getTokensChartData(period) {
- if (!this.currentUsageData) {
- return { labels: [], datasets: [{ data: [] }] };
- }
-
- let dataSource, labels, values;
-
- if (period === 'hour') {
- dataSource = this.currentUsageData.tokens_by_hour || {};
- labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
- values = labels.map(hour => dataSource[hour] || 0);
- } else {
- dataSource = this.currentUsageData.tokens_by_day || {};
- labels = Object.keys(dataSource).sort();
- values = labels.map(day => dataSource[day] || 0);
- }
-
- return {
- labels: labels,
- datasets: [{
- data: values
- }]
- };
- }
-
- // 切换请求图表时间周期
- switchRequestsPeriod(period) {
- // 更新按钮状态
- document.getElementById('requests-hour-btn').classList.toggle('active', period === 'hour');
- document.getElementById('requests-day-btn').classList.toggle('active', period === 'day');
-
- // 更新图表数据
- if (this.requestsChart) {
- const newData = this.getRequestsChartData(period);
- this.requestsChart.data = newData;
- this.requestsChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day');
- this.requestsChart.update();
- }
- }
-
- // 切换Token图表时间周期
- switchTokensPeriod(period) {
- // 更新按钮状态
- document.getElementById('tokens-hour-btn').classList.toggle('active', period === 'hour');
- document.getElementById('tokens-day-btn').classList.toggle('active', period === 'day');
-
- // 更新图表数据
- if (this.tokensChart) {
- const newData = this.getTokensChartData(period);
- this.tokensChart.data = newData;
- this.tokensChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day');
- this.tokensChart.update();
- }
- }
-
- // 更新API详细统计表格
- updateApiStatsTable(data) {
- const container = document.getElementById('api-stats-table');
- if (!container) return;
-
- const apis = data.apis || {};
-
- if (Object.keys(apis).length === 0) {
- container.innerHTML = `${i18n.t('usage_stats.no_data')}
`;
- return;
- }
-
- let tableHtml = `
-
-
-
- | ${i18n.t('usage_stats.api_endpoint')} |
- ${i18n.t('usage_stats.requests_count')} |
- ${i18n.t('usage_stats.tokens_count')} |
- ${i18n.t('usage_stats.success_rate')} |
- ${i18n.t('usage_stats.models')} |
-
-
-
- `;
-
- Object.entries(apis).forEach(([endpoint, apiData]) => {
- const totalRequests = apiData.total_requests || 0;
- const successCount = apiData.success_count ?? null;
- const successRate = successCount !== null && totalRequests > 0
- ? Math.round((successCount / totalRequests) * 100)
- : null;
-
- // 构建模型详情
- let modelsHtml = '';
- if (apiData.models && Object.keys(apiData.models).length > 0) {
- modelsHtml = '';
- Object.entries(apiData.models).forEach(([modelName, modelData]) => {
- const modelRequests = modelData.total_requests ?? 0;
- const modelTokens = modelData.total_tokens ?? 0;
- modelsHtml += `
-
- ${modelName}
- ${modelRequests} 请求 / ${modelTokens} tokens
-
- `;
- });
- modelsHtml += '
';
- }
-
- tableHtml += `
-
- | ${endpoint} |
- ${totalRequests} |
- ${apiData.total_tokens || 0} |
- ${successRate !== null ? successRate + '%' : '-'} |
- ${modelsHtml || '-'} |
-
- `;
- });
-
- tableHtml += '
';
- container.innerHTML = tableHtml;
- }
-
- 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:8317');
- }
- }
-
- updateLoginConnectionInfo() {
- const connectionUrlElement = document.getElementById('login-connection-url');
- const customInput = document.getElementById('login-api-base');
- if (connectionUrlElement) {
- connectionUrlElement.textContent = this.apiBase || '-';
- }
- if (customInput && customInput !== document.activeElement) {
- customInput.value = this.apiBase || '';
- }
- }
-
- 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;
- }
-
- renderOpenAIModelBadges(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;
- }
-}
-
-// 全局管理器实例
-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();
-});
+// 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 = null;
+ this.cacheTimestamp = null;
+ this.cacheExpiry = 30000; // 30秒缓存过期时间
+
+ // 状态更新定时器
+ this.statusUpdateTimer = null;
+
+ // 主题管理
+ this.currentTheme = 'light';
+
+ this.init();
+ }
+
+ // 简易防抖,减少频繁写 localStorage
+ debounce(fn, delay = 400) {
+ let timer;
+ return (...args) => {
+ clearTimeout(timer);
+ timer = setTimeout(() => fn.apply(this, args), delay);
+ };
+ }
+
+ // 初始化主题
+ initializeTheme() {
+ // 从本地存储获取用户偏好主题
+ const savedTheme = localStorage.getItem('preferredTheme');
+ if (savedTheme && ['light', 'dark'].includes(savedTheme)) {
+ this.currentTheme = savedTheme;
+ } else {
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ this.currentTheme = 'dark';
+ } else {
+ this.currentTheme = 'light';
+ }
+ }
+
+ this.applyTheme(this.currentTheme);
+ this.updateThemeButtons();
+
+ // 监听系统主题变化
+ if (window.matchMedia) {
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
+ if (!localStorage.getItem('preferredTheme')) {
+ this.currentTheme = e.matches ? 'dark' : 'light';
+ this.applyTheme(this.currentTheme);
+ this.updateThemeButtons();
+ }
+ });
+ }
+ }
+
+ // 应用主题
+ applyTheme(theme) {
+ if (theme === 'dark') {
+ document.documentElement.setAttribute('data-theme', 'dark');
+ } else {
+ document.documentElement.removeAttribute('data-theme');
+ }
+ this.currentTheme = theme;
+ }
+
+ // 切换主题
+ toggleTheme() {
+ const newTheme = this.currentTheme === 'light' ? 'dark' : 'light';
+ this.applyTheme(newTheme);
+ this.updateThemeButtons();
+ localStorage.setItem('preferredTheme', newTheme);
+ }
+
+ // 更新主题按钮状态
+ updateThemeButtons() {
+ const loginThemeBtn = document.getElementById('theme-toggle');
+ const mainThemeBtn = document.getElementById('theme-toggle-main');
+
+ const updateButton = (btn) => {
+ if (!btn) return;
+ const icon = btn.querySelector('i');
+ if (this.currentTheme === 'dark') {
+ icon.className = 'fas fa-sun';
+ btn.title = i18n.t('theme.switch_to_light');
+ } else {
+ icon.className = 'fas fa-moon';
+ btn.title = i18n.t('theme.switch_to_dark');
+ }
+ };
+
+ updateButton(loginThemeBtn);
+ updateButton(mainThemeBtn);
+ }
+
+ init() {
+ this.initializeTheme();
+ this.checkLoginStatus();
+ this.bindEvents();
+ this.setupNavigation();
+ this.setupLanguageSwitcher();
+ this.setupThemeSwitcher();
+ // loadSettings 将在登录成功后调用
+ this.updateLoginConnectionInfo();
+ }
+
+ // 检查登录状态
+ async checkLoginStatus() {
+ // 检查是否有保存的连接信息
+ const savedBase = localStorage.getItem('apiBase');
+ const savedKey = localStorage.getItem('managementKey');
+ const wasLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
+
+ // 如果有完整的连接信息且之前已登录,尝试自动登录
+ if (savedBase && savedKey && wasLoggedIn) {
+ try {
+ console.log('检测到本地连接数据,尝试自动登录...');
+ this.showAutoLoginLoading();
+ await this.attemptAutoLogin(savedBase, savedKey);
+ return; // 自动登录成功,不显示登录页面
+ } catch (error) {
+ console.log('自动登录失败:', error.message);
+ // 清除无效的登录状态
+ localStorage.removeItem('isLoggedIn');
+ this.hideAutoLoginLoading();
+ }
+ }
+
+ // 如果没有连接信息或自动登录失败,显示登录页面
+ this.showLoginPage();
+ this.loadLoginSettings();
+ }
+
+ // 显示自动登录加载页面
+ showAutoLoginLoading() {
+ document.getElementById('auto-login-loading').style.display = 'flex';
+ document.getElementById('login-page').style.display = 'none';
+ document.getElementById('main-page').style.display = 'none';
+ }
+
+ // 隐藏自动登录加载页面
+ hideAutoLoginLoading() {
+ document.getElementById('auto-login-loading').style.display = 'none';
+ }
+
+ // 尝试自动登录
+ async attemptAutoLogin(apiBase, managementKey) {
+ try {
+ // 设置API基础地址和密钥
+ this.setApiBase(apiBase);
+ this.managementKey = managementKey;
+
+ // 恢复代理设置(如果有)
+ const savedProxy = localStorage.getItem('proxyUrl');
+ if (savedProxy) {
+ // 代理设置会在后续的API请求中自动使用
+ }
+
+ // 测试连接
+ await this.testConnection();
+
+ // 自动登录成功
+ this.isLoggedIn = true;
+ this.hideAutoLoginLoading();
+ this.showMainPage();
+
+ console.log('自动登录成功');
+ return true;
+ } catch (error) {
+ console.error('自动登录失败:', error);
+ // 重置状态
+ this.isLoggedIn = false;
+ this.isConnected = false;
+ throw error;
+ }
+ }
+
+ // 显示登录页面
+ showLoginPage() {
+ document.getElementById('login-page').style.display = 'flex';
+ document.getElementById('main-page').style.display = 'none';
+ this.isLoggedIn = false;
+ this.updateLoginConnectionInfo();
+ }
+
+ // 显示主页面
+ showMainPage() {
+ document.getElementById('login-page').style.display = 'none';
+ document.getElementById('main-page').style.display = 'block';
+ this.isLoggedIn = true;
+ this.updateConnectionInfo();
+ }
+
+ // 登录验证
+ async login(apiBase, managementKey) {
+ try {
+ // 设置API基础地址和密钥
+ this.setApiBase(apiBase);
+ this.managementKey = managementKey;
+ localStorage.setItem('managementKey', this.managementKey);
+
+ // 测试连接并加载所有数据
+ await this.testConnection();
+
+ // 登录成功
+ this.isLoggedIn = true;
+ localStorage.setItem('isLoggedIn', 'true');
+
+ this.showMainPage();
+ // 不需要再调用loadSettings,因为内部状态已经在上面设置了
+
+ return true;
+ } catch (error) {
+ console.error('登录失败:', error);
+ throw error;
+ }
+ }
+
+ // 登出
+ logout() {
+ this.isLoggedIn = false;
+ this.isConnected = false;
+ this.clearCache();
+ this.stopStatusUpdateTimer();
+
+ // 清除本地存储
+ localStorage.removeItem('isLoggedIn');
+ localStorage.removeItem('managementKey');
+
+ this.showLoginPage();
+ }
+
+ // 处理登录表单提交
+ async handleLogin() {
+ const apiBaseInput = document.getElementById('login-api-base');
+ const managementKeyInput = document.getElementById('login-management-key');
+ const managementKey = managementKeyInput ? managementKeyInput.value.trim() : '';
+
+ if (!managementKey) {
+ this.showLoginError(i18n.t('login.error_required'));
+ return;
+ }
+
+ if (apiBaseInput && apiBaseInput.value.trim()) {
+ this.setApiBase(apiBaseInput.value.trim());
+ }
+
+ const submitBtn = document.getElementById('login-submit');
+ const originalText = submitBtn ? submitBtn.innerHTML : '';
+
+ try {
+ if (submitBtn) {
+ submitBtn.innerHTML = ` ${i18n.t('login.submitting')}`;
+ submitBtn.disabled = true;
+ }
+ this.hideLoginError();
+
+ this.managementKey = managementKey;
+ localStorage.setItem('managementKey', this.managementKey);
+
+ await this.login(this.apiBase, this.managementKey);
+ } catch (error) {
+ this.showLoginError(`${i18n.t('login.error_title')}: ${error.message}`);
+ } finally {
+ if (submitBtn) {
+ submitBtn.innerHTML = originalText;
+ submitBtn.disabled = false;
+ }
+ }
+ }
+
+ // 切换登录页面密钥可见性
+ toggleLoginKeyVisibility(button) {
+ const inputGroup = button.closest('.input-group');
+ const keyInput = inputGroup.querySelector('input[type="password"], input[type="text"]');
+
+ if (keyInput.type === 'password') {
+ keyInput.type = 'text';
+ button.innerHTML = '';
+ } else {
+ keyInput.type = 'password';
+ button.innerHTML = '';
+ }
+ }
+
+ // 显示登录错误
+ showLoginError(message) {
+ const errorDiv = document.getElementById('login-error');
+ const errorMessage = document.getElementById('login-error-message');
+
+ errorMessage.textContent = message;
+ errorDiv.style.display = 'flex';
+ }
+
+ // 隐藏登录错误
+ hideLoginError() {
+ const errorDiv = document.getElementById('login-error');
+ errorDiv.style.display = 'none';
+ }
+
+ // 更新连接信息显示
+ updateConnectionInfo() {
+ const apiUrlElement = document.getElementById('display-api-url');
+ const keyElement = document.getElementById('display-management-key');
+ const statusElement = document.getElementById('display-connection-status');
+
+ // 显示API地址
+ if (apiUrlElement) {
+ apiUrlElement.textContent = this.apiBase || '-';
+ }
+
+ // 显示密钥(遮蔽显示)
+ if (keyElement) {
+ if (this.managementKey) {
+ const maskedKey = this.maskApiKey(this.managementKey);
+ keyElement.textContent = maskedKey;
+ } else {
+ keyElement.textContent = '-';
+ }
+ }
+
+ // 显示连接状态
+ if (statusElement) {
+ let statusHtml = '';
+ if (this.isConnected) {
+ statusHtml = ` ${i18n.t('common.connected')}`;
+ } else {
+ statusHtml = ` ${i18n.t('common.disconnected')}`;
+ }
+ statusElement.innerHTML = statusHtml;
+ }
+ }
+
+
+ // 加载登录页面设置
+ loadLoginSettings() {
+ const savedBase = localStorage.getItem('apiBase');
+ const savedKey = localStorage.getItem('managementKey');
+ const loginKeyInput = document.getElementById('login-management-key');
+ const apiBaseInput = document.getElementById('login-api-base');
+
+ if (savedBase) {
+ this.setApiBase(savedBase);
+ } else {
+ this.setApiBase(this.detectApiBaseFromLocation());
+ }
+
+ if (apiBaseInput) {
+ apiBaseInput.value = this.apiBase || '';
+ }
+
+ if (loginKeyInput && savedKey) {
+ loginKeyInput.value = savedKey;
+ }
+
+ this.setupLoginAutoSave();
+ }
+
+ setupLoginAutoSave() {
+ const loginKeyInput = document.getElementById('login-management-key');
+ const apiBaseInput = document.getElementById('login-api-base');
+ const resetButton = document.getElementById('login-reset-api-base');
+
+ const saveKey = (val) => {
+ if (val.trim()) {
+ this.managementKey = val;
+ localStorage.setItem('managementKey', this.managementKey);
+ }
+ };
+ const saveKeyDebounced = this.debounce(saveKey, 500);
+
+ if (loginKeyInput) {
+ loginKeyInput.addEventListener('change', (e) => saveKey(e.target.value));
+ loginKeyInput.addEventListener('input', (e) => saveKeyDebounced(e.target.value));
+ }
+
+ if (apiBaseInput) {
+ const persistBase = (val) => {
+ const normalized = this.normalizeBase(val);
+ if (normalized) {
+ this.setApiBase(normalized);
+ }
+ };
+ const persistBaseDebounced = this.debounce(persistBase, 500);
+
+ apiBaseInput.addEventListener('change', (e) => persistBase(e.target.value));
+ apiBaseInput.addEventListener('input', (e) => persistBaseDebounced(e.target.value));
+ }
+
+ if (resetButton) {
+ resetButton.addEventListener('click', () => {
+ const detected = this.detectApiBaseFromLocation();
+ this.setApiBase(detected);
+ if (apiBaseInput) {
+ apiBaseInput.value = detected;
+ }
+ });
+ }
+ }
+
+ // 事件绑定
+ 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');
+
+ 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));
+ }
+
+ // 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());
+ }
+
+
+ // Gemini Web Token
+ const geminiWebTokenBtn = document.getElementById('gemini-web-token-btn');
+ if (geminiWebTokenBtn) {
+ geminiWebTokenBtn.addEventListener('click', () => this.showGeminiWebTokenModal());
+ }
+
+ // 认证文件管理
+ 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));
+ }
+
+ // 使用统计
+ 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');
+
+ 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'));
+ }
+
+ // 模态框
+ const closeBtn = document.querySelector('.close');
+ if (closeBtn) {
+ closeBtn.addEventListener('click', () => this.closeModal());
+ }
+
+ window.addEventListener('click', (e) => {
+ const modal = document.getElementById('modal');
+ if (modal && e.target === modal) {
+ 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();
+ }
+ });
+ });
+ }
+
+ // 切换移动端侧边栏
+ toggleMobileSidebar() {
+ const sidebar = document.getElementById('sidebar');
+ const overlay = document.getElementById('sidebar-overlay');
+ const layout = document.getElementById('layout-container');
+ const mainWrapper = document.getElementById('main-wrapper');
+
+ if (sidebar && overlay) {
+ const isOpen = sidebar.classList.toggle('mobile-open');
+ overlay.classList.toggle('active');
+ if (layout) {
+ layout.classList.toggle('sidebar-open', isOpen);
+ }
+ if (mainWrapper) {
+ mainWrapper.classList.toggle('sidebar-open', isOpen);
+ }
+ }
+ }
+
+ // 关闭移动端侧边栏
+ closeMobileSidebar() {
+ const sidebar = document.getElementById('sidebar');
+ const overlay = document.getElementById('sidebar-overlay');
+ const layout = document.getElementById('layout-container');
+ const mainWrapper = document.getElementById('main-wrapper');
+
+ if (sidebar && overlay) {
+ sidebar.classList.remove('mobile-open');
+ overlay.classList.remove('active');
+ if (layout) {
+ layout.classList.remove('sidebar-open');
+ }
+ if (mainWrapper) {
+ mainWrapper.classList.remove('sidebar-open');
+ }
+ }
+ }
+
+ // 切换侧边栏收起/展开状态
+ toggleSidebar() {
+ const sidebar = document.getElementById('sidebar');
+ const layout = document.getElementById('layout-container');
+
+ if (sidebar && layout) {
+ const isCollapsed = sidebar.classList.toggle('collapsed');
+ layout.classList.toggle('sidebar-collapsed', isCollapsed);
+
+ // 保存状态到本地存储
+ localStorage.setItem('sidebarCollapsed', isCollapsed ? 'true' : 'false');
+
+ // 更新按钮提示文本
+ const toggleBtn = document.getElementById('sidebar-toggle-btn-desktop');
+ if (toggleBtn) {
+ toggleBtn.setAttribute('title', isCollapsed ? '展开侧边栏' : '收起侧边栏');
+ }
+ }
+ }
+
+ // 恢复侧边栏状态
+ restoreSidebarState() {
+ // 只在桌面端恢复侧栏状态
+ if (window.innerWidth > 1024) {
+ const savedState = localStorage.getItem('sidebarCollapsed');
+ if (savedState === 'true') {
+ const sidebar = document.getElementById('sidebar');
+ const layout = document.getElementById('layout-container');
+
+ if (sidebar && layout) {
+ sidebar.classList.add('collapsed');
+ layout.classList.add('sidebar-collapsed');
+
+ // 更新按钮提示文本
+ const toggleBtn = document.getElementById('sidebar-toggle-btn-desktop');
+ if (toggleBtn) {
+ toggleBtn.setAttribute('title', '展开侧边栏');
+ }
+ }
+ }
+ }
+ }
+
+ // 设置导航
+ setupNavigation() {
+ const navItems = document.querySelectorAll('.nav-item');
+ navItems.forEach(item => {
+ item.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ // 移除所有活动状态
+ navItems.forEach(nav => nav.classList.remove('active'));
+ document.querySelectorAll('.content-section').forEach(section => section.classList.remove('active'));
+
+ // 添加活动状态
+ item.classList.add('active');
+ const sectionId = item.getAttribute('data-section');
+ document.getElementById(sectionId).classList.add('active');
+ });
+ });
+ }
+
+ // 设置语言切换
+ setupLanguageSwitcher() {
+ const loginToggle = document.getElementById('language-toggle');
+ const mainToggle = document.getElementById('language-toggle-main');
+
+ if (loginToggle) {
+ loginToggle.addEventListener('click', () => this.toggleLanguage());
+ }
+ if (mainToggle) {
+ mainToggle.addEventListener('click', () => this.toggleLanguage());
+ }
+ }
+
+ // 设置主题切换
+ setupThemeSwitcher() {
+ const loginToggle = document.getElementById('theme-toggle');
+ const mainToggle = document.getElementById('theme-toggle-main');
+
+ if (loginToggle) {
+ loginToggle.addEventListener('click', () => this.toggleTheme());
+ }
+ if (mainToggle) {
+ mainToggle.addEventListener('click', () => this.toggleTheme());
+ }
+ }
+
+ // 切换语言
+ toggleLanguage() {
+ const currentLang = i18n.currentLanguage;
+ const newLang = currentLang === 'zh-CN' ? 'en-US' : 'zh-CN';
+ i18n.setLanguage(newLang);
+
+ // 更新主题按钮文本
+ this.updateThemeButtons();
+
+ // 更新连接状态显示
+ this.updateConnectionStatus();
+
+ // 重新加载所有数据以更新动态内容
+ if (this.isLoggedIn && this.isConnected) {
+ this.loadAllData(true);
+ }
+ }
+
+ // 规范化基础地址,移除尾部斜杠与 /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 = '';
+ } else {
+ keyInput.type = 'password';
+ toggleButton.innerHTML = '';
+ }
+ }
+
+ // 测试连接(简化版,用于内部调用)
+ 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');
+
+ // 更新连接信息显示
+ 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() {
+ if (!this.configCache || !this.cacheTimestamp) {
+ return false;
+ }
+ return (Date.now() - this.cacheTimestamp) < this.cacheExpiry;
+ }
+
+ // 获取配置(优先使用缓存)
+ async getConfig(forceRefresh = false) {
+ if (!forceRefresh && this.isCacheValid()) {
+ this.updateConnectionStatus(); // 更新状态显示
+ return this.configCache;
+ }
+
+ try {
+ const config = await this.makeRequest('/config');
+ this.configCache = config;
+ this.cacheTimestamp = Date.now();
+ this.updateConnectionStatus(); // 更新状态显示
+ return config;
+ } catch (error) {
+ console.error('获取配置失败:', error);
+ throw error;
+ }
+ }
+
+ // 清除缓存
+ clearCache() {
+ this.configCache = null;
+ this.cacheTimestamp = null;
+ }
+
+ // 启动状态更新定时器
+ startStatusUpdateTimer() {
+ if (this.statusUpdateTimer) {
+ clearInterval(this.statusUpdateTimer);
+ }
+ this.statusUpdateTimer = setInterval(() => {
+ if (this.isConnected) {
+ this.updateConnectionStatus();
+ }
+ }, 1000); // 每秒更新一次
+ }
+
+ // 停止状态更新定时器
+ stopStatusUpdateTimer() {
+ if (this.statusUpdateTimer) {
+ clearInterval(this.statusUpdateTimer);
+ this.statusUpdateTimer = null;
+ }
+ }
+
+ // 加载所有数据 - 使用新的 /config 端点一次性获取所有配置
+ async loadAllData(forceRefresh = false) {
+ try {
+ console.log('使用新的 /config 端点加载所有配置...');
+ // 使用新的 /config 端点一次性获取所有配置
+ const config = await this.getConfig(forceRefresh);
+
+ // 从配置中提取并设置各个设置项
+ this.updateSettingsFromConfig(config);
+
+ // 认证文件需要单独加载,因为不在配置中
+ await this.loadAuthFiles();
+
+ // 使用统计需要单独加载
+ await this.loadUsageStats();
+
+ console.log('配置加载完成,使用缓存:', !forceRefresh && this.isCacheValid());
+ } catch (error) {
+ console.error('加载配置失败:', error);
+ console.log('回退到逐个加载方式...');
+ // 如果新方法失败,回退到原来的逐个加载方式
+ await this.loadAllDataLegacy();
+ }
+ }
+
+ // 从配置对象更新所有设置
+ updateSettingsFromConfig(config) {
+ // 调试设置
+ 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'];
+ }
+ }
+
+
+ // API 密钥
+ if (config['api-keys']) {
+ this.renderApiKeys(config['api-keys']);
+ }
+
+ // Gemini 密钥
+ if (config['generative-language-api-key']) {
+ this.renderGeminiKeys(config['generative-language-api-key']);
+ }
+
+ // Codex 密钥
+ if (config['codex-api-key']) {
+ this.renderCodexKeys(config['codex-api-key']);
+ }
+
+ // Claude 密钥
+ if (config['claude-api-key']) {
+ this.renderClaudeKeys(config['claude-api-key']);
+ }
+
+ // OpenAI 兼容提供商
+ if (config['openai-compatibility']) {
+ this.renderOpenAIProviders(config['openai-compatibility']);
+ }
+ }
+
+ // 回退方法:原来的逐个加载方式
+ async loadAllDataLegacy() {
+ await Promise.all([
+ this.loadDebugSettings(),
+ this.loadProxySettings(),
+ this.loadRetrySettings(),
+ this.loadQuotaSettings(),
+ this.loadApiKeys(),
+ this.loadGeminiKeys(),
+ this.loadCodexKeys(),
+ this.loadClaudeKeys(),
+ this.loadOpenAIProviders(),
+ this.loadAuthFiles()
+ ]);
+ }
+
+ // 加载调试设置
+ async loadDebugSettings() {
+ try {
+ const config = await this.getConfig();
+ if (config.debug !== undefined) {
+ document.getElementById('debug-toggle').checked = config.debug;
+ }
+ } catch (error) {
+ console.error('加载调试设置失败:', error);
+ }
+ }
+
+ // 更新调试设置
+ async updateDebug(enabled) {
+ try {
+ await this.makeRequest('/debug', {
+ method: 'PUT',
+ body: JSON.stringify({ value: enabled })
+ });
+ this.clearCache(); // 清除缓存
+ this.showNotification(i18n.t('notification.debug_updated'), 'success');
+ } catch (error) {
+ this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
+ // 恢复原状态
+ document.getElementById('debug-toggle').checked = !enabled;
+ }
+ }
+
+ // 加载代理设置
+ async loadProxySettings() {
+ try {
+ const config = await this.getConfig();
+ if (config['proxy-url'] !== undefined) {
+ document.getElementById('proxy-url').value = config['proxy-url'] || '';
+ }
+ } catch (error) {
+ console.error('加载代理设置失败:', error);
+ }
+ }
+
+ // 更新代理URL
+ async updateProxyUrl() {
+ const proxyUrl = document.getElementById('proxy-url').value.trim();
+
+ try {
+ await this.makeRequest('/proxy-url', {
+ method: 'PUT',
+ body: JSON.stringify({ value: proxyUrl })
+ });
+ this.clearCache(); // 清除缓存
+ this.showNotification(i18n.t('notification.proxy_updated'), 'success');
+ } catch (error) {
+ this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
+ }
+ }
+
+ // 清空代理URL
+ async clearProxyUrl() {
+ try {
+ await this.makeRequest('/proxy-url', { method: 'DELETE' });
+ document.getElementById('proxy-url').value = '';
+ this.clearCache(); // 清除缓存
+ this.showNotification(i18n.t('notification.proxy_cleared'), 'success');
+ } catch (error) {
+ this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载重试设置
+ async loadRetrySettings() {
+ try {
+ const config = await this.getConfig();
+ if (config['request-retry'] !== undefined) {
+ document.getElementById('request-retry').value = config['request-retry'];
+ }
+ } catch (error) {
+ console.error('加载重试设置失败:', error);
+ }
+ }
+
+ // 更新请求重试
+ async updateRequestRetry() {
+ const retryCount = parseInt(document.getElementById('request-retry').value);
+
+ try {
+ await this.makeRequest('/request-retry', {
+ method: 'PUT',
+ body: JSON.stringify({ value: retryCount })
+ });
+ this.clearCache(); // 清除缓存
+ this.showNotification(i18n.t('notification.retry_updated'), 'success');
+ } catch (error) {
+ this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载配额设置
+ async loadQuotaSettings() {
+ try {
+ const config = await this.getConfig();
+ 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'];
+ }
+ }
+ } catch (error) {
+ console.error('加载配额设置失败:', error);
+ }
+ }
+
+ // 更新项目切换设置
+ async updateSwitchProject(enabled) {
+ try {
+ await this.makeRequest('/quota-exceeded/switch-project', {
+ method: 'PUT',
+ body: JSON.stringify({ value: enabled })
+ });
+ this.clearCache(); // 清除缓存
+ this.showNotification(i18n.t('notification.quota_switch_project_updated'), 'success');
+ } catch (error) {
+ this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
+ document.getElementById('switch-project-toggle').checked = !enabled;
+ }
+ }
+
+ // 更新预览模型切换设置
+ async updateSwitchPreviewModel(enabled) {
+ try {
+ await this.makeRequest('/quota-exceeded/switch-preview-model', {
+ method: 'PUT',
+ body: JSON.stringify({ value: enabled })
+ });
+ this.clearCache(); // 清除缓存
+ this.showNotification(i18n.t('notification.quota_switch_preview_updated'), 'success');
+ } catch (error) {
+ this.showNotification(`${i18n.t('notification.update_failed')}: ${error.message}`, 'error');
+ document.getElementById('switch-preview-model-toggle').checked = !enabled;
+ }
+ }
+
+
+ // 加载API密钥
+ async loadApiKeys() {
+ try {
+ const config = await this.getConfig();
+ if (config['api-keys']) {
+ this.renderApiKeys(config['api-keys']);
+ }
+ } catch (error) {
+ console.error('加载API密钥失败:', error);
+ }
+ }
+
+ // 渲染API密钥列表
+ renderApiKeys(keys) {
+ const container = document.getElementById('api-keys-list');
+
+ if (keys.length === 0) {
+ container.innerHTML = `
+
+
+
${i18n.t('api_keys.empty_title')}
+
${i18n.t('api_keys.empty_desc')}
+
+ `;
+ return;
+ }
+
+ container.innerHTML = keys.map((key, index) => `
+
+
+
${i18n.t('api_keys.item_title')} #${index + 1}
+
${this.maskApiKey(key)}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // 遮蔽API密钥显示
+ maskApiKey(key) {
+ if (key.length <= 8) return key;
+ return key.substring(0, 4) + '...' + key.substring(key.length - 4);
+ }
+
+ // HTML 转义,防止 XSS
+ escapeHtml(value) {
+ if (value === null || value === undefined) return '';
+ return String(value)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+ // 显示添加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');
+ }
+ }
+
+ // 加载Gemini密钥
+ async loadGeminiKeys() {
+ try {
+ const config = await this.getConfig();
+ if (config['generative-language-api-key']) {
+ this.renderGeminiKeys(config['generative-language-api-key']);
+ }
+ } catch (error) {
+ console.error('加载Gemini密钥失败:', error);
+ }
+ }
+
+ // 渲染Gemini密钥列表
+ renderGeminiKeys(keys) {
+ const container = document.getElementById('gemini-keys-list');
+
+ if (keys.length === 0) {
+ container.innerHTML = `
+
+
+
${i18n.t('ai_providers.gemini_empty_title')}
+
${i18n.t('ai_providers.gemini_empty_desc')}
+
+ `;
+ return;
+ }
+
+ container.innerHTML = keys.map((key, index) => `
+
+
+
${i18n.t('ai_providers.gemini_item_title')} #${index + 1}
+
${this.maskApiKey(key)}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // 显示添加Gemini密钥模态框
+ showAddGeminiKeyModal() {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ 添加Gemini API密钥
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 添加Gemini密钥
+ async addGeminiKey() {
+ const newKey = document.getElementById('new-gemini-key').value.trim();
+
+ if (!newKey) {
+ this.showNotification('请输入Gemini API密钥', 'error');
+ return;
+ }
+
+ try {
+ const data = await this.makeRequest('/generative-language-api-key');
+ const currentKeys = data['generative-language-api-key'] || [];
+ currentKeys.push(newKey);
+
+ await this.makeRequest('/generative-language-api-key', {
+ method: 'PUT',
+ body: JSON.stringify(currentKeys)
+ });
+
+ this.clearCache(); // 清除缓存
+ this.closeModal();
+ this.loadGeminiKeys();
+ this.showNotification('Gemini密钥添加成功', 'success');
+ } catch (error) {
+ this.showNotification(`添加Gemini密钥失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 编辑Gemini密钥
+ editGeminiKey(index, currentKey) {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ 编辑Gemini API密钥
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 更新Gemini密钥
+ async updateGeminiKey(oldKey) {
+ const newKey = document.getElementById('edit-gemini-key').value.trim();
+
+ if (!newKey) {
+ this.showNotification('请输入Gemini API密钥', 'error');
+ return;
+ }
+
+ try {
+ await this.makeRequest('/generative-language-api-key', {
+ method: 'PATCH',
+ body: JSON.stringify({ old: oldKey, new: newKey })
+ });
+
+ this.clearCache(); // 清除缓存
+ this.closeModal();
+ this.loadGeminiKeys();
+ this.showNotification('Gemini密钥更新成功', 'success');
+ } catch (error) {
+ this.showNotification(`更新Gemini密钥失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除Gemini密钥
+ async deleteGeminiKey(key) {
+ if (!confirm(i18n.t('ai_providers.gemini_delete_confirm'))) return;
+
+ try {
+ await this.makeRequest(`/generative-language-api-key?value=${encodeURIComponent(key)}`, { method: 'DELETE' });
+ this.clearCache(); // 清除缓存
+ this.loadGeminiKeys();
+ this.showNotification('Gemini密钥删除成功', 'success');
+ } catch (error) {
+ this.showNotification(`删除Gemini密钥失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载Codex密钥
+ async loadCodexKeys() {
+ try {
+ const config = await this.getConfig();
+ if (config['codex-api-key']) {
+ this.renderCodexKeys(config['codex-api-key']);
+ }
+ } catch (error) {
+ console.error('加载Codex密钥失败:', error);
+ }
+ }
+
+ // 渲染Codex密钥列表
+ renderCodexKeys(keys) {
+ const container = document.getElementById('codex-keys-list');
+
+ if (keys.length === 0) {
+ container.innerHTML = `
+
+
+
${i18n.t('ai_providers.codex_empty_title')}
+
${i18n.t('ai_providers.codex_empty_desc')}
+
+ `;
+ return;
+ }
+
+ container.innerHTML = keys.map((config, index) => `
+
+
+
${i18n.t('ai_providers.codex_item_title')} #${index + 1}
+
${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}
+ ${config['base-url'] ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}
` : ''}
+ ${config['proxy-url'] ? `
${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}
` : ''}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // 显示添加Codex密钥模态框
+ showAddCodexKeyModal() {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ ${i18n.t('ai_providers.codex_add_modal_title')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 添加Codex密钥
+ async addCodexKey() {
+ const apiKey = document.getElementById('new-codex-key').value.trim();
+ const baseUrl = document.getElementById('new-codex-url').value.trim();
+ const proxyUrl = document.getElementById('new-codex-proxy').value.trim();
+
+ if (!apiKey) {
+ this.showNotification(i18n.t('notification.field_required'), 'error');
+ return;
+ }
+
+ try {
+ const data = await this.makeRequest('/codex-api-key');
+ const currentKeys = data['codex-api-key'] || [];
+
+ const newConfig = { 'api-key': apiKey };
+ if (baseUrl) {
+ newConfig['base-url'] = baseUrl;
+ }
+ if (proxyUrl) {
+ newConfig['proxy-url'] = proxyUrl;
+ }
+
+ currentKeys.push(newConfig);
+
+ await this.makeRequest('/codex-api-key', {
+ method: 'PUT',
+ body: JSON.stringify(currentKeys)
+ });
+
+ this.clearCache(); // 清除缓存
+ this.closeModal();
+ this.loadCodexKeys();
+ this.showNotification('Codex配置添加成功', 'success');
+ } catch (error) {
+ this.showNotification(`添加Codex配置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 编辑Codex密钥
+ editCodexKey(index, config) {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ ${i18n.t('ai_providers.codex_edit_modal_title')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 更新Codex密钥
+ async updateCodexKey(index) {
+ const apiKey = document.getElementById('edit-codex-key').value.trim();
+ const baseUrl = document.getElementById('edit-codex-url').value.trim();
+ const proxyUrl = document.getElementById('edit-codex-proxy').value.trim();
+
+ if (!apiKey) {
+ this.showNotification(i18n.t('notification.field_required'), 'error');
+ return;
+ }
+
+ try {
+ const newConfig = { 'api-key': apiKey };
+ if (baseUrl) {
+ newConfig['base-url'] = baseUrl;
+ }
+ if (proxyUrl) {
+ newConfig['proxy-url'] = proxyUrl;
+ }
+
+ await this.makeRequest('/codex-api-key', {
+ method: 'PATCH',
+ body: JSON.stringify({ index, value: newConfig })
+ });
+
+ this.clearCache(); // 清除缓存
+ this.closeModal();
+ this.loadCodexKeys();
+ this.showNotification('Codex配置更新成功', 'success');
+ } catch (error) {
+ this.showNotification(`更新Codex配置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除Codex密钥
+ async deleteCodexKey(apiKey) {
+ if (!confirm(i18n.t('ai_providers.codex_delete_confirm'))) return;
+
+ try {
+ await this.makeRequest(`/codex-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' });
+ this.clearCache(); // 清除缓存
+ this.loadCodexKeys();
+ this.showNotification('Codex配置删除成功', 'success');
+ } catch (error) {
+ this.showNotification(`删除Codex配置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载Claude密钥
+ async loadClaudeKeys() {
+ try {
+ const config = await this.getConfig();
+ if (config['claude-api-key']) {
+ this.renderClaudeKeys(config['claude-api-key']);
+ }
+ } catch (error) {
+ console.error('加载Claude密钥失败:', error);
+ }
+ }
+
+ // 渲染Claude密钥列表
+ renderClaudeKeys(keys) {
+ const container = document.getElementById('claude-keys-list');
+
+ if (keys.length === 0) {
+ container.innerHTML = `
+
+
+
${i18n.t('ai_providers.claude_empty_title')}
+
${i18n.t('ai_providers.claude_empty_desc')}
+
+ `;
+ return;
+ }
+
+ container.innerHTML = keys.map((config, index) => `
+
+
+
${i18n.t('ai_providers.claude_item_title')} #${index + 1}
+
${i18n.t('common.api_key')}: ${this.maskApiKey(config['api-key'])}
+ ${config['base-url'] ? `
${i18n.t('common.base_url')}: ${this.escapeHtml(config['base-url'])}
` : ''}
+ ${config['proxy-url'] ? `
${i18n.t('common.proxy_url')}: ${this.escapeHtml(config['proxy-url'])}
` : ''}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // 显示添加Claude密钥模态框
+ showAddClaudeKeyModal() {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ ${i18n.t('ai_providers.claude_add_modal_title')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 添加Claude密钥
+ async addClaudeKey() {
+ const apiKey = document.getElementById('new-claude-key').value.trim();
+ const baseUrl = document.getElementById('new-claude-url').value.trim();
+ const proxyUrl = document.getElementById('new-claude-proxy').value.trim();
+
+ if (!apiKey) {
+ this.showNotification(i18n.t('notification.field_required'), 'error');
+ return;
+ }
+
+ try {
+ const data = await this.makeRequest('/claude-api-key');
+ const currentKeys = data['claude-api-key'] || [];
+
+ const newConfig = { 'api-key': apiKey };
+ if (baseUrl) {
+ newConfig['base-url'] = baseUrl;
+ }
+ if (proxyUrl) {
+ newConfig['proxy-url'] = proxyUrl;
+ }
+
+ currentKeys.push(newConfig);
+
+ await this.makeRequest('/claude-api-key', {
+ method: 'PUT',
+ body: JSON.stringify(currentKeys)
+ });
+
+ this.clearCache(); // 清除缓存
+ this.closeModal();
+ this.loadClaudeKeys();
+ this.showNotification('Claude配置添加成功', 'success');
+ } catch (error) {
+ this.showNotification(`添加Claude配置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 编辑Claude密钥
+ editClaudeKey(index, config) {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ ${i18n.t('ai_providers.claude_edit_modal_title')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ }
+
+ // 更新Claude密钥
+ async updateClaudeKey(index) {
+ const apiKey = document.getElementById('edit-claude-key').value.trim();
+ const baseUrl = document.getElementById('edit-claude-url').value.trim();
+ const proxyUrl = document.getElementById('edit-claude-proxy').value.trim();
+
+ if (!apiKey) {
+ this.showNotification(i18n.t('notification.field_required'), 'error');
+ return;
+ }
+
+ try {
+ const newConfig = { 'api-key': apiKey };
+ if (baseUrl) {
+ newConfig['base-url'] = baseUrl;
+ }
+ if (proxyUrl) {
+ newConfig['proxy-url'] = proxyUrl;
+ }
+
+ await this.makeRequest('/claude-api-key', {
+ method: 'PATCH',
+ body: JSON.stringify({ index, value: newConfig })
+ });
+
+ this.clearCache(); // 清除缓存
+ this.closeModal();
+ this.loadClaudeKeys();
+ this.showNotification('Claude配置更新成功', 'success');
+ } catch (error) {
+ this.showNotification(`更新Claude配置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除Claude密钥
+ async deleteClaudeKey(apiKey) {
+ if (!confirm(i18n.t('ai_providers.claude_delete_confirm'))) return;
+
+ try {
+ await this.makeRequest(`/claude-api-key?api-key=${encodeURIComponent(apiKey)}`, { method: 'DELETE' });
+ this.clearCache(); // 清除缓存
+ this.loadClaudeKeys();
+ this.showNotification('Claude配置删除成功', 'success');
+ } catch (error) {
+ this.showNotification(`删除Claude配置失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载OpenAI提供商
+ async loadOpenAIProviders() {
+ try {
+ const config = await this.getConfig();
+ if (config['openai-compatibility']) {
+ this.renderOpenAIProviders(config['openai-compatibility']);
+ }
+ } catch (error) {
+ console.error('加载OpenAI提供商失败:', error);
+ }
+ }
+
+ // 渲染OpenAI提供商列表
+ renderOpenAIProviders(providers) {
+ const container = document.getElementById('openai-providers-list');
+
+ if (providers.length === 0) {
+ container.innerHTML = `
+
+
+
${i18n.t('ai_providers.openai_empty_title')}
+
${i18n.t('ai_providers.openai_empty_desc')}
+
+ `;
+ return;
+ }
+
+ container.innerHTML = providers.map((provider, index) => `
+
+
+
${this.escapeHtml(provider.name)}
+
${i18n.t('common.base_url')}: ${this.escapeHtml(provider['base-url'])}
+
${i18n.t('ai_providers.openai_keys_count')}: ${(provider['api-key-entries'] || []).length}
+
${i18n.t('ai_providers.openai_models_count')}: ${(provider.models || []).length}
+ ${this.renderOpenAIModelBadges(provider.models || [])}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // 显示添加OpenAI提供商模态框
+ showAddOpenAIProviderModal() {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ modalBody.innerHTML = `
+ ${i18n.t('ai_providers.openai_add_modal_title')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ this.populateModelFields('new-provider-models-wrapper', []);
+ }
+
+ // 添加OpenAI提供商
+ async addOpenAIProvider() {
+ const name = document.getElementById('new-provider-name').value.trim();
+ const baseUrl = document.getElementById('new-provider-url').value.trim();
+ const keysText = document.getElementById('new-provider-keys').value.trim();
+ const proxiesText = document.getElementById('new-provider-proxies').value.trim();
+ const models = this.collectModelInputs('new-provider-models-wrapper');
+
+ if (!this.validateOpenAIProviderInput(name, baseUrl, models)) {
+ return;
+ }
+
+ try {
+ const data = await this.makeRequest('/openai-compatibility');
+ const currentProviders = data['openai-compatibility'] || [];
+
+ const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : [];
+ const proxies = proxiesText ? proxiesText.split('\n').map(p => p.trim()).filter(p => p) : [];
+ const apiKeyEntries = apiKeys.map((key, idx) => ({
+ 'api-key': key,
+ 'proxy-url': proxies[idx] || ''
+ }));
+
+ const newProvider = {
+ name,
+ 'base-url': baseUrl,
+ 'api-key-entries': apiKeyEntries,
+ models
+ };
+
+ currentProviders.push(newProvider);
+
+ await this.makeRequest('/openai-compatibility', {
+ method: 'PUT',
+ body: JSON.stringify(currentProviders)
+ });
+
+ this.clearCache(); // 清除缓存
+ this.closeModal();
+ this.loadOpenAIProviders();
+ this.showNotification('OpenAI提供商添加成功', 'success');
+ } catch (error) {
+ this.showNotification(`添加OpenAI提供商失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 编辑OpenAI提供商
+ editOpenAIProvider(index, provider) {
+ const modal = document.getElementById('modal');
+ const modalBody = document.getElementById('modal-body');
+
+ const apiKeyEntries = provider['api-key-entries'] || [];
+ const apiKeysText = apiKeyEntries.map(entry => entry['api-key'] || '').join('\n');
+ const proxiesText = apiKeyEntries.map(entry => entry['proxy-url'] || '').join('\n');
+
+ modalBody.innerHTML = `
+ ${i18n.t('ai_providers.openai_edit_modal_title')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ modal.style.display = 'block';
+ this.populateModelFields('edit-provider-models-wrapper', provider.models || []);
+ }
+
+ // 更新OpenAI提供商
+ async updateOpenAIProvider(index) {
+ const name = document.getElementById('edit-provider-name').value.trim();
+ const baseUrl = document.getElementById('edit-provider-url').value.trim();
+ const keysText = document.getElementById('edit-provider-keys').value.trim();
+ const proxiesText = document.getElementById('edit-provider-proxies').value.trim();
+ const models = this.collectModelInputs('edit-provider-models-wrapper');
+
+ if (!this.validateOpenAIProviderInput(name, baseUrl, models)) {
+ return;
+ }
+
+ try {
+ const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : [];
+ const proxies = proxiesText ? proxiesText.split('\n').map(p => p.trim()).filter(p => p) : [];
+ const apiKeyEntries = apiKeys.map((key, idx) => ({
+ 'api-key': key,
+ 'proxy-url': proxies[idx] || ''
+ }));
+
+ const updatedProvider = {
+ name,
+ 'base-url': baseUrl,
+ 'api-key-entries': apiKeyEntries,
+ models
+ };
+
+ await this.makeRequest('/openai-compatibility', {
+ method: 'PATCH',
+ body: JSON.stringify({ index, value: updatedProvider })
+ });
+
+ this.clearCache(); // 清除缓存
+ this.closeModal();
+ this.loadOpenAIProviders();
+ this.showNotification('OpenAI提供商更新成功', 'success');
+ } catch (error) {
+ this.showNotification(`更新OpenAI提供商失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除OpenAI提供商
+ async deleteOpenAIProvider(name) {
+ if (!confirm(i18n.t('ai_providers.openai_delete_confirm'))) return;
+
+ try {
+ await this.makeRequest(`/openai-compatibility?name=${encodeURIComponent(name)}`, { method: 'DELETE' });
+ this.clearCache(); // 清除缓存
+ this.loadOpenAIProviders();
+ this.showNotification('OpenAI提供商删除成功', 'success');
+ } catch (error) {
+ this.showNotification(`删除OpenAI提供商失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 加载认证文件
+ async loadAuthFiles() {
+ try {
+ const data = await this.makeRequest('/auth-files');
+ this.renderAuthFiles(data.files || []);
+ } catch (error) {
+ console.error('加载认证文件失败:', error);
+ }
+ }
+
+ // 渲染认证文件列表
+ renderAuthFiles(files) {
+ const container = document.getElementById('auth-files-list');
+
+ if (files.length === 0) {
+ container.innerHTML = `
+
+
+
${i18n.t('auth_files.empty_title')}
+
${i18n.t('auth_files.empty_desc')}
+
+ `;
+ return;
+ }
+
+ container.innerHTML = files.map(file => `
+
+
+
${file.name}
+
${i18n.t('auth_files.file_size')}: ${this.formatFileSize(file.size)}
+
${i18n.t('auth_files.file_modified')}: ${new Date(file.modtime).toLocaleString(i18n.currentLanguage === 'zh-CN' ? 'zh-CN' : 'en-US')}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // 格式化文件大小
+ formatFileSize(bytes) {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ }
+
+ // 上传认证文件
+ uploadAuthFile() {
+ document.getElementById('auth-file-input').click();
+ }
+
+ // 处理文件上传
+ async handleFileUpload(event) {
+ const file = event.target.files[0];
+ if (!file) return;
+
+ if (!file.name.endsWith('.json')) {
+ this.showNotification(i18n.t('auth_files.upload_error_json'), 'error');
+ return;
+ }
+
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await fetch(`${this.apiUrl}/auth-files`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this.managementKey}`
+ },
+ body: formData
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.error || `HTTP ${response.status}`);
+ }
+
+ this.clearCache(); // 清除缓存
+ this.loadAuthFiles();
+ this.showNotification(i18n.t('auth_files.upload_success'), 'success');
+ } catch (error) {
+ this.showNotification(`文件上传失败: ${error.message}`, 'error');
+ }
+
+ // 清空文件输入
+ event.target.value = '';
+ }
+
+ // 下载认证文件
+ async downloadAuthFile(filename) {
+ try {
+ const response = await fetch(`${this.apiUrl}/auth-files/download?name=${encodeURIComponent(filename)}`, {
+ headers: {
+ 'Authorization': `Bearer ${this.managementKey}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ a.click();
+ window.URL.revokeObjectURL(url);
+
+ this.showNotification(i18n.t('auth_files.download_success'), 'success');
+ } catch (error) {
+ this.showNotification(`文件下载失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除认证文件
+ async deleteAuthFile(filename) {
+ if (!confirm(`${i18n.t('auth_files.delete_confirm')} "${filename}" 吗?`)) return;
+
+ try {
+ await this.makeRequest(`/auth-files?name=${encodeURIComponent(filename)}`, { method: 'DELETE' });
+ this.clearCache(); // 清除缓存
+ this.loadAuthFiles();
+ this.showNotification(i18n.t('auth_files.delete_success'), 'success');
+ } catch (error) {
+ this.showNotification(`文件删除失败: ${error.message}`, 'error');
+ }
+ }
+
+ // 删除所有认证文件
+ async deleteAllAuthFiles() {
+ if (!confirm(i18n.t('auth_files.delete_all_confirm'))) return;
+
+ try {
+ const response = await this.makeRequest('/auth-files?all=true', { method: 'DELETE' });
+ this.clearCache(); // 清除缓存
+ this.loadAuthFiles();
+ this.showNotification(`${i18n.t('auth_files.delete_all_success')} ${response.deleted} ${i18n.t('auth_files.files_count')}`, 'success');
+ } catch (error) {
+ this.showNotification(`删除文件失败: ${error.message}`, 'error');
+ }
+ }
+
+
+
+
+
+ // 显示 Gemini Web Token 模态框
+ showGeminiWebTokenModal() {
+ const inlineSecure1psid = document.getElementById('secure-1psid-input');
+ const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
+ const inlineLabel = document.getElementById('gemini-web-label-input');
+ const modalBody = document.getElementById('modal-body');
+ modalBody.innerHTML = `
+ ${i18n.t('auth_login.gemini_web_button')}
+
+ `;
+ this.showModal();
+
+ const modalSecure1psid = document.getElementById('modal-secure-1psid');
+ const modalSecure1psidts = document.getElementById('modal-secure-1psidts');
+ const modalLabel = document.getElementById('modal-gemini-web-label');
+
+ if (modalSecure1psid && inlineSecure1psid) {
+ modalSecure1psid.value = inlineSecure1psid.value.trim();
+ }
+ if (modalSecure1psidts && inlineSecure1psidts) {
+ modalSecure1psidts.value = inlineSecure1psidts.value.trim();
+ }
+ if (modalLabel && inlineLabel) {
+ modalLabel.value = inlineLabel.value.trim();
+ }
+
+ if (modalSecure1psid) {
+ modalSecure1psid.focus();
+ }
+ }
+
+ // 保存 Gemini Web Token
+ async saveGeminiWebToken() {
+ const secure1psid = document.getElementById('modal-secure-1psid').value.trim();
+ const secure1psidts = document.getElementById('modal-secure-1psidts').value.trim();
+ const label = document.getElementById('modal-gemini-web-label').value.trim();
+
+ if (!secure1psid || !secure1psidts) {
+ this.showNotification('请填写完整的 Cookie 信息', 'error');
+ return;
+ }
+
+ try {
+ const requestBody = {
+ secure_1psid: secure1psid,
+ secure_1psidts: secure1psidts
+ };
+
+ // 如果提供了 label,则添加到请求体中
+ if (label) {
+ requestBody.label = label;
+ }
+
+ const response = await this.makeRequest('/gemini-web-token', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(requestBody)
+ });
+
+ this.closeModal();
+ this.loadAuthFiles(); // 刷新认证文件列表
+ const inlineSecure1psid = document.getElementById('secure-1psid-input');
+ const inlineSecure1psidts = document.getElementById('secure-1psidts-input');
+ const inlineLabel = document.getElementById('gemini-web-label-input');
+ if (inlineSecure1psid) {
+ inlineSecure1psid.value = secure1psid;
+ }
+ if (inlineSecure1psidts) {
+ inlineSecure1psidts.value = secure1psidts;
+ }
+ if (inlineLabel) {
+ inlineLabel.value = label;
+ }
+ this.showNotification(`${i18n.t('auth_login.gemini_web_saved')}: ${response.file}`, 'success');
+ } catch (error) {
+ this.showNotification(`保存失败: ${error.message}`, 'error');
+ }
+ }
+
+ // ===== 使用统计相关方法 =====
+
+ // 初始化图表变量
+ requestsChart = null;
+ tokensChart = null;
+ currentUsageData = null;
+
+ // 加载使用统计
+ async loadUsageStats() {
+ try {
+ const response = await this.makeRequest('/usage');
+ const usage = response?.usage || null;
+ this.currentUsageData = usage;
+
+ if (!usage) {
+ throw new Error('usage payload missing');
+ }
+
+ // 更新概览卡片
+ this.updateUsageOverview(usage);
+
+ // 读取当前图表周期
+ const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
+ const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
+ const requestsPeriod = requestsHourActive ? 'hour' : 'day';
+ const tokensPeriod = tokensHourActive ? 'hour' : 'day';
+
+ // 初始化图表(使用当前周期)
+ this.initializeRequestsChart(requestsPeriod);
+ this.initializeTokensChart(tokensPeriod);
+
+ // 更新API详细统计表格
+ this.updateApiStatsTable(usage);
+
+ } catch (error) {
+ console.error('加载使用统计失败:', error);
+ this.currentUsageData = null;
+
+ // 清空概览数据
+ ['total-requests', 'success-requests', 'failed-requests', 'total-tokens'].forEach(id => {
+ const el = document.getElementById(id);
+ if (el) el.textContent = '-';
+ });
+
+ // 清空图表
+ if (this.requestsChart) {
+ this.requestsChart.destroy();
+ this.requestsChart = null;
+ }
+ if (this.tokensChart) {
+ this.tokensChart.destroy();
+ this.tokensChart = null;
+ }
+
+ const tableElement = document.getElementById('api-stats-table');
+ if (tableElement) {
+ tableElement.innerHTML = `${i18n.t('usage_stats.loading_error')}: ${error.message}
`;
+ }
+ }
+ }
+
+ // 更新使用统计概览
+ updateUsageOverview(data) {
+ const safeData = data || {};
+ document.getElementById('total-requests').textContent = safeData.total_requests ?? 0;
+ document.getElementById('success-requests').textContent = safeData.success_count ?? 0;
+ document.getElementById('failed-requests').textContent = safeData.failure_count ?? 0;
+ document.getElementById('total-tokens').textContent = safeData.total_tokens ?? 0;
+ }
+
+ // 初始化图表
+ initializeCharts() {
+ const requestsHourActive = document.getElementById('requests-hour-btn')?.classList.contains('active');
+ const tokensHourActive = document.getElementById('tokens-hour-btn')?.classList.contains('active');
+ this.initializeRequestsChart(requestsHourActive ? 'hour' : 'day');
+ this.initializeTokensChart(tokensHourActive ? 'hour' : 'day');
+ }
+
+ // 初始化请求趋势图表
+ initializeRequestsChart(period = 'day') {
+ const ctx = document.getElementById('requests-chart');
+ if (!ctx) return;
+
+ // 销毁现有图表
+ if (this.requestsChart) {
+ this.requestsChart.destroy();
+ }
+
+ const data = this.getRequestsChartData(period);
+
+ this.requestsChart = new Chart(ctx, {
+ type: 'line',
+ data: data,
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: false
+ }
+ },
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day')
+ }
+ },
+ y: {
+ beginAtZero: true,
+ title: {
+ display: true,
+ text: i18n.t('usage_stats.requests_count')
+ }
+ }
+ },
+ elements: {
+ line: {
+ borderColor: '#3b82f6',
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
+ fill: true,
+ tension: 0.4
+ },
+ point: {
+ backgroundColor: '#3b82f6',
+ borderColor: '#ffffff',
+ borderWidth: 2,
+ radius: 4
+ }
+ }
+ }
+ });
+ }
+
+ // 初始化Token使用趋势图表
+ initializeTokensChart(period = 'day') {
+ const ctx = document.getElementById('tokens-chart');
+ if (!ctx) return;
+
+ // 销毁现有图表
+ if (this.tokensChart) {
+ this.tokensChart.destroy();
+ }
+
+ const data = this.getTokensChartData(period);
+
+ this.tokensChart = new Chart(ctx, {
+ type: 'line',
+ data: data,
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: false
+ }
+ },
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day')
+ }
+ },
+ y: {
+ beginAtZero: true,
+ title: {
+ display: true,
+ text: i18n.t('usage_stats.tokens_count')
+ }
+ }
+ },
+ elements: {
+ line: {
+ borderColor: '#10b981',
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
+ fill: true,
+ tension: 0.4
+ },
+ point: {
+ backgroundColor: '#10b981',
+ borderColor: '#ffffff',
+ borderWidth: 2,
+ radius: 4
+ }
+ }
+ }
+ });
+ }
+
+ // 获取请求图表数据
+ getRequestsChartData(period) {
+ if (!this.currentUsageData) {
+ return { labels: [], datasets: [{ data: [] }] };
+ }
+
+ let dataSource, labels, values;
+
+ if (period === 'hour') {
+ dataSource = this.currentUsageData.requests_by_hour || {};
+ labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
+ values = labels.map(hour => dataSource[hour] || 0);
+ } else {
+ dataSource = this.currentUsageData.requests_by_day || {};
+ labels = Object.keys(dataSource).sort();
+ values = labels.map(day => dataSource[day] || 0);
+ }
+
+ return {
+ labels: labels,
+ datasets: [{
+ data: values
+ }]
+ };
+ }
+
+ // 获取Token图表数据
+ getTokensChartData(period) {
+ if (!this.currentUsageData) {
+ return { labels: [], datasets: [{ data: [] }] };
+ }
+
+ let dataSource, labels, values;
+
+ if (period === 'hour') {
+ dataSource = this.currentUsageData.tokens_by_hour || {};
+ labels = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
+ values = labels.map(hour => dataSource[hour] || 0);
+ } else {
+ dataSource = this.currentUsageData.tokens_by_day || {};
+ labels = Object.keys(dataSource).sort();
+ values = labels.map(day => dataSource[day] || 0);
+ }
+
+ return {
+ labels: labels,
+ datasets: [{
+ data: values
+ }]
+ };
+ }
+
+ // 切换请求图表时间周期
+ switchRequestsPeriod(period) {
+ // 更新按钮状态
+ document.getElementById('requests-hour-btn').classList.toggle('active', period === 'hour');
+ document.getElementById('requests-day-btn').classList.toggle('active', period === 'day');
+
+ // 更新图表数据
+ if (this.requestsChart) {
+ const newData = this.getRequestsChartData(period);
+ this.requestsChart.data = newData;
+ this.requestsChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day');
+ this.requestsChart.update();
+ }
+ }
+
+ // 切换Token图表时间周期
+ switchTokensPeriod(period) {
+ // 更新按钮状态
+ document.getElementById('tokens-hour-btn').classList.toggle('active', period === 'hour');
+ document.getElementById('tokens-day-btn').classList.toggle('active', period === 'day');
+
+ // 更新图表数据
+ if (this.tokensChart) {
+ const newData = this.getTokensChartData(period);
+ this.tokensChart.data = newData;
+ this.tokensChart.options.scales.x.title.text = i18n.t(period === 'hour' ? 'usage_stats.by_hour' : 'usage_stats.by_day');
+ this.tokensChart.update();
+ }
+ }
+
+ // 更新API详细统计表格
+ updateApiStatsTable(data) {
+ const container = document.getElementById('api-stats-table');
+ if (!container) return;
+
+ const apis = data.apis || {};
+
+ if (Object.keys(apis).length === 0) {
+ container.innerHTML = `${i18n.t('usage_stats.no_data')}
`;
+ return;
+ }
+
+ let tableHtml = `
+
+
+
+ | ${i18n.t('usage_stats.api_endpoint')} |
+ ${i18n.t('usage_stats.requests_count')} |
+ ${i18n.t('usage_stats.tokens_count')} |
+ ${i18n.t('usage_stats.success_rate')} |
+ ${i18n.t('usage_stats.models')} |
+
+
+
+ `;
+
+ Object.entries(apis).forEach(([endpoint, apiData]) => {
+ const totalRequests = apiData.total_requests || 0;
+ const successCount = apiData.success_count ?? null;
+ const successRate = successCount !== null && totalRequests > 0
+ ? Math.round((successCount / totalRequests) * 100)
+ : null;
+
+ // 构建模型详情
+ let modelsHtml = '';
+ if (apiData.models && Object.keys(apiData.models).length > 0) {
+ modelsHtml = '';
+ Object.entries(apiData.models).forEach(([modelName, modelData]) => {
+ const modelRequests = modelData.total_requests ?? 0;
+ const modelTokens = modelData.total_tokens ?? 0;
+ modelsHtml += `
+
+ ${modelName}
+ ${modelRequests} 请求 / ${modelTokens} tokens
+
+ `;
+ });
+ modelsHtml += '
';
+ }
+
+ tableHtml += `
+
+ | ${endpoint} |
+ ${totalRequests} |
+ ${apiData.total_tokens || 0} |
+ ${successRate !== null ? successRate + '%' : '-'} |
+ ${modelsHtml || '-'} |
+
+ `;
+ });
+
+ tableHtml += '
';
+ container.innerHTML = tableHtml;
+ }
+
+ 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:8317');
+ }
+ }
+
+ updateLoginConnectionInfo() {
+ const connectionUrlElement = document.getElementById('login-connection-url');
+ const customInput = document.getElementById('login-api-base');
+ if (connectionUrlElement) {
+ connectionUrlElement.textContent = this.apiBase || '-';
+ }
+ if (customInput && customInput !== document.activeElement) {
+ customInput.value = this.apiBase || '';
+ }
+ }
+
+ 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;
+ }
+
+ renderOpenAIModelBadges(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;
+ }
+}
+
+// 全局管理器实例
+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();
+});
diff --git a/index.html b/index.html
index 45beafd..2dc01c8 100644
--- a/index.html
+++ b/index.html
@@ -1,591 +1,593 @@
-
-
-
-
-
- CLI Proxy API Management Center
-
-
-
-
-
-
-
-
-
-
-
-
正在自动登录...
-
正在使用本地保存的连接信息尝试连接服务器
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
连接地址
-
- 当前地址
- :
- -
-
-
-
-
系统将自动使用当前访问地址进行连接
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 基础设置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- AI 提供商配置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 认证文件管理
-
-
-
-
- 这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。
-
-
-
-
-
-
-
-
-
- 从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 使用统计
-
-
-
-
-
-
-
-
-
-
-
-
-
- 系统信息
-
-
-
-
-
-
-
-
-
-
-
- 连接状态:
-
-
- 未连接
-
-
-
-
-
-
-
-
-
-
-
- API 状态:
- 未连接
-
-
- 配置状态:
- 未加载
-
-
- 最后更新:
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ CLI Proxy API Management Center
+
+
+
+
+
+
+
+
+
+
+
+
正在自动登录...
+
正在使用本地保存的连接信息尝试连接服务器
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
连接地址
+
+ 当前地址
+ :
+ -
+
+
+
+
系统将自动使用当前访问地址进行连接
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![Logo]()
+
CLI Proxy API Management Center
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 基础设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AI 提供商配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 认证文件管理
+
+
+
+
+ 这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。
+
+
+
+
+
+
+
+
+
+ 从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 使用统计
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 系统信息
+
+
+
+
+
+
+
+
+
+
+
+ 连接状态:
+
+
+ 未连接
+
+
+
+
+
+
+
+
+
+
+
+ API 状态:
+ 未连接
+
+
+ 配置状态:
+ 未加载
+
+
+ 最后更新:
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/styles.css b/styles.css
index ee54b32..fe73e38 100644
--- a/styles.css
+++ b/styles.css
@@ -7,6 +7,9 @@
/* CSS变量主题系统 */
:root {
+ /* 布局尺寸 */
+ --navbar-height: 69px;
+
/* 亮色主题(默认) */
--bg-primary: #f5f7fa;
--bg-secondary: #ffffff;
@@ -605,30 +608,92 @@ body {
overflow-x: hidden;
}
-.container {
- display: flex;
- min-height: 100vh;
- position: relative;
+/* 侧边栏样式 */
+.layout {
+ display: grid;
+ grid-template-columns: 240px 1fr;
+ min-height: calc(100vh - var(--navbar-height, 69px));
+ width: 100%;
+ transition: grid-template-columns 0.3s ease;
+}
+
+.layout.sidebar-collapsed {
+ grid-template-columns: 64px 1fr;
}
-/* 侧边栏样式 */
.sidebar {
width: 240px;
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
- height: 100vh;
- position: fixed;
- left: 0;
- top: 0;
+ height: calc(100vh - var(--navbar-height, 69px));
+ position: sticky;
+ top: var(--navbar-height, 69px);
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
- z-index: 100;
+ z-index: 5;
box-shadow: var(--shadow-sm);
transition: all 0.3s ease;
}
+.sidebar.collapsed {
+ width: 64px;
+}
+
+.sidebar.collapsed .nav-item span {
+ opacity: 0;
+ width: 0;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.sidebar.collapsed .nav-item {
+ justify-content: center;
+ padding: 10px 0;
+}
+
+.sidebar.collapsed .nav-item i {
+ margin-right: 0;
+}
+
+/* 侧边栏切换按钮(桌面端-在顶栏) */
+.sidebar-toggle-btn-desktop {
+ display: none;
+ width: 40px;
+ height: 40px;
+ border: none;
+ background: transparent;
+ color: var(--text-primary);
+ border-radius: 8px;
+ cursor: pointer;
+ align-items: center;
+ justify-content: center;
+ font-size: 18px;
+ transition: all 0.2s ease;
+ padding: 0;
+}
+
+.sidebar-toggle-btn-desktop:hover {
+ background: var(--bg-tertiary);
+ color: var(--primary-color);
+}
+
+.sidebar-toggle-btn-desktop i {
+ transition: transform 0.3s ease;
+}
+
+.layout.sidebar-collapsed .sidebar-toggle-btn-desktop i {
+ transform: rotate(90deg);
+}
+
+/* 在大屏幕上显示桌面端切换按钮 */
+@media (min-width: 1025px) {
+ .sidebar-toggle-btn-desktop {
+ display: flex;
+ }
+}
+
/* 侧边栏品牌区域 */
.sidebar-brand {
padding: 24px 20px;
@@ -662,6 +727,7 @@ body {
.sidebar .nav-menu li {
margin-bottom: 4px;
+ position: relative;
}
.sidebar .nav-item {
@@ -675,6 +741,7 @@ body {
font-size: 14px;
font-weight: 500;
cursor: pointer;
+ position: relative;
}
.sidebar .nav-item:hover {
@@ -692,16 +759,43 @@ body {
width: 18px;
font-size: 16px;
text-align: center;
+ transition: margin 0.3s ease;
+}
+
+/* 收起状态时的工具提示 */
+.sidebar.collapsed .nav-menu li:hover::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ left: 68px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ padding: 8px 12px;
+ border-radius: 6px;
+ font-size: 13px;
+ font-weight: 500;
+ white-space: nowrap;
+ box-shadow: var(--shadow-md);
+ border: 1px solid var(--border-primary);
+ z-index: 1000;
+ pointer-events: none;
+ opacity: 0;
+ animation: tooltipFadeIn 0.2s ease forwards;
+}
+
+@keyframes tooltipFadeIn {
+ to {
+ opacity: 1;
+ }
}
/* 主内容区域包装器 */
.main-wrapper {
- margin-left: 240px;
- min-height: 100vh;
- width: calc(100% - 240px);
display: flex;
flex-direction: column;
background: var(--bg-primary);
+ min-height: 100%;
}
/* 顶部导航栏 */
@@ -714,27 +808,47 @@ body {
align-items: center;
position: sticky;
top: 0;
- z-index: 90;
+ z-index: 200;
box-shadow: var(--shadow-sm);
+ height: var(--navbar-height, 69px);
+ min-height: var(--navbar-height, 69px);
+ box-sizing: border-box;
}
.top-navbar-left {
display: flex;
align-items: center;
- gap: 16px;
+ gap: 12px;
+ flex: 1;
}
-.top-navbar-title {
- font-size: 20px;
+.top-navbar-brand {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ max-width: max-content;
+}
+
+.top-navbar-brand-logo {
+ width: 32px;
+ height: 32px;
+ object-fit: contain;
+ border-radius: 8px;
+}
+
+.top-navbar-brand-text {
+ font-size: 18px;
font-weight: 600;
color: var(--text-primary);
- margin: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.top-navbar-actions {
display: flex;
align-items: center;
gap: 10px;
+ margin-left: auto;
}
.top-navbar-actions > * {
@@ -1476,17 +1590,54 @@ input:checked + .slider:before {
/* 响应式设计 */
@media (max-width: 1024px) {
- .sidebar {
- transform: translateX(-100%);
+ .layout {
+ position: relative;
+ grid-template-columns: 1fr;
+ min-height: calc(100vh - var(--navbar-height, 69px));
}
-
+
+ .sidebar {
+ position: fixed;
+ left: 0;
+ top: var(--navbar-height, 69px);
+ height: calc(100vh - var(--navbar-height, 69px));
+ transform: translateX(-100%);
+ z-index: 150;
+ box-shadow: var(--shadow-sm);
+ background: var(--sidebar-bg);
+ width: 240px !important;
+ }
+
.sidebar.mobile-open {
transform: translateX(0);
+ box-shadow: var(--shadow-secondary);
+ }
+
+ /* 移动端强制恢复侧栏展开状态 */
+ .sidebar.collapsed {
+ width: 240px !important;
+ }
+
+ .sidebar.collapsed .nav-item span {
+ opacity: 1 !important;
+ width: auto !important;
+ overflow: visible !important;
+ }
+
+ .sidebar.collapsed .nav-item {
+ justify-content: flex-start !important;
+ padding: 10px 14px !important;
+ }
+
+ .sidebar.collapsed .nav-item i {
+ margin-right: 12px !important;
}
.main-wrapper {
margin-left: 0;
width: 100%;
+ min-height: calc(100vh - var(--navbar-height, 69px));
+ overflow: hidden;
}
.main-content {
@@ -1605,29 +1756,38 @@ input:checked + .slider:before {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
- z-index: 99;
+ z-index: 120;
}
-@media (max-width: 1024px) {
- .sidebar-overlay.active {
- display: block;
- }
+.sidebar-overlay.active {
+ display: block;
}
/* 移动端菜单按钮 */
.mobile-menu-btn {
display: none;
- background: none;
+ width: 40px;
+ height: 40px;
+ background: transparent;
border: none;
- padding: 8px;
+ padding: 0;
cursor: pointer;
color: var(--text-primary);
font-size: 20px;
+ border-radius: 8px;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+}
+
+.mobile-menu-btn:hover {
+ background: var(--bg-tertiary);
+ color: var(--primary-color);
}
@media (max-width: 1024px) {
.mobile-menu-btn {
- display: block;
+ display: flex;
}
}