# ═══════════════════════════════════════════════ # 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 # 📜 Copyright: 国作登字-2026-A-00037559 # ═══════════════════════════════════════════════ name: "🔧 铸渊 · PM2 服务诊断与健康检查" # ━━━ PM2 服务诊断 + 全系统健康检查 + Notion 报告 ━━━ # 手动触发:对服务器 PM2 进程进行全面排查, # 自动处理 errored 进程,并将诊断报告写入 Notion 工单队列。 # # 必需 GitHub Secrets: # DEPLOY_HOST — 服务器 IP # DEPLOY_USER — SSH 用户名 # DEPLOY_KEY — SSH 私钥(完整 PEM 内容) # DEPLOY_PATH — 网站根目录(如 /var/www/guanghulab) # # 可选 Secrets(有则自动写 Notion 工单): # NOTION_API_TOKEN — Notion 集成 token # NOTION_TICKET_DB_ID — 工单队列数据库 ID(默认 84edd0640bf146a9a5a7840107013e8c) on: workflow_dispatch: inputs: auto_cleanup: description: '是否自动清理 errored 进程(删除启动路径不存在的进程)' required: false default: 'true' type: choice options: - 'true' - 'false' permissions: contents: read jobs: diagnose: name: "🔍 PM2 诊断 + 系统健康检查" runs-on: ubuntu-latest steps: - name: 📥 检出代码 uses: actions/checkout@v4 - name: 🟢 配置 Node.js uses: actions/setup-node@v4 with: node-version: "20" - name: 🔑 配置 SSH 连接 run: | mkdir -p ~/.ssh echo "${{ secrets.ZY_SERVER_KEY }}" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -H "${{ secrets.ZY_SERVER_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null || { echo "⚠️ ssh-keyscan 失败,使用 StrictHostKeyChecking=no 后备" } # ━━━ 阶段一:PM2 服务诊断 ━━━ - name: "🔧 阶段一 · PM2 errored 服务排查" id: pm2_diagnose run: | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " 阶段一 · PM2 errored 服务排查" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" PM2_REPORT=$(ssh -i ~/.ssh/deploy_key \ -o StrictHostKeyChecking=no \ "${{ secrets.ZY_SERVER_USER }}@${{ secrets.ZY_SERVER_HOST }}" ' echo "══════════════════════════════════════" echo " PM2 进程状态概览" echo "══════════════════════════════════════" pm2 status 2>/dev/null || echo "❌ pm2 未安装或不可用" echo "" echo "══════════════════════════════════════" echo " errored 进程详细日志" echo "══════════════════════════════════════" # 遍历所有 errored 进程 ERRORED_LIST=$(pm2 jlist 2>/dev/null | python3 -c " import sys, json try: procs = json.load(sys.stdin) for p in procs: if p.get(\"pm2_env\", {}).get(\"status\") == \"errored\": name = p.get(\"name\", \"unknown\") pm_id = p.get(\"pm_id\", \"?\") script = p.get(\"pm2_env\", {}).get(\"pm_exec_path\", \"unknown\") restarts = p.get(\"pm2_env\", {}).get(\"restart_time\", 0) print(f\"{pm_id}|{name}|{script}|{restarts}\") except: pass " 2>/dev/null) if [ -z "$ERRORED_LIST" ]; then echo "✅ 没有 errored 进程" else echo "$ERRORED_LIST" | while IFS="|" read -r pm_id name script restarts; do echo "" echo "━━━ 进程: $name (PM2 ID: $pm_id) ━━━" echo " 启动脚本: $script" echo " 重启次数: $restarts" # 检查脚本文件是否存在 if [ -f "$script" ]; then echo " 脚本文件: ✅ 存在" else echo " 脚本文件: ❌ 不存在" fi # 查看最近日志 echo " 最近错误日志 (最后 20 行):" pm2 logs "$name" --lines 20 --nostream --err 2>/dev/null || echo " (无法读取日志)" echo "" done fi echo "" echo "══════════════════════════════════════" echo " 所有进程详细信息" echo "══════════════════════════════════════" pm2 jlist 2>/dev/null | python3 -c " import sys, json try: procs = json.load(sys.stdin) for p in procs: env = p.get(\"pm2_env\", {}) print(f\" [{p.get(\"pm_id\")}] {p.get(\"name\")} | status={env.get(\"status\")} | script={env.get(\"pm_exec_path\",\"?\")} | restarts={env.get(\"restart_time\",0)} | memory={p.get(\"monit\",{}).get(\"memory\",0)}\") except Exception as e: print(f\" 解析失败: {e}\") " 2>/dev/null echo "" echo "══════════════════════════════════════" echo " 端口占用情况" echo "══════════════════════════════════════" netstat -tlnp 2>/dev/null | grep -E "3000|3001|3002|3721|8080" || \ ss -tlnp 2>/dev/null | grep -E "3000|3001|3002|3721|8080" || \ echo " (netstat/ss 不可用)" ') echo "$PM2_REPORT" # 保存到文件供后续步骤使用 echo "$PM2_REPORT" > /tmp/pm2-report.txt # 设置输出 { echo "pm2_report<> "$GITHUB_OUTPUT" # ━━━ 阶段一 · 自动清理 ━━━ - name: "🧹 阶段一 · 自动清理 errored 进程" if: ${{ inputs.auto_cleanup == 'true' }} id: pm2_cleanup run: | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " 自动清理 errored 进程" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" CLEANUP_REPORT=$(ssh -i ~/.ssh/deploy_key \ -o StrictHostKeyChecking=no \ "${{ secrets.ZY_SERVER_USER }}@${{ secrets.ZY_SERVER_HOST }}" ' ERRORED_LIST=$(pm2 jlist 2>/dev/null | python3 -c " import sys, json try: procs = json.load(sys.stdin) for p in procs: if p.get(\"pm2_env\", {}).get(\"status\") == \"errored\": name = p.get(\"name\", \"unknown\") script = p.get(\"pm2_env\", {}).get(\"pm_exec_path\", \"unknown\") print(f\"{name}|{script}\") except: pass " 2>/dev/null) CLEANED="" SKIPPED="" if [ -z "$ERRORED_LIST" ]; then echo "✅ 没有需要清理的 errored 进程" else echo "$ERRORED_LIST" | while IFS="|" read -r name script; do if [ ! -f "$script" ]; then echo "🗑️ 删除 $name(脚本 $script 不存在)" pm2 delete "$name" 2>/dev/null && echo " ✅ 已删除" || echo " ❌ 删除失败" else echo "⚠️ 保留 $name(脚本 $script 存在,可能是运行时错误)" echo " 尝试重启..." pm2 restart "$name" 2>/dev/null && echo " ✅ 重启成功" || echo " ❌ 重启失败" fi done echo "" echo "📊 清理后 PM2 状态:" pm2 status 2>/dev/null pm2 save 2>/dev/null || echo "⚠️ pm2 save 失败" fi ') echo "$CLEANUP_REPORT" echo "$CLEANUP_REPORT" > /tmp/cleanup-report.txt { echo "cleanup_report<> "$GITHUB_OUTPUT" # ━━━ 阶段二:全系统健康检查 ━━━ - name: "🏥 阶段二 · 全系统健康检查" id: health_check run: | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " 阶段二 · 全系统健康检查" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" HEALTH_REPORT=$(ssh -i ~/.ssh/deploy_key \ -o StrictHostKeyChecking=no \ "${{ secrets.ZY_SERVER_USER }}@${{ secrets.ZY_SERVER_HOST }}" ' echo "══════════════════════════════════════" echo " Nginx 状态" echo "══════════════════════════════════════" nginx -t 2>&1 || echo "❌ Nginx 配置检测失败" systemctl status nginx --no-pager -l 2>/dev/null | head -15 || echo "⚠️ systemctl 不可用" echo "" echo "══════════════════════════════════════" echo " SSL 证书有效期" echo "══════════════════════════════════════" if [ -f "/etc/letsencrypt/live/guanghulab.com/fullchain.pem" ]; then openssl x509 -in /etc/letsencrypt/live/guanghulab.com/fullchain.pem -noout -dates 2>/dev/null || echo "⚠️ 无法读取 SSL 证书" else echo "⚠️ SSL 证书文件不存在于默认路径" # 尝试查找其他证书路径 find /etc/letsencrypt/live/ -name "fullchain.pem" 2>/dev/null | head -3 || echo " 未找到 Let'\''s Encrypt 证书" find /etc/nginx/ -name "*.pem" -o -name "*.crt" 2>/dev/null | head -3 || echo " 未找到 Nginx SSL 文件" fi echo "" echo "══════════════════════════════════════" echo " 磁盘空间" echo "══════════════════════════════════════" df -h 2>/dev/null || echo "❌ df 命令不可用" echo "" echo "══════════════════════════════════════" echo " 内存使用" echo "══════════════════════════════════════" free -h 2>/dev/null || echo "❌ free 命令不可用" echo "" echo "══════════════════════════════════════" echo " PM2 最终状态" echo "══════════════════════════════════════" pm2 status 2>/dev/null || echo "❌ pm2 不可用" echo "" echo "══════════════════════════════════════" echo " Node.js 版本" echo "══════════════════════════════════════" node -v 2>/dev/null || echo "❌ Node.js 未安装" npm -v 2>/dev/null || echo "❌ npm 未安装" echo "" echo "══════════════════════════════════════" echo " 网站可访问性检查" echo "══════════════════════════════════════" curl -sI https://guanghulab.com 2>/dev/null | head -5 || echo "⚠️ HTTPS 访问失败" echo "---" curl -s http://127.0.0.1:3000/health 2>/dev/null | head -c 200 || echo "⚠️ 端口 3000 无响应" echo "" echo "---" curl -s http://127.0.0.1:3001/api/v1/ 2>/dev/null | head -c 200 || echo "⚠️ 端口 3001 无响应" echo "" echo "---" curl -s http://127.0.0.1:3002/api/ps/ 2>/dev/null | head -c 200 || echo "⚠️ 端口 3002 无响应" echo "" echo "" echo "══════════════════════════════════════" echo " 系统运行时间" echo "══════════════════════════════════════" uptime 2>/dev/null || echo "❌ uptime 不可用" ') echo "$HEALTH_REPORT" echo "$HEALTH_REPORT" > /tmp/health-report.txt { echo "health_report<> "$GITHUB_OUTPUT" # ━━━ 阶段三:生成 Notion 诊断报告 ━━━ - name: "📋 阶段三 · 写入 Notion 工单" env: NOTION_TOKEN: ${{ secrets.ZY_NOTION_TOKEN }} NOTION_TICKET_DB_ID: ${{ secrets.ZY_NOTION_TICKET_DB }} PM2_REPORT: ${{ steps.pm2_diagnose.outputs.pm2_report }} CLEANUP_REPORT: ${{ steps.pm2_cleanup.outputs.cleanup_report }} HEALTH_REPORT: ${{ steps.health_check.outputs.health_report }} run: | if [ -z "$NOTION_TOKEN" ]; then echo "⚠️ Notion credentials 未配置,跳过工单创建" echo "诊断报告仅输出到 Actions 日志" exit 0 fi node scripts/server-diagnose-report.js - name: "✅ 诊断完成" run: | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " ✅ PM2 诊断完毕 · $(date '+%Y-%m-%d %H:%M:%S')" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"