param( [string]$Server = '10.5.5.13', [int]$Passes = 1, [switch]$PriorityCache, [switch]$DpOnly, # 服务器并行试算:每个元素是一组额外参数(如 '--repair --bot-pen-w 1'), # 各自在独立副本里跑,取未完成最少的那个走下面同一套校验 / 接受逻辑。 [string[]]$Variants = @(), [int]$PollSec = 60, # 服务器上跑布线的解释器;PyPy 见 PROGRESS(纯 Python 的 A*,JIT 通常快数倍) [string]$Interp = 'python3' ) $ErrorActionPreference = 'Stop' $Here = Split-Path -Parent $MyInvocation.MyCommand.Path $Repo = Resolve-Path (Join-Path $Here '..\..') $Python = 'C:\Python314\python.exe' $Ssh = 'C:\Windows\System32\OpenSSH\ssh.exe' $Scp = 'C:\Windows\System32\OpenSSH\scp.exe' # 临时文件带 PID:允许同时起两批(快的单轮变体 + 慢的拆线重布),互不覆盖 $Archive = Join-Path ([System.IO.Path]::GetTempPath()) "dcdc-remote-route-$PID.tgz" $RunScript = Join-Path ([System.IO.Path]::GetTempPath()) "dcdc-remote-run-$PID.sh" $RemoteArchive = Split-Path -Leaf $Archive $Incoming = Join-Path $Here "_base_routes.server.$PID.json" $Cache = Join-Path $Here '_base_routes.json' $Backup = Join-Path $Here '_base_routes.pre-server.json' $Rejected = Join-Path $Here '_base_routes.rejected.json' $remoteRoot = $null if ($Passes -lt 1) { throw 'Passes must be at least 1.' } if ($DpOnly -and $Variants.Count -gt 0) { throw '-DpOnly cannot be combined with -Variants.' } Push-Location $Repo try { & $Python '.claude\r2\route_base.py' --snapshot-vias if ($LASTEXITCODE -ne 0) { throw 'Failed to refresh board-via snapshot.' } $remoteRoot = (& $Ssh $Server 'mktemp -d /tmp/dcdc-route.XXXXXX').Trim() if ($LASTEXITCODE -ne 0 -or $remoteRoot -notmatch '^/tmp/dcdc-route\.[A-Za-z0-9]+$') { throw "Unexpected remote work directory: $remoteRoot" } & tar.exe --exclude=*.png --exclude=*.bak --exclude=__pycache__ ` --exclude=_remote_route.tgz -czf $Archive ` '.claude\r2' '.claude\eda.py' '.claude\pcb_pour.py' '.claude\eda-parts.json' if ($LASTEXITCODE -ne 0) { throw 'Failed to build routing bundle.' } & $Scp $Archive "${Server}:${remoteRoot}/" if ($LASTEXITCODE -ne 0) { throw 'Failed to upload routing bundle.' } & $Ssh $Server "tar -xzf ${remoteRoot}/${RemoteArchive} -C ${remoteRoot}" if ($LASTEXITCODE -ne 0) { throw 'Failed to extract routing bundle.' } $priority = if ($PriorityCache) { ' --priority-cache' } else { '' } $scope = if ($DpOnly) { ' --dp-only' } else { '' } $timeFmt = "/usr/bin/time -f 'REMOTE_TIME=%e sec CPU=%P RSS=%M KB'" $remoteResult = "${remoteRoot}/.claude/r2/_base_routes.json" if ($Variants.Count -eq 0) { $command = "DCDC_ROUTE_SERVER=1 $timeFmt $Interp " + "${remoteRoot}/.claude/r2/route_base.py --offline --replan --passes $Passes$priority$scope" & $Ssh $Server $command if ($LASTEXITCODE -ne 0) { throw 'Remote routing failed.' } } else { # 变体各自一份副本(缓存文件同名,不能共用目录),脱离 ssh 会话在后台跑: # 拆线重布可能要跑很久,ssh 断线不能把计算带走。 $lines = @('#!/bin/bash', "cd ${remoteRoot}") for ($k = 0; $k -lt $Variants.Count; $k++) { $v = $Variants[$k] $lines += "mkdir -p v$k && cp -r .claude v$k/" $lines += "( DCDC_ROUTE_SERVER=1 $timeFmt $Interp ${remoteRoot}/v$k/.claude/r2/route_base.py " + "--offline --replan --passes $Passes$priority $v > v$k.log 2>&1; echo `$? > v$k.rc ) &" } $lines += 'wait' $lines += 'touch done' [System.IO.File]::WriteAllText($RunScript, (($lines -join "`n") + "`n")) & $Scp $RunScript "${Server}:${remoteRoot}/run.sh" if ($LASTEXITCODE -ne 0) { throw 'Failed to upload run script.' } & $Ssh $Server "setsid nohup bash ${remoteRoot}/run.sh > /dev/null 2>&1 < /dev/null &" if ($LASTEXITCODE -ne 0) { throw 'Failed to start remote variants.' } for ($k = 0; $k -lt $Variants.Count; $k++) { Write-Host "v${k}: $($Variants[$k])" } $start = Get-Date $poll = 0 while ($true) { Start-Sleep -Seconds $PollSec $poll++ $state = (& $Ssh $Server "test -f ${remoteRoot}/done && echo DONE || echo RUN").Trim() if ($state -eq 'DONE') { break } if ($poll % 5 -eq 0) { $mins = [int]((Get-Date) - $start).TotalMinutes Write-Host "--- $mins min ---" & $Ssh $Server "cd ${remoteRoot} && for f in v*.log; do echo `"`$f: `$(tail -n 1 `$f)`"; done" } } $summary = & $Ssh $Server ("cd ${remoteRoot} && for k in `$(seq 0 $($Variants.Count - 1)); do " + "echo `"v`$k rc=`$(cat v`$k.rc) `$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); " + "print(len(d[\`"logs\`"]), len(d[\`"segs\`"]), len(d[\`"vias\`"]))' " + "v`$k/.claude/r2/_base_routes.json 2>/dev/null || echo NA)`"; done") $best = $null foreach ($row in $summary) { Write-Host $row if ($row -match '^v(\d+) rc=0 (\d+) (\d+) (\d+)$') { $cand = @{ K = [int]$Matches[1]; Logs = [int]$Matches[2]; Vias = [int]$Matches[4] } if (-not $best -or $cand.Logs -lt $best.Logs -or ($cand.Logs -eq $best.Logs -and $cand.Vias -lt $best.Vias)) { $best = $cand } } } for ($k = 0; $k -lt $Variants.Count; $k++) { Write-Host "===== v${k}: $($Variants[$k]) =====" & $Ssh $Server "grep -E '拆线重布|第 [0-9]+ 轮|REMOTE_TIME|Traceback|Error' ${remoteRoot}/v$k.log | tail -n 25" } if (-not $best) { throw 'No remote variant finished successfully.' } Write-Host "Best variant: v$($best.K) ($($Variants[$best.K])) with $($best.Logs) unfinished." $remoteResult = "${remoteRoot}/v$($best.K)/.claude/r2/_base_routes.json" } if ($DpOnly) { Write-Host 'Server-side D+/- diagnostic completed; local route cache was not changed.' } else { & $Scp "${Server}:${remoteResult}" $Incoming if ($LASTEXITCODE -ne 0) { throw 'Failed to download routing result.' } $result = Get-Content -LiteralPath $Incoming -Raw | ConvertFrom-Json if ($result.schema -ne 7 -or $result.slot_rot -ne 0 -or $result.usb_slot_ref.'1' -ne 'J34' -or $result.usb_slot_ref.'5' -ne 'J38') { throw 'Downloaded routing cache failed topology validation.' } $current = if (Test-Path -LiteralPath $Cache) { Get-Content -LiteralPath $Cache -Raw | ConvertFrom-Json } else { $null } if ($current -and $result.logs.Count -gt $current.logs.Count) { Move-Item -LiteralPath $Incoming -Destination $Rejected -Force Write-Host "Rejected worse server result: $($result.logs.Count) unfinished > current $($current.logs.Count)." } else { if ($current) { Copy-Item -LiteralPath $Cache -Destination $Backup -Force } Move-Item -LiteralPath $Incoming -Destination $Cache -Force Write-Host "Accepted server result: $($result.segs.Count) segments, $($result.vias.Count) vias, $($result.logs.Count) unfinished." } } } finally { Pop-Location foreach ($f in @($Archive, $RunScript)) { if (Test-Path -LiteralPath $f) { Remove-Item -LiteralPath $f -Force } } if ($remoteRoot -and $remoteRoot -match '^/tmp/dcdc-route\.[A-Za-z0-9]+$') { # ⛔ 模式里的 `(v[0-9]+/)?[.]claude` 是故意的:直接写目录串的话,执行这条命令的 # 远程 shell 命令行里也含这个串,pkill 先把自己杀了(ssh 返回 255),后面的 rm # 根本不执行。按路径匹配而不是按 `python3`,换成 pypy3 也杀得到。 & $Ssh $Server "pkill -f '${remoteRoot}/(v[0-9]+/)?[.]claude/r2/route_base' ; rm -rf -- '${remoteRoot}'" } }