落地报价模板主账与后台:合同映射、灌种 SQL/脚本、智能体填表,并补测服运维探针与合同文档对齐。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+162
-59
@@ -1,15 +1,7 @@
|
||||
# 只读:拉服务器新系统 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 覆盖。
|
||||
# 只读:拉服务器新系统 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(
|
||||
@@ -26,8 +18,10 @@ $SshKey = if ($env:YTD_SSH_KEY) { $env:YTD_SSH_KEY } else { Join-Path $env:USERP
|
||||
$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'
|
||||
# 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='
|
||||
|
||||
@@ -41,30 +35,52 @@ try {
|
||||
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
} catch {
|
||||
Write-Host 'warn: 无法设置本机控制台 UTF-8'
|
||||
Write-Host 'warn: console UTF-8 setup failed'
|
||||
}
|
||||
|
||||
# 仓库根/logs(本脚本在 deploy/ 下)
|
||||
$RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$LocalLogDir = Join-Path $RepoRoot 'logs'
|
||||
# 在仓库 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)
|
||||
}
|
||||
|
||||
# 写入本地 *_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 {
|
||||
@@ -75,31 +91,29 @@ function Write-LocalLine([string]$line) {
|
||||
try {
|
||||
[System.IO.File]::AppendAllText($target, $line + [Environment]::NewLine, $Utf8NoBom)
|
||||
} catch {
|
||||
Write-Host ('warn: 写本地日志失败: ' + $_.Exception.Message)
|
||||
Write-Host ('warn: write local log failed: ' + $_.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")
|
||||
}
|
||||
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 '说明: 企微气泡里的 [test] 回复若已出现=发送成功;日志里看 reply= 字段'
|
||||
Write-LocalLine '说明: 本地日志为 UTF-8 BOM;请重新跑 Once 覆盖旧乱码内容(可先删当天 log 再跑)'
|
||||
Write-LocalLine '---'
|
||||
|
||||
$waitLiteral = if ($Once) { '$false' } else { '$true' }
|
||||
# follow=1 / once=0,避免 $true/$false 在远程被覆盖或解析歧义
|
||||
$waitFlag = if ($Once) { '0' } else { '1' }
|
||||
$allLiteral = if ($All) { '$true' } else { '$false' }
|
||||
$accessLiteral = if ($Access) { '$true' } else { '$false' }
|
||||
$encName = if ($Gbk) { 'Default' } else { 'UTF8' }
|
||||
$forceGbkLiteral = if ($Gbk) { '$true' } else { '$false' }
|
||||
|
||||
# 远程:逐行按 UTF-8 严格解码,失败再用 GBK(936);输出强制 Flush,避免持续监听被缓冲卡住
|
||||
$remoteScript = @"
|
||||
`$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
@@ -110,61 +124,151 @@ try {
|
||||
`$out = '$LogOut'
|
||||
`$err = '$LogErr'
|
||||
`$tail = $Tail
|
||||
`$wait = $waitLiteral
|
||||
`$waitFlag = '$waitFlag'
|
||||
`$wait = (`$waitFlag -eq '1')
|
||||
`$all = $allLiteral
|
||||
`$wantAccess = $accessLiteral
|
||||
`$pat = '$Pattern'
|
||||
`$enc = '$encName'
|
||||
`$forceGbk = $forceGbkLiteral
|
||||
`$utf8Strict = New-Object System.Text.UTF8Encoding `$false, `$true
|
||||
`$gbk = [System.Text.Encoding]::GetEncoding(936)
|
||||
|
||||
Write-Output '=== remote health 8910 ==='
|
||||
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
|
||||
Write-Output `$h.Content
|
||||
Emit `$h.Content
|
||||
} catch {
|
||||
Write-Output ('health_fail: ' + `$_.Exception.Message)
|
||||
Emit ('health_fail: ' + `$_.Exception.Message)
|
||||
}
|
||||
Write-Output '=== business/error log (err) ==='
|
||||
Emit '=== business/error log (err) ==='
|
||||
|
||||
function Show-Err([string]`$line) {
|
||||
if (`$all) { Write-Output ('[err] ' + `$line); return }
|
||||
if (`$line -match `$pat) { Write-Output ('[err] ' + `$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) { Write-Output ('[out] ' + `$line); return }
|
||||
if (`$line -match '/wecom/callback|inject-text') {
|
||||
Write-Output ('[out] ' + `$line)
|
||||
}
|
||||
if (`$all) { Emit ('[out] ' + `$line); return }
|
||||
if (`$line -match '/wecom/callback|inject-text') { Emit ('[out] ' + `$line) }
|
||||
}
|
||||
|
||||
if (-not (Test-Path `$err)) { Write-Output ('MISSING ' + `$err) }
|
||||
if (-not (Test-Path `$err)) { Emit ('MISSING ' + `$err) }
|
||||
|
||||
if (-not `$wait) {
|
||||
if (Test-Path `$err) {
|
||||
Get-Content -Path `$err -Tail `$tail -Encoding `$enc -ErrorAction SilentlyContinue | ForEach-Object { Show-Err `$_ }
|
||||
Get-SmartTailLines `$err `$tail | ForEach-Object { Show-Err `$_ }
|
||||
}
|
||||
if (`$wantAccess -or `$all) {
|
||||
Write-Output '=== access log (out) ==='
|
||||
Emit '=== access log (out) ==='
|
||||
if (Test-Path `$out) {
|
||||
Get-Content -Path `$out -Tail `$tail -Encoding `$enc -ErrorAction SilentlyContinue | ForEach-Object { Show-Out `$_ }
|
||||
Get-SmartTailLines `$out `$tail | 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
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
@@ -175,7 +279,6 @@ $localTemp = Join-Path $env:TEMP 'ytd-watch-agent8910-remote.ps1'
|
||||
& 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',
|
||||
|
||||
Reference in New Issue
Block a user