Files
goodbuddy/sites/release-index.js
T
mesalogo d9a554b4b1
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
feat: add LoongArch preview download to the website
The Chinese Linux download card now includes a LoongArch processor option and fixes its package type to DEB. It reads a separate strictly validated OSS preview index, while the formal six-target release index and English GitHub downloads remain unchanged.

The page keeps three symmetric platform cards, disables only the LoongArch selection when its index is unavailable, and labels the package as unverified on real LoongArch hardware.

Release note: 官网 Linux 下载新增龙芯 LoongArch 处理器选项,选择后提供独立 DEB 预览包;正式下载与更新索引保持不变。
2026-08-26 23:23:27 +08:00

297 lines
9.1 KiB
JavaScript

(() => {
"use strict";
const mirrorIndexUrl =
"https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/latest.json";
const loongArchPreviewIndexUrl =
"https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/loongarch-preview/latest.json";
const fallbackUrl =
"https://github.com/mesalogo/goodbuddy/releases/latest";
const maximumIndexBytes = 512 * 1024;
const maximumPreviewIndexBytes = 16 * 1024;
const semVerPattern =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+((?:[0-9a-zA-Z-]+)(?:\.[0-9a-zA-Z-]+)*))?$/u;
const sha256Pattern = /^[a-f0-9]{64}$/u;
const safeFileNamePattern = /^(?!\.{1,2}$)[^/\\\0]+$/u;
const targetDefinitions = Object.freeze({
"windows-x64": Object.freeze({
platform: "windows",
arch: "x64",
formats: Object.freeze(["nsis", "portable"]),
}),
"windows-arm64": Object.freeze({
platform: "windows",
arch: "arm64",
formats: Object.freeze(["nsis", "portable"]),
}),
"macos-x64": Object.freeze({
platform: "macos",
arch: "x64",
formats: Object.freeze(["dmg", "zip"]),
}),
"macos-arm64": Object.freeze({
platform: "macos",
arch: "arm64",
formats: Object.freeze(["dmg", "zip"]),
}),
"linux-x64": Object.freeze({
platform: "linux",
arch: "x64",
formats: Object.freeze(["AppImage", "deb", "rpm"]),
}),
"linux-arm64": Object.freeze({
platform: "linux",
arch: "arm64",
formats: Object.freeze(["AppImage", "deb", "rpm"]),
}),
});
const targetKeys = Object.freeze(Object.keys(targetDefinitions));
const isRecord = (value) =>
value !== null && typeof value === "object" && !Array.isArray(value);
const hasExactKeys = (value, keys) => {
if (!isRecord(value)) {
return false;
}
const actualKeys = Object.keys(value);
return (
actualKeys.length === keys.length &&
keys.every((key) => Object.prototype.hasOwnProperty.call(value, key))
);
};
const assert = (condition, message) => {
if (!condition) {
throw new Error(message);
}
};
const canonicalFileName = (version, platform, arch, format) => {
if (platform === "windows" && format === "nsis") {
return `GoodBuddy-${version}-windows-${arch}-setup.exe`;
}
if (platform === "windows" && format === "portable") {
return `GoodBuddy-${version}-windows-${arch}-portable.zip`;
}
if (platform === "macos") {
return `GoodBuddy-${version}-mac-${arch}.${format}`;
}
const artifactArch =
arch === "x64"
? format === "deb"
? "amd64"
: "x86_64"
: format === "rpm"
? "aarch64"
: "arm64";
return `GoodBuddy-${version}-linux-${artifactArch}.${format}`;
};
const assertExactUrl = (value, expected, label) => {
assert(typeof value === "string" && value.length <= 2_048, `${label} 无效`);
let url;
try {
url = new URL(value);
} catch {
throw new Error(`${label} 无效`);
}
assert(
value === expected &&
url.href === expected &&
url.protocol === "https:" &&
!url.username &&
!url.password &&
!url.port &&
!url.search &&
!url.hash,
`${label} 不是受信任的正式地址`,
);
};
const validateReleaseIndex = (index) => {
assert(
hasExactKeys(index, [
"formatVersion",
"productName",
"version",
"targets",
"checksumUrl",
"fallbackUrl",
]),
"发布索引结构无效",
);
assert(index.formatVersion === 1, "发布索引版本无效");
assert(index.productName === "GoodBuddy", "发布索引产品名称无效");
assert(
typeof index.version === "string" && index.version.length <= 256,
"发布版本无效",
);
const parsedVersion = semVerPattern.exec(index.version);
assert(parsedVersion && !parsedVersion[4], "发布索引必须指向稳定 SemVer 版本");
assert(
hasExactKeys(index.targets, targetKeys),
"发布索引必须包含且仅包含六个平台目标",
);
const releaseBase = new URL(`v${index.version}/`, mirrorIndexUrl);
const seenNames = new Set();
const seenUrls = new Set();
for (const key of targetKeys) {
const definition = targetDefinitions[key];
const target = index.targets[key];
assert(
hasExactKeys(target, ["platform", "arch", "files"]) &&
target.platform === definition.platform &&
target.arch === definition.arch,
`发布目标与键不匹配:${key}`,
);
assert(
hasExactKeys(target.files, definition.formats),
`发布目标文件格式无效:${key}`,
);
for (const format of definition.formats) {
const file = target.files[format];
assert(
hasExactKeys(file, ["name", "size", "sha256", "url"]),
`发布文件结构无效:${key}/${format}`,
);
assert(
typeof file.name === "string" &&
file.name.length >= 1 &&
file.name.length <= 255 &&
safeFileNamePattern.test(file.name) &&
file.name ===
canonicalFileName(
index.version,
target.platform,
target.arch,
format,
),
`发布文件名或扩展名无效:${key}/${format}`,
);
assert(
Number.isSafeInteger(file.size) && file.size > 0,
`发布文件大小无效:${key}/${format}`,
);
assert(
typeof file.sha256 === "string" && sha256Pattern.test(file.sha256),
`发布文件校验值无效:${key}/${format}`,
);
assert(!seenNames.has(file.name), `发布文件名重复:${file.name}`);
seenNames.add(file.name);
const expectedUrl = new URL(
encodeURIComponent(file.name),
releaseBase,
).href;
assertExactUrl(file.url, expectedUrl, "发布文件地址");
assert(!seenUrls.has(file.url), `发布文件地址重复:${file.url}`);
seenUrls.add(file.url);
}
}
assertExactUrl(
index.checksumUrl,
new URL("SHA256SUMS", releaseBase).href,
"校验清单地址",
);
assertExactUrl(index.fallbackUrl, fallbackUrl, "GitHub 回退地址");
return index;
};
const validateLoongArchPreviewIndex = (index) => {
assert(
hasExactKeys(index, [
"formatVersion",
"product",
"goodBuddyVersion",
"previewVersion",
"architecture",
"format",
"artifact",
"manifestUrl",
"checksumUrl",
]),
"龙芯预览索引结构无效",
);
assert(index.formatVersion === 1, "龙芯预览索引版本无效");
assert(
index.product === "GoodBuddy LoongArch Preview",
"龙芯预览产品名称无效",
);
assert(
typeof index.goodBuddyVersion === "string" &&
index.goodBuddyVersion.length <= 256,
"龙芯预览 GoodBuddy 版本无效",
);
const parsedVersion = semVerPattern.exec(index.goodBuddyVersion);
assert(
parsedVersion && !parsedVersion[4],
"龙芯预览必须基于稳定 GoodBuddy 版本",
);
const previewPrefix = `${index.goodBuddyVersion}-loong64-preview.`;
assert(
typeof index.previewVersion === "string" &&
index.previewVersion.startsWith(previewPrefix) &&
/^[1-9]\d*$/u.test(index.previewVersion.slice(previewPrefix.length)),
"龙芯预览版本无效",
);
assert(index.architecture === "loong64", "龙芯预览架构无效");
assert(index.format === "deb", "龙芯预览格式无效");
assert(
hasExactKeys(index.artifact, ["name", "size", "sha256", "url"]),
"龙芯预览文件结构无效",
);
const expectedName =
`GoodBuddy-${index.goodBuddyVersion}-linux-loong64-preview.deb`;
assert(
index.artifact.name === expectedName &&
safeFileNamePattern.test(index.artifact.name),
"龙芯预览文件名无效",
);
assert(
Number.isSafeInteger(index.artifact.size) && index.artifact.size > 0,
"龙芯预览文件大小无效",
);
assert(
typeof index.artifact.sha256 === "string" &&
sha256Pattern.test(index.artifact.sha256),
"龙芯预览 SHA-256 无效",
);
const previewBase = new URL(
`v${index.goodBuddyVersion}/`,
loongArchPreviewIndexUrl,
);
assertExactUrl(
index.artifact.url,
new URL(encodeURIComponent(expectedName), previewBase).href,
"龙芯预览文件地址",
);
assertExactUrl(
index.manifestUrl,
new URL("preview-manifest.json", previewBase).href,
"龙芯预览 manifest 地址",
);
assertExactUrl(
index.checksumUrl,
new URL("SHA256SUMS", previewBase).href,
"龙芯预览校验清单地址",
);
return index;
};
window.GoodBuddyReleaseIndex = Object.freeze({
maximumIndexBytes,
maximumPreviewIndexBytes,
validateReleaseIndex,
validateLoongArchPreviewIndex,
});
})();