diff --git a/VERSION b/VERSION index 338a5b5..e261122 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.6.6 +2.6.7 diff --git a/src/core/config-manager.js b/src/core/config-manager.js index 0b6d460..cc68110 100644 --- a/src/core/config-manager.js +++ b/src/core/config-manager.js @@ -75,6 +75,7 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP PROMPT_LOG_MODE: "none", REQUEST_MAX_RETRIES: 3, REQUEST_BASE_DELAY: 1000, + CREDENTIAL_SWITCH_MAX_RETRIES: 5, // 坏凭证切换最大重试次数(用于认证错误后切换凭证) CRON_NEAR_MINUTES: 15, CRON_REFRESH_TOKEN: false, PROVIDER_POOLS_FILE_PATH: null, // 新增号池配置文件路径 diff --git a/src/providers/claude/claude-kiro.js b/src/providers/claude/claude-kiro.js index 2a06752..0ffef43 100644 --- a/src/providers/claude/claude-kiro.js +++ b/src/providers/claude/claude-kiro.js @@ -9,7 +9,8 @@ import * as https from 'https'; import { getProviderModels } from '../provider-models.js'; import { countTokens } from '@anthropic-ai/tokenizer'; import { configureAxiosProxy } from '../../utils/proxy-utils.js'; -import { isRetryableNetworkError } from '../../utils/common.js'; +import { isRetryableNetworkError, MODEL_PROVIDER } from '../../utils/common.js'; +import { getProviderPoolManager } from '../../services/service-manager.js'; const KIRO_THINKING = { MAX_BUDGET_TOKENS: 24576, @@ -1212,16 +1213,27 @@ async initializeAuth(forceRefresh = false) { // 检查是否为可重试的网络错误 const isNetworkError = isRetryableNetworkError(error); - if (status === 403 && !isRetry) { - console.log('[Kiro] Received 403. Attempting token refresh and retrying...'); + // Handle 401 (Unauthorized) - try to refresh token first + if (status === 401 && !isRetry) { + console.log('[Kiro] Received 401. Attempting token refresh...'); try { await this.initializeAuth(true); // Force refresh token + console.log('[Kiro] Token refresh successful after 401, retrying request...'); return this.callApi(method, model, body, true, retryCount); } catch (refreshError) { - console.error('[Kiro] Token refresh failed during 403 retry:', refreshError.message); + console.error('[Kiro] Token refresh failed during 401 retry:', refreshError.message); + // Mark credential as unhealthy immediately and attach marker to error + this._markCredentialUnhealthy('401 Unauthorized - Token refresh failed', refreshError); throw refreshError; } } + + // Handle 403 (Forbidden) - mark as unhealthy immediately, no retry + if (status === 403) { + console.log('[Kiro] Received 403. Marking credential as unhealthy...'); + this._markCredentialUnhealthy('403 Forbidden', error); + throw error; + } // Handle 429 (Too Many Requests) with exponential backoff if (status === 429 && retryCount < maxRetries) { @@ -1253,6 +1265,31 @@ async initializeAuth(forceRefresh = false) { } } + /** + * Helper method to mark the current credential as unhealthy + * @param {string} reason - The reason for marking unhealthy + * @param {Error} [error] - Optional error object to attach the marker to + * @returns {boolean} - Whether the credential was successfully marked as unhealthy + * @private + */ + _markCredentialUnhealthy(reason, error = null) { + const poolManager = getProviderPoolManager(); + if (poolManager && this.uuid) { + console.log(`[Kiro] Marking credential ${this.uuid} as unhealthy. Reason: ${reason}`); + poolManager.markProviderUnhealthyImmediately(MODEL_PROVIDER.KIRO_API, { + uuid: this.uuid + }, reason); + // Attach marker to error object to prevent duplicate marking in upper layers + if (error) { + error.credentialMarkedUnhealthy = true; + } + return true; + } else { + console.warn(`[Kiro] Cannot mark credential as unhealthy: poolManager=${!!poolManager}, uuid=${this.uuid}`); + return false; + } + } + _processApiResponse(response) { const rawResponseText = Buffer.isBuffer(response.data) ? response.data.toString('utf8') : String(response.data); //console.log(`[Kiro] Raw response length: ${rawResponseText.length}`); @@ -1537,11 +1574,27 @@ async initializeAuth(forceRefresh = false) { // 检查是否为可重试的网络错误 const isNetworkError = isRetryableNetworkError(error); - if (status === 403 && !isRetry) { - console.log('[Kiro] Received 403 in stream. Attempting token refresh and retrying...'); - await this.initializeAuth(true); - yield* this.streamApiReal(method, model, body, true, retryCount); - return; + // Handle 401 (Unauthorized) - try to refresh token first + if (status === 401 && !isRetry) { + console.log('[Kiro] Received 401 in stream. Attempting token refresh...'); + try { + await this.initializeAuth(true); // Force refresh token + console.log('[Kiro] Token refresh successful after 401, retrying stream...'); + yield* this.streamApiReal(method, model, body, true, retryCount); + return; + } catch (refreshError) { + console.error('[Kiro] Token refresh failed during 401 retry:', refreshError.message); + // Mark credential as unhealthy immediately and attach marker to error + this._markCredentialUnhealthy('401 Unauthorized - Token refresh failed', refreshError); + throw refreshError; + } + } + + // Handle 403 (Forbidden) - mark as unhealthy immediately, no retry + if (status === 403) { + console.log('[Kiro] Received 403 in stream. Marking credential as unhealthy...'); + this._markCredentialUnhealthy('403 Forbidden', error); + throw error; } if (status === 429 && retryCount < maxRetries) { @@ -2355,24 +2408,42 @@ async initializeAuth(forceRefresh = false) { console.log('[Kiro] Usage limits fetched successfully'); return response.data; } catch (error) { - // 如果是 403 错误,尝试刷新 token 后重试 - if (error.response?.status === 403) { - console.log('[Kiro] Received 403 on getUsageLimits. Attempting token refresh and retrying...'); - try { - await this.initializeAuth(true); - // 更新 Authorization header - headers['Authorization'] = `Bearer ${this.accessToken}`; - headers['amz-sdk-invocation-id'] = uuidv4(); - const retryResponse = await this.axiosInstance.get(fullUrl, { headers }); - console.log('[Kiro] Usage limits fetched successfully after token refresh'); - return retryResponse.data; - } catch (refreshError) { - console.error('[Kiro] Token refresh failed during getUsageLimits retry:', refreshError.message); - throw refreshError; + const status = error.response?.status; + + // 从响应体中提取错误信息 + let errorMessage = error.message; + if (error.response?.data) { + // 尝试从响应体中获取错误描述 + const responseData = error.response.data; + if (typeof responseData === 'string') { + errorMessage = responseData; + } else if (responseData.message) { + errorMessage = responseData.message; + } else if (responseData.error) { + errorMessage = typeof responseData.error === 'string' ? responseData.error : responseData.error.message || JSON.stringify(responseData.error); } } - console.error('[Kiro] Failed to fetch usage limits:', error.message, error); - throw error; + + // 构建包含状态码和错误描述的错误信息 + const formattedError = status + ? new Error(`API call failed: ${status} - ${errorMessage}`) + : new Error(`API call failed: ${errorMessage}`); + + // 对于用量查询,401/403 错误直接标记凭证为不健康,不重试 + if (status === 401) { + console.log('[Kiro] Received 401 on getUsageLimits. Marking credential as unhealthy (no retry)...'); + this._markCredentialUnhealthy('401 Unauthorized on usage query', formattedError); + throw formattedError; + } + + if (status === 403) { + console.log('[Kiro] Received 403 on getUsageLimits. Marking credential as unhealthy (no retry)...'); + this._markCredentialUnhealthy('403 Forbidden on usage query', formattedError); + throw formattedError; + } + + console.error('[Kiro] Failed to fetch usage limits:', formattedError.message, error); + throw formattedError; } } } diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js index cc618fc..70d1f10 100644 --- a/src/providers/provider-pool-manager.js +++ b/src/providers/provider-pool-manager.js @@ -429,6 +429,35 @@ export class ProviderPoolManager { } } + /** + * Marks a provider as unhealthy immediately (without accumulating error count). + * Used for definitive authentication errors like 401/403. + * @param {string} providerType - The type of the provider. + * @param {object} providerConfig - The configuration of the provider to mark. + * @param {string} [errorMessage] - Optional error message to store. + */ + markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage = null) { + if (!providerConfig?.uuid) { + this._log('error', 'Invalid providerConfig in markProviderUnhealthyImmediately'); + return; + } + + const provider = this._findProvider(providerType, providerConfig.uuid); + if (provider) { + provider.config.isHealthy = false; + provider.config.errorCount = this.maxErrorCount; // Set to max to indicate definitive failure + provider.config.lastErrorTime = new Date().toISOString(); + provider.config.lastUsed = new Date().toISOString(); + + if (errorMessage) { + provider.config.lastErrorMessage = errorMessage; + } + + this._log('warn', `Immediately marked provider as unhealthy: ${providerConfig.uuid} for type ${providerType}. Reason: ${errorMessage || 'Authentication error'}`); + this._debouncedSave(providerType); + } + } + /** * Marks a provider as healthy. * @param {string} providerType - The type of the provider. diff --git a/src/ui-modules/config-api.js b/src/ui-modules/config-api.js index 4d36c23..1ddfdfd 100644 --- a/src/ui-modules/config-api.js +++ b/src/ui-modules/config-api.js @@ -83,6 +83,7 @@ export async function handleUpdateConfig(req, res, currentConfig) { if (newConfig.PROMPT_LOG_MODE !== undefined) currentConfig.PROMPT_LOG_MODE = newConfig.PROMPT_LOG_MODE; if (newConfig.REQUEST_MAX_RETRIES !== undefined) currentConfig.REQUEST_MAX_RETRIES = newConfig.REQUEST_MAX_RETRIES; if (newConfig.REQUEST_BASE_DELAY !== undefined) currentConfig.REQUEST_BASE_DELAY = newConfig.REQUEST_BASE_DELAY; + if (newConfig.CREDENTIAL_SWITCH_MAX_RETRIES !== undefined) currentConfig.CREDENTIAL_SWITCH_MAX_RETRIES = newConfig.CREDENTIAL_SWITCH_MAX_RETRIES; 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; @@ -131,6 +132,7 @@ export async function handleUpdateConfig(req, res, currentConfig) { PROMPT_LOG_MODE: currentConfig.PROMPT_LOG_MODE, REQUEST_MAX_RETRIES: currentConfig.REQUEST_MAX_RETRIES, REQUEST_BASE_DELAY: currentConfig.REQUEST_BASE_DELAY, + CREDENTIAL_SWITCH_MAX_RETRIES: currentConfig.CREDENTIAL_SWITCH_MAX_RETRIES, CRON_NEAR_MINUTES: currentConfig.CRON_NEAR_MINUTES, CRON_REFRESH_TOKEN: currentConfig.CRON_REFRESH_TOKEN, PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH, diff --git a/src/ui-modules/provider-api.js b/src/ui-modules/provider-api.js index 4eef5ad..965bd9b 100644 --- a/src/ui-modules/provider-api.js +++ b/src/ui-modules/provider-api.js @@ -715,7 +715,18 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan message: 'Healthy' }); } else { - providerPoolManager.markProviderUnhealthy(providerType, providerConfig, healthResult.errorMessage); + // 检查是否为认证错误(401/403),如果是则立即标记为不健康 + const errorMessage = healthResult.errorMessage || 'Check failed'; + const isAuthError = /\b(401|403)\b/.test(errorMessage) || + /\b(Unauthorized|Forbidden|AccessDenied|InvalidToken|ExpiredToken)\b/i.test(errorMessage); + + if (isAuthError) { + providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage); + console.log(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`); + } else { + providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage); + } + providerStatus.config.lastHealthCheckTime = new Date().toISOString(); if (healthResult.modelName) { providerStatus.config.lastHealthCheckModel = healthResult.modelName; @@ -724,15 +735,28 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan uuid: providerConfig.uuid, success: false, modelName: healthResult.modelName, - message: healthResult.errorMessage || 'Check failed' + message: errorMessage, + isAuthError: isAuthError }); } } catch (error) { - providerPoolManager.markProviderUnhealthy(providerType, providerConfig, error.message); + const errorMessage = error.message || 'Unknown error'; + // 检查是否为认证错误(401/403),如果是则立即标记为不健康 + const isAuthError = /\b(401|403)\b/.test(errorMessage) || + /\b(Unauthorized|Forbidden|AccessDenied|InvalidToken|ExpiredToken)\b/i.test(errorMessage); + + if (isAuthError) { + providerPoolManager.markProviderUnhealthyImmediately(providerType, providerConfig, errorMessage); + console.log(`[UI API] Auth error detected for ${providerConfig.uuid}, immediately marked as unhealthy`); + } else { + providerPoolManager.markProviderUnhealthy(providerType, providerConfig, errorMessage); + } + results.push({ uuid: providerConfig.uuid, success: false, - message: error.message + message: errorMessage, + isAuthError: isAuthError }); } } diff --git a/src/utils/common.js b/src/utils/common.js index 3e526d3..d3acc92 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -217,13 +217,22 @@ export async function handleUnifiedResponse(res, responsePayload, isStream) { } } -export async function handleStreamRequest(res, service, model, requestBody, fromProvider, toProvider, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid, customName) { +export async function handleStreamRequest(res, service, model, requestBody, fromProvider, toProvider, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid, customName, retryContext = null) { let fullResponseText = ''; let fullResponseJson = ''; let fullOldResponseJson = ''; let responseClosed = false; + + // 重试上下文:包含 CONFIG 和重试计数 + const maxRetries = retryContext?.maxRetries ?? 2; + const currentRetry = retryContext?.currentRetry ?? 0; + const CONFIG = retryContext?.CONFIG; + const isRetry = currentRetry > 0; - await handleUnifiedResponse(res, '', true); + // 只在首次请求时发送响应头,重试时跳过(响应头已发送) + if (!isRetry) { + await handleUnifiedResponse(res, '', true); + } // fs.writeFile('request'+Date.now()+'.json', JSON.stringify(requestBody)); // The service returns a stream in its native format (toProvider). @@ -283,12 +292,80 @@ export async function handleStreamRequest(res, service, model, requestBody, from } catch (error) { console.error('\n[Server] Error during stream processing:', error.stack); - if (providerPoolManager && pooluuid) { - console.log(`[Provider Pool] Marking ${toProvider} as unhealthy due to stream error`); + + // 如果已经发送了内容,不进行重试(避免响应数据损坏) + if (fullResponseText.length > 0) { + console.log(`[Stream Retry] Cannot retry: ${fullResponseText.length} bytes already sent to client`); + // 直接发送错误并结束 + const errorPayload = createStreamErrorResponse(error, fromProvider); + res.write(errorPayload); + res.end(); + responseClosed = true; + return; + } + + // 获取状态码(用于日志记录,不再用于判断是否重试) + const status = error.response?.status; + + // 检查凭证是否已在底层被标记为不健康(避免重复标记) + let credentialMarkedUnhealthy = error.credentialMarkedUnhealthy === true; + + // 如果底层未标记,则在此处标记 + if (!credentialMarkedUnhealthy && providerPoolManager && pooluuid) { + console.log(`[Provider Pool] Marking ${toProvider} as unhealthy due to stream error (status: ${status || 'unknown'})`); // 如果是号池模式,并且请求处理失败,则标记当前使用的提供者为不健康 providerPoolManager.markProviderUnhealthy(toProvider, { uuid: pooluuid }); + credentialMarkedUnhealthy = true; + } else if (credentialMarkedUnhealthy) { + console.log(`[Provider Pool] Credential ${pooluuid} already marked as unhealthy by lower layer, skipping duplicate marking`); + } + + // 凭证已被标记为不健康后,尝试切换到新凭证重试 + // 不再依赖状态码判断,只要凭证被标记不健康且可以重试,就尝试切换 + if (credentialMarkedUnhealthy && currentRetry < maxRetries && providerPoolManager && CONFIG) { + console.log(`[Stream Retry] Credential marked unhealthy. Attempting retry ${currentRetry + 1}/${maxRetries} with different credential...`); + + try { + // 动态导入以避免循环依赖 + const { getApiServiceWithFallback } = await import('../services/service-manager.js'); + const result = await getApiServiceWithFallback(CONFIG, model); + + if (result && result.service && result.uuid !== pooluuid) { + console.log(`[Stream Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`); + + // 使用新服务重试 + const newRetryContext = { + ...retryContext, + CONFIG, + currentRetry: currentRetry + 1, + maxRetries + }; + + // 递归调用,使用新的服务 + return await handleStreamRequest( + res, + result.service, + result.actualModel || model, + requestBody, + fromProvider, + result.actualProviderType || toProvider, + PROMPT_LOG_MODE, + PROMPT_LOG_FILENAME, + providerPoolManager, + result.uuid, + result.serviceConfig?.customName || customName, + newRetryContext + ); + } else if (result && result.uuid === pooluuid) { + console.log(`[Stream Retry] No different healthy credential available. Same credential selected.`); + } else { + console.log(`[Stream Retry] No healthy credential available for retry.`); + } + } catch (retryError) { + console.error(`[Stream Retry] Failed to get alternative service:`, retryError.message); + } } // 使用新方法创建符合 fromProvider 格式的流式错误响应 @@ -307,7 +384,12 @@ export async function handleStreamRequest(res, service, model, requestBody, from } -export async function handleUnaryRequest(res, service, model, requestBody, fromProvider, toProvider, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid, customName) { +export async function handleUnaryRequest(res, service, model, requestBody, fromProvider, toProvider, PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid, customName, retryContext = null) { + // 重试上下文:包含 CONFIG 和重试计数 + const maxRetries = retryContext?.maxRetries ?? 2; + const currentRetry = retryContext?.currentRetry ?? 0; + const CONFIG = retryContext?.CONFIG; + try{ // The service returns the response in its native format (toProvider). const needsConversion = getProtocolPrefix(fromProvider) !== getProtocolPrefix(toProvider); @@ -338,12 +420,69 @@ export async function handleUnaryRequest(res, service, model, requestBody, fromP } } catch (error) { console.error('\n[Server] Error during unary processing:', error.stack); - if (providerPoolManager && pooluuid) { - console.log(`[Provider Pool] Marking ${toProvider} as unhealthy due to stream error`); + + // 获取状态码(用于日志记录,不再用于判断是否重试) + const status = error.response?.status; + + // 检查凭证是否已在底层被标记为不健康(避免重复标记) + let credentialMarkedUnhealthy = error.credentialMarkedUnhealthy === true; + + // 如果底层未标记,则在此处标记 + if (!credentialMarkedUnhealthy && providerPoolManager && pooluuid) { + console.log(`[Provider Pool] Marking ${toProvider} as unhealthy due to unary error (status: ${status || 'unknown'})`); // 如果是号池模式,并且请求处理失败,则标记当前使用的提供者为不健康 providerPoolManager.markProviderUnhealthy(toProvider, { uuid: pooluuid }); + credentialMarkedUnhealthy = true; + } else if (credentialMarkedUnhealthy) { + console.log(`[Provider Pool] Credential ${pooluuid} already marked as unhealthy by lower layer, skipping duplicate marking`); + } + + // 凭证已被标记为不健康后,尝试切换到新凭证重试 + // 不再依赖状态码判断,只要凭证被标记不健康且可以重试,就尝试切换 + if (credentialMarkedUnhealthy && currentRetry < maxRetries && providerPoolManager && CONFIG) { + console.log(`[Unary Retry] Credential marked unhealthy. Attempting retry ${currentRetry + 1}/${maxRetries} with different credential...`); + + try { + // 动态导入以避免循环依赖 + const { getApiServiceWithFallback } = await import('../services/service-manager.js'); + const result = await getApiServiceWithFallback(CONFIG, model); + + if (result && result.service && result.uuid !== pooluuid) { + console.log(`[Unary Retry] Switched to new credential: ${result.uuid} (provider: ${result.actualProviderType})`); + + // 使用新服务重试 + const newRetryContext = { + ...retryContext, + CONFIG, + currentRetry: currentRetry + 1, + maxRetries + }; + + // 递归调用,使用新的服务 + return await handleUnaryRequest( + res, + result.service, + result.actualModel || model, + requestBody, + fromProvider, + result.actualProviderType || toProvider, + PROMPT_LOG_MODE, + PROMPT_LOG_FILENAME, + providerPoolManager, + result.uuid, + result.serviceConfig?.customName || customName, + newRetryContext + ); + } else if (result && result.uuid === pooluuid) { + console.log(`[Unary Retry] No different healthy credential available. Same credential selected.`); + } else { + console.log(`[Unary Retry] No healthy credential available for retry.`); + } + } catch (retryError) { + console.error(`[Unary Retry] Failed to get alternative service:`, retryError.message); + } } // 使用新方法创建符合 fromProvider 格式的错误响应 @@ -487,10 +626,19 @@ export async function handleContentGenerationRequest(req, res, service, endpoint await logConversation('input', promptText, CONFIG.PROMPT_LOG_MODE, PROMPT_LOG_FILENAME); // 5. Call the appropriate stream or unary handler, passing the provider info. + // 创建重试上下文,包含 CONFIG 以便在认证错误时切换凭证重试 + // 凭证切换重试次数(默认 5),可在配置中自定义更大的值 + // 注意:这与底层的 429/5xx 重试(REQUEST_MAX_RETRIES)是不同层次的重试机制 + // - 底层重试:同一凭证遇到 429/5xx 时的重试 + // - 凭证切换重试:凭证被标记不健康后切换到其他凭证 + // 当没有不同的健康凭证可用时,重试会自动停止 + const credentialSwitchMaxRetries = CONFIG.CREDENTIAL_SWITCH_MAX_RETRIES || 5; + const retryContext = providerPoolManager ? { CONFIG, currentRetry: 0, maxRetries: credentialSwitchMaxRetries } : null; + if (isStream) { - await handleStreamRequest(res, service, model, processedRequestBody, fromProvider, toProvider, CONFIG.PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, actualUuid, actualCustomName); + await handleStreamRequest(res, service, model, processedRequestBody, fromProvider, toProvider, CONFIG.PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, actualUuid, actualCustomName, retryContext); } else { - await handleUnaryRequest(res, service, model, processedRequestBody, fromProvider, toProvider, CONFIG.PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, actualUuid, actualCustomName); + await handleUnaryRequest(res, service, model, processedRequestBody, fromProvider, toProvider, CONFIG.PROMPT_LOG_MODE, PROMPT_LOG_FILENAME, providerPoolManager, actualUuid, actualCustomName, retryContext); } // 执行插件钩子:内容生成后 diff --git a/static/app/config-manager.js b/static/app/config-manager.js index 4371c7f..a669f85 100644 --- a/static/app/config-manager.js +++ b/static/app/config-manager.js @@ -90,6 +90,11 @@ async function loadConfiguration() { if (promptLogModeEl) promptLogModeEl.value = data.PROMPT_LOG_MODE || 'none'; if (requestMaxRetriesEl) requestMaxRetriesEl.value = data.REQUEST_MAX_RETRIES || 3; if (requestBaseDelayEl) requestBaseDelayEl.value = data.REQUEST_BASE_DELAY || 1000; + + // 坏凭证切换最大重试次数 + const credentialSwitchMaxRetriesEl = document.getElementById('credentialSwitchMaxRetries'); + if (credentialSwitchMaxRetriesEl) credentialSwitchMaxRetriesEl.value = data.CREDENTIAL_SWITCH_MAX_RETRIES || 5; + 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; @@ -187,6 +192,7 @@ async function saveConfiguration() { config.PROMPT_LOG_MODE = document.getElementById('promptLogMode')?.value || ''; config.REQUEST_MAX_RETRIES = parseInt(document.getElementById('requestMaxRetries')?.value || 3); config.REQUEST_BASE_DELAY = parseInt(document.getElementById('requestBaseDelay')?.value || 1000); + config.CREDENTIAL_SWITCH_MAX_RETRIES = parseInt(document.getElementById('credentialSwitchMaxRetries')?.value || 5); 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 || ''; diff --git a/static/components/section-config.html b/static/components/section-config.html index 12a7361..c51e6bb 100644 --- a/static/components/section-config.html +++ b/static/components/section-config.html @@ -162,6 +162,14 @@ +