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:
1 parent
96ff66eb80
commit
ff694c07c1
7 files changed
+284
-14
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -4,6 +4,7 @@ dist/
|
||||
*.local
|
||||
.env
|
||||
.vite/
|
||||
.artifacts/
|
||||
|
||||
# Tauri
|
||||
src-tauri/target/
|
||||
|
||||
@@ -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."
|
||||
@@ -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(())
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user