diff --git a/config.json.example b/config.json.example index c032ffa..d842c52 100644 --- a/config.json.example +++ b/config.json.example @@ -21,5 +21,6 @@ "REQUEST_BASE_DELAY": 1000, "CRON_NEAR_MINUTES": 1, "CRON_REFRESH_TOKEN": false, - "PROVIDER_POOLS_FILE_PATH": "provider_pools.json" + "PROVIDER_POOLS_FILE_PATH": "provider_pools.json", + "MAX_ERROR_COUNT": 3 } \ No newline at end of file diff --git a/src/config-manager.js b/src/config-manager.js index 01a57dd..f34257a 100644 --- a/src/config-manager.js +++ b/src/config-manager.js @@ -85,7 +85,8 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP REQUEST_BASE_DELAY: 1000, CRON_NEAR_MINUTES: 15, CRON_REFRESH_TOKEN: false, - PROVIDER_POOLS_FILE_PATH: '' // 新增号池配置文件路径 + PROVIDER_POOLS_FILE_PATH: '', // 新增号池配置文件路径 + MAX_ERROR_COUNT: 3 // 提供商最大错误次数 }; console.log('[Config] Using default configuration.'); } @@ -251,6 +252,13 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP } else { console.warn(`[Config Warning] --provider-pools-file flag requires a value.`); } + } else if (args[i] === '--max-error-count') { + if (i + 1 < args.length) { + currentConfig.MAX_ERROR_COUNT = parseInt(args[i + 1], 10); + i++; + } else { + console.warn(`[Config Warning] --max-error-count flag requires a value.`); + } } } diff --git a/src/openai/qwen-core.js b/src/openai/qwen-core.js index 6e199fe..5a6a1d4 100644 --- a/src/openai/qwen-core.js +++ b/src/openai/qwen-core.js @@ -491,7 +491,7 @@ export class QwenApiService { const maxRetries = (this.config && this.config.REQUEST_MAX_RETRIES) || 3; const baseDelay = (this.config && this.config.REQUEST_BASE_DELAY) || 1000; - const version = "0.0.9"; + const version = "0.2.1"; const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`; console.log(`[QwenApiService] User-Agent: ${userAgent}`); diff --git a/src/service-manager.js b/src/service-manager.js index 7b8c563..3a381f0 100644 --- a/src/service-manager.js +++ b/src/service-manager.js @@ -12,7 +12,10 @@ let providerPoolManager = null; */ export async function initApiService(config) { if (config.providerPools && Object.keys(config.providerPools).length > 0) { - providerPoolManager = new ProviderPoolManager(config.providerPools, { globalConfig: config }); + providerPoolManager = new ProviderPoolManager(config.providerPools, { + globalConfig: config, + maxErrorCount: config.MAX_ERROR_COUNT || 3 + }); console.log('[Initialization] ProviderPoolManager initialized with configured pools.'); // 健康检查将在服务器完全启动后执行 } else { diff --git a/src/ui-manager.js b/src/ui-manager.js index 97297fa..f3f2ec6 100644 --- a/src/ui-manager.js +++ b/src/ui-manager.js @@ -5,6 +5,8 @@ import multer from 'multer'; import crypto from 'crypto'; import { getRequestBody } from './common.js'; import { CONFIG } from './config-manager.js'; +import { serviceInstances } from './adapter.js'; +import { initApiService } from './service-manager.js'; // Token存储在内存中(生产环境建议使用Redis) const tokenStore = new Map(); @@ -252,6 +254,36 @@ export async function serveStaticFiles(pathParam, res) { * @param {Object} providerPoolManager - The provider pool manager instance * @returns {Promise} - True if the request was handled by UI API */ +/** + * 重载配置文件 + * 动态导入config-manager并重新初始化配置 + * @returns {Promise} 返回重载后的配置对象 + */ +async function reloadConfig() { + try { + // Import config manager dynamically + const { initializeConfig } = await import('./config-manager.js'); + + // Reload main config + const newConfig = await initializeConfig(process.argv.slice(2), 'config.json'); + + // Update global CONFIG + Object.assign(CONFIG, newConfig); + console.log('[UI API] Configuration reloaded:'); + + // Update initApiService - 清空并重新初始化服务实例 + Object.keys(serviceInstances).forEach(key => delete serviceInstances[key]); + initApiService(CONFIG); + + console.log('[UI API] Configuration reloaded successfully'); + + return newConfig; + } catch (error) { + console.error('[UI API] Failed to reload configuration:', error); + throw error; + } +} + export async function handleUIApiRequests(method, pathParam, req, res, currentConfig, providerPoolManager) { // 处理登录接口 if (method === 'POST' && pathParam === '/api/login') { @@ -407,6 +439,7 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo if (newConfig.CRON_NEAR_MINUTES !== undefined) currentConfig.CRON_NEAR_MINUTES = newConfig.CRON_NEAR_MINUTES; if (newConfig.CRON_REFRESH_TOKEN !== undefined) currentConfig.CRON_REFRESH_TOKEN = newConfig.CRON_REFRESH_TOKEN; if (newConfig.PROVIDER_POOLS_FILE_PATH !== undefined) currentConfig.PROVIDER_POOLS_FILE_PATH = newConfig.PROVIDER_POOLS_FILE_PATH; + if (newConfig.MAX_ERROR_COUNT !== undefined) currentConfig.MAX_ERROR_COUNT = newConfig.MAX_ERROR_COUNT; // Handle system prompt update if (newConfig.systemPrompt !== undefined) { @@ -414,7 +447,7 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo try { const relativePath = path.relative(process.cwd(), promptPath); writeFileSync(promptPath, newConfig.systemPrompt, 'utf-8'); - + // 广播更新事件 broadcastEvent('config_update', { action: 'update', @@ -457,7 +490,8 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo REQUEST_BASE_DELAY: currentConfig.REQUEST_BASE_DELAY, CRON_NEAR_MINUTES: currentConfig.CRON_NEAR_MINUTES, CRON_REFRESH_TOKEN: currentConfig.CRON_REFRESH_TOKEN, - PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH + PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH, + MAX_ERROR_COUNT: currentConfig.MAX_ERROR_COUNT }; writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8'); @@ -616,9 +650,6 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo providerPoolManager.initializeProviderStatus(); } - // Update CONFIG cache to maintain consistency - CONFIG.providerPools = providerPools; - // 广播更新事件 broadcastEvent('config_update', { action: 'add', @@ -716,9 +747,6 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo providerPoolManager.initializeProviderStatus(); } - // Update CONFIG cache to maintain consistency - CONFIG.providerPools = providerPools; - // 广播更新事件 broadcastEvent('config_update', { action: 'update', @@ -791,9 +819,6 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo providerPoolManager.initializeProviderStatus(); } - // Update CONFIG cache to maintain consistency - CONFIG.providerPools = providerPools; - // 广播更新事件 broadcastEvent('config_update', { action: 'delete', @@ -870,9 +895,6 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo } } - // Update CONFIG cache to maintain consistency - CONFIG.providerPools = providerPools; - // 广播更新事件 broadcastEvent('config_update', { action: action, @@ -1065,14 +1087,8 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo // Reload configuration files if (method === 'POST' && pathParam === '/api/reload-config') { try { - // Import config manager dynamically - const { initializeConfig } = await import('./config-manager.js'); - - // Reload main config - const newConfig = await initializeConfig(process.argv.slice(2), 'config.json'); - - // Update global CONFIG - Object.assign(CONFIG, newConfig); + // 调用重载配置函数 + const newConfig = await reloadConfig(); // 广播更新事件 broadcastEvent('config_update', { @@ -1110,9 +1126,8 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo /** * Initialize UI management features - * @param {Object} config - The server configuration */ -export function initializeUIManagement(config) { +export function initializeUIManagement() { // Initialize log broadcasting for UI if (!global.eventClients) { global.eventClients = []; diff --git a/static/app/config-manager.js b/static/app/config-manager.js index 6251f6e..7db816e 100644 --- a/static/app/config-manager.js +++ b/static/app/config-manager.js @@ -75,6 +75,7 @@ async function loadConfiguration() { const cronNearMinutesEl = document.getElementById('cronNearMinutes'); const cronRefreshTokenEl = document.getElementById('cronRefreshToken'); const providerPoolsFilePathEl = document.getElementById('providerPoolsFilePath'); + const maxErrorCountEl = document.getElementById('maxErrorCount'); if (systemPromptFilePathEl) systemPromptFilePathEl.value = data.SYSTEM_PROMPT_FILE_PATH || 'input_system_prompt.txt'; if (systemPromptModeEl) systemPromptModeEl.value = data.SYSTEM_PROMPT_MODE || 'append'; @@ -85,6 +86,7 @@ async function loadConfiguration() { if (cronNearMinutesEl) cronNearMinutesEl.value = data.CRON_NEAR_MINUTES || 1; if (cronRefreshTokenEl) cronRefreshTokenEl.checked = data.CRON_REFRESH_TOKEN || false; if (providerPoolsFilePathEl) providerPoolsFilePathEl.value = data.PROVIDER_POOLS_FILE_PATH || ''; + if (maxErrorCountEl) maxErrorCountEl.value = data.MAX_ERROR_COUNT || 3; // 触发提供商配置显示 handleProviderChange(); @@ -188,10 +190,11 @@ async function saveConfiguration() { config.CRON_NEAR_MINUTES = parseInt(document.getElementById('cronNearMinutes')?.value || 1); config.CRON_REFRESH_TOKEN = document.getElementById('cronRefreshToken')?.checked || false; config.PROVIDER_POOLS_FILE_PATH = document.getElementById('providerPoolsFilePath')?.value || ''; + config.MAX_ERROR_COUNT = parseInt(document.getElementById('maxErrorCount')?.value || 3); try { await window.apiClient.post('/config', config); - + await window.apiClient.post('/reload-config'); showToast('配置已保存', 'success'); // 检查当前是否在提供商池管理页面,如果是则刷新数据 diff --git a/static/app/file-upload.js b/static/app/file-upload.js index 61005dd..4d1bedb 100644 --- a/static/app/file-upload.js +++ b/static/app/file-upload.js @@ -21,7 +21,10 @@ class FileUploadHandler { const button = event.target.closest('.upload-btn'); const targetInputId = button.getAttribute('data-target'); if (targetInputId) { - this.handleFileUpload(button, targetInputId); + // 尝试从模态框获取 providerType + const modal = button.closest('.provider-modal'); + const providerType = modal ? modal.getAttribute('data-provider-type') : null; + this.handleFileUpload(button, targetInputId, providerType); } } }); @@ -61,8 +64,9 @@ class FileUploadHandler { * 处理文件上传 * @param {HTMLElement} button - 上传按钮元素 * @param {string} targetInputId - 目标输入框ID + * @param {string} providerType - 提供商类型 */ - async handleFileUpload(button, targetInputId) { + async handleFileUpload(button, targetInputId, providerType) { // 创建隐藏的文件输入元素 const fileInput = this.createFileInput(); @@ -73,7 +77,7 @@ class FileUploadHandler { if (file) { // 只有文件被实际选择后才显示加载状态并上传 this.setButtonLoading(button, true); - await this.uploadFile(file, targetInputId, button); + await this.uploadFile(file, targetInputId, button, providerType); } // 清理临时文件输入元素 @@ -102,8 +106,9 @@ class FileUploadHandler { * @param {File} file - 要上传的文件 * @param {string} targetInputId - 目标输入框ID * @param {HTMLElement} button - 上传按钮 + * @param {string} providerType - 提供商类型 */ - async uploadFile(file, targetInputId, button) { + async uploadFile(file, targetInputId, button, providerType) { try { // 验证文件类型 if (!this.validateFileType(file)) { @@ -119,10 +124,13 @@ class FileUploadHandler { return; } + // 使用传入的 providerType 或回退到 currentProvider + const provider = providerType ? this.getProviderKey(providerType) : this.currentProvider; + // 创建 FormData const formData = new FormData(); formData.append('file', file); - formData.append('provider', this.currentProvider); + formData.append('provider', provider); formData.append('targetInputId', targetInputId); // 使用封装接口发送上传请求 diff --git a/static/app/modal.js b/static/app/modal.js index 5605595..a5910bb 100644 --- a/static/app/modal.js +++ b/static/app/modal.js @@ -107,8 +107,9 @@ function addModalEventListeners(modal) { event.preventDefault(); event.stopPropagation(); const targetInputId = button.getAttribute('data-target'); + const providerType = modal.getAttribute('data-provider-type'); if (targetInputId && window.fileUploadHandler) { - window.fileUploadHandler.handleFileUpload(button, targetInputId); + window.fileUploadHandler.handleFileUpload(button, targetInputId, providerType); } } }; @@ -564,6 +565,7 @@ async function saveProvider(uuid, event) { try { await window.apiClient.put(`/providers/${encodeURIComponent(providerType)}/${uuid}`, { providerConfig }); + await window.apiClient.post('/reload-config'); showToast('提供商配置更新成功', 'success'); // 重新获取该提供商类型的最新配置 await refreshProviderConfig(providerType); @@ -590,6 +592,7 @@ async function deleteProvider(uuid, event) { try { await window.apiClient.delete(`/providers/${encodeURIComponent(providerType)}/${uuid}`); + await window.apiClient.post('/reload-config'); showToast('提供商配置删除成功', 'success'); // 重新获取最新配置 await refreshProviderConfig(providerType); @@ -870,6 +873,7 @@ async function addProvider(providerType) { providerType, providerConfig }); + await window.apiClient.post('/reload-config'); showToast('提供商配置添加成功', 'success'); // 移除添加表单 const form = document.querySelector('.add-provider-form'); @@ -909,6 +913,7 @@ async function toggleProviderStatus(uuid, event) { try { await window.apiClient.post(`/providers/${encodeURIComponent(providerType)}/${uuid}/${action}`, { action }); + await window.apiClient.post('/reload-config'); showToast(`提供商${isCurrentlyDisabled ? '启用' : '禁用'}成功`, 'success'); // 重新获取该提供商类型的最新配置 await refreshProviderConfig(providerType); diff --git a/static/index.html b/static/index.html index 65474ab..2317f2d 100644 --- a/static/index.html +++ b/static/index.html @@ -607,6 +607,12 @@ 配置了提供商池后,可在提供商池管理中查看详细信息 +
+ + + 提供商连续错误达到此次数后将被标记为不健康,默认为 3 次 +
+