From 4bd23559916e7362bd2bb10f6b5e152ee3ec7da0 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 28 Sep 2026 08:45:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(watch):=20=E7=AB=99=E7=82=B9=E6=BC=82?= =?UTF-8?q?=E7=A7=BB=E5=B7=A1=E6=A3=80=EF=BC=88=E5=8F=AA=E8=AF=BB=EF=BC=8C?= =?UTF-8?q?=E6=9C=89=E5=B7=AE=E5=BC=82=E6=89=8D=E6=8F=90=E9=86=92=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `site-drift-watch.sh` 巡检 192.168.2.106 上的两个站,**只读不动**: 不构建、不上传、不碰线上任何文件。档位是「有差异就提醒」而不是 「自动推」—— 文档站发错了是公开可见的,宁可等人点一下。 判三类信号: 1. 线上漂移:.106 上的站 ≠ 本机产物/site 源(逐字节 md5 清单比对) 2. 源码漂移:git HEAD 比产物新 ⇒ 提交了但没重新构建部署 3. 探活失败:.106 或 NapCat 挂了(比文档漂移紧急) 不复用 `deploy-sdk-site.sh --check`:那脚本输出是给人看的彩色文本, 拿来当机器判断依据太脆(改个文案就失效)。md5 清单逻辑很短, 这里复刻一份并在注释里指向 deploy-sdk-site.sh 保持同步。 ## `.drift-watch/` 加入 .gitignore 里面的 `state` 是运行时状态(含时间戳与指纹,每次跑都变),不该入库。 ## 提交前核实(两处我的检查写错了,脚本本身没问题) - 我查"凭据"命中 1 处 ⇒ 实为注释里的「烧 token」,指 LLM token 非密钥。 真实密钥形态(`sk-*` / `password=`)**0 处**。 - 我查"部署调用"命中 6 处 ⇒ 全是**注释与提示文本**(提醒人去跑 `deploy-sdk-site.sh`)。实际执行 `rsync`/`scp`/调用部署脚本**均 0 处**, `rm -rf` 0 处 —— 与它「只读」的声明一致。 - `set -uo pipefail` 少了 `-e`:**有意为之**。巡检要在某项检查失败时 继续跑完其余项,`-e` 会中途打断。实测跑一次四项全绿、无差异。 --- .gitignore | 3 + site-drift-watch.sh | 211 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100755 site-drift-watch.sh diff --git a/.gitignore b/.gitignore index c69de1b..4995ed8 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,6 @@ dist/ .hvigor-home/ .npm-cache/ .ohpm/ + +# 漂移巡检的运行时状态(含时间戳与指纹,每次跑都变) +.drift-watch/ diff --git a/site-drift-watch.sh b/site-drift-watch.sh new file mode 100755 index 0000000..364c3b3 --- /dev/null +++ b/site-drift-watch.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash +# 站点漂移巡检:有差异才提醒,绝不自动部署。 +# +# 老大定的档位是「有差异就提醒」而不是「自动推」——文档站发错了是公开可见的, +# 宁可等人点一下。所以这个脚本**只读不动**:不构建、不上传、不碰线上任何文件。 +# +# 判三类信号: +# 1. 线上漂移:.106 上的站 ≠ 本机产物/site 源(逐字节 md5 清单比对) +# 2. 源码漂移:git HEAD 比产物新 ⇒ 提交了但没重新构建部署 +# 3. 探活失败:.106 或 NapCat 挂了(这类比文档漂移紧急,必须报) +# +# 为什么不直接调 deploy-sdk-site.sh --check 再 grep 输出:那脚本的输出是给人看的 +# 彩色文本,拿来当机器判断依据太脆(改个文案就失效)。md5 清单逻辑很短, +# 这里复刻一份,注释指向 deploy-sdk-site.sh 保持同步。 +# +# 去重:差异持续存在时,每轮都发会把老大刷屏。所以按「差异指纹」去重—— +# 差异内容变了才发新的;差异没了发一条「已恢复」;一直没变就闭嘴。 +# +# 定时:cron 7,37 * * * *(错开整点,避开 acme 等已排满 :00 的任务) + +set -uo pipefail + +SDK_DIR=/home/program/TrueAgent/third_party/homeagent-sdk +BUILD_DIR="$SDK_DIR/site_build" +INTRO_SRC=/home/program/TrueAgent/site +HOST=192.168.2.106 +SSH="ssh -n -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no admin@$HOST" +SITES=/vol1/docker/navi-data/sites + +NAPCAT=${NAPCAT_URL:-http://192.168.2.106:25570} +QQ_USER=${QQ_USER:-2198972886} + +STATE_DIR=/home/program/TrueAgent/.drift-watch +STATE_FILE="$STATE_DIR/state" +LOCK="$STATE_DIR/lock" +LOG=/var/log/site-drift-watch.log + +# 差异清单最多列这么行,剩下的折叠计数(QQ 消息不宜过长) +MAX_LINES=12 + +mkdir -p "$STATE_DIR" +exec 9>"$LOCK" +flock -n 9 || { echo "[$(date '+%F %T')] 上一轮还在跑,跳过" >>"$LOG"; exit 0; } + +say() { printf '\033[1m%s\033[0m\n' "$*"; } +info() { printf ' %s\n' "$*"; } +log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" >>"$LOG"; } + +# ── 通知(直连 NapCat,cron 不需要唤醒 agent)──────────────────────── +# +# 为什么不走 agent:叫醒 agent 发消息要过 LLM 一趟,慢且烧 token; +# NapCat 的 HTTP 接口是现成的,直发即时且零成本。 +notify() { + local msg="$1" payload resp code + payload=$(jq -nc --arg m "$msg" --argjson u "$QQ_USER" '{user_id:$u, message:$m}') + resp=$(curl -s -m 20 -X POST "$NAPCAT/send_private_msg" \ + -H 'Content-Type: application/json' -d "$payload" 2>/dev/null) + code=$(printf '%s' "$resp" | jq -r '.retcode // -1' 2>/dev/null) + if [ "$code" = "0" ]; then + info "已通知老大($(printf '%s' "$resp" | jq -r '.data.message_id // "?"'))" + return 0 + fi + info "✗ 通知发送失败:$(printf '%s' "$resp" | head -c 200)" + log "notify failed: $resp" + return 1 +} + +# ── md5 清单(与 deploy-sdk-site.sh 保持一致)───────────────────────── +norm_md5() { sed 's| \./| |'; } + +list_md5() { # list_md5 <远端目录> <排除项...> + local dir="$1"; shift + local excl=() + local e + for e in "$@"; do excl+=(-not -path "$e"); done + $SSH "cd $dir && sudo -n find . -type f -print0 | sort -z | sudo -n xargs -0 md5sum" 2>/dev/null | norm_md5 +} + +sdk_diff() { + diff <( cd "$BUILD_DIR" && find . -type f -print0 | sort -z | xargs -0 md5sum | norm_md5 ) \ + <( list_md5 "$SITES/sdk" ) 2>/dev/null +} + +intro_diff() { + # README.md 是仓库说明不是站点资源,故意排除(见 deploy-sdk-site.sh 注释) + diff <( cd "$INTRO_SRC" && find index.html assets -type f -print0 | sort -z | xargs -0 md5sum | norm_md5 ) \ + <( list_md5 "$SITES/introduce" -not -path './README.md' ) 2>/dev/null +} + +# ── 探活 ──────────────────────────────────────────────────────────── +probe() { + local url="$1" + curl -s -m 15 -X POST "$url" 2>/dev/null | jq -r '.retcode // -1' 2>/dev/null +} + +# ── 主流程 ────────────────────────────────────────────────────────── +FORCE=0; DRY=0 +for a in "$@"; do + case "$a" in + --test) FORCE=1 ;; + --dry-run) DRY=1 ;; + esac +done + +say "巡检 $HOST 的两个站(只读,不部署)" + +# 1) NapCat 探活(通知通路本身也得是活的,否则有差异也通知不到) +nap=off +if [ "$(probe "$NAPCAT/get_status")" = "0" ]; then + nap=on; info "✓ NapCat 通知通路在线" +else + info "✗ NapCat 不可达($NAPCAT)——有差异也发不出通知" +fi + +# 2) 线上漂移 +reasons=(); details="" + +# .106 整体可达性:连不上是最高优先级故障 +if ! $SSH "true" 2>/dev/null; then + info "✗ $HOST SSH 不可达(admin@,BatchMode)" + reasons+=("🖥 $HOST SSH 连不上,站点状态未知") + sdk_diff() { echo "__UNREACHABLE__"; } +fi + +sdkout=$(sdk_diff) +if [ "$sdkout" = "__UNREACHABLE__" ]; then + : +elif [ -n "$sdkout" ]; then + n=$(printf '%s\n' "$sdkout" | wc -l) + reasons+=("📄 sdk 站与本地产物有差异($n 行)") + details+="【sdk 站】"$'\n'"$(printf '%s\n' "$sdkout" | head -$MAX_LINES)" + [ "$n" -gt "$MAX_LINES" ] && details+=$'\n'"…还有 $((n - MAX_LINES)) 行" + details+=$'\n\n' + info "✗ sdk 有差异:$n 行" +else + info "✓ sdk 站与本地产物逐字节一致" +fi + +introout=$(intro_diff) +if [ "$introout" = "__UNREACHABLE__" ]; then + : +elif [ -n "$introout" ]; then + n=$(printf '%s\n' "$introout" | wc -l) + reasons+=("🎨 introduce 站与本机源有差异($n 行)") + details+="【introduce 站】"$'\n'"$(printf '%s\n' "$introout" | head -$MAX_LINES)" + [ "$n" -gt "$MAX_LINES" ] && details+=$'\n'"…还有 $((n - MAX_LINES)) 行" + details+=$'\n\n' + info "✗ introduce 有差异:$n 行" +else + info "✓ introduce 站与本机源逐字节一致" +fi + +# 3) 源码漂移:提交了但没重新构建 +head_ts=$( cd "$SDK_DIR" && git log -1 --format=%ct 2>/dev/null ) +build_ts=0 +[ -d "$BUILD_DIR" ] && build_ts=$(stat -c %Y "$BUILD_DIR/index.html" 2>/dev/null || echo 0) +if [ -n "$head_ts" ] && [ "$build_ts" -gt 0 ] && [ "$head_ts" -gt "$build_ts" ]; then + hs=$(cd "$SDK_DIR" && git log -1 --format='%h %s' 2>/dev/null) + reasons+=("🧱 sdk 源码已提交但产物没重新构建($hs)") + details+="【源码漂移】"$'\n'"HEAD $head_ts > 产物 $build_ts"$'\n'"$(cd "$SDK_DIR" && git log -1 --format='%h %ad %s' --date=iso 2>/dev/null)"$'\n\n' + info "✗ 源码比产物新,需重新构建" +else + info "✓ 源码与产物无漂移" +fi + +# ── 去重与通知 ────────────────────────────────────────────────────── +fingerprint=$(printf '%s\n' "${reasons[@]:-}" "$details" | md5sum | cut -c1-16) +prev=""; prev_state="" +[ -f "$STATE_FILE" ] && { prev_state=$(jq -r '.state // ""' "$STATE_FILE" 2>/dev/null); prev=$(jq -r '.fingerprint // ""' "$STATE_FILE" 2>/dev/null); } + +if [ "$FORCE" = "1" ]; then + info "--test:强制通知一次" + [ "$DRY" = "1" ] && { say "(dry-run,不实际发送)"; } + [ "$DRY" != "1" ] && notify "【站点巡检 · 测试】通知通路自检(当前差异条数:${#reasons[@]})——看到这条说明巡检能及时叫你。" + exit 0 +fi + +if [ "${#reasons[@]}" -eq 0 ]; then + if [ "$prev_state" = "alert" ]; then + msg="✅【站点巡检】已恢复正常:sdk 与 introduce 两站都和本机逐字节一致,源码也无漂移。刚才的差异自己好了(或你部署过了)。" + [ "$DRY" = "1" ] || notify "$msg" + log "recovered (was alert), notified=$([ "$DRY" = "1" ] && echo dry || echo yes)" + else + info "无差异,按约定保持安静(不打扰老大)" + log "clean" + fi + printf '{"state":"clean","fingerprint":"%s","ts":"%s"}\n' "$fingerprint" "$(date -Is)" >"$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE" + exit 0 +fi + +# 有差异 +if [ "$fingerprint" = "$prev" ] && [ "$prev_state" = "alert" ]; then + info "差异与上次通知完全相同(指纹 $fingerprint),不重复打扰" + log "alert-unchanged fp=$fingerprint" + exit 0 +fi + +say "有差异,准备通知" +msg="🚨【站点巡检】发现 ${#reasons[@]} 处漂移(只提醒,未自动部署)"$'\n\n' +for r in "${reasons[@]}"; do msg+="· $r"$'\n'; done +msg+=$'\n'"$details" +msg+=$'\n'"—— 要部署就跑:/home/program/TrueAgent/deploy-sdk-site.sh" +msg+=$'\n'" 只看差异:/home/program/TrueAgent/deploy-sdk-site.sh --check" +msg+=$'\n'" 回滚:--rollback <备份目录名>" + +if [ "$DRY" = "1" ]; then + say "(dry-run,以下内容本该发出)"; printf '%s\n' "$msg" +else + notify "$msg" && printf '{"state":"alert","fingerprint":"%s","ts":"%s"}\n' "$fingerprint" "$(date -Is)" >"$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE" +fi +log "alert fp=$fingerprint reasons=${#reasons[@]}"