@@ -1,58 +1,120 @@
# 只读:拉服务器新系统 8910 智能体日志中的企微相关行与错误行 。
# 用法(仓库根或任意目录) :
# .\deploy\Watch-Agent8910.ps1 # 持续监听(Ctrl+C 结束)
# .\deploy\Watch-Agent8910.ps1 -Once # 只看最近一段
# .\deploy\ Watch-Agent8910.ps1 -All # 不过滤关键字,整行刷出
# 只读:拉服务器新系统 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
#
# 说明 :
# - 盯的是 E:\wwwroot\logs\agent-http.*.log(新系统 8910),不碰现网 8810/8811。
# - 切流前公网 /wecom/callback 仍进现网;本脚本主要看到 inject、出站、本机调 8910 的错误。
# - 依赖本机 SSH 密钥 ~/.ssh/id_ed25519_ytd。
# 本机落盘(仓库根 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 ,
[ int ] $Tail = 80
[ switch ] $Access ,
[ switch ] $Gbk ,
[ switch ] $NoLocalLog ,
[ int ] $Tail = 120
)
$ErrorActionPreference = 'Stop'
$SshKey = Join-Path $env:USERPROFILE '.ssh\id_ed25519_ytd'
$SshHost = 'ytd_extdev_0903@129.211.181.80'
$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'
# 企微回调 / 出站 / 注入 / 失败;故意不含纯 /health 探活行,避免刷屏
$Pattern = '/wecom/callback|inject-text|inject:|outbox|inbox|errcode|errmsg|60020|ERROR|WARNING|Traceback|Exception|ModuleNotFoundError|sender=|touser=|crypto|ChannelRuntime|dispatch intent'
$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 "
}
Write-Host " 目标: $SshHost "
Write-Host " 日志: $LogOut "
Write-Host " $LogErr "
if ( $All ) {
Write-Host '过滤: 关闭(全部输出)'
} else {
Write-Host " 过滤: $Pattern "
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'
}
Write-Host '---'
# 仓库根/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' }
# 远程执行体:先 health,再 Tail;持续模式用 -Wait
$remoteScript = @"
` $ErrorActionPreference = ' C o n t i n u e '
t r y {
c h c p 6 5 0 0 1 | O u t - N u l l
[ C o n s o l e ] : : O u t p u t E n c o d i n g = [ S y s t e m . T e x t . U T F 8 E n c o d i n g ] : : n e w ( ` $false )
` $OutputEncoding = [ S y s t e m . T e x t . U T F 8 E n c o d i n g ] : : n e w ( ` $false )
} c a t c h { }
` $out = ' $LogOut '
` $err = ' $LogErr '
` $tail = $Tail
` $wait = $waitLiteral
` $all = $allLiteral
` $wantAccess = $accessLiteral
` $pat = ' $Pattern '
` $enc = ' $encName '
W r i t e - O u t p u t ' = = = r e m o t e h e a l t h 8 9 1 0 = = = '
t r y {
@@ -61,41 +123,90 @@ try {
} c a t c h {
W r i t e - O u t p u t ( ' h e a l t h _ f a i l : ' + ` $_ . E x c e p t i o n . M e s s a g e )
}
W r i t e - O u t p u t ' = = = l o g t a i l = = = '
W r i t e - O u t p u t ' = = = b u s i n e s s / e r r o r l o g ( e r r ) = = = '
` $files = @ ( )
i f ( T e s t - P a t h ` $out ) { ` $files + = ` $out } e l s e { W r i t e - O u t p u t ( ' M I S S I N G ' + ` $out ) }
i f ( T e s t - P a t h ` $err ) { ` $files + = ` $err } e l s e { W r i t e - O u t p u t ( ' M I S S I N G ' + ` $err ) }
i f ( ` $files . C o u n t - e q 0 ) { t h r o w ' n o a g e n t - h t t p l o g f i l e s ' }
f u n c t i o n S h o w - E r r ( [ s t r i n g ] ` $line ) {
i f ( ` $all ) { W r i t e - O u t p u t ( ' [ e r r ] ' + ` $line ) ; r e t u r n }
i f ( ` $line - m a t c h ` $pat ) { W r i t e - O u t p u t ( ' [ e r r ] ' + ` $line ) }
}
f u n c t i o n S h o w - L i n e ( [ s t r i n g ] ` $line ) {
i f ( ` $all ) {
W r i t e - O u t p u t ` $line
r e t u r n
}
i f ( ` $line - m a t c h ` $pat ) {
W r i t e - O u t p u t ` $line
f u n c t i o n S h o w - O u t ( [ s t r i n g ] ` $line ) {
i f ( - n o t ` $wantAccess - a n d - n o t ` $all ) { r e t u r n }
i f ( ` $all ) { W r i t e - O u t p u t ( ' [ o u t ] ' + ` $line ) ; r e t u r n }
i f ( ` $line - m a t c h ' / w e c o m / c a l l b a c k | i n j e c t - t e x t ' ) {
W r i t e - O u t p u t ( ' [ o u t ] ' + ` $line )
}
}
i f ( - n o t ( T e s t - P a t h ` $err ) ) { W r i t e - O u t p u t ( ' M I S S I N G ' + ` $err ) }
i f ( - n o t ` $wait ) {
f o r e a c h ( ` $f i n ` $files ) {
W r i t e - O u t p u t ( ' - - - ' + ` $f + ' - - - ' )
G e t - C o n t e n t - P a t h ` $f - T a i l ` $tail - E r r o r A c t i o n S i l e n t l y C o n t i n u e | F o r E a c h - O b j e c t { S h o w - L i n e ` $_ }
i f ( T e s t - P a t h ` $err ) {
G e t - C o n t e n t - P a t h ` $err - T a i l ` $tail - E n c o d i n g ` $enc - E r r o r A c t i o n S i l e n t l y C o n t i n u e | F o r E a c h - O b j e c t { S h o w - E r r ` $_ }
}
i f ( ` $wantAccess - o r ` $all ) {
W r i t e - O u t p u t ' = = = a c c e s s l o g ( o u t ) = = = '
i f ( T e s t - P a t h ` $out ) {
G e t - C o n t e n t - P a t h ` $out - T a i l ` $tail - E n c o d i n g ` $enc - E r r o r A c t i o n S i l e n t l y C o n t i n u e | F o r E a c h - O b j e c t { S h o w - O u t ` $_ }
}
}
e x i t 0
}
W r i t e - O u t p u t ' f o l l o w i n g . . . ( C t r l + C t o s t o p ) '
G e t - C o n t e n t - P a t h ` $files - T a i l ` $tail - W a i t - E r r o r A c t i o n S i l e n t l y C o n t i n u e | F o r E a c h - O b j e c t { S h o w - L i n e ` $_ }
W r i t e - O u t p u t ' f o l l o w i n g e r r . l o g . . . ( C t r l + C t o s t o p ) '
` $watch = @ ( )
i f ( T e s t - P a t h ` $err ) { ` $watch + = ` $err }
i f ( ( ` $wantAccess - o r ` $all ) - a n d ( T e s t - P a t h ` $out ) ) { ` $watch + = ` $out }
i f ( ` $watch . C o u n t - e q 0 ) { t h r o w ' n o l o g f i l e s ' }
G e t - C o n t e n t - P a t h ` $watch - T a i l ` $tail - W a i t - E n c o d i n g ` $enc - E r r o r A c t i o n S i l e n t l y C o n t i n u e | F o r E a c h - O b j e c t {
` $line = ` $_
i f ( ` $line - m a t c h ' ^ \ s * I N F O : \ s + \ S + \ s + - \ s + " ( G E T | P O S T | P U T | D E L E T E ) ' ) {
S h o w - O u t ` $line
} e l s e {
S h o w - E r r ` $line
}
}
"@
$localTemp = Join-Path $env:TEMP 'ytd-watch-agent8910-remote.ps1'
Set-Content -Path $localTemp -Value $remoteScript -Encoding UTF8
[ 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 " }
& ssh . exe -o BatchMode = yes -o ConnectTimeout = 12 -i $SshKey $SshHost `
# 用 Process + UTF-8 读 SSH 输出,避免管道把中文解错再写入本地日志
$sshArgs = @ (
'-o' , 'BatchMode=yes' ,
'-o' , 'ConnectTimeout=12' ,
'-i' , $SshKey ,
$SshHost ,
" powershell -NoProfile -ExecutionPolicy Bypass -File `" $IncomingLocalRun `" "
exit $LASTEXITCODE
)
$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