From 948283187cbcf63f73c65b8649865bebf62f8e98 Mon Sep 17 00:00:00 2001 From: leonai <731962175@qq.com> Date: Tue, 13 Jan 2026 18:41:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(kiro):=20=E6=B7=BB=E5=8A=A0=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E9=94=99=E8=AF=AF=E8=87=AA=E5=8A=A8=E6=A0=87=E8=AE=B0?= =?UTF-8?q?=E4=B8=8D=E5=81=A5=E5=BA=B7=E5=8F=8A=E5=87=AD=E8=AF=81=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E9=87=8D=E8=AF=95=E6=9C=BA=E5=88=B6=EF=BC=8C=E8=A7=A3?= =?UTF-8?q?=E5=86=B3401=20=E5=92=8C403=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Kiro API 新增 401/403 错误处理逻辑,401 尝试刷新 token 后重试,403 直接标记凭证不健康 2. 新增 _markCredentialUnhealthy 辅助方法,统一处理凭证健康状态标记 3. ProviderPoolManager 新增 markProviderUnhealthyImmediately 方法,用于认证错误立即标记 4. handleStreamRequest/handleUnaryRequest 支持重试上下文,认证失败后自动切换健康凭证重试 5. provider-api 健康检查增加认证错误识别,触发立即标记不健康逻辑 --- src/providers/claude/claude-kiro.js | 121 +++++++++++++++---- src/providers/provider-pool-manager.js | 29 +++++ src/ui-modules/provider-api.js | 32 ++++- src/utils/common.js | 160 +++++++++++++++++++++++-- 4 files changed, 304 insertions(+), 38 deletions(-) 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/provider-api.js b/src/ui-modules/provider-api.js index 1da20b4..66394e5 100644 --- a/src/ui-modules/provider-api.js +++ b/src/ui-modules/provider-api.js @@ -521,7 +521,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; @@ -530,15 +541,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..aaa67fb 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,13 @@ 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 以便在认证错误时切换凭证重试 + const retryContext = providerPoolManager ? { CONFIG, currentRetry: 0, maxRetries: 2 } : 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); } // 执行插件钩子:内容生成后