feat: 闭环观察台角色、补齐 Web.config 路径覆盖、修 S1 竞态
- loop.ps1 新增 console 角色(:5090),传入 ABP / iXserver 日志目录与库连接 - overrides.psd1 补 WebchatPath、OffLineFormPath、AppConfigManager_AppConfigJsonFile(新增 config/ixserver/config.json) - loop.psd1 新增 Ports.Console、QuotaName - S1 等第三方回复记录改为轮询,失败时打印最新的记录 - README:观察台、iXserver 主日志位置 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
parent
ad136a9d53
commit
ff0e390238
|
|
@ -10,6 +10,7 @@
|
||||||
| 第三方 chatbot 客户端 | 5056 | `iXserver\Tools\ThirdPartyChatbotTestClient` |
|
| 第三方 chatbot 客户端 | 5056 | `iXserver\Tools\ThirdPartyChatbotTestClient` |
|
||||||
| IG 后端模拟 | 5070 | `iXserver\Tools\IgMockServer` |
|
| IG 后端模拟 | 5070 | `iXserver\Tools\IgMockServer` |
|
||||||
| SCRM 客服系统模拟 | 5080 | `iXserver\Tools\ScrmMockServer` |
|
| SCRM 客服系统模拟 | 5080 | `iXserver\Tools\ScrmMockServer` |
|
||||||
|
| 闭环观察台(只读,泳道时序图) | 5090 | `iXserver\Tools\LoopConsole` |
|
||||||
|
|
||||||
所有端口只绑本机。iXserver 的旧接口和 WebSocket **没有鉴权**,绝不能对外开放 8081 / 33346。
|
所有端口只绑本机。iXserver 的旧接口和 WebSocket **没有鉴权**,绝不能对外开放 8081 / 33346。
|
||||||
|
|
||||||
|
|
@ -51,12 +52,14 @@
|
||||||
.\scenarios\s8-human-chain.ps1 # SCRM 开工 → 新 IG 用户 → 人工工单推送 → 指派 / 回复 → IG 收到 → 结束 → 下班
|
.\scenarios\s8-human-chain.ps1 # SCRM 开工 → 新 IG 用户 → 人工工单推送 → 指派 / 回复 → IG 收到 → 结束 → 下班
|
||||||
```
|
```
|
||||||
|
|
||||||
|
跑的时候开着观察台 <http://127.0.0.1:5090>,每一跳和状态变化会实时画成泳道时序图;`?ticket=<工单号>` 只看一张。
|
||||||
|
|
||||||
S8 结束时会把服务号切回机器人、在线人数归零,所以 S1 / S8 先后跑互不影响;要停在人工状态看页面加 `-KeepShift`。
|
S8 结束时会把服务号切回机器人、在线人数归零,所以 S1 / S8 先后跑互不影响;要停在人工状态看页面加 `-KeepShift`。
|
||||||
|
|
||||||
从 Mac 看几个页面用 SSH 本地端口转发:
|
从 Mac 看几个页面用 SSH 本地端口转发:
|
||||||
|
|
||||||
```bash
|
```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。
|
`appsettings.Loop.json` 把所有连接串都盖成本机 iCONAbpFX。
|
||||||
- PowerShell 5.1 的两个坑已经在 `loop.ps1` 里绕开:原生命令往 stderr 写一行就抛异常(统一走 `Invoke-Native`);
|
- PowerShell 5.1 的两个坑已经在 `loop.ps1` 里绕开:原生命令往 stderr 写一行就抛异常(统一走 `Invoke-Native`);
|
||||||
`Get-Content` 默认按 GBK 读无 BOM 的 UTF-8(拷配置用 `Copy-Item`,脚本文件一律带 BOM)。
|
`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 与未提交文件。闭环结果离开「当时部署的是哪个版本」没法解读,
|
- `deployed.json` 记录每次部署时两个源码仓库的 commit 与未提交文件。闭环结果离开「当时部署的是哪个版本」没法解读,
|
||||||
跑场景的记录里引用它。注意 winbox 上 iXserver 仓库的 HEAD 不随 `sync.sh` 前进(只推文件),
|
跑场景的记录里引用它。注意 winbox 上 iXserver 仓库的 HEAD 不随 `sync.sh` 前进(只推文件),
|
||||||
`commit` 字段可能停在很久以前,以 `uncommitted` 清单和 Mac 上的提交为准。
|
`commit` 字段可能停在很久以前,以 `uncommitted` 清单和 Mac 上的提交为准。
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
|
|
@ -33,6 +33,11 @@
|
||||||
'FileFolder' = '{RunData}\API_FileFolder\'
|
'FileFolder' = '{RunData}\API_FileFolder\'
|
||||||
'DbTimeLogLocation' = '{RunData}\API_Log_DB\'
|
'DbTimeLogLocation' = '{RunData}\API_Log_DB\'
|
||||||
'AutoChatJsonSetting_JsonStoreDir' = '{RunData}\TicketJsonDir'
|
'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}/'
|
'ApiUrl' = 'http://localhost:{IxserverPort}/'
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
Chatbot = 5056
|
Chatbot = 5056
|
||||||
IgMock = 5070
|
IgMock = 5070
|
||||||
ScrmMock = 5080
|
ScrmMock = 5080
|
||||||
|
Console = 5090 # 闭环观察台:只读汇总各角色,画泳道时序图
|
||||||
}
|
}
|
||||||
|
|
||||||
# 闭环专用的假凭据:与 staging / 生产无关,可以进 git
|
# 闭环专用的假凭据:与 staging / 生产无关,可以进 git
|
||||||
|
|
@ -39,6 +40,7 @@
|
||||||
IgPageId = '568932443471849'
|
IgPageId = '568932443471849'
|
||||||
CompanyCode = 'SM_HKTB_IG'
|
CompanyCode = 'SM_HKTB_IG'
|
||||||
AdminCode = 'HKTB'
|
AdminCode = 'HKTB'
|
||||||
|
QuotaName = 'HKTB' # 额度池名,与管理员 code 同名只是碰巧
|
||||||
|
|
||||||
# 第三方客户端收到投递后自动回复:echo / 任意固定文本;留空则只能在页面上手动回
|
# 第三方客户端收到投递后自动回复:echo / 任意固定文本;留空则只能在页面上手动回
|
||||||
ChatbotAutoReply = 'echo'
|
ChatbotAutoReply = 'echo'
|
||||||
|
|
|
||||||
25
loop.ps1
25
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 } }
|
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 }
|
$Roles = if ($Only) { $AllRoles | Where-Object { $Only -contains $_ } } else { $AllRoles }
|
||||||
|
|
||||||
# 健康检查要连本机 ABP 的 dev 证书;只影响本 PowerShell 进程
|
# 健康检查要连本机 ABP 的 dev 证书;只影响本 PowerShell 进程
|
||||||
|
|
@ -70,6 +70,7 @@ function Write-Warn2([string]$text) { Write-Host " $text" -ForegroundColor Ye
|
||||||
function Expand-Template([string]$text) {
|
function Expand-Template([string]$text) {
|
||||||
$map = @{
|
$map = @{
|
||||||
'{RunData}' = $Dirs.Data
|
'{RunData}' = $Dirs.Data
|
||||||
|
'{LoopRoot}' = $Root
|
||||||
'{IxserverPort}' = $Cfg.Ports.Ixserver
|
'{IxserverPort}' = $Cfg.Ports.Ixserver
|
||||||
'{WebSocketPort}' = $Cfg.Ports.WebSocket
|
'{WebSocketPort}' = $Cfg.Ports.WebSocket
|
||||||
'{AbpPort}' = $Cfg.Ports.Abp
|
'{AbpPort}' = $Cfg.Ports.Abp
|
||||||
|
|
@ -230,11 +231,27 @@ function Get-RoleSpec([string]$role) {
|
||||||
}
|
}
|
||||||
'scrm' {
|
'scrm' {
|
||||||
$script = "$tools\ScrmMockServer\server.js"
|
$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)" +
|
$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 }
|
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)
|
$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
|
$d = Join-Path $Dirs.Data $sub
|
||||||
if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d | Out-Null }
|
if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d | Out-Null }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ function Fail([string]$why) {
|
||||||
Write-Host '--- IG 模拟记录'
|
Write-Host '--- IG 模拟记录'
|
||||||
(Invoke-RestMethod "$ig/_api/state").events | ForEach-Object { $_ | ConvertTo-Json -Depth 8 -Compress } | Out-Host
|
(Invoke-RestMethod "$ig/_api/state").events | ForEach-Object { $_ | ConvertTo-Json -Depth 8 -Compress } | Out-Host
|
||||||
Write-Host '--- chatbot 记录(最近 6 条)'
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,7 +37,12 @@ if (-not $hit) { Fail "$TimeoutSec 秒内 IG 用户没有收到「$expected」"
|
||||||
if (@($hit.issues).Count) { Fail "出站请求有问题:$($hit.issues -join ';')" }
|
if (@($hit.issues).Count) { Fail "出站请求有问题:$($hit.issues -join ';')" }
|
||||||
if (-not $hit.tokenPresent) { Fail '出站请求没带 access_token' }
|
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 (-not $reply) { Fail 'chatbot 侧没有对应的回复记录' }
|
||||||
if ($reply.response.status -ne 201) { Fail "send-txt 返回 $($reply.response.status)" }
|
if ($reply.response.status -ne 201) { Fail "send-txt 返回 $($reply.response.status)" }
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue