# ===================================================================== # SCRM 医助 watchdog(PowerShell 单文件,Windows 自带运行时,零依赖) # # 逻辑:每 N 秒查本机 hook 是否在线(复用 miniapp 的 check_online); # 连续 FailThreshold 次不在线 → kill hf-scrm + WXWork → 重跑 hf-scrm # (hf-scrm 自注入 hook + 拉起企微 + 自动登录)。 # 掉线「告警通知」不在本脚本管,仍由 miniapp 原系统按原方式推送到群。 # # 安全:不监听任何端口(纯出站,零端口冲突);检测+重启全本地,不碰任何后端; # DryRun 默认开,连续判死 + 超时 + 每小时上限 + 启动宽限 多重护栏。 # # 部署:必须跑在「登录用户的交互会话」里(GUI + hook 注入需要桌面), # 用 Task Scheduler 的 ONLOGON + Interactive 注册(见 README 安装命令)。 # 提权:注入企微需管理员权限,脚本会自动以管理员重启自己(计划任务已 Highest 提权则直接跑)。 # ===================================================================== # ===== 自提权(注入企微必须管理员;非管理员则自动以管理员重启自己)===== $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) if (-not $isAdmin) { Write-Host "当前非管理员,正在以管理员身份重新启动 ..." Start-Process powershell.exe -Verb RunAs -ArgumentList "-ExecutionPolicy Bypass -File `"$PSCommandPath`"" exit } # ===== 控制台/日志编码统一 UTF-8(防中文乱码)===== try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} # ===== 配置(按这台机器改)===================================== $MachineId = "小慧医助" # 机器代号,仅日志用 $HookCheckUrl = "http://127.0.0.1:6666/api" # 本机 hook 健康检查地址 $KillProcesses = @("hf-scrm", "WXWork") # 要 kill 的进程名(不带 .exe) $StartCommand = "D:\win-unpacked_prod_2.1.16\win-unpacked\hf-scrm.exe" # 小慧实测路径 $StartWorkDir = "D:\win-unpacked_prod_2.1.16\win-unpacked" # hf-scrm 所在目录 $CheckIntervalSec = 5 # 检测间隔 $CheckTimeoutSec = 8 # 单次检测超时(避免卡死阻塞) $FailThreshold = 3 # 连续几次不在线才判定挂掉 $BootGraceSec = 90 # 重启后等待启动的宽限 $MaxRestartsPerHour = 5 # 每小时最多重启次数(防风暴) $DryRun = $false # 已确认无误,真跑 $LogFile = Join-Path $PSScriptRoot "watchdog.log" # 日志放在脚本同目录 # 本脚本只负责自动重启,不发任何群消息;掉线告警仍由 miniapp 原系统负责。 # ============================================================== $ErrorActionPreference = "Continue" $logDir = Split-Path $LogFile if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Force -Path $logDir | Out-Null } function Write-Log($msg) { $line = "[{0}] {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $msg Write-Host $line try { Add-Content -Path $LogFile -Value $line -Encoding UTF8 } catch {} } # ===== 结构化事件日志(方案B:本地兜底 JSONL + 纯出站推送中央面板)===== $EventsFile = Join-Path $PSScriptRoot "watchdog_events.jsonl" # 本地兜底,断网也不丢 $CentralEventUrl = "http://124.71.148.24:5052/api/event" # 面板 /api/event 地址,如 http://:5052/api/event;留空=只写本地不推送 $CentralEventToken = "06taXkCC2kEl3jDtuSTFoB235b82WGeU2_x314_PAls" # 与面板 EVENT_INGEST_TOKEN 一致的共享密钥 function Write-Event($eventType, $consecutiveFails, $byWatchdog) { # 记录一条「掉线/自启」事件;本地写 + 推送都 try 包住,绝不中断主循环。 $ev = [ordered]@{ machine = $MachineId ts = (Get-Date -Format "yyyy-MM-dd HH:mm:ss") event = $eventType consecutive_fails = $consecutiveFails by_watchdog = [bool]$byWatchdog dry_run = [bool]$DryRun } $json = $ev | ConvertTo-Json -Compress try { Add-Content -Path $EventsFile -Value $json -Encoding UTF8 } catch {} if ($CentralEventUrl) { try { Invoke-RestMethod -Uri $CentralEventUrl -Method Post ` -ContentType "application/json; charset=utf-8" ` -Headers @{ "X-WD-Token" = $CentralEventToken } ` -Body ([System.Text.Encoding]::UTF8.GetBytes($json)) ` -TimeoutSec 5 | Out-Null } catch { Write-Log " [event] central push failed: $($_.Exception.Message)" } } } function Test-Online { # 复刻 miniapp check_online:POST {type:1000} 看 data.status 是否为 1 try { $r = Invoke-RestMethod -Uri $HookCheckUrl -Method Post -ContentType "application/json" -Body '{"type":1000}' -TimeoutSec $CheckTimeoutSec return ($r.data.status -eq 1) } catch { return $false } } function Invoke-Restart { foreach ($p in $KillProcesses) { Stop-Process -Name $p -Force -ErrorAction SilentlyContinue Write-Log " killed $p" } Start-Sleep -Seconds 3 Start-Process -FilePath $StartCommand -WorkingDirectory $StartWorkDir Write-Log " started $StartCommand" } # ===== 主循环 ===== $restartTimes = New-Object System.Collections.ArrayList $failCount = 0 Write-Log "watchdog start | machine=$MachineId | DryRun=$DryRun | check=$HookCheckUrl" while ($true) { if (Test-Online) { if ($failCount -gt 0) { Write-Log "back online, reset fail counter" Write-Event "recovered" $failCount $false } $failCount = 0 } else { $failCount++ Write-Log "offline detected ($failCount/$FailThreshold)" if ($failCount -eq 1) { Write-Event "offline_detected" $failCount $false } if ($failCount -ge $FailThreshold) { $cutoff = (Get-Date).AddHours(-1) $recent = @($restartTimes | Where-Object { $_ -gt $cutoff }) if ($recent.Count -ge $MaxRestartsPerHour) { Write-Log "hit hourly restart cap ($MaxRestartsPerHour), skip restart (likely cannot recover, need human; miniapp still alerts)" Write-Event "restart_skipped_cap" $failCount $false $failCount = 0 Start-Sleep -Seconds $BootGraceSec } else { Write-Log "judged down -> restart" if ($DryRun) { Write-Log "[DRY-RUN] would kill [$($KillProcesses -join ', ')] and relaunch $StartCommand (not executed)" } else { Invoke-Restart } Write-Event "restart_done" $failCount (-not $DryRun) [void]$restartTimes.Add((Get-Date)) $failCount = 0 Write-Log "boot grace $BootGraceSec s ..." Start-Sleep -Seconds $BootGraceSec } } } Start-Sleep -Seconds $CheckIntervalSec }