fix(windows): 收敛启动和开发产物路径 (#48)

* fix(windows): 收敛启动和开发产物路径

* ci(windows): 校验运行时 smoke 脚本

---------

Co-authored-by: Cooper-X-Oak <Cooper-X-Oak@users.noreply.github.com>
This commit is contained in:
Cooper OakandCooper-X-Oak authored and GitHub committed 2026-04-30 14:21:35 +08:00
1 parent 96ff66eb80
commit ff694c07c1
7 files changed
+284 -14

No files matched your search

+1 -1
View File
@@ -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) {
+1
View File
@@ -4,6 +4,7 @@ dist/
*.local
.env
.vite/
.artifacts/
# Tauri
src-tauri/target/
+54 -7
View File
@@ -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"
@@ -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)"
@@ -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."
+3 -2
View File
@@ -83,8 +83,6 @@ pub fn run() {
}
}
show_main_window(app.handle());
// 启动时主动弹 Accessibility 授权框(与 Swift `AppDelegate` 行为一致)。
// 用户首次必看到系统提示;已授权则静默返回。
#[cfg(target_os = "macos")]
@@ -130,6 +128,9 @@ pub fn run() {
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(())
})
+40 -4
View File
@@ -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 <Capsule />;
}
// 浏览器 dev 时跳过权限检查;只有真正在 Tauri 里才门控。
const [gate, setGate] = useState<Gate>(isTauri ? 'checking' : 'ready');
const os = detectOS();
// Windows 启动不应被权限探测阻塞首屏。
const [gate, setGate] = useState<Gate>(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 <StartupShell />;
}
return (
<HotkeySettingsProvider>
@@ -46,3 +62,23 @@ export function App({ isCapsule }: AppProps) {
</HotkeySettingsProvider>
);
}
function StartupShell() {
return (
<div
style={{
minHeight: '100vh',
display: 'grid',
placeItems: 'center',
background: 'linear-gradient(180deg, rgba(245,245,247,0.96) 0%, rgba(232,232,236,0.96) 100%)',
color: 'var(--ol-ink-3)',
fontFamily: 'var(--ol-font-sans)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: 13, fontWeight: 500 }}>
<img src="AppIcon.png" alt="" style={{ width: 18, height: 18, borderRadius: 4 }} />
<span>OpenLess 正在启动</span>
</div>
</div>
);
}