diff --git a/.github/workflows/build-desktop-tauri.yml b/.github/workflows/build-desktop-tauri.yml index 973f69e6..222b9703 100644 --- a/.github/workflows/build-desktop-tauri.yml +++ b/.github/workflows/build-desktop-tauri.yml @@ -98,13 +98,18 @@ jobs: version="" if [ "${should_build}" = "true" ]; then - workdir="$(mktemp -d)" - repo_dir="${workdir}/AstrBot" - git init "${repo_dir}" - git -C "${repo_dir}" remote add origin "${source_git_url}" - git -C "${repo_dir}" fetch --depth 1 origin "${source_git_ref}" - git -C "${repo_dir}" checkout --detach FETCH_HEAD - version="$(python3 scripts/ci/read-project-version.py "${repo_dir}/pyproject.toml")" + if printf '%s' "${source_git_ref}" | grep -Eq '^v[0-9]+(\.[0-9]+){1,2}([.-][0-9A-Za-z.-]+)?$'; then + version="${source_git_ref#v}" + echo "Resolved version directly from source tag: ${source_git_ref}" + else + workdir="$(mktemp -d)" + repo_dir="${workdir}/AstrBot" + git init "${repo_dir}" + git -C "${repo_dir}" remote add origin "${source_git_url}" + git -C "${repo_dir}" fetch --depth 1 origin "${source_git_ref}" + git -C "${repo_dir}" checkout --detach FETCH_HEAD + version="$(python3 scripts/ci/read-project-version.py "${repo_dir}/pyproject.toml")" + fi else version="${source_git_ref#v}" if [ -z "${version}" ] || [ "${version}" = "${source_git_ref}" ]; then @@ -122,9 +127,72 @@ jobs: echo "Resolved AstrBot version: ${version}" echo "Build enabled: ${should_build}" - build-linux: + sync_repo_version: + name: Sync Repository Version needs: resolve_build_context - if: ${{ needs.resolve_build_context.outputs.should_build == 'true' }} + if: ${{ needs.resolve_build_context.outputs.should_build == 'true' && github.event_name == 'schedule' }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout branch + uses: actions/checkout@v6.0.2 + with: + fetch-depth: 0 + ref: ${{ github.ref_name }} + + - name: Setup Toolchains + uses: ./.github/actions/setup-toolchains + with: + setup-python: 'false' + + - name: Setup pnpm + uses: pnpm/action-setup@v4.2.0 + with: + version: 10.28.2 + + - name: Sync desktop version to upstream tag + env: + ASTRBOT_SOURCE_GIT_URL: ${{ needs.resolve_build_context.outputs.source_git_url }} + ASTRBOT_SOURCE_GIT_REF: ${{ needs.resolve_build_context.outputs.source_git_ref }} + ASTRBOT_DESKTOP_VERSION: ${{ needs.resolve_build_context.outputs.astrbot_version }} + run: make update + + - name: Commit and push version files + env: + ASTRBOT_VERSION: ${{ needs.resolve_build_context.outputs.astrbot_version }} + TARGET_REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + + changed_files="$(git status --porcelain -- package.json src-tauri/Cargo.toml src-tauri/tauri.conf.json)" + if [ -z "${changed_files}" ]; then + echo "Version files are already up to date. Nothing to commit." + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add package.json src-tauri/Cargo.toml src-tauri/tauri.conf.json + git commit -m "chore(version): sync desktop version to v${ASTRBOT_VERSION}" + + git fetch origin "${TARGET_REF_NAME}" + if ! git pull --rebase origin "${TARGET_REF_NAME}"; then + echo "::warning::Failed to rebase onto origin/${TARGET_REF_NAME}. Skipping push to avoid noisy failures." + git rebase --abort || true + exit 0 + fi + + if ! git push origin "HEAD:${TARGET_REF_NAME}"; then + echo "::warning::Push to ${TARGET_REF_NAME} was rejected (likely branch protection or race). Skipping." + exit 0 + fi + + build-linux: + needs: + - resolve_build_context + - sync_repo_version + if: ${{ needs.resolve_build_context.outputs.should_build == 'true' && (needs.sync_repo_version.result == 'success' || needs.sync_repo_version.result == 'skipped') }} name: linux-${{ matrix.arch }} runs-on: ${{ matrix.runner }} strategy: @@ -157,6 +225,7 @@ jobs: env: ASTRBOT_SOURCE_GIT_URL: ${{ needs.resolve_build_context.outputs.source_git_url }} ASTRBOT_SOURCE_GIT_REF: ${{ needs.resolve_build_context.outputs.source_git_ref }} + ASTRBOT_DESKTOP_VERSION: ${{ needs.resolve_build_context.outputs.astrbot_version }} GITHUB_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }} run: cargo tauri build --bundles deb,rpm @@ -171,8 +240,10 @@ jobs: src-tauri/target/release/bundle/**/*.rpm build-macos: - needs: resolve_build_context - if: ${{ needs.resolve_build_context.outputs.should_build == 'true' }} + needs: + - resolve_build_context + - sync_repo_version + if: ${{ needs.resolve_build_context.outputs.should_build == 'true' && (needs.sync_repo_version.result == 'success' || needs.sync_repo_version.result == 'skipped') }} name: macos-${{ matrix.arch }} runs-on: ${{ matrix.runner }} strategy: @@ -199,6 +270,7 @@ jobs: env: ASTRBOT_SOURCE_GIT_URL: ${{ needs.resolve_build_context.outputs.source_git_url }} ASTRBOT_SOURCE_GIT_REF: ${{ needs.resolve_build_context.outputs.source_git_ref }} + ASTRBOT_DESKTOP_VERSION: ${{ needs.resolve_build_context.outputs.astrbot_version }} ASTRBOT_DESKTOP_CRYPTOGRAPHY_FALLBACK_VERSIONS: ${{ vars.ASTRBOT_DESKTOP_CRYPTOGRAPHY_FALLBACK_VERSIONS || '' }} GITHUB_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }} @@ -339,8 +411,10 @@ jobs: src-tauri/target/${{ matrix.target }}/release/bundle/**/*.app.tar.gz build-windows: - needs: resolve_build_context - if: ${{ needs.resolve_build_context.outputs.should_build == 'true' }} + needs: + - resolve_build_context + - sync_repo_version + if: ${{ needs.resolve_build_context.outputs.should_build == 'true' && (needs.sync_repo_version.result == 'success' || needs.sync_repo_version.result == 'skipped') }} name: windows-${{ matrix.arch }} runs-on: ${{ matrix.runner }} env: @@ -367,6 +441,7 @@ jobs: env: ASTRBOT_SOURCE_GIT_URL: ${{ needs.resolve_build_context.outputs.source_git_url }} ASTRBOT_SOURCE_GIT_REF: ${{ needs.resolve_build_context.outputs.source_git_ref }} + ASTRBOT_DESKTOP_VERSION: ${{ needs.resolve_build_context.outputs.astrbot_version }} ASTRBOT_DESKTOP_CRYPTOGRAPHY_FALLBACK_VERSIONS: ${{ vars.ASTRBOT_DESKTOP_CRYPTOGRAPHY_FALLBACK_VERSIONS || '' }} GITHUB_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }} diff --git a/Makefile b/Makefile index 8885d2a0..8ff5960c 100644 --- a/Makefile +++ b/Makefile @@ -7,12 +7,14 @@ RESOURCES_BACKEND_DIR ?= $(RESOURCES_DIR)/backend RESOURCES_WEBUI_DIR ?= $(RESOURCES_DIR)/webui ASTRBOT_LOCAL_DIR ?= $(VENDOR_DIR)/AstrBot-local ASTRBOT_LOCAL_DESKTOP_DIR ?= $(ASTRBOT_LOCAL_DIR)/desktop +ASTRBOT_SOURCE_GIT_URL ?= https://github.com/AstrBotDevs/AstrBot.git +ASTRBOT_SOURCE_GIT_REF ?= master RUST_MANIFEST ?= src-tauri/Cargo.toml NODE_MODULES_DIR ?= node_modules PNPM_STORE_DIR ?= .pnpm-store TAURI_TARGET_DIR ?= src-tauri/target -.PHONY: help deps sync-version prepare-webui prepare-backend prepare-resources dev build \ +.PHONY: help deps sync-version update prepare-webui prepare-backend prepare-resources dev build \ prepare rebuild lint test doctor prune size clean clean-rust clean-resources \ clean-vendor-local clean-vendor clean-node clean-all @@ -21,6 +23,7 @@ help: @echo "" @echo " make deps Install JS dependencies" @echo " make sync-version Sync desktop version from AstrBot source" + @echo " make update Sync desktop version from upstream AstrBot" @echo " make prepare Alias of prepare-resources" @echo " make prepare-webui Build and sync WebUI resources" @echo " make prepare-backend Build and sync backend runtime resources" @@ -48,6 +51,13 @@ deps: sync-version: pnpm run sync:version +update: + ASTRBOT_SOURCE_DIR= \ + ASTRBOT_SOURCE_GIT_URL=$(ASTRBOT_SOURCE_GIT_URL) \ + ASTRBOT_SOURCE_GIT_REF=$(ASTRBOT_SOURCE_GIT_REF) \ + ASTRBOT_DESKTOP_VERSION=$(ASTRBOT_DESKTOP_VERSION) \ + pnpm run sync:version + prepare-webui: pnpm run prepare:webui diff --git a/README.md b/README.md index 6ca5c859..dd2a5072 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,34 @@ make clean make prune ``` +## 版本维护(重要) + +桌面端版本会同步到以下三个文件: + +- `package.json` +- `src-tauri/Cargo.toml` +- `src-tauri/tauri.conf.json` + +### `make sync-version` 与 `make update` 的区别 + +- `make sync-version`:从当前解析到的 AstrBot 源同步版本,受本地环境变量影响(例如 `ASTRBOT_SOURCE_DIR`)。 +- `make update`:用于“对齐上游”,会忽略 `ASTRBOT_SOURCE_DIR`,并使用 `ASTRBOT_SOURCE_GIT_URL` + `ASTRBOT_SOURCE_GIT_REF` 同步版本。 + +推荐日常使用 `make update`,避免本地切换分支导致版本漂移。 + +示例: + +```bash +# 同步到上游 master +make update + +# 同步到指定上游 tag +make update ASTRBOT_SOURCE_GIT_REF=v4.17.5 + +# 强制写入指定版本(通常用于 CI) +make update ASTRBOT_DESKTOP_VERSION=4.17.5 +``` + ## 上游仓库策略 默认上游仓库: @@ -151,6 +179,13 @@ export ASTRBOT_SOURCE_GIT_URL=https://github.com/zouyonghe/AstrBot.git export ASTRBOT_SOURCE_GIT_REF=cpython-runtime-refactor ``` +## CI 版本同步策略 + +`build-desktop-tauri` 工作流在定时任务(`schedule`)检测到上游新 tag 且需要构建时,会先自动同步并提交上述三个版本文件,然后继续构建产物。 + +- 定时构建:会自动回写版本到仓库(commit + push)。 +- 手动触发(`workflow_dispatch`):默认只构建,不自动回写版本文件。 + ## 构建流程说明 `src-tauri/tauri.conf.json` 已配置 `beforeBuildCommand=pnpm run prepare:resources`,构建时会自动执行以下流程: diff --git a/package.json b/package.json index 8a586648..8bc7f3bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "astrbot-desktop-tauri", - "version": "4.17.5", + "version": "4.17.6", "description": "AstrBot desktop shell powered by Tauri", "private": true, "packageManager": "pnpm@10.28.2", diff --git a/scripts/prepare-resources.mjs b/scripts/prepare-resources.mjs index b7971293..44553de6 100644 --- a/scripts/prepare-resources.mjs +++ b/scripts/prepare-resources.mjs @@ -8,6 +8,7 @@ const DEFAULT_ASTRBOT_SOURCE_GIT_URL = 'https://github.com/AstrBotDevs/AstrBot.g const sourceRepoUrlRaw = process.env.ASTRBOT_SOURCE_GIT_URL?.trim() || DEFAULT_ASTRBOT_SOURCE_GIT_URL; const sourceRepoRefRaw = process.env.ASTRBOT_SOURCE_GIT_REF?.trim() || ''; +const desktopVersionOverride = process.env.ASTRBOT_DESKTOP_VERSION?.trim() || ''; const PYTHON_BUILD_STANDALONE_RELEASE = process.env.ASTRBOT_PBS_RELEASE?.trim() || '20260211'; const PYTHON_BUILD_STANDALONE_VERSION = @@ -427,12 +428,35 @@ const ensureStartupShellAssets = () => { const main = async () => { const sourceDir = resolveSourceDir(); + const needsSourceRepo = mode !== 'version' || !desktopVersionOverride; await mkdir(path.join(projectRoot, 'resources'), { recursive: true }); - ensureSourceRepo(sourceDir); + if (needsSourceRepo) { + ensureSourceRepo(sourceDir); + } else { + console.log( + '[prepare-resources] Skip source repo sync in version-only mode because ASTRBOT_DESKTOP_VERSION is set.', + ); + } ensureStartupShellAssets(); - const astrbotVersion = await readAstrbotVersionFromPyproject(sourceDir); + const astrbotVersion = desktopVersionOverride || (await readAstrbotVersionFromPyproject(sourceDir)); + + if (desktopVersionOverride && needsSourceRepo) { + const sourceVersion = await readAstrbotVersionFromPyproject(sourceDir); + if (sourceVersion !== desktopVersionOverride) { + console.warn( + `[prepare-resources] Version override drift detected: ASTRBOT_DESKTOP_VERSION=${desktopVersionOverride}, source pyproject version=${sourceVersion} (${sourceDir})`, + ); + } + } + await syncDesktopVersionFiles(astrbotVersion); - console.log(`[prepare-resources] Synced desktop version to AstrBot ${astrbotVersion}`); + if (desktopVersionOverride) { + console.log( + `[prepare-resources] Synced desktop version to override ${astrbotVersion} (ASTRBOT_DESKTOP_VERSION)`, + ); + } else { + console.log(`[prepare-resources] Synced desktop version to AstrBot ${astrbotVersion}`); + } if (mode === 'version') { return; diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e23c0a2a..c1c7f3b5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -49,7 +49,7 @@ checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "astrbot-desktop-tauri" -version = "4.17.5" +version = "4.17.6" dependencies = [ "chrono", "dirs 5.0.1", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0f491908..5074e41b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astrbot-desktop-tauri" -version = "4.17.5" +version = "4.17.6" description = "AstrBot desktop shell powered by Tauri" authors = ["AstrBot"] license = "AGPL-3.0" diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 06d8bbb3..fe0d3314 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -103,6 +103,7 @@ struct BackendState { is_quitting: AtomicBool, is_spawning: AtomicBool, is_restarting: AtomicBool, + exit_cleanup_started: AtomicBool, } #[derive(Debug, serde::Serialize)] @@ -137,7 +138,6 @@ enum GracefulRestartOutcome { #[derive(Debug, Clone, Copy)] struct TrayOriginDecision { uses_backend_origin: bool, - should_log_mismatch: bool, } struct AtomicFlagGuard<'a> { @@ -171,6 +171,7 @@ impl Default for BackendState { is_quitting: AtomicBool::new(false), is_spawning: AtomicBool::new(false), is_restarting: AtomicBool::new(false), + exit_cleanup_started: AtomicBool::new(false), } } } @@ -1050,6 +1051,12 @@ Content-Length: {}\r\n\ fn is_quitting(&self) -> bool { self.is_quitting.load(Ordering::Relaxed) } + + fn try_begin_exit_cleanup(&self) -> bool { + self.exit_cleanup_started + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + } } #[tauri::command] @@ -1057,11 +1064,6 @@ fn desktop_bridge_is_desktop_runtime() -> bool { true } -#[tauri::command] -fn desktop_bridge_is_electron_runtime() -> bool { - desktop_bridge_is_desktop_runtime() -} - #[tauri::command] fn desktop_bridge_get_backend_state(app_handle: AppHandle) -> BackendBridgeState { let state = app_handle.state::(); @@ -1147,7 +1149,6 @@ fn main() { .manage(BackendState::default()) .invoke_handler(tauri::generate_handler![ desktop_bridge_is_desktop_runtime, - desktop_bridge_is_electron_runtime, desktop_bridge_get_backend_state, desktop_bridge_set_auth_token, desktop_bridge_restart_backend, @@ -1239,19 +1240,41 @@ fn main() { .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| match event { - RunEvent::ExitRequested { .. } => { + RunEvent::ExitRequested { api, .. } => { let state = app_handle.state::(); + // Prevent immediate process exit so backend shutdown can run in the runtime's + // blocking pool; we exit explicitly after stop_backend() finishes. + api.prevent_exit(); state.mark_quitting(); - if let Err(error) = state.stop_backend() { - append_desktop_log(&format!( - "backend graceful stop on ExitRequested failed: {error}" - )); + if !state.try_begin_exit_cleanup() { + append_desktop_log("exit requested while backend cleanup is already running"); + return; } + + append_desktop_log("exit requested, stopping backend asynchronously"); + let app_handle_cloned = app_handle.clone(); + tauri::async_runtime::spawn_blocking(move || { + let state = app_handle_cloned.state::(); + if let Err(error) = state.stop_backend() { + append_desktop_log(&format!( + "backend graceful stop on ExitRequested failed: {error}" + )); + } + + append_desktop_log("backend stop finished, exiting desktop process"); + app_handle_cloned.exit(0); + }); } RunEvent::Exit => { let state = app_handle.state::(); + state.mark_quitting(); + if !state.try_begin_exit_cleanup() { + return; + } + + append_desktop_log("exit event triggered fallback backend cleanup"); if let Err(error) = state.stop_backend() { - append_desktop_log(&format!("backend graceful stop on Exit failed: {error}")); + append_desktop_log(&format!("backend fallback stop on Exit failed: {error}")); } } _ => {} @@ -1365,10 +1388,6 @@ fn handle_tray_menu_event(app_handle: &AppHandle, menu_id: &str) { TRAY_MENU_RESTART_BACKEND => { append_desktop_log("tray requested backend restart"); show_main_window(app_handle); - if main_window_uses_backend_origin(app_handle) { - emit_tray_restart_backend_event(app_handle); - return; - } let app_handle_cloned = app_handle.clone(); thread::spawn(move || match do_restart_backend(&app_handle_cloned, None) { @@ -1390,51 +1409,6 @@ fn handle_tray_menu_event(app_handle: &AppHandle, menu_id: &str) { } } -fn main_window_uses_backend_origin(app_handle: &AppHandle) -> bool { - let Some(window) = app_handle.get_webview_window("main") else { - return false; - }; - let Ok(window_url) = window.url() else { - return false; - }; - let state = app_handle.state::(); - let Ok(backend_url) = Url::parse(&state.backend_url) else { - return false; - }; - let decision = tray_origin_decision(&backend_url, &window_url); - if !decision.uses_backend_origin && decision.should_log_mismatch { - append_desktop_log(&format!( - "tray restart fallback to desktop-managed flow due to origin mismatch: backend={} window={}", - backend_url, window_url - )); - } - decision.uses_backend_origin -} - -fn emit_tray_restart_backend_event(app_handle: &AppHandle) { - let Some(window) = app_handle.get_webview_window("main") else { - return; - }; - - let script = r#" -(() => { - if (typeof window.__astrbotDesktopEmitTrayRestart === 'function') { - window.__astrbotDesktopEmitTrayRestart(); - return; - } - const state = - window.__astrbotDesktopTrayRestartState || - (window.__astrbotDesktopTrayRestartState = { handlers: new Set(), pending: 0 }); - state.pending = Number(state.pending || 0) + 1; -})(); -"#; - if let Err(error) = window.eval(script) { - append_desktop_log(&format!( - "failed to emit tray restart backend event to webview: {error}" - )); - } -} - fn do_restart_backend(app_handle: &AppHandle, auth_token: Option<&str>) -> Result<(), String> { let state = app_handle.state::(); state.restart_backend(app_handle, auth_token) @@ -1645,7 +1619,6 @@ const DESKTOP_BRIDGE_BOOTSTRAP_SCRIPT: &str = r#" const BRIDGE_COMMANDS = Object.freeze({ IS_DESKTOP_RUNTIME: 'desktop_bridge_is_desktop_runtime', - IS_ELECTRON_RUNTIME: 'desktop_bridge_is_electron_runtime', GET_BACKEND_STATE: 'desktop_bridge_get_backend_state', SET_AUTH_TOKEN: 'desktop_bridge_set_auth_token', RESTART_BACKEND: 'desktop_bridge_restart_backend', @@ -1943,10 +1916,6 @@ const DESKTOP_BRIDGE_BOOTSTRAP_SCRIPT: &str = r#" isDesktop: true, isDesktopRuntime: () => isRuntimeBridgeEnabled(BRIDGE_COMMANDS.IS_DESKTOP_RUNTIME, true), - // Legacy aliases for current dashboard compatibility. - isElectron: true, - isElectronRuntime: () => - isRuntimeBridgeEnabled(BRIDGE_COMMANDS.IS_ELECTRON_RUNTIME, true), getBackendState: () => invokeBridge(BRIDGE_COMMANDS.GET_BACKEND_STATE), restartBackend: async (authToken = null) => { const normalizedToken = @@ -1983,7 +1952,6 @@ fn tray_origin_decision(backend_url: &Url, window_url: &Url) -> TrayOriginDecisi if same_origin(backend_url, window_url) { return TrayOriginDecision { uses_backend_origin: true, - should_log_mismatch: false, }; } let backend_scheme = backend_url.scheme(); @@ -1991,7 +1959,6 @@ fn tray_origin_decision(backend_url: &Url, window_url: &Url) -> TrayOriginDecisi if !matches!(backend_scheme, "http" | "https") || !matches!(window_scheme, "http" | "https") { return TrayOriginDecision { uses_backend_origin: false, - should_log_mismatch: false, }; } @@ -2000,14 +1967,12 @@ fn tray_origin_decision(backend_url: &Url, window_url: &Url) -> TrayOriginDecisi if !loopback_http { return TrayOriginDecision { uses_backend_origin: false, - should_log_mismatch: false, }; } let same_port = backend_url.port_or_known_default() == window_url.port_or_known_default(); TrayOriginDecision { uses_backend_origin: same_port, - should_log_mismatch: !same_port, } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3940ca91..fad1579c 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": "AstrBot", - "version": "4.17.5", + "version": "4.17.6", "identifier": "com.astrbot.desktop.tauri", "build": { "beforeDevCommand": "",