diff --git a/README.md b/README.md index fbc9818..6f89c62 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ | 第三方 chatbot 客户端 | 5056 | `iXserver\Tools\ThirdPartyChatbotTestClient` | | IG 后端模拟 | 5070 | `iXserver\Tools\IgMockServer` | | SCRM 客服系统模拟 | 5080 | `iXserver\Tools\ScrmMockServer` | +| 闭环观察台(只读,泳道时序图) | 5090 | `iXserver\Tools\LoopConsole` | 所有端口只绑本机。iXserver 的旧接口和 WebSocket **没有鉴权**,绝不能对外开放 8081 / 33346。 @@ -51,12 +52,14 @@ .\scenarios\s8-human-chain.ps1 # SCRM 开工 → 新 IG 用户 → 人工工单推送 → 指派 / 回复 → IG 收到 → 结束 → 下班 ``` +跑的时候开着观察台 ,每一跳和状态变化会实时画成泳道时序图;`?ticket=<工单号>` 只看一张。 + S8 结束时会把服务号切回机器人、在线人数归零,所以 S1 / S8 先后跑互不影响;要停在人工状态看页面加 `-KeepShift`。 从 Mac 看几个页面用 SSH 本地端口转发: ```bash -ssh -N -L 5056:127.0.0.1:5056 -L 5070:127.0.0.1:5070 -L 5080:127.0.0.1:5080 FG-W11-ICON-DEV +ssh -N -L 5056:127.0.0.1:5056 -L 5070:127.0.0.1:5070 -L 5080:127.0.0.1:5080 -L 5090:127.0.0.1:5090 FG-W11-ICON-DEV ``` ## 几个容易踩的点 @@ -87,6 +90,8 @@ ssh -N -L 5056:127.0.0.1:5056 -L 5070:127.0.0.1:5070 -L 5080:127.0.0.1:5080 FG-W `appsettings.Loop.json` 把所有连接串都盖成本机 iCONAbpFX。 - PowerShell 5.1 的两个坑已经在 `loop.ps1` 里绕开:原生命令往 stderr 写一行就抛异常(统一走 `Invoke-Native`); `Get-Content` 默认按 GBK 读无 BOM 的 UTF-8(拷配置用 `Copy-Item`,脚本文件一律带 BOM)。 +- **iXserver 的主日志在 `run\ixserver\Log\{Debug,Information,Warning,Error}\<日期>\`**(注入的 ILogger,内部步骤很全,观察台就读它)。 + `run\data\API_Log` 是静态 `Logger` 写的另一份,几乎是空的,别看它下结论。排查先看 `Log\Error`。 - `deployed.json` 记录每次部署时两个源码仓库的 commit 与未提交文件。闭环结果离开「当时部署的是哪个版本」没法解读, 跑场景的记录里引用它。注意 winbox 上 iXserver 仓库的 HEAD 不随 `sync.sh` 前进(只推文件), `commit` 字段可能停在很久以前,以 `uncommitted` 清单和 Mac 上的提交为准。 diff --git a/config/ixserver/config.json b/config/ixserver/config.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/config/ixserver/config.json @@ -0,0 +1 @@ +{} diff --git a/config/ixserver/overrides.psd1 b/config/ixserver/overrides.psd1 index 89b34b2..d353973 100644 --- a/config/ixserver/overrides.psd1 +++ b/config/ixserver/overrides.psd1 @@ -33,6 +33,11 @@ 'FileFolder' = '{RunData}\API_FileFolder\' 'DbTimeLogLocation' = '{RunData}\API_Log_DB\' 'AutoChatJsonSetting_JsonStoreDir' = '{RunData}\TicketJsonDir' + # sys_language 启动时把生成的 JS 写进 WebchatPath;写死的 D:\ 不存在会记一条「设备未就绪」 + 'WebchatPath' = '{RunData}\Webchat' + 'OffLineFormPath' = '{RunData}\pages\' + # 仓库里没有 config.json,读不到就记 Error 并退回默认值;指到 LoopEnv 里那份(空对象 = 全用默认值) + 'AppConfigManager_AppConfigJsonFile' = '{LoopRoot}\config\ixserver\config.json' # ---- 自身地址:上传文件的访问地址要能从本机打开 'ApiUrl' = 'http://localhost:{IxserverPort}/' diff --git a/config/loop.psd1 b/config/loop.psd1 index 9e6ff59..ab37ed4 100644 --- a/config/loop.psd1 +++ b/config/loop.psd1 @@ -21,6 +21,7 @@ Chatbot = 5056 IgMock = 5070 ScrmMock = 5080 + Console = 5090 # 闭环观察台:只读汇总各角色,画泳道时序图 } # 闭环专用的假凭据:与 staging / 生产无关,可以进 git @@ -39,6 +40,7 @@ IgPageId = '568932443471849' CompanyCode = 'SM_HKTB_IG' AdminCode = 'HKTB' + QuotaName = 'HKTB' # 额度池名,与管理员 code 同名只是碰巧 # 第三方客户端收到投递后自动回复:echo / 任意固定文本;留空则只能在页面上手动回 ChatbotAutoReply = 'echo' diff --git a/loop.ps1 b/loop.ps1 index ed28918..ee2bb6b 100644 --- a/loop.ps1 +++ b/loop.ps1 @@ -54,7 +54,7 @@ $Dirs = @{ } foreach ($d in $Dirs.Values) { if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d | Out-Null } } -$AllRoles = @('abp', 'ixserver', 'chatbot', 'igmock', 'scrm') +$AllRoles = @('abp', 'ixserver', 'chatbot', 'igmock', 'scrm', 'console') $Roles = if ($Only) { $AllRoles | Where-Object { $Only -contains $_ } } else { $AllRoles } # 健康检查要连本机 ABP 的 dev 证书;只影响本 PowerShell 进程 @@ -70,6 +70,7 @@ function Write-Warn2([string]$text) { Write-Host " $text" -ForegroundColor Ye function Expand-Template([string]$text) { $map = @{ '{RunData}' = $Dirs.Data + '{LoopRoot}' = $Root '{IxserverPort}' = $Cfg.Ports.Ixserver '{WebSocketPort}' = $Cfg.Ports.WebSocket '{AbpPort}' = $Cfg.Ports.Abp @@ -230,11 +231,27 @@ function Get-RoleSpec([string]$role) { } 'scrm' { $script = "$tools\ScrmMockServer\server.js" - if (-not (Test-Path $script)) { return $null } # P7 之前还没有 + if (-not (Test-Path $script)) { return $null } # iXserver 仓库还没同步到带这个工具的版本 $cmd = "`"$(Get-NodeExe)`" `"$script`" --port $($p.ScrmMock) --ixserver http://localhost:$($p.Ixserver)" + - " --ws ws://localhost:$($p.WebSocket) --admin-code $($Cfg.AdminCode) --company-code $($Cfg.CompanyCode) --service-code $($Cfg.ServiceCode)" + " --ws ws://localhost:$($p.WebSocket) --admin-code $($Cfg.AdminCode) --company-code $($Cfg.CompanyCode) --service-code $($Cfg.ServiceCode) --quota-name $($Cfg.QuotaName)" return @{ Port = $p.ScrmMock; WorkDir = $Root; Command = $cmd; Health = { Test-HttpAlive "http://127.0.0.1:$($Cfg.Ports.ScrmMock)/_api/state" }; Timeout = 30 } } + 'console' { + $script = "$tools\LoopConsole\server.js" + if (-not (Test-Path $script)) { return $null } + $s = $Cfg.Sql + $cmd = "`"$(Get-NodeExe)`" `"$script`" --port $($p.Console)" + + " --ig http://127.0.0.1:$($p.IgMock) --chatbot http://127.0.0.1:$($p.Chatbot) --scrm http://127.0.0.1:$($p.ScrmMock)" + + " --ixserver http://localhost:$($p.Ixserver) --abp https://localhost:$($p.Abp)" + + " --abp-log-dir `"$(Join-Path $Dirs.Abp 'Logs\Information')`" --ix-log-dir `"$(Join-Path $Dirs.Ixserver 'Log')`" --deployed `"$(Join-Path $Root 'deployed.json')`"" + + " --sql-server `"$($s.Server)`" --sql-user $($s.User) --db $($s.IxserverDb)" + + " --company-code $($Cfg.CompanyCode) --service-code $($Cfg.ServiceCode) --quota-name $($Cfg.QuotaName)" + return @{ + Port = $p.Console; WorkDir = $Root; Command = $cmd + Env = @{ LOOP_SQL_PASSWORD = $s.Password } + Health = { Test-HttpAlive "http://127.0.0.1:$($Cfg.Ports.Console)/_api/state" }; Timeout = 30 + } + } } } @@ -276,7 +293,7 @@ function Set-WebConfigOverrides { $xml.Save($path) - foreach ($sub in 'API_Log', 'API_TempFolder', 'API_FileFolder', 'API_Log_DB', 'TicketJsonDir', 'aspnet-temp') { + foreach ($sub in 'API_Log', 'API_TempFolder', 'API_FileFolder', 'API_Log_DB', 'TicketJsonDir', 'aspnet-temp', 'Webchat\Scripts', 'pages') { $d = Join-Path $Dirs.Data $sub if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d | Out-Null } } diff --git a/scenarios/s1-ig-text-echo.ps1 b/scenarios/s1-ig-text-echo.ps1 index fd55db2..06d37a8 100644 --- a/scenarios/s1-ig-text-echo.ps1 +++ b/scenarios/s1-ig-text-echo.ps1 @@ -15,7 +15,8 @@ function Fail([string]$why) { Write-Host '--- IG 模拟记录' (Invoke-RestMethod "$ig/_api/state").events | ForEach-Object { $_ | ConvertTo-Json -Depth 8 -Compress } | Out-Host Write-Host '--- chatbot 记录(最近 6 条)' - (Invoke-RestMethod "$cb/_api/state").events | Select-Object -Last 6 | ForEach-Object { $_ | ConvertTo-Json -Depth 8 -Compress } | Out-Host + # 记录是新的在前 + (Invoke-RestMethod "$cb/_api/state").events | Select-Object -First 6 | ForEach-Object { $_ | ConvertTo-Json -Depth 8 -Compress } | Out-Host exit 1 } @@ -36,7 +37,12 @@ if (-not $hit) { Fail "$TimeoutSec 秒内 IG 用户没有收到「$expected」" if (@($hit.issues).Count) { Fail "出站请求有问题:$($hit.issues -join ';')" } if (-not $hit.tokenPresent) { Fail '出站请求没带 access_token' } -$reply = @((Invoke-RestMethod "$cb/_api/state").events | Where-Object { $_.kind -eq 'reply' -and $_.message -eq $expected }) | Select-Object -First 1 +# 也要等:iXserver 在 send-txt 应答之前就把消息发到 IG 了,客户端拿到应答才记 reply,会晚上百毫秒 +$reply = $null +while (-not $reply -and (Get-Date) -lt $deadline.AddSeconds(5)) { + $reply = @((Invoke-RestMethod "$cb/_api/state").events | Where-Object { $_.kind -eq 'reply' -and $_.message -eq $expected }) | Select-Object -First 1 + if (-not $reply) { Start-Sleep -Milliseconds 200 } +} if (-not $reply) { Fail 'chatbot 侧没有对应的回复记录' } if ($reply.response.status -ne 201) { Fail "send-txt 返回 $($reply.response.status)" }