Merge pull request #252 from leonaii/main
feat(pool): 添加账号池轮询上限配置默认全部,并优化服务端重载问题
This commit is contained in:
commit
2b09653d12
6 changed files with 41 additions and 2 deletions
|
|
@ -29,7 +29,11 @@ export class ProviderPoolManager {
|
||||||
// 使用 ?? 运算符确保 0 也能被正确设置,而不是被 || 替换为默认值
|
// 使用 ?? 运算符确保 0 也能被正确设置,而不是被 || 替换为默认值
|
||||||
this.maxErrorCount = options.maxErrorCount ?? 3; // Default to 3 errors before marking unhealthy
|
this.maxErrorCount = options.maxErrorCount ?? 3; // Default to 3 errors before marking unhealthy
|
||||||
this.healthCheckInterval = options.healthCheckInterval ?? 10 * 60 * 1000; // Default to 10 minutes
|
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'
|
this.logLevel = options.logLevel || 'info'; // 'debug', 'info', 'warn', 'error'
|
||||||
|
|
||||||
|
|
@ -185,9 +189,20 @@ export class ProviderPoolManager {
|
||||||
return null;
|
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)代替取模轮询
|
// 改进:使用"最久未被使用"策略(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 timeA = a.config.lastUsed ? new Date(a.config.lastUsed).getTime() : 0;
|
||||||
const timeB = b.config.lastUsed ? new Date(b.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.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.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.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.providerFallbackChain !== undefined) currentConfig.providerFallbackChain = newConfig.providerFallbackChain;
|
||||||
if (newConfig.modelFallbackMapping !== undefined) currentConfig.modelFallbackMapping = newConfig.modelFallbackMapping;
|
if (newConfig.modelFallbackMapping !== undefined) currentConfig.modelFallbackMapping = newConfig.modelFallbackMapping;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,12 @@ export class CredentialCacheManager {
|
||||||
const existingPid = await fs.readFile(pidPath, 'utf8');
|
const existingPid = await fs.readFile(pidPath, 'utf8');
|
||||||
const pid = parseInt(existingPid.trim(), 10);
|
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 {
|
try {
|
||||||
process.kill(pid, 0); // 0 信号仅检查进程存在性
|
process.kill(pid, 0); // 0 信号仅检查进程存在性
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,10 @@ async function loadConfiguration() {
|
||||||
if (cronRefreshTokenEl) cronRefreshTokenEl.checked = data.CRON_REFRESH_TOKEN || false;
|
if (cronRefreshTokenEl) cronRefreshTokenEl.checked = data.CRON_REFRESH_TOKEN || false;
|
||||||
if (providerPoolsFilePathEl) providerPoolsFilePathEl.value = data.PROVIDER_POOLS_FILE_PATH;
|
if (providerPoolsFilePathEl) providerPoolsFilePathEl.value = data.PROVIDER_POOLS_FILE_PATH;
|
||||||
if (maxErrorCountEl) maxErrorCountEl.value = data.MAX_ERROR_COUNT || 3;
|
if (maxErrorCountEl) maxErrorCountEl.value = data.MAX_ERROR_COUNT || 3;
|
||||||
|
|
||||||
|
// 账号池轮询上限
|
||||||
|
const poolSizeLimitEl = document.getElementById('poolSizeLimit');
|
||||||
|
if (poolSizeLimitEl) poolSizeLimitEl.value = data.POOL_SIZE_LIMIT || 0;
|
||||||
|
|
||||||
// 加载 Fallback 链配置
|
// 加载 Fallback 链配置
|
||||||
if (providerFallbackChainEl) {
|
if (providerFallbackChainEl) {
|
||||||
|
|
@ -197,6 +201,7 @@ async function saveConfiguration() {
|
||||||
config.CRON_REFRESH_TOKEN = document.getElementById('cronRefreshToken')?.checked || false;
|
config.CRON_REFRESH_TOKEN = document.getElementById('cronRefreshToken')?.checked || false;
|
||||||
config.PROVIDER_POOLS_FILE_PATH = document.getElementById('providerPoolsFilePath')?.value || '';
|
config.PROVIDER_POOLS_FILE_PATH = document.getElementById('providerPoolsFilePath')?.value || '';
|
||||||
config.MAX_ERROR_COUNT = parseInt(document.getElementById('maxErrorCount')?.value || 3);
|
config.MAX_ERROR_COUNT = parseInt(document.getElementById('maxErrorCount')?.value || 3);
|
||||||
|
config.POOL_SIZE_LIMIT = parseInt(document.getElementById('poolSizeLimit')?.value || 0);
|
||||||
|
|
||||||
// 保存 Fallback 链配置
|
// 保存 Fallback 链配置
|
||||||
const fallbackChainValue = document.getElementById('providerFallbackChain')?.value?.trim() || '';
|
const fallbackChainValue = document.getElementById('providerFallbackChain')?.value?.trim() || '';
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,9 @@ const translations = {
|
||||||
'config.advanced.maxErrorCount': '提供商最大错误次数',
|
'config.advanced.maxErrorCount': '提供商最大错误次数',
|
||||||
'config.advanced.maxErrorCountPlaceholder': '默认: 3',
|
'config.advanced.maxErrorCountPlaceholder': '默认: 3',
|
||||||
'config.advanced.maxErrorCountNote': '提供商连续错误达到此次数后将被标记为不健康,默认为 3 次',
|
'config.advanced.maxErrorCountNote': '提供商连续错误达到此次数后将被标记为不健康,默认为 3 次',
|
||||||
|
'config.advanced.poolSizeLimit': '账号池轮询上限',
|
||||||
|
'config.advanced.poolSizeLimitPlaceholder': '默认: 0 (不限制)',
|
||||||
|
'config.advanced.poolSizeLimitNote': '每个提供商类型参与轮询的最大健康凭证数量,0 表示不限制,使用所有健康凭证',
|
||||||
'config.advanced.credentialSwitchMaxRetries': '坏凭证切换最大重试次数',
|
'config.advanced.credentialSwitchMaxRetries': '坏凭证切换最大重试次数',
|
||||||
'config.advanced.credentialSwitchMaxRetriesNote': '认证错误(401/403)后切换凭证的最大重试次数,默认 5 次',
|
'config.advanced.credentialSwitchMaxRetriesNote': '认证错误(401/403)后切换凭证的最大重试次数,默认 5 次',
|
||||||
'config.advanced.fallbackChain': '跨类型 Fallback 链配置',
|
'config.advanced.fallbackChain': '跨类型 Fallback 链配置',
|
||||||
|
|
@ -985,6 +988,9 @@ const translations = {
|
||||||
'config.advanced.maxErrorCount': 'Provider Max Error Count',
|
'config.advanced.maxErrorCount': 'Provider Max Error Count',
|
||||||
'config.advanced.maxErrorCountPlaceholder': 'Default: 3',
|
'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.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.credentialSwitchMaxRetries': 'Credential Switch Max Retries',
|
||||||
'config.advanced.credentialSwitchMaxRetriesNote': 'Maximum retries for switching credentials after authentication errors (401/403), default is 5',
|
'config.advanced.credentialSwitchMaxRetriesNote': 'Maximum retries for switching credentials after authentication errors (401/403), default is 5',
|
||||||
'config.advanced.fallbackChain': 'Cross-Type Fallback Chain Config',
|
'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">
|
<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>
|
<small class="form-text" data-i18n="config.advanced.maxErrorCountNote">提供商连续错误达到此次数后将被标记为不健康,默认为 3 次</small>
|
||||||
</div>
|
</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">
|
<div class="form-group pool-section">
|
||||||
<label for="providerFallbackChain" data-i18n="config.advanced.fallbackChain">跨类型 Fallback 链配置</label>
|
<label for="providerFallbackChain" data-i18n="config.advanced.fallbackChain">跨类型 Fallback 链配置</label>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue