<# S8:人工链路本身(不经第三方 chatbot)。 SCRM 开工(SetSetting IsEnable=false + UpdateCSAvailable 3)→ 客服上线(UserReg + 额度池) → 新 IG 用户发消息 → 人工工单,SCRM 在 WebSocket 上收到推送 → 指派 → 打开(验证信息 + 已读) → 以客服身份回复 → IG 用户收到 → 结束工单 → SCRM 下班(切回机器人、在线 0),不影响 S1。 前提:.\loop.ps1 up 各角色都健康。每次用新的 IGSID,保证建出新工单。 通过退出 0,失败退出 1 并打印记录;两种情况都在 run\reports\ 生成场景报告(观察台在跑时)。 用法:.\scenarios\s8-human-chain.ps1 [-TimeoutSec 60] [-KeepShift] [-NoReport] #> param([int]$TimeoutSec = 60, [switch]$KeepShift, [switch]$NoReport) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $LoopNoReport = [bool]$NoReport . (Join-Path $PSScriptRoot '_lib.ps1') $Cfg = $LoopCfg $ig = "http://127.0.0.1:$($Cfg.Ports.IgMock)" $sc = "http://127.0.0.1:$($Cfg.Ports.ScrmMock)" function Post([string]$url, $body) { $bytes = [Text.Encoding]::UTF8.GetBytes(($body | ConvertTo-Json -Compress -Depth 6)) try { return Invoke-RestMethod $url -Method Post -ContentType 'application/json; charset=utf-8' -Body $bytes } catch { # 动作失败时模拟方回 502,正文里带着每一步的记录 $resp = $_.Exception.Response if (-not $resp) { throw } $reader = New-Object IO.StreamReader($resp.GetResponseStream(), [Text.Encoding]::UTF8) return ($reader.ReadToEnd() | ConvertFrom-Json) } } function Fail([string]$why) { Write-Host "S8 失败:$why" -ForegroundColor Red Write-Host '--- SCRM 模拟记录(最近 15 条)' (Invoke-RestMethod "$sc/_api/state").events | Select-Object -First 15 | ForEach-Object { ($_ | ConvertTo-Json -Depth 8 -Compress) } | Out-Host Write-Host '--- IG 模拟记录' (Invoke-RestMethod "$ig/_api/state").events | ForEach-Object { "{0} {1} {2}" -f $_.kind, $_.igsid, $_.summary } | Out-Host Add-LoopStep $why $false if (-not $KeepShift) { Post "$sc/_api/shift" @{ on = $false } | Out-Null } Save-LoopReport -Passed $false -Reason $why exit 1 } function Step([string]$name, [string]$action, $body) { $r = Post "$sc/_api/$action" $body if (-not $r.ok) { Fail "$name($action)未成功:$($r.error) $((@($r.calls) | ForEach-Object { $_.api + ' → ' + ($_.response.body.msg) }) -join ';')" } Add-LoopStep $name $true ((@($r.calls) | ForEach-Object { $_.api }) -join ' + ') return $r } function Wait-For([string]$what, [scriptblock]$probe) { $deadline = (Get-Date).AddSeconds($TimeoutSec) while ((Get-Date) -lt $deadline) { $hit = & $probe if ($hit) { return $hit } Start-Sleep -Milliseconds 500 } Fail "$TimeoutSec 秒内没等到:$what" } Start-LoopRun 'S8' 'SCRM 人工链路' $ws = (Invoke-RestMethod "$sc/_api/state").state.ws if ($ws.status -ne 'logged-in') { Fail "SCRM 模拟的 WebSocket 没登录($($ws.status) $($ws.lastError))" } Invoke-RestMethod "$sc/_api/clear" -Method Post | Out-Null Invoke-RestMethod "$ig/_api/clear" -Method Post | Out-Null Step 'SCRM 开工(切人工、在线 3)' 'shift' @{ on = $true } | Out-Null Step '客服上线' 'agent-online' @{} | Out-Null $igsid = 'loop-s8-' + [guid]::NewGuid().ToString('N').Substring(0, 8) # 人工状态下条款同样先拦第一句话;同意那一句就建出了人工工单,下面按 IGSID 认的还是这张 $err = Complete-LoopTnC $igsid $TimeoutSec if ($err) { Fail $err } $sent = Post "$ig/_api/send" @{ kind = 'text'; igsid = $igsid; text = 'S8 想找人工' } if ($sent.event.response.status -ne 200) { Fail "iXserver 收 webhook 返回 $($sent.event.response.status)" } Add-LoopStep '新 IG 用户发消息' $true "$igsid HTTP 200" # 推送报文没有统一类型字段,按 enduser_openid = IGSID 认出这张新工单 $ticketId = Wait-For 'SCRM 收到这位 IG 用户的工单推送' { $push = @((Invoke-RestMethod "$sc/_api/state").events | Where-Object { $_.kind -eq 'push' -and (($_.parsed | ConvertTo-Json -Depth 8 -Compress) -match [regex]::Escape($igsid)) }) | Select-Object -First 1 if ($push) { $push.ticketIds | Select-Object -First 1 } } Set-LoopTicket $ticketId Add-LoopStep 'SCRM 收到工单推送' $true "ticket $ticketId" Step '指派给客服' 'assign' @{ ticket_id = $ticketId } | Out-Null Step '打开工单(验证信息 + 已读)' 'open' @{ ticket_id = $ticketId } | Out-Null $replyText = 'S8 客服回复 ' + [guid]::NewGuid().ToString('N').Substring(0, 6) Step '客服回复' 'reply' @{ ticket_id = $ticketId; text = $replyText } | Out-Null $out = Wait-For "IG 用户收到「$replyText」" { @((Invoke-RestMethod "$ig/_api/state").events | Where-Object { $_.kind -eq 'outbound' -and $_.igsid -eq $igsid -and $_.summary -eq $replyText }) | Select-Object -First 1 } if (@($out.issues).Count) { Fail "出站请求有问题:$($out.issues -join ';')" } if ($out.response.status -ne 200) { Fail "Graph 应答 $($out.response.status)(IG 模拟档位 $($out.response.mode)),用户实际没收到" } Add-LoopStep 'IG 用户收到客服回复' $true $replyText Step '结束工单' 'complete' @{ ticket_id = $ticketId } | Out-Null if (-not $KeepShift) { Step 'SCRM 下班(切回机器人、在线 0)' 'shift' @{ on = $false } | Out-Null } Write-Host "S8 通过:$igsid → 人工工单 $ticketId → SCRM 收到推送、指派、回复 → IG 收到「$replyText」→ 已结束" -ForegroundColor Green Save-LoopReport -Passed $true exit 0