// electron/modules/version-fetcher.ts - 版本获取模块 // 通过各平台包管理器查询可用版本 import * as os from 'os' import { execa } from 'execa' import type { SoftwareType, VersionItem, VersionResult, Platform, GitMirrorType, NodejsMirrorType } from './types' import { MIN_SUPPORTED_NODE_VERSION, MAX_MAJOR_VERSIONS, VERSION_CACHE_TTL, ERROR_MESSAGES, BREW_PACKAGES, GIT_MIRRORS, NODEJS_MIRRORS, VSCODE_API } from './constants' import { setCache, getCache, clearCache } from './utils' import { updateAptSourceForQuery } from './source-updater' import { getGitMirrorFromConfig, saveGitMirrorConfig, getNodejsMirrorFromConfig, saveNodejsMirrorConfig } from './config' /** * 版本比较函数(降序) */ function compareVersions(a: string, b: string): number { const aParts = a.split('.').map(Number) const bParts = b.split('.').map(Number) for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { const diff = (bParts[i] || 0) - (aParts[i] || 0) if (diff !== 0) return diff } return 0 } /** * 添加版本到分组 Map */ function addToVersionMap( map: Map, fullVersion: string, label: string, options?: { extra?: Partial; groupKey?: string } ): void { const parts = fullVersion.split('.') const key = options?.groupKey ?? `${parts[0]}.${parts[1]}` const list = map.get(key) ?? [] // 检查是否已存在相同版本,避免重复 if (!list.some((item) => item.value === fullVersion)) { list.push({ value: fullVersion, label, ...options?.extra }) } map.set(key, list) } /** * 通用版本列表构建器 */ function buildVersionList( versionsByMajor: Map, options: { maxMajors?: number specialVersions?: VersionItem[] } = {} ): VersionItem[] { const { maxMajors = MAX_MAJOR_VERSIONS, specialVersions = [] } = options const versions: VersionItem[] = [] // 按版本号降序排列 const sortedMajors = Array.from(versionsByMajor.keys()) .sort(compareVersions) .slice(0, maxMajors) sortedMajors.forEach((major, index) => { const isFirst = index === 0 const label = isFirst ? `── v${major}.x (最新) ──` : `── v${major}.x ──` versions.push({ value: '', label, disabled: true, separator: true }) const majorVersions = versionsByMajor.get(major) ?? [] const sorted = majorVersions.sort((a, b) => compareVersions(a.value, b.value)) versions.push(...sorted) }) // 添加特殊版本 if (specialVersions.length > 0) { versions.push({ value: '', label: '── 其他版本 ──', disabled: true, separator: true }) versions.push(...specialVersions) } return versions } /** * 获取当前平台 */ function getPlatform(): Platform { return os.platform() as Platform } // ==================== Node.js 镜像配置 ==================== /** * 设置 Node.js 镜像(会持久化保存) */ export function setNodejsMirror(mirror: NodejsMirrorType): void { saveNodejsMirrorConfig(mirror) // 清除 Node.js 版本缓存,以便重新获取 clearCache('versions_nodejs') } /** * 获取当前 Node.js 镜像配置 */ export function getNodejsMirrorConfig(): { mirror: NodejsMirrorType } { return { mirror: getNodejsMirrorFromConfig() } } /** * 获取当前 Node.js 镜像类型(内部使用) */ function getCurrentNodejsMirror(): NodejsMirrorType { return getNodejsMirrorFromConfig() } /** * 获取 Node.js 下载 URL */ export function getNodejsDownloadUrl(version: string): string { const arch = os.arch() === 'x64' ? 'x64' : os.arch() === 'arm64' ? 'arm64' : 'x86' const mirror = NODEJS_MIRRORS[getCurrentNodejsMirror()] return mirror.getDownloadUrl(version, arch) } // ==================== Node.js API 版本数据类型 ==================== interface NodejsVersionInfo { version: string date: string lts: boolean | string security: boolean } // ==================== macOS brew 版本查询 ==================== /** * 使用 brew 查询软件信息 * brew 不支持安装历史版本,只返回当前可安装的版本 */ async function getBrewVersion(formula: string): Promise { try { const result = await execa('brew', ['info', '--json=v2', formula], { timeout: 30000 }) const data = JSON.parse(result.stdout) // 处理 formula 和 cask 两种情况 if (data.formulae && data.formulae.length > 0) { return data.formulae[0].versions?.stable || null } if (data.casks && data.casks.length > 0) { return data.casks[0].version || null } return null } catch (error) { console.error(`brew 查询 ${formula} 版本失败:`, error) return null } } /** * 获取 brew 可用的 Node.js 版本 * brew 支持 node (最新), node@20, node@18 等 */ async function getBrewNodeVersions(): Promise> { const versionsByMajor = new Map() // 查询各个 formula 的版本 const formulas = [ { name: BREW_PACKAGES.nodejs.default, majorHint: null }, { name: BREW_PACKAGES.nodejs['20'], majorHint: '20' }, { name: BREW_PACKAGES.nodejs['18'], majorHint: '18' } ] const results = await Promise.allSettled( formulas.map(async (f) => { const version = await getBrewVersion(f.name) return { formula: f.name, majorHint: f.majorHint, version } }) ) for (const result of results) { if (result.status === 'fulfilled' && result.value.version) { const { formula, version } = result.value const major = version.split('.')[0] const majorNum = parseInt(major) if (majorNum >= MIN_SUPPORTED_NODE_VERSION) { const label = formula === 'node' ? `Node.js ${version} (最新)` : `Node.js ${version}` addToVersionMap(versionsByMajor, version, label, { groupKey: major }) } } } return versionsByMajor } // ==================== Linux apt 版本查询 ==================== /** * 使用 apt 查询软件可用版本 * apt 通常只有仓库中的一个版本 */ async function getAptVersion(packageName: string): Promise { try { const result = await execa('apt-cache', ['policy', packageName], { timeout: 30000 }) // 解析输出,查找候选版本 const match = result.stdout.match(/Candidate:\s*(\S+)/) if (match) { // 提取版本号(去除 epoch 和 debian 修订号) const fullVersion = match[1] const versionMatch = fullVersion.match(/(\d+\.\d+\.\d+)/) return versionMatch ? versionMatch[1] : fullVersion } return null } catch (error) { console.error(`apt 查询 ${packageName} 版本失败:`, error) return null } } // ==================== Node.js 版本获取 ==================== /** * 从 Node.js API 获取版本列表 */ async function getNodeVersionsFromAPI(): Promise { const mirror = NODEJS_MIRRORS[getCurrentNodejsMirror()] try { const response = await fetch(mirror.versionsUrl, { headers: { 'User-Agent': 'ApqInstaller' }, signal: AbortSignal.timeout(15000) }) if (!response.ok) { throw new Error(`API 请求失败: ${response.status}`) } return await response.json() as NodejsVersionInfo[] } catch (error) { console.error(`从 ${mirror.name} 获取 Node.js 版本失败:`, error) return [] } } async function getNodeVersionsWindows(): Promise { const versionsByMajor = new Map() const versions = await getNodeVersionsFromAPI() if (versions.length === 0) { throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR) } // 用于记录每个主版本的最新 LTS 版本 const ltsVersions = new Map() for (const info of versions) { // 版本格式: v22.11.0 -> 22.11.0 const version = info.version.replace(/^v/, '') const major = version.split('.')[0] const majorNum = parseInt(major) if (majorNum >= MIN_SUPPORTED_NODE_VERSION) { const isLts = info.lts !== false const ltsName = typeof info.lts === 'string' ? info.lts : null // 记录每个主版本的第一个 LTS 版本(最新的) if (isLts && !ltsVersions.has(major)) { ltsVersions.set(major, version) } let label = `Node.js ${version}` if (ltsName) { label += ` (${ltsName})` } addToVersionMap(versionsByMajor, version, label, { groupKey: major, extra: { lts: isLts } }) } } if (versionsByMajor.size === 0) { throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR) } const mirrorName = NODEJS_MIRRORS[getCurrentNodejsMirror()].name return { versions: buildVersionList(versionsByMajor), warning: `下载源: ${mirrorName}` } } async function getNodeVersionsMac(): Promise { const versionsByMajor = await getBrewNodeVersions() if (versionsByMajor.size === 0) { throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR) } return { versions: buildVersionList(versionsByMajor), warning: 'brew 仅支持安装当前可用版本,不支持选择历史版本' } } async function getNodeVersionsLinux(): Promise { const version = await getAptVersion('nodejs') if (!version) { throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR) } const major = version.split('.')[0] const versionsByMajor = new Map() addToVersionMap(versionsByMajor, version, `Node.js ${version}`, { groupKey: major }) return { versions: buildVersionList(versionsByMajor), warning: 'apt 仅支持安装仓库中的版本,如需其他版本请使用 nvm' } } async function getNodeVersions(): Promise { const cacheKey = 'versions_nodejs' const cached = getCache(cacheKey) if (cached) return cached const platform = getPlatform() let result: VersionResult switch (platform) { case 'win32': result = await getNodeVersionsWindows() break case 'darwin': result = await getNodeVersionsMac() break case 'linux': result = await getNodeVersionsLinux() break default: throw new Error(`${ERROR_MESSAGES.UNSUPPORTED_PLATFORM}: ${platform}`) } setCache(cacheKey, result, VERSION_CACHE_TTL) return result } // ==================== VS Code 版本获取 ==================== /** * 从 VS Code API 获取版本列表 */ async function getVSCodeVersionsFromAPI(): Promise { try { const response = await fetch(VSCODE_API.versionsUrl, { headers: { 'User-Agent': 'ApqInstaller' }, signal: AbortSignal.timeout(15000) }) if (!response.ok) { throw new Error(`API 请求失败: ${response.status}`) } return await response.json() as string[] } catch (error) { console.error('从 VS Code API 获取版本失败:', error) return [] } } /** * 获取 VS Code 下载 URL */ export function getVSCodeDownloadUrl(version: string): string { const arch = os.arch() === 'x64' ? 'x64' : os.arch() === 'arm64' ? 'arm64' : 'x86' return VSCODE_API.getDownloadUrl(version, arch, 'user') } async function getVSCodeVersionsWindows(): Promise { const versionsByMajor = new Map() const versions = await getVSCodeVersionsFromAPI() if (versions.length === 0) { throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR) } for (const version of versions) { if (/^\d+\.\d+\.\d+$/.test(version)) { addToVersionMap(versionsByMajor, version, `VS Code ${version}`) } } if (versionsByMajor.size === 0) { throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR) } return { versions: buildVersionList(versionsByMajor, { specialVersions: [{ value: 'insiders', label: 'Insiders (预览版)' }] }), warning: null } } async function getVSCodeVersionsMac(): Promise { const version = await getBrewVersion(BREW_PACKAGES.vscode.stable) if (!version) { throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR) } const versionsByMajor = new Map() addToVersionMap(versionsByMajor, version, `VS Code ${version}`) return { versions: buildVersionList(versionsByMajor, { specialVersions: [{ value: 'insiders', label: 'Insiders (预览版)' }] }), warning: 'brew 仅支持安装当前最新版本' } } async function getVSCodeVersionsLinux(): Promise { // Linux 使用 snap 安装 VS Code,snap 不支持版本选择 // 返回一个占位版本 const versions: VersionItem[] = [ { value: 'stable', label: 'VS Code (最新稳定版)' }, { value: 'insiders', label: 'Insiders (预览版)' } ] return { versions, warning: 'snap 仅支持安装最新版本' } } async function getVSCodeVersions(): Promise { const cacheKey = 'versions_vscode' const cached = getCache(cacheKey) if (cached) return cached const platform = getPlatform() let result: VersionResult switch (platform) { case 'win32': result = await getVSCodeVersionsWindows() break case 'darwin': result = await getVSCodeVersionsMac() break case 'linux': result = await getVSCodeVersionsLinux() break default: throw new Error(`${ERROR_MESSAGES.UNSUPPORTED_PLATFORM}: ${platform}`) } setCache(cacheKey, result, VERSION_CACHE_TTL) return result } // ==================== Git 版本获取 ==================== /** * 设置 Git 镜像(会持久化保存) */ export function setGitMirror(mirror: GitMirrorType): void { saveGitMirrorConfig(mirror) // 清除版本缓存,以便重新获取 clearCache() } /** * 获取当前 Git 镜像配置 */ export function getGitMirrorConfig(): { mirror: GitMirrorType } { return { mirror: getGitMirrorFromConfig() } } /** * 获取当前 Git 镜像类型(内部使用) */ function getCurrentGitMirror(): GitMirrorType { return getGitMirrorFromConfig() } /** * 获取 Git 下载 URL */ export function getGitDownloadUrl(version: string): string { const arch = os.arch() === 'x64' ? '64' : '32' const mirror = GIT_MIRRORS[getCurrentGitMirror()] return mirror.getDownloadUrl(version, arch) } // 备用版本列表(当无法从任何源获取时使用) const FALLBACK_GIT_VERSIONS = [ '2.47.1', '2.47.0', '2.46.2', '2.46.1', '2.46.0', '2.45.2', '2.45.1', '2.45.0', '2.44.0', '2.43.0', '2.42.0' ] /** * 从 GitHub API 获取版本列表 */ async function getGitVersionsFromGitHub(): Promise { try { const response = await fetch('https://api.github.com/repos/git-for-windows/git/releases', { headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'ApqInstaller' }, signal: AbortSignal.timeout(10000) }) if (!response.ok) { throw new Error(`GitHub API 请求失败: ${response.status}`) } const releases = await response.json() as Array<{ tag_name: string; prerelease: boolean }> const versions: string[] = [] for (const release of releases) { if (release.prerelease) continue const match = release.tag_name.match(/^v?(\d+\.\d+\.\d+)/) if (match && !versions.includes(match[1])) { versions.push(match[1]) } } return versions } catch (error) { console.error('从 GitHub 获取 Git 版本失败:', error) return [] } } /** * 从华为云镜像获取版本列表 * 通过解析目录页面获取可用版本 */ async function getGitVersionsFromHuaweicloud(): Promise { try { const response = await fetch('https://mirrors.huaweicloud.com/git-for-windows/', { headers: { 'User-Agent': 'ApqInstaller' }, signal: AbortSignal.timeout(10000) }) if (!response.ok) { throw new Error(`华为云镜像请求失败: ${response.status}`) } const html = await response.text() const versions: string[] = [] // 解析 HTML 页面,查找版本目录链接 // 格式: v2.47.1.windows.1/ const regex = /href="v(\d+\.\d+\.\d+)\.windows\.\d+\/"/g let match while ((match = regex.exec(html)) !== null) { const version = match[1] if (!versions.includes(version)) { versions.push(version) } } // 按版本号降序排序 versions.sort((a, b) => { const partsA = a.split('.').map(Number) const partsB = b.split('.').map(Number) for (let i = 0; i < 3; i++) { if (partsA[i] !== partsB[i]) { return partsB[i] - partsA[i] } } return 0 }) return versions } catch (error) { console.error('从华为云镜像获取 Git 版本失败:', error) return [] } } async function getGitVersionsWindows(): Promise { const versionsByMajor = new Map() // 根据当前镜像源获取版本列表 let versions: string[] = [] const currentMirror = getCurrentGitMirror() if (currentMirror === 'github') { versions = await getGitVersionsFromGitHub() } else { // 华为云镜像 versions = await getGitVersionsFromHuaweicloud() } // 如果获取失败,使用备用版本列表 if (versions.length === 0) { console.log('使用备用 Git 版本列表') versions = FALLBACK_GIT_VERSIONS } for (const version of versions) { if (/^\d+\.\d+\.\d+/.test(version)) { addToVersionMap(versionsByMajor, version, `Git ${version}`) } } if (versionsByMajor.size === 0) { throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR) } const mirrorName = GIT_MIRRORS[getCurrentGitMirror()].name return { versions: buildVersionList(versionsByMajor, { specialVersions: [ { value: 'mingit', label: 'MinGit (精简版)' }, { value: 'lfs', label: 'Git LFS (大文件支持)' } ] }), warning: `下载源: ${mirrorName}` } } async function getGitVersionsMac(): Promise { const version = await getBrewVersion(BREW_PACKAGES.git.stable) if (!version) { throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR) } const versionsByMajor = new Map() addToVersionMap(versionsByMajor, version, `Git ${version}`) return { versions: buildVersionList(versionsByMajor, { specialVersions: [{ value: 'lfs', label: 'Git LFS (大文件支持)' }] }), warning: 'brew 仅支持安装当前最新版本' } } async function getGitVersionsLinux(): Promise { const version = await getAptVersion('git') if (!version) { throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR) } const versionsByMajor = new Map() addToVersionMap(versionsByMajor, version, `Git ${version}`) return { versions: buildVersionList(versionsByMajor, { specialVersions: [{ value: 'lfs', label: 'Git LFS (大文件支持)' }] }), warning: 'apt 仅支持安装仓库中的版本' } } async function getGitVersions(): Promise { const cacheKey = 'versions_git' const cached = getCache(cacheKey) if (cached) return cached const platform = getPlatform() let result: VersionResult switch (platform) { case 'win32': result = await getGitVersionsWindows() break case 'darwin': result = await getGitVersionsMac() break case 'linux': result = await getGitVersionsLinux() break default: throw new Error(`${ERROR_MESSAGES.UNSUPPORTED_PLATFORM}: ${platform}`) } setCache(cacheKey, result, VERSION_CACHE_TTL) return result } // ==================== 统一导出 ==================== export async function getVersions(software: SoftwareType): Promise { // Linux 下获取版本前先更新 apt 源(带缓存,1天内只更新一次) const platform = getPlatform() if (platform === 'linux') { await updateAptSourceForQuery() } // Windows 使用 API 获取版本,不需要更新源 // macOS 的 brew 不需要手动更新源,brew info 会自动获取最新信息 switch (software) { case 'nodejs': return await getNodeVersions() case 'vscode': return await getVSCodeVersions() case 'git': return await getGitVersions() default: return { versions: [], warning: ERROR_MESSAGES.UNKNOWN_SOFTWARE } } } export { clearCache }