aiclient-2-api / src /ui-modules /config-scanner.js
Jaasomn
Initial deployment
ceb3821
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;
}