Merge pull request #383 from Edric-Li/feature/smtp-notification

feat: 添加SMTP邮件通知功能
This commit is contained in:
Wesley Liddick
2025-09-09 12:19:26 +08:00
committed by GitHub
6 changed files with 543 additions and 49 deletions

10
package-lock.json generated
View File

@@ -26,6 +26,7 @@
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"ldapjs": "^3.0.7", "ldapjs": "^3.0.7",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"nodemailer": "^7.0.6",
"ora": "^5.4.1", "ora": "^5.4.1",
"rate-limiter-flexible": "^5.0.5", "rate-limiter-flexible": "^5.0.5",
"socks-proxy-agent": "^8.0.2", "socks-proxy-agent": "^8.0.2",
@@ -7077,6 +7078,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/nodemailer": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.6.tgz",
"integrity": "sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "3.1.10", "version": "3.1.10",
"resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.10.tgz", "resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.10.tgz",

View File

@@ -65,6 +65,7 @@
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"ldapjs": "^3.0.7", "ldapjs": "^3.0.7",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"nodemailer": "^7.0.6",
"ora": "^5.4.1", "ora": "^5.4.1",
"rate-limiter-flexible": "^5.0.5", "rate-limiter-flexible": "^5.0.5",
"socks-proxy-agent": "^8.0.2", "socks-proxy-agent": "^8.0.2",

View File

@@ -124,7 +124,16 @@ router.post('/test', authenticateAdmin, async (req, res) => {
serverUrl, serverUrl,
level, level,
sound, sound,
group group,
// SMTP 相关字段
host,
port,
secure,
user,
pass,
from,
to,
ignoreTLS
} = req.body } = req.body
// Bark平台特殊处理 // Bark平台特殊处理
@@ -149,6 +158,34 @@ router.post('/test', authenticateAdmin, async (req, res) => {
} }
logger.info(`🧪 测试webhook: ${type} - Device Key: ${deviceKey.substring(0, 8)}...`) logger.info(`🧪 测试webhook: ${type} - Device Key: ${deviceKey.substring(0, 8)}...`)
} else if (type === 'smtp') {
// SMTP平台验证
if (!host) {
return res.status(400).json({
error: 'Missing SMTP host',
message: '请提供SMTP服务器地址'
})
}
if (!user) {
return res.status(400).json({
error: 'Missing SMTP user',
message: '请提供SMTP用户名'
})
}
if (!pass) {
return res.status(400).json({
error: 'Missing SMTP password',
message: '请提供SMTP密码'
})
}
if (!to) {
return res.status(400).json({
error: 'Missing recipient email',
message: '请提供收件人邮箱'
})
}
logger.info(`🧪 测试webhook: ${type} - ${host}:${port || 587} -> ${to}`)
} else { } else {
// 其他平台验证URL // 其他平台验证URL
if (!url) { if (!url) {
@@ -188,6 +225,16 @@ router.post('/test', authenticateAdmin, async (req, res) => {
platform.level = level platform.level = level
platform.sound = sound platform.sound = sound
platform.group = group platform.group = group
} else if (type === 'smtp') {
// 添加SMTP特有字段
platform.host = host
platform.port = port || 587
platform.secure = secure || false
platform.user = user
platform.pass = pass
platform.from = from
platform.to = to
platform.ignoreTLS = ignoreTLS || false
} }
const result = await webhookService.testWebhook(platform) const result = await webhookService.testWebhook(platform)

View File

@@ -63,7 +63,8 @@ class WebhookConfigService {
'slack', 'slack',
'discord', 'discord',
'custom', 'custom',
'bark' 'bark',
'smtp'
] ]
for (const platform of config.platforms) { for (const platform of config.platforms) {
@@ -71,8 +72,8 @@ class WebhookConfigService {
throw new Error(`不支持的平台类型: ${platform.type}`) throw new Error(`不支持的平台类型: ${platform.type}`)
} }
// Bark平台使用deviceKey而不是url // Bark和SMTP平台使用标准URL
if (platform.type !== 'bark') { if (platform.type !== 'bark' && platform.type !== 'smtp') {
if (!platform.url || !this.isValidUrl(platform.url)) { if (!platform.url || !this.isValidUrl(platform.url)) {
throw new Error(`无效的webhook URL: ${platform.url}`) throw new Error(`无效的webhook URL: ${platform.url}`)
} }
@@ -201,6 +202,51 @@ class WebhookConfigService {
logger.warn('⚠️ Bark点击跳转URL格式可能不正确') logger.warn('⚠️ Bark点击跳转URL格式可能不正确')
} }
break break
case 'smtp': {
// 验证SMTP必需配置
if (!platform.host) {
throw new Error('SMTP平台必须提供主机地址')
}
if (!platform.user) {
throw new Error('SMTP平台必须提供用户名')
}
if (!platform.pass) {
throw new Error('SMTP平台必须提供密码')
}
if (!platform.to) {
throw new Error('SMTP平台必须提供接收邮箱')
}
// 验证端口
if (platform.port && (platform.port < 1 || platform.port > 65535)) {
throw new Error('SMTP端口必须在1-65535之间')
}
// 验证邮箱格式
// 支持两种格式1. 纯邮箱 user@domain.com 2. 带名称 Name <user@domain.com>
const simpleEmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
// 验证接收邮箱
const toEmails = Array.isArray(platform.to) ? platform.to : [platform.to]
for (const email of toEmails) {
// 提取实际邮箱地址(如果是 Name <email> 格式)
const actualEmail = email.includes('<') ? email.match(/<([^>]+)>/)?.[1] : email
if (!actualEmail || !simpleEmailRegex.test(actualEmail)) {
throw new Error(`无效的接收邮箱格式: ${email}`)
}
}
// 验证发送邮箱(支持 Name <email> 格式)
if (platform.from) {
const actualFromEmail = platform.from.includes('<')
? platform.from.match(/<([^>]+)>/)?.[1]
: platform.from
if (!actualFromEmail || !simpleEmailRegex.test(actualFromEmail)) {
throw new Error(`无效的发送邮箱格式: ${platform.from}`)
}
}
break
}
} }
} }

View File

@@ -1,5 +1,6 @@
const axios = require('axios') const axios = require('axios')
const crypto = require('crypto') const crypto = require('crypto')
const nodemailer = require('nodemailer')
const logger = require('../utils/logger') const logger = require('../utils/logger')
const webhookConfigService = require('./webhookConfigService') const webhookConfigService = require('./webhookConfigService')
const { getISOStringWithTimezone } = require('../utils/dateHelper') const { getISOStringWithTimezone } = require('../utils/dateHelper')
@@ -14,7 +15,8 @@ class WebhookService {
slack: this.sendToSlack.bind(this), slack: this.sendToSlack.bind(this),
discord: this.sendToDiscord.bind(this), discord: this.sendToDiscord.bind(this),
custom: this.sendToCustom.bind(this), custom: this.sendToCustom.bind(this),
bark: this.sendToBark.bind(this) bark: this.sendToBark.bind(this),
smtp: this.sendToSMTP.bind(this)
} }
this.timezone = appConfig.system.timezone || 'Asia/Shanghai' this.timezone = appConfig.system.timezone || 'Asia/Shanghai'
} }
@@ -243,6 +245,51 @@ class WebhookService {
await this.sendHttpRequest(url, payload, platform.timeout || 10000) 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请求 * 发送HTTP请求
*/ */
@@ -459,6 +506,121 @@ class WebhookService {
return lines.join('\n') 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'
}
/** /**
* 格式化通知详情 * 格式化通知详情
*/ */

View File

@@ -377,9 +377,7 @@
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200"> <h2 class="text-lg font-semibold text-gray-800 dark:text-gray-200">启用通知</h2>
启用 Webhook 通知
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400"> <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
开启后系统将按配置发送通知到指定平台 开启后系统将按配置发送通知到指定平台
</p> </p>
@@ -471,10 +469,22 @@
</div> </div>
</div> </div>
<div class="mt-3 space-y-1 text-sm"> <div class="mt-3 space-y-1 text-sm">
<div class="flex items-center text-gray-600 dark:text-gray-400"> <div
v-if="platform.type !== 'smtp'"
class="flex items-center text-gray-600 dark:text-gray-400"
>
<i class="fas fa-link mr-2"></i> <i class="fas fa-link mr-2"></i>
<span class="truncate">{{ platform.url }}</span> <span class="truncate">{{ platform.url }}</span>
</div> </div>
<div
v-if="platform.type === 'smtp' && platform.to"
class="flex items-center text-gray-600 dark:text-gray-400"
>
<i class="fas fa-envelope mr-2"></i>
<span class="truncate">{{
Array.isArray(platform.to) ? platform.to.join(', ') : platform.to
}}</span>
</div>
<div <div
v-if="platform.enableSign" v-if="platform.enableSign"
class="flex items-center text-gray-600 dark:text-gray-400" class="flex items-center text-gray-600 dark:text-gray-400"
@@ -655,6 +665,7 @@
<option value="slack">🟣 Slack</option> <option value="slack">🟣 Slack</option>
<option value="discord">🟪 Discord</option> <option value="discord">🟪 Discord</option>
<option value="bark">🔔 Bark</option> <option value="bark">🔔 Bark</option>
<option value="smtp">📧 邮件通知</option>
<option value="custom"> 自定义</option> <option value="custom"> 自定义</option>
</select> </select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"> <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
@@ -684,8 +695,8 @@
/> />
</div> </div>
<!-- Webhook URL (非Bark平台) --> <!-- Webhook URL (非Bark和SMTP平台) -->
<div v-if="platformForm.type !== 'bark'"> <div v-if="platformForm.type !== 'bark' && platformForm.type !== 'smtp'">
<label <label
class="mb-2 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300" class="mb-2 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300"
> >
@@ -836,6 +847,141 @@
</div> </div>
</div> </div>
<!-- SMTP 平台特有字段 -->
<div v-if="platformForm.type === 'smtp'" class="space-y-5">
<!-- SMTP 主机 -->
<div>
<label
class="mb-2 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300"
>
<i class="fas fa-server mr-2 text-gray-400"></i>
SMTP 服务器
<span class="ml-1 text-xs text-red-500">*</span>
</label>
<input
v-model="platformForm.host"
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 shadow-sm transition-all placeholder:text-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder:text-gray-500"
placeholder="例如: smtp.gmail.com"
required
type="text"
/>
</div>
<!-- SMTP 端口和安全设置 -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label
class="mb-2 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300"
>
<i class="fas fa-plug mr-2 text-gray-400"></i>
端口
</label>
<input
v-model.number="platformForm.port"
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 shadow-sm transition-all focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
max="65535"
min="1"
placeholder="587"
type="number"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
默认: 587 (TLS) 465 (SSL)
</p>
</div>
<div>
<label
class="mb-2 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300"
>
<i class="fas fa-shield-alt mr-2 text-gray-400"></i>
加密方式
</label>
<select
v-model="platformForm.secure"
class="w-full appearance-none rounded-xl border border-gray-300 bg-white px-4 py-3 pr-10 text-gray-900 shadow-sm transition-all focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
>
<option :value="false">STARTTLS (端口587)</option>
<option :value="true">SSL/TLS (端口465)</option>
</select>
</div>
</div>
<!-- 用户名 -->
<div>
<label
class="mb-2 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300"
>
<i class="fas fa-user mr-2 text-gray-400"></i>
用户名
<span class="ml-1 text-xs text-red-500">*</span>
</label>
<input
v-model="platformForm.user"
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 shadow-sm transition-all placeholder:text-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder:text-gray-500"
placeholder="user@example.com"
required
type="email"
/>
</div>
<!-- 密码 -->
<div>
<label
class="mb-2 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300"
>
<i class="fas fa-lock mr-2 text-gray-400"></i>
密码 / 应用密码
<span class="ml-1 text-xs text-red-500">*</span>
</label>
<input
v-model="platformForm.pass"
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 shadow-sm transition-all placeholder:text-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder:text-gray-500"
placeholder="邮箱密码或应用专用密码"
required
type="password"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
建议使用应用专用密码而非邮箱登录密码
</p>
</div>
<!-- 发件人邮箱 -->
<div>
<label
class="mb-2 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300"
>
<i class="fas fa-paper-plane mr-2 text-gray-400"></i>
发件人邮箱
<span class="ml-2 text-xs text-gray-500">(可选)</span>
</label>
<input
v-model="platformForm.from"
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 shadow-sm transition-all placeholder:text-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder:text-gray-500"
placeholder="默认使用用户名邮箱"
type="email"
/>
</div>
<!-- 收件人邮箱 -->
<div>
<label
class="mb-2 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300"
>
<i class="fas fa-envelope mr-2 text-gray-400"></i>
收件人邮箱
<span class="ml-1 text-xs text-red-500">*</span>
</label>
<input
v-model="platformForm.to"
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-gray-900 shadow-sm transition-all placeholder:text-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder:text-gray-500"
placeholder="admin@example.com"
required
type="email"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">接收通知的邮箱地址</p>
</div>
</div>
<!-- 签名设置钉钉/飞书 --> <!-- 签名设置钉钉/飞书 -->
<div <div
v-if="platformForm.type === 'dingtalk' || platformForm.type === 'feishu'" v-if="platformForm.type === 'dingtalk' || platformForm.type === 'feishu'"
@@ -1008,7 +1154,23 @@ const platformForm = ref({
name: '', name: '',
url: '', url: '',
enableSign: false, enableSign: false,
secret: '' secret: '',
// Bark特有字段
deviceKey: '',
serverUrl: '',
level: '',
sound: '',
group: '',
// SMTP特有字段
host: '',
port: null,
secure: false,
user: '',
pass: '',
from: '',
to: '',
timeout: null,
ignoreTLS: false
}) })
// 监听activeSection变化加载对应配置 // 监听activeSection变化加载对应配置
@@ -1030,17 +1192,48 @@ const platformTypeWatcher = watch(
// 如果不是编辑模式,清空相关字段 // 如果不是编辑模式,清空相关字段
if (!editingPlatform.value) { if (!editingPlatform.value) {
if (newType === 'bark') { if (newType === 'bark') {
// 切换到Bark时清空URL相关字段 // 切换到Bark时清空URL和SMTP相关字段
platformForm.value.url = '' platformForm.value.url = ''
platformForm.value.enableSign = false platformForm.value.enableSign = false
platformForm.value.secret = '' platformForm.value.secret = ''
} else { // 清空SMTP字段
// 切换到其他平台时清空Bark相关字段 platformForm.value.host = ''
platformForm.value.port = null
platformForm.value.secure = false
platformForm.value.user = ''
platformForm.value.pass = ''
platformForm.value.from = ''
platformForm.value.to = ''
platformForm.value.timeout = null
platformForm.value.ignoreTLS = false
} else if (newType === 'smtp') {
// 切换到SMTP时清空URL和Bark相关字段
platformForm.value.url = ''
platformForm.value.enableSign = false
platformForm.value.secret = ''
// 清空Bark字段
platformForm.value.deviceKey = '' platformForm.value.deviceKey = ''
platformForm.value.serverUrl = '' platformForm.value.serverUrl = ''
platformForm.value.level = '' platformForm.value.level = ''
platformForm.value.sound = '' platformForm.value.sound = ''
platformForm.value.group = '' platformForm.value.group = ''
} else {
// 切换到其他平台时清空Bark和SMTP相关字段
platformForm.value.deviceKey = ''
platformForm.value.serverUrl = ''
platformForm.value.level = ''
platformForm.value.sound = ''
platformForm.value.group = ''
// SMTP 字段
platformForm.value.host = ''
platformForm.value.port = null
platformForm.value.secure = false
platformForm.value.user = ''
platformForm.value.pass = ''
platformForm.value.from = ''
platformForm.value.to = ''
platformForm.value.timeout = null
platformForm.value.ignoreTLS = false
} }
} }
} }
@@ -1051,6 +1244,14 @@ const isPlatformFormValid = computed(() => {
if (platformForm.value.type === 'bark') { if (platformForm.value.type === 'bark') {
// Bark平台需要deviceKey // Bark平台需要deviceKey
return !!platformForm.value.deviceKey return !!platformForm.value.deviceKey
} else if (platformForm.value.type === 'smtp') {
// SMTP平台需要必要的配置
return !!(
platformForm.value.host &&
platformForm.value.user &&
platformForm.value.pass &&
platformForm.value.to
)
} else { } else {
// 其他平台需要URL且URL格式正确 // 其他平台需要URL且URL格式正确
return !!platformForm.value.url && !urlError.value return !!platformForm.value.url && !urlError.value
@@ -1134,8 +1335,8 @@ const saveWebhookConfig = async () => {
// 验证 URL // 验证 URL
const validateUrl = () => { const validateUrl = () => {
// Bark平台不需要验证URL // Bark和SMTP平台不需要验证URL
if (platformForm.value.type === 'bark') { if (platformForm.value.type === 'bark' || platformForm.value.type === 'smtp') {
urlError.value = false urlError.value = false
urlValid.value = false urlValid.value = false
return return
@@ -1163,27 +1364,46 @@ const validateUrl = () => {
} }
} }
// 添加/更新平台 // 验证平台配置
const savePlatform = async () => { const validatePlatformForm = () => {
if (!isMounted.value) return
// Bark平台只需要deviceKey其他平台需要URL
if (platformForm.value.type === 'bark') { if (platformForm.value.type === 'bark') {
if (!platformForm.value.deviceKey) { if (!platformForm.value.deviceKey) {
showToast('请输入Bark设备密钥', 'error') showToast('请输入Bark设备密钥', 'error')
return return false
}
} else if (platformForm.value.type === 'smtp') {
const requiredFields = [
{ field: 'host', message: 'SMTP服务器' },
{ field: 'user', message: '用户名' },
{ field: 'pass', message: '密码' },
{ field: 'to', message: '收件人邮箱' }
]
for (const { field, message } of requiredFields) {
if (!platformForm.value[field]) {
showToast(`请输入${message}`, 'error')
return false
}
} }
} else { } else {
if (!platformForm.value.url) { if (!platformForm.value.url) {
showToast('请输入Webhook URL', 'error') showToast('请输入Webhook URL', 'error')
return return false
} }
if (urlError.value) { if (urlError.value) {
showToast('请输入有效的Webhook URL', 'error') showToast('请输入有效的Webhook URL', 'error')
return return false
} }
} }
return true
}
// 添加/更新平台
const savePlatform = async () => {
if (!isMounted.value) return
// 验证表单
if (!validatePlatformForm()) return
savingPlatform.value = true savingPlatform.value = true
try { try {
@@ -1292,6 +1512,15 @@ const testPlatform = async (platform) => {
testData.level = platform.level testData.level = platform.level
testData.sound = platform.sound testData.sound = platform.sound
testData.group = platform.group testData.group = platform.group
} else if (platform.type === 'smtp') {
testData.host = platform.host
testData.port = platform.port
testData.secure = platform.secure
testData.user = platform.user
testData.pass = platform.pass
testData.from = platform.from
testData.to = platform.to
testData.ignoreTLS = platform.ignoreTLS
} else { } else {
testData.url = platform.url testData.url = platform.url
} }
@@ -1300,7 +1529,7 @@ const testPlatform = async (platform) => {
signal: abortController.value.signal signal: abortController.value.signal
}) })
if (response.success && isMounted.value) { if (response.success && isMounted.value) {
showToast('测试成功webhook连接正常', 'success') showToast('测试成功', 'success')
} }
} catch (error) { } catch (error) {
if (error.name === 'AbortError') return if (error.name === 'AbortError') return
@@ -1314,24 +1543,8 @@ const testPlatform = async (platform) => {
const testPlatformForm = async () => { const testPlatformForm = async () => {
if (!isMounted.value) return if (!isMounted.value) return
// Bark平台验证 // 验证表单
if (platformForm.value.type === 'bark') { if (!validatePlatformForm()) return
if (!platformForm.value.deviceKey) {
showToast('请先输入Bark设备密钥', 'error')
return
}
} else {
// 其他平台验证URL
if (!platformForm.value.url) {
showToast('请先输入Webhook URL', 'error')
return
}
if (urlError.value) {
showToast('请输入有效的Webhook URL', 'error')
return
}
}
testingConnection.value = true testingConnection.value = true
try { try {
@@ -1339,7 +1552,7 @@ const testPlatformForm = async () => {
signal: abortController.value.signal signal: abortController.value.signal
}) })
if (response.success && isMounted.value) { if (response.success && isMounted.value) {
showToast('测试成功webhook连接正常', 'success') showToast('测试成功', 'success')
} }
} catch (error) { } catch (error) {
if (error.name === 'AbortError') return if (error.name === 'AbortError') return
@@ -1397,7 +1610,17 @@ const closePlatformModal = () => {
serverUrl: '', serverUrl: '',
level: '', level: '',
sound: '', sound: '',
group: '' group: '',
// SMTP特有字段
host: '',
port: null,
secure: false,
user: '',
pass: '',
from: '',
to: '',
timeout: null,
ignoreTLS: false
} }
urlError.value = false urlError.value = false
urlValid.value = false urlValid.value = false
@@ -1415,6 +1638,7 @@ const getPlatformName = (type) => {
slack: 'Slack', slack: 'Slack',
discord: 'Discord', discord: 'Discord',
bark: 'Bark', bark: 'Bark',
smtp: '邮件通知',
custom: '自定义' custom: '自定义'
} }
return names[type] || type return names[type] || type
@@ -1428,6 +1652,7 @@ const getPlatformIcon = (type) => {
slack: 'fab fa-slack text-purple-600', slack: 'fab fa-slack text-purple-600',
discord: 'fab fa-discord text-indigo-600', discord: 'fab fa-discord text-indigo-600',
bark: 'fas fa-bell text-orange-500', bark: 'fas fa-bell text-orange-500',
smtp: 'fas fa-envelope text-blue-600',
custom: 'fas fa-webhook text-gray-600' custom: 'fas fa-webhook text-gray-600'
} }
return icons[type] || 'fas fa-bell' return icons[type] || 'fas fa-bell'
@@ -1441,6 +1666,7 @@ const getWebhookHint = (type) => {
slack: '请在Slack应用的Incoming Webhooks中获取地址', slack: '请在Slack应用的Incoming Webhooks中获取地址',
discord: '请在Discord服务器的集成设置中创建Webhook', discord: '请在Discord服务器的集成设置中创建Webhook',
bark: '请在Bark App中查看您的设备密钥', bark: '请在Bark App中查看您的设备密钥',
smtp: '请配置SMTP服务器信息支持Gmail、QQ邮箱等',
custom: '请输入完整的Webhook接收地址' custom: '请输入完整的Webhook接收地址'
} }
return hints[type] || '' return hints[type] || ''
@@ -1451,7 +1677,8 @@ const getNotificationTypeName = (type) => {
accountAnomaly: '账号异常', accountAnomaly: '账号异常',
quotaWarning: '配额警告', quotaWarning: '配额警告',
systemError: '系统错误', systemError: '系统错误',
securityAlert: '安全警报' securityAlert: '安全警报',
test: '测试通知'
} }
return names[type] || type return names[type] || type
} }
@@ -1461,7 +1688,8 @@ const getNotificationTypeDescription = (type) => {
accountAnomaly: '账号状态异常、认证失败等', accountAnomaly: '账号状态异常、认证失败等',
quotaWarning: 'API调用配额不足警告', quotaWarning: 'API调用配额不足警告',
systemError: '系统运行错误和故障', systemError: '系统运行错误和故障',
securityAlert: '安全相关的警报通知' securityAlert: '安全相关的警报通知',
test: '用于测试Webhook连接是否正常'
} }
return descriptions[type] || '' return descriptions[type] || ''
} }