mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:34:02 +00:00
893 lines
23 KiB
JavaScript
Executable File
893 lines
23 KiB
JavaScript
Executable File
const axios = require('axios')
|
||
const crypto = require('crypto')
|
||
const nodemailer = require('nodemailer')
|
||
const { HttpsProxyAgent } = require('https-proxy-agent')
|
||
const { SocksProxyAgent } = require('socks-proxy-agent')
|
||
const logger = require('../utils/logger')
|
||
const webhookConfigService = require('./webhookConfigService')
|
||
const { getISOStringWithTimezone } = require('../utils/dateHelper')
|
||
const appConfig = require('../../config/config')
|
||
|
||
class WebhookService {
|
||
constructor() {
|
||
this.platformHandlers = {
|
||
wechat_work: this.sendToWechatWork.bind(this),
|
||
dingtalk: this.sendToDingTalk.bind(this),
|
||
feishu: this.sendToFeishu.bind(this),
|
||
slack: this.sendToSlack.bind(this),
|
||
discord: this.sendToDiscord.bind(this),
|
||
telegram: this.sendToTelegram.bind(this),
|
||
custom: this.sendToCustom.bind(this),
|
||
bark: this.sendToBark.bind(this),
|
||
smtp: this.sendToSMTP.bind(this)
|
||
}
|
||
this.timezone = appConfig.system.timezone || 'Asia/Shanghai'
|
||
}
|
||
|
||
/**
|
||
* 发送通知到所有启用的平台
|
||
*/
|
||
async sendNotification(type, data) {
|
||
try {
|
||
const config = await webhookConfigService.getConfig()
|
||
|
||
// 检查是否启用webhook
|
||
if (!config.enabled) {
|
||
logger.debug('Webhook通知已禁用')
|
||
return
|
||
}
|
||
|
||
// 检查通知类型是否启用(test类型始终允许发送)
|
||
if (type !== 'test' && config.notificationTypes && !config.notificationTypes[type]) {
|
||
logger.debug(`通知类型 ${type} 已禁用`)
|
||
return
|
||
}
|
||
|
||
// 获取启用的平台
|
||
const enabledPlatforms = await webhookConfigService.getEnabledPlatforms()
|
||
if (enabledPlatforms.length === 0) {
|
||
logger.debug('没有启用的webhook平台')
|
||
return
|
||
}
|
||
|
||
logger.info(`📢 发送 ${type} 通知到 ${enabledPlatforms.length} 个平台`)
|
||
|
||
// 并发发送到所有平台
|
||
const promises = enabledPlatforms.map((platform) =>
|
||
this.sendToPlatform(platform, type, data, config.retrySettings)
|
||
)
|
||
|
||
const results = await Promise.allSettled(promises)
|
||
|
||
// 记录结果
|
||
const succeeded = results.filter((r) => r.status === 'fulfilled').length
|
||
const failed = results.filter((r) => r.status === 'rejected').length
|
||
|
||
if (failed > 0) {
|
||
logger.warn(`⚠️ Webhook通知: ${succeeded}成功, ${failed}失败`)
|
||
} else {
|
||
logger.info(`✅ 所有webhook通知发送成功`)
|
||
}
|
||
|
||
return { succeeded, failed }
|
||
} catch (error) {
|
||
logger.error('发送webhook通知失败:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送到特定平台
|
||
*/
|
||
async sendToPlatform(platform, type, data, retrySettings) {
|
||
try {
|
||
const handler = this.platformHandlers[platform.type]
|
||
if (!handler) {
|
||
throw new Error(`不支持的平台类型: ${platform.type}`)
|
||
}
|
||
|
||
// 使用平台特定的处理器
|
||
await this.retryWithBackoff(
|
||
() => handler(platform, type, data),
|
||
retrySettings?.maxRetries || 3,
|
||
retrySettings?.retryDelay || 1000
|
||
)
|
||
|
||
logger.info(`✅ 成功发送到 ${platform.name || platform.type}`)
|
||
} catch (error) {
|
||
logger.error(`❌ 发送到 ${platform.name || platform.type} 失败:`, error.message)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 企业微信webhook
|
||
*/
|
||
async sendToWechatWork(platform, type, data) {
|
||
const content = this.formatMessageForWechatWork(type, data)
|
||
|
||
const payload = {
|
||
msgtype: 'markdown',
|
||
markdown: {
|
||
content
|
||
}
|
||
}
|
||
|
||
await this.sendHttpRequest(platform.url, payload, platform.timeout || 10000)
|
||
}
|
||
|
||
/**
|
||
* 钉钉webhook
|
||
*/
|
||
async sendToDingTalk(platform, type, data) {
|
||
const content = this.formatMessageForDingTalk(type, data)
|
||
|
||
let { url } = platform
|
||
const payload = {
|
||
msgtype: 'markdown',
|
||
markdown: {
|
||
title: this.getNotificationTitle(type),
|
||
text: content
|
||
}
|
||
}
|
||
|
||
// 如果启用签名
|
||
if (platform.enableSign && platform.secret) {
|
||
const timestamp = Date.now()
|
||
const sign = this.generateDingTalkSign(platform.secret, timestamp)
|
||
url = `${url}×tamp=${timestamp}&sign=${encodeURIComponent(sign)}`
|
||
}
|
||
|
||
await this.sendHttpRequest(url, payload, platform.timeout || 10000)
|
||
}
|
||
|
||
/**
|
||
* 飞书webhook
|
||
*/
|
||
async sendToFeishu(platform, type, data) {
|
||
const content = this.formatMessageForFeishu(type, data)
|
||
|
||
const payload = {
|
||
msg_type: 'interactive',
|
||
card: {
|
||
elements: [
|
||
{
|
||
tag: 'markdown',
|
||
content
|
||
}
|
||
],
|
||
header: {
|
||
title: {
|
||
tag: 'plain_text',
|
||
content: this.getNotificationTitle(type)
|
||
},
|
||
template: this.getFeishuCardColor(type)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果启用签名
|
||
if (platform.enableSign && platform.secret) {
|
||
const timestamp = Math.floor(Date.now() / 1000)
|
||
const sign = this.generateFeishuSign(platform.secret, timestamp)
|
||
payload.timestamp = timestamp.toString()
|
||
payload.sign = sign
|
||
}
|
||
|
||
await this.sendHttpRequest(platform.url, payload, platform.timeout || 10000)
|
||
}
|
||
|
||
/**
|
||
* Slack webhook
|
||
*/
|
||
async sendToSlack(platform, type, data) {
|
||
const text = this.formatMessageForSlack(type, data)
|
||
|
||
const payload = {
|
||
text,
|
||
username: 'Claude Relay Service',
|
||
icon_emoji: this.getSlackEmoji(type)
|
||
}
|
||
|
||
await this.sendHttpRequest(platform.url, payload, platform.timeout || 10000)
|
||
}
|
||
|
||
/**
|
||
* Discord webhook
|
||
*/
|
||
async sendToDiscord(platform, type, data) {
|
||
const embed = this.formatMessageForDiscord(type, data)
|
||
|
||
const payload = {
|
||
username: 'Claude Relay Service',
|
||
embeds: [embed]
|
||
}
|
||
|
||
await this.sendHttpRequest(platform.url, payload, platform.timeout || 10000)
|
||
}
|
||
|
||
/**
|
||
* 自定义webhook
|
||
*/
|
||
async sendToCustom(platform, type, data) {
|
||
// 使用通用格式
|
||
const payload = {
|
||
type,
|
||
service: 'claude-relay-service',
|
||
timestamp: getISOStringWithTimezone(new Date()),
|
||
data
|
||
}
|
||
|
||
await this.sendHttpRequest(platform.url, payload, platform.timeout || 10000)
|
||
}
|
||
|
||
/**
|
||
* Telegram Bot 通知
|
||
*/
|
||
async sendToTelegram(platform, type, data) {
|
||
if (!platform.botToken) {
|
||
throw new Error('缺少 Telegram 机器人 Token')
|
||
}
|
||
if (!platform.chatId) {
|
||
throw new Error('缺少 Telegram Chat ID')
|
||
}
|
||
|
||
const baseUrl = this.normalizeTelegramApiBase(platform.apiBaseUrl)
|
||
const apiUrl = `${baseUrl}/bot${platform.botToken}/sendMessage`
|
||
const payload = {
|
||
chat_id: platform.chatId,
|
||
text: this.formatMessageForTelegram(type, data),
|
||
disable_web_page_preview: true
|
||
}
|
||
|
||
const axiosOptions = this.buildTelegramAxiosOptions(platform)
|
||
|
||
const response = await this.sendHttpRequest(
|
||
apiUrl,
|
||
payload,
|
||
platform.timeout || 10000,
|
||
axiosOptions
|
||
)
|
||
if (!response || response.ok !== true) {
|
||
throw new Error(`Telegram API 错误: ${response?.description || '未知错误'}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Bark webhook
|
||
*/
|
||
async sendToBark(platform, type, data) {
|
||
const payload = {
|
||
device_key: platform.deviceKey,
|
||
title: this.getNotificationTitle(type),
|
||
body: this.formatMessageForBark(type, data),
|
||
level: platform.level || this.getBarkLevel(type),
|
||
sound: platform.sound || this.getBarkSound(type),
|
||
group: platform.group || 'claude-relay',
|
||
badge: 1
|
||
}
|
||
|
||
// 添加可选参数
|
||
if (platform.icon) {
|
||
payload.icon = platform.icon
|
||
}
|
||
|
||
if (platform.clickUrl) {
|
||
payload.url = platform.clickUrl
|
||
}
|
||
|
||
const url = platform.serverUrl || 'https://api.day.app/push'
|
||
await this.sendHttpRequest(url, payload, platform.timeout || 10000)
|
||
}
|
||
|
||
/**
|
||
* SMTP邮件通知
|
||
*/
|
||
async sendToSMTP(platform, type, data) {
|
||
try {
|
||
// 创建SMTP传输器
|
||
const transporter = nodemailer.createTransport({
|
||
host: platform.host,
|
||
port: platform.port || 587,
|
||
secure: platform.secure || false, // true for 465, false for other ports
|
||
auth: {
|
||
user: platform.user,
|
||
pass: platform.pass
|
||
},
|
||
// 可选的TLS配置
|
||
tls: platform.ignoreTLS ? { rejectUnauthorized: false } : undefined,
|
||
// 连接超时
|
||
connectionTimeout: platform.timeout || 10000
|
||
})
|
||
|
||
// 构造邮件内容
|
||
const subject = this.getNotificationTitle(type)
|
||
const htmlContent = this.formatMessageForEmail(type, data)
|
||
const textContent = this.formatMessageForEmailText(type, data)
|
||
|
||
// 邮件选项
|
||
const mailOptions = {
|
||
from: platform.from || platform.user, // 发送者
|
||
to: platform.to, // 接收者(必填)
|
||
subject: `[Claude Relay Service] ${subject}`,
|
||
text: textContent,
|
||
html: htmlContent
|
||
}
|
||
|
||
// 发送邮件
|
||
const info = await transporter.sendMail(mailOptions)
|
||
logger.info(`✅ 邮件发送成功: ${info.messageId}`)
|
||
|
||
return info
|
||
} catch (error) {
|
||
logger.error('SMTP邮件发送失败:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送HTTP请求
|
||
*/
|
||
async sendHttpRequest(url, payload, timeout, axiosOptions = {}) {
|
||
const headers = {
|
||
'Content-Type': 'application/json',
|
||
'User-Agent': 'claude-relay-service/2.0',
|
||
...(axiosOptions.headers || {})
|
||
}
|
||
|
||
const response = await axios.post(url, payload, {
|
||
timeout,
|
||
...axiosOptions,
|
||
headers
|
||
})
|
||
|
||
if (response.status < 200 || response.status >= 300) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||
}
|
||
|
||
return response.data
|
||
}
|
||
|
||
/**
|
||
* 重试机制
|
||
*/
|
||
async retryWithBackoff(fn, maxRetries, baseDelay) {
|
||
let lastError
|
||
|
||
for (let i = 0; i < maxRetries; i++) {
|
||
try {
|
||
return await fn()
|
||
} catch (error) {
|
||
lastError = error
|
||
|
||
if (i < maxRetries - 1) {
|
||
const delay = baseDelay * Math.pow(2, i) // 指数退避
|
||
logger.debug(`🔄 重试 ${i + 1}/${maxRetries},等待 ${delay}ms`)
|
||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||
}
|
||
}
|
||
}
|
||
|
||
throw lastError
|
||
}
|
||
|
||
/**
|
||
* 生成钉钉签名
|
||
*/
|
||
generateDingTalkSign(secret, timestamp) {
|
||
const stringToSign = `${timestamp}\n${secret}`
|
||
const hmac = crypto.createHmac('sha256', secret)
|
||
hmac.update(stringToSign)
|
||
return hmac.digest('base64')
|
||
}
|
||
|
||
/**
|
||
* 生成飞书签名
|
||
*/
|
||
generateFeishuSign(secret, timestamp) {
|
||
const stringToSign = `${timestamp}\n${secret}`
|
||
const hmac = crypto.createHmac('sha256', stringToSign)
|
||
hmac.update('')
|
||
return hmac.digest('base64')
|
||
}
|
||
|
||
/**
|
||
* 格式化企业微信消息
|
||
*/
|
||
formatMessageForWechatWork(type, data) {
|
||
const title = this.getNotificationTitle(type)
|
||
const details = this.formatNotificationDetails(data)
|
||
return (
|
||
`## ${title}\n\n` +
|
||
`> **服务**: Claude Relay Service\n` +
|
||
`> **时间**: ${new Date().toLocaleString('zh-CN', { timeZone: this.timezone })}\n\n${details}`
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 格式化钉钉消息
|
||
*/
|
||
formatMessageForDingTalk(type, data) {
|
||
const details = this.formatNotificationDetails(data)
|
||
|
||
return (
|
||
`#### 服务: Claude Relay Service\n` +
|
||
`#### 时间: ${new Date().toLocaleString('zh-CN', { timeZone: this.timezone })}\n\n${details}`
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 格式化飞书消息
|
||
*/
|
||
formatMessageForFeishu(type, data) {
|
||
return this.formatNotificationDetails(data)
|
||
}
|
||
|
||
/**
|
||
* 格式化Slack消息
|
||
*/
|
||
formatMessageForSlack(type, data) {
|
||
const title = this.getNotificationTitle(type)
|
||
const details = this.formatNotificationDetails(data)
|
||
|
||
return `*${title}*\n${details}`
|
||
}
|
||
|
||
/**
|
||
* 规范化Telegram基础地址
|
||
*/
|
||
normalizeTelegramApiBase(baseUrl) {
|
||
const defaultBase = 'https://api.telegram.org'
|
||
if (!baseUrl) {
|
||
return defaultBase
|
||
}
|
||
|
||
try {
|
||
const parsed = new URL(baseUrl)
|
||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||
throw new Error('Telegram API 基础地址必须使用 http 或 https 协议')
|
||
}
|
||
|
||
// 移除结尾的 /
|
||
return parsed.href.replace(/\/$/, '')
|
||
} catch (error) {
|
||
logger.warn(`⚠️ Telegram API 基础地址无效,将使用默认值: ${error.message}`)
|
||
return defaultBase
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 构建 Telegram 请求的 axios 选项(代理等)
|
||
*/
|
||
buildTelegramAxiosOptions(platform) {
|
||
const options = {}
|
||
|
||
if (platform.proxyUrl) {
|
||
try {
|
||
const proxyUrl = new URL(platform.proxyUrl)
|
||
const { protocol } = proxyUrl
|
||
|
||
if (protocol.startsWith('socks')) {
|
||
const agent = new SocksProxyAgent(proxyUrl.toString())
|
||
options.httpAgent = agent
|
||
options.httpsAgent = agent
|
||
options.proxy = false
|
||
} else if (protocol === 'http:' || protocol === 'https:') {
|
||
const agent = new HttpsProxyAgent(proxyUrl.toString())
|
||
options.httpAgent = agent
|
||
options.httpsAgent = agent
|
||
options.proxy = false
|
||
} else {
|
||
logger.warn(`⚠️ 不支持的Telegram代理协议: ${protocol}`)
|
||
}
|
||
} catch (error) {
|
||
logger.warn(`⚠️ Telegram代理配置无效,将忽略: ${error.message}`)
|
||
}
|
||
}
|
||
|
||
return options
|
||
}
|
||
|
||
/**
|
||
* 格式化 Telegram 消息
|
||
*/
|
||
formatMessageForTelegram(type, data) {
|
||
const title = this.getNotificationTitle(type)
|
||
const timestamp = new Date().toLocaleString('zh-CN', { timeZone: this.timezone })
|
||
const details = this.buildNotificationDetails(data)
|
||
|
||
const lines = [`${title}`, '服务: Claude Relay Service']
|
||
|
||
if (details.length > 0) {
|
||
lines.push('')
|
||
for (const detail of details) {
|
||
lines.push(`${detail.label}: ${detail.value}`)
|
||
}
|
||
}
|
||
|
||
lines.push('', `时间: ${timestamp}`)
|
||
|
||
return lines.join('\n')
|
||
}
|
||
|
||
/**
|
||
* 格式化Discord消息
|
||
*/
|
||
formatMessageForDiscord(type, data) {
|
||
const title = this.getNotificationTitle(type)
|
||
const color = this.getDiscordColor(type)
|
||
const fields = this.formatNotificationFields(data)
|
||
|
||
return {
|
||
title,
|
||
color,
|
||
fields,
|
||
timestamp: getISOStringWithTimezone(new Date()),
|
||
footer: {
|
||
text: 'Claude Relay Service'
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取通知标题
|
||
*/
|
||
getNotificationTitle(type) {
|
||
const titles = {
|
||
accountAnomaly: '⚠️ 账号异常通知',
|
||
quotaWarning: '📊 配额警告',
|
||
systemError: '❌ 系统错误',
|
||
securityAlert: '🔒 安全警报',
|
||
rateLimitRecovery: '🎉 限流恢复通知',
|
||
test: '🧪 测试通知'
|
||
}
|
||
|
||
return titles[type] || '📢 系统通知'
|
||
}
|
||
|
||
/**
|
||
* 获取Bark通知级别
|
||
*/
|
||
getBarkLevel(type) {
|
||
const levels = {
|
||
accountAnomaly: 'timeSensitive',
|
||
quotaWarning: 'active',
|
||
systemError: 'critical',
|
||
securityAlert: 'critical',
|
||
rateLimitRecovery: 'active',
|
||
test: 'passive'
|
||
}
|
||
|
||
return levels[type] || 'active'
|
||
}
|
||
|
||
/**
|
||
* 获取Bark声音
|
||
*/
|
||
getBarkSound(type) {
|
||
const sounds = {
|
||
accountAnomaly: 'alarm',
|
||
quotaWarning: 'bell',
|
||
systemError: 'alert',
|
||
securityAlert: 'alarm',
|
||
rateLimitRecovery: 'success',
|
||
test: 'default'
|
||
}
|
||
|
||
return sounds[type] || 'default'
|
||
}
|
||
|
||
/**
|
||
* 格式化Bark消息
|
||
*/
|
||
formatMessageForBark(type, data) {
|
||
const lines = []
|
||
|
||
if (data.accountName) {
|
||
lines.push(`账号: ${data.accountName}`)
|
||
}
|
||
|
||
if (data.platform) {
|
||
lines.push(`平台: ${data.platform}`)
|
||
}
|
||
|
||
if (data.status) {
|
||
lines.push(`状态: ${data.status}`)
|
||
}
|
||
|
||
if (data.errorCode) {
|
||
lines.push(`错误: ${data.errorCode}`)
|
||
}
|
||
|
||
if (data.reason) {
|
||
lines.push(`原因: ${data.reason}`)
|
||
}
|
||
|
||
if (data.message) {
|
||
lines.push(`消息: ${data.message}`)
|
||
}
|
||
|
||
if (data.quota) {
|
||
lines.push(`剩余配额: ${data.quota.remaining}/${data.quota.total}`)
|
||
}
|
||
|
||
if (data.usage) {
|
||
lines.push(`使用率: ${data.usage}%`)
|
||
}
|
||
|
||
// 添加服务标识和时间戳
|
||
lines.push(`\n服务: Claude Relay Service`)
|
||
lines.push(`时间: ${new Date().toLocaleString('zh-CN', { timeZone: this.timezone })}`)
|
||
|
||
return lines.join('\n')
|
||
}
|
||
|
||
/**
|
||
* 构建通知详情数据
|
||
*/
|
||
buildNotificationDetails(data) {
|
||
const details = []
|
||
|
||
if (data.accountName) {
|
||
details.push({ label: '账号', value: data.accountName })
|
||
}
|
||
if (data.platform) {
|
||
details.push({ label: '平台', value: data.platform })
|
||
}
|
||
if (data.status) {
|
||
details.push({ label: '状态', value: data.status, color: this.getStatusColor(data.status) })
|
||
}
|
||
if (data.errorCode) {
|
||
details.push({ label: '错误代码', value: data.errorCode, isCode: true })
|
||
}
|
||
if (data.reason) {
|
||
details.push({ label: '原因', value: data.reason })
|
||
}
|
||
if (data.message) {
|
||
details.push({ label: '消息', value: data.message })
|
||
}
|
||
if (data.quota) {
|
||
details.push({ label: '配额', value: `${data.quota.remaining}/${data.quota.total}` })
|
||
}
|
||
if (data.usage) {
|
||
details.push({ label: '使用率', value: `${data.usage}%` })
|
||
}
|
||
|
||
return details
|
||
}
|
||
|
||
/**
|
||
* 格式化邮件HTML内容
|
||
*/
|
||
formatMessageForEmail(type, data) {
|
||
const title = this.getNotificationTitle(type)
|
||
const timestamp = new Date().toLocaleString('zh-CN', { timeZone: this.timezone })
|
||
const details = this.buildNotificationDetails(data)
|
||
|
||
let content = `
|
||
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
|
||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0; font-size: 24px;">${title}</h1>
|
||
<p style="margin: 10px 0 0 0; opacity: 0.9;">Claude Relay Service</p>
|
||
</div>
|
||
<div style="background: #f8f9fa; padding: 20px; border: 1px solid #e9ecef; border-top: none; border-radius: 0 0 8px 8px;">
|
||
<div style="background: white; padding: 16px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||
`
|
||
|
||
// 使用统一的详情数据渲染
|
||
details.forEach((detail) => {
|
||
if (detail.isCode) {
|
||
content += `<p><strong>${detail.label}:</strong> <code style="background: #f1f3f4; padding: 2px 6px; border-radius: 4px;">${detail.value}</code></p>`
|
||
} else if (detail.color) {
|
||
content += `<p><strong>${detail.label}:</strong> <span style="color: ${detail.color};">${detail.value}</span></p>`
|
||
} else {
|
||
content += `<p><strong>${detail.label}:</strong> ${detail.value}</p>`
|
||
}
|
||
})
|
||
|
||
content += `
|
||
</div>
|
||
<div style="margin-top: 20px; padding-top: 16px; border-top: 1px solid #e9ecef; font-size: 14px; color: #6c757d; text-align: center;">
|
||
<p>发送时间: ${timestamp}</p>
|
||
<p style="margin: 0;">此邮件由 Claude Relay Service 自动发送</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`
|
||
|
||
return content
|
||
}
|
||
|
||
/**
|
||
* 格式化邮件纯文本内容
|
||
*/
|
||
formatMessageForEmailText(type, data) {
|
||
const title = this.getNotificationTitle(type)
|
||
const timestamp = new Date().toLocaleString('zh-CN', { timeZone: this.timezone })
|
||
const details = this.buildNotificationDetails(data)
|
||
|
||
let content = `${title}\n`
|
||
content += `=====================================\n\n`
|
||
|
||
// 使用统一的详情数据渲染
|
||
details.forEach((detail) => {
|
||
content += `${detail.label}: ${detail.value}\n`
|
||
})
|
||
|
||
content += `\n发送时间: ${timestamp}\n`
|
||
content += `服务: Claude Relay Service\n`
|
||
content += `=====================================\n`
|
||
content += `此邮件由系统自动发送,请勿回复。`
|
||
|
||
return content
|
||
}
|
||
|
||
/**
|
||
* 获取状态颜色
|
||
*/
|
||
getStatusColor(status) {
|
||
const colors = {
|
||
error: '#dc3545',
|
||
unauthorized: '#fd7e14',
|
||
blocked: '#6f42c1',
|
||
disabled: '#6c757d',
|
||
active: '#28a745',
|
||
warning: '#ffc107'
|
||
}
|
||
return colors[status] || '#007bff'
|
||
}
|
||
|
||
/**
|
||
* 格式化通知详情
|
||
*/
|
||
formatNotificationDetails(data) {
|
||
const lines = []
|
||
|
||
if (data.accountName) {
|
||
lines.push(`**账号**: ${data.accountName}`)
|
||
}
|
||
|
||
if (data.platform) {
|
||
lines.push(`**平台**: ${data.platform}`)
|
||
}
|
||
|
||
if (data.platforms) {
|
||
lines.push(`**涉及平台**: ${data.platforms.join(', ')}`)
|
||
}
|
||
|
||
if (data.totalAccounts) {
|
||
lines.push(`**恢复账户数**: ${data.totalAccounts}`)
|
||
}
|
||
|
||
if (data.status) {
|
||
lines.push(`**状态**: ${data.status}`)
|
||
}
|
||
|
||
if (data.errorCode) {
|
||
lines.push(`**错误代码**: ${data.errorCode}`)
|
||
}
|
||
|
||
if (data.reason) {
|
||
lines.push(`**原因**: ${data.reason}`)
|
||
}
|
||
|
||
if (data.message) {
|
||
lines.push(`**消息**: ${data.message}`)
|
||
}
|
||
|
||
if (data.quota) {
|
||
lines.push(`**剩余配额**: ${data.quota.remaining}/${data.quota.total}`)
|
||
}
|
||
|
||
if (data.usage) {
|
||
lines.push(`**使用率**: ${data.usage}%`)
|
||
}
|
||
|
||
return lines.join('\n')
|
||
}
|
||
|
||
/**
|
||
* 格式化Discord字段
|
||
*/
|
||
formatNotificationFields(data) {
|
||
const fields = []
|
||
|
||
if (data.accountName) {
|
||
fields.push({ name: '账号', value: data.accountName, inline: true })
|
||
}
|
||
|
||
if (data.platform) {
|
||
fields.push({ name: '平台', value: data.platform, inline: true })
|
||
}
|
||
|
||
if (data.status) {
|
||
fields.push({ name: '状态', value: data.status, inline: true })
|
||
}
|
||
|
||
if (data.errorCode) {
|
||
fields.push({ name: '错误代码', value: data.errorCode, inline: false })
|
||
}
|
||
|
||
if (data.reason) {
|
||
fields.push({ name: '原因', value: data.reason, inline: false })
|
||
}
|
||
|
||
if (data.message) {
|
||
fields.push({ name: '消息', value: data.message, inline: false })
|
||
}
|
||
|
||
return fields
|
||
}
|
||
|
||
/**
|
||
* 获取飞书卡片颜色
|
||
*/
|
||
getFeishuCardColor(type) {
|
||
const colors = {
|
||
accountAnomaly: 'orange',
|
||
quotaWarning: 'yellow',
|
||
systemError: 'red',
|
||
securityAlert: 'red',
|
||
rateLimitRecovery: 'green',
|
||
test: 'blue'
|
||
}
|
||
|
||
return colors[type] || 'blue'
|
||
}
|
||
|
||
/**
|
||
* 获取Slack emoji
|
||
*/
|
||
getSlackEmoji(type) {
|
||
const emojis = {
|
||
accountAnomaly: ':warning:',
|
||
quotaWarning: ':chart_with_downwards_trend:',
|
||
systemError: ':x:',
|
||
securityAlert: ':lock:',
|
||
rateLimitRecovery: ':tada:',
|
||
test: ':test_tube:'
|
||
}
|
||
|
||
return emojis[type] || ':bell:'
|
||
}
|
||
|
||
/**
|
||
* 获取Discord颜色
|
||
*/
|
||
getDiscordColor(type) {
|
||
const colors = {
|
||
accountAnomaly: 0xff9800, // 橙色
|
||
quotaWarning: 0xffeb3b, // 黄色
|
||
systemError: 0xf44336, // 红色
|
||
securityAlert: 0xf44336, // 红色
|
||
rateLimitRecovery: 0x4caf50, // 绿色
|
||
test: 0x2196f3 // 蓝色
|
||
}
|
||
|
||
return colors[type] || 0x9e9e9e // 灰色
|
||
}
|
||
|
||
/**
|
||
* 测试webhook连接
|
||
*/
|
||
async testWebhook(platform) {
|
||
try {
|
||
const testData = {
|
||
message: 'Claude Relay Service webhook测试',
|
||
timestamp: getISOStringWithTimezone(new Date())
|
||
}
|
||
|
||
await this.sendToPlatform(platform, 'test', testData, { maxRetries: 1, retryDelay: 1000 })
|
||
|
||
return { success: true }
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
error: error.message
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = new WebhookService()
|