From b92f7e7ec9243e96614699c370db4c26044d1b02 Mon Sep 17 00:00:00 2001 From: chaos-zhu Date: Sun, 14 Dec 2025 15:41:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20tg=E6=B6=88=E6=81=AF=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/utils/notification.js | 133 ++++++++++++++++++++++++++++------- 1 file changed, 107 insertions(+), 26 deletions(-) diff --git a/server/utils/notification.js b/server/utils/notification.js index d4cb8e4..59904e2 100644 --- a/server/utils/notification.js +++ b/server/utils/notification.js @@ -3,9 +3,12 @@ * 后续可扩展支持其他通知方式(钉钉等) */ +import { readFile } from 'fs/promises' +import { existsSync } from 'fs' import db from './db.js' import TelegramBot from 'node-telegram-bot-api' import nodemailer from 'nodemailer' +import { getImagePath } from './upload.js' // 通知类型枚举 export const NOTIFICATION_TYPES = { @@ -218,6 +221,64 @@ function isValidImageUrl(url) { } } +/** + * 从本地文件系统读取图片 + * @param {string} filename - 图片文件名 + * @returns {Promise} 图片Buffer,失败返回null + */ +async function readLocalImage(filename) { + try { + const filepath = getImagePath(filename) + + if (!existsSync(filepath)) { + console.warn(`[Notification] 本地图片文件不存在: ${filepath}`) + return null + } + + const buffer = await readFile(filepath) + return buffer + } catch (error) { + console.warn('[Notification] 读取本地图片出错:', error.message) + return null + } +} + +/** + * 根据content-type获取文件扩展名 + */ +function getExtensionFromContentType(contentType) { + const map = { + 'image/jpeg': 'jpg', + 'image/jpg': 'jpg', + 'image/png': 'png', + 'image/gif': 'gif', + 'image/webp': 'webp', + 'image/bmp': 'bmp', + 'image/svg+xml': 'svg' + } + return map[contentType] || 'jpg' +} + +/** + * 构建 Telegram 消息文本(用于 caption 或普通消息) + */ +function buildTelegramMessage(payload, includeImageUrl = false) { + let message = `*${escapeMarkdown(payload.title)}*\n${escapeMarkdown(payload.message)}` + + // 如果有额外数据,添加到消息中 + if (payload.data && Object.keys(payload.data).length > 0) { + message += '\n\n*详细信息:*' + for (const [key, value] of Object.entries(payload.data)) { + // 跳过图片URL(除非明确要求包含) + if (!includeImageUrl && (key === 'url' || key === 'imageUrl')) continue + const displayValue = typeof value === 'object' ? JSON.stringify(value) : value + message += `\n• ${key}: \`${escapeMarkdown(String(displayValue))}\`` + } + } + + return message +} + /** * 发送 Telegram 通知 */ @@ -239,40 +300,60 @@ async function sendTelegramNotification(config, payload) { const imageUrl = payload.data?.url || payload.data?.imageUrl const hasValidImageUrl = isValidImageUrl(imageUrl) - if (hasValidImageUrl) { - // 使用文本消息 + 链接预览方式发送图片通知 - // 将图片 URL 放在消息开头,Telegram 会自动生成图片预览 - let message = `${imageUrl}\n\n` - message += `*${escapeMarkdown(payload.title)}*\n${escapeMarkdown(payload.message)}` + // 优先尝试从本地读取图片文件 + const localFilename = payload.data?.filename + let imageBuffer = null - // 如果有额外数据,添加到消息中(排除url字段,因为已经显示了链接) - if (payload.data && Object.keys(payload.data).length > 0) { - message += '\n\n*详细信息:*' - for (const [key, value] of Object.entries(payload.data)) { - if (key === 'url' || key === 'imageUrl') continue // 跳过图片URL - const displayValue = typeof value === 'object' ? JSON.stringify(value) : value - message += `\n• ${key}: \`${escapeMarkdown(String(displayValue))}\`` + if (localFilename) { + console.log('[Notification] 尝试读取本地图片:', localFilename) + imageBuffer = await readLocalImage(localFilename) + } + + if (imageBuffer) { + // 成功读取本地图片,使用Buffer发送 + const caption = buildTelegramMessage(payload, false) + const format = payload.data?.format || getExtensionFromContentType('image/jpeg') + const contentType = `image/${format === 'jpg' ? 'jpeg' : format}` + + try { + // 使用 sendPhoto API 发送图片(使用Buffer) + await bot.sendPhoto(chatId, imageBuffer, { + caption: caption, + parse_mode: 'Markdown' + }, { + filename: localFilename, + contentType: contentType + }) + console.log('[Notification] Telegram 图片通知发送成功:', payload.title) + } catch (photoError) { + // 如果 sendPhoto 失败,回退到普通消息 + console.warn('[Notification] Telegram sendPhoto 失败,回退到普通消息:', photoError.message) + + let fallbackMessage = buildTelegramMessage(payload, true) + if (hasValidImageUrl) { + fallbackMessage += `\n\n🔗 [查看图片](${imageUrl})` } - } - // 启用链接预览,Telegram 会自动为图片 URL 生成预览 - await bot.sendMessage(chatId, message, { + await bot.sendMessage(chatId, fallbackMessage, { + parse_mode: 'Markdown', + disable_web_page_preview: false + }) + console.log('[Notification] Telegram 回退消息发送成功:', payload.title) + } + } else if (hasValidImageUrl) { + // 本地读取失败但有URL,发送带链接的文本消息 + console.warn('[Notification] 本地图片读取失败,发送带链接的文本消息') + + let fallbackMessage = buildTelegramMessage(payload, true) + fallbackMessage += `\n\n🔗 [查看图片](${imageUrl})` + + await bot.sendMessage(chatId, fallbackMessage, { parse_mode: 'Markdown', disable_web_page_preview: false }) } else { // 没有有效图片URL时,发送普通文本消息 - let message = `*${escapeMarkdown(payload.title)}*\n${escapeMarkdown(payload.message)}` - - // 如果有额外数据,添加到消息中 - if (payload.data && Object.keys(payload.data).length > 0) { - message += '\n\n*详细信息:*' - for (const [key, value] of Object.entries(payload.data)) { - const displayValue = typeof value === 'object' ? JSON.stringify(value) : value - message += `\n• ${key}: \`${escapeMarkdown(String(displayValue))}\`` - } - } - + const message = buildTelegramMessage(payload, true) await bot.sendMessage(chatId, message, { parse_mode: 'Markdown' }) }