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') {