aiclient-2-api / src /utils /provider-utils.js
Jaasomn
Initial deployment
ceb3821
/**
* 提供商工具模块
* 包含 ui-manager.js 和 service-manager.js 共用的工具函数
*/
import * as path from 'path';
import { promises as fs } from 'fs';
/**
* 提供商目录映射配置
* 定义目录名称到提供商类型的映射关系
*/
export const PROVIDER_MAPPINGS = [
{
// Kiro OAuth 配置
dirName: 'kiro',
patterns: ['configs/kiro/', '/kiro/'],
providerType: 'claude-kiro-oauth',
credPathKey: 'KIRO_OAUTH_CREDS_FILE_PATH',
defaultCheckModel: 'claude-haiku-4-5',
displayName: 'Claude Kiro OAuth',
needsProjectId: false,
urlKeys: ['KIRO_BASE_URL', 'KIRO_REFRESH_URL', 'KIRO_REFRESH_IDC_URL']
},
{
// Gemini CLI OAuth 配置
dirName: 'gemini',
patterns: ['configs/gemini/', '/gemini/', 'configs/gemini-cli/'],
providerType: 'gemini-cli-oauth',
credPathKey: 'GEMINI_OAUTH_CREDS_FILE_PATH',
defaultCheckModel: 'gemini-2.5-flash',
displayName: 'Gemini CLI OAuth',
needsProjectId: true,
urlKeys: ['GEMINI_BASE_URL']
},
{
// Qwen OAuth 配置
dirName: 'qwen',
patterns: ['configs/qwen/', '/qwen/'],
providerType: 'openai-qwen-oauth',
credPathKey: 'QWEN_OAUTH_CREDS_FILE_PATH',
defaultCheckModel: 'qwen3-coder-plus',
displayName: 'Qwen OAuth',
needsProjectId: false,
urlKeys: ['QWEN_BASE_URL', 'QWEN_OAUTH_BASE_URL']
},
{
// Antigravity OAuth 配置
dirName: 'antigravity',
patterns: ['configs/antigravity/', '/antigravity/'],
providerType: 'gemini-antigravity',
credPathKey: 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH',
defaultCheckModel: 'gemini-2.5-computer-use-preview-10-2025',
displayName: 'Gemini Antigravity',
needsProjectId: true,
urlKeys: ['ANTIGRAVITY_BASE_URL_DAILY', 'ANTIGRAVITY_BASE_URL_AUTOPUSH']
},
{
// iFlow 配置
dirName: 'iflow',
patterns: ['configs/iflow/', '/iflow/'],
providerType: 'openai-iflow',
credPathKey: 'IFLOW_TOKEN_FILE_PATH',
defaultCheckModel: 'gpt-4o',
displayName: 'iFlow API',
needsProjectId: false,
urlKeys: ['IFLOW_BASE_URL']
},
{
// Codex OAuth 配置
dirName: 'codex',
patterns: ['configs/codex/', '/codex/'],
providerType: 'openai-codex-oauth',
credPathKey: 'CODEX_OAUTH_CREDS_FILE_PATH',
defaultCheckModel: 'gpt-5.2-codex',
displayName: 'OpenAI Codex OAuth',
needsProjectId: false,
urlKeys: ['CODEX_BASE_URL']
}
];
/**
* 生成 UUID
* @returns {string} UUID 字符串
*/
export function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* 标准化路径,用于跨平台兼容
* @param {string} filePath - 文件路径
* @returns {string} 使用正斜杠的标准化路径
*/
export function normalizePath(filePath) {
if (!filePath) return filePath;
// 使用 path 模块标准化,然后转换为正斜杠
const normalized = path.normalize(filePath);
return normalized.replace(/\\/g, '/');
}
/**
* 从路径中提取文件名
* @param {string} filePath - 文件路径
* @returns {string} 文件名
*/
export function getFileName(filePath) {
return path.basename(filePath);
}
/**
* 格式化相对路径为当前系统的路径格式
* @param {string} relativePath - 相对路径
* @returns {string} 格式化后的路径(带有 ./ 或 .\ 前缀)
*/
export function formatSystemPath(relativePath) {
if (!relativePath) return relativePath;
// 根据操作系统判断使用对应的路径分隔符
const isWindows = process.platform === 'win32';
const separator = isWindows ? '\\' : '/';
// 统一转换路径分隔符为当前系统的分隔符
const systemPath = relativePath.replace(/[\/\\]/g, separator);
return systemPath.startsWith('.' + separator) ? systemPath : '.' + separator + systemPath;
}
/**
* 检查两个路径是否指向同一文件(跨平台兼容)
* @param {string} path1 - 第一个路径
* @param {string} path2 - 第二个路径
* @returns {boolean} 如果路径指向同一文件则返回 true
*/
export function pathsEqual(path1, path2) {
if (!path1 || !path2) return false;
try {
// 标准化两个路径
const normalized1 = normalizePath(path1);
const normalized2 = normalizePath(path2);
// 直接匹配
if (normalized1 === normalized2) {
return true;
}
// 移除开头的 './' 后比较
const clean1 = normalized1.replace(/^\.\//, '');
const clean2 = normalized2.replace(/^\.\//, '');
if (clean1 === clean2) {
return true;
}
// 检查一个是否是另一个的子集(用于相对路径与绝对路径比较)
if (normalized1.endsWith('/' + clean2) || normalized2.endsWith('/' + clean1)) {
return true;
}
return false;
} catch (error) {
console.warn(`[Path Comparison] Error comparing paths: ${path1} vs ${path2}`, error.message);
return false;
}
}
/**
* 检查文件路径是否正在被使用(跨平台兼容)
* @param {string} relativePath - 相对路径
* @param {string} fileName - 文件名
* @param {Set} usedPaths - 已使用路径的集合
* @returns {boolean} 如果文件正在被使用则返回 true
*/
export function isPathUsed(relativePath, fileName, usedPaths) {
if (!relativePath) return false;
// 标准化相对路径
const normalizedRelativePath = normalizePath(relativePath);
const cleanRelativePath = normalizedRelativePath.replace(/^\.\//, '');
// 从相对路径获取文件名
const relativeFileName = getFileName(normalizedRelativePath);
// 遍历所有已使用路径进行匹配
for (const usedPath of usedPaths) {
if (!usedPath) continue;
// 1. 直接路径匹配
if (pathsEqual(relativePath, usedPath) || pathsEqual(relativePath, './' + usedPath)) {
return true;
}
// 2. 标准化路径匹配
if (pathsEqual(normalizedRelativePath, usedPath) ||
pathsEqual(normalizedRelativePath, './' + usedPath)) {
return true;
}
// 3. 清理后的路径匹配
if (pathsEqual(cleanRelativePath, usedPath) ||
pathsEqual(cleanRelativePath, './' + usedPath)) {
return true;
}
// 4. 文件名匹配(确保不是误匹配)
const usedFileName = getFileName(usedPath);
if (usedFileName === fileName || usedFileName === relativeFileName) {
// 确保是同一个目录下的文件
const usedDir = path.dirname(usedPath);
const relativeDir = path.dirname(normalizedRelativePath);
if (pathsEqual(usedDir, relativeDir) ||
pathsEqual(usedDir, cleanRelativePath.replace(/\/[^\/]+$/, '')) ||
pathsEqual(relativeDir.replace(/^\.\//, ''), usedDir.replace(/^\.\//, ''))) {
return true;
}
}
// 5. 绝对路径匹配(Windows 和 Unix)
try {
const resolvedUsedPath = path.resolve(usedPath);
const resolvedRelativePath = path.resolve(relativePath);
if (resolvedUsedPath === resolvedRelativePath) {
return true;
}
} catch (error) {
// 忽略路径解析错误
}
}
return false;
}
/**
* 根据文件路径检测提供商类型
* @param {string} normalizedPath - 标准化的文件路径(小写,正斜杠)
* @returns {Object|null} 提供商映射对象,如果未检测到则返回 null
*/
export function detectProviderFromPath(normalizedPath) {
// 遍历映射关系,查找匹配的提供商
for (const mapping of PROVIDER_MAPPINGS) {
for (const pattern of mapping.patterns) {
if (normalizedPath.includes(pattern)) {
return {
providerType: mapping.providerType,
credPathKey: mapping.credPathKey,
defaultCheckModel: mapping.defaultCheckModel,
displayName: mapping.displayName,
needsProjectId: mapping.needsProjectId
};
}
}
}
return null;
}
/**
* 根据目录名获取提供商映射
* @param {string} dirName - 目录名称
* @returns {Object|null} 提供商映射对象,如果未找到则返回 null
*/
export function getProviderMappingByDirName(dirName) {
return PROVIDER_MAPPINGS.find(m => m.dirName === dirName) || null;
}
/**
* 验证文件是否是有效的 OAuth 凭据文件
* @param {string} filePath - 文件路径
* @returns {Promise<boolean>} 是否有效
*/
export async function isValidOAuthCredentials(filePath) {
try {
const content = await fs.readFile(filePath, 'utf8');
const jsonData = JSON.parse(content);
// 检查是否包含 OAuth 相关字段
// 凭据通常包含 access_token/accessToken, refresh_token/refreshToken, client_id 等字段
// 支持下划线命名(access_token)和驼峰命名(accessToken)两种格式
if (jsonData.access_token || jsonData.refresh_token ||
jsonData.accessToken || jsonData.refreshToken ||
jsonData.client_id || jsonData.client_secret ||
jsonData.token || jsonData.credentials) {
return true;
}
// 也可能是包含嵌套结构的凭据文件
if (jsonData.installed || jsonData.web) {
return true;
}
return false;
} catch (error) {
// 如果无法解析,认为不是有效的凭据文件
return false;
}
}
/**
* 创建新的提供商配置对象
* @param {Object} options - 配置选项
* @param {string} options.credPathKey - 凭据路径键名
* @param {string} options.credPath - 凭据文件路径
* @param {string} options.defaultCheckModel - 默认检测模型
* @param {boolean} options.needsProjectId - 是否需要 PROJECT_ID
* @param {Array} options.urlKeys - 可选的 URL 配置项键名列表
* @returns {Object} 新的提供商配置对象
*/
export function createProviderConfig(options) {
const { credPathKey, credPath, defaultCheckModel, needsProjectId, urlKeys } = options;
const newProvider = {
[credPathKey]: credPath,
uuid: generateUUID(),
checkModelName: defaultCheckModel,
checkHealth: false,
isHealthy: true,
isDisabled: false,
lastUsed: null,
usageCount: 0,
errorCount: 0,
lastErrorTime: null,
lastHealthCheckTime: null,
lastHealthCheckModel: null,
lastErrorMessage: null
};
// 如果需要 PROJECT_ID,添加空字符串占位
if (needsProjectId) {
newProvider.PROJECT_ID = '';
}
// 初始化可选的 URL 配置项
if (urlKeys && Array.isArray(urlKeys)) {
urlKeys.forEach(key => {
newProvider[key] = '';
});
}
return newProvider;
}
/**
* 将路径添加到已使用路径集合(标准化多种格式)
* @param {Set} usedPaths - 已使用路径的集合
* @param {string} filePath - 要添加的文件路径
*/
export function addToUsedPaths(usedPaths, filePath) {
if (!filePath) return;
const normalizedPath = filePath.replace(/\\/g, '/');
usedPaths.add(filePath);
usedPaths.add(normalizedPath);
if (normalizedPath.startsWith('./')) {
usedPaths.add(normalizedPath.slice(2));
} else {
usedPaths.add('./' + normalizedPath);
}
}
/**
* 检查路径是否已关联(用于自动关联检测)
* @param {string} relativePath - 相对路径
* @param {Set} linkedPaths - 已关联路径的集合
* @returns {boolean} 是否已关联
*/
export function isPathLinked(relativePath, linkedPaths) {
return linkedPaths.has(relativePath) ||
linkedPaths.has('./' + relativePath) ||
linkedPaths.has(relativePath.replace(/^\.\//, ''));
}