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.
This commit is contained in:
手瓜一十雪
2026-01-04 12:41:21 +08:00
parent 4940d72867
commit f1756c4d1c
8 changed files with 420 additions and 134 deletions

View File

@@ -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 => {

View File

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