/** * ═══════════════════════════════════════════════════════════ * 📧 铸渊运维守卫 · 通知系统 * ═══════════════════════════════════════════════════════════ * * 编号: 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}

${issueRows}
严重度 服务 问题
${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}

服务状态

${serviceRows}
服务 状态 延迟
${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 ? `

建议

` : ''}

铸渊运维守卫 · 每日自动报告

`; return sendAlertEmail(`每日报告 ${report.report_id}`, html); } module.exports = { sendAlertEmail, alertOnIssues, notifyTicketCreated, sendDailyReport, getSmtpTransporter, isEmailEnabled, // 供测试使用 _resetCooldownForTests: () => _lastSentAt.clear() };