guanghulab/scripts/sync-patrol-to-notion.js
2026-05-10 13:12:44 +08:00

154 lines
4.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 autoFixed = report.auto_fixed || [];
const unresolvedAlerts = (report.alerts || []).filter(alert => {
// 检查是否有对应的自动修复记录(修复消息中包含完整的异常描述关键词)
return !autoFixed.some(fix => {
const alertKey = alert.replace(/[:].*/g, '').trim();
return fix.includes(alertKey);
});
});
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);
});