368 lines
No EOL
15 KiB
JavaScript
368 lines
No EOL
15 KiB
JavaScript
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;
|
||
} |