<# S8:人工链路本身(不经第三方 chatbot)。 SCRM 开工(SetSetting IsEnable=false + UpdateCSAvailable 3)→ 客服上线(UserReg + 额度池) → 新 IG 用户发消息 → 人工工单,SCRM 在 WebSocket 上收到推送 → 指派 → 打开(验证信息 + 已读) → 以客服身份回复 → IG 用户收到 → 结束工单 → SCRM 下班(切回机器人、在线 0),不影响 S1。 前提:.\loop.ps1 up 五个角色都健康。每次用新的 IGSID,保证建出新工单。 通过退出 0,失败退出 1 并打印记录。用法:.\scenarios\s8-human-chain.ps1 [-TimeoutSec 60] [-KeepShift] #> param([int]$TimeoutSec = 60, [switch]$KeepShift) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $Cfg = Import-PowerShellDataFile (Join-Path $PSScriptRoot '..\config\loop.psd1') $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 if (-not $KeepShift) { Post "$sc/_api/shift" @{ on = $false } | Out-Null } exit 1 } function Step([string]$action, $body) { $r = Post "$sc/_api/$action" $body if (-not $r.ok) { Fail "$action 未成功:$($r.error) $((@($r.calls) | ForEach-Object { $_.api + ' → ' + ($_.response.body.msg) }) -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" } $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 'shift' @{ on = $true } | Out-Null Step 'agent-online' @{} | Out-Null $igsid = 'loop-s8-' + [guid]::NewGuid().ToString('N').Substring(0, 8) $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)" } # 推送报文没有统一类型字段,按 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 } } 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 ';')" } Step 'complete' @{ ticket_id = $ticketId } | Out-Null if (-not $KeepShift) { Step 'shift' @{ on = $false } | Out-Null } Write-Host "S8 通过:$igsid → 人工工单 $ticketId → SCRM 收到推送、指派、回复 → IG 收到「$replyText」→ 已结束" -ForegroundColor Green exit 0