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:
copilot-swe-agent[bot] 2026-03-15 12:56:03 +00:00
parent bff9144e9a
commit 19e35fac73
4 changed files with 472 additions and 0 deletions

133
.github/workflows/server-patrol.yml vendored Normal file
View File

@ -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同步失败不阻断"

View File

189
scripts/server-patrol.sh Normal file
View File

@ -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} ====="
# ━━━ 检查项1Nginx 运行状态 ━━━
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
# ━━━ 检查项3PM2 进程状态 ━━━
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 " ✅ 沙盒权限检查完成"
# ━━━ 检查项6SSL证书有效期 ━━━
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
# ━━━ 检查项7Nginx配置语法 ━━━
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"

View File

@ -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);
});