Explorar o código

feat(monitor): 加 push 通道 (push.rrbrr.com WebSocket)

- health-push.py: Python urllib 调 push API,默认 monitor-api-key-001
- health-monitor.sh: send_alert() 同时走 mail + push 双通道
- health-monitor.env.example: 加 PUSH_ENABLED/PUSH_API_URL/PUSH_API_KEY/PUSH_USER_ID
- install.sh: 部署 health-push.py
- nginx/push.rrbrr.com.conf: 新域名 vhost(443 SSL + WS 反代 + 静态 H5)

线上需要用户在手机 https://push.rrbrr.com 登录 admin 订阅 monitor channel,
之后 book 监控告警可同时到邮件(f1f3@qq.com)+ push 推送
MyFramework User hai 1 mes
pai
achega
0e13e9ace5

+ 10 - 3
deploy-package/health-monitor/health-monitor.env.example

@@ -1,17 +1,24 @@
-# health-monitor.env — QQ 邮箱 SMTP 凭证
+# health-monitor.env — QQ 邮箱 SMTP + push 推送凭证
 #
 # ⚠️ 必须 chmod 600,只能 root 读写
 # ⚠️ 不要 commit 到 git(.gitignore 已忽略 *.env)
 #
 # 获取 QQ 邮箱授权码:
 #   1. 网页登录 https://mail.qq.com
-#   2. 设置 → 账户 → 找到 POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务
+#   2. 设置 → 账户 → POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务
 #   3. 开启 SMTP 服务(手机短信验证)
-#   4. 复制"授权码"(16位字符串,**不是你的 QQ 密码**)
+#   4. 复制"授权码"(16 位字符串,**不是你的 QQ 密码**)
 
 QQ_USER=f1f3@qq.com
 QQ_AUTH_CODE=把你的QQ授权码填这里
 
+# Push 推送通道(push.rrbrr.com,admin 在 H5 订 monitor channel 后能收到)
+PUSH_ENABLED=true
+PUSH_API_URL=https://push.rrbrr.com
+PUSH_API_KEY=monitor-api-key-001
+PUSH_USER_ID=admin
+PUSH_CHANNEL=monitor
+
 # 可选:覆盖默认(一般不用改)
 # HEALTH_URL=http://127.0.0.1:3100/health
 # API_URL=http://127.0.0.1:3100/api/navigation/config

+ 39 - 2
deploy-package/health-monitor/health-monitor.sh

@@ -47,6 +47,43 @@ send_mail() {
   fi
 }
 
+# ---- Push 推送(push.rrbrr.com WebSocket 通道)----
+PUSH_SCRIPT="${PUSH_SCRIPT:-/usr/local/bin/health-push.py}"
+send_push() {
+  local title="$1"
+  local body="$2"
+
+  if [ "${PUSH_ENABLED:-true}" != "true" ]; then
+    log "SKIP push: PUSH_ENABLED != true"
+    return 0
+  fi
+  if [ ! -x "$PUSH_SCRIPT" ]; then
+    log "PUSH FAIL: $PUSH_SCRIPT 不存在或不可执行"
+    return 1
+  fi
+
+  local out
+  out=$(PUSH_API_URL="$PUSH_API_URL" PUSH_API_KEY="$PUSH_API_KEY" \
+        PUSH_USER_ID="$PUSH_USER_ID" PUSH_CHANNEL="$PUSH_CHANNEL" \
+        python3 "$PUSH_SCRIPT" "$title" <<< "$body" 2>&1)
+  local rc=$?
+  if [ $rc -eq 0 ]; then
+    log "PUSH OK: $title | $out"
+    return 0
+  else
+    log "PUSH FAIL (rc=$rc): $title | $out"
+    return 1
+  fi
+}
+
+# ---- 统一告警入口:同时走 mail + push ----
+send_alert() {
+  local subject="$1"
+  local body="$2"
+  send_mail "$subject" "$body"
+  send_push  "$subject" "$body"
+}
+
 # ---- 加载配置 ----
 CONFIG_FILE="/etc/health-monitor.env"
 if [ ! -f "$CONFIG_FILE" ]; then
@@ -103,7 +140,7 @@ if [ -z "$fail_detail" ]; then
   # 恢复通知
   if [ "$prev_status" = "down" ]; then
     log "RECOVERED: $HEALTH_URL + $API_URL 均恢复"
-    send_mail "✅ [book] 服务已恢复" \
+    send_alert "✅ [book] 服务已恢复" \
       "时间: $(date '+%F %T')\n主机: $(hostname)\nURL: $HEALTH_URL\n之前: 连续 ${prev_consec} 次失败\n当前: 全部 200 OK"
   fi
 
@@ -127,7 +164,7 @@ log "FAIL: $fail_detail (consec=$new_consec)"
 # 达到阈值才发,且要过冷却期
 if [ "$new_consec" -ge "$CONSEC_FAIL_THRESHOLD" ] && [ "$elapsed" -ge "$ALERT_COOLDOWN_SEC" ]; then
   log "ALERT: 触发告警 (consec=$new_consec, elapsed=${elapsed}s)"
-  send_mail "🚨 [book] 服务异常 - $fail_detail" \
+  send_alert "🚨 [book] 服务异常 - $fail_detail" \
     "时间: $(date '+%F %T')\n主机: $(hostname)\nURL: $HEALTH_URL\nAPI:  $API_URL\n\n检测结果:\n  health: $health\n  api:    $api\n\n连续失败次数: $new_consec\n上次告警: $((elapsed / 60)) 分钟前\n\n可能原因:\n  1) PM2 进程退出: pm2 list\n  2) 端口被占用: ss -lntp | grep 3100\n  3) 进程 OOM: dmesg | tail -20\n  4) 磁盘满: df -h /\n\nssh 排查: ssh -p 22622 root@8.159.134.106 'pm2 logs server --lines 50'"
 
   if [ $? -eq 0 ]; then

+ 77 - 0
deploy-package/health-monitor/health-push.py

@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+"""
+health-push.py — 通过 push.rrbrr.com WebSocket 推送服务发推送
+
+Usage:
+  PUSH_API_URL=https://push.rrbrr.com PUSH_API_KEY=xxx PUSH_USER_ID=admin \
+    python3 health-push.py <title> <<< "content"
+
+Env required:
+  PUSH_API_URL     push 服务地址,默认 https://push.rrbrr.com
+  PUSH_API_KEY     API key(绑定 channel),默认 monitor-api-key-001
+  PUSH_USER_ID     接收推送的 userId,默认 admin
+  PUSH_CHANNEL     推送 channel,默认 monitor
+"""
+
+import os
+import sys
+import json
+import urllib.request
+import urllib.error
+import ssl
+
+
+def main():
+    if len(sys.argv) < 2:
+        print("Usage: health-push.py <title>", file=sys.stderr)
+        sys.exit(2)
+
+    title = sys.argv[1]
+    content = sys.stdin.read() if not sys.stdin.isatty() else "(empty)"
+
+    url = os.environ.get('PUSH_API_URL', 'https://push.rrbrr.com').rstrip('/')
+    api_key = os.environ.get('PUSH_API_KEY', 'monitor-api-key-001')
+    user_id = os.environ.get('PUSH_USER_ID', 'admin')
+    channel = os.environ.get('PUSH_CHANNEL', 'monitor')
+
+    body = json.dumps({
+        'userId': user_id,
+        'channel': channel,
+        'title': title,
+        'content': content[:500],   # 限制长度,H5 显示友好
+        'data': {'ts': __import__('time').time()},
+    }).encode('utf-8')
+
+    req = urllib.request.Request(
+        f'{url}/api/notify',
+        data=body,
+        method='POST',
+        headers={
+            'Authorization': f'Bearer {api_key}',
+            'Content-Type': 'application/json',
+        },
+    )
+
+    ctx = ssl.create_default_context()
+    ctx.check_hostname = False
+    ctx.verify_mode = ssl.CERT_NONE  # 自签证书先跳过
+
+    try:
+        with urllib.request.urlopen(req, timeout=10, context=ctx) as r:
+            resp = json.loads(r.read().decode('utf-8'))
+            if resp.get('ok'):
+                print(f"OK: sent '{title}' to userId={user_id} channel={channel}, online={resp.get('online', 0)}")
+                sys.exit(0)
+            else:
+                print(f"API NOK: {resp}", file=sys.stderr)
+                sys.exit(3)
+    except urllib.error.HTTPError as e:
+        print(f"HTTP {e.code}: {e.read().decode('utf-8', 'replace')[:200]}", file=sys.stderr)
+        sys.exit(4)
+    except Exception as e:
+        print(f"NET FAIL [{type(e).__name__}]: {e}", file=sys.stderr)
+        sys.exit(5)
+
+
+if __name__ == '__main__':
+    main()

+ 1 - 0
deploy-package/health-monitor/install.sh

@@ -35,6 +35,7 @@ fi
 # 2. 主脚本
 install -m 755 "$SCRIPT_DIR/health-monitor.sh" /usr/local/bin/health-monitor.sh
 install -m 755 "$SCRIPT_DIR/health-send-mail.py" /usr/local/bin/health-send-mail.py
+install -m 755 "$SCRIPT_DIR/health-push.py" /usr/local/bin/health-push.py
 
 # 3. 状态目录
 mkdir -p /var/lib/health-monitor /var/log

+ 79 - 0
deploy-package/nginx/push.rrbrr.com.conf

@@ -0,0 +1,79 @@
+server
+{
+    listen 80;
+    server_name push.rrbrr.com;
+    return 301 https://$server_name$request_uri;
+}
+
+server
+{
+    listen 443 ssl;
+    http2 on;
+    server_name push.rrbrr.com;
+    index index.html index.htm default.htm default.html;
+    root /data/ai/webhook/frontend/dist/build/h5;
+    include /www/server/panel/vhost/nginx/extension/push.rrbrr.com/*.conf;
+
+    ssl_certificate /www/server/panel/vhost/cert/push.rrbrr.com/fullchain.pem;
+    ssl_certificate_key /www/server/panel/vhost/cert/push.rrbrr.com/privkey.pem;
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:!MD5;
+    ssl_prefer_server_ciphers on;
+    ssl_session_tickets on;
+    ssl_session_cache shared:SSL:10m;
+    ssl_session_timeout 10m;
+    add_header Strict-Transport-Security "max-age=31536000";
+    error_page 497 https://$host$request_uri;
+
+    # 后端 API(业务系统调用 /api/notify 发推送)
+    location /api/ {
+        proxy_pass http://127.0.0.1:13000;
+        proxy_http_version 1.1;
+        proxy_set_header Host $http_host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+    }
+
+    # WebSocket(App 端长连接收推送)
+    location /ws {
+        proxy_pass http://127.0.0.1:13000;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        proxy_set_header Host $http_host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_read_timeout 600s;  # WS 长连接保活
+    }
+
+    # 后端管理台(可视化测试台)
+    location /admin {
+        proxy_pass http://127.0.0.1:13000;
+        proxy_http_version 1.1;
+        proxy_set_header Host $http_host;
+        proxy_set_header X-Real-IP $remote_addr;
+    }
+
+    include /www/server/panel/vhost/rewrite/html_push.rrbrr.com.conf;
+
+    location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md)
+    {
+        return 404;
+    }
+
+    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
+    {
+        expires      30d;
+        error_log /dev/null;
+        access_log /dev/null;
+    }
+
+    location ~ .*\.(js|css)?$
+    {
+        expires      12h;
+        error_log /dev/null;
+        access_log /dev/null;
+    }
+    access_log  /www/wwwlogs/push.rrbrr.com.log;
+    error_log  /www/wwwlogs/push.rrbrr.com.error.log;
+}