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:
parent
1dde8b5a74
commit
7746e94154
9 changed files with 84 additions and 35 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
// 检查当前是否在提供商池管理页面,如果是则刷新数据
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
// 使用封装接口发送上传请求
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue