新增通知渠道

This commit is contained in:
zhinianboke 2025-08-21 09:36:11 +08:00
parent 7da13c379c
commit 0f6b756c25
4 changed files with 283 additions and 3 deletions

View File

@ -338,7 +338,7 @@ python Start.py
- **多重安全验证** - 超级加密保护,防止误操作和数据泄露 - **多重安全验证** - 超级加密保护,防止误操作和数据泄露
- **批量处理能力** - 支持批量确认发货,提高处理效率 - **批量处理能力** - 支持批量确认发货,提高处理效率
- **异常处理机制** - 完善的错误处理和重试机制,确保发货成功 - **异常处理机制** - 完善的错误处理和重试机制,确保发货成功
- **多渠道通知** - 支持QQ、钉钉、邮件等多种发货通知方式 - **多渠道通知** - 支持QQ、钉钉、飞书、Bark、邮件等多种发货通知方式
### 👥 多用户系统 ### 👥 多用户系统
- **用户注册登录** - 支持邮箱验证和图形验证码,安全可靠 - **用户注册登录** - 支持邮箱验证和图形验证码,安全可靠
@ -362,10 +362,11 @@ python Start.py
- **账号状态验证** - 自动检查cookies启用状态确保搜索功能正常 - **账号状态验证** - 自动检查cookies启用状态确保搜索功能正常
### 📱 通知系统 ### 📱 通知系统
- **多渠道支持** - QQ、钉钉、邮件、微信、Telegram等6种通知方式 - **多渠道支持** - QQ、钉钉、飞书、Bark、邮件、微信、Telegram等8种通知方式
- **智能配置** - 可视化配置界面,支持复杂参数和加密设置 - **智能配置** - 可视化配置界面,支持复杂参数和加密设置
- **实时推送** - 重要事件实时通知,及时了解系统状态 - **实时推送** - 重要事件实时通知,及时了解系统状态
- **通知模板** - 自定义通知内容和格式,个性化消息推送 - **通知模板** - 自定义通知内容和格式,个性化消息推送
- **移动端支持** - Bark iOS推送随时随地接收通知
### 🔐 安全特性 ### 🔐 安全特性
- **Cookie安全管理** - 加密存储用户凭证,定期自动刷新 - **Cookie安全管理** - 加密存储用户凭证,定期自动刷新

View File

@ -1863,6 +1863,12 @@ class XianyuLive:
case 'ding_talk' | 'dingtalk': case 'ding_talk' | 'dingtalk':
logger.info(f"📱 开始发送钉钉通知...") logger.info(f"📱 开始发送钉钉通知...")
await self._send_dingtalk_notification(config_data, notification_msg) await self._send_dingtalk_notification(config_data, notification_msg)
case 'feishu' | 'lark':
logger.info(f"📱 开始发送飞书通知...")
await self._send_feishu_notification(config_data, notification_msg)
case 'bark':
logger.info(f"📱 开始发送Bark通知...")
await self._send_bark_notification(config_data, notification_msg)
case 'email': case 'email':
logger.info(f"📱 开始发送邮件通知...") logger.info(f"📱 开始发送邮件通知...")
await self._send_email_notification(config_data, notification_msg) await self._send_email_notification(config_data, notification_msg)
@ -1989,6 +1995,159 @@ class XianyuLive:
except Exception as e: except Exception as e:
logger.error(f"发送钉钉通知异常: {self._safe_str(e)}") logger.error(f"发送钉钉通知异常: {self._safe_str(e)}")
async def _send_feishu_notification(self, config_data: dict, message: str):
"""发送飞书通知"""
try:
import aiohttp
import json
import hmac
import hashlib
import base64
logger.info(f"📱 飞书通知 - 开始处理配置数据: {config_data}")
# 解析配置
webhook_url = config_data.get('webhook_url', '')
secret = config_data.get('secret', '')
logger.info(f"📱 飞书通知 - Webhook URL: {webhook_url[:50]}...")
logger.info(f"📱 飞书通知 - 是否有签名密钥: {'' if secret else ''}")
if not webhook_url:
logger.warning("📱 飞书通知 - Webhook URL配置为空无法发送通知")
return
# 如果有加签密钥,生成签名
timestamp = str(int(time.time()))
sign = ""
if secret:
string_to_sign = f'{timestamp}\n{secret}'
hmac_code = hmac.new(
secret.encode('utf-8'),
string_to_sign.encode('utf-8'),
digestmod=hashlib.sha256
).digest()
sign = base64.b64encode(hmac_code).decode('utf-8')
logger.info(f"📱 飞书通知 - 已生成签名")
# 构建请求数据
data = {
"msg_type": "text",
"content": {
"text": message
},
"timestamp": timestamp
}
# 如果有签名,添加到请求数据中
if sign:
data["sign"] = sign
logger.info(f"📱 飞书通知 - 请求数据构建完成")
# 发送POST请求
async with aiohttp.ClientSession() as session:
async with session.post(webhook_url, json=data, timeout=10) as response:
response_text = await response.text()
logger.info(f"📱 飞书通知 - 响应状态: {response.status}")
logger.info(f"📱 飞书通知 - 响应内容: {response_text}")
if response.status == 200:
try:
response_json = json.loads(response_text)
if response_json.get('code') == 0:
logger.info(f"📱 飞书通知发送成功")
else:
logger.warning(f"📱 飞书通知发送失败: {response_json.get('msg', '未知错误')}")
except json.JSONDecodeError:
logger.info(f"📱 飞书通知发送成功(响应格式异常)")
else:
logger.warning(f"📱 飞书通知发送失败: HTTP {response.status}, 响应: {response_text}")
except Exception as e:
logger.error(f"📱 发送飞书通知异常: {self._safe_str(e)}")
import traceback
logger.error(f"📱 飞书通知异常详情: {traceback.format_exc()}")
async def _send_bark_notification(self, config_data: dict, message: str):
"""发送Bark通知"""
try:
import aiohttp
import json
from urllib.parse import quote
logger.info(f"📱 Bark通知 - 开始处理配置数据: {config_data}")
# 解析配置
server_url = config_data.get('server_url', 'https://api.day.app').rstrip('/')
device_key = config_data.get('device_key', '')
title = config_data.get('title', '闲鱼自动回复通知')
sound = config_data.get('sound', 'default')
icon = config_data.get('icon', '')
group = config_data.get('group', 'xianyu')
url = config_data.get('url', '')
logger.info(f"📱 Bark通知 - 服务器: {server_url}")
logger.info(f"📱 Bark通知 - 设备密钥: {device_key[:10]}..." if device_key else "📱 Bark通知 - 设备密钥: 未设置")
logger.info(f"📱 Bark通知 - 标题: {title}")
if not device_key:
logger.warning("📱 Bark通知 - 设备密钥配置为空,无法发送通知")
return
# 构建请求URL和数据
# Bark支持两种方式URL路径方式和POST JSON方式
# 这里使用POST JSON方式更灵活且支持更多参数
api_url = f"{server_url}/push"
# 构建请求数据
data = {
"device_key": device_key,
"title": title,
"body": message,
"sound": sound,
"group": group
}
# 可选参数
if icon:
data["icon"] = icon
if url:
data["url"] = url
logger.info(f"📱 Bark通知 - API地址: {api_url}")
logger.info(f"📱 Bark通知 - 请求数据构建完成")
# 发送POST请求
async with aiohttp.ClientSession() as session:
async with session.post(api_url, json=data, timeout=10) as response:
response_text = await response.text()
logger.info(f"📱 Bark通知 - 响应状态: {response.status}")
logger.info(f"📱 Bark通知 - 响应内容: {response_text}")
if response.status == 200:
try:
response_json = json.loads(response_text)
if response_json.get('code') == 200:
logger.info(f"📱 Bark通知发送成功")
else:
logger.warning(f"📱 Bark通知发送失败: {response_json.get('message', '未知错误')}")
except json.JSONDecodeError:
# 某些Bark服务器可能返回纯文本
if 'success' in response_text.lower() or 'ok' in response_text.lower():
logger.info(f"📱 Bark通知发送成功")
else:
logger.warning(f"📱 Bark通知响应格式异常: {response_text}")
else:
logger.warning(f"📱 Bark通知发送失败: HTTP {response.status}, 响应: {response_text}")
except Exception as e:
logger.error(f"📱 发送Bark通知异常: {self._safe_str(e)}")
import traceback
logger.error(f"📱 Bark通知异常详情: {traceback.format_exc()}")
async def _send_email_notification(self, config_data: dict, message: str): async def _send_email_notification(self, config_data: dict, message: str):
"""发送邮件通知""" """发送邮件通知"""
try: try:
@ -2218,6 +2377,12 @@ class XianyuLive:
case 'ding_talk' | 'dingtalk': case 'ding_talk' | 'dingtalk':
await self._send_dingtalk_notification(config_data, notification_msg) await self._send_dingtalk_notification(config_data, notification_msg)
notification_sent = True notification_sent = True
case 'feishu' | 'lark':
await self._send_feishu_notification(config_data, notification_msg)
notification_sent = True
case 'bark':
await self._send_bark_notification(config_data, notification_msg)
notification_sent = True
case 'email': case 'email':
await self._send_email_notification(config_data, notification_msg) await self._send_email_notification(config_data, notification_msg)
notification_sent = True notification_sent = True

View File

@ -1064,6 +1064,40 @@
</div> </div>
</div> </div>
<div class="col-sm-6 col-md-4 col-lg-3 col-xl-2">
<div class="card h-100 channel-type-card" onclick="showAddChannelModal('feishu')">
<div class="card-body text-center">
<div class="channel-icon">
<i class="bi bi-chat-square-text-fill text-warning"></i>
</div>
<h6 class="card-title">飞书通知</h6>
<p class="card-text text-muted">飞书机器人消息</p>
<div class="mt-auto">
<button class="btn btn-outline-warning btn-sm">
<i class="bi bi-plus-circle me-1"></i>配置
</button>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3 col-xl-2">
<div class="card h-100 channel-type-card" onclick="showAddChannelModal('bark')">
<div class="card-body text-center">
<div class="channel-icon">
<i class="bi bi-phone-fill text-dark"></i>
</div>
<h6 class="card-title">Bark通知</h6>
<p class="card-text text-muted">iOS推送通知</p>
<div class="mt-auto">
<button class="btn btn-outline-dark btn-sm">
<i class="bi bi-plus-circle me-1"></i>配置
</button>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3 col-xl-2"> <div class="col-sm-6 col-md-4 col-lg-3 col-xl-2">
<div class="card h-100 channel-type-card" onclick="showAddChannelModal('email')"> <div class="card h-100 channel-type-card" onclick="showAddChannelModal('email')">
<div class="card-body text-center"> <div class="card-body text-center">
@ -2720,7 +2754,7 @@
<!-- JS依赖 --> <!-- JS依赖 -->
<script src="/static/lib/bootstrap/bootstrap.bundle.min.js"></script> <script src="/static/lib/bootstrap/bootstrap.bundle.min.js"></script>
<script src="/static/js/app.js"></script> <script src="/static/js/app.js?v=2.2.0"></script>
<!-- 默认回复管理模态框 --> <!-- 默认回复管理模态框 -->
<div class="modal fade" id="defaultReplyModal" tabindex="-1" aria-labelledby="defaultReplyModalLabel" aria-hidden="true"> <div class="modal fade" id="defaultReplyModal" tabindex="-1" aria-labelledby="defaultReplyModalLabel" aria-hidden="true">

View File

@ -2450,6 +2450,78 @@ const channelTypeConfigs = {
} }
] ]
}, },
feishu: {
title: '飞书通知',
description: '请设置飞书机器人Webhook URL支持自定义机器人和群机器人',
icon: 'bi-chat-square-text-fill',
color: 'warning',
fields: [
{
id: 'webhook_url',
label: '飞书机器人Webhook URL',
type: 'url',
placeholder: 'https://open.feishu.cn/open-apis/bot/v2/hook/...',
required: true,
help: '飞书机器人的Webhook地址'
},
{
id: 'secret',
label: '签名密钥(可选)',
type: 'text',
placeholder: '输入签名密钥',
required: false,
help: '如果机器人开启了签名验证,请填写密钥'
}
]
},
bark: {
title: 'Bark通知',
description: 'iOS推送通知服务支持自建服务器和官方服务器',
icon: 'bi-phone-fill',
color: 'dark',
fields: [
{
id: 'device_key',
label: '设备密钥',
type: 'text',
placeholder: '输入Bark设备密钥',
required: true,
help: 'Bark应用中显示的设备密钥'
},
{
id: 'server_url',
label: '服务器地址(可选)',
type: 'url',
placeholder: 'https://api.day.app',
required: false,
help: '自建Bark服务器地址留空使用官方服务器'
},
{
id: 'title',
label: '通知标题(可选)',
type: 'text',
placeholder: '闲鱼自动回复通知',
required: false,
help: '推送通知的标题'
},
{
id: 'sound',
label: '提示音(可选)',
type: 'text',
placeholder: 'default',
required: false,
help: '通知提示音alarm, anticipate, bell等'
},
{
id: 'group',
label: '分组(可选)',
type: 'text',
placeholder: 'xianyu',
required: false,
help: '通知分组名称,用于归类消息'
}
]
},
email: { email: {
title: '邮件通知', title: '邮件通知',
description: '通过SMTP服务器发送邮件通知支持各种邮箱服务商', description: '通过SMTP服务器发送邮件通知支持各种邮箱服务商',
@ -2753,6 +2825,8 @@ function renderNotificationChannels(channels) {
let channelType = channel.type; let channelType = channel.type;
if (channelType === 'ding_talk') { if (channelType === 'ding_talk') {
channelType = 'dingtalk'; // 兼容旧的类型名 channelType = 'dingtalk'; // 兼容旧的类型名
} else if (channelType === 'lark') {
channelType = 'feishu'; // 兼容lark类型名
} }
const typeConfig = channelTypeConfigs[channelType]; const typeConfig = channelTypeConfigs[channelType];
const typeDisplay = typeConfig ? typeConfig.title : channel.type; const typeDisplay = typeConfig ? typeConfig.title : channel.type;
@ -2867,6 +2941,8 @@ async function editNotificationChannel(channelId) {
let channelType = channel.type; let channelType = channel.type;
if (channelType === 'ding_talk') { if (channelType === 'ding_talk') {
channelType = 'dingtalk'; // 兼容旧的类型名 channelType = 'dingtalk'; // 兼容旧的类型名
} else if (channelType === 'lark') {
channelType = 'feishu'; // 兼容lark类型名
} }
const config = channelTypeConfigs[channelType]; const config = channelTypeConfigs[channelType];
@ -2891,6 +2967,10 @@ async function editNotificationChannel(channelId) {
configData = { qq_number: channel.config }; configData = { qq_number: channel.config };
} else if (channel.type === 'dingtalk' || channel.type === 'ding_talk') { } else if (channel.type === 'dingtalk' || channel.type === 'ding_talk') {
configData = { webhook_url: channel.config }; configData = { webhook_url: channel.config };
} else if (channel.type === 'feishu' || channel.type === 'lark') {
configData = { webhook_url: channel.config };
} else if (channel.type === 'bark') {
configData = { device_key: channel.config };
} else { } else {
configData = { config: channel.config }; configData = { config: channel.config };
} }