From d9a554b4b1e5cffc082767a2cfa30a71697f9b28 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Wed, 26 Aug 2026 23:23:27 +0800 Subject: [PATCH] feat: add LoongArch preview download to the website MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 预览包;正式下载与更新索引保持不变。 --- sites/README.md | 60 +++++- sites/app.js | 270 ++++++++++++++++++++------- sites/index.html | 11 +- sites/release-index.js | 88 +++++++++ sites/scripts/app.test.mjs | 161 ++++++++++++++++ sites/scripts/release-index.test.mjs | 81 +++++++- sites/scripts/validate.mjs | 38 +++- sites/styles.css | 9 +- 8 files changed, 639 insertions(+), 79 deletions(-) diff --git a/sites/README.md b/sites/README.md index 316a92e1..e159183e 100644 --- a/sites/README.md +++ b/sites/README.md @@ -4,7 +4,7 @@ 正式站点地址: -首页将 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-<版本>-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/` 下的精确 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。 ## 字体与可访问性 diff --git a/sites/app.js b/sites/app.js index e2547d52..e62bffb1 100644 --- a/sites/app.js +++ b/sites/app.js @@ -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,30 +114,165 @@ 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 = () => { + 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 getDownloadCardControls = (card) => { + const platform = card.dataset.downloadCard; + const archSelect = card.querySelector("[data-download-arch]"); + const formatSelect = card.querySelector("[data-download-format]"); + 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) || + !(formatSelect instanceof HTMLSelectElement) || + !(link instanceof HTMLAnchorElement) || + !(meta instanceof HTMLElement) + ) { + throw new Error("下载卡片结构无效"); + } + return { platform, archSelect, formatSelect, link, meta }; + }; + + 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" + ? "Apple 芯片" + : archSelect.value === "arm64" + ? "ARM64" + : "x64"; + const formatName = formatNames[formatSelect.value] ?? formatSelect.value; + setReleaseLink( + link, + file.url, + `下载 ${platformName} ${archName} ${formatName} →`, + ); + meta.textContent = + `GoodBuddy ${verifiedRelease.version} · ` + + `${formatFileSize(file.size)} · ` + + `${archSelect.options[archSelect.selectedIndex]?.text ?? archSelect.value}`; + }; + + const updateAllDownloadCards = () => { 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 中选择对应的安装文件。"; - } + 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); } }; @@ -142,71 +280,37 @@ if (!releaseIndexApi?.validateReleaseIndex) { throw new Error("发布索引校验器不可用"); } - const release = releaseIndexApi.validateReleaseIndex(payload); - - const updateCard = (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]"); - if ( - !platform || - !(archSelect instanceof HTMLSelectElement) || - !(formatSelect instanceof HTMLSelectElement) || - !(link instanceof HTMLAnchorElement) || - !(meta instanceof HTMLElement) - ) { - throw new Error("下载卡片结构无效"); - } - - const target = release.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" - ? "Apple 芯片" - : archSelect.value === "arm64" - ? "ARM64" - : "x64"; - const formatName = formatNames[formatSelect.value] ?? formatSelect.value; - setReleaseLink( - link, - file.url, - `下载 ${platformName} ${archName} ${formatName} →`, - ); - meta.textContent = - `GoodBuddy ${release.version} · ${formatFileSize(file.size)} · ` + - `${archSelect.options[archSelect.selectedIndex]?.text ?? archSelect.value}`; - }; + verifiedRelease = releaseIndexApi.validateReleaseIndex(payload); + updateAllDownloadCards(); + }; + const initializeDownloadCards = () => { for (const card of downloadCards) { - const selects = card.querySelectorAll("select"); - for (const select of selects) { + 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", () => { diff --git a/sites/index.html b/sites/index.html index a48199dd..655308a5 100644 --- a/sites/index.html +++ b/sites/index.html @@ -5,14 +5,14 @@ - GoodBuddy|首个支持龙芯的开源桌面 AI 助理产品 + GoodBuddy|首个全面覆盖国产化 CPU 架构的开源桌面 AI 助理产品 一站式 AI 助手。

- GoodBuddy 是首个支持龙芯的开源桌面 AI 助理产品,龙芯 LoongArch + GoodBuddy 是首个全面覆盖国产化 CPU 架构的开源桌面 AI 助理产品,龙芯 LoongArch 当前以独立编译预览版提供。作为桌面助手与 AI 编程工具台,GoodBuddy 管理对话、知识、笔记与任务, 也通过独创的统一 Agent Runtime 接入直连模型、OpenCode、Continue 和 DeepSeek Harness。 @@ -327,6 +327,7 @@