import { existsSync } from 'fs'; import { promises as fs } from 'fs'; import path from 'path'; import { addToUsedPaths, isPathUsed, pathsEqual } from '../utils/provider-utils.js'; /** * 扫描和分析配置文件 * @param {Object} currentConfig - The current configuration object * @param {Object} providerPoolManager - Provider pool manager instance * @returns {Promise} Array of configuration file objects */ export async function scanConfigFiles(currentConfig, providerPoolManager) { const configFiles = []; // 只扫描configs目录 const configsPath = path.join(process.cwd(), 'configs'); if (!existsSync(configsPath)) { // console.log('[Config Scanner] configs directory not found, creating empty result'); return configFiles; } const usedPaths = new Set(); // 存储已使用的路径,用于判断关联状态 // 从配置中提取所有OAuth凭据文件路径 - 标准化路径格式 addToUsedPaths(usedPaths, currentConfig.GEMINI_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, currentConfig.KIRO_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, currentConfig.QWEN_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, currentConfig.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, currentConfig.IFLOW_TOKEN_FILE_PATH); addToUsedPaths(usedPaths, currentConfig.CODEX_OAUTH_CREDS_FILE_PATH); // 使用最新的提供商池数据 let providerPools = currentConfig.providerPools; if (providerPoolManager && providerPoolManager.providerPools) { providerPools = providerPoolManager.providerPools; } // 检查提供商池文件中的所有OAuth凭据路径 - 标准化路径格式 if (providerPools) { for (const [providerType, providers] of Object.entries(providerPools)) { for (const provider of providers) { addToUsedPaths(usedPaths, provider.GEMINI_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, provider.KIRO_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, provider.QWEN_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, provider.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, provider.IFLOW_TOKEN_FILE_PATH); addToUsedPaths(usedPaths, provider.CODEX_OAUTH_CREDS_FILE_PATH); } } } try { // 扫描configs目录下的所有子目录和文件 const configsFiles = await scanOAuthDirectory(configsPath, usedPaths, currentConfig); configFiles.push(...configsFiles); } catch (error) { console.warn(`[Config Scanner] Failed to scan configs directory:`, error.message); } return configFiles; } /** * 分析 OAuth 配置文件并返回元数据 * @param {string} filePath - Full path to the file * @param {Set} usedPaths - Set of paths currently in use * @returns {Promise} OAuth file information object */ async function analyzeOAuthFile(filePath, usedPaths, currentConfig) { try { const stats = await fs.stat(filePath); const ext = path.extname(filePath).toLowerCase(); const filename = path.basename(filePath); const relativePath = path.relative(process.cwd(), filePath); // 读取文件内容进行分析 let content = ''; let type = 'oauth_credentials'; let isValid = true; let errorMessage = ''; let oauthProvider = 'unknown'; let usageInfo = getFileUsageInfo(relativePath, filename, usedPaths, currentConfig); try { if (ext === '.json') { const rawContent = await fs.readFile(filePath, 'utf8'); const jsonData = JSON.parse(rawContent); content = rawContent; // 识别OAuth提供商 if (jsonData.apiKey || jsonData.api_key) { type = 'api_key'; } else if (jsonData.client_id || jsonData.client_secret) { oauthProvider = 'oauth2'; } else if (jsonData.access_token || jsonData.refresh_token) { oauthProvider = 'token_based'; } else if (jsonData.credentials) { oauthProvider = 'service_account'; } if (jsonData.base_url || jsonData.endpoint) { if (jsonData.base_url.includes('openai.com')) { oauthProvider = 'openai'; } else if (jsonData.base_url.includes('anthropic.com')) { oauthProvider = 'claude'; } else if (jsonData.base_url.includes('googleapis.com')) { oauthProvider = 'gemini'; } } } else { content = await fs.readFile(filePath, 'utf8'); if (ext === '.key' || ext === '.pem') { if (content.includes('-----BEGIN') && content.includes('PRIVATE KEY-----')) { oauthProvider = 'private_key'; } } else if (ext === '.txt') { if (content.includes('api_key') || content.includes('apikey')) { oauthProvider = 'api_key'; } } else if (ext === '.oauth' || ext === '.creds') { oauthProvider = 'oauth_credentials'; } } } catch (readError) { isValid = false; errorMessage = `Unable to read file: ${readError.message}`; } return { name: filename, path: relativePath, size: stats.size, type: type, provider: oauthProvider, extension: ext, modified: stats.mtime.toISOString(), isValid: isValid, errorMessage: errorMessage, isUsed: isPathUsed(relativePath, filename, usedPaths), usageInfo: usageInfo, // 新增详细关联信息 preview: content.substring(0, 100) + (content.length > 100 ? '...' : '') }; } catch (error) { console.warn(`[OAuth Analyzer] Failed to analyze file ${filePath}:`, error.message); return null; } } /** * Get detailed usage information for a file * @param {string} relativePath - Relative file path * @param {string} fileName - File name * @param {Set} usedPaths - Set of used paths * @param {Object} currentConfig - Current configuration * @returns {Object} Usage information object */ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) { const usageInfo = { isUsed: false, usageType: null, usageDetails: [] }; // 检查是否被使用 const isUsed = isPathUsed(relativePath, fileName, usedPaths); if (!isUsed) { return usageInfo; } usageInfo.isUsed = true; // 检查主要配置中的使用情况 if (currentConfig.GEMINI_OAUTH_CREDS_FILE_PATH && (pathsEqual(relativePath, currentConfig.GEMINI_OAUTH_CREDS_FILE_PATH) || pathsEqual(relativePath, currentConfig.GEMINI_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) { usageInfo.usageType = 'main_config'; usageInfo.usageDetails.push({ type: 'Main Config', location: 'Gemini OAuth credentials file path', configKey: 'GEMINI_OAUTH_CREDS_FILE_PATH' }); } if (currentConfig.KIRO_OAUTH_CREDS_FILE_PATH && (pathsEqual(relativePath, currentConfig.KIRO_OAUTH_CREDS_FILE_PATH) || pathsEqual(relativePath, currentConfig.KIRO_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) { usageInfo.usageType = 'main_config'; usageInfo.usageDetails.push({ type: 'Main Config', location: 'Kiro OAuth credentials file path', configKey: 'KIRO_OAUTH_CREDS_FILE_PATH' }); } if (currentConfig.QWEN_OAUTH_CREDS_FILE_PATH && (pathsEqual(relativePath, currentConfig.QWEN_OAUTH_CREDS_FILE_PATH) || pathsEqual(relativePath, currentConfig.QWEN_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) { usageInfo.usageType = 'main_config'; usageInfo.usageDetails.push({ type: 'Main Config', location: 'Qwen OAuth credentials file path', configKey: 'QWEN_OAUTH_CREDS_FILE_PATH' }); } if (currentConfig.IFLOW_TOKEN_FILE_PATH && (pathsEqual(relativePath, currentConfig.IFLOW_TOKEN_FILE_PATH) || pathsEqual(relativePath, currentConfig.IFLOW_TOKEN_FILE_PATH.replace(/\\/g, '/')))) { usageInfo.usageType = 'main_config'; usageInfo.usageDetails.push({ type: 'Main Config', location: 'iFlow Token file path', configKey: 'IFLOW_TOKEN_FILE_PATH' }); } if (currentConfig.CODEX_OAUTH_CREDS_FILE_PATH && (pathsEqual(relativePath, currentConfig.CODEX_OAUTH_CREDS_FILE_PATH) || pathsEqual(relativePath, currentConfig.CODEX_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) { usageInfo.usageType = 'main_config'; usageInfo.usageDetails.push({ type: 'Main Config', location: 'Codex OAuth credentials file path', configKey: 'CODEX_OAUTH_CREDS_FILE_PATH' }); } // 检查提供商池中的使用情况 if (currentConfig.providerPools) { // 使用 flatMap 将双重循环优化为单层循环 O(n) const allProviders = Object.entries(currentConfig.providerPools).flatMap( ([providerType, providers]) => providers.map((provider, index) => ({ provider, providerType, index })) ); for (const { provider, providerType, index } of allProviders) { const providerUsages = []; if (provider.GEMINI_OAUTH_CREDS_FILE_PATH && (pathsEqual(relativePath, provider.GEMINI_OAUTH_CREDS_FILE_PATH) || pathsEqual(relativePath, provider.GEMINI_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) { providerUsages.push({ type: 'Provider Pool', location: `Gemini OAuth credentials (node ${index + 1})`, providerType: providerType, providerIndex: index, configKey: 'GEMINI_OAUTH_CREDS_FILE_PATH' }); } if (provider.KIRO_OAUTH_CREDS_FILE_PATH && (pathsEqual(relativePath, provider.KIRO_OAUTH_CREDS_FILE_PATH) || pathsEqual(relativePath, provider.KIRO_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) { providerUsages.push({ type: 'Provider Pool', location: `Kiro OAuth credentials (node ${index + 1})`, providerType: providerType, providerIndex: index, configKey: 'KIRO_OAUTH_CREDS_FILE_PATH' }); } if (provider.QWEN_OAUTH_CREDS_FILE_PATH && (pathsEqual(relativePath, provider.QWEN_OAUTH_CREDS_FILE_PATH) || pathsEqual(relativePath, provider.QWEN_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) { providerUsages.push({ type: 'Provider Pool', location: `Qwen OAuth credentials (node ${index + 1})`, providerType: providerType, providerIndex: index, configKey: 'QWEN_OAUTH_CREDS_FILE_PATH' }); } if (provider.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH && (pathsEqual(relativePath, provider.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH) || pathsEqual(relativePath, provider.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) { providerUsages.push({ type: 'Provider Pool', location: `Antigravity OAuth credentials (node ${index + 1})`, providerType: providerType, providerIndex: index, configKey: 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH' }); } if (provider.IFLOW_TOKEN_FILE_PATH && (pathsEqual(relativePath, provider.IFLOW_TOKEN_FILE_PATH) || pathsEqual(relativePath, provider.IFLOW_TOKEN_FILE_PATH.replace(/\\/g, '/')))) { providerUsages.push({ type: 'Provider Pool', location: `iFlow Token (node ${index + 1})`, providerType: providerType, providerIndex: index, configKey: 'IFLOW_TOKEN_FILE_PATH' }); } if (provider.CODEX_OAUTH_CREDS_FILE_PATH && (pathsEqual(relativePath, provider.CODEX_OAUTH_CREDS_FILE_PATH) || pathsEqual(relativePath, provider.CODEX_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) { providerUsages.push({ type: 'Provider Pool', location: `Codex OAuth credentials (node ${index + 1})`, providerType: providerType, providerIndex: index, configKey: 'CODEX_OAUTH_CREDS_FILE_PATH' }); } if (providerUsages.length > 0) { usageInfo.usageType = 'provider_pool'; usageInfo.usageDetails.push(...providerUsages); } } } // 如果有多个使用位置,标记为多种用途 if (usageInfo.usageDetails.length > 1) { usageInfo.usageType = 'multiple'; } return usageInfo; } /** * Scan OAuth directory for credential files * @param {string} dirPath - Directory path to scan * @param {Set} usedPaths - Set of used paths * @param {Object} currentConfig - Current configuration * @returns {Promise} Array of OAuth configuration file objects */ async function scanOAuthDirectory(dirPath, usedPaths, currentConfig) { const oauthFiles = []; try { const files = await fs.readdir(dirPath, { withFileTypes: true }); for (const file of files) { const fullPath = path.join(dirPath, file.name); if (file.isFile()) { const ext = path.extname(file.name).toLowerCase(); // 只关注OAuth相关的文件类型 if (['.json', '.oauth', '.creds', '.key', '.pem', '.txt'].includes(ext)) { const fileInfo = await analyzeOAuthFile(fullPath, usedPaths, currentConfig); if (fileInfo) { oauthFiles.push(fileInfo); } } } else if (file.isDirectory()) { // 递归扫描子目录(限制深度) const relativePath = path.relative(process.cwd(), fullPath); // 最大深度4层,以支持 configs/kiro/{subfolder}/file.json 这样的结构 if (relativePath.split(path.sep).length < 4) { const subFiles = await scanOAuthDirectory(fullPath, usedPaths, currentConfig); oauthFiles.push(...subFiles); } } } } catch (error) { console.warn(`[OAuth Scanner] Failed to scan directory ${dirPath}:`, error.message); } return oauthFiles; }