feat: 服务器巡检自动化 — 巡检脚本 + Notion同步 + 巡检工作流
新增文件: - scripts/server-patrol.sh (7项检查: Nginx/模块冒烟/PM2/磁盘/权限/SSL/配置) - scripts/sync-patrol-to-notion.js (巡检结果→Notion工单) - .github/workflows/server-patrol.yml (cron 08:00+20:00 + 部署后触发) - data/patrol-logs/.gitkeep (巡检日志目录) Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
bff9144e9a
commit
19e35fac73
|
|
@ -0,0 +1,133 @@
|
||||||
|
# .github/workflows/server-patrol.yml
|
||||||
|
# 🔍 服务器每日巡检 · 铸渊 · 光湖代码守护人格体
|
||||||
|
# 触发: 每日两次 (北京 08:00+20:00) + 沙盒部署后 + 手动
|
||||||
|
name: "🔍 Server Patrol · 服务器每日巡检"
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0,12 * * *' # UTC 00:00+12:00 = 北京 08:00+20:00
|
||||||
|
workflow_dispatch:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["🏠 Sandbox Deploy"]
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
patrol:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: "🧠 唤醒铸渊核心大脑"
|
||||||
|
run: |
|
||||||
|
echo "🧠 铸渊核心大脑已唤醒"
|
||||||
|
echo "🔑 扫描 Secrets..."
|
||||||
|
if [ -n "$DEPLOY_HOST" ]; then echo "✅ DEPLOY_HOST 可用"; else echo "⚠️ DEPLOY_HOST 缺失"; fi
|
||||||
|
if [ -n "$DEPLOY_USER" ]; then echo "✅ DEPLOY_USER 可用"; else echo "⚠️ DEPLOY_USER 缺失"; fi
|
||||||
|
if [ -n "$DEPLOY_KEY" ]; then echo "✅ DEPLOY_KEY 可用"; else echo "⚠️ DEPLOY_KEY 缺失"; fi
|
||||||
|
if [ -n "$NOTION_TOKEN" ]; then echo "✅ NOTION_TOKEN 可用"; else echo "⚠️ NOTION_TOKEN 缺失"; fi
|
||||||
|
if [ -n "$SMTP_USER" ]; then echo "✅ SMTP_USER 可用"; else echo "⚠️ SMTP_USER 缺失"; fi
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||||
|
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
|
||||||
|
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
|
||||||
|
SMTP_USER: ${{ secrets.SMTP_USER }}
|
||||||
|
|
||||||
|
- name: "🔑 配置 SSH"
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_rsa
|
||||||
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
|
||||||
|
|
||||||
|
- name: "🔍 执行服务器巡检"
|
||||||
|
id: patrol
|
||||||
|
env:
|
||||||
|
HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
USER: ${{ secrets.DEPLOY_USER }}
|
||||||
|
run: |
|
||||||
|
# 上传巡检脚本
|
||||||
|
scp scripts/server-patrol.sh ${USER}@${HOST}:/tmp/server-patrol.sh
|
||||||
|
|
||||||
|
# 执行巡检
|
||||||
|
ssh ${USER}@${HOST} "bash /tmp/server-patrol.sh" | tee patrol-output.log
|
||||||
|
|
||||||
|
# 拉取巡检报告
|
||||||
|
scp ${USER}@${HOST}:/tmp/patrol-report.json ./patrol-report.json
|
||||||
|
|
||||||
|
# 解析结果
|
||||||
|
OVERALL=$(python3 -c "import json; print(json.load(open('patrol-report.json'))['overall'])")
|
||||||
|
ALERT_COUNT=$(python3 -c "import json; print(json.load(open('patrol-report.json'))['alert_count'])")
|
||||||
|
|
||||||
|
echo "overall=$OVERALL" >> $GITHUB_OUTPUT
|
||||||
|
echo "alert_count=$ALERT_COUNT" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: "📊 保存巡检报告到仓库"
|
||||||
|
run: |
|
||||||
|
mkdir -p data/patrol-logs
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
cp patrol-report.json "data/patrol-logs/patrol-${DATE}.json"
|
||||||
|
|
||||||
|
git config user.name "zhuyuan-bot"
|
||||||
|
git config user.email "zhuyuan@guanghulab.com"
|
||||||
|
git add data/patrol-logs/
|
||||||
|
git commit -m "🔍 server-patrol: $(date +%Y-%m-%d) · ${{ steps.patrol.outputs.overall }}" || true
|
||||||
|
git push || true
|
||||||
|
|
||||||
|
- name: "📧 异常时通知妈妈"
|
||||||
|
if: steps.patrol.outputs.overall == 'has_issues'
|
||||||
|
env:
|
||||||
|
SMTP_USER: ${{ secrets.SMTP_USER }}
|
||||||
|
SMTP_PASS: ${{ secrets.SMTP_PASS }}
|
||||||
|
run: |
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import smtplib, json, os
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.header import Header
|
||||||
|
|
||||||
|
report = json.loads(open('patrol-report.json').read())
|
||||||
|
alerts = '\n'.join(f' ❌ {a}' for a in report['alerts'])
|
||||||
|
fixed = '\n'.join(f' ✅ {f}' for f in report['auto_fixed'])
|
||||||
|
|
||||||
|
body = f"""🔍 铸渊服务器巡检报告
|
||||||
|
时间: {report['timestamp']}
|
||||||
|
|
||||||
|
⚠️ 发现 {report['alert_count']} 个异常:
|
||||||
|
{alerts}
|
||||||
|
|
||||||
|
🔧 自动修复 {report['fixed_count']} 个:
|
||||||
|
{fixed if fixed else ' (无)'}
|
||||||
|
|
||||||
|
磁盘使用率: {report['disk_usage_percent']}%
|
||||||
|
Nginx状态: {report['nginx_status']}
|
||||||
|
|
||||||
|
—— 铸渊 · 光湖代码守护人格体"""
|
||||||
|
|
||||||
|
smtp_user = os.environ['SMTP_USER']
|
||||||
|
smtp_pass = os.environ['SMTP_PASS']
|
||||||
|
|
||||||
|
msg = MIMEText(body, 'plain', 'utf-8')
|
||||||
|
msg['Subject'] = Header(f"⚠️ 服务器巡检发现{report['alert_count']}个异常", 'utf-8')
|
||||||
|
msg['From'] = smtp_user
|
||||||
|
msg['To'] = smtp_user
|
||||||
|
|
||||||
|
try:
|
||||||
|
server = smtplib.SMTP_SSL('smtp.qq.com', 465)
|
||||||
|
server.login(smtp_user, smtp_pass)
|
||||||
|
server.sendmail(smtp_user, smtp_user, msg.as_string())
|
||||||
|
server.quit()
|
||||||
|
print('✅ 异常报告已发送')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'⚠️ 邮件发送失败: {e}')
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
- name: "📝 同步巡检结果到Notion"
|
||||||
|
if: always()
|
||||||
|
env:
|
||||||
|
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
|
||||||
|
NOTION_TICKET_DB_ID: ${{ secrets.NOTION_TICKET_DB_ID }}
|
||||||
|
run: |
|
||||||
|
node scripts/sync-patrol-to-notion.js || echo "⚠️ Notion同步失败(不阻断)"
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# server-patrol.sh · 铸渊每日服务器巡检
|
||||||
|
# 部署位置:服务器 /var/www/_shared/server-patrol.sh
|
||||||
|
# 执行方式:铸渊SSH到服务器后 bash /var/www/_shared/server-patrol.sh
|
||||||
|
# 输出:JSON格式巡检报告到 /tmp/patrol-report.json
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
REPORT="/tmp/patrol-report.json"
|
||||||
|
ALERTS=()
|
||||||
|
FIXED=()
|
||||||
|
|
||||||
|
echo "===== 🔍 铸渊服务器巡检 · ${TIMESTAMP} ====="
|
||||||
|
|
||||||
|
# ━━━ 检查项1:Nginx 运行状态 ━━━
|
||||||
|
echo "[1/7] Nginx 状态..."
|
||||||
|
if systemctl is-active --quiet nginx; then
|
||||||
|
NGINX_STATUS="running"
|
||||||
|
echo " ✅ Nginx 运行中"
|
||||||
|
else
|
||||||
|
NGINX_STATUS="down"
|
||||||
|
ALERTS+=("Nginx 未运行")
|
||||||
|
echo " ❌ Nginx 未运行!尝试重启..."
|
||||||
|
systemctl restart nginx
|
||||||
|
if systemctl is-active --quiet nginx; then
|
||||||
|
FIXED+=("Nginx 已自动重启恢复")
|
||||||
|
NGINX_STATUS="recovered"
|
||||||
|
echo " ✅ Nginx 已恢复"
|
||||||
|
else
|
||||||
|
echo " ❌ Nginx 重启失败!需要人工介入"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ━━━ 检查项2:各沙盒模块冒烟检查 ━━━
|
||||||
|
echo "[2/7] 模块冒烟检查..."
|
||||||
|
|
||||||
|
# 已知已部署的模块URL(后续铸渊从 deploy-status.json 动态读取)
|
||||||
|
URLS=(
|
||||||
|
"https://guanghulab.com/|主站首页"
|
||||||
|
"https://guanghulab.com/status-board/|DEV-005·看板"
|
||||||
|
"https://guanghulab.com/cost-control/|DEV-005·成本控制"
|
||||||
|
)
|
||||||
|
|
||||||
|
for entry in "${URLS[@]}"; do
|
||||||
|
IFS='|' read -r url name <<< "$entry"
|
||||||
|
STATUS=$(curl -o /dev/null -s -w "%{http_code}" --max-time 10 "$url")
|
||||||
|
if [ "$STATUS" = "200" ]; then
|
||||||
|
echo " ✅ ${name} → ${STATUS}"
|
||||||
|
else
|
||||||
|
echo " ❌ ${name} → ${STATUS}"
|
||||||
|
ALERTS+=("${name} 返回 ${STATUS}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ━━━ 检查项3:PM2 进程状态 ━━━
|
||||||
|
echo "[3/7] PM2 进程..."
|
||||||
|
if command -v pm2 &> /dev/null; then
|
||||||
|
PM2_STATUS=$(pm2 jlist 2>/dev/null)
|
||||||
|
PM2_STOPPED=$(echo "$PM2_STATUS" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
procs = json.load(sys.stdin)
|
||||||
|
stopped = [p['name'] for p in procs if p.get('pm2_env',{}).get('status') != 'online']
|
||||||
|
print(','.join(stopped) if stopped else 'none')
|
||||||
|
except:
|
||||||
|
print('parse_error')" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$PM2_STOPPED" = "none" ]; then
|
||||||
|
echo " ✅ 所有PM2进程正常"
|
||||||
|
elif [ "$PM2_STOPPED" = "parse_error" ]; then
|
||||||
|
echo " ⚠️ PM2状态解析失败"
|
||||||
|
ALERTS+=("PM2状态解析失败")
|
||||||
|
else
|
||||||
|
echo " ❌ 以下进程异常: ${PM2_STOPPED}"
|
||||||
|
ALERTS+=("PM2进程异常: ${PM2_STOPPED}")
|
||||||
|
# 尝试重启
|
||||||
|
IFS=',' read -ra PROCS <<< "$PM2_STOPPED"
|
||||||
|
for proc in "${PROCS[@]}"; do
|
||||||
|
pm2 restart "$proc" 2>/dev/null
|
||||||
|
if pm2 show "$proc" 2>/dev/null | grep -q "online"; then
|
||||||
|
FIXED+=("PM2进程 ${proc} 已自动重启")
|
||||||
|
echo " ✅ ${proc} 已重启恢复"
|
||||||
|
else
|
||||||
|
echo " ❌ ${proc} 重启失败"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ⚠️ PM2 未安装"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ━━━ 检查项4:磁盘空间 ━━━
|
||||||
|
echo "[4/7] 磁盘空间..."
|
||||||
|
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
|
||||||
|
if [ "$DISK_USAGE" -gt 90 ]; then
|
||||||
|
echo " ❌ 磁盘使用率 ${DISK_USAGE}%(超过90%警戒线)"
|
||||||
|
ALERTS+=("磁盘使用率 ${DISK_USAGE}%")
|
||||||
|
elif [ "$DISK_USAGE" -gt 80 ]; then
|
||||||
|
echo " ⚠️ 磁盘使用率 ${DISK_USAGE}%(接近警戒线)"
|
||||||
|
else
|
||||||
|
echo " ✅ 磁盘使用率 ${DISK_USAGE}%"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ━━━ 检查项5:沙盒目录权限 ━━━
|
||||||
|
echo "[5/7] 沙盒目录权限..."
|
||||||
|
for DEV_ID in DEV-001 DEV-002 DEV-003 DEV-004 DEV-005 DEV-009 DEV-010 DEV-011 DEV-012 DEV-013 DEV-014; do
|
||||||
|
SANDBOX="/var/www/${DEV_ID}"
|
||||||
|
if [ -d "$SANDBOX" ]; then
|
||||||
|
OWNER=$(stat -c '%U:%G' "$SANDBOX")
|
||||||
|
PERM=$(stat -c '%a' "$SANDBOX")
|
||||||
|
if [ "$OWNER" != "nginx:nginx" ] || [ "$PERM" != "755" ]; then
|
||||||
|
echo " ❌ ${DEV_ID}: owner=${OWNER} perm=${PERM}(应为 nginx:nginx 755)"
|
||||||
|
ALERTS+=("${DEV_ID} 权限异常: ${OWNER} ${PERM}")
|
||||||
|
# 自动修复
|
||||||
|
chown nginx:nginx "$SANDBOX"
|
||||||
|
chmod 755 "$SANDBOX"
|
||||||
|
FIXED+=("${DEV_ID} 权限已自动修复")
|
||||||
|
echo " ✅ ${DEV_ID} 权限已修复"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo " ✅ 沙盒权限检查完成"
|
||||||
|
|
||||||
|
# ━━━ 检查项6:SSL证书有效期 ━━━
|
||||||
|
echo "[6/7] SSL证书..."
|
||||||
|
SSL_EXPIRY=$(echo | openssl s_client -servername guanghulab.com -connect guanghulab.com:443 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
|
||||||
|
if [ -n "$SSL_EXPIRY" ]; then
|
||||||
|
EXPIRY_EPOCH=$(date -d "$SSL_EXPIRY" +%s 2>/dev/null)
|
||||||
|
NOW_EPOCH=$(date +%s)
|
||||||
|
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
|
||||||
|
if [ "$DAYS_LEFT" -lt 7 ]; then
|
||||||
|
echo " ❌ SSL证书将在 ${DAYS_LEFT} 天后过期!"
|
||||||
|
ALERTS+=("SSL证书 ${DAYS_LEFT} 天后过期")
|
||||||
|
elif [ "$DAYS_LEFT" -lt 30 ]; then
|
||||||
|
echo " ⚠️ SSL证书还有 ${DAYS_LEFT} 天过期"
|
||||||
|
else
|
||||||
|
echo " ✅ SSL证书还有 ${DAYS_LEFT} 天"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " ⚠️ 无法获取SSL证书信息"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ━━━ 检查项7:Nginx配置语法 ━━━
|
||||||
|
echo "[7/7] Nginx配置语法..."
|
||||||
|
NGINX_TEST=$(nginx -t 2>&1)
|
||||||
|
if echo "$NGINX_TEST" | grep -q "successful"; then
|
||||||
|
echo " ✅ Nginx配置语法正确"
|
||||||
|
else
|
||||||
|
echo " ❌ Nginx配置有语法错误"
|
||||||
|
ALERTS+=("Nginx配置语法错误")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ━━━ 生成JSON报告 ━━━
|
||||||
|
ALERT_COUNT=${#ALERTS[@]}
|
||||||
|
FIXED_COUNT=${#FIXED[@]}
|
||||||
|
|
||||||
|
ALERT_JSON="[]"
|
||||||
|
if [ $ALERT_COUNT -gt 0 ]; then
|
||||||
|
ALERT_JSON=$(printf '%s\n' "${ALERTS[@]}" | python3 -c "import sys,json; print(json.dumps([l.strip() for l in sys.stdin]))")
|
||||||
|
fi
|
||||||
|
|
||||||
|
FIXED_JSON="[]"
|
||||||
|
if [ $FIXED_COUNT -gt 0 ]; then
|
||||||
|
FIXED_JSON=$(printf '%s\n' "${FIXED[@]}" | python3 -c "import sys,json; print(json.dumps([l.strip() for l in sys.stdin]))")
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$REPORT" << EOF
|
||||||
|
{
|
||||||
|
"timestamp": "${TIMESTAMP}",
|
||||||
|
"nginx_status": "${NGINX_STATUS}",
|
||||||
|
"disk_usage_percent": ${DISK_USAGE},
|
||||||
|
"alert_count": ${ALERT_COUNT},
|
||||||
|
"fixed_count": ${FIXED_COUNT},
|
||||||
|
"alerts": ${ALERT_JSON},
|
||||||
|
"auto_fixed": ${FIXED_JSON},
|
||||||
|
"overall": "$([ $ALERT_COUNT -eq 0 ] && echo 'healthy' || echo 'has_issues')"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "===== 📊 巡检结果 ====="
|
||||||
|
if [ $ALERT_COUNT -eq 0 ]; then
|
||||||
|
echo "✅ 全部正常 · 无异常"
|
||||||
|
else
|
||||||
|
echo "⚠️ 发现 ${ALERT_COUNT} 个异常 · 自动修复 ${FIXED_COUNT} 个"
|
||||||
|
fi
|
||||||
|
echo "报告已保存到 ${REPORT}"
|
||||||
|
cat "$REPORT"
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* sync-patrol-to-notion.js — 同步巡检结果到 Notion
|
||||||
|
* 铸渊 · 光湖服务器巡检自动化
|
||||||
|
*
|
||||||
|
* 环境变量:
|
||||||
|
* NOTION_TOKEN — Notion API Token
|
||||||
|
* NOTION_TICKET_DB_ID — 霜砚工单队列 Database ID(异常时自动开工单)
|
||||||
|
*
|
||||||
|
* 读取 patrol-report.json,写入 Notion 工单队列(仅有异常时)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
const NOTION_TOKEN = process.env.NOTION_TOKEN;
|
||||||
|
const TICKET_DB_ID = process.env.NOTION_TICKET_DB_ID;
|
||||||
|
|
||||||
|
if (!NOTION_TOKEN) {
|
||||||
|
console.error('⚠️ 缺少环境变量: NOTION_TOKEN');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const REPORT_FILE = path.join(process.cwd(), 'patrol-report.json');
|
||||||
|
|
||||||
|
function notionRequest(method, endpoint, body) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const data = body ? JSON.stringify(body) : null;
|
||||||
|
const options = {
|
||||||
|
hostname: 'api.notion.com',
|
||||||
|
path: `/v1/${endpoint}`,
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${NOTION_TOKEN}`,
|
||||||
|
'Notion-Version': '2022-06-28',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (data) {
|
||||||
|
options.headers['Content-Length'] = Buffer.byteLength(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', (chunk) => { body += chunk; });
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
resolve({ status: res.statusCode, data: JSON.parse(body) });
|
||||||
|
} catch {
|
||||||
|
resolve({ status: res.statusCode, data: body });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
if (data) req.write(data);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTicket(alert, report) {
|
||||||
|
if (!TICKET_DB_ID) {
|
||||||
|
console.log(` ⚠️ 未配置 NOTION_TICKET_DB_ID,跳过工单创建: ${alert}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await notionRequest('POST', 'pages', {
|
||||||
|
parent: { database_id: TICKET_DB_ID },
|
||||||
|
properties: {
|
||||||
|
title: { title: [{ text: { content: `🔍 服务器巡检异常: ${alert}` } }] },
|
||||||
|
'来源': { select: { name: '铸渊巡检' } },
|
||||||
|
'状态': { select: { name: '待处理' } },
|
||||||
|
'优先级': { select: { name: '高' } },
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
object: 'block',
|
||||||
|
type: 'paragraph',
|
||||||
|
paragraph: {
|
||||||
|
rich_text: [{
|
||||||
|
text: {
|
||||||
|
content: `巡检时间: ${report.timestamp}\nNginx状态: ${report.nginx_status}\n磁盘使用率: ${report.disk_usage_percent}%\n\n异常详情: ${alert}`,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
console.log(` ✅ 工单已创建: ${alert}`);
|
||||||
|
} else {
|
||||||
|
console.log(` ⚠️ 工单创建失败: ${alert} (HTTP ${res.status})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('📝 sync-patrol-to-notion · 开始同步巡检结果');
|
||||||
|
|
||||||
|
if (!fs.existsSync(REPORT_FILE)) {
|
||||||
|
console.log('⚠️ patrol-report.json 不存在,跳过同步');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let report;
|
||||||
|
try {
|
||||||
|
report = JSON.parse(fs.readFileSync(REPORT_FILE, 'utf-8'));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ 解析 patrol-report.json 失败:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📊 巡检状态: ${report.overall}`);
|
||||||
|
console.log(` 异常数: ${report.alert_count} | 自动修复: ${report.fixed_count}`);
|
||||||
|
|
||||||
|
if (report.overall === 'healthy') {
|
||||||
|
console.log('✅ 服务器健康,无需创建工单');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找出未能自动修复的异常 → 开工单
|
||||||
|
const autoFixedSet = new Set(report.auto_fixed || []);
|
||||||
|
const unresolvedAlerts = (report.alerts || []).filter(alert => {
|
||||||
|
// 如果 alert 对应有一条 auto_fixed,说明已修复,不开工单
|
||||||
|
return !report.auto_fixed.some(fix => fix.includes(alert.split(' ')[0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unresolvedAlerts.length === 0) {
|
||||||
|
console.log('✅ 所有异常已自动修复,无需创建工单');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📋 ${unresolvedAlerts.length} 个未修复异常,创建工单...`);
|
||||||
|
|
||||||
|
for (const alert of unresolvedAlerts) {
|
||||||
|
try {
|
||||||
|
await createTicket(alert, report);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(` ⚠️ 工单创建异常: ${alert} (${err.message})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ 同步完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('❌ sync-patrol-to-notion 异常:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue