mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 新增 telegram 通知
This commit is contained in:
@@ -133,7 +133,11 @@ router.post('/test', authenticateAdmin, async (req, res) => {
|
|||||||
pass,
|
pass,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
ignoreTLS
|
ignoreTLS,
|
||||||
|
botToken,
|
||||||
|
chatId,
|
||||||
|
apiBaseUrl,
|
||||||
|
proxyUrl
|
||||||
} = req.body
|
} = req.body
|
||||||
|
|
||||||
// Bark平台特殊处理
|
// Bark平台特殊处理
|
||||||
@@ -186,6 +190,56 @@ router.post('/test', authenticateAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`🧪 测试webhook: ${type} - ${host}:${port || 587} -> ${to}`)
|
logger.info(`🧪 测试webhook: ${type} - ${host}:${port || 587} -> ${to}`)
|
||||||
|
} else if (type === 'telegram') {
|
||||||
|
if (!botToken) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing Telegram bot token',
|
||||||
|
message: '请提供 Telegram 机器人 Token'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!chatId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing Telegram chat id',
|
||||||
|
message: '请提供 Telegram Chat ID'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiBaseUrl) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(apiBaseUrl)
|
||||||
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid Telegram API base url protocol',
|
||||||
|
message: 'Telegram API 基础地址仅支持 http 或 https'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (urlError) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid Telegram API base url',
|
||||||
|
message: '请提供有效的 Telegram API 基础地址'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxyUrl) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(proxyUrl)
|
||||||
|
const supportedProtocols = ['http:', 'https:', 'socks4:', 'socks4a:', 'socks5:']
|
||||||
|
if (!supportedProtocols.includes(parsed.protocol)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Unsupported proxy protocol',
|
||||||
|
message: 'Telegram 代理仅支持 http/https/socks 协议'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (urlError) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid proxy url',
|
||||||
|
message: '请提供有效的代理地址'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`🧪 测试webhook: ${type} - Chat ID: ${chatId}`)
|
||||||
} else {
|
} else {
|
||||||
// 其他平台验证URL
|
// 其他平台验证URL
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@@ -235,12 +289,30 @@ router.post('/test', authenticateAdmin, async (req, res) => {
|
|||||||
platform.from = from
|
platform.from = from
|
||||||
platform.to = to
|
platform.to = to
|
||||||
platform.ignoreTLS = ignoreTLS || false
|
platform.ignoreTLS = ignoreTLS || false
|
||||||
|
} else if (type === 'telegram') {
|
||||||
|
platform.botToken = botToken
|
||||||
|
platform.chatId = chatId
|
||||||
|
platform.apiBaseUrl = apiBaseUrl
|
||||||
|
platform.proxyUrl = proxyUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await webhookService.testWebhook(platform)
|
const result = await webhookService.testWebhook(platform)
|
||||||
|
|
||||||
|
const identifier = (() => {
|
||||||
|
if (type === 'bark') {
|
||||||
|
return `Device: ${deviceKey.substring(0, 8)}...`
|
||||||
|
}
|
||||||
|
if (type === 'smtp') {
|
||||||
|
const recipients = Array.isArray(to) ? to.join(', ') : to
|
||||||
|
return `${host}:${port || 587} -> ${recipients}`
|
||||||
|
}
|
||||||
|
if (type === 'telegram') {
|
||||||
|
return `Chat ID: ${chatId}`
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
})()
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const identifier = type === 'bark' ? `Device: ${deviceKey.substring(0, 8)}...` : url
|
|
||||||
logger.info(`✅ Webhook测试成功: ${identifier}`)
|
logger.info(`✅ Webhook测试成功: ${identifier}`)
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -249,7 +321,6 @@ router.post('/test', authenticateAdmin, async (req, res) => {
|
|||||||
deviceKey: type === 'bark' ? `${deviceKey.substring(0, 8)}...` : undefined
|
deviceKey: type === 'bark' ? `${deviceKey.substring(0, 8)}...` : undefined
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const identifier = type === 'bark' ? `Device: ${deviceKey.substring(0, 8)}...` : url
|
|
||||||
logger.warn(`❌ Webhook测试失败: ${identifier} - ${result.error}`)
|
logger.warn(`❌ Webhook测试失败: ${identifier} - ${result.error}`)
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class WebhookConfigService {
|
|||||||
'feishu',
|
'feishu',
|
||||||
'slack',
|
'slack',
|
||||||
'discord',
|
'discord',
|
||||||
|
'telegram',
|
||||||
'custom',
|
'custom',
|
||||||
'bark',
|
'bark',
|
||||||
'smtp'
|
'smtp'
|
||||||
@@ -73,7 +74,7 @@ class WebhookConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bark和SMTP平台不使用标准URL
|
// Bark和SMTP平台不使用标准URL
|
||||||
if (platform.type !== 'bark' && platform.type !== 'smtp') {
|
if (!['bark', 'smtp', 'telegram'].includes(platform.type)) {
|
||||||
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}`)
|
||||||
}
|
}
|
||||||
@@ -117,6 +118,43 @@ class WebhookConfigService {
|
|||||||
logger.warn('⚠️ Discord webhook URL格式可能不正确')
|
logger.warn('⚠️ Discord webhook URL格式可能不正确')
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case 'telegram':
|
||||||
|
if (!platform.botToken) {
|
||||||
|
throw new Error('Telegram 平台必须提供机器人 Token')
|
||||||
|
}
|
||||||
|
if (!platform.chatId) {
|
||||||
|
throw new Error('Telegram 平台必须提供 Chat ID')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!platform.botToken.includes(':')) {
|
||||||
|
logger.warn('⚠️ Telegram 机器人 Token 格式可能不正确')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[-\d]+$/.test(String(platform.chatId))) {
|
||||||
|
logger.warn('⚠️ Telegram Chat ID 应该是数字,如为频道请确认已获取正确ID')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform.apiBaseUrl) {
|
||||||
|
if (!this.isValidUrl(platform.apiBaseUrl)) {
|
||||||
|
throw new Error('Telegram API 基础地址格式无效')
|
||||||
|
}
|
||||||
|
const { protocol } = new URL(platform.apiBaseUrl)
|
||||||
|
if (!['http:', 'https:'].includes(protocol)) {
|
||||||
|
throw new Error('Telegram API 基础地址仅支持 http 或 https 协议')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform.proxyUrl) {
|
||||||
|
if (!this.isValidUrl(platform.proxyUrl)) {
|
||||||
|
throw new Error('Telegram 代理地址格式无效')
|
||||||
|
}
|
||||||
|
const proxyProtocol = new URL(platform.proxyUrl).protocol
|
||||||
|
const supportedProtocols = ['http:', 'https:', 'socks4:', 'socks4a:', 'socks5:']
|
||||||
|
if (!supportedProtocols.includes(proxyProtocol)) {
|
||||||
|
throw new Error('Telegram 代理仅支持 http/https/socks 协议')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
case 'custom':
|
case 'custom':
|
||||||
// 自定义webhook,用户自行负责格式
|
// 自定义webhook,用户自行负责格式
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
const nodemailer = require('nodemailer')
|
const nodemailer = require('nodemailer')
|
||||||
|
const { HttpsProxyAgent } = require('https-proxy-agent')
|
||||||
|
const { SocksProxyAgent } = require('socks-proxy-agent')
|
||||||
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,6 +16,7 @@ class WebhookService {
|
|||||||
feishu: this.sendToFeishu.bind(this),
|
feishu: this.sendToFeishu.bind(this),
|
||||||
slack: this.sendToSlack.bind(this),
|
slack: this.sendToSlack.bind(this),
|
||||||
discord: this.sendToDiscord.bind(this),
|
discord: this.sendToDiscord.bind(this),
|
||||||
|
telegram: this.sendToTelegram.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)
|
smtp: this.sendToSMTP.bind(this)
|
||||||
@@ -218,6 +221,38 @@ class WebhookService {
|
|||||||
await this.sendHttpRequest(platform.url, payload, platform.timeout || 10000)
|
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
|
* Bark webhook
|
||||||
*/
|
*/
|
||||||
@@ -293,13 +328,17 @@ class WebhookService {
|
|||||||
/**
|
/**
|
||||||
* 发送HTTP请求
|
* 发送HTTP请求
|
||||||
*/
|
*/
|
||||||
async sendHttpRequest(url, payload, timeout) {
|
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, {
|
const response = await axios.post(url, payload, {
|
||||||
timeout,
|
timeout,
|
||||||
headers: {
|
...axiosOptions,
|
||||||
'Content-Type': 'application/json',
|
headers
|
||||||
'User-Agent': 'claude-relay-service/2.0'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.status < 200 || response.status >= 300) {
|
if (response.status < 200 || response.status >= 300) {
|
||||||
@@ -394,6 +433,83 @@ class WebhookService {
|
|||||||
return `*${title}*\n${details}`
|
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消息
|
* 格式化Discord消息
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -470,12 +470,42 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-3 space-y-1 text-sm">
|
<div class="mt-3 space-y-1 text-sm">
|
||||||
<div
|
<div
|
||||||
v-if="platform.type !== 'smtp'"
|
v-if="platform.type !== 'smtp' && platform.type !== 'telegram'"
|
||||||
class="flex items-center text-gray-600 dark:text-gray-400"
|
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 === 'telegram'"
|
||||||
|
class="flex items-center text-gray-600 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<i class="fas fa-comments mr-2"></i>
|
||||||
|
<span class="truncate">Chat ID: {{ platform.chatId || '未配置' }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="platform.type === 'telegram' && platform.botToken"
|
||||||
|
class="flex items-center text-gray-600 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<i class="fas fa-key mr-2"></i>
|
||||||
|
<span class="truncate"
|
||||||
|
>Token: {{ formatTelegramToken(platform.botToken) }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="platform.type === 'telegram' && platform.apiBaseUrl"
|
||||||
|
class="flex items-center text-gray-600 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<i class="fas fa-globe mr-2"></i>
|
||||||
|
<span class="truncate">API: {{ platform.apiBaseUrl }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="platform.type === 'telegram' && platform.proxyUrl"
|
||||||
|
class="flex items-center text-gray-600 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
<i class="fas fa-route mr-2"></i>
|
||||||
|
<span class="truncate">代理: {{ platform.proxyUrl }}</span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="platform.type === 'smtp' && platform.to"
|
v-if="platform.type === 'smtp' && platform.to"
|
||||||
class="flex items-center text-gray-600 dark:text-gray-400"
|
class="flex items-center text-gray-600 dark:text-gray-400"
|
||||||
@@ -664,6 +694,7 @@
|
|||||||
<option value="feishu">🟦 飞书</option>
|
<option value="feishu">🟦 飞书</option>
|
||||||
<option value="slack">🟣 Slack</option>
|
<option value="slack">🟣 Slack</option>
|
||||||
<option value="discord">🟪 Discord</option>
|
<option value="discord">🟪 Discord</option>
|
||||||
|
<option value="telegram">✈️ Telegram</option>
|
||||||
<option value="bark">🔔 Bark</option>
|
<option value="bark">🔔 Bark</option>
|
||||||
<option value="smtp">📧 邮件通知</option>
|
<option value="smtp">📧 邮件通知</option>
|
||||||
<option value="custom">⚙️ 自定义</option>
|
<option value="custom">⚙️ 自定义</option>
|
||||||
@@ -696,7 +727,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Webhook URL (非Bark和SMTP平台) -->
|
<!-- Webhook URL (非Bark和SMTP平台) -->
|
||||||
<div v-if="platformForm.type !== 'bark' && platformForm.type !== 'smtp'">
|
<div
|
||||||
|
v-if="
|
||||||
|
platformForm.type !== 'bark' &&
|
||||||
|
platformForm.type !== 'smtp' &&
|
||||||
|
platformForm.type !== 'telegram'
|
||||||
|
"
|
||||||
|
>
|
||||||
<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"
|
||||||
>
|
>
|
||||||
@@ -735,6 +772,94 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Telegram 平台特有字段 -->
|
||||||
|
<div v-if="platformForm.type === 'telegram'" class="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="mb-2 flex items-center text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<i class="fas fa-robot mr-2 text-gray-400"></i>
|
||||||
|
Bot Token
|
||||||
|
<span class="ml-1 text-xs text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="platformForm.botToken"
|
||||||
|
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 font-mono text-sm 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="例如:123456789:ABCDEFghijk-xyz"
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
在 Telegram 的 @BotFather 中创建机器人后获得的 Token
|
||||||
|
</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-comments mr-2 text-gray-400"></i>
|
||||||
|
Chat ID
|
||||||
|
<span class="ml-1 text-xs text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="platformForm.chatId"
|
||||||
|
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 font-mono text-sm 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="例如:123456789 或 -1001234567890"
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
可使用 @userinfobot、@RawDataBot 或 API 获取聊天/频道的 Chat ID
|
||||||
|
</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-globe mr-2 text-gray-400"></i>
|
||||||
|
API 基础地址
|
||||||
|
<span class="ml-2 text-xs text-gray-500">(可选)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="platformForm.apiBaseUrl"
|
||||||
|
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 font-mono text-sm 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="默认: https://api.telegram.org"
|
||||||
|
type="url"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
使用自建 Bot API 时可覆盖默认域名,需以 http 或 https 开头
|
||||||
|
</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-route mr-2 text-gray-400"></i>
|
||||||
|
代理地址
|
||||||
|
<span class="ml-2 text-xs text-gray-500">(可选)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="platformForm.proxyUrl"
|
||||||
|
class="w-full rounded-xl border border-gray-300 bg-white px-4 py-3 font-mono text-sm 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="例如:socks5://user:pass@127.0.0.1:1080"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
支持 http、https、socks4/4a/5 代理,留空则直接连接 Telegram 官方 API
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-start rounded-lg bg-blue-50 p-3 text-sm text-blue-700 dark:bg-blue-900/20 dark:text-blue-300"
|
||||||
|
>
|
||||||
|
<i class="fas fa-info-circle mr-2 mt-0.5"></i>
|
||||||
|
<div>机器人需先加入对应群组或频道并授予发送消息权限,通知会以纯文本方式发送。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Bark 平台特有字段 -->
|
<!-- Bark 平台特有字段 -->
|
||||||
<div v-if="platformForm.type === 'bark'" class="space-y-5">
|
<div v-if="platformForm.type === 'bark'" class="space-y-5">
|
||||||
<!-- 设备密钥 -->
|
<!-- 设备密钥 -->
|
||||||
@@ -1155,6 +1280,11 @@ const platformForm = ref({
|
|||||||
url: '',
|
url: '',
|
||||||
enableSign: false,
|
enableSign: false,
|
||||||
secret: '',
|
secret: '',
|
||||||
|
// Telegram特有字段
|
||||||
|
botToken: '',
|
||||||
|
chatId: '',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
proxyUrl: '',
|
||||||
// Bark特有字段
|
// Bark特有字段
|
||||||
deviceKey: '',
|
deviceKey: '',
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
@@ -1196,6 +1326,11 @@ const platformTypeWatcher = watch(
|
|||||||
platformForm.value.url = ''
|
platformForm.value.url = ''
|
||||||
platformForm.value.enableSign = false
|
platformForm.value.enableSign = false
|
||||||
platformForm.value.secret = ''
|
platformForm.value.secret = ''
|
||||||
|
// 清空Telegram字段
|
||||||
|
platformForm.value.botToken = ''
|
||||||
|
platformForm.value.chatId = ''
|
||||||
|
platformForm.value.apiBaseUrl = ''
|
||||||
|
platformForm.value.proxyUrl = ''
|
||||||
// 清空SMTP字段
|
// 清空SMTP字段
|
||||||
platformForm.value.host = ''
|
platformForm.value.host = ''
|
||||||
platformForm.value.port = null
|
platformForm.value.port = null
|
||||||
@@ -1217,6 +1352,33 @@ const platformTypeWatcher = watch(
|
|||||||
platformForm.value.level = ''
|
platformForm.value.level = ''
|
||||||
platformForm.value.sound = ''
|
platformForm.value.sound = ''
|
||||||
platformForm.value.group = ''
|
platformForm.value.group = ''
|
||||||
|
// 清空Telegram字段
|
||||||
|
platformForm.value.botToken = ''
|
||||||
|
platformForm.value.chatId = ''
|
||||||
|
platformForm.value.apiBaseUrl = ''
|
||||||
|
platformForm.value.proxyUrl = ''
|
||||||
|
} else if (newType === 'telegram') {
|
||||||
|
platformForm.value.url = ''
|
||||||
|
platformForm.value.enableSign = false
|
||||||
|
platformForm.value.secret = ''
|
||||||
|
platformForm.value.deviceKey = ''
|
||||||
|
platformForm.value.serverUrl = ''
|
||||||
|
platformForm.value.level = ''
|
||||||
|
platformForm.value.sound = ''
|
||||||
|
platformForm.value.group = ''
|
||||||
|
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
|
||||||
|
platformForm.value.botToken = ''
|
||||||
|
platformForm.value.chatId = ''
|
||||||
|
platformForm.value.apiBaseUrl = ''
|
||||||
|
platformForm.value.proxyUrl = ''
|
||||||
} else {
|
} else {
|
||||||
// 切换到其他平台时,清空Bark和SMTP相关字段
|
// 切换到其他平台时,清空Bark和SMTP相关字段
|
||||||
platformForm.value.deviceKey = ''
|
platformForm.value.deviceKey = ''
|
||||||
@@ -1234,6 +1396,11 @@ const platformTypeWatcher = watch(
|
|||||||
platformForm.value.to = ''
|
platformForm.value.to = ''
|
||||||
platformForm.value.timeout = null
|
platformForm.value.timeout = null
|
||||||
platformForm.value.ignoreTLS = false
|
platformForm.value.ignoreTLS = false
|
||||||
|
// Telegram 字段
|
||||||
|
platformForm.value.botToken = ''
|
||||||
|
platformForm.value.chatId = ''
|
||||||
|
platformForm.value.apiBaseUrl = ''
|
||||||
|
platformForm.value.proxyUrl = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1244,6 +1411,9 @@ 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 === 'telegram') {
|
||||||
|
// Telegram需要机器人Token和Chat ID
|
||||||
|
return !!(platformForm.value.botToken && platformForm.value.chatId)
|
||||||
} else if (platformForm.value.type === 'smtp') {
|
} else if (platformForm.value.type === 'smtp') {
|
||||||
// SMTP平台需要必要的配置
|
// SMTP平台需要必要的配置
|
||||||
return !!(
|
return !!(
|
||||||
@@ -1336,7 +1506,7 @@ const saveWebhookConfig = async () => {
|
|||||||
// 验证 URL
|
// 验证 URL
|
||||||
const validateUrl = () => {
|
const validateUrl = () => {
|
||||||
// Bark和SMTP平台不需要验证URL
|
// Bark和SMTP平台不需要验证URL
|
||||||
if (platformForm.value.type === 'bark' || platformForm.value.type === 'smtp') {
|
if (['bark', 'smtp', 'telegram'].includes(platformForm.value.type)) {
|
||||||
urlError.value = false
|
urlError.value = false
|
||||||
urlValid.value = false
|
urlValid.value = false
|
||||||
return
|
return
|
||||||
@@ -1371,6 +1541,40 @@ const validatePlatformForm = () => {
|
|||||||
showToast('请输入Bark设备密钥', 'error')
|
showToast('请输入Bark设备密钥', 'error')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
} else if (platformForm.value.type === 'telegram') {
|
||||||
|
if (!platformForm.value.botToken) {
|
||||||
|
showToast('请输入 Telegram 机器人 Token', 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!platformForm.value.chatId) {
|
||||||
|
showToast('请输入 Telegram Chat ID', 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (platformForm.value.apiBaseUrl) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(platformForm.value.apiBaseUrl)
|
||||||
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||||
|
showToast('Telegram API 基础地址仅支持 http 或 https', 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast('请输入有效的 Telegram API 基础地址', 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (platformForm.value.proxyUrl) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(platformForm.value.proxyUrl)
|
||||||
|
const supportedProtocols = ['http:', 'https:', 'socks4:', 'socks4a:', 'socks5:']
|
||||||
|
if (!supportedProtocols.includes(parsed.protocol)) {
|
||||||
|
showToast('Telegram 代理仅支持 http/https/socks 协议', 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast('请输入有效的 Telegram 代理地址', 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (platformForm.value.type === 'smtp') {
|
} else if (platformForm.value.type === 'smtp') {
|
||||||
const requiredFields = [
|
const requiredFields = [
|
||||||
{ field: 'host', message: 'SMTP服务器' },
|
{ field: 'host', message: 'SMTP服务器' },
|
||||||
@@ -1442,7 +1646,34 @@ const savePlatform = async () => {
|
|||||||
// 编辑平台
|
// 编辑平台
|
||||||
const editPlatform = (platform) => {
|
const editPlatform = (platform) => {
|
||||||
editingPlatform.value = platform
|
editingPlatform.value = platform
|
||||||
platformForm.value = { ...platform }
|
platformForm.value = {
|
||||||
|
type: platform.type || 'wechat_work',
|
||||||
|
name: platform.name || '',
|
||||||
|
url: platform.url || '',
|
||||||
|
enableSign: platform.enableSign || false,
|
||||||
|
secret: platform.secret || '',
|
||||||
|
// Telegram特有字段
|
||||||
|
botToken: platform.botToken || '',
|
||||||
|
chatId: platform.chatId || '',
|
||||||
|
apiBaseUrl: platform.apiBaseUrl || '',
|
||||||
|
proxyUrl: platform.proxyUrl || '',
|
||||||
|
// Bark特有字段
|
||||||
|
deviceKey: platform.deviceKey || '',
|
||||||
|
serverUrl: platform.serverUrl || '',
|
||||||
|
level: platform.level || '',
|
||||||
|
sound: platform.sound || '',
|
||||||
|
group: platform.group || '',
|
||||||
|
// SMTP特有字段
|
||||||
|
host: platform.host || '',
|
||||||
|
port: platform.port ?? null,
|
||||||
|
secure: platform.secure || false,
|
||||||
|
user: platform.user || '',
|
||||||
|
pass: platform.pass || '',
|
||||||
|
from: platform.from || '',
|
||||||
|
to: Array.isArray(platform.to) ? platform.to.join(', ') : platform.to || '',
|
||||||
|
timeout: platform.timeout ?? null,
|
||||||
|
ignoreTLS: platform.ignoreTLS || false
|
||||||
|
}
|
||||||
showAddPlatformModal.value = true
|
showAddPlatformModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1521,6 +1752,11 @@ const testPlatform = async (platform) => {
|
|||||||
testData.from = platform.from
|
testData.from = platform.from
|
||||||
testData.to = platform.to
|
testData.to = platform.to
|
||||||
testData.ignoreTLS = platform.ignoreTLS
|
testData.ignoreTLS = platform.ignoreTLS
|
||||||
|
} else if (platform.type === 'telegram') {
|
||||||
|
testData.botToken = platform.botToken
|
||||||
|
testData.chatId = platform.chatId
|
||||||
|
testData.apiBaseUrl = platform.apiBaseUrl
|
||||||
|
testData.proxyUrl = platform.proxyUrl
|
||||||
} else {
|
} else {
|
||||||
testData.url = platform.url
|
testData.url = platform.url
|
||||||
}
|
}
|
||||||
@@ -1584,7 +1820,9 @@ const sendTestNotification = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === 'AbortError') return
|
if (error.name === 'AbortError') return
|
||||||
if (!isMounted.value) return
|
if (!isMounted.value) return
|
||||||
showToast('发送失败', 'error')
|
const errorMessage =
|
||||||
|
error?.response?.data?.message || error?.response?.data?.error || error?.message || '发送失败'
|
||||||
|
showToast(errorMessage, 'error')
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1605,6 +1843,11 @@ const closePlatformModal = () => {
|
|||||||
url: '',
|
url: '',
|
||||||
enableSign: false,
|
enableSign: false,
|
||||||
secret: '',
|
secret: '',
|
||||||
|
// Telegram特有字段
|
||||||
|
botToken: '',
|
||||||
|
chatId: '',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
proxyUrl: '',
|
||||||
// Bark特有字段
|
// Bark特有字段
|
||||||
deviceKey: '',
|
deviceKey: '',
|
||||||
serverUrl: '',
|
serverUrl: '',
|
||||||
@@ -1637,6 +1880,7 @@ const getPlatformName = (type) => {
|
|||||||
feishu: '飞书',
|
feishu: '飞书',
|
||||||
slack: 'Slack',
|
slack: 'Slack',
|
||||||
discord: 'Discord',
|
discord: 'Discord',
|
||||||
|
telegram: 'Telegram',
|
||||||
bark: 'Bark',
|
bark: 'Bark',
|
||||||
smtp: '邮件通知',
|
smtp: '邮件通知',
|
||||||
custom: '自定义'
|
custom: '自定义'
|
||||||
@@ -1651,6 +1895,7 @@ const getPlatformIcon = (type) => {
|
|||||||
feishu: 'fas fa-dove text-blue-600',
|
feishu: 'fas fa-dove text-blue-600',
|
||||||
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',
|
||||||
|
telegram: 'fab fa-telegram-plane text-sky-500',
|
||||||
bark: 'fas fa-bell text-orange-500',
|
bark: 'fas fa-bell text-orange-500',
|
||||||
smtp: 'fas fa-envelope text-blue-600',
|
smtp: 'fas fa-envelope text-blue-600',
|
||||||
custom: 'fas fa-webhook text-gray-600'
|
custom: 'fas fa-webhook text-gray-600'
|
||||||
@@ -1665,6 +1910,7 @@ const getWebhookHint = (type) => {
|
|||||||
feishu: '请在飞书群机器人设置中获取Webhook地址',
|
feishu: '请在飞书群机器人设置中获取Webhook地址',
|
||||||
slack: '请在Slack应用的Incoming Webhooks中获取地址',
|
slack: '请在Slack应用的Incoming Webhooks中获取地址',
|
||||||
discord: '请在Discord服务器的集成设置中创建Webhook',
|
discord: '请在Discord服务器的集成设置中创建Webhook',
|
||||||
|
telegram: '使用 @BotFather 创建机器人并复制 Token,Chat ID 可通过 @userinfobot 或相关工具获取',
|
||||||
bark: '请在Bark App中查看您的设备密钥',
|
bark: '请在Bark App中查看您的设备密钥',
|
||||||
smtp: '请配置SMTP服务器信息,支持Gmail、QQ邮箱等',
|
smtp: '请配置SMTP服务器信息,支持Gmail、QQ邮箱等',
|
||||||
custom: '请输入完整的Webhook接收地址'
|
custom: '请输入完整的Webhook接收地址'
|
||||||
@@ -1672,6 +1918,12 @@ const getWebhookHint = (type) => {
|
|||||||
return hints[type] || ''
|
return hints[type] || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatTelegramToken = (token) => {
|
||||||
|
if (!token) return ''
|
||||||
|
if (token.length <= 12) return token
|
||||||
|
return `${token.slice(0, 6)}...${token.slice(-4)}`
|
||||||
|
}
|
||||||
|
|
||||||
const getNotificationTypeName = (type) => {
|
const getNotificationTypeName = (type) => {
|
||||||
const names = {
|
const names = {
|
||||||
accountAnomaly: '账号异常',
|
accountAnomaly: '账号异常',
|
||||||
|
|||||||
Reference in New Issue
Block a user