diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b280160 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,60 @@ +name: Release + +# 打 vX.Y.Z 标签即触发:各平台用 wails 构建桌面端安装包 → 发布到 GitHub Release。 +# 用户旧版 App 启动时查 /releases/latest,发现新版即提示下载(见 frontend/src/lib/version.ts)。 +on: + push: + tags: ["v*"] + +permissions: + contents: write # 创建 release + 上传产物需要 + +jobs: + build: + name: 构建 ${{ matrix.label }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + label: macOS (universal) + platform: darwin/universal + - os: windows-latest + label: Windows (amd64) + platform: windows/amd64 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + - uses: actions/setup-node@v4 + with: + node-version: "20" + - name: 安装 wails CLI + run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0 + + - name: 构建桌面端(wails build;GOWORK=off 因 desktop 不在工作区) + working-directory: sundynix-desktop + env: + GOWORK: off + run: wails build -clean -platform ${{ matrix.platform }} + + # macOS:.app 是目录,打包成 zip 才能作为单文件资产分发。 + - name: 打包 macOS 产物 + if: runner.os == 'macOS' + working-directory: sundynix-desktop/build/bin + run: zip -r -y sundynix_desktop_macos_universal.zip sundynix_desktop.app + + - name: 重命名 Windows 产物 + if: runner.os == 'Windows' + working-directory: sundynix-desktop/build/bin + run: ren sundynix_desktop.exe sundynix_desktop_windows_amd64.exe + + - name: 发布到 GitHub Release(按 tag 创建/追加资产) + uses: softprops/action-gh-release@v2 + with: + fail_on_unmatched_files: false + files: | + sundynix-desktop/build/bin/sundynix_desktop_macos_universal.zip + sundynix-desktop/build/bin/sundynix_desktop_windows_amd64.exe diff --git a/sundynix-desktop/frontend/src/App.tsx b/sundynix-desktop/frontend/src/App.tsx index 4009511..7171ff2 100644 --- a/sundynix-desktop/frontend/src/App.tsx +++ b/sundynix-desktop/frontend/src/App.tsx @@ -12,6 +12,7 @@ import { RunsView } from "./views/RunsView"; import { Home } from "./views/Home"; import { Placeholder } from "./views/Placeholder"; import { CommandPalette, type Command } from "./components/CommandPalette"; +import { UpdateBanner } from "./components/UpdateBanner"; import { Login } from "./views/Login"; import { submitTask, streamTokens, streamExec, authMe, logout, type Identity, type AuthUser } from "./lib/api"; import type { TaskDsl } from "./lib/dsl"; @@ -158,6 +159,7 @@ export default function App() { className="pointer-events-none absolute inset-x-0 top-0 h-64 opacity-60" style={{ background: "radial-gradient(60% 100% at 50% 0%, rgba(124,92,246,0.10), transparent 70%)" }} /> + setCmdOpen(true)} />
diff --git a/sundynix-desktop/frontend/src/components/UpdateBanner.tsx b/sundynix-desktop/frontend/src/components/UpdateBanner.tsx new file mode 100644 index 0000000..633c00b --- /dev/null +++ b/sundynix-desktop/frontend/src/components/UpdateBanner.tsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from "react"; +import { Download, X } from "lucide-react"; +import { checkUpdate, openExternal, type ReleaseInfo } from "../lib/version"; + +// 启动时查 GitHub Releases,有新版则在顶部显示一条可关闭的横幅,点击去下载页。 +export function UpdateBanner() { + const [rel, setRel] = useState(null); + const [dismissed, setDismissed] = useState(false); + + useEffect(() => { + let alive = true; + void checkUpdate().then((r) => { + if (alive) setRel(r); + }); + return () => { + alive = false; + }; + }, []); + + if (!rel || dismissed) return null; + return ( +
+ + + 新版本 v{rel.version} 可用 + + + +
+ ); +} diff --git a/sundynix-desktop/frontend/src/lib/version.ts b/sundynix-desktop/frontend/src/lib/version.ts new file mode 100644 index 0000000..34d4348 --- /dev/null +++ b/sundynix-desktop/frontend/src/lib/version.ts @@ -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 { + 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; +}