Spaces:
Running
Running
| 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>} 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<Object|null>} 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>} 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; | |
| } |