website-scripts/test-wechat-messages.mjs

292 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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) })