From f1756c4d1cff3f1bc09ef9eae84dd33e8e657067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Sun, 4 Jan 2026 12:41:21 +0800 Subject: [PATCH] Optimize version fetching and update logic Introduces lazy loading for release and action artifact versions, adds support for nightly.link mirrors, and improves artifact retrieval reliability. Removes unused loginService references, refactors update logic to handle action artifacts, and streamlines frontend/backend API parameters for version selection. --- packages/napcat-common/src/mirror.ts | 377 +++++++++++++++--- packages/napcat-core/index.ts | 2 - packages/napcat-framework/napcat.ts | 4 +- packages/napcat-shell/base.ts | 3 - .../napcat-webui-backend/src/api/BaseInfo.ts | 68 ++-- .../src/api/UpdateNapCat.ts | 92 +++-- .../src/components/system_info.tsx | 2 +- .../src/controllers/webui_manager.ts | 6 +- 8 files changed, 420 insertions(+), 134 deletions(-) diff --git a/packages/napcat-common/src/mirror.ts b/packages/napcat-common/src/mirror.ts index fde9f6ab..45accaaf 100644 --- a/packages/napcat-common/src/mirror.ts +++ b/packages/napcat-common/src/mirror.ts @@ -114,6 +114,16 @@ export const GITHUB_RAW_MIRRORS = [ 'https://gh-proxy.net/https://raw.githubusercontent.com', ]; +/** + * Nightly.link 镜像 + * 用于访问 GitHub Actions artifacts + * 优先使用官方服务,出现问题时可切换镜像 + */ +export const NIGHTLY_LINK_MIRRORS = [ + 'https://nightly.link', + // 可以添加其他 nightly.link 镜像(如果有的话) +]; + // ============== 镜像配置接口 ============== export interface MirrorConfig { @@ -123,6 +133,8 @@ export interface MirrorConfig { apiMirrors: string[]; /** Raw 文件镜像 */ rawMirrors: string[]; + /** Nightly.link 镜像(用于 Actions artifacts) */ + nightlyLinkMirrors: string[]; /** 超时时间(毫秒) */ timeout: number; /** 是否启用镜像 */ @@ -137,6 +149,7 @@ const defaultConfig: MirrorConfig = { fileMirrors: GITHUB_FILE_MIRRORS, apiMirrors: GITHUB_API_MIRRORS, rawMirrors: GITHUB_RAW_MIRRORS, + nightlyLinkMirrors: NIGHTLY_LINK_MIRRORS, timeout: 10000, // 10秒超时,平衡速度和可靠性 enabled: true, customMirror: undefined, @@ -530,7 +543,11 @@ export async function findAvailableDownloadUrl ( // 获取镜像列表 let mirrors = options.mirrors; if (!mirrors) { - if (useFastMirrors) { + // 检查是否是 nightly.link URL + if (originalUrl.includes('nightly.link')) { + // 使用 nightly.link 镜像列表(保持完整的 URL 格式) + mirrors = currentConfig.nightlyLinkMirrors; + } else if (useFastMirrors) { // 使用懒加载的快速镜像列表 mirrors = await getFastMirrors(); } else { @@ -564,11 +581,20 @@ export async function findAvailableDownloadUrl ( return originalUrl; } - // 3. 测试镜像源(已按延迟排序) + // 3. 测试镜像源 let testedCount = 0; for (const mirror of mirrors) { if (!mirror) continue; // 跳过空字符串 - const mirrorUrl = buildMirrorUrl(originalUrl, mirror); + + // 特殊处理 nightly.link URL + let mirrorUrl: string; + if (originalUrl.includes('nightly.link')) { + // 替换 nightly.link 域名 + mirrorUrl = originalUrl.replace('https://nightly.link', mirror.startsWith('http') ? mirror : `https://${mirror}`); + } else { + mirrorUrl = buildMirrorUrl(originalUrl, mirror); + } + testedCount++; if (await testWithValidation(mirrorUrl)) { return mirrorUrl; @@ -748,13 +774,13 @@ interface TagsCache { timestamp: number; } -// 缓存 tags 结果(5 分钟有效) -const TAGS_CACHE_TTL = 5 * 60 * 1000; +// 缓存 tags 结果(10 分钟有效,release 版本不会频繁变动) +const TAGS_CACHE_TTL = 10 * 60 * 1000; const tagsCache: Map = new Map(); /** * 获取所有 GitHub tags(带缓存) - * 使用懒加载的快速镜像列表,按测速延迟排序依次尝试 + * 优化:并行请求多个镜像,使用第一个成功返回的结果 */ export async function getAllGitHubTags (owner: string, repo: string): Promise<{ tags: string[], mirror: string; }> { const cacheKey = `${owner}/${repo}`; @@ -779,7 +805,7 @@ export async function getAllGitHubTags (owner: string, repo: string): Promise<{ }; // 尝试从 URL 获取 tags - const fetchFromUrl = async (url: string): Promise => { + const fetchFromUrl = async (url: string, mirror: string): Promise<{ tags: string[], mirror: string; } | null> => { try { const raw = await PromiseTimer( RequestUtil.HttpGetText(url), @@ -793,7 +819,7 @@ export async function getAllGitHubTags (owner: string, repo: string): Promise<{ const tags = parseTags(raw); if (tags.length > 0) { - return tags; + return { tags, mirror }; } return null; } catch { @@ -801,40 +827,57 @@ export async function getAllGitHubTags (owner: string, repo: string): Promise<{ } }; - // 获取快速镜像列表(懒加载,首次调用会测速,已按延迟排序) + // 获取快速镜像列表 let fastMirrors: string[] = []; try { fastMirrors = await getFastMirrors(); - } catch (e) { - // 忽略错误,继续使用空列表 + } catch { + // 忽略错误 } - // 构建 URL 列表(快速镜像 + 原始 URL) - const mirrorUrls = fastMirrors.filter(m => m).map(m => ({ url: buildMirrorUrl(baseUrl, m), mirror: m })); - mirrorUrls.push({ url: baseUrl, mirror: 'github.com' }); // 添加原始 URL + // 构建 URL 列表(取前 5 个快速镜像 + 原始 URL 并行请求) + const topMirrors = fastMirrors.slice(0, 5); + const mirrorUrls = [ + { url: baseUrl, mirror: 'github.com' }, // 原始 URL + ...topMirrors.filter(m => m).map(m => ({ url: buildMirrorUrl(baseUrl, m), mirror: m })), + ]; - // 按顺序尝试每个镜像(已按延迟排序),成功即返回 - for (const { url, mirror } of mirrorUrls) { - const tags = await fetchFromUrl(url); - if (tags && tags.length > 0) { - // 缓存结果 - tagsCache.set(cacheKey, { tags, mirror, timestamp: Date.now() }); - return { tags, mirror }; + // 并行请求所有镜像,使用 Promise.any 获取第一个成功的结果 + try { + const result = await Promise.any( + mirrorUrls.map(async ({ url, mirror }) => { + const res = await fetchFromUrl(url, mirror); + if (res) return res; + throw new Error('Failed'); + }) + ); + + // 缓存结果 + tagsCache.set(cacheKey, { tags: result.tags, mirror: result.mirror, timestamp: Date.now() }); + return result; + } catch { + // Promise.any 全部失败,回退到顺序尝试剩余镜像 + } + + // 回退:顺序尝试剩余镜像 + const remainingMirrors = fastMirrors.slice(5).filter(m => m); + for (const mirror of remainingMirrors) { + const url = buildMirrorUrl(baseUrl, mirror); + const result = await fetchFromUrl(url, mirror); + if (result) { + tagsCache.set(cacheKey, { tags: result.tags, mirror: result.mirror, timestamp: Date.now() }); + return result; } } - // 如果快速镜像都失败,回退到原始镜像列表 - const allMirrors = currentConfig.fileMirrors.filter(m => m); + // 最后尝试所有镜像 + const allMirrors = currentConfig.fileMirrors.filter(m => m && !fastMirrors.includes(m)); for (const mirror of allMirrors) { - // 跳过已经尝试过的镜像 - if (fastMirrors.includes(mirror)) continue; - const url = buildMirrorUrl(baseUrl, mirror); - const tags = await fetchFromUrl(url); - if (tags && tags.length > 0) { - // 缓存结果 - tagsCache.set(cacheKey, { tags, mirror, timestamp: Date.now() }); - return { tags, mirror }; + const result = await fetchFromUrl(url, mirror); + if (result) { + tagsCache.set(cacheKey, { tags: result.tags, mirror: result.mirror, timestamp: Date.now() }); + return result; } } @@ -854,11 +897,168 @@ export interface ActionArtifact { head_sha?: string; } +// ============== Action Artifacts 缓存 ============== + +interface ArtifactsCache { + artifacts: ActionArtifact[]; + timestamp: number; +} + +// 缓存 artifacts 结果(10 分钟有效) +const ARTIFACTS_CACHE_TTL = 10 * 60 * 1000; +const artifactsCache: Map = new Map(); + /** - * 获取 GitHub Action 最新运行的 artifacts - * 用于下载 nightly/dev 版本 + * 清除 artifacts 缓存 */ -export async function getLatestActionArtifacts ( +export function clearArtifactsCache (): void { + artifactsCache.clear(); +} + +/** + * 通过解析 GitHub Actions HTML 页面获取 workflow runs(备用方案) + * 当 api.github.com 不可用时使用 + * 页面格式: https://github.com/{owner}/{repo}/actions/workflows/{workflow} + */ +async function getWorkflowRunsFromHtml ( + owner: string, + repo: string, + workflow: string = 'build.yml', + maxRuns: number = 10 +): Promise> { + const baseUrl = `https://github.com/${owner}/${repo}/actions/workflows/${workflow}`; + + // 尝试使用镜像获取 HTML + const mirrors = ['', ...currentConfig.fileMirrors.filter(m => m)]; + + for (const mirror of mirrors) { + try { + const url = mirror ? buildMirrorUrl(baseUrl, mirror) : baseUrl; + + const html = await PromiseTimer( + RequestUtil.HttpGetText(url), + 10000 + ); + + // 从 HTML 中提取 run IDs 和时间 + // 格式: href="/NapNeko/NapCatQQ/actions/runs/20676123968" + // 时间格式: = []; + const foundIds = new Set(); + let timeIndex = 0; + + let match; + while ((match = runPattern.exec(html)) !== null && runs.length < maxRuns) { + const id = parseInt(match[1]); + if (!foundIds.has(id)) { + foundIds.add(id); + // 尝试获取对应的时间,每个 run 通常有两个时间(桌面和移动端显示) + // 所以每找到一个 run,跳过两个时间 + const created_at = times[timeIndex] || new Date().toISOString(); + timeIndex += 2; // 跳过两个时间(桌面端和移动端各一个) + runs.push({ + id, + created_at, + }); + } + } + + if (runs.length > 0) { + return runs; + } + } catch { + continue; + } + } + + return []; +} + +/** + * 通过 API 获取最新的 workflow runs,然后直接拼接 nightly.link 下载链接 + * 无需解析 HTML,直接使用固定的 URL 格式 + * + * 策略: + * 1. 优先使用 GitHub API + * 2. API 失败时,从 GitHub Actions HTML 页面解析 + */ +async function getArtifactsFromNightlyLink ( + owner: string, + repo: string, + workflow: string = 'build.yml', + branch: string = 'main', + maxRuns: number = 10 +): Promise { + let workflowRuns: Array<{ id: number; head_sha?: string; created_at: string; }> = []; + + // 策略1: 优先尝试 GitHub API + try { + const endpoint = `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflow}/runs?branch=${branch}&status=success&per_page=${maxRuns}`; + + const runsResponse = await PromiseTimer( + RequestUtil.HttpGetJson<{ + workflow_runs: Array<{ id: number; head_sha: string; created_at: string; }>; + }>(endpoint, 'GET', undefined, { + 'User-Agent': 'NapCat', + 'Accept': 'application/vnd.github.v3+json', + }), + 10000 + ); + + if (runsResponse.workflow_runs && runsResponse.workflow_runs.length > 0) { + workflowRuns = runsResponse.workflow_runs; + } + } catch { + // API 请求失败,继续尝试 HTML 解析 + } + + // 策略2: API 失败时,从 HTML 页面解析 + if (workflowRuns.length === 0) { + workflowRuns = await getWorkflowRunsFromHtml(owner, repo, workflow, maxRuns); + } + + if (workflowRuns.length === 0) { + return []; + } + + // 直接拼接 nightly.link URL + // 格式: https://nightly.link/{owner}/{repo}/actions/runs/{run_id}/{artifact_name}.zip + const artifacts: ActionArtifact[] = []; + const artifactNames = ['NapCat.Framework', 'NapCat.Shell']; // 已知的 artifact 名称 + + for (const run of workflowRuns) { + for (const artifactName of artifactNames) { + const mirror = currentConfig.nightlyLinkMirrors[0] || 'https://nightly.link'; + artifacts.push({ + id: run.id, + name: artifactName, + size_in_bytes: 0, + created_at: run.created_at, + expires_at: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(), + archive_download_url: `${mirror}/${owner}/${repo}/actions/runs/${run.id}/${artifactName}.zip`, + workflow_run_id: run.id, + head_sha: run.head_sha, + }); + } + } + + return artifacts; +} + +/** + * 通过 GitHub API 获取 artifacts(主要方案) + */ +async function getArtifactsFromAPI ( owner: string, repo: string, workflow: string = 'build.yml', @@ -867,47 +1067,102 @@ export async function getLatestActionArtifacts ( ): Promise { const endpoint = `https://api.github.com/repos/${owner}/${repo}/actions/workflows/${workflow}/runs?branch=${branch}&status=success&per_page=${maxRuns}`; - try { - const runsResponse = await RequestUtil.HttpGetJson<{ + const runsResponse = await PromiseTimer( + RequestUtil.HttpGetJson<{ workflow_runs: Array<{ id: number; head_sha: string; created_at: string; }>; }>(endpoint, 'GET', undefined, { 'User-Agent': 'NapCat', 'Accept': 'application/vnd.github.v3+json', - }); + }), + 10000 + ); - const workflowRuns = runsResponse.workflow_runs; - if (!workflowRuns || workflowRuns.length === 0) { - throw new Error('No successful workflow runs found'); - } + const workflowRuns = runsResponse.workflow_runs; + if (!workflowRuns || workflowRuns.length === 0) { + throw new Error('No successful workflow runs found'); + } - // 获取所有 runs 的 artifacts - const allArtifacts: ActionArtifact[] = []; + // 获取所有 runs 的 artifacts + const allArtifacts: ActionArtifact[] = []; - for (const run of workflowRuns) { - try { - const artifactsEndpoint = `https://api.github.com/repos/${owner}/${repo}/actions/runs/${run.id}/artifacts`; - const artifactsResponse = await RequestUtil.HttpGetJson<{ + for (const run of workflowRuns) { + try { + const artifactsEndpoint = `https://api.github.com/repos/${owner}/${repo}/actions/runs/${run.id}/artifacts`; + const artifactsResponse = await PromiseTimer( + RequestUtil.HttpGetJson<{ artifacts: ActionArtifact[]; }>(artifactsEndpoint, 'GET', undefined, { 'User-Agent': 'NapCat', 'Accept': 'application/vnd.github.v3+json', - }); + }), + 10000 + ); - if (artifactsResponse.artifacts) { - // 为每个 artifact 添加 run 信息 - for (const artifact of artifactsResponse.artifacts) { - artifact.workflow_run_id = run.id; - artifact.head_sha = run.head_sha; - allArtifacts.push(artifact); - } + if (artifactsResponse.artifacts) { + // 为每个 artifact 添加 run 信息 + for (const artifact of artifactsResponse.artifacts) { + artifact.workflow_run_id = run.id; + artifact.head_sha = run.head_sha; + allArtifacts.push(artifact); } - } catch { - // 单个 run 获取失败,继续下一个 } + } catch { + // 单个 run 获取失败,继续下一个 } - - return allArtifacts; - } catch { - return []; } + + return allArtifacts; +} + +/** + * 获取 GitHub Action 最新运行的 artifacts + * 用于下载 nightly/dev 版本 + * + * 策略: + * 1. 检查缓存(10分钟有效) + * 2. 优先尝试从 nightly.link 获取(无需认证,更稳定) + * 3. 如果失败,回退到 GitHub API + */ +export async function getLatestActionArtifacts ( + owner: string, + repo: string, + workflow: string = 'build.yml', + branch: string = 'main', + maxRuns: number = 10 +): Promise { + const cacheKey = `${owner}/${repo}/${workflow}/${branch}`; + + // 检查缓存 + const cached = artifactsCache.get(cacheKey); + if (cached && (Date.now() - cached.timestamp) < ARTIFACTS_CACHE_TTL) { + return cached.artifacts; + } + + let artifacts: ActionArtifact[] = []; + + // 策略1: 优先使用 nightly.link(更稳定,无需认证) + try { + artifacts = await getArtifactsFromNightlyLink(owner, repo, workflow, branch, maxRuns); + } catch { + // nightly.link 获取失败 + } + + // 策略2: 回退到 GitHub API + if (artifacts.length === 0) { + try { + artifacts = await getArtifactsFromAPI(owner, repo, workflow, branch, maxRuns); + } catch { + // API 获取失败 + } + } + + // 缓存结果(即使为空也缓存,避免频繁请求) + if (artifacts.length > 0) { + artifactsCache.set(cacheKey, { + artifacts, + timestamp: Date.now(), + }); + } + + return artifacts; } diff --git a/packages/napcat-core/index.ts b/packages/napcat-core/index.ts index 765491fc..37817a2e 100644 --- a/packages/napcat-core/index.ts +++ b/packages/napcat-core/index.ts @@ -17,7 +17,6 @@ import { WrapperSessionInitConfig, } from '@/napcat-core/wrapper'; import { LogLevel, LogWrapper } from '@/napcat-core/helper/log'; -import { NodeIKernelLoginService } from '@/napcat-core/services'; import { QQBasicInfoWrapper } from '@/napcat-core/helper/qq-basic-info'; import { NapCatPathWrapper } from 'napcat-common/src/path'; import path from 'node:path'; @@ -278,7 +277,6 @@ export interface InstanceContext { readonly wrapper: WrapperNodeApi; readonly session: NodeIQQNTWrapperSession; readonly logger: LogWrapper; - readonly loginService: NodeIKernelLoginService; readonly basicInfoWrapper: QQBasicInfoWrapper; readonly pathWrapper: NapCatPathWrapper; readonly packetHandler: NativePacketHandler; diff --git a/packages/napcat-framework/napcat.ts b/packages/napcat-framework/napcat.ts index 23d3648d..35123b21 100644 --- a/packages/napcat-framework/napcat.ts +++ b/packages/napcat-framework/napcat.ts @@ -73,7 +73,7 @@ export async function NCoreInitFramework ( // 过早进入会导致addKernelMsgListener等Listener添加失败 // await sleep(2500); // 初始化 NapCatFramework - const loaderObject = new NapCatFramework(wrapper, session, logger, loginService, selfInfo, basicInfoWrapper, pathWrapper, nativePacketHandler); + const loaderObject = new NapCatFramework(wrapper, session, logger, selfInfo, basicInfoWrapper, pathWrapper, nativePacketHandler); await loaderObject.core.initCore(); // 启动WebUi @@ -94,7 +94,6 @@ export class NapCatFramework { wrapper: WrapperNodeApi, session: NodeIQQNTWrapperSession, logger: LogWrapper, - loginService: NodeIKernelLoginService, selfInfo: SelfInfo, basicInfoWrapper: QQBasicInfoWrapper, pathWrapper: NapCatPathWrapper, @@ -106,7 +105,6 @@ export class NapCatFramework { wrapper, session, logger, - loginService, basicInfoWrapper, pathWrapper, }; diff --git a/packages/napcat-shell/base.ts b/packages/napcat-shell/base.ts index a8dbac21..19110f1d 100644 --- a/packages/napcat-shell/base.ts +++ b/packages/napcat-shell/base.ts @@ -418,7 +418,6 @@ export async function NCoreInitShell () { wrapper, session, logger, - loginService, selfInfo, basicInfoWrapper, pathWrapper, @@ -434,7 +433,6 @@ export class NapCatShell { wrapper: WrapperNodeApi, session: NodeIQQNTWrapperSession, logger: LogWrapper, - loginService: NodeIKernelLoginService, selfInfo: SelfInfo, basicInfoWrapper: QQBasicInfoWrapper, pathWrapper: NapCatPathWrapper, @@ -446,7 +444,6 @@ export class NapCatShell { wrapper, session, logger, - loginService, basicInfoWrapper, pathWrapper, }; diff --git a/packages/napcat-webui-backend/src/api/BaseInfo.ts b/packages/napcat-webui-backend/src/api/BaseInfo.ts index 2aae010e..123889c5 100644 --- a/packages/napcat-webui-backend/src/api/BaseInfo.ts +++ b/packages/napcat-webui-backend/src/api/BaseInfo.ts @@ -39,43 +39,47 @@ export interface VersionInfo { /** * 获取所有可用的版本(release + action artifacts) - * 支持分页 + * 支持分页,懒加载:根据 type 参数只获取需要的版本类型 */ export const getAllReleasesHandler: RequestHandler = async (req, res) => { try { const page = parseInt(req.query['page'] as string) || 1; const pageSize = parseInt(req.query['pageSize'] as string) || 20; - const includeActions = req.query['includeActions'] !== 'false'; const typeFilter = req.query['type'] as string | undefined; // 'release' | 'action' | 'all' const searchQuery = (req.query['search'] as string || '').toLowerCase().trim(); - let tags: string[] = []; + let versions: VersionInfo[] = []; + let actionVersions: VersionInfo[] = []; let usedMirror = ''; - try { - const result = await getAllTags(); - tags = result.tags; - usedMirror = result.mirror; - } catch { - // 如果获取 tags 失败,返回空列表而不是抛出错误 - tags = []; + + // 懒加载:只获取需要的版本类型 + const needReleases = !typeFilter || typeFilter === 'all' || typeFilter === 'release'; + const needActions = typeFilter === 'action' || typeFilter === 'all'; + + // 获取正式版本(仅当需要时) + if (needReleases) { + try { + const result = await getAllTags(); + usedMirror = result.mirror; + + versions = result.tags.map(tag => { + const isPrerelease = /-(alpha|beta|rc|dev|pre|snapshot)/i.test(tag); + return { + tag, + type: isPrerelease ? 'prerelease' : 'release', + } as VersionInfo; + }); + + // 使用语义化版本排序(最新的在前) + versions.sort((a, b) => -compareSemVer(a.tag, b.tag)); + } catch { + // 如果获取 tags 失败,返回空列表而不是抛出错误 + versions = []; + } } - // 解析版本信息 - const versions: VersionInfo[] = tags.map(tag => { - // 检查是否是预发布版本 - const isPrerelease = /-(alpha|beta|rc|dev|pre|snapshot)/i.test(tag); - return { - tag, - type: isPrerelease ? 'prerelease' : 'release', - }; - }); - - // 使用语义化版本排序(最新的在前) - versions.sort((a, b) => -compareSemVer(a.tag, b.tag)); - - // 获取 Action Artifacts(如果请求) - let actionVersions: VersionInfo[] = []; - if (includeActions) { + // 获取 Action Artifacts(仅当需要时) + if (needActions) { try { const artifacts = await getLatestActionArtifacts('NapNeko', 'NapCatQQ', 'build.yml', 'main'); @@ -97,22 +101,14 @@ export const getAllReleasesHandler: RequestHandler = async (req, res) => { headSha: a.head_sha, })); } catch { - // 忽略 action artifacts 获取失败 + // 获取失败时返回空列表 + actionVersions = []; } } // 合并版本列表(action 在最前面) let allVersions = [...actionVersions, ...versions]; - // 按类型过滤 - if (typeFilter && typeFilter !== 'all') { - if (typeFilter === 'release') { - allVersions = allVersions.filter(v => v.type === 'release' || v.type === 'prerelease'); - } else if (typeFilter === 'action') { - allVersions = allVersions.filter(v => v.type === 'action'); - } - } - // 搜索过滤 if (searchQuery) { allVersions = allVersions.filter(v => { diff --git a/packages/napcat-webui-backend/src/api/UpdateNapCat.ts b/packages/napcat-webui-backend/src/api/UpdateNapCat.ts index 37a4211a..83fa4f80 100644 --- a/packages/napcat-webui-backend/src/api/UpdateNapCat.ts +++ b/packages/napcat-webui-backend/src/api/UpdateNapCat.ts @@ -134,23 +134,73 @@ export const UpdateNapCatHandler: RequestHandler = async (req, res) => { const targetTag = targetVersion || 'latest'; webUiLogger?.log(`[NapCat Update] Target version: ${targetTag}`); - // 使用 mirror 模块获取 release 信息(不依赖 API) - // 通过 assetNames 参数直接构建下载 URL,避免调用 GitHub API - const release = await getGitHubRelease('NapNeko', 'NapCatQQ', targetTag, { - assetNames: [ReleaseName, 'NapCat.Framework.zip', 'NapCat.Shell.zip'], - fetchChangelog: false, // 不需要 changelog,避免 API 调用 - }); + // 检查是否是 action 临时版本 + const isActionVersion = targetTag.startsWith('action-'); + let downloadUrl: string; + let actualVersion: string; - const shellZipAsset = release.assets.find(asset => asset.name === ReleaseName); - if (!shellZipAsset) { - throw new Error(`未找到${ReleaseName}文件`); + if (isActionVersion) { + // 处理 action 临时版本 + const runId = parseInt(targetTag.replace('action-', '')); + if (isNaN(runId)) { + throw new Error(`Invalid action version format: ${targetTag}`); + } + + webUiLogger?.log(`[NapCat Update] Downloading action artifact from run: ${runId}`); + + // 根据当前工作环境确定 artifact 名称 + const artifactName = ReleaseName.replace('.zip', ''); // NapCat.Framework 或 NapCat.Shell + + // Action artifacts 通过 nightly.link 下载 + // 格式:https://nightly.link/{owner}/{repo}/actions/runs/{run_id}/{artifact_name}.zip + const baseUrl = `https://nightly.link/NapNeko/NapCatQQ/actions/runs/${runId}/${artifactName}.zip`; + actualVersion = targetTag; + + webUiLogger?.log(`[NapCat Update] Action artifact URL: ${baseUrl}`); + + // 使用 mirror 模块查找可用的 nightly.link 镜像 + try { + downloadUrl = await findAvailableDownloadUrl(baseUrl, { + validateContent: true, + minFileSize: 1024 * 1024, + timeout: 10000, + }); + webUiLogger?.log(`[NapCat Update] Using download URL: ${downloadUrl}`); + } catch (error) { + // 如果镜像都不可用,直接使用原始 URL + webUiLogger?.logWarn(`[NapCat Update] All nightly.link mirrors failed, using original URL`); + downloadUrl = baseUrl; + } + } else { + // 处理标准 release 版本 + // 使用 mirror 模块获取 release 信息(不依赖 API) + // 通过 assetNames 参数直接构建下载 URL,避免调用 GitHub API + const release = await getGitHubRelease('NapNeko', 'NapCatQQ', targetTag, { + assetNames: [ReleaseName, 'NapCat.Framework.zip', 'NapCat.Shell.zip'], + fetchChangelog: false, // 不需要 changelog,避免 API 调用 + }); + + const shellZipAsset = release.assets.find(asset => asset.name === ReleaseName); + if (!shellZipAsset) { + throw new Error(`未找到${ReleaseName}文件`); + } + + actualVersion = release.tag_name; + + // 使用 mirror 模块查找可用的下载 URL + // 启用内容验证,确保返回的是有效文件而非错误页面 + downloadUrl = await findAvailableDownloadUrl(shellZipAsset.browser_download_url, { + validateContent: true, // 验证 Content-Type 和状态码 + minFileSize: 1024 * 1024, // 最小 1MB,确保不是错误页面 + timeout: 10000, // 10秒超时 + }); } // 检查是否需要强制更新(降级警告) const currentVersion = WebUiDataRuntime.GetNapCatVersion(); - webUiLogger?.log(`[NapCat Update] Current version: ${currentVersion}, Target version: ${release.tag_name}`); + webUiLogger?.log(`[NapCat Update] Current version: ${currentVersion}, Target version: ${actualVersion}`); - if (!force && currentVersion) { + if (!force && currentVersion && !isActionVersion) { // 简单的版本比较(可选的降级保护) const parseVersion = (v: string): [number, number, number] => { const match = v.match(/^v?(\d+)\.(\d+)\.(\d+)/); @@ -158,7 +208,7 @@ export const UpdateNapCatHandler: RequestHandler = async (req, res) => { return [parseInt(match[1] || '0'), parseInt(match[2] || '0'), parseInt(match[3] || '0')]; }; const [currMajor, currMinor, currPatch] = parseVersion(currentVersion); - const [targetMajor, targetMinor, targetPatch] = parseVersion(release.tag_name); + const [targetMajor, targetMinor, targetPatch] = parseVersion(actualVersion); const isDowngrade = targetMajor < currMajor || @@ -166,12 +216,12 @@ export const UpdateNapCatHandler: RequestHandler = async (req, res) => { (targetMajor === currMajor && targetMinor === currMinor && targetPatch < currPatch); if (isDowngrade) { - webUiLogger?.log(`[NapCat Update] Downgrade from ${currentVersion} to ${release.tag_name}, force=${force}`); + webUiLogger?.log(`[NapCat Update] Downgrade from ${currentVersion} to ${actualVersion}, force=${force}`); // 不阻止降级,只是记录日志 } } - webUiLogger?.log(`[NapCat Update] Updating to version: ${release.tag_name}`); + webUiLogger?.log(`[NapCat Update] Updating to version: ${actualVersion}`); // 创建临时目录 const tempDir = path.join(webUiPathWrapper.binaryPath, './temp'); @@ -179,14 +229,6 @@ export const UpdateNapCatHandler: RequestHandler = async (req, res) => { fs.mkdirSync(tempDir, { recursive: true }); } - // 使用 mirror 模块查找可用的下载 URL - // 启用内容验证,确保返回的是有效文件而非错误页面 - const downloadUrl = await findAvailableDownloadUrl(shellZipAsset.browser_download_url, { - validateContent: true, // 验证 Content-Type 和状态码 - minFileSize: 1024 * 1024, // 最小 1MB,确保不是错误页面 - timeout: 10000, // 10秒超时 - }); - webUiLogger?.log(`[NapCat Update] Using download URL: ${downloadUrl}`); // 下载zip @@ -250,10 +292,10 @@ export const UpdateNapCatHandler: RequestHandler = async (req, res) => { // 如果有替换失败的文件,创建更新配置文件 if (failedFiles.length > 0) { const updateConfig: UpdateConfig = { - version: release.tag_name, + version: actualVersion, updateTime: new Date().toISOString(), files: failedFiles, - changelog: release.body || '' + changelog: '' }; // 保存更新配置文件 @@ -269,7 +311,7 @@ export const UpdateNapCatHandler: RequestHandler = async (req, res) => { sendSuccess(res, { status: 'completed', message, - newVersion: release.tag_name, + newVersion: actualVersion, failedFilesCount: failedFiles.length }); diff --git a/packages/napcat-webui-frontend/src/components/system_info.tsx b/packages/napcat-webui-frontend/src/components/system_info.tsx index 731a4184..653479d2 100644 --- a/packages/napcat-webui-frontend/src/components/system_info.tsx +++ b/packages/napcat-webui-frontend/src/components/system_info.tsx @@ -293,11 +293,11 @@ const VersionSelectDialogContent: React.FC = ({ const pageSize = 15; // 获取所有可用版本(带分页、过滤和搜索) + // 懒加载:根据 activeTab 只获取对应类型的版本 const { data: releasesData, loading: releasesLoading, error: releasesError } = useRequest( () => WebUIManager.getAllReleases({ page: currentPage, pageSize, - includeActions: true, type: activeTab, search: debouncedSearch }), diff --git a/packages/napcat-webui-frontend/src/controllers/webui_manager.ts b/packages/napcat-webui-frontend/src/controllers/webui_manager.ts index 5bb344c5..9e5098e1 100644 --- a/packages/napcat-webui-frontend/src/controllers/webui_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/webui_manager.ts @@ -65,15 +65,15 @@ export default class WebUIManager { /** * 获取所有可用的版本列表(支持分页、过滤和搜索) + * 懒加载:根据 type 参数只获取对应类型的版本 */ public static async getAllReleases (options: { page?: number; pageSize?: number; - includeActions?: boolean; type?: 'release' | 'action' | 'all'; search?: string; } = {}) { - const { page = 1, pageSize = 20, includeActions = true, type = 'all', search = '' } = options; + const { page = 1, pageSize = 20, type = 'release', search = '' } = options; const { data } = await serverRequest.get>('/base/getAllReleases', { - params: { page, pageSize, includeActions, type, search }, + params: { page, pageSize, type, search }, }); return data.data; }