# 换机一键还原 # iex (irm https://scripts.cong.in/restore.ps1) # # 注意:URL 必须写全 https://,PowerShell 5.1 不支持 308 重定向。 # # 本脚本不含任何密钥或凭据,只负责:解密 -> 解包 -> 放置 -> 修权限 -> 验证。 # 加密包需自行从网盘下载,密码单独输入。 $ErrorActionPreference = "Stop" function Step($m) { Write-Host "`n==> $m" -ForegroundColor Cyan } function Ok($m) { Write-Host " $m" -ForegroundColor Green } function Warn($m) { Write-Host " $m" -ForegroundColor Yellow } function Bad($m) { Write-Host " $m" -ForegroundColor Red } Write-Host @" 换机一键还原 ------------------------------------------------ 会做四件事:解密加密包 / 放置文件 / 修私钥权限 / 连通性验证 加密包请先从网盘下载到本机。 "@ -ForegroundColor White # ---------- 1. 前置工具 ---------- Step "1/7 检查前置工具" $gitCmd = Get-Command git -ErrorAction SilentlyContinue if (-not $gitCmd) { Warn "未检测到 Git,正在安装(提供 openssl / ssh)..." winget install --id Git.Git -e --accept-source-agreements --accept-package-agreements Bad "Git 装好了,但需要重开终端刷新 PATH。请关闭本窗口,重开后再跑一次本脚本。" return } $gitRoot = Split-Path (Split-Path $gitCmd.Source -Parent) -Parent $openssl = @("$gitRoot\mingw64\bin\openssl.exe", "$gitRoot\usr\bin\openssl.exe") | Where-Object { Test-Path $_ } | Select-Object -First 1 if (-not $openssl) { $openssl = (Get-Command openssl -ErrorAction SilentlyContinue).Source } if (-not $openssl) { Bad "找不到 openssl,无法解密。"; return } # 必须用 Windows 自带 bsdtar:MSYS tar 会把 C:\ 的冒号当成远程主机分隔符 $tarExe = @("$env:SystemRoot\System32\tar.exe", "C:\Windows\System32\tar.exe") | Where-Object { Test-Path $_ } | Select-Object -First 1 if (-not $tarExe) { Bad "找不到 Windows 自带的 tar.exe"; return } Ok "git $($gitCmd.Source)" Ok "openssl $openssl" Ok "tar $tarExe" # ---------- 2. 找加密包 ---------- Step "2/7 定位加密包" # 搜索顺序:当前目录 -> 下载 -> 桌面 $searchDirs = @((Get-Location).Path, "$env:USERPROFILE\Downloads", "$env:USERPROFILE\Desktop") | Select-Object -Unique | Where-Object { Test-Path $_ } $found = @( foreach ($d in $searchDirs) { Get-ChildItem $d -Filter "*.tar.gz.enc" -File -ErrorAction SilentlyContinue | ForEach-Object { $_ | Add-Member -NotePropertyName SrcDir -NotePropertyValue $d -PassThru } } ) | Sort-Object LastWriteTime -Descending | Group-Object FullName | ForEach-Object { $_.Group[0] } if ($found.Count -gt 0) { Ok "找到 $($found.Count) 个候选:" for ($i = 0; $i -lt $found.Count; $i++) { $tag = if ($found[$i].SrcDir -eq (Get-Location).Path) { "当前目录" } elseif ($found[$i].SrcDir -like "*Downloads") { "下载" } else { "桌面" } Write-Host (" [{0}] {1} ({2:N0} 字节, {3})" -f ($i+1), $found[$i].Name, $found[$i].Length, $tag) } $ans = Read-Host " 选哪个?回车=第 1 个,或输序号,或粘贴其它路径" $pkg = if ([string]::IsNullOrWhiteSpace($ans)) { $found[0].FullName } elseif ($ans -match '^\d+$' -and [int]$ans -ge 1 -and [int]$ans -le $found.Count) { $found[[int]$ans - 1].FullName } else { $ans.Trim('"') } } else { Warn "当前目录、下载、桌面均未找到 .tar.gz.enc" $pkg = (Read-Host " 请粘贴完整路径").Trim('"') } if (-not (Test-Path $pkg)) { Bad "文件不存在:$pkg"; return } Ok "使用:$pkg" # ---------- 3. 解密解包 ---------- Step "3/7 解密并解包" $work = Join-Path $env:TEMP ("restore-" + (Get-Date -Format "HHmmss")) New-Item -ItemType Directory -Force $work | Out-Null $tgz = Join-Path $work "pack.tar.gz" $sec = Read-Host " 请输入解密密码" -AsSecureString $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec) try { $env:RESTORE_PASS = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } & $openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -in $pkg -out $tgz -pass env:RESTORE_PASS 2>$null Remove-Item Env:\RESTORE_PASS -ErrorAction SilentlyContinue if ($LASTEXITCODE -ne 0 -or -not (Test-Path $tgz)) { Bad "解密失败。密码错误,或下载不完整(请核对文件大小后重新下载)。" Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue return } Ok "解密成功" & $tarExe xzf $tgz -C $work Remove-Item $tgz -Force # 明文归档立刻删 Ok "解包完成:$((Get-ChildItem $work).Name -join ', ')" # ---------- 4. 选项目目录 ---------- Step "4/7 确定项目目录" Write-Host " 以下两处位置固定,不可更改(Claude Code 只认这里):" -ForegroundColor Gray Write-Host " SSH 密钥 $env:USERPROFILE\.ssh\" -ForegroundColor DarkGray Write-Host " 记忆库 $env:USERPROFILE\.claude\projects\<自动命名>\memory\" -ForegroundColor DarkGray Write-Host " 只有「项目文件」的位置可以自选,记忆目录名会按它自动推导。" -ForegroundColor Gray Write-Host "" $defaultProj = (Get-Location).Path $ans = Read-Host " 项目目录(回车=当前目录 $defaultProj)" $projDir = if ([string]::IsNullOrWhiteSpace($ans)) { $defaultProj } else { $ans.Trim('"') } $projDir = $projDir.TrimEnd('\') # 路径 -> 记忆目录名:冒号和反斜杠都替换为连字符 # F:\project -> F--project # C:\Users\Bob\Desktop\test -> C--Users-Bob-Desktop-test $memName = $projDir -replace '[:\\]', '-' $memDir = "$env:USERPROFILE\.claude\projects\$memName\memory" Ok "项目目录 : $projDir" Ok "记忆目录 : $memDir" # ---------- 5. 放置 ---------- Step "5/7 放置文件" if (Test-Path "$work\ssh") { New-Item -ItemType Directory -Force "$env:USERPROFILE\.ssh" | Out-Null Copy-Item "$work\ssh\*" "$env:USERPROFILE\.ssh\" -Force Ok "SSH -> $env:USERPROFILE\.ssh\ ($((Get-ChildItem "$work\ssh").Count) 个文件)" } if (Test-Path "$work\project") { New-Item -ItemType Directory -Force $projDir | Out-Null Copy-Item "$work\project\*" $projDir -Force Ok "项目文件 -> $projDir" } if (Test-Path "$work\memory") { New-Item -ItemType Directory -Force $memDir | Out-Null Copy-Item "$work\memory\*" $memDir -Force Ok "记忆库 -> $memDir ($((Get-ChildItem "$work\memory").Count) 个文件)" } if (Test-Path "$work\claude") { $cDir = "$env:USERPROFILE\.claude" New-Item -ItemType Directory -Force $cDir | Out-Null $existing = Join-Path $cDir "settings.json" if (Test-Path $existing) { Copy-Item $existing "$existing.bak-$(Get-Date -Format 'yyyyMMdd-HHmmss')" -Force Warn "已存在 settings.json,旧文件已备份为 .bak-<时间戳>" } Copy-Item "$work\claude\*" $cDir -Force Ok "CC 设置 -> $cDir\settings.json (权限模式 / 模型 / effort)" } # ---------- 6. 私钥权限 + nmap ---------- Step "6/7 修私钥权限 / 检查代理依赖" $keys = Get-ChildItem "$env:USERPROFILE\.ssh" -File | Where-Object { $_.Extension -ne ".pub" -and $_.Name -notin @("config","known_hosts") } foreach ($k in $keys) { icacls $k.FullName /inheritance:r /grant:r "$($env:USERNAME):R" | Out-Null Ok "权限已收紧: $($k.Name)" } if ($keys.Count -eq 0) { Warn "未发现私钥文件" } # config 里若用了 ProxyCommand,检查所需程序是否存在 $cfg = "$env:USERPROFILE\.ssh\config" if (Test-Path $cfg) { $proxyLines = Select-String -Path $cfg -Pattern 'ProxyCommand' -ErrorAction SilentlyContinue if ($proxyLines) { foreach ($m in [regex]::Matches((Get-Content $cfg -Raw), 'ProxyCommand\s+"([^"]+)"')) { $exe = $m.Groups[1].Value if (Test-Path $exe) { Ok "ProxyCommand 程序就位: $exe" } else { Bad "ProxyCommand 找不到: $exe" Warn "请执行: winget install Insecure.Nmap" Warn "若装到别的路径,需手工修改 $cfg 中的这一行" } } Warn "走代理的主机还需要代理客户端保持运行" } } # ---------- 7. 验证 ---------- Step "7/7 连通性验证" if (Test-Path $cfg) { $hosts = (Get-Content $cfg | Select-String '^\s*Host\s+(\S+)').Matches | ForEach-Object { $_.Groups[1].Value } | Where-Object { $_ -ne '*' } foreach ($h in $hosts) { Write-Host " 测试 $h ... " -NoNewline $r = ssh -o ConnectTimeout=15 -o BatchMode=yes $h "hostname" 2>&1 if ($LASTEXITCODE -eq 0) { Write-Host "OK ($r)" -ForegroundColor Green } else { Write-Host "失败" -ForegroundColor Red } } Write-Host "`n 直连主机通、代理主机不通 = nmap 或代理的问题" -ForegroundColor Gray Write-Host " 全部不通 = 私钥权限或文件本身的问题" -ForegroundColor Gray } else { Warn "未找到 ssh config,跳过验证" } # ---------- 收尾 ---------- Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue Write-Host "`n完成。临时解压目录已清理。" -ForegroundColor Green Write-Host "下载目录里的加密包如不再需要,建议一并删除。`n" -ForegroundColor Gray