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; }