diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml
new file mode 100644
index 0000000..82373d0
--- /dev/null
+++ b/.github/workflows/dev.yml
@@ -0,0 +1,52 @@
+name: Dev CI
+
+on:
+ push:
+ branches:
+ - dev
+ workflow_dispatch:
+
+concurrency:
+ group: dev-ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ${{ matrix.platform }}
+ strategy:
+ fail-fast: false
+ matrix:
+ platform: [windows-latest, ubuntu-22.04]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: yarn
+ cache-dependency-path: yarn.lock
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache Rust
+ uses: swatinem/rust-cache@v2
+ with:
+ workspaces: |
+ src-tauri -> src-tauri/target
+
+ - name: Install Linux dependencies
+ if: matrix.platform == 'ubuntu-22.04'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
+
+ - name: Install dependencies
+ run: yarn install --frozen-lockfile
+
+ - name: Build
+ run: yarn build
+
diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml
new file mode 100644
index 0000000..bd3c5da
--- /dev/null
+++ b/.github/workflows/preview.yml
@@ -0,0 +1,118 @@
+name: Preview Prerelease
+
+on:
+ pull_request:
+ types: [closed]
+ branches:
+ - preview
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+concurrency:
+ group: preview-prerelease
+ cancel-in-progress: false
+
+jobs:
+ prepare:
+ if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'preview' && github.event.pull_request.head.ref == 'dev')
+ runs-on: ubuntu-latest
+ outputs:
+ tag_name: ${{ steps.vars.outputs.tag_name }}
+ app_version: ${{ steps.vars.outputs.app_version }}
+
+ steps:
+ - name: Checkout (preview)
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ ref: preview
+
+ - name: Configure git
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+
+ - name: Bump version, commit and tag
+ id: vars
+ shell: pwsh
+ run: |
+ $pkg = Get-Content -Raw package.json | ConvertFrom-Json
+ $baseVersion = [string]$pkg.version
+ if ($baseVersion.Contains("-")) { $baseVersion = $baseVersion.Split("-", 2)[0] }
+
+ $shortSha = (git rev-parse --short=7 HEAD).Trim()
+ $epoch = [int64](Get-Date -AsUTC -UFormat %s)
+ $hex = ("{0:x}" -f $epoch)
+ $preVersion = "$baseVersion-pre.$hex.$shortSha"
+ $tag = "v$preVersion"
+
+ if ((git ls-remote --tags origin "refs/tags/$tag")) {
+ throw "Tag already exists on origin: $tag"
+ }
+
+ ./scripts/set-version.ps1 -Version $preVersion
+ git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml
+ git commit -m "chore(version): 更新版本号为 $preVersion"
+ git tag $tag
+
+ git push origin preview
+ git push origin $tag
+
+ "tag_name=$tag" >> $env:GITHUB_OUTPUT
+ "app_version=$preVersion" >> $env:GITHUB_OUTPUT
+
+ build:
+ needs: prepare
+ runs-on: ${{ matrix.platform }}
+ strategy:
+ fail-fast: false
+ matrix:
+ platform: [windows-latest, ubuntu-22.04]
+
+ steps:
+ - name: Checkout (tag)
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ ref: ${{ needs.prepare.outputs.tag_name }}
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: yarn
+ cache-dependency-path: yarn.lock
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache Rust
+ uses: swatinem/rust-cache@v2
+ with:
+ workspaces: |
+ src-tauri -> src-tauri/target
+
+ - name: Install Linux dependencies
+ if: matrix.platform == 'ubuntu-22.04'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
+
+ - name: Install frontend dependencies
+ run: yarn install --frozen-lockfile
+
+ - name: Quick web build
+ run: yarn build
+
+ - name: Build Tauri app and publish prerelease assets
+ uses: tauri-apps/tauri-action@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ projectPath: .
+ releaseDraft: false
+ prerelease: true
+ tagName: ${{ needs.prepare.outputs.tag_name }}
+ releaseName: EndCat ${{ needs.prepare.outputs.tag_name }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..c1969c4
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,114 @@
+name: Release Draft
+
+on:
+ pull_request:
+ types: [closed]
+ branches:
+ - master
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+concurrency:
+ group: master-release
+ cancel-in-progress: false
+
+jobs:
+ prepare:
+ if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'master' && github.event.pull_request.head.ref == 'preview')
+ runs-on: ubuntu-latest
+ outputs:
+ tag_name: ${{ steps.vars.outputs.tag_name }}
+ app_version: ${{ steps.vars.outputs.app_version }}
+
+ steps:
+ - name: Checkout (master)
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ ref: master
+
+ - name: Configure git
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+
+ - name: Bump version, commit and tag
+ id: vars
+ shell: pwsh
+ run: |
+ $pkg = Get-Content -Raw package.json | ConvertFrom-Json
+ $current = [string]$pkg.version
+ $releaseVersion = $current
+ if ($releaseVersion.Contains("-")) { $releaseVersion = $releaseVersion.Split("-", 2)[0] }
+
+ $tag = "v$releaseVersion"
+ if ((git ls-remote --tags origin "refs/tags/$tag")) {
+ throw "Tag already exists on origin: $tag"
+ }
+
+ ./scripts/set-version.ps1 -Version $releaseVersion
+ git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml
+ git commit -m "chore(version): 更新版本号为 $releaseVersion"
+ git tag $tag
+
+ git push origin master
+ git push origin $tag
+
+ "tag_name=$tag" >> $env:GITHUB_OUTPUT
+ "app_version=$releaseVersion" >> $env:GITHUB_OUTPUT
+
+ build:
+ needs: prepare
+ runs-on: ${{ matrix.platform }}
+ strategy:
+ fail-fast: false
+ matrix:
+ platform: [windows-latest, ubuntu-22.04]
+
+ steps:
+ - name: Checkout (tag)
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ ref: ${{ needs.prepare.outputs.tag_name }}
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: yarn
+ cache-dependency-path: yarn.lock
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache Rust
+ uses: swatinem/rust-cache@v2
+ with:
+ workspaces: |
+ src-tauri -> src-tauri/target
+
+ - name: Install Linux dependencies
+ if: matrix.platform == 'ubuntu-22.04'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
+
+ - name: Install frontend dependencies
+ run: yarn install --frozen-lockfile
+
+ - name: Quick web build
+ run: yarn build
+
+ - name: Build Tauri app and create draft release
+ uses: tauri-apps/tauri-action@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ projectPath: .
+ releaseDraft: true
+ prerelease: false
+ tagName: ${{ needs.prepare.outputs.tag_name }}
+ releaseName: EndCat ${{ needs.prepare.outputs.tag_name }}
diff --git a/.github/workflows/tauri.yml b/.github/workflows/tauri.yml
deleted file mode 100644
index 4bf0dbc..0000000
--- a/.github/workflows/tauri.yml
+++ /dev/null
@@ -1,72 +0,0 @@
-name: Tauri Build
-
-on:
- push:
- branches:
- - main
- tags:
- - "v*"
- pull_request:
-
-jobs:
- build:
- runs-on: ${{ matrix.platform }}
- strategy:
- fail-fast: false
- matrix:
- platform: [windows-latest, ubuntu-22.04]
-
- permissions:
- contents: write
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 18
- cache: yarn
- cache-dependency-path: yarn.lock
-
- - name: Setup Rust
- uses: dtolnay/rust-toolchain@stable
-
- - name: Cache Rust
- uses: swatinem/rust-cache@v2
- with:
- workspaces: |
- src-tauri -> src-tauri/target
-
- - name: Install Linux dependencies
- if: matrix.platform == 'ubuntu-22.04'
- run: |
- sudo apt-get update
- sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
-
- - name: Install frontend dependencies
- run: yarn install --frozen-lockfile
-
- - name: Quick web build (PR only)
- if: github.event_name == 'pull_request'
- run: yarn build
-
- - name: Build Tauri app
- uses: tauri-apps/tauri-action@v1
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- projectPath: .
- releaseDraft: ${{ startsWith(github.ref, 'refs/tags/') }}
- prerelease: false
- tagName: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '' }}
- releaseName: ${{ startsWith(github.ref, 'refs/tags/') && format('EndCat {0}', github.ref_name) || '' }}
-
- - name: Upload bundles
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: tauri-bundles-${{ matrix.platform }}
- path: src-tauri/target/release/bundle/**
- retention-days: 7
diff --git a/.gitignore b/.gitignore
index c555555..6a25e00 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,4 +42,8 @@ endfield-cat-metadata
# Local TLS certs (self-signed)
/certs/*
-scripts
\ No newline at end of file
+scripts
+
+findings.md
+progress.md
+task_plan.md
diff --git a/README.md b/README.md
index f8e338c..20ba21f 100644
--- a/README.md
+++ b/README.md
@@ -83,9 +83,10 @@
## 🚀 开发指南
### 前置要求
-- Node.js (v18+)
+- Node.js (24.x)
- Rust (最新稳定版)
- VS Code (推荐)
+- Yarn v1(推荐)
### 启动项目
@@ -97,19 +98,27 @@
2. **安装依赖**
```bash
- npm install
+ yarn
```
3. **启动开发模式**
```bash
- npm run tauri dev
+ yarn tauri dev
```
4. **构建生产版本**
```bash
- npm run tauri build
+ yarn tauri build
```
+## 🧩 分支与发布流程
+
+- `dev`:push 触发 CI(`.github/workflows/dev.yml`),不发版
+- `preview`:合并 `dev -> preview` 的 PR 后自动创建 Prerelease(版本号:`{version}-pre.{hex_timestamp}.{short_sha}`)
+- `master`:合并 `preview -> master` 的 PR 后自动创建 Draft Release(正式版本号)
+
+> 版本号会同步写入 `package.json`、`src-tauri/tauri.conf.json`、`src-tauri/Cargo.toml`(脚本:`scripts/set-version.ps1`)。
+
## 贡献者
diff --git a/package.json b/package.json
index c29bf4a..3abf19c 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,11 @@
{
"name": "endfield-cat",
"private": true,
- "version": "0.1.3",
+ "version": "0.2.0",
"type": "module",
+ "engines": {
+ "node": ">=24 <25"
+ },
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
@@ -16,6 +19,7 @@
"@varlet/ui": "^3.13.1",
"echarts": "^5",
"pinia": "^3.0.4",
+ "semver": "^7.7.4",
"vue": "^3.5.13",
"vue-echarts": "^6",
"vue-i18n": "^11.2.8",
@@ -23,6 +27,7 @@
},
"devDependencies": {
"@tauri-apps/cli": "^2",
+ "@types/semver": "^7.7.1",
"@varlet/import-resolver": "^3.13.1",
"@vitejs/plugin-vue": "^5.2.1",
"typescript": "~5.6.2",
@@ -31,4 +36,4 @@
"vite": "^6.0.3",
"vue-tsc": "^2.1.10"
}
-}
\ No newline at end of file
+}
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 0cf8151..41b2e54 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -878,7 +878,7 @@ checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
[[package]]
name = "endfield-cat"
-version = "0.1.3"
+version = "0.2.0"
dependencies = [
"futures-util",
"reqwest",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 1ce5a29..26495d6 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "endfield-cat"
-version = "0.1.3"
+version = "0.2.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
diff --git a/src-tauri/src/app_cmd.rs b/src-tauri/src/app_cmd.rs
index 665fb2d..97e6aed 100644
--- a/src-tauri/src/app_cmd.rs
+++ b/src-tauri/src/app_cmd.rs
@@ -111,6 +111,11 @@ pub async fn fetch_latest_release(client: State<'_, reqwest::Client>) -> Result<
release::fetch_latest_release(&client).await
}
+#[tauri::command]
+pub async fn fetch_latest_prerelease(client: State<'_, reqwest::Client>) -> Result {
+ release::fetch_latest_prerelease(&client).await
+}
+
#[tauri::command]
pub async fn download_and_apply_update(
window: tauri::Window,
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 367ee10..4d894b5 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -44,6 +44,7 @@ pub fn run() {
app_cmd::fetch_metadata_manifest,
app_cmd::check_metadata,
app_cmd::fetch_latest_release,
+ app_cmd::fetch_latest_prerelease,
app_cmd::download_and_apply_update,
app_cmd::test_github_mirror,
hg_api::auth::hg_exchange_user_token,
diff --git a/src-tauri/src/services/release.rs b/src-tauri/src/services/release.rs
index 52522eb..71ded7a 100644
--- a/src-tauri/src/services/release.rs
+++ b/src-tauri/src/services/release.rs
@@ -46,19 +46,23 @@ pub async fn fetch_latest_release(client: &reqwest::Client) -> Result Result Err(err.message),
}
}
+
+pub async fn fetch_latest_prerelease(client: &reqwest::Client) -> Result {
+ let url = "https://api.github.com/repos/BoxCatTeam/endfield-cat/releases?per_page=20";
+ let resp = client
+ .get(url)
+ .header("Accept", "application/vnd.github+json")
+ .header("User-Agent", "endfield-cat/tauri")
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ let status = resp.status();
+ if !status.is_success() {
+ return Err(format!("GitHub API status {}", status));
+ }
+
+ let json: serde_json::Value = resp.json().await.map_err(|e| e.to_string())?;
+ let releases = json.as_array().ok_or("Invalid GitHub response: expected array")?;
+
+ let target = releases.iter().find(|r| {
+ r.get("draft").and_then(|v| v.as_bool()) == Some(false)
+ && r.get("prerelease").and_then(|v| v.as_bool()) == Some(true)
+ });
+
+ let Some(target) = target else {
+ return Err("No prerelease found".to_string());
+ };
+
+ let tag_name = target
+ .get("tag_name")
+ .or_else(|| target.get("name"))
+ .and_then(|v| v.as_str())
+ .unwrap_or("")
+ .to_string();
+
+ if tag_name.is_empty() {
+ return Err("Missing tag_name in GitHub response".to_string());
+ }
+
+ let name = target.get("name").and_then(|v| v.as_str()).map(|s| s.to_string());
+ let html_url = target.get("html_url").and_then(|v| v.as_str()).map(|s| s.to_string());
+ let body = target.get("body").and_then(|v| v.as_str()).map(|s| s.to_string());
+
+ let download_url = if cfg!(target_os = "windows") {
+ target
+ .get("assets")
+ .and_then(|v| v.as_array())
+ .and_then(|assets| {
+ assets.iter().find_map(|asset| {
+ let name = asset.get("name").and_then(|v| v.as_str())?;
+ if name.ends_with(".exe") {
+ asset.get("browser_download_url").and_then(|v| v.as_str()).map(|s| s.to_string())
+ } else {
+ None
+ }
+ })
+ })
+ } else {
+ None
+ };
+
+ Ok(LatestRelease { tag_name, name, html_url, download_url, body })
+}
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index cfa2b8d..54d4bff 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "endfield-cat",
- "version": "0.1.3",
+ "version": "0.2.0",
"identifier": "org.boxcat.endfield-cat",
"build": {
"beforeDevCommand": "yarn dev",
diff --git a/src/api/tauriCommands.ts b/src/api/tauriCommands.ts
index bc61247..ecd0ddb 100644
--- a/src/api/tauriCommands.ts
+++ b/src/api/tauriCommands.ts
@@ -39,6 +39,10 @@ export function fetchLatestRelease() {
return invoke("fetch_latest_release");
}
+export function fetchLatestPrerelease() {
+ return invoke("fetch_latest_prerelease");
+}
+
export function downloadAndApplyUpdate(downloadUrl: string) {
return invoke("download_and_apply_update", { downloadUrl });
}
diff --git a/src/components/UpdateDialog.vue b/src/components/UpdateDialog.vue
index 540838a..dda406f 100644
--- a/src/components/UpdateDialog.vue
+++ b/src/components/UpdateDialog.vue
@@ -1,17 +1,12 @@
@@ -36,14 +31,33 @@ onMounted(async ()=>{
{{ t('settings.update.later') }}
-
+
{{ t('settings.update.manualDownload') }}
+
+
+ {{ t('settings.update.installStable') }}
+
+
+ {{ t('settings.update.installPreview') }}
+
+
{{ t('settings.update.installNow') }}
diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts
index 356125f..47bcf78 100644
--- a/src/i18n/locales/en-US.ts
+++ b/src/i18n/locales/en-US.ts
@@ -102,6 +102,8 @@ export default {
downloadProgress: "Download progress: {progress}%",
preparing: "Preparing installation...",
installNow: "Update Now",
+ installStable: "Install Stable",
+ installPreview: "Install Preview",
manualDownload: "Manual Download",
later: "Later",
installFailed: "Update installation failed",
diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts
index b3ff934..fd96f67 100644
--- a/src/i18n/locales/zh-CN.ts
+++ b/src/i18n/locales/zh-CN.ts
@@ -102,6 +102,8 @@ export default {
downloadProgress: "下载进度: {progress}%",
preparing: "准备安装...",
installNow: "立即更新",
+ installStable: "安装稳定版",
+ installPreview: "安装预览版",
manualDownload: "手动下载",
later: "稍后",
installFailed: "更新安装失败",
diff --git a/src/stores/updater.ts b/src/stores/updater.ts
index f6d63a6..da29118 100644
--- a/src/stores/updater.ts
+++ b/src/stores/updater.ts
@@ -1,100 +1,174 @@
-import { defineStore } from 'pinia'
-import { ref } from 'vue'
-import { openUrl } from '@tauri-apps/plugin-opener'
-import { Snackbar } from '@varlet/ui'
-import i18n from '../i18n' // 直接使用全局 i18n 实例
-import { downloadAndApplyUpdate, fetchLatestRelease, getAppVersion } from '../api/tauriCommands'
+import { defineStore } from "pinia";
+import { ref } from "vue";
+import { openUrl } from "@tauri-apps/plugin-opener";
+import { Snackbar } from "@varlet/ui";
+import * as semver from "semver";
+import i18n from "../i18n";
+import { downloadAndApplyUpdate, fetchLatestPrerelease, fetchLatestRelease, getAppVersion } from "../api/tauriCommands";
export type LatestRelease = {
- tag_name: string
- name?: string
- html_url?: string
- download_url?: string
- body?: string
-}
-
-export const useUpdaterStore = defineStore('updater', () => {
- const updateInfo = ref(null)
- const showUpdateDialog = ref(false)
- const isUpdating = ref(false)
- const isChecking = ref(false)
-
- // 版本比较:返回远端是否更高
- const isNewerVersion = (local: string, remote: string): boolean => {
- const parseVersion = (v: string) => v.replace(/^v/, '').split('.').map(Number)
- const localParts = parseVersion(local)
- const remoteParts = parseVersion(remote)
-
- for (let i = 0; i < Math.max(localParts.length, remoteParts.length); i++) {
- const l = localParts[i] || 0
- const r = remoteParts[i] || 0
- if (r > l) return true
- if (r < l) return false
- }
- return false
+ tag_name: string;
+ name?: string;
+ html_url?: string;
+ download_url?: string;
+ body?: string;
+};
+
+type UpdateTarget = "primary" | "alt";
+
+const normalizeVersion = (v: string) => v.replace(/^v/i, "").trim();
+
+const parseSemver = (v: string) => {
+ const normalized = normalizeVersion(v);
+ const valid = semver.valid(normalized, { loose: true }) ?? semver.clean(normalized, { loose: true });
+ if (!valid) return null;
+ return semver.parse(valid, { loose: true });
+};
+
+const isHexLowerLike = (s: unknown) => typeof s === "string" && /^[0-9a-f]+$/i.test(s);
+
+const parsePreHexTimestamp = (v: semver.SemVer) => {
+ const pre = v.prerelease;
+ if (pre.length < 2) return null;
+ if (pre[0] !== "pre") return null;
+ const ts = pre[1];
+ if (!isHexLowerLike(ts)) return null;
+ try {
+ return BigInt(`0x${String(ts)}`);
+ } catch {
+ return null;
+ }
+};
+
+export const useUpdaterStore = defineStore("updater", () => {
+ const localVersion = ref("");
+ const updateInfo = ref(null);
+ const altUpdateInfo = ref(null);
+ const showUpdateDialog = ref(false);
+ const isUpdating = ref(false);
+ const isChecking = ref(false);
+
+ const isRemoteNewer = (local: string, remote: string) => {
+ const localParsed = parseSemver(local);
+ const remoteParsed = parseSemver(remote);
+ if (!localParsed || !remoteParsed) return false;
+
+ if (
+ localParsed.major === remoteParsed.major &&
+ localParsed.minor === remoteParsed.minor &&
+ localParsed.patch === remoteParsed.patch
+ ) {
+ const localTs = parsePreHexTimestamp(localParsed);
+ const remoteTs = parsePreHexTimestamp(remoteParsed);
+ if (localTs !== null && remoteTs !== null) {
+ if (remoteTs > localTs) return true;
+ if (remoteTs < localTs) return false;
+ }
}
- const checkForUpdate = async (silent = false) => {
- if (isChecking.value) return
- isChecking.value = true
- try {
- const [localVersion, release] = await Promise.all([
- getAppVersion(),
- fetchLatestRelease()
- ])
-
- if (release && isNewerVersion(localVersion, release.tag_name)) {
- updateInfo.value = release
- showUpdateDialog.value = true
- } else if (!silent) {
- // 主动检查时提示“已是最新”
- Snackbar.success(i18n.global.t('settings.update.alreadyLatest') || 'Already latest version')
- }
- } catch (error) {
- console.error("Failed to check for updates:", error)
- if (!silent) {
- Snackbar.error(i18n.global.t('settings.update.checkFailed') || 'Check failed')
- }
- } finally {
- isChecking.value = false
+ return semver.compare(remoteParsed, localParsed) > 0;
+ };
+
+ const checkForUpdate = async (silent = false) => {
+ if (isChecking.value) return;
+ isChecking.value = true;
+
+ showUpdateDialog.value = false;
+ updateInfo.value = null;
+ altUpdateInfo.value = null;
+
+ try {
+ localVersion.value = await getAppVersion();
+ const isPreviewBuild = normalizeVersion(localVersion.value).toLowerCase().includes("-pre");
+
+ if (isPreviewBuild) {
+ const [stableRes, preRes] = await Promise.allSettled([
+ fetchLatestRelease(),
+ fetchLatestPrerelease(),
+ ]);
+
+ const stable = stableRes.status === "fulfilled" ? stableRes.value : null;
+ const prerelease = preRes.status === "fulfilled" ? preRes.value : null;
+
+ const canUpdateStable = !!stable && isRemoteNewer(localVersion.value, stable.tag_name);
+ const canUpdatePre = !!prerelease && isRemoteNewer(localVersion.value, prerelease.tag_name);
+
+ if (canUpdateStable && canUpdatePre && stable && prerelease) {
+ updateInfo.value = stable;
+ altUpdateInfo.value = prerelease;
+ showUpdateDialog.value = true;
+ return;
}
- }
- const installUpdate = async () => {
- if (!updateInfo.value?.download_url) {
- Snackbar.error(i18n.global.t('settings.update.installFailed') || 'Install failed')
- return
+ const target = (canUpdateStable ? stable : null) ?? (canUpdatePre ? prerelease : null);
+ if (target) {
+ updateInfo.value = target;
+ showUpdateDialog.value = true;
+ return;
}
- isUpdating.value = true
- try {
- await downloadAndApplyUpdate(updateInfo.value.download_url)
- } catch (error) {
- console.error("Update failed:", error)
- Snackbar.error(i18n.global.t('settings.update.installFailed') || 'Install failed')
- isUpdating.value = false
+ if (!silent) {
+ Snackbar.success(i18n.global.t("settings.update.alreadyLatest") || "Already latest version");
}
+ return;
+ }
+
+ const release = await fetchLatestRelease();
+ if (release && isRemoteNewer(localVersion.value, release.tag_name)) {
+ updateInfo.value = release;
+ showUpdateDialog.value = true;
+ } else if (!silent) {
+ Snackbar.success(i18n.global.t("settings.update.alreadyLatest") || "Already latest version");
+ }
+ } catch (error) {
+ console.error("Failed to check for updates:", error);
+ if (!silent) {
+ Snackbar.error(i18n.global.t("settings.update.checkFailed") || "Check failed");
+ }
+ } finally {
+ isChecking.value = false;
}
+ };
- const manualDownload = async () => {
- if (updateInfo.value?.html_url) {
- await openUrl(updateInfo.value.html_url)
- }
- showUpdateDialog.value = false
+ const installUpdate = async (target: UpdateTarget = "primary") => {
+ const info = target === "alt" ? altUpdateInfo.value : updateInfo.value;
+ if (!info?.download_url) {
+ Snackbar.error(i18n.global.t("settings.update.installFailed") || "Install failed");
+ return;
}
- const dismissDialog = () => {
- showUpdateDialog.value = false
+ isUpdating.value = true;
+ try {
+ await downloadAndApplyUpdate(info.download_url);
+ } catch (error) {
+ console.error("Update failed:", error);
+ Snackbar.error(i18n.global.t("settings.update.installFailed") || "Install failed");
+ isUpdating.value = false;
}
+ };
- return {
- updateInfo,
- showUpdateDialog,
- isUpdating,
- isChecking,
- checkForUpdate,
- installUpdate,
- manualDownload,
- dismissDialog
+ const manualDownload = async (target: UpdateTarget = "primary") => {
+ const info = target === "alt" ? altUpdateInfo.value : updateInfo.value;
+ if (info?.html_url) {
+ await openUrl(info.html_url);
}
-})
+ showUpdateDialog.value = false;
+ };
+
+ const dismissDialog = () => {
+ showUpdateDialog.value = false;
+ };
+
+ return {
+ localVersion,
+ updateInfo,
+ altUpdateInfo,
+ showUpdateDialog,
+ isUpdating,
+ isChecking,
+ checkForUpdate,
+ installUpdate,
+ manualDownload,
+ dismissDialog,
+ };
+});
diff --git a/yarn.lock b/yarn.lock
index 879bd26..d5c368a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -438,6 +438,11 @@
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
+"@types/semver@^7.7.1":
+ version "7.7.1"
+ resolved "https://registry.npmmirror.com/@types/semver/-/semver-7.7.1.tgz#3ce3af1a5524ef327d2da9e4fd8b6d95c8d70528"
+ integrity sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==
+
"@varlet/icons@3.13.1":
version "3.13.1"
resolved "https://registry.npmmirror.com/@varlet/icons/-/icons-3.13.1.tgz#fe496e4913826cb1c456849d496261b9537287cb"
@@ -982,6 +987,11 @@ scule@^1.3.0:
resolved "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz#6efbd22fd0bb801bdcc585c89266a7d2daa8fbd3"
integrity sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==
+semver@^7.7.4:
+ version "7.7.4"
+ resolved "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
+ integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
+
source-map-js@^1.0.2, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"