diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index b210619..f94398d 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -131,14 +131,17 @@ jobs: retention-days: 30 release: - name: Publish GitHub Release + name: Publish GitHub and OSS release if: github.event_name == 'push' && github.ref_type == 'tag' needs: package runs-on: ubuntu-24.04 - timeout-minutes: 20 + timeout-minutes: 35 + environment: + name: aliyun-oss-release permissions: contents: write actions: read + id-token: write steps: - uses: actions/checkout@v7 @@ -181,7 +184,102 @@ jobs: - name: Verify and aggregate release assets run: node build/aggregate-release.cjs --input dist/release-downloads --output dist/release-upload - - name: Create or update draft release + - name: Verify OSS release configuration + shell: bash + env: + OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }} + OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }} + OIDC_PROVIDER_ARN: ${{ vars.ALIYUN_OIDC_PROVIDER_ARN }} + ROLE_ARN: ${{ vars.ALIYUN_ROLE_ARN }} + run: | + set -euo pipefail + test -n "$OSS_BUCKET" + test -n "$OSS_ENDPOINT" + test -n "$OIDC_PROVIDER_ARN" + test -n "$ROLE_ARN" + case "$OSS_BUCKET" in + *[!a-z0-9-]*|'') echo "OSS Bucket 名称无效" >&2; exit 1 ;; + esac + case "$OSS_ENDPOINT" in + https://oss-*.aliyuncs.com) ;; + *) echo "OSS Endpoint 必须使用标准 HTTPS 地址" >&2; exit 1 ;; + esac + case "$OIDC_PROVIDER_ARN" in + acs:ram::*:oidc-provider/*) ;; + *) echo "OIDC Provider ARN 无效" >&2; exit 1 ;; + esac + case "$ROLE_ARN" in + acs:ram::*:role/*) ;; + *) echo "RAM Role ARN 无效" >&2; exit 1 ;; + esac + + - name: Authenticate to Alibaba Cloud + uses: aliyun/configure-aliyun-credentials-action@v1 + with: + role-to-assume: ${{ vars.ALIYUN_ROLE_ARN }} + oidc-provider-arn: ${{ vars.ALIYUN_OIDC_PROVIDER_ARN }} + role-session-name: goodbuddy-release-${{ github.run_id }} + role-session-expiration: 3600 + audience: sts.aliyuncs.com + + - name: Install ossutil + shell: bash + run: | + set -euo pipefail + version="2.3.0" + archive="$RUNNER_TEMP/ossutil.zip" + directory="$RUNNER_TEMP/ossutil" + curl --fail --silent --show-error --location \ + "https://gosspublic.alicdn.com/ossutil/v2/$version/ossutil-$version-linux-amd64.zip" \ + --output "$archive" + mkdir "$directory" + unzip -q "$archive" -d "$directory" + binary="$(find "$directory" -type f -name ossutil -print -quit)" + test -n "$binary" + chmod +x "$binary" + echo "$(dirname "$binary")" >> "$GITHUB_PATH" + + - name: Prepare OSS website release index + id: oss-release + shell: bash + env: + OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }} + OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }} + run: | + set -euo pipefail + endpoint_host="${OSS_ENDPOINT#https://}" + base_url="https://${OSS_BUCKET}.${endpoint_host}/releases/${GITHUB_REF_NAME}/" + node build/create-site-release.cjs \ + --manifest dist/release-upload/release-manifest.json \ + --base-url "$base_url" \ + --output dist/site-release.json + echo "base-url=$base_url" >> "$GITHUB_OUTPUT" + + - name: Upload immutable release assets to OSS + shell: bash + env: + OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }} + OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }} + run: | + set -euo pipefail + export OSS_ACCESS_KEY_ID="$ALIBABA_CLOUD_ACCESS_KEY_ID" + export OSS_ACCESS_KEY_SECRET="$ALIBABA_CLOUD_ACCESS_KEY_SECRET" + export OSS_SESSION_TOKEN="$ALIBABA_CLOUD_SECURITY_TOKEN" + test -n "$OSS_ACCESS_KEY_ID" + test -n "$OSS_ACCESS_KEY_SECRET" + test -n "$OSS_SESSION_TOKEN" + for file in dist/release-upload/* dist/site-release.json; do + name="$(basename "$file")" + ossutil cp "$file" \ + "oss://${OSS_BUCKET}/releases/${GITHUB_REF_NAME}/${name}" \ + --endpoint "$OSS_ENDPOINT" \ + --update + done + + - name: Verify public OSS release assets + run: node build/verify-site-release.cjs --manifest dist/site-release.json + + - name: Create or update draft GitHub release shell: bash env: GH_TOKEN: ${{ github.token }} @@ -196,3 +294,18 @@ jobs: fi gh release upload "$tag" dist/release-upload/* --clobber gh release edit "$tag" --draft=false --latest + + - name: Point website to verified OSS release + shell: bash + env: + OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }} + OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }} + run: | + set -euo pipefail + export OSS_ACCESS_KEY_ID="$ALIBABA_CLOUD_ACCESS_KEY_ID" + export OSS_ACCESS_KEY_SECRET="$ALIBABA_CLOUD_ACCESS_KEY_SECRET" + export OSS_SESSION_TOKEN="$ALIBABA_CLOUD_SECURITY_TOKEN" + ossutil cp dist/site-release.json \ + "oss://${OSS_BUCKET}/releases/latest.json" \ + --endpoint "$OSS_ENDPOINT" \ + --force diff --git a/BUILD.md b/BUILD.md index 302e57c..de7dc0b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -169,8 +169,9 @@ ZIP,以及 Linux 的 AppImage 与 DEB。Windows portable ZIP 解压后可直 推送 `v${package.version}` 标签时,工作流运行验证和六平台打包。只有在 全部目标成功后,才会严格校验并聚合所有平台产物,生成按平台重命名的 manifests、总 `release-manifest.json` 和 `SHA256SUMS`。随后工作流创建或 -更新 draft GitHub Release,上传全部资产成功后才发布。重跑会保留人工 -编辑的 Release notes 和未知附件。 +更新 draft GitHub Release,上传全部资产成功后才发布,并在全部下载资产 +验证通过后切换官网最新版本索引。任一步失败都不会切换官网最新版本。 +重跑会保留人工编辑的 Release notes 和未知附件。 中英文发布说明统一维护在 `resources/release-notes.json`。新版本按“本次 亮点 / Highlights”“功能更新 / Features”“问题修复 / Bug Fixes”“使用前 diff --git a/FEATURES.md b/FEATURES.md index 9490018..c2cad21 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -71,7 +71,7 @@ - [x] **企业微信与钉钉连接**:支持 Main-only 加密设置、环境变量只读覆盖、连接测试、动态启停、发送者范围和状态诊断。 - [x] **可选本地语音模型管理**:应用不内置模型权重;提供校验下载、进度与取消、来源链接、本地目录导入、切换和删除。 - [x] **本地录音与离线转写**:采集麦克风音频并使用已选择的本地模型离线转写,支持停止、取消、状态反馈和资源释放。 -- [x] **版本检查**:仅检查固定官方 Release 和当前平台清单,不自动下载或安装。 +- [x] **版本检查与镜像节点**:在“关于与更新”中选择 GitHub(默认)或镜像节点;手动检查、启动时检查和下载页使用同一选择,并只读取固定可信的发布索引,不自动下载或安装。 - [x] **内网兼容模式**:默认开启;允许应用内 HTTP 与无效、自签名或过期的 HTTPS 证书,关闭后恢复严格地址和证书校验。 ### 开源、构建与发布 diff --git a/UI-DESIGN.md b/UI-DESIGN.md index 531e0c5..e7f81d1 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -534,6 +534,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证 - 当前分类存在“保存”或“测试”等未提交配置操作时,统一放在分类页头右侧;主保存操作在最右侧,测试等次操作排列在其左侧。 - 自动生效、仅执行即时命令或自行管理编辑流程的分类不显示全局保存操作。窄窗口下操作区可以换行,但保存入口必须保持清晰可见。 - 保存或测试成功统一进入应用通知视口,并按全局规则自动消失,不在分类页头或内容卡片中保留持久成功文案。加载、保存和测试错误显示在分类页头下方,并保留可处理的上下文。 +- “关于与更新”的更新源位于“启动时检查新版本”开关下方,常规宽度下将标签、原生单选下拉框和用途说明放在同一行,并复用设置表单的统一控件样式;关闭启动检查后,下拉框置灰且不可操作。选项显示“GitHub(默认)”和中性的“镜像节点”。该选择同时控制手动检查、启动时检查和下载页,不显示底层服务商名称。 - Agent Runtime 分类页头的“保存设置”同时保存 Runtime 基础配置与 Runtime 原生定制,不在原生定制卡片内提供第二个保存入口。原生定制存在未保存更改时持续显示状态和撤销入口;切换设置分类或 Runtime 不丢弃草稿,关闭设置中心前必须先保存或撤销。 - Agent Runtime 页面在低层程序与配置覆盖之外提供“能力与默认配置”区域。能力清单使用共享 `PageTabs`,按 Agents、Tools、Commands、Skills、MCP、Rules、Prompts、Resources、LSP、Formatters 和上下文 11 类单行滚动展示,一次只呈现当前分类的 `tabpanel`;清单只显示 Runtime 自有能力,不混入 GoodBuddy 分配的 Skills、临时 MCP 或 Continue 预设。Tools 必须独立于 Commands、LSP 和 Formatters,显示工具类型、来源及 Ask/Execute 可用性;清单状态必须区分完整、部分、不可用、仅连接和不支持,不能用进程连通性冒充清单可读。 - “能力与默认配置”只显示一个模块标题,刷新入口位于该标题右侧,能力状态压缩为一行并排在默认 Agent 或 Continue 预设编辑器之前;不得再复制“Runtime 原生能力”等同义标题、说明或状态结论。刷新只更新能力快照,不覆盖未保存的原生定制草稿。 diff --git a/build/create-site-release.cjs b/build/create-site-release.cjs new file mode 100644 index 0000000..7383777 --- /dev/null +++ b/build/create-site-release.cjs @@ -0,0 +1,203 @@ +const { + mkdirSync, + readFileSync, + writeFileSync +} = require('node:fs') +const { dirname, resolve } = require('node:path') + +const supportedTargets = new Map([ + ['windows-x64', ['nsis', 'portable']], + ['windows-arm64', ['nsis', 'portable']], + ['macos-x64', ['dmg', 'zip']], + ['macos-arm64', ['dmg', 'zip']], + ['linux-x64', ['AppImage', 'deb']], + ['linux-arm64', ['AppImage', 'deb']] +]) + +function parseArguments(argv) { + const options = {} + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index] + if ( + argument === '--manifest' || + argument === '--base-url' || + argument === '--output' + ) { + const value = argv[index + 1] + if (!value) { + throw new Error(`${argument} 缺少值`) + } + options[argument.slice(2)] = value + index += 1 + } else { + throw new Error(`未知参数:${argument}`) + } + } + if (!options.manifest || !options.baseUrl || !options.output) { + throw new Error('必须指定 --manifest、--base-url 和 --output') + } + return { + manifest: resolve(options.manifest), + baseUrl: options.baseUrl, + output: resolve(options.output) + } +} + +function validateBaseUrl(value) { + let url + try { + url = new URL(value) + } catch { + throw new Error(`OSS 基础地址无效:${value}`) + } + if ( + url.protocol !== 'https:' || + url.username || + url.password || + url.search || + url.hash + ) { + throw new Error('OSS 基础地址必须是无凭据、查询参数和片段的 HTTPS 地址') + } + if (!url.pathname.endsWith('/')) { + url.pathname += '/' + } + return url +} + +function assertFile(file, targetKey) { + if ( + !file || + typeof file.name !== 'string' || + !/^GoodBuddy-[A-Za-z0-9._-]+$/u.test(file.name) || + !Number.isSafeInteger(file.size) || + file.size < 1 || + typeof file.sha256 !== 'string' || + !/^[a-f0-9]{64}$/u.test(file.sha256) + ) { + throw new Error(`发布文件元数据无效:${targetKey}`) + } +} + +function formatForFile(fileName, platform) { + if (platform === 'windows') { + if (/-setup\.exe$/u.test(fileName)) { + return 'nsis' + } + if (/-portable\.zip$/u.test(fileName)) { + return 'portable' + } + } else if (platform === 'macos') { + if (fileName.endsWith('.dmg')) { + return 'dmg' + } + if (fileName.endsWith('.zip')) { + return 'zip' + } + } else if (platform === 'linux') { + if (fileName.endsWith('.AppImage')) { + return 'AppImage' + } + if (fileName.endsWith('.deb')) { + return 'deb' + } + } + return undefined +} + +function createSiteRelease(manifest, baseUrlValue) { + if ( + !manifest || + manifest.formatVersion !== 1 || + manifest.productName !== 'GoodBuddy' || + typeof manifest.version !== 'string' || + !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(manifest.version) || + !Array.isArray(manifest.targets) + ) { + throw new Error('聚合发布 manifest 元数据无效') + } + const baseUrl = validateBaseUrl(baseUrlValue) + const seenTargets = new Set() + const targets = {} + for (const target of manifest.targets) { + const key = `${target?.platform}-${target?.arch}` + const expectedFormats = supportedTargets.get(key) + if ( + !expectedFormats || + seenTargets.has(key) || + !Array.isArray(target.formats) || + !Array.isArray(target.files) || + target.formats.length !== expectedFormats.length || + !expectedFormats.every( + (format, index) => target.formats[index] === format + ) || + target.files.length !== expectedFormats.length + ) { + throw new Error(`发布目标元数据无效:${key}`) + } + const files = {} + for (const file of target.files) { + assertFile(file, key) + const format = formatForFile(file.name, target.platform) + if (!format || !expectedFormats.includes(format) || files[format]) { + throw new Error(`发布文件格式无效:${file.name}`) + } + files[format] = { + name: file.name, + size: file.size, + sha256: file.sha256, + url: new URL(encodeURIComponent(file.name), baseUrl).href + } + } + if (expectedFormats.some((format) => !files[format])) { + throw new Error(`发布目标文件不完整:${key}`) + } + targets[key] = { + platform: target.platform, + arch: target.arch, + files + } + seenTargets.add(key) + } + if (seenTargets.size !== supportedTargets.size) { + throw new Error( + `发布目标数量错误:期望 ${supportedTargets.size},实际 ${seenTargets.size}` + ) + } + return { + formatVersion: 1, + productName: manifest.productName, + version: manifest.version, + targets, + checksumUrl: new URL('SHA256SUMS', baseUrl).href, + fallbackUrl: 'https://github.com/mesalogo/goodbuddy/releases/latest' + } +} + +function main(argv = process.argv.slice(2)) { + const options = parseArguments(argv) + const manifest = JSON.parse(readFileSync(options.manifest, 'utf8')) + const siteRelease = createSiteRelease(manifest, options.baseUrl) + mkdirSync(dirname(options.output), { recursive: true }) + writeFileSync( + options.output, + `${JSON.stringify(siteRelease, null, 2)}\n`, + 'utf8' + ) + console.log(`官网发布索引已生成:${options.output}`) +} + +module.exports = { + createSiteRelease, + parseArguments, + validateBaseUrl +} + +if (require.main === module) { + try { + main() + } catch (error) { + console.error(error) + process.exitCode = 1 + } +} diff --git a/build/verify-site-release.cjs b/build/verify-site-release.cjs new file mode 100644 index 0000000..78925ea --- /dev/null +++ b/build/verify-site-release.cjs @@ -0,0 +1,63 @@ +const { readFileSync } = require('node:fs') +const { resolve } = require('node:path') + +function parseArguments(argv) { + if (argv.length !== 2 || argv[0] !== '--manifest' || !argv[1]) { + throw new Error('必须指定 --manifest') + } + return { manifest: resolve(argv[1]) } +} + +async function verifySiteRelease(manifest, request = fetch) { + const files = Object.values(manifest?.targets ?? {}).flatMap( + (target) => Object.values(target?.files ?? {}) + ) + if (files.length !== 12) { + throw new Error(`官网发布文件数量错误:${files.length}`) + } + const urls = new Set() + for (const file of files) { + if ( + typeof file?.url !== 'string' || + !Number.isSafeInteger(file.size) || + file.size < 1 || + urls.has(file.url) + ) { + throw new Error('官网发布文件元数据无效') + } + urls.add(file.url) + const response = await request(file.url, { + method: 'HEAD', + redirect: 'error' + }) + if (!response.ok) { + throw new Error(`OSS 文件不可访问:${file.url}(${response.status})`) + } + const contentLength = Number(response.headers.get('content-length')) + if (contentLength !== file.size) { + throw new Error( + `OSS 文件大小不匹配:${file.url},期望 ${file.size},实际 ${contentLength}` + ) + } + } + return files.length +} + +async function main(argv = process.argv.slice(2)) { + const options = parseArguments(argv) + const manifest = JSON.parse(readFileSync(options.manifest, 'utf8')) + const count = await verifySiteRelease(manifest) + console.log(`OSS 发布文件验证通过:${count} 个`) +} + +module.exports = { + parseArguments, + verifySiteRelease +} + +if (require.main === module) { + main().catch((error) => { + console.error(error) + process.exitCode = 1 + }) +} diff --git a/sites/README.md b/sites/README.md index 62029ab..4e0a0ed 100644 --- a/sites/README.md +++ b/sites/README.md @@ -39,25 +39,27 @@ node sites/scripts/validate.mjs node --check sites/app.js ``` -校验脚本会检查必需文件、页内链接、本地资源、关键产品文案、主题与响应式规则,以及下载入口是否始终指向官方最新 Release。 +校验脚本会检查必需文件、页内链接、本地资源、关键产品文案、主题与响应式 +规则,以及下载选择器是否从受信任的正式发布索引加载并保留 GitHub +Release 回退入口。 ## 下载入口 -官网正文不展示具体版本号,三个系统下载按钮直接指向 GitHub 最新正式 +官网正文不写死版本号,页面启动后读取最新正式发布索引。 +Windows、macOS 和 Linux 下载卡片分别提供处理器架构与安装包类型选择器, +选择后直接下载经过发布校验的不可变版本对象。发布索引请求失败、 +格式无效或返回非受信任的官方下载地址时,按钮继续指向 GitHub 最新正式 Release: ```text https://github.com/mesalogo/goodbuddy/releases/latest ``` -新版本发布后 GitHub 会自动更新该地址的目标,官网无需同步修改版本号 -或安装资产名称。用户在 Release 页面按系统与架构选择文件并核对 -SHA-256 清单。 - ## 文件 - `index.html`:页面结构与简体中文内容 - `styles.css`:语义令牌、浅深主题、焦点与响应式布局 - `app.js`:主题、移动导航和当前章节 - `assets/goodbuddy-light.png`、`assets/goodbuddy-dark.png`:由 `npm run icons` 与桌面应用同步生成的官方品牌图标 +- `assets/linux-plain.svg`:Devicon v2.17.0 提供的黑白 Linux 图标,许可见 `assets/devicon-LICENSE` - `scripts/validate.mjs`:无依赖静态检查 diff --git a/sites/app.js b/sites/app.js index 9a08416..7f6184e 100644 --- a/sites/app.js +++ b/sites/app.js @@ -12,6 +12,137 @@ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)"); const finePointer = window.matchMedia("(hover: hover) and (pointer: fine)"); const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); + const releaseManifestUrl = + "https://goodbuddy.oss-cn-hangzhou.aliyuncs.com/releases/latest.json"; + const releaseFallbackUrl = + "https://github.com/mesalogo/goodbuddy/releases/latest"; + const releaseStatus = document.querySelector("[data-release-status]"); + const downloadCards = [ + ...document.querySelectorAll("[data-download-card]"), + ]; + const platformNames = { + windows: "Windows", + macos: "macOS", + linux: "Linux", + }; + const formatNames = { + nsis: "安装版", + portable: "便携版", + dmg: "DMG", + zip: "ZIP", + AppImage: "AppImage", + deb: "DEB", + }; + + const formatFileSize = (bytes) => { + const megabytes = bytes / (1024 * 1024); + return `${megabytes >= 100 ? megabytes.toFixed(0) : megabytes.toFixed(1)} MB`; + }; + + const isTrustedReleaseUrl = (value) => { + try { + const url = new URL(value); + return ( + url.protocol === "https:" && + url.hostname === "goodbuddy.oss-cn-hangzhou.aliyuncs.com" && + url.pathname.startsWith("/releases/") + ); + } catch { + return false; + } + }; + + const configureDownloads = (release) => { + if ( + release?.formatVersion !== 1 || + release?.productName !== "GoodBuddy" || + typeof release?.version !== "string" || + !release?.targets + ) { + throw new Error("发布索引格式无效"); + } + + 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) + ) { + return; + } + + const target = release.targets[`${platform}-${archSelect.value}`]; + const file = target?.files?.[formatSelect.value]; + if ( + !file || + typeof file.name !== "string" || + !Number.isSafeInteger(file.size) || + file.size < 1 || + !isTrustedReleaseUrl(file.url) + ) { + link.href = releaseFallbackUrl; + link.textContent = + `前往 GitHub 下载 ${platformNames[platform] ?? platform} →`; + meta.textContent = "当前选项暂不可用,请在 GitHub Release 中选择文件。"; + return; + } + + link.href = file.url; + 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; + link.textContent = `下载 ${platformName} ${archName} ${formatName} →`; + meta.textContent = + `GoodBuddy ${release.version} · ${formatFileSize(file.size)} · ` + + `${archSelect.options[archSelect.selectedIndex]?.text ?? archSelect.value}`; + }; + + for (const card of downloadCards) { + const selects = card.querySelectorAll("select"); + for (const select of selects) { + select.addEventListener("change", () => updateCard(card)); + } + updateCard(card); + } + + if (releaseStatus instanceof HTMLElement) { + releaseStatus.textContent = + `官方下载源已就绪:GoodBuddy ${release.version}。` + + "请选择处理器和安装包类型。"; + releaseStatus.classList.add("is-ready"); + } + }; + + const loadRelease = async () => { + try { + const response = await fetch(releaseManifestUrl, { + cache: "no-store", + credentials: "omit", + }); + if (!response.ok) { + throw new Error(`发布索引请求失败:${response.status}`); + } + configureDownloads(await response.json()); + } catch { + if (releaseStatus instanceof HTMLElement) { + releaseStatus.textContent = + "官方下载源暂不可用,下载按钮已切换到 GitHub Release。"; + releaseStatus.classList.add("is-fallback"); + } + } + }; const getSavedTheme = () => { try { @@ -51,6 +182,7 @@ applyTheme(getSavedTheme() ?? (systemTheme.matches ? "dark" : "light")); setHeaderState(); + void loadRelease(); themeToggle?.addEventListener("click", () => { applyTheme(root.dataset.theme === "dark" ? "light" : "dark", true); diff --git a/sites/assets/devicon-LICENSE b/sites/assets/devicon-LICENSE new file mode 100644 index 0000000..fcc9b66 --- /dev/null +++ b/sites/assets/devicon-LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2015 konpa + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/sites/assets/linux-plain.svg b/sites/assets/linux-plain.svg new file mode 100644 index 0000000..26ced8f --- /dev/null +++ b/sites/assets/linux-plain.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sites/index.html b/sites/index.html index 88876fb..1e7b341 100644 --- a/sites/index.html +++ b/sites/index.html @@ -229,14 +229,33 @@ -

Windows

x64 / arm64 · NSIS / 便携版

+

Windows

x64 / arm64 · 安装版 / 便携版

+
+ + +
下载 Windows 版(在新窗口打开) + >下载 Windows 版 →(在新窗口打开) +

+ 正在获取最新版本,暂时可前往 GitHub 下载。 +

@@ -244,32 +263,70 @@
-

macOS

x64 / arm64 · DMG / ZIP

+

macOS

Apple 芯片 / Intel · DMG / ZIP

+
+ + +
下载 macOS 版(在新窗口打开) + >下载 macOS 版 →(在新窗口打开) +

+ 正在获取最新版本,暂时可前往 GitHub 下载。 +

- +

Linux

x64 / arm64 · AppImage / DEB

+
+ + +
下载 Linux 版(在新窗口打开) + >下载 Linux 版 →(在新窗口打开) +

+ 正在获取最新版本,暂时可前往 GitHub 下载。 +

+
+ 正在连接官方下载源… +