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