| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746 |
- // electron/modules/utils.ts - 工具函数
- import axios, { AxiosError, CancelTokenSource } from 'axios'
- import * as os from 'os'
- import * as fs from 'fs'
- import * as path from 'path'
- import { execa } from 'execa'
- import { REQUEST_TIMEOUT, MAX_RETRIES, RETRY_DELAY, ERROR_MESSAGES, SOURCE_UPDATE_CACHE_TTL } from './constants'
- // 版本缓存
- const versionCache = new Map<string, { data: unknown; expiry: number }>()
- // 包管理器源更新缓存(记录上次更新时间)
- const sourceUpdateCache = new Map<string, number>()
- // 创建 axios 实例
- const axiosInstance = axios.create({
- headers: { 'User-Agent': 'ApqInstaller/2.0' },
- timeout: REQUEST_TIMEOUT
- })
- /**
- * 验证 URL 是否合法
- */
- export function isValidUrl(url: string): boolean {
- try {
- const parsed = new URL(url)
- return ['http:', 'https:'].includes(parsed.protocol)
- } catch {
- return false
- }
- }
- /**
- * 带超时和重试的 HTTP GET 请求(使用 axios)
- */
- export async function httpsGet<T = unknown>(
- url: string,
- options: {
- timeout?: number
- retries?: number
- retryDelay?: number
- } = {}
- ): Promise<T> {
- const {
- timeout = REQUEST_TIMEOUT,
- retries = MAX_RETRIES,
- retryDelay = RETRY_DELAY
- } = options
- let lastError: Error | null = null
- for (let attempt = 1; attempt <= retries; attempt++) {
- try {
- const response = await axiosInstance.get<T>(url, {
- timeout,
- maxRedirects: 5
- })
- return response.data
- } catch (error) {
- lastError = error as Error
- const axiosError = error as AxiosError
- if (axiosError.code === 'ECONNABORTED' || axiosError.message.includes('timeout')) {
- console.log(`请求超时,${retryDelay}ms 后重试... (剩余 ${retries - attempt} 次)`)
- } else {
- console.log(`请求失败: ${axiosError.message},${retryDelay}ms 后重试... (剩余 ${retries - attempt} 次)`)
- }
- if (attempt < retries) {
- await delay(retryDelay)
- }
- }
- }
- if (lastError) {
- const axiosError = lastError as AxiosError
- if (axiosError.code === 'ECONNABORTED' || axiosError.message.includes('timeout')) {
- throw new Error(ERROR_MESSAGES.TIMEOUT_ERROR)
- }
- throw new Error(ERROR_MESSAGES.NETWORK_ERROR + ': ' + lastError.message)
- }
- throw new Error(ERROR_MESSAGES.NETWORK_ERROR)
- }
- /**
- * 获取 Windows 系统最新的 PATH 环境变量
- * 安装软件后,系统 PATH 会更新,但当前进程的 PATH 不会自动更新
- * 通过 PowerShell 从注册表读取最新的 PATH
- * 同时添加 pnpm 和 npm 的全局 bin 目录,确保能找到全局安装的命令
- */
- async function getRefreshedWindowsPath(): Promise<string> {
- try {
- const result = await execa('powershell', [
- '-NoProfile',
- '-Command',
- `[Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User')`
- ])
- let newPath = result.stdout.trim()
- // 添加 pnpm 全局 bin 目录(如果不在 PATH 中)
- // pnpm 在 Windows 上的默认全局 bin 目录是 %LOCALAPPDATA%\pnpm
- const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
- const pnpmGlobalBin = path.join(localAppData, 'pnpm')
- if (!newPath.toLowerCase().includes(pnpmGlobalBin.toLowerCase())) {
- newPath = `${pnpmGlobalBin};${newPath}`
- }
- // 添加 npm 全局 bin 目录(如果不在 PATH 中)
- // npm 在 Windows 上的默认全局 bin 目录是 %APPDATA%\npm
- const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
- const npmGlobalBin = path.join(appData, 'npm')
- if (!newPath.toLowerCase().includes(npmGlobalBin.toLowerCase())) {
- newPath = `${npmGlobalBin};${newPath}`
- }
- return newPath
- } catch {
- // 如果失败,返回当前进程的 PATH,并添加常用的全局 bin 目录
- let fallbackPath = process.env.PATH || ''
- const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
- const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
- fallbackPath = `${path.join(localAppData, 'pnpm')};${path.join(appData, 'npm')};${fallbackPath}`
- return fallbackPath
- }
- }
- /**
- * 获取刷新后的 PATH 环境变量(跨平台)
- * Windows: 从注册表读取最新的 PATH
- * 其他平台: 返回当前进程的 PATH
- */
- export async function getRefreshedPath(): Promise<string> {
- if (os.platform() === 'win32') {
- return await getRefreshedWindowsPath()
- }
- return process.env.PATH || ''
- }
- /**
- * 检测命令是否存在
- * Windows: 会尝试使用刷新后的 PATH 来检测新安装的软件
- */
- export async function commandExists(command: string, refreshPath = false): Promise<boolean> {
- try {
- const platform = os.platform()
- if (platform === 'win32') {
- if (refreshPath) {
- // 使用刷新后的 PATH 执行 where 命令
- const newPath = await getRefreshedWindowsPath()
- await execa('where', [command], { env: { ...process.env, PATH: newPath } })
- } else {
- await execa('where', [command])
- }
- } else {
- await execa('which', [command])
- }
- return true
- } catch {
- return false
- }
- }
- /**
- * 使用刷新后的 PATH 检测命令是否存在
- * Windows: 直接从注册表读取最新的 PATH 来检测
- * 其他平台: 使用当前 PATH
- */
- export async function commandExistsWithRefresh(command: string): Promise<boolean> {
- const platform = os.platform()
- if (platform === 'win32') {
- // Windows 上直接使用刷新后的 PATH 检测
- return await commandExists(command, true)
- }
- return await commandExists(command, false)
- }
- /**
- * 从 Windows 注册表获取 VS Code 安装路径
- * VS Code 安装后会在多个注册表位置写入信息
- * 使用 reg query 命令直接查询,比 PowerShell 更快更可靠
- * @returns VS Code 的 code.cmd 路径,如果找不到则返回 null
- */
- async function getVscodePathFromRegistry(): Promise<string | null> {
- if (os.platform() !== 'win32') return null
- // VS Code 可能的注册表位置(使用固定的 GUID 键,与 ipc-handlers.ts 保持一致)
- const registryPaths = [
- // 系统安装 (64位)
- { key: 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1', value: 'InstallLocation' },
- // 系统安装 (32位 on 64位系统)
- { key: 'HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1', value: 'InstallLocation' },
- // 用户安装
- { key: 'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{771FD6B0-FA20-440A-A002-3B3BAC16DC50}_is1', value: 'InstallLocation' },
- // VS Code Insiders 系统安装
- { key: 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{1287CAD5-7C8D-410D-88B9-0D1EE4A83FF2}_is1', value: 'InstallLocation' },
- // VS Code Insiders 用户安装
- { key: 'HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{217B4C08-948D-4276-BFBB-BEE930AE5A2C}_is1', value: 'InstallLocation' },
- ]
- for (const reg of registryPaths) {
- try {
- const result = await execa('reg', ['query', reg.key, '/v', reg.value], {
- encoding: 'utf8',
- timeout: 5000
- })
- // 解析注册表输出,格式如: " InstallLocation REG_SZ C:\Program Files\Microsoft VS Code\"
- const match = result.stdout.match(/InstallLocation\s+REG_SZ\s+(.+)/i)
- if (match) {
- const installLocation = match[1].trim()
- // 构建 code.cmd 的完整路径
- const codeCmd = path.join(installLocation, 'bin', 'code.cmd')
- if (fs.existsSync(codeCmd)) {
- return codeCmd
- }
- // 也检查 code.exe (某些版本可能直接使用 exe)
- const codeExe = path.join(installLocation, 'bin', 'code.exe')
- if (fs.existsSync(codeExe)) {
- return codeExe
- }
- }
- } catch {
- // 该注册表项不存在,继续尝试下一个
- }
- }
- return null
- }
- /**
- * 获取命令的完整路径
- * Windows: 使用刷新后的 PATH 执行 where 命令
- * macOS/Linux: 使用 which 命令
- * 注意:对于 VS Code,请使用专门的 getVscodeCliPath 函数
- * @param command 命令名称
- * @returns 命令的完整路径,如果找不到则返回 null
- */
- export async function getCommandPath(command: string): Promise<string | null> {
- try {
- const platform = os.platform()
- if (platform === 'win32') {
- // 使用刷新后的 PATH 执行 where 命令
- const newPath = await getRefreshedWindowsPath()
- const result = await execa('where', [command], { env: { ...process.env, PATH: newPath } })
- // where 可能返回多行(多个路径)
- // 优先选择 .cmd 或 .exe 文件,因为这些是 Windows 上可直接执行的
- const paths = result.stdout.trim().split(/\r?\n/)
- const cmdPath = paths.find(p => p.toLowerCase().endsWith('.cmd'))
- const exePath = paths.find(p => p.toLowerCase().endsWith('.exe'))
- // 优先返回 .cmd,其次 .exe,最后返回第一个结果
- return cmdPath || exePath || paths[0] || null
- } else {
- const result = await execa('which', [command])
- // which 通常只返回一个路径
- return result.stdout.trim() || null
- }
- } catch {
- return null
- }
- }
- /**
- * 获取 VS Code CLI (code) 命令的路径
- * 优先级:PATH 中的 code 命令 > 注册表路径 > 常见安装路径
- * @returns VS Code CLI 的完整路径,如果都找不到则返回 'code'
- */
- export async function getVscodeCliPath(): Promise<string> {
- const platform = os.platform()
- // 首先检查 code 命令是否在 PATH 中
- try {
- await execa('code', ['--version'], { timeout: 5000 })
- return 'code'
- } catch {
- // code 命令不在 PATH 中,尝试其他方法
- }
- if (platform === 'win32') {
- // Windows: 优先从注册表获取安装路径
- const registryPath = await getVscodePathFromRegistry()
- if (registryPath) {
- return registryPath
- }
- // 注册表找不到,尝试常见安装路径作为后备
- const fallbackPaths = [
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Microsoft VS Code', 'bin', 'code.cmd'),
- path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Microsoft VS Code', 'bin', 'code.cmd'),
- ]
- for (const codePath of fallbackPaths) {
- if (fs.existsSync(codePath)) {
- return codePath
- }
- }
- } else if (platform === 'darwin') {
- // macOS: 使用 mdfind 查找应用程序
- try {
- const result = await execa('mdfind', ['kMDItemCFBundleIdentifier == "com.microsoft.VSCode"'], {
- encoding: 'utf8',
- timeout: 5000
- })
- const appPath = result.stdout.trim().split('\n')[0]
- if (appPath) {
- const codePath = path.join(appPath, 'Contents', 'Resources', 'app', 'bin', 'code')
- if (fs.existsSync(codePath)) {
- return codePath
- }
- }
- } catch {
- // mdfind 失败,尝试常见路径
- }
- const fallbackPaths = [
- '/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
- path.join(os.homedir(), 'Applications', 'Visual Studio Code.app', 'Contents', 'Resources', 'app', 'bin', 'code'),
- ]
- for (const codePath of fallbackPaths) {
- if (fs.existsSync(codePath)) {
- return codePath
- }
- }
- } else {
- // Linux: 使用 which 或检查常见路径
- try {
- const result = await execa('which', ['code'], { encoding: 'utf8', timeout: 5000 })
- const codePath = result.stdout.trim()
- if (codePath && fs.existsSync(codePath)) {
- return codePath
- }
- } catch {
- // which 失败
- }
- const fallbackPaths = [
- '/usr/bin/code',
- '/usr/share/code/bin/code',
- '/snap/bin/code',
- ]
- for (const codePath of fallbackPaths) {
- if (fs.existsSync(codePath)) {
- return codePath
- }
- }
- }
- // 如果都找不到,返回 'code' 让系统尝试
- return 'code'
- }
- /**
- * 获取命令版本
- * @param command 命令名称
- * @param versionArgs 版本参数
- * @param refreshPath 是否使用刷新后的 PATH(仅 Windows)
- */
- export async function getCommandVersion(
- command: string,
- versionArgs: string[] = ['--version'],
- refreshPath = false
- ): Promise<string | null> {
- try {
- let result
- if (refreshPath && os.platform() === 'win32') {
- const newPath = await getRefreshedWindowsPath()
- result = await execa(command, versionArgs, { env: { ...process.env, PATH: newPath } })
- } else {
- result = await execa(command, versionArgs)
- }
- const output = result.stdout || result.stderr
- const match = output.match(/(\d+\.\d+\.\d+)/)
- return match ? match[1] : null
- } catch {
- return null
- }
- }
- /**
- * 使用刷新后的 PATH 获取命令版本
- * Windows: 直接从注册表读取最新的 PATH 来执行命令
- * 其他平台: 使用当前 PATH
- */
- export async function getCommandVersionWithRefresh(
- command: string,
- versionArgs: string[] = ['--version']
- ): Promise<string | null> {
- const platform = os.platform()
- if (platform === 'win32') {
- // Windows 上直接使用刷新后的 PATH
- return await getCommandVersion(command, versionArgs, true)
- }
- return await getCommandVersion(command, versionArgs, false)
- }
- /**
- * 设置版本缓存
- */
- export function setCache(key: string, data: unknown, ttl: number): void {
- versionCache.set(key, {
- data,
- expiry: Date.now() + ttl
- })
- }
- /**
- * 获取版本缓存
- */
- export function getCache<T = unknown>(key: string): T | null {
- const cached = versionCache.get(key)
- if (cached && cached.expiry > Date.now()) {
- return cached.data as T
- }
- versionCache.delete(key)
- return null
- }
- /**
- * 清除版本缓存
- */
- export function clearCache(key?: string): void {
- if (key) {
- versionCache.delete(key)
- } else {
- versionCache.clear()
- }
- }
- /**
- * 延迟函数
- */
- export function delay(ms: number): Promise<void> {
- return new Promise((resolve) => setTimeout(resolve, ms))
- }
- /**
- * 检测网络连接
- */
- export async function checkNetworkConnection(): Promise<boolean> {
- try {
- await httpsGet('https://www.baidu.com', { timeout: 5000, retries: 1 })
- return true
- } catch {
- try {
- await httpsGet('https://www.google.com', { timeout: 5000, retries: 1 })
- return true
- } catch {
- return false
- }
- }
- }
- /**
- * 下载进度回调类型
- */
- export type DownloadProgressCallback = (downloaded: number, total: number, percent: number) => void
- /**
- * 下载任务信息
- */
- interface DownloadTask {
- cancelSource: CancelTokenSource
- tempPath: string
- destPath: string
- }
- // 活跃的下载任务(支持多个并行下载)
- const activeDownloads = new Map<string, DownloadTask>()
- /**
- * 取消指定下载并删除临时文件
- * @param downloadId 下载ID(通常是目标文件路径)
- * @returns 是否成功取消
- */
- export function cancelDownload(downloadId: string): boolean {
- const task = activeDownloads.get(downloadId)
- if (task) {
- task.cancelSource.cancel('用户取消下载')
- // 删除临时文件
- fs.unlink(task.tempPath, () => {})
- activeDownloads.delete(downloadId)
- return true
- }
- return false
- }
- /**
- * 取消所有下载并删除临时文件
- * @returns 取消的下载数量
- */
- export function cancelAllDownloads(): number {
- let count = 0
- for (const [downloadId, task] of activeDownloads) {
- task.cancelSource.cancel('用户取消下载')
- fs.unlink(task.tempPath, () => {})
- activeDownloads.delete(downloadId)
- count++
- }
- return count
- }
- /**
- * 取消当前下载并删除临时文件(兼容旧接口)
- * @returns 是否有下载被取消
- */
- export function cancelCurrentDownload(): boolean {
- return cancelAllDownloads() > 0
- }
- /**
- * 获取已下载的文件大小
- */
- function getDownloadedSize(tempPath: string): number {
- try {
- if (fs.existsSync(tempPath)) {
- return fs.statSync(tempPath).size
- }
- } catch {
- // 忽略错误
- }
- return 0
- }
- /**
- * 下载文件到指定路径(支持断点续传和多文件并行下载)
- * @param url 下载地址
- * @param destPath 目标文件路径
- * @param onProgress 进度回调
- * @param options 下载选项
- */
- export async function downloadFile(
- url: string,
- destPath: string,
- onProgress?: DownloadProgressCallback,
- options: {
- timeout?: number
- } = {}
- ): Promise<string> {
- const { timeout = 60000 } = options
- // 临时文件路径(用于断点续传)
- const tempPath = destPath + '.downloading'
- // 创建取消令牌
- const cancelSource = axios.CancelToken.source()
- // 注册下载任务
- const downloadId = destPath
- activeDownloads.set(downloadId, {
- cancelSource,
- tempPath,
- destPath
- })
- try {
- // 获取已下载的文件大小
- const downloadedSize = getDownloadedSize(tempPath)
- // 构建请求头,支持断点续传
- const headers: Record<string, string> = {}
- if (downloadedSize > 0) {
- headers['Range'] = `bytes=${downloadedSize}-`
- console.log(`断点续传: 从 ${(downloadedSize / 1024 / 1024).toFixed(1)}MB 处继续下载`)
- }
- // 发起请求
- const response = await axiosInstance.get(url, {
- headers,
- timeout,
- responseType: 'stream',
- cancelToken: cancelSource.token,
- maxRedirects: 5,
- // 不验证状态码,手动处理
- validateStatus: () => true
- })
- // 处理 HTTP 错误状态码
- if (response.status >= 400) {
- // 416 表示 Range 请求无效(可能文件已完成或服务器不支持)
- if (response.status === 416 && downloadedSize > 0) {
- console.log('文件可能已下载完成,尝试验证...')
- try {
- if (fs.existsSync(destPath)) {
- fs.unlinkSync(destPath)
- }
- fs.renameSync(tempPath, destPath)
- activeDownloads.delete(downloadId)
- return destPath
- } catch {
- // 如果重命名失败,删除临时文件重新下载
- fs.unlinkSync(tempPath)
- }
- }
- throw new Error(`HTTP ${response.status}: 下载失败`)
- }
- // 206 表示部分内容(断点续传成功)
- const isPartialContent = response.status === 206
- // 计算总大小
- let totalSize = 0
- if (isPartialContent) {
- // 从 Content-Range 头获取总大小: bytes 0-999/1000
- const contentRange = response.headers['content-range']
- if (contentRange) {
- const match = contentRange.match(/\/(\d+)$/)
- if (match) {
- totalSize = parseInt(match[1], 10)
- }
- }
- } else {
- // 新下载,获取 Content-Length
- totalSize = parseInt(response.headers['content-length'] || '0', 10)
- // 如果是新下载但存在临时文件,说明服务器不支持断点续传,删除重新下载
- if (downloadedSize > 0) {
- console.log('服务器不支持断点续传,重新下载')
- fs.unlinkSync(tempPath)
- }
- }
- // 确保目标目录存在
- const dir = path.dirname(destPath)
- if (!fs.existsSync(dir)) {
- fs.mkdirSync(dir, { recursive: true })
- }
- // 以追加模式打开文件(断点续传)或创建新文件
- const fileStream = fs.createWriteStream(tempPath, {
- flags: isPartialContent ? 'a' : 'w'
- })
- let currentDownloaded = isPartialContent ? downloadedSize : 0
- // 返回 Promise
- return new Promise<string>((resolve, reject) => {
- response.data.on('data', (chunk: Buffer) => {
- currentDownloaded += chunk.length
- if (onProgress && totalSize > 0) {
- const percent = Math.round((currentDownloaded / totalSize) * 100)
- onProgress(currentDownloaded, totalSize, percent)
- }
- })
- response.data.on('error', (err: Error) => {
- fileStream.close()
- activeDownloads.delete(downloadId)
- reject(new Error(ERROR_MESSAGES.NETWORK_ERROR + ': ' + err.message))
- })
- response.data.pipe(fileStream)
- fileStream.on('finish', () => {
- fileStream.close()
- // 验证文件大小
- const finalSize = getDownloadedSize(tempPath)
- if (totalSize > 0 && finalSize < totalSize) {
- // 文件不完整,保留临时文件以便下次续传
- console.log(`下载不完整: ${finalSize}/${totalSize} 字节,可点击重新安装继续下载`)
- activeDownloads.delete(downloadId)
- reject(new Error('下载不完整,请重试'))
- return
- }
- // 重命名临时文件为最终文件
- try {
- if (fs.existsSync(destPath)) {
- fs.unlinkSync(destPath)
- }
- fs.renameSync(tempPath, destPath)
- activeDownloads.delete(downloadId)
- resolve(destPath)
- } catch (err) {
- activeDownloads.delete(downloadId)
- reject(err)
- }
- })
- fileStream.on('error', (err) => {
- // 保留临时文件以便续传
- activeDownloads.delete(downloadId)
- reject(err)
- })
- })
- } catch (error) {
- activeDownloads.delete(downloadId)
- if (axios.isCancel(error)) {
- // 用户取消,删除临时文件
- fs.unlink(tempPath, () => {})
- throw new Error(ERROR_MESSAGES.INSTALL_CANCELLED || '下载已取消')
- }
- const axiosError = error as AxiosError
- if (axiosError.code === 'ECONNABORTED' || axiosError.message?.includes('timeout')) {
- // 超时,保留临时文件以便续传
- throw new Error(ERROR_MESSAGES.TIMEOUT_ERROR)
- }
- // 其他错误,保留临时文件以便续传
- throw error
- }
- }
- /**
- * 获取临时目录路径
- */
- export function getTempDir(): string {
- return os.tmpdir()
- }
- /**
- * 检查包管理器源是否需要更新
- * @param manager 包管理器名称 (apt, brew)
- * @returns 是否需要更新
- */
- export function needsSourceUpdate(manager: string): boolean {
- const lastUpdate = sourceUpdateCache.get(manager)
- if (!lastUpdate) {
- return true
- }
- return Date.now() - lastUpdate > SOURCE_UPDATE_CACHE_TTL
- }
- /**
- * 标记包管理器源已更新
- * @param manager 包管理器名称
- */
- export function markSourceUpdated(manager: string): void {
- sourceUpdateCache.set(manager, Date.now())
- }
- /**
- * 清除源更新缓存
- * @param manager 可选,指定清除某个包管理器的缓存,不传则清除所有
- */
- export function clearSourceUpdateCache(manager?: string): void {
- if (manager) {
- sourceUpdateCache.delete(manager)
- } else {
- sourceUpdateCache.clear()
- }
- }
|