diff --git a/packages/napcat-common/src/helper.ts b/packages/napcat-common/src/helper.ts index f3b3d5ad..d7f8a2a4 100644 --- a/packages/napcat-common/src/helper.ts +++ b/packages/napcat-common/src/helper.ts @@ -220,13 +220,13 @@ export function parseAppidFromMajor (nodeMajor: string): string | undefined { // ============== GitHub Tags 获取 ============== // 使用 mirror 模块统一管理镜像 -export async function getAllTags (): Promise<{ tags: string[], mirror: string; }> { - return getAllTagsFromMirror('NapNeko', 'NapCatQQ'); +export async function getAllTags (mirror?: string): Promise<{ tags: string[], mirror: string; }> { + return getAllTagsFromMirror('NapNeko', 'NapCatQQ', mirror); } -export async function getLatestTag (): Promise { - const { tags } = await getAllTags(); +export async function getLatestTag (mirror?: string): Promise { + const { tags } = await getAllTags(mirror); // 使用 SemVer 规范排序 tags.sort((a, b) => compareSemVer(a, b)); diff --git a/packages/napcat-common/src/mirror.ts b/packages/napcat-common/src/mirror.ts index 45accaaf..6899124b 100644 --- a/packages/napcat-common/src/mirror.ts +++ b/packages/napcat-common/src/mirror.ts @@ -23,66 +23,50 @@ import { PromiseTimer } from './helper'; * 懒加载测速:首次使用时自动测速,缓存 30 分钟 */ export const GITHUB_FILE_MIRRORS = [ - // 延迟 < 800ms 的最快镜像 - 'https://github.chenc.dev/', // 666ms - 'https://ghproxy.cfd/', // 719ms - 支持重定向 - 'https://github.tbedu.top/', // 760ms - 'https://ghps.cc/', // 768ms - 'https://gh.llkk.cc/', // 774ms - 'https://ghproxy.cc/', // 777ms - 'https://gh.monlor.com/', // 779ms - 'https://cdn.akaere.online/', // 784ms - // 延迟 800-1000ms 的快速镜像 - 'https://gh.idayer.com/', // 869ms - 'https://gh-proxy.net/', // 885ms - 'https://ghpxy.hwinzniej.top/', // 890ms - 'https://github-proxy.memory-echoes.cn/', // 896ms - 'https://git.yylx.win/', // 917ms - 'https://gitproxy.mrhjx.cn/', // 950ms - 'https://jiashu.1win.eu.org/', // 954ms - 'https://ghproxy.cn/', // 981ms - // 延迟 1000-1500ms 的中速镜像 - 'https://gh.fhjhy.top/', // 1014ms - 'https://gp.zkitefly.eu.org/', // 1015ms - 'https://gh-proxy.com/', // 1022ms - 'https://hub.gitmirror.com/', // 1027ms - 'https://ghfile.geekertao.top/', // 1029ms - 'https://j.1lin.dpdns.org/', // 1037ms - 'https://ghproxy.imciel.com/', // 1047ms - 'https://github-proxy.teach-english.tech/', // 1047ms - 'https://gh.927223.xyz/', // 1071ms - 'https://github.ednovas.xyz/', // 1099ms - 'https://ghf.xn--eqrr82bzpe.top/',// 1122ms - 'https://gh.dpik.top/', // 1131ms - 'https://gh.jasonzeng.dev/', // 1139ms - 'https://gh.xxooo.cf/', // 1157ms - 'https://gh.bugdey.us.kg/', // 1228ms - 'https://ghm.078465.xyz/', // 1289ms - 'https://j.1win.ggff.net/', // 1329ms - 'https://tvv.tw/', // 1393ms - 'https://gh.chjina.com/', // 1446ms - 'https://gitproxy.127731.xyz/', // 1458ms - // 延迟 1500-2500ms 的较慢镜像 - 'https://gh.inkchills.cn/', // 1617ms - 'https://ghproxy.cxkpro.top/', // 1651ms - 'https://gh.sixyin.com/', // 1686ms - 'https://github.geekery.cn/', // 1734ms - 'https://git.669966.xyz/', // 1824ms - 'https://gh.5050net.cn/', // 1858ms - 'https://gh.felicity.ac.cn/', // 1903ms - 'https://gh.ddlc.top/', // 2056ms - 'https://cf.ghproxy.cc/', // 2058ms - 'https://gitproxy.click/', // 2068ms - 'https://github.dpik.top/', // 2313ms - 'https://gh.zwnes.xyz/', // 2434ms - 'https://ghp.keleyaa.com/', // 2440ms - 'https://gh.wsmdn.dpdns.org/', // 2744ms - // 延迟 > 2500ms 的慢速镜像(作为备用) - 'https://ghproxy.monkeyray.net/', // 3023ms - 'https://fastgit.cc/', // 3369ms - 'https://cdn.gh-proxy.com/', // 3394ms - 'https://gh.catmak.name/', // 4119ms - 'https://gh.noki.icu/', // 5990ms + 'https://github.chenc.dev/', + 'https://ghproxy.cfd/', + 'https://github.tbedu.top/', + 'https://ghproxy.cc/', + 'https://gh.monlor.com/', + 'https://cdn.akaere.online/', + 'https://gh.idayer.com/', + 'https://gh.llkk.cc/', + 'https://ghpxy.hwinzniej.top/', + 'https://github-proxy.memory-echoes.cn/', + 'https://git.yylx.win/', + 'https://gitproxy.mrhjx.cn/', + 'https://gh.fhjhy.top/', + 'https://gp.zkitefly.eu.org/', + 'https://gh-proxy.com/', + 'https://ghfile.geekertao.top/', + 'https://j.1lin.dpdns.org/', + 'https://ghproxy.imciel.com/', + 'https://github-proxy.teach-english.tech/', + 'https://gh.927223.xyz/', + 'https://github.ednovas.xyz/', + 'https://ghf.xn--eqrr82bzpe.top/', + 'https://gh.dpik.top/', + 'https://gh.jasonzeng.dev/', + 'https://gh.xxooo.cf/', + 'https://gh.bugdey.us.kg/', + 'https://ghm.078465.xyz/', + 'https://j.1win.ggff.net/', + 'https://tvv.tw/', + 'https://gitproxy.127731.xyz/', + 'https://gh.inkchills.cn/', + 'https://ghproxy.cxkpro.top/', + 'https://gh.sixyin.com/', + 'https://github.geekery.cn/', + 'https://git.669966.xyz/', + 'https://gh.5050net.cn/', + 'https://gh.felicity.ac.cn/', + 'https://github.dpik.top/', + 'https://ghp.keleyaa.com/', + 'https://gh.wsmdn.dpdns.org/', + 'https://ghproxy.monkeyray.net/', + 'https://fastgit.cc/', + 'https://gh.catmak.name/', + 'https://gh.noki.icu/', '', // 原始 URL(无镜像) ]; @@ -109,7 +93,6 @@ export const GITHUB_RAW_MIRRORS = [ // 测试确认支持 raw 文件的镜像 'https://github.chenc.dev/https://raw.githubusercontent.com', 'https://ghproxy.cfd/https://raw.githubusercontent.com', - 'https://gh.llkk.cc/https://raw.githubusercontent.com', 'https://ghproxy.cc/https://raw.githubusercontent.com', 'https://gh-proxy.net/https://raw.githubusercontent.com', ]; @@ -585,7 +568,7 @@ export async function findAvailableDownloadUrl ( let testedCount = 0; for (const mirror of mirrors) { if (!mirror) continue; // 跳过空字符串 - + // 特殊处理 nightly.link URL let mirrorUrl: string; if (originalUrl.includes('nightly.link')) { @@ -594,7 +577,7 @@ export async function findAvailableDownloadUrl ( } else { mirrorUrl = buildMirrorUrl(originalUrl, mirror); } - + testedCount++; if (await testWithValidation(mirrorUrl)) { return mirrorUrl; @@ -650,8 +633,15 @@ function compareSemVerSimple (a: string, b: string): number { * 从 tags 列表中获取最新的 release tag * 不依赖 GitHub API */ -export async function getLatestReleaseTag (owner: string, repo: string): Promise { - const result = await getAllGitHubTags(owner, repo); +// Update definitions validation locally first if possible. +// I'll assume valid typescript. +// I will split this into two tool calls to avoid complexity. +// 1. Update mirror.ts +// 2. Update UpdateNapCat.ts + +// This tool call: Update mirror.ts +export async function getLatestReleaseTag (owner: string, repo: string, mirror?: string): Promise { + const result = await getAllGitHubTags(owner, repo, mirror); // 过滤出符合 semver 的 tags const releaseTags = result.tags.filter(tag => SEMVER_REGEX.test(tag)); @@ -701,6 +691,8 @@ export async function getGitHubRelease ( assetNames?: string[]; /** 是否需要获取 changelog(需要调用 API) */ fetchChangelog?: boolean; + /** 指定镜像 */ + mirror?: string; } = {} ): Promise<{ tag_name: string; @@ -710,15 +702,16 @@ export async function getGitHubRelease ( }>; body?: string; }> { - const { assetNames = [], fetchChangelog = false } = options; + const { assetNames = [], fetchChangelog = false, mirror } = options; // 1. 获取实际的 tag 名称 let actualTag: string; if (tag === 'latest') { - actualTag = await getLatestReleaseTag(owner, repo); + actualTag = await getLatestReleaseTag(owner, repo, mirror); } else { actualTag = tag; } + // ... // 2. 构建 assets 列表(不需要 API) const assets = assetNames.map(name => ({ @@ -782,8 +775,8 @@ const tagsCache: Map = new Map(); * 获取所有 GitHub tags(带缓存) * 优化:并行请求多个镜像,使用第一个成功返回的结果 */ -export async function getAllGitHubTags (owner: string, repo: string): Promise<{ tags: string[], mirror: string; }> { - const cacheKey = `${owner}/${repo}`; +export async function getAllGitHubTags (owner: string, repo: string, mirror?: string): Promise<{ tags: string[], mirror: string; }> { + const cacheKey = `${owner}/${repo}/${mirror || 'auto'}`; // 检查缓存 const cached = tagsCache.get(cacheKey); @@ -805,7 +798,7 @@ export async function getAllGitHubTags (owner: string, repo: string): Promise<{ }; // 尝试从 URL 获取 tags - const fetchFromUrl = async (url: string, mirror: string): Promise<{ tags: string[], mirror: string; } | null> => { + const fetchFromUrl = async (url: string, usedMirror: string): Promise<{ tags: string[], mirror: string; } | null> => { try { const raw = await PromiseTimer( RequestUtil.HttpGetText(url), @@ -813,79 +806,55 @@ export async function getAllGitHubTags (owner: string, repo: string): Promise<{ ); // 检查返回内容是否有效(不是 HTML 错误页面) - if (raw.includes(' 0) { - return { tags, mirror }; - } - return null; } catch { - return null; + // 忽略错误 } + return null; }; - // 获取快速镜像列表 - let fastMirrors: string[] = []; - try { - fastMirrors = await getFastMirrors(); - } catch { - // 忽略错误 + // 准备镜像列表 + let mirrors: string[] = []; + if (mirror) { + // 如果指定了镜像,只使用该镜像 + mirrors = [mirror]; + } else { + // 否则使用 auto 逻辑 + mirrors = ['', ...currentConfig.fileMirrors.filter(m => m)]; } - // 构建 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 })), - ]; + // 并行请求 + const promises = mirrors.map(m => { + const url = m ? buildMirrorUrl(baseUrl, m) : baseUrl; + return fetchFromUrl(url, m || 'https://github.com'); + }); - // 并行请求所有镜像,使用 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); + const result = await Promise.any(promises.filter(p => p !== null) as Promise<{ tags: string[], mirror: string; } | null>[]); if (result) { - tagsCache.set(cacheKey, { tags: result.tags, mirror: result.mirror, timestamp: Date.now() }); + tagsCache.set(cacheKey, { + tags: result.tags, + mirror: result.mirror, + timestamp: Date.now(), + }); return result; } + } catch { + // all failed } - // 最后尝试所有镜像 - const allMirrors = currentConfig.fileMirrors.filter(m => m && !fastMirrors.includes(m)); - for (const mirror of allMirrors) { - 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; - } + if (mirror) { + throw new Error(`指定镜像 ${mirror} 获取 tags 失败`); } - throw new Error('无法获取 tags,所有源都不可用'); + throw new Error('无法获取 tags,所有镜像源都不可用'); } // ============== Action Artifacts 支持 ============== +// ActionArtifact 接口定义 export interface ActionArtifact { id: number; name: string; @@ -895,12 +864,14 @@ export interface ActionArtifact { archive_download_url: string; workflow_run_id?: number; head_sha?: string; + workflow_title?: string; } // ============== Action Artifacts 缓存 ============== interface ArtifactsCache { artifacts: ActionArtifact[]; + mirror: string; timestamp: number; } @@ -920,68 +891,115 @@ export function clearArtifactsCache (): void { * 当 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> { + maxRuns: number = 10, + mirror?: string +): Promise<{ runs: Array<{ id: number; created_at: string; title: string; }>; mirror: string; }> { const baseUrl = `https://github.com/${owner}/${repo}/actions/workflows/${workflow}`; - + // 尝试使用镜像获取 HTML - const mirrors = ['', ...currentConfig.fileMirrors.filter(m => m)]; - - for (const mirror of mirrors) { + // 如果指定了 mirror,则只使用该 mirror + let mirrors: string[] = []; + if (mirror) { + mirrors = [mirror]; + } else { + mirrors = ['', ...currentConfig.fileMirrors.filter(m => m)]; + } + + for (const mirrorItem 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 allRuns: Array<{ id: number; created_at: string; title: string; }> = []; 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, - }); + let page = 1; + const maxPages = 10; // 防止无限请求,最多翻10页(约250个条目) + + while (allRuns.length < maxRuns && page <= maxPages) { + const pageUrl = page > 1 ? `${baseUrl}?page=${page}` : baseUrl; + const url = mirrorItem ? buildMirrorUrl(pageUrl, mirrorItem) : pageUrl; + + const html = await PromiseTimer( + RequestUtil.HttpGetText(url), + 10000 + ); + + // 使用 Block 分割策略,更稳健地关联 ID 和时间 + const rows = html.split('
+ const runMatch = new RegExp(`href="/${owner}/${repo}/actions/runs/(\\d+)"[^>]*aria-label="([^"]*)"`, 'i').exec(row); + + if (!runMatch || !runMatch[1] || !runMatch[2]) continue; + + const id = parseInt(runMatch[1]); + const ariaLabel = runMatch[2]; + const ariaLabelLower = ariaLabel.toLowerCase(); + + // 只需要判断 completed + if (ariaLabelLower.includes('completed')) { + if (!foundIds.has(id)) { + // 提取时间 (取 Block 内的第一个 relative-time) + const timeMatch = /]*>([\s\S]*?)<\/span>/i.exec(row); + if (titleMatch && titleMatch[1]) { + title = titleMatch[1].trim(); + } + + // 如果没找到,回退到 aria-label 逻辑 + if (!title) { + title = ariaLabel; + const prefixMatch = /^(completed successfully:\s*)/i.exec(title); + if (prefixMatch) { + title = title.substring(prefixMatch[0].length); + } + } + + allRuns.push({ + id, + created_at: timeMatch[1], + title: title.trim() + }); + } + } + } } + + // 如果本页没有找到任何 completed 的 run(但页面可能不为空),或者页面内容太少(可能是最后一页或错误) + // 这里简化判断: 如果本页没提取到任何有效数据,就认为没有更多数据了 + if (foundOnThisPage === 0) { + // 也要考虑到可能是页面解析失败或者全是 failed 状态 + // 检查是否有翻页按钮可能更复杂,暂时假设如果一整页都没有 successful run,可能后面也没有了,或者我们已经获取够多了 + // 为了稳健,如果本页没找到,且 allRuns 还没满,尝试下一页 (除非页面很小说明是空页) + if (rows.length < 2) { // 只有 split 的第一个空元素 + break; + } + } + + // 分页逻辑:总是尝试下一页,直到满足 maxRuns + page++; } - - if (runs.length > 0) { - return runs; + + if (allRuns.length > 0) { + return { runs: allRuns, mirror: mirrorItem || 'https://github.com' }; } } catch { continue; } } - - return []; + + return { runs: [], mirror: '' }; } /** @@ -996,122 +1014,49 @@ 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 + _branch: string = 'main', + maxRuns: number = 10, + mirror?: string +): Promise<{ artifacts: ActionArtifact[], mirror: string; }> { + // 策略: 优先使用 nightly.link(更稳定,无需认证)+ HTML 解析 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 - ); + // 以前尝试使用 GitHub API,现在弃用,完全使用 HTML 解析逻辑 + // 并获取 workflow // 直接从 HTML 页面解析 + const { runs: workflowRuns, mirror: runsMirror } = await getWorkflowRunsFromHtml(owner, repo, workflow, maxRuns, mirror); - if (runsResponse.workflow_runs && runsResponse.workflow_runs.length > 0) { - workflowRuns = runsResponse.workflow_runs; + if (workflowRuns.length === 0) { + return { artifacts: [], mirror: runsMirror }; } - } catch { - // API 请求失败,继续尝试 HTML 解析 - } - // 策略2: API 失败时,从 HTML 页面解析 - if (workflowRuns.length === 0) { - workflowRuns = await getWorkflowRunsFromHtml(owner, repo, workflow, maxRuns); - } + // 直接拼接 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 名称 - if (workflowRuns.length === 0) { - return []; - } + // 如果 HTML 解析使用的 mirror 是 github.com(空),则 nightly.link 使用默认配置 + // 如果使用了镜像,可能需要特殊的 nightly.link 镜像,或者这里仅记录 HTML 来源镜像 + // 实际上 nightly.link 本身就是一个服务,我们使用配置中的 nightlyLinkMirrors + const baseNightlyMirror = currentConfig.nightlyLinkMirrors[0] || 'https://nightly.link'; - // 直接拼接 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', - branch: string = 'main', - maxRuns: number = 10 -): Promise { - 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 - ); - - const workflowRuns = runsResponse.workflow_runs; - if (!workflowRuns || workflowRuns.length === 0) { - throw new Error('No successful workflow runs found'); - } - - // 获取所有 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 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); - } + for (const run of workflowRuns) { + for (const artifactName of artifactNames) { + artifacts.push({ + id: run.id, + name: artifactName, + size_in_bytes: 0, + created_at: run.created_at, + expires_at: new Date(new Date(run.created_at).getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(), + archive_download_url: `${baseNightlyMirror}/${owner}/${repo}/actions/runs/${run.id}/${artifactName}.zip`, + workflow_run_id: run.id, + workflow_title: run.title, + }); } - } catch { - // 单个 run 获取失败,继续下一个 } - } + return { artifacts, mirror: runsMirror }; - return allArtifacts; + } catch { + return { artifacts: [], mirror: '' }; + } } /** @@ -1121,48 +1066,41 @@ async function getArtifactsFromAPI ( * 策略: * 1. 检查缓存(10分钟有效) * 2. 优先尝试从 nightly.link 获取(无需认证,更稳定) - * 3. 如果失败,回退到 GitHub API + * 3. 这里的实现已经完全移除了对 GitHub API 的依赖,直接解析 HTML */ 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}`; - + maxRuns: number = 10, + mirror?: string +): Promise<{ artifacts: ActionArtifact[], mirror: string; }> { + const cacheKey = `${owner}/${repo}/${workflow}/${branch}/${mirror || 'auto'}`; + // 检查缓存 const cached = artifactsCache.get(cacheKey); if (cached && (Date.now() - cached.timestamp) < ARTIFACTS_CACHE_TTL) { - return cached.artifacts; + return { artifacts: cached.artifacts, mirror: cached.mirror }; } - let artifacts: ActionArtifact[] = []; + let result: { artifacts: ActionArtifact[], mirror: string; } = { artifacts: [], mirror: '' }; - // 策略1: 优先使用 nightly.link(更稳定,无需认证) + // 策略: 优先使用 nightly.link(更稳定,无需认证)+ HTML 解析 try { - artifacts = await getArtifactsFromNightlyLink(owner, repo, workflow, branch, maxRuns); + result = await getArtifactsFromNightlyLink(owner, repo, workflow, branch, maxRuns, mirror); } 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) { + if (result.artifacts.length > 0) { artifactsCache.set(cacheKey, { - artifacts, + artifacts: result.artifacts, + mirror: result.mirror, timestamp: Date.now(), }); } - return artifacts; + return result; } diff --git a/packages/napcat-common/src/version.ts b/packages/napcat-common/src/version.ts index 5dd34e8f..723b5308 100644 --- a/packages/napcat-common/src/version.ts +++ b/packages/napcat-common/src/version.ts @@ -65,9 +65,11 @@ export function compareSemVer (v1: string, v2: string): -1 | 0 | 1 { const a = parseSemVer(v1); const b = parseSemVer(v2); - if (!a.valid || !b.valid) { - return 0; + if (!a.valid && !b.valid) { + return v1.localeCompare(v2) as -1 | 0 | 1; } + if (!a.valid) return -1; + if (!b.valid) return 1; // 比较主版本号 if (a.major !== b.major) return a.major > b.major ? 1 : -1; diff --git a/packages/napcat-shell/napcat.ts b/packages/napcat-shell/napcat.ts index 1b0f2853..5015592b 100644 --- a/packages/napcat-shell/napcat.ts +++ b/packages/napcat-shell/napcat.ts @@ -3,6 +3,7 @@ import { NapCatPathWrapper } from '@/napcat-common/src/path'; import { LogWrapper } from '@/napcat-core/helper/log'; import { connectToNamedPipe } from './pipe'; import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data'; +import { AuthHelper } from '@/napcat-webui-backend/src/helper/SignToken'; import { createProcessManager, type IProcessManager, type IWorkerProcess } from './process-api'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -71,7 +72,7 @@ function forceKillProcess (pid: number): void { /** * 重启 Worker 进程 */ -export async function restartWorker (): Promise { +export async function restartWorker (secretKey?: string): Promise { logger.log('[NapCat] [Process] 正在重启Worker进程...'); isRestarting = true; @@ -134,8 +135,8 @@ export async function restartWorker (): Promise { logger.log('[NapCat] [Process] Worker进程已关闭,等待 3 秒后启动新进程...'); await new Promise(resolve => setTimeout(resolve, 3000)); - // 5. 启动新进程(重启模式不传递快速登录参数) - await startWorker(false); + // 5. 启动新进程(重启模式不传递快速登录参数,传递密钥) + await startWorker(false, secretKey); isRestarting = false; logger.log('[NapCat] [Process] Worker进程重启完成'); } @@ -144,7 +145,7 @@ export async function restartWorker (): Promise { * 启动 Worker 进程 * @param passQuickLogin 是否传递快速登录参数,默认为 true,重启时为 false */ -async function startWorker (passQuickLogin: boolean = true): Promise { +async function startWorker (passQuickLogin: boolean = true, secretKey?: string): Promise { if (!processManager) { throw new Error('进程管理器未初始化'); } @@ -170,6 +171,7 @@ async function startWorker (passQuickLogin: boolean = true): Promise { env: { ...process.env, NAPCAT_WORKER_PROCESS: '1', + ...(secretKey ? { NAPCAT_WEBUI_JWT_SECRET_KEY: secretKey } : {}), }, stdio: isElectron ? 'pipe' : ['inherit', 'pipe', 'pipe', 'ipc'], }); @@ -197,7 +199,8 @@ async function startWorker (passQuickLogin: boolean = true): Promise { // 处理重启请求 if (typeof msg === 'object' && msg !== null && 'type' in msg && msg.type === 'restart') { logger.log(`[NapCat] [${processType}] 收到重启请求,正在重启Worker进程...`); - restartWorker().catch(e => { + const secretKey = 'secretKey' in msg ? (msg as any).secretKey : undefined; + restartWorker(secretKey).catch(e => { logger.logError(`[NapCat] [${processType}] 重启Worker进程失败:`, e); }); } @@ -292,7 +295,8 @@ async function startWorkerProcess (): Promise { // 注册重启进程函数到 WebUI WebUiDataRuntime.setRestartProcessCall(async () => { try { - const success = processManager!.sendToParent({ type: 'restart' }); + const success = processManager!.sendToParent({ type: 'restart', secretKey: AuthHelper.getSecretKey() }); + if (success) { return { result: true, message: '进程重启请求已发送' }; diff --git a/packages/napcat-shell/process-api.ts b/packages/napcat-shell/process-api.ts index c4316b7c..be5ca721 100644 --- a/packages/napcat-shell/process-api.ts +++ b/packages/napcat-shell/process-api.ts @@ -60,7 +60,7 @@ class ElectronProcessManager implements IProcessManager { const child: any = this.utilityProcess.fork(modulePath, args, options); return { - pid: child.pid as number | undefined, + get pid () { return child.pid as number | undefined; }, stdout: child.stdout as Readable | null, stderr: child.stderr as Readable | null, @@ -113,7 +113,7 @@ class NodeProcessManager implements IProcessManager { const child = this.forkFn(modulePath, args, options as any); return { - pid: child.pid, + get pid () { return child.pid; }, stdout: child.stdout, stderr: child.stderr, @@ -164,6 +164,9 @@ export async function createProcessManager (): Promise<{ if (isElectron) { // @ts-ignore - electron 运行时存在但类型声明可能缺失 const electron = await import('electron'); + if (electron.app && !electron.app.isReady()) { + await electron.app.whenReady(); + } return { manager: new ElectronProcessManager(electron.utilityProcess), isElectron: true, diff --git a/packages/napcat-webui-backend/index.ts b/packages/napcat-webui-backend/index.ts index eb1e71db..01823a9d 100644 --- a/packages/napcat-webui-backend/index.ts +++ b/packages/napcat-webui-backend/index.ts @@ -72,7 +72,7 @@ export function setPendingTokenToSend (token: string | null) { export async function InitPort (parsedConfig: WebUiConfigType): Promise<[string, number, string]> { try { await tryUseHost(parsedConfig.host); - const port = await tryUsePort(parsedConfig.port, parsedConfig.host); + const port = await tryUsePort(parsedConfig.port, parsedConfig.host, 0, 5); return [parsedConfig.host, port, parsedConfig.token]; } catch (error) { console.log('host或port不可用', error); @@ -356,7 +356,7 @@ async function tryUseHost (host: string): Promise { }); } -async function tryUsePort (port: number, host: string, tryCount: number = 0): Promise { +async function tryUsePort (port: number, host: string, tryCount: number = 0, retryCurrentCount: number = 0): Promise { return new Promise((resolve, reject) => { try { const server = net.createServer(); @@ -367,9 +367,17 @@ async function tryUsePort (port: number, host: string, tryCount: number = 0): Pr server.on('error', (err: any) => { if (err.code === 'EADDRINUSE') { + if (retryCurrentCount > 0) { + console.log(`[NapCat] [WebUi] 端口 ${port} 被占用,1秒后重试... (剩余重试次数: ${retryCurrentCount})`); + setTimeout(() => { + resolve(tryUsePort(port, host, tryCount, retryCurrentCount - 1)); + }, 1000); + return; + } + if (tryCount < MAX_PORT_TRY) { // 使用循环代替递归 - resolve(tryUsePort(port + 1, host, tryCount + 1)); + resolve(tryUsePort(port + 1, host, tryCount + 1, 0)); } else { reject(new Error(`端口尝试失败,达到最大尝试次数: ${MAX_PORT_TRY}`)); } @@ -386,3 +394,4 @@ async function tryUsePort (port: number, host: string, tryCount: number = 0): Pr } }); } + diff --git a/packages/napcat-webui-backend/src/api/BaseInfo.ts b/packages/napcat-webui-backend/src/api/BaseInfo.ts index 123889c5..33e3a342 100644 --- a/packages/napcat-webui-backend/src/api/BaseInfo.ts +++ b/packages/napcat-webui-backend/src/api/BaseInfo.ts @@ -4,7 +4,7 @@ import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data'; import { sendSuccess } from '@/napcat-webui-backend/src/utils/response'; import { WebUiConfig } from '@/napcat-webui-backend/index'; import { getLatestTag, getAllTags, compareSemVer } from 'napcat-common/src/helper'; -import { getLatestActionArtifacts } from '@/napcat-common/src/mirror'; +import { getLatestActionArtifacts, getMirrorConfig } from '@/napcat-common/src/mirror'; import { NapCatCoreWorkingEnv } from '@/napcat-webui-backend/src/types'; export const GetNapCatVersion: RequestHandler = (_, res) => { @@ -35,6 +35,7 @@ export interface VersionInfo { size?: number; workflowRunId?: number; headSha?: string; + workflowTitle?: string; } /** @@ -47,11 +48,17 @@ export const getAllReleasesHandler: RequestHandler = async (req, res) => { const pageSize = parseInt(req.query['pageSize'] as string) || 20; const typeFilter = req.query['type'] as string | undefined; // 'release' | 'action' | 'all' const searchQuery = (req.query['search'] as string || '').toLowerCase().trim(); + const mirror = req.query['mirror'] as string | undefined; let versions: VersionInfo[] = []; let actionVersions: VersionInfo[] = []; let usedMirror = ''; + // If mirror is specified, report it as used (will be confirmed by actual fetching response) + if (mirror) { + usedMirror = mirror; + } + // 懒加载:只获取需要的版本类型 const needReleases = !typeFilter || typeFilter === 'all' || typeFilter === 'release'; const needActions = typeFilter === 'action' || typeFilter === 'all'; @@ -59,9 +66,12 @@ export const getAllReleasesHandler: RequestHandler = async (req, res) => { // 获取正式版本(仅当需要时) if (needReleases) { try { - const result = await getAllTags(); - usedMirror = result.mirror; - + const result = await getAllTags(mirror); + // 如果没有指定镜像,使用实际上使用的镜像 + if (!mirror) { + usedMirror = result.mirror; + } + versions = result.tags.map(tag => { const isPrerelease = /-(alpha|beta|rc|dev|pre|snapshot)/i.test(tag); return { @@ -81,14 +91,19 @@ export const getAllReleasesHandler: RequestHandler = async (req, res) => { // 获取 Action Artifacts(仅当需要时) if (needActions) { try { - const artifacts = await getLatestActionArtifacts('NapNeko', 'NapCatQQ', 'build.yml', 'main'); + const { artifacts, mirror: actionMirror } = await getLatestActionArtifacts('NapNeko', 'NapCatQQ', 'build.yml', 'main', 10, mirror); // 根据当前工作环境自动过滤对应的 artifact 类型 const isFramework = WebUiDataRuntime.getWorkingEnv() === NapCatCoreWorkingEnv.Framework; const targetArtifactName = isFramework ? 'NapCat.Framework' : 'NapCat.Shell'; + // 如果没有指定镜像,且 action 实际上用了一个镜像(自动选择的),更新 usedMirror + if (!mirror && actionMirror) { + usedMirror = actionMirror; + } + actionVersions = artifacts - .filter(a => a.name === targetArtifactName) + .filter(a => a && a.name === targetArtifactName) .map(a => ({ tag: `action-${a.id}`, type: 'action' as const, @@ -99,6 +114,7 @@ export const getAllReleasesHandler: RequestHandler = async (req, res) => { size: a.size_in_bytes, workflowRunId: a.workflow_run_id, headSha: a.head_sha, + workflowTitle: a.workflow_title, })); } catch { // 获取失败时返回空列表 @@ -114,7 +130,9 @@ export const getAllReleasesHandler: RequestHandler = async (req, res) => { allVersions = allVersions.filter(v => { const tagMatch = v.tag.toLowerCase().includes(searchQuery); const nameMatch = v.artifactName?.toLowerCase().includes(searchQuery); - return tagMatch || nameMatch; + const titleMatch = v.workflowTitle?.toLowerCase().includes(searchQuery); + const shaMatch = v.headSha?.toLowerCase().includes(searchQuery); + return tagMatch || nameMatch || titleMatch || shaMatch; }); } @@ -155,3 +173,8 @@ export const SetThemeConfigHandler: RequestHandler = async (req, res) => { await WebUiConfig.UpdateTheme(theme); sendSuccess(res, { message: '更新成功' }); }; + +export const GetMirrorsHandler: RequestHandler = (_, res) => { + const config = getMirrorConfig(); + sendSuccess(res, { mirrors: config.fileMirrors }); +}; diff --git a/packages/napcat-webui-backend/src/api/UpdateNapCat.ts b/packages/napcat-webui-backend/src/api/UpdateNapCat.ts index 83fa4f80..43126080 100644 --- a/packages/napcat-webui-backend/src/api/UpdateNapCat.ts +++ b/packages/napcat-webui-backend/src/api/UpdateNapCat.ts @@ -20,6 +20,8 @@ interface UpdateRequestBody { targetVersion?: string; /** 是否强制更新(即使是降级也更新) */ force?: boolean; + /** 指定使用的镜像 */ + mirror?: string; } // 更新配置文件接口 @@ -124,7 +126,7 @@ async function downloadFile (url: string, dest: string): Promise { export const UpdateNapCatHandler: RequestHandler = async (req, res) => { try { // 从请求体获取目标版本(可选) - const { targetVersion, force } = req.body as UpdateRequestBody; + const { targetVersion, force, mirror } = req.body as UpdateRequestBody; // 确定要下载的文件名 const ReleaseName = WebUiDataRuntime.getWorkingEnv() === NapCatCoreWorkingEnv.Framework ? 'NapCat.Framework.zip' : 'NapCat.Shell.zip'; @@ -150,20 +152,21 @@ export const UpdateNapCatHandler: RequestHandler = async (req, res) => { // 根据当前工作环境确定 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, + customMirror: mirror, }); webUiLogger?.log(`[NapCat Update] Using download URL: ${downloadUrl}`); } catch (error) { @@ -178,6 +181,7 @@ export const UpdateNapCatHandler: RequestHandler = async (req, res) => { const release = await getGitHubRelease('NapNeko', 'NapCatQQ', targetTag, { assetNames: [ReleaseName, 'NapCat.Framework.zip', 'NapCat.Shell.zip'], fetchChangelog: false, // 不需要 changelog,避免 API 调用 + mirror, }); const shellZipAsset = release.assets.find(asset => asset.name === ReleaseName); @@ -193,6 +197,7 @@ export const UpdateNapCatHandler: RequestHandler = async (req, res) => { validateContent: true, // 验证 Content-Type 和状态码 minFileSize: 1024 * 1024, // 最小 1MB,确保不是错误页面 timeout: 10000, // 10秒超时 + customMirror: mirror, }); } diff --git a/packages/napcat-webui-backend/src/helper/SignToken.ts b/packages/napcat-webui-backend/src/helper/SignToken.ts index 40d61683..ff541d20 100644 --- a/packages/napcat-webui-backend/src/helper/SignToken.ts +++ b/packages/napcat-webui-backend/src/helper/SignToken.ts @@ -4,6 +4,10 @@ import type { WebUiCredentialJson, WebUiCredentialInnerJson } from '@/napcat-web export class AuthHelper { private static readonly secretKey = process.env['NAPCAT_WEBUI_JWT_SECRET_KEY'] || Math.random().toString(36).slice(2); + public static getSecretKey (): string { + return AuthHelper.secretKey; + } + /** * 签名凭证方法。 * @param hash 待签名的凭证字符串。 diff --git a/packages/napcat-webui-backend/src/router/Base.ts b/packages/napcat-webui-backend/src/router/Base.ts index a12016b3..5efd1108 100644 --- a/packages/napcat-webui-backend/src/router/Base.ts +++ b/packages/napcat-webui-backend/src/router/Base.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { GetThemeConfigHandler, GetNapCatVersion, QQVersionHandler, SetThemeConfigHandler, getLatestTagHandler, getAllReleasesHandler } from '../api/BaseInfo'; +import { GetThemeConfigHandler, GetNapCatVersion, QQVersionHandler, SetThemeConfigHandler, getLatestTagHandler, getAllReleasesHandler, GetMirrorsHandler } from '../api/BaseInfo'; import { StatusRealTimeHandler } from '@/napcat-webui-backend/src/api/Status'; import { GetProxyHandler } from '../api/Proxy'; @@ -9,6 +9,7 @@ router.get('/QQVersion', QQVersionHandler); router.get('/GetNapCatVersion', GetNapCatVersion); router.get('/getLatestTag', getLatestTagHandler); router.get('/getAllReleases', getAllReleasesHandler); +router.get('/getMirrors', GetMirrorsHandler); router.get('/GetSysStatusRealTime', StatusRealTimeHandler); router.get('/proxy', GetProxyHandler); router.get('/Theme', GetThemeConfigHandler); diff --git a/packages/napcat-webui-frontend/src/components/system_info.tsx b/packages/napcat-webui-frontend/src/components/system_info.tsx index 5be0f274..516aef51 100644 --- a/packages/napcat-webui-frontend/src/components/system_info.tsx +++ b/packages/napcat-webui-frontend/src/components/system_info.tsx @@ -269,6 +269,7 @@ interface VersionInfo { size?: number; workflowRunId?: number; headSha?: string; + workflowTitle?: string; } // 版本选择对话框内容 @@ -290,6 +291,14 @@ const VersionSelectDialogContent: React.FC = ({ const [activeTab, setActiveTab] = useState<'release' | 'action'>('release'); const [searchQuery, setSearchQuery] = useState(''); const debouncedSearch = useDebounce(searchQuery, 300); + + const [selectedMirror, setSelectedMirror] = useState(undefined); + const { data: mirrorsData } = useRequest(WebUIManager.getMirrors, { + cacheKey: 'napcat-mirrors', + staleTime: 60 * 60 * 1000, + }); + const mirrors = mirrorsData?.mirrors || []; + const pageSize = 15; // 获取所有可用版本(带分页、过滤和搜索) @@ -299,15 +308,16 @@ const VersionSelectDialogContent: React.FC = ({ page: currentPage, pageSize, type: activeTab, - search: debouncedSearch + search: debouncedSearch, + mirror: selectedMirror }), { - refreshDeps: [currentPage, activeTab, debouncedSearch], + refreshDeps: [currentPage, activeTab, debouncedSearch, selectedMirror], } ); // 版本列表已在后端过滤,直接使用 - const filteredVersions = releasesData?.versions || []; + const filteredVersions = (releasesData?.versions || []) as VersionInfo[]; // 检查是否是降级(使用语义化版本比较) const isDowngrade = useCallback((targetTag: string): boolean => { @@ -320,6 +330,22 @@ const VersionSelectDialogContent: React.FC = ({ const selectedVersionTag = selectedVersion?.tag || ''; const isSelectedDowngrade = isDowngrade(selectedVersionTag); + const performUpdate = async (force: boolean) => { + if (!selectedVersion) return; + setUpdateStatus('updating'); + setErrorMessage(''); + + try { + await WebUIManager.UpdateNapCatToVersion(selectedVersionTag, force, selectedMirror); + setUpdateStatus('success'); + } catch (err) { + console.error('Update failed:', err); + const errMsg = err instanceof Error ? err.message : '未知错误'; + setErrorMessage(errMsg); + setUpdateStatus('error'); + } + }; + const handleUpdate = async () => { if (!selectedVersion) return; @@ -346,22 +372,6 @@ const VersionSelectDialogContent: React.FC = ({ await performUpdate(forceUpdate); }; - const performUpdate = async (force: boolean) => { - if (!selectedVersion) return; - setUpdateStatus('updating'); - setErrorMessage(''); - - try { - await WebUIManager.UpdateNapCatToVersion(selectedVersionTag, force); - setUpdateStatus('success'); - } catch (err) { - console.error('Update failed:', err); - const errMsg = err instanceof Error ? err.message : '未知错误'; - setErrorMessage(errMsg); - setUpdateStatus('error'); - } - }; - // 处理分页变化 const handlePageChange = (page: number) => { setCurrentPage(page); @@ -375,13 +385,30 @@ const VersionSelectDialogContent: React.FC = ({
-
+

更新到 {selectedVersionTag} 完成

-

+

请重启 NapCat 以应用新版本

+
+ + +
); @@ -463,23 +490,46 @@ const VersionSelectDialogContent: React.FC = ({ - {/* 搜索框 */} - { - setSearchQuery(value); - setCurrentPage(1); - setSelectedVersion(null); - }} - startContent={} - isClearable - onClear={() => setSearchQuery('')} - classNames={{ - inputWrapper: 'h-9', - }} - /> +
+ {/* 搜索框 */} + { + setSearchQuery(value); + setCurrentPage(1); + setSelectedVersion(null); + }} + startContent={} + isClearable + onClear={() => setSearchQuery('')} + classNames={{ + inputWrapper: 'h-9', + base: 'flex-1' + }} + /> + + {/* 镜像选择 */} + +
{/* 版本选择 */}
@@ -528,7 +578,12 @@ const VersionSelectDialogContent: React.FC = ({ >
- {version.type === 'action' && version.artifactName ? version.artifactName : version.tag} + + {version.type === 'action' + ? (version.workflowTitle || version.artifactName || version.tag) + : version.tag + } + {version.type === 'prerelease' && ( 预发布 )} @@ -543,10 +598,11 @@ const VersionSelectDialogContent: React.FC = ({ )}
{version.type === 'action' && ( -
- {version.headSha && {version.headSha.slice(0, 7)}} - {version.createdAt && {new Date(version.createdAt).toLocaleString()}} - {version.size && {(version.size / 1024 / 1024).toFixed(1)} MB} +
+ {version.tag} + {version.headSha && {version.headSha.slice(0, 7)}} + {version.createdAt && {new Date(version.createdAt).toLocaleString()}} + {version.size && {(version.size / 1024 / 1024).toFixed(1)} MB}
)}
@@ -568,6 +624,7 @@ const VersionSelectDialogContent: React.FC = ({ )}

+
)} diff --git a/packages/napcat-webui-frontend/src/controllers/webui_manager.ts b/packages/napcat-webui-frontend/src/controllers/webui_manager.ts index 9e5098e1..695d0cb6 100644 --- a/packages/napcat-webui-frontend/src/controllers/webui_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/webui_manager.ts @@ -72,8 +72,9 @@ export default class WebUIManager { pageSize?: number; type?: 'release' | 'action' | 'all'; search?: string; + mirror?: string; } = {}) { - const { page = 1, pageSize = 20, type = 'release', search = '' } = options; + const { page = 1, pageSize = 20, type = 'release', search = '', mirror } = options; const { data } = await serverRequest.get>('/base/getAllReleases', { - params: { page, pageSize, type, search }, + params: { page, pageSize, type, search, mirror }, }); return data.data; } - public static async UpdateNapCat () { + public static async getMirrors () { + const { data } = + await serverRequest.get>('/base/getMirrors'); + return data.data; + } + + public static async UpdateNapCat (mirror?: string) { const { data } = await serverRequest.post>( '/UpdateNapCat/update', - {}, + { mirror }, { timeout: 120000 } // 2分钟超时 ); return data; @@ -112,11 +119,12 @@ export default class WebUIManager { * 更新到指定版本 * @param targetVersion 目标版本 tag,如 "v4.9.9" 或 "action-123456" * @param force 是否强制更新(允许降级) + * @param mirror 指定使用的镜像 */ - public static async UpdateNapCatToVersion (targetVersion: string, force: boolean = false) { + public static async UpdateNapCatToVersion (targetVersion: string, force: boolean = false, mirror?: string) { const { data } = await serverRequest.post>( '/UpdateNapCat/update', - { targetVersion, force }, + { targetVersion, force, mirror }, { timeout: 120000 } // 2分钟超时 ); return data; @@ -142,6 +150,16 @@ export default class WebUIManager { return data.data; } + public static async restart () { + const { data } = await serverRequest.post>('/Process/Restart'); + return data.data; + } + + public static async getAllUsers (): Promise { + const { data } = await serverRequest.get>('/QQLogin/GetAllUsers'); + return data.data; + } + public static async getLogList () { const { data } = await serverRequest.get>('/Log/GetLogList');