/**
* ═══════════════════════════════════════════════════════════
* 📧 铸渊运维守卫 · 通知系统
* ═══════════════════════════════════════════════════════════
*
* 编号: ZY-OPS-NTF-001
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 通知通道:
* 1. QQ邮箱 SMTP(主通道)
* 2. 本地文件日志(始终启用)
* 3. 工单推送到前端(通过 HTTP API)
*/
'use strict';
const nodemailer = require('nodemailer');
const fs = require('fs');
const path = require('path');
const memory = require('./memory');
// ── 全局开关 + 冷却 ──────────────────────────
//
// OPS_EMAIL_ENABLED:
// - 默认 true。设为 'false'/'0' 时所有邮件函数立即 no-op(kill switch)。
// - 用法:`pm2 set ops-agent:OPS_EMAIL_ENABLED false && pm2 restart ops-agent`
//
// OPS_ALERT_COOLDOWN_MINUTES:
// - 默认 360(6 小时)。同一 (service+severity) 的告警与工单邮件在该间隔内只发一次,
// 避免高频持续故障(例如 MCP 连接超时)刷屏。
const COOLDOWN_MINUTES = (() => {
const v = parseInt(process.env.OPS_ALERT_COOLDOWN_MINUTES || '360', 10);
return Number.isFinite(v) && v >= 0 ? v : 360;
})();
const _lastSentAt = new Map(); // key -> epoch ms
function isEmailEnabled() {
// ⚠️ D72 (2026-05-03) 冰朔决策:默认禁用所有邮件告警。
// 服务器持续给冰朔发"大脑服务器异常"邮件,已成为噪音。
// 如需重新启用:显式设置 OPS_EMAIL_ENABLED=true (大小写不敏感)。
// 之前默认是 true,本次翻转默认为 false。
const v = process.env.OPS_EMAIL_ENABLED;
if (v === undefined || v === null || v === '') return false;
const norm = String(v).trim().toLowerCase();
return norm === 'true' || norm === '1' || norm === 'yes' || norm === 'on';
}
function cooldownRemainingMs(key) {
const last = _lastSentAt.get(key);
if (!last) return 0;
const elapsed = Date.now() - last;
const windowMs = COOLDOWN_MINUTES * 60 * 1000;
return elapsed >= windowMs ? 0 : windowMs - elapsed;
}
function markSent(key) {
_lastSentAt.set(key, Date.now());
}
// ── SMTP 传输器(懒初始化) ──────────────
let _transporter = null;
function getSmtpTransporter() {
if (_transporter) return _transporter;
const smtpUser = process.env.ZY_SMTP_USER;
const smtpPass = process.env.ZY_SMTP_PASS;
if (!smtpUser || !smtpPass) {
return null;
}
_transporter = nodemailer.createTransport({
host: 'smtp.qq.com',
port: 465,
secure: true,
auth: {
user: smtpUser,
pass: smtpPass
},
tls: {
rejectUnauthorized: true
}
});
return _transporter;
}
// ── 发送邮件告警 ────────────────────────────
async function sendAlertEmail(subject, htmlBody) {
if (!isEmailEnabled()) {
console.log('[Notifier] OPS_EMAIL_ENABLED=false,跳过邮件发送');
return { sent: false, reason: 'disabled' };
}
const transporter = getSmtpTransporter();
if (!transporter) {
console.log('[Notifier] SMTP 未配置,跳过邮件发送');
return { sent: false, reason: 'smtp_not_configured' };
}
const smtpUser = process.env.ZY_SMTP_USER;
const notifyTo = process.env.OPS_NOTIFY_EMAIL || smtpUser;
try {
const info = await transporter.sendMail({
from: `"铸渊运维守卫" <${smtpUser}>`,
to: notifyTo,
subject: `🛡️ ${subject}`,
html: htmlBody
});
console.log(`[Notifier] 邮件已发送: ${info.messageId}`);
return { sent: true, messageId: info.messageId };
} catch (err) {
console.error(`[Notifier] 邮件发送失败: ${err.message}`);
return { sent: false, reason: err.message };
}
}
// ── 生成告警邮件 HTML ──────────────────────
function buildAlertHTML(title, issues, checkResult) {
const issueRows = issues.map(i => `
| ${i.severity?.toUpperCase() || 'UNKNOWN'} |
${i.service} |
${i.error || i.status} |
`).join('');
return `
🛡️ 铸渊运维守卫 · 告警通知
${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}
${title}
${checkResult?.summary ? `
${checkResult.summary}
` : ''}
铸渊运维守卫 · ZY-OPS-001 · 光湖实验室
`;
}
// ── 告警通知(巡检发现问题时调用) ──────────
async function alertOnIssues(checkResult) {
if (!checkResult.issues || checkResult.issues.length === 0) return;
const criticalIssues = checkResult.issues.filter(i => i.severity === 'critical' || i.severity === 'high');
if (criticalIssues.length === 0) return; // 只对高/紧急级别发邮件
// 冷却:剔除最近已通知过的 (service+severity)
const freshIssues = [];
const skipped = [];
for (const i of criticalIssues) {
const key = `alert:${i.service || 'unknown'}:${i.severity || 'unknown'}`;
const remaining = cooldownRemainingMs(key);
if (remaining > 0) {
skipped.push({ key, remainingMinutes: Math.ceil(remaining / 60000) });
} else {
freshIssues.push({ issue: i, key });
}
}
if (skipped.length > 0) {
console.log(`[Notifier] 冷却中跳过 ${skipped.length} 个告警: ${skipped.map(s => `${s.key}(${s.remainingMinutes}m)`).join(', ')}`);
}
if (freshIssues.length === 0) {
return { sent: false, reason: 'cooldown', skipped: skipped.length };
}
const issuesToSend = freshIssues.map(f => f.issue);
const title = `发现 ${issuesToSend.length} 个严重问题`;
const html = buildAlertHTML(title, issuesToSend, checkResult);
// 发邮件
const emailResult = await sendAlertEmail(title, html);
// 仅在真正发出去时记录冷却时间戳,避免 SMTP 失败导致永久静默
if (emailResult && emailResult.sent) {
for (const f of freshIssues) markSent(f.key);
}
// 记录事件
memory.logEvent('alert', {
title,
issueCount: issuesToSend.length,
skippedByCooldown: skipped.length,
emailSent: emailResult.sent,
summary: checkResult.summary
});
return emailResult;
}
// ── 工单升级通知 ────────────────────────────
async function notifyTicketCreated(ticket) {
// 冷却兜底:同一 (service+severity) 在窗口内只发一次工单邮件
const cooldownKey = `ticket:${ticket.relatedService || ticket.title || 'unknown'}:${ticket.severity || 'unknown'}`;
const remaining = cooldownRemainingMs(cooldownKey);
if (remaining > 0) {
console.log(`[Notifier] 工单邮件冷却中(${Math.ceil(remaining / 60000)}分钟剩余),跳过 ${ticket.id}`);
return { sent: false, reason: 'cooldown' };
}
const html = `
🎫 运维工单 · ${ticket.id}
${ticket.title}
严重度: ${ticket.severity}
处理方向: ${ticket.direction}
诊断: ${ticket.diagnosis}
${ticket.suggestedFix ? `
建议: ${ticket.suggestedFix}
` : ''}
自动修复尝试: ${ticket.autoRepairAttempts}次(均未成功)
`;
const result = await sendAlertEmail(`新工单 ${ticket.id}: ${ticket.title}`, html);
if (result && result.sent) markSent(cooldownKey);
return result;
}
// ── 每日报告邮件 ────────────────────────────
async function sendDailyReport(report) {
const serviceRows = (report.services || []).map(s => `
| ${s.name} |
${s.status} |
${s.latency || '-'}ms |
`).join('');
const html = `
📊 光湖每日健康报告
${report.report_id}
服务状态
${report.resources ? `
系统资源
内存: ${report.resources.memory.used_pct}% ·
磁盘: ${report.resources.disk.used_pct}% ·
负载: ${report.resources.load?.[0]?.toFixed(2) || '-'} ·
运行: ${report.resources.uptime_hours}h
` : ''}
${report.summary}
${report.recommendations ? `
建议
${report.recommendations.map(r => `- ${r}
`).join('')}
` : ''}
`;
return sendAlertEmail(`每日报告 ${report.report_id}`, html);
}
module.exports = {
sendAlertEmail,
alertOnIssues,
notifyTicketCreated,
sendDailyReport,
getSmtpTransporter,
isEmailEnabled,
// 供测试使用
_resetCooldownForTests: () => _lastSentAt.clear()
};