feat(credentials): 增强凭证解析容错能力并添加预验证机制

新增从损坏 JSON 中提取关键凭证字段的恢复方法

凭证缓存管理器增加 token 有效性预检查,跳过无效凭证

配置 API 补充 POOL_SIZE_LIMIT 字段同步

优化日志输出,区分加载失败和跳过的凭证统计
This commit is contained in:
leonai 2026-01-16 17:16:38 +08:00
parent 025828868e
commit 228898bbf7
3 changed files with 171 additions and 6 deletions

View file

@ -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) {

View file

@ -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,

View file

@ -158,6 +158,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;
@ -171,6 +172,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;
@ -179,6 +181,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,
@ -191,16 +201,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
);
}
}
/**
@ -251,7 +308,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;
@ -260,6 +329,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 - 提供商类型