Spaces:
Running
Running
| /** | |
| * 提供商工具模块 | |
| * 包含 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(/^\.\//, '')); | |
| } |