ForcePilot/backend/package/yuxi/channel/extensions/tencent_im/webhook.py
Kris 331daa1216 feat(channel): 添加腾讯 IM 渠道扩展
新增腾讯 IM(Tencent IM)渠道扩展,支持在 Yuxi 平台中集成腾讯即时通讯 IM 渠道。

包含以下功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- pairing: 用户配对与绑定
- security: 安全校验
- signature: 请求签名验证
- usersig: UserSig 生成
- dedupe: 消息去重
- status: 会话状态管理
- group: 群组管理
- types: 类型定义
2026-05-21 11:49:29 +08:00

140 lines
4.6 KiB
Python

import json
import logging
import time
from collections import defaultdict
from datetime import UTC, datetime
from yuxi.channel.message.models import MessageType, PeerInfo, UnifiedMessage
from yuxi.channel.routing.models import PeerKind
logger = logging.getLogger(__name__)
class RateLimiter:
def __init__(self, max_requests: int = 10, window: int = 10):
self._max = max_requests
self._window = window
self._buckets: dict[str, list[float]] = defaultdict(list)
def allow(self, key: str) -> bool:
now = time.time()
bucket = self._buckets[key]
bucket[:] = [t for t in bucket if now - t < self._window]
if len(bucket) >= self._max:
return False
bucket.append(now)
return True
def _convert_to_unified_event(
event: dict,
account,
) -> UnifiedMessage | None:
cmd = event.get("CallbackCommand", "")
msg_body = event.get("MsgBody", [])
if not msg_body:
return None
first_msg = msg_body[0]
msg_type = first_msg.get("MsgType", "")
msg_content = first_msg.get("MsgContent", {})
content, unified_type = _parse_msg_content(msg_type, msg_content)
if not content:
return None
is_group = "Group" in cmd
from_account = event.get("From_Account", "")
to_account = event.get("To_Account", "")
group_id = event.get("GroupId", "")
media_urls = _extract_media_urls(msg_type, msg_content)
return UnifiedMessage(
msg_id=f"{from_account}_{event.get('MsgSeq', '')}_{event.get('MsgRandom', '')}",
channel_type="tencent-im",
account_id=account.account_id,
content=content,
message_type=unified_type,
sender=PeerInfo(
id=from_account,
kind=PeerKind.GROUP if is_group else PeerKind.DIRECT,
display_name=from_account,
),
timestamp=datetime.fromtimestamp(event.get("MsgTime", 0), tz=UTC),
raw_payload=event,
body_for_agent=content,
media_urls=media_urls,
metadata={
"group_id": group_id if is_group else "",
"callback_command": cmd,
"msg_type": msg_type,
"to_account": to_account,
},
)
def _parse_msg_content(msg_type: str, msg_content: dict) -> tuple[str, str]:
if msg_type == "TIMTextElem":
return msg_content.get("Text", ""), MessageType.TEXT
if msg_type == "TIMCustomElem":
data = msg_content.get("Data", "")
content = data if isinstance(data, str) else json.dumps(data)
return content, MessageType.TEXT
if msg_type == "TIMImageElem":
return "[图片]", MessageType.IMAGE
if msg_type == "TIMSoundElem":
return "[语音]", MessageType.VOICE
if msg_type == "TIMVideoFileElem":
return "[视频]", MessageType.VIDEO
if msg_type == "TIMFileElem":
return f"[文件: {msg_content.get('FileName', 'unknown')}]", MessageType.FILE
if msg_type == "TIMFaceElem":
return f"[表情: {msg_content.get('Index', 0)}]", MessageType.TEXT
if msg_type == "TIMLocationElem":
lat = msg_content.get("Latitude", 0)
lng = msg_content.get("Longitude", 0)
desc = msg_content.get("Desc", "")
return f"[位置: {desc} ({lat}, {lng})]", MessageType.TEXT
if msg_type == "TIMRelayElem":
title = msg_content.get("Title", "合并转发消息")
relay_count = len(msg_content.get("MsgList", []))
return f"[合并转发: {title} ({relay_count}条)]", MessageType.TEXT
return f"[{msg_type}]", MessageType.TEXT
def _extract_media_urls(msg_type: str, msg_content: dict) -> list[str]:
urls = []
if msg_type == "TIMImageElem":
for img_info in msg_content.get("ImageInfoArray", []):
if url := img_info.get("URL"):
urls.append(url)
elif msg_type == "TIMSoundElem":
if url := msg_content.get("Url"):
urls.append(url)
elif msg_type == "TIMVideoFileElem":
if url := msg_content.get("VideoUrl"):
urls.append(url)
if thumb_url := msg_content.get("ThumbUrl"):
urls.append(thumb_url)
elif msg_type == "TIMFileElem":
if url := msg_content.get("Url"):
urls.append(url)
return urls
def resolve_event_type(raw_event: dict) -> str:
cmd = raw_event.get("CallbackCommand", "")
if "SendMsg" in cmd:
return "message"
if "WithDraw" in cmd:
return "withdraw"
if "StateChange" in cmd:
return "state"
if "Report" in cmd:
return "read_report"
if "Group" in cmd:
return "group_event"
return "unknown"