Launcher.ps1
|
# 执行层(spec 5):先切目录再启动 claude(顺序不可颠倒,发现 9) # 降级链(决策 23 + 三轮反馈修正):-r 失败 → -c 接续最近会话 → 带 prompt 兜底 # 第四轮根因修复(2026-09-01 探针矩阵实锤): # 旧形态 $null = Invoke-WithSpinner { Invoke-Expression ... } 中 PowerShell 一旦捕获子进程输出 # 就给它挂 stdout 管道 → claude(Node)检测 stdout 非 TTY → 进 headless print 模式 → # 无 prompt 启动报 "No deferred tool marker found"(L1 -r 与 L2 -c 同报此错的根因)。 # 探针矩阵:$null= / 管道 / 上层赋值捕获全管道化(B/C/G/I 形态);Start-Process -NoNewWindow # 子进程继承控制台(TTY 保持),ExitCode 是 Int32 本尊,绝无污染可能。 # 最终形态:Start-Process 直启 + -RedirectStandardError 分流(stderr 不与 TUI 输出竞争时序) # 运行一条 claude 命令:旋转 spinner + stderr 分流;返回退出码(Int32 本尊,绝不被输出污染) function Invoke-CctClaude { param([string]$Command, [string]$SpinnerText) # 命令拆分:首 token 为可执行文件(含引号形态),其余为参数行 $m = [regex]::Match($Command, '^\s*("[^"]+"|\S+)\s*(.*)$') $exe = $m.Groups[1].Value.Trim('"') $argLine = $m.Groups[2].Value $errFile = Join-Path ([System.IO.Path]::GetTempPath()) ("cct_err_" + [guid]::NewGuid().ToString('N') + '.txt') try { # 旋转 spinner(反馈 3-1:恢复 |/-\ 动画)。决策 55 的「后台线程与 claude 输出并发写屏撕裂」 # 根因已被第四轮 Start-Process 保 TTY 根治消除——claude 打开真实 TUI 接管屏幕后 spinner # 自然被覆盖,残留风险仅在 TUI 接管前 1-2s(claude 冷启动期)。autoStop 6s 兜底: # 长时间交互式会话期间后台线程不持续写屏。测试/重定向环境不启动 spinner(Invoke-WithSpinner 直跑)。 # -NoNewWindow:继承当前控制台(TTY 保持,claude 开 TUI 而非 headless) # stdout 不重定向:交互式 TUI 直接渲染;stderr 分流到文件,结束后统一回放 $code = Invoke-WithSpinner -Text $SpinnerText -AutoStopMs 6000 -ScriptBlock { $p = Start-Process -FilePath $exe -ArgumentList $argLine -RedirectStandardError $errFile -NoNewWindow -Wait -PassThru return [int]$p.ExitCode } } catch { # Start-Process 失败(命令不存在等):清理临时 errFile 后报错 if ($errFile -and (Test-Path -LiteralPath $errFile)) { Remove-Item -LiteralPath $errFile -ErrorAction SilentlyContinue } Write-Host "" Write-Error "启动命令失败: $($_.Exception.Message)" return 1 } # stderr 回放(无后台线程写屏,时序干净) if (Test-Path -LiteralPath $errFile) { $err = Get-Content -LiteralPath $errFile -Raw -ErrorAction SilentlyContinue if ($err) { [Console]::Error.WriteLine($err.TrimEnd()) } Remove-Item -LiteralPath $errFile -ErrorAction SilentlyContinue } return $code } # spinner 文本(反馈 3-2):正在启动会话:[账号调优(管理_sub2api)]——会话项带父目录括号,与卡片显示一致 function Get-CctSpinnerText { param([pscustomobject]$Item) $display = if ($Item.Kind -eq 'Session') { $parent = Split-Path -Leaf $Item.Path "$($Item.Name)($parent)" } else { [string]$Item.Name } return "正在启动会话:[$display]" } function Invoke-CctTask { [CmdletBinding()] param([pscustomobject]$Item, [pscustomobject]$Config, [switch]$DryRun) # 目录校验(决策 26:不存在报错留原地) if (-not (Test-Path -LiteralPath $Item.Path)) { Write-Error "任务目录不存在: $($Item.Path)" return [pscustomobject]@{ Started = $false; Command = $null; Location = (Get-Location).Path } } # 1. 先切目录(发现 9:--resume 不会自动回原目录,必须先 Set-Location) Set-Location -LiteralPath $Item.Path # 2. 组命令(决策 4/10) $cmd = if ($Item.Kind -eq 'Session' -and $Item.SessionId) { $Config.resumeCommand -replace '\{sessionId\}', $Item.SessionId } else { $Config.launchCommand } if ($DryRun) { return [pscustomobject]@{ Started = $true; Command = $cmd; Location = $Item.Path } } # spinner 文本带卡片名(反馈 3-2) $spinnerText = Get-CctSpinnerText $Item # 3. 启动(交互式 claude,直接调用透传错误——决策 27) try { $exit = Invoke-CctClaude $cmd $spinnerText if ($exit -eq 0) { return [pscustomobject]@{ Started = $true; Command = $cmd; Location = $Item.Path } } # 用户 Ctrl+C 主动中断(exit 130/143):不降级不报错,静默收尾 if (130, 143 -contains $exit) { return [pscustomobject]@{ Started = $true; Command = $cmd; Location = $Item.Path; Interrupted = $true } } # 降级 L2:接续最近会话(反馈 3-3:降级也要 spinner,接续会话启动也要时间) Write-Host "该会话无法恢复(退出码 $exit),已改为接续最近会话" -ForegroundColor Yellow $exit2 = Invoke-CctClaude $Config.launchCommand "$spinnerText(接续)" if ($exit2 -eq 0) { return [pscustomobject]@{ Started = $true; Command = $Config.launchCommand; Location = $Item.Path } } if ($exit2 -in 130, 143) { return [pscustomobject]@{ Started = $true; Command = $Config.launchCommand; Location = $Item.Path; Interrupted = $true } } # (-in 对 int 安全;Invoke-CctClaude 返回值已 [int] 强转,不可能再是数组) # 降级 L3:带 prompt 兜底(绕过 CC 无 prompt 空输入判定,实测退出码 0) Write-Host "接续最近会话也失败(退出码 $exit2),改为带提示词恢复" -ForegroundColor Yellow $fallbackCmd = if ($Item.Kind -eq 'Session' -and $Item.SessionId) { "$($Config.resumeCommand -replace '\{sessionId\}', $Item.SessionId) `"继续`"" } else { "$($Config.launchCommand) `"继续`"" } $exit3 = Invoke-CctClaude $fallbackCmd "$spinnerText(兜底)" if ($exit3 -eq 0) { return [pscustomobject]@{ Started = $true; Command = $fallbackCmd; Location = $Item.Path } } if ($exit3 -in 130, 143) { return [pscustomobject]@{ Started = $true; Command = $fallbackCmd; Location = $Item.Path; Interrupted = $true } } # 全失败:诊断块(反馈 3-4:输出任务完整地址 + 会话 ID,方便排错) Write-Host "带提示词恢复也失败(退出码 $exit3)" -ForegroundColor Red Write-Host "" Write-Host "===== 诊断信息 =====" -ForegroundColor Red Write-Host "任务路径: $($Item.Path)" if ($Item.SessionId) { Write-Host "会话 ID: $($Item.SessionId)" } Write-Host "目录存在: $(Test-Path -LiteralPath $Item.Path)" Write-Host "退出码链: L1(-r)=$exit → L2(-c)=$exit2 → L3(prompt)=$exit3" return [pscustomobject]@{ Started = $false; Command = $fallbackCmd; Location = $Item.Path } } catch { Write-Error "启动失败: $_" return [pscustomobject]@{ Started = $false; Command = $cmd; Location = $Item.Path } } } |