name: hermes-health-monitoring description: "Hermes 系统健康监控体系 — 每日+每周两级主动探测,在问题发生前发现。基于 5/11 脚本误报教训(SSL、微信检查脚本写死路径导致假告警)。" version: 1.1.0 author: Hermes Agent metadata: hermes: tags: [monitoring, health-check, cron, devops, resilience] prerequisites: commands: [hermes, systemctl, openssl, curl, python3]
两级主动探测:每日简单检查 + 每周全面检查。核心理念:不等报错,提前验证关键链路是否通畅。
| 层级 | cron | schedule | 模式 | 脚本 | 范围 |
|---|---|---|---|---|---|
| 每日 | 82cddbdae47 |
0 22 * * * |
no-agent | daily_health_check.sh |
gateway 存活、依赖完整、SSL、微信 token、gbrain 清理 |
| 每周 | 46d7431f440c |
0 10 * * 0 |
no-agent | weekly_health_check.sh |
以上 + 飞书 API 权限 + wiki 成员 + 配置一致性 |
脚本路径:~/.hermes/scripts/daily_health_check.sh 和 ~/.hermes/scripts/weekly_health_check.sh
交付方式:异常才发飞书群 oc_569a01e75d67f5f0422a994aa7132365,静默=健康。
所有检查用 bash 脚本完成,不经过 LLM。脚本 stdout 直接交付 cron delivery。原因:简单检查不需要 LLM 判断,节省 token 且避免 agent 误判。
5/11 核心教训:profiles 隔离后,脚本中的硬编码路径/账号必然失效。所有脚本必须从环境变量或配置文件动态读取:
# ❌ 错误:硬编码路径
TOKEN_FILE="$HOME/.hermes/weixin/accounts/e7bd38ce7e39@im.bot.json"
# ✅ 正确:从 aide .env 读取
ACCOUNT_ID=$(grep "^WEIXIN_ACCOUNT_ID=" "$HOME/.hermes/profiles/aide/.env" | cut -d= -f2)
TOKEN_DIR="$HOME/.hermes/profiles/aide/weixin/accounts"
# ❌ 错误:直接读 /etc/letsencrypt/live/(需要 root)
[ -f "/etc/letsencrypt/live/$domain/fullchain.pem" ]
# ✅ 正确:通过 HTTPS 连接验证证书
echo | openssl s_client -servername "$domain" -connect "${domain}:443" 2>/dev/null | openssl x509 -noout -enddate
检查脚本发现可自动修复的问题时直接修复(如 gbrain 孤儿进程清理),修复后不再告警。
# ❌ 错误:count-based 会误杀 CLI 会话的 gbrain
COUNT=$(pgrep -f "bun.*cli.ts" | wc -l)
[ "$COUNT" -gt 2 ] && kill_oldest
# ✅ 正确:parent PID 精确判断孤儿
ppid=$(ps -o ppid= -p "$pid")
[ "$ppid" = "$GATEWAY_PID" ] && continue # gateway 的合法子进程
进程计数无法区分"gateway 的子进程"和"CLI 会话的子进程",在 multi-gateway + CLI 会话并存的环境下必然误判。parent PID 链检查是唯一准确的孤儿判断方式。
症状:systemctl status 显示 activating (auto-restart),重启计数器暴涨(数万次),journal 里每 5 秒报 address already in use。
根因:旧进程僵尸占用端口,systemd 新进程绑不上去,陷入死循环。
诊断:
# 1. 确认端口被谁占着(ss 比 netstat 更快)
ss -tlnp | grep <PORT>
# 2. 看 systemd 崩溃日志
journalctl -u <service>.service --no-pager -n 20
修复:
# 杀僵尸进程,systemd 自动拉起新进程
kill <OLD_PID>
# 等几秒验证
sleep 3 && journalctl -u <service>.service --no-pager -n 5
防范(三层):
[Service]
ExecStartPre=/bin/bash -c 'fuser -k <PORT>/tcp 2>/dev/null; sleep 1'
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=10
StartLimitBurst=5
StartLimitIntervalSec=60
ExecStartPre 在启动前强制释放端口,KillMode=mixed 确保子进程一并清理,StartLimitBurst 防止无限重启刷日志。
健康检查用 HTTP 连接测试,不用 systemctl is-active:
crash-loop 状态下 systemctl 可能显示 active (running) 瞬间然后退出,systemctl is-active 可能漏报。必须用 curl --max-time 5 http://127.0.0.1:<PORT>/ 实际探测。
健康检查覆盖所有关键端口:不仅是 gateway,还包括 admin-auth(7891)、dashboard(9119) 等所有用户可见链路上的服务。
问题:单搜 gateway.log 会在日志轮转后(06:30)找不到 "Gateway running with" 记录,误报为"平台数未知"。
修复:用 ls -tr + zcat -f 扫所有轮转日志:
LATEST_ENTRY=$(ls -tr "$LOG_DIR"/gateway.log* 2>/dev/null | xargs zcat -f 2>/dev/null | grep "Gateway running with" | tail -1)
原理:ls -tr 按 mtime 最旧→最新排序文件,zcat -f 同时处理 .gz 和纯文本,tail -1 取全局最新条目。
同样的模式适用于任何跨轮转日志的 grep 检查。
ps aux | sort -k9 日期/时间混排失效问题:ps aux 第 9 列(START)跨天时混排 "May11" 和 "22:51" 两种格式。sort -k9 按字典序排,新进程的 "22:51" 会被排到旧进程 "May11" 前面,导致清理脚本保留了旧的、杀了新的。
修复:按 PID(第 2 列)排序,PID 越大=越新:
ps aux | sort -k2 -n | head -n -N | awk '{print $2}' | xargs kill
# 保留最新的 N 个进程(head -n -N 表示去掉最后 N 行)
问题:原始脚本统计 ps aux | grep "bun.*cli.ts.*serve" | wc -l,如果 >2 就 kill 最旧的。但这个 count 包括 CLI 会话产生的 gbrain(1-2 个),导致健康检查误杀正常 CLI 会话的 gbrain。
修复:改用 parent PID 精确识别孤儿。只清理"不在任何 gateway 和 CLI 会话进程树下的 standalone gbrain":
MAIN_GW_PID=$(systemctl --user show hermes-gateway --property=MainPID --value 2>/dev/null)
AIDE_GW_PID=$(systemctl --user show hermes-gateway-aide --property=MainPID --value 2>/dev/null)
for pid in $(pgrep -f "bun.*cli.ts.*serve" 2>/dev/null); do
ppid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
[ "$ppid" = "$MAIN_GW_PID" ] && continue # gateway 的子进程
[ "$ppid" = "$AIDE_GW_PID" ] && continue # aide 的子进程
parent_cmd=$(ps -p "$ppid" -o comm= 2>/dev/null)
echo "$parent_cmd" | grep -qE "hermes|python3" && continue # CLI 会话的子进程
kill "$pid" 2>/dev/null # 真孤儿,清理
done
教训:任何涉及多个父进程的进程清理,count-based 方法必然误杀或漏杀。parent-PID-based 检测更精确。
问题:添加 aide gateway profile 后,系统从 1 个 gateway 变成 2 个。每个 gateway 各跑一个 gbrain MCP 进程。但脚本的阈值仍为 >1,导致每次检查都告警"僵尸进程"。
注意:此 Pitfall 已被 parent-PID-based 检测方案取代(见 Pitfall 3),新脚本不再依赖固定计数阈值,理论上不受进程数量变化影响。保留此记录仅作历史参考。
# ❌ 单 gateway 时期的阈值
if [ "$BUN_COUNT" -gt 1 ]; then ...
# ✅ 双 gateway 时期的阈值
if [ "$BUN_COUNT" -gt 2 ]; then ...
lark-cli 在服务器上不可用(keychain 依赖 secret-tool,未安装)。飞书 API 改用 curl 直调:
# 1. 获取 tenant_access_token
TOKEN=$(curl -s -X POST "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" \
-H "Content-Type: application/json" \
-d "{\"app_id\":\"$APP_ID\",\"app_secret\":\"$APP_SECRET\"}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['tenant_access_token'])")
# 2. 调业务 API
curl -s "https://open.feishu.cn/open-apis/wiki/v2/spaces/7634951676289387725/members" \
-H "Authorization: Bearer $TOKEN"
凭据从 ~/.hermes/.env 读取:FEISHU_APP_ID 和 FEISHU_APP_SECRET。
curl http://127.0.0.1:7891/api/auth/verify,crash-loop 时 systemctl 可能仍显示 active)curl -sI http://127.0.0.1:9119/ 返回 200/401)两步都要做,不能只写文档不写脚本(5/26 教训:skill 文档已列出 admin-auth 和 dashboard 检查但脚本缺失,导致 7891 崩溃 58K 次无人知晓)。
ISSUES 变量追加异常信息。格式:if [ 异常条件 ]; then
ISSUES="${ISSUES}\n❌ 检查项描述"
fi
脚本末尾自动判断:ISSUES 非空则输出异常信息,空则静默退出。
references/script-hardcoded-paths-lessons.md:5/11 SSL 和微信脚本误报的根因分析和修复原则references/zombie-cleanup-script-gap.md:5/23 发现 zombie_cleanup.sh 保留最旧进程导致 gbrain MCP 断裂的缺陷分析references/dashboard-502-recovery.md:Dashboard 502 诊断和恢复步骤(hermes dashboard 无自启动机制)