diff --git a/auto-setup.ps1 b/auto-setup.ps1 index ff47b75..614c897 100644 --- a/auto-setup.ps1 +++ b/auto-setup.ps1 @@ -146,6 +146,77 @@ function Close-ProgressForm { function Test-Cmd($cmd) { [bool](Get-Command $cmd -ErrorAction SilentlyContinue) } +# ─── 非阻塞子进程执行 (解决 PS2EXE UI 冻结) ─────────── +# 所有耗时子进程都必须经过这两个函数, 保持 GUI 消息泵活跃 + +function Wait-ProcessWithUI { + <# 替代 System.Diagnostics.Process.WaitForExit(N) + 在等待期间每 200ms 泵一次 DoEvents, 防止 "(未响应)" #> + param( + [System.Diagnostics.Process]$proc, + [int]$timeoutMs = 60000, + [string]$label = "" + ) + $sw = [System.Diagnostics.Stopwatch]::StartNew() + while (-not $proc.HasExited -and $sw.ElapsedMilliseconds -lt $timeoutMs) { + [System.Windows.Forms.Application]::DoEvents() + # 每 5 秒更新一次副状态, 显示等待耗时 + if ($label -and ($sw.ElapsedMilliseconds % 5000) -lt 250) { + $elapsed = [int]($sw.ElapsedMilliseconds / 1000) + Update-Progress-SubStatus "$label ($($elapsed)s)" + } + Start-Sleep -Milliseconds 200 + } + if (-not $proc.HasExited) { + try { $proc.Kill() } catch {} + Bw-Log "WARN" "子进程超时 ($timeoutMs ms): $label" + return $false + } + return $true +} + +function Run-CmdWithUI { + <# 替代 & cmd args 2>&1 | ForEach-Object { Write-Host } + 将阻塞调用转为 Start-Process + Wait-ProcessWithUI #> + param( + [string]$exe, + [string[]]$arguments, + [string]$label = "", + [int]$timeoutMs = 180000, # 默认 3 分钟 + [switch]$captureOutput # 返回 stdout 内容 + ) + Bw-Log "CMD" "$exe $($arguments -join ' ')" + Update-Progress-SubStatus $label + + $outFile = Join-Path $env:TEMP "bw-cmd-out-$(Get-Random).tmp" + $errFile = Join-Path $env:TEMP "bw-cmd-err-$(Get-Random).tmp" + try { + $proc = Start-Process -FilePath $exe -ArgumentList $arguments ` + -NoNewWindow -PassThru ` + -RedirectStandardOutput $outFile ` + -RedirectStandardError $errFile + $ok = Wait-ProcessWithUI $proc $timeoutMs $label + $exitCode = if ($proc.HasExited) { $proc.ExitCode } else { -1 } + + # 日志记录 stdout/stderr (不超过 20 行) + if (Test-Path $outFile) { + $out = Get-Content $outFile -TotalCount 20 -ErrorAction SilentlyContinue + if ($out) { $out | ForEach-Object { Bw-Log "OUT" $_ } } + } + if (Test-Path $errFile) { + $err = Get-Content $errFile -TotalCount 10 -ErrorAction SilentlyContinue + if ($err) { $err | ForEach-Object { Bw-Log "ERR" $_ } } + } + + if ($captureOutput -and (Test-Path $outFile)) { + return @{ OK = ($ok -and $exitCode -eq 0); Output = (Get-Content $outFile -Raw -ErrorAction SilentlyContinue); ExitCode = $exitCode } + } + return @{ OK = ($ok -and $exitCode -eq 0); ExitCode = $exitCode } + } finally { + Remove-Item $outFile, $errFile -Force -ErrorAction SilentlyContinue + } +} + # ─── GUI 对话框 ───────────────────────────────────── function Show-MsgBox($text, $title = "Bookworm 安装", $buttons = "OK", $icon = "Information") { @@ -407,9 +478,7 @@ foreach ($dep in $deps) { if ($dep.WingetId -and $hasWinget) { try { - $output = winget install $dep.WingetId --accept-source-agreements --accept-package-agreements 2>&1 - $output | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - # 刷新 PATH + $r = Run-CmdWithUI "winget" @("install", $dep.WingetId, "--accept-source-agreements", "--accept-package-agreements") "安装 $($dep.Name)" 300000 $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") if (Test-Cmd $dep.Cmd) { Log-OK "$($dep.Name) 安装成功" @@ -423,8 +492,7 @@ foreach ($dep in $deps) { } elseif ($dep.NpmPkg -and (Test-Cmd "npm")) { try { - Write-Host " npm i -g $($dep.NpmPkg) ..." -ForegroundColor Gray - npm i -g $dep.NpmPkg 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } + $r = Run-CmdWithUI "npm" @("i", "-g", $dep.NpmPkg) "npm 安装 $($dep.Name)" 120000 $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") if (Test-Cmd $dep.Cmd) { Log-OK "$($dep.Name) 安装成功" @@ -442,7 +510,7 @@ foreach ($dep in $deps) { # Claude Code 依赖 npm, 需要在 Node.js 安装后再检查 if (-not (Test-Cmd "claude") -and (Test-Cmd "npm")) { Log-Info "安装 Claude Code..." - npm i -g @anthropic-ai/claude-code 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } + $r = Run-CmdWithUI "npm" @("i", "-g", "@anthropic-ai/claude-code") "安装 Claude Code (首次约 2 分钟)" 180000 $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") if (Test-Cmd "claude") { Log-OK "Claude Code 安装成功" } else { Log-Fail "Claude Code 安装失败" } } @@ -638,7 +706,6 @@ if (-not $proxyFound) { Log-OK "NO_PROXY: bww.letcareme.com, code.letcareme.com" # 连通性测试 -Write-Host "" Log-Info "测试网络连通性..." $netTests = @( @@ -677,33 +744,24 @@ Log-Phase 3 "同步 Bookworm 配置" # 配置 git credential helper git config --global credential.helper store 2>$null -# 克隆/更新 config 仓库 (.claude/) +# 克隆/更新 config 仓库 (.claude/) — 使用 Run-CmdWithUI 防止 UI 冻结 if (Test-Path (Join-Path $ClaudeDir ".git")) { Log-Info "配置仓库已存在, 更新中..." - Push-Location $ClaudeDir try { - $stash = git stash 2>&1 - git pull --rebase 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - if ($stash -notmatch 'No local changes') { git stash pop 2>&1 | Out-Null } - Log-OK "配置仓库已更新" + Run-CmdWithUI "git" @("-C", $ClaudeDir, "stash") "git stash" 15000 | Out-Null + $r = Run-CmdWithUI "git" @("-C", $ClaudeDir, "pull", "--rebase") "同步配置仓库" 120000 + if ($r.OK) { Log-OK "配置仓库已更新" } else { Log-Warn "git pull 失败, 使用本地版本" } + Run-CmdWithUI "git" @("-C", $ClaudeDir, "stash", "pop") "git stash pop" 15000 | Out-Null } catch { Log-Warn "git pull 失败, 使用本地版本" } - finally { Pop-Location } } elseif (Test-Path $ClaudeDir) { - # 已有 .claude 但非 git — 备份后克隆 Log-Info "备份现有 .claude/ 并克隆..." if (Test-Path $BackupDir) { Remove-Item $BackupDir -Recurse -Force } Rename-Item $ClaudeDir $BackupDir - # 可能需要 Gitea 凭证 $cred = Show-GiteaCredentialDialog - if ($cred) { - $credUrl = $GitUrl -replace '://', "://$($cred.User):$($cred.Pass)@" - git clone --depth 1 $credUrl $ClaudeDir 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - } else { - git clone --depth 1 $GitUrl $ClaudeDir 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - } - + $cloneUrl = if ($cred) { $GitUrl -replace '://', "://$($cred.User):$($cred.Pass)@" } else { $GitUrl } + $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $cloneUrl, $ClaudeDir) "克隆配置仓库" 180000 if (Test-Path (Join-Path $ClaudeDir "CLAUDE.md")) { Log-OK "配置仓库克隆成功 (旧目录已备份)" } else { @@ -714,16 +772,10 @@ elseif (Test-Path $ClaudeDir) { } } else { - # 全新安装 Log-Info "首次安装, 克隆配置仓库..." $cred = Show-GiteaCredentialDialog - if ($cred) { - $credUrl = $GitUrl -replace '://', "://$($cred.User):$($cred.Pass)@" - git clone --depth 1 $credUrl $ClaudeDir 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - } else { - git clone --depth 1 $GitUrl $ClaudeDir 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - } - + $cloneUrl = if ($cred) { $GitUrl -replace '://', "://$($cred.User):$($cred.Pass)@" } else { $GitUrl } + $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $cloneUrl, $ClaudeDir) "克隆配置仓库" 180000 if (Test-Path (Join-Path $ClaudeDir "CLAUDE.md")) { Log-OK "配置仓库克隆成功" } else { @@ -743,21 +795,15 @@ foreach ($d in $dirs) { # ─── 克隆/更新 bookworm-boot (含 crypto-helper.js + secrets-*.enc + install.ps1) ─── if (Test-Path (Join-Path $BootDir ".git")) { Log-Info "boot 仓库已存在, 更新中..." - Push-Location $BootDir try { - git pull --rebase 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - Log-OK "boot 仓库已更新" + $r = Run-CmdWithUI "git" @("-C", $BootDir, "pull", "--rebase") "同步 boot 仓库" 120000 + if ($r.OK) { Log-OK "boot 仓库已更新" } else { Log-Warn "boot 仓库更新失败, 使用本地版本" } } catch { Log-Warn "boot 仓库更新失败, 使用本地版本" } - finally { Pop-Location } } else { Log-Info "克隆 boot 仓库 (含解密工具与凭证)..." if (-not $cred) { $cred = Show-GiteaCredentialDialog } - if ($cred) { - $bootCredUrl = $BootUrl -replace '://', "://$($cred.User):$($cred.Pass)@" - git clone --depth 1 $bootCredUrl $BootDir 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - } else { - git clone --depth 1 $BootUrl $BootDir 2>&1 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } - } + $bootCloneUrl = if ($cred) { $BootUrl -replace '://', "://$($cred.User):$($cred.Pass)@" } else { $BootUrl } + $r = Run-CmdWithUI "git" @("clone", "--depth", "1", $bootCloneUrl, $BootDir) "克隆 boot 仓库" 180000 if (-not (Test-Path (Join-Path $BootDir "crypto-helper.js"))) { Log-Fail "boot 仓库克隆失败 (crypto-helper.js 缺失)" Show-MsgBox "boot 仓库克隆失败。`n请检查网络和 Gitea 凭证。" "克隆失败" "OK" "Error" @@ -945,16 +991,13 @@ foreach ($c in $checks) { } # ── 6b: API 凭证检查 ── -Write-Host "" Log-Info "API 凭证检查..." if ($env:ANTHROPIC_API_KEY) { Log-OK "ANTHROPIC_API_KEY 已配置" } else { Log-Fail "ANTHROPIC_API_KEY 未配置" } if ($env:ANTHROPIC_BASE_URL) { Log-OK "ANTHROPIC_BASE_URL 已配置" } else { Log-Warn "ANTHROPIC_BASE_URL 未配置 (将使用默认)" } -# ── 6c: MCP npx 包预缓存 ── -Write-Host "" +# ── 6c: MCP npx 包预缓存 (非阻塞 UI) ── Log-Info "MCP 预安装 (npx 包预缓存)..." -# Tier 1: 纯 npx, 无需 Key $npxPackages = @( @{ Name = "context7"; Pkg = "@upstash/context7-mcp@2.1.1" } @{ Name = "sequential-thinking"; Pkg = "@modelcontextprotocol/server-sequential-thinking@2025.12.18" } @@ -963,7 +1006,6 @@ $npxPackages = @( @{ Name = "notebooklm"; Pkg = "notebooklm-mcp@latest" } @{ Name = "cloudflare-docs"; Pkg = "mcp-remote" } @{ Name = "chrome-devtools"; Pkg = "chrome-devtools-mcp@0.18.1" } - # Tier 2: npx + Key @{ Name = "github"; Pkg = "@modelcontextprotocol/server-github" } @{ Name = "slack"; Pkg = "@modelcontextprotocol/server-slack" } @{ Name = "firecrawl"; Pkg = "firecrawl-mcp" } @@ -974,35 +1016,44 @@ $npxPackages = @( $mcpOK = 0; $mcpFail = 0 foreach ($mcp in $npxPackages) { $idx = $mcpOK + $mcpFail + 1 - Write-Host " [$idx/$($npxPackages.Count)] $($mcp.Name) " -NoNewline -ForegroundColor Gray + $label = "[$idx/$($npxPackages.Count)] $($mcp.Name)" + Update-Progress-SubStatus "$label ..." try { - # npm cache add 只下载到缓存, 不执行 (MCP 是 stdio 长驻进程, 不能用 --help) - $proc = Start-Process npm.cmd -ArgumentList "cache", "add", $mcp.Pkg -NoNewWindow -PassThru -RedirectStandardOutput "NUL" -RedirectStandardError "NUL" - $exited = $proc.WaitForExit(60000) # 60 秒超时 - if (-not $exited) { $proc.Kill(); throw "timeout" } - if ($proc.ExitCode -eq 0) { - Write-Host "[cached]" -ForegroundColor Green + $outTmp = Join-Path $env:TEMP "bw-npm-$($mcp.Name).tmp" + $errTmp = Join-Path $env:TEMP "bw-npm-$($mcp.Name)-err.tmp" + $proc = Start-Process npm.cmd -ArgumentList "cache", "add", $mcp.Pkg ` + -NoNewWindow -PassThru ` + -RedirectStandardOutput $outTmp ` + -RedirectStandardError $errTmp + $ok = Wait-ProcessWithUI $proc 60000 $label + if ($ok -and $proc.ExitCode -eq 0) { + Bw-Log "OK" "$label cached" $mcpOK++ - } else { throw "exit $($proc.ExitCode)" } + } else { throw "exit=$($proc.ExitCode)" } + Remove-Item $outTmp, $errTmp -Force -ErrorAction SilentlyContinue } catch { - Write-Host "[FAIL: $_]" -ForegroundColor Red + Bw-Log "WARN" "$label failed: $_" $mcpFail++ } } -Log-OK "npx 预缓存: $mcpOK 成功, $mcpFail 失败 (共 $($npxPackages.Count))" +Log-OK "npx 预缓存: $mcpOK/$($npxPackages.Count) 成功" -# ── 6d: Playwright 浏览器安装 ── -Write-Host "" +# ── 6d: Playwright 浏览器安装 (非阻塞 UI) ── Log-Info "Playwright 浏览器安装..." try { $pwBrowserPath = Join-Path $env:USERPROFILE "AppData\Local\ms-playwright" if (Test-Path (Join-Path $pwBrowserPath "chromium-*")) { Log-OK "Playwright Chromium 已存在" } else { - Write-Host " 下载 Chromium (首次约 150MB, 最长等 5 分钟)..." -ForegroundColor Gray - $pwProc = Start-Process npx.cmd -ArgumentList "-y", "playwright", "install", "chromium" -NoNewWindow -PassThru - $pwExited = $pwProc.WaitForExit(300000) # 5 分钟超时 - if (-not $pwExited) { $pwProc.Kill(); Log-Warn "Playwright 下载超时, 跳过" } + $outTmp = Join-Path $env:TEMP "bw-playwright.tmp" + $errTmp = Join-Path $env:TEMP "bw-playwright-err.tmp" + $pwProc = Start-Process npx.cmd -ArgumentList "-y", "playwright", "install", "chromium" ` + -NoNewWindow -PassThru ` + -RedirectStandardOutput $outTmp ` + -RedirectStandardError $errTmp + $pwOk = Wait-ProcessWithUI $pwProc 300000 "下载 Chromium (~150MB)" + Remove-Item $outTmp, $errTmp -Force -ErrorAction SilentlyContinue + if (-not $pwOk) { Log-Warn "Playwright 下载超时, 跳过" } elseif (Test-Path (Join-Path $pwBrowserPath "chromium-*")) { Log-OK "Playwright Chromium 安装成功" } else { @@ -1011,31 +1062,31 @@ try { } } catch { Log-Warn "Playwright 浏览器安装失败: $_ (不影响核心功能)" } -# ── 6e: Python MCP (uvx) 验证 ── -Write-Host "" +# ── 6e: Python MCP (uvx) 验证 (非阻塞 UI) ── if (Test-Cmd "uvx") { Log-Info "Python MCP 验证 (uvx)..." - $uvxPackages = @( @{ Name = "windows-mcp"; Args = @("--python", "3.13", "windows-mcp") } @{ Name = "atlassian"; Args = @("mcp-atlassian") } ) - foreach ($pkg in $uvxPackages) { - Write-Host " $($pkg.Name) " -NoNewline -ForegroundColor Gray try { - # uv tool install 只下载不启动 (MCP 是 stdio 长驻进程, --help 会挂起) + $outTmp = Join-Path $env:TEMP "bw-uvx-$($pkg.Name).tmp" + $errTmp = Join-Path $env:TEMP "bw-uvx-$($pkg.Name)-err.tmp" $installArgs = @("tool", "install") + $pkg.Args - $proc = Start-Process uv -ArgumentList $installArgs -NoNewWindow -PassThru -RedirectStandardOutput "NUL" -RedirectStandardError "NUL" - $exited = $proc.WaitForExit(90000) # 90 秒超时 (Python 包较大) - if (-not $exited) { $proc.Kill(); throw "timeout" } - Write-Host "[ready]" -ForegroundColor Green + $proc = Start-Process uv -ArgumentList $installArgs ` + -NoNewWindow -PassThru ` + -RedirectStandardOutput $outTmp ` + -RedirectStandardError $errTmp + $ok = Wait-ProcessWithUI $proc 90000 "uvx $($pkg.Name)" + Remove-Item $outTmp, $errTmp -Force -ErrorAction SilentlyContinue + if ($ok) { Bw-Log "OK" "uvx $($pkg.Name) ready" } } catch { - Write-Host "[skip: $_]" -ForegroundColor Yellow + Bw-Log "WARN" "uvx $($pkg.Name): $_" } } } else { - Log-Warn "uvx 不可用, 跳过 Python MCP (windows-mcp, atlassian)" + Bw-Log "INFO" "uvx 不可用, 跳过 Python MCP" } # ── 6f: 可选 API Key 提示 ── @@ -1049,9 +1100,7 @@ $optional = @( ) $missingOpt = $optional | Where-Object { -not [System.Environment]::GetEnvironmentVariable($_.Key, "Process") } if ($missingOpt.Count -gt 0) { - Write-Host "" - Write-Host " 可选 MCP (Key 未配置, 不影响核心功能):" -ForegroundColor DarkGray - foreach ($m in $missingOpt) { Write-Host " [-] $($m.Name) ($($m.Key))" -ForegroundColor DarkGray } + foreach ($m in $missingOpt) { Bw-Log "INFO" "可选 Key 未配置: $($m.Name) ($($m.Key))" } } # ========================================================================