diff --git a/src/providers/claude/claude-kiro.js b/src/providers/claude/claude-kiro.js index e4d7e18..8b0adc0 100644 --- a/src/providers/claude/claude-kiro.js +++ b/src/providers/claude/claude-kiro.js @@ -205,6 +205,44 @@ function repairJson(jsonStr) { return repaired; } +/** + * 从损坏的 JSON 中提取关键凭证字段 + * 当标准 JSON 解析和 repairJson 都失败时使用 + * @param {string} content - 文件内容 + * @returns {Object|null} 提取的凭证对象或 null + */ +function extractCredentialsFromCorruptedJson(content) { + const extracted = {}; + + // 定义需要提取的关键字段及其正则模式 + const fieldPatterns = { + refreshToken: /"refreshToken"\s*:\s*"([^"]+)"/, + accessToken: /"accessToken"\s*:\s*"([^"]+)"/, + clientId: /"clientId"\s*:\s*"([^"]+)"/, + clientSecret: /"clientSecret"\s*:\s*"([^"]+)"/, + profileArn: /"profileArn"\s*:\s*"([^"]+)"/, + region: /"region"\s*:\s*"([^"]+)"/, + authMethod: /"authMethod"\s*:\s*"([^"]+)"/, + expiresAt: /"expiresAt"\s*:\s*"([^"]+)"/, + startUrl: /"startUrl"\s*:\s*"([^"]+)"/, + }; + + for (const [field, pattern] of Object.entries(fieldPatterns)) { + const match = content.match(pattern); + if (match && match[1]) { + extracted[field] = match[1]; + } + } + + // 至少需要 refreshToken 或 accessToken 才算有效 + if (extracted.refreshToken || extracted.accessToken) { + console.info(`[Kiro Auth] Extracted ${Object.keys(extracted).length} fields from corrupted JSON: ${Object.keys(extracted).join(', ')}`); + return extracted; + } + + return null; +} + /** * 解析单个工具调用文本 * @param {string} toolCallText - 工具调用文本 @@ -448,7 +486,14 @@ async initializeAuth(forceRefresh = false) { console.info('[Kiro Auth] JSON repair successful'); return result; } catch (repairError) { - console.error('[Kiro Auth] JSON repair failed:', repairError.message); + console.warn('[Kiro Auth] JSON repair failed, attempting field extraction...'); + // 尝试从损坏的 JSON 中提取关键字段 + const extracted = extractCredentialsFromCorruptedJson(fileContent); + if (extracted) { + console.info('[Kiro Auth] Field extraction successful, credentials recovered'); + return extracted; + } + console.error('[Kiro Auth] All recovery methods failed:', repairError.message); return null; } } @@ -489,8 +534,15 @@ async initializeAuth(forceRefresh = false) { existingData = JSON.parse(repaired); console.info('[Kiro Auth] JSON repair successful'); } catch (repairError) { - console.error('[Kiro Auth] JSON repair failed:', repairError.message); - existingData = {}; + console.warn('[Kiro Auth] JSON repair failed, attempting field extraction...'); + const extracted = extractCredentialsFromCorruptedJson(fileContent); + if (extracted) { + existingData = extracted; + console.info('[Kiro Auth] Field extraction successful'); + } else { + console.error('[Kiro Auth] All recovery methods failed:', repairError.message); + existingData = {}; + } } } } catch (readError) { diff --git a/src/ui-modules/config-api.js b/src/ui-modules/config-api.js index 0438137..dac8323 100644 --- a/src/ui-modules/config-api.js +++ b/src/ui-modules/config-api.js @@ -138,6 +138,7 @@ export async function handleUpdateConfig(req, res, currentConfig) { CRON_REFRESH_TOKEN: currentConfig.CRON_REFRESH_TOKEN, PROVIDER_POOLS_FILE_PATH: currentConfig.PROVIDER_POOLS_FILE_PATH, MAX_ERROR_COUNT: currentConfig.MAX_ERROR_COUNT, + POOL_SIZE_LIMIT: currentConfig.POOL_SIZE_LIMIT, providerFallbackChain: currentConfig.providerFallbackChain, modelFallbackMapping: currentConfig.modelFallbackMapping, PROXY_URL: currentConfig.PROXY_URL, diff --git a/src/utils/credential-cache-manager.js b/src/utils/credential-cache-manager.js index ea6f6f5..2957bce 100644 --- a/src/utils/credential-cache-manager.js +++ b/src/utils/credential-cache-manager.js @@ -159,6 +159,7 @@ export class CredentialCacheManager { let loadedCount = 0; let failedCount = 0; + let skippedCount = 0; for (const [providerType, providers] of Object.entries(providerPools)) { if (!Array.isArray(providers)) continue; @@ -172,6 +173,7 @@ export class CredentialCacheManager { for (const providerConfig of providers) { const uuid = providerConfig.uuid; const credPath = providerConfig[credPathKey]; + const customName = providerConfig.customName || uuid; if (!uuid || !credPath) { continue; @@ -180,6 +182,14 @@ export class CredentialCacheManager { try { const credentials = await this._loadCredentialsFromFile(credPath); if (credentials) { + // 预检查:验证凭证是否有效(至少有 token) + const hasValidToken = this._validateCredentials(providerType, credentials); + if (!hasValidToken) { + skippedCount++; + console.warn(`[CredentialCache] Skipping ${providerType}:${customName} - no valid token found in credentials`); + continue; + } + const cacheKey = this._getCacheKey(providerType, uuid); this.credentialCache.set(cacheKey, { providerType, @@ -192,16 +202,63 @@ export class CredentialCacheManager { retryCount: 0 }); loadedCount++; + } else { + skippedCount++; + console.warn(`[CredentialCache] Skipping ${providerType}:${customName} - credentials file empty or unreadable`); } } catch (error) { failedCount++; - console.warn(`[CredentialCache] Failed to load credentials for ${providerType}:${uuid}: ${error.message}`); + console.warn(`[CredentialCache] Failed to load credentials for ${providerType}:${customName}: ${error.message}`); } } } this.isInitialized = true; - console.log(`[CredentialCache] Preloaded ${loadedCount} credentials (${failedCount} failed)`); + console.log(`[CredentialCache] Preloaded ${loadedCount} credentials (${failedCount} failed, ${skippedCount} skipped)`); + } + + /** + * 验证凭证是否有效 + * @param {string} providerType - 提供商类型 + * @param {Object} credentials - 凭证对象 + * @returns {boolean} + */ + _validateCredentials(providerType, credentials) { + if (!credentials || typeof credentials !== 'object') { + return false; + } + + // 根据提供商类型检查必要的 token 字段 + switch (providerType) { + case 'claude-kiro-oauth': + // Kiro 需要 refreshToken 或 accessToken + return !!(credentials.refreshToken || credentials.accessToken); + + case 'gemini-cli-oauth': + case 'gemini-antigravity': + // Gemini 需要 refresh_token 或 access_token + return !!(credentials.refresh_token || credentials.access_token); + + case 'openai-qwen-oauth': + // Qwen 需要 refresh_token 或 access_token + return !!(credentials.refresh_token || credentials.access_token); + + case 'openai-iflow': + // iFlow 需要 refresh_token 或 access_token + return !!(credentials.refresh_token || credentials.access_token); + + case 'claude-orchids-oauth': + // Orchids 需要 sessionKey 或 accessToken + return !!(credentials.sessionKey || credentials.accessToken); + + default: + // 默认:只要有任何 token 相关字段就认为有效 + return !!( + credentials.refreshToken || credentials.accessToken || + credentials.refresh_token || credentials.access_token || + credentials.sessionKey || credentials.apiKey + ); + } } /** @@ -252,7 +309,19 @@ export class CredentialCacheManager { : path.join(process.cwd(), filePath); const content = await fs.readFile(absolutePath, 'utf8'); - return JSON.parse(content); + try { + return JSON.parse(content); + } catch (parseError) { + console.warn(`[CredentialCache] JSON parse failed for ${filePath}, attempting field extraction...`); + // 尝试从损坏的 JSON 中提取关键字段 + const extracted = this._extractCredentialsFromCorruptedJson(content); + if (extracted) { + console.info(`[CredentialCache] Field extraction successful for ${filePath}`); + return extracted; + } + console.error(`[CredentialCache] All recovery methods failed for ${filePath}: ${parseError.message}`); + return null; + } } catch (error) { if (error.code === 'ENOENT') { return null; @@ -261,6 +330,49 @@ export class CredentialCacheManager { } } + /** + * 从损坏的 JSON 中提取关键凭证字段 + * @param {string} content - 文件内容 + * @returns {Object|null} + */ + _extractCredentialsFromCorruptedJson(content) { + const extracted = {}; + + // 定义需要提取的关键字段及其正则模式 + const fieldPatterns = { + refreshToken: /"refreshToken"\s*:\s*"([^"]+)"/, + accessToken: /"accessToken"\s*:\s*"([^"]+)"/, + clientId: /"clientId"\s*:\s*"([^"]+)"/, + clientSecret: /"clientSecret"\s*:\s*"([^"]+)"/, + profileArn: /"profileArn"\s*:\s*"([^"]+)"/, + region: /"region"\s*:\s*"([^"]+)"/, + authMethod: /"authMethod"\s*:\s*"([^"]+)"/, + expiresAt: /"expiresAt"\s*:\s*"([^"]+)"/, + startUrl: /"startUrl"\s*:\s*"([^"]+)"/, + // Gemini/Qwen 相关字段 + refresh_token: /"refresh_token"\s*:\s*"([^"]+)"/, + access_token: /"access_token"\s*:\s*"([^"]+)"/, + client_id: /"client_id"\s*:\s*"([^"]+)"/, + client_secret: /"client_secret"\s*:\s*"([^"]+)"/, + }; + + for (const [field, pattern] of Object.entries(fieldPatterns)) { + const match = content.match(pattern); + if (match && match[1]) { + extracted[field] = match[1]; + } + } + + // 至少需要某种 token 才算有效 + if (extracted.refreshToken || extracted.accessToken || + extracted.refresh_token || extracted.access_token) { + console.info(`[CredentialCache] Extracted ${Object.keys(extracted).length} fields: ${Object.keys(extracted).join(', ')}`); + return extracted; + } + + return null; + } + /** * 获取凭证(从内存) * @param {string} providerType - 提供商类型