#!/usr/bin/env node /** * 企业微信消息推送模拟测试 * 模拟 29.237.253.1 服务器 OEM 诊断和 OEM 维修故障完整生命周期 * 每条消息间隔 5 秒 */ const WEBHOOK_URL = process.env.WECOM_WEBHOOK_URL || (() => { throw new Error('WECOM_WEBHOOK_URL not set') })() const SERVER_IP = '29.237.253.1' const SERVER_SN = '210235A4M7H241000098' const RACK = 'D01-404-I02-3U' const INTERVAL = 5000 // 随机故障内容 const OEM_DIAG_FAULTS = [ 'agent已经5分钟没有上报数据', 'GPU温度传感器读数异常,超过阈值', '系统日志显示内核恐慌(Kernel Panic)', 'NVMe硬盘SMART检测到坏块', '内存ECC错误计数超标', ] const OEM_REPAIR_FAULTS = [ 'GPU缓存发生不可纠正错误', 'RAID阵列降级,一块硬盘离线', '电源模块冗余失效,单电源运行', '主板BMC固件异常,无法远程管理', '网卡绑定故障,网络中断', ] const randomFrom = (arr) => arr[Math.floor(Math.random() * arr.length)] const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) // 北京时间格式化 function bjTime(date = new Date()) { const d = new Date(date.getTime() + 8 * 3600_000) return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')} ${String(d.getUTCHours()).padStart(2, '0')}:${String(d.getUTCMinutes()).padStart(2, '0')}:${String(d.getUTCSeconds()).padStart(2, '0')}` } // 向前推算时间 function minusHours(date, hours) { return new Date(date.getTime() - hours * 3600_000) } function minusMinutes(date, minutes) { return new Date(date.getTime() - minutes * 60_000) } async function sendText(text) { const res = await fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ msgtype: 'text', text: { content: text } }), }) const data = await res.json() return data.errcode === 0 } async function send(label, text) { console.log(`\n📤 [${label}]`) console.log(text) console.log('─'.repeat(50)) const ok = await sendText(text) console.log(ok ? '✅ 发送成功' : '❌ 发送失败') await sleep(INTERVAL) } // ============================================================ // OEM 诊断完整生命周期 // ============================================================ async function runOemDiag() { const now = new Date() const faultTime = minusHours(now, 2) const orderNum = 'S' + Date.now().toString().slice(-10) const fault = randomFrom(OEM_DIAG_FAULTS) const oemDeadline = minusHours(faultTime, -6) // 故障后6小时到期 console.log('\n' + '═'.repeat(60)) console.log(' OEM 诊断 — 完整生命周期模拟') console.log('═'.repeat(60)) console.log(`工单号: ${orderNum}`) console.log(`故障时间: ${bjTime(faultTime)}`) console.log(`到期时间: ${bjTime(oemDeadline)}`) // 1. 初始故障通知 await send('初始通知', [ `【服务器故障单】 - OEM诊断`, '', `工单号:${orderNum}`, `服务器IP:${SERVER_IP}`, `服务器SN:${SERVER_SN}`, `机架位置:${RACK}`, `故障时间:${bjTime(faultTime)}`, `到期时间:${bjTime(oemDeadline)}`, `故障信息:${fault}`, ].join('\n')) // 2. 每小时提醒(剩余4小时) const t4h = minusMinutes(oemDeadline, 240) await send('每小时提醒(剩余4小时0分)', [ `⏰ OEM诊断工单提醒(剩余4小时0分)`, '', `工单号:${orderNum}`, `服务器IP:${SERVER_IP}`, `服务器SN:${SERVER_SN}`, `机架位置:${RACK}`, `到期时间:${bjTime(oemDeadline)}`, '', `服务器尚未录入恢复时间,请及时处理!`, ].join('\n')) // 3. 30分钟到期提醒 await send('30分钟到期提醒', [ `⚠️ OEM诊断工单即将到期(剩余30分钟)`, '', `工单号:${orderNum}`, `服务器IP:${SERVER_IP}`, `服务器SN:${SERVER_SN}`, `机架位置:${RACK}`, `到期时间:${bjTime(oemDeadline)}`, '', `服务器尚未录入恢复时间,请尽快处理!`, ].join('\n')) // 4. 超时提醒 await send('超时提醒', [ `⚠️ OEM诊断工单已超时`, '', `工单号:${orderNum}`, `服务器IP:${SERVER_IP}`, `服务器SN:${SERVER_SN}`, `机架位置:${RACK}`, `到期时间:${bjTime(oemDeadline)}`, '', `服务器尚未录入恢复时间,请尽快处理!`, ].join('\n')) // 5. 结单消息 const closeTime = minusMinutes(now, -30) const durationMin = Math.floor((closeTime.getTime() - faultTime.getTime()) / 60000) const durH = Math.floor(durationMin / 60) const durM = durationMin % 60 await send('结单消息', [ `✅ ${orderNum}工单已结单`, '', `服务器IP:${SERVER_IP}`, `服务器SN:${SERVER_SN}`, `机架位置:${RACK}`, `故障类型:OEM诊断`, `故障时间:${bjTime(faultTime)}`, `结单时间:${bjTime(closeTime)}`, `本次处理时长:${durH}小时${durM}分钟`, ].join('\n')) } // ============================================================ // OEM 维修完整生命周期 // ============================================================ async function runOemRepair() { const now = new Date() const faultTime = minusHours(now, 4) const orderNum = 'S' + Date.now().toString().slice(-10) const fault = randomFrom(OEM_REPAIR_FAULTS) // 五档截止时间 const tiers = [ { name: '第一档', desc: '99%(含)以上(不扣费)', hours: 8, penalty: '10%' }, { name: '第二档', desc: '99%-97%(含)(扣10%)', hours: 24, penalty: '25%' }, { name: '第三档', desc: '97%-95%(含)(扣25%)', hours: 48, penalty: '50%' }, { name: '第四档', desc: '95%-90%(含)(扣50%)', hours: 72, penalty: '100%' }, { name: '第五档', desc: '90%(不含)以下(扣100%)', hours: 168, penalty: '' }, ] const deadlines = tiers.map(t => ({ ...t, deadline: new Date(faultTime.getTime() + t.hours * 3600_000), })) console.log('\n' + '═'.repeat(60)) console.log(' OEM 维修 — 完整生命周期模拟') console.log('═'.repeat(60)) console.log(`工单号: ${orderNum}`) console.log(`故障时间: ${bjTime(faultTime)}`) deadlines.forEach(d => console.log(` ${d.name}截止: ${bjTime(d.deadline)} (${d.hours}h)`)) // 1. 初始故障通知(含5档截止时间) const tierLines = deadlines.map((d, i) => { const emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣'] return `${emojis[i]} ${d.desc}\n 截止时间:${bjTime(d.deadline)}(${d.hours}小时)` }).join('\n\n') await send('初始通知', [ `【服务器故障单】 - OEM维修`, '', `工单号:${orderNum}`, `服务器IP:${SERVER_IP}`, `服务器SN:${SERVER_SN}`, `机架位置:${RACK}`, `故障时间:${bjTime(faultTime)}`, `故障信息:${fault}`, '', `📊 服务可用性各档位截止时间(基于月度内累计故障时长计算):`, '', tierLines, ].join('\n')) // 2-5. 每个档位的提前1小时提醒 for (let i = 0; i < 4; i++) { const t = deadlines[i] const isFinalTier = t.name === '第四档' const tierLabel = isFinalTier ? '全额扣费(100%)' : `${t.name}(${t.penalty})` await send(`${t.name}提前1小时提醒`, [ `OEM维修工单超时提醒(距离${tierLabel}还剩1小时)`, '', `工单号:${orderNum}`, `服务器IP:${SERVER_IP}`, `服务器SN:${SERVER_SN}`, `机架位置:${RACK}`, `${t.name}到期时间:${bjTime(t.deadline)}`, '', `服务器尚未录入恢复时间,请及时处理!`, ].join('\n')) } // 6-9. 每个档位的超时提醒 for (let i = 0; i < 4; i++) { const t = deadlines[i] const isFinalTier = t.name === '第四档' const title = isFinalTier ? `OEM维修工单超时提醒(${t.name}已超时,已触发全额扣费(100%))` : `OEM维修工单超时提醒(${t.name}已超时,达到${t.name}扣${t.penalty})` await send(`${t.name}超时提醒`, [ title, '', `工单号:${orderNum}`, `服务器IP:${SERVER_IP}`, `服务器SN:${SERVER_SN}`, `机架位置:${RACK}`, `${t.name}到期时间:${bjTime(t.deadline)}`, '', `服务器尚未录入恢复时间,请尽快处理!`, ].join('\n')) } // 10. 结单消息(含本月统计) const closeTime = minusMinutes(now, -30) const durationMin = Math.floor((closeTime.getTime() - faultTime.getTime()) / 60000) const durH = Math.floor(durationMin / 60) const durM = durationMin % 60 await send('结单消息', [ `✅ ${orderNum}工单已结单`, '', `服务器IP:${SERVER_IP}`, `服务器SN:${SERVER_SN}`, `机架位置:${RACK}`, `故障类型:OEM维修`, `故障时间:${bjTime(faultTime)}`, `结单时间:${bjTime(closeTime)}`, `本次处理时长:${durH}小时${durM}分钟`, `本月故障次数:3次`, `本月总处理时长:18小时30分钟`, ].join('\n')) } // ============================================================ // 主流程 // ============================================================ async function main() { console.log('🚀 开始模拟企业微信消息推送') console.log(`Webhook: ${WEBHOOK_URL.slice(0, 50)}...`) console.log(`服务器: ${SERVER_IP} (${SERVER_SN})`) console.log(`机架: ${RACK}`) console.log(`间隔: ${INTERVAL / 1000}s`) console.log('\n' + '═'.repeat(60)) console.log(' 第一部分:OEM 诊断(5 条消息)') console.log('═'.repeat(60)) await runOemDiag() console.log('\n⏸️ 等待 10 秒...\n') await sleep(10000) console.log('═'.repeat(60)) console.log(' 第二部分:OEM 维修(10 条消息)') console.log('═'.repeat(60)) await runOemRepair() console.log('\n' + '═'.repeat(60)) console.log('✅ 全部消息发送完成(共 15 条)') console.log('═'.repeat(60)) } main().catch(e => { console.error(e); process.exit(1) })