From ccd912da0877f37047bd5e9d97ca2c3da890105a Mon Sep 17 00:00:00 2001 From: yiqi-017 <22300246009@m.fudan.edu.cn> Date: Sun, 21 Jun 2026 21:48:24 +0800 Subject: [PATCH 1/8] feat(desktop): auto-create desktop shortcut on first run (Windows) Windows portable bundle now offers to create a desktop shortcut on first launch via a native yes/no dialog, remembers the choice in ~/.ga_desktop_settings.json (desktop_shortcut), and self-heals the .lnk target to current_exe() on every enabled launch so a relocated bundle keeps working. Also add read_settings/merge_settings and route the three existing settings writers through merge, so sibling keys (like desktop_shortcut) are no longer clobbered by the python_path/project_dir rewrite. --- frontends/desktop/src-tauri/Cargo.lock | 221 +++++++++++++++++++++++++ frontends/desktop/src-tauri/src/lib.rs | 124 ++++++++++++-- 2 files changed, 334 insertions(+), 11 deletions(-) diff --git a/frontends/desktop/src-tauri/Cargo.lock b/frontends/desktop/src-tauri/Cargo.lock index 5108f2d3..72a180b5 100644 --- a/frontends/desktop/src-tauri/Cargo.lock +++ b/frontends/desktop/src-tauri/Cargo.lock @@ -47,6 +47,28 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "ashpd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -85,6 +107,17 @@ dependencies = [ "slab", ] +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + [[package]] name = "async-io" version = "2.6.0" @@ -114,6 +147,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.5.0" @@ -767,6 +811,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.8.2" @@ -805,6 +858,12 @@ dependencies = [ "tendril", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -1135,6 +1194,7 @@ name = "ga-desktop" version = "0.1.0" dependencies = [ "dirs 5.0.1", + "rfd", "serde", "serde_json", "tauri", @@ -2515,6 +2575,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "potential_utf" version = "0.1.5" @@ -2530,6 +2596,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -2638,6 +2713,35 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2758,6 +2862,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2", + "dispatch2", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -2852,6 +2980,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -4005,6 +4139,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" @@ -4218,6 +4358,66 @@ dependencies = [ "semver", ] +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.1", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.98" @@ -5094,6 +5294,26 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -5163,6 +5383,7 @@ dependencies = [ "endi", "enumflags2", "serde", + "url", "winnow 1.0.3", "zvariant_derive", "zvariant_utils", diff --git a/frontends/desktop/src-tauri/src/lib.rs b/frontends/desktop/src-tauri/src/lib.rs index b028abd9..3e2d7614 100644 --- a/frontends/desktop/src-tauri/src/lib.rs +++ b/frontends/desktop/src-tauri/src/lib.rs @@ -148,6 +148,110 @@ fn settings_path() -> PathBuf { .join(".ga_desktop_settings.json") } +/// Read the settings file as a JSON object (empty object when missing/unparseable). +fn read_settings() -> serde_json::Map { + let path = settings_path(); + if let Ok(content) = std::fs::read_to_string(&path) { + if let Ok(serde_json::Value::Object(m)) = serde_json::from_str(&content) { + return m; + } + } + serde_json::Map::new() +} + +/// Merge `updates` into the existing settings file and write it back, preserving any keys +/// we don't touch. The old code rewrote the file with only python_path/project_dir, which +/// would silently drop sibling keys like `desktop_shortcut`. Always go through here. +fn merge_settings(updates: serde_json::Value) { + let mut obj = read_settings(); + if let serde_json::Value::Object(m) = updates { + for (k, v) in m { + obj.insert(k, v); + } + } + let val = serde_json::Value::Object(obj); + if let Ok(text) = serde_json::to_string_pretty(&val) { + let _ = std::fs::write(settings_path(), text); + } +} + +/// Desktop-shortcut preference stored in settings under `desktop_shortcut`. +/// None = never asked (first run) +/// Some(true)/Some(false) = user's remembered choice. +fn read_shortcut_pref() -> Option { + read_settings().get("desktop_shortcut").and_then(|v| v.as_bool()) +} + +fn write_shortcut_pref(enabled: bool) { + merge_settings(serde_json::json!({ "desktop_shortcut": enabled })); +} + +/// Create (or overwrite) a desktop shortcut pointing at the CURRENT exe. Overwriting on every +/// enabled launch is what makes the portable bundle relocatable: move the folder, relaunch, and +/// the shortcut is rewritten to the new path. Windows-only (uses a .lnk via WScript.Shell). +#[cfg(windows)] +fn ensure_desktop_shortcut() { + let Ok(exe) = std::env::current_exe() else { return; }; + let Some(desktop) = dirs::desktop_dir() else { return; }; + let lnk = desktop.join("GenericAgent.lnk"); + let work_dir = exe.parent().map(|p| p.to_path_buf()).unwrap_or_else(|| exe.clone()); + + let exe_s = exe.to_string_lossy().replace('\'', "''"); + let lnk_s = lnk.to_string_lossy().replace('\'', "''"); + let work_s = work_dir.to_string_lossy().replace('\'', "''"); + + // Build the shortcut via WScript.Shell COM, consistent with the existing powershell usage + // elsewhere in this file. No extra crate needed. + let script = format!( + "$ws = New-Object -ComObject WScript.Shell; \ + $sc = $ws.CreateShortcut('{lnk}'); \ + $sc.TargetPath = '{exe}'; \ + $sc.WorkingDirectory = '{work}'; \ + $sc.IconLocation = '{exe}'; \ + $sc.Save()", + lnk = lnk_s, exe = exe_s, work = work_s + ); + + let mut cmd = Command::new("powershell.exe"); + cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &script]); + cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW + let _ = cmd.status(); +} + +#[cfg(not(windows))] +fn ensure_desktop_shortcut() {} + +/// First-run shortcut handling. Windows portable bundles only. Asks once via a native dialog, +/// remembers the choice, and self-heals the shortcut path on every enabled launch. +fn maybe_setup_shortcut() { + // Only for the self-contained Windows portable bundle; skip dev builds and other platforms. + #[cfg(not(windows))] + { return; } + + #[cfg(windows)] + { + if bundle_root().is_none() { + return; + } + match read_shortcut_pref() { + Some(true) => ensure_desktop_shortcut(), + Some(false) => {} + None => { + let create = rfd::MessageDialog::new() + .set_title("GenericAgent") + .set_description("是否在桌面创建 GenericAgent 快捷方式?\nCreate a desktop shortcut for GenericAgent?") + .set_buttons(rfd::MessageButtons::YesNo) + .show(); + let enabled = matches!(create, rfd::MessageDialogResult::Yes); + write_shortcut_pref(enabled); + if enabled { + ensure_desktop_shortcut(); + } + } + } + } +} + /// True when this binary is running from inside a macOS .app bundle (packaged build). /// Used to refuse stale ~/.ga_desktop_settings.json that could point at an old checkout /// when App Translocation hides our own runtime/ from current_exe(). @@ -173,11 +277,10 @@ pub fn get_or_discover_config() -> (String, String) { let python = find_python(); let project = find_project_dir().unwrap_or_default(); if !python.is_empty() && !project.is_empty() { - let json = serde_json::json!({ + merge_settings(serde_json::json!({ "python_path": python, "project_dir": project - }); - let _ = std::fs::write(&path, serde_json::to_string_pretty(&json).unwrap()); + })); return (python, project); } } @@ -217,11 +320,10 @@ pub fn get_or_discover_config() -> (String, String) { // Save discovered config if !python.is_empty() && !project.is_empty() { - let json = serde_json::json!({ + merge_settings(serde_json::json!({ "python_path": python, "project_dir": project - }); - let _ = std::fs::write(&path, serde_json::to_string_pretty(&json).unwrap()); + })); } (python, project) @@ -463,11 +565,8 @@ fn show_bridge_window(app_handle: &tauri::AppHandle) { #[tauri::command] fn start_bridge_with_config(app_handle: tauri::AppHandle, python_path: String, project_dir: String) -> Result<(), String> { - // Save to settings - let path = settings_path(); - let obj = serde_json::json!({"python_path": python_path, "project_dir": project_dir}); - std::fs::write(&path, serde_json::to_string_pretty(&obj).unwrap()) - .map_err(|e| format!("Failed to write settings: {}", e))?; + // Save to settings (merge so sibling keys like desktop_shortcut survive). + merge_settings(serde_json::json!({"python_path": python_path, "project_dir": project_dir})); spawn_bridge_process(&python_path, &project_dir)?; @@ -655,6 +754,9 @@ pub fn run() { let _ = w.set_focus(); } if let Some(sw) = handle.get_webview_window("setup") { let _ = sw.hide(); } + // App is up and reachable: ask-once / self-heal the desktop shortcut. + // Runs last so it never blocks the loading/navigation path. + maybe_setup_shortcut(); } else { // Bridge never came up -> let the user fix paths in the setup window. if let Some(sw) = handle.get_webview_window("setup") { From 15c59dd1e731538ba59879beff60ce8464a40747 Mon Sep 17 00:00:00 2001 From: yiqi-017 <22300246009@m.fudan.edu.cn> Date: Sun, 21 Jun 2026 22:48:26 +0800 Subject: [PATCH 2/8] feat(desktop): add portable uninstaller for Windows Ship uninstall.bat (top-level, double-click entry) + uninstall_windows.ps1 (worker under runtime/) in the Windows portable bundle. The uninstaller: - gracefully stops the bridge, then force-kills bundle-owned processes on ports 14168/8900 (filtered by exe path so other bundles are untouched) - removes the desktop shortcut only when it targets this bundle - removes ~/.ga_desktop_settings.json - schedules deletion of the bundle folder after exit (detached cmd) Wire both scripts into the release-package workflow. --- .github/workflows/desktop-release-package.yml | 3 + desktop_release/scripts/windows/uninstall.bat | 32 +++++ .../scripts/windows/uninstall_windows.ps1 | 115 ++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 desktop_release/scripts/windows/uninstall.bat create mode 100644 desktop_release/scripts/windows/uninstall_windows.ps1 diff --git a/.github/workflows/desktop-release-package.yml b/.github/workflows/desktop-release-package.yml index e8869f5f..59a4b784 100644 --- a/.github/workflows/desktop-release-package.yml +++ b/.github/workflows/desktop-release-package.yml @@ -73,6 +73,9 @@ jobs: # exe at the top level, named GenericAgent (typical portable form) cp "$EXE_SRC" "$PKG/GenericAgent.exe" cp desktop_release/scripts/windows/install_windows.ps1 "$RUNTIME/install_windows.ps1" + # Uninstaller: double-click entry at top level, worker script under runtime/. + cp desktop_release/scripts/windows/uninstall.bat "$PKG/uninstall.bat" + cp desktop_release/scripts/windows/uninstall_windows.ps1 "$RUNTIME/uninstall_windows.ps1" # Embedded Python (python-build-standalone, windows x86_64, 3.12 install_only). # browser_download_url is URL-encoded ('+' -> '%2B'); match loosely with '.*'. diff --git a/desktop_release/scripts/windows/uninstall.bat b/desktop_release/scripts/windows/uninstall.bat new file mode 100644 index 00000000..3b557e5a --- /dev/null +++ b/desktop_release/scripts/windows/uninstall.bat @@ -0,0 +1,32 @@ +@echo off +setlocal +cd /d "%~dp0" + +echo ============================================================ +echo GenericAgent Desktop - Uninstall +echo ============================================================ +echo. +echo This will completely remove GenericAgent from this computer: +echo - stop its background services (ports 14168 / 8900) +echo - delete the desktop shortcut +echo - delete settings (%%USERPROFILE%%\.ga_desktop_settings.json) +echo - delete THIS folder and everything in it: +echo "%~dp0" +echo. +echo This cannot be undone. +echo. +set /p CONFIRM="Type Y to uninstall, anything else to cancel: " +if /i not "%CONFIRM%"=="Y" ( + echo. + echo Cancelled. Nothing was changed. + pause + exit /b 0 +) + +echo. +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0runtime\uninstall_windows.ps1" -BundleDir "%~dp0" + +echo. +echo You can close this window now. +timeout /t 3 >nul +exit /b 0 diff --git a/desktop_release/scripts/windows/uninstall_windows.ps1 b/desktop_release/scripts/windows/uninstall_windows.ps1 new file mode 100644 index 00000000..8fe94e0a --- /dev/null +++ b/desktop_release/scripts/windows/uninstall_windows.ps1 @@ -0,0 +1,115 @@ +<# +GenericAgent Desktop — portable uninstall (Windows). + +Removes everything THIS portable bundle put on the machine, then deletes the +bundle folder itself: + 1. Stop the bundle's backend processes (bridge 14168 / conductor 8900 / scheduler) + — only processes whose executable lives inside this bundle, so other bundles + on the same machine are left alone. + 2. Remove the desktop shortcut (GenericAgent.lnk) — only when it points into + this bundle. + 3. Remove ~/.ga_desktop_settings.json (shared settings; other bundles rebuild it + automatically on next launch). + 4. Schedule deletion of the bundle folder after this script exits (a folder + cannot delete itself while code runs inside it). + +Invoked by uninstall.bat (which confirms with the user first). Not meant to be +run directly without -BundleDir. +#> + +param( + [Parameter(Mandatory = $true)] + [string]$BundleDir +) + +$ErrorActionPreference = "SilentlyContinue" + +function Write-Step([string]$m) { Write-Host "==> $m" -ForegroundColor Cyan } +function Write-Ok([string]$m) { Write-Host "[OK] $m" -ForegroundColor Green } +function Write-Info([string]$m) { Write-Host " $m" -ForegroundColor Gray } + +# Normalize the bundle dir to an absolute path with a trailing separator for prefix checks. +try { $bundle = (Resolve-Path -LiteralPath $BundleDir).Path } catch { $bundle = $BundleDir } +$bundlePrefix = ($bundle.TrimEnd('\') + '\').ToLowerInvariant() + +function Path-IsInsideBundle([string]$p) { + if (-not $p) { return $false } + try { $rp = (Resolve-Path -LiteralPath $p -ErrorAction Stop).Path } catch { $rp = $p } + return $rp.ToLowerInvariant().StartsWith($bundlePrefix) +} + +# ── 1. Graceful backend shutdown, then force-kill bundle-owned processes ────── +Write-Step "Stopping GenericAgent backend services" + +# Best-effort graceful exit: tell the bridge to stop its managed extras and quit. +try { + Invoke-WebRequest -Uri "http://127.0.0.1:14168/services/bridge/exit" -Method Post ` + -TimeoutSec 3 -UseBasicParsing | Out-Null + Start-Sleep -Milliseconds 800 +} catch { } + +# Force-kill anything still listening on our ports, but ONLY if the owning process +# executable lives inside this bundle (don't disturb a second installed copy). +foreach ($port in 14168, 8900) { + foreach ($conn in (Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue)) { + $proc = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue + if ($proc -and (Path-IsInsideBundle $proc.Path)) { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + Write-Info "killed PID $($proc.Id) ($($proc.ProcessName)) on port $port" + } elseif ($proc) { + Write-Info "port $port held by a process outside this bundle (PID $($proc.Id)); left running" + } + } +} + +# Kill any remaining GenericAgent.exe / python.exe whose image is inside this bundle +# (e.g. the desktop shell itself, or a child python not bound to the two ports). +foreach ($p in (Get-Process -ErrorAction SilentlyContinue | Where-Object { + $_.Name -in @('GenericAgent', 'python', 'pythonw') })) { + if (Path-IsInsideBundle $p.Path) { + Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue + Write-Info "killed PID $($p.Id) ($($p.ProcessName))" + } +} +Write-Ok "backend stopped" + +# ── 2. Desktop shortcut (only if it targets this bundle) ───────────────────── +Write-Step "Removing desktop shortcut" +$desktop = [Environment]::GetFolderPath('Desktop') +$lnk = Join-Path $desktop 'GenericAgent.lnk' +if (Test-Path -LiteralPath $lnk) { + $target = $null + try { + $ws = New-Object -ComObject WScript.Shell + $target = $ws.CreateShortcut($lnk).TargetPath + } catch { } + if ((-not $target) -or (Path-IsInsideBundle $target)) { + Remove-Item -LiteralPath $lnk -Force -ErrorAction SilentlyContinue + Write-Ok "removed $lnk" + } else { + Write-Info "desktop shortcut points to another bundle; left in place" + } +} else { + Write-Info "no desktop shortcut found" +} + +# ── 3. Shared settings file ────────────────────────────────────────────────── +Write-Step "Removing settings file" +$settings = Join-Path $env:USERPROFILE '.ga_desktop_settings.json' +if (Test-Path -LiteralPath $settings) { + Remove-Item -LiteralPath $settings -Force -ErrorAction SilentlyContinue + Write-Ok "removed $settings" +} else { + Write-Info "no settings file found" +} + +# ── 4. Schedule deletion of the bundle folder ──────────────────────────────── +# The folder cannot remove itself while this script runs from inside it. Spawn a +# detached cmd that waits for our process tree to exit, then deletes the folder. +Write-Step "Scheduling removal of the bundle folder" +$cmd = "cd /d `"$env:TEMP`" & ping 127.0.0.1 -n 3 >nul & rd /s /q `"$bundle`"" +Start-Process -FilePath "cmd.exe" -ArgumentList "/c", $cmd -WindowStyle Hidden | Out-Null +Write-Ok "bundle folder will be deleted after exit: $bundle" + +Write-Host "" +Write-Host "GenericAgent has been uninstalled." -ForegroundColor Green From 4e1fa78ecfa0b54d4ea949c55012045c7d247167 Mon Sep 17 00:00:00 2001 From: yiqi-017 <22300246009@m.fudan.edu.cn> Date: Sun, 21 Jun 2026 23:18:29 +0800 Subject: [PATCH 3/8] fix(desktop): first-run shortcut prompt + uninstaller folder deletion Bug 1 (shortcut prompt only showed on the second launch): a native rfd dialog spawned from the Rust startup thread has no parent window and got buried behind the main window on first launch. Move the prompt to the web UI: add shortcut_should_ask / shortcut_decide commands; the frontend asks once via window.confirm after load (always on top). The startup thread now only self-heals the shortcut path when already enabled. Bug 2 (uninstall.bat could not delete the folder): kill ANY process whose exe lives inside the bundle (not just GenericAgent/python by name), and run the folder delete from a detached temp .bat that retries the recursive remove ~20x to cover the window where uninstall.bat releases its handle. --- .../scripts/windows/uninstall_windows.ps1 | 29 +++++++--- .../src-tauri/capabilities/default.json | 4 +- .../permissions/bridge-commands.toml | 10 ++++ frontends/desktop/src-tauri/src/lib.rs | 55 +++++++++++-------- frontends/desktop/static/app.js | 16 ++++++ 5 files changed, 83 insertions(+), 31 deletions(-) diff --git a/desktop_release/scripts/windows/uninstall_windows.ps1 b/desktop_release/scripts/windows/uninstall_windows.ps1 index 8fe94e0a..1cfb97cb 100644 --- a/desktop_release/scripts/windows/uninstall_windows.ps1 +++ b/desktop_release/scripts/windows/uninstall_windows.ps1 @@ -62,10 +62,10 @@ foreach ($port in 14168, 8900) { } } -# Kill any remaining GenericAgent.exe / python.exe whose image is inside this bundle -# (e.g. the desktop shell itself, or a child python not bound to the two ports). -foreach ($p in (Get-Process -ErrorAction SilentlyContinue | Where-Object { - $_.Name -in @('GenericAgent', 'python', 'pythonw') })) { +# Kill ANY process whose executable image lives inside this bundle, regardless of name +# (GenericAgent.exe, python.exe, or any child tool it spawned). Limiting to the bundle path +# means we never touch a second installed copy. +foreach ($p in (Get-Process -ErrorAction SilentlyContinue)) { if (Path-IsInsideBundle $p.Path) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue Write-Info "killed PID $($p.Id) ($($p.ProcessName))" @@ -104,11 +104,24 @@ if (Test-Path -LiteralPath $settings) { } # ── 4. Schedule deletion of the bundle folder ──────────────────────────────── -# The folder cannot remove itself while this script runs from inside it. Spawn a -# detached cmd that waits for our process tree to exit, then deletes the folder. +# The folder cannot remove itself while this script (and the uninstall.bat that +# launched it) run from inside it. Spawn a detached cmd that waits for our process +# tree to fully exit, then retries the delete a few times in case a handle lingers. Write-Step "Scheduling removal of the bundle folder" -$cmd = "cd /d `"$env:TEMP`" & ping 127.0.0.1 -n 3 >nul & rd /s /q `"$bundle`"" -Start-Process -FilePath "cmd.exe" -ArgumentList "/c", $cmd -WindowStyle Hidden | Out-Null +$deleter = @" +cd /d "%TEMP%" +for /l %%i in (1,1,20) do ( + rd /s /q "$bundle" 2>nul + if not exist "$bundle" goto done + ping 127.0.0.1 -n 2 >nul +) +:done +"@ +$deleterPath = Join-Path $env:TEMP ("ga_uninstall_{0}.bat" -f ([guid]::NewGuid().ToString('N'))) +Set-Content -LiteralPath $deleterPath -Value $deleter -Encoding ASCII +# Start detached so it survives this script + uninstall.bat exiting. The retry loop +# (20 tries x ~1s) covers the brief window where the parent processes release handles. +Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "`"$deleterPath`"" -WindowStyle Hidden | Out-Null Write-Ok "bundle folder will be deleted after exit: $bundle" Write-Host "" diff --git a/frontends/desktop/src-tauri/capabilities/default.json b/frontends/desktop/src-tauri/capabilities/default.json index 2840cdb3..cf96e341 100644 --- a/frontends/desktop/src-tauri/capabilities/default.json +++ b/frontends/desktop/src-tauri/capabilities/default.json @@ -17,6 +17,8 @@ "allow-start-bridge", "allow-start-bridge-with-config", "allow-get-config", - "allow-export-mykey" + "allow-export-mykey", + "allow-shortcut-should-ask", + "allow-shortcut-decide" ] } diff --git a/frontends/desktop/src-tauri/permissions/bridge-commands.toml b/frontends/desktop/src-tauri/permissions/bridge-commands.toml index 76a3b513..cf78df39 100644 --- a/frontends/desktop/src-tauri/permissions/bridge-commands.toml +++ b/frontends/desktop/src-tauri/permissions/bridge-commands.toml @@ -17,3 +17,13 @@ commands.allow = ["get_config"] identifier = "allow-export-mykey" description = "Save mykey.py via native file dialog." commands.allow = ["export_mykey"] + +[[permission]] +identifier = "allow-shortcut-should-ask" +description = "Check whether to show the first-run desktop-shortcut prompt." +commands.allow = ["shortcut_should_ask"] + +[[permission]] +identifier = "allow-shortcut-decide" +description = "Persist the user's desktop-shortcut choice and create it if enabled." +commands.allow = ["shortcut_decide"] diff --git a/frontends/desktop/src-tauri/src/lib.rs b/frontends/desktop/src-tauri/src/lib.rs index 3e2d7614..502622f3 100644 --- a/frontends/desktop/src-tauri/src/lib.rs +++ b/frontends/desktop/src-tauri/src/lib.rs @@ -221,33 +221,44 @@ fn ensure_desktop_shortcut() { #[cfg(not(windows))] fn ensure_desktop_shortcut() {} -/// First-run shortcut handling. Windows portable bundles only. Asks once via a native dialog, -/// remembers the choice, and self-heals the shortcut path on every enabled launch. +/// First-run shortcut handling. Windows portable bundles only. Self-heals the shortcut path on +/// every enabled launch (cheap, no UI). The first-run ASK is driven by the frontend (see the +/// `shortcut_should_ask` / `shortcut_decide` commands): a native dialog from this background +/// startup thread has no parent window and gets buried behind the main window on first launch, +/// so the prompt is owned by the web UI instead, which always renders on top. fn maybe_setup_shortcut() { - // Only for the self-contained Windows portable bundle; skip dev builds and other platforms. - #[cfg(not(windows))] - { return; } - #[cfg(windows)] { if bundle_root().is_none() { return; } - match read_shortcut_pref() { - Some(true) => ensure_desktop_shortcut(), - Some(false) => {} - None => { - let create = rfd::MessageDialog::new() - .set_title("GenericAgent") - .set_description("是否在桌面创建 GenericAgent 快捷方式?\nCreate a desktop shortcut for GenericAgent?") - .set_buttons(rfd::MessageButtons::YesNo) - .show(); - let enabled = matches!(create, rfd::MessageDialogResult::Yes); - write_shortcut_pref(enabled); - if enabled { - ensure_desktop_shortcut(); - } - } + // Only self-heal when the user already opted in. Never prompt here. + if read_shortcut_pref() == Some(true) { + ensure_desktop_shortcut(); + } + } +} + +/// Frontend asks whether to show the first-run "create desktop shortcut?" prompt. +/// True only on a Windows portable bundle whose preference has never been set. +#[tauri::command] +fn shortcut_should_ask() -> bool { + #[cfg(windows)] + { + return bundle_root().is_some() && read_shortcut_pref().is_none(); + } + #[cfg(not(windows))] + { false } +} + +/// Frontend reports the user's choice. Persists it and creates the shortcut when enabled. +#[tauri::command] +fn shortcut_decide(create: bool) { + write_shortcut_pref(create); + #[cfg(windows)] + { + if create { + ensure_desktop_shortcut(); } } } @@ -651,7 +662,7 @@ pub fn run() { let _ = w.set_focus(); } })) - .invoke_handler(tauri::generate_handler![start_bridge_with_config, start_bridge, get_config, export_mykey]) + .invoke_handler(tauri::generate_handler![start_bridge_with_config, start_bridge, get_config, export_mykey, shortcut_should_ask, shortcut_decide]) .setup(move |app| { // Show the loading window immediately so the first-run prepare isn't a blank screen. // The window starts on loading.html (a local page), so no "connection refused" flash. diff --git a/frontends/desktop/static/app.js b/frontends/desktop/static/app.js index b23512bf..e21d66e8 100644 --- a/frontends/desktop/static/app.js +++ b/frontends/desktop/static/app.js @@ -333,6 +333,7 @@ const I18N = { 'customPreset.editTitle': '编辑', 'builtinPreset.restoreBtn': '恢复默认预设', 'set.appearance': '外观', 'set.plainUi': '素色', 'set.fontSize': '聊天字号', 'set.lang': '语言', 'set.model': '模型', 'set.addModel': '添加模型', 'set.features': '功能', 'set.importMykey': '导入已有模型配置(mykey.py)', 'set.exportMykey': '导出当前模型配置', 'set.serviceManager': '后台服务管理', + 'shortcut.askConfirm': '是否在桌面创建 GenericAgent 快捷方式?', 'appearance.light': '浅色', 'appearance.dark': '深色', 'set.noModels': '暂无模型,点击下方添加', 'lang.zh': '简体中文', 'lang.en': 'English', @@ -500,6 +501,7 @@ const I18N = { 'customPreset.editTitle': 'Edit', 'builtinPreset.restoreBtn': 'Restore defaults', 'set.appearance': 'Appearance', 'set.plainUi': 'Plain', 'set.fontSize': 'Chat font size', 'set.lang': 'Language', 'set.model': 'Model', 'set.addModel': 'Add model', 'set.features': 'Features', 'set.importMykey': 'Import model config (mykey.py)', 'set.exportMykey': 'Export current model config', 'set.serviceManager': 'Service manager', + 'shortcut.askConfirm': 'Create a desktop shortcut for GenericAgent?', 'appearance.light': 'Light', 'appearance.dark': 'Dark', 'set.noModels': 'No models yet — add one below', 'lang.zh': '简体中文', 'lang.en': 'English', @@ -4966,6 +4968,20 @@ function showChanToast(title, detail, kind) { // Preset form: 每字段独立内联提示(标题/Prompt 各自独立,互不串扰) bindFieldInlineLimit(document.getElementById('cp-title')); bindFieldInlineLimit(document.getElementById('cp-prompt')); + + // First-run desktop-shortcut prompt (Windows portable bundle only). Driven from the web UI + // so the dialog always renders on top — a native dialog from the Rust startup thread had no + // parent window and got buried behind the main window on first launch. + maybeAskDesktopShortcut(); + } + + async function maybeAskDesktopShortcut() { + try { + const should = await window.ga.tauriInvoke('shortcut_should_ask'); + if (!should) return; + const create = window.confirm(t('shortcut.askConfirm')); + await window.ga.tauriInvoke('shortcut_decide', { create }); + } catch (_) { /* not in tauri / not a bundle — ignore */ } } if (document.readyState === 'loading') { From 0191b286094d3fa0ebe9ca3379014061685c6ac0 Mon Sep 17 00:00:00 2001 From: yiqi-017 <22300246009@m.fudan.edu.cn> Date: Sun, 21 Jun 2026 23:41:33 +0800 Subject: [PATCH 4/8] chore(desktop): TEMP diagnostic banner for first-run shortcut prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not a fix — instruments maybeAskDesktopShortcut with an on-page banner reporting __TAURI__ availability, shortcut_should_ask result, and whether window.confirm fires/returns, so we can locate why the first-run prompt never shows. To be reverted once the cause is confirmed. --- frontends/desktop/static/app.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/frontends/desktop/static/app.js b/frontends/desktop/static/app.js index e21d66e8..42885e07 100644 --- a/frontends/desktop/static/app.js +++ b/frontends/desktop/static/app.js @@ -4976,12 +4976,35 @@ function showChanToast(title, detail, kind) { } async function maybeAskDesktopShortcut() { + // ── TEMP DIAGNOSTIC (Bug 2): show a visible on-page banner reporting each step, + // so we can tell "should_ask returned false" vs "confirm got swallowed" without devtools. + const diag = (txt, color) => { + let b = document.getElementById('ga-diag-banner'); + if (!b) { + b = document.createElement('div'); + b.id = 'ga-diag-banner'; + b.style.cssText = 'position:fixed;left:0;right:0;top:0;z-index:999999;padding:10px 14px;' + + 'font:13px/1.4 monospace;white-space:pre-wrap;background:#222;color:#0f0;' + + 'border-bottom:2px solid #0f0;max-height:50vh;overflow:auto'; + document.body.appendChild(b); + } + if (color) b.style.color = color; + b.textContent += txt + '\n'; + }; try { + diag('[diag] start; __TAURI__=' + (typeof window.__TAURI__) + ' invoke=' + + (typeof window.__TAURI__?.core?.invoke)); const should = await window.ga.tauriInvoke('shortcut_should_ask'); - if (!should) return; + diag('[diag] shortcut_should_ask = ' + JSON.stringify(should)); + if (!should) { diag('[diag] should_ask is false -> not prompting', '#ff0'); return; } + diag('[diag] calling window.confirm now...'); const create = window.confirm(t('shortcut.askConfirm')); + diag('[diag] confirm returned = ' + create, '#0ff'); await window.ga.tauriInvoke('shortcut_decide', { create }); - } catch (_) { /* not in tauri / not a bundle — ignore */ } + diag('[diag] shortcut_decide done (create=' + create + ')', '#0f0'); + } catch (e) { + diag('[diag] EXCEPTION: ' + (e && e.message ? e.message : String(e)), '#f55'); + } } if (document.readyState === 'loading') { From eab0b4a02b91f79d8541c7ddf9b24dd1238c0f4b Mon Sep 17 00:00:00 2001 From: yiqi-017 <22300246009@m.fudan.edu.cn> Date: Mon, 22 Jun 2026 00:08:26 +0800 Subject: [PATCH 5/8] chore(desktop): remove temp diagnostic banner from shortcut prompt Bug 2 root cause was WebView2 serving a stale cached app.js (no Cache-Control header on the bridge static response). With a fresh extract / cleared cache the first-run prompt works. Revert the diagnostic instrumentation back to the clean maybeAskDesktopShortcut. --- frontends/desktop/static/app.js | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/frontends/desktop/static/app.js b/frontends/desktop/static/app.js index 42885e07..e21d66e8 100644 --- a/frontends/desktop/static/app.js +++ b/frontends/desktop/static/app.js @@ -4976,35 +4976,12 @@ function showChanToast(title, detail, kind) { } async function maybeAskDesktopShortcut() { - // ── TEMP DIAGNOSTIC (Bug 2): show a visible on-page banner reporting each step, - // so we can tell "should_ask returned false" vs "confirm got swallowed" without devtools. - const diag = (txt, color) => { - let b = document.getElementById('ga-diag-banner'); - if (!b) { - b = document.createElement('div'); - b.id = 'ga-diag-banner'; - b.style.cssText = 'position:fixed;left:0;right:0;top:0;z-index:999999;padding:10px 14px;' - + 'font:13px/1.4 monospace;white-space:pre-wrap;background:#222;color:#0f0;' - + 'border-bottom:2px solid #0f0;max-height:50vh;overflow:auto'; - document.body.appendChild(b); - } - if (color) b.style.color = color; - b.textContent += txt + '\n'; - }; try { - diag('[diag] start; __TAURI__=' + (typeof window.__TAURI__) + ' invoke=' - + (typeof window.__TAURI__?.core?.invoke)); const should = await window.ga.tauriInvoke('shortcut_should_ask'); - diag('[diag] shortcut_should_ask = ' + JSON.stringify(should)); - if (!should) { diag('[diag] should_ask is false -> not prompting', '#ff0'); return; } - diag('[diag] calling window.confirm now...'); + if (!should) return; const create = window.confirm(t('shortcut.askConfirm')); - diag('[diag] confirm returned = ' + create, '#0ff'); await window.ga.tauriInvoke('shortcut_decide', { create }); - diag('[diag] shortcut_decide done (create=' + create + ')', '#0f0'); - } catch (e) { - diag('[diag] EXCEPTION: ' + (e && e.message ? e.message : String(e)), '#f55'); - } + } catch (_) { /* not in tauri / not a bundle — ignore */ } } if (document.readyState === 'loading') { From cd9f029edcda961760f0c69c70b62b96c3e4c706 Mon Sep 17 00:00:00 2001 From: yiqi-017 <22300246009@m.fudan.edu.cn> Date: Mon, 22 Jun 2026 00:31:11 +0800 Subject: [PATCH 6/8] fix(desktop): uninstaller arg bug + no-cache for bridge static assets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1 (uninstall.bat could not delete the folder): %~dp0 ends with a backslash, so -BundleDir "%~dp0" passed "...\dir\" — the trailing \" escaped the closing quote and PowerShell received a path with a literal quote ("Illegal characters in path"). Resolve-Path failed, the deleter ran rd on a garbage path, and nothing was removed. Strip the trailing backslash in the .bat before passing, and defensively trim stray quotes/separators in the ps1. Bug 2 root cause (shortcut prompt / UI showed stale code after updates): the bridge served app.js via aiohttp's static handler with only ETag/Last-Modified and no Cache-Control, so WebView2 heuristically cached it and kept serving an old app.js after rebuilds. Add a no_cache_middleware that sets Cache-Control: no-cache, no-store, must-revalidate on GET/HEAD. --- desktop_release/scripts/windows/uninstall.bat | 7 ++++++- .../scripts/windows/uninstall_windows.ps1 | 3 +++ frontends/desktop_bridge.py | 17 ++++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/desktop_release/scripts/windows/uninstall.bat b/desktop_release/scripts/windows/uninstall.bat index 3b557e5a..077652ec 100644 --- a/desktop_release/scripts/windows/uninstall.bat +++ b/desktop_release/scripts/windows/uninstall.bat @@ -24,7 +24,12 @@ if /i not "%CONFIRM%"=="Y" ( ) echo. -powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0runtime\uninstall_windows.ps1" -BundleDir "%~dp0" +rem %~dp0 ends with a backslash; passing "...\dir\" makes the trailing \" escape the closing +rem quote, so PowerShell receives a path with a literal quote ("Illegal characters in path"). +rem Strip the trailing backslash before passing the bundle dir. +set "BUNDLE=%~dp0" +if "%BUNDLE:~-1%"=="\" set "BUNDLE=%BUNDLE:~0,-1%" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0runtime\uninstall_windows.ps1" -BundleDir "%BUNDLE%" echo. echo You can close this window now. diff --git a/desktop_release/scripts/windows/uninstall_windows.ps1 b/desktop_release/scripts/windows/uninstall_windows.ps1 index 1cfb97cb..1351eca2 100644 --- a/desktop_release/scripts/windows/uninstall_windows.ps1 +++ b/desktop_release/scripts/windows/uninstall_windows.ps1 @@ -29,6 +29,9 @@ function Write-Ok([string]$m) { Write-Host "[OK] $m" -ForegroundColor Green } function Write-Info([string]$m) { Write-Host " $m" -ForegroundColor Gray } # Normalize the bundle dir to an absolute path with a trailing separator for prefix checks. +# Defensive: strip stray quotes / trailing separators a caller may have leaked in (e.g. the +# classic "%~dp0" trailing-backslash-before-quote bug). +$BundleDir = $BundleDir.Trim().Trim('"').TrimEnd('\', '/') try { $bundle = (Resolve-Path -LiteralPath $BundleDir).Path } catch { $bundle = $BundleDir } $bundlePrefix = ($bundle.TrimEnd('\') + '\').ToLowerInvariant() diff --git a/frontends/desktop_bridge.py b/frontends/desktop_bridge.py index ff1cf3bd..be8d96b0 100644 --- a/frontends/desktop_bridge.py +++ b/frontends/desktop_bridge.py @@ -1050,6 +1050,21 @@ async def cors_middleware(request, handler): return resp +@web.middleware +async def no_cache_middleware(request, handler): + # WebView2 (Chromium) heuristically caches assets that ship only ETag/Last-Modified with no + # Cache-Control (aiohttp's static handler does exactly that), so a rebuilt app.js/index.html + # keeps serving stale from cache for ~10% of its age — users saw old UI after updates. Force + # revalidation on every GET so the desktop shell always loads the bundle's current frontend. + resp = await handler(request) + try: + if request.method in ("GET", "HEAD"): + resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" + except Exception: + pass + return resp + + def json_ok(data: dict, status: int = 200): return web.json_response(data, status=status, headers=cors_headers(), dumps=lambda x: json.dumps(x, ensure_ascii=False, default=str)) @@ -1585,7 +1600,7 @@ async def post_token_history_handler(request): def create_app(): - app = web.Application(middlewares=[cors_middleware], client_max_size=500 * 1024 * 1024) + app = web.Application(middlewares=[cors_middleware, no_cache_middleware], client_max_size=500 * 1024 * 1024) app.router.add_get("/ws", ws_handler) app.router.add_get("/status", status_handler) app.router.add_get("/config", get_config_handler) From 24c6a64d0ed5189aadeac2a5c60c369ae7bd9064 Mon Sep 17 00:00:00 2001 From: yiqi-017 <22300246009@m.fudan.edu.cn> Date: Mon, 22 Jun 2026 00:38:08 +0800 Subject: [PATCH 7/8] Revert no-cache middleware for bridge static assets Back out the no_cache_middleware added in f3a85d1. Fresh installs have no cache so it is not needed for first run; cache during testing is cleared manually. The uninstaller arg-bug fix from the same commit is kept. --- frontends/desktop_bridge.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/frontends/desktop_bridge.py b/frontends/desktop_bridge.py index be8d96b0..ff1cf3bd 100644 --- a/frontends/desktop_bridge.py +++ b/frontends/desktop_bridge.py @@ -1050,21 +1050,6 @@ async def cors_middleware(request, handler): return resp -@web.middleware -async def no_cache_middleware(request, handler): - # WebView2 (Chromium) heuristically caches assets that ship only ETag/Last-Modified with no - # Cache-Control (aiohttp's static handler does exactly that), so a rebuilt app.js/index.html - # keeps serving stale from cache for ~10% of its age — users saw old UI after updates. Force - # revalidation on every GET so the desktop shell always loads the bundle's current frontend. - resp = await handler(request) - try: - if request.method in ("GET", "HEAD"): - resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" - except Exception: - pass - return resp - - def json_ok(data: dict, status: int = 200): return web.json_response(data, status=status, headers=cors_headers(), dumps=lambda x: json.dumps(x, ensure_ascii=False, default=str)) @@ -1600,7 +1585,7 @@ async def post_token_history_handler(request): def create_app(): - app = web.Application(middlewares=[cors_middleware, no_cache_middleware], client_max_size=500 * 1024 * 1024) + app = web.Application(middlewares=[cors_middleware], client_max_size=500 * 1024 * 1024) app.router.add_get("/ws", ws_handler) app.router.add_get("/status", status_handler) app.router.add_get("/config", get_config_handler) From cf17adab24297ddc17ffad4cb76e5b8f5e3a0408 Mon Sep 17 00:00:00 2001 From: yiqi-017 <22300246009@m.fudan.edu.cn> Date: Mon, 22 Jun 2026 00:50:42 +0800 Subject: [PATCH 8/8] feat(desktop): maximize main window + uninstall clears WebView2 data - Main window now opens maximized (maximized: true); 1280x800 stays as the restore size. Previously it always opened windowed. - Uninstaller now also removes %LOCALAPPDATA%\com.genericagent.app (WebView2 EBWebView cache + localStorage), which lives outside the bundle. Only the Tauri desktop shell uses it, so removal is safe. --- .../scripts/windows/uninstall_windows.ps1 | 15 +++++++++++++++ frontends/desktop/src-tauri/tauri.conf.json | 1 + 2 files changed, 16 insertions(+) diff --git a/desktop_release/scripts/windows/uninstall_windows.ps1 b/desktop_release/scripts/windows/uninstall_windows.ps1 index 1351eca2..e2262585 100644 --- a/desktop_release/scripts/windows/uninstall_windows.ps1 +++ b/desktop_release/scripts/windows/uninstall_windows.ps1 @@ -106,6 +106,21 @@ if (Test-Path -LiteralPath $settings) { Write-Info "no settings file found" } +# ── 3b. WebView2 data dir (cache + localStorage) ───────────────────────────── +# Tauri creates %LOCALAPPDATA%\com.genericagent.app\ (EBWebView: HTTP cache + localStorage) +# outside the bundle, keyed by the app identifier. Only the Tauri desktop shell uses it (the +# project's other frontends — qt/tui/conductor — do not), so removing it is safe. Other +# GenericAgent bundles share the same identifier and would just rebuild it on next launch. +Write-Step "Removing WebView2 data" +$wv = Join-Path $env:LOCALAPPDATA 'com.genericagent.app' +if (Test-Path -LiteralPath $wv) { + Remove-Item -LiteralPath $wv -Recurse -Force -ErrorAction SilentlyContinue + if (Test-Path -LiteralPath $wv) { Write-Info "WebView2 data partially locked; some files remain" } + else { Write-Ok "removed $wv" } +} else { + Write-Info "no WebView2 data found" +} + # ── 4. Schedule deletion of the bundle folder ──────────────────────────────── # The folder cannot remove itself while this script (and the uninstall.bat that # launched it) run from inside it. Spawn a detached cmd that waits for our process diff --git a/frontends/desktop/src-tauri/tauri.conf.json b/frontends/desktop/src-tauri/tauri.conf.json index 1bb06aea..5681ffa1 100644 --- a/frontends/desktop/src-tauri/tauri.conf.json +++ b/frontends/desktop/src-tauri/tauri.conf.json @@ -17,6 +17,7 @@ "width": 1280, "height": 800, "resizable": true, + "maximized": true, "visible": false, "url": "loading.html" },