feat: add LoongArch preview download to the website
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
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
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 预览包;正式下载与更新索引保持不变。
This commit is contained in:
+59
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
正式站点地址:<https://mesalogo.github.io/goodbuddy/>
|
||||
|
||||
首页将 GoodBuddy 定位为“首个支持龙芯的开源桌面 AI 助理产品”和
|
||||
首页将 GoodBuddy 定位为“首个全面覆盖国产化 CPU 架构的开源桌面 AI 助理产品”和
|
||||
“免注册、支持信创软硬件的一站式 AI 助手”,优先展示三大桌面系统、正式
|
||||
x64 / arm64 下载入口、独立编译的龙芯 loong64 预览版、统一 Agent Runtime,
|
||||
以及知识库、魔法笔记、智能心跳、桌面上下文和远程消息通道等桌面助手能力。
|
||||
@@ -89,6 +89,64 @@ https://github.com/mesalogo/goodbuddy/releases/latest
|
||||
`GoodBuddy-${version}-mac-${arch}.dmg|zip`;Linux x64 的 AppImage、DEB 与
|
||||
RPM 分别使用 electron-builder 的 `x86_64`、`amd64` 与 `x86_64` 架构名。
|
||||
Linux arm64 的 AppImage/DEB 使用 `arm64`,RPM 使用 `aarch64`。
|
||||
Linux 处理器选择器另外提供龙芯 `loong64` 实验预览;选中后安装包类型固定为
|
||||
DEB,并改用下述独立预览索引,不会把 `loong64` 注入正式六目标索引。
|
||||
|
||||
### 龙芯实验预览通道
|
||||
|
||||
龙芯包不加入上述正式索引,而使用独立 OSS 前缀:
|
||||
|
||||
```text
|
||||
releases/
|
||||
└── loongarch-preview/
|
||||
├── latest.json
|
||||
└── v<GoodBuddy 版本>/
|
||||
├── GoodBuddy-<版本>-linux-loong64-preview.deb
|
||||
├── preview-manifest.json
|
||||
└── SHA256SUMS
|
||||
```
|
||||
|
||||
版本目录不可变。先上传并公开验证三个版本对象,最后才覆盖
|
||||
`releases/loongarch-preview/latest.json`;不得把预览包放入 `releases/v*`、
|
||||
`releases/latest.json`、标准 GitHub Release 资产或桌面自动更新索引。
|
||||
版本对象建议使用 OSS Standard 存储、继承 Bucket 的公开读取策略,并设置长期
|
||||
immutable 缓存;`latest.json` 应使用 `application/json` 和短缓存或
|
||||
`no-cache`。`SHA256SUMS` 必须只记录 DEB 文件名,不得记录构建容器绝对路径。
|
||||
|
||||
官网只读取以下严格结构,且仅接受北京 OSS
|
||||
`releases/loongarch-preview/v<goodBuddyVersion>/` 下的精确 HTTPS 地址:
|
||||
|
||||
```json
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"product": "GoodBuddy LoongArch Preview",
|
||||
"goodBuddyVersion": "0.11.5",
|
||||
"previewVersion": "0.11.5-loong64-preview.1",
|
||||
"architecture": "loong64",
|
||||
"format": "deb",
|
||||
"artifact": {
|
||||
"name": "GoodBuddy-0.11.5-linux-loong64-preview.deb",
|
||||
"size": 186853872,
|
||||
"sha256": "672321e314fbdb91c6d7b2b549a838e556bd7e597746a3e6d346b936f2ff8369",
|
||||
"url": "https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/loongarch-preview/v0.11.5/GoodBuddy-0.11.5-linux-loong64-preview.deb"
|
||||
},
|
||||
"manifestUrl": "https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/loongarch-preview/v0.11.5/preview-manifest.json",
|
||||
"checksumUrl": "https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/loongarch-preview/v0.11.5/SHA256SUMS"
|
||||
}
|
||||
```
|
||||
|
||||
上传仍应使用 GitHub OIDC 获取的短期 STS 凭据和固定 `ossutil 2.3.0`,Bucket
|
||||
为 `goodbuddy`,Endpoint 为 `https://oss-cn-beijing.aliyuncs.com`,Region
|
||||
为 `cn-beijing`。上传版本对象前先读取目标对象元数据:不存在时才上传,已存在
|
||||
且大小和 SHA-256 相同时跳过,任一字节不同时必须停止;`--update` 只用于传输
|
||||
优化,不能代替不可变性检查。也可为版本前缀启用 OSS 保留策略/WORM。指针仅在
|
||||
所有公开 GET、大小、SHA-256、manifest 和 DEB 元数据检查通过后使用
|
||||
`--force` 更新。手工上传时也应遵循同一顺序,不使用长期 AccessKey。
|
||||
|
||||
若独立索引不存在、超时、过大、字段多缺、版本不稳定、摘要格式错误或任一
|
||||
URL 不匹配受信任前缀,中文官网选择龙芯处理器时会保持下载按钮禁用;Linux
|
||||
x64 / arm64 及另外两个正式平台入口不受影响。英文页面继续只展示正式
|
||||
GitHub Release。
|
||||
|
||||
## 字体与可访问性
|
||||
|
||||
|
||||
+174
-42
@@ -17,6 +17,8 @@
|
||||
const isEnglish = root.lang.toLowerCase().startsWith("en");
|
||||
const releaseManifestUrl =
|
||||
"https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/latest.json";
|
||||
const loongArchPreviewManifestUrl =
|
||||
"https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/loongarch-preview/latest.json";
|
||||
const releaseFallbackUrl =
|
||||
"https://github.com/mesalogo/goodbuddy/releases/latest";
|
||||
const releaseRequestTimeoutMs = 10_000;
|
||||
@@ -24,6 +26,8 @@
|
||||
const downloadCards = [
|
||||
...document.querySelectorAll("[data-download-card]"),
|
||||
];
|
||||
let verifiedRelease;
|
||||
let verifiedLoongArchPreview;
|
||||
const interfaceCopy = isEnglish
|
||||
? {
|
||||
themeDark: "Switch to dark theme",
|
||||
@@ -65,8 +69,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
const readBoundedJson = async (response) => {
|
||||
const maximumBytes = releaseIndexApi?.maximumIndexBytes;
|
||||
const readBoundedJson = async (response, maximumBytes) => {
|
||||
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1) {
|
||||
throw new Error("发布索引大小上限无效");
|
||||
}
|
||||
@@ -111,45 +114,31 @@
|
||||
return JSON.parse(new TextDecoder().decode(bytes));
|
||||
};
|
||||
|
||||
const setReleaseLink = (link, url, visibleText) => {
|
||||
const setLinkText = (link, visibleText) => {
|
||||
const newWindowNotice = link.querySelector(".sr-only");
|
||||
link.href = url;
|
||||
link.replaceChildren(document.createTextNode(visibleText));
|
||||
if (newWindowNotice) {
|
||||
link.append(newWindowNotice);
|
||||
}
|
||||
};
|
||||
|
||||
const setFallbackDownloads = () => {
|
||||
for (const card of downloadCards) {
|
||||
const platform = card.dataset.downloadCard;
|
||||
const link = card.closest(".download-card")?.querySelector("[data-release-link]");
|
||||
const meta = card.closest(".download-card")?.querySelector("[data-download-meta]");
|
||||
if (link instanceof HTMLAnchorElement) {
|
||||
setReleaseLink(
|
||||
link,
|
||||
releaseFallbackUrl,
|
||||
`前往 GitHub 下载 ${platformNames[platform] ?? platform ?? ""} →`,
|
||||
);
|
||||
}
|
||||
if (meta instanceof HTMLElement) {
|
||||
meta.textContent = "请在 GitHub Release 中选择对应的安装文件。";
|
||||
}
|
||||
}
|
||||
const setReleaseLink = (link, url, visibleText) => {
|
||||
link.href = url;
|
||||
link.target = "_blank";
|
||||
link.rel = "noreferrer";
|
||||
link.removeAttribute("aria-disabled");
|
||||
link.removeAttribute("tabindex");
|
||||
link.classList.remove("is-disabled");
|
||||
setLinkText(link, visibleText);
|
||||
};
|
||||
|
||||
const configureDownloads = (payload) => {
|
||||
if (!releaseIndexApi?.validateReleaseIndex) {
|
||||
throw new Error("发布索引校验器不可用");
|
||||
}
|
||||
const release = releaseIndexApi.validateReleaseIndex(payload);
|
||||
|
||||
const updateCard = (card) => {
|
||||
const getDownloadCardControls = (card) => {
|
||||
const platform = card.dataset.downloadCard;
|
||||
const archSelect = card.querySelector("[data-download-arch]");
|
||||
const formatSelect = card.querySelector("[data-download-format]");
|
||||
const link = card.closest(".download-card")?.querySelector("[data-release-link]");
|
||||
const meta = card.closest(".download-card")?.querySelector("[data-download-meta]");
|
||||
const container = card.closest(".download-card");
|
||||
const link = container?.querySelector("[data-release-link]");
|
||||
const meta = container?.querySelector("[data-download-meta]");
|
||||
if (
|
||||
!platform ||
|
||||
!(archSelect instanceof HTMLSelectElement) ||
|
||||
@@ -159,13 +148,80 @@
|
||||
) {
|
||||
throw new Error("下载卡片结构无效");
|
||||
}
|
||||
return { platform, archSelect, formatSelect, link, meta };
|
||||
};
|
||||
|
||||
const target = release.targets[`${platform}-${archSelect.value}`];
|
||||
const setDownloadUnavailable = (link, meta) => {
|
||||
link.removeAttribute("href");
|
||||
link.removeAttribute("target");
|
||||
link.removeAttribute("rel");
|
||||
link.setAttribute("aria-disabled", "true");
|
||||
link.tabIndex = -1;
|
||||
link.classList.add("is-disabled");
|
||||
setLinkText(link, "龙芯预览包暂不可用");
|
||||
meta.textContent =
|
||||
"实验预览 · 仅 DEB · 索引尚未发布或校验失败;" +
|
||||
"尚未完成真机验证。";
|
||||
};
|
||||
|
||||
const setLoongArchFormat = (formatSelect, selected) => {
|
||||
for (const option of formatSelect.options) {
|
||||
const unavailable = selected && option.value !== "deb";
|
||||
option.hidden = unavailable;
|
||||
option.disabled = unavailable;
|
||||
}
|
||||
if (selected) {
|
||||
formatSelect.value = "deb";
|
||||
}
|
||||
formatSelect.disabled = selected;
|
||||
};
|
||||
|
||||
const updateDownloadCard = (card) => {
|
||||
const {
|
||||
platform,
|
||||
archSelect,
|
||||
formatSelect,
|
||||
link,
|
||||
meta,
|
||||
} = getDownloadCardControls(card);
|
||||
const loongArchSelected =
|
||||
platform === "linux" && archSelect.value === "loong64";
|
||||
setLoongArchFormat(formatSelect, loongArchSelected);
|
||||
|
||||
if (loongArchSelected) {
|
||||
if (!verifiedLoongArchPreview) {
|
||||
setDownloadUnavailable(link, meta);
|
||||
return;
|
||||
}
|
||||
setReleaseLink(
|
||||
link,
|
||||
verifiedLoongArchPreview.artifact.url,
|
||||
"下载 Linux 龙芯 LoongArch DEB 预览版 →",
|
||||
);
|
||||
meta.textContent =
|
||||
`GoodBuddy ${verifiedLoongArchPreview.goodBuddyVersion} · ` +
|
||||
`${formatFileSize(verifiedLoongArchPreview.artifact.size)} · ` +
|
||||
`实验预览 · 未完成龙芯真机验证 · SHA-256 ` +
|
||||
`${verifiedLoongArchPreview.artifact.sha256.slice(0, 12)}…`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!verifiedRelease) {
|
||||
setReleaseLink(
|
||||
link,
|
||||
releaseFallbackUrl,
|
||||
`前往 GitHub 下载 ${platformNames[platform] ?? platform} →`,
|
||||
);
|
||||
meta.textContent = "请在 GitHub Release 中选择对应的安装文件。";
|
||||
return;
|
||||
}
|
||||
|
||||
const target =
|
||||
verifiedRelease.targets[`${platform}-${archSelect.value}`];
|
||||
const file = target?.files?.[formatSelect.value];
|
||||
if (!file) {
|
||||
throw new Error("下载选项不在已校验的发布索引中");
|
||||
}
|
||||
|
||||
const platformName = platformNames[platform] ?? platform;
|
||||
const archName =
|
||||
platform === "macos" && archSelect.value === "arm64"
|
||||
@@ -180,33 +236,81 @@
|
||||
`下载 ${platformName} ${archName} ${formatName} →`,
|
||||
);
|
||||
meta.textContent =
|
||||
`GoodBuddy ${release.version} · ${formatFileSize(file.size)} · ` +
|
||||
`GoodBuddy ${verifiedRelease.version} · ` +
|
||||
`${formatFileSize(file.size)} · ` +
|
||||
`${archSelect.options[archSelect.selectedIndex]?.text ?? archSelect.value}`;
|
||||
};
|
||||
|
||||
const updateAllDownloadCards = () => {
|
||||
for (const card of downloadCards) {
|
||||
const selects = card.querySelectorAll("select");
|
||||
for (const select of selects) {
|
||||
updateDownloadCard(card);
|
||||
}
|
||||
};
|
||||
|
||||
const setFallbackDownloads = () => {
|
||||
verifiedRelease = undefined;
|
||||
updateAllDownloadCards();
|
||||
};
|
||||
|
||||
const setLoongArchPreviewUnavailable = () => {
|
||||
verifiedLoongArchPreview = undefined;
|
||||
const linuxCard = downloadCards.find(
|
||||
(card) => card.dataset.downloadCard === "linux",
|
||||
);
|
||||
if (linuxCard) {
|
||||
updateDownloadCard(linuxCard);
|
||||
}
|
||||
};
|
||||
|
||||
const configureLoongArchPreview = (payload) => {
|
||||
if (!releaseIndexApi?.validateLoongArchPreviewIndex) {
|
||||
throw new Error("龙芯预览索引校验器不可用");
|
||||
}
|
||||
verifiedLoongArchPreview =
|
||||
releaseIndexApi.validateLoongArchPreviewIndex(payload);
|
||||
const linuxCard = downloadCards.find(
|
||||
(card) => card.dataset.downloadCard === "linux",
|
||||
);
|
||||
if (linuxCard) {
|
||||
updateDownloadCard(linuxCard);
|
||||
}
|
||||
};
|
||||
|
||||
const configureDownloads = (payload) => {
|
||||
if (!releaseIndexApi?.validateReleaseIndex) {
|
||||
throw new Error("发布索引校验器不可用");
|
||||
}
|
||||
verifiedRelease = releaseIndexApi.validateReleaseIndex(payload);
|
||||
updateAllDownloadCards();
|
||||
};
|
||||
|
||||
const initializeDownloadCards = () => {
|
||||
for (const card of downloadCards) {
|
||||
for (const select of card.querySelectorAll("select")) {
|
||||
select.addEventListener("change", () => {
|
||||
try {
|
||||
updateCard(card);
|
||||
updateDownloadCard(card);
|
||||
} catch {
|
||||
setFallbackDownloads();
|
||||
}
|
||||
});
|
||||
}
|
||||
updateCard(card);
|
||||
updateDownloadCard(card);
|
||||
}
|
||||
};
|
||||
|
||||
const loadRelease = async () => {
|
||||
const fetchBoundedIndex = async (
|
||||
url,
|
||||
maximumBytes,
|
||||
errorLabel,
|
||||
) => {
|
||||
const controller = new AbortController();
|
||||
const timeout = window.setTimeout(
|
||||
() => controller.abort(),
|
||||
releaseRequestTimeoutMs,
|
||||
);
|
||||
try {
|
||||
const response = await fetch(releaseManifestUrl, {
|
||||
const response = await fetch(url, {
|
||||
cache: "no-store",
|
||||
credentials: "omit",
|
||||
redirect: "error",
|
||||
@@ -214,16 +318,42 @@
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`发布索引请求失败:${response.status}`);
|
||||
throw new Error(`${errorLabel}请求失败:${response.status}`);
|
||||
}
|
||||
configureDownloads(await readBoundedJson(response));
|
||||
} catch {
|
||||
setFallbackDownloads();
|
||||
return await readBoundedJson(response, maximumBytes);
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
|
||||
const loadRelease = async () => {
|
||||
try {
|
||||
configureDownloads(
|
||||
await fetchBoundedIndex(
|
||||
releaseManifestUrl,
|
||||
releaseIndexApi?.maximumIndexBytes,
|
||||
"发布索引",
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
setFallbackDownloads();
|
||||
}
|
||||
};
|
||||
|
||||
const loadLoongArchPreview = async () => {
|
||||
try {
|
||||
configureLoongArchPreview(
|
||||
await fetchBoundedIndex(
|
||||
loongArchPreviewManifestUrl,
|
||||
releaseIndexApi?.maximumPreviewIndexBytes,
|
||||
"龙芯预览索引",
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
setLoongArchPreviewUnavailable();
|
||||
}
|
||||
};
|
||||
|
||||
const getSavedTheme = () => {
|
||||
try {
|
||||
const savedTheme = localStorage.getItem("goodbuddy-site-theme");
|
||||
@@ -311,8 +441,10 @@
|
||||
|
||||
applyTheme(getSavedTheme() ?? (systemTheme.matches ? "dark" : "light"));
|
||||
setHeaderState();
|
||||
initializeDownloadCards();
|
||||
if (!isEnglish) {
|
||||
void loadRelease();
|
||||
void loadLoongArchPreview();
|
||||
}
|
||||
|
||||
themeToggle?.addEventListener("click", () => {
|
||||
|
||||
+6
-5
@@ -5,14 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="description"
|
||||
content="GoodBuddy 是首个支持龙芯的开源桌面 AI 助理产品,也是免注册、支持信创软硬件的一站式 AI 助手;龙芯 LoongArch 当前提供独立编译预览版。"
|
||||
content="GoodBuddy 是首个全面覆盖国产化 CPU 架构的开源桌面 AI 助理产品,也是免注册、支持信创软硬件的一站式 AI 助手;龙芯 LoongArch 当前提供独立编译预览版。"
|
||||
/>
|
||||
<link rel="canonical" href="https://mesalogo.github.io/goodbuddy/" />
|
||||
<link rel="alternate" hreflang="zh-CN" href="https://mesalogo.github.io/goodbuddy/" />
|
||||
<link rel="alternate" hreflang="en" href="https://mesalogo.github.io/goodbuddy/en.html" />
|
||||
<link rel="alternate" hreflang="x-default" href="https://mesalogo.github.io/goodbuddy/en.html" />
|
||||
<meta name="theme-color" content="#f6f8fb" />
|
||||
<title>GoodBuddy|首个支持龙芯的开源桌面 AI 助理产品</title>
|
||||
<title>GoodBuddy|首个全面覆盖国产化 CPU 架构的开源桌面 AI 助理产品</title>
|
||||
<link
|
||||
rel="icon"
|
||||
href="./assets/goodbuddy-light.png"
|
||||
@@ -124,7 +124,7 @@
|
||||
<span>一站式 AI 助手。</span>
|
||||
</h1>
|
||||
<p class="hero-lead">
|
||||
GoodBuddy 是首个支持龙芯的开源桌面 AI 助理产品,龙芯 LoongArch
|
||||
GoodBuddy 是首个全面覆盖国产化 CPU 架构的开源桌面 AI 助理产品,龙芯 LoongArch
|
||||
当前以独立编译预览版提供。作为桌面助手与 AI 编程工具台,GoodBuddy
|
||||
管理对话、知识、笔记与任务,
|
||||
也通过独创的统一 Agent Runtime 接入直连模型、OpenCode、Continue 和 DeepSeek Harness。
|
||||
@@ -327,6 +327,7 @@
|
||||
<select data-download-arch aria-label="Linux 处理器架构">
|
||||
<option value="x64">x64(Intel / AMD)</option>
|
||||
<option value="arm64">ARM64</option>
|
||||
<option value="loong64">龙芯 LoongArch(实验预览)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
@@ -353,7 +354,7 @@
|
||||
<aside class="domestic-support" aria-labelledby="domestic-support-title">
|
||||
<div>
|
||||
<p class="kicker">信创软硬件支持</p>
|
||||
<h3 id="domestic-support-title">首个支持龙芯的开源桌面 AI 助理产品</h3>
|
||||
<h3 id="domestic-support-title">首个全面覆盖国产化 CPU 架构的开源桌面 AI 助理产品</h3>
|
||||
<p>
|
||||
正式下载覆盖国产 x64 与 ARM64;龙芯 LoongArch 使用独立编译的
|
||||
loong64 预览版。
|
||||
@@ -525,7 +526,7 @@
|
||||
</span>
|
||||
<span>GoodBuddy</span>
|
||||
</a>
|
||||
<p>首个支持龙芯的开源桌面 AI 助理产品</p>
|
||||
<p>首个全面覆盖国产化 CPU 架构的开源桌面 AI 助理产品</p>
|
||||
<div class="footer-links">
|
||||
<a href="#download">下载</a>
|
||||
<a href="#features">Agent Runtime</a>
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
|
||||
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;
|
||||
@@ -201,8 +204,93 @@
|
||||
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,
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -86,6 +86,99 @@ const renderApp = (legacy = false) => {
|
||||
return { dom, queries, window };
|
||||
};
|
||||
|
||||
const renderLoongArchPreview = (payload) => {
|
||||
const dom = new JSDOM(
|
||||
`<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta name="theme-color" content="#f6f8fb"></head>
|
||||
<body>
|
||||
<header data-site-header>
|
||||
<button type="button" data-menu-toggle>菜单</button>
|
||||
<nav data-navigation></nav>
|
||||
<button type="button" data-theme-toggle>主题</button>
|
||||
</header>
|
||||
<div data-menu-backdrop></div>
|
||||
<main>
|
||||
<article class="download-card">
|
||||
<div data-download-card="linux">
|
||||
<select data-download-arch>
|
||||
<option value="x64">x64</option>
|
||||
<option value="arm64">ARM64</option>
|
||||
<option value="loong64">龙芯 LoongArch(实验预览)</option>
|
||||
</select>
|
||||
<select data-download-format>
|
||||
<option value="AppImage">AppImage</option>
|
||||
<option value="deb">DEB</option>
|
||||
<option value="rpm">RPM</option>
|
||||
</select>
|
||||
</div>
|
||||
<a
|
||||
class="button button--download"
|
||||
href="https://github.com/mesalogo/goodbuddy/releases/latest"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
data-release-link
|
||||
>下载 Linux 版<span class="sr-only">(在新窗口打开)</span></a>
|
||||
<p data-download-meta></p>
|
||||
</article>
|
||||
</main>
|
||||
<span data-current-year></span>
|
||||
</body>
|
||||
</html>`,
|
||||
{
|
||||
runScripts: "outside-only",
|
||||
url: "https://example.test/",
|
||||
},
|
||||
);
|
||||
const { window } = dom;
|
||||
createMediaQueries(window, false);
|
||||
window.TextDecoder = TextDecoder;
|
||||
window.GoodBuddyReleaseIndex = {
|
||||
maximumIndexBytes: 512 * 1024,
|
||||
maximumPreviewIndexBytes: 16 * 1024,
|
||||
validateReleaseIndex() {
|
||||
throw new Error("正式发布索引在此测试中不可用");
|
||||
},
|
||||
validateLoongArchPreviewIndex(value) {
|
||||
return value;
|
||||
},
|
||||
};
|
||||
window.fetch = async (url) => {
|
||||
if (!String(url).includes("/loongarch-preview/")) {
|
||||
throw new Error("正式发布索引在此测试中不可用");
|
||||
}
|
||||
if (!payload) {
|
||||
throw new Error("龙芯预览索引不可用");
|
||||
}
|
||||
const bytes = new TextEncoder().encode(JSON.stringify(payload));
|
||||
let delivered = false;
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: {
|
||||
get(name) {
|
||||
return name === "content-length" ? String(bytes.length) : null;
|
||||
},
|
||||
},
|
||||
body: {
|
||||
getReader() {
|
||||
return {
|
||||
async read() {
|
||||
if (delivered) {
|
||||
return { done: true, value: undefined };
|
||||
}
|
||||
delivered = true;
|
||||
return { done: false, value: bytes };
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
window.eval(source);
|
||||
return { dom, window };
|
||||
};
|
||||
|
||||
for (const legacy of [false, true]) {
|
||||
test(
|
||||
`mobile menu isolates content and restores focus with ${
|
||||
@@ -149,3 +242,71 @@ for (const legacy of [false, true]) {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
test("enables only a validated LoongArch preview download", async () => {
|
||||
const artifactUrl =
|
||||
"https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/loongarch-preview/v0.11.5/GoodBuddy-0.11.5-linux-loong64-preview.deb";
|
||||
const { dom, window } = renderLoongArchPreview({
|
||||
goodBuddyVersion: "0.11.5",
|
||||
artifact: {
|
||||
size: 186_853_872,
|
||||
sha256: "a".repeat(64),
|
||||
url: artifactUrl,
|
||||
},
|
||||
});
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||
|
||||
const arch = window.document.querySelector("[data-download-arch]");
|
||||
const format = window.document.querySelector(
|
||||
"[data-download-format]",
|
||||
);
|
||||
const link = window.document.querySelector("[data-release-link]");
|
||||
const meta = window.document.querySelector("[data-download-meta]");
|
||||
arch.value = "loong64";
|
||||
arch.dispatchEvent(new window.Event("change"));
|
||||
|
||||
assert.equal(format.value, "deb");
|
||||
assert.equal(format.disabled, true);
|
||||
assert.equal(format.options[0].hidden, true);
|
||||
assert.equal(format.options[1].hidden, false);
|
||||
assert.equal(format.options[2].hidden, true);
|
||||
assert.equal(link.getAttribute("href"), artifactUrl);
|
||||
assert.equal(link.getAttribute("target"), "_blank");
|
||||
assert.equal(link.getAttribute("rel"), "noreferrer");
|
||||
assert.equal(link.hasAttribute("aria-disabled"), false);
|
||||
assert.equal(link.classList.contains("is-disabled"), false);
|
||||
assert.match(meta.textContent, /GoodBuddy 0\.11\.5/);
|
||||
assert.match(meta.textContent, /178 MB/);
|
||||
assert.match(meta.textContent, /未完成龙芯真机验证/);
|
||||
dom.window.close();
|
||||
});
|
||||
|
||||
test("keeps the LoongArch preview disabled when its index is unavailable", async () => {
|
||||
const { dom, window } = renderLoongArchPreview(null);
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 0));
|
||||
|
||||
const arch = window.document.querySelector("[data-download-arch]");
|
||||
const format = window.document.querySelector(
|
||||
"[data-download-format]",
|
||||
);
|
||||
const link = window.document.querySelector("[data-release-link]");
|
||||
const meta = window.document.querySelector("[data-download-meta]");
|
||||
arch.value = "loong64";
|
||||
arch.dispatchEvent(new window.Event("change"));
|
||||
|
||||
assert.equal(format.value, "deb");
|
||||
assert.equal(format.disabled, true);
|
||||
assert.equal(link.hasAttribute("href"), false);
|
||||
assert.equal(link.getAttribute("aria-disabled"), "true");
|
||||
assert.equal(link.tabIndex, -1);
|
||||
assert.equal(link.classList.contains("is-disabled"), true);
|
||||
assert.match(meta.textContent, /尚未发布或校验失败/);
|
||||
|
||||
arch.value = "x64";
|
||||
arch.dispatchEvent(new window.Event("change"));
|
||||
assert.equal(format.disabled, false);
|
||||
assert.equal(format.options[0].hidden, false);
|
||||
assert.equal(link.getAttribute("href"), "https://github.com/mesalogo/goodbuddy/releases/latest");
|
||||
assert.equal(link.hasAttribute("aria-disabled"), false);
|
||||
dom.window.close();
|
||||
});
|
||||
|
||||
@@ -9,7 +9,10 @@ const { test } = process.env.VITEST
|
||||
const source = await readFile(path.resolve("sites/release-index.js"), "utf8");
|
||||
const context = vm.createContext({ URL, window: {} });
|
||||
vm.runInContext(source, context, { filename: "release-index.js" });
|
||||
const { validateReleaseIndex } = context.window.GoodBuddyReleaseIndex;
|
||||
const {
|
||||
validateReleaseIndex,
|
||||
validateLoongArchPreviewIndex,
|
||||
} = context.window.GoodBuddyReleaseIndex;
|
||||
|
||||
const definitions = [
|
||||
["windows", "x64", ["nsis", "portable"]],
|
||||
@@ -90,6 +93,36 @@ const expectRejected = (mutate) => {
|
||||
assert.throws(() => validateReleaseIndex(index));
|
||||
};
|
||||
|
||||
const validLoongArchPreviewIndex = () => {
|
||||
const goodBuddyVersion = "1.2.3";
|
||||
const previewBase =
|
||||
`https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/loongarch-preview/v${goodBuddyVersion}/`;
|
||||
const name =
|
||||
`GoodBuddy-${goodBuddyVersion}-linux-loong64-preview.deb`;
|
||||
return {
|
||||
formatVersion: 1,
|
||||
product: "GoodBuddy LoongArch Preview",
|
||||
goodBuddyVersion,
|
||||
previewVersion: `${goodBuddyVersion}-loong64-preview.1`,
|
||||
architecture: "loong64",
|
||||
format: "deb",
|
||||
artifact: {
|
||||
name,
|
||||
size: 186_000_000,
|
||||
sha256: "a".repeat(64),
|
||||
url: new URL(encodeURIComponent(name), previewBase).href,
|
||||
},
|
||||
manifestUrl: new URL("preview-manifest.json", previewBase).href,
|
||||
checksumUrl: new URL("SHA256SUMS", previewBase).href,
|
||||
};
|
||||
};
|
||||
|
||||
const expectLoongArchPreviewRejected = (mutate) => {
|
||||
const index = validLoongArchPreviewIndex();
|
||||
mutate(index);
|
||||
assert.throws(() => validateLoongArchPreviewIndex(index));
|
||||
};
|
||||
|
||||
test("accepts the canonical stable six-target release index", () => {
|
||||
const index = validIndex();
|
||||
assert.equal(validateReleaseIndex(index), index);
|
||||
@@ -204,3 +237,49 @@ test("requires exact checksum and GitHub fallback URLs", () => {
|
||||
index.fallbackUrl = "https://github.com/mesalogo/goodbuddy/releases";
|
||||
});
|
||||
});
|
||||
|
||||
test("accepts the isolated immutable LoongArch preview index", () => {
|
||||
const index = validLoongArchPreviewIndex();
|
||||
assert.equal(validateLoongArchPreviewIndex(index), index);
|
||||
});
|
||||
|
||||
test("binds the LoongArch preview to its stable version and exact OSS prefix", () => {
|
||||
for (const version of ["v1.2.3", "1.2.3-rc.1", "1.2"]) {
|
||||
expectLoongArchPreviewRejected((index) => {
|
||||
index.goodBuddyVersion = version;
|
||||
});
|
||||
}
|
||||
expectLoongArchPreviewRejected((index) => {
|
||||
index.previewVersion = "1.2.3-loong64-preview.0";
|
||||
});
|
||||
expectLoongArchPreviewRejected((index) => {
|
||||
index.artifact.url = index.artifact.url.replace(
|
||||
"/releases/loongarch-preview/v1.2.3/",
|
||||
"/releases/v1.2.3/",
|
||||
);
|
||||
});
|
||||
expectLoongArchPreviewRejected((index) => {
|
||||
index.manifestUrl += "?download=1";
|
||||
});
|
||||
expectLoongArchPreviewRejected((index) => {
|
||||
index.unexpected = true;
|
||||
});
|
||||
});
|
||||
|
||||
test("requires one canonical LoongArch DEB with bounded metadata", () => {
|
||||
expectLoongArchPreviewRejected((index) => {
|
||||
index.architecture = "arm64";
|
||||
});
|
||||
expectLoongArchPreviewRejected((index) => {
|
||||
index.format = "rpm";
|
||||
});
|
||||
expectLoongArchPreviewRejected((index) => {
|
||||
index.artifact.name = "GoodBuddy-1.2.3-linux-loong64.deb";
|
||||
});
|
||||
expectLoongArchPreviewRejected((index) => {
|
||||
index.artifact.size = 0;
|
||||
});
|
||||
expectLoongArchPreviewRejected((index) => {
|
||||
index.artifact.sha256 = "A".repeat(64);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -250,8 +250,9 @@ for (const breakpoint of ["1199px", "959px", "719px"]) {
|
||||
const requiredCopy = [
|
||||
"免注册",
|
||||
"支持信创软硬件的一站式 AI 助手",
|
||||
"首个支持龙芯的开源桌面 AI 助理产品",
|
||||
"首个全面覆盖国产化 CPU 架构的开源桌面 AI 助理产品",
|
||||
"龙芯 LoongArch 独立编译预览版",
|
||||
"龙芯 LoongArch(实验预览)",
|
||||
"桌面助手",
|
||||
"AI 编程工具台",
|
||||
"Windows、macOS、Linux",
|
||||
@@ -328,6 +329,18 @@ report(
|
||||
(html.match(/data-download-format/g) ?? []).length === 3,
|
||||
"每个平台必须提供安装包类型选择器",
|
||||
);
|
||||
report(
|
||||
/<option\s+value="loong64">龙芯 LoongArch(实验预览)<\/option>/.test(
|
||||
html,
|
||||
),
|
||||
"Linux 处理器选项必须包含龙芯实验预览",
|
||||
);
|
||||
report(
|
||||
/const setLoongArchFormat =[\s\S]*option\.value !== "deb"[\s\S]*formatSelect\.value = "deb"[\s\S]*formatSelect\.disabled = selected/.test(
|
||||
appJs,
|
||||
),
|
||||
"龙芯处理器必须只允许选择 DEB",
|
||||
);
|
||||
report(
|
||||
!/data-release-status|download-release-status/.test(`${html}\n${englishHtml}\n${css}\n${appJs}`),
|
||||
"官网不得显示下载源状态提示",
|
||||
@@ -362,6 +375,27 @@ report(
|
||||
),
|
||||
"官网必须从 GoodBuddy OSS 加载最新发布索引",
|
||||
);
|
||||
report(
|
||||
appJs.includes(
|
||||
"https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/loongarch-preview/latest.json",
|
||||
),
|
||||
"官网必须从独立 OSS 索引加载龙芯预览包",
|
||||
);
|
||||
report(
|
||||
/validateLoongArchPreviewIndex\(payload\)/.test(appJs),
|
||||
"龙芯预览下载链接必须先通过独立索引校验",
|
||||
);
|
||||
report(
|
||||
/catch\s*\{[\s\S]*setLoongArchPreviewUnavailable\(\)/.test(appJs),
|
||||
"龙芯预览索引失败时必须保持下载入口禁用",
|
||||
);
|
||||
report(
|
||||
releaseIndexJs.includes(
|
||||
"https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/loongarch-preview/latest.json",
|
||||
) &&
|
||||
/const validateLoongArchPreviewIndex =/.test(releaseIndexJs),
|
||||
"龙芯预览索引必须使用独立受信任前缀和校验器",
|
||||
);
|
||||
report(
|
||||
appJs.includes("https://github.com/mesalogo/goodbuddy/releases/latest"),
|
||||
"官网必须保留 GitHub Release 回退地址",
|
||||
@@ -502,7 +536,7 @@ const expectedDownloadOptions = {
|
||||
formats: ["dmg", "zip"],
|
||||
},
|
||||
linux: {
|
||||
arches: ["x64", "arm64"],
|
||||
arches: ["x64", "arm64", "loong64"],
|
||||
formats: ["AppImage", "deb", "rpm"],
|
||||
},
|
||||
};
|
||||
|
||||
+8
-1
@@ -1274,7 +1274,7 @@ p {
|
||||
|
||||
.download-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
@@ -1385,6 +1385,13 @@ p {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.button--download.is-disabled {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.download-meta {
|
||||
grid-column: 1 / -1;
|
||||
min-height: 18px;
|
||||
|
||||
Reference in New Issue
Block a user