# agent-browser-auth-wait.ps1 # # 功能:使用 agent-browser 打开登录页(含二维码),导出二维码图片, # 等待用户扫码完成后继续执行,自动保存认证状态供下次复用。 # # 设计要点: # 1. 复用 agent-browser v0.27.0+ 的 state save/load 能力(内置), # 不自造轮询 cookie 的轮子。 # 2. 使用 eval -b 注入 JS,避开 PowerShell pipe -> eval --stdin # 会卡死的坑(实测 v0.27.0 中 PowerShell pipe 到 eval --stdin 永不返回)。 # 3. 默认 headless + --no-sandbox,因为当前 sandbox 环境 headless chromium # 会因沙箱权限问题直接 exit 3;如需看到浏览器窗口,加 -Headed。 # 4. wait --url 单次默认 25s 超时,因此用"轮询 wait + 累计超时"实现长等待。 # 5. 不主动 close daemon(除非 -CloseOnExit),便于调用方在同一 daemon 内 # 继续执行后续业务。 # # 用法示例: # # 等用户扫码登录,登录完成后进入业务页 # .\agent-browser-auth-wait.ps1 ` # -LoginUrl "https://itsupport.servyou.com.cn/login" ` # -ExpectedPath "dashboard" ` # -StateFile "$PSScriptRoot\auth-state.json" # # # 测试 headed 模式(用户能看到浏览器窗口) # .\agent-browser-auth-wait.ps1 -LoginUrl "..." -Headed # # # 等待登录后页面上出现"欢迎"字样 # .\agent-browser-auth-wait.ps1 -LoginUrl "..." -ExpectedText "欢迎" # # Exit code: # 0 = 登录成功(或检测到已登录) # 1 = 等待超时 # 2 = 其他错误 [CmdletBinding()] param( # ── 必填 ───────────────────────────────────────────────────────────── [Parameter(Mandatory = $true)] [string]$LoginUrl, # ── 等待条件(二选一或组合)─────────────────────────────────────────── # 登录后跳转的目标 URL 子串(如 "dashboard"),会与 current URL 做 -like 匹配 [string]$ExpectedPath = "dashboard", # 登录后页面出现的文本(substring 匹配),可选 [string]$ExpectedText, # ── 超时与轮询 ───────────────────────────────────────────────────────── # 等待用户扫码的总秒数(默认 2 分钟) [int]$TimeoutSec = 120, # 轮询间隔(毫秒),默认 1.5 秒 [int]$PollIntervalMs = 1500, # ── 持久化 ───────────────────────────────────────────────────────────── # 登录成功后保存 state 的路径(cookies + localStorage),下次 -StateFile 加载可跳过登录 [string]$StateFile, # ── 二维码 ───────────────────────────────────────────────────────────── # 二维码截图导出路径(headless 模式下用户看不到浏览器,必须导出) [string]$QrImagePath = "./qr-code.png", # ── 浏览器模式 ───────────────────────────────────────────────────────── # 显示浏览器窗口(默认 headless + --no-sandbox,适合 CI/无人值守) [switch]$Headed, # 完成后关闭 daemon(默认不关,便于调用方继续在同一 daemon 内执行业务) [switch]$CloseOnExit, # 调试模式:打印每个 agent-browser 调用的命令和耗时(避免命名 Debug 以免与 CmdletBinding 冲突) [switch]$Trace ) # ── 全局设置 ──────────────────────────────────────────────────────────── $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # ── 辅助函数:统一注入 --args ──────────────────────────────────────────── # 用途:所有 agent-browser 调用统一通过此函数,自动添加 --no-sandbox # (headless 模式必须),便于复用。 # # 注意:必须将全局参数和命令参数合并成**一个数组**再 splat。 # PowerShell 中 `& cmd @arr1 @arr2` 双 splat 在某些版本下会被 # 解释成"@arr1 当作第一个参数,@arr2 是剩余参数",导致 # agent-browser 收到错误的 argv 然后 hang。本次实测复现。 function Invoke-AgentBrowser { [CmdletBinding()] param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Args ) # 准备全局参数数组 $globalArgs = @() if (-not $Headed) { # headless 模式必须 --no-sandbox,否则 sandbox 环境会 chrome exit 3 $globalArgs = @('--args', '--no-sandbox') } # 合并为单一数组(避免双 splat hang 坑) $allArgs = @($globalArgs + $Args) # 调试输出 if ($script:DebugMode) { Write-Host " [DEBUG] agent-browser $($allArgs -join ' ')" -ForegroundColor DarkGray } # 调用 agent-browser,捕获输出 $sw = [System.Diagnostics.Stopwatch]::StartNew() $output = & agent-browser @allArgs 2>&1 $sw.Stop() $exitCode = $LASTEXITCODE if ($script:DebugMode) { Write-Host " [DEBUG] exit=$exitCode, $($sw.ElapsedMilliseconds)ms, output_lines=$(($output | Measure-Object).Count)" -ForegroundColor DarkGray } # 返回 PSObject 便于调用方检查 return [PSCustomObject]@{ ExitCode = $exitCode Output = ($output -join "`n") } } # ── 辅助函数:用 base64 执行 JS(避开 --stdin pipe 卡死问题)──────────── function Invoke-ABEvalJS { [CmdletBinding()] param([Parameter(Mandatory = $true)][string]$Js) $bytes = [System.Text.Encoding]::UTF8.GetBytes($Js) $base64 = [Convert]::ToBase64String($bytes) return (Invoke-AgentBrowser -Args @('eval', '-b', $base64)) } # ── 辅助函数:等待用户扫码登录完成 ───────────────────────────────────── # 策略:用 `get url`(瞬时命令)做短周期轮询,避免 `wait --url` 内置 25s # 超时卡住短超时场景。ExpectedPath 作为子串匹配(如 "dashboard")。 function Wait-ForLoginCompletion { [CmdletBinding()] param( [string]$UrlPattern, [string]$TextPattern, [int]$TimeoutSec, [int]$PollIntervalMs = 1500 ) $deadline = (Get-Date).AddSeconds($TimeoutSec) $attempt = 0 while ((Get-Date) -lt $deadline) { $attempt++ $remaining = [int]([Math]::Max(0, ($deadline - (Get-Date)).TotalSeconds)) Write-Host " ⏳ 第 $attempt 次检测(剩余 ${remaining}s)..." # ── 检测 1:URL 子串匹配 ── if ($UrlPattern) { $urlResult = Invoke-AgentBrowser -Args @('get', 'url') if ($urlResult.ExitCode -eq 0) { $currentUrl = ($urlResult.Output -join "`n").Trim() if ($currentUrl -like "*$UrlPattern*") { Write-Host " ✅ URL 匹配登录后路径: $currentUrl" -ForegroundColor Green return $true } } } # ── 检测 2:页面文本子串匹配 ── if ($TextPattern) { $checkJs = "document.body && document.body.innerText.includes('$TextPattern')" $b = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($checkJs)) $textResult = Invoke-AgentBrowser -Args @('eval', '-b', $b) if ($textResult.ExitCode -eq 0) { # eval 返回 "true" 或 "false" if ($textResult.Output -match '^\s*true\s*$') { Write-Host " ✅ 页面文本匹配: $TextPattern" -ForegroundColor Green return $true } } } Start-Sleep -Milliseconds $PollIntervalMs } return $false } # ── 主流程 ────────────────────────────────────────────────────────────── try { Write-Host "" Write-Host "════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " agent-browser 扫码登录等待" -ForegroundColor Cyan Write-Host "════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " 登录页: $LoginUrl" Write-Host " 等待路径: $ExpectedPath" if ($StateFile) { Write-Host " 状态文件: $StateFile" } Write-Host " Headed: $Headed" Write-Host "" # 调试钩子:-Trace 模式下打印每一步 $script:DebugMode = [bool]$Trace # ── Step 1: 加载已保存的 state(如提供)或直接打开登录页 ────────────── # 注意:--state 是启动时的全局参数(位于命令前),daemon 已启动时不能附加。 # 如果 daemon 没启动:第一次 open 会启动 daemon 并应用 --state。 # 如果 daemon 已启动:--state 被忽略,需要先关闭再重启。 # 本脚本不主动关闭 daemon,让调用方管理生命周期。 if ($StateFile -and (Test-Path $StateFile)) { Write-Host "📂 发现已保存的认证状态,尝试复用..." -ForegroundColor Yellow Write-Host " agent-browser --state $StateFile open $LoginUrl" $args = @('--state', $StateFile, 'open', $LoginUrl) $null = Invoke-AgentBrowser -Args $args } else { Write-Host "🌐 打开登录页..." $null = Invoke-AgentBrowser -Args @('open', $LoginUrl) } # ── Step 2: 等待页面加载完成 ────────────────────────────────────────── Write-Host "⏳ 等待页面加载..." $null = Invoke-AgentBrowser -Args @('wait', '--load', 'networkidle') # ── Step 3: 截图导出二维码 ──────────────────────────────────────────── # headless 模式下浏览器窗口不可见,二维码必须导出;headed 模式截图仅为冗余 $qrDir = Split-Path -Parent $QrImagePath if ($qrDir -and -not (Test-Path $qrDir)) { New-Item -ItemType Directory -Path $qrDir -Force | Out-Null } Write-Host "📸 导出二维码: $QrImagePath" -ForegroundColor Yellow $r = Invoke-AgentBrowser -Args @('screenshot', $QrImagePath) if ($r.ExitCode -ne 0) { Write-Warning "二维码截图失败:$($r.Output)" } elseif (Test-Path $QrImagePath) { Write-Host " 请用企微 APP 扫描二维码完成登录" if (-not $Headed) { Write-Host " (headless 模式:在文件管理器中打开 $QrImagePath 扫描)" -ForegroundColor Gray } } # ── Step 4: 提示用户 ────────────────────────────────────────────────── Write-Host "" Write-Host "📱 等待扫码登录(超时 $TimeoutSec 秒)..." -ForegroundColor Yellow Write-Host "" # ── Step 5: 轮询等待登录完成 ────────────────────────────────────────── $ok = Wait-ForLoginCompletion ` -UrlPattern $ExpectedPath ` -TextPattern $ExpectedText ` -TimeoutSec $TimeoutSec ` -PollIntervalMs $PollIntervalMs if (-not $ok) { Write-Host "" Write-Host "❌ 等待扫码登录超时(${TimeoutSec}s)" -ForegroundColor Red Write-Host " 请重试,或检查登录页是否正常显示二维码" -ForegroundColor Red Write-Host " daemon 保持打开,可手动操作后重跑脚本" -ForegroundColor Gray exit 1 } # ── Step 6: 保存 state(供下次复用)────────────────────────────────── if ($StateFile) { $stateDir = Split-Path -Parent $StateFile if ($stateDir -and -not (Test-Path $stateDir)) { New-Item -ItemType Directory -Path $stateDir -Force | Out-Null } Write-Host "💾 保存认证状态: $StateFile" $r = Invoke-AgentBrowser -Args @('state', 'save', $StateFile) if ($r.ExitCode -ne 0) { Write-Warning "state 保存失败:$($r.Output)" } } Write-Host "" Write-Host "✅ 扫码登录完成" -ForegroundColor Green Write-Host "" # ── Step 7: CloseOnExit ────────────────────────────────────────────── # 注意:脚本不主动关闭 daemon(实测 close --all 在某些 Windows 状态下 # 会让后续命令 hang)。如需关闭,调用方自己执行 `agent-browser close --all`。 if ($CloseOnExit) { Write-Host "ℹ️ CloseOnExit 已请求,但脚本不主动关闭 daemon 以避免 hang" -ForegroundColor Yellow Write-Host " 请手动执行: agent-browser close --all" -ForegroundColor Yellow } Write-Host "ℹ️ daemon 保持打开,可继续执行后续 agent-browser 命令" -ForegroundColor Gray exit 0 } catch { Write-Host "" Write-Host "❌ 脚本异常: $_" -ForegroundColor Red Write-Host $_.ScriptStackTrace -ForegroundColor Red exit 2 }