# 只读:拉服务器新系统 8910 的「业务/错误」日志,并同时落到本机 logs/。 # 用法: # powershell -NoProfile -ExecutionPolicy Bypass -File .\deploy\Watch-Agent8910.ps1 # powershell -NoProfile -ExecutionPolicy Bypass -File .\deploy\Watch-Agent8910.ps1 -Once # 双击 Watch-Agent8910.bat / Watch-Agent8910-Once.bat # # 本机落盘(仓库根 logs/,按当天日期): # logs/20260913_info.log — 业务信息(入队、reply=、发送成功等) # logs/20260913_error.log — 告警/异常(WARNING、ERROR、Traceback、errcode 等) # # 乱码:默认 UTF-8;仍乱码可加 -Gbk。 # SSH:默认密钥 ~/.ssh/id_ed25519_ytd;可用 YTD_SSH_KEY / YTD_SSH_HOST 覆盖。 [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' $IncomingRemotePath = 'C:/Users/ytd_extdev_0903/incoming/_watch_agent8910.ps1' $IncomingLocalRun = 'C:\Users\ytd_extdev_0903\incoming\_watch_agent8910.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: 无法设置本机控制台 UTF-8' } # 仓库根/logs(本脚本在 deploy/ 下) $RepoRoot = Split-Path -Parent $PSScriptRoot $LocalLogDir = Join-Path $RepoRoot '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 if (-not $NoLocalLog) { New-Item -ItemType Directory -Force -Path $LocalLogDir | Out-Null $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) } # 写入本地 *_error.log 的判定(其余进 *_info.log) # 注意:不能用裸 ERROR(-match 不区分大小写,会误伤 “error.log” 说明行) function Write-LocalLine([string]$line) { Write-Host $line if ($NoLocalLog) { return } # 脚本自身说明行一律进 info,避免关键字误伤 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: 写本地日志失败: ' + $_.Exception.Message) } } Write-LocalLine ("目标: $SshHost") Write-LocalLine ("主日志(业务/错误): $LogErr") if ($Access -or $All) { Write-LocalLine ("访问日志: $LogOut") } Write-LocalLine ("日志编码: " + $(if ($Gbk) { 'Default/GBK' } else { 'UTF-8' })) if ($All) { Write-LocalLine '过滤: 关闭' } else { Write-LocalLine ("过滤: $Pattern") } if (-not $NoLocalLog) { Write-LocalLine ("本机落盘: $LocalInfoLog") Write-LocalLine ("本机落盘: $LocalErrorLog") } Write-LocalLine '说明: 企微气泡里的 [test] 回复若已出现=发送成功;日志里看 reply= 字段' Write-LocalLine '---' $waitLiteral = if ($Once) { '$false' } else { '$true' } $allLiteral = if ($All) { '$true' } else { '$false' } $accessLiteral = if ($Access) { '$true' } else { '$false' } $encName = if ($Gbk) { 'Default' } else { 'UTF8' } $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 `$wait = $waitLiteral `$all = $allLiteral `$wantAccess = $accessLiteral `$pat = '$Pattern' `$enc = '$encName' Write-Output '=== remote health 8910 ===' try { `$h = Invoke-WebRequest -Uri 'http://127.0.0.1:8910/health' -UseBasicParsing -TimeoutSec 5 Write-Output `$h.Content } catch { Write-Output ('health_fail: ' + `$_.Exception.Message) } Write-Output '=== business/error log (err) ===' function Show-Err([string]`$line) { if (`$all) { Write-Output ('[err] ' + `$line); return } if (`$line -match `$pat) { Write-Output ('[err] ' + `$line) } } function Show-Out([string]`$line) { if (-not `$wantAccess -and -not `$all) { return } if (`$all) { Write-Output ('[out] ' + `$line); return } if (`$line -match '/wecom/callback|inject-text') { Write-Output ('[out] ' + `$line) } } if (-not (Test-Path `$err)) { Write-Output ('MISSING ' + `$err) } if (-not `$wait) { if (Test-Path `$err) { Get-Content -Path `$err -Tail `$tail -Encoding `$enc -ErrorAction SilentlyContinue | ForEach-Object { Show-Err `$_ } } if (`$wantAccess -or `$all) { Write-Output '=== access log (out) ===' if (Test-Path `$out) { Get-Content -Path `$out -Tail `$tail -Encoding `$enc -ErrorAction SilentlyContinue | ForEach-Object { Show-Out `$_ } } } exit 0 } Write-Output 'following err.log ... (Ctrl+C to stop)' `$watch = @() if (Test-Path `$err) { `$watch += `$err } if ((`$wantAccess -or `$all) -and (Test-Path `$out)) { `$watch += `$out } if (`$watch.Count -eq 0) { throw 'no log files' } Get-Content -Path `$watch -Tail `$tail -Wait -Encoding `$enc -ErrorAction SilentlyContinue | ForEach-Object { `$line = `$_ if (`$line -match '^\s*INFO:\s+\S+\s+-\s+"(GET|POST|PUT|DELETE)') { 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" } # 用 Process + UTF-8 读 SSH 输出,避免管道把中文解错再写入本地日志 $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