Files
HomeAgent/site-drift-watch.sh
JianFeeeee 4bd2355991 feat(watch): 站点漂移巡检(只读,有差异才提醒)
`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` 会中途打断。实测跑一次四项全绿、无差异。
2026-09-28 08:45:03 +08:00

212 lines
8.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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[@]}"