Files
inquiry_robot/友通达询价智能体监听包/Watch-Agent8910.ps1
T

316 lines
12 KiB
PowerShell

# 只读:拉服务器新系统 8910 的业务/错误日志,并落到本机 logs/(UTF-8 BOM,记事本可直接打开)。
# 用法:双击 Watch-Agent8910.bat / Watch-Agent8910-Once.bat
# 本机:logs/yyyyMMdd_info.log 、 logs/yyyyMMdd_error.log
# 服务器旧日志可能是 GBK、新日志是 UTF-8;远程按「逐行 UTF-8,失败则 GBK」解码,避免乱码。
[CmdletBinding()]
param(
[switch]$Once,
[switch]$All,
[switch]$Access,
[switch]$Gbk,
[switch]$NoLocalLog,
[int]$Tail = 120
)
$ErrorActionPreference = 'Stop'
$SshKey = if ($env:YTD_SSH_KEY) { $env:YTD_SSH_KEY } else { Join-Path $env:USERPROFILE '.ssh\id_ed25519_ytd' }
$SshHost = if ($env:YTD_SSH_HOST) { $env:YTD_SSH_HOST } else { 'ytd_extdev_0903@129.211.181.80' }
$LogOut = 'E:\wwwroot\logs\agent-http.out.log'
$LogErr = 'E:\wwwroot\logs\agent-http.err.log'
# once / follow 用不同远程脚本名,避免互相覆盖导致「持续监听」其实跑成 Once
$WatchTag = if ($Once) { 'once' } else { 'follow' }
$IncomingRemotePath = "C:/Users/ytd_extdev_0903/incoming/_watch_agent8910_$WatchTag.ps1"
$IncomingLocalRun = "C:\Users\ytd_extdev_0903\incoming\_watch_agent8910_$WatchTag.ps1"
$Pattern = '回调|inject-text|inject:|outbox|inbox|errcode|errmsg|60020|ERROR|WARNING|Traceback|Exception|ModuleNotFoundError|sender=|touser=|crypto|ChannelRuntime|dispatch intent|企微发送|验签|解密|reply='
if (-not (Test-Path $SshKey)) {
throw "缺少 SSH 密钥: $SshKey"
}
try {
chcp 65001 | Out-Null
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
} catch {
Write-Host 'warn: console UTF-8 setup failed'
}
$RepoRoot = Split-Path -Parent $PSScriptRoot
# 在仓库 deploy/ 下:写到仓库根 logs/;单独解压给同事:写到本工具目录 logs/
if ((Split-Path -Leaf $PSScriptRoot) -eq 'deploy') {
$LocalLogDir = Join-Path $RepoRoot 'logs'
} else {
$LocalLogDir = Join-Path $PSScriptRoot 'logs'
}
$Day = Get-Date -Format 'yyyyMMdd'
$LocalInfoLog = Join-Path $LocalLogDir ($Day + '_info.log')
$LocalErrorLog = Join-Path $LocalLogDir ($Day + '_error.log')
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
$Utf8Bom = New-Object System.Text.UTF8Encoding $true
function Ensure-Utf8BomFile([string]$path) {
if (-not (Test-Path $path) -or ((Get-Item $path).Length -eq 0)) {
[System.IO.File]::WriteAllText($path, '', $Utf8Bom)
return
}
$fs = [System.IO.File]::OpenRead($path)
try {
$b0 = $fs.ReadByte(); $b1 = $fs.ReadByte(); $b2 = $fs.ReadByte()
} finally { $fs.Close() }
if ($b0 -eq 0xEF -and $b1 -eq 0xBB -and $b2 -eq 0xBF) { return }
$raw = [System.IO.File]::ReadAllBytes($path)
$bom = [byte[]](0xEF, 0xBB, 0xBF)
$merged = New-Object byte[] ($bom.Length + $raw.Length)
[Array]::Copy($bom, 0, $merged, 0, 3)
[Array]::Copy($raw, 0, $merged, 3, $raw.Length)
[System.IO.File]::WriteAllBytes($path, $merged)
}
if (-not $NoLocalLog) {
New-Item -ItemType Directory -Force -Path $LocalLogDir | Out-Null
Ensure-Utf8BomFile $LocalInfoLog
Ensure-Utf8BomFile $LocalErrorLog
$sessionHdr = '===== watch start ' + (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + ' host=' + $SshHost + ' once=' + $Once.IsPresent + ' ====='
[System.IO.File]::AppendAllText($LocalInfoLog, $sessionHdr + [Environment]::NewLine, $Utf8NoBom)
[System.IO.File]::AppendAllText($LocalErrorLog, $sessionHdr + [Environment]::NewLine, $Utf8NoBom)
}
function Write-LocalLine([string]$line) {
Write-Host $line
if ($NoLocalLog) { return }
if ($line -match '^(目标:|主日志|访问日志|日志编码|过滤:|本机落盘:|说明:|---+|=====)') {
$isErr = $false
} else {
$isErr = ($line -cmatch '\] WARNING |\] ERROR |Traceback|Exception|ModuleNotFoundError') -or
($line -match 'errcode=\d+|errmsg=.+|health_fail|MISSING |解密失败|验签/解密失败|relation "agent_inbox"|not allow to access from your ip')
}
$target = if ($isErr) { $LocalErrorLog } else { $LocalInfoLog }
try {
[System.IO.File]::AppendAllText($target, $line + [Environment]::NewLine, $Utf8NoBom)
} catch {
Write-Host ('warn: write local log failed: ' + $_.Exception.Message)
}
}
Write-LocalLine ("目标: $SshHost")
Write-LocalLine ("主日志(业务/错误): $LogErr")
if ($Access -or $All) { Write-LocalLine ("访问日志: $LogOut") }
Write-LocalLine ("日志编码: smart UTF-8/GBK" + $(if ($Gbk) { ' (force GBK)' } else { '' }))
if ($All) { Write-LocalLine '过滤: 关闭' } else { Write-LocalLine ("过滤: $Pattern") }
if (-not $NoLocalLog) {
Write-LocalLine ("本机落盘: $LocalInfoLog")
Write-LocalLine ("本机落盘: $LocalErrorLog")
}
Write-LocalLine '说明: 本地日志为 UTF-8 BOM;请重新跑 Once 覆盖旧乱码内容(可先删当天 log 再跑)'
Write-LocalLine '---'
# follow=1 / once=0,避免 $true/$false 在远程被覆盖或解析歧义
$waitFlag = if ($Once) { '0' } else { '1' }
$allLiteral = if ($All) { '$true' } else { '$false' }
$accessLiteral = if ($Access) { '$true' } else { '$false' }
$forceGbkLiteral = if ($Gbk) { '$true' } else { '$false' }
# 远程:逐行按 UTF-8 严格解码,失败再用 GBK(936);输出强制 Flush,避免持续监听被缓冲卡住
$remoteScript = @"
`$ErrorActionPreference = 'Continue'
try {
chcp 65001 | Out-Null
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(`$false)
`$OutputEncoding = [System.Text.UTF8Encoding]::new(`$false)
} catch {}
`$out = '$LogOut'
`$err = '$LogErr'
`$tail = $Tail
`$waitFlag = '$waitFlag'
`$wait = (`$waitFlag -eq '1')
`$all = $allLiteral
`$wantAccess = $accessLiteral
`$pat = '$Pattern'
`$forceGbk = $forceGbkLiteral
`$utf8Strict = New-Object System.Text.UTF8Encoding `$false, `$true
`$gbk = [System.Text.Encoding]::GetEncoding(936)
function Emit([string]`$s) {
[Console]::Out.WriteLine(`$s)
[Console]::Out.Flush()
}
function Decode-LogBytes([byte[]]`$chunk) {
if (`$null -eq `$chunk -or `$chunk.Length -eq 0) { return '' }
if (`$forceGbk) { return `$gbk.GetString(`$chunk) }
try { return `$utf8Strict.GetString(`$chunk) } catch { return `$gbk.GetString(`$chunk) }
}
function Get-SmartTailLines([string]`$path, [int]`$n) {
if (-not (Test-Path `$path)) { return @() }
`$fs = [System.IO.File]::Open(`$path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
try {
`$ms = New-Object System.IO.MemoryStream
`$fs.CopyTo(`$ms)
`$bytes = `$ms.ToArray()
} finally {
`$fs.Close()
}
`$lines = New-Object System.Collections.Generic.List[string]
`$start = 0
for (`$i = 0; `$i -lt `$bytes.Length; `$i++) {
if (`$bytes[`$i] -ne 10) { continue }
`$end = `$i
if (`$end -gt `$start -and `$bytes[`$end - 1] -eq 13) { `$end = `$end - 1 }
`$len = `$end - `$start
if (`$len -lt 0) { `$len = 0 }
`$chunk = New-Object byte[] `$len
if (`$len -gt 0) { [Array]::Copy(`$bytes, `$start, `$chunk, 0, `$len) }
`$lines.Add((Decode-LogBytes `$chunk))
`$start = `$i + 1
}
if (`$start -lt `$bytes.Length) {
`$len = `$bytes.Length - `$start
`$chunk = New-Object byte[] `$len
[Array]::Copy(`$bytes, `$start, `$chunk, 0, `$len)
`$lines.Add((Decode-LogBytes `$chunk))
}
if (`$lines.Count -le `$n) { return `$lines.ToArray() }
return `$lines.GetRange(`$lines.Count - `$n, `$n).ToArray()
}
Emit ('mode=' + `$waitFlag + ' follow=' + `$wait)
Emit '=== remote health 8910 ==='
try {
`$h = Invoke-WebRequest -Uri 'http://127.0.0.1:8910/health' -UseBasicParsing -TimeoutSec 5
Emit `$h.Content
} catch {
Emit ('health_fail: ' + `$_.Exception.Message)
}
Emit '=== business/error log (err) ==='
function Show-Err([string]`$line) {
if (`$all) { Emit ('[err] ' + `$line); return }
if (`$line -match `$pat) { Emit ('[err] ' + `$line) }
}
function Show-Out([string]`$line) {
if (-not `$wantAccess -and -not `$all) { return }
if (`$all) { Emit ('[out] ' + `$line); return }
if (`$line -match '/wecom/callback|inject-text') { Emit ('[out] ' + `$line) }
}
if (-not (Test-Path `$err)) { Emit ('MISSING ' + `$err) }
if (-not `$wait) {
if (Test-Path `$err) {
Get-SmartTailLines `$err `$tail | ForEach-Object { Show-Err `$_ }
}
if (`$wantAccess -or `$all) {
Emit '=== access log (out) ==='
if (Test-Path `$out) {
Get-SmartTailLines `$out `$tail | ForEach-Object { Show-Out `$_ }
}
}
exit 0
}
Emit 'following err.log ... (Ctrl+C to stop)'
`$lastLen = @{}
`$carry = @{}
foreach (`$f in @(`$err, `$out)) {
if (Test-Path `$f) { `$lastLen[`$f] = (Get-Item `$f).Length } else { `$lastLen[`$f] = 0 }
`$carry[`$f] = ''
}
if (Test-Path `$err) { Get-SmartTailLines `$err `$tail | ForEach-Object { Show-Err `$_ } }
# 尾部打完后,从当前文件末尾继续跟,避免重复刷旧行
foreach (`$f in @(`$err, `$out)) {
if (Test-Path `$f) { `$lastLen[`$f] = (Get-Item `$f).Length }
}
`$heartbeatAt = [DateTime]::UtcNow
while (`$true) {
Start-Sleep -Milliseconds 500
if (([DateTime]::UtcNow - `$heartbeatAt).TotalSeconds -ge 30) {
Emit ('... still watching ' + (Get-Date).ToString('HH:mm:ss'))
`$heartbeatAt = [DateTime]::UtcNow
}
`$targets = @()
if (Test-Path `$err) { `$targets += `$err }
if ((`$wantAccess -or `$all) -and (Test-Path `$out)) { `$targets += `$out }
foreach (`$f in `$targets) {
if (-not `$lastLen.ContainsKey(`$f)) { `$lastLen[`$f] = 0; `$carry[`$f] = '' }
`$len = (Get-Item `$f).Length
if (`$len -lt `$lastLen[`$f]) { `$lastLen[`$f] = 0; `$carry[`$f] = '' }
if (`$len -le `$lastLen[`$f]) { continue }
`$fs = [System.IO.File]::Open(`$f, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
try {
[void]`$fs.Seek(`$lastLen[`$f], [System.IO.SeekOrigin]::Begin)
`$buf = New-Object byte[] (`$len - `$lastLen[`$f])
`$read = `$fs.Read(`$buf, 0, `$buf.Length)
} finally { `$fs.Close() }
`$lastLen[`$f] = `$len
if (`$read -le 0) { continue }
`$chunk = New-Object byte[] `$read
[Array]::Copy(`$buf, 0, `$chunk, 0, `$read)
`$text = `$carry[`$f] + (Decode-LogBytes `$chunk)
`$parts = `$text -split "`n"
if (-not (`$text.EndsWith("`n") -or `$text.EndsWith("`r"))) {
if (`$parts.Count -ge 1) {
`$carry[`$f] = `$parts[`$parts.Count - 1].TrimEnd("`r")
`$limit = `$parts.Count - 1
} else {
`$carry[`$f] = `$text
`$limit = 0
}
} else {
`$carry[`$f] = ''
`$limit = `$parts.Count
}
for (`$pi = 0; `$pi -lt `$limit; `$pi++) {
`$line = `$parts[`$pi].TrimEnd("`r")
if (-not `$line) { continue }
if (`$f -eq `$out) { Show-Out `$line } else { Show-Err `$line }
}
}
}
"@
$localTemp = Join-Path $env:TEMP 'ytd-watch-agent8910-remote.ps1'
[System.IO.File]::WriteAllText($localTemp, $remoteScript, $Utf8NoBom)
& scp.exe -o BatchMode=yes -o ConnectTimeout=12 -i $SshKey $localTemp "${SshHost}:${IncomingRemotePath}"
if ($LASTEXITCODE -ne 0) { throw "scp failed: $LASTEXITCODE" }
$sshArgs = @(
'-o', 'BatchMode=yes',
'-o', 'ConnectTimeout=12',
'-i', $SshKey,
$SshHost,
"powershell -NoProfile -ExecutionPolicy Bypass -File `"$IncomingLocalRun`""
)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = 'ssh.exe'
$psi.Arguments = ($sshArgs | ForEach-Object {
if ($_ -match '\s') { '"' + $_ + '"' } else { $_ }
}) -join ' '
$psi.UseShellExecute = $false
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.CreateNoWindow = $true
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
$proc = New-Object System.Diagnostics.Process
$proc.StartInfo = $psi
[void]$proc.Start()
while (-not $proc.StandardOutput.EndOfStream) {
$line = $proc.StandardOutput.ReadLine()
if ($null -ne $line) { Write-LocalLine $line }
}
$errTail = $proc.StandardError.ReadToEnd()
if ($errTail) {
foreach ($eline in ($errTail -split "`r?`n")) {
if ($eline) { Write-LocalLine $eline }
}
}
$proc.WaitForExit()
exit $proc.ExitCode