Merge origin/test_fast into test: Windows 快捷方式/卸载器/窗口最大化
This commit is contained in:
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
|
||||
Generated
+221
@@ -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"]
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in new issue
Block a user