From 07790ccd3f8744ccfc6b4eac1c24c3c662fa0c36 Mon Sep 17 00:00:00 2001 From: Cooper-X-Oak Date: Thu, 30 Apr 2026 12:48:55 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(windows):=20=E6=94=B6=E6=95=9B=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=92=8C=E5=BC=80=E5=8F=91=E4=BA=A7=E7=89=A9=E8=B7=AF?= =?UTF-8?q?=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openless-all/app/.gitignore | 1 + .../app/scripts/windows-build-gnu.ps1 | 61 ++++++++-- openless-all/app/scripts/windows-open-dev.ps1 | 77 +++++++++++++ .../app/scripts/windows-runtime-smoke.ps1 | 108 ++++++++++++++++++ openless-all/app/src-tauri/src/lib.rs | 49 ++++---- openless-all/app/src/App.tsx | 44 ++++++- 6 files changed, 307 insertions(+), 33 deletions(-) create mode 100644 openless-all/app/scripts/windows-open-dev.ps1 create mode 100644 openless-all/app/scripts/windows-runtime-smoke.ps1 diff --git a/openless-all/app/.gitignore b/openless-all/app/.gitignore index 112f301d..625e21f7 100644 --- a/openless-all/app/.gitignore +++ b/openless-all/app/.gitignore @@ -4,6 +4,7 @@ dist/ *.local .env .vite/ +.artifacts/ # Tauri src-tauri/target/ diff --git a/openless-all/app/scripts/windows-build-gnu.ps1 b/openless-all/app/scripts/windows-build-gnu.ps1 index e9af3657..2d875d96 100644 --- a/openless-all/app/scripts/windows-build-gnu.ps1 +++ b/openless-all/app/scripts/windows-build-gnu.ps1 @@ -1,39 +1,86 @@ param( - [string]$MirrorRoot = "$env:TEMP\openless-windows-gnu" + [string]$MirrorRoot = "$env:TEMP\openless-windows-gnu", + [string]$ArtifactsRoot = "", + [switch]$KeepMirror ) $ErrorActionPreference = "Stop" $appRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $buildRoot = $appRoot +$usedMirror = $false +if ([string]::IsNullOrWhiteSpace($ArtifactsRoot)) { + $ArtifactsRoot = Join-Path $appRoot ".artifacts\windows-gnu" +} if ($appRoot -match "\s") { Write-Host "[info] App path contains spaces: $appRoot" - Write-Host "[info] Mirroring to no-space build root: $MirrorRoot" + Write-Host "[info] Mirroring to no-space scratch build root: $MirrorRoot" New-Item -ItemType Directory -Force -Path $MirrorRoot | Out-Null - robocopy $appRoot $MirrorRoot /MIR /XD "$appRoot\node_modules" "$appRoot\dist" "$appRoot\src-tauri\target" "$MirrorRoot\node_modules" "$MirrorRoot\dist" "$MirrorRoot\src-tauri\target" | Out-Host + robocopy $appRoot $MirrorRoot /MIR /XD "$appRoot\.artifacts" "$appRoot\node_modules" "$appRoot\dist" "$appRoot\src-tauri\target" "$MirrorRoot\.artifacts" "$MirrorRoot\node_modules" "$MirrorRoot\dist" "$MirrorRoot\src-tauri\target" | Out-Host if ($LASTEXITCODE -gt 7) { throw "robocopy failed with exit code $LASTEXITCODE" } $buildRoot = (Resolve-Path $MirrorRoot).Path + $usedMirror = $true } $env:PATH = "$env:USERPROFILE\.cargo\bin;$env:USERPROFILE\scoop\persist\rustup\.cargo\bin;$env:USERPROFILE\scoop\apps\rustup\current\.cargo\bin;$env:USERPROFILE\scoop\apps\mingw\current\bin;$env:PATH" $env:RUSTUP_TOOLCHAIN = "stable-x86_64-pc-windows-gnu" $env:CARGO_BUILD_TARGET = "x86_64-pc-windows-gnu" +function Resolve-WebView2Loader { + $cargoHome = if ($env:CARGO_HOME) { $env:CARGO_HOME } else { Join-Path $env:USERPROFILE ".cargo" } + $registrySrc = Join-Path $cargoHome "registry\src" + $loader = Get-ChildItem -Path $registrySrc -Recurse -Filter WebView2Loader.dll -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match "\\x64\\WebView2Loader\.dll$" } | + Select-Object -First 1 + if ($null -eq $loader) { + throw "WebView2Loader.dll x64 not found under $registrySrc" + } + return $loader.FullName +} + Push-Location $buildRoot try { if (-not (Test-Path "node_modules")) { npm ci } - npm run tauri build -- --target x86_64-pc-windows-gnu + npm run tauri build -- --target x86_64-pc-windows-gnu --no-bundle + $releaseRoot = Join-Path $buildRoot "src-tauri\target\x86_64-pc-windows-gnu\release" + $artifactDevRoot = Join-Path $ArtifactsRoot "dev" + New-Item -ItemType Directory -Force -Path $artifactDevRoot | Out-Null + Copy-Item -LiteralPath (Join-Path $releaseRoot "openless.exe") -Destination (Join-Path $artifactDevRoot "openless.exe") -Force + Copy-Item -LiteralPath (Resolve-WebView2Loader) -Destination (Join-Path $artifactDevRoot "WebView2Loader.dll") -Force + + npm run tauri build -- --target x86_64-pc-windows-gnu --bundles msi nsis } finally { Pop-Location } +$releaseRoot = Join-Path $buildRoot "src-tauri\target\x86_64-pc-windows-gnu\release" +$artifactReleaseRoot = Join-Path $ArtifactsRoot "release" +New-Item -ItemType Directory -Force -Path $artifactReleaseRoot | Out-Null +Remove-Item -LiteralPath (Join-Path $artifactReleaseRoot "openless.exe") -Force -ErrorAction SilentlyContinue + +if (Test-Path (Join-Path $releaseRoot "bundle")) { + Copy-Item -LiteralPath (Join-Path $releaseRoot "bundle") -Destination $artifactReleaseRoot -Recurse -Force +} + +if ($usedMirror -and (-not $KeepMirror)) { + $resolvedMirror = (Resolve-Path $MirrorRoot).Path + $resolvedTemp = (Resolve-Path $env:TEMP).Path + if ($resolvedMirror.StartsWith($resolvedTemp, [System.StringComparison]::OrdinalIgnoreCase) -and + ((Split-Path $resolvedMirror -Leaf) -eq "openless-windows-gnu")) { + Write-Host "[info] Removing scratch build root: $resolvedMirror" + Remove-Item -LiteralPath $resolvedMirror -Recurse -Force + } else { + Write-Warning "Refusing to remove unexpected mirror path: $resolvedMirror" + } +} + Write-Host "" Write-Host "Windows GNU artifacts:" -Write-Host "$buildRoot\src-tauri\target\x86_64-pc-windows-gnu\release\openless.exe" -Write-Host "$buildRoot\src-tauri\target\x86_64-pc-windows-gnu\release\bundle\msi" -Write-Host "$buildRoot\src-tauri\target\x86_64-pc-windows-gnu\release\bundle\nsis" +Write-Host "$ArtifactsRoot\dev\openless.exe" +Write-Host "$artifactReleaseRoot\bundle\msi" +Write-Host "$artifactReleaseRoot\bundle\nsis" diff --git a/openless-all/app/scripts/windows-open-dev.ps1 b/openless-all/app/scripts/windows-open-dev.ps1 new file mode 100644 index 00000000..f25bb8e7 --- /dev/null +++ b/openless-all/app/scripts/windows-open-dev.ps1 @@ -0,0 +1,77 @@ +param( + [string]$ExePath = "" +) + +$ErrorActionPreference = "Stop" + +if ([string]::IsNullOrWhiteSpace($ExePath)) { + $appRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path + $ExePath = Join-Path $appRoot ".artifacts\windows-gnu\dev\openless.exe" +} + +Add-Type @" +using System; +using System.Runtime.InteropServices; + +public static class OpenLessWindow { + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll")] + public static extern bool SetForegroundWindow(IntPtr hWnd); +} +"@ + +function Show-OpenLessWindow($Process) { + if ($null -eq $Process -or $Process.MainWindowHandle -eq 0) { + return $false + } + + # 9 = SW_RESTORE. This restores minimized windows and leaves normal windows visible. + [OpenLessWindow]::ShowWindow($Process.MainWindowHandle, 9) | Out-Null + [OpenLessWindow]::SetForegroundWindow($Process.MainWindowHandle) | Out-Null + return $true +} + +$running = Get-Process openless -ErrorAction SilentlyContinue | + Where-Object { $_.MainWindowHandle -ne 0 } | + Select-Object -First 1 + +if (Show-OpenLessWindow $running) { + Write-Host "OpenLess is already running; brought window to foreground. pid=$($running.Id)" + exit 0 +} + +if (-not (Test-Path $ExePath)) { + throw "OpenLess executable not found: $ExePath. Run scripts/windows-build-gnu.ps1 first." +} + +if (-not (Test-Path (Join-Path (Split-Path $ExePath -Parent) "WebView2Loader.dll"))) { + throw "WebView2Loader.dll not found beside $ExePath. Run scripts/windows-build-gnu.ps1 again." +} + +if (-not $env:SystemDrive) { + $env:SystemDrive = "C:" +} +if (-not $env:ProgramData) { + $env:ProgramData = Join-Path $env:SystemDrive "ProgramData" +} +$env:PATH = "$env:USERPROFILE\.cargo\bin;$env:USERPROFILE\scoop\persist\rustup\.cargo\bin;$env:USERPROFILE\scoop\apps\rustup\current\.cargo\bin;$env:USERPROFILE\scoop\apps\mingw\current\bin;$env:PATH" +$env:OPENLESS_SHOW_MAIN_ON_START = "1" +try { + $process = Start-Process -FilePath $ExePath -WorkingDirectory (Split-Path $ExePath -Parent) -PassThru +} finally { + Remove-Item Env:OPENLESS_SHOW_MAIN_ON_START -ErrorAction SilentlyContinue +} +$deadline = (Get-Date).AddSeconds(10) + +while ((Get-Date) -lt $deadline) { + Start-Sleep -Milliseconds 250 + $current = Get-Process -Id $process.Id -ErrorAction SilentlyContinue + if (Show-OpenLessWindow $current) { + Write-Host "OpenLess started and brought to foreground. pid=$($current.Id)" + exit 0 + } +} + +throw "OpenLess started but no main window was visible within 10 seconds. pid=$($process.Id)" diff --git a/openless-all/app/scripts/windows-runtime-smoke.ps1 b/openless-all/app/scripts/windows-runtime-smoke.ps1 new file mode 100644 index 00000000..237b153b --- /dev/null +++ b/openless-all/app/scripts/windows-runtime-smoke.ps1 @@ -0,0 +1,108 @@ +param( + [string]$ExePath = "", + [int]$StartupTimeoutSeconds = 12, + [switch]$RequireCredentials +) + +$ErrorActionPreference = "Stop" + +if ([string]::IsNullOrWhiteSpace($ExePath)) { + $appRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path + $ExePath = Join-Path $appRoot ".artifacts\windows-gnu\dev\openless.exe" +} + +if (-not $env:SystemDrive) { + $env:SystemDrive = "C:" +} +if (-not $env:ProgramData) { + $env:ProgramData = Join-Path $env:SystemDrive "ProgramData" +} + +function Test-CredentialValue($Value) { + return ($null -ne $Value) -and ($Value -is [string]) -and ($Value.Trim().Length -gt 0) +} + +function Get-OpenLessCredentialStatus { + $path = Join-Path $env:APPDATA "OpenLess\credentials.json" + if (-not (Test-Path $path)) { + return [pscustomobject]@{ + Path = $path + VolcengineConfigured = $false + ArkConfigured = $false + Present = $false + } + } + + $json = Get-Content -Raw $path | ConvertFrom-Json + $asr = $json.providers.asr.volcengine + $llm = $json.providers.llm.ark + [pscustomobject]@{ + Path = $path + Present = $true + VolcengineConfigured = (Test-CredentialValue $asr.appKey) -and (Test-CredentialValue $asr.accessKey) + ArkConfigured = Test-CredentialValue $llm.apiKey + } +} + +function Wait-LogPattern($Path, $Pattern, $TimeoutSeconds) { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if ((Test-Path $Path) -and ((Get-Content -Raw $Path) -match $Pattern)) { + return $true + } + Start-Sleep -Milliseconds 500 + } + return $false +} + +if (-not (Test-Path $ExePath)) { + throw "OpenLess executable not found: $ExePath" +} + +$logPath = Join-Path $env:LOCALAPPDATA "OpenLess\Logs\openless.log" +$credentialStatus = Get-OpenLessCredentialStatus + +Write-Host "== Credential status ==" +$credentialStatus | Format-List +if (-not $credentialStatus.VolcengineConfigured) { + Write-Host "[warn] Volcengine ASR credentials are not configured; real transcription cannot be completed." +} +if (-not $credentialStatus.ArkConfigured) { + Write-Host "[warn] Ark LLM credentials are not configured; polishing will fall back or fail depending on mode." +} +if ($RequireCredentials -and (-not $credentialStatus.VolcengineConfigured -or -not $credentialStatus.ArkConfigured)) { + throw "Real regression requires configured Volcengine ASR and Ark LLM credentials." +} + +Write-Host "" +Write-Host "== Launch smoke ==" +$process = Start-Process -FilePath $ExePath -PassThru +try { + Start-Sleep -Seconds 4 + $live = Get-Process -Id $process.Id -ErrorAction SilentlyContinue + if (-not $live) { + throw "OpenLess exited during startup." + } + if (-not $live.Responding) { + throw "OpenLess process is not responding." + } + Write-Host "[ok] Process responding: id=$($live.Id), title='$($live.MainWindowTitle)'" + + if (Wait-LogPattern $logPath "hotkey listener installed" $StartupTimeoutSeconds) { + Write-Host "[ok] Hotkey listener installed according to log." + } else { + throw "Hotkey listener did not report installed within $StartupTimeoutSeconds seconds." + } + + Write-Host "" + Write-Host "Manual checks still required:" + Write-Host "- Press the configured physical global hotkey to start/stop recording." + Write-Host "- Speak a short phrase with valid ASR credentials configured." + Write-Host "- Focus Notepad or another text field and verify Windows insert status falls back to copied/Ctrl+V when insertion cannot be confirmed." + Write-Host "- Toggle Windows microphone privacy off/on and rerun Settings -> Permissions." +} finally { + Get-Process openless -ErrorAction SilentlyContinue | Stop-Process -Force +} + +Write-Host "" +Write-Host "Runtime smoke passed." diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 915050d8..d441cb4d 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -76,8 +76,6 @@ pub fn run() { } } - show_main_window(app.handle()); - // 启动时主动弹 Accessibility 授权框(与 Swift `AppDelegate` 行为一致)。 // 用户首次必看到系统提示;已授权则静默返回。 #[cfg(target_os = "macos")] @@ -94,31 +92,38 @@ pub fn run() { // 与 Swift `StatusBarIcon.swift` 行为一致:用全彩 AppIcon,**不**走 template 模式 // (走 template 会被 macOS 染成单色 → 看起来像个黑方块)。 - let _tray = TrayIconBuilder::with_id("main-tray") - .icon(app.default_window_icon().unwrap().clone()) - .icon_as_template(false) - .menu(&menu) - .show_menu_on_left_click(false) - .on_menu_event(|app, event| match event.id.as_ref() { - "toggle" => show_main_window(app), - "quit" => app.exit(0), - _ => {} - }) - .on_tray_icon_event(|tray, event| { - if let TrayIconEvent::Click { - button: MouseButton::Left, - .. - } = event - { - show_main_window(tray.app_handle()); - } - }) - .build(app)?; + if let Some(icon) = app.default_window_icon() { + let _tray = TrayIconBuilder::with_id("main-tray") + .icon(icon.clone()) + .icon_as_template(false) + .menu(&menu) + .show_menu_on_left_click(false) + .on_menu_event(|app, event| match event.id.as_ref() { + "toggle" => show_main_window(app), + "quit" => app.exit(0), + _ => {} + }) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + .. + } = event + { + show_main_window(tray.app_handle()); + } + }) + .build(app)?; + } else { + log::warn!("[startup] default window icon missing; tray icon disabled"); + } // Spin up hotkey listener; coordinator owns the lifecycle. let app_handle = app.handle().clone(); coordinator.bind_app(app_handle); coordinator.start_hotkey_listener(); + if std::env::var("OPENLESS_SHOW_MAIN_ON_START").ok().as_deref() == Some("1") { + show_main_window(app.handle()); + } Ok(()) }) diff --git a/openless-all/app/src/App.tsx b/openless-all/app/src/App.tsx index e4550a7d..f0a22e59 100644 --- a/openless-all/app/src/App.tsx +++ b/openless-all/app/src/App.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { Capsule } from './components/Capsule'; import { FloatingShell } from './components/FloatingShell'; import { Onboarding } from './components/Onboarding'; +import { detectOS } from './components/WindowChrome'; import { checkAccessibilityPermission, checkMicrophonePermission, isTauri } from './lib/ipc'; import { HotkeySettingsProvider } from './state/HotkeySettingsContext'; @@ -16,12 +17,27 @@ export function App({ isCapsule }: AppProps) { return ; } - // 浏览器 dev 时跳过权限检查;只有真正在 Tauri 里才门控。 - const [gate, setGate] = useState(isTauri ? 'checking' : 'ready'); + const os = detectOS(); + // Windows 启动不应被权限探测阻塞首屏。 + const [gate, setGate] = useState(isTauri && os !== 'win' ? 'checking' : 'ready'); useEffect(() => { if (!isTauri) return; let cancelled = false; + requestAnimationFrame(() => { + if (cancelled) return; + import('@tauri-apps/api/window') + .then(({ getCurrentWindow }) => getCurrentWindow().show()) + .catch(error => console.warn('[startup] show main window failed', error)); + }); + return () => { + cancelled = true; + }; + }, []); + + useEffect(() => { + if (!isTauri || os === 'win') return; + let cancelled = false; (async () => { const [a, m] = await Promise.all([ checkAccessibilityPermission(), @@ -35,10 +51,10 @@ export function App({ isCapsule }: AppProps) { return () => { cancelled = true; }; - }, []); + }, [os]); if (gate === 'checking') { - return null; + return ; } return ( @@ -46,3 +62,23 @@ export function App({ isCapsule }: AppProps) { ); } + +function StartupShell() { + return ( +
+
+ + OpenLess 正在启动 +
+
+ ); +} From c940abe929b71b2370e11f903d7b382b225afe90 Mon Sep 17 00:00:00 2001 From: Cooper-X-Oak Date: Thu, 30 Apr 2026 13:34:25 +0800 Subject: [PATCH 2/2] =?UTF-8?q?ci(windows):=20=E6=A0=A1=E9=AA=8C=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=20smoke=20=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8899bb7..d769e900 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - name: Check PowerShell scripts shell: pwsh run: | - foreach ($script in @("./scripts/windows-preflight.ps1", "./scripts/windows-build-gnu.ps1")) { + foreach ($script in @("./scripts/windows-preflight.ps1", "./scripts/windows-build-gnu.ps1", "./scripts/windows-runtime-smoke.ps1")) { $errors = $null [System.Management.Automation.PSParser]::Tokenize((Get-Content -Raw $script), [ref]$errors) | Out-Null if ($errors) {