- 通知渠道增加 sign_secret 字段存储加签密钥 - 发送钉钉消息时自动计算签名 - 前端增加加签密钥输入框(仅钉钉机器人显示)
This commit is contained in:
@@ -13,6 +13,7 @@ class TaskNotifyChannel(Base):
|
|||||||
channel_name = Column(String(100), nullable=False)
|
channel_name = Column(String(100), nullable=False)
|
||||||
channel_type = Column(Enum('dingtalk_bot', 'wecom_bot'), nullable=False)
|
channel_type = Column(Enum('dingtalk_bot', 'wecom_bot'), nullable=False)
|
||||||
webhook_url = Column(String(500), nullable=False)
|
webhook_url = Column(String(500), nullable=False)
|
||||||
|
sign_secret = Column(String(200)) # 钉钉加签密钥
|
||||||
description = Column(String(255))
|
description = Column(String(255))
|
||||||
is_enabled = Column(Boolean, default=True)
|
is_enabled = Column(Boolean, default=True)
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class ChannelCreate(BaseModel):
|
|||||||
channel_name: str
|
channel_name: str
|
||||||
channel_type: str # dingtalk_bot, wecom_bot
|
channel_type: str # dingtalk_bot, wecom_bot
|
||||||
webhook_url: str
|
webhook_url: str
|
||||||
|
sign_secret: Optional[str] = None # 钉钉加签密钥
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ class ChannelUpdate(BaseModel):
|
|||||||
channel_name: Optional[str] = None
|
channel_name: Optional[str] = None
|
||||||
channel_type: Optional[str] = None
|
channel_type: Optional[str] = None
|
||||||
webhook_url: Optional[str] = None
|
webhook_url: Optional[str] = None
|
||||||
|
sign_secret: Optional[str] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
is_enabled: Optional[bool] = 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_name=data.channel_name,
|
||||||
channel_type=data.channel_type,
|
channel_type=data.channel_type,
|
||||||
webhook_url=data.webhook_url,
|
webhook_url=data.webhook_url,
|
||||||
|
sign_secret=data.sign_secret,
|
||||||
description=data.description,
|
description=data.description,
|
||||||
is_enabled=True
|
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
|
channel.channel_type = data.channel_type
|
||||||
if data.webhook_url is not None:
|
if data.webhook_url is not None:
|
||||||
channel.webhook_url = data.webhook_url
|
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:
|
if data.description is not None:
|
||||||
channel.description = data.description
|
channel.description = data.description
|
||||||
if data.is_enabled is not None:
|
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)):
|
async def test_channel(channel_id: int, db: Session = Depends(get_db)):
|
||||||
"""测试通知渠道"""
|
"""测试通知渠道"""
|
||||||
import httpx
|
import httpx
|
||||||
|
import time
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
channel = db.query(TaskNotifyChannel).filter(TaskNotifyChannel.id == channel_id).first()
|
channel = db.query(TaskNotifyChannel).filter(TaskNotifyChannel.id == channel_id).first()
|
||||||
if not channel:
|
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发送时间: 测试中..."
|
test_content = f"**测试消息**\n\n渠道名称: {channel.channel_name}\n发送时间: 测试中..."
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
url = channel.webhook_url
|
||||||
|
|
||||||
if channel.channel_type == 'dingtalk_bot':
|
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 = {
|
payload = {
|
||||||
"msgtype": "markdown",
|
"msgtype": "markdown",
|
||||||
"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:
|
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()
|
result = response.json()
|
||||||
|
|
||||||
# 钉钉返回 errcode=0,企微返回 errcode=0
|
# 钉钉返回 errcode=0,企微返回 errcode=0
|
||||||
@@ -174,6 +203,7 @@ def format_channel(channel: TaskNotifyChannel) -> dict:
|
|||||||
"channel_name": channel.channel_name,
|
"channel_name": channel.channel_name,
|
||||||
"channel_type": channel.channel_type,
|
"channel_type": channel.channel_type,
|
||||||
"webhook_url": channel.webhook_url,
|
"webhook_url": channel.webhook_url,
|
||||||
|
"sign_secret": channel.sign_secret,
|
||||||
"description": channel.description,
|
"description": channel.description,
|
||||||
"is_enabled": channel.is_enabled,
|
"is_enabled": channel.is_enabled,
|
||||||
"created_at": channel.created_at,
|
"created_at": channel.created_at,
|
||||||
|
|||||||
@@ -282,7 +282,31 @@ class SchedulerService:
|
|||||||
|
|
||||||
async def _send_to_channel(self, channel: TaskNotifyChannel, content: str, title: str):
|
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.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 = {
|
payload = {
|
||||||
"msgtype": "markdown",
|
"msgtype": "markdown",
|
||||||
"markdown": {
|
"markdown": {
|
||||||
@@ -299,7 +323,7 @@ class SchedulerService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=10) as client:
|
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()
|
result = response.json()
|
||||||
if result.get('errcode') != 0:
|
if result.get('errcode') != 0:
|
||||||
print(f"通知发送失败: {result}")
|
print(f"通知发送失败: {result}")
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const form = reactive({
|
|||||||
channel_name: '',
|
channel_name: '',
|
||||||
channel_type: 'dingtalk_bot',
|
channel_type: 'dingtalk_bot',
|
||||||
webhook_url: '',
|
webhook_url: '',
|
||||||
|
sign_secret: '',
|
||||||
description: ''
|
description: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -78,6 +79,7 @@ function handleCreate() {
|
|||||||
channel_name: '',
|
channel_name: '',
|
||||||
channel_type: 'dingtalk_bot',
|
channel_type: 'dingtalk_bot',
|
||||||
webhook_url: '',
|
webhook_url: '',
|
||||||
|
sign_secret: '',
|
||||||
description: ''
|
description: ''
|
||||||
})
|
})
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
@@ -91,6 +93,7 @@ function handleEdit(row) {
|
|||||||
channel_name: row.channel_name,
|
channel_name: row.channel_name,
|
||||||
channel_type: row.channel_type,
|
channel_type: row.channel_type,
|
||||||
webhook_url: row.webhook_url,
|
webhook_url: row.webhook_url,
|
||||||
|
sign_secret: row.sign_secret || '',
|
||||||
description: row.description || ''
|
description: row.description || ''
|
||||||
})
|
})
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
@@ -258,6 +261,12 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.channel_type === 'dingtalk_bot'" label="加签密钥">
|
||||||
|
<el-input v-model="form.sign_secret" placeholder="SEC开头的密钥(可选)" />
|
||||||
|
<div class="form-tip">
|
||||||
|
如果创建机器人时选择了「加签」安全设置,请填写密钥(以 SEC 开头)
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="描述">
|
<el-form-item label="描述">
|
||||||
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="渠道描述(可选)" />
|
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="渠道描述(可选)" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|||||||
Reference in New Issue
Block a user