Files

292 lines
14 KiB
PowerShell
Raw Permalink Normal View History

# agent-browser-auth-wait.ps1
#
# 功能:使用 agent-browser 打开登录页(含二维码),导出二维码图片,
# 等待用户扫码完成后继续执行,自动保存认证状态供下次复用。
#
# 设计要点:
# 1. 复用 agent-browser v0.27.0+ 的 state save/load 能力(内置),
# 不自造轮询 cookie 的轮子。
# 2. 使用 eval -b <base64> 注入 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
}