From b61155d2157f3ca487e389ab47b5fd7f1e4e91e0 Mon Sep 17 00:00:00 2001
From: Supra4E8C <69194597+LTbinglingfeng@users.noreply.github.com>
Date: Thu, 2 Oct 2025 17:34:26 +0800
Subject: [PATCH] v0.0.6
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
添加代理 URL 支持,更新 API 配置模态框,增强 XSS 防护,优化界面样式,修复若干 UI 问题,版本更新至 0.0.6
---
app.js | 5446 +++++++++++++++++++++++++++-------------------------
i18n.js | 1353 ++++++-------
index.html | 1158 +++++------
styles.css | 3545 +++++++++++++++++-----------------
4 files changed, 5876 insertions(+), 5626 deletions(-)
diff --git a/app.js b/app.js
index 7a96db2..0741144 100644
--- a/app.js
+++ b/app.js
@@ -1,2635 +1,2811 @@
-// 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();
- }
- });
- }
-
- // 设置导航
- 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);
- }
-
- // 显示添加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')}: ${config['base-url']}
` : ''}
-
-
-
-
-
-
- `).join('');
- }
-
- // 显示添加Codex密钥模态框
- showAddCodexKeyModal() {
- const modal = document.getElementById('modal');
- const modalBody = document.getElementById('modal-body');
-
- modalBody.innerHTML = `
- 添加Codex API配置
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- 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();
-
- if (!apiKey) {
- this.showNotification('请输入API密钥', '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;
- }
-
- 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 = `
- 编辑Codex API配置
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- 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();
-
- if (!apiKey) {
- this.showNotification('请输入API密钥', 'error');
- return;
- }
-
- try {
- const newConfig = { 'api-key': apiKey };
- if (baseUrl) {
- newConfig['base-url'] = baseUrl;
- }
-
- 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')}: ${config['base-url']}
` : ''}
-
-
-
-
-
-
- `).join('');
- }
-
- // 显示添加Claude密钥模态框
- showAddClaudeKeyModal() {
- const modal = document.getElementById('modal');
- const modalBody = document.getElementById('modal-body');
-
- modalBody.innerHTML = `
- 添加Claude API配置
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- 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();
-
- if (!apiKey) {
- this.showNotification('请输入API密钥', '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;
- }
-
- 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 = `
- 编辑Claude API配置
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- 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();
-
- if (!apiKey) {
- this.showNotification('请输入API密钥', 'error');
- return;
- }
-
- try {
- const newConfig = { 'api-key': apiKey };
- if (baseUrl) {
- newConfig['base-url'] = baseUrl;
- }
-
- 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) => `
-
-
-
${provider.name}
-
${i18n.t('common.base_url')}: ${provider['base-url']}
-
${i18n.t('ai_providers.openai_keys_count')}: ${(provider['api-keys'] || []).length}
-
${i18n.t('ai_providers.openai_models_count')}: ${(provider.models || []).length}
-
-
-
-
-
-
- `).join('');
- }
-
- // 显示添加OpenAI提供商模态框
- showAddOpenAIProviderModal() {
- const modal = document.getElementById('modal');
- const modalBody = document.getElementById('modal-body');
-
- modalBody.innerHTML = `
- 添加OpenAI兼容提供商
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- modal.style.display = 'block';
- }
-
- // 添加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();
-
- if (!name || !baseUrl) {
- this.showNotification('请填写提供商名称和Base URL', 'error');
- 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 newProvider = {
- name,
- 'base-url': baseUrl,
- 'api-keys': apiKeys,
- 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 apiKeysText = (provider['api-keys'] || []).join('\n');
-
- modalBody.innerHTML = `
- 编辑OpenAI兼容提供商
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- modal.style.display = 'block';
- }
-
- // 更新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();
-
- if (!name || !baseUrl) {
- this.showNotification('请填写提供商名称和Base URL', 'error');
- return;
- }
-
- try {
- const apiKeys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : [];
-
- const updatedProvider = {
- name,
- 'base-url': baseUrl,
- 'api-keys': apiKeys,
- 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 || '';
- }
- }
-}
-
-// 全局管理器实例
-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();
+ }
+ });
+ }
+
+ // 设置导航
+ 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/i18n.js b/i18n.js
index 0d500b2..28a9dcc 100644
--- a/i18n.js
+++ b/i18n.js
@@ -1,666 +1,687 @@
-// 国际化语言包
-const i18n = {
- // 语言配置
- currentLanguage: 'zh-CN',
- fallbackLanguage: 'zh-CN',
-
- // 语言包
- translations: {
- 'zh-CN': {
- // 通用
- 'common.login': '登录',
- 'common.logout': '登出',
- 'common.cancel': '取消',
- 'common.confirm': '确认',
- 'common.save': '保存',
- 'common.delete': '删除',
- 'common.edit': '编辑',
- 'common.add': '添加',
- 'common.update': '更新',
- 'common.refresh': '刷新',
- 'common.close': '关闭',
- 'common.success': '成功',
- 'common.error': '错误',
- 'common.info': '信息',
- 'common.warning': '警告',
- 'common.loading': '加载中...',
- 'common.connecting': '连接中...',
- 'common.connected': '已连接',
- 'common.disconnected': '未连接',
- 'common.connecting_status': '连接中',
- 'common.connected_status': '已连接',
- 'common.disconnected_status': '未连接',
- 'common.yes': '是',
- 'common.no': '否',
- 'common.optional': '可选',
- 'common.required': '必填',
- 'common.api_key': '密钥',
- 'common.base_url': '地址',
-
- // 页面标题
- 'title.main': 'CLI Proxy API Management Center',
- 'title.login': 'CLI Proxy API Management Center',
-
- // 自动登录
- 'auto_login.title': '正在自动登录...',
- 'auto_login.message': '正在使用本地保存的连接信息尝试连接服务器',
-
- // 登录页面
- 'login.subtitle': '请输入连接信息以访问管理界面',
- 'login.connection_title': '连接地址',
- 'login.connection_current': '当前地址',
- 'login.connection_auto_hint': '系统将自动使用当前访问地址进行连接',
- 'login.custom_connection_label': '自定义连接地址:',
- 'login.custom_connection_placeholder': '例如: https://example.com:8317',
- 'login.custom_connection_hint': '默认使用当前访问地址,若需要可手动输入其他地址。',
- 'login.use_current_address': '使用当前地址',
- 'login.management_key_label': '管理密钥:',
- 'login.management_key_placeholder': '请输入管理密钥',
- 'login.connect_button': '连接',
- 'login.submit_button': '登录',
- 'login.submitting': '连接中...',
- 'login.error_title': '登录失败',
- 'login.error_required': '请填写完整的连接信息',
- 'login.error_invalid': '连接失败,请检查地址和密钥',
-
- // 头部导航
- 'header.check_connection': '检查连接',
- 'header.refresh_all': '刷新全部',
- 'header.logout': '登出',
-
- // 连接信息
- 'connection.title': '连接信息',
- 'connection.server_address': '服务器地址:',
- 'connection.management_key': '管理密钥:',
- 'connection.status': '连接状态:',
-
- // 侧边栏导航
- 'nav.basic_settings': '基础设置',
- 'nav.api_keys': 'API 密钥',
- 'nav.ai_providers': 'AI 提供商',
- 'nav.auth_files': '认证文件',
- 'nav.usage_stats': '使用统计',
- 'nav.system_info': '系统信息',
-
- // 基础设置
- 'basic_settings.title': '基础设置',
- 'basic_settings.debug_title': '调试模式',
- 'basic_settings.debug_enable': '启用调试模式',
- 'basic_settings.proxy_title': '代理设置',
- 'basic_settings.proxy_url_label': '代理 URL:',
- 'basic_settings.proxy_url_placeholder': '例如: socks5://user:pass@127.0.0.1:1080/',
- 'basic_settings.proxy_update': '更新',
- 'basic_settings.proxy_clear': '清空',
- 'basic_settings.retry_title': '请求重试',
- 'basic_settings.retry_count_label': '重试次数:',
- 'basic_settings.retry_update': '更新',
- 'basic_settings.quota_title': '配额超出行为',
- 'basic_settings.quota_switch_project': '自动切换项目',
- 'basic_settings.quota_switch_preview': '切换到预览模型',
-
- // API 密钥管理
- 'api_keys.title': 'API 密钥管理',
- 'api_keys.proxy_auth_title': '代理服务认证密钥',
- 'api_keys.add_button': '添加密钥',
- 'api_keys.empty_title': '暂无API密钥',
- 'api_keys.empty_desc': '点击上方按钮添加第一个密钥',
- 'api_keys.item_title': 'API密钥',
- 'api_keys.add_modal_title': '添加API密钥',
- 'api_keys.add_modal_key_label': 'API密钥:',
- 'api_keys.add_modal_key_placeholder': '请输入API密钥',
- 'api_keys.edit_modal_title': '编辑API密钥',
- 'api_keys.edit_modal_key_label': 'API密钥:',
- 'api_keys.delete_confirm': '确定要删除这个API密钥吗?',
-
- // AI 提供商
- 'ai_providers.title': 'AI 提供商配置',
- 'ai_providers.gemini_title': 'Gemini API 密钥',
- 'ai_providers.gemini_add_button': '添加密钥',
- 'ai_providers.gemini_empty_title': '暂无Gemini密钥',
- 'ai_providers.gemini_empty_desc': '点击上方按钮添加第一个密钥',
- 'ai_providers.gemini_item_title': 'Gemini密钥',
- 'ai_providers.gemini_add_modal_title': '添加Gemini API密钥',
- 'ai_providers.gemini_add_modal_key_label': 'API密钥:',
- 'ai_providers.gemini_add_modal_key_placeholder': '请输入Gemini API密钥',
- 'ai_providers.gemini_edit_modal_title': '编辑Gemini API密钥',
- 'ai_providers.gemini_edit_modal_key_label': 'API密钥:',
- 'ai_providers.gemini_delete_confirm': '确定要删除这个Gemini密钥吗?',
-
- 'ai_providers.codex_title': 'Codex API 配置',
- 'ai_providers.codex_add_button': '添加配置',
- 'ai_providers.codex_empty_title': '暂无Codex配置',
- 'ai_providers.codex_empty_desc': '点击上方按钮添加第一个配置',
- 'ai_providers.codex_item_title': 'Codex配置',
- 'ai_providers.codex_add_modal_title': '添加Codex API配置',
- 'ai_providers.codex_add_modal_key_label': 'API密钥:',
- 'ai_providers.codex_add_modal_key_placeholder': '请输入Codex API密钥',
- 'ai_providers.codex_add_modal_url_label': 'Base URL (可选):',
- 'ai_providers.codex_add_modal_url_placeholder': '例如: https://api.example.com',
- 'ai_providers.codex_edit_modal_title': '编辑Codex API配置',
- 'ai_providers.codex_edit_modal_key_label': 'API密钥:',
- 'ai_providers.codex_edit_modal_url_label': 'Base URL (可选):',
- 'ai_providers.codex_delete_confirm': '确定要删除这个Codex配置吗?',
-
- 'ai_providers.claude_title': 'Claude API 配置',
- 'ai_providers.claude_add_button': '添加配置',
- 'ai_providers.claude_empty_title': '暂无Claude配置',
- 'ai_providers.claude_empty_desc': '点击上方按钮添加第一个配置',
- 'ai_providers.claude_item_title': 'Claude配置',
- 'ai_providers.claude_add_modal_title': '添加Claude API配置',
- 'ai_providers.claude_add_modal_key_label': 'API密钥:',
- 'ai_providers.claude_add_modal_key_placeholder': '请输入Claude API密钥',
- 'ai_providers.claude_add_modal_url_label': 'Base URL (可选):',
- 'ai_providers.claude_add_modal_url_placeholder': '例如: https://api.anthropic.com',
- 'ai_providers.claude_edit_modal_title': '编辑Claude API配置',
- 'ai_providers.claude_edit_modal_key_label': 'API密钥:',
- 'ai_providers.claude_edit_modal_url_label': 'Base URL (可选):',
- 'ai_providers.claude_delete_confirm': '确定要删除这个Claude配置吗?',
-
- 'ai_providers.openai_title': 'OpenAI 兼容提供商',
- 'ai_providers.openai_add_button': '添加提供商',
- 'ai_providers.openai_empty_title': '暂无OpenAI兼容提供商',
- 'ai_providers.openai_empty_desc': '点击上方按钮添加第一个提供商',
- 'ai_providers.openai_add_modal_title': '添加OpenAI兼容提供商',
- 'ai_providers.openai_add_modal_name_label': '提供商名称:',
- 'ai_providers.openai_add_modal_name_placeholder': '例如: openrouter',
- 'ai_providers.openai_add_modal_url_label': 'Base URL:',
- 'ai_providers.openai_add_modal_url_placeholder': '例如: https://openrouter.ai/api/v1',
- 'ai_providers.openai_add_modal_keys_label': 'API密钥 (每行一个):',
- 'ai_providers.openai_add_modal_keys_placeholder': 'sk-key1\nsk-key2',
- 'ai_providers.openai_edit_modal_title': '编辑OpenAI兼容提供商',
- 'ai_providers.openai_edit_modal_name_label': '提供商名称:',
- 'ai_providers.openai_edit_modal_url_label': 'Base URL:',
- 'ai_providers.openai_edit_modal_keys_label': 'API密钥 (每行一个):',
- 'ai_providers.openai_delete_confirm': '确定要删除这个OpenAI提供商吗?',
- 'ai_providers.openai_keys_count': '密钥数量',
- 'ai_providers.openai_models_count': '模型数量',
-
-
- // 认证文件管理
- 'auth_files.title': '认证文件管理',
- 'auth_files.title_section': '认证文件',
- 'auth_files.description': '这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。',
- 'auth_files.upload_button': '上传文件',
- 'auth_files.delete_all_button': '删除全部',
- 'auth_files.empty_title': '暂无认证文件',
- 'auth_files.empty_desc': '点击上方按钮上传第一个文件',
- 'auth_files.file_size': '大小',
- 'auth_files.file_modified': '修改时间',
- 'auth_files.download_button': '下载',
- 'auth_files.delete_button': '删除',
- 'auth_files.delete_confirm': '确定要删除文件',
- 'auth_files.delete_all_confirm': '确定要删除所有认证文件吗?此操作不可恢复!',
- 'auth_files.upload_error_json': '只能上传JSON文件',
- 'auth_files.upload_success': '文件上传成功',
- 'auth_files.download_success': '文件下载成功',
- 'auth_files.delete_success': '文件删除成功',
- 'auth_files.delete_all_success': '成功删除',
- 'auth_files.files_count': '个文件',
-
- // Gemini Web Token
- 'auth_login.gemini_web_title': 'Gemini Web Token',
- 'auth_login.gemini_web_button': '保存 Gemini Web Token',
- 'auth_login.gemini_web_hint': '从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。',
- 'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:',
- 'auth_login.secure_1psid_placeholder': '输入 __Secure-1PSID cookie 值',
- 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:',
- 'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值',
- 'auth_login.gemini_web_label_label': '标签 (可选):',
- 'auth_login.gemini_web_label_placeholder': '输入标签名称 (可选)',
- 'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功',
-
- // 使用统计
- 'usage_stats.title': '使用统计',
- 'usage_stats.total_requests': '总请求数',
- 'usage_stats.success_requests': '成功请求',
- 'usage_stats.failed_requests': '失败请求',
- 'usage_stats.total_tokens': '总Token数',
- 'usage_stats.requests_trend': '请求趋势',
- 'usage_stats.tokens_trend': 'Token 使用趋势',
- 'usage_stats.api_details': 'API 详细统计',
- 'usage_stats.by_hour': '按小时',
- 'usage_stats.by_day': '按天',
- 'usage_stats.refresh': '刷新',
- 'usage_stats.no_data': '暂无数据',
- 'usage_stats.loading_error': '加载失败',
- 'usage_stats.api_endpoint': 'API端点',
- 'usage_stats.requests_count': '请求次数',
- 'usage_stats.tokens_count': 'Token数量',
- 'usage_stats.models': '模型统计',
- 'usage_stats.success_rate': '成功率',
-
- // 系统信息
- 'system_info.title': '系统信息',
- 'system_info.connection_status_title': '连接状态',
- 'system_info.api_status_label': 'API 状态:',
- 'system_info.config_status_label': '配置状态:',
- 'system_info.last_update_label': '最后更新:',
- 'system_info.cache_data': '缓存数据',
- 'system_info.real_time_data': '实时数据',
- 'system_info.not_loaded': '未加载',
- 'system_info.seconds_ago': '秒前',
-
- // 通知消息
- 'notification.debug_updated': '调试设置已更新',
- 'notification.proxy_updated': '代理设置已更新',
- 'notification.proxy_cleared': '代理设置已清空',
- 'notification.retry_updated': '重试设置已更新',
- 'notification.quota_switch_project_updated': '项目切换设置已更新',
- 'notification.quota_switch_preview_updated': '预览模型切换设置已更新',
- 'notification.api_key_added': 'API密钥添加成功',
- 'notification.api_key_updated': 'API密钥更新成功',
- 'notification.api_key_deleted': 'API密钥删除成功',
- 'notification.gemini_key_added': 'Gemini密钥添加成功',
- 'notification.gemini_key_updated': 'Gemini密钥更新成功',
- 'notification.gemini_key_deleted': 'Gemini密钥删除成功',
- 'notification.codex_config_added': 'Codex配置添加成功',
- 'notification.codex_config_updated': 'Codex配置更新成功',
- 'notification.codex_config_deleted': 'Codex配置删除成功',
- 'notification.claude_config_added': 'Claude配置添加成功',
- 'notification.claude_config_updated': 'Claude配置更新成功',
- 'notification.claude_config_deleted': 'Claude配置删除成功',
- 'notification.openai_provider_added': 'OpenAI提供商添加成功',
- 'notification.openai_provider_updated': 'OpenAI提供商更新成功',
- 'notification.openai_provider_deleted': 'OpenAI提供商删除成功',
- 'notification.data_refreshed': '数据刷新成功',
- 'notification.connection_required': '请先建立连接',
- 'notification.refresh_failed': '刷新失败',
- 'notification.update_failed': '更新失败',
- 'notification.add_failed': '添加失败',
- 'notification.delete_failed': '删除失败',
- 'notification.upload_failed': '上传失败',
- 'notification.download_failed': '下载失败',
- 'notification.login_failed': '登录失败',
- 'notification.please_enter': '请输入',
- 'notification.please_fill': '请填写',
- 'notification.provider_name_url': '提供商名称和Base URL',
- 'notification.api_key': 'API密钥',
- 'notification.gemini_api_key': 'Gemini API密钥',
- 'notification.codex_api_key': 'Codex API密钥',
- 'notification.claude_api_key': 'Claude API密钥',
-
- // 语言切换
- 'language.switch': '语言',
- 'language.chinese': '中文',
- 'language.english': 'English',
-
- // 主题切换
- 'theme.switch': '主题',
- 'theme.light': '亮色',
- 'theme.dark': '暗色',
- 'theme.switch_to_light': '切换到亮色模式',
- 'theme.switch_to_dark': '切换到暗色模式',
- 'theme.auto': '跟随系统',
-
- // 页脚
- 'footer.version': '版本',
- 'footer.author': '作者'
- },
-
- 'en-US': {
- // Common
- 'common.login': 'Login',
- 'common.logout': 'Logout',
- 'common.cancel': 'Cancel',
- 'common.confirm': 'Confirm',
- 'common.save': 'Save',
- 'common.delete': 'Delete',
- 'common.edit': 'Edit',
- 'common.add': 'Add',
- 'common.update': 'Update',
- 'common.refresh': 'Refresh',
- 'common.close': 'Close',
- 'common.success': 'Success',
- 'common.error': 'Error',
- 'common.info': 'Info',
- 'common.warning': 'Warning',
- 'common.loading': 'Loading...',
- 'common.connecting': 'Connecting...',
- 'common.connected': 'Connected',
- 'common.disconnected': 'Disconnected',
- 'common.connecting_status': 'Connecting',
- 'common.connected_status': 'Connected',
- 'common.disconnected_status': 'Disconnected',
- 'common.yes': 'Yes',
- 'common.no': 'No',
- 'common.optional': 'Optional',
- 'common.required': 'Required',
- 'common.api_key': 'Key',
- 'common.base_url': 'Address',
-
- // Page titles
- 'title.main': 'CLI Proxy API Management Center',
- 'title.login': 'CLI Proxy API Management Center',
-
- // Auto login
- 'auto_login.title': 'Auto Login in Progress...',
- 'auto_login.message': 'Attempting to connect to server using locally saved connection information',
-
- // Login page
- 'login.subtitle': 'Please enter connection information to access the management interface',
- 'login.connection_title': 'Connection Address',
- 'login.connection_current': 'Current URL',
- 'login.connection_auto_hint': 'The system will automatically use the current URL for connection',
- 'login.custom_connection_label': 'Custom Connection URL:',
- 'login.custom_connection_placeholder': 'Eg: https://example.com:8317',
- 'login.custom_connection_hint': 'By default the current URL is used. Override it here if needed.',
- 'login.use_current_address': 'Use Current URL',
- 'login.management_key_label': 'Management Key:',
- 'login.management_key_placeholder': 'Enter the management key',
- 'login.connect_button': 'Connect',
- 'login.submit_button': 'Login',
- 'login.submitting': 'Connecting...',
- 'login.error_title': 'Login Failed',
- 'login.error_required': 'Please fill in complete connection information',
- 'login.error_invalid': 'Connection failed, please check address and key',
-
- // Header navigation
- 'header.check_connection': 'Check Connection',
- 'header.refresh_all': 'Refresh All',
- 'header.logout': 'Logout',
-
- // Connection info
- 'connection.title': 'Connection Information',
- 'connection.server_address': 'Server Address:',
- 'connection.management_key': 'Management Key:',
- 'connection.status': 'Connection Status:',
-
- // Sidebar navigation
- 'nav.basic_settings': 'Basic Settings',
- 'nav.api_keys': 'API Keys',
- 'nav.ai_providers': 'AI Providers',
- 'nav.auth_files': 'Auth Files',
- 'nav.usage_stats': 'Usage Statistics',
- 'nav.system_info': 'System Info',
-
- // Basic settings
- 'basic_settings.title': 'Basic Settings',
- 'basic_settings.debug_title': 'Debug Mode',
- 'basic_settings.debug_enable': 'Enable Debug Mode',
- 'basic_settings.proxy_title': 'Proxy Settings',
- 'basic_settings.proxy_url_label': 'Proxy URL:',
- 'basic_settings.proxy_url_placeholder': 'e.g.: socks5://user:pass@127.0.0.1:1080/',
- 'basic_settings.proxy_update': 'Update',
- 'basic_settings.proxy_clear': 'Clear',
- 'basic_settings.retry_title': 'Request Retry',
- 'basic_settings.retry_count_label': 'Retry Count:',
- 'basic_settings.retry_update': 'Update',
- 'basic_settings.quota_title': 'Quota Exceeded Behavior',
- 'basic_settings.quota_switch_project': 'Auto Switch Project',
- 'basic_settings.quota_switch_preview': 'Switch to Preview Model',
-
- // API Keys management
- 'api_keys.title': 'API Keys Management',
- 'api_keys.proxy_auth_title': 'Proxy Service Authentication Keys',
- 'api_keys.add_button': 'Add Key',
- 'api_keys.empty_title': 'No API Keys',
- 'api_keys.empty_desc': 'Click the button above to add the first key',
- 'api_keys.item_title': 'API Key',
- 'api_keys.add_modal_title': 'Add API Key',
- 'api_keys.add_modal_key_label': 'API Key:',
- 'api_keys.add_modal_key_placeholder': 'Please enter API key',
- 'api_keys.edit_modal_title': 'Edit API Key',
- 'api_keys.edit_modal_key_label': 'API Key:',
- 'api_keys.delete_confirm': 'Are you sure you want to delete this API key?',
-
- // AI Providers
- 'ai_providers.title': 'AI Providers Configuration',
- 'ai_providers.gemini_title': 'Gemini API Keys',
- 'ai_providers.gemini_add_button': 'Add Key',
- 'ai_providers.gemini_empty_title': 'No Gemini Keys',
- 'ai_providers.gemini_empty_desc': 'Click the button above to add the first key',
- 'ai_providers.gemini_item_title': 'Gemini Key',
- 'ai_providers.gemini_add_modal_title': 'Add Gemini API Key',
- 'ai_providers.gemini_add_modal_key_label': 'API Key:',
- 'ai_providers.gemini_add_modal_key_placeholder': 'Please enter Gemini API key',
- 'ai_providers.gemini_edit_modal_title': 'Edit Gemini API Key',
- 'ai_providers.gemini_edit_modal_key_label': 'API Key:',
- 'ai_providers.gemini_delete_confirm': 'Are you sure you want to delete this Gemini key?',
-
- 'ai_providers.codex_title': 'Codex API Configuration',
- 'ai_providers.codex_add_button': 'Add Configuration',
- 'ai_providers.codex_empty_title': 'No Codex Configuration',
- 'ai_providers.codex_empty_desc': 'Click the button above to add the first configuration',
- 'ai_providers.codex_item_title': 'Codex Configuration',
- 'ai_providers.codex_add_modal_title': 'Add Codex API Configuration',
- 'ai_providers.codex_add_modal_key_label': 'API Key:',
- 'ai_providers.codex_add_modal_key_placeholder': 'Please enter Codex API key',
- 'ai_providers.codex_add_modal_url_label': 'Base URL (Optional):',
- 'ai_providers.codex_add_modal_url_placeholder': 'e.g.: https://api.example.com',
- 'ai_providers.codex_edit_modal_title': 'Edit Codex API Configuration',
- 'ai_providers.codex_edit_modal_key_label': 'API Key:',
- 'ai_providers.codex_edit_modal_url_label': 'Base URL (Optional):',
- 'ai_providers.codex_delete_confirm': 'Are you sure you want to delete this Codex configuration?',
-
- 'ai_providers.claude_title': 'Claude API Configuration',
- 'ai_providers.claude_add_button': 'Add Configuration',
- 'ai_providers.claude_empty_title': 'No Claude Configuration',
- 'ai_providers.claude_empty_desc': 'Click the button above to add the first configuration',
- 'ai_providers.claude_item_title': 'Claude Configuration',
- 'ai_providers.claude_add_modal_title': 'Add Claude API Configuration',
- 'ai_providers.claude_add_modal_key_label': 'API Key:',
- 'ai_providers.claude_add_modal_key_placeholder': 'Please enter Claude API key',
- 'ai_providers.claude_add_modal_url_label': 'Base URL (Optional):',
- 'ai_providers.claude_add_modal_url_placeholder': 'e.g.: https://api.anthropic.com',
- 'ai_providers.claude_edit_modal_title': 'Edit Claude API Configuration',
- 'ai_providers.claude_edit_modal_key_label': 'API Key:',
- 'ai_providers.claude_edit_modal_url_label': 'Base URL (Optional):',
- 'ai_providers.claude_delete_confirm': 'Are you sure you want to delete this Claude configuration?',
-
- 'ai_providers.openai_title': 'OpenAI Compatible Providers',
- 'ai_providers.openai_add_button': 'Add Provider',
- 'ai_providers.openai_empty_title': 'No OpenAI Compatible Providers',
- 'ai_providers.openai_empty_desc': 'Click the button above to add the first provider',
- 'ai_providers.openai_add_modal_title': 'Add OpenAI Compatible Provider',
- 'ai_providers.openai_add_modal_name_label': 'Provider Name:',
- 'ai_providers.openai_add_modal_name_placeholder': 'e.g.: openrouter',
- 'ai_providers.openai_add_modal_url_label': 'Base URL:',
- 'ai_providers.openai_add_modal_url_placeholder': 'e.g.: https://openrouter.ai/api/v1',
- 'ai_providers.openai_add_modal_keys_label': 'API Keys (one per line):',
- 'ai_providers.openai_add_modal_keys_placeholder': 'sk-key1\nsk-key2',
- 'ai_providers.openai_edit_modal_title': 'Edit OpenAI Compatible Provider',
- 'ai_providers.openai_edit_modal_name_label': 'Provider Name:',
- 'ai_providers.openai_edit_modal_url_label': 'Base URL:',
- 'ai_providers.openai_edit_modal_keys_label': 'API Keys (one per line):',
- 'ai_providers.openai_delete_confirm': 'Are you sure you want to delete this OpenAI provider?',
- 'ai_providers.openai_keys_count': 'Keys Count',
- 'ai_providers.openai_models_count': 'Models Count',
-
-
- // Auth files management
- 'auth_files.title': 'Auth Files Management',
- 'auth_files.title_section': 'Auth Files',
- 'auth_files.description': 'Here you can manage authentication configuration files for Qwen and Gemini. Upload JSON format authentication files to enable the corresponding AI services.',
- 'auth_files.upload_button': 'Upload File',
- 'auth_files.delete_all_button': 'Delete All',
- 'auth_files.empty_title': 'No Auth Files',
- 'auth_files.empty_desc': 'Click the button above to upload the first file',
- 'auth_files.file_size': 'Size',
- 'auth_files.file_modified': 'Modified',
- 'auth_files.download_button': 'Download',
- 'auth_files.delete_button': 'Delete',
- 'auth_files.delete_confirm': 'Are you sure you want to delete file',
- 'auth_files.delete_all_confirm': 'Are you sure you want to delete all auth files? This operation cannot be undone!',
- 'auth_files.upload_error_json': 'Only JSON files are allowed',
- 'auth_files.upload_success': 'File uploaded successfully',
- 'auth_files.download_success': 'File downloaded successfully',
- 'auth_files.delete_success': 'File deleted successfully',
- 'auth_files.delete_all_success': 'Successfully deleted',
- 'auth_files.files_count': 'files',
-
- // Gemini Web Token
- 'auth_login.gemini_web_title': 'Gemini Web Token',
- 'auth_login.gemini_web_button': 'Save Gemini Web Token',
- 'auth_login.gemini_web_hint': 'Obtain the Cookie value of the Gemini web version from the browser\'s developer tools, used for direct authentication to access Gemini.',
- 'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:',
- 'auth_login.secure_1psid_placeholder': 'Enter __Secure-1PSID cookie value',
- 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:',
- 'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value',
- 'auth_login.gemini_web_label_label': 'Label (Optional):',
- 'auth_login.gemini_web_label_placeholder': 'Enter label name (optional)',
- 'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully',
-
- // Usage Statistics
- 'usage_stats.title': 'Usage Statistics',
- 'usage_stats.total_requests': 'Total Requests',
- 'usage_stats.success_requests': 'Success Requests',
- 'usage_stats.failed_requests': 'Failed Requests',
- 'usage_stats.total_tokens': 'Total Tokens',
- 'usage_stats.requests_trend': 'Request Trends',
- 'usage_stats.tokens_trend': 'Token Usage Trends',
- 'usage_stats.api_details': 'API Details',
- 'usage_stats.by_hour': 'By Hour',
- 'usage_stats.by_day': 'By Day',
- 'usage_stats.refresh': 'Refresh',
- 'usage_stats.no_data': 'No Data Available',
- 'usage_stats.loading_error': 'Loading Failed',
- 'usage_stats.api_endpoint': 'API Endpoint',
- 'usage_stats.requests_count': 'Request Count',
- 'usage_stats.tokens_count': 'Token Count',
- 'usage_stats.models': 'Model Statistics',
- 'usage_stats.success_rate': 'Success Rate',
-
- // System info
- 'system_info.title': 'System Information',
- 'system_info.connection_status_title': 'Connection Status',
- 'system_info.api_status_label': 'API Status:',
- 'system_info.config_status_label': 'Config Status:',
- 'system_info.last_update_label': 'Last Update:',
- 'system_info.cache_data': 'Cache Data',
- 'system_info.real_time_data': 'Real-time Data',
- 'system_info.not_loaded': 'Not Loaded',
- 'system_info.seconds_ago': 'seconds ago',
-
- // Notification messages
- 'notification.debug_updated': 'Debug settings updated',
- 'notification.proxy_updated': 'Proxy settings updated',
- 'notification.proxy_cleared': 'Proxy settings cleared',
- 'notification.retry_updated': 'Retry settings updated',
- 'notification.quota_switch_project_updated': 'Project switch settings updated',
- 'notification.quota_switch_preview_updated': 'Preview model switch settings updated',
- 'notification.api_key_added': 'API key added successfully',
- 'notification.api_key_updated': 'API key updated successfully',
- 'notification.api_key_deleted': 'API key deleted successfully',
- 'notification.gemini_key_added': 'Gemini key added successfully',
- 'notification.gemini_key_updated': 'Gemini key updated successfully',
- 'notification.gemini_key_deleted': 'Gemini key deleted successfully',
- 'notification.codex_config_added': 'Codex configuration added successfully',
- 'notification.codex_config_updated': 'Codex configuration updated successfully',
- 'notification.codex_config_deleted': 'Codex configuration deleted successfully',
- 'notification.claude_config_added': 'Claude configuration added successfully',
- 'notification.claude_config_updated': 'Claude configuration updated successfully',
- 'notification.claude_config_deleted': 'Claude configuration deleted successfully',
- 'notification.openai_provider_added': 'OpenAI provider added successfully',
- 'notification.openai_provider_updated': 'OpenAI provider updated successfully',
- 'notification.openai_provider_deleted': 'OpenAI provider deleted successfully',
- 'notification.data_refreshed': 'Data refreshed successfully',
- 'notification.connection_required': 'Please establish connection first',
- 'notification.refresh_failed': 'Refresh failed',
- 'notification.update_failed': 'Update failed',
- 'notification.add_failed': 'Add failed',
- 'notification.delete_failed': 'Delete failed',
- 'notification.upload_failed': 'Upload failed',
- 'notification.download_failed': 'Download failed',
- 'notification.login_failed': 'Login failed',
- 'notification.please_enter': 'Please enter',
- 'notification.please_fill': 'Please fill',
- 'notification.provider_name_url': 'provider name and Base URL',
- 'notification.api_key': 'API key',
- 'notification.gemini_api_key': 'Gemini API key',
- 'notification.codex_api_key': 'Codex API key',
- 'notification.claude_api_key': 'Claude API key',
-
- // Language switch
- 'language.switch': 'Language',
- 'language.chinese': '中文',
- 'language.english': 'English',
-
- // Theme switch
- 'theme.switch': 'Theme',
- 'theme.light': 'Light',
- 'theme.dark': 'Dark',
- 'theme.switch_to_light': 'Switch to light mode',
- 'theme.switch_to_dark': 'Switch to dark mode',
- 'theme.auto': 'Follow system',
-
- // Footer
- 'footer.version': 'Version',
- 'footer.author': 'Author'
- }
- },
-
- // 获取翻译文本
- t(key, params = {}) {
- const translation = this.translations[this.currentLanguage]?.[key] ||
- this.translations[this.fallbackLanguage]?.[key] ||
- key;
-
- // 简单的参数替换
- return translation.replace(/\{(\w+)\}/g, (match, param) => {
- return params[param] || match;
- });
- },
-
- // 设置语言
- setLanguage(lang) {
- if (this.translations[lang]) {
- this.currentLanguage = lang;
- localStorage.setItem('preferredLanguage', lang);
- this.updatePageLanguage();
- this.updateAllTexts();
- }
- },
-
- // 更新页面语言属性
- updatePageLanguage() {
- document.documentElement.lang = this.currentLanguage;
- },
-
- // 更新所有文本
- updateAllTexts() {
- // 更新所有带有 data-i18n 属性的元素
- document.querySelectorAll('[data-i18n]').forEach(element => {
- const key = element.getAttribute('data-i18n');
- const text = this.t(key);
-
- if (element.tagName === 'INPUT' && (element.type === 'text' || element.type === 'password')) {
- element.placeholder = text;
- } else if (element.tagName === 'TITLE') {
- element.textContent = text;
- } else {
- element.textContent = text;
- }
- });
-
- // 更新所有带有 data-i18n-html 属性的元素(支持HTML)
- document.querySelectorAll('[data-i18n-html]').forEach(element => {
- const key = element.getAttribute('data-i18n-html');
- const html = this.t(key);
- element.innerHTML = html;
- });
- },
-
- // 初始化
- init() {
- // 从本地存储获取用户偏好语言
- const savedLanguage = localStorage.getItem('preferredLanguage');
- if (savedLanguage && this.translations[savedLanguage]) {
- this.currentLanguage = savedLanguage;
- } else {
- // 根据浏览器语言自动选择
- const browserLang = navigator.language || navigator.userLanguage;
- if (browserLang.startsWith('zh')) {
- this.currentLanguage = 'zh-CN';
- } else {
- this.currentLanguage = 'en-US';
- }
- }
-
- this.updatePageLanguage();
- this.updateAllTexts();
- }
-};
-
-// 全局函数,供HTML调用
-window.t = (key, params) => i18n.t(key, params);
-window.setLanguage = (lang) => i18n.setLanguage(lang);
+// 国际化语言包
+const i18n = {
+ // 语言配置
+ currentLanguage: 'zh-CN',
+ fallbackLanguage: 'zh-CN',
+
+ // 语言包
+ translations: {
+ 'zh-CN': {
+ // 通用
+ 'common.login': '登录',
+ 'common.logout': '登出',
+ 'common.cancel': '取消',
+ 'common.confirm': '确认',
+ 'common.save': '保存',
+ 'common.delete': '删除',
+ 'common.edit': '编辑',
+ 'common.add': '添加',
+ 'common.update': '更新',
+ 'common.refresh': '刷新',
+ 'common.close': '关闭',
+ 'common.success': '成功',
+ 'common.error': '错误',
+ 'common.info': '信息',
+ 'common.warning': '警告',
+ 'common.loading': '加载中...',
+ 'common.connecting': '连接中...',
+ 'common.connected': '已连接',
+ 'common.disconnected': '未连接',
+ 'common.connecting_status': '连接中',
+ 'common.connected_status': '已连接',
+ 'common.disconnected_status': '未连接',
+ 'common.yes': '是',
+ 'common.no': '否',
+ 'common.optional': '可选',
+ 'common.required': '必填',
+ 'common.api_key': '密钥',
+ 'common.base_url': '地址',
+ 'common.proxy_url': '代理',
+ 'common.alias': '别名',
+
+ // 页面标题
+ 'title.main': 'CLI Proxy API Management Center',
+ 'title.login': 'CLI Proxy API Management Center',
+
+ // 自动登录
+ 'auto_login.title': '正在自动登录...',
+ 'auto_login.message': '正在使用本地保存的连接信息尝试连接服务器',
+
+ // 登录页面
+ 'login.subtitle': '请输入连接信息以访问管理界面',
+ 'login.connection_title': '连接地址',
+ 'login.connection_current': '当前地址',
+ 'login.connection_auto_hint': '系统将自动使用当前访问地址进行连接',
+ 'login.custom_connection_label': '自定义连接地址:',
+ 'login.custom_connection_placeholder': '例如: https://example.com:8317',
+ 'login.custom_connection_hint': '默认使用当前访问地址,若需要可手动输入其他地址。',
+ 'login.use_current_address': '使用当前地址',
+ 'login.management_key_label': '管理密钥:',
+ 'login.management_key_placeholder': '请输入管理密钥',
+ 'login.connect_button': '连接',
+ 'login.submit_button': '登录',
+ 'login.submitting': '连接中...',
+ 'login.error_title': '登录失败',
+ 'login.error_required': '请填写完整的连接信息',
+ 'login.error_invalid': '连接失败,请检查地址和密钥',
+
+ // 头部导航
+ 'header.check_connection': '检查连接',
+ 'header.refresh_all': '刷新全部',
+ 'header.logout': '登出',
+
+ // 连接信息
+ 'connection.title': '连接信息',
+ 'connection.server_address': '服务器地址:',
+ 'connection.management_key': '管理密钥:',
+ 'connection.status': '连接状态:',
+
+ // 侧边栏导航
+ 'nav.basic_settings': '基础设置',
+ 'nav.api_keys': 'API 密钥',
+ 'nav.ai_providers': 'AI 提供商',
+ 'nav.auth_files': '认证文件',
+ 'nav.usage_stats': '使用统计',
+ 'nav.system_info': '系统信息',
+
+ // 基础设置
+ 'basic_settings.title': '基础设置',
+ 'basic_settings.debug_title': '调试模式',
+ 'basic_settings.debug_enable': '启用调试模式',
+ 'basic_settings.proxy_title': '代理设置',
+ 'basic_settings.proxy_url_label': '代理 URL:',
+ 'basic_settings.proxy_url_placeholder': '例如: socks5://user:pass@127.0.0.1:1080/',
+ 'basic_settings.proxy_update': '更新',
+ 'basic_settings.proxy_clear': '清空',
+ 'basic_settings.retry_title': '请求重试',
+ 'basic_settings.retry_count_label': '重试次数:',
+ 'basic_settings.retry_update': '更新',
+ 'basic_settings.quota_title': '配额超出行为',
+ 'basic_settings.quota_switch_project': '自动切换项目',
+ 'basic_settings.quota_switch_preview': '切换到预览模型',
+
+ // API 密钥管理
+ 'api_keys.title': 'API 密钥管理',
+ 'api_keys.proxy_auth_title': '代理服务认证密钥',
+ 'api_keys.add_button': '添加密钥',
+ 'api_keys.empty_title': '暂无API密钥',
+ 'api_keys.empty_desc': '点击上方按钮添加第一个密钥',
+ 'api_keys.item_title': 'API密钥',
+ 'api_keys.add_modal_title': '添加API密钥',
+ 'api_keys.add_modal_key_label': 'API密钥:',
+ 'api_keys.add_modal_key_placeholder': '请输入API密钥',
+ 'api_keys.edit_modal_title': '编辑API密钥',
+ 'api_keys.edit_modal_key_label': 'API密钥:',
+ 'api_keys.delete_confirm': '确定要删除这个API密钥吗?',
+
+ // AI 提供商
+ 'ai_providers.title': 'AI 提供商配置',
+ 'ai_providers.gemini_title': 'Gemini API 密钥',
+ 'ai_providers.gemini_add_button': '添加密钥',
+ 'ai_providers.gemini_empty_title': '暂无Gemini密钥',
+ 'ai_providers.gemini_empty_desc': '点击上方按钮添加第一个密钥',
+ 'ai_providers.gemini_item_title': 'Gemini密钥',
+ 'ai_providers.gemini_add_modal_title': '添加Gemini API密钥',
+ 'ai_providers.gemini_add_modal_key_label': 'API密钥:',
+ 'ai_providers.gemini_add_modal_key_placeholder': '请输入Gemini API密钥',
+ 'ai_providers.gemini_edit_modal_title': '编辑Gemini API密钥',
+ 'ai_providers.gemini_edit_modal_key_label': 'API密钥:',
+ 'ai_providers.gemini_delete_confirm': '确定要删除这个Gemini密钥吗?',
+
+ 'ai_providers.codex_title': 'Codex API 配置',
+ 'ai_providers.codex_add_button': '添加配置',
+ 'ai_providers.codex_empty_title': '暂无Codex配置',
+ 'ai_providers.codex_empty_desc': '点击上方按钮添加第一个配置',
+ 'ai_providers.codex_item_title': 'Codex配置',
+ 'ai_providers.codex_add_modal_title': '添加Codex API配置',
+ 'ai_providers.codex_add_modal_key_label': 'API密钥:',
+ 'ai_providers.codex_add_modal_key_placeholder': '请输入Codex API密钥',
+ 'ai_providers.codex_add_modal_url_label': 'Base URL (可选):',
+ 'ai_providers.codex_add_modal_url_placeholder': '例如: https://api.example.com',
+ 'ai_providers.codex_add_modal_proxy_label': '代理 URL (可选):',
+ 'ai_providers.codex_add_modal_proxy_placeholder': '例如: socks5://proxy.example.com:1080',
+ 'ai_providers.codex_edit_modal_title': '编辑Codex API配置',
+ 'ai_providers.codex_edit_modal_key_label': 'API密钥:',
+ 'ai_providers.codex_edit_modal_url_label': 'Base URL (可选):',
+ 'ai_providers.codex_edit_modal_proxy_label': '代理 URL (可选):',
+ 'ai_providers.codex_delete_confirm': '确定要删除这个Codex配置吗?',
+
+ 'ai_providers.claude_title': 'Claude API 配置',
+ 'ai_providers.claude_add_button': '添加配置',
+ 'ai_providers.claude_empty_title': '暂无Claude配置',
+ 'ai_providers.claude_empty_desc': '点击上方按钮添加第一个配置',
+ 'ai_providers.claude_item_title': 'Claude配置',
+ 'ai_providers.claude_add_modal_title': '添加Claude API配置',
+ 'ai_providers.claude_add_modal_key_label': 'API密钥:',
+ 'ai_providers.claude_add_modal_key_placeholder': '请输入Claude API密钥',
+ 'ai_providers.claude_add_modal_url_label': 'Base URL (可选):',
+ 'ai_providers.claude_add_modal_url_placeholder': '例如: https://api.anthropic.com',
+ 'ai_providers.claude_add_modal_proxy_label': '代理 URL (可选):',
+ 'ai_providers.claude_add_modal_proxy_placeholder': '例如: socks5://proxy.example.com:1080',
+ 'ai_providers.claude_edit_modal_title': '编辑Claude API配置',
+ 'ai_providers.claude_edit_modal_key_label': 'API密钥:',
+ 'ai_providers.claude_edit_modal_url_label': 'Base URL (可选):',
+ 'ai_providers.claude_edit_modal_proxy_label': '代理 URL (可选):',
+ 'ai_providers.claude_delete_confirm': '确定要删除这个Claude配置吗?',
+
+ 'ai_providers.openai_title': 'OpenAI 兼容提供商',
+ 'ai_providers.openai_add_button': '添加提供商',
+ 'ai_providers.openai_empty_title': '暂无OpenAI兼容提供商',
+ 'ai_providers.openai_empty_desc': '点击上方按钮添加第一个提供商',
+ 'ai_providers.openai_add_modal_title': '添加OpenAI兼容提供商',
+ 'ai_providers.openai_add_modal_name_label': '提供商名称:',
+ 'ai_providers.openai_add_modal_name_placeholder': '例如: openrouter',
+ 'ai_providers.openai_add_modal_url_label': 'Base URL:',
+ 'ai_providers.openai_add_modal_url_placeholder': '例如: https://openrouter.ai/api/v1',
+ 'ai_providers.openai_add_modal_keys_label': 'API密钥 (每行一个):',
+ 'ai_providers.openai_add_modal_keys_placeholder': 'sk-key1\nsk-key2',
+ 'ai_providers.openai_add_modal_keys_proxy_label': '代理 URL (按行对应,可选):',
+ 'ai_providers.openai_add_modal_keys_proxy_placeholder': 'socks5://proxy.example.com:1080\n',
+ 'ai_providers.openai_add_modal_models_label': '模型列表 (name[, alias] 每行一个):',
+ 'ai_providers.openai_models_hint': '示例:gpt-4o-mini 或 moonshotai/kimi-k2:free, kimi-k2',
+ 'ai_providers.openai_model_name_placeholder': '模型名称,如 moonshotai/kimi-k2:free',
+ 'ai_providers.openai_model_alias_placeholder': '模型别名 (可选)',
+ 'ai_providers.openai_models_add_btn': '添加模型',
+ 'ai_providers.openai_edit_modal_title': '编辑OpenAI兼容提供商',
+ 'ai_providers.openai_edit_modal_name_label': '提供商名称:',
+ 'ai_providers.openai_edit_modal_url_label': 'Base URL:',
+ 'ai_providers.openai_edit_modal_keys_label': 'API密钥 (每行一个):',
+ 'ai_providers.openai_edit_modal_keys_proxy_label': '代理 URL (按行对应,可选):',
+ 'ai_providers.openai_edit_modal_models_label': '模型列表 (name[, alias] 每行一个):',
+ 'ai_providers.openai_delete_confirm': '确定要删除这个OpenAI提供商吗?',
+ 'ai_providers.openai_keys_count': '密钥数量',
+ 'ai_providers.openai_models_count': '模型数量',
+
+
+ // 认证文件管理
+ 'auth_files.title': '认证文件管理',
+ 'auth_files.title_section': '认证文件',
+ 'auth_files.description': '这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。',
+ 'auth_files.upload_button': '上传文件',
+ 'auth_files.delete_all_button': '删除全部',
+ 'auth_files.empty_title': '暂无认证文件',
+ 'auth_files.empty_desc': '点击上方按钮上传第一个文件',
+ 'auth_files.file_size': '大小',
+ 'auth_files.file_modified': '修改时间',
+ 'auth_files.download_button': '下载',
+ 'auth_files.delete_button': '删除',
+ 'auth_files.delete_confirm': '确定要删除文件',
+ 'auth_files.delete_all_confirm': '确定要删除所有认证文件吗?此操作不可恢复!',
+ 'auth_files.upload_error_json': '只能上传JSON文件',
+ 'auth_files.upload_success': '文件上传成功',
+ 'auth_files.download_success': '文件下载成功',
+ 'auth_files.delete_success': '文件删除成功',
+ 'auth_files.delete_all_success': '成功删除',
+ 'auth_files.files_count': '个文件',
+
+ // Gemini Web Token
+ 'auth_login.gemini_web_title': 'Gemini Web Token',
+ 'auth_login.gemini_web_button': '保存 Gemini Web Token',
+ 'auth_login.gemini_web_hint': '从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。',
+ 'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:',
+ 'auth_login.secure_1psid_placeholder': '输入 __Secure-1PSID cookie 值',
+ 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:',
+ 'auth_login.secure_1psidts_placeholder': '输入 __Secure-1PSIDTS cookie 值',
+ 'auth_login.gemini_web_label_label': '标签 (可选):',
+ 'auth_login.gemini_web_label_placeholder': '输入标签名称 (可选)',
+ 'auth_login.gemini_web_saved': 'Gemini Web Token 保存成功',
+
+ // 使用统计
+ 'usage_stats.title': '使用统计',
+ 'usage_stats.total_requests': '总请求数',
+ 'usage_stats.success_requests': '成功请求',
+ 'usage_stats.failed_requests': '失败请求',
+ 'usage_stats.total_tokens': '总Token数',
+ 'usage_stats.requests_trend': '请求趋势',
+ 'usage_stats.tokens_trend': 'Token 使用趋势',
+ 'usage_stats.api_details': 'API 详细统计',
+ 'usage_stats.by_hour': '按小时',
+ 'usage_stats.by_day': '按天',
+ 'usage_stats.refresh': '刷新',
+ 'usage_stats.no_data': '暂无数据',
+ 'usage_stats.loading_error': '加载失败',
+ 'usage_stats.api_endpoint': 'API端点',
+ 'usage_stats.requests_count': '请求次数',
+ 'usage_stats.tokens_count': 'Token数量',
+ 'usage_stats.models': '模型统计',
+ 'usage_stats.success_rate': '成功率',
+
+ // 系统信息
+ 'system_info.title': '系统信息',
+ 'system_info.connection_status_title': '连接状态',
+ 'system_info.api_status_label': 'API 状态:',
+ 'system_info.config_status_label': '配置状态:',
+ 'system_info.last_update_label': '最后更新:',
+ 'system_info.cache_data': '缓存数据',
+ 'system_info.real_time_data': '实时数据',
+ 'system_info.not_loaded': '未加载',
+ 'system_info.seconds_ago': '秒前',
+
+ // 通知消息
+ 'notification.debug_updated': '调试设置已更新',
+ 'notification.proxy_updated': '代理设置已更新',
+ 'notification.proxy_cleared': '代理设置已清空',
+ 'notification.retry_updated': '重试设置已更新',
+ 'notification.quota_switch_project_updated': '项目切换设置已更新',
+ 'notification.quota_switch_preview_updated': '预览模型切换设置已更新',
+ 'notification.api_key_added': 'API密钥添加成功',
+ 'notification.api_key_updated': 'API密钥更新成功',
+ 'notification.api_key_deleted': 'API密钥删除成功',
+ 'notification.gemini_key_added': 'Gemini密钥添加成功',
+ 'notification.gemini_key_updated': 'Gemini密钥更新成功',
+ 'notification.gemini_key_deleted': 'Gemini密钥删除成功',
+ 'notification.codex_config_added': 'Codex配置添加成功',
+ 'notification.codex_config_updated': 'Codex配置更新成功',
+ 'notification.codex_config_deleted': 'Codex配置删除成功',
+ 'notification.claude_config_added': 'Claude配置添加成功',
+ 'notification.claude_config_updated': 'Claude配置更新成功',
+ 'notification.claude_config_deleted': 'Claude配置删除成功',
+ 'notification.field_required': '必填字段不能为空',
+ 'notification.openai_provider_required': '请填写提供商名称和Base URL',
+ 'notification.openai_provider_added': 'OpenAI提供商添加成功',
+ 'notification.openai_provider_updated': 'OpenAI提供商更新成功',
+ 'notification.openai_provider_deleted': 'OpenAI提供商删除成功',
+ 'notification.openai_model_name_required': '请填写模型名称',
+ 'notification.data_refreshed': '数据刷新成功',
+ 'notification.connection_required': '请先建立连接',
+ 'notification.refresh_failed': '刷新失败',
+ 'notification.update_failed': '更新失败',
+ 'notification.add_failed': '添加失败',
+ 'notification.delete_failed': '删除失败',
+ 'notification.upload_failed': '上传失败',
+ 'notification.download_failed': '下载失败',
+ 'notification.login_failed': '登录失败',
+ 'notification.please_enter': '请输入',
+ 'notification.please_fill': '请填写',
+ 'notification.provider_name_url': '提供商名称和Base URL',
+ 'notification.api_key': 'API密钥',
+ 'notification.gemini_api_key': 'Gemini API密钥',
+ 'notification.codex_api_key': 'Codex API密钥',
+ 'notification.claude_api_key': 'Claude API密钥',
+
+ // 语言切换
+ 'language.switch': '语言',
+ 'language.chinese': '中文',
+ 'language.english': 'English',
+
+ // 主题切换
+ 'theme.switch': '主题',
+ 'theme.light': '亮色',
+ 'theme.dark': '暗色',
+ 'theme.switch_to_light': '切换到亮色模式',
+ 'theme.switch_to_dark': '切换到暗色模式',
+ 'theme.auto': '跟随系统',
+
+ // 页脚
+ 'footer.version': '版本',
+ 'footer.author': '作者'
+ },
+
+ 'en-US': {
+ // Common
+ 'common.login': 'Login',
+ 'common.logout': 'Logout',
+ 'common.cancel': 'Cancel',
+ 'common.confirm': 'Confirm',
+ 'common.save': 'Save',
+ 'common.delete': 'Delete',
+ 'common.edit': 'Edit',
+ 'common.add': 'Add',
+ 'common.update': 'Update',
+ 'common.refresh': 'Refresh',
+ 'common.close': 'Close',
+ 'common.success': 'Success',
+ 'common.error': 'Error',
+ 'common.info': 'Info',
+ 'common.warning': 'Warning',
+ 'common.loading': 'Loading...',
+ 'common.connecting': 'Connecting...',
+ 'common.connected': 'Connected',
+ 'common.disconnected': 'Disconnected',
+ 'common.connecting_status': 'Connecting',
+ 'common.connected_status': 'Connected',
+ 'common.disconnected_status': 'Disconnected',
+ 'common.yes': 'Yes',
+ 'common.no': 'No',
+ 'common.optional': 'Optional',
+ 'common.required': 'Required',
+ 'common.api_key': 'Key',
+ 'common.base_url': 'Address',
+
+ // Page titles
+ 'title.main': 'CLI Proxy API Management Center',
+ 'title.login': 'CLI Proxy API Management Center',
+
+ // Auto login
+ 'auto_login.title': 'Auto Login in Progress...',
+ 'auto_login.message': 'Attempting to connect to server using locally saved connection information',
+
+ // Login page
+ 'login.subtitle': 'Please enter connection information to access the management interface',
+ 'login.connection_title': 'Connection Address',
+ 'login.connection_current': 'Current URL',
+ 'login.connection_auto_hint': 'The system will automatically use the current URL for connection',
+ 'login.custom_connection_label': 'Custom Connection URL:',
+ 'login.custom_connection_placeholder': 'Eg: https://example.com:8317',
+ 'login.custom_connection_hint': 'By default the current URL is used. Override it here if needed.',
+ 'login.use_current_address': 'Use Current URL',
+ 'login.management_key_label': 'Management Key:',
+ 'login.management_key_placeholder': 'Enter the management key',
+ 'login.connect_button': 'Connect',
+ 'login.submit_button': 'Login',
+ 'login.submitting': 'Connecting...',
+ 'login.error_title': 'Login Failed',
+ 'login.error_required': 'Please fill in complete connection information',
+ 'login.error_invalid': 'Connection failed, please check address and key',
+
+ // Header navigation
+ 'header.check_connection': 'Check Connection',
+ 'header.refresh_all': 'Refresh All',
+ 'header.logout': 'Logout',
+
+ // Connection info
+ 'connection.title': 'Connection Information',
+ 'connection.server_address': 'Server Address:',
+ 'connection.management_key': 'Management Key:',
+ 'connection.status': 'Connection Status:',
+
+ // Sidebar navigation
+ 'nav.basic_settings': 'Basic Settings',
+ 'nav.api_keys': 'API Keys',
+ 'nav.ai_providers': 'AI Providers',
+ 'nav.auth_files': 'Auth Files',
+ 'nav.usage_stats': 'Usage Statistics',
+ 'nav.system_info': 'System Info',
+
+ // Basic settings
+ 'basic_settings.title': 'Basic Settings',
+ 'basic_settings.debug_title': 'Debug Mode',
+ 'basic_settings.debug_enable': 'Enable Debug Mode',
+ 'basic_settings.proxy_title': 'Proxy Settings',
+ 'basic_settings.proxy_url_label': 'Proxy URL:',
+ 'basic_settings.proxy_url_placeholder': 'e.g.: socks5://user:pass@127.0.0.1:1080/',
+ 'basic_settings.proxy_update': 'Update',
+ 'basic_settings.proxy_clear': 'Clear',
+ 'basic_settings.retry_title': 'Request Retry',
+ 'basic_settings.retry_count_label': 'Retry Count:',
+ 'basic_settings.retry_update': 'Update',
+ 'basic_settings.quota_title': 'Quota Exceeded Behavior',
+ 'basic_settings.quota_switch_project': 'Auto Switch Project',
+ 'basic_settings.quota_switch_preview': 'Switch to Preview Model',
+
+ // API Keys management
+ 'api_keys.title': 'API Keys Management',
+ 'api_keys.proxy_auth_title': 'Proxy Service Authentication Keys',
+ 'api_keys.add_button': 'Add Key',
+ 'api_keys.empty_title': 'No API Keys',
+ 'api_keys.empty_desc': 'Click the button above to add the first key',
+ 'api_keys.item_title': 'API Key',
+ 'api_keys.add_modal_title': 'Add API Key',
+ 'api_keys.add_modal_key_label': 'API Key:',
+ 'api_keys.add_modal_key_placeholder': 'Please enter API key',
+ 'api_keys.edit_modal_title': 'Edit API Key',
+ 'api_keys.edit_modal_key_label': 'API Key:',
+ 'api_keys.delete_confirm': 'Are you sure you want to delete this API key?',
+
+ // AI Providers
+ 'ai_providers.title': 'AI Providers Configuration',
+ 'ai_providers.gemini_title': 'Gemini API Keys',
+ 'ai_providers.gemini_add_button': 'Add Key',
+ 'ai_providers.gemini_empty_title': 'No Gemini Keys',
+ 'ai_providers.gemini_empty_desc': 'Click the button above to add the first key',
+ 'ai_providers.gemini_item_title': 'Gemini Key',
+ 'ai_providers.gemini_add_modal_title': 'Add Gemini API Key',
+ 'ai_providers.gemini_add_modal_key_label': 'API Key:',
+ 'ai_providers.gemini_add_modal_key_placeholder': 'Please enter Gemini API key',
+ 'ai_providers.gemini_edit_modal_title': 'Edit Gemini API Key',
+ 'ai_providers.gemini_edit_modal_key_label': 'API Key:',
+ 'ai_providers.gemini_delete_confirm': 'Are you sure you want to delete this Gemini key?',
+
+ 'ai_providers.codex_title': 'Codex API Configuration',
+ 'ai_providers.codex_add_button': 'Add Configuration',
+ 'ai_providers.codex_empty_title': 'No Codex Configuration',
+ 'ai_providers.codex_empty_desc': 'Click the button above to add the first configuration',
+ 'ai_providers.codex_item_title': 'Codex Configuration',
+ 'ai_providers.codex_add_modal_title': 'Add Codex API Configuration',
+ 'ai_providers.codex_add_modal_key_label': 'API Key:',
+ 'ai_providers.codex_add_modal_key_placeholder': 'Please enter Codex API key',
+ 'ai_providers.codex_add_modal_url_label': 'Base URL (Optional):',
+ 'ai_providers.codex_add_modal_url_placeholder': 'e.g.: https://api.example.com',
+ 'ai_providers.codex_edit_modal_title': 'Edit Codex API Configuration',
+ 'ai_providers.codex_edit_modal_key_label': 'API Key:',
+ 'ai_providers.codex_edit_modal_url_label': 'Base URL (Optional):',
+ 'ai_providers.codex_delete_confirm': 'Are you sure you want to delete this Codex configuration?',
+
+ 'ai_providers.claude_title': 'Claude API Configuration',
+ 'ai_providers.claude_add_button': 'Add Configuration',
+ 'ai_providers.claude_empty_title': 'No Claude Configuration',
+ 'ai_providers.claude_empty_desc': 'Click the button above to add the first configuration',
+ 'ai_providers.claude_item_title': 'Claude Configuration',
+ 'ai_providers.claude_add_modal_title': 'Add Claude API Configuration',
+ 'ai_providers.claude_add_modal_key_label': 'API Key:',
+ 'ai_providers.claude_add_modal_key_placeholder': 'Please enter Claude API key',
+ 'ai_providers.claude_add_modal_url_label': 'Base URL (Optional):',
+ 'ai_providers.claude_add_modal_url_placeholder': 'e.g.: https://api.anthropic.com',
+ 'ai_providers.claude_edit_modal_title': 'Edit Claude API Configuration',
+ 'ai_providers.claude_edit_modal_key_label': 'API Key:',
+ 'ai_providers.claude_edit_modal_url_label': 'Base URL (Optional):',
+ 'ai_providers.claude_delete_confirm': 'Are you sure you want to delete this Claude configuration?',
+
+ 'ai_providers.openai_title': 'OpenAI Compatible Providers',
+ 'ai_providers.openai_add_button': 'Add Provider',
+ 'ai_providers.openai_empty_title': 'No OpenAI Compatible Providers',
+ 'ai_providers.openai_empty_desc': 'Click the button above to add the first provider',
+ 'ai_providers.openai_add_modal_title': 'Add OpenAI Compatible Provider',
+ 'ai_providers.openai_add_modal_name_label': 'Provider Name:',
+ 'ai_providers.openai_add_modal_name_placeholder': 'e.g.: openrouter',
+ 'ai_providers.openai_add_modal_url_label': 'Base URL:',
+ 'ai_providers.openai_add_modal_url_placeholder': 'e.g.: https://openrouter.ai/api/v1',
+ 'ai_providers.openai_add_modal_keys_label': 'API Keys (one per line):',
+ 'ai_providers.openai_add_modal_keys_placeholder': 'sk-key1\nsk-key2',
+ 'ai_providers.openai_edit_modal_title': 'Edit OpenAI Compatible Provider',
+ 'ai_providers.openai_edit_modal_name_label': 'Provider Name:',
+ 'ai_providers.openai_edit_modal_url_label': 'Base URL:',
+ 'ai_providers.openai_edit_modal_keys_label': 'API Keys (one per line):',
+ 'ai_providers.openai_delete_confirm': 'Are you sure you want to delete this OpenAI provider?',
+ 'ai_providers.openai_keys_count': 'Keys Count',
+ 'ai_providers.openai_models_count': 'Models Count',
+
+
+ // Auth files management
+ 'auth_files.title': 'Auth Files Management',
+ 'auth_files.title_section': 'Auth Files',
+ 'auth_files.description': 'Here you can manage authentication configuration files for Qwen and Gemini. Upload JSON format authentication files to enable the corresponding AI services.',
+ 'auth_files.upload_button': 'Upload File',
+ 'auth_files.delete_all_button': 'Delete All',
+ 'auth_files.empty_title': 'No Auth Files',
+ 'auth_files.empty_desc': 'Click the button above to upload the first file',
+ 'auth_files.file_size': 'Size',
+ 'auth_files.file_modified': 'Modified',
+ 'auth_files.download_button': 'Download',
+ 'auth_files.delete_button': 'Delete',
+ 'auth_files.delete_confirm': 'Are you sure you want to delete file',
+ 'auth_files.delete_all_confirm': 'Are you sure you want to delete all auth files? This operation cannot be undone!',
+ 'auth_files.upload_error_json': 'Only JSON files are allowed',
+ 'auth_files.upload_success': 'File uploaded successfully',
+ 'auth_files.download_success': 'File downloaded successfully',
+ 'auth_files.delete_success': 'File deleted successfully',
+ 'auth_files.delete_all_success': 'Successfully deleted',
+ 'auth_files.files_count': 'files',
+
+ // Gemini Web Token
+ 'auth_login.gemini_web_title': 'Gemini Web Token',
+ 'auth_login.gemini_web_button': 'Save Gemini Web Token',
+ 'auth_login.gemini_web_hint': 'Obtain the Cookie value of the Gemini web version from the browser\'s developer tools, used for direct authentication to access Gemini.',
+ 'auth_login.secure_1psid_label': '__Secure-1PSID Cookie:',
+ 'auth_login.secure_1psid_placeholder': 'Enter __Secure-1PSID cookie value',
+ 'auth_login.secure_1psidts_label': '__Secure-1PSIDTS Cookie:',
+ 'auth_login.secure_1psidts_placeholder': 'Enter __Secure-1PSIDTS cookie value',
+ 'auth_login.gemini_web_label_label': 'Label (Optional):',
+ 'auth_login.gemini_web_label_placeholder': 'Enter label name (optional)',
+ 'auth_login.gemini_web_saved': 'Gemini Web Token saved successfully',
+
+ // Usage Statistics
+ 'usage_stats.title': 'Usage Statistics',
+ 'usage_stats.total_requests': 'Total Requests',
+ 'usage_stats.success_requests': 'Success Requests',
+ 'usage_stats.failed_requests': 'Failed Requests',
+ 'usage_stats.total_tokens': 'Total Tokens',
+ 'usage_stats.requests_trend': 'Request Trends',
+ 'usage_stats.tokens_trend': 'Token Usage Trends',
+ 'usage_stats.api_details': 'API Details',
+ 'usage_stats.by_hour': 'By Hour',
+ 'usage_stats.by_day': 'By Day',
+ 'usage_stats.refresh': 'Refresh',
+ 'usage_stats.no_data': 'No Data Available',
+ 'usage_stats.loading_error': 'Loading Failed',
+ 'usage_stats.api_endpoint': 'API Endpoint',
+ 'usage_stats.requests_count': 'Request Count',
+ 'usage_stats.tokens_count': 'Token Count',
+ 'usage_stats.models': 'Model Statistics',
+ 'usage_stats.success_rate': 'Success Rate',
+
+ // System info
+ 'system_info.title': 'System Information',
+ 'system_info.connection_status_title': 'Connection Status',
+ 'system_info.api_status_label': 'API Status:',
+ 'system_info.config_status_label': 'Config Status:',
+ 'system_info.last_update_label': 'Last Update:',
+ 'system_info.cache_data': 'Cache Data',
+ 'system_info.real_time_data': 'Real-time Data',
+ 'system_info.not_loaded': 'Not Loaded',
+ 'system_info.seconds_ago': 'seconds ago',
+
+ // Notification messages
+ 'notification.debug_updated': 'Debug settings updated',
+ 'notification.proxy_updated': 'Proxy settings updated',
+ 'notification.proxy_cleared': 'Proxy settings cleared',
+ 'notification.retry_updated': 'Retry settings updated',
+ 'notification.quota_switch_project_updated': 'Project switch settings updated',
+ 'notification.quota_switch_preview_updated': 'Preview model switch settings updated',
+ 'notification.api_key_added': 'API key added successfully',
+ 'notification.api_key_updated': 'API key updated successfully',
+ 'notification.api_key_deleted': 'API key deleted successfully',
+ 'notification.gemini_key_added': 'Gemini key added successfully',
+ 'notification.gemini_key_updated': 'Gemini key updated successfully',
+ 'notification.gemini_key_deleted': 'Gemini key deleted successfully',
+ 'notification.codex_config_added': 'Codex configuration added successfully',
+ 'notification.codex_config_updated': 'Codex configuration updated successfully',
+ 'notification.codex_config_deleted': 'Codex configuration deleted successfully',
+ 'notification.claude_config_added': 'Claude configuration added successfully',
+ 'notification.claude_config_updated': 'Claude configuration updated successfully',
+ 'notification.claude_config_deleted': 'Claude configuration deleted successfully',
+ 'notification.openai_provider_added': 'OpenAI provider added successfully',
+ 'notification.openai_provider_updated': 'OpenAI provider updated successfully',
+ 'notification.openai_provider_deleted': 'OpenAI provider deleted successfully',
+ 'notification.openai_model_name_required': 'Model name is required',
+ 'notification.data_refreshed': 'Data refreshed successfully',
+ 'notification.connection_required': 'Please establish connection first',
+ 'notification.refresh_failed': 'Refresh failed',
+ 'notification.update_failed': 'Update failed',
+ 'notification.add_failed': 'Add failed',
+ 'notification.delete_failed': 'Delete failed',
+ 'notification.upload_failed': 'Upload failed',
+ 'notification.download_failed': 'Download failed',
+ 'notification.login_failed': 'Login failed',
+ 'notification.please_enter': 'Please enter',
+ 'notification.please_fill': 'Please fill',
+ 'notification.provider_name_url': 'provider name and Base URL',
+ 'notification.api_key': 'API key',
+ 'notification.gemini_api_key': 'Gemini API key',
+ 'notification.codex_api_key': 'Codex API key',
+ 'notification.claude_api_key': 'Claude API key',
+
+ // Language switch
+ 'language.switch': 'Language',
+ 'language.chinese': '中文',
+ 'language.english': 'English',
+
+ // Theme switch
+ 'theme.switch': 'Theme',
+ 'theme.light': 'Light',
+ 'theme.dark': 'Dark',
+ 'theme.switch_to_light': 'Switch to light mode',
+ 'theme.switch_to_dark': 'Switch to dark mode',
+ 'theme.auto': 'Follow system',
+
+ // Footer
+ 'footer.version': 'Version',
+ 'footer.author': 'Author'
+ }
+ },
+
+ // 获取翻译文本
+ t(key, params = {}) {
+ const translation = this.translations[this.currentLanguage]?.[key] ||
+ this.translations[this.fallbackLanguage]?.[key] ||
+ key;
+
+ // 简单的参数替换
+ return translation.replace(/\{(\w+)\}/g, (match, param) => {
+ return params[param] || match;
+ });
+ },
+
+ // 设置语言
+ setLanguage(lang) {
+ if (this.translations[lang]) {
+ this.currentLanguage = lang;
+ localStorage.setItem('preferredLanguage', lang);
+ this.updatePageLanguage();
+ this.updateAllTexts();
+ }
+ },
+
+ // 更新页面语言属性
+ updatePageLanguage() {
+ document.documentElement.lang = this.currentLanguage;
+ },
+
+ // 更新所有文本
+ updateAllTexts() {
+ // 更新所有带有 data-i18n 属性的元素
+ document.querySelectorAll('[data-i18n]').forEach(element => {
+ const key = element.getAttribute('data-i18n');
+ const text = this.t(key);
+
+ if (element.tagName === 'INPUT' && (element.type === 'text' || element.type === 'password')) {
+ element.placeholder = text;
+ } else if (element.tagName === 'TITLE') {
+ element.textContent = text;
+ } else {
+ element.textContent = text;
+ }
+ });
+
+ // 更新所有带有 data-i18n-html 属性的元素(支持HTML)
+ document.querySelectorAll('[data-i18n-html]').forEach(element => {
+ const key = element.getAttribute('data-i18n-html');
+ const html = this.t(key);
+ element.innerHTML = html;
+ });
+ },
+
+ // 初始化
+ init() {
+ // 从本地存储获取用户偏好语言
+ const savedLanguage = localStorage.getItem('preferredLanguage');
+ if (savedLanguage && this.translations[savedLanguage]) {
+ this.currentLanguage = savedLanguage;
+ } else {
+ // 根据浏览器语言自动选择
+ const browserLang = navigator.language || navigator.userLanguage;
+ if (browserLang.startsWith('zh')) {
+ this.currentLanguage = 'zh-CN';
+ } else {
+ this.currentLanguage = 'en-US';
+ }
+ }
+
+ this.updatePageLanguage();
+ this.updateAllTexts();
+ }
+};
+
+// 全局函数,供HTML调用
+window.t = (key, params) => i18n.t(key, params);
+window.setLanguage = (lang) => i18n.setLanguage(lang);
diff --git a/index.html b/index.html
index 06ee2fd..817bb93 100644
--- a/index.html
+++ b/index.html
@@ -1,579 +1,579 @@
-
-
-
-
-
- CLI Proxy API Management Center
-
-
-
-
-
-
-
-
-
-
-
-
正在自动登录...
-
正在使用本地保存的连接信息尝试连接服务器
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
连接地址
-
- 当前地址
- :
- -
-
-
-
-
系统将自动使用当前访问地址进行连接
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 连接状态:
-
-
- 未连接
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 基础设置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- AI 提供商配置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 认证文件管理
-
-
-
-
- 这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。
-
-
-
-
-
-
-
-
-
- 从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 使用统计
-
-
-
-
-
-
-
-
-
-
-
-
-
- 系统信息
-
-
-
-
-
-
- API 状态:
- 未连接
-
-
- 配置状态:
- 未加载
-
-
- 最后更新:
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ CLI Proxy API Management Center
+
+
+
+
+
+
+
+
+
+
+
+
正在自动登录...
+
正在使用本地保存的连接信息尝试连接服务器
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
连接地址
+
+ 当前地址
+ :
+ -
+
+
+
+
系统将自动使用当前访问地址进行连接
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 连接状态:
+
+
+ 未连接
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 基础设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AI 提供商配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 认证文件管理
+
+
+
+
+ 这里管理 Qwen 和 Gemini 的认证配置文件。上传 JSON 格式的认证文件以启用相应的 AI 服务。
+
+
+
+
+
+
+
+
+
+ 从浏览器开发者工具中获取 Gemini 网页版的 Cookie 值,用于直接认证访问 Gemini。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 使用统计
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 系统信息
+
+
+
+
+
+
+ API 状态:
+ 未连接
+
+
+ 配置状态:
+ 未加载
+
+
+ 最后更新:
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/styles.css b/styles.css
index bb96c59..9de6236 100644
--- a/styles.css
+++ b/styles.css
@@ -1,1746 +1,1799 @@
-/* 全局样式 */
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-/* CSS变量主题系统 */
-:root {
- /* 亮色主题(默认) */
- --bg-primary: #f3f4f6;
- --bg-secondary: rgba(255, 255, 255, 0.95);
- --bg-tertiary: #f8fafc;
- --bg-quaternary: #f7fafc;
- --bg-modal: rgba(0, 0, 0, 0.5);
-
- --text-primary: #333;
- --text-secondary: #4a5568;
- --text-tertiary: #64748b;
- --text-quaternary: #6b7280;
- --text-inverse: white;
-
- --border-primary: #e2e8f0;
- --border-secondary: #cbd5e0;
- --border-focus: #475569;
-
- --accent-primary: linear-gradient(135deg, #475569, #334155);
- --accent-secondary: #e2e8f0;
- --accent-tertiary: #f8fafc;
- --primary-color: #3b82f6;
- --card-bg: #ffffff;
- --border-color: #e2e8f0;
-
- --success-bg: linear-gradient(135deg, #dcfce7, #bbf7d0);
- --success-text: #166534;
- --success-border: #86efac;
-
- --error-bg: linear-gradient(135deg, #fef2f2, #fecaca);
- --error-text: #dc2626;
- --error-border: #fca5a5;
-
- --warning-bg: linear-gradient(135deg, #fef3c7, #fed7aa);
- --warning-text: #d97706;
- --warning-border: #fdba74;
-
- --shadow-primary: 0 8px 32px rgba(0, 0, 0, 0.1);
- --shadow-secondary: 0 20px 60px rgba(0, 0, 0, 0.15);
- --shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.3);
-
- --glass-bg: rgba(255, 255, 255, 0.95);
- --glass-border: rgba(255, 255, 255, 0.2);
-}
-
-/* 暗色主题 */
-[data-theme="dark"] {
- --bg-primary: #0f172a;
- --bg-secondary: rgba(30, 41, 59, 0.95);
- --bg-tertiary: #1e293b;
- --bg-quaternary: #334155;
- --bg-modal: rgba(0, 0, 0, 0.7);
-
- --text-primary: #f1f5f9;
- --text-secondary: #cbd5e1;
- --text-tertiary: #94a3b8;
- --text-quaternary: #64748b;
- --text-inverse: #ffffff;
-
- --border-primary: #334155;
- --border-secondary: #475569;
- --border-focus: #64748b;
-
- --accent-primary: linear-gradient(135deg, #64748b, #475569);
- --accent-secondary: #334155;
- --accent-tertiary: #1e293b;
- --primary-color: #38bdf8;
- --card-bg: #1e293b;
- --border-color: #334155;
-
- --success-bg: linear-gradient(135deg, #064e3b, #047857);
- --success-text: #bbf7d0;
- --success-border: #059669;
-
- --error-bg: linear-gradient(135deg, #7f1d1d, #b91c1c);
- --error-text: #fecaca;
- --error-border: #dc2626;
-
- --warning-bg: linear-gradient(135deg, #78350f, #d97706);
- --warning-text: #fed7aa;
- --warning-border: #f59e0b;
-
- --shadow-primary: 0 8px 32px rgba(0, 0, 0, 0.3);
- --shadow-secondary: 0 20px 60px rgba(0, 0, 0, 0.4);
- --shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.8);
-
- --glass-bg: rgba(30, 41, 59, 0.95);
- --glass-border: rgba(100, 116, 139, 0.2);
-}
-
-/* 登录页面样式 */
-.login-container {
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--bg-primary);
- padding: 20px;
-}
-
-/* 自动登录加载页面样式 */
-.auto-login-content {
- text-align: center;
- padding: 20px;
-}
-
-.loading-spinner {
- margin: 0 auto 20px;
- width: 60px;
- height: 60px;
-}
-
-.spinner {
- width: 100%;
- height: 100%;
- border: 4px solid #e5e7eb;
- border-top: 4px solid #3b82f6;
- border-radius: 50%;
- animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
-}
-
-.auto-login-content h2 {
- color: var(--text-secondary);
- margin-bottom: 10px;
- font-size: 24px;
- font-weight: 600;
-}
-
-.auto-login-content p {
- color: var(--text-tertiary);
- font-size: 16px;
- line-height: 1.5;
-}
-
-/* 登录页面头部布局 */
-.login-header-top {
- display: flex;
- flex-direction: column;
- align-items: center;
- margin-bottom: 30px;
- position: relative;
-}
-
-.login-title {
- width: 100%;
- text-align: center;
- margin-bottom: 30px;
-}
-
-/* 头部控制按钮组 */
-.header-controls {
- display: flex;
- gap: 8px;
- align-items: center;
- height: 100%;
-}
-
-/* 登录页面的控制按钮组样式 */
-.login-header-top .header-controls {
- background: rgba(255, 255, 255, 0.1);
- backdrop-filter: blur(10px);
- border-radius: 12px;
- padding: 8px;
- border: 1px solid rgba(255, 255, 255, 0.2);
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
- transition: all 0.3s ease;
- margin-top: 8px; /* 与标题拉开距离,避免遮挡 */
- margin-bottom: 20px;
-}
-
-.login-header-top .header-controls:hover {
- background: rgba(255, 255, 255, 0.15);
- border-color: rgba(255, 255, 255, 0.3);
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
- transform: translateY(-1px);
-}
-
-[data-theme="dark"] .login-header-top .header-controls {
- background: rgba(30, 41, 59, 0.8);
- border: 1px solid rgba(100, 116, 139, 0.3);
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
-}
-
-[data-theme="dark"] .login-header-top .header-controls:hover {
- background: rgba(30, 41, 59, 0.9);
- border-color: rgba(100, 116, 139, 0.5);
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
-}
-
-.language-switcher,
-.theme-switcher {
- position: relative;
- flex-shrink: 0;
-}
-
-.language-btn,
-.theme-btn {
- padding: 10px 20px;
- font-size: 14px;
- font-weight: 500;
- border-radius: 8px;
- background: var(--bg-tertiary);
- border: 1px solid var(--border-primary);
- color: var(--text-tertiary);
- transition: all 0.3s ease;
- white-space: nowrap;
- backdrop-filter: blur(5px);
- height: 40px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
-}
-
-/* 登录页面的按钮样式优化 */
-.login-header-top .language-btn,
-.login-header-top .theme-btn {
- padding: 8px 12px;
- font-size: 13px;
- height: 36px;
- min-width: 36px;
- border-radius: 8px;
- background: rgba(255, 255, 255, 0.9);
- border: 1px solid rgba(255, 255, 255, 0.3);
- color: #64748b;
- backdrop-filter: blur(10px);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-}
-
-.login-header-top .language-btn:hover,
-.login-header-top .theme-btn:hover {
- background: rgba(255, 255, 255, 1);
- border-color: rgba(100, 116, 139, 0.3);
- color: #475569;
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-}
-
-[data-theme="dark"] .login-header-top .language-btn,
-[data-theme="dark"] .login-header-top .theme-btn {
- background: rgba(30, 41, 59, 0.9);
- border: 1px solid rgba(100, 116, 139, 0.3);
- color: #94a3b8;
-}
-
-[data-theme="dark"] .login-header-top .language-btn:hover,
-[data-theme="dark"] .login-header-top .theme-btn:hover {
- background: rgba(51, 65, 85, 0.95);
- border-color: rgba(100, 116, 139, 0.5);
- color: #cbd5e1;
-}
-
-.language-btn:hover,
-.theme-btn:hover {
- background: var(--accent-secondary);
- border-color: var(--border-secondary);
- color: var(--text-secondary);
-}
-
-.language-btn i,
-.theme-btn i {
- margin-right: 6px;
-}
-
-.theme-btn.active {
- background: var(--accent-primary);
- color: var(--text-inverse);
-}
-
-.login-card {
- background: var(--glass-bg);
- backdrop-filter: blur(10px);
- border-radius: 20px;
- padding: 40px;
- width: 100%;
- max-width: 500px;
- box-shadow: var(--shadow-secondary);
- border: 1px solid var(--glass-border);
-}
-
-.login-header {
- text-align: center;
- margin-bottom: 25px;
-}
-
-.login-title {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 15px;
- color: var(--text-secondary);
- font-size: 1.8rem;
- font-weight: 600;
- margin-bottom: 0;
- line-height: 1.2;
-}
-
-#login-logo {
- height: 60px;
- width: auto;
- object-fit: contain;
- border-radius: 6px;
- padding: 2px;
- background: #fff;
- border: 1px solid rgba(15, 23, 42, 0.06);
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
-}
-
-.login-subtitle {
- color: #64748b;
- font-size: 1rem;
- margin: 0;
- text-align: center;
-}
-
-.login-body {
- display: flex;
- flex-direction: column;
- gap: 24px;
-}
-
-.login-connection-info {
- background: var(--bg-tertiary);
- border: 1px solid var(--border-primary);
- border-radius: 16px;
- padding: 20px;
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-[data-theme="dark"] .login-connection-info {
- background: rgba(30, 41, 59, 0.7);
- border-color: rgba(100, 116, 139, 0.3);
-}
-
-.connection-summary {
- display: flex;
- align-items: center;
- gap: 16px;
- color: var(--text-secondary);
-}
-
-.connection-summary i {
- font-size: 24px;
- color: var(--primary-color);
-}
-
-.connection-url {
- font-size: 16px;
- color: var(--text-secondary);
-}
-
-.connection-url-separator {
- margin: 0 8px;
- color: var(--text-tertiary);
-}
-
-#login-connection-url {
- font-family: "Fira Code", "Consolas", "Courier New", monospace;
- color: var(--text-primary);
- word-break: break-all;
-}
-
-[data-theme="dark"] #login-connection-url {
- color: var(--text-secondary);
-}
-
-.login-form {
- display: flex;
- flex-direction: column;
- gap: 20px;
-}
-
-.login-form .form-group {
- margin-bottom: 25px;
-}
-
-.login-form .form-group label {
- display: block;
- margin-bottom: 8px;
- color: var(--text-secondary);
- font-weight: 600;
- font-size: 0.95rem;
-}
-
-.login-form .form-hint {
- margin-top: 6px;
- color: #64748b;
- font-size: 12px;
-}
-
-.login-form .input-group {
- display: flex;
- gap: 10px;
- align-items: center;
-}
-
-.login-form input[type="text"],
-.login-form input[type="password"] {
- flex: 1;
- padding: 14px 16px;
- border: 2px solid var(--border-primary);
- border-radius: 10px;
- font-size: 15px;
- transition: all 0.3s ease;
- background: var(--bg-secondary);
- color: var(--text-primary);
-}
-
-.login-form input:focus {
- outline: none;
- border-color: var(--border-focus);
- box-shadow: 0 0 0 3px var(--border-primary);
-}
-
-.login-form .btn-secondary {
- padding: 14px 16px;
- border: 2px solid #e2e8f0;
- background: #f8fafc;
- color: #64748b;
- border-radius: 10px;
- transition: all 0.3s ease;
-}
-
-.login-form .btn-secondary:hover {
- background: #e2e8f0;
- border-color: #cbd5e0;
-}
-
-.form-actions {
- margin-top: 30px;
- text-align: center;
-}
-
-.login-btn {
- width: 100%;
- padding: 16px 24px;
- font-size: 16px;
- font-weight: 600;
- border-radius: 12px;
- background: linear-gradient(135deg, #475569, #334155);
- color: white;
- border: none;
- cursor: pointer;
- transition: all 0.3s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
-}
-
-.login-btn:hover {
- transform: translateY(-2px);
- box-shadow: 0 8px 25px rgba(51, 65, 85, 0.4);
-}
-
-.login-btn:disabled {
- opacity: 0.7;
- cursor: not-allowed;
- transform: none;
-}
-
-.login-error {
- background: var(--error-bg);
- border: 1px solid var(--error-border);
- color: var(--error-text);
- padding: 12px 16px;
- border-radius: 8px;
- margin-top: 20px;
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 14px;
- font-weight: 500;
-}
-
-.login-error i {
- color: var(--error-text);
-}
-
-/* 响应式设计 - 登录页面 */
-@media (max-width: 640px) {
- .login-card {
- padding: 30px 20px;
- margin: 10px;
- max-width: 100%;
- }
-
- .login-header-top {
- flex-direction: column;
- gap: 20px;
- align-items: center;
- margin-bottom: 30px;
- }
-
- .header-controls {
- flex-direction: row;
- justify-content: center;
- gap: 8px;
- margin-bottom: 10px; /* 在小屏幕上给控制按钮组添加下边距 */
- }
-
- /* 登录页面小屏幕优化 */
- .login-header-top .header-controls {
- margin: 8px auto 0 auto; /* 顶部留白,避免与标题拥挤 */
- background: rgba(255, 255, 255, 0.08);
- padding: 6px;
- border-radius: 10px;
- }
-
- .login-header-top .language-btn,
- .login-header-top .theme-btn {
- padding: 6px 10px;
- font-size: 12px;
- height: 32px;
- min-width: 32px;
- }
-
- .language-btn,
- .theme-btn {
- padding: 8px 16px;
- font-size: 13px;
- height: 36px; /* 在小屏幕上稍微减小高度 */
- }
-
- .login-title {
- font-size: 1.5rem;
- flex-direction: column;
- gap: 10px;
- text-align: center;
- justify-content: center;
- margin-bottom: 25px;
- }
-
- #login-logo {
- height: 50px;
- }
-
- .login-form .input-group {
- flex-direction: column;
- align-items: stretch;
- }
-
- .login-form .btn-secondary {
- width: 100%;
- margin-top: 10px;
- }
-}
-
-body {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background: var(--bg-primary);
- min-height: 100vh;
- color: var(--text-primary);
- transition: background-color 0.3s ease, color 0.3s ease;
-}
-
-.container {
- max-width: 1400px;
- margin: 0 auto;
- padding: 20px;
-}
-
-/* 头部样式 */
-.header {
- background: var(--glass-bg);
- backdrop-filter: blur(10px);
- border-radius: 15px;
- padding: 48px 28px; /* 再次拉长,适配更大 Logo */
- margin-bottom: 20px;
- box-shadow: var(--shadow-primary);
-}
-
-.header-content {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.header h1 {
- color: var(--text-secondary);
- font-size: 1.6rem; /* 字体小一号 */
- font-weight: 600;
-}
-
-.header h1.brand {
- display: flex;
- align-items: flex-end; /* 文字在 Logo 右下角(底对齐) */
- gap: 18px;
- line-height: 1; /* 减少标题内上下空白 */
-}
-
-.header h1 i {
- color: #64748b; /* slate-500 */
- margin-right: 10px;
-}
-
-/* 站点 Logo(嵌入头部) */
-#site-logo {
- height: 88px;
- width: auto;
- display: inline-block;
- object-fit: contain;
- border-radius: 6px;
- padding: 2px;
- background: #fff;
- border: 1px solid rgba(15, 23, 42, 0.06);
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
-}
-
-.brand-title {
- display: inline-block;
- line-height: 1.1; /* 保持原字号不变,同时更紧凑 */
- margin-bottom: 4px; /* 让文字更贴近更高的 Logo 底部 */
-}
-
-/* 小屏幕适配:避免 Logo 过大占位 */
-@media (max-width: 640px) {
- #site-logo { height: 64px; }
-}
-
-.header-actions {
- display: flex;
- gap: 12px;
- align-items: center;
- height: 40px; /* 统一高度 */
-}
-
-
-/* 认证区域 */
-.auth-section {
- margin-bottom: 20px;
-}
-
-/* 连接信息样式 */
-.connection-info {
- background: var(--bg-tertiary);
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 20px;
-}
-
-.info-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 12px 0;
- border-bottom: 1px solid var(--border-primary);
-}
-
-.info-item:last-child {
- border-bottom: none;
- padding-bottom: 0;
-}
-
-.info-label {
- display: flex;
- align-items: center;
- gap: 8px;
- color: var(--text-secondary);
- font-weight: 600;
- font-size: 14px;
-}
-
-.info-label i {
- color: var(--text-tertiary);
- width: 16px;
-}
-
-.info-value {
- color: var(--text-primary);
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- font-size: 13px;
- background: var(--bg-secondary);
- padding: 6px 12px;
- border-radius: 6px;
- border: 1px solid var(--border-primary);
- max-width: 300px;
- word-break: break-all;
-}
-
-.status-indicator {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 4px 12px;
- border-radius: 20px;
- font-size: 12px;
- font-weight: 600;
-}
-
-.status-indicator.connected {
- background: var(--success-bg);
- color: var(--success-text);
- border: 1px solid var(--success-border);
-}
-
-.status-indicator.disconnected {
- background: var(--error-bg);
- color: var(--error-text);
- border: 1px solid var(--error-border);
-}
-
-.status-indicator.connecting {
- background: var(--warning-bg);
- color: var(--warning-text);
- border: 1px solid var(--warning-border);
-}
-
-.connection-actions {
- display: flex;
- gap: 12px;
- justify-content: center;
-}
-
-.connection-actions .btn {
- flex: 1;
- max-width: 150px;
-}
-
-/* 响应式设计 - 连接信息 */
-@media (max-width: 768px) {
- .info-item {
- flex-direction: column;
- align-items: flex-start;
- gap: 8px;
- }
-
- .info-value {
- max-width: 100%;
- width: 100%;
- }
-
- .connection-actions {
- flex-direction: column;
- }
-
- .connection-actions .btn {
- max-width: 100%;
- }
-}
-
-/* 主要内容区域 */
-.main-content {
- display: flex;
- gap: 20px;
-}
-
-/* 侧边栏 */
-.sidebar {
- width: 250px;
- background: var(--glass-bg);
- backdrop-filter: blur(10px);
- border-radius: 15px;
- padding: 20px;
- height: fit-content;
- box-shadow: var(--shadow-primary);
-}
-
-.nav-menu {
- list-style: none;
-}
-
-.nav-menu li {
- margin-bottom: 8px;
-}
-
-.nav-item {
- display: flex;
- align-items: center;
- padding: 12px 16px;
- color: var(--text-secondary);
- text-decoration: none;
- border-radius: 10px;
- transition: all 0.3s ease;
- border: 2px solid transparent;
-}
-
-.nav-item:hover {
- background: var(--accent-tertiary);
- color: var(--text-secondary);
-}
-
-.nav-item.active {
- background: var(--accent-primary);
- color: var(--text-inverse);
- box-shadow: var(--shadow-primary);
-}
-
-.nav-item i {
- margin-right: 10px;
- width: 20px;
-}
-
-/* 内容区域 */
-.content-area {
- flex: 1;
-}
-
-.content-section {
- display: none;
-}
-
-.content-section.active {
- display: block;
-}
-
-.content-section h2 {
- color: var(--text-secondary);
- margin-bottom: 20px;
- font-size: 1.5rem;
- font-weight: 600;
-}
-
-/* 卡片样式 */
-.card {
- background: var(--glass-bg);
- backdrop-filter: blur(10px);
- border-radius: 15px;
- margin-bottom: 20px;
- box-shadow: var(--shadow-primary);
- overflow: hidden;
-}
-
-.card-header {
- background: var(--bg-tertiary);
- padding: 20px;
- border-bottom: 1px solid var(--border-primary);
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.card-header h3 {
- color: var(--text-secondary);
- font-size: 1.2rem;
- font-weight: 600;
-}
-
-.card-header h3 i {
- color: var(--text-tertiary);
- margin-right: 10px;
-}
-
-.card-content {
- padding: 20px;
-}
-
-/* 按钮样式 */
-.btn {
- padding: 10px 20px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- font-size: 14px;
- font-weight: 500;
- transition: all 0.3s ease;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- text-decoration: none;
- height: 40px;
- white-space: nowrap;
-}
-
-.btn-primary {
- background: var(--accent-primary);
- color: var(--text-inverse);
-}
-
-.btn-primary:hover {
- transform: translateY(-2px);
- box-shadow: var(--shadow-primary);
-}
-
-.btn-secondary {
- background: var(--accent-secondary);
- color: var(--text-secondary);
-}
-
-.btn-secondary:hover {
- background: var(--border-secondary);
- transform: translateY(-1px);
-}
-
-.btn-danger {
- background: linear-gradient(135deg, #ef4444, #b91c1c);
- color: white;
-}
-
-.btn-danger:hover {
- transform: translateY(-2px);
- box-shadow: 0 6px 20px rgba(185, 28, 28, 0.45);
-}
-
-.btn-success {
- background: linear-gradient(135deg, #22c55e, #15803d);
- color: white;
-}
-
-.btn-success:hover {
- transform: translateY(-2px);
- box-shadow: 0 6px 20px rgba(21, 128, 61, 0.45);
-}
-
-.btn:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none !important;
-}
-
-/* 表单元素 */
-.form-group {
- margin-bottom: 20px;
-}
-
-.form-group label {
- display: block;
- margin-bottom: 8px;
- color: var(--text-secondary);
- font-weight: 500;
-}
-
-.form-hint {
- margin-top: 6px;
- color: var(--text-tertiary);
- font-size: 12px;
-}
-
-.input-group {
- display: flex;
- gap: 10px;
- align-items: center;
-}
-
-input[type="text"],
-input[type="password"],
-input[type="number"],
-input[type="url"],
-textarea,
-select {
- flex: 1;
- padding: 12px 16px;
- border: 2px solid var(--border-primary);
- border-radius: 8px;
- font-size: 14px;
- transition: all 0.3s ease;
- background: var(--bg-secondary);
- color: var(--text-primary);
-}
-
-input:focus,
-textarea:focus,
-select:focus {
- outline: none;
- border-color: var(--border-focus);
- box-shadow: 0 0 0 3px var(--border-primary);
-}
-
-/* 切换开关 */
-.toggle-group {
- display: flex;
- align-items: center;
- gap: 15px;
- margin-bottom: 15px;
-}
-
-.toggle-switch {
- position: relative;
- display: inline-block;
- width: 50px;
- height: 25px;
-}
-
-.toggle-switch input {
- opacity: 0;
- width: 0;
- height: 0;
-}
-
-.slider {
- position: absolute;
- cursor: pointer;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: var(--accent-secondary);
- transition: .4s;
- border-radius: 25px;
-}
-
-.slider:before {
- position: absolute;
- content: "";
- height: 19px;
- width: 19px;
- left: 3px;
- bottom: 3px;
- background-color: var(--text-inverse);
- transition: .4s;
- border-radius: 50%;
-}
-
-input:checked + .slider {
- background: var(--accent-primary);
-}
-
-input:checked + .slider:before {
- transform: translateX(25px);
-}
-
-.toggle-label {
- color: var(--text-secondary);
- font-weight: 500;
-}
-
-/* 列表样式 */
-.key-list,
-.provider-list,
-.file-list {
- max-height: 400px;
- overflow-y: auto;
-}
-
-.key-item,
-.provider-item,
-.file-item {
- background: var(--bg-quaternary);
- border: 1px solid var(--border-primary);
- border-radius: 8px;
- padding: 15px;
- margin-bottom: 10px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- transition: all 0.3s ease;
-}
-
-.key-item:hover,
-.provider-item:hover,
-.file-item:hover {
- background: var(--bg-tertiary);
- border-color: var(--border-secondary);
- transform: translateY(-1px);
-}
-
-.item-content {
- flex: 1;
-}
-
-.item-actions {
- display: flex;
- gap: 8px;
-}
-
-.item-title {
- font-weight: 600;
- color: var(--text-secondary);
- margin-bottom: 4px;
-}
-
-.item-subtitle {
- color: var(--text-tertiary);
- font-size: 0.9rem;
-}
-
-.item-value {
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- background: var(--bg-tertiary);
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 0.85rem;
- color: var(--text-secondary);
- word-break: break-all;
-}
-
-/* 状态信息 */
-.status-info {
- background: var(--bg-quaternary);
- border-radius: 8px;
- padding: 20px;
-}
-
-.status-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 15px;
- padding-bottom: 15px;
- border-bottom: 1px solid var(--border-primary);
-}
-
-.status-item:last-child {
- margin-bottom: 0;
- padding-bottom: 0;
- border-bottom: none;
-}
-
-.status-label {
- font-weight: 600;
- color: var(--text-secondary);
-}
-
-.status-value {
- color: var(--text-tertiary);
-}
-
-/* 模态框 */
-.modal {
- display: none;
- position: fixed;
- z-index: 1000;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- background-color: var(--bg-modal);
- backdrop-filter: blur(5px);
-}
-
-.modal-content {
- background-color: var(--bg-secondary);
- margin: 8% auto;
- padding: 0;
- border-radius: 15px;
- width: 90%;
- max-width: 550px;
- box-shadow: var(--shadow-modal);
- animation: modalSlideIn 0.3s ease;
- position: relative;
-}
-
-@keyframes modalSlideIn {
- from {
- opacity: 0;
- transform: translateY(-50px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.close {
- color: var(--text-tertiary);
- position: absolute;
- top: 15px;
- right: 20px;
- font-size: 28px;
- font-weight: bold;
- cursor: pointer;
- z-index: 1001;
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- transition: all 0.3s ease;
-}
-
-.close:hover,
-.close:focus {
- color: var(--text-secondary);
- background-color: var(--bg-tertiary);
-}
-
-#modal-body {
- padding: 50px 35px 35px 35px;
-}
-
-/* 模态框标题样式 */
-#modal-body h3 {
- color: var(--text-secondary);
- font-size: 1.3rem;
- font-weight: 600;
- margin: 0 0 25px 0;
- text-align: center;
- border-bottom: 2px solid var(--border-primary);
- padding-bottom: 15px;
-}
-
-/* 模态框表单组 */
-#modal-body .form-group {
- margin-bottom: 20px;
-}
-
-#modal-body .form-group label {
- display: block;
- margin-bottom: 8px;
- color: var(--text-secondary);
- font-weight: 600;
- font-size: 14px;
-}
-
-#modal-body .form-group input,
-#modal-body .form-group textarea {
- width: 100%;
- padding: 12px 16px;
- border: 2px solid var(--border-primary);
- border-radius: 8px;
- font-size: 14px;
- transition: all 0.3s ease;
- background: var(--bg-tertiary);
- color: var(--text-primary);
- font-family: inherit;
-}
-
-#modal-body .form-group input:focus,
-#modal-body .form-group textarea:focus {
- outline: none;
- border-color: var(--border-focus);
- box-shadow: 0 0 0 3px var(--border-primary);
-}
-
-#modal-body .form-group textarea {
- resize: vertical;
- min-height: 80px;
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- font-size: 13px;
-}
-
-/* 模态框按钮组 */
-#modal-body .modal-actions {
- display: flex;
- justify-content: flex-end;
- gap: 12px;
- margin-top: 30px;
- padding-top: 20px;
- border-top: 1px solid var(--border-primary);
-}
-
-#modal-body .modal-actions .btn {
- min-width: 80px;
- padding: 10px 20px;
-}
-
-/* 通知 */
-.notification {
- position: fixed;
- top: 20px;
- right: 20px;
- padding: 15px 20px;
- border-radius: 8px;
- color: white;
- font-weight: 500;
- z-index: 1001;
- transform: translateX(400px);
- transition: all 0.3s ease;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
-}
-
-.notification.show {
- transform: translateX(0);
-}
-
-.notification.success {
- background: linear-gradient(135deg, #68d391, #38a169);
-}
-
-.notification.error {
- background: linear-gradient(135deg, #fc8181, #e53e3e);
-}
-
-.notification.info {
- background: linear-gradient(135deg, #63b3ed, #3182ce);
-}
-
-/* 响应式设计 */
-@media (max-width: 768px) {
- .main-content {
- flex-direction: column;
- }
-
- .sidebar {
- width: 100%;
- order: 2;
- }
-
- .content-area {
- order: 1;
- }
-
- .nav-menu {
- display: flex;
- overflow-x: auto;
- gap: 10px;
- }
-
- .nav-menu li {
- margin-bottom: 0;
- flex-shrink: 0;
- }
-
- .header-content {
- flex-direction: column;
- gap: 15px;
- text-align: center;
- }
-
- .input-group {
- flex-direction: column;
- align-items: stretch;
- }
-
- .card-header {
- flex-direction: column;
- gap: 15px;
- align-items: flex-start;
- }
-
- .header-actions {
- width: 100%;
- justify-content: center;
- flex-wrap: wrap;
- gap: 8px;
- }
-
- .header-controls {
- order: -1; /* 确保控制按钮在小屏幕上排在前面 */
- }
-
- /* 模态框响应式 */
- .modal-content {
- margin: 5% auto;
- width: 95%;
- max-width: none;
- }
-
- #modal-body {
- padding: 40px 25px 25px 25px;
- }
-
- #modal-body h3 {
- font-size: 1.2rem;
- margin-bottom: 20px;
- }
-
- #modal-body .modal-actions {
- flex-direction: column-reverse;
- gap: 10px;
- }
-
- #modal-body .modal-actions .btn {
- width: 100%;
- margin: 0;
- }
-}
-
-/* 加载动画 */
-.loading {
- display: inline-block;
- width: 20px;
- height: 20px;
- border: 3px solid rgba(255, 255, 255, 0.3);
- border-radius: 50%;
- border-top-color: #fff;
- animation: spin 1s ease-in-out infinite;
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-/* 空状态 */
-.empty-state {
- text-align: center;
- padding: 40px 20px;
- color: var(--text-tertiary);
-}
-
-.empty-state i {
- font-size: 48px;
- margin-bottom: 16px;
- color: var(--border-secondary);
-}
-
-.empty-state h3 {
- margin-bottom: 8px;
- color: var(--text-secondary);
-}
-
-/* 滚动条样式 */
-::-webkit-scrollbar {
- width: 8px;
-}
-
-::-webkit-scrollbar-track {
- background: #f1f1f1;
- border-radius: 4px;
-}
-
-::-webkit-scrollbar-thumb {
- background: linear-gradient(135deg, #475569, #334155);
- border-radius: 4px;
-}
-
-::-webkit-scrollbar-thumb:hover {
- background: linear-gradient(135deg, #334155, #1f2937);
-}
-
-/* 连接状态指示器 */
-.connection-indicator {
- display: inline-block;
- width: 10px;
- height: 10px;
- border-radius: 50%;
- margin-right: 8px;
-}
-
-.connection-indicator.connected {
- background-color: #68d391;
- box-shadow: 0 0 8px rgba(104, 211, 145, 0.6);
-}
-
-.connection-indicator.disconnected {
- background-color: #fc8181;
- box-shadow: 0 0 8px rgba(252, 129, 129, 0.6);
-}
-
-.connection-indicator.connecting {
- background-color: #fbb040;
- animation: pulse 1.5s infinite;
-}
-
-@keyframes pulse {
- 0% {
- opacity: 1;
- }
- 50% {
- opacity: 0.5;
- }
- 100% {
- opacity: 1;
- }
-}
-
-
-/* Gemini Web Token 模态框样式 */
-.gemini-web-form .form-group {
- margin-bottom: 20px;
-}
-
-.gemini-web-form .form-group label {
- display: block;
- margin-bottom: 8px;
- color: var(--text-secondary);
- font-weight: 600;
- font-size: 14px;
-}
-
-.gemini-web-form .form-group input {
- width: 100%;
- padding: 12px 16px;
- border: 2px solid var(--border-primary);
- border-radius: 8px;
- font-size: 14px;
- transition: all 0.3s ease;
- background: var(--bg-tertiary);
- color: var(--text-primary);
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
-}
-
-.gemini-web-form .form-group input:focus {
- outline: none;
- border-color: var(--border-focus);
- box-shadow: 0 0 0 3px var(--border-primary);
-}
-
-.gemini-web-form .form-hint {
- margin-top: 6px;
- color: var(--text-tertiary);
- font-size: 12px;
- line-height: 1.4;
-}
-
-/* 使用统计样式 */
-.stats-overview {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: 20px;
- margin-bottom: 30px;
-}
-
-.stat-card {
- background: var(--card-bg);
- border: 1px solid var(--border-color);
- border-radius: 12px;
- padding: 24px;
- display: flex;
- align-items: center;
- gap: 16px;
- transition: all 0.2s ease;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-
-.stat-card:hover {
- border-color: var(--border-primary);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- transform: translateY(-2px);
-}
-
-.stat-icon {
- width: 50px;
- height: 50px;
- border-radius: 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--primary-color);
- color: white;
- font-size: 20px;
- flex-shrink: 0;
-}
-
-.stat-icon.success {
- background: #10b981;
-}
-
-.stat-icon.error {
- background: #ef4444;
-}
-
-.stat-content {
- flex: 1;
-}
-
-.stat-number {
- font-size: 28px;
- font-weight: 700;
- color: var(--text-primary);
- line-height: 1;
- margin-bottom: 4px;
-}
-
-.stat-label {
- font-size: 14px;
- color: var(--text-secondary);
- font-weight: 500;
-}
-
-.charts-container {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 20px;
- margin-bottom: 30px;
-}
-
-@media (max-width: 1200px) {
- .charts-container {
- grid-template-columns: 1fr;
- }
-}
-
-.chart-card {
- min-height: 400px;
-}
-
-.chart-container {
- position: relative;
- height: 300px;
- width: 100%;
-}
-
-.chart-controls {
- display: flex;
- gap: 8px;
-}
-
-.btn.btn-small {
- padding: 6px 12px;
- font-size: 12px;
- border-radius: 6px;
- border: 1px solid var(--border-color);
- background: var(--card-bg);
- color: var(--text-secondary);
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-.btn.btn-small:hover {
- border-color: var(--border-primary);
- color: var(--text-primary);
-}
-
-.btn.btn-small.active {
- background: var(--primary-color);
- border-color: var(--primary-color);
- color: white;
-}
-
-.api-stats-table {
- overflow-x: auto;
-}
-
-.stats-table {
- width: 100%;
- border-collapse: collapse;
- margin-top: 16px;
-}
-
-.stats-table th,
-.stats-table td {
- padding: 12px 16px;
- text-align: left;
- border-bottom: 1px solid var(--border-color);
-}
-
-.stats-table th {
- background: var(--bg-secondary);
- font-weight: 600;
- color: var(--text-primary);
- font-size: 14px;
-}
-
-.stats-table td {
- color: var(--text-secondary);
- font-size: 14px;
-}
-
-.stats-table tr:hover {
- background: var(--bg-secondary);
-}
-
-.model-details {
- margin-top: 8px;
- padding: 8px 12px;
- background: var(--bg-tertiary);
- border-radius: 6px;
- font-size: 12px;
-}
-
-.model-item {
- display: flex;
- justify-content: space-between;
- margin-bottom: 4px;
- color: var(--text-tertiary);
-}
-
-.model-name {
- font-weight: 500;
- color: var(--text-secondary);
-}
-
-.loading-placeholder {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100px;
- color: var(--text-tertiary);
- font-size: 14px;
-}
-
-.no-data-message {
- text-align: center;
- color: var(--text-tertiary);
- font-style: italic;
- padding: 40px;
-}
-
-/* 暗色主题适配 */
-[data-theme="dark"] .stat-card {
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
-}
-
-[data-theme="dark"] .stat-card:hover {
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
-}
-
-[data-theme="dark"] .btn.btn-small {
- background: var(--bg-tertiary);
-}
-
-/* 版本信息样式 */
-.version-footer {
- margin-top: 2rem;
- padding: 1rem 0;
- border-top: 1px solid var(--border);
- background: var(--bg-secondary);
-}
-
-.version-info {
- text-align: center;
- font-size: 0.875rem;
- color: var(--text-secondary);
- opacity: 0.8;
-}
-
-.version-info .separator {
- margin: 0 0.75rem;
- color: var(--text-secondary);
- opacity: 0.6;
-}
-
-/* 暗黑主题下的版本信息 */
-[data-theme="dark"] .version-footer {
- border-top-color: var(--border);
- background: var(--bg-secondary);
-}
-
-[data-theme="dark"] .version-info {
- color: var(--text-secondary);
-}
-
-.connection-reset-btn {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- white-space: nowrap;
-}
-
-.connection-reset-btn i {
- margin: 0;
-}
-
-.connection-reset-btn span {
- font-size: 13px;
-}
-
-[data-theme="dark"] .connection-reset-btn {
- background: rgba(30, 41, 59, 0.9);
- border-color: rgba(100, 116, 139, 0.4);
- color: #cbd5e1;
-}
-
-[data-theme="dark"] .connection-reset-btn:hover {
- background: rgba(51, 65, 85, 0.95);
- border-color: rgba(100, 116, 139, 0.6);
-}
-
+/* 全局样式 */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+/* CSS变量主题系统 */
+:root {
+ /* 亮色主题(默认) */
+ --bg-primary: #f3f4f6;
+ --bg-secondary: rgba(255, 255, 255, 0.95);
+ --bg-tertiary: #f8fafc;
+ --bg-quaternary: #f7fafc;
+ --bg-modal: rgba(0, 0, 0, 0.5);
+
+ --text-primary: #333;
+ --text-secondary: #4a5568;
+ --text-tertiary: #64748b;
+ --text-quaternary: #6b7280;
+ --text-inverse: white;
+
+ --border-primary: #e2e8f0;
+ --border-secondary: #cbd5e0;
+ --border-focus: #475569;
+
+ --accent-primary: linear-gradient(135deg, #475569, #334155);
+ --accent-secondary: #e2e8f0;
+ --accent-tertiary: #f8fafc;
+ --primary-color: #3b82f6;
+ --card-bg: #ffffff;
+ --border-color: #e2e8f0;
+
+ --success-bg: linear-gradient(135deg, #dcfce7, #bbf7d0);
+ --success-text: #166534;
+ --success-border: #86efac;
+
+ --error-bg: linear-gradient(135deg, #fef2f2, #fecaca);
+ --error-text: #dc2626;
+ --error-border: #fca5a5;
+
+ --warning-bg: linear-gradient(135deg, #fef3c7, #fed7aa);
+ --warning-text: #d97706;
+ --warning-border: #fdba74;
+
+ --shadow-primary: 0 8px 32px rgba(0, 0, 0, 0.1);
+ --shadow-secondary: 0 20px 60px rgba(0, 0, 0, 0.15);
+ --shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.3);
+
+ --glass-bg: rgba(255, 255, 255, 0.95);
+ --glass-border: rgba(255, 255, 255, 0.2);
+}
+
+/* 暗色主题 */
+[data-theme="dark"] {
+ --bg-primary: #0f172a;
+ --bg-secondary: rgba(30, 41, 59, 0.95);
+ --bg-tertiary: #1e293b;
+ --bg-quaternary: #334155;
+ --bg-modal: rgba(0, 0, 0, 0.7);
+
+ --text-primary: #f1f5f9;
+ --text-secondary: #cbd5e1;
+ --text-tertiary: #94a3b8;
+ --text-quaternary: #64748b;
+ --text-inverse: #ffffff;
+
+ --border-primary: #334155;
+ --border-secondary: #475569;
+ --border-focus: #64748b;
+
+ --accent-primary: linear-gradient(135deg, #64748b, #475569);
+ --accent-secondary: #334155;
+ --accent-tertiary: #1e293b;
+ --primary-color: #38bdf8;
+ --card-bg: #1e293b;
+ --border-color: #334155;
+
+ --success-bg: linear-gradient(135deg, #064e3b, #047857);
+ --success-text: #bbf7d0;
+ --success-border: #059669;
+
+ --error-bg: linear-gradient(135deg, #7f1d1d, #b91c1c);
+ --error-text: #fecaca;
+ --error-border: #dc2626;
+
+ --warning-bg: linear-gradient(135deg, #78350f, #d97706);
+ --warning-text: #fed7aa;
+ --warning-border: #f59e0b;
+
+ --shadow-primary: 0 8px 32px rgba(0, 0, 0, 0.3);
+ --shadow-secondary: 0 20px 60px rgba(0, 0, 0, 0.4);
+ --shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.8);
+
+ --glass-bg: rgba(30, 41, 59, 0.95);
+ --glass-border: rgba(100, 116, 139, 0.2);
+}
+
+/* 登录页面样式 */
+.login-container {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-primary);
+ padding: 20px;
+}
+
+/* 自动登录加载页面样式 */
+.auto-login-content {
+ text-align: center;
+ padding: 20px;
+}
+
+.loading-spinner {
+ margin: 0 auto 20px;
+ width: 60px;
+ height: 60px;
+}
+
+.spinner {
+ width: 100%;
+ height: 100%;
+ border: 4px solid #e5e7eb;
+ border-top: 4px solid #3b82f6;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.auto-login-content h2 {
+ color: var(--text-secondary);
+ margin-bottom: 10px;
+ font-size: 24px;
+ font-weight: 600;
+}
+
+.auto-login-content p {
+ color: var(--text-tertiary);
+ font-size: 16px;
+ line-height: 1.5;
+}
+
+/* 登录页面头部布局 */
+.login-header-top {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 30px;
+ position: relative;
+}
+
+.login-title {
+ width: 100%;
+ text-align: center;
+ margin-bottom: 30px;
+}
+
+/* 头部控制按钮组 */
+.header-controls {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ height: 100%;
+}
+
+/* 登录页面的控制按钮组样式 */
+.login-header-top .header-controls {
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ border-radius: 12px;
+ padding: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ margin-top: 8px; /* 与标题拉开距离,避免遮挡 */
+ margin-bottom: 20px;
+}
+
+.login-header-top .header-controls:hover {
+ background: rgba(255, 255, 255, 0.15);
+ border-color: rgba(255, 255, 255, 0.3);
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
+ transform: translateY(-1px);
+}
+
+[data-theme="dark"] .login-header-top .header-controls {
+ background: rgba(30, 41, 59, 0.8);
+ border: 1px solid rgba(100, 116, 139, 0.3);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
+}
+
+[data-theme="dark"] .login-header-top .header-controls:hover {
+ background: rgba(30, 41, 59, 0.9);
+ border-color: rgba(100, 116, 139, 0.5);
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
+}
+
+.language-switcher,
+.theme-switcher {
+ position: relative;
+ flex-shrink: 0;
+}
+
+.language-btn,
+.theme-btn {
+ padding: 10px 20px;
+ font-size: 14px;
+ font-weight: 500;
+ border-radius: 8px;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-primary);
+ color: var(--text-tertiary);
+ transition: all 0.3s ease;
+ white-space: nowrap;
+ backdrop-filter: blur(5px);
+ height: 40px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+/* 登录页面的按钮样式优化 */
+.login-header-top .language-btn,
+.login-header-top .theme-btn {
+ padding: 8px 12px;
+ font-size: 13px;
+ height: 36px;
+ min-width: 36px;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.9);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ color: #64748b;
+ backdrop-filter: blur(10px);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.login-header-top .language-btn:hover,
+.login-header-top .theme-btn:hover {
+ background: rgba(255, 255, 255, 1);
+ border-color: rgba(100, 116, 139, 0.3);
+ color: #475569;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+[data-theme="dark"] .login-header-top .language-btn,
+[data-theme="dark"] .login-header-top .theme-btn {
+ background: rgba(30, 41, 59, 0.9);
+ border: 1px solid rgba(100, 116, 139, 0.3);
+ color: #94a3b8;
+}
+
+[data-theme="dark"] .login-header-top .language-btn:hover,
+[data-theme="dark"] .login-header-top .theme-btn:hover {
+ background: rgba(51, 65, 85, 0.95);
+ border-color: rgba(100, 116, 139, 0.5);
+ color: #cbd5e1;
+}
+
+.language-btn:hover,
+.theme-btn:hover {
+ background: var(--accent-secondary);
+ border-color: var(--border-secondary);
+ color: var(--text-secondary);
+}
+
+.language-btn i,
+.theme-btn i {
+ margin-right: 6px;
+}
+
+.theme-btn.active {
+ background: var(--accent-primary);
+ color: var(--text-inverse);
+}
+
+.login-card {
+ background: var(--glass-bg);
+ backdrop-filter: blur(10px);
+ border-radius: 20px;
+ padding: 40px;
+ width: 100%;
+ max-width: 500px;
+ box-shadow: var(--shadow-secondary);
+ border: 1px solid var(--glass-border);
+}
+
+.login-header {
+ text-align: center;
+ margin-bottom: 25px;
+}
+
+.login-title {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 15px;
+ color: var(--text-secondary);
+ font-size: 1.8rem;
+ font-weight: 600;
+ margin-bottom: 0;
+ line-height: 1.2;
+}
+
+#login-logo {
+ height: 60px;
+ width: auto;
+ object-fit: contain;
+ border-radius: 6px;
+ padding: 2px;
+ background: #fff;
+ border: 1px solid rgba(15, 23, 42, 0.06);
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
+}
+
+.login-subtitle {
+ color: #64748b;
+ font-size: 1rem;
+ margin: 0;
+ text-align: center;
+}
+
+.login-body {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.login-connection-info {
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-primary);
+ border-radius: 16px;
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+[data-theme="dark"] .login-connection-info {
+ background: rgba(30, 41, 59, 0.7);
+ border-color: rgba(100, 116, 139, 0.3);
+}
+
+.connection-summary {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ color: var(--text-secondary);
+}
+
+.connection-summary i {
+ font-size: 24px;
+ color: var(--primary-color);
+}
+
+.connection-url {
+ font-size: 16px;
+ color: var(--text-secondary);
+}
+
+.connection-url-separator {
+ margin: 0 8px;
+ color: var(--text-tertiary);
+}
+
+#login-connection-url {
+ font-family: "Fira Code", "Consolas", "Courier New", monospace;
+ color: var(--text-primary);
+ word-break: break-all;
+}
+
+[data-theme="dark"] #login-connection-url {
+ color: var(--text-secondary);
+}
+
+.login-form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.login-form .form-group {
+ margin-bottom: 25px;
+}
+
+.login-form .form-group label {
+ display: block;
+ margin-bottom: 8px;
+ color: var(--text-secondary);
+ font-weight: 600;
+ font-size: 0.95rem;
+}
+
+.login-form .form-hint {
+ margin-top: 6px;
+ color: #64748b;
+ font-size: 12px;
+}
+
+.login-form .input-group {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+.login-form input[type="text"],
+.login-form input[type="password"] {
+ flex: 1;
+ padding: 14px 16px;
+ border: 2px solid var(--border-primary);
+ border-radius: 10px;
+ font-size: 15px;
+ transition: all 0.3s ease;
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+.login-form input:focus {
+ outline: none;
+ border-color: var(--border-focus);
+ box-shadow: 0 0 0 3px var(--border-primary);
+}
+
+.login-form .btn-secondary {
+ padding: 14px 16px;
+ border: 2px solid #e2e8f0;
+ background: #f8fafc;
+ color: #64748b;
+ border-radius: 10px;
+ transition: all 0.3s ease;
+}
+
+.login-form .btn-secondary:hover {
+ background: #e2e8f0;
+ border-color: #cbd5e0;
+}
+
+.form-actions {
+ margin-top: 30px;
+ text-align: center;
+}
+
+.login-btn {
+ width: 100%;
+ padding: 16px 24px;
+ font-size: 16px;
+ font-weight: 600;
+ border-radius: 12px;
+ background: linear-gradient(135deg, #475569, #334155);
+ color: white;
+ border: none;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+}
+
+.login-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(51, 65, 85, 0.4);
+}
+
+.login-btn:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.login-error {
+ background: var(--error-bg);
+ border: 1px solid var(--error-border);
+ color: var(--error-text);
+ padding: 12px 16px;
+ border-radius: 8px;
+ margin-top: 20px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.login-error i {
+ color: var(--error-text);
+}
+
+/* 响应式设计 - 登录页面 */
+@media (max-width: 640px) {
+ .login-card {
+ padding: 30px 20px;
+ margin: 10px;
+ max-width: 100%;
+ }
+
+ .login-header-top {
+ flex-direction: column;
+ gap: 20px;
+ align-items: center;
+ margin-bottom: 30px;
+ }
+
+ .header-controls {
+ flex-direction: row;
+ justify-content: center;
+ gap: 8px;
+ margin-bottom: 10px; /* 在小屏幕上给控制按钮组添加下边距 */
+ }
+
+ /* 登录页面小屏幕优化 */
+ .login-header-top .header-controls {
+ margin: 8px auto 0 auto; /* 顶部留白,避免与标题拥挤 */
+ background: rgba(255, 255, 255, 0.08);
+ padding: 6px;
+ border-radius: 10px;
+ }
+
+ .login-header-top .language-btn,
+ .login-header-top .theme-btn {
+ padding: 6px 10px;
+ font-size: 12px;
+ height: 32px;
+ min-width: 32px;
+ }
+
+ .language-btn,
+ .theme-btn {
+ padding: 8px 16px;
+ font-size: 13px;
+ height: 36px; /* 在小屏幕上稍微减小高度 */
+ }
+
+ .login-title {
+ font-size: 1.5rem;
+ flex-direction: column;
+ gap: 10px;
+ text-align: center;
+ justify-content: center;
+ margin-bottom: 25px;
+ }
+
+ #login-logo {
+ height: 50px;
+ }
+
+ .login-form .input-group {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .login-form .btn-secondary {
+ width: 100%;
+ margin-top: 10px;
+ }
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: var(--bg-primary);
+ min-height: 100vh;
+ color: var(--text-primary);
+ transition: background-color 0.3s ease, color 0.3s ease;
+}
+
+.container {
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+/* 头部样式 */
+.header {
+ background: var(--glass-bg);
+ backdrop-filter: blur(10px);
+ border-radius: 15px;
+ padding: 48px 28px; /* 再次拉长,适配更大 Logo */
+ margin-bottom: 20px;
+ box-shadow: var(--shadow-primary);
+}
+
+.header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.header h1 {
+ color: var(--text-secondary);
+ font-size: 1.6rem; /* 字体小一号 */
+ font-weight: 600;
+}
+
+.header h1.brand {
+ display: flex;
+ align-items: flex-end; /* 文字在 Logo 右下角(底对齐) */
+ gap: 18px;
+ line-height: 1; /* 减少标题内上下空白 */
+}
+
+.header h1 i {
+ color: #64748b; /* slate-500 */
+ margin-right: 10px;
+}
+
+/* 站点 Logo(嵌入头部) */
+#site-logo {
+ height: 88px;
+ width: auto;
+ display: inline-block;
+ object-fit: contain;
+ border-radius: 6px;
+ padding: 2px;
+ background: #fff;
+ border: 1px solid rgba(15, 23, 42, 0.06);
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
+}
+
+.brand-title {
+ display: inline-block;
+ line-height: 1.1; /* 保持原字号不变,同时更紧凑 */
+ margin-bottom: 4px; /* 让文字更贴近更高的 Logo 底部 */
+}
+
+/* 小屏幕适配:避免 Logo 过大占位 */
+@media (max-width: 640px) {
+ #site-logo { height: 64px; }
+}
+
+.header-actions {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ height: 40px; /* 统一高度 */
+}
+
+
+/* 认证区域 */
+.auth-section {
+ margin-bottom: 20px;
+}
+
+/* 连接信息样式 */
+.connection-info {
+ background: var(--bg-tertiary);
+ border-radius: 12px;
+ padding: 20px;
+ margin-bottom: 20px;
+}
+
+.info-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid var(--border-primary);
+}
+
+.info-item:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+}
+
+.info-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--text-secondary);
+ font-weight: 600;
+ font-size: 14px;
+}
+
+.info-label i {
+ color: var(--text-tertiary);
+ width: 16px;
+}
+
+.info-value {
+ color: var(--text-primary);
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 13px;
+ background: var(--bg-secondary);
+ padding: 6px 12px;
+ border-radius: 6px;
+ border: 1px solid var(--border-primary);
+ max-width: 300px;
+ word-break: break-all;
+}
+
+.status-indicator {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 12px;
+ border-radius: 20px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.status-indicator.connected {
+ background: var(--success-bg);
+ color: var(--success-text);
+ border: 1px solid var(--success-border);
+}
+
+.status-indicator.disconnected {
+ background: var(--error-bg);
+ color: var(--error-text);
+ border: 1px solid var(--error-border);
+}
+
+.status-indicator.connecting {
+ background: var(--warning-bg);
+ color: var(--warning-text);
+ border: 1px solid var(--warning-border);
+}
+
+.connection-actions {
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+}
+
+.connection-actions .btn {
+ flex: 1;
+ max-width: 150px;
+}
+
+/* 响应式设计 - 连接信息 */
+@media (max-width: 768px) {
+ .info-item {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ }
+
+ .info-value {
+ max-width: 100%;
+ width: 100%;
+ }
+
+ .connection-actions {
+ flex-direction: column;
+ }
+
+ .connection-actions .btn {
+ max-width: 100%;
+ }
+}
+
+/* 主要内容区域 */
+.main-content {
+ display: flex;
+ gap: 20px;
+}
+
+/* 侧边栏 */
+.sidebar {
+ width: 250px;
+ background: var(--glass-bg);
+ backdrop-filter: blur(10px);
+ border-radius: 15px;
+ padding: 20px;
+ height: fit-content;
+ box-shadow: var(--shadow-primary);
+}
+
+.nav-menu {
+ list-style: none;
+}
+
+.nav-menu li {
+ margin-bottom: 8px;
+}
+
+.nav-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ color: var(--text-secondary);
+ text-decoration: none;
+ border-radius: 10px;
+ transition: all 0.3s ease;
+ border: 2px solid transparent;
+}
+
+.nav-item:hover {
+ background: var(--accent-tertiary);
+ color: var(--text-secondary);
+}
+
+.nav-item.active {
+ background: var(--accent-primary);
+ color: var(--text-inverse);
+ box-shadow: var(--shadow-primary);
+}
+
+.nav-item i {
+ margin-right: 10px;
+ width: 20px;
+}
+
+/* 内容区域 */
+.content-area {
+ flex: 1;
+}
+
+.content-section {
+ display: none;
+}
+
+.content-section.active {
+ display: block;
+}
+
+.content-section h2 {
+ color: var(--text-secondary);
+ margin-bottom: 20px;
+ font-size: 1.5rem;
+ font-weight: 600;
+}
+
+/* 卡片样式 */
+.card {
+ background: var(--glass-bg);
+ backdrop-filter: blur(10px);
+ border-radius: 15px;
+ margin-bottom: 20px;
+ box-shadow: var(--shadow-primary);
+ overflow: hidden;
+}
+
+.card-header {
+ background: var(--bg-tertiary);
+ padding: 20px;
+ border-bottom: 1px solid var(--border-primary);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.card-header h3 {
+ color: var(--text-secondary);
+ font-size: 1.2rem;
+ font-weight: 600;
+}
+
+.card-header h3 i {
+ color: var(--text-tertiary);
+ margin-right: 10px;
+}
+
+.card-content {
+ padding: 20px;
+}
+
+/* 按钮样式 */
+.btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ text-decoration: none;
+ height: 40px;
+ white-space: nowrap;
+}
+
+.btn-primary {
+ background: var(--accent-primary);
+ color: var(--text-inverse);
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-primary);
+}
+
+.btn-secondary {
+ background: var(--accent-secondary);
+ color: var(--text-secondary);
+}
+
+.btn-secondary:hover {
+ background: var(--border-secondary);
+ transform: translateY(-1px);
+}
+
+.btn-danger {
+ background: linear-gradient(135deg, #ef4444, #b91c1c);
+ color: white;
+}
+
+.btn-danger:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(185, 28, 28, 0.45);
+}
+
+.btn-success {
+ background: linear-gradient(135deg, #22c55e, #15803d);
+ color: white;
+}
+
+.btn-success:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(21, 128, 61, 0.45);
+}
+
+.btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none !important;
+}
+
+/* 表单元素 */
+.form-group {
+ margin-bottom: 16px;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 6px;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.form-hint {
+ font-size: 0.85rem;
+ color: var(--text-tertiary);
+ margin: 4px 0 8px;
+}
+
+.input-group {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+}
+
+input[type="text"],
+input[type="password"],
+input[type="number"],
+input[type="url"],
+textarea,
+select {
+ flex: 1;
+ padding: 12px 16px;
+ border: 2px solid var(--border-primary);
+ border-radius: 8px;
+ font-size: 14px;
+ transition: all 0.3s ease;
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+}
+
+input:focus,
+textarea:focus,
+select:focus {
+ outline: none;
+ border-color: var(--border-focus);
+ box-shadow: 0 0 0 3px var(--border-primary);
+}
+
+/* 切换开关 */
+.toggle-group {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ margin-bottom: 15px;
+}
+
+.toggle-switch {
+ position: relative;
+ display: inline-block;
+ width: 50px;
+ height: 25px;
+}
+
+.toggle-switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: var(--accent-secondary);
+ transition: .4s;
+ border-radius: 25px;
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 19px;
+ width: 19px;
+ left: 3px;
+ bottom: 3px;
+ background-color: var(--text-inverse);
+ transition: .4s;
+ border-radius: 50%;
+}
+
+input:checked + .slider {
+ background: var(--accent-primary);
+}
+
+input:checked + .slider:before {
+ transform: translateX(25px);
+}
+
+.toggle-label {
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+/* 列表样式 */
+.key-list,
+.provider-list,
+.file-list {
+ max-height: 400px;
+ overflow-y: auto;
+}
+
+.key-item,
+.provider-item,
+.file-item {
+ background: var(--bg-quaternary);
+ border: 1px solid var(--border-primary);
+ border-radius: 8px;
+ padding: 15px;
+ margin-bottom: 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ transition: all 0.3s ease;
+}
+
+.key-item:hover,
+.provider-item:hover,
+.file-item:hover {
+ background: var(--bg-tertiary);
+ border-color: var(--border-secondary);
+ transform: translateY(-1px);
+}
+
+.item-content {
+ flex: 1;
+}
+
+.item-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.item-title {
+ font-weight: 600;
+ color: var(--text-secondary);
+ margin-bottom: 4px;
+}
+
+.item-subtitle {
+ color: var(--text-tertiary);
+ font-size: 0.9rem;
+}
+
+.provider-item .provider-models {
+ margin-top: 8px;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.provider-model-tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ background: var(--bg-quinary);
+ color: var(--text-secondary);
+ border: 1px solid var(--border-secondary);
+ border-radius: 14px;
+ padding: 4px 10px;
+ margin-right: 6px;
+ margin-top: 6px;
+ font-size: 0.85rem;
+}
+
+.provider-model-tag .model-name {
+ font-weight: 600;
+}
+
+.provider-model-tag .model-alias {
+ color: var(--text-tertiary);
+ font-style: italic;
+}
+
+.item-value {
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ background: var(--bg-tertiary);
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ word-break: break-all;
+}
+
+/* 状态信息 */
+.status-info {
+ background: var(--bg-quaternary);
+ border-radius: 8px;
+ padding: 20px;
+}
+
+.status-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid var(--border-primary);
+}
+
+.status-item:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ border-bottom: none;
+}
+
+.status-label {
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+.status-value {
+ color: var(--text-tertiary);
+}
+
+/* 模态框 */
+.modal {
+ display: none;
+ position: fixed;
+ z-index: 1000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background-color: var(--bg-modal);
+ backdrop-filter: blur(5px);
+}
+
+.modal-content {
+ background-color: var(--bg-secondary);
+ margin: 4% auto;
+ padding: 0;
+ border-radius: 15px;
+ width: 90%;
+ max-width: 550px;
+ box-shadow: var(--shadow-modal);
+ animation: modalSlideIn 0.3s ease;
+ position: relative;
+}
+
+@keyframes modalSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-30px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.close {
+ color: var(--text-tertiary);
+ position: absolute;
+ top: 15px;
+ right: 20px;
+ font-size: 28px;
+ font-weight: bold;
+ cursor: pointer;
+ z-index: 1001;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: all 0.3s ease;
+}
+
+.close:hover,
+.close:focus {
+ color: var(--text-secondary);
+ background-color: var(--bg-tertiary);
+}
+
+#modal-body {
+ padding: 35px 30px 30px 30px;
+}
+
+/* 模态框标题样式 */
+#modal-body h3 {
+ color: var(--text-secondary);
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin: 0 0 20px 0;
+ text-align: center;
+ border-bottom: 2px solid var(--border-primary);
+ padding-bottom: 12px;
+}
+
+/* 模态框表单组 */
+#modal-body .form-group {
+ margin-bottom: 16px;
+}
+
+#modal-body .form-group label {
+ display: block;
+ margin-bottom: 6px;
+ color: var(--text-secondary);
+ font-weight: 600;
+ font-size: 14px;
+}
+
+#modal-body .form-group input,
+#modal-body .form-group textarea {
+ width: 100%;
+ padding: 12px 16px;
+ border: 2px solid var(--border-primary);
+ border-radius: 8px;
+ font-size: 14px;
+ transition: all 0.3s ease;
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ font-family: inherit;
+}
+
+#modal-body .form-group input:focus,
+#modal-body .form-group textarea:focus {
+ outline: none;
+ border-color: var(--border-focus);
+ box-shadow: 0 0 0 3px var(--border-primary);
+}
+
+#modal-body .form-group textarea {
+ resize: vertical;
+ min-height: 80px;
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 13px;
+}
+
+/* 模态框按钮组 */
+#modal-body .modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ margin-top: 24px;
+ padding-top: 18px;
+ border-top: 1px solid var(--border-primary);
+}
+
+#modal-body .modal-actions .btn {
+ min-width: 80px;
+ padding: 10px 20px;
+}
+
+/* 通知 */
+.notification {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ padding: 15px 20px;
+ border-radius: 8px;
+ color: white;
+ font-weight: 500;
+ z-index: 1001;
+ transform: translateX(400px);
+ transition: all 0.3s ease;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
+}
+
+.notification.show {
+ transform: translateX(0);
+}
+
+.notification.success {
+ background: linear-gradient(135deg, #68d391, #38a169);
+}
+
+.notification.error {
+ background: linear-gradient(135deg, #fc8181, #e53e3e);
+}
+
+.notification.info {
+ background: linear-gradient(135deg, #63b3ed, #3182ce);
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .main-content {
+ flex-direction: column;
+ }
+
+ .sidebar {
+ width: 100%;
+ order: 2;
+ }
+
+ .content-area {
+ order: 1;
+ }
+
+ .nav-menu {
+ display: flex;
+ overflow-x: auto;
+ gap: 10px;
+ }
+
+ .nav-menu li {
+ margin-bottom: 0;
+ flex-shrink: 0;
+ }
+
+ .header-content {
+ flex-direction: column;
+ gap: 15px;
+ text-align: center;
+ }
+
+ .input-group {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .card-header {
+ flex-direction: column;
+ gap: 15px;
+ align-items: flex-start;
+ }
+
+ .header-actions {
+ width: 100%;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .header-controls {
+ order: -1; /* 确保控制按钮在小屏幕上排在前面 */
+ }
+
+ /* 模态框响应式 */
+ .modal-content {
+ margin: 5% auto;
+ width: 95%;
+ max-width: none;
+ }
+
+ #modal-body {
+ padding: 40px 25px 25px 25px;
+ }
+
+ #modal-body h3 {
+ font-size: 1.2rem;
+ margin-bottom: 20px;
+ }
+
+ #modal-body .modal-actions {
+ flex-direction: column-reverse;
+ gap: 10px;
+ }
+
+ #modal-body .modal-actions .btn {
+ width: 100%;
+ margin: 0;
+ }
+}
+
+/* 加载动画 */
+.loading {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ border: 3px solid rgba(255, 255, 255, 0.3);
+ border-radius: 50%;
+ border-top-color: #fff;
+ animation: spin 1s ease-in-out infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* 空状态 */
+.empty-state {
+ text-align: center;
+ padding: 40px 20px;
+ color: var(--text-tertiary);
+}
+
+.empty-state i {
+ font-size: 48px;
+ margin-bottom: 16px;
+ color: var(--border-secondary);
+}
+
+.empty-state h3 {
+ margin-bottom: 8px;
+ color: var(--text-secondary);
+}
+
+/* 滚动条样式 */
+::-webkit-scrollbar {
+ width: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: linear-gradient(135deg, #475569, #334155);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: linear-gradient(135deg, #334155, #1f2937);
+}
+
+/* 连接状态指示器 */
+.connection-indicator {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ margin-right: 8px;
+}
+
+.connection-indicator.connected {
+ background-color: #68d391;
+ box-shadow: 0 0 8px rgba(104, 211, 145, 0.6);
+}
+
+.connection-indicator.disconnected {
+ background-color: #fc8181;
+ box-shadow: 0 0 8px rgba(252, 129, 129, 0.6);
+}
+
+.connection-indicator.connecting {
+ background-color: #fbb040;
+ animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+
+/* Gemini Web Token 模态框样式 */
+.gemini-web-form .form-group {
+ margin-bottom: 20px;
+}
+
+.gemini-web-form .form-group label {
+ display: block;
+ margin-bottom: 8px;
+ color: var(--text-secondary);
+ font-weight: 600;
+ font-size: 14px;
+}
+
+.gemini-web-form .form-group input {
+ width: 100%;
+ padding: 12px 16px;
+ border: 2px solid var(--border-primary);
+ border-radius: 8px;
+ font-size: 14px;
+ transition: all 0.3s ease;
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+}
+
+.gemini-web-form .form-group input:focus {
+ outline: none;
+ border-color: var(--border-focus);
+ box-shadow: 0 0 0 3px var(--border-primary);
+}
+
+.gemini-web-form .form-hint {
+ margin-top: 6px;
+ color: var(--text-tertiary);
+ font-size: 12px;
+ line-height: 1.4;
+}
+
+/* 使用统计样式 */
+.stats-overview {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.stat-card {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 12px;
+ padding: 24px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.stat-card:hover {
+ border-color: var(--border-primary);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ transform: translateY(-2px);
+}
+
+.stat-icon {
+ width: 50px;
+ height: 50px;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--primary-color);
+ color: white;
+ font-size: 20px;
+ flex-shrink: 0;
+}
+
+.stat-icon.success {
+ background: #10b981;
+}
+
+.stat-icon.error {
+ background: #ef4444;
+}
+
+.stat-content {
+ flex: 1;
+}
+
+.stat-number {
+ font-size: 28px;
+ font-weight: 700;
+ color: var(--text-primary);
+ line-height: 1;
+ margin-bottom: 4px;
+}
+
+.stat-label {
+ font-size: 14px;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.charts-container {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+@media (max-width: 1200px) {
+ .charts-container {
+ grid-template-columns: 1fr;
+ }
+}
+
+.chart-card {
+ min-height: 400px;
+}
+
+.chart-container {
+ position: relative;
+ height: 300px;
+ width: 100%;
+}
+
+.chart-controls {
+ display: flex;
+ gap: 8px;
+}
+
+.btn.btn-small {
+ padding: 6px 12px;
+ font-size: 12px;
+ border-radius: 6px;
+ border: 1px solid var(--border-color);
+ background: var(--card-bg);
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.btn.btn-small:hover {
+ border-color: var(--border-primary);
+ color: var(--text-primary);
+}
+
+.btn.btn-small.active {
+ background: var(--primary-color);
+ border-color: var(--primary-color);
+ color: white;
+}
+
+.api-stats-table {
+ overflow-x: auto;
+}
+
+.stats-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 16px;
+}
+
+.stats-table th,
+.stats-table td {
+ padding: 12px 16px;
+ text-align: left;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.stats-table th {
+ background: var(--bg-secondary);
+ font-weight: 600;
+ color: var(--text-primary);
+ font-size: 14px;
+}
+
+.stats-table td {
+ color: var(--text-secondary);
+ font-size: 14px;
+}
+
+.stats-table tr:hover {
+ background: var(--bg-secondary);
+}
+
+.model-details {
+ margin-top: 8px;
+ padding: 8px 12px;
+ background: var(--bg-tertiary);
+ border-radius: 6px;
+ font-size: 12px;
+}
+
+.model-item {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 4px;
+ color: var(--text-tertiary);
+}
+
+.model-name {
+ font-weight: 500;
+ color: var(--text-secondary);
+}
+
+.loading-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100px;
+ color: var(--text-tertiary);
+ font-size: 14px;
+}
+
+.no-data-message {
+ text-align: center;
+ color: var(--text-tertiary);
+ font-style: italic;
+ padding: 40px;
+}
+
+/* 暗色主题适配 */
+[data-theme="dark"] .stat-card {
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+[data-theme="dark"] .stat-card:hover {
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
+}
+
+[data-theme="dark"] .btn.btn-small {
+ background: var(--bg-tertiary);
+}
+
+/* 版本信息样式 */
+.version-footer {
+ margin-top: 2rem;
+ padding: 1rem 0;
+ border-top: 1px solid var(--border);
+ background: var(--bg-secondary);
+}
+
+.version-info {
+ text-align: center;
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ opacity: 0.8;
+}
+
+.version-info .separator {
+ margin: 0 0.75rem;
+ color: var(--text-secondary);
+ opacity: 0.6;
+}
+
+/* 暗黑主题下的版本信息 */
+[data-theme="dark"] .version-footer {
+ border-top-color: var(--border);
+ background: var(--bg-secondary);
+}
+
+[data-theme="dark"] .version-info {
+ color: var(--text-secondary);
+}
+
+.connection-reset-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ white-space: nowrap;
+}
+
+.connection-reset-btn i {
+ margin: 0;
+}
+
+.connection-reset-btn span {
+ font-size: 13px;
+}
+
+[data-theme="dark"] .connection-reset-btn {
+ background: rgba(30, 41, 59, 0.9);
+ border-color: rgba(100, 116, 139, 0.4);
+ color: #cbd5e1;
+}
+
+[data-theme="dark"] .connection-reset-btn:hover {
+ background: rgba(51, 65, 85, 0.95);
+ border-color: rgba(100, 116, 139, 0.6);
+}
+
+.model-input-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.model-input-row .input-group {
+ display: flex;
+ gap: 8px;
+}
+
+.model-input-row .model-name-input,
+.model-input-row .model-alias-input {
+ flex: 1;
+}
+
+.model-input-row .model-alias-input {
+ max-width: 220px;
+}
+
+.model-input-row .model-remove-btn {
+ align-self: center;
+}
+