202 lines
7.0 KiB
JavaScript
202 lines
7.0 KiB
JavaScript
|
|
// scripts/server-diagnose-report.js
|
|||
|
|
// 铸渊 · PM2 服务诊断报告 → Notion 工单
|
|||
|
|
//
|
|||
|
|
// 将 PM2 诊断和系统健康检查结果写入 Notion 工单队列。
|
|||
|
|
// 由 pm2-server-diagnose.yml 工作流调用。
|
|||
|
|
//
|
|||
|
|
// 环境变量:
|
|||
|
|
// NOTION_TOKEN Notion API token
|
|||
|
|
// NOTION_TICKET_DB_ID 工单队列数据库 ID
|
|||
|
|
// PM2_REPORT PM2 诊断输出
|
|||
|
|
// CLEANUP_REPORT 清理操作输出(可选)
|
|||
|
|
// HEALTH_REPORT 健康检查输出
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const https = require('https');
|
|||
|
|
|
|||
|
|
const NOTION_TOKEN = process.env.NOTION_TOKEN || '';
|
|||
|
|
const NOTION_TICKET_DB_ID = process.env.NOTION_TICKET_DB_ID || '';
|
|||
|
|
const PM2_REPORT = process.env.PM2_REPORT || '';
|
|||
|
|
const CLEANUP_REPORT = process.env.CLEANUP_REPORT || '';
|
|||
|
|
const HEALTH_REPORT = process.env.HEALTH_REPORT || '';
|
|||
|
|
|
|||
|
|
const NOTION_VERSION = '2022-06-28';
|
|||
|
|
const NOTION_API_HOSTNAME = 'api.notion.com';
|
|||
|
|
const NOTION_RICH_TEXT_MAX = 2000;
|
|||
|
|
const MAX_PM2_REPORT_LENGTH = 3000;
|
|||
|
|
const MAX_CLEANUP_REPORT_LENGTH = 2000;
|
|||
|
|
const MAX_HEALTH_REPORT_LENGTH = 3000;
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════
|
|||
|
|
// Notion API 工具
|
|||
|
|
// ══════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
function notionPost(endpoint, body) {
|
|||
|
|
return new Promise(function (resolve, reject) {
|
|||
|
|
var payload = JSON.stringify(body);
|
|||
|
|
var opts = {
|
|||
|
|
hostname: NOTION_API_HOSTNAME,
|
|||
|
|
port: 443,
|
|||
|
|
path: endpoint,
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Authorization': 'Bearer ' + NOTION_TOKEN,
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Notion-Version': NOTION_VERSION,
|
|||
|
|
'Content-Length': Buffer.byteLength(payload),
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
var req = https.request(opts, function (res) {
|
|||
|
|
var data = '';
|
|||
|
|
res.on('data', function (chunk) { data += chunk; });
|
|||
|
|
res.on('end', function () {
|
|||
|
|
try {
|
|||
|
|
var parsed = JSON.parse(data);
|
|||
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|||
|
|
resolve(parsed);
|
|||
|
|
} else {
|
|||
|
|
reject(new Error('Notion API ' + res.statusCode + ': ' + (parsed.message || data)));
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
reject(new Error('Notion API parse error: ' + data));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
req.on('error', reject);
|
|||
|
|
req.write(payload);
|
|||
|
|
req.end();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function richTextChunks(content) {
|
|||
|
|
var str = String(content || '');
|
|||
|
|
var chunks = [];
|
|||
|
|
for (var i = 0; i < str.length; i += NOTION_RICH_TEXT_MAX) {
|
|||
|
|
chunks.push({ type: 'text', text: { content: str.slice(i, i + NOTION_RICH_TEXT_MAX) } });
|
|||
|
|
}
|
|||
|
|
if (chunks.length === 0) {
|
|||
|
|
chunks.push({ type: 'text', text: { content: '' } });
|
|||
|
|
}
|
|||
|
|
return chunks;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════
|
|||
|
|
// 构建报告内容
|
|||
|
|
// ══════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
function buildReportContent() {
|
|||
|
|
var now = new Date().toISOString();
|
|||
|
|
var dateStr = now.slice(0, 10);
|
|||
|
|
|
|||
|
|
var sections = [];
|
|||
|
|
|
|||
|
|
sections.push('## 📋 PM2 服务排查报告 · ' + dateStr);
|
|||
|
|
sections.push('');
|
|||
|
|
sections.push('| 字段 | 值 |');
|
|||
|
|
sections.push('|------|-----|');
|
|||
|
|
sections.push('| 执行者 | 铸渊Agent |');
|
|||
|
|
sections.push('| 执行时间 | ' + now + ' |');
|
|||
|
|
sections.push('| 来源 | GitHub Actions · pm2-server-diagnose |');
|
|||
|
|
sections.push('');
|
|||
|
|
|
|||
|
|
if (PM2_REPORT) {
|
|||
|
|
sections.push('## 阶段一 · PM2 诊断结果');
|
|||
|
|
sections.push('');
|
|||
|
|
sections.push(PM2_REPORT.slice(0, MAX_PM2_REPORT_LENGTH));
|
|||
|
|
sections.push('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (CLEANUP_REPORT) {
|
|||
|
|
sections.push('## 清理操作结果');
|
|||
|
|
sections.push('');
|
|||
|
|
sections.push(CLEANUP_REPORT.slice(0, MAX_CLEANUP_REPORT_LENGTH));
|
|||
|
|
sections.push('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (HEALTH_REPORT) {
|
|||
|
|
sections.push('## 阶段二 · 全系统健康检查');
|
|||
|
|
sections.push('');
|
|||
|
|
sections.push(HEALTH_REPORT.slice(0, MAX_HEALTH_REPORT_LENGTH));
|
|||
|
|
sections.push('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return sections.join('\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ══════════════════════════════════════════════════════════
|
|||
|
|
// 主函数 — 创建 Notion 诊断工单
|
|||
|
|
// ══════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
async function createDiagnoseTicket() {
|
|||
|
|
if (!NOTION_TOKEN || !NOTION_TICKET_DB_ID) {
|
|||
|
|
console.log('⚠️ Notion credentials not configured, skipping ticket creation');
|
|||
|
|
console.log(' NOTION_TOKEN: ' + (NOTION_TOKEN ? '✅' : '❌'));
|
|||
|
|
console.log(' NOTION_TICKET_DB_ID: ' + (NOTION_TICKET_DB_ID ? '✅' : '❌'));
|
|||
|
|
process.exit(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var now = new Date();
|
|||
|
|
var dateStr = now.toISOString().slice(0, 10);
|
|||
|
|
var title = 'PM2服务排查报告 · ' + dateStr;
|
|||
|
|
var contentText = buildReportContent();
|
|||
|
|
|
|||
|
|
// 将内容拆分成多个段落块(Notion 单个 rich_text 限制 2000 字符)
|
|||
|
|
var contentBlocks = [];
|
|||
|
|
var remaining = contentText;
|
|||
|
|
|
|||
|
|
while (remaining.length > 0) {
|
|||
|
|
var chunk = remaining.slice(0, NOTION_RICH_TEXT_MAX);
|
|||
|
|
remaining = remaining.slice(NOTION_RICH_TEXT_MAX);
|
|||
|
|
contentBlocks.push({
|
|||
|
|
object: 'block',
|
|||
|
|
type: 'paragraph',
|
|||
|
|
paragraph: {
|
|||
|
|
rich_text: [{ type: 'text', text: { content: chunk } }],
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 确保至少有一个内容块
|
|||
|
|
if (contentBlocks.length === 0) {
|
|||
|
|
contentBlocks.push({
|
|||
|
|
object: 'block',
|
|||
|
|
type: 'paragraph',
|
|||
|
|
paragraph: {
|
|||
|
|
rich_text: [{ type: 'text', text: { content: '诊断报告(无详细输出)' } }],
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var body = {
|
|||
|
|
parent: { database_id: NOTION_TICKET_DB_ID },
|
|||
|
|
properties: {
|
|||
|
|
'标题': { title: [{ type: 'text', text: { content: title } }] },
|
|||
|
|
'操作类型': { select: { name: '其他' } },
|
|||
|
|
'提交者': { rich_text: [{ type: 'text', text: { content: '铸渊Agent' } }] },
|
|||
|
|
'状态': { select: { name: '已完成' } },
|
|||
|
|
'优先级': { select: { name: 'P1' } },
|
|||
|
|
},
|
|||
|
|
children: contentBlocks,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
console.log('📋 创建 PM2 诊断工单...');
|
|||
|
|
console.log(' 标题: ' + title);
|
|||
|
|
console.log(' 数据库: ' + NOTION_TICKET_DB_ID);
|
|||
|
|
console.log(' 内容长度: ' + contentText.length + ' 字符');
|
|||
|
|
console.log(' 内容块数: ' + contentBlocks.length);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
var result = await notionPost('/v1/pages', body);
|
|||
|
|
console.log('✅ Notion 诊断工单已创建: ' + result.id);
|
|||
|
|
console.log(' URL: ' + (result.url || 'N/A'));
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('❌ 工单创建失败: ' + err.message);
|
|||
|
|
// 不以失败退出,诊断本身已成功
|
|||
|
|
console.log('⚠️ 诊断结果已输出到 Actions 日志,Notion 写入失败不影响诊断');
|
|||
|
|
process.exit(0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createDiagnoseTicket();
|