From 333bbe57ebd4c2674ef2c84ec72b4c1dc22f12f2 Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 28 Jan 2026 17:19:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=92=89=E9=92=89=E6=9C=BA=E5=99=A8?= =?UTF-8?q?=E4=BA=BA=E6=94=AF=E6=8C=81=E5=8A=A0=E7=AD=BE=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 通知渠道增加 sign_secret 字段存储加签密钥 - 发送钉钉消息时自动计算签名 - 前端增加加签密钥输入框(仅钉钉机器人显示) --- backend/app/models/notification_channel.py | 1 + backend/app/routers/notification_channels.py | 32 ++++++++++++++++++- backend/app/services/scheduler.py | 26 ++++++++++++++- .../src/views/notification-channels/index.vue | 9 ++++++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/backend/app/models/notification_channel.py b/backend/app/models/notification_channel.py index 313eb6c..16c5157 100644 --- a/backend/app/models/notification_channel.py +++ b/backend/app/models/notification_channel.py @@ -13,6 +13,7 @@ class TaskNotifyChannel(Base): channel_name = Column(String(100), nullable=False) channel_type = Column(Enum('dingtalk_bot', 'wecom_bot'), nullable=False) webhook_url = Column(String(500), nullable=False) + sign_secret = Column(String(200)) # 钉钉加签密钥 description = Column(String(255)) is_enabled = Column(Boolean, default=True) diff --git a/backend/app/routers/notification_channels.py b/backend/app/routers/notification_channels.py index 84932b9..51fb30c 100644 --- a/backend/app/routers/notification_channels.py +++ b/backend/app/routers/notification_channels.py @@ -19,6 +19,7 @@ class ChannelCreate(BaseModel): channel_name: str channel_type: str # dingtalk_bot, wecom_bot webhook_url: str + sign_secret: Optional[str] = None # 钉钉加签密钥 description: Optional[str] = None @@ -26,6 +27,7 @@ class ChannelUpdate(BaseModel): channel_name: Optional[str] = None channel_type: Optional[str] = None webhook_url: Optional[str] = None + sign_secret: Optional[str] = None description: Optional[str] = None is_enabled: Optional[bool] = None @@ -77,6 +79,7 @@ async def create_channel(data: ChannelCreate, db: Session = Depends(get_db)): channel_name=data.channel_name, channel_type=data.channel_type, webhook_url=data.webhook_url, + sign_secret=data.sign_secret, description=data.description, is_enabled=True ) @@ -101,6 +104,8 @@ async def update_channel(channel_id: int, data: ChannelUpdate, db: Session = Dep channel.channel_type = data.channel_type if data.webhook_url is not None: channel.webhook_url = data.webhook_url + if data.sign_secret is not None: + channel.sign_secret = data.sign_secret if data.description is not None: channel.description = data.description if data.is_enabled is not None: @@ -126,6 +131,11 @@ async def delete_channel(channel_id: int, db: Session = Depends(get_db)): async def test_channel(channel_id: int, db: Session = Depends(get_db)): """测试通知渠道""" import httpx + import time + import hmac + import hashlib + import base64 + import urllib.parse channel = db.query(TaskNotifyChannel).filter(TaskNotifyChannel.id == channel_id).first() if not channel: @@ -134,7 +144,26 @@ async def test_channel(channel_id: int, db: Session = Depends(get_db)): test_content = f"**测试消息**\n\n渠道名称: {channel.channel_name}\n发送时间: 测试中..." try: + url = channel.webhook_url + if channel.channel_type == 'dingtalk_bot': + # 钉钉加签 + if channel.sign_secret: + timestamp = str(round(time.time() * 1000)) + string_to_sign = f'{timestamp}\n{channel.sign_secret}' + hmac_code = hmac.new( + channel.sign_secret.encode('utf-8'), + string_to_sign.encode('utf-8'), + digestmod=hashlib.sha256 + ).digest() + sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) + + # 拼接签名参数 + if '?' in url: + url = f"{url}×tamp={timestamp}&sign={sign}" + else: + url = f"{url}?timestamp={timestamp}&sign={sign}" + payload = { "msgtype": "markdown", "markdown": { @@ -151,7 +180,7 @@ async def test_channel(channel_id: int, db: Session = Depends(get_db)): } async with httpx.AsyncClient(timeout=10) as client: - response = await client.post(channel.webhook_url, json=payload) + response = await client.post(url, json=payload) result = response.json() # 钉钉返回 errcode=0,企微返回 errcode=0 @@ -174,6 +203,7 @@ def format_channel(channel: TaskNotifyChannel) -> dict: "channel_name": channel.channel_name, "channel_type": channel.channel_type, "webhook_url": channel.webhook_url, + "sign_secret": channel.sign_secret, "description": channel.description, "is_enabled": channel.is_enabled, "created_at": channel.created_at, diff --git a/backend/app/services/scheduler.py b/backend/app/services/scheduler.py index 7558ffe..6520543 100644 --- a/backend/app/services/scheduler.py +++ b/backend/app/services/scheduler.py @@ -282,7 +282,31 @@ class SchedulerService: async def _send_to_channel(self, channel: TaskNotifyChannel, content: str, title: str): """发送消息到通知渠道""" + import time + import hmac + import hashlib + import base64 + import urllib.parse + + url = channel.webhook_url + if channel.channel_type == 'dingtalk_bot': + # 钉钉加签 + if channel.sign_secret: + timestamp = str(round(time.time() * 1000)) + string_to_sign = f'{timestamp}\n{channel.sign_secret}' + hmac_code = hmac.new( + channel.sign_secret.encode('utf-8'), + string_to_sign.encode('utf-8'), + digestmod=hashlib.sha256 + ).digest() + sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) + + if '?' in url: + url = f"{url}×tamp={timestamp}&sign={sign}" + else: + url = f"{url}?timestamp={timestamp}&sign={sign}" + payload = { "msgtype": "markdown", "markdown": { @@ -299,7 +323,7 @@ class SchedulerService: } async with httpx.AsyncClient(timeout=10) as client: - response = await client.post(channel.webhook_url, json=payload) + response = await client.post(url, json=payload) result = response.json() if result.get('errcode') != 0: print(f"通知发送失败: {result}") diff --git a/frontend/src/views/notification-channels/index.vue b/frontend/src/views/notification-channels/index.vue index 23425de..1137b28 100644 --- a/frontend/src/views/notification-channels/index.vue +++ b/frontend/src/views/notification-channels/index.vue @@ -25,6 +25,7 @@ const form = reactive({ channel_name: '', channel_type: 'dingtalk_bot', webhook_url: '', + sign_secret: '', description: '' }) @@ -78,6 +79,7 @@ function handleCreate() { channel_name: '', channel_type: 'dingtalk_bot', webhook_url: '', + sign_secret: '', description: '' }) dialogVisible.value = true @@ -91,6 +93,7 @@ function handleEdit(row) { channel_name: row.channel_name, channel_type: row.channel_type, webhook_url: row.webhook_url, + sign_secret: row.sign_secret || '', description: row.description || '' }) dialogVisible.value = true @@ -258,6 +261,12 @@ onMounted(() => { + + +
+ 如果创建机器人时选择了「加签」安全设置,请填写密钥(以 SEC 开头) +
+