@@ -0,0 +1,315 @@
# 只读:拉服务器新系统 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 = ' 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
` $waitFlag = ' $waitFlag '
` $wait = ( ` $waitFlag - e q ' 1 ' )
` $all = $allLiteral
` $wantAccess = $accessLiteral
` $pat = ' $Pattern '
` $forceGbk = $forceGbkLiteral
` $utf8Strict = N e w - O b j e c t S y s t e m . T e x t . U T F 8 E n c o d i n g ` $false , ` $true
` $gbk = [ S y s t e m . T e x t . E n c o d i n g ] : : G e t E n c o d i n g ( 9 3 6 )
f u n c t i o n E m i t ( [ s t r i n g ] ` $s ) {
[ C o n s o l e ] : : O u t . W r i t e L i n e ( ` $s )
[ C o n s o l e ] : : O u t . F l u s h ( )
}
f u n c t i o n D e c o d e - L o g B y t e s ( [ b y t e [ ] ] ` $chunk ) {
i f ( ` $null - e q ` $chunk - o r ` $chunk . L e n g t h - e q 0 ) { r e t u r n ' ' }
i f ( ` $forceGbk ) { r e t u r n ` $gbk . G e t S t r i n g ( ` $chunk ) }
t r y { r e t u r n ` $utf8Strict . G e t S t r i n g ( ` $chunk ) } c a t c h { r e t u r n ` $gbk . G e t S t r i n g ( ` $chunk ) }
}
f u n c t i o n G e t - S m a r t T a i l L i n e s ( [ s t r i n g ] ` $path , [ i n t ] ` $n ) {
i f ( - n o t ( T e s t - P a t h ` $path ) ) { r e t u r n @ ( ) }
` $fs = [ S y s t e m . I O . F i l e ] : : O p e n ( ` $path , [ S y s t e m . I O . F i l e M o d e ] : : O p e n , [ S y s t e m . I O . F i l e A c c e s s ] : : R e a d , [ S y s t e m . I O . F i l e S h a r e ] : : R e a d W r i t e )
t r y {
` $ms = N e w - O b j e c t S y s t e m . I O . M e m o r y S t r e a m
` $fs . C o p y T o ( ` $ms )
` $bytes = ` $ms . T o A r r a y ( )
} f i n a l l y {
` $fs . C l o s e ( )
}
` $lines = N e w - O b j e c t S y s t e m . C o l l e c t i o n s . G e n e r i c . L i s t [ s t r i n g ]
` $start = 0
f o r ( ` $i = 0 ; ` $i - l t ` $bytes . L e n g t h ; ` $i + + ) {
i f ( ` $bytes [ ` $i ] - n e 1 0 ) { c o n t i n u e }
` $end = ` $i
i f ( ` $end - g t ` $start - a n d ` $bytes [ ` $end - 1 ] - e q 1 3 ) { ` $end = ` $end - 1 }
` $len = ` $end - ` $start
i f ( ` $len - l t 0 ) { ` $len = 0 }
` $chunk = N e w - O b j e c t b y t e [ ] ` $len
i f ( ` $len - g t 0 ) { [ A r r a y ] : : C o p y ( ` $bytes , ` $start , ` $chunk , 0 , ` $len ) }
` $lines . A d d ( ( D e c o d e - L o g B y t e s ` $chunk ) )
` $start = ` $i + 1
}
i f ( ` $start - l t ` $bytes . L e n g t h ) {
` $len = ` $bytes . L e n g t h - ` $start
` $chunk = N e w - O b j e c t b y t e [ ] ` $len
[ A r r a y ] : : C o p y ( ` $bytes , ` $start , ` $chunk , 0 , ` $len )
` $lines . A d d ( ( D e c o d e - L o g B y t e s ` $chunk ) )
}
i f ( ` $lines . C o u n t - l e ` $n ) { r e t u r n ` $lines . T o A r r a y ( ) }
r e t u r n ` $lines . G e t R a n g e ( ` $lines . C o u n t - ` $n , ` $n ) . T o A r r a y ( )
}
E m i t ( ' m o d e = ' + ` $waitFlag + ' f o l l o w = ' + ` $wait )
E m i t ' = = = r e m o t e h e a l t h 8 9 1 0 = = = '
t r y {
` $h = I n v o k e - W e b R e q u e s t - U r i ' h t t p : / / 1 2 7 . 0 . 0 . 1 : 8 9 1 0 / h e a l t h ' - U s e B a s i c P a r s i n g - T i m e o u t S e c 5
E m i t ` $h . C o n t e n t
} c a t c h {
E m i 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 )
}
E m i t ' = = = b u s i n e s s / e r r o r l o g ( e r r ) = = = '
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 ) { E m i t ( ' [ e r r ] ' + ` $line ) ; r e t u r n }
i f ( ` $line - m a t c h ` $pat ) { E m i t ( ' [ e r r ] ' + ` $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 ) { E m i 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 ' ) { E m i t ( ' [ o u t ] ' + ` $line ) }
}
i f ( - n o t ( T e s t - P a t h ` $err ) ) { E m i t ( ' M I S S I N G ' + ` $err ) }
i f ( - n o t ` $wait ) {
i f ( T e s t - P a t h ` $err ) {
G e t - S m a r t T a i l L i n e s ` $err ` $tail | 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 ) {
E m i 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 - S m a r t T a i l L i n e s ` $out ` $tail | 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
}
E m i 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 ) '
` $lastLen = @ { }
` $carry = @ { }
f o r e a c h ( ` $f i n @ ( ` $err , ` $out ) ) {
i f ( T e s t - P a t h ` $f ) { ` $lastLen [ ` $f ] = ( G e t - I t e m ` $f ) . L e n g t h } e l s e { ` $lastLen [ ` $f ] = 0 }
` $carry [ ` $f ] = ' '
}
i f ( T e s t - P a t h ` $err ) { G e t - S m a r t T a i l L i n e s ` $err ` $tail | F o r E a c h - O b j e c t { S h o w - E r r ` $_ } }
# 尾 部 打 完 后 , 从 当 前 文 件 末 尾 继 续 跟 , 避 免 重 复 刷 旧 行
f o r e a c h ( ` $f i n @ ( ` $err , ` $out ) ) {
i f ( T e s t - P a t h ` $f ) { ` $lastLen [ ` $f ] = ( G e t - I t e m ` $f ) . L e n g t h }
}
` $heartbeatAt = [ D a t e T i m e ] : : U t c N o w
w h i l e ( ` $true ) {
S t a r t - S l e e p - M i l l i s e c o n d s 5 0 0
i f ( ( [ D a t e T i m e ] : : U t c N o w - ` $heartbeatAt ) . T o t a l S e c o n d s - g e 3 0 ) {
E m i t ( ' . . . s t i l l w a t c h i n g ' + ( G e t - D a t e ) . T o S t r i n g ( ' H H : m m : s s ' ) )
` $heartbeatAt = [ D a t e T i m e ] : : U t c N o w
}
` $targets = @ ( )
i f ( T e s t - P a t h ` $err ) { ` $targets + = ` $err }
i f ( ( ` $wantAccess - o r ` $all ) - a n d ( T e s t - P a t h ` $out ) ) { ` $targets + = ` $out }
f o r e a c h ( ` $f i n ` $targets ) {
i f ( - n o t ` $lastLen . C o n t a i n s K e y ( ` $f ) ) { ` $lastLen [ ` $f ] = 0 ; ` $carry [ ` $f ] = ' ' }
` $len = ( G e t - I t e m ` $f ) . L e n g t h
i f ( ` $len - l t ` $lastLen [ ` $f ] ) { ` $lastLen [ ` $f ] = 0 ; ` $carry [ ` $f ] = ' ' }
i f ( ` $len - l e ` $lastLen [ ` $f ] ) { c o n t i n u e }
` $fs = [ S y s t e m . I O . F i l e ] : : O p e n ( ` $f , [ S y s t e m . I O . F i l e M o d e ] : : O p e n , [ S y s t e m . I O . F i l e A c c e s s ] : : R e a d , [ S y s t e m . I O . F i l e S h a r e ] : : R e a d W r i t e )
t r y {
[ v o i d ] ` $fs . S e e k ( ` $lastLen [ ` $f ] , [ S y s t e m . I O . S e e k O r i g i n ] : : B e g i n )
` $buf = N e w - O b j e c t b y t e [ ] ( ` $len - ` $lastLen [ ` $f ] )
` $read = ` $fs . R e a d ( ` $buf , 0 , ` $buf . L e n g t h )
} f i n a l l y { ` $fs . C l o s e ( ) }
` $lastLen [ ` $f ] = ` $len
i f ( ` $read - l e 0 ) { c o n t i n u e }
` $chunk = N e w - O b j e c t b y t e [ ] ` $read
[ A r r a y ] : : C o p y ( ` $buf , 0 , ` $chunk , 0 , ` $read )
` $text = ` $carry [ ` $f ] + ( D e c o d e - L o g B y t e s ` $chunk )
` $parts = ` $text - s p l i t " ` n "
i f ( - n o t ( ` $text . E n d s W i t h ( " ` n " ) - o r ` $text . E n d s W i t h ( " ` r " ) ) ) {
i f ( ` $parts . C o u n t - g e 1 ) {
` $carry [ ` $f ] = ` $parts [ ` $parts . C o u n t - 1 ] . T r i m E n d ( " ` r " )
` $limit = ` $parts . C o u n t - 1
} e l s e {
` $carry [ ` $f ] = ` $text
` $limit = 0
}
} e l s e {
` $carry [ ` $f ] = ' '
` $limit = ` $parts . C o u n t
}
f o r ( ` $pi = 0 ; ` $pi - l t ` $limit ; ` $pi + + ) {
` $line = ` $parts [ ` $pi ] . T r i m E n d ( " ` r " )
i f ( - n o t ` $line ) { c o n t i n u e }
i f ( ` $f - e q ` $out ) { 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'
[ 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