feat(config): 添加提供商最大错误次数配置和动态重载功能

新增MAX_ERROR_COUNT配置项用于控制提供商连续错误次数阈值,
当达到此阈值时提供商将被标记为不健康。同时实现配置动态重载功能,
支持在不重启服务的情况下更新配置,包括提供商池管理和UI配置界面的实时同步。

- 在config.json.example中添加MAX_ERROR_COUNT默认值
- 在config-manager.js中实现命令行参数--max-error-count解析
- 在service-manager.js中将maxErrorCount传递给ProviderPoolManager
- 在ui-manager.js中实现reloadConfig函数和配置重载API
- 更新前端配置管理界面,添加maxErrorCount输入控件
- 改进文件上传处理,支持模态框提供商类型识别
- 在所有提供商操作后自动触发配置重载
This commit is contained in:
hex2077 2025-11-16 18:18:43 +08:00
parent 1dde8b5a74
commit 7746e94154
9 changed files with 84 additions and 35 deletions

View file

@ -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
}

View file

@ -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.`);
}
}
}

View file

@ -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}`);

View file

@ -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 {

View file

@ -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<boolean>} - True if the request was handled by UI API
*/
/**
* 重载配置文件
* 动态导入config-manager并重新初始化配置
* @returns {Promise<Object>} 返回重载后的配置对象
*/
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 = [];

View file

@ -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');
// 检查当前是否在提供商池管理页面,如果是则刷新数据

View file

@ -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);
// 使用封装接口发送上传请求

View file

@ -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);

View file

@ -607,6 +607,12 @@
<small class="form-text">配置了提供商池后,可在提供商池管理中查看详细信息</small>
</div>
<div class="form-group pool-section">
<label for="maxErrorCount">提供商最大错误次数</label>
<input type="number" id="maxErrorCount" class="form-control" value="3" min="1" max="10" placeholder="默认: 3">
<small class="form-text">提供商连续错误达到此次数后将被标记为不健康,默认为 3 次</small>
</div>
<!-- 系统提示配置移到最下面 -->
<div class="form-group system-prompt-section">
<label for="systemPrompt">系统提示</label>