feat(desktop,ci): GitHub Releases 分发 + 桌面端检查更新

让桌面端像开源项目一样用 GitHub Releases 分发,旧版用户自动获知更新:
- CD:.github/workflows/release.yml —— 打 vX.Y.Z 标签触发,macOS(universal)/
  Windows(amd64) 用 wails build 出安装包,softprops/action-gh-release 发布到
  GitHub Release(GOWORK=off,因 desktop 不在工作区)。
- 桌面端检查更新:lib/version.ts(APP_VERSION + 查 GitHub /releases/latest +
  语义版本比较 + openExternal 走 Wails BrowserOpenURL);UpdateBanner 启动时
  检查,有新版顶部横幅「前往下载」。404/限流/离线均静默不打扰。

注:未签名/公证(macOS Gatekeeper / Win SmartScreen 需后续证书);release
workflow 需真实 tag push 才能验证(GitHub 侧运行)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-23 16:26:02 +08:00
parent 9fc3597317
commit 8c980d2b37
4 changed files with 150 additions and 0 deletions
@@ -0,0 +1,50 @@
// 桌面端版本与"检查更新"——查 GitHub Releases 最新版,比当前版本新则提示下载。
// 发版流程:bump 此处 APP_VERSION= package.json version)→ 打 tag vX.Y.Z → release workflow 自动构建+发布。
export const APP_VERSION = "0.1.0"; // 当前桌面端版本(与 git tag 去掉 v 前缀对齐)
const REPO = "blizzardzhang/sundynix-agentix";
export interface ReleaseInfo {
version: string; // 最新版本号(不含 v
url: string; // release 页面(用户在此选 mac/win 安装包)
notes: string; // 更新日志
}
// checkUpdate 查 GitHub 最新 release;有比当前更新的版本则返回信息,否则 null(含出错/限流时静默)。
export async function checkUpdate(): Promise<ReleaseInfo | null> {
try {
const res = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
headers: { Accept: "application/vnd.github+json" },
});
if (!res.ok) return null; // 404=还没发过 release / 403=限流 → 静默
const d = (await res.json()) as { tag_name?: string; html_url?: string; body?: string };
const latest = String(d.tag_name ?? "").replace(/^v/, "");
if (!latest || !isNewer(latest, APP_VERSION)) return null;
return { version: latest, url: d.html_url ?? `https://github.com/${REPO}/releases/latest`, notes: d.body ?? "" };
} catch {
return null; // 离线等 → 不打扰
}
}
// openExternal 在系统浏览器打开链接(Wails 用 runtime.BrowserOpenURL,浏览器模式回退 window.open)。
export function openExternal(url: string): void {
const w = window as unknown as { runtime?: { BrowserOpenURL?: (u: string) => void } };
if (w.runtime?.BrowserOpenURL) {
w.runtime.BrowserOpenURL(url);
} else {
window.open(url, "_blank");
}
}
// isNewer 语义化版本比较:a 是否比 b 新(按 major.minor.patch)。
function isNewer(a: string, b: string): boolean {
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
for (let i = 0; i < 3; i++) {
const x = pa[i] ?? 0;
const y = pb[i] ?? 0;
if (x !== y) return x > y;
}
return false;
}