| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742 |
- // 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<string, VersionItem[]>,
- fullVersion: string,
- label: string,
- options?: { extra?: Partial<VersionItem>; 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<string, VersionItem[]>,
- 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<string | null> {
- 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<Map<string, VersionItem[]>> {
- const versionsByMajor = new Map<string, VersionItem[]>()
- // 查询各个 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<string | null> {
- 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<NodejsVersionInfo[]> {
- 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<VersionResult> {
- const versionsByMajor = new Map<string, VersionItem[]>()
- const versions = await getNodeVersionsFromAPI()
- if (versions.length === 0) {
- throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR)
- }
- // 用于记录每个主版本的最新 LTS 版本
- const ltsVersions = new Map<string, string>()
- 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<VersionResult> {
- 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<VersionResult> {
- const version = await getAptVersion('nodejs')
- if (!version) {
- throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR)
- }
- const major = version.split('.')[0]
- const versionsByMajor = new Map<string, VersionItem[]>()
- addToVersionMap(versionsByMajor, version, `Node.js ${version}`, { groupKey: major })
- return {
- versions: buildVersionList(versionsByMajor),
- warning: 'apt 仅支持安装仓库中的版本,如需其他版本请使用 nvm'
- }
- }
- async function getNodeVersions(): Promise<VersionResult> {
- const cacheKey = 'versions_nodejs'
- const cached = getCache<VersionResult>(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<string[]> {
- 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<VersionResult> {
- const versionsByMajor = new Map<string, VersionItem[]>()
- 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<VersionResult> {
- const version = await getBrewVersion(BREW_PACKAGES.vscode.stable)
- if (!version) {
- throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR)
- }
- const versionsByMajor = new Map<string, VersionItem[]>()
- addToVersionMap(versionsByMajor, version, `VS Code ${version}`)
- return {
- versions: buildVersionList(versionsByMajor, {
- specialVersions: [{ value: 'insiders', label: 'Insiders (预览版)' }]
- }),
- warning: 'brew 仅支持安装当前最新版本'
- }
- }
- async function getVSCodeVersionsLinux(): Promise<VersionResult> {
- // 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<VersionResult> {
- const cacheKey = 'versions_vscode'
- const cached = getCache<VersionResult>(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<string[]> {
- 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<string[]> {
- 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 页面,查找版本目录链接
- // 格式: <a href="v2.47.1.windows.1/">v2.47.1.windows.1/</a>
- 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<VersionResult> {
- const versionsByMajor = new Map<string, VersionItem[]>()
- // 根据当前镜像源获取版本列表
- 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<VersionResult> {
- const version = await getBrewVersion(BREW_PACKAGES.git.stable)
- if (!version) {
- throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR)
- }
- const versionsByMajor = new Map<string, VersionItem[]>()
- addToVersionMap(versionsByMajor, version, `Git ${version}`)
- return {
- versions: buildVersionList(versionsByMajor, {
- specialVersions: [{ value: 'lfs', label: 'Git LFS (大文件支持)' }]
- }),
- warning: 'brew 仅支持安装当前最新版本'
- }
- }
- async function getGitVersionsLinux(): Promise<VersionResult> {
- const version = await getAptVersion('git')
- if (!version) {
- throw new Error(ERROR_MESSAGES.VERSION_FETCH_ERROR)
- }
- const versionsByMajor = new Map<string, VersionItem[]>()
- addToVersionMap(versionsByMajor, version, `Git ${version}`)
- return {
- versions: buildVersionList(versionsByMajor, {
- specialVersions: [{ value: 'lfs', label: 'Git LFS (大文件支持)' }]
- }),
- warning: 'apt 仅支持安装仓库中的版本'
- }
- }
- async function getGitVersions(): Promise<VersionResult> {
- const cacheKey = 'versions_git'
- const cached = getCache<VersionResult>(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<VersionResult> {
- // 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 }
|