154 lines
4.3 KiB
JavaScript
154 lines
4.3 KiB
JavaScript
|
|
#!/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);
|
|||
|
|
});
|