feat(pool): 添加账号池轮询上限配置默认全部,并优化服务端重载问题
1. 新增 POOL_SIZE_LIMIT 配置项,限制每个提供商类型参与轮询的最大健康凭证数量 2. 优化 provider-pool-manager 选择逻辑,按使用次数升序取 Top N 候选池 3. 修复 credential-cache-manager 配置重载时的实例锁检测问题 4. 完善前端配置界面和中英文国际化支持
This commit is contained in:
parent
9ab139b294
commit
025828868e
6 changed files with 41 additions and 2 deletions
|
|
@ -29,7 +29,11 @@ export class ProviderPoolManager {
|
|||
// 使用 ?? 运算符确保 0 也能被正确设置,而不是被 || 替换为默认值
|
||||
this.maxErrorCount = options.maxErrorCount ?? 3; // Default to 3 errors before marking unhealthy
|
||||
this.healthCheckInterval = options.healthCheckInterval ?? 10 * 60 * 1000; // Default to 10 minutes
|
||||
|
||||
|
||||
// 账号池上限配置:每个 providerType 最多使用多少个健康凭证进行轮询
|
||||
// 0 或 undefined 表示不限制,使用所有健康凭证
|
||||
this.poolSizeLimit = options.globalConfig?.POOL_SIZE_LIMIT ?? 0;
|
||||
|
||||
// 日志级别控制
|
||||
this.logLevel = options.logLevel || 'info'; // 'debug', 'info', 'warn', 'error'
|
||||
|
||||
|
|
@ -185,9 +189,20 @@ export class ProviderPoolManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
// 账号池上限:如果配置了 poolSizeLimit,只使用 Top N 个健康凭证
|
||||
// 按 lastUsed 排序后取前 N 个,确保轮询范围受限
|
||||
let candidateProviders = availableAndHealthyProviders;
|
||||
if (this.poolSizeLimit > 0 && availableAndHealthyProviders.length > this.poolSizeLimit) {
|
||||
// 先按 usageCount 升序排序,取使用次数最少的 Top N 个作为候选池
|
||||
candidateProviders = [...availableAndHealthyProviders]
|
||||
.sort((a, b) => (a.config.usageCount || 0) - (b.config.usageCount || 0))
|
||||
.slice(0, this.poolSizeLimit);
|
||||
this._log('debug', `Pool size limited to ${this.poolSizeLimit}, using top ${candidateProviders.length} providers for ${providerType}`);
|
||||
}
|
||||
|
||||
// 改进:使用"最久未被使用"策略(LRU)代替取模轮询
|
||||
// 这样即使可用列表长度动态变化,也能确保每个账号被平均轮到
|
||||
const selected = availableAndHealthyProviders.sort((a, b) => {
|
||||
const selected = candidateProviders.sort((a, b) => {
|
||||
const timeA = a.config.lastUsed ? new Date(a.config.lastUsed).getTime() : 0;
|
||||
const timeB = b.config.lastUsed ? new Date(b.config.lastUsed).getTime() : 0;
|
||||
// 优先选择从未用过的,或者最久没用的
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export async function handleUpdateConfig(req, res, currentConfig) {
|
|||
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;
|
||||
if (newConfig.POOL_SIZE_LIMIT !== undefined) currentConfig.POOL_SIZE_LIMIT = newConfig.POOL_SIZE_LIMIT;
|
||||
if (newConfig.providerFallbackChain !== undefined) currentConfig.providerFallbackChain = newConfig.providerFallbackChain;
|
||||
if (newConfig.modelFallbackMapping !== undefined) currentConfig.modelFallbackMapping = newConfig.modelFallbackMapping;
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,12 @@ export class CredentialCacheManager {
|
|||
const existingPid = await fs.readFile(pidPath, 'utf8');
|
||||
const pid = parseInt(existingPid.trim(), 10);
|
||||
|
||||
// 如果是当前进程的 PID,说明是配置重载,直接返回(已持有锁)
|
||||
if (pid === process.pid) {
|
||||
console.log(`[CredentialCache] Instance lock already held by current process (PID: ${pid})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查进程是否还在运行
|
||||
try {
|
||||
process.kill(pid, 0); // 0 信号仅检查进程存在性
|
||||
|
|
|
|||
|
|
@ -99,6 +99,10 @@ async function loadConfiguration() {
|
|||
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;
|
||||
|
||||
// 账号池轮询上限
|
||||
const poolSizeLimitEl = document.getElementById('poolSizeLimit');
|
||||
if (poolSizeLimitEl) poolSizeLimitEl.value = data.POOL_SIZE_LIMIT || 0;
|
||||
|
||||
// 加载 Fallback 链配置
|
||||
if (providerFallbackChainEl) {
|
||||
|
|
@ -197,6 +201,7 @@ async function saveConfiguration() {
|
|||
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);
|
||||
config.POOL_SIZE_LIMIT = parseInt(document.getElementById('poolSizeLimit')?.value || 0);
|
||||
|
||||
// 保存 Fallback 链配置
|
||||
const fallbackChainValue = document.getElementById('providerFallbackChain')?.value?.trim() || '';
|
||||
|
|
|
|||
|
|
@ -285,6 +285,9 @@ const translations = {
|
|||
'config.advanced.maxErrorCount': '提供商最大错误次数',
|
||||
'config.advanced.maxErrorCountPlaceholder': '默认: 3',
|
||||
'config.advanced.maxErrorCountNote': '提供商连续错误达到此次数后将被标记为不健康,默认为 3 次',
|
||||
'config.advanced.poolSizeLimit': '账号池轮询上限',
|
||||
'config.advanced.poolSizeLimitPlaceholder': '默认: 0 (不限制)',
|
||||
'config.advanced.poolSizeLimitNote': '每个提供商类型参与轮询的最大健康凭证数量,0 表示不限制,使用所有健康凭证',
|
||||
'config.advanced.credentialSwitchMaxRetries': '坏凭证切换最大重试次数',
|
||||
'config.advanced.credentialSwitchMaxRetriesNote': '认证错误(401/403)后切换凭证的最大重试次数,默认 5 次',
|
||||
'config.advanced.fallbackChain': '跨类型 Fallback 链配置',
|
||||
|
|
@ -847,6 +850,9 @@ const translations = {
|
|||
'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',
|
||||
'config.advanced.poolSizeLimit': 'Pool Size Limit',
|
||||
'config.advanced.poolSizeLimitPlaceholder': 'Default: 0 (no limit)',
|
||||
'config.advanced.poolSizeLimitNote': 'Maximum number of healthy credentials per provider type for rotation. 0 means no limit, use all healthy credentials',
|
||||
'config.advanced.credentialSwitchMaxRetries': 'Credential Switch Max Retries',
|
||||
'config.advanced.credentialSwitchMaxRetriesNote': 'Maximum retries for switching credentials after authentication errors (401/403), default is 5',
|
||||
'config.advanced.fallbackChain': 'Cross-Type Fallback Chain Config',
|
||||
|
|
|
|||
|
|
@ -195,6 +195,12 @@
|
|||
<input type="number" id="maxErrorCount" class="form-control" value="3" min="1" max="10" data-i18n-placeholder="config.advanced.maxErrorCountPlaceholder" placeholder="默认: 3">
|
||||
<small class="form-text" data-i18n="config.advanced.maxErrorCountNote">提供商连续错误达到此次数后将被标记为不健康,默认为 3 次</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group pool-section">
|
||||
<label for="poolSizeLimit" data-i18n="config.advanced.poolSizeLimit">账号池轮询上限</label>
|
||||
<input type="number" id="poolSizeLimit" class="form-control" value="0" min="0" max="100" data-i18n-placeholder="config.advanced.poolSizeLimitPlaceholder" placeholder="默认: 0 (不限制)">
|
||||
<small class="form-text" data-i18n="config.advanced.poolSizeLimitNote">每个提供商类型参与轮询的最大健康凭证数量,0 表示不限制,使用所有健康凭证</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group pool-section">
|
||||
<label for="providerFallbackChain" data-i18n="config.advanced.fallbackChain">跨类型 Fallback 链配置</label>
|
||||
|
|
|
|||
Loading…
Reference in a new issue