Merge origin/test_fast into test: Windows 快捷方式/卸载器/窗口最大化

This commit is contained in:
yanghang committed 2026-06-22 01:27:45 +08:00
commit 9e7f8dc70a
9 files changed
+562 -13

No files matched your search

@@ -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 '.*'.
@@ -0,0 +1,37 @@
@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.
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.
timeout /t 3 >nul
exit /b 0
@@ -0,0 +1,146 @@
<#
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.
# 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()
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 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))"
}
}
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"
}
# ── 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
# tree to fully exit, then retries the delete a few times in case a handle lingers.
Write-Step "Scheduling removal of the bundle folder"
$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 ""
Write-Host "GenericAgent has been uninstalled." -ForegroundColor Green
+221
View File
@@ -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",
@@ -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"
]
}
@@ -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"]
+125 -12
View File
@@ -148,6 +148,121 @@ 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<String, serde_json::Value> {
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<bool> {
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. 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() {
#[cfg(windows)]
{
if bundle_root().is_none() {
return;
}
// 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();
}
}
}
/// 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 +288,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 +331,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 +576,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)?;
@@ -552,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.
@@ -655,6 +765,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") {
@@ -17,6 +17,7 @@
"width": 1280,
"height": 800,
"resizable": true,
"maximized": true,
"visible": false,
"url": "loading.html"
},
+16
View File
@@ -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',
@@ -505,6 +506,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',
@@ -5163,6 +5165,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') {