} 是否复制成功
- */
-async function copyToClipboard(text) {
- try {
- if (navigator.clipboard && navigator.clipboard.writeText) {
- await navigator.clipboard.writeText(text);
- return true;
- }
-
- // Fallback for older browsers
- const textArea = document.createElement('textarea');
- textArea.value = text;
- textArea.style.position = 'fixed';
- textArea.style.left = '-9999px';
- textArea.style.top = '-9999px';
- document.body.appendChild(textArea);
- textArea.focus();
- textArea.select();
-
- const successful = document.execCommand('copy');
- document.body.removeChild(textArea);
- return successful;
- } catch (error) {
- console.error('[Models Manager] Failed to copy to clipboard:', error);
- return false;
- }
-}
-
-/**
- * 显示复制成功的 Toast 提示
- * @param {string} modelName - 模型名称
- */
-function showCopyToast(modelName) {
- const toastContainer = document.getElementById('toastContainer');
- if (!toastContainer) return;
-
- const toast = document.createElement('div');
- toast.className = 'toast toast-success';
- toast.innerHTML = `
-
- ${t('models.copied') || '已复制'}: ${modelName}
- `;
-
- toastContainer.appendChild(toast);
-
- // 自动移除
- setTimeout(() => {
- toast.classList.add('toast-fade-out');
- setTimeout(() => {
- toast.remove();
- }, 300);
- }, 2000);
-}
-
-/**
- * 渲染模型列表
- * @param {Object} models - 模型数据
- */
-function renderModelsList(models) {
- const container = document.getElementById('modelsList');
- if (!container) return;
-
- // 检查是否有模型数据
- const providerTypes = Object.keys(models);
- if (providerTypes.length === 0) {
- container.innerHTML = `
-
-
-
${t('models.empty') || '暂无可用模型'}
-
- `;
- return;
- }
-
- // 渲染每个提供商的模型组
- let html = '';
-
- for (const providerType of providerTypes) {
- const modelList = models[providerType];
- if (!modelList || modelList.length === 0) continue;
-
- // 如果配置了不可见,则跳过
- if (currentProviderConfigs) {
- const config = currentProviderConfigs.find(c => c.id === providerType);
- if (config && config.visible === false) continue;
- }
-
- const providerDisplayName = getProviderDisplayName(providerType);
- const providerIcon = getProviderIcon(providerType);
-
- html += `
-
-
-
- ${modelList.map(model => `
-
-
-
-
-
${escapeHtml(model)}
-
-
-
-
- `).join('')}
-
-
- `;
- }
-
- container.innerHTML = html;
-}
-
-/**
- * 获取提供商显示名称
- * @param {string} providerType - 提供商类型
- * @returns {string} 显示名称
- */
-function getProviderDisplayName(providerType) {
- // 优先从外部传入的配置中获取名称
- if (currentProviderConfigs) {
- const config = currentProviderConfigs.find(c => c.id === providerType);
- if (config && config.name) {
- return config.name;
- }
- }
-
- const displayNames = {
- 'gemini-cli-oauth': 'Gemini CLI (OAuth)',
- 'gemini-antigravity': 'Gemini Antigravity',
- 'claude-custom': 'Claude Custom',
- 'claude-kiro-oauth': 'Claude Kiro (OAuth)',
- 'openai-custom': 'OpenAI Custom',
- 'openaiResponses-custom': 'OpenAI Responses Custom',
- 'openai-qwen-oauth': 'Qwen (OAuth)',
- 'openai-iflow': 'iFlow',
- 'openai-codex-oauth': 'OpenAI Codex (OAuth)'
- };
-
- return displayNames[providerType] || providerType;
-}
-
-/**
- * 获取提供商图标
- * @param {string} providerType - 提供商类型
- * @returns {string} 图标类名
- */
-function getProviderIcon(providerType) {
- // 优先从外部传入的配置中获取图标
- if (currentProviderConfigs) {
- const config = currentProviderConfigs.find(c => c.id === providerType);
- if (config && config.icon) {
- // 如果 icon 已经包含 fa- 则直接使用,否则加上 fas
- return config.icon.startsWith('fa-') ? `fas ${config.icon}` : config.icon;
- }
- }
-
- if (providerType.includes('gemini')) {
- return 'fas fa-gem';
- } else if (providerType.includes('claude')) {
- return 'fas fa-robot';
- } else if (providerType.includes('openai') || providerType.includes('qwen') || providerType.includes('iflow')) {
- return 'fas fa-brain';
- }
- return 'fas fa-server';
-}
-
-/**
- * HTML 转义
- * @param {string} text - 原始文本
- * @returns {string} 转义后的文本
- */
-function escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
-}
-
-/**
- * 切换提供商模型列表的展开/折叠状态
- * @param {string} providerType - 提供商类型
- */
-function toggleProviderModels(providerType) {
- const group = document.querySelector(`.provider-models-group[data-provider="${providerType}"]`);
- if (!group) return;
-
- const header = group.querySelector('.provider-models-header');
- const content = group.querySelector('.provider-models-content');
-
- if (content.classList.contains('collapsed')) {
- content.classList.remove('collapsed');
- header.classList.remove('collapsed');
- } else {
- content.classList.add('collapsed');
- header.classList.add('collapsed');
- }
-}
-
-/**
- * 复制模型名称
- * @param {string} modelName - 模型名称
- * @param {HTMLElement} element - 点击的元素
- */
-async function copyModelName(modelName, element) {
- const success = await copyToClipboard(modelName);
-
- if (success) {
- // 添加复制成功的视觉反馈
- element.classList.add('copied');
- setTimeout(() => {
- element.classList.remove('copied');
- }, 1000);
-
- // 显示 Toast 提示
- showCopyToast(modelName);
- }
-}
-
-/**
- * 初始化模型管理器
- */
-async function initModelsManager() {
- const container = document.getElementById('modelsList');
- if (!container) return;
-
- try {
- const models = await fetchProviderModels();
- renderModelsList(models);
- } catch (error) {
- container.innerHTML = `
-
-
-
${t('models.loadError') || '加载模型列表失败'}
-
- `;
- }
-}
-
-/**
- * 刷新模型列表
- */
-async function refreshModels() {
- modelsCache = null;
- await initModelsManager();
-}
-
-// 导出到全局作用域供 HTML 调用
-window.toggleProviderModels = toggleProviderModels;
-window.copyModelName = copyModelName;
-window.refreshModels = refreshModels;
-
-// 监听组件加载完成事件
-window.addEventListener('componentsLoaded', () => {
- initModelsManager();
-});
-
-// 导出函数
-export {
- initModelsManager,
- refreshModels,
- fetchProviderModels,
- updateModelsProviderConfigs
-};
diff --git a/static/app/navigation.js b/static/app/navigation.js
deleted file mode 100644
index 142b80ae9462ee42e14badc8031632271f512ad8..0000000000000000000000000000000000000000
--- a/static/app/navigation.js
+++ /dev/null
@@ -1,115 +0,0 @@
-// 导航功能模块
-
-import { elements } from './constants.js';
-
-/**
- * 初始化导航功能
- */
-function initNavigation() {
- if (!elements.navItems || !elements.sections) {
- console.warn('导航元素未找到');
- return;
- }
-
- elements.navItems.forEach(item => {
- item.addEventListener('click', (e) => {
- e.preventDefault();
- const sectionId = item.dataset.section;
-
- // 更新导航状态
- elements.navItems.forEach(nav => nav.classList.remove('active'));
- item.classList.add('active');
-
- // 显示对应章节
- elements.sections.forEach(section => {
- section.classList.remove('active');
- if (section.id === sectionId) {
- section.classList.add('active');
-
- // 如果是日志页面,默认滚动到底部
- if (sectionId === 'logs') {
- setTimeout(() => {
- const logsContainer = document.getElementById('logsContainer');
- if (logsContainer) {
- logsContainer.scrollTop = logsContainer.scrollHeight;
- }
- }, 100);
- }
- }
- });
-
- // 滚动到顶部
- scrollToTop();
- });
- });
-}
-
-/**
- * 切换到指定章节
- * @param {string} sectionId - 章节ID
- */
-function switchToSection(sectionId) {
- // 更新导航状态
- elements.navItems.forEach(nav => {
- nav.classList.remove('active');
- if (nav.dataset.section === sectionId) {
- nav.classList.add('active');
- }
- });
-
- // 显示对应章节
- elements.sections.forEach(section => {
- section.classList.remove('active');
- if (section.id === sectionId) {
- section.classList.add('active');
-
- // 如果是日志页面,默认滚动到底部
- if (sectionId === 'logs') {
- setTimeout(() => {
- const logsContainer = document.getElementById('logsContainer');
- if (logsContainer) {
- logsContainer.scrollTop = logsContainer.scrollHeight;
- }
- }, 100);
- }
- }
- });
-
- // 滚动到顶部
- scrollToTop();
-}
-
-/**
- * 滚动到页面顶部
- */
-function scrollToTop() {
- // 尝试滚动内容区域
- const contentContainer = document.getElementById('content-container');
- if (contentContainer) {
- contentContainer.scrollTop = 0;
- }
-
- // 同时滚动窗口到顶部
- window.scrollTo(0, 0);
-}
-
-/**
- * 切换到仪表盘页面
- */
-function switchToDashboard() {
- switchToSection('dashboard');
-}
-
-/**
- * 切换到提供商页面
- */
-function switchToProviders() {
- switchToSection('providers');
-}
-
-export {
- initNavigation,
- switchToSection,
- switchToDashboard,
- switchToProviders
-};
\ No newline at end of file
diff --git a/static/app/plugin-manager.js b/static/app/plugin-manager.js
deleted file mode 100644
index b0e21cec528f6bc15f510d6461bce68f9e2b0161..0000000000000000000000000000000000000000
--- a/static/app/plugin-manager.js
+++ /dev/null
@@ -1,143 +0,0 @@
-import { t } from './i18n.js';
-import { showToast, apiRequest } from './utils.js';
-
-// 插件列表状态
-let pluginsList = [];
-
-/**
- * 初始化插件管理器
- */
-export function initPluginManager() {
- const refreshBtn = document.getElementById('refreshPluginsBtn');
- if (refreshBtn) {
- refreshBtn.addEventListener('click', loadPlugins);
- }
-
- // 初始加载
- loadPlugins();
-}
-
-/**
- * 加载插件列表
- */
-export async function loadPlugins() {
- const loadingEl = document.getElementById('pluginsLoading');
- const emptyEl = document.getElementById('pluginsEmpty');
- const listEl = document.getElementById('pluginsList');
- const totalEl = document.getElementById('totalPlugins');
- const enabledEl = document.getElementById('enabledPlugins');
- const disabledEl = document.getElementById('disabledPlugins');
-
- if (loadingEl) loadingEl.style.display = 'block';
- if (emptyEl) emptyEl.style.display = 'none';
- if (listEl) listEl.innerHTML = '';
-
- try {
- const response = await apiRequest('/api/plugins');
-
- if (response && response.plugins) {
- pluginsList = response.plugins;
- renderPluginsList();
-
- // 更新统计信息
- if (totalEl) totalEl.textContent = pluginsList.length;
- if (enabledEl) enabledEl.textContent = pluginsList.filter(p => p.enabled).length;
- if (disabledEl) disabledEl.textContent = pluginsList.filter(p => !p.enabled).length;
- } else {
- if (emptyEl) emptyEl.style.display = 'flex';
- }
- } catch (error) {
- console.error('Failed to load plugins:', error);
- showToast(t('common.error'), t('plugins.load.failed'), 'error');
- if (emptyEl) emptyEl.style.display = 'flex';
- } finally {
- if (loadingEl) loadingEl.style.display = 'none';
- }
-}
-
-/**
- * 渲染插件列表
- */
-function renderPluginsList() {
- const listEl = document.getElementById('pluginsList');
- const emptyEl = document.getElementById('pluginsEmpty');
-
- if (!listEl) return;
-
- listEl.innerHTML = '';
-
- if (pluginsList.length === 0) {
- if (emptyEl) emptyEl.style.display = 'flex';
- return;
- }
-
- if (emptyEl) emptyEl.style.display = 'none';
-
- pluginsList.forEach(plugin => {
- const card = document.createElement('div');
- card.className = `plugin-card ${plugin.enabled ? 'enabled' : 'disabled'}`;
-
- // 构建标签 HTML
- let badgesHtml = '';
- if (plugin.hasMiddleware) {
- badgesHtml += `Middleware`;
- }
- if (plugin.hasRoutes) {
- badgesHtml += `Routes`;
- }
- if (plugin.hasHooks) {
- badgesHtml += `Hooks`;
- }
-
- card.innerHTML = `
-
- ${plugin.description || t('plugins.noDescription')}
-
- ${badgesHtml}
-
-
- ${plugin.enabled ? t('plugins.status.enabled') : t('plugins.status.disabled')}
-
- `;
-
- listEl.appendChild(card);
- });
-}
-
-/**
- * 切换插件启用状态
- * @param {string} pluginName - 插件名称
- * @param {boolean} enabled - 是否启用
- */
-export async function togglePlugin(pluginName, enabled) {
- try {
- await apiRequest(`/api/plugins/${encodeURIComponent(pluginName)}/toggle`, {
- method: 'POST',
- body: JSON.stringify({ enabled })
- });
-
- showToast(t('common.success'), t('plugins.toggle.success', { name: pluginName, status: enabled ? t('common.enabled') : t('common.disabled') }), 'success');
-
- // 重新加载列表以更新状态
- loadPlugins();
-
- // 提示需要重启
- showToast(t('common.info'), t('plugins.restart.required'), 'info');
- } catch (error) {
- console.error(`Failed to toggle plugin ${pluginName}:`, error);
- showToast(t('common.error'), t('plugins.toggle.failed'), 'error');
- // 恢复开关状态
- loadPlugins();
- }
-}
\ No newline at end of file
diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js
deleted file mode 100644
index 1a60c14df85fa49b0d24af480803269c11937cae..0000000000000000000000000000000000000000
--- a/static/app/provider-manager.js
+++ /dev/null
@@ -1,3106 +0,0 @@
-// 提供商管理功能模块
-
-import { providerStats, updateProviderStats } from './constants.js';
-import { showToast, formatUptime, getProviderConfigs } from './utils.js';
-import { fileUploadHandler } from './file-upload.js';
-import { t, getCurrentLanguage } from './i18n.js';
-import { renderRoutingExamples } from './routing-examples.js';
-import { updateModelsProviderConfigs } from './models-manager.js';
-import { updateTutorialProviderConfigs } from './tutorial-manager.js';
-import { updateUsageProviderConfigs } from './usage-manager.js';
-import { updateConfigProviderConfigs } from './config-manager.js';
-import { loadConfigList, updateProviderFilterOptions } from './upload-config-manager.js';
-import { setServiceMode } from './event-handlers.js';
-
-// 保存初始服务器时间和运行时间
-let initialServerTime = null;
-let initialUptime = null;
-let initialLoadTime = null;
-let isStaticProviderConfigsUpdated = false;
-let cachedSupportedProviders = null;
-
-/**
- * 加载系统信息
- */
-async function loadSystemInfo() {
- try {
- const data = await window.apiClient.get('/system');
-
- const appVersionEl = document.getElementById('appVersion');
- const nodeVersionEl = document.getElementById('nodeVersion');
- const serverTimeEl = document.getElementById('serverTime');
- const memoryUsageEl = document.getElementById('memoryUsage');
- const cpuUsageEl = document.getElementById('cpuUsage');
- const uptimeEl = document.getElementById('uptime');
-
- if (appVersionEl) appVersionEl.textContent = data.appVersion ? `v${data.appVersion}` : '--';
-
- // 自动检查更新
- if (data.appVersion) {
- checkUpdate(true);
- }
-
- if (nodeVersionEl) nodeVersionEl.textContent = data.nodeVersion || '--';
- if (memoryUsageEl) memoryUsageEl.textContent = data.memoryUsage || '--';
- if (cpuUsageEl) cpuUsageEl.textContent = data.cpuUsage || '--';
-
- // 保存初始时间用于本地计算
- if (data.serverTime && data.uptime !== undefined) {
- initialServerTime = new Date(data.serverTime);
- initialUptime = data.uptime;
- initialLoadTime = Date.now();
- }
-
- // 初始显示
- if (serverTimeEl) {
- serverTimeEl.textContent = data.serverTime ? new Date(data.serverTime).toLocaleString(getCurrentLanguage()) : '--';
- }
- if (uptimeEl) uptimeEl.textContent = data.uptime ? formatUptime(data.uptime) : '--';
-
- // 加载服务模式信息
- await loadServiceModeInfo();
-
- } catch (error) {
- console.error('Failed to load system info:', error);
- }
-}
-
-/**
- * 加载服务运行模式信息
- */
-async function loadServiceModeInfo() {
- try {
- const data = await window.apiClient.get('/service-mode');
-
- const serviceModeEl = document.getElementById('serviceMode');
- const processPidEl = document.getElementById('processPid');
- const platformInfoEl = document.getElementById('platformInfo');
-
- // 更新服务模式到 event-handlers
- setServiceMode(data.mode || 'worker');
-
- // 更新重启/重载按钮显示
- updateRestartButton(data.mode);
-
- if (serviceModeEl) {
- const modeText = data.mode === 'worker'
- ? t('dashboard.serviceMode.worker')
- : t('dashboard.serviceMode.standalone');
- const canRestartIcon = data.canAutoRestart
- ? ''
- : '';
- serviceModeEl.innerHTML = modeText;
- }
-
- if (processPidEl) {
- processPidEl.textContent = data.pid || '--';
- }
-
- if (platformInfoEl) {
- // 格式化平台信息
- const platformMap = {
- 'win32': 'Windows',
- 'darwin': 'macOS',
- 'linux': 'Linux',
- 'freebsd': 'FreeBSD'
- };
- platformInfoEl.textContent = platformMap[data.platform] || data.platform || '--';
- }
-
- } catch (error) {
- console.error('Failed to load service mode info:', error);
- }
-}
-
-/**
- * 根据服务模式更新重启/重载按钮显示
- * @param {string} mode - 服务模式 ('worker' 或 'standalone')
- */
-function updateRestartButton(mode) {
- const restartBtn = document.getElementById('restartBtn');
- const restartBtnIcon = document.getElementById('restartBtnIcon');
- const restartBtnText = document.getElementById('restartBtnText');
-
- if (!restartBtn) return;
-
- if (mode === 'standalone') {
- // 独立模式:显示"重载"按钮
- if (restartBtnIcon) {
- restartBtnIcon.className = 'fas fa-sync-alt';
- }
- if (restartBtnText) {
- restartBtnText.textContent = t('header.reload');
- restartBtnText.setAttribute('data-i18n', 'header.reload');
- }
- restartBtn.setAttribute('aria-label', t('header.reload'));
- restartBtn.setAttribute('data-i18n-aria-label', 'header.reload');
- restartBtn.title = t('header.reload');
- } else {
- // 子进程模式:显示"重启"按钮
- if (restartBtnIcon) {
- restartBtnIcon.className = 'fas fa-redo';
- }
- if (restartBtnText) {
- restartBtnText.textContent = t('header.restart');
- restartBtnText.setAttribute('data-i18n', 'header.restart');
- }
- restartBtn.setAttribute('aria-label', t('header.restart'));
- restartBtn.setAttribute('data-i18n-aria-label', 'header.restart');
- restartBtn.title = t('header.restart');
- }
-}
-
-/**
- * 更新服务器时间和运行时间显示(本地计算)
- */
-function updateTimeDisplay() {
- if (!initialServerTime || initialUptime === null || !initialLoadTime) {
- return;
- }
-
- const serverTimeEl = document.getElementById('serverTime');
- const uptimeEl = document.getElementById('uptime');
-
- // 计算经过的秒数
- const elapsedSeconds = Math.floor((Date.now() - initialLoadTime) / 1000);
-
- // 更新服务器时间
- if (serverTimeEl) {
- const currentServerTime = new Date(initialServerTime.getTime() + elapsedSeconds * 1000);
- serverTimeEl.textContent = currentServerTime.toLocaleString(getCurrentLanguage());
- }
-
- // 更新运行时间
- if (uptimeEl) {
- const currentUptime = initialUptime + elapsedSeconds;
- uptimeEl.textContent = formatUptime(currentUptime);
- }
-}
-
-/**
- * 加载提供商列表
- */
-async function loadProviders() {
- try {
- const providers = await window.apiClient.get('/providers');
-
- // 动态更新其他模块的提供商信息,只需更新一次
- if (!isStaticProviderConfigsUpdated) {
- cachedSupportedProviders = await window.apiClient.get('/providers/supported');
- const providerConfigs = getProviderConfigs(cachedSupportedProviders);
-
- // 动态更新凭据文件管理的提供商类型筛选项
- updateProviderFilterOptions(providerConfigs);
-
- // 动态更新仪表盘页面的路径路由调用示例
- renderRoutingExamples(providerConfigs);
-
- // 动态更新仪表盘页面的可用模型列表提供商信息
- updateModelsProviderConfigs(providerConfigs);
-
- // 动态更新配置教程页面的提供商信息
- updateTutorialProviderConfigs(providerConfigs);
-
- // 动态更新用量查询页面的提供商信息
- updateUsageProviderConfigs(providerConfigs);
-
- // 动态更新配置管理页面的提供商选择标签
- updateConfigProviderConfigs(providerConfigs);
-
- isStaticProviderConfigsUpdated = true;
- }
-
- renderProviders(providers, cachedSupportedProviders);
- } catch (error) {
- console.error('Failed to load providers:', error);
- }
-}
-
-/**
- * 渲染提供商列表
- * @param {Object} providers - 提供商数据
- * @param {string[]} supportedProviders - 已注册的提供商类型列表
- */
-function renderProviders(providers, supportedProviders = []) {
- const container = document.getElementById('providersList');
- if (!container) return;
-
- container.innerHTML = '';
-
- // 检查是否有提供商池数据
- const hasProviders = Object.keys(providers).length > 0;
- const statsGrid = document.querySelector('#providers .stats-grid');
-
- // 始终显示统计卡片
- if (statsGrid) statsGrid.style.display = 'grid';
-
- const providerConfigs = getProviderConfigs(supportedProviders);
-
- // 提取显示的 ID 顺序
- const providerDisplayOrder = providerConfigs.filter(c => c.visible !== false).map(c => c.id);
-
- // 建立 ID 到配置的映射,方便获取显示名称
- const configMap = providerConfigs.reduce((map, config) => {
- map[config.id] = config;
- return map;
- }, {});
-
- // 获取所有提供商类型并按指定顺序排序
- // 优先显示预定义的所有提供商类型,即使某些提供商没有数据也要显示
- let allProviderTypes;
- if (hasProviders) {
- // 合并预定义类型和实际存在的类型,确保显示所有预定义提供商
- const actualProviderTypes = Object.keys(providers);
- // 只保留配置中标记为 visible 的,或者不在配置中的(默认显示)
- allProviderTypes = [...new Set([...providerDisplayOrder, ...actualProviderTypes])];
- } else {
- allProviderTypes = providerDisplayOrder;
- }
-
- // 过滤掉明确设置为不显示的提供商
- const sortedProviderTypes = providerDisplayOrder.filter(type => allProviderTypes.includes(type))
- .concat(allProviderTypes.filter(type => !providerDisplayOrder.some(t => t === type) && !configMap[type]?.visible === false));
-
- // 计算总统计
- let totalAccounts = 0;
- let totalHealthy = 0;
-
- // 按照排序后的提供商类型渲染
- sortedProviderTypes.forEach((providerType) => {
- // 如果配置中明确设置为不显示,则跳过
- if (configMap[providerType] && configMap[providerType].visible === false) {
- return;
- }
-
- const accounts = hasProviders ? providers[providerType] || [] : [];
- const providerDiv = document.createElement('div');
- providerDiv.className = 'provider-item';
- providerDiv.dataset.providerType = providerType;
- providerDiv.style.cursor = 'pointer';
-
- const healthyCount = accounts.filter(acc => acc.isHealthy && !acc.isDisabled).length;
- const totalCount = accounts.length;
- const usageCount = accounts.reduce((sum, acc) => sum + (acc.usageCount || 0), 0);
- const errorCount = accounts.reduce((sum, acc) => sum + (acc.errorCount || 0), 0);
-
- totalAccounts += totalCount;
- totalHealthy += healthyCount;
-
- // 更新全局统计变量
- if (!providerStats.providerTypeStats[providerType]) {
- providerStats.providerTypeStats[providerType] = {
- totalAccounts: 0,
- healthyAccounts: 0,
- totalUsage: 0,
- totalErrors: 0,
- lastUpdate: null
- };
- }
-
- const typeStats = providerStats.providerTypeStats[providerType];
- typeStats.totalAccounts = totalCount;
- typeStats.healthyAccounts = healthyCount;
- typeStats.totalUsage = usageCount;
- typeStats.totalErrors = errorCount;
- typeStats.lastUpdate = new Date().toISOString();
-
- // 为无数据状态设置特殊样式
- const isEmptyState = !hasProviders || totalCount === 0;
- const statusClass = isEmptyState ? 'status-empty' : (healthyCount === totalCount ? 'status-healthy' : 'status-unhealthy');
- const statusIcon = isEmptyState ? 'fa-info-circle' : (healthyCount === totalCount ? 'fa-check-circle' : 'fa-exclamation-triangle');
- const statusText = isEmptyState ? t('providers.status.empty') : t('providers.status.healthy', { healthy: healthyCount, total: totalCount });
-
- // 获取显示名称
- const displayName = configMap[providerType]?.name || providerType;
-
- providerDiv.innerHTML = `
-
-
-
- ${t('providers.stat.totalAccounts')}
- ${totalCount}
-
-
- ${t('providers.stat.healthyAccounts')}
- ${healthyCount}
-
-
- ${t('providers.stat.usageCount')}
- ${usageCount}
-
-
- ${t('providers.stat.errorCount')}
- ${errorCount}
-
-
- `;
-
- // 如果是空状态,添加特殊样式
- if (isEmptyState) {
- providerDiv.classList.add('empty-provider');
- }
-
- // 添加点击事件 - 整个提供商组都可以点击
- providerDiv.addEventListener('click', (e) => {
- e.preventDefault();
- openProviderManager(providerType);
- });
-
- container.appendChild(providerDiv);
-
- // 为授权按钮添加事件监听
- const authBtn = providerDiv.querySelector('.generate-auth-btn');
- if (authBtn) {
- authBtn.addEventListener('click', (e) => {
- e.stopPropagation(); // 阻止事件冒泡到父元素
- handleGenerateAuthUrl(providerType);
- });
- }
- });
-
- // 更新统计卡片数据
- const activeProviders = hasProviders ? Object.keys(providers).length : 0;
- updateProviderStatsDisplay(activeProviders, totalHealthy, totalAccounts);
-}
-
-/**
- * 更新提供商统计信息
- * @param {number} activeProviders - 活跃提供商数
- * @param {number} healthyProviders - 健康提供商数
- * @param {number} totalAccounts - 总账户数
- */
-function updateProviderStatsDisplay(activeProviders, healthyProviders, totalAccounts) {
- // 更新全局统计变量
- const newStats = {
- activeProviders,
- healthyProviders,
- totalAccounts,
- lastUpdateTime: new Date().toISOString()
- };
-
- updateProviderStats(newStats);
-
- // 计算总请求数和错误数
- let totalUsage = 0;
- let totalErrors = 0;
- Object.values(providerStats.providerTypeStats).forEach(typeStats => {
- totalUsage += typeStats.totalUsage || 0;
- totalErrors += typeStats.totalErrors || 0;
- });
-
- const finalStats = {
- ...newStats,
- totalRequests: totalUsage,
- totalErrors: totalErrors
- };
-
- updateProviderStats(finalStats);
-
- // 修改:根据使用次数统计"活跃提供商"和"活动连接"
- // "活跃提供商":统计有使用次数(usageCount > 0)的提供商类型数量
- let activeProvidersByUsage = 0;
- Object.entries(providerStats.providerTypeStats).forEach(([providerType, typeStats]) => {
- if (typeStats.totalUsage > 0) {
- activeProvidersByUsage++;
- }
- });
-
- // "活动连接":统计所有提供商账户的使用次数总和
- const activeConnections = totalUsage;
-
- // 更新页面显示
- const activeProvidersEl = document.getElementById('activeProviders');
- const healthyProvidersEl = document.getElementById('healthyProviders');
- const activeConnectionsEl = document.getElementById('activeConnections');
-
- if (activeProvidersEl) activeProvidersEl.textContent = activeProvidersByUsage;
- if (healthyProvidersEl) healthyProvidersEl.textContent = healthyProviders;
- if (activeConnectionsEl) activeConnectionsEl.textContent = activeConnections;
-
- // 打印调试信息到控制台
- console.log('Provider Stats Updated:', {
- activeProviders,
- activeProvidersByUsage,
- healthyProviders,
- totalAccounts,
- totalUsage,
- totalErrors,
- providerTypeStats: providerStats.providerTypeStats
- });
-}
-
-/**
- * 打开提供商管理模态框
- * @param {string} providerType - 提供商类型
- */
-async function openProviderManager(providerType) {
- try {
- const data = await window.apiClient.get(`/providers/${encodeURIComponent(providerType)}`);
-
- showProviderManagerModal(data);
- } catch (error) {
- console.error('Failed to load provider details:', error);
- showToast(t('common.error'), t('modal.provider.load.failed'), 'error');
- }
-}
-
-/**
- * 生成授权按钮HTML
- * @param {string} providerType - 提供商类型
- * @returns {string} 授权按钮HTML
- */
-function generateAuthButton(providerType) {
- // 只为支持OAuth的提供商显示授权按钮
- const oauthProviders = ['gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth', 'claude-kiro-oauth', 'openai-iflow', 'openai-codex-oauth'];
-
- if (!oauthProviders.includes(providerType)) {
- return '';
- }
-
- // Codex 提供商使用特殊图标
- if (providerType === 'openai-codex-oauth') {
- return `
-
- `;
- }
-
- return `
-
- `;
-}
-
-/**
- * 处理生成授权链接
- * @param {string} providerType - 提供商类型
- */
-async function handleGenerateAuthUrl(providerType) {
- // 如果是 Kiro OAuth,先显示认证方式选择对话框
- if (providerType === 'claude-kiro-oauth') {
- showKiroAuthMethodSelector(providerType);
- return;
- }
-
- // 如果是 Gemini OAuth 或 Antigravity,显示认证方式选择对话框
- if (providerType === 'gemini-cli-oauth' || providerType === 'gemini-antigravity') {
- showGeminiAuthMethodSelector(providerType);
- return;
- }
-
- // 如果是 Codex OAuth,显示认证方式选择对话框
- if (providerType === 'openai-codex-oauth') {
- showCodexAuthMethodSelector(providerType);
- return;
- }
-
- await executeGenerateAuthUrl(providerType, {});
-}
-
-/**
- * 显示 Codex OAuth 认证方式选择对话框
- * @param {string} providerType - 提供商类型
- */
-function showCodexAuthMethodSelector(providerType) {
- const modal = document.createElement('div');
- modal.className = 'modal-overlay';
- modal.style.display = 'flex';
-
- modal.innerHTML = `
-
-
-
-
-
-
-
-
-
-
- `;
-
- document.body.appendChild(modal);
-
- // 关闭按钮事件
- const closeBtn = modal.querySelector('.modal-close');
- const cancelBtn = modal.querySelector('.modal-cancel');
- [closeBtn, cancelBtn].forEach(btn => {
- btn.addEventListener('click', () => {
- modal.remove();
- });
- });
-
- // 认证方式选择按钮事件
- const methodBtns = modal.querySelectorAll('.auth-method-btn');
- methodBtns.forEach(btn => {
- btn.addEventListener('mouseenter', () => {
- btn.style.borderColor = '#4285f4';
- btn.style.background = '#f8faff';
- });
- btn.addEventListener('mouseleave', () => {
- btn.style.borderColor = '#e0e0e0';
- btn.style.background = 'white';
- });
- btn.addEventListener('click', async () => {
- const method = btn.dataset.method;
- modal.remove();
-
- if (method === 'batch-import') {
- showCodexBatchImportModal(providerType);
- } else {
- await executeGenerateAuthUrl(providerType, {});
- }
- });
- });
-}
-
-/**
- * 显示 Codex 批量导入模态框
- * @param {string} providerType - 提供商类型
- */
-function showCodexBatchImportModal(providerType) {
- const modal = document.createElement('div');
- modal.className = 'modal-overlay';
- modal.style.display = 'flex';
-
- modal.innerHTML = `
-
-
-
-
-
-
- ${t('oauth.codex.importInstructions')}
-
-
-
-
-
-
-
-
-
- ${t('oauth.codex.tokenCount')}
- 0
-
-
-
-
-
- ${t('oauth.codex.importing')}
-
-
-
-
-
-
-
- `;
-
- document.body.appendChild(modal);
-
- const textarea = modal.querySelector('#batchCodexTokens');
- const statsDiv = modal.querySelector('#codexBatchStats');
- const tokenCountValue = modal.querySelector('#codexTokenCountValue');
- const progressDiv = modal.querySelector('#codexBatchProgress');
- const progressBar = modal.querySelector('#codexImportProgressBar');
- const resultDiv = modal.querySelector('#codexBatchResult');
- const submitBtn = modal.querySelector('#codexBatchSubmit');
- const closeBtn = modal.querySelector('.modal-close');
- const cancelBtn = modal.querySelector('.modal-cancel');
-
- // 实时统计 token 数量
- textarea.addEventListener('input', () => {
- try {
- const val = textarea.value.trim();
- if (!val) {
- statsDiv.style.display = 'none';
- return;
- }
- const data = JSON.parse(val);
- const tokens = Array.isArray(data) ? data : [data];
- statsDiv.style.display = 'block';
- tokenCountValue.textContent = tokens.length;
- } catch (e) {
- statsDiv.style.display = 'none';
- }
- });
-
- // 关闭按钮事件
- [closeBtn, cancelBtn].forEach(btn => {
- btn.addEventListener('click', () => {
- modal.remove();
- });
- });
-
- // 提交按钮事件
- submitBtn.addEventListener('click', async () => {
- let tokens = [];
- try {
- const val = textarea.value.trim();
- const data = JSON.parse(val);
- tokens = Array.isArray(data) ? data : [data];
- } catch (e) {
- showToast(t('common.error'), t('oauth.codex.noTokens'), 'error');
- return;
- }
-
- if (tokens.length === 0) {
- showToast(t('common.warning'), t('oauth.codex.noTokens'), 'warning');
- return;
- }
-
- // 禁用输入和按钮
- textarea.disabled = true;
- submitBtn.disabled = true;
- cancelBtn.disabled = true;
- progressDiv.style.display = 'block';
- resultDiv.style.display = 'none';
- progressBar.style.width = '0%';
-
- // 创建实时结果显示区域
- resultDiv.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #f3f4f6; border: 1px solid #d1d5db;';
- resultDiv.innerHTML = `
-
-
- ${t('oauth.codex.importingProgress', { current: 0, total: tokens.length })}
-
-
- `;
-
- const progressText = resultDiv.querySelector('#codexBatchProgressText');
- const resultsList = resultDiv.querySelector('#codexBatchResultsList');
-
- let importSuccess = false; // 标记是否导入成功
-
- try {
- const response = await fetch('/api/codex/batch-import-tokens', {
- method: 'POST',
- headers: window.apiClient ? window.apiClient.getAuthHeaders() : {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ tokens })
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = '';
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split('\n');
- buffer = lines.pop() || '';
-
- let eventType = '';
- let eventData = '';
-
- for (const line of lines) {
- if (line.startsWith('event: ')) {
- eventType = line.substring(7).trim();
- } else if (line.startsWith('data: ')) {
- eventData = line.substring(6).trim();
-
- if (eventType && eventData) {
- try {
- const data = JSON.parse(eventData);
-
- if (eventType === 'progress') {
- const { index, total, current } = data;
- const percentage = Math.round((index / total) * 100);
- progressBar.style.width = `${percentage}%`;
- progressText.textContent = t('oauth.codex.importingProgress', { current: index, total: total });
-
- const resultItem = document.createElement('div');
- resultItem.style.cssText = 'padding: 4px 0; border-bottom: 1px solid rgba(0,0,0,0.1);';
- if (current.success) {
- resultItem.innerHTML = `Token ${current.index}: ✓ ${current.path}`;
- } else if (current.error === 'duplicate') {
- resultItem.innerHTML = `Token ${current.index}: ⚠ ${t('oauth.kiro.duplicateToken')}
- ${current.existingPath ? `(${current.existingPath})` : ''}`;
- } else {
- resultItem.innerHTML = `Token ${current.index}: ✗ ${current.error}`;
- }
- resultsList.appendChild(resultItem);
- resultsList.scrollTop = resultsList.scrollHeight;
- } else if (eventType === 'complete') {
- progressBar.style.width = '100%';
- progressDiv.style.display = 'none';
-
- const isAllSuccess = data.failedCount === 0;
- const isAllFailed = data.successCount === 0;
- let resultClass, resultIcon, resultMessage;
-
- if (isAllSuccess) {
- resultClass = 'background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;';
- resultIcon = 'fa-check-circle';
- resultMessage = t('oauth.codex.importSuccess', { count: data.successCount });
- } else if (isAllFailed) {
- resultClass = 'background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
- resultIcon = 'fa-times-circle';
- resultMessage = t('oauth.codex.importAllFailed', { count: data.failedCount });
- } else {
- resultClass = 'background: #fffbeb; border: 1px solid #fde68a; color: #92400e;';
- resultIcon = 'fa-exclamation-triangle';
- resultMessage = t('oauth.codex.importPartial', { success: data.successCount, failed: data.failedCount });
- }
-
- resultDiv.style.cssText = `display: block; margin-top: 16px; padding: 12px; border-radius: 8px; ${resultClass}`;
- const headerDiv = resultDiv.querySelector('div:first-child');
- headerDiv.innerHTML = ` ${resultMessage}`;
-
- if (data.successCount > 0) {
- importSuccess = true;
- loadProviders();
- loadConfigList();
- }
- } else if (eventType === 'error') {
- throw new Error(data.error);
- }
- } catch (parseError) {
- console.warn('Failed to parse SSE data:', parseError);
- }
- eventType = '';
- eventData = '';
- }
- }
- }
- }
- } catch (error) {
- console.error('[Codex Batch Import] Failed:', error);
- progressDiv.style.display = 'none';
- resultDiv.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
- resultDiv.innerHTML = `
-
-
- ${t('oauth.codex.importError')}: ${error.message}
-
- `;
- } finally {
- cancelBtn.disabled = false;
-
- if (!importSuccess) {
- textarea.disabled = false;
- submitBtn.disabled = false;
- submitBtn.innerHTML = ` ${t('oauth.codex.startImport')}`;
- } else {
- submitBtn.innerHTML = ` ${t('common.success')}`;
- }
- }
- });
-}
-
-/**
- * 显示 Kiro OAuth 认证方式选择对话框
- * @param {string} providerType - 提供商类型
- */
-function showKiroAuthMethodSelector(providerType) {
- const modal = document.createElement('div');
- modal.className = 'modal-overlay';
- modal.style.display = 'flex';
-
- modal.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- document.body.appendChild(modal);
-
- // 关闭按钮事件
- const closeBtn = modal.querySelector('.modal-close');
- const cancelBtn = modal.querySelector('.modal-cancel');
- [closeBtn, cancelBtn].forEach(btn => {
- btn.addEventListener('click', () => {
- modal.remove();
- });
- });
-
- // 认证方式选择按钮事件
- const methodBtns = modal.querySelectorAll('.auth-method-btn');
- methodBtns.forEach(btn => {
- btn.addEventListener('mouseenter', () => {
- btn.style.borderColor = '#00a67e';
- btn.style.background = '#f8fffe';
- });
- btn.addEventListener('mouseleave', () => {
- btn.style.borderColor = '#e0e0e0';
- btn.style.background = 'white';
- });
- btn.addEventListener('click', async () => {
- const method = btn.dataset.method;
- modal.remove();
-
- if (method === 'batch-import') {
- showKiroBatchImportModal();
- } else if (method === 'aws-import') {
- showKiroAwsImportModal();
- } else {
- await executeGenerateAuthUrl(providerType, { method });
- }
- });
- });
-}
-
-/**
- * 显示 Gemini OAuth 认证方式选择对话框
- * @param {string} providerType - 提供商类型
- */
-function showGeminiAuthMethodSelector(providerType) {
- const modal = document.createElement('div');
- modal.className = 'modal-overlay';
- modal.style.display = 'flex';
-
- modal.innerHTML = `
-
-
-
-
-
-
-
-
-
-
- `;
-
- document.body.appendChild(modal);
-
- // 关闭按钮事件
- const closeBtn = modal.querySelector('.modal-close');
- const cancelBtn = modal.querySelector('.modal-cancel');
- [closeBtn, cancelBtn].forEach(btn => {
- btn.addEventListener('click', () => {
- modal.remove();
- });
- });
-
- // 认证方式选择按钮事件
- const methodBtns = modal.querySelectorAll('.auth-method-btn');
- methodBtns.forEach(btn => {
- btn.addEventListener('mouseenter', () => {
- btn.style.borderColor = '#4285f4';
- btn.style.background = '#f8faff';
- });
- btn.addEventListener('mouseleave', () => {
- btn.style.borderColor = '#e0e0e0';
- btn.style.background = 'white';
- });
- btn.addEventListener('click', async () => {
- const method = btn.dataset.method;
- modal.remove();
-
- if (method === 'batch-import') {
- showGeminiBatchImportModal(providerType);
- } else {
- await executeGenerateAuthUrl(providerType, {});
- }
- });
- });
-}
-
-/**
- * 显示 Gemini 批量导入模态框
- * @param {string} providerType - 提供商类型
- */
-function showGeminiBatchImportModal(providerType) {
- const modal = document.createElement('div');
- modal.className = 'modal-overlay';
- modal.style.display = 'flex';
-
- modal.innerHTML = `
-
-
-
-
-
-
- ${t('oauth.gemini.importInstructions')}
-
-
-
-
-
-
-
-
-
- ${t('oauth.gemini.tokenCount')}
- 0
-
-
-
-
-
- ${t('oauth.gemini.importing')}
-
-
-
-
-
-
-
- `;
-
- document.body.appendChild(modal);
-
- const textarea = modal.querySelector('#batchGeminiTokens');
- const statsDiv = modal.querySelector('#geminiBatchStats');
- const tokenCountValue = modal.querySelector('#geminiTokenCountValue');
- const progressDiv = modal.querySelector('#geminiBatchProgress');
- const progressBar = modal.querySelector('#geminiImportProgressBar');
- const resultDiv = modal.querySelector('#geminiBatchResult');
- const submitBtn = modal.querySelector('#geminiBatchSubmit');
- const closeBtn = modal.querySelector('.modal-close');
- const cancelBtn = modal.querySelector('.modal-cancel');
-
- // 实时统计 token 数量
- textarea.addEventListener('input', () => {
- try {
- const val = textarea.value.trim();
- if (!val) {
- statsDiv.style.display = 'none';
- return;
- }
- const data = JSON.parse(val);
- const tokens = Array.isArray(data) ? data : [data];
- statsDiv.style.display = 'block';
- tokenCountValue.textContent = tokens.length;
- } catch (e) {
- statsDiv.style.display = 'none';
- }
- });
-
- // 关闭按钮事件
- [closeBtn, cancelBtn].forEach(btn => {
- btn.addEventListener('click', () => {
- modal.remove();
- });
- });
-
- // 提交按钮事件
- submitBtn.addEventListener('click', async () => {
- let tokens = [];
- try {
- const val = textarea.value.trim();
- const data = JSON.parse(val);
- tokens = Array.isArray(data) ? data : [data];
- } catch (e) {
- showToast(t('common.error'), t('oauth.gemini.noTokens'), 'error');
- return;
- }
-
- if (tokens.length === 0) {
- showToast(t('common.warning'), t('oauth.gemini.noTokens'), 'warning');
- return;
- }
-
- // 禁用输入和按钮
- textarea.disabled = true;
- submitBtn.disabled = true;
- cancelBtn.disabled = true;
- progressDiv.style.display = 'block';
- resultDiv.style.display = 'none';
- progressBar.style.width = '0%';
-
- // 创建实时结果显示区域
- resultDiv.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #f3f4f6; border: 1px solid #d1d5db;';
- resultDiv.innerHTML = `
-
-
- ${t('oauth.gemini.importingProgress', { current: 0, total: tokens.length })}
-
-
- `;
-
- const progressText = resultDiv.querySelector('#geminiBatchProgressText');
- const resultsList = resultDiv.querySelector('#geminiBatchResultsList');
-
- let importSuccess = false; // 标记是否导入成功
-
- try {
- const response = await fetch('/api/gemini/batch-import-tokens', {
- method: 'POST',
- headers: window.apiClient ? window.apiClient.getAuthHeaders() : {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ providerType, tokens })
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = '';
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split('\n');
- buffer = lines.pop() || '';
-
- let eventType = '';
- let eventData = '';
-
- for (const line of lines) {
- if (line.startsWith('event: ')) {
- eventType = line.substring(7).trim();
- } else if (line.startsWith('data: ')) {
- eventData = line.substring(6).trim();
-
- if (eventType && eventData) {
- try {
- const data = JSON.parse(eventData);
-
- if (eventType === 'progress') {
- const { index, total, current } = data;
- const percentage = Math.round((index / total) * 100);
- progressBar.style.width = `${percentage}%`;
- progressText.textContent = t('oauth.gemini.importingProgress', { current: index, total: total });
-
- const resultItem = document.createElement('div');
- resultItem.style.cssText = 'padding: 4px 0; border-bottom: 1px solid rgba(0,0,0,0.1);';
- if (current.success) {
- resultItem.innerHTML = `Token ${current.index}: ✓ ${current.path}`;
- } else if (current.error === 'duplicate') {
- resultItem.innerHTML = `Token ${current.index}: ⚠ ${t('oauth.kiro.duplicateToken')}
- ${current.existingPath ? `(${current.existingPath})` : ''}`;
- } else {
- resultItem.innerHTML = `Token ${current.index}: ✗ ${current.error}`;
- }
- resultsList.appendChild(resultItem);
- resultsList.scrollTop = resultsList.scrollHeight;
- } else if (eventType === 'complete') {
- progressBar.style.width = '100%';
- progressDiv.style.display = 'none';
-
- const isAllSuccess = data.failedCount === 0;
- const isAllFailed = data.successCount === 0;
- let resultClass, resultIcon, resultMessage;
-
- if (isAllSuccess) {
- resultClass = 'background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;';
- resultIcon = 'fa-check-circle';
- resultMessage = t('oauth.gemini.importSuccess', { count: data.successCount });
- } else if (isAllFailed) {
- resultClass = 'background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
- resultIcon = 'fa-times-circle';
- resultMessage = t('oauth.gemini.importAllFailed', { count: data.failedCount });
- } else {
- resultClass = 'background: #fffbeb; border: 1px solid #fde68a; color: #92400e;';
- resultIcon = 'fa-exclamation-triangle';
- resultMessage = t('oauth.gemini.importPartial', { success: data.successCount, failed: data.failedCount });
- }
-
- resultDiv.style.cssText = `display: block; margin-top: 16px; padding: 12px; border-radius: 8px; ${resultClass}`;
- const headerDiv = resultDiv.querySelector('div:first-child');
- headerDiv.innerHTML = ` ${resultMessage}`;
-
- if (data.successCount > 0) {
- importSuccess = true;
- loadProviders();
- loadConfigList();
- }
- } else if (eventType === 'error') {
- throw new Error(data.error);
- }
- } catch (parseError) {
- console.warn('Failed to parse SSE data:', parseError);
- }
- eventType = '';
- eventData = '';
- }
- }
- }
- }
- } catch (error) {
- console.error('[Gemini Batch Import] Failed:', error);
- progressDiv.style.display = 'none';
- resultDiv.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
- resultDiv.innerHTML = `
-
-
- ${t('oauth.gemini.importError')}: ${error.message}
-
- `;
- } finally {
- cancelBtn.disabled = false;
-
- if (!importSuccess) {
- textarea.disabled = false;
- submitBtn.disabled = false;
- submitBtn.innerHTML = ` ${t('oauth.gemini.startImport')}`;
- } else {
- submitBtn.innerHTML = ` ${t('common.success')}`;
- }
- }
- });
-}
-
-/**
- * 显示 Kiro 批量导入 refreshToken 模态框
- */
-function showKiroBatchImportModal() {
- const modal = document.createElement('div');
- modal.className = 'modal-overlay';
- modal.style.display = 'flex';
-
- modal.innerHTML = `
-
-
-
-
-
-
- ${t('oauth.kiro.batchImportInstructions')}
-
-
-
-
-
-
-
-
- ${t('oauth.kiro.tokenCount')}
- 0
-
-
-
-
-
- ${t('oauth.kiro.importing')}
-
-
-
-
-
-
-
- `;
-
- document.body.appendChild(modal);
-
- const textarea = modal.querySelector('#batchRefreshTokens');
- const statsDiv = modal.querySelector('#batchImportStats');
- const tokenCountValue = modal.querySelector('#tokenCountValue');
- const progressDiv = modal.querySelector('#batchImportProgress');
- const progressBar = modal.querySelector('#importProgressBar');
- const resultDiv = modal.querySelector('#batchImportResult');
- const submitBtn = modal.querySelector('#batchImportSubmit');
- const closeBtn = modal.querySelector('.modal-close');
- const cancelBtn = modal.querySelector('.modal-cancel');
-
- // 实时统计 token 数量
- textarea.addEventListener('input', () => {
- const tokens = textarea.value.split('\n').filter(line => line.trim());
- if (tokens.length > 0) {
- statsDiv.style.display = 'block';
- tokenCountValue.textContent = tokens.length;
- } else {
- statsDiv.style.display = 'none';
- }
- });
-
- // 关闭按钮事件
- [closeBtn, cancelBtn].forEach(btn => {
- btn.addEventListener('click', () => {
- modal.remove();
- });
- });
-
- // 提交按钮事件 - 使用 SSE 流式响应实时显示进度
- submitBtn.addEventListener('click', async () => {
- const tokens = textarea.value.split('\n').filter(line => line.trim());
-
- if (tokens.length === 0) {
- showToast(t('common.warning'), t('oauth.kiro.noTokens'), 'warning');
- return;
- }
-
- // 禁用输入和按钮
- textarea.disabled = true;
- submitBtn.disabled = true;
- cancelBtn.disabled = true;
- progressDiv.style.display = 'block';
- resultDiv.style.display = 'none';
- progressBar.style.width = '0%';
-
- // 创建实时结果显示区域
- resultDiv.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #f3f4f6; border: 1px solid #d1d5db;';
- resultDiv.innerHTML = `
-
-
- ${t('oauth.kiro.importingProgress', { current: 0, total: tokens.length })}
-
-
- `;
-
- const progressText = resultDiv.querySelector('#batchProgressText');
- const resultsList = resultDiv.querySelector('#batchResultsList');
-
- let successCount = 0;
- let failedCount = 0;
- const details = [];
- let importSuccess = false; // 标记是否导入成功
-
- try {
- // 使用 fetch + SSE 获取流式响应(需要带认证头)
- const response = await fetch('/api/kiro/batch-import-tokens', {
- method: 'POST',
- headers: window.apiClient ? window.apiClient.getAuthHeaders() : {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ refreshTokens: tokens })
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = '';
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- buffer += decoder.decode(value, { stream: true });
-
- // 解析 SSE 事件
- const lines = buffer.split('\n');
- buffer = lines.pop() || ''; // 保留最后一个可能不完整的行
-
- let eventType = '';
- let eventData = '';
-
- for (const line of lines) {
- if (line.startsWith('event: ')) {
- eventType = line.substring(7).trim();
- } else if (line.startsWith('data: ')) {
- eventData = line.substring(6).trim();
-
- if (eventType && eventData) {
- try {
- const data = JSON.parse(eventData);
-
- if (eventType === 'start') {
- // 开始事件
- console.log(`[Batch Import] Starting import of ${data.total} tokens`);
- } else if (eventType === 'progress') {
- // 进度更新
- const { index, total, current, successCount: sc, failedCount: fc } = data;
- successCount = sc;
- failedCount = fc;
- details.push(current);
-
- // 更新进度条
- const percentage = Math.round((index / total) * 100);
- progressBar.style.width = `${percentage}%`;
-
- // 更新进度文本
- progressText.textContent = t('oauth.kiro.importingProgress', { current: index, total: total });
-
- // 添加结果项
- const resultItem = document.createElement('div');
- resultItem.style.cssText = 'padding: 4px 0; border-bottom: 1px solid rgba(0,0,0,0.1);';
-
- if (current.success) {
- resultItem.innerHTML = `Token ${current.index}: ✓ ${current.path}`;
- } else if (current.error === 'duplicate') {
- resultItem.innerHTML = `Token ${current.index}: ⚠ ${t('oauth.kiro.duplicateToken')}
- ${current.existingPath ? `(${current.existingPath})` : ''}`;
- } else {
- resultItem.innerHTML = `Token ${current.index}: ✗ ${current.error}`;
- }
-
- resultsList.appendChild(resultItem);
- // 自动滚动到底部
- resultsList.scrollTop = resultsList.scrollHeight;
-
- } else if (eventType === 'complete') {
- // 完成事件
- progressBar.style.width = '100%';
- progressDiv.style.display = 'none';
-
- const isAllSuccess = data.failedCount === 0;
- const isAllFailed = data.successCount === 0;
-
- let resultClass, resultIcon, resultMessage;
- if (isAllSuccess) {
- resultClass = 'background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;';
- resultIcon = 'fa-check-circle';
- resultMessage = t('oauth.kiro.importSuccess', { count: data.successCount });
- } else if (isAllFailed) {
- resultClass = 'background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
- resultIcon = 'fa-times-circle';
- resultMessage = t('oauth.kiro.importAllFailed', { count: data.failedCount });
- } else {
- resultClass = 'background: #fffbeb; border: 1px solid #fde68a; color: #92400e;';
- resultIcon = 'fa-exclamation-triangle';
- resultMessage = t('oauth.kiro.importPartial', { success: data.successCount, failed: data.failedCount });
- }
-
- // 更新结果区域样式
- resultDiv.style.cssText = `display: block; margin-top: 16px; padding: 12px; border-radius: 8px; ${resultClass}`;
-
- // 更新标题
- const headerDiv = resultDiv.querySelector('div:first-child');
- headerDiv.innerHTML = ` ${resultMessage}`;
-
- // 如果有成功的,刷新提供商列表
- if (data.successCount > 0) {
- importSuccess = true;
- loadProviders();
- loadConfigList();
- }
-
- } else if (eventType === 'error') {
- throw new Error(data.error);
- }
- } catch (parseError) {
- console.warn('Failed to parse SSE data:', parseError);
- }
-
- eventType = '';
- eventData = '';
- }
- }
- }
- }
-
- } catch (error) {
- console.error('[Kiro Batch Import] Failed:', error);
- progressDiv.style.display = 'none';
- resultDiv.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
- resultDiv.innerHTML = `
-
-
- ${t('oauth.kiro.importError')}: ${error.message}
-
- `;
- } finally {
- // 重新启用按钮
- cancelBtn.disabled = false;
- if (!importSuccess) {
- textarea.disabled = false;
- submitBtn.disabled = false;
- submitBtn.innerHTML = ` ${t('oauth.kiro.startImport')}`;
- } else {
- submitBtn.innerHTML = ` ${t('common.success')}`;
- }
- }
- });
-}
-
-/**
- * 显示 Kiro AWS 账号导入模态框
- * 支持从 AWS SSO cache 目录导入凭据文件,或直接粘贴 JSON
- */
-function showKiroAwsImportModal() {
- const modal = document.createElement('div');
- modal.className = 'modal-overlay';
- modal.style.display = 'flex';
-
- modal.innerHTML = `
-
-
-
-
-
-
- ${t('oauth.kiro.awsImportInstructions')}
-
-
-
- C:\\Users\\{username}\\.aws\\sso\\cache
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${t('oauth.kiro.awsJsonExample')}
-
-
-
-
// 单个凭据导入示例:
-
{
- "clientId": "VYZBSTx3Q7QEq1W3Wn8c5nVzLWVhc3QtMQ",
- "clientSecret": "eyJraWQi...OAMc",
- "expiresAt": "2026-01-09T04:43:18.079944400+00:00",
- "accessToken": "aoaAAAAAGlgghoSqRgQK...2tfhmdNZDA",
- "authMethod": "IdC",
- "provider": "BuilderId",
- "refreshToken": "aorAAAAAGn...uKw+E3",
- "region": "us-east-1"
-}
-
-
-
// 批量导入示例(JSON数组):
-
[
- {
- "clientId": "VYZBSTx3Q7QEq1W3Wn8c5nVzLWVhc3QtMQ",
- "clientSecret": "eyJraWQi...OAMc",
- "accessToken": "aoaAAAAAGlgghoSqRgQK...2tfhmdNZDA",
- "refreshToken": "aorAAAAAGn...uKw+E3",
- "region": "us-east-1"
- },
- {
- "clientId": "AnotherClientId123",
- "clientSecret": "eyJraWQi...xyz",
- "accessToken": "aoaAAAAAGlgghoSqRgQK...abc",
- "refreshToken": "aorAAAAAGn...def",
- "region": "us-west-2",
- "idcRegion": "us-west-2"
- }
-]
-
-
-
- 注意:AWS企业用户需要额外添加 idcRegion 字段
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- document.body.appendChild(modal);
-
- const fileInput = modal.querySelector('#awsFilesInput');
- const uploadArea = modal.querySelector('.aws-file-upload-area');
- const filesListDiv = modal.querySelector('#awsFilesList');
- const filesContainer = modal.querySelector('#awsFilesContainer');
- const clearFilesBtn = modal.querySelector('#clearFilesBtn');
- const validationResult = modal.querySelector('#awsValidationResult');
- const jsonPreview = modal.querySelector('#awsJsonPreview');
- const jsonContent = modal.querySelector('#awsJsonContent');
- const submitBtn = modal.querySelector('#awsImportSubmit');
- const closeBtn = modal.querySelector('.modal-close');
- const cancelBtn = modal.querySelector('.modal-cancel');
- const modeBtns = modal.querySelectorAll('.mode-btn');
- const fileModeSection = modal.querySelector('#fileModeSection');
- const jsonModeSection = modal.querySelector('#jsonModeSection');
- const jsonInputTextarea = modal.querySelector('#awsJsonInput');
-
- let uploadedFiles = [];
- let mergedCredentials = null;
- let currentMode = 'file';
-
- // 清空文件按钮事件
- clearFilesBtn.addEventListener('click', () => {
- uploadedFiles = [];
- filesContainer.innerHTML = '';
- filesListDiv.style.display = 'none';
- validationResult.style.display = 'none';
- jsonPreview.style.display = 'none';
- submitBtn.disabled = true;
- mergedCredentials = null;
- // 清空 file input
- fileInput.value = '';
- });
-
- // 清空按钮 hover 效果
- clearFilesBtn.addEventListener('mouseenter', () => {
- clearFilesBtn.style.background = '#fef2f2';
- });
- clearFilesBtn.addEventListener('mouseleave', () => {
- clearFilesBtn.style.background = 'none';
- });
-
- // 模式切换
- modeBtns.forEach(btn => {
- btn.addEventListener('click', () => {
- const mode = btn.dataset.mode;
- if (mode === currentMode) return;
-
- currentMode = mode;
-
- // 更新按钮样式
- modeBtns.forEach(b => {
- if (b.dataset.mode === mode) {
- b.style.borderColor = '#ff9900';
- b.style.background = '#fff7ed';
- b.style.color = '#9a3412';
- b.classList.add('active');
- } else {
- b.style.borderColor = '#d1d5db';
- b.style.background = 'white';
- b.style.color = '#6b7280';
- b.classList.remove('active');
- }
- });
-
- // 切换显示区域
- if (mode === 'file') {
- fileModeSection.style.display = 'block';
- jsonModeSection.style.display = 'none';
- // 重新验证文件模式的内容
- validateAndPreview();
- } else {
- fileModeSection.style.display = 'none';
- jsonModeSection.style.display = 'block';
- // 验证 JSON 输入
- validateJsonInput();
- }
- });
- });
-
- // JSON 输入实时验证
- jsonInputTextarea.addEventListener('input', () => {
- validateJsonInput();
- });
-
- // 验证 JSON 输入
- function validateJsonInput() {
- const inputValue = jsonInputTextarea.value.trim();
-
- if (!inputValue) {
- validationResult.style.display = 'none';
- jsonPreview.style.display = 'none';
- submitBtn.disabled = true;
- mergedCredentials = null;
- return;
- }
-
- try {
- mergedCredentials = JSON.parse(inputValue);
- validateAndShowResult();
- } catch (error) {
- validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
- validationResult.innerHTML = `
-
-
- ${t('oauth.kiro.awsJsonParseError')}
-
- ${error.message}
- `;
- jsonPreview.style.display = 'none';
- submitBtn.disabled = true;
- mergedCredentials = null;
- }
- }
-
- // 文件上传区域交互
- uploadArea.addEventListener('click', () => fileInput.click());
-
- uploadArea.addEventListener('dragover', (e) => {
- e.preventDefault();
- uploadArea.style.borderColor = '#ff9900';
- uploadArea.style.background = '#fffbeb';
- });
-
- uploadArea.addEventListener('dragleave', (e) => {
- e.preventDefault();
- uploadArea.style.borderColor = '#d1d5db';
- uploadArea.style.background = 'transparent';
- });
-
- uploadArea.addEventListener('drop', (e) => {
- e.preventDefault();
- uploadArea.style.borderColor = '#d1d5db';
- uploadArea.style.background = 'transparent';
-
- const files = Array.from(e.dataTransfer.files).filter(f => f.name.endsWith('.json'));
- if (files.length > 0) {
- processFiles(files);
- }
- });
-
- fileInput.addEventListener('change', () => {
- const files = Array.from(fileInput.files);
- if (files.length > 0) {
- processFiles(files);
- }
- });
-
- // 处理上传的文件(支持追加)
- async function processFiles(files) {
- for (const file of files) {
- // 检查是否已存在同名文件
- const existingIndex = uploadedFiles.findIndex(f => f.name === file.name);
-
- try {
- const content = await readFileAsText(file);
- const json = JSON.parse(content);
-
- if (existingIndex >= 0) {
- // 替换已存在的同名文件
- uploadedFiles[existingIndex] = {
- name: file.name,
- content: json
- };
- showToast(t('common.info'), t('oauth.kiro.awsFileReplaced', { filename: file.name }), 'info');
- } else {
- // 追加新文件
- uploadedFiles.push({
- name: file.name,
- content: json
- });
- }
- } catch (error) {
- console.error(`Failed to parse ${file.name}:`, error);
- showToast(t('common.error'), t('oauth.kiro.awsParseError', { filename: file.name }), 'error');
- }
- }
-
- // 重新渲染文件列表
- renderFilesList();
-
- filesListDiv.style.display = uploadedFiles.length > 0 ? 'block' : 'none';
-
- // 清空 file input 以便可以再次选择相同文件
- fileInput.value = '';
-
- validateAndPreview();
- }
-
- // 渲染文件列表
- function renderFilesList() {
- filesContainer.innerHTML = '';
-
- for (const file of uploadedFiles) {
- const fileDiv = document.createElement('div');
- fileDiv.style.cssText = 'display: flex; justify-content: space-between; align-items: center; padding: 8px; background: white; border-radius: 4px; margin-bottom: 4px;';
- fileDiv.dataset.filename = file.name;
-
- const fields = Object.keys(file.content).slice(0, 5).join(', ');
- const moreFields = Object.keys(file.content).length > 5 ? '...' : '';
-
- fileDiv.innerHTML = `
-
-
-
${file.name}
-
${fields}${moreFields}
-
-
- `;
- filesContainer.appendChild(fileDiv);
- }
-
- // 添加删除文件按钮事件
- filesContainer.querySelectorAll('.remove-file-btn').forEach(btn => {
- btn.addEventListener('click', (e) => {
- const filename = e.currentTarget.dataset.filename;
- uploadedFiles = uploadedFiles.filter(f => f.name !== filename);
- renderFilesList();
- filesListDiv.style.display = uploadedFiles.length > 0 ? 'block' : 'none';
- validateAndPreview();
- });
- });
- }
-
- // 验证并预览(文件模式)
- function validateAndPreview() {
- if (currentMode !== 'file') return;
-
- if (uploadedFiles.length === 0) {
- validationResult.style.display = 'none';
- jsonPreview.style.display = 'none';
- submitBtn.disabled = true;
- mergedCredentials = null;
- return;
- }
-
- // 智能合并所有文件的内容
- // 如果多个文件都有 expiresAt,使用包含 refreshToken 的文件中的 expiresAt
- mergedCredentials = {};
- let expiresAtFromRefreshTokenFile = null;
-
- for (const file of uploadedFiles) {
- // 如果这个文件包含 refreshToken,记录它的 expiresAt
- if (file.content.refreshToken && file.content.expiresAt) {
- expiresAtFromRefreshTokenFile = file.content.expiresAt;
- }
- Object.assign(mergedCredentials, file.content);
- }
-
- // 如果找到了包含 refreshToken 的文件的 expiresAt,使用它
- if (expiresAtFromRefreshTokenFile) {
- mergedCredentials.expiresAt = expiresAtFromRefreshTokenFile;
- }
-
- validateAndShowResult();
- }
-
- // 验证并显示结果(通用)
- function validateAndShowResult() {
- if (!mergedCredentials) {
- validationResult.style.display = 'none';
- jsonPreview.style.display = 'none';
- submitBtn.disabled = true;
- return;
- }
-
- // 检查是否为批量导入(数组)
- const isBatchImport = Array.isArray(mergedCredentials);
-
- if (isBatchImport) {
- // 批量导入模式:验证数组中的每个对象
- let allValid = true;
- const credentialsValidation = mergedCredentials.map((cred, index) => {
- const hasClientId = !!cred.clientId;
- const hasClientSecret = !!cred.clientSecret;
- const hasAccessToken = !!cred.accessToken;
- const hasRefreshToken = !!cred.refreshToken;
- const isValid = hasClientId && hasClientSecret && hasAccessToken && hasRefreshToken;
-
- if (!isValid) allValid = false;
-
- return {
- index: index + 1,
- isValid,
- fields: [
- { key: 'clientId', has: hasClientId },
- { key: 'clientSecret', has: hasClientSecret },
- { key: 'accessToken', has: hasAccessToken },
- { key: 'refreshToken', has: hasRefreshToken }
- ]
- };
- });
-
- // 构建批量验证结果HTML
- const credentialsHtml = credentialsValidation.map(cv => {
- const statusIcon = cv.isValid ? '✓' : '✗';
- const statusColor = cv.isValid ? '#166534' : '#991b1b';
- const fieldsHtml = cv.fields.map(f => `
- ${f.key}: ${f.has
- ? `✓`
- : `✗`
- }
- `).join('');
-
- return `
-
-
- ${statusIcon} 凭据 ${cv.index}
-
-
- ${fieldsHtml}
-
-
- `;
- }).join('');
-
- if (allValid) {
- validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;';
- validationResult.innerHTML = `
-
-
- 批量验证通过 (${mergedCredentials.length} 个凭据)
-
-
- ${credentialsHtml}
-
- `;
- submitBtn.disabled = false;
- } else {
- const validCount = credentialsValidation.filter(cv => cv.isValid).length;
- const invalidCount = credentialsValidation.length - validCount;
- validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
- validationResult.innerHTML = `
-
-
- 批量验证失败
- (${invalidCount} 个凭据缺少必需字段)
-
-
- ${credentialsHtml}
-
-
-
- 请确保每个凭据都包含所有必需字段:clientId, clientSecret, accessToken, refreshToken
-
- `;
- submitBtn.disabled = true;
- }
-
- // 显示 JSON 预览(批量模式)
- jsonPreview.style.display = 'block';
- const previewData = mergedCredentials.map(cred => {
- const preview = { ...cred };
- if (preview.clientSecret) {
- preview.clientSecret = preview.clientSecret.substring(0, 8) + '...' + preview.clientSecret.slice(-4);
- }
- if (preview.accessToken) {
- preview.accessToken = preview.accessToken.substring(0, 20) + '...' + preview.accessToken.slice(-10);
- }
- if (preview.refreshToken) {
- preview.refreshToken = preview.refreshToken.substring(0, 10) + '...' + preview.refreshToken.slice(-6);
- }
- return preview;
- });
- jsonContent.textContent = JSON.stringify(previewData, null, 2);
-
- } else {
- // 单个导入模式:原有逻辑
- const hasClientId = !!mergedCredentials.clientId;
- const hasClientSecret = !!mergedCredentials.clientSecret;
- const hasAccessToken = !!mergedCredentials.accessToken;
- const hasRefreshToken = !!mergedCredentials.refreshToken;
-
- // 所有四个字段都必须存在
- const isValid = hasClientId && hasClientSecret && hasAccessToken && hasRefreshToken;
-
- // 构建字段状态列表
- const fieldsList = [
- { key: 'clientId', has: hasClientId },
- { key: 'clientSecret', has: hasClientSecret },
- { key: 'accessToken', has: hasAccessToken },
- { key: 'refreshToken', has: hasRefreshToken }
- ];
-
- const fieldsHtml = fieldsList.map(f => `
- ${f.key}: ${f.has
- ? `✓ ${t('common.found')}`
- : `✗ ${t('common.missing')}`
- }
- `).join('');
-
- if (isValid) {
- validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;';
- validationResult.innerHTML = `
-
-
- ${t('oauth.kiro.awsValidationSuccess')}
-
-
- `;
- submitBtn.disabled = false;
- } else {
- const missingCount = fieldsList.filter(f => !f.has).length;
- validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
- validationResult.innerHTML = `
-
-
- ${t('oauth.kiro.awsValidationFailed')}
- (${t('oauth.kiro.awsMissingFields', { count: missingCount })})
-
-
-
-
- ${t('oauth.kiro.awsUploadMore')}
-
- `;
- submitBtn.disabled = true;
- }
-
- // 显示 JSON 预览(单个模式)
- jsonPreview.style.display = 'block';
-
- // 隐藏敏感信息的部分内容
- const previewData = { ...mergedCredentials };
- if (previewData.clientSecret) {
- previewData.clientSecret = previewData.clientSecret.substring(0, 8) + '...' + previewData.clientSecret.slice(-4);
- }
- if (previewData.accessToken) {
- previewData.accessToken = previewData.accessToken.substring(0, 20) + '...' + previewData.accessToken.slice(-10);
- }
- if (previewData.refreshToken) {
- previewData.refreshToken = previewData.refreshToken.substring(0, 10) + '...' + previewData.refreshToken.slice(-6);
- }
-
- jsonContent.textContent = JSON.stringify(previewData, null, 2);
- }
- }
-
- // 读取文件内容
- function readFileAsText(file) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = (e) => resolve(e.target.result);
- reader.onerror = (e) => reject(e);
- reader.readAsText(file);
- });
- }
-
- // 关闭按钮事件
- [closeBtn, cancelBtn].forEach(btn => {
- btn.addEventListener('click', () => {
- modal.remove();
- });
- });
-
- // 提交按钮事件
- submitBtn.addEventListener('click', async () => {
- if (!mergedCredentials) {
- showToast(t('common.warning'), t('oauth.kiro.awsNoCredentials'), 'warning');
- return;
- }
-
- // 检查是否为批量导入(数组)
- const isBatchImport = Array.isArray(mergedCredentials);
-
- // 禁用按钮和输入
- submitBtn.disabled = true;
- cancelBtn.disabled = true;
- submitBtn.innerHTML = ` ${t('oauth.kiro.awsImporting')}`;
-
- if (currentMode === 'json') {
- jsonInputTextarea.disabled = true;
- }
-
- let importSuccess = false; // 标记是否导入成功
-
- try {
- if (isBatchImport) {
- // 批量导入模式 - 使用 SSE 流式响应
- // 确保每个凭据都有 authMethod
- const credentialsToImport = mergedCredentials.map(cred => ({
- ...cred,
- authMethod: cred.authMethod || 'builder-id'
- }));
-
- // 创建进度显示区域
- validationResult.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #f3f4f6; border: 1px solid #d1d5db;';
- validationResult.innerHTML = `
-
-
- ${t('oauth.kiro.importingProgress', { current: 0, total: credentialsToImport.length })}
-
-
-
- `;
-
- const progressText = validationResult.querySelector('#awsBatchProgressText');
- const progressBar = validationResult.querySelector('#awsImportProgressBar');
- const resultsList = validationResult.querySelector('#awsBatchResultsList');
-
- // 使用 fetch + SSE 获取流式响应
- const response = await fetch('/api/kiro/import-aws-credentials', {
- method: 'POST',
- headers: window.apiClient ? window.apiClient.getAuthHeaders() : {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ credentials: credentialsToImport })
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = '';
-
- let successCount = 0;
- let failedCount = 0;
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- buffer += decoder.decode(value, { stream: true });
-
- // 解析 SSE 事件
- const lines = buffer.split('\n');
- buffer = lines.pop() || '';
-
- let eventType = '';
- let eventData = '';
-
- for (const line of lines) {
- if (line.startsWith('event: ')) {
- eventType = line.substring(7).trim();
- } else if (line.startsWith('data: ')) {
- eventData = line.substring(6).trim();
-
- if (eventType && eventData) {
- try {
- const data = JSON.parse(eventData);
-
- if (eventType === 'start') {
- console.log(`[AWS Batch Import] Starting import of ${data.total} credentials`);
- } else if (eventType === 'progress') {
- const { index, total, current, successCount: sc, failedCount: fc } = data;
- successCount = sc;
- failedCount = fc;
-
- // 更新进度条
- const percentage = Math.round((index / total) * 100);
- progressBar.style.width = `${percentage}%`;
-
- // 更新进度文本
- progressText.textContent = t('oauth.kiro.importingProgress', { current: index, total: total });
-
- // 添加结果项
- const resultItem = document.createElement('div');
- resultItem.style.cssText = 'padding: 4px 0; border-bottom: 1px solid rgba(0,0,0,0.1);';
-
- if (current.success) {
- resultItem.innerHTML = `凭据 ${current.index}: ✓ ${current.path}`;
- } else if (current.error === 'duplicate') {
- resultItem.innerHTML = `凭据 ${current.index}: ⚠ ${t('oauth.kiro.duplicateCredentials')}
- ${current.existingPath ? `(${current.existingPath})` : ''}`;
- } else {
- resultItem.innerHTML = `凭据 ${current.index}: ✗ ${current.error}`;
- }
-
- resultsList.appendChild(resultItem);
- resultsList.scrollTop = resultsList.scrollHeight;
-
- } else if (eventType === 'complete') {
- progressBar.style.width = '100%';
-
- const isAllSuccess = data.failedCount === 0;
- const isAllFailed = data.successCount === 0;
-
- let resultClass, resultIcon, resultMessage;
- if (isAllSuccess) {
- resultClass = 'background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;';
- resultIcon = 'fa-check-circle';
- resultMessage = t('oauth.kiro.awsImportSuccess') + ` (${data.successCount})`;
- } else if (isAllFailed) {
- resultClass = 'background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
- resultIcon = 'fa-times-circle';
- resultMessage = t('oauth.kiro.awsImportAllFailed', { count: data.failedCount });
- } else {
- resultClass = 'background: #fffbeb; border: 1px solid #fde68a; color: #92400e;';
- resultIcon = 'fa-exclamation-triangle';
- resultMessage = t('oauth.kiro.importPartial', { success: data.successCount, failed: data.failedCount });
- }
-
- validationResult.style.cssText = `display: block; margin-top: 16px; padding: 12px; border-radius: 8px; ${resultClass}`;
-
- const headerDiv = validationResult.querySelector('div:first-child');
- headerDiv.innerHTML = ` ${resultMessage}`;
-
- // 如果有成功的,标记为成功并刷新提供商列表
- if (data.successCount > 0) {
- importSuccess = true;
- loadProviders();
- loadConfigList();
- }
-
- } else if (eventType === 'error') {
- throw new Error(data.error);
- }
- } catch (parseError) {
- console.warn('Failed to parse SSE data:', parseError);
- }
-
- eventType = '';
- eventData = '';
- }
- }
- }
- }
-
- } else {
- // 单个导入模式
- // 确保 authMethod 为 builder-id(AWS 账号模式)
- if (!mergedCredentials.authMethod) {
- mergedCredentials.authMethod = 'builder-id';
- }
-
- const response = await window.apiClient.post('/kiro/import-aws-credentials', {
- credentials: mergedCredentials
- });
-
- if (response.success) {
- importSuccess = true;
- showToast(t('common.success'), t('oauth.kiro.awsImportSuccess'), 'success');
- modal.remove();
-
- // 刷新提供商列表和配置列表
- loadProviders();
- loadConfigList();
- } else if (response.error === 'duplicate') {
- // 显示重复凭据警告
- const existingPath = response.existingPath || '';
- showToast(t('common.warning'), t('oauth.kiro.duplicateCredentials') + (existingPath ? ` (${existingPath})` : ''), 'warning');
- } else {
- showToast(t('common.error'), response.error || t('oauth.kiro.awsImportFailed'), 'error');
- }
- }
- } catch (error) {
- console.error('AWS import failed:', error);
-
- // 更新错误显示
- validationResult.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
- validationResult.innerHTML = `
-
-
- ${t('oauth.kiro.awsImportFailed')}: ${error.message}
-
- `;
-
- showToast(t('common.error'), t('oauth.kiro.awsImportFailed') + ': ' + error.message, 'error');
- } finally {
- // 取消按钮始终可用
- cancelBtn.disabled = false;
-
- // 只有在导入失败时才重新启用提交按钮
- if (!importSuccess) {
- submitBtn.disabled = false;
- submitBtn.innerHTML = ` ${t('oauth.kiro.awsConfirmImport')}`;
-
- if (currentMode === 'json') {
- jsonInputTextarea.disabled = false;
- }
- } else {
- // 导入成功后,保持提交按钮禁用状态,并显示成功图标
- submitBtn.innerHTML = ` ${t('common.success')}`;
- }
- }
- });
-}
-
-/**
- * 执行生成授权链接
- * @param {string} providerType - 提供商类型
- * @param {Object} extraOptions - 额外选项
- */
-async function executeGenerateAuthUrl(providerType, extraOptions = {}) {
- try {
- showToast(t('common.info'), t('modal.provider.auth.initializing'), 'info');
-
- // 使用 fileUploadHandler 中的 getProviderKey 获取目录名称
- const providerDir = fileUploadHandler.getProviderKey(providerType);
-
- const response = await window.apiClient.post(
- `/providers/${encodeURIComponent(providerType)}/generate-auth-url`,
- {
- saveToConfigs: true,
- providerDir: providerDir,
- ...extraOptions
- }
- );
-
- if (response.success && response.authUrl) {
- // 如果提供了 targetInputId,设置成功监听器
- if (extraOptions.targetInputId) {
- const targetInputId = extraOptions.targetInputId;
- const handleSuccess = (e) => {
- const data = e.detail;
- if (data.provider === providerType && data.relativePath) {
- const input = document.getElementById(targetInputId);
- if (input) {
- input.value = data.relativePath;
- input.dispatchEvent(new Event('input', { bubbles: true }));
- showToast(t('common.success'), t('modal.provider.auth.success'), 'success');
- }
- window.removeEventListener('oauth_success_event', handleSuccess);
- }
- };
- window.addEventListener('oauth_success_event', handleSuccess);
- }
-
- // 显示授权信息模态框
- showAuthModal(response.authUrl, response.authInfo);
- } else {
- showToast(t('common.error'), t('modal.provider.auth.failed'), 'error');
- }
- } catch (error) {
- console.error('生成授权链接失败:', error);
- showToast(t('common.error'), t('modal.provider.auth.failed') + `: ${error.message}`, 'error');
- }
-}
-
-/**
- * 获取提供商的授权文件路径
- * @param {string} provider - 提供商类型
- * @returns {string} 授权文件路径
- */
-function getAuthFilePath(provider) {
- const authFilePaths = {
- 'gemini-cli-oauth': '~/.gemini/oauth_creds.json',
- 'gemini-antigravity': '~/.antigravity/oauth_creds.json',
- 'openai-qwen-oauth': '~/.qwen/oauth_creds.json',
- 'claude-kiro-oauth': '~/.aws/sso/cache/kiro-auth-token.json',
- 'openai-iflow': '~/.iflow/oauth_creds.json'
- };
- return authFilePaths[provider] || (getCurrentLanguage() === 'en-US' ? 'Unknown Path' : '未知路径');
-}
-
-/**
- * 显示授权信息模态框
- * @param {string} authUrl - 授权URL
- * @param {Object} authInfo - 授权信息
- */
-function showAuthModal(authUrl, authInfo) {
- const modal = document.createElement('div');
- modal.className = 'modal-overlay';
- modal.style.display = 'flex';
-
- // 获取授权文件路径
- const authFilePath = getAuthFilePath(authInfo.provider);
-
- // 获取需要开放的端口号(从 authInfo 或当前页面 URL)
- const requiredPort = authInfo.callbackPort || authInfo.port || window.location.port || '3000';
- const isDeviceFlow = authInfo.provider === 'openai-qwen-oauth' || (authInfo.provider === 'claude-kiro-oauth' && authInfo.authMethod === 'builder-id');
-
- let instructionsHtml = '';
- if (authInfo.provider === 'openai-qwen-oauth') {
- instructionsHtml = `
-
-
${t('oauth.modal.steps')}
-
- - ${t('oauth.modal.step1')}
- - ${t('oauth.modal.step2.qwen')}
- - ${t('oauth.modal.step3')}
- - ${t('oauth.modal.step4.qwen', { min: Math.floor(authInfo.expiresIn / 60) })}
-
-
- `;
- } else if (authInfo.provider === 'claude-kiro-oauth') {
- const methodDisplay = authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : `Social (${authInfo.socialProvider || 'Google'})`;
- const methodAccount = authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : authInfo.socialProvider || 'Google';
- instructionsHtml = `
-
-
${t('oauth.modal.steps')}
-
${t('oauth.kiro.authMethodLabel')} ${methodDisplay}
-
- - ${t('oauth.kiro.step1')}
- - ${t('oauth.kiro.step2', { method: methodAccount })}
- - ${t('oauth.kiro.step3')}
- - ${t('oauth.kiro.step4')}
-
-
- `;
- } else if (authInfo.provider === 'openai-iflow') {
- instructionsHtml = `
-
-
${t('oauth.modal.steps')}
-
- - ${t('oauth.iflow.step1')}
- - ${t('oauth.iflow.step2')}
- - ${t('oauth.iflow.step3')}
- - ${t('oauth.iflow.step4')}
-
-
- `;
- } else {
- instructionsHtml = `
-
-
${t('oauth.modal.steps')}
-
- - ${t('oauth.modal.step1')}
- - ${t('oauth.modal.step2.google')}
- - ${t('oauth.modal.step4.google')}
- - ${t('oauth.modal.step3')}
-
-
- `;
- }
-
- modal.innerHTML = `
-
-
-
-
-
${t('oauth.modal.provider')} ${authInfo.provider}
-
- ${(authInfo.provider === 'claude-kiro-oauth' && authInfo.authMethod === 'builder-id') ? `
-
- ` : ''}
-
-
-
${t('oauth.modal.requiredPort')}
- ${isDeviceFlow ?
- `
${requiredPort}` :
- `
-
-
-
`
- }
-
-
${t('oauth.modal.portNote')}
- ${(authInfo.provider === 'claude-kiro-oauth' && authInfo.authMethod === 'builder-id') ? `
-
-
-
-
-
-
-
- ${t('oauth.kiro.builderIDStartURLHint') || '如果您使用 AWS IAM Identity Center,请输入您的 Start URL'}
-
-
-
-
-
-
-
-
- ` : ''}
-
- ${instructionsHtml}
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- document.body.appendChild(modal);
-
- // 关闭按钮事件
- const closeBtn = modal.querySelector('.modal-close');
- const cancelBtn = modal.querySelector('.modal-cancel');
- [closeBtn, cancelBtn].forEach(btn => {
- btn.addEventListener('click', () => {
- modal.remove();
- });
- });
-
- // 重新生成按钮事件
- const regenerateBtn = modal.querySelector('.regenerate-port-btn');
- if (regenerateBtn) {
- regenerateBtn.onclick = async () => {
- const newPort = modal.querySelector('.auth-port-input').value;
- if (newPort && newPort !== requiredPort) {
- modal.remove();
- // 构造重新请求的参数
- const options = { ...authInfo, port: newPort };
- // 移除不需要传递回后端的字段
- delete options.provider;
- delete options.redirectUri;
- delete options.callbackPort;
-
- await executeGenerateAuthUrl(authInfo.provider, options);
- }
- };
- }
-
- // Builder ID Start URL 重新生成按钮事件
- const regenerateBuilderIdBtn = modal.querySelector('.regenerate-builder-id-btn');
- if (regenerateBuilderIdBtn) {
- regenerateBuilderIdBtn.onclick = async () => {
- const builderIdStartUrl = modal.querySelector('.builder-id-start-url-input').value.trim();
- const region = modal.querySelector('.builder-id-region-input').value.trim();
- modal.remove();
- // 构造重新请求的参数
- const options = {
- ...authInfo,
- builderIDStartURL: builderIdStartUrl || 'https://view.awsapps.com/start',
- region: region || 'us-east-1'
- };
- // 移除不需要传递回后端的字段
- delete options.provider;
- delete options.redirectUri;
- delete options.callbackPort;
-
- await executeGenerateAuthUrl(authInfo.provider, options);
- };
- }
-
- // 复制链接按钮
- const copyBtn = modal.querySelector('.copy-btn');
- copyBtn.addEventListener('click', () => {
- const input = modal.querySelector('.auth-url-input');
- input.select();
- document.execCommand('copy');
- showToast(t('common.success'), t('oauth.success.msg'), 'success');
- });
-
- // 在浏览器中打开按钮
- const openBtn = modal.querySelector('.open-auth-btn');
- openBtn.addEventListener('click', () => {
- // 使用子窗口打开,以便监听 URL 变化
- const width = 600;
- const height = 700;
- const left = (window.screen.width - width) / 2 + 600;
- const top = (window.screen.height - height) / 2;
-
- const authWindow = window.open(
- authUrl,
- 'OAuthAuthWindow',
- `width=${width},height=${height},left=${left},top=${top},status=no,resizable=yes,scrollbars=yes`
- );
-
- // 监听 OAuth 成功事件,自动关闭窗口和模态框
- const handleOAuthSuccess = () => {
- if (authWindow && !authWindow.closed) {
- authWindow.close();
- }
- modal.remove();
- window.removeEventListener('oauth_success_event', handleOAuthSuccess);
-
- // 授权成功后刷新配置和提供商列表
- loadProviders();
- loadConfigList();
- };
- window.addEventListener('oauth_success_event', handleOAuthSuccess);
-
- if (authWindow) {
- showToast(t('common.info'), t('oauth.window.opened'), 'info');
-
- // 添加手动输入回调 URL 的 UI
- const urlSection = modal.querySelector('.auth-url-section');
- if (urlSection && !modal.querySelector('.manual-callback-section')) {
- const manualInputHtml = `
-
-
${t('oauth.manual.title')}
-
${t('oauth.manual.desc')}
-
-
-
-
-
- `;
- urlSection.insertAdjacentHTML('afterend', manualInputHtml);
- }
-
- const manualInput = modal.querySelector('.manual-callback-input');
- const applyBtn = modal.querySelector('.apply-callback-btn');
-
- // 处理回调 URL 的核心逻辑
- const processCallback = (urlStr, isManualInput = false) => {
- try {
- // 尝试清理 URL(有些用户可能会复制多余的文字)
- const cleanUrlStr = urlStr.trim().match(/https?:\/\/[^\s]+/)?.[0] || urlStr.trim();
- const url = new URL(cleanUrlStr);
-
- if (url.searchParams.has('code') || url.searchParams.has('token')) {
- clearInterval(pollTimer);
- // 构造本地可处理的 URL,只修改 hostname,保持原始 URL 的端口号不变
- const localUrl = new URL(url.href);
- localUrl.hostname = window.location.hostname;
- localUrl.protocol = window.location.protocol;
-
- showToast(t('common.info'), t('oauth.processing'), 'info');
-
- // 如果是手动输入,直接通过 fetch 请求处理,然后关闭子窗口
- if (isManualInput) {
- // 关闭子窗口
- if (authWindow && !authWindow.closed) {
- authWindow.close();
- }
- // 通过服务端API处理手动输入的回调URL
- window.apiClient.post('/oauth/manual-callback', {
- provider: authInfo.provider,
- callbackUrl: url.href, //使用localhost访问
- authMethod: authInfo.authMethod
- })
- .then(response => {
- if (response.success) {
- console.log('OAuth 回调处理成功');
- showToast(t('common.success'), t('oauth.success.msg'), 'success');
- } else {
- console.error('OAuth 回调处理失败:', response.error);
- showToast(t('common.error'), response.error || t('oauth.error.process'), 'error');
- }
- })
- .catch(err => {
- console.error('OAuth 回调请求失败:', err);
- showToast(t('common.error'), t('oauth.error.process'), 'error');
- });
- } else {
- // 自动监听模式:优先在子窗口中跳转(如果没关)
- if (authWindow && !authWindow.closed) {
- authWindow.location.href = localUrl.href;
- } else {
- // 备选方案:通过 fetch 请求
- // 通过 fetch 请求本地服务器处理回调
- fetch(localUrl.href)
- .then(response => {
- if (response.ok) {
- console.log('OAuth 回调处理成功');
- } else {
- console.error('OAuth 回调处理失败:', response.status);
- }
- })
- .catch(err => {
- console.error('OAuth 回调请求失败:', err);
- });
- }
- }
-
- } else {
- showToast(t('common.warning'), t('oauth.invalid.url'), 'warning');
- }
- } catch (err) {
- console.error('处理回调失败:', err);
- showToast(t('common.error'), t('oauth.error.format'), 'error');
- }
- };
-
- applyBtn.addEventListener('click', () => {
- processCallback(manualInput.value, true);
- });
-
- // 启动定时器轮询子窗口 URL
- const pollTimer = setInterval(() => {
- try {
- if (authWindow.closed) {
- clearInterval(pollTimer);
- return;
- }
- // 如果能读到说明回到了同域
- const currentUrl = authWindow.location.href;
- if (currentUrl && (currentUrl.includes('code=') || currentUrl.includes('token='))) {
- processCallback(currentUrl);
- }
- } catch (e) {
- // 跨域受限是正常的
- }
- }, 1000);
- } else {
- showToast(t('common.error'), t('oauth.window.blocked'), 'error');
- }
- });
-
-}
-
-/**
- * 显示需要重启的提示模态框
- * @param {string} version - 更新到的版本号
- */
-function showRestartRequiredModal(version) {
- const modal = document.createElement('div');
- modal.className = 'modal-overlay restart-required-modal';
- modal.style.display = 'flex';
-
- modal.innerHTML = `
-
-
-
-
${t('dashboard.update.restartMsg', { version })}
-
-
-
- `;
-
- document.body.appendChild(modal);
-
- // 关闭按钮事件
- const closeBtn = modal.querySelector('.modal-close');
- const confirmBtn = modal.querySelector('.restart-confirm-btn');
-
- const closeModal = () => {
- modal.remove();
- };
-
- closeBtn.addEventListener('click', closeModal);
- confirmBtn.addEventListener('click', closeModal);
-
- // 点击遮罩层关闭
- modal.addEventListener('click', (e) => {
- if (e.target === modal) {
- closeModal();
- }
- });
-}
-
-/**
- * 检查更新
- * @param {boolean} silent - 是否静默检查(不显示 Toast)
- */
-async function checkUpdate(silent = false) {
- const checkBtn = document.getElementById('checkUpdateBtn');
- const updateBtn = document.getElementById('performUpdateBtn');
- const updateBadge = document.getElementById('updateBadge');
- const latestVersionText = document.getElementById('latestVersionText');
- const checkBtnIcon = checkBtn?.querySelector('i');
- const checkBtnText = checkBtn?.querySelector('span');
-
- try {
- if (!silent && checkBtn) {
- checkBtn.disabled = true;
- if (checkBtnIcon) checkBtnIcon.className = 'fas fa-spinner fa-spin';
- if (checkBtnText) checkBtnText.textContent = t('dashboard.update.checking');
- }
-
- const data = await window.apiClient.get('/check-update');
-
- if (data.hasUpdate) {
- if (updateBtn) updateBtn.style.display = 'inline-flex';
- if (updateBadge) updateBadge.style.display = 'inline-flex';
- if (latestVersionText) latestVersionText.textContent = data.latestVersion;
-
- if (!silent) {
- showToast(t('common.info'), t('dashboard.update.hasUpdate', { version: data.latestVersion }), 'info');
- }
- } else {
- if (updateBtn) updateBtn.style.display = 'none';
- if (updateBadge) updateBadge.style.display = 'none';
- if (!silent) {
- showToast(t('common.info'), t('dashboard.update.upToDate'), 'success');
- }
- }
- } catch (error) {
- console.error('Check update failed:', error);
- if (!silent) {
- showToast(t('common.error'), t('dashboard.update.failed', { error: error.message }), 'error');
- }
- } finally {
- if (checkBtn) {
- checkBtn.disabled = false;
- if (checkBtnIcon) checkBtnIcon.className = 'fas fa-sync-alt';
- if (checkBtnText) checkBtnText.textContent = t('dashboard.update.check');
- }
- }
-}
-
-/**
- * 执行更新
- */
-async function performUpdate() {
- const updateBtn = document.getElementById('performUpdateBtn');
- const latestVersionText = document.getElementById('latestVersionText');
- const version = latestVersionText?.textContent || '';
-
- if (!confirm(t('dashboard.update.confirmMsg', { version }))) {
- return;
- }
-
- const updateBtnIcon = updateBtn?.querySelector('i');
- const updateBtnText = updateBtn?.querySelector('span');
-
- try {
- if (updateBtn) {
- updateBtn.disabled = true;
- if (updateBtnIcon) updateBtnIcon.className = 'fas fa-spinner fa-spin';
- if (updateBtnText) updateBtnText.textContent = t('dashboard.update.updating');
- }
-
- showToast(t('common.info'), t('dashboard.update.updating'), 'info');
-
- const data = await window.apiClient.post('/update');
-
- if (data.success) {
- if (data.updated) {
- // 代码已更新,直接调用重启服务
- showToast(t('common.success'), t('dashboard.update.success'), 'success');
-
- // 自动重启服务
- await restartServiceAfterUpdate();
- } else {
- // 已是最新版本
- showToast(t('common.info'), t('dashboard.update.upToDate'), 'info');
- }
- }
- } catch (error) {
- console.error('Update failed:', error);
- showToast(t('common.error'), t('dashboard.update.failed', { error: error.message }), 'error');
- } finally {
- if (updateBtn) {
- updateBtn.disabled = false;
- if (updateBtnIcon) updateBtnIcon.className = 'fas fa-download';
- if (updateBtnText) updateBtnText.textContent = t('dashboard.update.perform');
- }
- }
-}
-
-/**
- * 更新后自动重启服务
- */
-async function restartServiceAfterUpdate() {
- try {
- showToast(t('common.info'), t('header.restart.requesting'), 'info');
-
- const token = localStorage.getItem('authToken');
- const response = await fetch('/api/restart-service', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': token ? `Bearer ${token}` : ''
- }
- });
-
- const result = await response.json();
-
- if (response.ok && result.success) {
- showToast(t('common.success'), result.message || t('header.restart.success'), 'success');
-
- // 如果是 worker 模式,服务会自动重启,等待几秒后刷新页面
- if (result.mode === 'worker') {
- setTimeout(() => {
- showToast(t('common.info'), t('header.restart.reconnecting'), 'info');
- // 等待服务重启后刷新页面
- setTimeout(() => {
- window.location.reload();
- }, 3000);
- }, 2000);
- }
- } else {
- // 显示错误信息
- const errorMsg = result.message || result.error?.message || t('header.restart.failed');
- showToast(t('common.error'), errorMsg, 'error');
-
- // 如果是独立模式,显示提示
- if (result.mode === 'standalone') {
- showToast(t('common.info'), result.hint, 'warning');
- }
- }
- } catch (error) {
- console.error('Restart after update failed:', error);
- showToast(t('common.error'), t('header.restart.failed') + ': ' + error.message, 'error');
- }
-}
-
-export {
- loadSystemInfo,
- updateTimeDisplay,
- loadProviders,
- renderProviders,
- updateProviderStatsDisplay,
- openProviderManager,
- showAuthModal,
- executeGenerateAuthUrl,
- handleGenerateAuthUrl,
- checkUpdate,
- performUpdate
-};
\ No newline at end of file
diff --git a/static/app/routing-examples.js b/static/app/routing-examples.js
deleted file mode 100644
index ed71b3b05f5bdd920cde80943345b3044c64284c..0000000000000000000000000000000000000000
--- a/static/app/routing-examples.js
+++ /dev/null
@@ -1,544 +0,0 @@
-// 路径路由示例功能模块
-
-import { showToast } from './utils.js';
-import { t } from './i18n.js';
-
-/**
- * 初始化路径路由示例功能
- */
-function initRoutingExamples() {
- // 延迟初始化,确保所有DOM都加载完成
- setTimeout(() => {
- initProtocolTabs();
- initCopyButtons();
- initCardInteractions();
- }, 100);
-}
-
-/**
- * 初始化协议标签切换功能
- */
-function initProtocolTabs() {
- // 使用事件委托方式绑定点击事件
- document.addEventListener('click', function(e) {
- // 检查点击的是不是协议标签或者其子元素
- const tab = e.target.classList.contains('protocol-tab') ? e.target : e.target.closest('.protocol-tab');
-
- if (tab) {
- e.preventDefault();
- e.stopPropagation();
-
- const targetProtocol = tab.dataset.protocol;
- const card = tab.closest('.routing-example-card');
-
- if (!card) {
- return;
- }
-
- // 移除当前卡片中所有标签和内容的活动状态
- const cardTabs = card.querySelectorAll('.protocol-tab');
- const cardContents = card.querySelectorAll('.protocol-content');
-
- cardTabs.forEach(t => t.classList.remove('active'));
- cardContents.forEach(c => c.classList.remove('active'));
-
- // 为当前标签和对应内容添加活动状态
- tab.classList.add('active');
-
- // 使用更精确的选择器来查找对应的内容
- const targetContent = card.querySelector(`.protocol-content[data-protocol="${targetProtocol}"]`);
- if (targetContent) {
- targetContent.classList.add('active');
- }
- }
- });
-}
-
-/**
- * 初始化复制按钮功能
- */
-function initCopyButtons() {
- document.addEventListener('click', async function(e) {
- if (e.target.closest('.copy-btn')) {
- e.stopPropagation();
-
- const button = e.target.closest('.copy-btn');
- const path = button.dataset.path;
- if (!path) return;
-
- try {
- await navigator.clipboard.writeText(path);
- showToast(t('common.success'), `${t('common.success')}: ${path}`, 'success');
-
- // 临时更改按钮图标
- const icon = button.querySelector('i');
- if (icon) {
- const originalClass = icon.className;
- icon.className = 'fas fa-check';
- button.style.color = 'var(--success-color)';
-
- setTimeout(() => {
- icon.className = originalClass;
- button.style.color = '';
- }, 2000);
- }
-
- } catch (error) {
- console.error('Failed to copy to clipboard:', error);
- showToast(t('common.error'), t('common.error'), 'error');
- }
- }
- });
-}
-
-/**
- * 初始化卡片交互功能
- */
-function initCardInteractions() {
- const routingCards = document.querySelectorAll('.routing-example-card');
-
- routingCards.forEach(card => {
- // 添加悬停效果
- card.addEventListener('mouseenter', () => {
- card.style.transform = 'translateY(-4px)';
- card.style.boxShadow = 'var(--shadow-lg)';
- });
-
- card.addEventListener('mouseleave', () => {
- card.style.transform = '';
- card.style.boxShadow = '';
- });
-
- });
-}
-
-/**
- * 获取所有可用的路由端点
- * @returns {Array} 路由端点数组
- */
-function getAvailableRoutes() {
- return [
- {
- provider: 'forward-api',
- name: 'NewAPI',
- paths: {
- openai: '/forward-api/v1/chat/completions',
- claude: '/forward-api/v1/messages'
- },
- description: t('dashboard.routing.official'),
- badge: t('dashboard.routing.official'),
- badgeClass: 'official'
- },
- {
- provider: 'claude-custom',
- name: 'Claude Custom',
- paths: {
- openai: '/claude-custom/v1/chat/completions',
- claude: '/claude-custom/v1/messages'
- },
- description: t('dashboard.routing.official'),
- badge: t('dashboard.routing.official'),
- badgeClass: 'official'
- },
- {
- provider: 'claude-kiro-oauth',
- name: 'Claude Kiro OAuth',
- paths: {
- openai: '/claude-kiro-oauth/v1/chat/completions',
- claude: '/claude-kiro-oauth/v1/messages'
- },
- description: t('dashboard.routing.free'),
- badge: t('dashboard.routing.free'),
- badgeClass: 'oauth'
- },
- {
- provider: 'openai-custom',
- name: 'OpenAI Custom',
- paths: {
- openai: '/openai-custom/v1/chat/completions',
- claude: '/openai-custom/v1/messages'
- },
- description: t('dashboard.routing.official'),
- badge: t('dashboard.routing.official'),
- badgeClass: 'official'
- },
- {
- provider: 'gemini-cli-oauth',
- name: 'Gemini CLI OAuth',
- paths: {
- openai: '/gemini-cli-oauth/v1/chat/completions',
- claude: '/gemini-cli-oauth/v1/messages'
- },
- description: t('dashboard.routing.oauth'),
- badge: t('dashboard.routing.oauth'),
- badgeClass: 'oauth'
- },
- {
- provider: 'gemini-antigravity',
- name: 'Gemini Antigravity',
- paths: {
- openai: '/gemini-antigravity/v1/chat/completions',
- claude: '/gemini-antigravity/v1/messages'
- },
- description: t('dashboard.routing.experimental') || '实验性',
- badge: t('dashboard.routing.experimental') || '实验性',
- badgeClass: 'oauth'
- },
- {
- provider: 'openai-qwen-oauth',
- name: 'Qwen OAuth',
- paths: {
- openai: '/openai-qwen-oauth/v1/chat/completions',
- claude: '/openai-qwen-oauth/v1/messages'
- },
- description: 'Qwen Code Plus',
- badge: t('dashboard.routing.oauth'),
- badgeClass: 'oauth'
- },
- {
- provider: 'openai-iflow',
- name: 'iFlow OAuth',
- paths: {
- openai: '/openai-iflow/v1/chat/completions',
- claude: '/openai-iflow/v1/messages'
- },
- description: t('dashboard.routing.oauth'),
- badge: t('dashboard.routing.oauth'),
- badgeClass: 'oauth'
- },
- {
- provider: 'openai-codex-oauth',
- name: 'OpenAI Codex OAuth',
- paths: {
- openai: '/openai-codex-oauth/v1/chat/completions',
- claude: '/openai-codex-oauth/v1/messages'
- },
- description: t('dashboard.routing.oauth'),
- badge: t('dashboard.routing.oauth'),
- badgeClass: 'oauth'
- },
- {
- provider: 'openaiResponses-custom',
- name: 'OpenAI Responses',
- paths: {
- openai: '/openaiResponses-custom/v1/responses',
- claude: '/openaiResponses-custom/v1/messages'
- },
- description: '结构化对话API',
- badge: 'Responses',
- badgeClass: 'responses'
- },
- {
- provider: 'grok-custom',
- name: 'Grok Reverse',
- paths: {
- openai: '/grok-custom/v1/chat/completions',
- claude: '/grok-custom/v1/messages'
- },
- description: t('dashboard.routing.free'),
- badge: t('dashboard.routing.free'),
- badgeClass: 'oauth'
- }
- ];
-}
-
-/**
- * 高亮显示特定提供商路由
- * @param {string} provider - 提供商标识
- */
-function highlightProviderRoute(provider) {
- const card = document.querySelector(`[data-provider="${provider}"]`);
- if (card) {
- card.scrollIntoView({ behavior: 'smooth', block: 'center' });
- card.style.borderColor = 'var(--success-color)';
- card.style.boxShadow = '0 0 0 3px rgba(16, 185, 129, 0.1)';
-
- setTimeout(() => {
- card.style.borderColor = '';
- card.style.boxShadow = '';
- }, 3000);
-
- showToast(t('common.success'), t('common.success') + `: ${provider}`, 'success');
- }
-}
-
-/**
- * 复制curl命令示例
- * @param {string} provider - 提供商标识
- * @param {Object} options - 选项参数
- */
-async function copyCurlExample(provider, options = {}) {
- const routes = getAvailableRoutes();
- const route = routes.find(r => r.provider === provider);
-
- if (!route) {
- showToast(t('common.error'), t('common.error'), 'error');
- return;
- }
-
- const { protocol = 'openai', model = 'default-model', message = 'Hello!' } = options;
- const path = route.paths[protocol];
-
- if (!path) {
- showToast(t('common.error'), t('common.error'), 'error');
- return;
- }
-
- let curlCommand = '';
-
- // 根据不同提供商和协议生成对应的curl命令
- switch (provider) {
- case 'claude-custom':
- case 'claude-kiro-oauth':
- if (protocol === 'openai') {
- curlCommand = `curl http://localhost:3000${path} \\
- -H "Content-Type: application/json" \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -d '{
- "model": "${model}",
- "messages": [{"role": "user", "content": "${message}"}],
- "max_tokens": 1000
- }'`;
- } else {
- curlCommand = `curl http://localhost:3000${path} \\
- -H "Content-Type: application/json" \\
- -d '{
- "model": "${model}",
- "max_tokens": 1000,
- "messages": [{"role": "user", "content": "${message}"}]
- }'`;
- }
- break;
-
- case 'openai-custom':
- case 'openai-qwen-oauth':
- if (protocol === 'openai') {
- curlCommand = `curl http://localhost:3000${path} \\
- -H "Content-Type: application/json" \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -d '{
- "model": "${model}",
- "messages": [{"role": "user", "content": "${message}"}],
- "max_tokens": 1000
- }'`;
- } else {
- curlCommand = `curl http://localhost:3000${path} \\
- -H "Content-Type: application/json" \\
- -H "X-API-Key: YOUR_API_KEY" \\
- -d '{
- "model": "${model}",
- "max_tokens": 1000,
- "messages": [{"role": "user", "content": "${message}"}]
- }'`;
- }
- break;
-
- case 'gemini-cli-oauth':
- if (protocol === 'openai') {
- curlCommand = `curl http://localhost:3000${path} \\
- -H "Content-Type: application/json" \\
- -d '{
- "model": "gemini-3.1-pro-preview",
- "messages": [{"role": "user", "content": "${message}"}],
- "max_tokens": 1000
- }'`;
- } else {
- curlCommand = `curl http://localhost:3000${path} \\
- -H "Content-Type: application/json" \\
- -d '{
- "model": "gemini-3.1-pro-preview",
- "max_tokens": 1000,
- "messages": [{"role": "user", "content": "${message}"}]
- }'`;
- }
- break;
-
- case 'openaiResponses-custom':
- if (protocol === 'openai') {
- curlCommand = `curl http://localhost:3000${path} \\
- -H "Content-Type: application/json" \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -d '{
- "model": "${model}",
- "input": "${message}",
- "max_output_tokens": 1000
- }'`;
- } else {
- curlCommand = `curl http://localhost:3000${path} \\
- -H "Content-Type: application/json" \\
- -H "X-API-Key: YOUR_API_KEY" \\
- -d '{
- "model": "${model}",
- "max_tokens": 1000,
- "messages": [{"role": "user", "content": "${message}"}]
- }'`;
- }
- break;
- case 'grok-custom':
- if (protocol === 'openai') {
- curlCommand = `curl http://localhost:3000${path} \\
- -H "Content-Type: application/json" \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -d '{
- "model": "grok-3",
- "messages": [{"role": "user", "content": "${message}"}],
- "stream": true
- }'`;
- } else {
- curlCommand = `curl http://localhost:3000${path} \\
- -H "Content-Type: application/json" \\
- -H "X-API-Key: YOUR_API_KEY" \\
- -d '{
- "model": "grok-3",
- "max_tokens": 1000,
- "messages": [{"role": "user", "content": "${message}"}]
- }'`;
- }
- break;
- }
-
- try {
- await navigator.clipboard.writeText(curlCommand);
- showToast(t('common.success'), t('oauth.success.msg'), 'success');
- } catch (error) {
- console.error('Failed to copy curl command:', error);
- showToast(t('common.error'), t('common.error'), 'error');
- }
-}
-
-/**
- * 动态渲染路径路由示例
- * @param {Array} providerConfigs - 提供商配置列表
- */
-function renderRoutingExamples(providerConfigs) {
- const container = document.querySelector('.routing-examples-grid');
- if (!container) return;
-
- container.innerHTML = '';
-
- // 获取路由端点基础信息
- const routes = getAvailableRoutes();
-
- // 图标映射
- const iconMap = {
- 'forward-api': 'fa-share-square',
- 'gemini-cli-oauth': 'fa-gem',
- 'gemini-antigravity': 'fa-rocket',
- 'openai-custom': 'fa-comments',
- 'claude-custom': 'fa-brain',
- 'claude-kiro-oauth': 'fa-robot',
- 'openai-qwen-oauth': 'fa-code',
- 'openaiResponses-custom': 'fa-comment-alt',
- 'openai-iflow': 'fa-wind',
- 'openai-codex-oauth': 'fa-keyboard',
- 'grok-custom': 'fa-search'
- };
-
- // 默认模型映射 (用于 curl 示例)
- const modelMap = {
- 'gemini-cli-oauth': 'gemini-3-flash-preview',
- 'gemini-antigravity': 'gemini-3-flash-preview',
- 'claude-custom': 'claude-sonnet-4-6',
- 'claude-kiro-oauth': 'claude-sonnet-4-6',
- 'openai-custom': 'gpt-4o',
- 'openai-qwen-oauth': 'qwen3-coder-plus',
- 'openai-iflow': 'qwen3-max',
- 'openai-codex-oauth': 'gpt-5',
- 'grok-custom': 'grok-3',
- 'openaiResponses-custom': 'gpt-4o'
- };
-
- providerConfigs.forEach(config => {
- if (config.visible === false) return;
-
- let routeInfo = routes.find(r => r.provider === config.id);
-
- // 如果没找到,则创建一个默认的
- if (!routeInfo) {
- routeInfo = {
- provider: config.id,
- name: config.name,
- paths: {
- openai: `/${config.id}/v1/chat/completions`,
- claude: `/${config.id}/v1/messages`
- },
- description: t('dashboard.routing.oauth'),
- badge: t('dashboard.routing.oauth'),
- badgeClass: 'oauth'
- };
- }
-
- const icon = iconMap[config.id] || 'fa-route';
- const defaultModel = modelMap[config.id] || 'default-model';
- const hostname = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' ?
- `http://${window.location.host}` :
- `${window.location.protocol}//${window.location.host}`;
-
- const card = document.createElement('div');
- card.className = 'routing-example-card';
- card.dataset.provider = `${config.id}-card`;
-
- card.innerHTML = `
-
-
-
-
-
-
-
-
-
-
- ${routeInfo.paths.openai}
-
-
-
-
curl ${hostname}${routeInfo.paths.openai} \\
- -H "Content-Type: application/json" \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -d '{
- "model": "${defaultModel}",
- "messages": [{"role": "user", "content": "Hello!"}],
- "max_tokens": 1000
- }'
-
-
-
-
-
-
- ${routeInfo.paths.claude}
-
-
-
-
curl ${hostname}${routeInfo.paths.claude} \\
- -H "Content-Type: application/json" \\
- -H "X-API-Key: YOUR_API_KEY" \\
- -d '{
- "model": "${defaultModel}",
- "max_tokens": 1000,
- "messages": [{"role": "user", "content": "Hello!"}]
- }'
-
-
-
- `;
-
- container.appendChild(card);
- });
-
- // 重新初始化卡片交互
- initCardInteractions();
-}
-
-export {
- initRoutingExamples,
- getAvailableRoutes,
- highlightProviderRoute,
- copyCurlExample,
- renderRoutingExamples
-};
diff --git a/static/app/theme-switcher.js b/static/app/theme-switcher.js
deleted file mode 100644
index 70dc42a8ecf3eb06d42961a9951364a47de519ca..0000000000000000000000000000000000000000
--- a/static/app/theme-switcher.js
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * 主题切换模块
- * 支持亮色/暗黑主题切换,并保存用户偏好到 localStorage
- */
-
-// 主题常量
-const THEME_KEY = 'theme';
-const THEME_LIGHT = 'light';
-const THEME_DARK = 'dark';
-
-/**
- * 获取当前主题
- * @returns {string} 当前主题 ('light' 或 'dark')
- */
-export function getCurrentTheme() {
- // 优先从 localStorage 获取
- const savedTheme = localStorage.getItem(THEME_KEY);
- if (savedTheme) {
- return savedTheme;
- }
-
- // 检查系统偏好
- if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
- return THEME_DARK;
- }
-
- return THEME_LIGHT;
-}
-
-/**
- * 设置主题
- * @param {string} theme - 主题名称 ('light' 或 'dark')
- */
-export function setTheme(theme) {
- const root = document.documentElement;
-
- if (theme === THEME_DARK) {
- root.setAttribute('data-theme', THEME_DARK);
- } else {
- root.removeAttribute('data-theme');
- }
-
- // 保存到 localStorage
- localStorage.setItem(THEME_KEY, theme);
-
- // 更新 meta theme-color
- updateMetaThemeColor(theme);
-
- // 触发自定义事件
- window.dispatchEvent(new CustomEvent('themechange', { detail: { theme } }));
-}
-
-/**
- * 切换主题
- * @returns {string} 切换后的主题
- */
-export function toggleTheme() {
- const currentTheme = getCurrentTheme();
- const newTheme = currentTheme === THEME_DARK ? THEME_LIGHT : THEME_DARK;
- setTheme(newTheme);
- return newTheme;
-}
-
-/**
- * 更新 meta theme-color
- * @param {string} theme - 主题名称
- */
-function updateMetaThemeColor(theme) {
- const metaThemeColor = document.querySelector('meta[name="theme-color"]');
- if (metaThemeColor) {
- // 暗黑主题使用深色,亮色主题使用主色调
- metaThemeColor.setAttribute('content', theme === THEME_DARK ? '#1f2937' : '#059669');
- }
-}
-
-/**
- * 初始化主题切换器
- * @param {string} [toggleButtonId='themeToggleBtn'] - 切换按钮的 ID
- */
-export function initThemeSwitcher(toggleButtonId = 'themeToggleBtn') {
- // 应用保存的主题或系统偏好
- const savedTheme = getCurrentTheme();
- setTheme(savedTheme);
-
- // 绑定切换按钮事件
- const toggleBtn = document.getElementById(toggleButtonId);
- if (toggleBtn) {
- toggleBtn.addEventListener('click', () => {
- const newTheme = toggleTheme();
- console.log(`主题已切换为: ${newTheme === THEME_DARK ? '暗黑模式' : '亮色模式'}`);
- });
- }
-
- // 监听系统主题变化
- if (window.matchMedia) {
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
- mediaQuery.addEventListener('change', (e) => {
- // 只有在用户没有手动设置主题时才跟随系统
- const savedTheme = localStorage.getItem(THEME_KEY);
- if (!savedTheme) {
- setTheme(e.matches ? THEME_DARK : THEME_LIGHT);
- }
- });
- }
-
- console.log(`主题切换器已初始化,当前主题: ${savedTheme === THEME_DARK ? '暗黑模式' : '亮色模式'}`);
-}
-
-/**
- * 检查当前是否为暗黑主题
- * @returns {boolean}
- */
-export function isDarkTheme() {
- return getCurrentTheme() === THEME_DARK;
-}
-
-// 导出常量
-export { THEME_LIGHT, THEME_DARK };
\ No newline at end of file
diff --git a/static/app/tutorial-manager.js b/static/app/tutorial-manager.js
deleted file mode 100644
index f3b01b87d9e6d23a2a2c791c1c48e2dffc509c28..0000000000000000000000000000000000000000
--- a/static/app/tutorial-manager.js
+++ /dev/null
@@ -1,57 +0,0 @@
-// 教程管理模块
-import { getProviderConfigs } from './utils.js';
-
-// 提供商配置缓存
-let currentProviderConfigs = null;
-
-/**
- * 初始化教程功能
- */
-function initTutorialManager() {
- renderOauthPaths();
-
- // 监听语言切换事件
- window.addEventListener('languageChanged', () => {
- renderOauthPaths(currentProviderConfigs);
- });
-}
-
-/**
- * 更新提供商配置
- * @param {Array} configs - 提供商配置列表
- */
-function updateTutorialProviderConfigs(configs) {
- currentProviderConfigs = configs;
- renderOauthPaths(configs);
-}
-
-/**
- * 渲染 OAuth 授权路径列表
- * @param {Array} configs - 提供商配置列表(可选)
- */
-function renderOauthPaths(configs = null) {
- const oauthPathList = document.getElementById('oauthPathList');
- if (!oauthPathList) return;
-
- // 获取所有提供商配置
- const providers = configs || getProviderConfigs([]);
-
- // 过滤出有默认路径配置的提供商(即 OAuth 类提供商)且可见的
- const oauthProviders = providers.filter(p => p.defaultPath && p.visible !== false);
-
- oauthPathList.innerHTML = oauthProviders.map(p => `
-
-
- ${p.defaultPath}
-
- `).join('');
-}
-
-export {
- initTutorialManager,
- renderOauthPaths,
- updateTutorialProviderConfigs
-};
diff --git a/static/app/upload-config-manager.js b/static/app/upload-config-manager.js
deleted file mode 100644
index 967b26cd4d7633bbf54db940b8b540194cf8a2be..0000000000000000000000000000000000000000
--- a/static/app/upload-config-manager.js
+++ /dev/null
@@ -1,1357 +0,0 @@
-// 配置管理功能模块
-
-import { showToast } from './utils.js';
-import { t } from './i18n.js';
-
-let allConfigs = []; // 存储所有配置数据
-let filteredConfigs = []; // 存储过滤后的配置数据
-let isLoadingConfigs = false; // 防止重复加载配置
-
-/**
- * 搜索配置
- * @param {string} searchTerm - 搜索关键词
- * @param {string} statusFilter - 状态过滤
- */
-function searchConfigs(searchTerm = '', statusFilter = '', providerFilter = '') {
- // 确保 searchTerm 是字符串,防止事件对象等非字符串被传入
- if (typeof searchTerm !== 'string') {
- searchTerm = '';
- }
-
- if (!allConfigs.length) {
- console.log('没有配置数据可搜索');
- return;
- }
-
- filteredConfigs = allConfigs.filter(config => {
- // 搜索过滤
- const matchesSearch = !searchTerm ||
- config.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
- config.path.toLowerCase().includes(searchTerm.toLowerCase()) ||
- (config.content && config.content.toLowerCase().includes(searchTerm.toLowerCase()));
-
- // 状态过滤 - 从布尔值 isUsed 转换为状态字符串
- const configStatus = config.isUsed ? 'used' : 'unused';
- const matchesStatus = !statusFilter || configStatus === statusFilter;
-
- // 提供商类型过滤
- let matchesProvider = true;
- if (providerFilter) {
- const providerInfo = detectProviderFromPath(config.path);
- if (providerFilter === 'other') {
- // "其他/未识别" 选项:匹配没有识别到提供商的配置
- matchesProvider = providerInfo === null;
- } else {
- // 匹配特定提供商类型
- matchesProvider = providerInfo !== null && providerInfo.providerType === providerFilter;
- }
- }
-
- return matchesSearch && matchesStatus && matchesProvider;
- });
-
- renderConfigList();
- updateStats();
-}
-
-/**
- * 渲染配置列表
- */
-function renderConfigList() {
- const container = document.getElementById('configList');
- if (!container) return;
-
- container.innerHTML = '';
-
- if (!filteredConfigs.length) {
- container.innerHTML = ``;
- return;
- }
-
- filteredConfigs.forEach((config, index) => {
- const configItem = createConfigItemElement(config, index);
- container.appendChild(configItem);
- });
-}
-
-/**
- * 创建配置项元素
- * @param {Object} config - 配置数据
- * @param {number} index - 索引
- * @returns {HTMLElement} 配置项元素
- */
-function createConfigItemElement(config, index) {
- // 从布尔值 isUsed 转换为状态字符串用于显示
- const configStatus = config.isUsed ? 'used' : 'unused';
- const item = document.createElement('div');
- item.className = `config-item-manager ${configStatus}`;
- item.dataset.index = index;
-
- const statusIcon = config.isUsed ? 'fa-check-circle' : 'fa-circle-question';
- const statusText = config.isUsed ? t('upload.statusFilter.used') : t('upload.statusFilter.unused');
-
- const typeIcon = config.type === 'oauth' ? 'fa-key' :
- config.type === 'api-key' ? 'fa-lock' :
- config.type === 'provider-pool' ? 'fa-network-wired' :
- config.type === 'system-prompt' ? 'fa-file-text' :
- config.type === 'plugins' ? 'fa-plug' :
- config.type === 'usage' ? 'fa-chart-line' :
- config.type === 'config' ? 'fa-cog' :
- config.type === 'database' ? 'fa-database' : 'fa-file-code';
-
- // 检测提供商信息
- const providerInfo = detectProviderFromPath(config.path);
- const providerBadge = providerInfo ?
- `
- ${providerInfo.displayName}
- ` : '';
-
- // 生成关联详情HTML
- const usageInfoHtml = generateUsageInfoHtml(config);
-
- // 获取关联的节点简要信息
- let linkedNodesInfo = '';
- if (config.isUsed && config.usageInfo && config.usageInfo.usageDetails) {
- const details = config.usageInfo.usageDetails;
-
- // 收集节点信息及其状态
- const nodes = details.map(d => {
- let name = '';
- let isPool = false;
- if (d.type === 'Provider Pool' || d.type === '提供商池') {
- isPool = true;
- if (d.nodeName) name = d.nodeName;
- else if (d.uuid) name = d.uuid.substring(0, 8);
- else name = d.location;
- } else if (d.type === 'Main Config' || d.type === '主要配置') {
- name = t('upload.usage.mainConfig');
- }
-
- if (!name) return null;
-
- return {
- name,
- isPool,
- isHealthy: d.isHealthy,
- isDisabled: d.isDisabled
- };
- }).filter(Boolean);
-
- if (nodes.length > 0) {
- // 去重,但保留状态信息(如果有多个相同名称的节点,状态可能不同,这里按名称去重以节省空间,取第一个)
- const uniqueNodes = [];
- const seenNames = new Set();
- for (const node of nodes) {
- if (!seenNames.has(node.name)) {
- uniqueNodes.push(node);
- seenNames.add(node.name);
- }
- }
-
- linkedNodesInfo = `
- ${uniqueNodes.map(node => {
- let statusClass = '';
- let statusIcon = 'fa-link';
-
- if (node.isPool) {
- if (node.isDisabled) {
- statusClass = 'status-disabled';
- statusIcon = 'fa-ban';
- } else if (!node.isHealthy) {
- statusClass = 'status-unhealthy';
- statusIcon = 'fa-exclamation-circle';
- } else {
- statusClass = 'status-healthy';
- statusIcon = 'fa-check-circle';
- }
- }
-
- return ` ${node.name}`;
- }).join('')}
-
`;
- }
- }
-
- // 判断是否可以一键关联(未关联且路径包含支持的提供商目录)
- const canQuickLink = !config.isUsed && providerInfo !== null;
- const quickLinkBtnHtml = canQuickLink ?
- `` : '';
-
- item.innerHTML = `
-
-
-
-
-
-
-
- ${config.name}
- ${providerBadge}
-
-
- ${config.path}
-
-
-
-
-
-
-
- ${formatFileSize(config.size)}
-
-
- ${formatDate(config.modified)}
-
-
-
-
-
-
-
-
- ${statusText}
-
- ${linkedNodesInfo}
- ${quickLinkBtnHtml}
-
-
-
-
-
-
-
-
-
-
-
文件完整路径
-
${config.path}
-
-
-
文件大小
-
${formatFileSize(config.size)}
-
-
-
最后修改时间
-
${formatDate(config.modified)}
-
-
-
当前关联状态
-
${statusText}
-
-
- ${usageInfoHtml}
-
-
-
-
-
-
- `;
-
- // 添加按钮事件监听器
- const viewBtn = item.querySelector('.btn-view');
- const downloadBtn = item.querySelector('.btn-download');
- const deleteBtn = item.querySelector('.btn-delete-small');
-
- if (viewBtn) {
- viewBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- viewConfig(config.path);
- });
- }
-
- if (downloadBtn) {
- downloadBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- downloadSingleConfig(config.path);
- });
- }
-
- if (deleteBtn) {
- deleteBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- deleteConfig(config.path);
- });
- }
-
- // 一键关联按钮事件
- const quickLinkBtn = item.querySelector('.btn-quick-link-main');
- if (quickLinkBtn) {
- quickLinkBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- quickLinkProviderConfig(config.path);
- });
- }
-
- // 添加点击事件展开/折叠详情
- item.addEventListener('click', (e) => {
- if (!e.target.closest('.config-item-actions') && !e.target.closest('.config-detail-value')) {
- item.classList.toggle('expanded');
- }
- });
-
- // 点击路径复制
- const pathValueEl = item.querySelector('.path-item .config-detail-value');
- if (pathValueEl) {
- pathValueEl.addEventListener('click', async (e) => {
- e.stopPropagation();
- const textToCopy = config.path;
-
- // 优先使用 Clipboard API
- if (navigator.clipboard && navigator.clipboard.writeText) {
- try {
- await navigator.clipboard.writeText(textToCopy);
- showToast(t('common.success'), t('common.copy.success'), 'success');
- } catch (err) {
- console.error('Clipboard API failed:', err);
- fallbackCopyTextToClipboard(textToCopy);
- }
- } else {
- fallbackCopyTextToClipboard(textToCopy);
- }
- });
- pathValueEl.title = t('models.clickToCopy') || '点击复制';
- }
-
- return item;
-}
-
-/**
- * 降级复制方案
- * @param {string} text - 要复制的文本
- */
-function fallbackCopyTextToClipboard(text) {
- const textArea = document.createElement("textarea");
- textArea.value = text;
-
- // 确保不可见且不影响布局
- textArea.style.position = "fixed";
- textArea.style.left = "-9999px";
- textArea.style.top = "0";
- document.body.appendChild(textArea);
-
- textArea.focus();
- textArea.select();
-
- try {
- const successful = document.execCommand('copy');
- if (successful) {
- showToast(t('common.success'), t('common.copy.success'), 'success');
- } else {
- showToast(t('common.error'), t('common.copy.failed'), 'error');
- }
- } catch (err) {
- console.error('Fallback copy failed:', err);
- showToast(t('common.error'), t('common.copy.failed'), 'error');
- }
-
- document.body.removeChild(textArea);
-}
-
-/**
- * 生成关联详情HTML
- * @param {Object} config - 配置数据
- * @returns {string} HTML字符串
- */
-function generateUsageInfoHtml(config) {
- if (!config.usageInfo || !config.usageInfo.isUsed) {
- return '';
- }
-
- const { usageType, usageDetails } = config.usageInfo;
-
- if (!usageDetails || usageDetails.length === 0) {
- return '';
- }
-
- const typeLabels = {
- 'main_config': t('upload.usage.mainConfig'),
- 'provider_pool': t('upload.usage.providerPool'),
- 'multiple': t('upload.usage.multiple')
- };
-
- const typeLabel = typeLabels[usageType] || (t('common.info') === 'Info' ? 'Unknown' : '未知用途');
-
- let detailsHtml = '';
- usageDetails.forEach(detail => {
- const isMain = detail.type === '主要配置' || detail.type === 'Main Config';
- const icon = isMain ? 'fa-cog' : 'fa-network-wired';
- const usageTypeKey = isMain ? 'main_config' : 'provider_pool';
-
- // 严格遵循显示优先级:自定义名称 > UUID > 默认位置描述
- let displayTitle = '';
- let subtitle = '';
-
- if (detail.nodeName) {
- displayTitle = detail.nodeName;
- subtitle = detail.providerType ? `${detail.providerType} - ${detail.location}` : detail.location;
- } else if (detail.uuid) {
- displayTitle = detail.uuid;
- subtitle = detail.providerType ? `${detail.providerType} - ${detail.location}` : detail.location;
- } else {
- displayTitle = detail.location;
- subtitle = detail.providerType || '';
- }
-
- // 生成节点状态标签
- let statusTag = '';
- if (detail.type === 'Provider Pool' || detail.type === '提供商池') {
- if (detail.isDisabled) {
- statusTag = `${t('modal.provider.status.disabled')}`;
- } else if (!detail.isHealthy) {
- statusTag = `${t('modal.provider.status.unhealthy')}`;
- } else {
- statusTag = `${t('modal.provider.status.healthy')}`;
- }
- }
-
- detailsHtml += `
-
-
-
-
- ${detail.type}
- ${displayTitle}
- ${statusTag}
-
- ${subtitle ? `
${subtitle}
` : ''}
-
-
- `;
- });
-
- return `
-
- `;
-}
-
-/**
- * 对配置列表进行排序
- * 规则:未关联的排在前面,然后按修改时间倒序排列
- * @param {Array} configs - 配置列表
- * @returns {Array} 排序后的列表
- */
-function sortConfigs(configs) {
- if (!configs || !configs.length) return [];
-
- return configs.sort((a, b) => {
- // 1. 未关联优先 (isUsed 为 false 的排在前面)
- if (a.isUsed !== b.isUsed) {
- return a.isUsed ? 1 : -1;
- }
-
- // 2. 时间倒序 (最新的排在前面)
- const dateA = new Date(a.modified);
- const dateB = new Date(b.modified);
- return dateB - dateA;
- });
-}
-
-/**
- * 格式化文件大小
- * @param {number} bytes - 字节数
- * @returns {string} 格式化后的大小
- */
-function formatFileSize(bytes) {
- if (bytes === 0) return '0 B';
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
-}
-
-/**
- * 格式化日期
- * @param {string} dateString - 日期字符串
- * @returns {string} 格式化后的日期
- */
-function formatDate(dateString) {
- const date = new Date(dateString);
- return date.toLocaleString('zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit'
- });
-}
-
-/**
- * 更新统计信息
- */
-function updateStats() {
- const totalCount = filteredConfigs.length;
- const usedCount = filteredConfigs.filter(config => config.isUsed).length;
- const unusedCount = filteredConfigs.filter(config => !config.isUsed).length;
-
- const totalEl = document.getElementById('configCount');
- const usedEl = document.getElementById('usedConfigCount');
- const unusedEl = document.getElementById('unusedConfigCount');
-
- if (totalEl) {
- totalEl.textContent = t('upload.count', { count: totalCount });
- totalEl.setAttribute('data-i18n-params', JSON.stringify({ count: totalCount.toString() }));
- }
- if (usedEl) {
- usedEl.textContent = t('upload.usedCount', { count: usedCount });
- usedEl.setAttribute('data-i18n-params', JSON.stringify({ count: usedCount.toString() }));
- }
- if (unusedEl) {
- unusedEl.textContent = t('upload.unusedCount', { count: unusedCount });
- unusedEl.setAttribute('data-i18n-params', JSON.stringify({ count: unusedCount.toString() }));
- }
-}
-
-/**
- * 加载配置文件列表
- * @param {string} searchTerm - 搜索关键词
- * @param {string} statusFilter - 状态过滤
- * @param {string} providerFilter - 提供商过滤
- */
-async function loadConfigList(searchTerm = '', statusFilter = '', providerFilter = '') {
- // 确保 searchTerm 是字符串,处理事件监听器直接调用的情况
- if (typeof searchTerm !== 'string') {
- searchTerm = '';
- }
-
- // 防止重复加载
- if (isLoadingConfigs) {
- console.log('正在加载配置列表,跳过重复调用');
- return;
- }
-
- isLoadingConfigs = true;
- console.log('开始加载配置列表...');
-
- try {
- const result = await window.apiClient.get('/upload-configs');
- allConfigs = sortConfigs(result);
-
- // 如果提供了过滤参数,则执行搜索过滤,否则显示全部
- if (searchTerm || statusFilter || providerFilter) {
- searchConfigs(searchTerm, statusFilter, providerFilter);
- } else {
- filteredConfigs = [...allConfigs];
- renderConfigList();
- updateStats();
- }
-
- console.log('配置列表加载成功,共', allConfigs.length, '个项目');
- } catch (error) {
- console.error('加载配置列表失败:', error);
- showToast(t('common.error'), t('common.error') + ': ' + error.message, 'error');
- allConfigs = [];
- filteredConfigs = [];
- renderConfigList();
- updateStats();
- } finally {
- isLoadingConfigs = false;
- console.log('配置列表加载完成');
- }
-}
-
-/**
- * 下载单个配置文件
- * @param {string} filePath - 文件路径
- */
-async function downloadSingleConfig(filePath) {
- if (!filePath) return;
-
- try {
- const fileName = filePath.split(/[/\\]/).pop();
-
- const token = localStorage.getItem('authToken');
- const headers = {
- 'Authorization': token ? `Bearer ${token}` : ''
- };
-
- const response = await fetch(`/api/upload-configs/download/${encodeURIComponent(filePath)}`, {
- method: 'GET',
- headers: headers
- });
-
- 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;
- document.body.appendChild(a);
- a.click();
- window.URL.revokeObjectURL(url);
- document.body.removeChild(a);
-
- showToast(t('common.success'), t('usage.card.downloadSuccess') || '文件下载成功', 'success');
- } catch (error) {
- console.error('下载配置文件失败:', error);
- showToast(t('common.error'), (t('usage.card.downloadFailed') || '下载配置文件失败') + ': ' + error.message, 'error');
- }
-}
-
-/**
- * 查看配置
- * @param {string} path - 文件路径
- */
-async function viewConfig(path) {
- try {
- const fileData = await window.apiClient.get(`/upload-configs/view/${encodeURIComponent(path)}`);
- showConfigModal(fileData);
- } catch (error) {
- console.error('查看配置失败:', error);
- showToast(t('common.error'), t('upload.action.view.failed') + ': ' + error.message, 'error');
- }
-}
-
-/**
- * 显示配置模态框
- * @param {Object} fileData - 文件数据
- */
-function showConfigModal(fileData) {
- // 创建模态框
- const modal = document.createElement('div');
- modal.className = 'config-view-modal';
- modal.innerHTML = `
-
-
-
-
-
- ${t('upload.detail.path')}:
- ${fileData.path}
-
-
- ${t('upload.detail.size')}:
- ${formatFileSize(fileData.size)}
-
-
- ${t('upload.detail.modified')}:
- ${formatDate(fileData.modified)}
-
-
-
-
-
${escapeHtml(fileData.content)}
-
-
-
-
- `;
-
- // 添加到页面
- document.body.appendChild(modal);
-
- // 添加按钮事件监听器
- const closeBtn = modal.querySelector('.btn-close-modal');
- const copyBtn = modal.querySelector('.btn-copy-content');
- const modalCloseBtn = modal.querySelector('.modal-close');
-
- if (closeBtn) {
- closeBtn.addEventListener('click', () => {
- closeConfigModal();
- });
- }
-
- if (copyBtn) {
- copyBtn.addEventListener('click', () => {
- const path = copyBtn.dataset.path;
- copyConfigContent(path);
- });
- }
-
- if (modalCloseBtn) {
- modalCloseBtn.addEventListener('click', () => {
- closeConfigModal();
- });
- }
-
- // 显示模态框
- setTimeout(() => modal.classList.add('show'), 10);
-}
-
-/**
- * 关闭配置模态框
- */
-function closeConfigModal() {
- const modal = document.querySelector('.config-view-modal');
- if (modal) {
- modal.classList.remove('show');
- setTimeout(() => modal.remove(), 300);
- }
-}
-
-/**
- * 复制配置内容
- * @param {string} path - 文件路径
- */
-async function copyConfigContent(path) {
- try {
- const fileData = await window.apiClient.get(`/upload-configs/view/${encodeURIComponent(path)}`);
- const textToCopy = fileData.content;
-
- // 优先使用 Clipboard API
- if (navigator.clipboard && navigator.clipboard.writeText) {
- try {
- await navigator.clipboard.writeText(textToCopy);
- showToast(t('common.success'), t('common.copy.success'), 'success');
- } catch (err) {
- console.error('Clipboard API failed:', err);
- fallbackCopyTextToClipboard(textToCopy);
- }
- } else {
- fallbackCopyTextToClipboard(textToCopy);
- }
- } catch (error) {
- console.error('复制失败:', error);
- showToast(t('common.error'), t('common.copy.failed') + ': ' + error.message, 'error');
- }
-}
-
-/**
- * HTML转义
- * @param {string} text - 要转义的文本
- * @returns {string} 转义后的文本
- */
-function escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
-}
-
-/**
- * 显示删除确认模态框
- * @param {Object} config - 配置数据
- */
-function showDeleteConfirmModal(config) {
- const isUsed = config.isUsed;
- const modalClass = isUsed ? 'delete-confirm-modal used' : 'delete-confirm-modal unused';
- const title = isUsed ? t('upload.delete.confirmTitleUsed') : t('upload.delete.confirmTitle');
- const icon = isUsed ? 'fas fa-exclamation-triangle' : 'fas fa-trash';
- const buttonClass = isUsed ? 'btn btn-danger' : 'btn btn-warning';
-
- const modal = document.createElement('div');
- modal.className = modalClass;
-
- modal.innerHTML = `
-
-
-
-
-
-
-
-
- ${isUsed ?
- `
${t('upload.delete.warningUsedTitle')}
${t('upload.delete.warningUsedDesc')}
` :
- `
${t('upload.delete.warningUnusedTitle')}
${t('upload.delete.warningUnusedDesc')}
`
- }
-
-
-
-
-
- 文件名:
- ${config.name}
-
-
- 文件路径:
- ${config.path}
-
-
- 文件大小:
- ${formatFileSize(config.size)}
-
-
- 关联状态:
-
- ${isUsed ? t('upload.statusFilter.used') : t('upload.statusFilter.unused')}
-
-
-
-
- ${isUsed ? `
-
-
-
-
-
-
${t('upload.delete.usageAlertTitle')}
-
${t('upload.delete.usageAlertDesc')}
-
- - ${t('upload.delete.usageAlertItem1')}
- - ${t('upload.delete.usageAlertItem2')}
- - ${t('upload.delete.usageAlertItem3')}
-
-
${t('upload.delete.usageAlertAdvice')}
-
-
- ` : ''}
-
-
-
- `;
-
- // 添加到页面
- document.body.appendChild(modal);
-
- // 添加事件监听器
- const closeBtn = modal.querySelector('.modal-close');
- const cancelBtn = modal.querySelector('.btn-cancel-delete');
- const confirmBtn = modal.querySelector('.btn-confirm-delete');
-
- const closeModal = () => {
- modal.classList.remove('show');
- setTimeout(() => modal.remove(), 300);
- };
-
- if (closeBtn) {
- closeBtn.addEventListener('click', closeModal);
- }
-
- if (cancelBtn) {
- cancelBtn.addEventListener('click', closeModal);
- }
-
- if (confirmBtn) {
- confirmBtn.addEventListener('click', () => {
- const path = confirmBtn.dataset.path;
- performDelete(path);
- closeModal();
- });
- }
-
- // 点击外部关闭
- modal.addEventListener('click', (e) => {
- if (e.target === modal) {
- closeModal();
- }
- });
-
- // ESC键关闭
- const handleEsc = (e) => {
- if (e.key === 'Escape') {
- closeModal();
- document.removeEventListener('keydown', handleEsc);
- }
- };
- document.addEventListener('keydown', handleEsc);
-
- // 显示模态框
- setTimeout(() => modal.classList.add('show'), 10);
-}
-
-/**
- * 执行删除操作
- * @param {string} path - 文件路径
- */
-async function performDelete(path) {
- try {
- const result = await window.apiClient.delete(`/upload-configs/delete/${encodeURIComponent(path)}`);
- showToast(t('common.success'), result.message, 'success');
-
- // 从本地列表中移除
- allConfigs = allConfigs.filter(c => c.path !== path);
- filteredConfigs = filteredConfigs.filter(c => c.path !== path);
- renderConfigList();
- updateStats();
- } catch (error) {
- console.error('删除配置失败:', error);
- showToast(t('common.error'), t('upload.action.delete.failed') + ': ' + error.message, 'error');
- }
-}
-
-/**
- * 删除配置
- * @param {string} path - 文件路径
- */
-async function deleteConfig(path) {
- const config = filteredConfigs.find(c => c.path === path) || allConfigs.find(c => c.path === path);
- if (!config) {
- showToast(t('common.error'), t('upload.config.notExist'), 'error');
- return;
- }
-
- // 显示删除确认模态框
- showDeleteConfirmModal(config);
-}
-
-/**
- * 初始化配置管理页面
- */
-function initUploadConfigManager() {
- // 绑定搜索事件
- const searchInput = document.getElementById('configSearch');
- const searchBtn = document.getElementById('searchConfigBtn');
- const statusFilter = document.getElementById('configStatusFilter');
- const providerFilter = document.getElementById('configProviderFilter');
- const refreshBtn = document.getElementById('refreshConfigList');
- const downloadAllBtn = document.getElementById('downloadAllConfigs');
-
- if (searchInput) {
- searchInput.addEventListener('input', debounce(() => {
- const searchTerm = searchInput.value.trim();
- const currentStatusFilter = statusFilter?.value || '';
- const currentProviderFilter = providerFilter?.value || '';
- searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter);
- }, 300));
- }
-
- if (searchBtn) {
- searchBtn.addEventListener('click', () => {
- const searchTerm = searchInput?.value.trim() || '';
- const currentStatusFilter = statusFilter?.value || '';
- const currentProviderFilter = providerFilter?.value || '';
- // 点击搜索按钮时,调接口刷新数据
- loadConfigList(searchTerm, currentStatusFilter, currentProviderFilter);
- });
- }
-
- if (statusFilter) {
- statusFilter.addEventListener('change', () => {
- const searchTerm = searchInput?.value.trim() || '';
- const currentStatusFilter = statusFilter.value;
- const currentProviderFilter = providerFilter?.value || '';
- searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter);
- });
- }
-
- if (providerFilter) {
- providerFilter.addEventListener('change', () => {
- const searchTerm = searchInput?.value.trim() || '';
- const currentStatusFilter = statusFilter?.value || '';
- const currentProviderFilter = providerFilter.value;
- searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter);
- });
- }
-
- if (refreshBtn) {
- refreshBtn.addEventListener('click', () => loadConfigList());
- }
-
- if (downloadAllBtn) {
- downloadAllBtn.addEventListener('click', downloadAllConfigs);
- }
-
- // 批量关联配置按钮
- const batchLinkBtn = document.getElementById('batchLinkKiroBtn') || document.getElementById('batchLinkProviderBtn');
- if (batchLinkBtn) {
- batchLinkBtn.addEventListener('click', batchLinkProviderConfigs);
- }
-
- // 删除未绑定配置按钮
- const deleteUnboundBtn = document.getElementById('deleteUnboundBtn');
- if (deleteUnboundBtn) {
- deleteUnboundBtn.addEventListener('click', deleteUnboundConfigs);
- }
-
- // 初始加载配置列表
- loadConfigList();
-}
-
-/**
- * 重新加载配置文件
- */
-async function reloadConfig() {
- // 防止重复重载
- if (isLoadingConfigs) {
- console.log('正在重载配置,跳过重复调用');
- return;
- }
-
- try {
- const result = await window.apiClient.post('/reload-config');
- showToast(t('common.success'), result.message, 'success');
-
- // 重新加载配置列表以反映最新的关联状态
- await loadConfigList();
-
- // 注意:不再发送 configReloaded 事件,避免重复调用
- // window.dispatchEvent(new CustomEvent('configReloaded', {
- // detail: result.details
- // }));
-
- } catch (error) {
- console.error('重载配置失败:', error);
- showToast(t('common.error'), t('common.refresh.failed') + ': ' + error.message, 'error');
- }
-}
-
-/**
- * 根据文件路径检测对应的提供商类型
- * @param {string} filePath - 文件路径
- * @returns {Object|null} 提供商信息对象或null
- */
-function detectProviderFromPath(filePath) {
- const normalizedPath = filePath.replace(/\\/g, '/').toLowerCase();
-
- // 定义目录到提供商的映射关系
- const providerMappings = [
- {
- patterns: ['configs/kiro/', '/kiro/'],
- providerType: 'claude-kiro-oauth',
- displayName: 'Claude Kiro OAuth',
- shortName: 'kiro-oauth'
- },
- {
- patterns: ['configs/gemini/', '/gemini/', 'configs/gemini-cli/'],
- providerType: 'gemini-cli-oauth',
- displayName: 'Gemini CLI OAuth',
- shortName: 'gemini-oauth'
- },
- {
- patterns: ['configs/qwen/', '/qwen/'],
- providerType: 'openai-qwen-oauth',
- displayName: 'Qwen OAuth',
- shortName: 'qwen-oauth'
- },
- {
- patterns: ['configs/antigravity/', '/antigravity/'],
- providerType: 'gemini-antigravity',
- displayName: 'Gemini Antigravity',
- shortName: 'antigravity'
- },
- {
- patterns: ['configs/codex/', '/codex/'],
- providerType: 'openai-codex-oauth',
- displayName: 'OpenAI Codex OAuth',
- shortName: 'codex-oauth'
- },
- {
- patterns: ['configs/iflow/', '/iflow/'],
- providerType: 'openai-iflow',
- displayName: 'OpenAI iFlow OAuth',
- shortName: 'iflow-oauth'
- }
- ];
-
- // 遍历映射关系,查找匹配的提供商
- for (const mapping of providerMappings) {
- for (const pattern of mapping.patterns) {
- if (normalizedPath.includes(pattern)) {
- return {
- providerType: mapping.providerType,
- displayName: mapping.displayName,
- shortName: mapping.shortName
- };
- }
- }
- }
-
- return null;
-}
-
-/**
- * 一键关联配置到对应的提供商
- * @param {string} filePath - 配置文件路径
- */
-async function quickLinkProviderConfig(filePath) {
- try {
- const providerInfo = detectProviderFromPath(filePath);
- if (!providerInfo) {
- showToast(t('common.error'), t('upload.link.failed.identify'), 'error');
- return;
- }
-
- showToast(t('common.info'), t('upload.link.processing', { name: providerInfo.displayName }), 'info');
-
- const result = await window.apiClient.post('/quick-link-provider', {
- filePath: filePath
- });
-
- showToast(t('common.success'), result.message || t('upload.link.success'), 'success');
-
- // 刷新配置列表
- await loadConfigList();
- } catch (error) {
- console.error('一键关联失败:', error);
- showToast(t('common.error'), t('upload.link.failed') + ': ' + error.message, 'error');
- }
-}
-
-/**
- * 批量关联所有支持的提供商目录下的未关联配置
- */
-async function batchLinkProviderConfigs() {
- // 筛选出所有支持的提供商目录下的未关联配置
- const unlinkedConfigs = allConfigs.filter(config => {
- if (config.isUsed) return false;
- const providerInfo = detectProviderFromPath(config.path);
- return providerInfo !== null;
- });
-
- if (unlinkedConfigs.length === 0) {
- showToast(t('common.info'), t('upload.batchLink.none'), 'info');
- return;
- }
-
- // 按提供商类型分组统计
- const groupedByProvider = {};
- unlinkedConfigs.forEach(config => {
- const providerInfo = detectProviderFromPath(config.path);
- if (providerInfo) {
- if (!groupedByProvider[providerInfo.displayName]) {
- groupedByProvider[providerInfo.displayName] = 0;
- }
- groupedByProvider[providerInfo.displayName]++;
- }
- });
-
- const providerSummary = Object.entries(groupedByProvider)
- .map(([name, count]) => `${name}: ${count}个`)
- .join(', ');
-
- const confirmMsg = t('upload.batchLink.confirm', { count: unlinkedConfigs.length, summary: providerSummary });
- if (!confirm(confirmMsg)) {
- return;
- }
-
- showToast(t('common.info'), t('upload.batchLink.processing', { count: unlinkedConfigs.length }), 'info');
-
- try {
- // 一次性传递所有文件路径进行批量关联
- const filePaths = unlinkedConfigs.map(config => config.path);
- const result = await window.apiClient.post('/quick-link-provider', {
- filePaths: filePaths
- });
-
- // 刷新配置列表
- await loadConfigList();
-
- if (result.failCount === 0) {
- showToast(t('common.success'), t('upload.batchLink.success', { count: result.successCount }), 'success');
- } else {
- showToast(t('common.warning'), t('upload.batchLink.partial', { success: result.successCount, fail: result.failCount }), 'warning');
- }
- } catch (error) {
- console.error('批量关联失败:', error);
- showToast(t('common.error'), t('upload.batchLink.failed') + ': ' + error.message, 'error');
-
- // 即使失败也刷新列表,可能部分成功
- await loadConfigList();
- }
-}
-
-/**
- * 删除所有未绑定的配置文件
- * 只删除 configs/xxx/ 子目录下的未绑定配置文件
- */
-async function deleteUnboundConfigs() {
- // 统计未绑定的配置数量,并且必须在 configs/xxx/ 子目录下
- const unboundConfigs = allConfigs.filter(config => {
- if (config.isUsed) return false;
-
- // 检查路径是否在 configs/xxx/ 子目录下
- const normalizedPath = config.path.replace(/\\/g, '/');
- const pathParts = normalizedPath.split('/');
-
- // 路径至少需要3部分:configs/子目录/文件名
- // 例如:configs/kiro/xxx.json 或 configs/gemini/xxx.json
- if (pathParts.length >= 3 && pathParts[0] === 'configs') {
- return true;
- }
-
- return false;
- });
-
- if (unboundConfigs.length === 0) {
- showToast(t('common.info'), t('upload.deleteUnbound.none'), 'info');
- return;
- }
-
- // 显示确认对话框
- const confirmMsg = t('upload.deleteUnbound.confirm', { count: unboundConfigs.length });
- if (!confirm(confirmMsg)) {
- return;
- }
-
- try {
- showToast(t('common.info'), t('upload.deleteUnbound.processing'), 'info');
-
- const result = await window.apiClient.delete('/upload-configs/delete-unbound');
-
- if (result.deletedCount > 0) {
- showToast(t('common.success'), t('upload.deleteUnbound.success', { count: result.deletedCount }), 'success');
-
- // 刷新配置列表
- await loadConfigList();
- } else {
- showToast(t('common.info'), t('upload.deleteUnbound.none'), 'info');
- }
-
- // 如果有失败的文件,显示警告
- if (result.failedCount > 0) {
- console.warn('部分文件删除失败:', result.failedFiles);
- showToast(t('common.warning'), t('upload.deleteUnbound.partial', {
- success: result.deletedCount,
- fail: result.failedCount
- }), 'warning');
- }
- } catch (error) {
- console.error('删除未绑定配置失败:', error);
- showToast(t('common.error'), t('upload.deleteUnbound.failed') + ': ' + error.message, 'error');
- }
-}
-
-/**
- * 防抖函数
- * @param {Function} func - 要防抖的函数
- * @param {number} wait - 等待时间(毫秒)
- * @returns {Function} 防抖后的函数
- */
-function debounce(func, wait) {
- let timeout;
- return function executedFunction(...args) {
- const later = () => {
- clearTimeout(timeout);
- func(...args);
- };
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- };
-}
-
-/**
- * 打包下载所有配置文件
- */
-async function downloadAllConfigs() {
- try {
- showToast(t('common.info'), t('common.loading'), 'info');
-
- // 使用 window.apiClient.get 获取 Blob 数据
- // 由于 apiClient 默认可能是处理 JSON 的,我们需要直接调用 fetch 或者确保 apiClient 支持返回原始响应
- const token = localStorage.getItem('authToken');
- const headers = {
- 'Authorization': token ? `Bearer ${token}` : ''
- };
-
- const response = await fetch('/api/upload-configs/download-all', { headers });
-
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.error?.message || '下载失败');
- }
-
- const blob = await response.blob();
- const url = window.URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
-
- // 从 Content-Disposition 中提取文件名,或者使用默认名
- const contentDisposition = response.headers.get('Content-Disposition');
- let filename = `configs_backup_${new Date().toISOString().slice(0, 10)}.zip`;
- if (contentDisposition && contentDisposition.indexOf('filename=') !== -1) {
- const matches = /filename="([^"]+)"/.exec(contentDisposition);
- if (matches && matches[1]) filename = matches[1];
- }
-
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- window.URL.revokeObjectURL(url);
- document.body.removeChild(a);
-
- showToast(t('common.success'), t('common.success'), 'success');
- } catch (error) {
- console.error('打包下载失败:', error);
- showToast(t('common.error'), t('common.error') + ': ' + error.message, 'error');
- }
-}
-
-/**
- * 动态更新提供商筛选下拉框选项
- * @param {Array} providerConfigs - 提供商配置列表
- */
-function updateProviderFilterOptions(providerConfigs) {
- const filterSelect = document.getElementById('configProviderFilter');
- if (!filterSelect) return;
-
- // 保存当前选中的值
- const currentValue = filterSelect.value;
-
- // 清空现有选项(保留第一个"全部提供商")
- const firstOption = filterSelect.options[0];
- filterSelect.innerHTML = '';
- if (firstOption) {
- filterSelect.appendChild(firstOption);
- } else {
- const option = document.createElement('option');
- option.value = '';
- option.setAttribute('data-i18n', 'upload.providerFilter.all');
- option.textContent = t('upload.providerFilter.all');
- filterSelect.appendChild(option);
- }
-
- // 添加动态选项
- providerConfigs.forEach(config => {
- // 根据是否有 defaultPath 来过滤,这意味着该提供商支持 OAuth 凭据文件管理
- if (config.visible !== false && config.defaultPath) {
- const option = document.createElement('option');
- option.value = config.id;
- option.textContent = config.name;
- filterSelect.appendChild(option);
- }
- });
-
- // 添加"其他"选项
- const otherOption = document.createElement('option');
- otherOption.value = 'other';
- otherOption.setAttribute('data-i18n', 'upload.providerFilter.other');
- otherOption.textContent = t('upload.providerFilter.other');
- filterSelect.appendChild(otherOption);
-
- // 恢复选中的值(如果还存在)
- filterSelect.value = currentValue;
-}
-
-// 导出函数
-export {
- initUploadConfigManager,
- searchConfigs,
- loadConfigList,
- viewConfig,
- deleteConfig,
- closeConfigModal,
- copyConfigContent,
- reloadConfig,
- deleteUnboundConfigs,
- updateProviderFilterOptions
-};
diff --git a/static/app/usage-manager.js b/static/app/usage-manager.js
deleted file mode 100644
index 62c2b20db931b687f664745bd5d252b8cff9a92e..0000000000000000000000000000000000000000
--- a/static/app/usage-manager.js
+++ /dev/null
@@ -1,999 +0,0 @@
-// 用量管理模块
-
-import { showToast } from './utils.js';
-import { getAuthHeaders } from './auth.js';
-import { t, getCurrentLanguage } from './i18n.js';
-
-/**
- * 不支持显示用量数据的提供商列表
- * 这些提供商只显示模型名称和重置时间,不显示用量数字和进度条
- */
-const PROVIDERS_WITHOUT_USAGE_DISPLAY = [
- 'gemini-antigravity'
-];
-
-// 提供商配置缓存
-let currentProviderConfigs = null;
-
-/**
- * 更新提供商配置
- * @param {Array} configs - 提供商配置列表
- */
-export function updateUsageProviderConfigs(configs) {
- currentProviderConfigs = configs;
- // 重新触发列表加载,以应用最新的可见性过滤、名称和图标
- loadSupportedProviders();
- loadUsage();
-}
-
-/**
- * 检查提供商是否支持显示用量
- * @param {string} providerType - 提供商类型
- * @returns {boolean} 是否支持显示用量
- */
-function shouldShowUsage(providerType) {
- return !PROVIDERS_WITHOUT_USAGE_DISPLAY.includes(providerType);
-}
-
-/**
- * 初始化用量管理功能
- */
-export function initUsageManager() {
- const refreshBtn = document.getElementById('refreshUsageBtn');
- if (refreshBtn) {
- refreshBtn.addEventListener('click', refreshUsage);
- }
-
- // 初始化时自动加载缓存数据
- loadUsage();
- loadSupportedProviders();
-}
-
-/**
- * 加载支持用量查询的提供商列表
- */
-async function loadSupportedProviders() {
- const listEl = document.getElementById('supportedProvidersList');
- if (!listEl) return;
-
- try {
- const response = await fetch('/api/usage/supported-providers', {
- method: 'GET',
- headers: getAuthHeaders()
- });
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
-
- const providers = await response.json();
-
- listEl.innerHTML = '';
-
- // 按照 currentProviderConfigs 的顺序渲染,确保顺序一致性
- const displayOrder = currentProviderConfigs
- ? currentProviderConfigs.map(c => c.id)
- : providers;
-
- displayOrder.forEach(providerId => {
- // 必须是后端支持且前端配置可见的提供商
- const isSupported = providers.includes(providerId);
- if (!isSupported) return;
-
- if (currentProviderConfigs) {
- const config = currentProviderConfigs.find(c => c.id === providerId);
- if (config && config.visible === false) return;
- }
-
- const tag = document.createElement('span');
- tag.className = 'provider-tag';
- tag.textContent = getProviderDisplayName(providerId);
- tag.title = t('usage.doubleClickToRefresh') || '双击刷新该提供商用量';
- tag.setAttribute('data-i18n-title', 'usage.doubleClickToRefresh');
-
- // 添加双击事件
- tag.addEventListener('dblclick', () => {
- refreshProviderUsage(providerId);
- });
-
- listEl.appendChild(tag);
- });
- } catch (error) {
- console.error('获取支持的提供商列表失败:', error);
- listEl.innerHTML = `${t('usage.failedToLoad')}`;
- }
-}
-
-/**
- * 加载用量数据(优先从缓存读取)
- */
-export async function loadUsage() {
- const loadingEl = document.getElementById('usageLoading');
- const errorEl = document.getElementById('usageError');
- const contentEl = document.getElementById('usageContent');
- const emptyEl = document.getElementById('usageEmpty');
- const lastUpdateEl = document.getElementById('usageLastUpdate');
-
- // 显示加载状态
- if (loadingEl) loadingEl.style.display = 'block';
- if (errorEl) errorEl.style.display = 'none';
- if (emptyEl) emptyEl.style.display = 'none';
-
- try {
- // 不带 refresh 参数,优先读取缓存
- const response = await fetch('/api/usage', {
- method: 'GET',
- headers: getAuthHeaders()
- });
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- }
-
- const data = await response.json();
-
- // 隐藏加载状态
- if (loadingEl) loadingEl.style.display = 'none';
-
- // 渲染用量数据
- renderUsageData(data, contentEl);
-
- // 更新服务端系统时间
- if (data.serverTime) {
- const serverTimeEl = document.getElementById('serverTimeValue');
- if (serverTimeEl) {
- serverTimeEl.textContent = new Date(data.serverTime).toLocaleString(getCurrentLanguage());
- }
- }
-
- // 更新最后更新时间
- if (lastUpdateEl) {
- const timeStr = new Date(data.timestamp || Date.now()).toLocaleString(getCurrentLanguage());
- if (data.fromCache && data.timestamp) {
- lastUpdateEl.textContent = t('usage.lastUpdateCache', { time: timeStr });
- lastUpdateEl.setAttribute('data-i18n', 'usage.lastUpdateCache');
- lastUpdateEl.setAttribute('data-i18n-params', JSON.stringify({ time: timeStr }));
- } else {
- lastUpdateEl.textContent = t('usage.lastUpdate', { time: timeStr });
- lastUpdateEl.setAttribute('data-i18n', 'usage.lastUpdate');
- lastUpdateEl.setAttribute('data-i18n-params', JSON.stringify({ time: timeStr }));
- }
- }
- } catch (error) {
- console.error('获取用量数据失败:', error);
-
- if (loadingEl) loadingEl.style.display = 'none';
- if (errorEl) {
- errorEl.style.display = 'block';
- const errorMsgEl = document.getElementById('usageErrorMessage');
- if (errorMsgEl) {
- errorMsgEl.textContent = error.message || (t('usage.title') + t('common.refresh.failed'));
- }
- }
- }
-}
-
-/**
- * 刷新用量数据(强制从服务器获取最新数据)
- */
-export async function refreshUsage() {
- const loadingEl = document.getElementById('usageLoading');
- const errorEl = document.getElementById('usageError');
- const contentEl = document.getElementById('usageContent');
- const emptyEl = document.getElementById('usageEmpty');
- const lastUpdateEl = document.getElementById('usageLastUpdate');
- const refreshBtn = document.getElementById('refreshUsageBtn');
-
- // 显示加载状态
- if (loadingEl) loadingEl.style.display = 'block';
- if (errorEl) errorEl.style.display = 'none';
- if (emptyEl) emptyEl.style.display = 'none';
- if (refreshBtn) refreshBtn.disabled = true;
-
- try {
- // 带 refresh=true 参数,强制刷新
- const response = await fetch('/api/usage?refresh=true', {
- method: 'GET',
- headers: getAuthHeaders()
- });
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- }
-
- const data = await response.json();
-
- // 隐藏加载状态
- if (loadingEl) loadingEl.style.display = 'none';
-
- // 渲染用量数据
- renderUsageData(data, contentEl);
-
- // 更新服务端系统时间
- if (data.serverTime) {
- const serverTimeEl = document.getElementById('serverTimeValue');
- if (serverTimeEl) {
- serverTimeEl.textContent = new Date(data.serverTime).toLocaleString(getCurrentLanguage());
- }
- }
-
- // 更新最后更新时间
- if (lastUpdateEl) {
- const timeStr = new Date().toLocaleString(getCurrentLanguage());
- lastUpdateEl.textContent = t('usage.lastUpdate', { time: timeStr });
- lastUpdateEl.setAttribute('data-i18n', 'usage.lastUpdate');
- lastUpdateEl.setAttribute('data-i18n-params', JSON.stringify({ time: timeStr }));
- }
-
- showToast(t('common.success'), t('common.refresh.success'), 'success');
- } catch (error) {
- console.error('获取用量数据失败:', error);
-
- if (loadingEl) loadingEl.style.display = 'none';
- if (errorEl) {
- errorEl.style.display = 'block';
- const errorMsgEl = document.getElementById('usageErrorMessage');
- if (errorMsgEl) {
- errorMsgEl.textContent = error.message || (t('usage.title') + t('common.refresh.failed'));
- }
- }
-
- showToast(t('common.error'), t('common.refresh.failed') + ': ' + error.message, 'error');
- } finally {
- if (refreshBtn) refreshBtn.disabled = false;
- }
-}
-
-/**
- * 渲染用量数据
- * @param {Object} data - 用量数据
- * @param {HTMLElement} container - 容器元素
- */
-function renderUsageData(data, container) {
- if (!container) return;
-
- // 清空容器
- container.innerHTML = '';
-
- if (!data || !data.providers || Object.keys(data.providers).length === 0) {
- container.innerHTML = `
-
-
-
${t('usage.noData')}
-
- `;
- return;
- }
-
- // 按提供商分组收集已初始化且未禁用的实例
- const groupedInstances = {};
-
- for (const [providerType, providerData] of Object.entries(data.providers)) {
- // 如果配置了不可见,则跳过
- if (currentProviderConfigs) {
- const config = currentProviderConfigs.find(c => c.id === providerType);
- if (config && config.visible === false) continue;
- }
-
- if (providerData.instances && providerData.instances.length > 0) {
- const validInstances = [];
- for (const instance of providerData.instances) {
- // 过滤掉服务实例未初始化的
- if (instance.error === '服务实例未初始化' || instance.error === 'Service instance not initialized') {
- continue;
- }
- // 过滤掉已禁用的提供商
- if (instance.isDisabled) {
- continue;
- }
- validInstances.push(instance);
- }
- if (validInstances.length > 0) {
- groupedInstances[providerType] = validInstances;
- }
- }
- }
-
- if (Object.keys(groupedInstances).length === 0) {
- container.innerHTML = `
-
-
-
${t('usage.noInstances')}
-
- `;
- return;
- }
-
- // 按提供商分组渲染,使用统一的显示顺序
- const displayOrder = currentProviderConfigs
- ? currentProviderConfigs.map(c => c.id)
- : Object.keys(groupedInstances);
-
- displayOrder.forEach(providerType => {
- const instances = groupedInstances[providerType];
- if (instances && instances.length > 0) {
- const groupContainer = createProviderGroup(providerType, instances);
- container.appendChild(groupContainer);
- }
- });
-}
-
-/**
- * 刷新特定提供商类型的用量数据
- * @param {string} providerType - 提供商类型
- */
-export async function refreshProviderUsage(providerType) {
- const loadingEl = document.getElementById('usageLoading');
- const refreshBtn = document.getElementById('refreshUsageBtn');
- const contentEl = document.getElementById('usageContent');
-
- // 显示加载状态
- if (loadingEl) loadingEl.style.display = 'block';
- if (refreshBtn) refreshBtn.disabled = true;
-
- try {
- const providerName = getProviderDisplayName(providerType);
- showToast(t('common.info'), t('usage.refreshingProvider', { name: providerName }), 'info');
-
- // 调用按提供商刷新的 API
- const response = await fetch(`/api/usage/${providerType}?refresh=true`, {
- method: 'GET',
- headers: getAuthHeaders()
- });
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- }
-
- const providerData = await response.json();
-
- // 获取当前完整数据并更新其中一个提供商的数据
- // 注意:这里为了保持页面一致性,我们重新获取一次完整数据(走缓存)来重新渲染
- // 或者手动在当前 DOM 中更新该提供商的部分。
- // 为了简单可靠,我们重新 loadUsage(),它会读取刚刚更新过的后端缓存
- await loadUsage();
-
- showToast(t('common.success'), t('common.refresh.success'), 'success');
- } catch (error) {
- console.error(`刷新提供商 ${providerType} 失败:`, error);
- showToast(t('common.error'), t('common.refresh.failed') + ': ' + error.message, 'error');
- } finally {
- if (loadingEl) loadingEl.style.display = 'none';
- if (refreshBtn) refreshBtn.disabled = false;
- }
-}
-
-/**
- * 创建提供商分组容器
- * @param {string} providerType - 提供商类型
- * @param {Array} instances - 实例数组
- * @returns {HTMLElement} 分组容器元素
- */
-function createProviderGroup(providerType, instances) {
- const groupContainer = document.createElement('div');
- groupContainer.className = 'usage-provider-group collapsed';
-
- const providerDisplayName = getProviderDisplayName(providerType);
- const providerIcon = getProviderIcon(providerType);
- const instanceCount = instances.length;
- const successCount = instances.filter(i => i.success).length;
-
- // 分组头部(可点击折叠)
- const header = document.createElement('div');
- header.className = 'usage-group-header';
- header.innerHTML = `
-
-
-
- ${providerDisplayName}
- ${t('usage.group.instances', { count: instanceCount })}
- ${t('usage.group.success', { count: successCount, total: instanceCount })}
-
-
-
-
- `;
-
- // 点击头部切换分组折叠状态
- const titleDiv = header.querySelector('.usage-group-title');
- titleDiv.addEventListener('click', () => {
- groupContainer.classList.toggle('collapsed');
- });
-
- groupContainer.appendChild(header);
-
- // 展开/折叠所有卡片按钮事件
- const toggleCardsBtn = header.querySelector('.btn-toggle-cards');
- toggleCardsBtn.addEventListener('click', (e) => {
- e.stopPropagation(); // 阻止事件冒泡到分组头部
-
- const cards = groupContainer.querySelectorAll('.usage-instance-card');
- const allCollapsed = Array.from(cards).every(card => card.classList.contains('collapsed'));
-
- // 如果全部折叠,则全部展开;否则全部折叠
- cards.forEach(card => {
- if (allCollapsed) {
- card.classList.remove('collapsed');
- } else {
- card.classList.add('collapsed');
- }
- });
-
- // 更新按钮图标和提示文本
- const icon = toggleCardsBtn.querySelector('i');
- if (allCollapsed) {
- icon.className = 'fas fa-compress-alt';
- toggleCardsBtn.title = t('usage.group.collapseAll');
- } else {
- icon.className = 'fas fa-expand-alt';
- toggleCardsBtn.title = t('usage.group.expandAll');
- }
- });
-
- // 分组内容(卡片网格)
- const content = document.createElement('div');
- content.className = 'usage-group-content';
-
- const gridContainer = document.createElement('div');
- gridContainer.className = 'usage-cards-grid';
-
- for (const instance of instances) {
- const instanceCard = createInstanceUsageCard(instance, providerType);
- gridContainer.appendChild(instanceCard);
- }
-
- content.appendChild(gridContainer);
- groupContainer.appendChild(content);
-
- return groupContainer;
-}
-
-/**
- * 创建实例用量卡片
- * @param {Object} instance - 实例数据
- * @param {string} providerType - 提供商类型
- * @returns {HTMLElement} 卡片元素
- */
-function createInstanceUsageCard(instance, providerType) {
- const card = document.createElement('div');
- card.className = `usage-instance-card ${instance.success ? 'success' : 'error'} collapsed`;
-
- const providerDisplayName = getProviderDisplayName(providerType);
- const providerIcon = getProviderIcon(providerType);
-
- // 检查是否应该显示用量信息
- const showUsage = shouldShowUsage(providerType);
-
- // 计算总用量(用于折叠摘要显示)
- const totalUsage = instance.usage ? calculateTotalUsage(instance.usage.usageBreakdown) : { hasData: false, percent: 0 };
- const progressClass = totalUsage.percent >= 90 ? 'danger' : (totalUsage.percent >= 70 ? 'warning' : 'normal');
-
- // 折叠摘要 - 两行显示
- const collapsedSummary = document.createElement('div');
- collapsedSummary.className = 'usage-card-collapsed-summary';
-
- const statusIcon = instance.success
- ? ''
- : '';
-
- // 显示名称:优先自定义名称,其次 uuid
- const displayName = instance.name || instance.uuid;
-
- const displayUsageText = totalUsage.isCodex
- ? `${totalUsage.percent.toFixed(1)}%`
- : `${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)}`;
-
- collapsedSummary.innerHTML = `
-
-
- ${displayName}
- ${statusIcon}
-
- ${showUsage ? `
-
- ${totalUsage.hasData ? `
-
-
${totalUsage.percent.toFixed(1)}%
-
${displayUsageText}
- ` : (instance.error ? `
${t('common.error')}` : '')}
-
- ` : ''}
- `;
-
- // 点击折叠摘要切换展开状态
- collapsedSummary.addEventListener('click', (e) => {
- e.stopPropagation();
- card.classList.toggle('collapsed');
- });
-
- card.appendChild(collapsedSummary);
-
- // 展开内容区域
- const expandedContent = document.createElement('div');
- expandedContent.className = 'usage-card-expanded-content';
-
- // 实例头部 - 整合用户信息
- const header = document.createElement('div');
- header.className = 'usage-instance-header';
-
- const healthBadge = instance.isDisabled
- ? `${t('usage.card.status.disabled')}`
- : (instance.isHealthy
- ? `${t('usage.card.status.healthy')}`
- : `${t('usage.card.status.unhealthy')}`);
-
- // 下载按钮
- const downloadBtnHTML = instance.configFilePath ? `
-
- ` : '';
-
- // 获取用户邮箱和订阅信息
- const userEmail = instance.usage?.user?.email || '';
- const subscriptionTitle = instance.usage?.subscription?.title || '';
-
- // 用户信息行
- const userInfoHTML = userEmail ? `
-
- ${userEmail}
- ${subscriptionTitle ? `${subscriptionTitle}` : ''}
-
- ` : '';
-
- header.innerHTML = `
-
-
- ${instance.name || instance.uuid}
-
- ${userInfoHTML}
- `;
-
- // 添加下载按钮点击事件
- if (instance.configFilePath) {
- const downloadBtn = header.querySelector('.btn-download-config');
- if (downloadBtn) {
- downloadBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- downloadConfigFile(instance.configFilePath);
- });
- }
- }
-
- expandedContent.appendChild(header);
-
- // 实例内容 - 只显示用量和到期时间
- const content = document.createElement('div');
- content.className = 'usage-instance-content';
-
- if (instance.error) {
- content.innerHTML = `
-
-
- ${instance.error}
-
- `;
- } else if (instance.usage) {
- content.appendChild(renderUsageDetails(instance.usage, providerType));
- }
-
- expandedContent.appendChild(content);
- card.appendChild(expandedContent);
-
- return card;
-}
-
-/**
- * 渲染用量详情 - 显示总用量、用量明细和到期时间
- * @param {Object} usage - 用量数据
- * @param {string} providerType - 提供商类型
- * @returns {HTMLElement} 详情元素
- */
-function renderUsageDetails(usage, providerType) {
- const container = document.createElement('div');
- container.className = 'usage-details';
-
- // 检查是否应该显示用量信息
- const showUsage = shouldShowUsage(providerType);
-
- // 计算总用量
- const totalUsage = calculateTotalUsage(usage.usageBreakdown);
-
- // 总用量进度条(不支持显示用量的提供商不显示)
- if (totalUsage.hasData && showUsage) {
- const totalSection = document.createElement('div');
- totalSection.className = 'usage-section total-usage';
-
- const progressClass = totalUsage.percent >= 90 ? 'danger' : (totalUsage.percent >= 70 ? 'warning' : 'normal');
-
- // 提取第一个有重置时间的条目(通常是总配额)
- let resetTimeHTML = '';
- if (totalUsage.isCodex && totalUsage.resetAfterSeconds !== undefined) {
- const resetTimeText = formatTimeRemaining(totalUsage.resetAfterSeconds);
- resetTimeHTML = `
-
- ${t('usage.resetInfo', { time: resetTimeText })}
-
- `;
- } else {
- const resetTimeEntry = usage.usageBreakdown.find(b => b.resetTime && b.resetTime !== '--');
- if (resetTimeEntry) {
- const formattedResetTime = formatDate(resetTimeEntry.resetTime);
- resetTimeHTML = `
-
- ${t('usage.card.resetAt', { time: formattedResetTime })}
-
- `;
- }
- }
-
- const displayValue = totalUsage.isCodex
- ? `${totalUsage.percent.toFixed(1)}%`
- : `${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)}`;
-
- totalSection.innerHTML = `
-
-
-
- `;
-
- container.appendChild(totalSection);
- }
-
- // 用量明细(包含免费试用和奖励信息)
- if (usage.usageBreakdown && usage.usageBreakdown.length > 0) {
- const breakdownSection = document.createElement('div');
- breakdownSection.className = 'usage-section usage-breakdown-compact';
-
- let breakdownHTML = '';
-
- for (const breakdown of usage.usageBreakdown) {
- breakdownHTML += createUsageBreakdownHTML(breakdown, providerType);
- }
-
- breakdownSection.innerHTML = breakdownHTML;
- container.appendChild(breakdownSection);
- }
-
- return container;
-}
-
-/**
- * 创建用量明细 HTML(紧凑版)
- * @param {Object} breakdown - 用量明细数据
- * @param {string} providerType - 提供商类型
- * @returns {string} HTML 字符串
- */
-function createUsageBreakdownHTML(breakdown, providerType) {
- // 特殊处理 Codex
- if (breakdown.rateLimit && breakdown.rateLimit.primary_window) {
- return createCodexUsageBreakdownHTML(breakdown);
- }
-
- // 检查是否应该显示用量信息
- const showUsage = shouldShowUsage(providerType);
-
- const usagePercent = breakdown.usageLimit > 0
- ? Math.min(100, (breakdown.currentUsage / breakdown.usageLimit) * 100)
- : 0;
-
- const progressClass = usagePercent >= 90 ? 'danger' : (usagePercent >= 70 ? 'warning' : 'normal');
-
- let html = `
-
-
- ${showUsage ? `
-
- ` : ''}
- `;
-
- // 如果有重置时间,则显示
- if (breakdown.resetTime && breakdown.resetTime !== '--') {
- const formattedResetTime = formatDate(breakdown.resetTime);
- const resetText = t('usage.card.resetAt', { time: formattedResetTime });
- html += `
-
- `;
- }
-
- // 免费试用信息
- if (breakdown.freeTrial && breakdown.freeTrial.status === 'ACTIVE') {
- html += `
-
- `;
- }
-
- // 奖励信息
- if (breakdown.bonuses && breakdown.bonuses.length > 0) {
- for (const bonus of breakdown.bonuses) {
- if (bonus.status === 'ACTIVE') {
- html += `
-
- `;
- }
- }
- }
-
- html += '
';
- return html;
-}
-
-/**
- * 创建 Codex 专用的用量明细 HTML
- * @param {Object} breakdown - 包含 rateLimit 的用量明细
- * @returns {string} HTML 字符串
- */
-function createCodexUsageBreakdownHTML(breakdown) {
- const rl = breakdown.rateLimit;
- const secondary = rl.secondary_window;
-
- if (!secondary) return '';
-
- const secondaryPercent = secondary.used_percent || 0;
- const secondaryProgressClass = secondaryPercent >= 90 ? 'danger' : (secondaryPercent >= 70 ? 'warning' : 'normal');
- const secondaryResetText = formatTimeRemaining(secondary.reset_after_seconds);
-
- return `
-
-
-
-
- ${t('usage.resetInfo', { time: secondaryResetText })}
-
-
- `;
-}
-
-/**
- * 格式化剩余时间
- * @param {number} seconds - 秒数
- * @returns {string} 格式化后的时间
- */
-function formatTimeRemaining(seconds) {
- if (seconds <= 0) return t('usage.time.soon');
-
- const days = Math.floor(seconds / 86400);
- const hours = Math.floor((seconds % 86400) / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
-
- if (days > 0) return t('usage.time.days', { days, hours });
- if (hours > 0) return t('usage.time.hours', { hours, minutes });
- return t('usage.time.minutes', { minutes });
-}
-
-/**
- * 计算总用量(包含基础用量、免费试用和奖励)
- * @param {Array} usageBreakdown - 用量明细数组
- * @returns {Object} 总用量信息
- */
-function calculateTotalUsage(usageBreakdown) {
- if (!usageBreakdown || usageBreakdown.length === 0) {
- return { hasData: false, used: 0, limit: 0, percent: 0 };
- }
-
- // 特殊处理 Codex
- const codexEntry = usageBreakdown.find(b => b.rateLimit && b.rateLimit.secondary_window);
- if (codexEntry) {
- const secondary = codexEntry.rateLimit.secondary_window;
- const secondaryPercent = secondary.used_percent || 0;
-
- // 只有当周限制达到 100% 时,总用量才显示 100%
- // 否则按正常逻辑计算(或者这里可以理解为非 100% 时不改变原有的总用量逻辑,
- // 但根据用户反馈,Codex 应该主要关注周限制)
- // 重新审视需求:达到周限制时,总用量直接100%,重置时间设置为周限制时间
-
- if (secondaryPercent >= 100) {
- return {
- hasData: true,
- used: 100,
- limit: 100,
- percent: 100,
- isCodex: true,
- resetAfterSeconds: secondary.reset_after_seconds
- };
- }
- // 如果未达到 100%,则继续执行下面的常规计算逻辑
- }
-
- let totalUsed = 0;
- let totalLimit = 0;
-
- for (const breakdown of usageBreakdown) {
- // 基础用量
- totalUsed += breakdown.currentUsage || 0;
- totalLimit += breakdown.usageLimit || 0;
-
- // 免费试用用量
- if (breakdown.freeTrial && breakdown.freeTrial.status === 'ACTIVE') {
- totalUsed += breakdown.freeTrial.currentUsage || 0;
- totalLimit += breakdown.freeTrial.usageLimit || 0;
- }
-
- // 奖励用量
- if (breakdown.bonuses && breakdown.bonuses.length > 0) {
- for (const bonus of breakdown.bonuses) {
- if (bonus.status === 'ACTIVE') {
- totalUsed += bonus.currentUsage || 0;
- totalLimit += bonus.usageLimit || 0;
- }
- }
- }
- }
-
- const percent = totalLimit > 0 ? Math.min(100, (totalUsed / totalLimit) * 100) : 0;
-
- return {
- hasData: true,
- used: totalUsed,
- limit: totalLimit,
- percent: percent
- };
-}
-
-/**
- * 获取提供商显示名称
- * @param {string} providerType - 提供商类型
- * @returns {string} 显示名称
- */
-function getProviderDisplayName(providerType) {
- // 优先从外部传入的配置中获取名称
- if (currentProviderConfigs) {
- const config = currentProviderConfigs.find(c => c.id === providerType);
- if (config && config.name) {
- return config.name;
- }
- }
-
- const names = {
- 'claude-kiro-oauth': 'Claude Kiro OAuth',
- 'gemini-cli-oauth': 'Gemini CLI OAuth',
- 'gemini-antigravity': 'Gemini Antigravity',
- 'openai-codex-oauth': 'Codex OAuth',
- 'openai-qwen-oauth': 'Qwen OAuth',
- 'grok-custom': 'Grok Reverse'
- };
- return names[providerType] || providerType;
-}
-
-/**
- * 获取提供商图标
- * @param {string} providerType - 提供商类型
- * @returns {string} 图标类名
- */
-function getProviderIcon(providerType) {
- // 优先从外部传入的配置中获取图标
- if (currentProviderConfigs) {
- const config = currentProviderConfigs.find(c => c.id === providerType);
- if (config && config.icon) {
- // 如果 icon 已经包含 fa- 则直接使用,否则加上 fas
- return config.icon.startsWith('fa-') ? `fas ${config.icon}` : config.icon;
- }
- }
-
- const icons = {
- 'claude-kiro-oauth': 'fas fa-robot',
- 'gemini-cli-oauth': 'fas fa-gem',
- 'gemini-antigravity': 'fas fa-rocket',
- 'openai-codex-oauth': 'fas fa-terminal',
- 'openai-qwen-oauth': 'fas fa-code',
- 'grok-custom': 'fas fa-brain'
- };
- return icons[providerType] || 'fas fa-server';
-}
-
-
-/**
- * 下载配置文件
- * @param {string} filePath - 文件路径
- */
-async function downloadConfigFile(filePath) {
- if (!filePath) return;
-
- try {
- const fileName = filePath.split(/[/\\]/).pop();
- const response = await fetch(`/api/upload-configs/download/${encodeURIComponent(filePath)}`, {
- method: 'GET',
- headers: getAuthHeaders()
- });
-
- 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;
- document.body.appendChild(a);
- a.click();
- window.URL.revokeObjectURL(url);
- document.body.removeChild(a);
-
- showToast(t('common.success'), t('usage.card.downloadSuccess') || '文件下载成功', 'success');
- } catch (error) {
- console.error('下载配置文件失败:', error);
- showToast(t('common.error'), (t('usage.card.downloadFailed') || '下载配置文件失败') + ': ' + error.message, 'error');
- }
-}
-
-/**
- * 格式化数字(向上取整保留两位小数)
- * @param {number} num - 数字
- * @returns {string} 格式化后的数字
- */
-function formatNumber(num) {
- if (num === null || num === undefined) return '0.00';
- // 向上取整到两位小数
- const rounded = Math.ceil(num * 100) / 100;
- return rounded.toFixed(2);
-}
-
-/**
- * 格式化日期
- * @param {string} dateStr - ISO 日期字符串
- * @returns {string} 格式化后的日期
- */
-function formatDate(dateStr) {
- if (!dateStr) return '--';
- try {
- const date = new Date(dateStr);
- return date.toLocaleString(getCurrentLanguage(), {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit'
- });
- } catch (e) {
- return dateStr;
- }
-}
\ No newline at end of file
diff --git a/static/app/utils.js b/static/app/utils.js
deleted file mode 100644
index f796c454330c4d4d7d9adde58ee7c19ce8780886..0000000000000000000000000000000000000000
--- a/static/app/utils.js
+++ /dev/null
@@ -1,466 +0,0 @@
-// 工具函数
-import { t, getCurrentLanguage } from './i18n.js';
-import { apiClient } from './auth.js';
-
-/**
- * 获取所有支持的提供商配置列表
- * @param {string[]} supportedProviders - 已注册的提供商类型列表
- * @returns {Object[]} 提供商配置对象数组
- */
-function getProviderConfigs(supportedProviders = []) {
- return [
- {
- id: 'forward-api',
- name: 'NewAPI',
- icon: 'fa-share-square',
- visible: supportedProviders.includes('forward-api')
- },
- {
- id: 'gemini-cli-oauth',
- name: t('dashboard.routing.nodeName.gemini'),
- icon: 'fa-robot',
- defaultPath: 'configs/gemini/',
- visible: supportedProviders.includes('gemini-cli-oauth')
- },
- {
- id: 'gemini-antigravity',
- name: t('dashboard.routing.nodeName.antigravity'),
- icon: 'fa-rocket',
- defaultPath: 'configs/antigravity/',
- visible: supportedProviders.includes('gemini-antigravity')
- },
- {
- id: 'claude-kiro-oauth',
- name: t('dashboard.routing.nodeName.kiro'),
- icon: 'fa-key',
- defaultPath: 'configs/kiro/',
- visible: supportedProviders.includes('claude-kiro-oauth')
- },
- {
- id: 'openai-codex-oauth',
- name: t('dashboard.routing.nodeName.codex'),
- icon: 'fa-code',
- defaultPath: 'configs/codex/',
- visible: supportedProviders.includes('openai-codex-oauth')
- },
- {
- id: 'openai-qwen-oauth',
- name: t('dashboard.routing.nodeName.qwen'),
- icon: 'fa-cloud',
- defaultPath: 'configs/qwen/',
- visible: supportedProviders.includes('openai-qwen-oauth')
- },
- {
- id: 'openai-iflow',
- name: t('dashboard.routing.nodeName.iflow'),
- icon: 'fa-stream',
- defaultPath: 'configs/iflow/',
- visible: supportedProviders.includes('openai-iflow')
- },
- {
- id: 'grok-custom',
- name: t('dashboard.routing.nodeName.grok'),
- icon: 'fa-user-secret',
- visible: supportedProviders.includes('grok-custom')
- },
- {
- id: 'openai-custom',
- name: t('dashboard.routing.nodeName.openai'),
- icon: 'fa-microchip',
- visible: supportedProviders.includes('openai-custom')
- },
- {
- id: 'claude-custom',
- name: t('dashboard.routing.nodeName.claude'),
- icon: 'fa-brain',
- visible: supportedProviders.includes('claude-custom')
- },
- {
- id: 'openaiResponses-custom',
- name: 'OpenAI Responses',
- icon: 'fa-reply-all',
- visible: supportedProviders.includes('openaiResponses-custom')
- },
- ];
-}
-
-/**
- * 格式化运行时间
- * @param {number} seconds - 秒数
- * @returns {string} 格式化的时间字符串
- */
-function formatUptime(seconds) {
- const days = Math.floor(seconds / 86400);
- const hours = Math.floor((seconds % 86400) / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- const secs = Math.floor(seconds % 60);
-
- if (getCurrentLanguage() === 'en-US') {
- return `${days}d ${hours}h ${minutes}m ${secs}s`;
- }
- return `${days}天 ${hours}小时 ${minutes}分 ${secs}秒`;
-}
-
-/**
- * HTML转义
- * @param {string} text - 要转义的文本
- * @returns {string} 转义后的文本
- */
-function escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
-}
-
-/**
- * 显示提示消息
- * @param {string} title - 提示标题 (可选,旧接口为 message)
- * @param {string} message - 提示消息
- * @param {string} type - 消息类型 (info, success, error)
- */
-function showToast(title, message, type = 'info') {
- // 兼容旧接口 (message, type)
- if (arguments.length === 2 && (message === 'success' || message === 'error' || message === 'info' || message === 'warning')) {
- type = message;
- message = title;
- title = t(`common.${type}`);
- }
-
- const toast = document.createElement('div');
- toast.className = `toast ${type}`;
- toast.innerHTML = `
- ${escapeHtml(title)}
- ${escapeHtml(message)}
- `;
-
- // 获取toast容器
- const toastContainer = document.getElementById('toastContainer') || document.querySelector('.toast-container');
- if (toastContainer) {
- toastContainer.appendChild(toast);
-
- setTimeout(() => {
- toast.remove();
- }, 3000);
- }
-}
-
-/**
- * 获取字段显示文案
- * @param {string} key - 字段键
- * @returns {string} 显示文案
- */
-function getFieldLabel(key) {
- const labelMap = {
- 'customName': t('modal.provider.customName') + ' ' + t('config.optional'),
- 'checkModelName': t('modal.provider.checkModelName') + ' ' + t('config.optional'),
- 'checkHealth': t('modal.provider.healthCheckLabel'),
- 'concurrencyLimit': t('modal.provider.concurrencyLimit') + ' ' + t('config.optional'),
- 'queueLimit': t('modal.provider.queueLimit') + ' ' + t('config.optional'),
- 'OPENAI_API_KEY': 'OpenAI API Key',
- 'OPENAI_BASE_URL': 'OpenAI Base URL',
- 'CLAUDE_API_KEY': 'Claude API Key',
- 'CLAUDE_BASE_URL': 'Claude Base URL',
- 'PROJECT_ID': t('modal.provider.field.projectId'),
- 'GEMINI_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'),
- 'KIRO_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'),
- 'QWEN_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'),
- 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'),
- 'IFLOW_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'),
- 'CODEX_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'),
- 'GROK_COOKIE_TOKEN': t('modal.provider.field.ssoToken'),
- 'GROK_CF_CLEARANCE': t('modal.provider.field.cfClearance'),
- 'GROK_USER_AGENT': t('modal.provider.field.userAgent'),
- 'GEMINI_BASE_URL': 'Gemini Base URL',
- 'KIRO_BASE_URL': t('modal.provider.field.baseUrl'),
- 'KIRO_REFRESH_URL': t('modal.provider.field.refreshUrl'),
- 'KIRO_REFRESH_IDC_URL': t('modal.provider.field.refreshIdcUrl'),
- 'QWEN_BASE_URL': 'Qwen Base URL',
- 'QWEN_OAUTH_BASE_URL': t('modal.provider.field.oauthBaseUrl'),
- 'ANTIGRAVITY_BASE_URL_DAILY': t('modal.provider.field.dailyBaseUrl'),
- 'ANTIGRAVITY_BASE_URL_AUTOPUSH': t('modal.provider.field.autopushBaseUrl'),
- 'IFLOW_BASE_URL': t('modal.provider.field.iflowBaseUrl'),
- 'CODEX_BASE_URL': t('modal.provider.field.codexBaseUrl'),
- 'GROK_BASE_URL': t('modal.provider.field.grokBaseUrl'),
- 'FORWARD_API_KEY': 'Forward API Key',
- 'FORWARD_BASE_URL': 'Forward Base URL',
- 'FORWARD_HEADER_NAME': t('modal.provider.field.headerName'),
- 'FORWARD_HEADER_VALUE_PREFIX': t('modal.provider.field.headerPrefix'),
- 'USE_SYSTEM_PROXY_FORWARD': t('modal.provider.field.useSystemProxy')
- };
-
- return labelMap[key] || key;
-}
-
-/**
- * 获取提供商类型的字段配置
- * @param {string} providerType - 提供商类型
- * @returns {Array} 字段配置数组
- */
-function getProviderTypeFields(providerType) {
- const fieldConfigs = {
- 'openai-custom': [
- {
- id: 'OPENAI_API_KEY',
- label: t('modal.provider.field.apiKey'),
- type: 'password',
- placeholder: 'sk-...'
- },
- {
- id: 'OPENAI_BASE_URL',
- label: 'OpenAI Base URL',
- type: 'text',
- placeholder: 'https://api.openai.com/v1'
- }
- ],
- 'openaiResponses-custom': [
- {
- id: 'OPENAI_API_KEY',
- label: t('modal.provider.field.apiKey'),
- type: 'password',
- placeholder: 'sk-...'
- },
- {
- id: 'OPENAI_BASE_URL',
- label: 'OpenAI Base URL',
- type: 'text',
- placeholder: 'https://api.openai.com/v1'
- }
- ],
- 'claude-custom': [
- {
- id: 'CLAUDE_API_KEY',
- label: 'Claude API Key',
- type: 'password',
- placeholder: 'sk-ant-...'
- },
- {
- id: 'CLAUDE_BASE_URL',
- label: 'Claude Base URL',
- type: 'text',
- placeholder: 'https://api.anthropic.com'
- }
- ],
- 'gemini-cli-oauth': [
- {
- id: 'PROJECT_ID',
- label: t('modal.provider.field.projectId'),
- type: 'text',
- placeholder: t('modal.provider.field.projectId.placeholder')
- },
- {
- id: 'GEMINI_OAUTH_CREDS_FILE_PATH',
- label: t('modal.provider.field.oauthPath'),
- type: 'text',
- placeholder: t('modal.provider.field.oauthPath.gemini.placeholder')
- },
- {
- id: 'GEMINI_BASE_URL',
- label: `Gemini Base URL ${t('config.optional')}`,
- type: 'text',
- placeholder: 'https://cloudcode-pa.googleapis.com'
- }
- ],
- 'claude-kiro-oauth': [
- {
- id: 'KIRO_OAUTH_CREDS_FILE_PATH',
- label: t('modal.provider.field.oauthPath'),
- type: 'text',
- placeholder: t('modal.provider.field.oauthPath.kiro.placeholder')
- },
- {
- id: 'KIRO_BASE_URL',
- label: `${t('modal.provider.field.baseUrl')} ${t('config.optional')}`,
- type: 'text',
- placeholder: 'https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse'
- },
- {
- id: 'KIRO_REFRESH_URL',
- label: `${t('modal.provider.field.refreshUrl')} ${t('config.optional')}`,
- type: 'text',
- placeholder: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken'
- },
- {
- id: 'KIRO_REFRESH_IDC_URL',
- label: `${t('modal.provider.field.refreshIdcUrl')} ${t('config.optional')}`,
- type: 'text',
- placeholder: 'https://oidc.{{region}}.amazonaws.com/token'
- }
- ],
- 'openai-qwen-oauth': [
- {
- id: 'QWEN_OAUTH_CREDS_FILE_PATH',
- label: t('modal.provider.field.oauthPath'),
- type: 'text',
- placeholder: t('modal.provider.field.oauthPath.qwen.placeholder')
- },
- {
- id: 'QWEN_BASE_URL',
- label: `Qwen Base URL ${t('config.optional')}`,
- type: 'text',
- placeholder: 'https://portal.qwen.ai/v1'
- },
- {
- id: 'QWEN_OAUTH_BASE_URL',
- label: `${t('modal.provider.field.oauthBaseUrl')} ${t('config.optional')}`,
- type: 'text',
- placeholder: 'https://chat.qwen.ai'
- }
- ],
- 'gemini-antigravity': [
- {
- id: 'PROJECT_ID',
- label: `${t('modal.provider.field.projectId')} ${t('config.optional')}`,
- type: 'text',
- placeholder: t('modal.provider.field.projectId.optional.placeholder')
- },
- {
- id: 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH',
- label: t('modal.provider.field.oauthPath'),
- type: 'text',
- placeholder: t('modal.provider.field.oauthPath.antigravity.placeholder')
- },
- {
- id: 'ANTIGRAVITY_BASE_URL_DAILY',
- label: `${t('modal.provider.field.dailyBaseUrl')} ${t('config.optional')}`,
- type: 'text',
- placeholder: 'https://daily-cloudcode-pa.sandbox.googleapis.com'
- },
- {
- id: 'ANTIGRAVITY_BASE_URL_AUTOPUSH',
- label: `${t('modal.provider.field.autopushBaseUrl')} ${t('config.optional')}`,
- type: 'text',
- placeholder: 'https://autopush-cloudcode-pa.sandbox.googleapis.com'
- }
- ],
- 'openai-iflow': [
- {
- id: 'IFLOW_OAUTH_CREDS_FILE_PATH',
- label: t('modal.provider.field.oauthPath'),
- type: 'text',
- placeholder: t('modal.provider.field.oauthPath.iflow.placeholder')
- },
- {
- id: 'IFLOW_BASE_URL',
- label: `iFlow Base URL ${t('config.optional')}`,
- type: 'text',
- placeholder: 'https://iflow.cn/api'
- }
- ],
- 'openai-codex-oauth': [
- {
- id: 'CODEX_OAUTH_CREDS_FILE_PATH',
- label: t('modal.provider.field.oauthPath'),
- type: 'text',
- placeholder: t('modal.provider.field.oauthPath.codex.placeholder')
- },
- {
- id: 'CODEX_EMAIL',
- label: `${t('modal.provider.field.email')} ${t('config.optional')}`,
- type: 'email',
- placeholder: t('modal.provider.field.email.placeholder')
- },
- {
- id: 'CODEX_BASE_URL',
- label: `${t('modal.provider.field.codexBaseUrl')} ${t('config.optional')}`,
- type: 'text',
- placeholder: 'https://api.openai.com/v1/codex'
- }
- ],
- 'grok-custom': [
- {
- id: 'GROK_COOKIE_TOKEN',
- label: t('modal.provider.field.ssoToken'),
- type: 'password',
- placeholder: 'sso cookie token'
- },
- {
- id: 'GROK_CF_CLEARANCE',
- label: `${t('modal.provider.field.cfClearance')} ${t('config.optional')}`,
- type: 'text',
- placeholder: 'cf_clearance cookie value'
- },
- {
- id: 'GROK_USER_AGENT',
- label: `${t('modal.provider.field.userAgent')} ${t('config.optional')}`,
- type: 'text',
- placeholder: 'Mozilla/5.0 ...'
- },
- {
- id: 'GROK_BASE_URL',
- label: `${t('modal.provider.field.grokBaseUrl')} ${t('config.optional')}`,
- type: 'text',
- placeholder: 'https://grok.com'
- }
- ],
- 'forward-api': [
- {
- id: 'FORWARD_API_KEY',
- label: t('modal.provider.field.apiKey'),
- type: 'password',
- placeholder: t('modal.provider.field.apiKey.placeholder')
- },
- {
- id: 'FORWARD_BASE_URL',
- label: t('modal.provider.field.baseUrl'),
- type: 'text',
- placeholder: 'https://api.example.com'
- },
- {
- id: 'FORWARD_HEADER_NAME',
- label: `${t('modal.provider.field.headerName')} ${t('config.optional')}`,
- type: 'text',
- placeholder: 'Authorization'
- },
- {
- id: 'FORWARD_HEADER_VALUE_PREFIX',
- label: `${t('modal.provider.field.headerPrefix')} ${t('config.optional')}`,
- type: 'text',
- placeholder: 'Bearer '
- }
- ]
- };
-
- return fieldConfigs[providerType] || [];
-}
-
-/**
- * 调试函数:获取当前提供商统计信息
- * @param {Object} providerStats - 提供商统计对象
- * @returns {Object} 扩展的统计信息
- */
-function getProviderStats(providerStats) {
- return {
- ...providerStats,
- // 添加计算得出的统计信息
- successRate: providerStats.totalRequests > 0 ?
- ((providerStats.totalRequests - providerStats.totalErrors) / providerStats.totalRequests * 100).toFixed(2) + '%' : '0%',
- avgUsagePerProvider: providerStats.activeProviders > 0 ?
- Math.round(providerStats.totalRequests / providerStats.activeProviders) : 0,
- healthRatio: providerStats.totalAccounts > 0 ?
- (providerStats.healthyProviders / providerStats.totalAccounts * 100).toFixed(2) + '%' : '0%'
- };
-}
-
-/**
- * 通用 API 请求函数
- * @param {string} url - API 端点 URL
- * @param {Object} options - fetch 选项
- * @returns {Promise} 响应数据
- */
-async function apiRequest(url, options = {}) {
- // 如果 URL 以 /api 开头,去掉它(因为 apiClient.request 会自动添加)
- const endpoint = url.startsWith('/api') ? url.slice(4) : url;
- return apiClient.request(endpoint, options);
-}
-
-// 导出所有工具函数
-export {
- formatUptime,
- escapeHtml,
- showToast,
- getFieldLabel,
- getProviderTypeFields,
- getProviderConfigs,
- getProviderStats,
- apiRequest
-};
\ No newline at end of file
diff --git a/static/components/header.css b/static/components/header.css
deleted file mode 100644
index e06b2130f016280c6929daad093a8af0aa343545..0000000000000000000000000000000000000000
--- a/static/components/header.css
+++ /dev/null
@@ -1,161 +0,0 @@
-/* Header - 玻璃拟态效果 */
-.header {
- background: var(--bg-glass);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- border-bottom: 1px solid var(--border-color);
- position: sticky;
- top: 0;
- z-index: 100;
- transition: var(--transition);
-}
-
-.header-content {
- max-width: 1600px;
- margin: 0 auto;
- padding: 0.75rem 2rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.header h1 {
- font-size: 1.25rem;
- font-weight: 700;
- color: var(--text-primary);
- display: flex;
- align-items: center;
- letter-spacing: -0.025em;
-}
-
-.header h1 i {
- margin-right: 0.5rem;
-}
-
-.header-controls {
- display: flex;
- gap: 1rem;
- align-items: center;
-}
-
-.status-badge {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 1rem;
- background: var(--bg-tertiary);
- border-radius: var(--radius-full);
- font-size: 0.75rem;
- font-weight: 600;
- letter-spacing: 0.05em;
- text-transform: uppercase;
-}
-
-.status-badge i {
- color: var(--success-color);
- animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
- font-size: 0.6rem;
-}
-
-.status-badge.error i {
- color: var(--danger-color);
-}
-
-.logout-btn {
- padding: 0.5rem 1rem;
- background: transparent;
- color: var(--text-secondary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-lg);
- cursor: pointer;
- font-size: 0.875rem;
- font-weight: 600;
- transition: var(--transition);
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.logout-btn:hover {
- background: var(--bg-tertiary);
- color: var(--danger-color);
- border-color: var(--danger-color);
-}
-
-.logout-btn:active {
- transform: scale(0.98);
-}
-
-.logout-btn i {
- font-size: 14px;
-}
-
-.github-link {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 40px;
- height: 40px;
- padding: 0;
- background: transparent;
- color: var(--text-secondary);
- border: 1px solid var(--border-color);
- border-radius: 50%;
- cursor: pointer;
- font-size: 1.125rem;
- transition: var(--transition);
- text-decoration: none;
- position: relative;
- overflow: hidden;
-}
-
-.github-link:hover {
- background: var(--bg-tertiary);
- color: var(--primary-color);
- border-color: var(--primary-color);
- transform: translateY(-2px);
- box-shadow: var(--shadow-md);
-}
-
-.github-link:active {
- transform: translateY(0);
-}
-
-.github-link i {
- transition: transform 0.3s ease;
-}
-
-/* KIRO 购买链接 */
-.kiro-buy-link {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 1rem;
- background: linear-gradient(135deg, var(--warning-color) 0%, #f97316 100%);
- color: var(--white);
- text-decoration: none;
- border-radius: 9999px;
- font-size: 0.875rem;
- font-weight: 600;
- transition: var(--transition);
- box-shadow: 0 2px 8px var(--warning-30);
-}
-
-.kiro-buy-link:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px var(--warning-40);
- background: linear-gradient(135deg, #f97316 0%, var(--warning-color) 100%);
-}
-
-.kiro-buy-link:active {
- transform: translateY(0);
-}
-
-.kiro-buy-link i {
- font-size: 0.875rem;
-}
-
-/* 暗黑主题适配 */
-[data-theme="dark"] .header {
- background: var(--bg-glass);
-}
diff --git a/static/components/header.html b/static/components/header.html
deleted file mode 100644
index 40de0f10aceac9ac0f6965b13648fabbdcb685bd..0000000000000000000000000000000000000000
--- a/static/components/header.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/static/components/section-config.css b/static/components/section-config.css
deleted file mode 100644
index 6cd16ca4b1dbe4c9fee89ae286719093f1be5a16..0000000000000000000000000000000000000000
--- a/static/components/section-config.css
+++ /dev/null
@@ -1,465 +0,0 @@
-/* 表单样式 */
-.config-panel {
- background: var(--bg-primary);
- padding: 2rem;
- border-radius: var(--radius-xl);
- box-shadow: var(--shadow-sm);
- border: 1px solid var(--border-color);
-}
-
-.config-form {
- max-width: 800px;
- margin: 0 auto;
-}
-
-.form-group {
- margin-bottom: 1.5rem;
-}
-
-.form-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 1.5rem;
-}
-
-.form-group label {
- display: block;
- margin-bottom: 0.5rem;
- font-weight: 600;
- color: var(--text-primary);
- font-size: 0.9rem;
-}
-
-.optional-tag, .form-group label .optional-mark {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- font-weight: 400;
- margin-left: 0.5rem;
- background: var(--bg-tertiary);
- padding: 0.125rem 0.375rem;
- border-radius: var(--radius-sm);
-}
-
-.form-control::placeholder {
- color: var(--text-tertiary);
-}
-
-textarea.form-control {
- resize: vertical;
- font-family: inherit;
-}
-
-/* 密码输入框样式 */
-.password-input-group {
- position: relative;
-}
-
-.password-input-wrapper {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.input-with-toggle {
- position: relative;
- flex: 1;
- display: flex;
- align-items: center;
-}
-
-.generate-key-btn {
- flex-shrink: 0;
- height: 38px;
- display: flex;
- align-items: center;
- gap: 0.4rem;
- white-space: nowrap;
-}
-
-.password-input-wrapper .form-control {
- padding-right: 3rem;
-}
-
-.password-input-wrapper input[type="password"],
-.password-input-wrapper input[type="text"] {
- flex: 1;
- padding-right: 3rem;
-}
-
-.password-toggle {
- position: absolute;
- right: 0.75rem;
- background: none;
- border: none;
- cursor: pointer;
- padding: 0.25rem;
- color: var(--text-secondary);
- transition: var(--transition);
- z-index: 1;
- width: auto;
- flex-shrink: 0;
-}
-
-.password-toggle:hover {
- color: var(--primary-color);
-}
-
-.password-toggle i {
- font-size: 1rem;
- width: 1rem;
- text-align: center;
-}
-
-/* 授权刷新切换开关 */
-.oauth-refresh-toggle {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- margin-top: 0.5rem;
-}
-
-.toggle-switch {
- position: relative;
- display: inline-block;
- width: 48px;
- height: 24px;
-}
-
-.toggle-switch input {
- opacity: 0;
- width: 0;
- height: 0;
-}
-
-.toggle-slider {
- position: absolute;
- cursor: pointer;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: var(--bg-tertiary);
- border: 1px solid var(--border-color);
- transition: var(--transition);
- border-radius: 24px;
-}
-
-.toggle-slider:before {
- position: absolute;
- content: "";
- height: 18px;
- width: 18px;
- left: 3px;
- bottom: 2px;
- background-color: white;
- transition: var(--transition);
- border-radius: 50%;
- box-shadow: 0 1px 3px var(--neutral-shadow-30);
-}
-
-input:checked + .toggle-slider {
- background-color: var(--primary-color);
- border-color: var(--primary-color);
-}
-
-input:checked + .toggle-slider:before {
- transform: translateX(24px);
-}
-
-.toggle-label {
- font-weight: 500;
- color: var(--text-primary);
- font-size: 0.875rem;
-}
-
-/* 系统提示区域 */
-.system-prompt-section {
- margin-top: 2rem;
- padding-top: 1.5rem;
- border-top: 1px solid var(--border-color);
-}
-
-.form-actions {
- display: flex;
- gap: 1rem;
- margin-top: 2rem;
- justify-content: flex-end;
-}
-
-/* 重启提示模态框样式 */
-.restart-required-modal .restart-modal-content {
- max-width: 550px;
- border: 2px solid var(--primary-color);
- box-shadow: 0 25px 80px var(--primary-30);
-}
-
-.restart-required-modal .restart-modal-header {
- background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
- color: white;
- border-bottom: none;
-}
-
-.restart-required-modal .restart-modal-header h3 {
- color: white;
-}
-
-.restart-required-modal .restart-modal-header h3 i {
- color: white;
-}
-
-.restart-icon-container {
- text-align: center;
- margin-bottom: 1.5rem;
-}
-
-.restart-icon-container i {
- font-size: 3rem;
- color: var(--primary-color);
- animation: spin 2s linear infinite;
-}
-
-@keyframes spin {
- from { transform: rotate(0deg); }
- to { transform: rotate(360deg); }
-}
-
-.restart-notice {
- background: linear-gradient(135deg, var(--success-bg) 0%, var(--success-bg-light) 100%);
- border: 1px solid var(--secondary-color);
- border-radius: 0.5rem;
- padding: 1rem;
- margin-bottom: 1rem;
- border-left: 4px solid var(--primary-color);
-}
-
-.restart-notice p {
- margin: 0;
- color: var(--success-text);
- font-weight: 500;
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.restart-instructions {
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- padding: 1rem;
-}
-
-.restart-instructions p {
- margin: 0;
- color: var(--text-primary);
- white-space: pre-line;
- line-height: 1.6;
- font-size: 0.875rem;
-}
-
-.restart-confirm-btn {
- background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
- color: white;
- border: none;
- padding: 0.75rem 1.5rem;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- font-weight: 500;
- cursor: pointer;
- transition: var(--transition);
- box-shadow: 0 2px 8px var(--primary-30);
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.restart-confirm-btn:hover {
- background: linear-gradient(135deg, var(--btn-primary-hover) 0%, var(--primary-color) 100%);
- transform: translateY(-2px);
- box-shadow: 0 4px 12px var(--primary-40);
-}
-
-/* 提供商标签选择器样式 */
-.provider-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 0.75rem;
- padding: 1rem;
- background: var(--bg-tertiary);
- border-radius: 12px;
- border: 1px solid var(--border-color);
-}
-
-.provider-tag {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.625rem 1rem;
- background: var(--bg-primary);
- border: 2px solid var(--border-color);
- border-radius: 50px;
- cursor: pointer;
- transition: all 0.2s ease;
- font-size: 0.875rem;
- font-weight: 500;
- color: var(--text-secondary);
- user-select: none;
-}
-
-.provider-tag:hover {
- background: var(--bg-secondary);
- border-color: var(--primary-color);
- color: var(--primary-color);
- transform: translateY(-1px);
- box-shadow: 0 4px 12px var(--neutral-shadow-10);
-}
-
-.provider-tag.selected {
- background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
- border-color: transparent;
- color: white;
- box-shadow: 0 4px 12px var(--primary-30);
-}
-
-.provider-tag.selected:hover {
- background: linear-gradient(135deg, var(--btn-primary-hover) 0%, var(--primary-color) 100%);
- transform: translateY(-1px);
- box-shadow: 0 6px 16px var(--primary-40);
- color: white;
-}
-
-.provider-tag i {
- font-size: 0.875rem;
- opacity: 0.8;
-}
-
-.provider-tag.selected i {
- opacity: 1;
-}
-
-.provider-tag span {
- white-space: nowrap;
-}
-
-
-/* 高级配置区域 */
-.advanced-config-section {
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- padding: 1.5rem;
- margin-top: 1.5rem;
- background: var(--bg-secondary);
-}
-
-.advanced-config-section h3 {
- color: var(--text-primary);
- font-size: 1.25rem;
- font-weight: 600;
- margin-bottom: 1.5rem;
- padding-bottom: 0.5rem;
- border-bottom: 1px solid var(--border-color);
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.advanced-config-section h3 i {
- color: var(--primary-color);
-}
-
-.pool-section .form-text {
- margin-top: 0.5rem;
- color: var(--text-secondary);
- font-size: 0.75rem;
- font-style: italic;
-}
-
-.config-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 1rem;
- margin-bottom: 1.5rem;
-}
-
-.config-row:last-child {
- margin-bottom: 0;
-}
-
-
-/* 组合配置区块样式 */
-.config-group-section {
- border: 1px solid var(--border-color);
- border-radius: var(--radius-lg);
- padding: 1.5rem;
- margin-bottom: 2rem;
- background: var(--bg-primary);
- transition: var(--transition);
-}
-
-.config-group-section:hover {
- box-shadow: var(--shadow-md);
- border-color: var(--primary-color);
-}
-
-.config-group-section h3 {
- color: var(--text-primary);
- font-size: 1.1rem;
- font-weight: 600;
- margin-bottom: 1.5rem;
- padding-bottom: 0.75rem;
- border-bottom: 2px solid var(--bg-tertiary);
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-.config-group-section h3 i {
- color: var(--primary-color);
- width: 1.25rem;
- text-align: center;
-}
-
-.config-group-section hr {
- border: none;
- border-top: 1px solid var(--border-color);
- margin: 1.5rem 0;
- opacity: 0.5;
-}
-
-/* 响应式调整 */
-@media (max-width: 768px) {
- .form-row, .config-row {
- grid-template-columns: 1fr;
- gap: 0;
- }
-
- .config-panel {
- padding: 1rem;
- }
-
- .config-group-section {
- padding: 1rem;
- }
-}
-
-/* 暗黑主题适配 */
-[data-theme="dark"] .config-panel {
- background: var(--bg-primary);
-}
-
-[data-theme="dark"] .restart-required-modal .restart-modal-content {
- border-color: var(--primary-color);
-}
-
-[data-theme="dark"] .restart-notice {
- background: linear-gradient(135deg, var(--success-bg) 0%, var(--success-bg-light) 100%);
- border-color: var(--secondary-color);
-}
-
-[data-theme="dark"] .restart-notice p {
- color: var(--success-text);
-}
-
-[data-theme="dark"] .restart-instructions {
- background: var(--bg-secondary);
- border-color: var(--border-color);
-}
diff --git a/static/components/section-config.html b/static/components/section-config.html
deleted file mode 100644
index 37e0fe18e7f9c7aaf7d672604a4445fc474f8f25..0000000000000000000000000000000000000000
--- a/static/components/section-config.html
+++ /dev/null
@@ -1,359 +0,0 @@
-
-
-
diff --git a/static/components/section-dashboard.css b/static/components/section-dashboard.css
deleted file mode 100644
index 8edc7491802f501cb4c512c6dadbc48493f874cd..0000000000000000000000000000000000000000
--- a/static/components/section-dashboard.css
+++ /dev/null
@@ -1,774 +0,0 @@
-/* 统计卡片 */
-.stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: 1.5rem;
- margin-bottom: 2rem;
-}
-
-.stat-card {
- background: var(--bg-primary);
- padding: 1.5rem;
- border-radius: var(--radius-xl);
- border: 1px solid var(--border-color);
- box-shadow: var(--shadow-sm);
- display: flex;
- align-items: center;
- gap: 1.25rem;
- transition: var(--transition);
- position: relative;
- overflow: hidden;
-}
-
-.stat-card:hover {
- transform: translateY(-4px);
- box-shadow: var(--shadow-lg);
- border-color: var(--primary-30);
-}
-
-.stat-icon {
- width: 56px;
- height: 56px;
- border-radius: var(--radius-lg);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.5rem;
- color: var(--primary-color);
- background: var(--primary-10);
- flex-shrink: 0;
-}
-
-.stat-info h3 {
- font-size: 2rem;
- font-weight: 700;
- margin-bottom: 0.25rem;
-}
-
-.stat-info p {
- color: var(--text-secondary);
- font-size: 0.875rem;
-}
-
-/* System Info Panel in Dashboard */
-.system-info-panel {
- background: var(--bg-primary);
- padding: 1.5rem;
- border-radius: 0.5rem;
- box-shadow: var(--shadow-md);
- margin-top: 1.5rem;
-}
-
-.system-info-panel h3 {
- font-size: 1.25rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0;
-}
-
-.system-info-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1.5rem;
-}
-
-.update-controls {
- display: flex;
- gap: 0.75rem;
- align-items: center;
-}
-
-.update-badge {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- background: var(--warning-bg);
- color: var(--warning-text);
- padding: 2px 8px;
- border-radius: 9999px;
- font-size: 11px;
- font-weight: 600;
- margin-left: 8px;
- border: 1px solid var(--warning-bg-light);
- animation: bounce-in 0.5s ease-out;
-}
-
-@keyframes bounce-in {
- 0% { transform: scale(0.8); opacity: 0; }
- 70% { transform: scale(1.1); }
- 100% { transform: scale(1); opacity: 1; }
-}
-
-.info-grid {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 1rem;
-}
-
-.info-item {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.info-item .info-label {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- color: var(--text-secondary);
- font-size: 0.875rem;
- font-weight: 500;
-}
-
-.info-item .info-label i {
- color: var(--primary-color);
- width: 16px;
- text-align: center;
-}
-
-.info-item .info-value {
- color: var(--text-primary);
- font-size: 1rem;
- font-weight: 600;
-}
-
-.version-display-wrapper {
- display: flex;
- align-items: center;
- padding-left: 1.5rem;
- flex-wrap: wrap;
- gap: 0.5rem;
-}
-
-.status-healthy {
- background: var(--success-bg);
- color: var(--success-text);
-}
-
-.status-unhealthy {
- background: var(--danger-bg);
- color: var(--danger-text);
-}
-
-/* Dashboard Top Row Layout */
-.dashboard-top-row {
- display: flex;
- gap: 1.5rem;
- margin-bottom: 2rem;
- align-items: stretch;
- justify-content: flex-start;
-}
-
-.dashboard-top-row .stats-grid {
- flex: 1;
- margin-bottom: 0;
- display: flex;
- flex-direction: column;
-}
-
-.dashboard-top-row .stats-grid .stat-card {
- flex: 1;
- height: 100%;
-}
-
-.dashboard-top-row .dashboard-contact {
- flex: 1;
- margin-top: 0;
- padding-top: 0;
- border-top: none;
-}
-
-.dashboard-top-row .dashboard-contact .contact-grid {
- margin-top: 0;
- height: 100%;
- grid-template-columns: repeat(2, 1fr);
- gap: 1rem;
-}
-
-.dashboard-top-row .dashboard-contact .contact-card {
- padding: 1.25rem;
- height: 100%;
- display: flex;
- flex-direction: column;
- justify-content: center;
-}
-
-.dashboard-top-row .dashboard-contact .qr-container {
- margin: 0.75rem 0;
-}
-
-.dashboard-top-row .dashboard-contact .qr-code {
- width: 100px;
- height: 100px;
-}
-
-.dashboard-top-row .dashboard-contact .contact-card h3 {
- font-size: 1rem;
-}
-
-.dashboard-top-row .dashboard-contact .qr-description {
- font-size: 0.75rem;
-}
-
-/* Contact and Sponsor Section */
-.contact-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: 2rem;
- margin-top: 1.5rem;
-}
-
-.contact-card {
- background: var(--bg-primary);
- padding: 2rem;
- border-radius: 0.5rem;
- box-shadow: var(--shadow-md);
- text-align: center;
- transition: var(--transition);
- border: 1px solid var(--border-color);
-}
-
-.contact-card:hover {
- transform: translateY(-2px);
- box-shadow: var(--shadow-lg);
- border-color: var(--primary-color);
-}
-
-.contact-card h3 {
- font-size: 1.25rem;
- font-weight: 600;
- margin-bottom: 1rem;
- color: var(--text-primary);
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0.5rem;
-}
-
-.contact-card h3 i {
- color: var(--primary-color);
-}
-
-.qr-container {
- margin: 1.5rem 0;
- display: flex;
- justify-content: center;
-}
-
-.qr-code {
- width: 200px;
- height: 200px;
- object-fit: contain;
- border: 4px solid var(--border-color);
- border-radius: 0.5rem;
- padding: 0.5rem;
- background: white;
- transition: var(--transition);
-}
-
-.qr-code:hover {
- border-color: var(--primary-color);
- transform: scale(1.05);
-}
-
-.clickable-qr {
- cursor: zoom-in;
-}
-
-/* Image Zoom Overlay */
-.image-zoom-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: var(--neutral-shadow-85);
- display: none;
- justify-content: center;
- align-items: center;
- z-index: 2000;
- cursor: zoom-out;
- opacity: 0;
- transition: opacity 0.3s ease;
-}
-
-.image-zoom-overlay.show {
- display: flex;
- opacity: 1;
-}
-
-.image-zoom-overlay img {
- max-width: 90%;
- max-height: 90%;
- border-radius: 0.5rem;
- box-shadow: 0 0 20px var(--neutral-shadow-50);
- transform: scale(0.9);
- transition: transform 0.3s ease;
-}
-
-.image-zoom-overlay.show img {
- transform: scale(1);
-}
-
-.qr-description {
- font-size: 0.875rem;
- color: var(--text-secondary);
- margin: 0;
- line-height: 1.5;
-}
-
-/* Contact section styling for dashboard */
-.contact-section {
- margin-top: 2rem;
- padding-top: 1.5rem;
- border-top: 1px solid var(--border-color);
-}
-
-.contact-section h3 {
- font-size: 1.25rem;
- font-weight: 600;
- margin-bottom: 1.5rem;
- color: var(--text-primary);
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.contact-section h3 i {
- color: var(--primary-color);
-}
-
-/* 响应式调整 */
-@media (max-width: 1024px) {
- .dashboard-top-row {
- flex-direction: column;
- }
-
- .dashboard-top-row .stats-grid {
- flex: none;
- }
-
- .dashboard-top-row .dashboard-contact .qr-code {
- width: 160px;
- height: 160px;
- }
-}
-
-/* ========================================
- 可用模型列表样式
- ======================================== */
-
-.models-section {
- margin-top: 2rem;
- padding-top: 1.5rem;
- border-top: 1px solid var(--border-color);
-}
-
-.models-section-title {
- font-size: 1.1rem;
- font-weight: 600;
- margin-bottom: 1.25rem;
- color: var(--text-primary);
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.models-section-title i {
- color: var(--primary-color);
-}
-
-/* 模型描述区域 */
-.models-description {
- margin-bottom: 1.5rem;
-}
-
-/* 模型容器 */
-.models-container {
- background: var(--bg-secondary);
- padding: 1.25rem;
- border-radius: var(--radius-lg);
- border: 1px solid var(--border-color);
-}
-
-.models-list {
- display: flex;
- flex-direction: column;
- gap: 1.25rem;
-}
-
-/* 加载状态 */
-.models-loading {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0.75rem;
- padding: 2.5rem;
- color: var(--text-secondary);
- font-size: 1rem;
-}
-
-.models-loading i {
- font-size: 1.25rem;
- color: var(--primary-color);
-}
-
-/* 提供商模型组 */
-.provider-models-group {
- border: 1px solid var(--border-color);
- border-radius: var(--radius-lg);
- overflow: hidden;
- transition: var(--transition);
- background: var(--bg-primary);
-}
-
-.provider-models-group:hover {
- border-color: var(--primary-color);
- box-shadow: var(--shadow-md);
-}
-
-/* 提供商标题 */
-.provider-models-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0.875rem 1.25rem;
- background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
- border-bottom: 1px solid var(--border-color);
- cursor: pointer;
- transition: var(--transition);
-}
-
-.provider-models-header:hover {
- background: linear-gradient(135deg, var(--primary-10) 0%, var(--bg-tertiary) 100%);
-}
-
-.provider-models-title {
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-.provider-models-title i {
- font-size: 1.125rem;
- color: var(--primary-color);
-}
-
-.provider-models-title h3 {
- margin: 0;
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.provider-models-count {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-width: 1.5rem;
- height: 1.5rem;
- padding: 0 0.4rem;
- background: var(--primary-color);
- color: white;
- border-radius: 9999px;
- font-size: 0.7rem;
- font-weight: 600;
-}
-
-.provider-models-toggle {
- color: var(--text-secondary);
- transition: var(--transition);
-}
-
-.provider-models-header.collapsed .provider-models-toggle {
- transform: rotate(-90deg);
-}
-
-/* 模型列表内容 */
-.provider-models-content {
- padding: 0.875rem 1.25rem;
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
- gap: 0.625rem;
-}
-
-.provider-models-content.collapsed {
- display: none;
-}
-
-/* 单个模型项 */
-.model-item {
- display: flex;
- align-items: center;
- gap: 0.625rem;
- padding: 0.625rem 0.875rem;
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: var(--transition);
- position: relative;
- overflow: hidden;
-}
-
-.model-item:hover {
- border-color: var(--primary-color);
- background: var(--primary-10);
- transform: translateY(-2px);
- box-shadow: var(--shadow-sm);
-}
-
-.model-item:active {
- transform: translateY(0);
-}
-
-.model-item-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 1.75rem;
- height: 1.75rem;
- background: var(--primary-10);
- border-radius: var(--radius-sm);
- color: var(--primary-color);
- flex-shrink: 0;
- font-size: 0.75rem;
-}
-
-.model-item-name {
- flex: 1;
- font-size: 0.8rem;
- font-weight: 500;
- color: var(--text-primary);
- word-break: break-all;
-}
-
-.model-item-copy {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 1.25rem;
- height: 1.25rem;
- color: var(--text-tertiary);
- opacity: 0;
- transition: var(--transition);
- font-size: 0.75rem;
-}
-
-.model-item:hover .model-item-copy {
- opacity: 1;
- color: var(--primary-color);
-}
-
-/* 复制成功动画 */
-.model-item.copied {
- border-color: var(--success-color);
- background: var(--success-10);
-}
-
-.model-item.copied .model-item-copy {
- opacity: 1;
- color: var(--success-color);
-}
-
-.model-item.copied::after {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: var(--success-color);
- opacity: 0.1;
- animation: copyFlash 0.3s ease-out;
-}
-
-@keyframes copyFlash {
- 0% {
- opacity: 0.3;
- }
- 100% {
- opacity: 0;
- }
-}
-
-/* 空状态 */
-.models-empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 2.5rem;
- color: var(--text-secondary);
- text-align: center;
-}
-
-.models-empty i {
- font-size: 2.5rem;
- margin-bottom: 0.75rem;
- opacity: 0.5;
-}
-
-.models-empty p {
- margin: 0;
- font-size: 0.9rem;
-}
-
-/* 高亮说明样式 */
-.models-description .highlight-note {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.875rem 1rem;
- background: linear-gradient(135deg, var(--info-bg) 0%, var(--info-bg-light, var(--info-bg)) 100%);
- border: 1px solid var(--info-border);
- border-radius: 0.5rem;
- color: var(--info-text);
- font-weight: 500;
- width: 100%;
- box-sizing: border-box;
- font-size: 0.875rem;
-}
-
-.models-description .highlight-note i {
- color: var(--info-color, var(--info-text));
- font-size: 1.125rem;
- flex-shrink: 0;
-}
-
-.models-description .highlight-note span {
- flex: 1;
- text-align: center;
-}
-
-@media (max-width: 768px) {
- .provider-models-content {
- grid-template-columns: 1fr;
- }
-
- .provider-models-header {
- padding: 0.75rem 1rem;
- }
-
- .provider-models-title h3 {
- font-size: 0.9rem;
- }
-
- .model-item {
- padding: 0.5rem 0.75rem;
- }
-}
-
-/* 暗黑主题适配 */
-[data-theme="dark"] .stat-card {
- background: var(--bg-primary);
-}
-
-[data-theme="dark"] .provider-models-group {
- background: var(--bg-primary);
- border-color: var(--border-color);
-}
-
-[data-theme="dark"] .provider-models-header {
- background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
-}
-
-[data-theme="dark"] .provider-models-header:hover {
- background: linear-gradient(135deg, var(--primary-10) 0%, var(--bg-tertiary) 100%);
-}
-
-[data-theme="dark"] .model-item {
- background: var(--bg-secondary);
- border-color: var(--border-color);
-}
-
-[data-theme="dark"] .model-item:hover {
- background: var(--primary-10);
- border-color: var(--primary-color);
-}
-
-[data-theme="dark"] .models-description .highlight-note {
- background: linear-gradient(135deg, var(--info-bg) 0%, var(--info-bg-light, var(--info-bg)) 100%);
- border-color: var(--info-border);
- color: var(--info-text);
-}
-
-[data-theme="dark"] .models-container {
- background: var(--bg-secondary);
-}
-
-@media (max-width: 1024px) {
- .dashboard-top-row {
- flex-direction: column;
- }
-
- .dashboard-top-row .stats-grid {
- flex: none;
- }
-
- .dashboard-top-row .dashboard-contact .qr-code {
- width: 160px;
- height: 160px;
- }
-}
-
-@media (max-width: 768px) {
- .stats-grid {
- grid-template-columns: 1fr;
- }
- .contact-grid {
- grid-template-columns: 1fr;
- gap: 1.5rem;
- }
- .contact-card {
- padding: 1.5rem;
- }
- .qr-code {
- width: 180px;
- height: 180px;
- }
-}
-
-@media (max-width: 480px) {
- .qr-code {
- width: 150px;
- height: 150px;
- }
- .contact-card {
- padding: 1rem;
- }
-}
-
-/* 暗黑主题适配 */
-[data-theme="dark"] .stat-card {
- background: var(--bg-primary);
-}
-
-[data-theme="dark"] .status-healthy {
- background: var(--success-bg);
- color: var(--success-text);
-}
-
-[data-theme="dark"] .status-unhealthy {
- background: var(--danger-bg);
- color: var(--danger-text);
-}
-
-[data-theme="dark"] .update-badge {
- background: var(--warning-bg);
- color: var(--warning-text);
- border-color: var(--warning-border);
-}
-
-[data-theme="dark"] .contact-card {
- background: var(--bg-primary);
-}
-
-[data-theme="dark"] .qr-code {
- background: white;
-}
-
-[data-theme="dark"] .image-zoom-overlay {
- background: var(--neutral-shadow-95);
-}
diff --git a/static/components/section-dashboard.html b/static/components/section-dashboard.html
deleted file mode 100644
index 83a1b93517dafd516b84a7cfee766e9b967c58e8..0000000000000000000000000000000000000000
--- a/static/components/section-dashboard.html
+++ /dev/null
@@ -1,168 +0,0 @@
-
-
-
- 系统概览
-
-
-
-
-
-
-
-
- 版本号
-
-
- --
-
- --
-
-
-
-
-
- Node.js版本
-
-
- --
-
-
-
-
-
-
-
-
-
-
-
-
-
-
路径路由调用示例
-
通过不同路径路由访问不同的AI模型提供商,支持灵活的模型切换
-
-
-
-
-
使用提示
-
- - 即时切换: 通过修改URL路径即可切换不同的AI模型提供商
- - 客户端配置: 在Cherry-Studio、NextChat、Cline等客户端中设置API端点为对应路径
- - 跨协议调用: 支持OpenAI协议调用Claude模型,或Claude协议调用OpenAI模型
-
-
-
-
-
-
可用模型列表
-
-
-
- 点击模型名称可直接复制到剪贴板
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/static/components/section-guide.css b/static/components/section-guide.css
deleted file mode 100644
index e1b977e247d2cc739a6080a8c21ea7b5a4501946..0000000000000000000000000000000000000000
--- a/static/components/section-guide.css
+++ /dev/null
@@ -1,592 +0,0 @@
-/* Guide Section Styles */
-
-/* 操作流程图样式 */
-.process-flow {
- display: flex;
- flex-wrap: wrap;
- align-items: flex-start;
- justify-content: center;
- gap: 0.5rem;
- padding: 1.5rem 0;
-}
-
-.flow-step {
- display: flex;
- flex-direction: column;
- align-items: center;
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 12px;
- padding: 1.25rem;
- width: 200px;
- min-height: 200px;
- transition: all 0.3s ease;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
-}
-
-.flow-step:hover {
- transform: translateY(-4px);
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
- border-color: var(--primary-color);
-}
-
-.step-number {
- width: 40px;
- height: 40px;
- background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
- color: white;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.25rem;
- font-weight: 700;
- margin-bottom: 1rem;
- box-shadow: 0 4px 12px rgba(var(--primary-rgb, 59, 130, 246), 0.3);
-}
-
-.step-content {
- text-align: center;
- flex: 1;
-}
-
-.step-content h4 {
- font-size: 0.95rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0 0 0.5rem 0;
-}
-
-.step-content p {
- font-size: 0.8rem;
- color: var(--text-secondary);
- margin: 0 0 0.75rem 0;
- line-height: 1.4;
-}
-
-.step-content ul {
- list-style: none;
- padding: 0;
- margin: 0;
- text-align: left;
-}
-
-.step-content ul li {
- font-size: 0.75rem;
- color: var(--text-secondary);
- padding: 0.25rem 0;
- padding-left: 1rem;
- position: relative;
-}
-
-.step-content ul li::before {
- content: "•";
- position: absolute;
- left: 0;
- color: var(--primary-color);
- font-weight: bold;
-}
-
-.step-content ul li code {
- font-size: 0.7rem;
- background: var(--bg-tertiary);
- padding: 0.1rem 0.3rem;
- border-radius: 3px;
-}
-
-/* 并行分支样式 */
-.flow-step-branch {
- width: 280px;
-}
-
-.branch-options {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
- margin-top: 0.5rem;
-}
-
-.branch-option {
- background: var(--bg-tertiary);
- padding: 0.75rem;
- border-radius: 8px;
- border: 1px solid var(--border-color);
-}
-
-.branch-label {
- font-size: 0.8rem;
- font-weight: 600;
- color: var(--primary-color);
- margin-bottom: 0.5rem;
-}
-
-.branch-divider {
- text-align: center;
- font-size: 0.75rem;
- color: var(--text-muted);
- font-weight: 500;
- position: relative;
-}
-
-.branch-divider::before,
-.branch-divider::after {
- content: "";
- position: absolute;
- top: 50%;
- width: 40%;
- height: 1px;
- background: var(--border-color);
-}
-
-.branch-divider::before {
- left: 0;
-}
-
-.branch-divider::after {
- right: 0;
-}
-
-.branch-option ul {
- margin: 0;
-}
-
-.branch-option ul li {
- font-size: 0.7rem;
- padding: 0.15rem 0;
-}
-
-.flow-arrow {
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.5rem;
- color: var(--primary-color);
- font-weight: bold;
- padding: 0 0.25rem;
- margin-top: 80px;
-}
-
-/* 响应式流程图 */
-@media (max-width: 1200px) {
- .process-flow {
- gap: 0.75rem;
- }
-
- .flow-step {
- width: 180px;
- min-height: 180px;
- padding: 1rem;
- }
-
- .flow-arrow {
- font-size: 1.25rem;
- margin-top: 70px;
- }
-}
-
-@media (max-width: 992px) {
- .process-flow {
- flex-direction: column;
- align-items: center;
- }
-
- .flow-step {
- width: 100%;
- max-width: 400px;
- min-height: auto;
- flex-direction: row;
- gap: 1rem;
- }
-
- .step-number {
- margin-bottom: 0;
- flex-shrink: 0;
- }
-
- .step-content {
- text-align: left;
- }
-
- .flow-arrow {
- transform: rotate(90deg);
- margin-top: 0;
- padding: 0.5rem 0;
- }
-}
-
-/* Guide Panel */
-.guide-panel {
- background: var(--bg-primary);
- padding: 1.5rem;
- border-radius: var(--radius-lg);
- box-shadow: var(--shadow-md);
- margin-bottom: 1.5rem;
- border: 1px solid var(--border-color);
-}
-
-.guide-panel h3 {
- font-size: 1.25rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0 0 1rem 0;
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.guide-panel h3 i {
- color: var(--primary-color);
-}
-
-.guide-content {
- color: var(--text-secondary);
- line-height: 1.6;
-}
-
-.guide-content > p {
- margin-bottom: 1.5rem;
-}
-
-/* Feature Grid */
-.feature-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
- gap: 1rem;
- margin-top: 1rem;
-}
-
-.feature-card {
- background: var(--bg-secondary);
- padding: 1.25rem;
- border-radius: var(--radius-md);
- border: 1px solid var(--border-color);
- transition: var(--transition);
-}
-
-.feature-card:hover {
- transform: translateY(-2px);
- box-shadow: var(--shadow-md);
- border-color: var(--primary-30);
-}
-
-.feature-icon {
- width: 48px;
- height: 48px;
- border-radius: var(--radius-md);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.25rem;
- color: var(--primary-color);
- background: var(--primary-10);
- margin-bottom: 0.75rem;
-}
-
-.feature-card h4 {
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0 0 0.5rem 0;
-}
-
-.feature-card p {
- font-size: 0.875rem;
- color: var(--text-secondary);
- margin: 0;
-}
-
-/* Guide Provider List - 使用 guide- 前缀避免与 section-providers.css 冲突 */
-.guide-provider-list {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: 1rem;
-}
-
-.guide-provider-item {
- background: var(--bg-secondary);
- padding: 1rem;
- border-radius: var(--radius-md);
- border: 1px solid var(--border-color);
- transition: var(--transition);
-}
-
-.guide-provider-item:hover {
- border-color: var(--primary-30);
- box-shadow: var(--shadow-sm);
-}
-
-.guide-provider-header {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- margin-bottom: 0.5rem;
- flex-wrap: wrap;
-}
-
-.guide-provider-icon {
- font-size: 1.25rem;
-}
-
-.guide-provider-icon.gemini { color: #4285f4; }
-.guide-provider-icon.antigravity { color: #ea4335; }
-.guide-provider-icon.kiro { color: #9b59b6; }
-.guide-provider-icon.qwen { color: #ff6a00; }
-.guide-provider-icon.claude { color: #d97706; }
-.guide-provider-icon.openai { color: #10a37f; }
-.guide-provider-icon.iflow { color: #3b82f6; }
-
-.guide-provider-name {
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.guide-provider-badge {
- font-size: 0.7rem;
- padding: 0.15rem 0.5rem;
- border-radius: 9999px;
- font-weight: 500;
-}
-
-.guide-provider-badge.oauth {
- background: var(--primary-10);
- color: var(--primary-color);
-}
-
-.guide-provider-badge.experimental {
- background: var(--warning-bg);
- color: var(--warning-text);
-}
-
-.guide-provider-badge.free {
- background: var(--success-bg);
- color: var(--success-text);
-}
-
-.guide-provider-badge.official {
- background: var(--info-bg);
- color: var(--info-text);
-}
-
-.guide-provider-desc {
- font-size: 0.875rem;
- color: var(--text-secondary);
- margin: 0;
-}
-
-/* Client Config List */
-.client-config-list {
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
-}
-
-.client-config-item {
- background: var(--bg-secondary);
- padding: 1.25rem;
- border-radius: var(--radius-md);
- border: 1px solid var(--border-color);
-}
-
-.client-config-item h4 {
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0 0 1rem 0;
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.client-config-item h4 i {
- color: var(--primary-color);
-}
-
-.config-steps ol {
- margin: 0;
- padding-left: 1.5rem;
-}
-
-.config-steps ol li {
- margin-bottom: 0.5rem;
- color: var(--text-secondary);
-}
-
-.config-steps ol li:last-child {
- margin-bottom: 0;
-}
-
-.config-steps code {
- background: var(--bg-tertiary);
- padding: 0.15rem 0.4rem;
- border-radius: var(--radius-sm);
- font-size: 0.85rem;
- color: var(--primary-color);
-}
-
-.config-steps pre {
- background: var(--bg-tertiary);
- padding: 1rem;
- border-radius: var(--radius-md);
- overflow-x: auto;
- margin: 0;
-}
-
-.config-steps pre code {
- background: none;
- padding: 0;
- font-size: 0.8rem;
- color: var(--text-primary);
- white-space: pre;
-}
-
-/* Guide Note */
-.guide-note {
- display: flex;
- align-items: flex-start;
- gap: 0.75rem;
- background: var(--info-bg);
- padding: 1rem;
- border-radius: var(--radius-md);
- margin-top: 1rem;
- border: 1px solid var(--info-border);
-}
-
-.guide-note i {
- color: var(--info-text);
- font-size: 1rem;
- margin-top: 0.1rem;
-}
-
-.guide-note span {
- color: var(--info-text);
- font-size: 0.875rem;
- line-height: 1.5;
-}
-
-/* API Example */
-.api-example {
- background: var(--bg-secondary);
- padding: 1rem;
- border-radius: var(--radius-md);
- margin-bottom: 1rem;
- border: 1px solid var(--border-color);
-}
-
-.api-example h4 {
- font-size: 0.9rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0 0 0.75rem 0;
-}
-
-.api-example pre {
- background: var(--bg-tertiary);
- padding: 1rem;
- border-radius: var(--radius-md);
- overflow-x: auto;
- margin: 0;
-}
-
-.api-example pre code {
- font-size: 0.8rem;
- color: var(--text-primary);
- white-space: pre;
-}
-
-/* Model Prefix List */
-.model-prefix-list {
- background: var(--bg-secondary);
- padding: 1rem;
- border-radius: var(--radius-md);
- border: 1px solid var(--border-color);
-}
-
-.model-prefix-list h4 {
- font-size: 0.9rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0 0 0.75rem 0;
-}
-
-.model-prefix-list ul {
- margin: 0;
- padding-left: 1.5rem;
-}
-
-.model-prefix-list ul li {
- margin-bottom: 0.5rem;
- color: var(--text-secondary);
-}
-
-.model-prefix-list ul li:last-child {
- margin-bottom: 0;
-}
-
-.model-prefix-list code {
- background: var(--bg-tertiary);
- padding: 0.15rem 0.4rem;
- border-radius: var(--radius-sm);
- font-size: 0.85rem;
- color: var(--primary-color);
- font-weight: 600;
-}
-
-/* FAQ List */
-.faq-list {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.faq-item {
- background: var(--bg-secondary);
- padding: 1rem;
- border-radius: var(--radius-md);
- border: 1px solid var(--border-color);
-}
-
-.faq-question {
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 0.5rem;
-}
-
-.faq-answer {
- color: var(--text-secondary);
- font-size: 0.9rem;
- line-height: 1.6;
-}
-
-/* Responsive */
-@media (max-width: 768px) {
- .feature-grid {
- grid-template-columns: 1fr;
- }
-
- .guide-provider-list {
- grid-template-columns: 1fr;
- }
-
- .guide-panel {
- padding: 1rem;
- }
-}
-
-/* Dark Theme */
-[data-theme="dark"] .guide-panel {
- background: var(--bg-primary);
-}
-
-[data-theme="dark"] .feature-card,
-[data-theme="dark"] .guide-provider-item,
-[data-theme="dark"] .client-config-item,
-[data-theme="dark"] .api-example,
-[data-theme="dark"] .model-prefix-list,
-[data-theme="dark"] .faq-item {
- background: var(--bg-secondary);
-}
-
-[data-theme="dark"] .config-steps pre,
-[data-theme="dark"] .api-example pre {
- background: var(--bg-tertiary);
-}
diff --git a/static/components/section-guide.html b/static/components/section-guide.html
deleted file mode 100644
index b7c39197f9162992dae1ecf8d70b35a5649f042d..0000000000000000000000000000000000000000
--- a/static/components/section-guide.html
+++ /dev/null
@@ -1,210 +0,0 @@
-
-
-
- 使用指南
-
-
-
-
项目简介
-
-
AIClient2API 是一个突破客户端限制的 API 代理服务,将 Gemini、Antigravity、Qwen Code、Kiro 等原本仅限客户端内使用的免费大模型,转换为可供任何应用调用的标准 OpenAI 兼容接口。
-
-
-
-
统一接入
-
通过标准 OpenAI 兼容协议,一次配置即可接入多种大模型
-
-
-
-
突破限制
-
利用 OAuth 授权机制,有效突破免费 API 速率和配额限制
-
-
-
-
协议转换
-
支持 OpenAI、Claude、Gemini 三大协议间的智能转换
-
-
-
-
账号池管理
-
支持多账号轮询、自动故障转移和配置降级
-
-
-
-
-
-
-
-
操作流程图
-
-
-
-
1
-
-
配置管理
-
在「配置管理」页面设置基本参数
-
- - 设置 API Key
- - 选择启动时初始化的模型提供商
- - 配置高级选项
-
-
-
-
-
→
-
-
-
2
-
-
生成授权
-
在「提供商池管理」页面生成 OAuth 授权
-
-
-
方式一:OAuth 授权
-
- - 点击「生成授权」按钮
- - 在弹窗中完成 OAuth 登录
- - 凭据自动保存
-
-
-
或
-
-
方式二:手动上传
-
- - 新增提供商节点
- - 上传已有的授权文件
- - 手动关联凭据路径
-
-
-
或
-
-
方式三:对接提供商 API
-
- - 在「配置管理」设置 API Key 和端点
- - 系统自动识别并对接
- - 无需手动上传凭据
-
-
-
-
-
-
-
→
-
-
-
3
-
-
管理凭据
-
在「凭据文件管理」页面查看和管理凭据
-
- - 查看已生成的凭据文件
- - 自动关联到提供商池
- - 删除无效凭据
-
-
-
-
-
→
-
-
-
4
-
-
开始使用
-
在「仪表盘」查看路由示例并开始调用 API
-
- - 查看路由调用示例
- - 复制 API 端点地址
- - 在客户端中配置使用
-
-
-
-
-
-
-
-
-
-
客户端配置指南
-
-
以下是常见 AI 客户端的配置方法,将 API 端点设置为本服务地址即可使用:
-
-
-
-
Cherry Studio
-
-
- - 打开设置 → 模型服务商
- - 添加自定义服务商
- - 设置 API 地址为:
http://localhost:3000/{provider}/v1
- - 填入 API Key(配置文件中的 REQUIRED_API_KEY)
-
-
-
-
-
-
Cline / Continue
-
-
- - 打开 VS Code 设置
- - 搜索 Cline 或 Continue 配置
- - 设置 API Base URL 为:
http://localhost:3000/{provider}/v1
- - 填入 API Key 和模型名称
-
-
-
-
-
-
通用 cURL 调用
-
-
curl http://localhost:3000/{provider}/v1/chat/completions \
- -H "Content-Type: application/json" \
- -H "Authorization: Bearer YOUR_API_KEY" \
- -d '{
- "model": "模型名称",
- "messages": [{"role": "user", "content": "Hello!"}],
- "max_tokens": 1000
- }'
-
-
-
-
-
-
- 提示:将 {provider} 替换为实际的提供商路径,如 gemini-cli-oauth、claude-kiro-oauth 等。可在仪表盘的路由示例中查看完整路径。
-
-
-
-
-
-
-
常见问题
-
-
-
-
Q: 请求返回 404 错误怎么办?
-
A: 检查接口路径是否正确。某些客户端会自动在 Base URL 后追加路径,导致路径重复。请查看控制台中的实际请求 URL,移除多余的路径部分。
-
-
-
Q: 请求返回 429 错误怎么办?
-
A: 429 表示请求频率过高。建议配置多个账号到提供商池,启用轮询机制;或配置 Fallback 链实现跨类型降级。
-
-
-
Q: OAuth 授权失败怎么办?
-
A: 确保 OAuth 回调端口可访问(Gemini: 8085, Antigravity: 8086, Kiro: 19876-19880)。Docker 用户需确保已正确映射这些端口。
-
-
-
Q: 流式响应中断怎么办?
-
A: 检查网络稳定性,增加客户端请求超时时间。如使用代理,确保代理支持长连接。
-
-
-
Q: 请求返回 "No available and healthy providers" 错误怎么办?
-
A: 这表示对应类型的提供商都不可用。请在"提供商池"页面检查提供商健康状态,确认 OAuth 凭据未过期,或配置 Fallback 链实现自动切换到备用提供商。
-
-
-
Q: 请求返回 403 Forbidden 错误怎么办?
-
A: 403 表示访问被拒绝。首先检查"提供商池"页面中节点状态,如果节点健康检查正常,可以忽略此报错。其他可能原因包括:账号权限不足、API Key 权限受限、地区访问限制、凭据已失效等。
-
-
-
-
-
\ No newline at end of file
diff --git a/static/components/section-logs.css b/static/components/section-logs.css
deleted file mode 100644
index aa338255e26382fbbdd6148656dc08da580dd538..0000000000000000000000000000000000000000
--- a/static/components/section-logs.css
+++ /dev/null
@@ -1,89 +0,0 @@
-/* 日志 */
-.logs-controls {
- display: flex;
- gap: 1rem;
- margin-bottom: 1rem;
-}
-
-.logs-container {
- background: var(--code-bg);
- color: var(--code-text);
- padding: 1.5rem;
- border-radius: 0.5rem;
- height: 800px;
- overflow-y: auto;
- font-family: 'Courier New', monospace;
- font-size: 0.875rem;
- box-shadow: var(--shadow-md);
-}
-
-.log-entry {
- margin-bottom: 0.5rem;
- padding: 0.25rem 0;
-}
-
-.log-time {
- color: var(--log-time);
-}
-
-.log-level-info {
- color: var(--log-info);
-}
-
-.log-level-error {
- color: var(--log-error);
-}
-
-.log-level-warn {
- color: var(--log-warn);
-}
-
-/* 系统信息卡片样式 */
-.system-info {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: 1.5rem;
-}
-
-.info-card {
- background: var(--bg-primary);
- padding: 1.5rem;
- border-radius: 0.5rem;
- box-shadow: var(--shadow-md);
-}
-
-.info-card h3 {
- font-size: 1.125rem;
- font-weight: 600;
- margin-bottom: 1rem;
- color: var(--text-primary);
-}
-
-.info-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0.5rem 0;
- border-bottom: 1px solid var(--border-color);
-}
-
-.info-row:last-child {
- border-bottom: none;
-}
-
-.info-label {
- color: var(--text-secondary);
- font-size: 0.875rem;
-}
-
-.info-value {
- color: var(--text-primary);
- font-weight: 500;
- font-size: 0.875rem;
-}
-
-/* 暗黑主题适配 */
-[data-theme="dark"] .logs-container {
- background: var(--code-bg);
- color: var(--code-text);
-}
diff --git a/static/components/section-logs.html b/static/components/section-logs.html
deleted file mode 100644
index b8bb791eed71993caa028300f206ecbc4f532d03..0000000000000000000000000000000000000000
--- a/static/components/section-logs.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
- 实时日志
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/static/components/section-plugins.css b/static/components/section-plugins.css
deleted file mode 100644
index 7924e4a05020f1b30db82b88e6a689755e5b3651..0000000000000000000000000000000000000000
--- a/static/components/section-plugins.css
+++ /dev/null
@@ -1,151 +0,0 @@
-/* 插件管理样式 */
-.plugins-panel {
- background: var(--bg-primary);
- padding: 2rem;
- border-radius: 0.5rem;
- box-shadow: var(--shadow-md);
-}
-
-.plugins-description {
- margin-bottom: 2rem;
-}
-
-.plugins-stats {
- margin-bottom: 2rem;
-}
-
-.plugins-controls {
- display: flex;
- justify-content: flex-end;
- margin-bottom: 1.5rem;
-}
-
-.plugins-list-container {
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- overflow: hidden;
-}
-
-.plugins-list {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
- gap: 1.5rem;
- padding: 1.5rem;
-}
-
-.plugin-card {
- background: var(--bg-primary);
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- padding: 1.5rem;
- transition: var(--transition);
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.plugin-card:hover {
- transform: translateY(-2px);
- box-shadow: var(--shadow-md);
- border-color: var(--primary-color);
-}
-
-.plugin-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
-}
-
-.plugin-title h3 {
- margin: 0;
- font-size: 1.125rem;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.plugin-version {
- font-size: 0.75rem;
- color: var(--text-secondary);
- background: var(--bg-tertiary);
- padding: 0.125rem 0.5rem;
- border-radius: 9999px;
- margin-top: 0.25rem;
- display: inline-block;
-}
-
-.plugin-description {
- font-size: 0.875rem;
- color: var(--text-secondary);
- line-height: 1.5;
- flex: 1;
-}
-
-.plugin-badges {
- display: flex;
- gap: 0.5rem;
- flex-wrap: wrap;
-}
-
-.plugin-badge {
- font-size: 0.75rem;
- padding: 0.25rem 0.5rem;
- border-radius: 0.25rem;
- font-weight: 500;
- text-transform: uppercase;
- letter-spacing: 0.05em;
-}
-
-.plugin-badge.middleware { background: var(--info-bg); color: var(--info-text); }
-.plugin-badge.routes { background: var(--success-bg); color: var(--success-text); }
-.plugin-badge.hooks { background: var(--warning-bg); color: var(--warning-text); }
-
-.plugin-status {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- font-size: 0.875rem;
- font-weight: 500;
- padding-top: 1rem;
- border-top: 1px solid var(--border-color);
-}
-
-.plugin-card.enabled .plugin-status {
- color: var(--success-color);
-}
-
-.plugin-card.disabled .plugin-status {
- color: var(--text-secondary);
-}
-
-.plugin-card.disabled {
- opacity: 0.8;
- background: var(--bg-secondary);
-}
-
-.plugins-loading,
-.plugins-empty {
- text-align: center;
- padding: 3rem;
- color: var(--text-secondary);
-}
-
-.plugins-empty {
- display: none;
- flex-direction: column;
- align-items: center;
- gap: 1rem;
-}
-
-.plugins-empty i {
- font-size: 3rem;
- opacity: 0.5;
-}
-
-/* 暗黑主题适配 */
-[data-theme="dark"] .plugins-list-container { background: var(--bg-secondary); }
-[data-theme="dark"] .plugin-card { background: var(--bg-primary); }
-[data-theme="dark"] .plugin-card.disabled { background: var(--bg-tertiary); }
-[data-theme="dark"] .plugin-badge.middleware { background: var(--info-bg); color: var(--info-text); }
-[data-theme="dark"] .plugin-badge.routes { background: var(--success-bg); color: var(--success-text); }
-[data-theme="dark"] .plugin-badge.hooks { background: var(--warning-bg); color: var(--warning-text); }
diff --git a/static/components/section-plugins.html b/static/components/section-plugins.html
deleted file mode 100644
index e9a042356fe73e4e2bfaf78abba99bac522f6cbf..0000000000000000000000000000000000000000
--- a/static/components/section-plugins.html
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
- 插件管理
-
-
-
-
- 插件系统允许您扩展系统功能,启用或禁用插件需要重启服务才能生效
-
-
-
-
-
-
-
-
-
-
-
-
-
- 正在加载插件列表...
-
-
-
-
-
-
\ No newline at end of file
diff --git a/static/components/section-providers.css b/static/components/section-providers.css
deleted file mode 100644
index c14907d1c3caebd4a6642b235e2e0dfa3a820c07..0000000000000000000000000000000000000000
--- a/static/components/section-providers.css
+++ /dev/null
@@ -1,1458 +0,0 @@
-/* 统计卡片 */
-.stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: 1.5rem;
- margin-bottom: 2rem;
-}
-
-.stat-card {
- background: var(--bg-primary);
- padding: 1.5rem;
- border-radius: var(--radius-xl);
- border: 1px solid var(--border-color);
- box-shadow: var(--shadow-sm);
- display: flex;
- align-items: center;
- gap: 1.25rem;
- transition: var(--transition);
- position: relative;
- overflow: hidden;
-}
-
-.stat-card:hover {
- transform: translateY(-4px);
- box-shadow: var(--shadow-lg);
- border-color: var(--primary-30);
-}
-
-.stat-icon {
- width: 56px;
- height: 56px;
- border-radius: var(--radius-lg);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.5rem;
- color: var(--primary-color);
- background: var(--primary-10);
- flex-shrink: 0;
-}
-
-.stat-info h3 {
- font-size: 2rem;
- font-weight: 700;
- margin-bottom: 0.25rem;
-}
-
-.stat-info p {
- color: var(--text-secondary);
- font-size: 0.875rem;
-}
-
-/* 提供商列表 */
-.providers-container {
- background: var(--bg-primary);
- padding: 1.5rem;
- border-radius: var(--radius-xl);
- box-shadow: var(--shadow-sm);
- border: 1px solid var(--border-color);
-}
-
-.providers-list {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-/* 模态框中的提供商列表 */
-.provider-list {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.provider-item {
- border: 1px solid var(--border-color);
- border-radius: var(--radius-lg);
- padding: 1.5rem;
- transition: var(--transition);
- background: var(--bg-secondary);
-}
-
-.provider-item:hover {
- border-color: var(--primary-color);
- box-shadow: var(--shadow-md);
- background: var(--bg-primary);
-}
-
-.provider-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1rem;
-}
-
-.provider-name {
- font-size: 1.125rem;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.provider-header-right {
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-.provider-status {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.25rem 0.75rem;
- border-radius: 9999px;
- font-size: 0.75rem;
- font-weight: 500;
-}
-
-/* Path Routing Examples Panel */
-.routing-examples-panel {
- background: var(--bg-primary);
- padding: 1.5rem;
- border-radius: 0.5rem;
- box-shadow: var(--shadow-md);
- margin-top: 1.5rem;
- margin-bottom: 1.5rem;
-}
-
-.routing-examples-panel h3 {
- font-size: 1.25rem;
- font-weight: 600;
- margin-bottom: 0.5rem;
- color: var(--text-primary);
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.routing-examples-panel h3 i {
- color: var(--primary-color);
-}
-
-.routing-description {
- color: var(--text-secondary);
- font-size: 0.875rem;
- margin-bottom: 1.5rem;
- line-height: 1.5;
-}
-
-.routing-examples-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
- gap: 1.5rem;
- margin-bottom: 2rem;
-}
-
-.routing-example-card {
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- overflow: hidden;
- transition: var(--transition);
- background: var(--bg-secondary);
-}
-
-.routing-example-card:hover {
- border-color: var(--primary-color);
- box-shadow: var(--shadow-md);
- transform: translateY(-2px);
-}
-
-.routing-card-header {
- background: var(--bg-primary);
- padding: 1rem 1.5rem;
- display: flex;
- align-items: center;
- gap: 0.75rem;
- border-bottom: 1px solid var(--border-color);
-}
-
-.routing-card-header i {
- font-size: 1.25rem;
- color: var(--primary-color);
-}
-
-.routing-card-header h4 {
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0;
- flex: 1;
-}
-
-.provider-badge {
- padding: 0.25rem 0.5rem;
- border-radius: 9999px;
- font-size: 0.75rem;
- font-weight: 500;
- text-transform: uppercase;
- letter-spacing: 0.05em;
-}
-
-.provider-badge.official { background: var(--info-bg); color: var(--info-text); }
-.provider-badge.oauth { background: var(--success-bg); color: var(--success-text); }
-.provider-badge.responses { background: var(--warning-bg); color: var(--warning-text); }
-
-.routing-card-content {
- padding: 1.5rem;
-}
-
-/* 协议标签样式 */
-.protocol-tabs {
- display: flex;
- margin-bottom: 1rem;
- border-bottom: 1px solid var(--border-color);
-}
-
-.protocol-tab {
- background: none;
- border: none;
- padding: 0.75rem 1rem;
- cursor: pointer;
- font-size: 0.875rem;
- font-weight: 500;
- color: var(--text-secondary);
- border-bottom: 2px solid transparent;
- transition: var(--transition);
- position: relative;
-}
-
-.protocol-tab:hover {
- color: var(--primary-color);
- background: var(--bg-tertiary);
-}
-
-.protocol-tab.active {
- color: var(--primary-color);
- border-bottom-color: var(--primary-color);
- background: var(--bg-secondary);
-}
-
-/* 协议内容区域 */
-.protocol-content {
- display: none;
- animation: fadeIn 0.3s ease;
-}
-
-.provider-stats {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
- gap: 1rem;
- margin-top: 1rem;
-}
-
-.provider-stat {
- display: flex;
- flex-direction: column;
-}
-
-.provider-stat-label {
- font-size: 0.75rem;
- color: var(--text-secondary);
- margin-bottom: 0.25rem;
-}
-
-.provider-stat-value {
- font-size: 1.125rem;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.protocol-content.active {
- display: block;
-}
-
-.endpoint-info {
- margin-bottom: 1rem;
-}
-
-.endpoint-info label {
- display: block;
- font-size: 0.875rem;
- font-weight: 500;
- color: var(--text-secondary);
- margin-bottom: 0.5rem;
-}
-
-.endpoint-path {
- display: inline-block;
- background: var(--bg-primary);
- padding: 0.5rem 0.75rem;
- border-radius: 0.375rem;
- font-family: 'Courier New', monospace;
- font-size: 0.875rem;
- color: var(--text-primary);
- border: 1px solid var(--border-color);
- position: relative;
- padding-right: 2.5rem;
-}
-
-.copy-btn {
- position: absolute;
- right: 0.5rem;
- top: 50%;
- transform: translateY(-50%);
- background: none;
- border: none;
- color: var(--text-secondary);
- cursor: pointer;
- padding: 0.25rem;
- border-radius: 0.25rem;
- transition: var(--transition);
-}
-
-.copy-btn:hover {
- background: var(--bg-tertiary);
- color: var(--primary-color);
-}
-
-.usage-example {
- margin-top: 1rem;
-}
-
-.usage-example label {
- display: block;
- font-size: 0.875rem;
- font-weight: 500;
- color: var(--text-secondary);
- margin-bottom: 0.5rem;
-}
-
-.usage-example pre {
- background: var(--code-bg);
- color: var(--code-text);
- padding: 1rem;
- border-radius: 0.375rem;
- overflow-x: auto;
- font-size: 0.75rem;
- line-height: 1.4;
- margin: 0;
-}
-
-.usage-example code {
- font-family: 'Courier New', monospace;
- white-space: pre-wrap;
-}
-
-.routing-tips {
- background: var(--bg-secondary);
- padding: 1.5rem;
- border-radius: 0.5rem;
- border-left: 4px solid var(--primary-color);
-}
-
-.routing-tips h4 {
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 1rem;
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.routing-tips h4 i {
- color: var(--warning-color);
-}
-
-.routing-tips ul {
- margin: 0;
- padding-left: 1.5rem;
-}
-
-.routing-tips li {
- margin-bottom: 0.75rem;
- color: var(--text-secondary);
- font-size: 0.875rem;
- line-height: 1.5;
-}
-
-.routing-tips code {
- background: var(--bg-primary);
- padding: 0.125rem 0.25rem;
- border-radius: 0.25rem;
- font-family: 'Courier New', monospace;
- font-size: 0.75rem;
- color: var(--primary-color);
-}
-
-/* 提供商类型显示 */
-.provider-type-text {
- font-size: 16px;
- font-weight: 600;
- color: var(--secondary-color);
- cursor: pointer;
- padding: 4px 8px;
- border-radius: 4px;
- transition: all 0.3s ease;
-}
-
-.provider-type-text:hover {
- background: var(--bg-tertiary);
- color: var(--primary-color);
-}
-
-/* 模态框样式 */
-.provider-modal {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.4);
- backdrop-filter: blur(8px);
- -webkit-backdrop-filter: blur(8px);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 1000;
- animation: fadeIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-.provider-modal-content {
- background: var(--bg-primary);
- border-radius: var(--radius-xl);
- width: 95%;
- max-width: 1200px;
- max-height: 85vh;
- overflow: hidden;
- box-shadow: var(--shadow-xl);
- animation: slideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
- border: 1px solid var(--border-color);
-}
-
-.provider-modal-header {
- padding: 1.5rem;
- border-bottom: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: var(--bg-secondary);
-}
-
-.provider-modal-header h3 {
- margin: 0;
- color: var(--neutral-700);
- font-size: 20px;
- font-weight: 600;
-}
-
-.modal-close {
- background: none;
- border: none;
- font-size: 1.5rem;
- cursor: pointer;
- color: var(--neutral-500);
- padding: 0.5rem;
- border-radius: 50%;
- transition: var(--transition);
- width: 2rem;
- height: 2rem;
- display: flex;
- align-items: center;
- justify-content: center;
- line-height: 1;
-}
-
-.modal-close:hover {
- background: var(--neutral-200);
- color: var(--neutral-600);
- transform: rotate(90deg);
-}
-
-.modal-cancel {
- padding: 0.75rem 1.5rem;
- background: var(--bg-tertiary);
- color: var(--text-primary);
- border: 1px solid var(--border-color);
- border-radius: 0.375rem;
- font-size: 0.875rem;
- font-weight: 500;
- cursor: pointer;
- transition: var(--transition);
-}
-
-.modal-cancel:hover {
- background: var(--neutral-200);
- border-color: var(--neutral-500);
- transform: translateY(-1px);
-}
-
-/* 授权信息样式 */
-.auth-info {
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
-}
-
-.auth-info p {
- margin: 0;
- font-size: 0.875rem;
- color: var(--text-secondary);
-}
-
-.auth-info strong {
- color: var(--text-primary);
- font-weight: 600;
-}
-
-.auth-url-container {
- position: relative;
- display: flex;
- align-items: center;
-}
-
-.auth-url-container .copy-btn {
- position: absolute;
- right: 0.5rem;
- top: 50%;
- transform: translateY(-50%);
- background: var(--primary-color);
- color: white;
- border: none;
- padding: 0.5rem;
- border-radius: 0.25rem;
- cursor: pointer;
- transition: var(--transition);
-}
-
-.auth-url-container .copy-btn:hover {
- background: var(--btn-primary-hover);
-}
-
-.provider-modal-body {
- padding: 24px;
- max-height: calc(85vh - 80px);
- overflow-y: auto;
-}
-
-.provider-summary {
- display: flex;
- gap: 24px;
- align-items: center;
- margin-bottom: 24px;
- padding: 20px;
- background: linear-gradient(135deg, var(--neutral-100) 0%, var(--bg-primary) 100%);
- border-radius: 12px;
- border: 1px solid var(--neutral-200);
-}
-
-.provider-summary-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 12px;
-}
-
-.provider-summary-item .label {
- font-size: 13px;
- color: var(--neutral-500);
- margin-bottom: 8px;
- font-weight: 500;
-}
-
-.provider-summary-item .value {
- font-size: 24px;
- font-weight: bold;
- color: var(--neutral-700);
-}
-
-.provider-summary-actions {
- margin-left: auto;
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-.provider-actions {
- margin-bottom: 24px;
-}
-
-.provider-item-detail {
- border: 1px solid var(--neutral-200);
- border-radius: 12px;
- margin-bottom: 16px;
- overflow: hidden;
- box-shadow: 0 2px 8px var(--neutral-shadow-md);
- transition: all 0.3s ease;
- position: relative;
-}
-
-.provider-item-detail:hover {
- box-shadow: 0 4px 16px var(--neutral-shadow-lg);
- transform: translateY(-1px);
-}
-
-.provider-item-header {
- padding: 20px;
- background: linear-gradient(135deg, var(--neutral-100) 0%, var(--bg-primary) 100%);
- display: flex;
- justify-content: space-between;
- align-items: center;
- cursor: pointer;
- transition: all 0.3s ease;
-}
-
-.provider-item-header:hover {
- background: linear-gradient(135deg, var(--neutral-200) 0%, var(--neutral-100) 100%);
-}
-
-.provider-info {
- flex: 1;
-}
-
-.provider-name {
- font-weight: 600;
- margin-bottom: 8px;
- color: var(--neutral-700);
- font-size: 15px;
-}
-
-.provider-meta {
- font-size: 13px;
- color: var(--neutral-500);
- line-height: 1.4;
-}
-
-.provider-health-meta {
- font-size: 12px;
- color: var(--neutral-800);
- margin-top: 4px;
- line-height: 1.4;
-}
-
-.provider-health-meta i {
- margin-right: 4px;
- opacity: 0.7;
-}
-
-.provider-error-info {
- margin-top: 8px;
- padding: 8px 12px;
- background: linear-gradient(135deg, var(--danger-bg-alt) 0%, var(--danger-bg-medium) 100%);
- border: 1px solid var(--danger-border-light);
- border-radius: 6px;
- font-size: 12px;
- display: flex;
- align-items: flex-start;
- gap: 8px;
-}
-
-.provider-error-info i {
- color: var(--danger-icon);
- flex-shrink: 0;
- margin-top: 2px;
-}
-
-.provider-error-info .error-label {
- color: var(--danger-label);
- font-weight: 600;
- white-space: nowrap;
-}
-
-.provider-error-info .error-message {
- color: var(--danger-text-dark);
- word-break: break-word;
- max-height: 60px;
- overflow-y: auto;
-}
-
-.provider-actions-group {
- display: flex;
- gap: 8px;
- align-items: center;
-}
-
-.btn-edit {
- background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
- color: var(--white);
- box-shadow: 0 2px 8px var(--primary-30);
-}
-
-.btn-edit:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px var(--primary-40);
-}
-
-.btn-delete {
- background: linear-gradient(135deg, var(--danger-alt) 0%, var(--danger-secondary) 100%);
- color: var(--white);
- box-shadow: 0 2px 8px var(--danger-30);
-}
-
-.btn-delete:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px var(--danger-40);
-}
-
-.btn-quick-link {
- background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
- color: white;
- border: none;
- border-radius: 4px;
- padding: 4px 8px;
- font-size: 11px;
- cursor: pointer;
- margin-left: 8px;
- display: inline-flex;
- align-items: center;
- gap: 4px;
- transition: all 0.2s ease;
- box-shadow: 0 2px 6px var(--indigo-30);
-}
-
-.btn-quick-link:hover {
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
- background: linear-gradient(135deg, var(--btn-primary-hover) 0%, var(--primary-color) 100%);
-}
-
-.btn-batch-link {
- background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
- color: white;
- border: none;
- border-radius: 6px;
- padding: 6px 12px;
- font-size: 12px;
- cursor: pointer;
- margin-left: 12px;
- display: inline-flex;
- align-items: center;
- gap: 6px;
- transition: all 0.2s ease;
- box-shadow: 0 2px 8px var(--indigo-30);
- font-weight: 500;
-}
-
-.btn-batch-link:hover {
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
- background: linear-gradient(135deg, var(--btn-primary-hover) 0%, var(--primary-color) 100%);
-}
-
-.btn-save {
- background: linear-gradient(135deg, var(--btn-success) 0%, var(--btn-success-secondary) 100%);
- color: var(--white);
- box-shadow: 0 2px 8px var(--danger-30);
-}
-
-.btn-cancel {
- background: linear-gradient(135deg, var(--neutral-500) 0%, var(--neutral-600) 100%);
- color: var(--white);
- box-shadow: 0 2px 8px var(--primary-30);
-}
-
-.provider-item-content {
- padding: 20px;
- display: none;
- border-top: 1px solid var(--neutral-200);
- background: var(--bg-primary);
-}
-
-.provider-item-content.expanded {
- display: block;
-}
-
-.config-item {
- display: flex;
- flex-direction: column;
-}
-
-.config-item label {
- font-size: 13px;
- color: var(--neutral-600);
- margin-bottom: 8px;
- font-weight: 500;
-}
-
-.config-item input, .config-item textarea, .config-item select {
- padding: 12px;
- border: 2px solid var(--neutral-200);
- border-radius: 8px;
- font-size: 13px;
- transition: all 0.3s ease;
-}
-
-.config-item input:focus, .config-item textarea:focus, .config-item select:focus {
- outline: none;
- border-color: var(--primary-color);
- box-shadow: 0 0 0 3px var(--primary-10);
-}
-
-.config-item input[readonly], .config-item select[disabled] {
- background: var(--neutral-100);
- color: var(--neutral-500);
-}
-
-/* 模态框中的文件上传输入框样式 */
-.config-item .file-input-group {
- position: relative;
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.config-item .file-input-group input {
- flex: 1;
- padding-right: 0.75rem;
- box-sizing: border-box;
-}
-
-/* 模态框中的密码输入框样式 */
-.config-item .password-input-wrapper {
- position: relative;
- width: 100%;
-}
-
-.config-item .password-input-wrapper input {
- width: 100%;
- padding-right: 2.5rem;
- box-sizing: border-box;
-}
-
-.config-item .password-toggle {
- position: absolute;
- right: 0.5rem;
- padding: 0.25rem;
- background: none;
- border: none;
- cursor: pointer;
- color: var(--text-secondary);
- transition: var(--transition);
- width: auto;
- height: auto;
- line-height: 1;
-}
-
-.add-provider-form {
- background: linear-gradient(135deg, var(--neutral-100) 0%, var(--bg-primary) 100%);
- padding: 24px;
- border-radius: 12px;
- margin-bottom: 24px;
- border: 1px solid var(--neutral-200);
-}
-
-.add-provider-form h4 {
- margin: 0 0 20px 0;
- color: var(--neutral-700);
- font-size: 18px;
- font-weight: 600;
-}
-
-.form-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: 20px;
- margin-bottom: 20px;
-}
-
-.form-grid.full-width {
- grid-column: 1 / -1;
-}
-
-/* 无提供商提示 */
-.no-providers {
- text-align: center;
- padding: 2rem;
- color: var(--text-secondary);
-}
-
-/* 健康状态高亮样式 */
-.provider-item-detail.unhealthy {
- border: 2px solid var(--warning-color);
- background: linear-gradient(135deg, var(--warning-bg) 0%, var(--bg-primary) 100%);
- box-shadow: 0 4px 12px var(--warning-15);
- animation: pulseWarning 2s infinite;
-}
-
-.provider-item-detail.unhealthy:hover {
- border-color: var(--warning-color);
- box-shadow: 0 6px 20px var(--warning-25);
- transform: translateY(-2px);
-}
-
-.provider-item-detail.unhealthy .provider-item-header {
- background: linear-gradient(135deg, var(--warning-bg) 0%, var(--bg-primary) 100%);
- border-bottom: 1px solid var(--warning-20);
-}
-
-.provider-item-detail.healthy {
- border: 1px solid var(--neutral-200);
- background: linear-gradient(135deg, var(--neutral-100) 0%, var(--bg-primary) 100%);
-}
-
-.provider-item-detail.healthy:hover {
- border-color: var(--primary-color);
- box-shadow: 0 4px 16px var(--neutral-shadow-lg);
-}
-
-.health-status {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- font-weight: 500;
- padding: 0.25rem 0.5rem;
- border-radius: 6px;
- transition: var(--transition);
-}
-
-.provider-item-detail.unhealthy .health-status {
- color: var(--warning-text);
- background: var(--warning-15);
-}
-
-.provider-item-detail.healthy .health-status {
- color: var(--success-text);
- background: var(--success-10);
-}
-
-@keyframes pulseWarning {
- 0%, 100% { box-shadow: 0 4px 12px var(--warning-15); }
- 50% { box-shadow: 0 4px 12px var(--warning-30); }
-}
-
-.provider-item-detail.unhealthy::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 4px;
- background: linear-gradient(135deg, var(--warning-color) 0%, var(--warning-color) 100%);
- border-radius: 0 2px 2px 0;
- z-index: 1;
-}
-
-/* 禁用提供商状态样式 */
-.provider-item-detail.disabled {
- opacity: 0.6;
- background: linear-gradient(135deg, var(--neutral-100) 0%, var(--bg-primary) 100%);
- border: 1px solid var(--neutral-300);
- position: relative;
-}
-
-.provider-item-detail.disabled::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 4px;
- background: linear-gradient(135deg, var(--neutral-500) 0%, var(--neutral-600) 100%);
- border-radius: 0 2px 2px 0;
- z-index: 1;
-}
-
-.provider-item-detail.disabled:hover {
- opacity: 0.8;
- border-color: var(--neutral-400);
- box-shadow: 0 2px 8px var(--neutral-shadow-lg);
- transform: none;
-}
-
-.provider-item-detail.disabled .provider-item-header {
- background: linear-gradient(135deg, var(--neutral-200) 0%, var(--bg-primary) 100%);
-}
-
-.provider-item-detail.disabled .provider-name {
- color: var(--neutral-500);
- text-decoration: line-through;
-}
-
-.disabled-status {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- font-weight: 500;
- padding: 0.25rem 0.5rem;
- border-radius: 6px;
- transition: var(--transition);
-}
-
-.provider-item-detail.disabled .disabled-status {
- color: var(--neutral-500);
- background: var(--primary-10);
-}
-
-.provider-item-detail:not(.disabled) .disabled-status {
- color: var(--primary-color);
- background: var(--success-10);
-}
-
-/* 禁用/启用按钮特殊样式 */
-.btn-warning {
- background: linear-gradient(135deg, var(--warning-color) 0%, var(--warning-text-dark) 100%);
- color: var(--white);
- box-shadow: 0 2px 8px var(--warning-30);
-}
-
-.btn-warning:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px var(--warning-40);
-}
-
-/* 健康检测按钮样式 */
-.btn-info {
- background: linear-gradient(135deg, var(--info-color) 0%, var(--info-color-dark) 100%);
- color: var(--white);
-}
-
-.btn-info:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px var(--info-hover);
-}
-
-/* 删除不健康节点按钮样式 */
-.provider-summary-actions .btn-danger {
- background: linear-gradient(135deg, var(--danger-alt) 0%, var(--danger-secondary) 100%);
- color: var(--white);
- border: none;
- box-shadow: 0 2px 8px var(--danger-30);
-}
-
-.provider-summary-actions .btn-danger:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px var(--danger-40);
- background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-alt) 100%);
-}
-
-/* 刷新不健康UUID按钮样式 */
-.provider-summary-actions .btn-secondary {
- background: linear-gradient(135deg, var(--neutral-500) 0%, var(--neutral-600) 100%);
- color: var(--white);
- border: none;
- box-shadow: 0 2px 8px var(--neutral-shadow-lg);
-}
-
-.provider-summary-actions .btn-secondary:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px var(--neutral-shadow-lg);
- background: linear-gradient(135deg, var(--neutral-600) 0%, var(--neutral-700) 100%);
-}
-
-/* 不支持的模型选择器样式 */
-.not-supported-models-section {
- grid-column: 1 / -1;
- margin-top: 16px;
-}
-
-.not-supported-models-section label {
- display: flex;
- align-items: center;
- gap: 8px;
- font-weight: 600;
- color: var(--neutral-700);
- margin-bottom: 12px;
-}
-
-.not-supported-models-section .help-text {
- font-size: 12px;
- font-weight: normal;
- color: var(--neutral-500);
- margin-left: 4px;
-}
-
-.not-supported-models-container {
- background: var(--neutral-100);
- border: 1px solid var(--neutral-300);
- border-radius: 8px;
- padding: 16px;
- min-height: 100px;
-}
-
-.models-loading {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- color: var(--neutral-500);
- padding: 20px;
-}
-
-.models-checkbox-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
- gap: 10px;
-}
-
-.model-checkbox-label {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 6px 10px;
- background: var(--white);
- border: 1px solid var(--neutral-300);
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.2s ease;
- min-width: 0;
-}
-
-.model-checkbox-label:hover {
- background: var(--neutral-200);
- border-color: var(--neutral-400);
-}
-
-.model-checkbox-label .model-name {
- font-size: 12px;
- color: var(--neutral-600);
- user-select: none;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.model-checkbox-label input[type="checkbox"]:checked + .model-name {
- color: var(--danger-alt);
- font-weight: 500;
-}
-
-/* 授权按钮样式 */
-.generate-auth-btn {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.25rem 0.75rem;
- background: var(--info-bg-light);
- color: var(--info-text-dark);
- border: none;
- border-radius: 9999px;
- font-size: 0.75rem;
- font-weight: 500;
- cursor: pointer;
- transition: var(--transition);
-}
-
-.generate-auth-btn:hover {
- background: var(--info-hover);
- color: var(--info-text-darker);
- transform: translateY(-1px);
-}
-
-/* 授权模态框样式 */
-.modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: var(--overlay-bg);
- backdrop-filter: blur(4px);
- display: none;
- justify-content: center;
- align-items: center;
- z-index: 1000;
- animation: fadeIn 0.3s ease;
-}
-
-.modal-content {
- background: var(--bg-primary);
- border-radius: 0.75rem;
- width: 90%;
- max-width: 600px;
- max-height: 85vh;
- overflow: hidden;
- box-shadow: 0 25px 80px var(--neutral-shadow-40);
- display: flex;
- flex-direction: column;
- animation: modalSlideIn 0.3s ease;
-}
-
-.modal-header {
- padding: 1.5rem 2rem;
- border-bottom: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: linear-gradient(135deg, var(--info-bg-alt) 0%, var(--white) 100%);
-}
-
-.modal-header h3 {
- margin: 0;
- font-size: 1.25rem;
- font-weight: 600;
- color: var(--text-primary);
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.modal-body {
- padding: 2rem;
- flex: 1;
- overflow-y: auto;
-}
-
-.modal-footer {
- padding: 1.5rem 2rem;
- border-top: 1px solid var(--border-color);
- display: flex;
- justify-content: flex-end;
- gap: 1rem;
- background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--white) 100%);
-}
-
-.open-auth-btn {
- padding: 0.75rem 1.5rem;
- background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
- color: white;
- border: none;
- border-radius: 0.375rem;
- font-size: 0.875rem;
- font-weight: 500;
- cursor: pointer;
- transition: var(--transition);
- box-shadow: 0 2px 8px var(--primary-30);
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.open-auth-btn:hover {
- background: linear-gradient(135deg, var(--btn-primary-hover) 0%, var(--primary-color) 100%);
- transform: translateY(-2px);
- box-shadow: 0 4px 12px var(--primary-40);
-}
-
-.auth-instructions {
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- padding: 1.5rem;
- border-left: 4px solid var(--primary-color);
-}
-
-.auth-instructions h4 { margin: 0 0 1rem 0; font-size: 1rem; font-weight: 600; }
-.auth-instructions ol { margin: 0 0 1rem 1.5rem; padding: 0; }
-.auth-instructions li { margin-bottom: 0.5rem; font-size: 0.875rem; line-height: 1.5; }
-
-.auth-url-input {
- flex: 1;
- padding: 0.75rem;
- padding-right: 3rem;
- border: 1px solid var(--border-color);
- border-radius: 0.375rem;
- font-size: 0.875rem;
- font-family: 'Courier New', monospace;
- background: var(--bg-tertiary);
- color: var(--text-primary);
-}
-
-.auth-url-input:focus {
- outline: none;
- border-color: var(--primary-color);
- box-shadow: 0 0 0 3px var(--primary-10);
-}
-
-/* 高亮说明样式 */
-.highlight-note {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 1rem;
- background: linear-gradient(135deg, var(--warning-bg) 0%, var(--warning-bg-light) 100%);
- border: 1px solid var(--warning-border);
- border-radius: 0.5rem;
- margin-bottom: 1.5rem;
- color: var(--warning-text);
- font-weight: 500;
- width: 100%;
- box-sizing: border-box;
-}
-
-.highlight-note i {
- color: var(--warning-color);
- font-size: 1.25rem;
- flex-shrink: 0;
-}
-
-.highlight-note span {
- flex: 1;
- text-align: center;
-}
-
-
-.form-group {
- margin-bottom: 1.5rem;
-}
-
-.form-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 1.5rem;
-}
-
-.form-group label {
- display: block;
- margin-bottom: 0.5rem;
- font-weight: 600;
- color: var(--text-primary);
- font-size: 0.9rem;
-}
-
-.optional-tag, .form-group label .optional-mark {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- font-weight: 400;
- margin-left: 0.5rem;
- background: var(--bg-tertiary);
- padding: 0.125rem 0.375rem;
- border-radius: var(--radius-sm);
-}
-
-.form-control {
- width: 100%;
- padding: 0.75rem 1rem;
- border: 1px solid var(--border-color);
- border-radius: var(--radius-lg);
- font-size: 0.9rem;
- transition: var(--transition);
- background: var(--bg-secondary);
- color: var(--text-primary);
-}
-
-.form-control:focus {
- outline: none;
- border-color: var(--primary-color);
- background: var(--bg-primary);
- box-shadow: 0 0 0 4px var(--primary-10);
-}
-
-.form-control::placeholder {
- color: var(--text-tertiary);
-}
-
-
-/* 复选框样式 */
-.form-group input[type="checkbox"] {
- width: 1rem;
- height: 1rem;
- accent-color: var(--primary-color);
- cursor: pointer;
-}
-
-.form-group {
- display: flex;
- flex-direction: column;
-}
-
-.form-group label {
- font-size: 13px;
- color: var(--neutral-600);
- margin-bottom: 8px;
- font-weight: 500;
-}
-
-.form-group input, .form-group select {
- padding: 12px;
- border: 2px solid var(--neutral-200);
- border-radius: 8px;
- font-size: 14px;
- transition: all 0.3s ease;
-}
-
-.form-group input:focus, .form-group select:focus {
- outline: none;
- border-color: var(--primary-color);
- box-shadow: 0 0 0 3px var(--primary-10);
-}
-
-/* 文件上传输入框样式 */
-.file-input-group {
- position: relative;
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.file-input-group .form-control {
- flex: 1;
- padding-right: 0.75rem;
-}
-
-.file-input-group .btn-outline {
- height: 38px;
- width: 38px;
- padding: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 6px;
- flex-shrink: 0;
- background: var(--bg-tertiary);
- color: var(--text-secondary);
- border: 1px solid var(--border-color);
-}
-
-.file-input-group .btn-outline:hover {
- background: var(--bg-secondary);
- color: var(--primary-color);
- border-color: var(--primary-color);
-}
-
-/* 模态框中的文件上传输入框样式 */
-.config-item .file-input-group {
- position: relative;
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.config-item .file-input-group input {
- flex: 1;
- padding-right: 0.75rem;
- box-sizing: border-box;
-}
-
-/* 响应式调整 */
-@media (max-width: 768px) {
- .routing-examples-grid { grid-template-columns: 1fr; }
- .provider-modal-content { width: 98%; max-height: 95vh; }
- .provider-summary { flex-direction: column; align-items: flex-start; gap: 16px; }
- .provider-summary-actions { margin-left: 0; }
- .form-grid { grid-template-columns: 1fr; }
- .provider-item-detail.disabled { opacity: 0.5; }
- .disabled-status { font-size: 0.75rem; padding: 0.2rem 0.4rem; }
- .provider-actions-group { flex-wrap: wrap; gap: 0.5rem; }
- .modal-content { width: 95%; max-height: 90vh; }
- .stats-grid { grid-template-columns: 1fr; }
-}
-
-/* 暗黑主题适配 */
-[data-theme="dark"] .provider-modal-content { background: var(--bg-primary); }
-[data-theme="dark"] .provider-modal-header { background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%); }
-[data-theme="dark"] .provider-summary { background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-primary) 100%); border-color: var(--border-color); }
-[data-theme="dark"] .add-provider-form { background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-primary) 100%); border-color: var(--border-color); }
-[data-theme="dark"] .provider-item-detail { border-color: var(--border-color); }
-[data-theme="dark"] .provider-item-header { background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-primary) 100%); }
-[data-theme="dark"] .provider-item-header:hover { background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%); }
-[data-theme="dark"] .provider-item-detail.unhealthy { border-color: var(--warning-color); background: linear-gradient(135deg, var(--warning-bg) 0%, var(--bg-primary) 100%); }
-[data-theme="dark"] .provider-error-info { background: linear-gradient(135deg, var(--danger-bg) 0%, var(--danger-bg-medium) 100%); border-color: var(--danger-border); }
-[data-theme="dark"] .routing-tips { background: var(--bg-secondary); border-left-color: var(--primary-color); }
-[data-theme="dark"] .endpoint-path { background: var(--bg-tertiary); border-color: var(--border-color); color: var(--text-primary); }
-[data-theme="dark"] .modal-content { background: var(--bg-primary); }
-[data-theme="dark"] .modal-header, [data-theme="dark"] .modal-footer { background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-primary) 100%); }
-[data-theme="dark"] .auth-instructions { background: var(--bg-secondary); border-color: var(--border-color); border-left-color: var(--primary-color); }
-[data-theme="dark"] .auth-url-input { background: var(--bg-tertiary); border-color: var(--border-color); color: var(--text-primary); }
-[data-theme="dark"] .model-checkbox-label { background: var(--bg-primary); border-color: var(--border-color); }
-[data-theme="dark"] .not-supported-models-container { background: var(--bg-secondary); border-color: var(--border-color); }
-[data-theme="dark"] .stat-card { transition: background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; }
-/* 高亮说明样式 - 暗黑主题 */
-[data-theme="dark"] .highlight-note {
- background: linear-gradient(135deg, var(--warning-bg) 0%, var(--warning-bg-light) 100%);
- border-color: var(--warning-border);
- color: var(--warning-text);
-}
-
-[data-theme="dark"] .highlight-note i {
- color: var(--warning-color);
-}
\ No newline at end of file
diff --git a/static/components/section-providers.html b/static/components/section-providers.html
deleted file mode 100644
index 93b481af5881ec244a6daf9607791a50e9644f5f..0000000000000000000000000000000000000000
--- a/static/components/section-providers.html
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
- 提供商池管理
-
-
-
- 使用默认路径配置需添加一个空节点
-
-
-
-
-
-
\ No newline at end of file
diff --git a/static/components/section-tutorial.css b/static/components/section-tutorial.css
deleted file mode 100644
index b0c28c6d56e0ff71a029b487c6f6b1427f93b9da..0000000000000000000000000000000000000000
--- a/static/components/section-tutorial.css
+++ /dev/null
@@ -1,476 +0,0 @@
-/* Tutorial Section Styles */
-
-/* 操作流程图样式 */
-.process-flow {
- display: flex;
- flex-wrap: wrap;
- align-items: flex-start;
- justify-content: center;
- gap: 0.5rem;
- padding: 1.5rem 0;
-}
-
-.flow-step {
- display: flex;
- flex-direction: column;
- align-items: center;
- background: var(--card-bg);
- border: 1px solid var(--border-color);
- border-radius: 12px;
- padding: 1.25rem;
- width: 180px;
- min-height: 200px;
- transition: all 0.3s ease;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
-}
-
-.flow-step:hover {
- transform: translateY(-4px);
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
- border-color: var(--primary-color);
-}
-
-.step-number {
- width: 40px;
- height: 40px;
- background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
- color: white;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.25rem;
- font-weight: 700;
- margin-bottom: 1rem;
- box-shadow: 0 4px 12px rgba(var(--primary-rgb, 59, 130, 246), 0.3);
-}
-
-.step-content {
- text-align: center;
- flex: 1;
-}
-
-.step-content h4 {
- font-size: 0.95rem;
- font-weight: 600;
- color: var(--text-color);
- margin: 0 0 0.5rem 0;
-}
-
-.step-content p {
- font-size: 0.8rem;
- color: var(--text-secondary);
- margin: 0 0 0.75rem 0;
- line-height: 1.4;
-}
-
-.step-content ul {
- list-style: none;
- padding: 0;
- margin: 0;
- text-align: left;
-}
-
-.step-content ul li {
- font-size: 0.75rem;
- color: var(--text-secondary);
- padding: 0.25rem 0;
- padding-left: 1rem;
- position: relative;
-}
-
-.step-content ul li::before {
- content: "•";
- position: absolute;
- left: 0;
- color: var(--primary-color);
- font-weight: bold;
-}
-
-.step-content ul li code {
- font-size: 0.7rem;
- background: var(--code-bg);
- padding: 0.1rem 0.3rem;
- border-radius: 3px;
-}
-
-.flow-arrow {
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.5rem;
- color: var(--primary-color);
- font-weight: bold;
- padding: 0 0.25rem;
- margin-top: 80px;
-}
-
-/* 响应式流程图 */
-@media (max-width: 1200px) {
- .process-flow {
- gap: 0.75rem;
- }
-
- .flow-step {
- width: 160px;
- min-height: 180px;
- padding: 1rem;
- }
-
- .flow-arrow {
- font-size: 1.25rem;
- margin-top: 70px;
- }
-}
-
-@media (max-width: 992px) {
- .process-flow {
- flex-direction: column;
- align-items: center;
- }
-
- .flow-step {
- width: 100%;
- max-width: 400px;
- min-height: auto;
- flex-direction: row;
- gap: 1rem;
- }
-
- .step-number {
- margin-bottom: 0;
- flex-shrink: 0;
- }
-
- .step-content {
- text-align: left;
- }
-
- .flow-arrow {
- transform: rotate(90deg);
- margin-top: 0;
- padding: 0.5rem 0;
- }
-}
-
-/* Tutorial Panel */
-.tutorial-panel {
- background: var(--bg-primary);
- padding: 1.5rem;
- border-radius: var(--radius-lg);
- box-shadow: var(--shadow-md);
- margin-bottom: 1.5rem;
- border: 1px solid var(--border-color);
-}
-
-.tutorial-panel h3 {
- font-size: 1.25rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0 0 1rem 0;
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.tutorial-panel h3 i {
- color: var(--primary-color);
-}
-
-.tutorial-content {
- color: var(--text-secondary);
- line-height: 1.6;
-}
-
-.tutorial-content > p {
- margin-bottom: 1rem;
-}
-
-.tutorial-content code {
- background: var(--bg-tertiary);
- padding: 0.15rem 0.4rem;
- border-radius: var(--radius-sm);
- font-size: 0.85rem;
- color: var(--primary-color);
-}
-
-/* Config File List */
-.config-file-list {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: 1rem;
- margin-top: 1rem;
-}
-
-.config-file-item {
- background: var(--bg-secondary);
- padding: 1rem;
- border-radius: var(--radius-md);
- border: 1px solid var(--border-color);
- transition: var(--transition);
-}
-
-.config-file-item:hover {
- border-color: var(--primary-30);
- box-shadow: var(--shadow-sm);
-}
-
-.file-header {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- margin-bottom: 0.5rem;
-}
-
-.file-header i {
- color: var(--primary-color);
-}
-
-.file-name {
- font-weight: 600;
- color: var(--text-primary);
- font-family: monospace;
-}
-
-.file-badge {
- font-size: 0.65rem;
- padding: 0.1rem 0.4rem;
- border-radius: 9999px;
- font-weight: 500;
- margin-left: auto;
-}
-
-.file-badge.required {
- background: var(--danger-bg);
- color: var(--danger-text);
-}
-
-.file-badge.optional {
- background: var(--info-bg);
- color: var(--info-text);
-}
-
-.file-desc {
- font-size: 0.875rem;
- color: var(--text-secondary);
- margin: 0;
-}
-
-/* Config Section */
-.config-section {
- margin-bottom: 1.5rem;
-}
-
-.config-section:last-child {
- margin-bottom: 0;
-}
-
-.config-section h4 {
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0 0 0.75rem 0;
- padding-bottom: 0.5rem;
- border-bottom: 1px solid var(--border-color);
-}
-
-/* Config Table */
-.config-table {
- overflow-x: auto;
-}
-
-.config-table table {
- width: 100%;
- border-collapse: collapse;
- font-size: 0.875rem;
-}
-
-.config-table th,
-.config-table td {
- padding: 0.75rem;
- text-align: left;
- border-bottom: 1px solid var(--border-color);
-}
-
-.config-table th {
- background: var(--bg-secondary);
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.config-table td {
- color: var(--text-secondary);
-}
-
-.config-table td code {
- background: var(--bg-tertiary);
- padding: 0.15rem 0.4rem;
- border-radius: var(--radius-sm);
- font-size: 0.8rem;
- color: var(--primary-color);
- white-space: nowrap;
-}
-
-.config-table tr:hover td {
- background: var(--bg-secondary);
-}
-
-/* Config Example */
-.config-example {
- background: var(--bg-secondary);
- padding: 1rem;
- border-radius: var(--radius-md);
- margin-top: 1rem;
- border: 1px solid var(--border-color);
-}
-
-.config-example h4 {
- font-size: 0.9rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0 0 0.75rem 0;
-}
-
-.config-example pre {
- background: var(--bg-tertiary);
- padding: 1rem;
- border-radius: var(--radius-md);
- overflow-x: auto;
- margin: 0;
-}
-
-.config-example pre code {
- background: none;
- padding: 0;
- font-size: 0.8rem;
- color: var(--text-primary);
- white-space: pre;
-}
-
-.config-section pre {
- background: var(--bg-tertiary);
- padding: 1rem;
- border-radius: var(--radius-md);
- overflow-x: auto;
- margin: 0.75rem 0 0 0;
-}
-
-.config-section pre code {
- background: none;
- padding: 0;
- font-size: 0.8rem;
- color: var(--text-primary);
- white-space: pre;
-}
-
-/* Tutorial Note */
-.tutorial-note {
- display: flex;
- align-items: flex-start;
- gap: 0.75rem;
- background: var(--info-bg);
- padding: 1rem;
- border-radius: var(--radius-md);
- margin-top: 1rem;
- border: 1px solid var(--info-border);
-}
-
-.tutorial-note i {
- color: var(--info-text);
- font-size: 1rem;
- margin-top: 0.1rem;
-}
-
-.tutorial-note span {
- color: var(--info-text);
- font-size: 0.875rem;
- line-height: 1.5;
-}
-
-/* OAuth Path List */
-.oauth-path-list {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: 1rem;
- margin-top: 1rem;
-}
-
-.oauth-path-item {
- background: var(--bg-secondary);
- padding: 1rem;
- border-radius: var(--radius-md);
- border: 1px solid var(--border-color);
-}
-
-.path-header {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- margin-bottom: 0.5rem;
-}
-
-.path-header i {
- font-size: 1.25rem;
-}
-
-.path-header i.fa-gem { color: #4285f4; }
-.path-header i.fa-rocket { color: #ea4335; }
-.path-header i.fa-robot { color: #9b59b6; }
-.path-header i.fa-code { color: #ff6a00; }
-
-.path-provider {
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.path-value {
- display: block;
- background: var(--bg-tertiary);
- padding: 0.5rem 0.75rem;
- border-radius: var(--radius-sm);
- font-size: 0.8rem;
- color: var(--text-primary);
- font-family: monospace;
- word-break: break-all;
-}
-
-/* Responsive */
-@media (max-width: 768px) {
- .config-file-list {
- grid-template-columns: 1fr;
- }
-
- .oauth-path-list {
- grid-template-columns: 1fr;
- }
-
- .tutorial-panel {
- padding: 1rem;
- }
-
- .config-table th,
- .config-table td {
- padding: 0.5rem;
- font-size: 0.8rem;
- }
-}
-
-/* Dark Theme */
-[data-theme="dark"] .tutorial-panel {
- background: var(--bg-primary);
-}
-
-[data-theme="dark"] .config-file-item,
-[data-theme="dark"] .config-example,
-[data-theme="dark"] .oauth-path-item {
- background: var(--bg-secondary);
-}
-
-[data-theme="dark"] .config-table th {
- background: var(--bg-secondary);
-}
-
-[data-theme="dark"] .config-example pre,
-[data-theme="dark"] .config-section pre {
- background: var(--bg-tertiary);
-}
\ No newline at end of file
diff --git a/static/components/section-tutorial.html b/static/components/section-tutorial.html
deleted file mode 100644
index e616e933ad888f6262478573123539f2f2848154..0000000000000000000000000000000000000000
--- a/static/components/section-tutorial.html
+++ /dev/null
@@ -1,364 +0,0 @@
-
-
-
- 配置教程
-
-
-
-
配置文件说明
-
-
所有配置文件都存放在 configs/ 目录下。主要配置文件包括:
-
-
-
-
-
主配置文件,包含 API Key、端口、模型提供商等核心设置 (保存配置管理后自动新建)
-
-
-
-
提供商池配置,用于多账号轮询和故障转移 (保存节点后自动新建)
-
-
-
-
-
后台登录密码文件,默认密码为 admin123
-
-
-
-
-
-
-
-
主配置详解 (config.json)
-
-
-
-
基础设置
-
-
-
-
- | 参数 |
- 类型 |
- 默认值 |
- 说明 |
-
-
-
-
- REQUIRED_API_KEY |
- string |
- - |
- 访问本服务所需的 API Key |
-
-
- SERVER_PORT |
- number |
- 3000 |
- 服务监听端口 |
-
-
- HOST |
- string |
- 0.0.0.0 |
- 服务监听地址 |
-
-
- MODEL_PROVIDER |
- string |
- - |
- 默认模型提供商 |
-
-
-
-
-
-
-
-
-
代理设置
-
-
-
-
- | 参数 |
- 类型 |
- 说明 |
-
-
-
-
- PROXY_URL |
- string |
- 代理地址,支持 HTTP、HTTPS、SOCKS5 |
-
-
- PROXY_ENABLED_PROVIDERS |
- array |
- 启用代理的提供商列表 |
-
-
-
-
-
-
-
-
-
服务治理
-
-
-
-
- | 参数 |
- 类型 |
- 默认值 |
- 说明 |
-
-
-
-
- REQUEST_MAX_RETRIES |
- number |
- 3 |
- 最大重试次数 |
-
-
- REQUEST_BASE_DELAY |
- number |
- 1000 |
- 重试基础延迟(毫秒) |
-
-
- CREDENTIAL_SWITCH_MAX_RETRIES |
- number |
- 5 |
- 坏凭证切换最大重试次数 |
-
-
- MAX_ERROR_COUNT |
- number |
- 10 |
- 提供商最大错误次数,超过后标记为不健康 |
-
-
- WARMUP_TARGET |
- number |
- 0 |
- 系统启动时自动刷新的节点数量 |
-
-
- REFRESH_CONCURRENCY_PER_PROVIDER |
- number |
- 1 |
- 提供商内刷新并发数 |
-
-
-
-
-
-
-
-
-
日志设置
-
-
-
-
- | 参数 |
- 类型 |
- 说明 |
-
-
-
-
- LOG_ENABLED |
- boolean |
- 启用日志 |
-
-
- LOG_OUTPUT_MODE |
- string |
- 日志输出模式 (all/console/file/none) |
-
-
- PROMPT_LOG_MODE |
- string |
- 提示词日志模式:none(关闭)、console(控制台)、file(文件) |
-
-
-
-
-
-
-
-
配置示例
-
{
- "REQUIRED_API_KEY": "your-api-key",
- "SERVER_PORT": 3000,
- "HOST": "0.0.0.0",
- "MODEL_PROVIDER": "gemini-cli-oauth,claude-kiro-oauth",
- "PROXY_URL": "http://127.0.0.1:7890",
- "PROXY_ENABLED_PROVIDERS": ["gemini-cli-oauth", "claude-kiro-oauth"],
- "REQUEST_MAX_RETRIES": 3,
- "MAX_ERROR_COUNT": 10,
- "WARMUP_TARGET": 5,
- "LOG_ENABLED": true,
- "LOG_OUTPUT_MODE": "all"
-}
-
-
-
-
-
-
-
提供商池配置 (provider_pools.json)
-
-
提供商池用于配置多个账号,实现负载均衡和故障转移。每个提供商类型可以配置多个账号节点。
-
-
-
节点配置参数
-
-
-
-
- | 参数 |
- 类型 |
- 说明 |
-
-
-
-
- uuid |
- string |
- 节点唯一标识,自动生成 |
-
-
- name |
- string |
- 节点自定义名称 |
-
-
- oauthCredsFilePath |
- string |
- OAuth 凭据文件路径 |
-
-
- checkHealth |
- boolean |
- 是否启用健康检查 |
-
-
- checkModel |
- string |
- 健康检查使用的模型 |
-
-
- notSupportedModels |
- array |
- 该节点不支持的模型列表 |
-
-
- disabled |
- boolean |
- 是否禁用该节点 |
-
-
-
-
-
-
-
-
配置示例
-
{
- "gemini-cli-oauth": [
- {
- "uuid": "gemini-account-1",
- "name": "Gemini 账号 1",
- "oauthCredsFilePath": "configs/gemini/oauth_creds_1.json",
- "checkHealth": true,
- "checkModel": "gemini-3-flash-preview"
- }
- ],
- "claude-kiro-oauth": [
- {
- "uuid": "kiro-account-1",
- "name": "Kiro 账号 1",
- "oauthCredsFilePath": "configs/kiro/kiro-auth-token.json",
- "checkHealth": true
- }
- ]
-}
-
-
-
-
-
-
-
Fallback 降级配置
-
-
当某一提供商类型的所有账号都不可用时,可以自动切换到配置的备用提供商。
-
-
-
跨类型 Fallback 链
-
在 config.json 中配置 providerFallbackChain,指定每个提供商类型的备用类型:
-
{
- "providerFallbackChain": {
- "gemini-cli-oauth": ["gemini-antigravity"],
- "gemini-antigravity": ["gemini-cli-oauth"],
- "claude-kiro-oauth": ["claude-custom"],
- "claude-custom": ["claude-kiro-oauth"]
- }
-}
-
-
-
-
跨协议模型映射
-
当主提供商不可用时,可以将特定模型映射到其他协议的提供商:
-
{
- "modelFallbackMapping": {
- "gemini-claude-opus-4-5-thinking": {
- "targetProviderType": "claude-kiro-oauth",
- "targetModel": "claude-opus-4-5"
- }
- }
-}
-
-
-
-
-
-
-
OAuth 授权路径
-
-
各提供商的 OAuth 凭据文件默认存储位置(建议保持在 configs/ 目录下以便统一管理):
-
-
-
-
-
-
-
- 推荐通过 Web UI 控制台的"提供商池管理"页面点击"生成授权"按钮进行可视化授权,系统会自动保存凭据文件。
-
-
-
-
diff --git a/static/components/section-upload-config.css b/static/components/section-upload-config.css
deleted file mode 100644
index e6f3217b8ec053cf3c25ad1b5db53c6c519c2492..0000000000000000000000000000000000000000
--- a/static/components/section-upload-config.css
+++ /dev/null
@@ -1,841 +0,0 @@
-/* 配置管理页面样式 */
-.upload-config-panel {
- background: var(--bg-primary);
- padding: 2rem;
- border-radius: 0.5rem;
- box-shadow: var(--shadow-md);
-}
-
-.config-search-panel {
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- padding: 1.5rem;
- margin-bottom: 2rem;
-}
-
-.search-controls {
- display: grid;
- grid-template-columns: 2fr 1fr auto;
- gap: 1rem;
- align-items: end;
-}
-
-.search-input-group {
- position: relative;
- display: flex;
- align-items: center;
-}
-
-.search-input-group .form-control {
- flex: 1;
- padding-right: 3rem;
-}
-
-.search-input-group .btn {
- position: absolute;
- right: 0.5rem;
- padding: 0.5rem 0.75rem;
- background: var(--primary-color);
- color: white;
- border: none;
- border-radius: 0.375rem;
- cursor: pointer;
- transition: var(--transition);
-}
-
-.search-input-group .btn:hover {
- background: var(--btn-primary-hover);
- transform: translateY(-1px);
-}
-
-.config-list-container {
- background: var(--bg-primary);
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- overflow: hidden;
-}
-
-.config-list-header {
- background: var(--bg-tertiary);
- padding: 1rem 1.5rem;
- border-bottom: 1px solid var(--border-color);
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.config-list-header h3 {
- margin: 0;
- font-size: 1.125rem;
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.config-stats {
- display: flex;
- gap: 1rem;
- align-items: center;
-}
-
-.config-stats span {
- font-size: 0.875rem;
- font-weight: 500;
- padding: 0.25rem 0.5rem;
- border-radius: 0.25rem;
-}
-
-.status-used { background: var(--success-bg); color: var(--success-text); }
-.status-unused { background: var(--warning-bg); color: var(--warning-text); }
-.status-invalid { background: var(--danger-bg); color: var(--danger-text); }
-
-.config-list {
- overflow-y: auto;
-}
-
-/* 无配置文件提示样式 */
-.no-configs {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 4rem 2rem;
- text-align: center;
- background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
- border-radius: 0.5rem;
- margin: 1rem;
-}
-
-.no-configs p {
- font-size: 1rem;
- color: var(--text-secondary);
- margin: 0;
- padding: 1rem 2rem;
- background: var(--bg-tertiary);
- border: 1px dashed var(--border-color);
- border-radius: 0.5rem;
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.no-configs p::before {
- content: '\f07c';
- font-family: 'Font Awesome 6 Free';
- font-weight: 400;
- font-size: 1.25rem;
- color: var(--text-tertiary);
-}
-
-.config-item-manager {
- padding: 0;
- border-bottom: 1px solid var(--border-color);
- transition: var(--transition);
- cursor: pointer;
- background: var(--bg-primary);
-}
-
-.config-item-manager:hover {
- background: var(--bg-secondary);
-}
-
-.config-item-main-row {
- display: flex;
- padding: 1.25rem 1.5rem;
- align-items: center;
- gap: 1.5rem;
-}
-
-.config-item-left {
- display: flex;
- align-items: center;
- gap: 1rem;
- flex: 1.5;
- min-width: 0;
-}
-
-.config-item-icon-wrapper {
- width: 40px;
- height: 40px;
- border-radius: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.25rem;
- flex-shrink: 0;
-}
-
-.config-item-icon-wrapper.oauth { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
-.config-item-icon-wrapper.api-key { background: rgba(16, 185, 129, 0.1); color: #10b981; }
-.config-item-icon-wrapper.provider-pool { background: rgba(139, 92, 246, 0.1); color: #8b5cf6; }
-.config-item-icon-wrapper.system-prompt { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
-.config-item-icon-wrapper.plugins { background: rgba(236, 72, 153, 0.1); color: #ec4899; }
-.config-item-icon-wrapper.usage { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
-.config-item-icon-wrapper.config { background: rgba(107, 114, 128, 0.1); color: #6b7280; }
-.config-item-icon-wrapper.database { background: rgba(16, 185, 129, 0.1); color: #10b981; }
-.config-item-icon-wrapper.other { background: rgba(107, 114, 128, 0.1); color: #6b7280; }
-
-.config-item-title-area {
- display: flex;
- flex-direction: column;
- gap: 0.25rem;
- min-width: 0;
-}
-
-.config-item-name-line {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- flex-wrap: wrap;
-}
-
-.config-item-display-name {
- font-size: 1.05rem;
- font-weight: 600;
- color: var(--text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.provider-type-tag {
- font-size: 0.7rem;
- font-weight: 600;
- padding: 0.15rem 0.5rem;
- border-radius: 20px;
- display: inline-flex;
- align-items: center;
- gap: 0.25rem;
- text-transform: uppercase;
-}
-
-.tag-kiro-oauth { background: rgba(236, 72, 153, 0.1); color: #ec4899; border: 1px solid rgba(236, 72, 153, 0.2); }
-.tag-gemini-oauth { background: rgba(59, 130, 246, 0.1); color: #3b82f6; border: 1px solid rgba(59, 130, 246, 0.2); }
-.tag-qwen-oauth { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); }
-.tag-antigravity { background: rgba(139, 92, 246, 0.1); color: #8b5cf6; border: 1px solid rgba(139, 92, 246, 0.2); }
-.tag-codex-oauth { background: rgba(245, 158, 11, 0.1); color: #f59e0b; border: 1px solid rgba(245, 158, 11, 0.2); }
-.tag-iflow-oauth { background: rgba(20, 184, 166, 0.1); color: #14b8a6; border: 1px solid rgba(20, 184, 166, 0.2); }
-
-.config-item-path-line {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: flex;
- align-items: center;
- gap: 0.35rem;
-}
-
-.config-item-middle {
- flex: 1.2;
- display: flex;
- align-items: center;
-}
-
-.config-meta-info {
- display: flex;
- flex-direction: column;
- gap: 0.35rem;
-}
-
-.meta-item {
- font-size: 0.8rem;
- color: var(--text-secondary);
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.meta-item i {
- width: 14px;
- color: var(--text-tertiary);
-}
-
-.config-item-right {
- display: flex;
- align-items: center;
- gap: 1.5rem;
- flex: 1.3;
- justify-content: flex-end;
-}
-
-.config-status-col {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- gap: 0.5rem;
- width: 100%;
-}
-
-.config-status-indicator {
- display: inline-flex;
- align-items: center;
- gap: 0.35rem;
- padding: 0.35rem 0.75rem;
- border-radius: 30px;
- font-size: 0.75rem;
- font-weight: 600;
-}
-
-.config-status-indicator.used {
- background: rgba(16, 185, 129, 0.1);
- color: #10b981;
-}
-
-.config-status-indicator.unused {
- background: rgba(245, 158, 11, 0.1);
- color: #f59e0b;
-}
-
-.linked-nodes-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 0.35rem;
- justify-content: flex-end;
- margin-top: 0.25rem;
- width: 100%;
-}
-
-.node-tag {
- font-size: 0.65rem;
- color: #3b82f6;
- background: rgba(59, 130, 246, 0.08);
- padding: 0.15rem 0.45rem;
- border-radius: 4px;
- border: 1px solid rgba(59, 130, 246, 0.15);
- white-space: nowrap;
- max-width: 250px;
- overflow: hidden;
- text-overflow: ellipsis;
- display: inline-flex;
- align-items: center;
- gap: 0.25rem;
- font-weight: 500;
-}
-
-.node-tag.status-healthy {
- color: #10b981;
- background: rgba(16, 185, 129, 0.08);
- border-color: rgba(16, 185, 129, 0.15);
-}
-
-.node-tag.status-healthy i {
- color: #10b981;
-}
-
-.node-tag.status-unhealthy {
- color: #ef4444;
- background: rgba(239, 68, 68, 0.08);
- border-color: rgba(239, 68, 68, 0.15);
-}
-
-.node-tag.status-unhealthy i {
- color: #ef4444;
-}
-
-.node-tag.status-disabled {
- color: #6b7280;
- background: rgba(107, 114, 128, 0.08);
- border-color: rgba(107, 114, 128, 0.15);
-}
-
-.node-tag.status-disabled i {
- color: #6b7280;
-}
-
-.node-tag i {
- font-size: 0.6rem;
- color: #3b82f6;
-}
-
-.btn-quick-link-main {
- background: var(--primary-color);
- color: white;
- border: none;
- padding: 0.35rem 0.75rem;
- border-radius: 6px;
- font-size: 0.75rem;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: center;
- gap: 0.35rem;
-}
-
-.btn-quick-link-main:hover {
- background: var(--btn-primary-hover);
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
-}
-
-.config-item-chevron {
- color: var(--text-tertiary);
- transition: transform 0.3s ease;
-}
-
-.config-item-manager.expanded .config-item-chevron {
- transform: rotate(90deg);
-}
-
-.config-item-details {
- margin: 0;
- padding: 1.5rem;
- border-top: 1px solid var(--border-color);
- background: var(--bg-tertiary);
- display: none;
-}
-
-.config-item-manager.expanded .config-item-details {
- display: block;
-}
-
-.config-item-actions {
- display: flex;
- gap: 0.75rem;
- margin-top: 1.25rem;
- padding-top: 1.25rem;
- border-top: 1px dashed var(--border-color);
-}
-
-@media (max-width: 992px) {
- .config-item-main-row {
- flex-direction: column;
- align-items: flex-start;
- gap: 1rem;
- }
-
- .config-item-middle {
- width: 100%;
- }
-
- .config-meta-info {
- flex-direction: row;
- gap: 1.5rem;
- }
-
- .config-item-right {
- width: 100%;
- justify-content: space-between;
- }
-
- .config-status-col {
- flex-direction: row;
- align-items: center;
- }
-}
-
-.config-details-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 1rem;
- margin-bottom: 1rem;
-}
-
-.config-detail-item {
- display: flex;
- flex-direction: column;
- gap: 0.25rem;
-}
-
-.config-detail-label {
- font-size: 0.75rem;
- color: var(--text-secondary);
- font-weight: 500;
- text-transform: uppercase;
- letter-spacing: 0.05em;
-}
-
-.config-detail-item.path-item {
- grid-column: 1 / -1;
-}
-
-.config-detail-item.path-item .config-detail-value {
- cursor: pointer;
- transition: var(--transition);
- border: 1px solid transparent;
- padding: 2px 4px;
- margin-left: -4px;
- border-radius: 4px;
-}
-
-.config-detail-item.path-item .config-detail-value:hover {
- color: var(--primary-color);
-}
-
-.config-detail-value {
- font-size: 0.875rem;
- color: var(--text-primary);
- word-break: break-all;
- overflow-wrap: break-word;
-}
-
-.config-detail-value.status-text-used { color: #10b981; font-weight: 600; }
-.config-detail-value.status-text-unused { color: #f59e0b; font-weight: 600; }
-
-.config-item-actions {
- display: flex;
- gap: 0.5rem;
- margin-top: 0.75rem;
- padding-top: 0.75rem;
- border-top: 1px solid var(--border-color);
-}
-
-.btn-view { background: var(--primary-color); color: white; }
-.btn-view:hover { background: var(--btn-primary-hover); }
-.btn-download { background: #10b981; color: white; }
-.btn-download:hover { background: #059669; }
-.btn-delete-small { background: var(--danger-color); color: white; }
-
-.config-item-manager.expanded {
- background: var(--bg-secondary);
-}
-
-/* 配置查看模态框样式 */
-.config-view-modal {
- position: fixed;
- top: 0; left: 0; width: 100%; height: 100%;
- background: var(--overlay-bg);
- backdrop-filter: blur(4px);
- display: flex; justify-content: center; align-items: center;
- z-index: 1000; opacity: 0; visibility: hidden;
- transition: all 0.3s ease;
-}
-
-.config-view-modal.show { opacity: 1; visibility: visible; }
-
-.config-modal-content {
- background: var(--bg-primary);
- border-radius: 0.5rem;
- width: 90%; max-width: 800px; max-height: 80vh;
- overflow: hidden;
- box-shadow: 0 20px 60px var(--neutral-shadow-30);
- display: flex; flex-direction: column;
- animation: modalSlideIn 0.3s ease;
-}
-
-.config-modal-header {
- padding: 1.5rem;
- border-bottom: 1px solid var(--border-color);
- display: flex; justify-content: space-between; align-items: center;
- background: var(--bg-secondary);
-}
-
-.config-modal-body {
- padding: 1.5rem; flex: 1; overflow-y: auto;
-}
-
-.config-file-info {
- display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 1rem; margin-bottom: 1.5rem; padding: 1rem;
- background: var(--bg-secondary); border-radius: 0.5rem;
-}
-
-.config-content-display {
- background: var(--code-bg); color: var(--code-text);
- padding: 1rem; border-radius: 0.5rem;
- font-family: 'Courier New', monospace; font-size: 0.875rem;
- line-height: 1.5; max-height: 400px; overflow-y: auto;
- white-space: pre-wrap; word-wrap: break-word;
-}
-
-.config-modal-footer {
- padding: 1.5rem; border-top: 1px solid var(--border-color);
- display: flex; justify-content: flex-end; gap: 1rem;
- background: var(--bg-secondary);
-}
-
-/* 关联信息显示样式 */
-.config-usage-info {
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- margin: 1rem 0;
- padding: 1rem;
- border-left: 4px solid var(--primary-color);
-}
-
-.usage-info-header {
- display: flex; align-items: center; gap: 0.5rem;
- margin-bottom: 1rem; padding-bottom: 0.75rem;
- border-bottom: 1px solid var(--border-color);
-}
-
-.usage-details-list {
- display: flex; flex-direction: column; gap: 0.75rem;
-}
-
-.usage-detail-item {
- display: flex;
- align-items: flex-start;
- gap: 1rem;
- padding: 1rem;
- background: var(--bg-primary);
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- transition: var(--transition);
-}
-
-.usage-detail-item:hover {
- border-color: var(--primary-color);
- box-shadow: var(--shadow-sm);
-}
-
-.usage-detail-item i {
- margin-top: 0.25rem;
- color: var(--primary-color);
- font-size: 1.1rem;
-}
-
-.usage-detail-content {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.usage-detail-top {
- display: flex;
- align-items: center;
- gap: 1rem;
- flex-wrap: wrap;
-}
-
-.node-status-tag {
- font-size: 0.65rem;
- font-weight: 600;
- padding: 0.1rem 0.4rem;
- border-radius: 4px;
- text-transform: uppercase;
-}
-
-.node-status-tag.healthy {
- background: rgba(16, 185, 129, 0.1);
- color: #10b981;
- border: 1px solid rgba(16, 185, 129, 0.2);
-}
-
-.node-status-tag.unhealthy {
- background: rgba(239, 68, 68, 0.1);
- color: #ef4444;
- border: 1px solid rgba(239, 68, 68, 0.2);
-}
-
-.node-status-tag.disabled {
- background: rgba(107, 114, 128, 0.1);
- color: #6b7280;
- border: 1px solid rgba(107, 114, 128, 0.2);
-}
-
-.usage-detail-subtitle {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- font-family: 'Courier New', monospace;
- background: var(--bg-secondary);
- padding: 0.25rem 0.5rem;
- border-radius: 4px;
- display: inline-block;
- word-break: break-all;
-}
-
-.usage-detail-type {
- font-size: 0.7rem;
- font-weight: 700;
- background: var(--primary-color);
- color: white;
- padding: 0.15rem 0.5rem;
- border-radius: 4px;
- text-transform: uppercase;
- letter-spacing: 0.02em;
-}
-
-.usage-detail-location {
- font-size: 0.9rem;
- color: var(--text-primary);
- font-weight: 500;
- word-break: break-all;
-}
-
-/* 删除确认模态框样式 */
-.delete-confirm-modal {
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
- background: var(--overlay-bg); backdrop-filter: blur(4px);
- display: flex; justify-content: center; align-items: center;
- z-index: 1000; opacity: 0; visibility: hidden;
- transition: all 0.3s ease;
-}
-
-.delete-confirm-modal.show { opacity: 1; visibility: visible; }
-
-.delete-modal-content {
- background: var(--bg-primary); border-radius: 0.75rem;
- width: 90%; max-width: 600px; max-height: 85vh;
- overflow: hidden; box-shadow: 0 25px 80px var(--neutral-shadow-40);
- display: flex; flex-direction: column;
- animation: modalSlideIn 0.3s ease; border: 2px solid transparent;
-}
-
-.delete-confirm-modal.used .delete-modal-content { border-color: var(--danger-color); box-shadow: 0 25px 80px var(--danger-30); }
-.delete-confirm-modal.unused .delete-modal-content { border-color: var(--warning-color); box-shadow: 0 25px 80px var(--warning-20); }
-
-.delete-modal-header {
- padding: 1.5rem 2rem; border-bottom: 1px solid var(--border-color);
- display: flex; justify-content: space-between; align-items: center;
- background: var(--bg-secondary);
-}
-
-.delete-modal-body {
- padding: 2rem; flex: 1; overflow-y: auto; max-height: calc(85vh - 160px);
-}
-
-.delete-warning {
- display: flex; align-items: flex-start; gap: 1rem;
- padding: 1.5rem; border-radius: 0.5rem; margin-bottom: 1.5rem; border: 2px solid;
-}
-
-.delete-warning.warning-used { background: linear-gradient(135deg, var(--danger-bg-alt) 0%, var(--bg-primary) 100%); border-color: var(--danger-border-light); color: var(--danger-color); }
-.delete-warning.warning-unused { background: linear-gradient(135deg, var(--warning-bg-alt) 0%, var(--bg-primary) 100%); border-color: var(--warning-border-light); color: var(--warning-text-dark); }
-
-.config-info {
- background: var(--bg-secondary); border: 1px solid var(--border-color);
- border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 1.5rem;
-}
-
-.config-info-item {
- display: flex; justify-content: space-between; align-items: center;
- padding: 0.5rem 0; border-bottom: 1px solid var(--border-color);
-}
-
-.usage-alert {
- display: flex; align-items: flex-start; gap: 1rem; padding: 1.5rem;
- background: linear-gradient(135deg, var(--danger-bg) 0%, var(--danger-bg-alt) 100%);
- border: 1px solid var(--danger-border-light); border-radius: 0.5rem; margin-top: 1rem;
-}
-
-.delete-modal-footer {
- padding: 1.5rem 2rem; border-top: 1px solid var(--border-color);
- display: flex; justify-content: flex-end; gap: 1rem; background: var(--bg-secondary);
-}
-
-.btn-confirm-delete { position: relative; overflow: hidden; }
-.delete-confirm-modal.used .btn-confirm-delete { background: linear-gradient(135deg, var(--warning-color) 0%, var(--warning-text-dark) 100%); color: var(--white); box-shadow: 0 4px 15px var(--warning-40); animation: pulseDanger 2s infinite; }
-
-@keyframes pulseDanger {
- 0%, 100% { box-shadow: 0 4px 15px var(--danger-40); }
- 50% { box-shadow: 0 4px 15px var(--danger-70); }
-}
-
-/* 响应式设计 */
-@media (max-width: 768px) {
- .search-controls { grid-template-columns: 1fr; gap: 1rem; }
- .config-list-header { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
- .config-item-header { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
- .config-item-path { max-width: 100%; margin: 0; }
- .config-modal-content { width: 95%; max-height: 90vh; }
- .config-file-info { grid-template-columns: 1fr; }
- .delete-modal-content { width: 95%; max-height: 90vh; }
- .config-info-item { flex-direction: column; align-items: flex-start; gap: 0.25rem; }
- .delete-modal-footer { flex-direction: column; }
-}
-
-/* 删除未绑定按钮样式 */
-.btn-delete-unbound {
- background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
- color: white;
- border: none;
- border-radius: 6px;
- padding: 6px 12px;
- font-size: 12px;
- cursor: pointer;
- margin-left: 8px;
- display: inline-flex;
- align-items: center;
- gap: 6px;
- transition: all 0.2s ease;
- box-shadow: 0 2px 8px var(--danger-30);
- font-weight: 500;
-}
-
-.btn-delete-unbound:hover {
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
- background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
-}
-
-.btn-delete-unbound:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none;
-}
-
-/* 配置列表头部的刷新按钮样式 */
-.btn-refresh {
- background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
- color: white;
- border: none;
- border-radius: 6px;
- padding: 6px 12px;
- font-size: 12px;
- cursor: pointer;
- margin-left: 8px;
- display: inline-flex;
- align-items: center;
- gap: 6px;
- transition: all 0.2s ease;
- box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
- font-weight: 500;
-}
-
-.btn-refresh:hover {
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
- background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
-}
-
-.btn-refresh:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none;
-}
-
-/* 配置列表头部的打包下载按钮样式 */
-.btn-download {
- background: linear-gradient(135deg, #10b981 0%, #059669 100%);
- color: white;
- border: none;
- border-radius: 6px;
- padding: 6px 12px;
- font-size: 12px;
- cursor: pointer;
- margin-left: 8px;
- display: inline-flex;
- align-items: center;
- gap: 6px;
- transition: all 0.2s ease;
- box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
- font-weight: 500;
-}
-
-.btn-download:hover {
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
- background: linear-gradient(135deg, #059669 0%, #10b981 100%);
-}
-
-.btn-download:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none;
-}
-
-/* 暗黑主题适配 */
-[data-theme="dark"] .status-used { background: var(--success-bg); color: var(--success-text); }
-[data-theme="dark"] .status-unused { background: var(--warning-bg); color: var(--warning-text); }
-[data-theme="dark"] .status-invalid { background: var(--danger-bg); color: var(--danger-text); }
-[data-theme="dark"] .config-modal-content { background: var(--bg-primary); }
-[data-theme="dark"] .config-modal-header, [data-theme="dark"] .config-modal-footer { background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-primary) 100%); }
-[data-theme="dark"] .delete-modal-content { background: var(--bg-primary); }
-[data-theme="dark"] .delete-confirm-modal.used .delete-modal-header { background: linear-gradient(135deg, var(--danger-bg) 0%, var(--bg-primary) 100%); }
-[data-theme="dark"] .delete-confirm-modal.unused .delete-modal-header { background: linear-gradient(135deg, var(--warning-bg) 0%, var(--bg-primary) 100%); }
-[data-theme="dark"] .config-info { background: var(--bg-secondary); border-color: var(--border-color); }
diff --git a/static/components/section-upload-config.html b/static/components/section-upload-config.html
deleted file mode 100644
index 95a34b2723935b15b8003f70ad9a4b38e18c757c..0000000000000000000000000000000000000000
--- a/static/components/section-upload-config.html
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
- 凭据文件管理
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/static/components/section-usage.css b/static/components/section-usage.css
deleted file mode 100644
index 8ae272f9cd2ecc7f79e86201162619cf0952082b..0000000000000000000000000000000000000000
--- a/static/components/section-usage.css
+++ /dev/null
@@ -1,480 +0,0 @@
-/* 用量查询页面样式 */
-.usage-panel {
- background: var(--bg-primary);
- border-radius: 0.5rem;
- box-shadow: var(--shadow-md);
- padding: 1.5rem;
-}
-
-.usage-controls {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1.5rem;
- padding-bottom: 1rem;
- border-bottom: 1px solid var(--border-color);
-}
-
-.usage-last-update {
- font-size: 0.875rem;
- color: var(--text-secondary);
- margin-left: auto;
-}
-
-.server-time-display {
- font-size: 0.875rem;
- color: var(--text-secondary);
- margin-left: 1.5rem;
- padding-left: 1.5rem;
- border-left: 1px solid var(--border-color);
-}
-
-.server-time-display i {
- color: var(--primary-color);
- margin-right: 0.25rem;
-}
-
-.usage-info-banner {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 0.75rem 1rem;
- background: var(--bg-secondary);
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- margin-bottom: 1.5rem;
- font-size: 0.875rem;
- color: var(--text-secondary);
-}
-
-.usage-info-banner i {
- color: var(--primary-color);
-}
-
-.supported-providers-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
-}
-
-.loading-inline {
- display: inline-flex;
- align-items: center;
- gap: 0.25rem;
- color: var(--text-tertiary);
-}
-
-.usage-loading, .usage-error, .usage-empty {
- text-align: center;
- padding: 3rem;
- color: var(--text-secondary);
-}
-
-.usage-error {
- color: var(--danger-color);
- background: var(--danger-bg-light);
- border-radius: 0.5rem;
- border: 1px solid var(--danger-bg);
-}
-
-/* 提供商分组样式 */
-.usage-provider-group {
- background: var(--bg-primary);
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- margin-bottom: 1rem;
- overflow: hidden;
-}
-
-.usage-group-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0.75rem 1rem;
- background: var(--bg-secondary);
- user-select: none;
- transition: var(--transition);
-}
-
-.usage-group-header:hover {
- background: var(--bg-tertiary);
-}
-
-.usage-group-title {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- flex: 1;
- cursor: pointer;
-}
-
-.usage-group-actions {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- margin-left: 1rem;
-}
-
-.btn-toggle-cards {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 32px; height: 32px; padding: 0;
- background: var(--bg-primary);
- color: var(--text-secondary);
- border: 1px solid var(--border-color);
- border-radius: 0.375rem;
- cursor: pointer;
- font-size: 0.875rem;
- transition: var(--transition);
-}
-
-.btn-toggle-cards:hover {
- background: var(--primary-color);
- color: white;
- border-color: var(--primary-color);
- transform: translateY(-1px);
- box-shadow: var(--shadow-sm);
-}
-
-.usage-group-title .toggle-icon {
- font-size: 0.75rem;
- color: var(--text-secondary);
- transition: transform 0.3s ease;
- width: 1rem;
-}
-
-.usage-provider-group:not(.collapsed) .toggle-icon {
- transform: rotate(90deg);
-}
-
-.usage-group-title .provider-icon {
- font-size: 1.25rem;
- color: var(--primary-color);
-}
-
-.usage-group-title .provider-name {
- font-weight: 600;
- font-size: 1rem;
- color: var(--text-primary);
-}
-
-.usage-group-title .instance-count {
- font-size: 0.875rem;
- color: var(--text-secondary);
- background: var(--bg-tertiary);
- padding: 0.25rem 0.5rem;
- border-radius: 9999px;
-}
-
-.usage-group-title .success-count {
- font-size: 0.75rem;
- color: var(--text-secondary);
- margin-left: auto;
-}
-
-.usage-group-title .success-count.all-success {
- color: var(--success-color);
-}
-
-.usage-group-content {
- padding: 1rem;
- display: block;
-}
-
-.usage-provider-group.collapsed .usage-group-content {
- display: none;
-}
-
-/* 用量卡片网格 */
-.usage-cards-grid {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 1rem;
-}
-
-.stat-success { color: var(--success-color); }
-.stat-error { color: var(--danger-color); }
-.stat-total { color: var(--text-secondary); }
-
-/* 实例卡片 */
-.usage-instance-card {
- background: var(--bg-primary);
- border: 1px solid var(--border-color);
- border-radius: 0.5rem;
- overflow: hidden;
- transition: var(--transition);
- display: flex;
- flex-direction: column;
-}
-
-.usage-instance-card:hover {
- box-shadow: var(--shadow-md);
- transform: translateY(-2px);
-}
-
-.usage-instance-card.error { border-color: var(--danger-border); }
-
-.usage-instance-header {
- padding: 0.75rem 1rem;
- border-bottom: 1px solid var(--border-color);
- background: linear-gradient(to right, var(--bg-tertiary), var(--bg-secondary));
-}
-
-.instance-header-top {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 0.375rem;
-}
-
-.instance-provider-type {
- display: flex;
- align-items: center;
- gap: 0.375rem;
- font-size: 0.7rem;
- color: var(--primary-color);
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.05em;
-}
-
-.instance-name-text {
- font-size: 0.875rem;
- font-weight: 600;
- color: var(--text-primary);
- display: block;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.instance-user-info {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- flex-wrap: wrap;
- padding-top: 0.375rem;
- border-top: 1px dashed var(--border-color);
- margin-top: 0.375rem;
-}
-
-.user-email {
- font-size: 0.7rem; color: var(--text-secondary);
- display: flex; align-items: center; gap: 0.25rem;
- max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
-}
-
-.user-subscription {
- font-size: 0.65rem; padding: 0.125rem 0.375rem;
- background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
- color: white; border-radius: 9999px; font-weight: 500;
-}
-
-.instance-status-badges {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.btn-download-config {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- padding: 0;
- background: transparent;
- color: var(--text-secondary);
- border: 1px solid var(--border-color);
- border-radius: 4px;
- cursor: pointer;
- font-size: 0.75rem;
- transition: var(--transition);
-}
-
-.btn-download-config:hover {
- background: var(--primary-color);
- color: white;
- border-color: var(--primary-color);
- transform: translateY(-1px);
- box-shadow: var(--shadow-sm);
-}
-
-.badge {
- font-size: 0.75rem; padding: 0.125rem 0.5rem; border-radius: 9999px; font-weight: 500;
-}
-
-.badge-healthy { background: var(--success-bg); color: var(--success-text); }
-.badge-unhealthy { background: var(--danger-bg); color: var(--danger-text); }
-.badge-disabled { background: var(--bg-tertiary); color: var(--text-secondary); }
-
-.usage-instance-content {
- padding: 0.75rem; flex: 1;
-}
-
-.usage-error-message {
- display: flex; align-items: center; gap: 0.5rem; color: var(--danger-color);
- padding: 0.75rem; background: var(--danger-bg-light); border-radius: 0.375rem; font-size: 0.75rem;
-}
-
-.usage-details {
- display: flex; flex-direction: column; gap: 0.75rem;
-}
-
-.usage-section {
- background: var(--bg-secondary); padding: 0.75rem; border-radius: 0.375rem; border: 1px solid var(--border-color);
-}
-
-.usage-section h4 {
- font-size: 0.75rem; font-weight: 600; color: var(--text-secondary);
- margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.375rem;
- text-transform: uppercase; letter-spacing: 0.05em;
-}
-
-.usage-section .info-grid {
- display: flex; flex-direction: column; gap: 0.375rem;
-}
-
-.usage-section .info-item {
- display: flex; justify-content: space-between; align-items: center;
- padding-bottom: 0.375rem; border-bottom: 1px solid var(--border-color);
-}
-
-.usage-section .label { font-size: 0.75rem; color: var(--text-secondary); }
-.usage-section .value { font-size: 0.75rem; font-weight: 600; color: var(--text-primary); text-align: right; max-width: 60%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-
-.total-usage { background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%); border: 1px solid var(--border-color); }
-.total-usage-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
-.total-label { font-size: 0.8rem; font-weight: 600; color: var(--text-primary); display: flex; align-items: center; gap: 0.375rem; }
-.total-value { font-size: 0.75rem; font-weight: 600; color: var(--text-secondary); font-family: monospace; }
-
-.total-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 0.5rem;
-}
-
-.total-percent {
- font-size: 0.75rem;
- font-weight: 600;
- color: var(--text-secondary);
-}
-
-.total-reset-info {
- font-size: 0.65rem;
- color: var(--text-tertiary);
- font-style: italic;
- display: flex;
- align-items: center;
- gap: 0.25rem;
-}
-
-.reset-info-compact { background: var(--bg-secondary); padding: 0.5rem 0.75rem; border-radius: 0.375rem; border: 1px solid var(--border-color); }
-.reset-info-row { display: flex; justify-content: space-between; align-items: center; padding: 0.25rem 0; }
-.reset-info-row:first-child { border-bottom: 1px solid var(--border-color); padding-bottom: 0.375rem; margin-bottom: 0.25rem; }
-.reset-label { font-size: 0.7rem; color: var(--text-secondary); display: flex; align-items: center; gap: 0.25rem; }
-.reset-value { font-size: 0.7rem; font-weight: 600; color: var(--text-primary); }
-
-.usage-breakdown-compact { background: var(--bg-secondary); padding: 0.5rem; border-radius: 0.375rem; border: 1px solid var(--border-color); }
-.breakdown-item-compact { padding: 0.5rem; background: var(--bg-primary); border-radius: 0.25rem; margin-bottom: 0.5rem; border: 1px solid var(--border-color); }
-
-.breakdown-header-compact { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.375rem; font-size: 0.7rem; }
-.breakdown-header-compact .breakdown-usage { color: var(--text-secondary); font-family: monospace; }
-
-.progress-bar-small { height: 0.25rem; background: var(--bg-tertiary); border-radius: 9999px; overflow: hidden; margin-bottom: 0.375rem; }
-.progress-bar-small .progress-fill { height: 100%; border-radius: 9999px; transition: width 0.3s ease; }
-.progress-bar-small.normal .progress-fill { background: var(--success-color); }
-.progress-bar-small.warning .progress-fill { background: var(--warning-color); }
-.progress-bar-small.danger .progress-fill { background: var(--danger-color); }
-
-.extra-usage-info { display: flex; flex-wrap: wrap; align-items: center; gap: 0.375rem; font-size: 0.6rem; padding: 0.375rem; border-radius: 0.25rem; margin-top: 0.375rem; }
-.extra-usage-info.free-trial { background: var(--info-bg-lighter); color: var(--info-text); border: 1px solid var(--info-bg); }
-.extra-usage-info.bonus { background: var(--warning-bg-alt); color: var(--warning-text); border: 1px solid var(--warning-bg); }
-
-.usage-card-collapsed-summary {
- display: flex; flex-direction: column; padding: 0.5rem 0.75rem; cursor: pointer; user-select: none;
- transition: var(--transition); background: linear-gradient(to right, var(--bg-tertiary), var(--bg-secondary));
- border-bottom: 1px solid var(--border-color); gap: 0.375rem;
-}
-
-.collapsed-summary-row { display: flex; align-items: center; gap: 0.5rem; }
-.collapsed-name { font-size: 0.8rem; font-weight: 600; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; }
-.collapsed-progress-bar { width: 80px; height: 0.375rem; background: var(--bg-tertiary); border-radius: 9999px; overflow: hidden; flex-shrink: 0; }
-.collapsed-progress-bar .progress-fill {
- height: 100%;
- border-radius: 9999px;
- transition: width 0.3s ease;
-}
-
-.collapsed-progress-bar.normal .progress-fill { background: var(--success-color); }
-.collapsed-progress-bar.warning .progress-fill { background: var(--warning-color); }
-.collapsed-progress-bar.danger .progress-fill { background: var(--danger-color); }
-.collapsed-percent { font-size: 0.7rem; font-weight: 600; color: var(--text-secondary); min-width: 36px; }
-.collapsed-usage-text { font-size: 0.65rem; color: var(--text-tertiary); width: 100%; margin-top: 0.125rem; text-align: center; }
-
-.usage-toggle-icon { font-size: 0.6rem; color: var(--text-secondary); transition: transform 0.3s ease; flex-shrink: 0; }
-.usage-instance-card:not(.collapsed) .usage-toggle-icon { transform: rotate(90deg); }
-
-.usage-instance-card.collapsed .usage-card-expanded-content { display: none; }
-.usage-instance-card.collapsed .usage-card-collapsed-summary { border-bottom: none; }
-
-/* 分页控件样式 */
-.pagination-container {
- display: flex; align-items: center; justify-content: space-between;
- padding: 12px 16px; background: var(--bg-tertiary); border-radius: 8px;
- margin: 12px 0; flex-wrap: wrap; gap: 12px;
-}
-
-.page-btn {
- min-width: 36px; height: 36px; padding: 0 10px; border: 1px solid var(--border-color);
- background: var(--bg-primary); color: var(--text-primary); border-radius: 6px;
- cursor: pointer; font-size: 0.875rem; font-weight: 500; transition: var(--transition);
- display: inline-flex; align-items: center; justify-content: center;
-}
-
-.page-btn:hover:not(:disabled), .page-btn.active { background: var(--primary-color); color: white; border-color: var(--primary-color); }
-
-.page-jump-input {
- width: 60px; height: 36px; padding: 0 8px; border: 1px solid var(--border-color);
- border-radius: 6px; text-align: center; font-size: 0.875rem;
- background: var(--bg-primary); color: var(--text-primary);
-}
-
-/* 响应式调整 */
-@media (max-width: 1400px) { .usage-cards-grid { grid-template-columns: repeat(3, 1fr); } }
-@media (max-width: 1024px) { .usage-cards-grid { grid-template-columns: repeat(2, 1fr); } }
-@media (max-width: 768px) {
- .usage-cards-grid { grid-template-columns: 1fr; }
- .pagination-container { flex-direction: column; align-items: stretch; gap: 10px; }
- .usage-instance-header { padding: 0.5rem 0.75rem; }
- .usage-instance-content { padding: 0.5rem; }
-}
-
-/* 暗黑主题适配 */
-[data-theme="dark"] .usage-error-message { background: var(--danger-bg); color: var(--danger-text); }
-[data-theme="dark"] .usage-section { background: var(--bg-secondary); border-color: var(--border-color); }
-[data-theme="dark"] .total-usage { background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%); border-color: var(--primary-color); }
-[data-theme="dark"] .reset-info-compact { background: var(--bg-secondary); border-color: var(--border-color); }
-[data-theme="dark"] .usage-breakdown-compact { background: var(--bg-secondary); border-color: var(--border-color); }
-[data-theme="dark"] .extra-usage-info.free-trial { background: var(--info-bg); color: var(--info-text); border-color: var(--info-border); }
-[data-theme="dark"] .extra-usage-info.bonus { background: var(--warning-bg); color: var(--warning-text); border-color: var(--warning-border); }
-[data-theme="dark"] .pagination-container { background: var(--bg-tertiary); }
-[data-theme="dark"] .page-btn { background: var(--bg-primary); border-color: var(--border-color); color: var(--text-primary); }
-[data-theme="dark"] .page-jump-input { background: var(--bg-primary); border-color: var(--border-color); color: var(--text-primary); }
-
-.codex-reset-info {
- font-size: 0.65rem;
- color: var(--text-tertiary);
- margin-top: 0.25rem;
- display: flex;
- align-items: center;
- gap: 0.25rem;
-}
-
-.codex-secondary-usage {
- margin-top: 0.75rem;
- padding-top: 0.75rem;
- border-top: 1px dashed var(--border-color);
-}
diff --git a/static/components/section-usage.html b/static/components/section-usage.html
deleted file mode 100644
index 63b7843b1d1e3ba0a0324d5b1ffea5e52771fa96..0000000000000000000000000000000000000000
--- a/static/components/section-usage.html
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
- 用量查询
-
-
-
- 上次更新: --
-
- 服务端时间: --
-
-
-
-
-
- 支持用量查询的提供商:
-
-
-
-
-
-
- 正在加载用量数据...
-
-
-
-
-
-
-
-
-
-
-
-
点击"刷新用量"按钮获取授权文件用量信息
-
-
-
-
\ No newline at end of file
diff --git a/static/components/sidebar.css b/static/components/sidebar.css
deleted file mode 100644
index c622ce15b4aa2e2dcd6eb5862e02bbb5c1a66c6d..0000000000000000000000000000000000000000
--- a/static/components/sidebar.css
+++ /dev/null
@@ -1,67 +0,0 @@
-/* 侧边栏 */
-.sidebar {
- width: 260px;
- background: var(--bg-glass);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- border: 1px solid var(--border-color);
- border-radius: var(--radius-xl);
- padding: 1rem;
- display: flex;
- flex-direction: column;
- height: fit-content;
- position: sticky;
- top: 5rem;
-}
-
-.sidebar-nav {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.nav-item {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 0.75rem 1rem;
- color: var(--text-secondary);
- text-decoration: none;
- transition: var(--transition);
- font-weight: 500;
- cursor: pointer;
- border-radius: var(--radius-lg);
- user-select: none;
-}
-
-.nav-item:hover {
- background: var(--bg-tertiary);
- color: var(--text-primary);
-}
-
-.nav-item.active {
- background: var(--primary-10);
- color: var(--primary-color);
- font-weight: 600;
-}
-
-.nav-item i {
- width: 20px;
- text-align: center;
- font-size: 1.1em;
-}
-
-/* 响应式调整 */
-@media (max-width: 768px) {
- .sidebar {
- width: 100%;
- border-right: none;
- border-bottom: 1px solid var(--border-color);
- }
-
- .sidebar-nav {
- flex-direction: row;
- overflow-x: auto;
- padding: 0 1rem;
- }
-}
diff --git a/static/components/sidebar.html b/static/components/sidebar.html
deleted file mode 100644
index 7d5c9d37aa3700c24e29bfc73eae977606ac7659..0000000000000000000000000000000000000000
--- a/static/components/sidebar.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/static/index.html b/static/index.html
deleted file mode 100644
index 8e28fc8e684c6cda0b9cd980761c2cab35f23426..0000000000000000000000000000000000000000
--- a/static/index.html
+++ /dev/null
@@ -1,133 +0,0 @@
-
-
-
-
-
-
-
- AIClient2API - 管理控制台
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/static/login.html b/static/login.html
deleted file mode 100644
index 84ecb10001f042cd57173acb3c1f69b8c2cbd008..0000000000000000000000000000000000000000
--- a/static/login.html
+++ /dev/null
@@ -1,350 +0,0 @@
-
-
-
-
-
- 登录 - AIClient2API
-
-
-
-
-
-
-
-
-
-

-
AIClient2API
-
请登录以继续
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/static/potluck-user.html b/static/potluck-user.html
deleted file mode 100644
index a2aa147063c96d61f58b6a2ed05b55cbc9f7dce1..0000000000000000000000000000000000000000
--- a/static/potluck-user.html
+++ /dev/null
@@ -1,746 +0,0 @@
-
-
-
-
-
-
- API 大锅饭 - 我的用量
-
-
-
-
-
-
-
-
-
-
-
-
登录
-
使用您的 API Key 登录查看用量
-
-
-
-
-
-
-
-
-
-
-
-
-
-
个人使用统计
-
-
-
-
-
-
-
我的使用分布 (最近 7 天)
-
-
-
-
- 常用提供商
- 0 次
-
-
-
-
-
-
-
-
-
-
-
-
-
API 密钥
-
-
-
-
-
-
-
-
-
-
- API Key 用于访问 API 服务。请妥善保管,不要泄露给他人。
-
-
-
-
-
-
-
diff --git a/static/potluck.html b/static/potluck.html
deleted file mode 100644
index 62096bba954274f0d8bfc3b50eaff9b622de2b09..0000000000000000000000000000000000000000
--- a/static/potluck.html
+++ /dev/null
@@ -1,1197 +0,0 @@
-
-
-
-
-
-
- API 大锅饭 - Key 管理
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 最近 7 天使用分布统计
-
-
-
-
-
- 提供商占比 (Providers)
- 0 次
-
-
-
-
-
-
-
-
- 模型活跃度 (Models)
- 0 次
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
请妥善保存此 Key,关闭后将无法再次查看完整内容:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
风险提示:
-
- - 所有 Key 的每日限额将被覆盖
- - 已单独设置限额的 Key 也会被修改
- - 此操作不可撤销
-
-
-
-
-
-
-
-
-
-
diff --git a/tls-sidecar/.gitignore b/tls-sidecar/.gitignore
deleted file mode 100644
index 87bc36f70b989910170f332407bbb5398cf12f27..0000000000000000000000000000000000000000
--- a/tls-sidecar/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-tls-sidecar
-tls-sidecar.exe
diff --git a/tls-sidecar/go.mod b/tls-sidecar/go.mod
deleted file mode 100644
index 3e8b6aa0412f23d6e410865a065ea3ee7a531fad..0000000000000000000000000000000000000000
--- a/tls-sidecar/go.mod
+++ /dev/null
@@ -1,17 +0,0 @@
-module tls-sidecar
-
-go 1.22
-
-require (
- github.com/refraction-networking/utls v1.6.7
- golang.org/x/net v0.33.0
-)
-
-require (
- github.com/andybalholm/brotli v1.1.1 // indirect
- github.com/cloudflare/circl v1.5.0 // indirect
- github.com/klauspost/compress v1.17.11 // indirect
- golang.org/x/crypto v0.31.0 // indirect
- golang.org/x/sys v0.28.0 // indirect
- golang.org/x/text v0.21.0 // indirect
-)
diff --git a/tls-sidecar/main.go b/tls-sidecar/main.go
deleted file mode 100644
index 13415c13b302ca6833ab0437150b6039c4e3005e..0000000000000000000000000000000000000000
--- a/tls-sidecar/main.go
+++ /dev/null
@@ -1,424 +0,0 @@
-package main
-
-import (
- "context"
- "encoding/base64"
- "fmt"
- "io"
- "log"
- "net"
- "net/http"
- "net/url"
- "os"
- "os/signal"
- "strconv"
- "strings"
- "sync"
- "syscall"
- "time"
-
- utls "github.com/refraction-networking/utls"
- "golang.org/x/net/http2"
- "golang.org/x/net/proxy"
-)
-
-// ──────────────────────────────────────────────
-// TLS Sidecar — Go uTLS reverse proxy
-//
-// Node.js 发请求到 http://127.0.0.1:,
-// 通过以下自定义 Header 传递目标信息:
-// X-Target-Url: 实际目标 URL(必填)
-// X-Proxy-Url: 上游代理(可选,支持 http/socks5)
-//
-// 所有其他 Header 原样转发给目标服务器。
-// 响应(包括 SSE 流式)透传回 Node.js。
-//
-// uTLS 使用 Chrome 最新指纹,ALPN 协商 h2/http1.1,
-// 根据服务器返回的 ALPN 自动选择 HTTP/2 或 HTTP/1.1 传输。
-// ──────────────────────────────────────────────
-
-const (
- defaultPort = 9090
- headerTarget = "X-Target-Url"
- headerProxy = "X-Proxy-Url"
- readTimeout = 30 * time.Second
- writeTimeout = 0 // SSE 流式响应不设写超时(仅监听 localhost,安全)
- idleTimeout = 120 * time.Second
-)
-
-// 全局 RoundTripper 缓存(按 proxyURL 分组,复用 H2 连接)
-var (
- rtCacheMu sync.Mutex
- rtCache = make(map[string]*utlsRoundTripper)
-)
-
-func getOrCreateRT(proxyURL string) *utlsRoundTripper {
- rtCacheMu.Lock()
- defer rtCacheMu.Unlock()
- if rt, ok := rtCache[proxyURL]; ok {
- return rt
- }
- rt := newUTLSRoundTripper(proxyURL)
- rtCache[proxyURL] = rt
- return rt
-}
-
-// ──────────────── uTLS RoundTripper ────────────────
-// 根据 ALPN 协商结果自动选择 H2 或 H1 传输
-
-type utlsRoundTripper struct {
- proxyURL string
-
- mu sync.Mutex
- h2Conns map[string]*http2.ClientConn // H2 连接缓存 (per host)
-}
-
-func newUTLSRoundTripper(proxyURL string) *utlsRoundTripper {
- return &utlsRoundTripper{
- proxyURL: proxyURL,
- h2Conns: make(map[string]*http2.ClientConn),
- }
-}
-
-func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
- addr := req.URL.Host
- if !strings.Contains(addr, ":") {
- if req.URL.Scheme == "https" {
- addr += ":443"
- } else {
- addr += ":80"
- }
- }
-
- // 尝试复用已有的 H2 连接
- rt.mu.Lock()
- if cc, ok := rt.h2Conns[addr]; ok {
- rt.mu.Unlock()
- if cc.CanTakeNewRequest() {
- resp, err := cc.RoundTrip(req)
- if err == nil {
- return resp, nil
- }
- // H2 连接已失效,清除缓存重建
- log.Printf("[TLS-Sidecar] Cached H2 conn failed for %s: %v, reconnecting", addr, err)
- }
- rt.mu.Lock()
- delete(rt.h2Conns, addr)
- rt.mu.Unlock()
- } else {
- rt.mu.Unlock()
- }
-
- // 建立新的 uTLS 连接
- conn, err := dialUTLS(req.Context(), "tcp", addr, rt.proxyURL)
- if err != nil {
- return nil, err
- }
-
- // 根据 ALPN 协商结果决定走 H2 还是 H1
- alpn := conn.ConnectionState().NegotiatedProtocol
- log.Printf("[TLS-Sidecar] Connected to %s, ALPN: %q", addr, alpn)
-
- if alpn == "h2" {
- // HTTP/2: 创建 H2 ClientConn
- t2 := &http2.Transport{
- StrictMaxConcurrentStreams: true,
- AllowHTTP: false,
- }
- cc, err := t2.NewClientConn(conn)
- if err != nil {
- conn.Close()
- return nil, fmt.Errorf("h2 client conn: %w", err)
- }
-
- rt.mu.Lock()
- rt.h2Conns[addr] = cc
- rt.mu.Unlock()
-
- return cc.RoundTrip(req)
- }
-
- // HTTP/1.1: 通过一次性 Transport 使用已建立的 TLS 连接
- // DialTLSContext 返回已完成 TLS 握手的 conn,http.Transport 不会重复握手
- used := false
- t1 := &http.Transport{
- DialTLSContext: func(ctx context.Context, network, a string) (net.Conn, error) {
- if !used {
- used = true
- return conn, nil
- }
- // 后续连接走正常 uTLS dial
- return dialUTLS(ctx, network, a, rt.proxyURL)
- },
- MaxIdleConnsPerHost: 1,
- IdleConnTimeout: 90 * time.Second,
- }
-
- resp, err := t1.RoundTrip(req)
- if err != nil {
- conn.Close()
- t1.CloseIdleConnections()
- }
- return resp, err
-}
-
-func (rt *utlsRoundTripper) CloseIdleConnections() {
- rt.mu.Lock()
- defer rt.mu.Unlock()
- for k, cc := range rt.h2Conns {
- cc.Close()
- delete(rt.h2Conns, k)
- }
-}
-
-// ──────────────── Main ────────────────
-
-func main() {
- // 强制将日志输出到 Stdout,避免 Node.js 侧将其误判为 Error
- log.SetOutput(os.Stdout)
-
- port := defaultPort
- if p := os.Getenv("TLS_SIDECAR_PORT"); p != "" {
- if v, err := strconv.Atoi(p); err == nil {
- port = v
- }
- }
-
- mux := http.NewServeMux()
- mux.HandleFunc("/health", handleHealth)
- mux.HandleFunc("/", handleProxy)
-
- srv := &http.Server{
- Addr: fmt.Sprintf("127.0.0.1:%d", port),
- Handler: mux,
- ReadTimeout: readTimeout,
- WriteTimeout: writeTimeout,
- IdleTimeout: idleTimeout,
- }
-
- // Graceful shutdown
- go func() {
- sigCh := make(chan os.Signal, 1)
- signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
- <-sigCh
- log.Println("[TLS-Sidecar] Shutting down...")
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- srv.Shutdown(ctx)
- }()
-
- log.Printf("[TLS-Sidecar] Listening on 127.0.0.1:%d (Chrome uTLS, H2+H1 auto)\n", port)
- if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- log.Fatalf("[TLS-Sidecar] Fatal: %v", err)
- }
-}
-
-// ──────────────── Health ────────────────
-
-func handleHealth(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(200)
- fmt.Fprintf(w, `{"status":"ok","tls":"utls-chrome-auto","protocols":"h2,http/1.1"}`)
-}
-
-// ──────────────── Proxy Handler ────────────────
-
-func handleProxy(w http.ResponseWriter, r *http.Request) {
- targetURL := r.Header.Get(headerTarget)
- if targetURL == "" {
- http.Error(w, `{"error":"missing X-Target-Url header"}`, http.StatusBadRequest)
- return
- }
-
- proxyURL := r.Header.Get(headerProxy)
-
- // Parse target
- parsed, err := url.Parse(targetURL)
- if err != nil {
- http.Error(w, fmt.Sprintf(`{"error":"invalid target url: %s"}`, err), http.StatusBadRequest)
- return
- }
-
- // Build outgoing request
- outReq, err := http.NewRequestWithContext(r.Context(), r.Method, targetURL, r.Body)
- if err != nil {
- http.Error(w, fmt.Sprintf(`{"error":"failed to create request: %s"}`, err), http.StatusInternalServerError)
- return
- }
-
- // Copy headers (skip internal + hop-by-hop)
- // 403 关键修复:彻底清理所有非浏览器标头,严格保持小写
- for key, vals := range r.Header {
- lk := strings.ToLower(key)
- if lk == strings.ToLower(headerTarget) || lk == strings.ToLower(headerProxy) {
- continue
- }
- // 移除所有代理、本地网络特征标头,防止 Cloudflare 识别
- if lk == "connection" || lk == "keep-alive" || lk == "transfer-encoding" ||
- lk == "te" || lk == "trailer" || lk == "upgrade" || lk == "host" ||
- lk == "x-forwarded-for" || lk == "x-real-ip" || lk == "x-forwarded-proto" ||
- lk == "x-forwarded-host" || lk == "via" || lk == "proxy-connection" ||
- lk == "cf-connecting-ip" || lk == "true-client-ip" {
- continue
- }
- // 直接通过 map 赋值,确保 Go 的 http2 栈能识别并以原始(小写)形式发出
- outReq.Header[key] = vals
- }
- outReq.Host = parsed.Host
-
- // 针对 Grok 的特殊处理:如果 Accept-Encoding 包含 br 且环境可能存在压缩协商问题
- // 强制设置为标准的浏览器组合
- if ae := outReq.Header["Accept-Encoding"]; len(ae) > 0 {
- outReq.Header["Accept-Encoding"] = []string{"gzip, deflate, br, zstd"}
- }
-
- // Execute via uTLS RoundTripper
- rt := getOrCreateRT(proxyURL)
- resp, err := rt.RoundTrip(outReq)
- if err != nil {
- log.Printf("[TLS-Sidecar] RoundTrip error → %s: %v", parsed.Host, err)
- http.Error(w, fmt.Sprintf(`{"error":"upstream request failed: %s"}`, err), http.StatusBadGateway)
- return
- }
- defer resp.Body.Close()
-
- // Copy response headers
- for key, vals := range resp.Header {
- for _, v := range vals {
- w.Header().Add(key, v)
- }
- }
- w.WriteHeader(resp.StatusCode)
-
- // Stream body (SSE-friendly: flush after every read)
- flusher, canFlush := w.(http.Flusher)
- buf := make([]byte, 32*1024)
- for {
- n, readErr := resp.Body.Read(buf)
- if n > 0 {
- if _, writeErr := w.Write(buf[:n]); writeErr != nil {
- log.Printf("[TLS-Sidecar] Write error: %v", writeErr)
- return
- }
- if canFlush {
- flusher.Flush()
- }
- }
- if readErr != nil {
- if readErr != io.EOF {
- log.Printf("[TLS-Sidecar] Read error: %v", readErr)
- }
- return
- }
- }
-}
-
-// ──────────────── uTLS Dial ────────────────
-
-func dialUTLS(ctx context.Context, network, addr string, proxyURL string) (*utls.UConn, error) {
- host, _, err := net.SplitHostPort(addr)
- if err != nil {
- host = addr
- }
-
- // TCP 连接(可能经过代理)
- var rawConn net.Conn
- if proxyURL != "" {
- rawConn, err = dialViaProxy(ctx, network, addr, proxyURL)
- } else {
- var d net.Dialer
- rawConn, err = d.DialContext(ctx, network, addr)
- }
- if err != nil {
- return nil, fmt.Errorf("tcp dial failed: %w", err)
- }
-
- // uTLS 握手 — 使用 Chrome 最新自动指纹
- // 403 错误通过保持标头小写和清理转发标头来解决
- tlsConn := utls.UClient(rawConn, &utls.Config{
- ServerName: host,
- NextProtos: []string{"h2", "http/1.1"},
- }, utls.HelloChrome_Auto)
-
- // 握手超时
- if deadline, ok := ctx.Deadline(); ok {
- tlsConn.SetDeadline(deadline)
- } else {
- tlsConn.SetDeadline(time.Now().Add(15 * time.Second))
- }
-
- if err := tlsConn.Handshake(); err != nil {
- rawConn.Close()
- return nil, fmt.Errorf("utls handshake failed: %w", err)
- }
-
- // 握手完成,清除超时
- tlsConn.SetDeadline(time.Time{})
- return tlsConn, nil
-}
-
-// ──────────────── Proxy Dialer ────────────────
-
-func dialViaProxy(ctx context.Context, network, addr string, proxyURL string) (net.Conn, error) {
- parsed, err := url.Parse(proxyURL)
- if err != nil {
- return nil, fmt.Errorf("invalid proxy url: %w", err)
- }
-
- switch strings.ToLower(parsed.Scheme) {
- case "socks5", "socks5h", "socks4", "socks":
- var auth *proxy.Auth
- if parsed.User != nil {
- auth = &proxy.Auth{
- User: parsed.User.Username(),
- }
- auth.Password, _ = parsed.User.Password()
- }
- dialer, err := proxy.SOCKS5("tcp", parsed.Host, auth, &net.Dialer{
- Timeout: 15 * time.Second,
- })
- if err != nil {
- return nil, fmt.Errorf("socks5 dialer: %w", err)
- }
- if ctxDialer, ok := dialer.(proxy.ContextDialer); ok {
- return ctxDialer.DialContext(ctx, network, addr)
- }
- return dialer.Dial(network, addr)
-
- case "http", "https":
- proxyConn, err := net.DialTimeout("tcp", parsed.Host, 15*time.Second)
- if err != nil {
- return nil, fmt.Errorf("connect to http proxy: %w", err)
- }
-
- connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n", addr, addr)
- if parsed.User != nil {
- username := parsed.User.Username()
- password, _ := parsed.User.Password()
- cred := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
- connectReq += fmt.Sprintf("Proxy-Authorization: Basic %s\r\n", cred)
- }
- connectReq += "\r\n"
-
- if _, err = proxyConn.Write([]byte(connectReq)); err != nil {
- proxyConn.Close()
- return nil, fmt.Errorf("proxy CONNECT write: %w", err)
- }
-
- buf := make([]byte, 4096)
- n, err := proxyConn.Read(buf)
- if err != nil {
- proxyConn.Close()
- return nil, fmt.Errorf("proxy CONNECT read: %w", err)
- }
- if !strings.Contains(string(buf[:n]), "200") {
- proxyConn.Close()
- return nil, fmt.Errorf("proxy CONNECT rejected: %s", strings.TrimSpace(string(buf[:n])))
- }
-
- return proxyConn, nil
-
- default:
- return nil, fmt.Errorf("unsupported proxy scheme: %s", parsed.Scheme)
- }
-}