All checks were successful
continuous-integration/drone/push Build is passing
- 扩展 ToolConfig 配置类型,新增 external_api 类型
- 实现接口注册表,包含 90+ 睿美云开放接口定义
- 实现 TPOS SHA256WithRSA 签名鉴权
- 实现睿美云 API 客户端,支持多租户配置
- 新增代理路由 /api/ruimeiyun/call/{api_name}
- 支持接口权限控制和健康检查
212 lines
6.8 KiB
Python
212 lines
6.8 KiB
Python
"""通知渠道API路由"""
|
||
from typing import Optional, List
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from pydantic import BaseModel
|
||
from sqlalchemy.orm import Session
|
||
from sqlalchemy import desc
|
||
|
||
from ..database import get_db
|
||
from ..models.notification_channel import TaskNotifyChannel
|
||
|
||
|
||
router = APIRouter(prefix="/api/notification-channels", tags=["notification-channels"])
|
||
|
||
|
||
# ==================== Schemas ====================
|
||
|
||
class ChannelCreate(BaseModel):
|
||
tenant_id: str
|
||
channel_name: str
|
||
channel_type: str # dingtalk_bot, wecom_bot
|
||
webhook_url: str
|
||
sign_secret: Optional[str] = None # 钉钉加签密钥
|
||
description: Optional[str] = None
|
||
|
||
|
||
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
|
||
|
||
|
||
# ==================== CRUD ====================
|
||
|
||
@router.get("")
|
||
async def list_channels(
|
||
tenant_id: Optional[str] = None,
|
||
channel_type: Optional[str] = None,
|
||
is_enabled: Optional[bool] = None,
|
||
page: int = Query(1, ge=1),
|
||
size: int = Query(50, ge=1, le=100),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""获取通知渠道列表"""
|
||
query = db.query(TaskNotifyChannel)
|
||
|
||
if tenant_id:
|
||
query = query.filter(TaskNotifyChannel.tenant_id == tenant_id)
|
||
if channel_type:
|
||
query = query.filter(TaskNotifyChannel.channel_type == channel_type)
|
||
if is_enabled is not None:
|
||
query = query.filter(TaskNotifyChannel.is_enabled == is_enabled)
|
||
|
||
total = query.count()
|
||
items = query.order_by(desc(TaskNotifyChannel.created_at)).offset((page - 1) * size).limit(size).all()
|
||
|
||
return {
|
||
"total": total,
|
||
"items": [format_channel(c) for c in items]
|
||
}
|
||
|
||
|
||
@router.get("/{channel_id}")
|
||
async def get_channel(channel_id: int, db: Session = Depends(get_db)):
|
||
"""获取渠道详情"""
|
||
channel = db.query(TaskNotifyChannel).filter(TaskNotifyChannel.id == channel_id).first()
|
||
if not channel:
|
||
raise HTTPException(status_code=404, detail="渠道不存在")
|
||
return format_channel(channel)
|
||
|
||
|
||
@router.post("")
|
||
async def create_channel(data: ChannelCreate, db: Session = Depends(get_db)):
|
||
"""创建通知渠道"""
|
||
channel = TaskNotifyChannel(
|
||
tenant_id=data.tenant_id,
|
||
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
|
||
)
|
||
|
||
db.add(channel)
|
||
db.commit()
|
||
db.refresh(channel)
|
||
|
||
return {"success": True, "id": channel.id}
|
||
|
||
|
||
@router.put("/{channel_id}")
|
||
async def update_channel(channel_id: int, data: ChannelUpdate, db: Session = Depends(get_db)):
|
||
"""更新通知渠道"""
|
||
channel = db.query(TaskNotifyChannel).filter(TaskNotifyChannel.id == channel_id).first()
|
||
if not channel:
|
||
raise HTTPException(status_code=404, detail="渠道不存在")
|
||
|
||
if data.channel_name is not None:
|
||
channel.channel_name = data.channel_name
|
||
if data.channel_type is not None:
|
||
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:
|
||
channel.is_enabled = data.is_enabled
|
||
|
||
db.commit()
|
||
return {"success": True}
|
||
|
||
|
||
@router.delete("/{channel_id}")
|
||
async def delete_channel(channel_id: int, db: Session = Depends(get_db)):
|
||
"""删除通知渠道"""
|
||
channel = db.query(TaskNotifyChannel).filter(TaskNotifyChannel.id == channel_id).first()
|
||
if not channel:
|
||
raise HTTPException(status_code=404, detail="渠道不存在")
|
||
|
||
db.delete(channel)
|
||
db.commit()
|
||
return {"success": True}
|
||
|
||
|
||
@router.post("/{channel_id}/test")
|
||
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:
|
||
raise HTTPException(status_code=404, detail="渠道不存在")
|
||
|
||
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": {
|
||
"title": "渠道测试",
|
||
"text": test_content
|
||
}
|
||
}
|
||
else: # wecom_bot
|
||
payload = {
|
||
"msgtype": "markdown",
|
||
"markdown": {
|
||
"content": test_content
|
||
}
|
||
}
|
||
|
||
async with httpx.AsyncClient(timeout=10) as client:
|
||
response = await client.post(url, json=payload)
|
||
result = response.json()
|
||
|
||
# 钉钉返回 errcode=0,企微返回 errcode=0
|
||
if result.get('errcode') == 0:
|
||
return {"success": True, "message": "发送成功"}
|
||
else:
|
||
return {"success": False, "message": f"发送失败: {result}"}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "message": f"发送失败: {str(e)}"}
|
||
|
||
|
||
# ==================== Helpers ====================
|
||
|
||
def format_channel(channel: TaskNotifyChannel) -> dict:
|
||
"""格式化渠道数据"""
|
||
return {
|
||
"id": channel.id,
|
||
"tenant_id": channel.tenant_id,
|
||
"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,
|
||
"updated_at": channel.updated_at
|
||
}
|