feat: 新增主题切换功能并优化提供商池初始化
refactor: 重构配置管理移除冗余提供商配置 fix: 修复手动OAuth回调处理逻辑 style: 优化用量卡片UI增加折叠功能 perf: 提升服务启动时提供商池节点初始化效率 docs: 更新i18n翻译文本和配置说明 chore: 清理无用代码和配置文件
This commit is contained in:
parent
ed634050a9
commit
f691380482
14 changed files with 1263 additions and 652 deletions
|
|
@ -3,17 +3,6 @@
|
|||
"SERVER_PORT": 3000,
|
||||
"HOST": "0.0.0.0",
|
||||
"MODEL_PROVIDER": "gemini-cli-oauth",
|
||||
"OPENAI_API_KEY": "xxx",
|
||||
"OPENAI_BASE_URL": "https://openai/v1",
|
||||
"CLAUDE_API_KEY": "xxx",
|
||||
"CLAUDE_BASE_URL": "https://anthropic/v1",
|
||||
"PROJECT_ID": null,
|
||||
"GEMINI_OAUTH_CREDS_BASE64": null,
|
||||
"GEMINI_OAUTH_CREDS_FILE_PATH": null,
|
||||
"ANTIGRAVITY_OAUTH_CREDS_FILE_PATH": null,
|
||||
"KIRO_OAUTH_CREDS_BASE64": null,
|
||||
"KIRO_OAUTH_CREDS_FILE_PATH": null,
|
||||
"QWEN_OAUTH_CREDS_FILE_PATH": null,
|
||||
"SYSTEM_PROMPT_FILE_PATH": "configs/input_system_prompt.txt",
|
||||
"SYSTEM_PROMPT_MODE": "overwrite",
|
||||
"PROMPT_LOG_BASE_NAME": "prompt_log",
|
||||
|
|
@ -24,16 +13,6 @@
|
|||
"CRON_REFRESH_TOKEN": false,
|
||||
"PROVIDER_POOLS_FILE_PATH": "configs/provider_pools.json",
|
||||
"MAX_ERROR_COUNT": 3,
|
||||
"QWEN_BASE_URL": "https://portal.qwen.ai/v1",
|
||||
"QWEN_OAUTH_BASE_URL": "https://chat.qwen.ai",
|
||||
"GEMINI_BASE_URL": "https://cloudcode-pa.googleapis.com",
|
||||
"ANTIGRAVITY_BASE_URL_DAILY": "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
"ANTIGRAVITY_BASE_URL_AUTOPUSH": "https://autopush-cloudcode-pa.sandbox.googleapis.com",
|
||||
"KIRO_REFRESH_URL": "https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken",
|
||||
"KIRO_REFRESH_IDC_URL": "https://oidc.{{region}}.amazonaws.com/token",
|
||||
"KIRO_BASE_URL": "https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse",
|
||||
"KIRO_AMAZON_Q_URL": "https://codewhisperer.{{region}}.amazonaws.com/SendMessageStreaming",
|
||||
"KIRO_USAGE_LIMITS_URL": "https://q.{{region}}.amazonaws.com/getUsageLimits",
|
||||
"providerFallbackChain": {
|
||||
"gemini-cli-oauth": ["gemini-antigravity"],
|
||||
"gemini-antigravity": ["gemini-cli-oauth"],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as http from 'http';
|
||||
import { initializeConfig, CONFIG, logProviderSpecificDetails } from './config-manager.js';
|
||||
import { initializeConfig, CONFIG } from './config-manager.js';
|
||||
import { initApiService, autoLinkProviderConfigs } from './service-manager.js';
|
||||
import { initializeUIManagement } from './ui-manager.js';
|
||||
import { initializeAPIManagement } from './api-manager.js';
|
||||
|
|
@ -247,7 +247,6 @@ async function startServer() {
|
|||
if (uniqueProviders.length > 1) {
|
||||
console.log(` Additional Model Providers: ${uniqueProviders.slice(1).join(', ')}`);
|
||||
}
|
||||
uniqueProviders.forEach((provider) => logProviderSpecificDetails(provider, CONFIG));
|
||||
console.log(` System Prompt File: ${CONFIG.SYSTEM_PROMPT_FILE_PATH || 'Default'}`);
|
||||
console.log(` System Prompt Mode: ${CONFIG.SYSTEM_PROMPT_MODE}`);
|
||||
console.log(` Host: ${CONFIG.HOST}`);
|
||||
|
|
@ -267,6 +266,8 @@ async function startServer() {
|
|||
// if (CONFIG.HOST === '0.0.0.0' || CONFIG.HOST === '127.0.0.1') {
|
||||
try {
|
||||
const open = (await import('open')).default;
|
||||
// 作为子进程启动时,需要更长的延迟确保服务完全就绪
|
||||
const openDelay = IS_WORKER_PROCESS ? 3000 : 1000;
|
||||
setTimeout(() => {
|
||||
let openUrl = `http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}/login.html`;
|
||||
if(CONFIG.HOST === '0.0.0.0'){
|
||||
|
|
@ -279,7 +280,7 @@ async function startServer() {
|
|||
.catch(err => {
|
||||
console.log('[UI] Please open manually: http://' + CONFIG.HOST + ':' + CONFIG.SERVER_PORT + '/login.html');
|
||||
});
|
||||
}, 1000);
|
||||
}, openDelay);
|
||||
} catch (err) {
|
||||
console.log(`[UI] Login page available at: http://${CONFIG.HOST}:${CONFIG.SERVER_PORT}/login.html`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,25 +67,6 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
|
|||
SERVER_PORT: 3000,
|
||||
HOST: '0.0.0.0',
|
||||
MODEL_PROVIDER: MODEL_PROVIDER.GEMINI_CLI,
|
||||
OPENAI_API_KEY: null,
|
||||
OPENAI_BASE_URL: null,
|
||||
CLAUDE_API_KEY: null,
|
||||
CLAUDE_BASE_URL: null,
|
||||
GEMINI_OAUTH_CREDS_BASE64: null,
|
||||
GEMINI_OAUTH_CREDS_FILE_PATH: null,
|
||||
KIRO_OAUTH_CREDS_BASE64: null,
|
||||
KIRO_OAUTH_CREDS_FILE_PATH: null,
|
||||
QWEN_OAUTH_CREDS_FILE_PATH: null,
|
||||
PROJECT_ID: null,
|
||||
// Provider URLs
|
||||
QWEN_BASE_URL: null,
|
||||
QWEN_OAUTH_BASE_URL: null,
|
||||
GEMINI_BASE_URL: null,
|
||||
ANTIGRAVITY_BASE_URL_DAILY: null,
|
||||
ANTIGRAVITY_BASE_URL_AUTOPUSH: null,
|
||||
KIRO_REFRESH_URL: null,
|
||||
KIRO_REFRESH_IDC_URL: null,
|
||||
KIRO_BASE_URL: null,
|
||||
SYSTEM_PROMPT_FILE_PATH: INPUT_SYSTEM_PROMPT_FILE, // Default value
|
||||
SYSTEM_PROMPT_MODE: 'append',
|
||||
PROMPT_LOG_BASE_NAME: "prompt_log",
|
||||
|
|
@ -136,99 +117,6 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
|
|||
} else {
|
||||
console.warn(`[Config Warning] --model-provider flag requires a value.`);
|
||||
}
|
||||
} else if (args[i] === '--openai-api-key') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.OPENAI_API_KEY = args[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
console.warn(`[Config Warning] --openai-api-key flag requires a value.`);
|
||||
}
|
||||
} else if (args[i] === '--openai-base-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.OPENAI_BASE_URL = args[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
console.warn(`[Config Warning] --openai-base-url flag requires a value.`);
|
||||
}
|
||||
} else if (args[i] === '--claude-api-key') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.CLAUDE_API_KEY = args[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
console.warn(`[Config Warning] --claude-api-key flag requires a value.`);
|
||||
}
|
||||
} else if (args[i] === '--claude-base-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.CLAUDE_BASE_URL = args[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
console.warn(`[Config Warning] --claude-base-url flag requires a value.`);
|
||||
}
|
||||
}
|
||||
// Provider URL arguments
|
||||
else if (args[i] === '--qwen-base-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.QWEN_BASE_URL = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--qwen-oauth-base-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.QWEN_OAUTH_BASE_URL = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--gemini-base-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.GEMINI_BASE_URL = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--antigravity-base-url-daily') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.ANTIGRAVITY_BASE_URL_DAILY = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--antigravity-base-url-autopush') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.ANTIGRAVITY_BASE_URL_AUTOPUSH = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--kiro-refresh-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.KIRO_REFRESH_URL = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--kiro-refresh-idc-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.KIRO_REFRESH_IDC_URL = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
} else if (args[i] === '--kiro-base-url') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.KIRO_BASE_URL = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
// Gemini-specific arguments
|
||||
else if (args[i] === '--gemini-oauth-creds-base64') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.GEMINI_OAUTH_CREDS_BASE64 = args[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
console.warn(`[Config Warning] --gemini-oauth-creds-base64 flag requires a value.`);
|
||||
}
|
||||
} else if (args[i] === '--gemini-oauth-creds-file') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.GEMINI_OAUTH_CREDS_FILE_PATH = args[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
console.warn(`[Config Warning] --gemini-oauth-creds-file flag requires a value.`);
|
||||
}
|
||||
} else if (args[i] === '--project-id') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.PROJECT_ID = args[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
console.warn(`[Config Warning] --project-id flag requires a value.`);
|
||||
}
|
||||
} else if (args[i] === '--system-prompt-file') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.SYSTEM_PROMPT_FILE_PATH = args[i + 1];
|
||||
|
|
@ -262,28 +150,7 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
|
|||
} else {
|
||||
console.warn(`[Config Warning] --prompt-log-base-name flag requires a value.`);
|
||||
}
|
||||
} else if (args[i] === '--kiro-oauth-creds-base64') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.KIRO_OAUTH_CREDS_BASE64 = args[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
console.warn(`[Config Warning] --kiro-oauth-creds-base64 flag requires a value.`);
|
||||
}
|
||||
} else if (args[i] === '--kiro-oauth-creds-file') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.KIRO_OAUTH_CREDS_FILE_PATH = args[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
console.warn(`[Config Warning] --kiro-oauth-creds-file flag requires a value.`);
|
||||
}
|
||||
} else if (args[i] === '--qwen-oauth-creds-file') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.QWEN_OAUTH_CREDS_FILE_PATH = args[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
console.warn(`[Config Warning] --qwen-oauth-creds-file flag requires a value.`);
|
||||
}
|
||||
} else if (args[i] === '--cron-near-minutes') {
|
||||
} else if (args[i] === '--cron-near-minutes') {
|
||||
if (i + 1 < args.length) {
|
||||
currentConfig.CRON_NEAR_MINUTES = parseInt(args[i + 1], 10);
|
||||
i++;
|
||||
|
|
@ -383,46 +250,5 @@ export async function getSystemPromptFileContent(filePath) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs provider-specific configuration details
|
||||
* @param {string} provider - The model provider
|
||||
* @param {Object} config - The configuration object
|
||||
*/
|
||||
export function logProviderSpecificDetails(provider, config) {
|
||||
switch (provider) {
|
||||
case MODEL_PROVIDER.OPENAI_CUSTOM:
|
||||
console.log(` [openai-custom] API Key: ${config.OPENAI_API_KEY ? '******' : 'Not Set'}`);
|
||||
console.log(` [openai-custom] Base URL: ${config.OPENAI_BASE_URL || 'Default'}`);
|
||||
break;
|
||||
case MODEL_PROVIDER.CLAUDE_CUSTOM:
|
||||
console.log(` [claude-custom] API Key: ${config.CLAUDE_API_KEY ? '******' : 'Not Set'}`);
|
||||
console.log(` [claude-custom] Base URL: ${config.CLAUDE_BASE_URL || 'Default'}`);
|
||||
break;
|
||||
case MODEL_PROVIDER.GEMINI_CLI:
|
||||
if (config.GEMINI_OAUTH_CREDS_FILE_PATH) {
|
||||
console.log(` [gemini-cli-oauth] OAuth Creds File Path: ${config.GEMINI_OAUTH_CREDS_FILE_PATH}`);
|
||||
} else if (config.GEMINI_OAUTH_CREDS_BASE64) {
|
||||
console.log(` [gemini-cli-oauth] OAuth Creds Source: Provided via Base64 string`);
|
||||
} else {
|
||||
console.log(` [gemini-cli-oauth] OAuth Creds: Default discovery`);
|
||||
}
|
||||
// console.log(` [gemini-cli-oauth] Project ID: ${config.PROJECT_ID || 'Auto-discovered'}`);
|
||||
break;
|
||||
case MODEL_PROVIDER.KIRO_API:
|
||||
if (config.KIRO_OAUTH_CREDS_FILE_PATH) {
|
||||
console.log(` [claude-kiro-oauth] OAuth Creds File Path: ${config.KIRO_OAUTH_CREDS_FILE_PATH}`);
|
||||
} else if (config.KIRO_OAUTH_CREDS_BASE64) {
|
||||
console.log(` [claude-kiro-oauth] OAuth Creds Source: Provided via Base64 string`);
|
||||
} else {
|
||||
console.log(` [claude-kiro-oauth] OAuth Creds: Default`);
|
||||
}
|
||||
break;
|
||||
case MODEL_PROVIDER.QWEN_API:
|
||||
console.log(` [openai-qwen-oauth] OAuth Creds File Path: ${config.QWEN_OAUTH_CREDS_FILE_PATH || 'Default'}`);
|
||||
break;
|
||||
default:
|
||||
console.log(` [${provider}] Provider initialized.`);
|
||||
}
|
||||
}
|
||||
|
||||
export { ALL_MODEL_PROVIDERS };
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ export class ProviderPoolManager {
|
|||
providerConfig.lastHealthCheckTime = providerConfig.lastHealthCheckTime || null;
|
||||
providerConfig.lastHealthCheckModel = providerConfig.lastHealthCheckModel || null;
|
||||
providerConfig.lastErrorMessage = providerConfig.lastErrorMessage || null;
|
||||
providerConfig.customName = providerConfig.customName || null;
|
||||
|
||||
this.providerStatus[providerType].push({
|
||||
config: providerConfig,
|
||||
|
|
|
|||
|
|
@ -177,36 +177,59 @@ export async function initApiService(config) {
|
|||
console.log('[Initialization] No provider pools configured. Using single provider mode.');
|
||||
}
|
||||
|
||||
// Initialize configured service adapters at startup
|
||||
// 对于未纳入号池的提供者,提前初始化以避免首个请求的额外延迟
|
||||
const providersToInit = new Set();
|
||||
if (Array.isArray(config.DEFAULT_MODEL_PROVIDERS)) {
|
||||
config.DEFAULT_MODEL_PROVIDERS.forEach((provider) => providersToInit.add(provider));
|
||||
}
|
||||
if (config.providerPools) {
|
||||
Object.keys(config.providerPools).forEach((provider) => providersToInit.add(provider));
|
||||
}
|
||||
if (providersToInit.size === 0) {
|
||||
const { ALL_MODEL_PROVIDERS } = await import('./config-manager.js');
|
||||
ALL_MODEL_PROVIDERS.forEach((provider) => providersToInit.add(provider));
|
||||
}
|
||||
|
||||
for (const provider of providersToInit) {
|
||||
const { ALL_MODEL_PROVIDERS } = await import('./config-manager.js');
|
||||
if (!ALL_MODEL_PROVIDERS.includes(provider)) {
|
||||
console.warn(`[Initialization Warning] Skipping unknown model provider '${provider}' during adapter initialization.`);
|
||||
continue;
|
||||
}
|
||||
if (config.providerPools && config.providerPools[provider] && config.providerPools[provider].length > 0) {
|
||||
// 由号池管理器负责按需初始化
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
console.log(`[Initialization] Initializing single service adapter for ${provider}...`);
|
||||
getServiceAdapter({ ...config, MODEL_PROVIDER: provider });
|
||||
} catch (error) {
|
||||
console.warn(`[Initialization Warning] Failed to initialize single service adapter for ${provider}: ${error.message}`);
|
||||
// Initialize all provider pool nodes at startup
|
||||
// 初始化号池中所有提供商的所有节点,以避免首个请求的额外延迟
|
||||
if (config.providerPools && Object.keys(config.providerPools).length > 0) {
|
||||
let totalInitialized = 0;
|
||||
let totalFailed = 0;
|
||||
|
||||
for (const [providerType, providerConfigs] of Object.entries(config.providerPools)) {
|
||||
// 验证提供商类型是否在 DEFAULT_MODEL_PROVIDERS 中
|
||||
if (config.DEFAULT_MODEL_PROVIDERS && Array.isArray(config.DEFAULT_MODEL_PROVIDERS)) {
|
||||
if (!config.DEFAULT_MODEL_PROVIDERS.includes(providerType)) {
|
||||
console.log(`[Initialization] Skipping provider type '${providerType}' (not in DEFAULT_MODEL_PROVIDERS).`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(providerConfigs) || providerConfigs.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[Initialization] Initializing ${providerConfigs.length} node(s) for provider '${providerType}'...`);
|
||||
|
||||
// 初始化该提供商类型的所有节点
|
||||
for (const providerConfig of providerConfigs) {
|
||||
// 跳过已禁用的节点
|
||||
if (providerConfig.isDisabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 合并全局配置和节点配置
|
||||
const nodeConfig = deepmerge(config, {
|
||||
...providerConfig,
|
||||
MODEL_PROVIDER: providerType
|
||||
});
|
||||
delete nodeConfig.providerPools; // 移除 providerPools 避免递归
|
||||
|
||||
// 初始化服务适配器
|
||||
getServiceAdapter(nodeConfig);
|
||||
totalInitialized++;
|
||||
|
||||
const identifier = providerConfig.customName || providerConfig.uuid || 'unknown';
|
||||
console.log(` ✓ Initialized node: ${identifier}`);
|
||||
} catch (error) {
|
||||
totalFailed++;
|
||||
const identifier = providerConfig.customName || providerConfig.uuid || 'unknown';
|
||||
console.warn(` ✗ Failed to initialize node ${identifier}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Initialization] Provider pool initialization complete: ${totalInitialized} succeeded, ${totalFailed} failed.`);
|
||||
} else {
|
||||
console.log('[Initialization] No provider pools configured. Skipping node initialization.');
|
||||
}
|
||||
return serviceInstances; // Return the collection of initialized service instances
|
||||
}
|
||||
|
|
@ -231,10 +254,11 @@ export async function getApiService(config, requestedModel = null, options = {})
|
|||
config.uuid = serviceConfig.uuid;
|
||||
console.log(`[API Service] Using pooled configuration for ${config.MODEL_PROVIDER}: ${serviceConfig.uuid}${requestedModel ? ` (model: ${requestedModel})` : ''}`);
|
||||
} else {
|
||||
console.warn(`[API Service] No healthy provider found in pool for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}. Falling back to main config.`);
|
||||
const errorMsg = `[API Service] No healthy provider found in pool for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}`;
|
||||
console.error(errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
}
|
||||
// 号池不可用时降级,直接使用当前请求的 config 初始化服务适配器
|
||||
return getServiceAdapter(serviceConfig);
|
||||
}
|
||||
|
||||
|
|
@ -273,6 +297,10 @@ export async function getApiServiceWithFallback(config, requestedModel = null, o
|
|||
if (isFallback) {
|
||||
serviceConfig.MODEL_PROVIDER = actualProviderType;
|
||||
}
|
||||
} else {
|
||||
const errorMsg = `[API Service] No healthy provider found in pool (including fallback) for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}`;
|
||||
console.error(errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -690,26 +690,6 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
if (newConfig.HOST !== undefined) currentConfig.HOST = newConfig.HOST;
|
||||
if (newConfig.SERVER_PORT !== undefined) currentConfig.SERVER_PORT = newConfig.SERVER_PORT;
|
||||
if (newConfig.MODEL_PROVIDER !== undefined) currentConfig.MODEL_PROVIDER = newConfig.MODEL_PROVIDER;
|
||||
if (newConfig.PROJECT_ID !== undefined) currentConfig.PROJECT_ID = newConfig.PROJECT_ID;
|
||||
if (newConfig.OPENAI_API_KEY !== undefined) currentConfig.OPENAI_API_KEY = newConfig.OPENAI_API_KEY;
|
||||
if (newConfig.OPENAI_BASE_URL !== undefined) currentConfig.OPENAI_BASE_URL = newConfig.OPENAI_BASE_URL;
|
||||
if (newConfig.CLAUDE_API_KEY !== undefined) currentConfig.CLAUDE_API_KEY = newConfig.CLAUDE_API_KEY;
|
||||
if (newConfig.CLAUDE_BASE_URL !== undefined) currentConfig.CLAUDE_BASE_URL = newConfig.CLAUDE_BASE_URL;
|
||||
if (newConfig.GEMINI_OAUTH_CREDS_BASE64 !== undefined) currentConfig.GEMINI_OAUTH_CREDS_BASE64 = newConfig.GEMINI_OAUTH_CREDS_BASE64;
|
||||
if (newConfig.GEMINI_OAUTH_CREDS_FILE_PATH !== undefined) currentConfig.GEMINI_OAUTH_CREDS_FILE_PATH = newConfig.GEMINI_OAUTH_CREDS_FILE_PATH;
|
||||
if (newConfig.KIRO_OAUTH_CREDS_BASE64 !== undefined) currentConfig.KIRO_OAUTH_CREDS_BASE64 = newConfig.KIRO_OAUTH_CREDS_BASE64;
|
||||
if (newConfig.KIRO_OAUTH_CREDS_FILE_PATH !== undefined) currentConfig.KIRO_OAUTH_CREDS_FILE_PATH = newConfig.KIRO_OAUTH_CREDS_FILE_PATH;
|
||||
if (newConfig.QWEN_OAUTH_CREDS_FILE_PATH !== undefined) currentConfig.QWEN_OAUTH_CREDS_FILE_PATH = newConfig.QWEN_OAUTH_CREDS_FILE_PATH;
|
||||
|
||||
// New Provider URLs
|
||||
if (newConfig.QWEN_BASE_URL !== undefined) currentConfig.QWEN_BASE_URL = newConfig.QWEN_BASE_URL;
|
||||
if (newConfig.QWEN_OAUTH_BASE_URL !== undefined) currentConfig.QWEN_OAUTH_BASE_URL = newConfig.QWEN_OAUTH_BASE_URL;
|
||||
if (newConfig.GEMINI_BASE_URL !== undefined) currentConfig.GEMINI_BASE_URL = newConfig.GEMINI_BASE_URL;
|
||||
if (newConfig.ANTIGRAVITY_BASE_URL_DAILY !== undefined) currentConfig.ANTIGRAVITY_BASE_URL_DAILY = newConfig.ANTIGRAVITY_BASE_URL_DAILY;
|
||||
if (newConfig.ANTIGRAVITY_BASE_URL_AUTOPUSH !== undefined) currentConfig.ANTIGRAVITY_BASE_URL_AUTOPUSH = newConfig.ANTIGRAVITY_BASE_URL_AUTOPUSH;
|
||||
if (newConfig.KIRO_REFRESH_URL !== undefined) currentConfig.KIRO_REFRESH_URL = newConfig.KIRO_REFRESH_URL;
|
||||
if (newConfig.KIRO_REFRESH_IDC_URL !== undefined) currentConfig.KIRO_REFRESH_IDC_URL = newConfig.KIRO_REFRESH_IDC_URL;
|
||||
if (newConfig.KIRO_BASE_URL !== undefined) currentConfig.KIRO_BASE_URL = newConfig.KIRO_BASE_URL;
|
||||
if (newConfig.SYSTEM_PROMPT_FILE_PATH !== undefined) currentConfig.SYSTEM_PROMPT_FILE_PATH = newConfig.SYSTEM_PROMPT_FILE_PATH;
|
||||
if (newConfig.SYSTEM_PROMPT_MODE !== undefined) currentConfig.SYSTEM_PROMPT_MODE = newConfig.SYSTEM_PROMPT_MODE;
|
||||
if (newConfig.PROMPT_LOG_BASE_NAME !== undefined) currentConfig.PROMPT_LOG_BASE_NAME = newConfig.PROMPT_LOG_BASE_NAME;
|
||||
|
|
@ -753,27 +733,6 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
SERVER_PORT: currentConfig.SERVER_PORT,
|
||||
HOST: currentConfig.HOST,
|
||||
MODEL_PROVIDER: currentConfig.MODEL_PROVIDER,
|
||||
OPENAI_API_KEY: currentConfig.OPENAI_API_KEY,
|
||||
OPENAI_BASE_URL: currentConfig.OPENAI_BASE_URL,
|
||||
CLAUDE_API_KEY: currentConfig.CLAUDE_API_KEY,
|
||||
CLAUDE_BASE_URL: currentConfig.CLAUDE_BASE_URL,
|
||||
PROJECT_ID: currentConfig.PROJECT_ID,
|
||||
GEMINI_OAUTH_CREDS_BASE64: currentConfig.GEMINI_OAUTH_CREDS_BASE64,
|
||||
GEMINI_OAUTH_CREDS_FILE_PATH: currentConfig.GEMINI_OAUTH_CREDS_FILE_PATH,
|
||||
KIRO_OAUTH_CREDS_BASE64: currentConfig.KIRO_OAUTH_CREDS_BASE64,
|
||||
KIRO_OAUTH_CREDS_FILE_PATH: currentConfig.KIRO_OAUTH_CREDS_FILE_PATH,
|
||||
QWEN_OAUTH_CREDS_FILE_PATH: currentConfig.QWEN_OAUTH_CREDS_FILE_PATH,
|
||||
// Provider URLs
|
||||
QWEN_BASE_URL: currentConfig.QWEN_BASE_URL,
|
||||
QWEN_OAUTH_BASE_URL: currentConfig.QWEN_OAUTH_BASE_URL,
|
||||
GEMINI_BASE_URL: currentConfig.GEMINI_BASE_URL,
|
||||
ANTIGRAVITY_BASE_URL_DAILY: currentConfig.ANTIGRAVITY_BASE_URL_DAILY,
|
||||
ANTIGRAVITY_BASE_URL_AUTOPUSH: currentConfig.ANTIGRAVITY_BASE_URL_AUTOPUSH,
|
||||
KIRO_REFRESH_URL: currentConfig.KIRO_REFRESH_URL,
|
||||
KIRO_REFRESH_IDC_URL: currentConfig.KIRO_REFRESH_IDC_URL,
|
||||
KIRO_BASE_URL: currentConfig.KIRO_BASE_URL,
|
||||
KIRO_AMAZON_Q_URL: currentConfig.KIRO_AMAZON_Q_URL,
|
||||
KIRO_USAGE_LIMITS_URL: currentConfig.KIRO_USAGE_LIMITS_URL,
|
||||
SYSTEM_PROMPT_FILE_PATH: currentConfig.SYSTEM_PROMPT_FILE_PATH,
|
||||
SYSTEM_PROMPT_MODE: currentConfig.SYSTEM_PROMPT_MODE,
|
||||
PROMPT_LOG_BASE_NAME: currentConfig.PROMPT_LOG_BASE_NAME,
|
||||
|
|
@ -1498,6 +1457,84 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
}
|
||||
}
|
||||
|
||||
// Handle manual OAuth callback
|
||||
if (method === 'POST' && pathParam === '/api/oauth/manual-callback') {
|
||||
try {
|
||||
const body = await getRequestBody(req);
|
||||
const { provider, callbackUrl, authMethod } = body;
|
||||
|
||||
if (!provider || !callbackUrl) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: 'provider and callbackUrl are required'
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`[OAuth Manual Callback] Processing manual callback for ${provider}`);
|
||||
console.log(`[OAuth Manual Callback] Callback URL: ${callbackUrl}`);
|
||||
|
||||
// 解析回调URL
|
||||
const url = new URL(callbackUrl);
|
||||
const code = url.searchParams.get('code');
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
if (!code && !token) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: 'Callback URL must contain code or token parameter'
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
// 通过fetch请求本地OAuth回调服务器处理
|
||||
// 使用localhost而不是原始hostname,确保请求到达本地服务器
|
||||
const localUrl = new URL(callbackUrl);
|
||||
localUrl.hostname = 'localhost';
|
||||
localUrl.protocol = 'http:';
|
||||
|
||||
try {
|
||||
const response = await fetch(localUrl.href);
|
||||
|
||||
if (response.ok) {
|
||||
console.log(`[OAuth Manual Callback] Successfully processed callback for ${provider}`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
message: 'OAuth callback processed successfully'
|
||||
}));
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error(`[OAuth Manual Callback] Callback processing failed:`, errorText);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: `Callback processing failed: ${response.status}`
|
||||
}));
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.error(`[OAuth Manual Callback] Failed to process callback:`, fetchError);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: `Failed to process callback: ${fetchError.message}`
|
||||
}));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[OAuth Manual Callback] Error:', error);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Server-Sent Events for real-time updates
|
||||
if (method === 'GET' && pathParam === '/api/events') {
|
||||
res.writeHead(200, {
|
||||
|
|
@ -2646,6 +2683,11 @@ async function getAdapterUsage(adapter, providerType) {
|
|||
* @returns {string} 显示名称
|
||||
*/
|
||||
function getProviderDisplayName(provider, providerType) {
|
||||
// 优先使用自定义名称
|
||||
if (provider.customName) {
|
||||
return provider.customName;
|
||||
}
|
||||
|
||||
// 尝试从凭据文件路径提取名称
|
||||
const credPathKey = {
|
||||
'claude-kiro-oauth': 'KIRO_OAUTH_CREDS_FILE_PATH',
|
||||
|
|
|
|||
|
|
@ -22,65 +22,41 @@ async function loadConfiguration() {
|
|||
if (apiKeyEl) apiKeyEl.value = data.REQUIRED_API_KEY || '';
|
||||
if (hostEl) hostEl.value = data.HOST || '127.0.0.1';
|
||||
if (portEl) portEl.value = data.SERVER_PORT || 3000;
|
||||
if (modelProviderEl) modelProviderEl.value = data.MODEL_PROVIDER || 'gemini-cli-oauth';
|
||||
|
||||
if (modelProviderEl) {
|
||||
// 处理多选 MODEL_PROVIDER (复选框)
|
||||
const providers = Array.isArray(data.DEFAULT_MODEL_PROVIDERS)
|
||||
? data.DEFAULT_MODEL_PROVIDERS
|
||||
: (typeof data.MODEL_PROVIDER === 'string' ? data.MODEL_PROVIDER.split(',') : []);
|
||||
|
||||
const checkboxes = modelProviderEl.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = providers.includes(checkbox.value);
|
||||
});
|
||||
|
||||
// 如果没有任何选中的,默认选中第一个(保持兼容性)
|
||||
const anyChecked = Array.from(checkboxes).some(cb => cb.checked);
|
||||
if (!anyChecked && checkboxes.length > 0) {
|
||||
checkboxes[0].checked = true;
|
||||
}
|
||||
|
||||
// 为复选框添加事件监听,防止取消勾选最后一个
|
||||
checkboxes.forEach(checkbox => {
|
||||
// 移除旧的监听器(如果有的话,虽然这里大概率没有)
|
||||
const newCheckbox = checkbox.cloneNode(true);
|
||||
checkbox.parentNode.replaceChild(newCheckbox, checkbox);
|
||||
|
||||
newCheckbox.addEventListener('change', (e) => {
|
||||
const checkedCount = modelProviderEl.querySelectorAll('input[type="checkbox"]:checked').length;
|
||||
if (checkedCount === 0) {
|
||||
newCheckbox.checked = true;
|
||||
showToast(t('common.warning'), t('config.modelProviderRequired'), 'warning');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (systemPromptEl) systemPromptEl.value = data.systemPrompt || '';
|
||||
|
||||
// Gemini CLI OAuth
|
||||
const projectIdEl = document.getElementById('projectId');
|
||||
const geminiOauthCredsBase64El = document.getElementById('geminiOauthCredsBase64');
|
||||
const geminiOauthCredsFilePathEl = document.getElementById('geminiOauthCredsFilePath');
|
||||
|
||||
if (projectIdEl) projectIdEl.value = data.PROJECT_ID || '';
|
||||
if (geminiOauthCredsBase64El) geminiOauthCredsBase64El.value = data.GEMINI_OAUTH_CREDS_BASE64 || '';
|
||||
if (geminiOauthCredsFilePathEl) geminiOauthCredsFilePathEl.value = data.GEMINI_OAUTH_CREDS_FILE_PATH || '';
|
||||
const geminiBaseUrlEl = document.getElementById('geminiBaseUrl');
|
||||
if (geminiBaseUrlEl) geminiBaseUrlEl.value = data.GEMINI_BASE_URL || '';
|
||||
const antigravityBaseUrlDailyEl = document.getElementById('antigravityBaseUrlDaily');
|
||||
if (antigravityBaseUrlDailyEl) antigravityBaseUrlDailyEl.value = data.ANTIGRAVITY_BASE_URL_DAILY || '';
|
||||
const antigravityBaseUrlAutopushEl = document.getElementById('antigravityBaseUrlAutopush');
|
||||
if (antigravityBaseUrlAutopushEl) antigravityBaseUrlAutopushEl.value = data.ANTIGRAVITY_BASE_URL_AUTOPUSH || '';
|
||||
|
||||
// OpenAI Custom
|
||||
const openaiApiKeyEl = document.getElementById('openaiApiKey');
|
||||
const openaiBaseUrlEl = document.getElementById('openaiBaseUrl');
|
||||
|
||||
if (openaiApiKeyEl) openaiApiKeyEl.value = data.OPENAI_API_KEY || '';
|
||||
if (openaiBaseUrlEl) openaiBaseUrlEl.value = data.OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
||||
|
||||
// Claude Custom
|
||||
const claudeApiKeyEl = document.getElementById('claudeApiKey');
|
||||
const claudeBaseUrlEl = document.getElementById('claudeBaseUrl');
|
||||
|
||||
if (claudeApiKeyEl) claudeApiKeyEl.value = data.CLAUDE_API_KEY || '';
|
||||
if (claudeBaseUrlEl) claudeBaseUrlEl.value = data.CLAUDE_BASE_URL || 'https://api.anthropic.com';
|
||||
|
||||
// Claude Kiro OAuth
|
||||
const kiroOauthCredsBase64El = document.getElementById('kiroOauthCredsBase64');
|
||||
const kiroOauthCredsFilePathEl = document.getElementById('kiroOauthCredsFilePath');
|
||||
|
||||
if (kiroOauthCredsBase64El) kiroOauthCredsBase64El.value = data.KIRO_OAUTH_CREDS_BASE64 || '';
|
||||
if (kiroOauthCredsFilePathEl) kiroOauthCredsFilePathEl.value = data.KIRO_OAUTH_CREDS_FILE_PATH || '';
|
||||
const kiroBaseUrlEl = document.getElementById('kiroBaseUrl');
|
||||
if (kiroBaseUrlEl) kiroBaseUrlEl.value = data.KIRO_BASE_URL || '';
|
||||
const kiroRefreshUrlEl = document.getElementById('kiroRefreshUrl');
|
||||
if (kiroRefreshUrlEl) kiroRefreshUrlEl.value = data.KIRO_REFRESH_URL || '';
|
||||
const kiroRefreshIdcUrlEl = document.getElementById('kiroRefreshIdcUrl');
|
||||
if (kiroRefreshIdcUrlEl) kiroRefreshIdcUrlEl.value = data.KIRO_REFRESH_IDC_URL || '';
|
||||
|
||||
// Qwen OAuth
|
||||
const qwenOauthCredsFilePathEl = document.getElementById('qwenOauthCredsFilePath');
|
||||
if (qwenOauthCredsFilePathEl) qwenOauthCredsFilePathEl.value = data.QWEN_OAUTH_CREDS_FILE_PATH || '';
|
||||
const qwenBaseUrlEl = document.getElementById('qwenBaseUrl');
|
||||
if (qwenBaseUrlEl) qwenBaseUrlEl.value = data.QWEN_BASE_URL || '';
|
||||
const qwenOauthBaseUrlEl = document.getElementById('qwenOauthBaseUrl');
|
||||
if (qwenOauthBaseUrlEl) qwenOauthBaseUrlEl.value = data.QWEN_OAUTH_BASE_URL || '';
|
||||
|
||||
// OpenAI Responses
|
||||
const openaiResponsesApiKeyEl = document.getElementById('openaiResponsesApiKey');
|
||||
const openaiResponsesBaseUrlEl = document.getElementById('openaiResponsesBaseUrl');
|
||||
|
||||
if (openaiResponsesApiKeyEl) openaiResponsesApiKeyEl.value = data.OPENAI_API_KEY || '';
|
||||
if (openaiResponsesBaseUrlEl) openaiResponsesBaseUrlEl.value = data.OPENAI_BASE_URL || 'https://api.openai.com/v1';
|
||||
|
||||
// 高级配置参数
|
||||
const systemPromptFilePathEl = document.getElementById('systemPromptFilePath');
|
||||
|
|
@ -114,34 +90,6 @@ async function loadConfiguration() {
|
|||
providerFallbackChainEl.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 触发提供商配置显示
|
||||
handleProviderChange();
|
||||
|
||||
// 根据Gemini凭据类型设置显示
|
||||
const geminiCredsType = data.GEMINI_OAUTH_CREDS_BASE64 ? 'base64' : 'file';
|
||||
const geminiRadio = document.querySelector(`input[name="geminiCredsType"][value="${geminiCredsType}"]`);
|
||||
if (geminiRadio) {
|
||||
geminiRadio.checked = true;
|
||||
handleGeminiCredsTypeChange({ target: geminiRadio });
|
||||
}
|
||||
|
||||
// 根据Kiro凭据类型设置显示
|
||||
const kiroCredsType = data.KIRO_OAUTH_CREDS_BASE64 ? 'base64' : 'file';
|
||||
const kiroRadio = document.querySelector(`input[name="kiroCredsType"][value="${kiroCredsType}"]`);
|
||||
if (kiroRadio) {
|
||||
kiroRadio.checked = true;
|
||||
handleKiroCredsTypeChange({ target: kiroRadio });
|
||||
}
|
||||
|
||||
// 检查并设置提供商池菜单显示状态
|
||||
// const providerPoolsFilePath = data.PROVIDER_POOLS_FILE_PATH;
|
||||
// const providersMenuItem = document.querySelector('.nav-item[data-section="providers"]');
|
||||
// if (providerPoolsFilePath && providerPoolsFilePath.trim() !== '') {
|
||||
// if (providersMenuItem) providersMenuItem.style.display = 'flex';
|
||||
// } else {
|
||||
// if (providersMenuItem) providersMenuItem.style.display = 'none';
|
||||
// }
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load configuration:', error);
|
||||
|
|
@ -152,76 +100,31 @@ async function loadConfiguration() {
|
|||
* 保存配置
|
||||
*/
|
||||
async function saveConfiguration() {
|
||||
const modelProviderEl = document.getElementById('modelProvider');
|
||||
let selectedProviders = [];
|
||||
if (modelProviderEl) {
|
||||
// 从复选框中获取选中的提供商
|
||||
selectedProviders = Array.from(modelProviderEl.querySelectorAll('input[type="checkbox"]:checked'))
|
||||
.map(cb => cb.value);
|
||||
}
|
||||
|
||||
// 校验:必须至少勾选一个
|
||||
if (selectedProviders.length === 0) {
|
||||
showToast(t('common.error'), t('config.modelProviderRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
REQUIRED_API_KEY: document.getElementById('apiKey')?.value || '',
|
||||
HOST: document.getElementById('host')?.value || '127.0.0.1',
|
||||
SERVER_PORT: parseInt(document.getElementById('port')?.value || 3000),
|
||||
MODEL_PROVIDER: document.getElementById('modelProvider')?.value || 'gemini-cli-oauth',
|
||||
MODEL_PROVIDER: selectedProviders.length > 0 ? selectedProviders.join(',') : 'gemini-cli-oauth',
|
||||
systemPrompt: document.getElementById('systemPrompt')?.value || '',
|
||||
};
|
||||
|
||||
// 获取后台登录密码(如果有输入)
|
||||
const adminPassword = document.getElementById('adminPassword')?.value || '';
|
||||
|
||||
// 根据不同提供商保存不同的配置
|
||||
const provider = document.getElementById('modelProvider')?.value;
|
||||
|
||||
switch (provider) {
|
||||
case 'gemini-cli-oauth':
|
||||
config.PROJECT_ID = document.getElementById('projectId')?.value || '';
|
||||
const geminiCredsType = document.querySelector('input[name="geminiCredsType"]:checked')?.value;
|
||||
if (geminiCredsType === 'base64') {
|
||||
config.GEMINI_OAUTH_CREDS_BASE64 = document.getElementById('geminiOauthCredsBase64')?.value || '';
|
||||
config.GEMINI_OAUTH_CREDS_FILE_PATH = null;
|
||||
} else {
|
||||
config.GEMINI_OAUTH_CREDS_BASE64 = null;
|
||||
config.GEMINI_OAUTH_CREDS_FILE_PATH = document.getElementById('geminiOauthCredsFilePath')?.value || '';
|
||||
}
|
||||
config.GEMINI_BASE_URL = document.getElementById('geminiBaseUrl')?.value || null;
|
||||
break;
|
||||
|
||||
case 'gemini-antigravity':
|
||||
config.ANTIGRAVITY_BASE_URL_DAILY = document.getElementById('antigravityBaseUrlDaily')?.value || null;
|
||||
config.ANTIGRAVITY_BASE_URL_AUTOPUSH = document.getElementById('antigravityBaseUrlAutopush')?.value || null;
|
||||
config.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH = document.getElementById('antigravityOauthCredsFilePath')?.value || '';
|
||||
break;
|
||||
|
||||
case 'openai-custom':
|
||||
config.OPENAI_API_KEY = document.getElementById('openaiApiKey')?.value || '';
|
||||
config.OPENAI_BASE_URL = document.getElementById('openaiBaseUrl')?.value || '';
|
||||
break;
|
||||
|
||||
case 'claude-custom':
|
||||
config.CLAUDE_API_KEY = document.getElementById('claudeApiKey')?.value || '';
|
||||
config.CLAUDE_BASE_URL = document.getElementById('claudeBaseUrl')?.value || '';
|
||||
break;
|
||||
|
||||
case 'claude-kiro-oauth':
|
||||
const kiroCredsType = document.querySelector('input[name="kiroCredsType"]:checked')?.value;
|
||||
if (kiroCredsType === 'base64') {
|
||||
config.KIRO_OAUTH_CREDS_BASE64 = document.getElementById('kiroOauthCredsBase64')?.value || '';
|
||||
config.KIRO_OAUTH_CREDS_FILE_PATH = null;
|
||||
} else {
|
||||
config.KIRO_OAUTH_CREDS_BASE64 = null;
|
||||
config.KIRO_OAUTH_CREDS_FILE_PATH = document.getElementById('kiroOauthCredsFilePath')?.value || '';
|
||||
}
|
||||
config.KIRO_BASE_URL = document.getElementById('kiroBaseUrl')?.value || null;
|
||||
config.KIRO_REFRESH_URL = document.getElementById('kiroRefreshUrl')?.value || null;
|
||||
config.KIRO_REFRESH_IDC_URL = document.getElementById('kiroRefreshIdcUrl')?.value || null;
|
||||
break;
|
||||
|
||||
case 'openai-qwen-oauth':
|
||||
config.QWEN_OAUTH_CREDS_FILE_PATH = document.getElementById('qwenOauthCredsFilePath')?.value || '';
|
||||
config.QWEN_BASE_URL = document.getElementById('qwenBaseUrl')?.value || null;
|
||||
config.QWEN_OAUTH_BASE_URL = document.getElementById('qwenOauthBaseUrl')?.value || null;
|
||||
break;
|
||||
|
||||
case 'openaiResponses-custom':
|
||||
config.OPENAI_API_KEY = document.getElementById('openaiResponsesApiKey')?.value || '';
|
||||
config.OPENAI_BASE_URL = document.getElementById('openaiResponsesBaseUrl')?.value || '';
|
||||
break;
|
||||
}
|
||||
|
||||
// 保存高级配置参数
|
||||
config.SYSTEM_PROMPT_FILE_PATH = document.getElementById('systemPromptFilePath')?.value || 'configs/input_system_prompt.txt';
|
||||
config.SYSTEM_PROMPT_MODE = document.getElementById('systemPromptMode')?.value || 'append';
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ const translations = {
|
|||
'config.host': '监听地址',
|
||||
'config.port': '端口',
|
||||
'config.modelProvider': '模型提供商',
|
||||
'config.modelProviderHelp': '勾选启动时初始化的模型提供商 (必须至少勾选一个)',
|
||||
'config.modelProviderRequired': '必须至少勾选一个模型提供商',
|
||||
'config.optional': '(选填)',
|
||||
'config.gemini.baseUrl': 'Gemini Base URL',
|
||||
'config.gemini.baseUrlPlaceholder': 'https://cloudcode-pa.googleapis.com',
|
||||
|
|
@ -196,7 +198,7 @@ const translations = {
|
|||
'config.advanced.cronEnabled': '启用OAuth令牌自动刷新(需重启服务)',
|
||||
'config.advanced.poolFilePath': '提供商池配置文件路径(不能为空)',
|
||||
'config.advanced.poolFilePathPlaceholder': '默认: configs/provider_pools.json',
|
||||
'config.advanced.poolNote': '配置了提供商池后,默认使用提供商池的配置,提供商池配置失效降级到默认配置',
|
||||
'config.advanced.poolNote': '使用默认路径配置需添加一个空节点',
|
||||
'config.advanced.maxErrorCount': '提供商最大错误次数',
|
||||
'config.advanced.maxErrorCountPlaceholder': '默认: 3',
|
||||
'config.advanced.maxErrorCountNote': '提供商连续错误达到此次数后将被标记为不健康,默认为 3 次',
|
||||
|
|
@ -272,7 +274,7 @@ const translations = {
|
|||
|
||||
// Providers
|
||||
'providers.title': '提供商池管理',
|
||||
'providers.note': '配置了提供商池后,默认使用提供商池的配置,提供商池配置失效降级到默认配置',
|
||||
'providers.note': '使用默认路径配置需添加一个空节点',
|
||||
'providers.activeConnections': '活动连接',
|
||||
'providers.activeProviders': '活跃提供商',
|
||||
'providers.healthyProviders': '健康提供商',
|
||||
|
|
@ -361,6 +363,8 @@ const translations = {
|
|||
'usage.card.freeTrial': '免费试用',
|
||||
'usage.card.bonus': '奖励',
|
||||
'usage.card.expires': '到期: {time}',
|
||||
'usage.group.expandAll': '展开所有卡片',
|
||||
'usage.group.collapseAll': '折叠所有卡片',
|
||||
|
||||
// Logs
|
||||
'logs.title': '实时日志',
|
||||
|
|
@ -541,6 +545,8 @@ const translations = {
|
|||
'config.host': 'Listen Address',
|
||||
'config.port': 'Port',
|
||||
'config.modelProvider': 'Model Provider',
|
||||
'config.modelProviderHelp': 'Check model providers to initialize on startup (must select at least one)',
|
||||
'config.modelProviderRequired': 'At least one model provider must be selected',
|
||||
'config.optional': '(Optional)',
|
||||
'config.gemini.baseUrl': 'Gemini Base URL',
|
||||
'config.gemini.baseUrlPlaceholder': 'https://cloudcode-pa.googleapis.com',
|
||||
|
|
@ -601,7 +607,7 @@ const translations = {
|
|||
'config.advanced.cronEnabled': 'Enable OAuth Token Auto Refresh (requires restart)',
|
||||
'config.advanced.poolFilePath': 'Provider Pool Config File Path (required)',
|
||||
'config.advanced.poolFilePathPlaceholder': 'Default: configs/provider_pools.json',
|
||||
'config.advanced.poolNote': 'When provider pool is configured, it will be used by default. Falls back to default config if pool config fails',
|
||||
'config.advanced.poolNote': 'To use default path configuration, add an empty node',
|
||||
'config.advanced.maxErrorCount': 'Provider Max Error Count',
|
||||
'config.advanced.maxErrorCountPlaceholder': 'Default: 3',
|
||||
'config.advanced.maxErrorCountNote': 'Provider will be marked as unhealthy after consecutive errors reach this count, default is 3',
|
||||
|
|
@ -677,7 +683,7 @@ const translations = {
|
|||
|
||||
// Providers
|
||||
'providers.title': 'Provider Pool Management',
|
||||
'providers.note': 'When provider pool is configured, it will be used by default. Falls back to default config if pool config fails',
|
||||
'providers.note': 'To use default path configuration, add an empty node',
|
||||
'providers.activeConnections': 'Active Connections',
|
||||
'providers.activeProviders': 'Active Providers',
|
||||
'providers.healthyProviders': 'Healthy Providers',
|
||||
|
|
@ -766,6 +772,8 @@ const translations = {
|
|||
'usage.card.freeTrial': 'Free Trial',
|
||||
'usage.card.bonus': 'Bonus',
|
||||
'usage.card.expires': 'Expires: {time}',
|
||||
'usage.group.expandAll': 'Expand All Cards',
|
||||
'usage.group.collapseAll': 'Collapse All Cards',
|
||||
|
||||
// Logs
|
||||
'logs.title': 'Real-time Logs',
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ export function initLanguageSwitcher() {
|
|||
const headerControls = document.querySelector('.header-controls');
|
||||
if (headerControls) {
|
||||
const switcher = createLanguageSwitcher();
|
||||
// 插入到第一个位置
|
||||
headerControls.insertBefore(switcher, headerControls.firstChild);
|
||||
// 追加到最后位置(最右边)
|
||||
headerControls.appendChild(switcher);
|
||||
|
||||
// 绑定事件
|
||||
bindLanguageSwitcherEvents();
|
||||
|
|
|
|||
|
|
@ -791,7 +791,7 @@ function showAuthModal(authUrl, authInfo) {
|
|||
const applyBtn = modal.querySelector('.apply-callback-btn');
|
||||
|
||||
// 处理回调 URL 的核心逻辑
|
||||
const processCallback = (urlStr) => {
|
||||
const processCallback = (urlStr, isManualInput = false) => {
|
||||
try {
|
||||
// 尝试清理 URL(有些用户可能会复制多余的文字)
|
||||
const cleanUrlStr = urlStr.trim().match(/https?:\/\/[^\s]+/)?.[0] || urlStr.trim();
|
||||
|
|
@ -806,13 +806,50 @@ function showAuthModal(authUrl, authInfo) {
|
|||
|
||||
showToast(t('common.info'), t('oauth.processing'), 'info');
|
||||
|
||||
// 优先在子窗口中跳转(如果没关)
|
||||
if (authWindow && !authWindow.closed) {
|
||||
authWindow.location.href = localUrl.href;
|
||||
// 如果是手动输入,直接通过 fetch 请求处理,然后关闭子窗口
|
||||
if (isManualInput) {
|
||||
// 关闭子窗口
|
||||
if (authWindow && !authWindow.closed) {
|
||||
authWindow.close();
|
||||
}
|
||||
// 通过服务端API处理手动输入的回调URL
|
||||
window.apiClient.post('/oauth/manual-callback', {
|
||||
provider: authInfo.provider,
|
||||
callbackUrl: localUrl.href,
|
||||
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 {
|
||||
// 备选方案:通过隐藏 iframe 或者是 fetch
|
||||
const img = new Image();
|
||||
img.src = localUrl.href;
|
||||
// 自动监听模式:优先在子窗口中跳转(如果没关)
|
||||
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 {
|
||||
|
|
@ -825,7 +862,7 @@ function showAuthModal(authUrl, authInfo) {
|
|||
};
|
||||
|
||||
applyBtn.addEventListener('click', () => {
|
||||
processCallback(manualInput.value);
|
||||
processCallback(manualInput.value, true);
|
||||
});
|
||||
|
||||
// 启动定时器轮询子窗口 URL
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* CSS变量 */
|
||||
/* CSS变量 - 亮色主题(默认) */
|
||||
:root {
|
||||
--primary-color: #059669;
|
||||
--secondary-color: #10b981;
|
||||
|
|
@ -15,6 +15,40 @@
|
|||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
--transition: all 0.3s ease;
|
||||
|
||||
/* 代码块和日志区域 */
|
||||
--code-bg: #1e1e1e;
|
||||
--code-text: #d4d4d4;
|
||||
|
||||
/* 主题切换按钮 */
|
||||
--theme-toggle-bg: var(--bg-tertiary);
|
||||
--theme-toggle-icon: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* CSS变量 - 暗黑主题 */
|
||||
[data-theme="dark"] {
|
||||
--primary-color: #10b981;
|
||||
--secondary-color: #34d399;
|
||||
--success-color: #34d399;
|
||||
--danger-color: #f87171;
|
||||
--warning-color: #fbbf24;
|
||||
--bg-primary: #1f2937;
|
||||
--bg-secondary: #111827;
|
||||
--bg-tertiary: #374151;
|
||||
--text-primary: #f9fafb;
|
||||
--text-secondary: #9ca3af;
|
||||
--border-color: #4b5563;
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* 代码块和日志区域 */
|
||||
--code-bg: #0d1117;
|
||||
--code-text: #e6edf3;
|
||||
|
||||
/* 主题切换按钮 */
|
||||
--theme-toggle-bg: var(--bg-tertiary);
|
||||
--theme-toggle-icon: #fbbf24;
|
||||
}
|
||||
|
||||
/* 基础样式 */
|
||||
|
|
@ -536,6 +570,47 @@ textarea.form-control {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 复选框列表样式 */
|
||||
.provider-checklist {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.checkbox-item:hover {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checkbox-item span {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.config-row {
|
||||
|
|
@ -2174,7 +2249,7 @@ input:checked + .toggle-slider:before {
|
|||
}
|
||||
|
||||
.config-item-manager.expanded:hover {
|
||||
background: #e5e7eb;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
|
|
@ -3882,9 +3957,9 @@ input:checked + .toggle-slider:before {
|
|||
.usage-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-secondary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
|
@ -3898,6 +3973,46 @@ input:checked + .toggle-slider:before {
|
|||
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);
|
||||
}
|
||||
|
||||
.btn-toggle-cards:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-toggle-cards i {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.usage-group-title .toggle-icon {
|
||||
|
|
@ -3999,7 +4114,6 @@ input:checked + .toggle-slider:before {
|
|||
}
|
||||
|
||||
.usage-instance-card.success {
|
||||
border-left: 3px solid var(--success-color);
|
||||
}
|
||||
|
||||
.usage-instance-header {
|
||||
|
|
@ -4202,8 +4316,7 @@ input:checked + .toggle-slider:before {
|
|||
/* 总用量样式 */
|
||||
.total-usage {
|
||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
||||
border: 1px solid var(--primary-color);
|
||||
border-left: 3px solid var(--primary-color);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.total-usage-header {
|
||||
|
|
@ -4505,6 +4618,121 @@ input:checked + .toggle-slider:before {
|
|||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
/* 用量卡片折叠/展开样式 */
|
||||
|
||||
/* 折叠摘要 - 两行布局 */
|
||||
.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;
|
||||
}
|
||||
|
||||
.usage-card-collapsed-summary:hover {
|
||||
background: linear-gradient(to right, var(--bg-secondary), var(--bg-tertiary));
|
||||
}
|
||||
|
||||
/* 折叠摘要行 */
|
||||
.collapsed-summary-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* 第一行:名称行 */
|
||||
.collapsed-summary-name-row {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.collapsed-summary-name-row .provider-icon-small {
|
||||
font-size: 0.75rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.collapsed-summary-name-row .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-summary-usage-row {
|
||||
padding-left: 1.25rem; /* 与名称对齐,跳过图标 */
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.collapsed-error {
|
||||
font-size: 0.7rem;
|
||||
color: var(--danger-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.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-card-expanded-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.usage-instance-card.collapsed .usage-card-expanded-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 折叠状态下隐藏展开内容的边框 */
|
||||
.usage-instance-card.collapsed .usage-card-collapsed-summary {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* 用量卡片响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.usage-instance-header {
|
||||
|
|
@ -4766,3 +4994,525 @@ input:checked + .toggle-slider:before {
|
|||
padding: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 主题切换按钮样式 ==================== */
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
background: var(--theme-toggle-bg);
|
||||
color: var(--theme-toggle-icon);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 1.125rem;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.theme-toggle:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.theme-toggle i {
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-toggle .fa-sun {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-toggle .fa-moon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .theme-toggle .fa-sun {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .theme-toggle .fa-moon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 暗黑主题下的特殊样式调整 */
|
||||
|
||||
/* 日志容器 */
|
||||
[data-theme="dark"] .logs-container {
|
||||
background: var(--code-bg);
|
||||
color: var(--code-text);
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
[data-theme="dark"] .usage-example pre {
|
||||
background: var(--code-bg);
|
||||
color: var(--code-text);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .config-content-display {
|
||||
background: var(--code-bg);
|
||||
color: var(--code-text);
|
||||
}
|
||||
|
||||
/* 高亮说明样式 - 暗黑主题 */
|
||||
[data-theme="dark"] .highlight-note {
|
||||
background: linear-gradient(135deg, #78350f 0%, #92400e 100%);
|
||||
border-color: #b45309;
|
||||
color: #fef3c7;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .highlight-note i {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* 提供商徽章 - 暗黑主题 */
|
||||
[data-theme="dark"] .provider-badge.official {
|
||||
background: #1e3a5f;
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .provider-badge.oauth {
|
||||
background: #064e3b;
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .provider-badge.responses {
|
||||
background: #78350f;
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
/* 状态徽章 - 暗黑主题 */
|
||||
[data-theme="dark"] .status-healthy {
|
||||
background: #064e3b;
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .status-unhealthy {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .status-used {
|
||||
background: #064e3b;
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .status-unused {
|
||||
background: #78350f;
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .status-invalid {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
/* 更新徽章 - 暗黑主题 */
|
||||
[data-theme="dark"] .update-badge {
|
||||
background: #78350f;
|
||||
color: #fde68a;
|
||||
border-color: #b45309;
|
||||
}
|
||||
|
||||
/* 模态框 - 暗黑主题 */
|
||||
[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-modal-header h3 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[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, #78350f 0%, var(--bg-primary) 100%);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .provider-item-detail.unhealthy .provider-item-header {
|
||||
background: linear-gradient(135deg, #78350f 0%, var(--bg-primary) 100%);
|
||||
}
|
||||
|
||||
/* 错误信息 - 暗黑主题 */
|
||||
[data-theme="dark"] .provider-error-info {
|
||||
background: linear-gradient(135deg, #7f1d1d 0%, #991b1b 100%);
|
||||
border-color: #dc2626;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .provider-error-info i {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .provider-error-info .error-label {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .provider-error-info .error-message {
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
/* 路由提示 - 暗黑主题 */
|
||||
[data-theme="dark"] .routing-tips {
|
||||
background: var(--bg-secondary);
|
||||
border-left-color: var(--primary-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .routing-tips code {
|
||||
background: var(--bg-tertiary);
|
||||
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"] .delete-modal-content {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .delete-confirm-modal.used .delete-modal-header {
|
||||
background: linear-gradient(135deg, #7f1d1d 0%, var(--bg-primary) 100%);
|
||||
border-bottom-color: #dc2626;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .delete-confirm-modal.unused .delete-modal-header {
|
||||
background: linear-gradient(135deg, #78350f 0%, var(--bg-primary) 100%);
|
||||
border-bottom-color: #b45309;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .delete-warning.warning-used {
|
||||
background: linear-gradient(135deg, #7f1d1d 0%, var(--bg-primary) 100%);
|
||||
border-color: #dc2626;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .delete-warning.warning-unused {
|
||||
background: linear-gradient(135deg, #78350f 0%, var(--bg-primary) 100%);
|
||||
border-color: #b45309;
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .usage-alert {
|
||||
background: linear-gradient(135deg, #7f1d1d 0%, #991b1b 100%);
|
||||
border-color: #dc2626;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .alert-content h5 {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .alert-content p,
|
||||
[data-theme="dark"] .alert-content ul {
|
||||
color: #fecaca;
|
||||
}
|
||||
|
||||
/* 配置信息 - 暗黑主题 */
|
||||
[data-theme="dark"] .config-info {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .config-info-item {
|
||||
border-bottom-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .config-info-item .info-value.status-used {
|
||||
background: #064e3b;
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .config-info-item .info-value.status-unused {
|
||||
background: #78350f;
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
/* 授权模态框 - 暗黑主题 */
|
||||
[data-theme="dark"] .modal-content {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .modal-header {
|
||||
background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
|
||||
}
|
||||
|
||||
[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-note {
|
||||
background: #78350f;
|
||||
border-color: #b45309;
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .auth-url-input {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 重启提示模态框 - 暗黑主题 */
|
||||
[data-theme="dark"] .restart-required-modal .restart-modal-content {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .restart-notice {
|
||||
background: linear-gradient(135deg, #064e3b 0%, #065f46 100%);
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .restart-notice p {
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .restart-instructions {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
/* 用量卡片 - 暗黑主题 */
|
||||
[data-theme="dark"] .usage-instance-card.error {
|
||||
border-color: #dc2626;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .usage-instance-header {
|
||||
background: linear-gradient(to right, var(--bg-tertiary), var(--bg-secondary));
|
||||
}
|
||||
|
||||
[data-theme="dark"] .usage-error-message {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
[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"] .breakdown-item-compact {
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .extra-usage-info.free-trial {
|
||||
background: #1e3a5f;
|
||||
color: #93c5fd;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .extra-usage-info.bonus {
|
||||
background: #78350f;
|
||||
color: #fde68a;
|
||||
border-color: #b45309;
|
||||
}
|
||||
|
||||
/* 分页控件 - 暗黑主题 */
|
||||
[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-btn.nav-btn {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .page-jump-input {
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 联系卡片 - 暗黑主题 */
|
||||
[data-theme="dark"] .contact-card {
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .contact-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .qr-code {
|
||||
border-color: var(--border-color);
|
||||
background: white;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .qr-code:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 图片放大遮罩 - 暗黑主题 */
|
||||
[data-theme="dark"] .image-zoom-overlay {
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
}
|
||||
|
||||
/* Toast 通知 - 暗黑主题 */
|
||||
[data-theme="dark"] .toast {
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
/* 模型复选框 - 暗黑主题 */
|
||||
[data-theme="dark"] .model-checkbox-label {
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .model-checkbox-label:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .not-supported-models-container {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
/* 表单控件 - 暗黑主题 */
|
||||
[data-theme="dark"] .form-control {
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
[data-theme="dark"] select.form-control {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] select.form-control option {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 按钮 - 暗黑主题调整 */
|
||||
[data-theme="dark"] .btn-secondary {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-secondary:hover {
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-outline {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .btn-outline:hover {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 语言切换器 - 暗黑主题 */
|
||||
[data-theme="dark"] .language-btn {
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .language-btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .language-dropdown {
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .language-option:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* 主题切换动画 */
|
||||
body {
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.header,
|
||||
.sidebar,
|
||||
.content,
|
||||
.stat-card,
|
||||
.config-panel,
|
||||
.providers-container,
|
||||
.routing-examples-panel,
|
||||
.system-info-panel,
|
||||
.upload-config-panel,
|
||||
.usage-panel,
|
||||
.logs-container,
|
||||
.toast,
|
||||
.modal-content,
|
||||
.provider-modal-content {
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
|
|
|||
118
static/app/theme-switcher.js
Normal file
118
static/app/theme-switcher.js
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
/**
|
||||
* 主题切换模块
|
||||
* 支持亮色/暗黑主题切换,并保存用户偏好到 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 };
|
||||
|
|
@ -228,15 +228,49 @@ function createProviderGroup(providerType, instances) {
|
|||
<span class="instance-count" data-i18n="usage.group.instances" data-i18n-params='{"count":"${instanceCount}"}'>${t('usage.group.instances', { count: instanceCount })}</span>
|
||||
<span class="success-count ${successCount === instanceCount ? 'all-success' : ''}" data-i18n="usage.group.success" data-i18n-params='{"count":"${successCount}","total":"${instanceCount}"}'>${t('usage.group.success', { count: successCount, total: instanceCount })}</span>
|
||||
</div>
|
||||
<div class="usage-group-actions">
|
||||
<button class="btn-toggle-cards" title="${t('usage.group.expandAll')}">
|
||||
<i class="fas fa-expand-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 点击头部切换折叠状态
|
||||
header.addEventListener('click', () => {
|
||||
// 点击头部切换分组折叠状态
|
||||
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';
|
||||
|
|
@ -263,19 +297,59 @@ function createProviderGroup(providerType, instances) {
|
|||
*/
|
||||
function createInstanceUsageCard(instance, providerType) {
|
||||
const card = document.createElement('div');
|
||||
card.className = `usage-instance-card ${instance.success ? 'success' : 'error'}`;
|
||||
card.className = `usage-instance-card ${instance.success ? 'success' : 'error'} collapsed`;
|
||||
|
||||
const providerDisplayName = getProviderDisplayName(providerType);
|
||||
const providerIcon = getProviderIcon(providerType);
|
||||
|
||||
// 实例头部 - 整合用户信息
|
||||
const header = document.createElement('div');
|
||||
header.className = 'usage-instance-header';
|
||||
// 计算总用量(用于折叠摘要显示)
|
||||
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
|
||||
? '<i class="fas fa-check-circle status-success"></i>'
|
||||
: '<i class="fas fa-times-circle status-error"></i>';
|
||||
|
||||
// 显示名称:优先自定义名称,其次 uuid
|
||||
const displayName = instance.name || instance.uuid;
|
||||
|
||||
collapsedSummary.innerHTML = `
|
||||
<div class="collapsed-summary-row collapsed-summary-name-row">
|
||||
<i class="fas fa-chevron-right usage-toggle-icon"></i>
|
||||
<span class="collapsed-name" title="${displayName}">${displayName}</span>
|
||||
${statusIcon}
|
||||
</div>
|
||||
<div class="collapsed-summary-row collapsed-summary-usage-row">
|
||||
${totalUsage.hasData ? `
|
||||
<div class="collapsed-progress-bar ${progressClass}">
|
||||
<div class="progress-fill" style="width: ${totalUsage.percent}%"></div>
|
||||
</div>
|
||||
<span class="collapsed-percent">${totalUsage.percent.toFixed(1)}%</span>
|
||||
<span class="collapsed-usage-text">${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)}</span>
|
||||
` : (instance.error ? `<span class="collapsed-error">${t('common.error')}</span>` : '')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 点击折叠摘要切换展开状态
|
||||
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
|
||||
? `<span class="badge badge-disabled" data-i18n="usage.card.status.disabled">${t('usage.card.status.disabled')}</span>`
|
||||
: (instance.isHealthy
|
||||
|
|
@ -310,7 +384,7 @@ function createInstanceUsageCard(instance, providerType) {
|
|||
</div>
|
||||
${userInfoHTML}
|
||||
`;
|
||||
card.appendChild(header);
|
||||
expandedContent.appendChild(header);
|
||||
|
||||
// 实例内容 - 只显示用量和到期时间
|
||||
const content = document.createElement('div');
|
||||
|
|
@ -327,7 +401,9 @@ function createInstanceUsageCard(instance, providerType) {
|
|||
content.appendChild(renderUsageDetails(instance.usage));
|
||||
}
|
||||
|
||||
card.appendChild(content);
|
||||
expandedContent.appendChild(content);
|
||||
card.appendChild(expandedContent);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
|
|
@ -352,7 +428,10 @@ function renderUsageDetails(usage) {
|
|||
|
||||
totalSection.innerHTML = `
|
||||
<div class="total-usage-header">
|
||||
<span class="total-label"><i class="fas fa-chart-pie"></i> <span data-i18n="usage.card.totalUsage">${t('usage.card.totalUsage')}</span></span>
|
||||
<span class="total-label">
|
||||
<i class="fas fa-chart-pie"></i>
|
||||
<span data-i18n="usage.card.totalUsage">${t('usage.card.totalUsage')}</span>
|
||||
</span>
|
||||
<span class="total-value">${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)}</span>
|
||||
</div>
|
||||
<div class="progress-bar ${progressClass}">
|
||||
|
|
@ -360,6 +439,7 @@ function renderUsageDetails(usage) {
|
|||
</div>
|
||||
<div class="total-percent">${totalUsage.percent.toFixed(2)}%</div>
|
||||
`;
|
||||
|
||||
container.appendChild(totalSection);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,10 +20,13 @@
|
|||
<span class="status-badge" id="serverStatus">
|
||||
<i class="fas fa-circle"></i> <span class="status-text" data-i18n="header.status.connecting">连接中...</span>
|
||||
</span>
|
||||
<button id="themeToggleBtn" class="theme-toggle" aria-label="Toggle Theme" data-i18n-aria-label="header.themeToggle" title="切换主题" data-i18n-title="header.themeToggle">
|
||||
<i class="fas fa-moon"></i>
|
||||
<i class="fas fa-sun"></i>
|
||||
</button>
|
||||
<button id="logoutBtn" class="logout-btn" data-i18n="header.logout" title="Logout" data-i18n-title="header.logout">
|
||||
<i class="fas fa-sign-out-alt"></i> <span data-i18n="header.logout">登出</span>
|
||||
</button>
|
||||
</span>
|
||||
<button id="restartBtn" class="logout-btn" aria-label="Restart Service" data-i18n-aria-label="header.restart">
|
||||
<i id="restartBtnIcon" class="fas fa-redo"></i> <span id="restartBtnText" class="btn-text" data-i18n="header.restart">重启</span>
|
||||
</button>
|
||||
|
|
@ -543,208 +546,38 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="modelProvider" data-i18n="config.modelProvider">模型提供商</label>
|
||||
<select id="modelProvider" class="form-control">
|
||||
<option value="gemini-cli-oauth">Gemini CLI OAuth</option>
|
||||
<option value="gemini-antigravity">Gemini Antigravity</option>
|
||||
<option value="openai-custom">OpenAI Custom</option>
|
||||
<option value="claude-custom">Claude Custom</option>
|
||||
<option value="claude-kiro-oauth">Claude Kiro OAuth</option>
|
||||
<option value="openai-qwen-oauth">Qwen OAuth</option>
|
||||
<option value="openaiResponses-custom">OpenAI Responses</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Gemini CLI OAuth 配置 -->
|
||||
<div class="provider-config" data-provider="gemini-cli-oauth">
|
||||
<div class="form-group">
|
||||
<label for="geminiBaseUrl"><span data-i18n="config.gemini.baseUrl">Gemini Base URL</span> <span class="optional-tag" data-i18n="config.optional">(选填)</span></label>
|
||||
<input type="text" id="geminiBaseUrl" class="form-control" data-i18n-placeholder="config.gemini.baseUrlPlaceholder" placeholder="https://cloudcode-pa.googleapis.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="projectId"><span data-i18n="config.gemini.projectId">项目ID</span> <span class="optional-tag" data-i18n="config.optional">(选填)</span></label>
|
||||
<input type="text" id="projectId" class="form-control" data-i18n-placeholder="config.gemini.projectIdPlaceholder" placeholder="Google Cloud项目ID">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="config.gemini.oauthCreds">OAuth凭据</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="geminiCredsType" value="file" checked>
|
||||
<span data-i18n="config.gemini.credsType.file">文件路径</span>
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="geminiCredsType" value="base64">
|
||||
<span data-i18n="config.gemini.credsType.base64">Base64编码</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="geminiCredsBase64Group">
|
||||
<label for="geminiOauthCredsBase64" data-i18n="config.gemini.credsBase64">OAuth凭据 (Base64)</label>
|
||||
<textarea id="geminiOauthCredsBase64" class="form-control" rows="3" data-i18n-placeholder="config.gemini.credsBase64Placeholder" placeholder="请输入Base64编码的OAuth凭据"></textarea>
|
||||
</div>
|
||||
<div class="form-group" id="geminiCredsFileGroup" style="display: none;">
|
||||
<label for="geminiOauthCredsFilePath" data-i18n="config.gemini.credsFilePath">OAuth凭据文件路径</label>
|
||||
<div class="file-input-group">
|
||||
<input type="text" id="geminiOauthCredsFilePath" class="form-control" data-i18n-placeholder="config.gemini.credsFilePathPlaceholder" placeholder="例如: ~/.gemini/oauth_creds.json">
|
||||
<button type="button" class="btn btn-outline upload-btn" data-target="geminiOauthCredsFilePath" data-i18n-title="common.upload" title="上传文件" aria-label="上传文件" data-i18n-aria-label="common.upload">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline generate-creds-btn" data-target="geminiOauthCredsFilePath" data-provider="gemini-cli-oauth" data-i18n-title="common.generate" title="生成凭据文件">
|
||||
<i class="fas fa-magic"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gemini Antigravity 配置 -->
|
||||
<div class="provider-config" data-provider="gemini-antigravity" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="antigravityBaseUrlDaily"><span data-i18n="config.antigravity.dailyUrl">Daily Base URL</span> <span class="optional-tag" data-i18n="config.optional">(选填)</span></label>
|
||||
<input type="text" id="antigravityBaseUrlDaily" class="form-control" data-i18n-placeholder="config.antigravity.dailyUrlPlaceholder" placeholder="https://daily-cloudcode-pa.sandbox.googleapis.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="antigravityBaseUrlAutopush"><span data-i18n="config.antigravity.autopushUrl">Autopush Base URL</span> <span class="optional-tag" data-i18n="config.optional">(选填)</span></label>
|
||||
<input type="text" id="antigravityBaseUrlAutopush" class="form-control" data-i18n-placeholder="config.antigravity.autopushUrlPlaceholder" placeholder="https://autopush-cloudcode-pa.sandbox.googleapis.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="antigravityOauthCredsFilePath" data-i18n="config.antigravity.credsFilePath">OAuth凭据文件路径</label>
|
||||
<div class="file-input-group">
|
||||
<input type="text" id="antigravityOauthCredsFilePath" class="form-control" data-i18n-placeholder="config.antigravity.credsFilePathPlaceholder" placeholder="例如: ~/.antigravity/oauth_creds.json">
|
||||
<button type="button" class="btn btn-outline upload-btn" data-target="antigravityOauthCredsFilePath" data-i18n-title="common.upload" title="上传文件" aria-label="上传文件" data-i18n-aria-label="common.upload">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline generate-creds-btn" data-target="antigravityOauthCredsFilePath" data-provider="gemini-antigravity" data-i18n-title="common.generate" title="生成凭据文件">
|
||||
<i class="fas fa-magic"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text" data-i18n="config.antigravity.note">Antigravity 使用 Google OAuth 认证,需要提供凭据文件路径</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI Custom 配置 -->
|
||||
<div class="provider-config" data-provider="openai-custom" style="display: none;">
|
||||
<div class="form-group password-input-group">
|
||||
<label for="openaiApiKey" data-i18n="config.openai.apiKey">OpenAI API Key</label>
|
||||
<div class="password-input-wrapper">
|
||||
<input type="password" id="openaiApiKey" class="form-control" data-i18n-placeholder="config.openai.apiKeyPlaceholder" placeholder="sk-...">
|
||||
<button type="button" class="password-toggle" data-target="openaiApiKey">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openaiBaseUrl" data-i18n="config.openai.baseUrl">OpenAI Base URL</label>
|
||||
<input type="text" id="openaiBaseUrl" class="form-control" value="https://api.openai.com/v1" data-i18n-placeholder="config.openai.baseUrlPlaceholder" placeholder="例如: https://api.openai.com/v1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude Custom 配置 -->
|
||||
<div class="provider-config" data-provider="claude-custom" style="display: none;">
|
||||
<div class="form-group password-input-group">
|
||||
<label for="claudeApiKey" data-i18n="config.claude.apiKey">Claude API Key</label>
|
||||
<div class="password-input-wrapper">
|
||||
<input type="password" id="claudeApiKey" class="form-control" data-i18n-placeholder="config.claude.apiKeyPlaceholder" placeholder="sk-ant-...">
|
||||
<button type="button" class="password-toggle" data-target="claudeApiKey">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="claudeBaseUrl" data-i18n="config.claude.baseUrl">Claude Base URL</label>
|
||||
<input type="text" id="claudeBaseUrl" class="form-control" value="https://api.anthropic.com" data-i18n-placeholder="config.claude.baseUrlPlaceholder" placeholder="例如: https://api.anthropic.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude Kiro OAuth 配置 -->
|
||||
<div class="provider-config" data-provider="claude-kiro-oauth" style="display: none;">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="kiroBaseUrl"><span data-i18n="config.kiro.baseUrl">Base URL</span> <span class="optional-tag" data-i18n="config.optional">(选填)</span></label>
|
||||
<input type="text" id="kiroBaseUrl" class="form-control" data-i18n-placeholder="config.kiro.baseUrlPlaceholder" placeholder="https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="kiroRefreshUrl"><span data-i18n="config.kiro.refreshUrl">Refresh URL</span> <span class="optional-tag" data-i18n="config.optional">(选填)</span></label>
|
||||
<input type="text" id="kiroRefreshUrl" class="form-control" data-i18n-placeholder="config.kiro.refreshUrlPlaceholder" placeholder="https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="kiroRefreshIdcUrl"><span data-i18n="config.kiro.refreshIdcUrl">Refresh IDC URL</span> <span class="optional-tag" data-i18n="config.optional">(选填)</span></label>
|
||||
<input type="text" id="kiroRefreshIdcUrl" class="form-control" data-i18n-placeholder="config.kiro.refreshIdcUrlPlaceholder" placeholder="https://oidc.{{region}}.amazonaws.com/token">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="config.gemini.oauthCreds">OAuth凭据</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="kiroCredsType" value="file" checked>
|
||||
<span data-i18n="config.gemini.credsType.file">文件路径</span>
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="kiroCredsType" value="base64">
|
||||
<span data-i18n="config.gemini.credsType.base64">Base64编码</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="kiroCredsBase64Group">
|
||||
<label for="kiroOauthCredsBase64" data-i18n="config.gemini.credsBase64">OAuth凭据 (Base64)</label>
|
||||
<textarea id="kiroOauthCredsBase64" class="form-control" rows="3" data-i18n="config.gemini.credsBase64Placeholder" placeholder="请输入Base64编码的OAuth凭据"></textarea>
|
||||
</div>
|
||||
<div class="form-group" id="kiroCredsFileGroup" style="display: none;">
|
||||
<label for="kiroOauthCredsFilePath" data-i18n="config.kiro.credsFilePath">OAuth凭据文件路径</label>
|
||||
<div class="file-input-group">
|
||||
<input type="text" id="kiroOauthCredsFilePath" class="form-control" data-i18n-placeholder="config.kiro.credsFilePathPlaceholder" placeholder="例如: ~/.aws/sso/cache/kiro-auth-token.json">
|
||||
<button type="button" class="btn btn-outline upload-btn" data-target="kiroOauthCredsFilePath" data-i18n-title="common.upload" title="上传文件" aria-label="上传文件" data-i18n-aria-label="common.upload">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline generate-creds-btn" data-target="kiroOauthCredsFilePath" data-provider="claude-kiro-oauth" data-i18n-title="common.generate" title="生成凭据文件">
|
||||
<i class="fas fa-magic"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span data-i18n="config.kiro.note">使用 AWS 登录方式时,请确保授权文件中包含 clientId 和 clientSecret 字段</span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Qwen OAuth 配置 -->
|
||||
<div class="provider-config" data-provider="openai-qwen-oauth" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="qwenBaseUrl"><span data-i18n="config.qwen.baseUrl">Qwen Base URL</span> <span class="optional-tag" data-i18n="config.optional">(选填)</span></label>
|
||||
<input type="text" id="qwenBaseUrl" class="form-control" data-i18n-placeholder="config.qwen.baseUrlPlaceholder" placeholder="https://portal.qwen.ai/v1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="qwenOauthBaseUrl"><span data-i18n="config.qwen.oauthBaseUrl">OAuth Base URL</span> <span class="optional-tag" data-i18n="config.optional">(选填)</span></label>
|
||||
<input type="text" id="qwenOauthBaseUrl" class="form-control" data-i18n-placeholder="config.qwen.oauthBaseUrlPlaceholder" placeholder="https://chat.qwen.ai">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="qwenOauthCredsFilePath" data-i18n="config.qwen.credsFilePath">OAuth凭据文件路径</label>
|
||||
<div class="file-input-group">
|
||||
<input type="text" id="qwenOauthCredsFilePath" class="form-control" data-i18n-placeholder="config.qwen.credsFilePathPlaceholder" placeholder="例如: ~/.qwen/oauth_creds.json">
|
||||
<button type="button" class="btn btn-outline upload-btn" data-target="qwenOauthCredsFilePath" data-i18n-title="common.upload" title="上传文件" aria-label="上传文件" data-i18n-aria-label="common.upload">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline generate-creds-btn" data-target="qwenOauthCredsFilePath" data-provider="openai-qwen-oauth" data-i18n-title="common.generate" title="生成凭据文件">
|
||||
<i class="fas fa-magic"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI Responses 配置 -->
|
||||
<div class="provider-config" data-provider="openaiResponses-custom" style="display: none;">
|
||||
<div class="form-group password-input-group">
|
||||
<label for="openaiResponsesApiKey" data-i18n="config.openai.apiKey">OpenAI API Key</label>
|
||||
<div class="password-input-wrapper">
|
||||
<input type="password" id="openaiResponsesApiKey" class="form-control" data-i18n-placeholder="config.openai.apiKeyPlaceholder" placeholder="sk-...">
|
||||
<button type="button" class="password-toggle" data-target="openaiResponsesApiKey">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openaiResponsesBaseUrl" data-i18n="config.openai.baseUrl">OpenAI Base URL</label>
|
||||
<input type="text" id="openaiResponsesBaseUrl" class="form-control" value="https://api.openai.com/v1" data-i18n-placeholder="config.openai.baseUrlPlaceholder" placeholder="例如: https://api.openai.com/v1">
|
||||
<label data-i18n="config.modelProvider">模型提供商 (可多选)</label>
|
||||
<div id="modelProvider" class="provider-checklist">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="gemini-cli-oauth">
|
||||
<span>Gemini CLI OAuth</span>
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="gemini-antigravity">
|
||||
<span>Gemini Antigravity</span>
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="openai-custom">
|
||||
<span>OpenAI Custom</span>
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="claude-custom">
|
||||
<span>Claude Custom</span>
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="claude-kiro-oauth">
|
||||
<span>Claude Kiro OAuth</span>
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="openai-qwen-oauth">
|
||||
<span>Qwen OAuth</span>
|
||||
</label>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="openaiResponses-custom">
|
||||
<span>OpenAI Responses</span>
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-muted" data-i18n="config.modelProviderHelp">勾选启动时初始化的模型提供商 (必须至少勾选一个)</small>
|
||||
</div>
|
||||
|
||||
<!-- 高级配置区域 -->
|
||||
|
|
@ -808,7 +641,7 @@
|
|||
<div class="form-group pool-section">
|
||||
<label for="providerPoolsFilePath" data-i18n="config.advanced.poolFilePath">提供商池配置文件路径(不能为空)</label>
|
||||
<input type="text" id="providerPoolsFilePath" class="form-control" value="" data-i18n-placeholder="config.advanced.poolFilePathPlaceholder" placeholder="默认: configs/provider_pools.json">
|
||||
<small class="form-text" data-i18n="config.advanced.poolNote">配置了提供商池后,默认使用提供商池的配置,提供商池配置失效降级到默认配置</small>
|
||||
<small class="form-text" data-i18n="config.advanced.poolNote">使用默认路径配置需添加一个空节点</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group pool-section">
|
||||
|
|
@ -923,7 +756,7 @@
|
|||
<div class="pool-description">
|
||||
<div class="highlight-note">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span data-i18n="providers.note">配置了提供商池后,默认使用提供商池的配置,提供商池配置失效降级到默认配置</span>
|
||||
<span data-i18n="providers.note">使用默认路径配置需添加一个空节点</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Provider Pool Stats -->
|
||||
|
|
@ -1019,16 +852,21 @@
|
|||
<!-- Scripts -->
|
||||
<script type="module" src="app/i18n.js"></script>
|
||||
<script type="module" src="app/language-switcher.js"></script>
|
||||
<script type="module" src="app/theme-switcher.js"></script>
|
||||
<script type="module" src="app/auth.js"></script>
|
||||
<script type="module">
|
||||
// 导入多语言和认证函数
|
||||
// 导入多语言、主题切换和认证函数
|
||||
import { initI18n, t } from './app/i18n.js';
|
||||
import { initLanguageSwitcher } from './app/language-switcher.js';
|
||||
import { initThemeSwitcher } from './app/theme-switcher.js';
|
||||
import { initAuth, logout } from './app/auth.js';
|
||||
|
||||
// 初始化多语言
|
||||
initI18n();
|
||||
|
||||
// 初始化主题切换器(尽早初始化以避免闪烁)
|
||||
initThemeSwitcher();
|
||||
|
||||
// 页面加载时检查登录状态
|
||||
(async function() {
|
||||
const isAuthenticated = await initAuth();
|
||||
|
|
|
|||
Loading…
Reference in a new issue