diff --git a/auto-setup.ps1 b/auto-setup.ps1 index d2df9f2..7a702a9 100644 --- a/auto-setup.ps1 +++ b/auto-setup.ps1 @@ -20,7 +20,7 @@ param( $ErrorActionPreference = "Stop" # ─── 版本号 (每次更新递增, build.ps1 自动读取) ────── -$BWVersion = "3.0.4" # fix: Key 验证模型兜底 + 默认模型反推 + Win11 指纹 wmic→CIM 三级 fallback +$BWVersion = "3.0.5" # feat: winget 三层 fallback + 截图粘贴助手 + 启动器自检 + 僵尸快捷方式清理 # DryRun 模式日志标记 if ($DryRun) { $global:BWDryRun = $DryRun } else { $global:BWDryRun = $null } @@ -791,17 +791,48 @@ if (Is-DryRun "env") { } $deps = @( - # 核心依赖 (缺失则尝试自动安装) - @{ Name = "Node.js"; Cmd = "node"; WingetId = "OpenJS.NodeJS.LTS"; NpmPkg = $null; PipPkg = $null; Core = $true } - @{ Name = "Git"; Cmd = "git"; WingetId = "Git.Git"; NpmPkg = $null; PipPkg = $null; Core = $true } - @{ Name = "PowerShell 7"; Cmd = "pwsh"; WingetId = "Microsoft.PowerShell"; NpmPkg = $null; PipPkg = $null; Core = $true; MsiUrl = "https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/PowerShell-7.4.6-win-x64.msi" } - @{ Name = "Claude Code"; Cmd = "claude"; WingetId = $null; NpmPkg = "@anthropic-ai/claude-code"; PipPkg = $null; Core = $true } + # 核心依赖 (三层 fallback: winget → MSI/EXE 直链 → 手动指引) + # v3.0.5: 所有核心依赖都补齐直链兜底,防 winget 不可用时 Phase 1 直接挂 + @{ Name = "Node.js"; Cmd = "node"; WingetId = "OpenJS.NodeJS.LTS"; NpmPkg = $null; PipPkg = $null; Core = $true + MsiUrl = "https://nodejs.org/dist/v22.11.0/node-v22.11.0-x64.msi" + ManualUrl = "https://nodejs.org/zh-cn/download" } + @{ Name = "Git"; Cmd = "git"; WingetId = "Git.Git"; NpmPkg = $null; PipPkg = $null; Core = $true + # Git for Windows 是 Inno Setup (非 MSI), 用 /VERYSILENT /NORESTART 静默 + ExeUrl = "https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.2/Git-2.47.1.2-64-bit.exe" + ExeArgs = @("/VERYSILENT", "/NORESTART", "/NOCANCEL", "/SP-", "/SUPPRESSMSGBOXES") + ManualUrl = "https://git-scm.com/download/win" } + @{ Name = "PowerShell 7"; Cmd = "pwsh"; WingetId = "Microsoft.PowerShell"; NpmPkg = $null; PipPkg = $null; Core = $true + MsiUrl = "https://github.com/PowerShell/PowerShell/releases/download/v7.4.6/PowerShell-7.4.6-win-x64.msi" + ManualUrl = "https://github.com/PowerShell/PowerShell/releases/latest" } + @{ Name = "Claude Code"; Cmd = "claude"; WingetId = $null; NpmPkg = "@anthropic-ai/claude-code"; PipPkg = $null; Core = $true + ManualUrl = "https://docs.anthropic.com/en/docs/claude-code/overview" } # Python 移到可选依赖 (不在此列表, 由 line 753 单独处理) ) $hasWinget = Test-Cmd "winget" $installed = @() +# v3.0.5: winget 不可用时醒目提示 (但不阻断, MSI/EXE 直链兜底可以继续) +if (-not $hasWinget) { + $wingetHint = @" +检测到 winget (Windows 包管理器) 未就绪。 + +Bookworm 将改用 MSI/EXE 直链自动下载 Node.js 和 Git,无需 winget 也能完成安装,请耐心等待。 + +【若想获取 winget】推荐两条路径: +1. 应用商店 — 搜索"应用安装程序" (App Installer) +2. GitHub — https://aka.ms/getwinget + +【系统要求】Windows 10 1809+ / Windows 11 任意版本 + +【仍无法安装 winget?】很可能是企业镜像锁定或 Win10 1803 及更早版本 — + 无需纠结,本安装器下一步的 MSI/EXE 直链兜底会自动处理。 + 若直链也失败,会弹窗给出手动安装清单。 +"@ + Show-MsgBox $wingetHint "winget 未就绪 (不阻断)" "OK" "Information" + Log-Warn "winget 不可用, 将走 MSI/EXE 直链兜底" +} + foreach ($dep in $deps) { if (Test-Cmd $dep.Cmd) { $ver = try { & $dep.Cmd --version 2>$null | Select-Object -First 1 } catch { "installed" } @@ -823,30 +854,49 @@ foreach ($dep in $deps) { Log-Fail "$($dep.Name) 安装失败: $_" } } - # v3.0.3: winget 缺席/失败时, 用 MSI 直链兜底 (主要针对 Win10 21H1 及更早版本无 winget / AppInstaller 过老) - if (-not (Test-Cmd $dep.Cmd) -and $dep.MsiUrl) { - Log-Warn "$($dep.Name) winget 路径失败, 改走 MSI 直链..." - $msiPath = Join-Path $env:TEMP "bw-$($dep.Cmd)-installer.msi" + # v3.0.3/v3.0.5: winget 缺席/失败时, 用 MSI/EXE 直链兜底 + # v3.0.5 扩展: Git 是 Inno Setup (非 MSI), 分 MsiUrl 和 ExeUrl 两条分派 + if (-not (Test-Cmd $dep.Cmd) -and ($dep.MsiUrl -or $dep.ExeUrl)) { + $isExe = [bool]$dep.ExeUrl + $installerUrl = if ($isExe) { $dep.ExeUrl } else { $dep.MsiUrl } + $ext = if ($isExe) { "exe" } else { "msi" } + $installerPath = Join-Path $env:TEMP "bw-$($dep.Cmd)-installer.$ext" + Log-Warn "$($dep.Name) winget 路径失败, 改走 $($ext.ToUpper()) 直链..." try { # TLS 1.2 强制 (Win10 默认 TLS 1.0/1.1 访问 GitHub 会断) [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 - Log-Info " 下载 $($dep.MsiUrl.Substring($dep.MsiUrl.LastIndexOf('/')+1))" - Invoke-WebRequest -Uri $dep.MsiUrl -OutFile $msiPath -UseBasicParsing -TimeoutSec 300 -EA Stop - if ((Get-Item $msiPath).Length -lt 1MB) { throw "MSI 下载不完整: $((Get-Item $msiPath).Length) bytes" } - Log-Info " 安装 MSI (静默, 需 1-2 分钟)..." - $p = Start-Process "msiexec.exe" -ArgumentList "/i", "`"$msiPath`"", "/quiet", "/qn", "/norestart", "ADD_PATH=1" -Wait -PassThru -NoNewWindow - Remove-Item $msiPath -Force -EA SilentlyContinue + Log-Info " 下载 $($installerUrl.Substring($installerUrl.LastIndexOf('/')+1))" + Invoke-WebRequest -Uri $installerUrl -OutFile $installerPath -UseBasicParsing -TimeoutSec 300 -EA Stop + if ((Get-Item $installerPath).Length -lt 1MB) { throw "$($ext.ToUpper()) 下载不完整: $((Get-Item $installerPath).Length) bytes" } + Log-Info " 安装 $($ext.ToUpper()) (静默, 需 1-3 分钟)..." + if ($isExe) { + # Inno Setup (Git for Windows): /VERYSILENT /NORESTART /SP- /SUPPRESSMSGBOXES + $exeArgs = if ($dep.ExeArgs) { $dep.ExeArgs } else { @("/VERYSILENT", "/NORESTART", "/NOCANCEL", "/SP-", "/SUPPRESSMSGBOXES") } + $p = Start-Process $installerPath -ArgumentList $exeArgs -Wait -PassThru -NoNewWindow + } else { + # MSI (Node.js / PS7): msiexec /i ... /quiet /qn /norestart ADD_PATH=1 + $p = Start-Process "msiexec.exe" -ArgumentList "/i", "`"$installerPath`"", "/quiet", "/qn", "/norestart", "ADD_PATH=1" -Wait -PassThru -NoNewWindow + } + Remove-Item $installerPath -Force -EA SilentlyContinue $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") - # PS7 MSI 默认装到 %ProgramFiles%\PowerShell\7\pwsh.exe, 强制补 PATH - $ps7Dir = "$env:ProgramFiles\PowerShell\7" - if ((Test-Path "$ps7Dir\pwsh.exe") -and ($env:Path -notlike "*$ps7Dir*")) { $env:Path = "$ps7Dir;$env:Path" } + # 常见安装位置强制补 PATH (避免 User env 延迟刷新) + $commonDirs = @( + "$env:ProgramFiles\PowerShell\7", + "$env:ProgramFiles\Git\cmd", + "$env:ProgramFiles\Git\bin", + "$env:ProgramFiles\nodejs", + "$env:APPDATA\npm" + ) + foreach ($d in $commonDirs) { + if ((Test-Path $d) -and ($env:Path -notlike "*$d*")) { $env:Path = "$d;$env:Path" } + } if (Test-Cmd $dep.Cmd) { - Log-OK "$($dep.Name) MSI 安装成功 (exit $($p.ExitCode))" + Log-OK "$($dep.Name) $($ext.ToUpper()) 安装成功 (exit $($p.ExitCode))" $installed += $dep.Name } else { - Log-Fail "$($dep.Name) MSI 安装后仍找不到 pwsh (exit $($p.ExitCode))" + Log-Fail "$($dep.Name) $($ext.ToUpper()) 安装后仍找不到 $($dep.Cmd) (exit $($p.ExitCode))" } - } catch { Log-Fail "$($dep.Name) MSI 直链失败: $_" } + } catch { Log-Fail "$($dep.Name) $($ext.ToUpper()) 直链失败: $_" } } elseif ($dep.NpmPkg -and (Test-Cmd "npm")) { try { @@ -978,7 +1028,45 @@ if (-not (Test-Cmd "node") -or -not (Test-Cmd "git") -or -not (Test-Cmd "claude" if (-not (Test-Cmd "node")) { $missing += "Node.js" } if (-not (Test-Cmd "git")) { $missing += "Git" } if (-not (Test-Cmd "claude")) { $missing += "Claude Code" } - Show-MsgBox "以下核心依赖安装失败: $($missing -join ', ')`n`n请手动安装后重新运行。`n`nNode.js: https://nodejs.org`nGit: https://git-scm.com" "安装中断" "OK" "Error" + + # v3.0.5: 清除桌面僵尸快捷方式, 防用户点击空快捷方式触发 'claude.exe not found' + try { + $desktop = [System.Environment]::GetFolderPath("Desktop") + foreach ($lnk in @("启动Bookworm.lnk", "更新Bookworm.lnk", "Bookworm.lnk")) { + $lnkPath = Join-Path $desktop $lnk + if (Test-Path $lnkPath) { + Remove-Item -LiteralPath $lnkPath -Force -EA SilentlyContinue + Log-Warn "已移除僵尸快捷方式: $lnk (Phase 1 未完成)" + } + } + } catch {} + + # v3.0.5: 手动安装清单 (winget 失败 + 直链失败时最后一根稻草) + $manualGuide = "以下核心依赖自动安装失败:`n " + ($missing -join ", ") + "`n`n" + $manualGuide += "【手动安装清单】请按顺序操作:`n`n" + if ($missing -contains "Node.js") { + $manualGuide += "1. Node.js`n" + $manualGuide += " 下载: https://nodejs.org/zh-cn/download`n" + $manualGuide += " 选 LTS → Windows Installer (.msi) → 双击安装`n`n" + } + if ($missing -contains "Git") { + $manualGuide += "2. Git for Windows`n" + $manualGuide += " 下载: https://git-scm.com/download/win`n" + $manualGuide += " 选 64-bit Git → 双击安装 (默认选项即可)`n`n" + } + if ($missing -contains "Claude Code") { + $manualGuide += "3. Claude Code (需先装好 Node.js)`n" + $manualGuide += " 命令行执行: npm i -g @anthropic-ai/claude-code`n`n" + } + $manualGuide += "【完成后】`n" + $manualGuide += " 重新双击 Bookworm-Setup.exe 即可继续安装`n" + $manualGuide += " (已装成功的依赖会被自动跳过)`n`n" + $manualGuide += "【仍有问题?】`n" + $manualGuide += " 1. 检查是否以管理员身份运行`n" + $manualGuide += " 2. 检查网络能否访问上述下载地址`n" + $manualGuide += " 3. 联系 Bookworm 管理员`n" + + Show-MsgBox $manualGuide "安装中断 — 手动安装指引" "OK" "Error" exit 1 } if (-not (Test-Cmd "pwsh")) { @@ -1675,6 +1763,133 @@ if (Test-Path $templateFile) { $env:CLAUDE_HOME = $ClaudeDir +# ======================================================================== +# Phase 5.5: 截图粘贴助手部署 (v3.0.5 新增) +# ======================================================================== +# 目标: Shift+Win+S 截图后, 在 Claude Code CLI Ctrl+V 直接粘贴为 [Image #N] 附件 +# 分发文件: $ClaudeDir\scripts\ClipImageWatcher.ps1 (由 bookworm-portable-config 仓库提供) +# 注入点: +# 1. $HOME\Documents\PowerShell\profile.ps1 (sentinel 追加式, Q1=A) +# 2. HKCU:\...Microsoft.ScreenSketch\...\Enabled=0 (备份原值到 HKCU:\Software\Bookworm\ToastBackup, 卸载时还原, Q2=B) +# ======================================================================== +Log-Info "[5.5/$TOTAL_PHASES] 截图粘贴助手部署" +Update-Progress-SubStatus "配置截图粘贴助手..." + +$clipWatcherSrc = Join-Path $ClaudeDir "scripts\ClipImageWatcher.ps1" +if (Test-Path $clipWatcherSrc) { + # ── 5.5a: profile sentinel 注入 (幂等, 精准替换 BW_CLIP_START..END 中间块) ── + $psProfileDir = Join-Path $env:USERPROFILE "Documents\PowerShell" + $psProfilePath = Join-Path $psProfileDir "profile.ps1" + if (-not (Test-Path $psProfileDir)) { New-Item -ItemType Directory -Path $psProfileDir -Force | Out-Null } + + $sentinelStart = "# BW_CLIP_START v3.0.5 — 不要手动修改此块 (Bookworm 自动生成)" + $sentinelEnd = "# BW_CLIP_END" + $bwClipBlock = @" +$sentinelStart +`$script:ClipWatcherPath = "`$HOME\.claude\scripts\ClipImageWatcher.ps1" + +function Start-ClipWatch { + [CmdletBinding()] + param([int]`$IntervalMs = 400, [switch]`$Foreground) + if (-not (Test-Path `$script:ClipWatcherPath)) { + Write-Error "找不到监听脚本: `$script:ClipWatcherPath" + return + } + if (`$Foreground) { + & pwsh -NoProfile -ExecutionPolicy Bypass -File `$script:ClipWatcherPath -IntervalMs `$IntervalMs + return + } + `$logDir = Join-Path `$env:TEMP 'claude-clip' + if (-not (Test-Path `$logDir)) { New-Item -ItemType Directory -Path `$logDir -Force | Out-Null } + `$logPath = Join-Path `$logDir 'watcher.log' + `$psi = New-Object System.Diagnostics.ProcessStartInfo + `$psi.FileName = 'pwsh' + `$psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -File ```"`$(`$script:ClipWatcherPath)```" -IntervalMs `$IntervalMs" + `$psi.UseShellExecute = `$false + `$psi.CreateNoWindow = `$true + `$psi.WindowStyle = 'Hidden' + `$psi.RedirectStandardOutput = `$true + `$psi.RedirectStandardError = `$true + `$p = [System.Diagnostics.Process]::Start(`$psi) + `$global:ClipWatchPid = `$p.Id + Register-ObjectEvent -InputObject `$p -EventName OutputDataReceived -Action { + if (`$EventArgs.Data) { Add-Content -Path `$using:logPath -Value `$EventArgs.Data -Encoding UTF8 } + } | Out-Null + Register-ObjectEvent -InputObject `$p -EventName ErrorDataReceived -Action { + if (`$EventArgs.Data) { Add-Content -Path `$using:logPath -Value "[ERR] `$(`$EventArgs.Data)" -Encoding UTF8 } + } | Out-Null + `$p.BeginOutputReadLine() + `$p.BeginErrorReadLine() + Write-Host "[ClipWatch] 已静默启动 (PID=`$(`$p.Id))。日志: `$logPath" -ForegroundColor Cyan +} + +function Stop-ClipWatch { + if (`$global:ClipWatchPid) { + try { Stop-Process -Id `$global:ClipWatchPid -Force -ErrorAction Stop; Write-Host "[ClipWatch] 已停止 PID=`$global:ClipWatchPid" -ForegroundColor Yellow } + catch { Write-Warning "停止失败: `$(`$_.Exception.Message)" } + `$global:ClipWatchPid = `$null + } else { Write-Host "[ClipWatch] 未在运行。" -ForegroundColor DarkGray } +} + +function Get-ClipWatchDir { + `$d = Join-Path `$env:TEMP 'claude-clip' + if (Test-Path `$d) { Get-ChildItem `$d | Sort-Object LastWriteTime -Descending | Select-Object -First 10 } + else { Write-Host "尚未产生截图。" } +} + +# 自动启动监听器 (全局只跑一份, 用 CommandLine 查重) +try { + `$watcherName = Split-Path `$script:ClipWatcherPath -Leaf + `$running = Get-CimInstance Win32_Process -Filter "Name='pwsh.exe'" -ErrorAction SilentlyContinue | + Where-Object { `$_.CommandLine -like "*`$watcherName*" } + if (-not `$running -and (Test-Path `$script:ClipWatcherPath)) { Start-ClipWatch } +} catch {} +$sentinelEnd +"@ + + # 读现有 profile, 精准替换 sentinel 中间块 (不动其他内容) + $existing = if (Test-Path $psProfilePath) { Get-Content $psProfilePath -Raw -Encoding UTF8 } else { "" } + $pattern = [regex]::Escape($sentinelStart) + "[\s\S]*?" + [regex]::Escape($sentinelEnd) + if ($existing -match $pattern) { + $updated = [regex]::Replace($existing, $pattern, [regex]::Escape($bwClipBlock) -replace '\\(.)', '$1', 1) + # 上面 Regex.Replace 的 replacement 参数需要 escape 一次, 改用简单字符串替换 + $updated = $existing -replace $pattern, [System.Text.RegularExpressions.Regex]::Unescape([regex]::Escape($bwClipBlock)) + } else { + $updated = if ($existing) { $existing.TrimEnd() + "`n`n" + $bwClipBlock + "`n" } else { $bwClipBlock + "`n" } + } + # 无 BOM UTF-8 写入 (PS7 能正确识别, PS5.1 也 OK) + [System.IO.File]::WriteAllText($psProfilePath, $updated, [System.Text.UTF8Encoding]::new($false)) + Log-OK "PowerShell profile 已注入 BW_CLIP 块: $psProfilePath" + + # ── 5.5b: 截图 Toast 关闭 + 备份原值用于卸载还原 ── + $toastReg = "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings\Microsoft.ScreenSketch_8wekyb3d8bbwe!App" + $bwBackup = "HKCU:\Software\Bookworm\ToastBackup" + try { + $origVal = $null + if (Test-Path $toastReg) { + $origVal = (Get-ItemProperty -Path $toastReg -Name "Enabled" -EA SilentlyContinue).Enabled + } + # 备份原值到 HKCU:\Software\Bookworm\ToastBackup (卸载时用于还原) + if (-not (Test-Path $bwBackup)) { New-Item -Path $bwBackup -Force | Out-Null } + $bwBackupProps = Get-ItemProperty -Path $bwBackup -EA SilentlyContinue + if (-not $bwBackupProps -or -not $bwBackupProps.ScreenSketchToast_Backed) { + # 只在首次备份 (防重装覆盖用户手动改过的值) + $bakValue = if ($null -eq $origVal) { "__ABSENT__" } else { $origVal.ToString() } + New-ItemProperty -Path $bwBackup -Name "ScreenSketchToast_Original" -Value $bakValue -PropertyType String -Force | Out-Null + New-ItemProperty -Path $bwBackup -Name "ScreenSketchToast_Backed" -Value 1 -PropertyType DWord -Force | Out-Null + Log-Info "已备份 Toast 原值到 $bwBackup (卸载时自动还原)" + } + # 关 Toast + if (-not (Test-Path $toastReg)) { New-Item -Path $toastReg -Force | Out-Null } + New-ItemProperty -Path $toastReg -Name "Enabled" -Value 0 -PropertyType DWord -Force | Out-Null + Log-OK "截图工具 Toast 已关闭 (原值已备份)" + } catch { + Log-Warn "截图 Toast 关闭失败 (不影响截图本身): $_" + } +} else { + Log-Warn "ClipImageWatcher.ps1 未分发到 $clipWatcherSrc, 跳过截图助手部署" +} + # ======================================================================== # Phase 6: MCP 验证 + 自动安装 # ======================================================================== diff --git a/install.ps1 b/install.ps1 index 8e6c027..f67850c 100644 --- a/install.ps1 +++ b/install.ps1 @@ -429,14 +429,46 @@ foreach ($c in $checks) { if (-not (Test-Command "claude") -or -not (Test-Command "node") -or -not (Test-Command "git")) { Install-MissingDeps } -# 再次验证 +# v3.0.5: 再次验证 — StartOnly 场景加 GUI 弹窗 (防止 console 闪退用户看不见) +# 触发条件: 用户双击老快捷方式, 但 Phase 1 之前失败导致 claude/node 未装 +function Show-MissingDepGui { + param([string]$depName, [string]$installCmd) + try { + Add-Type -AssemblyName System.Windows.Forms -EA Stop + $msg = @" +检测到 $depName 未安装,无法启动 Bookworm。 + +最可能原因: + 上次安装器未完成 (Phase 1 环境检测被中断) + +【推荐修复】 + 1. 双击桌面或下载目录的 Bookworm-Setup.exe + 2. 安装器会自动补装缺失的依赖 + 3. 已装好的部分会被跳过, 不会重复 + +【手动修复】 + $installCmd + +完成后再次点击启动快捷方式即可。 +"@ + [System.Windows.Forms.MessageBox]::Show($msg, "Bookworm 启动失败 — $depName 未安装", 'OK', 'Error') | Out-Null + } catch { } +} + if (-not (Test-Command "claude")) { Write-Host "`n [ABORT] Claude Code 未安装" -ForegroundColor Red Write-Host " 安装: npm i -g @anthropic-ai/claude-code" -ForegroundColor Gray + if ($StartOnly) { Show-MissingDepGui "Claude Code" "npm i -g @anthropic-ai/claude-code" } exit 1 } if (-not (Test-Command "node")) { Write-Host "`n [ABORT] Node.js 未安装" -ForegroundColor Red + if ($StartOnly) { Show-MissingDepGui "Node.js" "https://nodejs.org/zh-cn/download 下载 LTS .msi" } + exit 1 +} +if (-not (Test-Command "git")) { + Write-Host "`n [ABORT] Git 未安装" -ForegroundColor Red + if ($StartOnly) { Show-MissingDepGui "Git" "https://git-scm.com/download/win 下载 64-bit" } exit 1 }