本次提交新增了完整的多渠道消息网关系统,包括: 1. 支持飞书、钉钉、Web、Hook 四种渠道的适配器与配置 2. 领域模型层:消息、会话、绑定、出箱等核心实体 3. 应用服务层:管道、中间件、DTO 与业务逻辑 4. 基础设施层:持久化、过滤器、队列等端口实现 5. 接口层:REST API、SSE、WebSocket 通信端点 6. 前端页面与路由配置,添加渠道管理菜单 7. 新增相关依赖包与 docker-compose 部署配置
114 lines
3.8 KiB
Python
114 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import threading
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DingTalkWsConnection:
|
|
def __init__(self, *, client_id: str, client_secret: str) -> None:
|
|
self._client_id = client_id
|
|
self._client_secret = client_secret
|
|
self._stream_client = None
|
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
self._on_message = None
|
|
self._connected = False
|
|
self._thread: threading.Thread | None = None
|
|
|
|
@property
|
|
def channel_type(self) -> str:
|
|
return "dingtalk"
|
|
|
|
@property
|
|
def is_connected(self) -> bool:
|
|
return self._connected
|
|
|
|
async def start(
|
|
self,
|
|
loop: asyncio.AbstractEventLoop,
|
|
on_message,
|
|
) -> None:
|
|
self._loop = loop
|
|
self._on_message = on_message
|
|
|
|
from dingtalk_stream import AuthClientCredential, DingTalkStreamClient
|
|
|
|
credential = AuthClientCredential(self._client_id, self._client_secret)
|
|
self._stream_client = DingTalkStreamClient(credential)
|
|
self._stream_client.register_callback_listener(
|
|
"/v1.0/im/bot/messages/get",
|
|
self._on_dingtalk_event,
|
|
)
|
|
|
|
self._thread = threading.Thread(target=self._run_stream_client, daemon=True, name="dingtalk-ws")
|
|
self._thread.start()
|
|
|
|
await self._wait_for_connection(timeout=10.0)
|
|
logger.info(
|
|
"dingtalk ws started, client_id=%s",
|
|
self._client_id[:4] + "..." if self._client_id else "N/A",
|
|
)
|
|
|
|
def _run_stream_client(self) -> None:
|
|
try:
|
|
self._connected = True
|
|
self._stream_client.start()
|
|
except Exception:
|
|
self._connected = False
|
|
logger.exception("dingtalk ws client crashed")
|
|
|
|
async def _wait_for_connection(self, timeout: float = 10.0) -> None:
|
|
elapsed = 0.0
|
|
interval = 0.5
|
|
while elapsed < timeout:
|
|
if self._thread and not self._thread.is_alive():
|
|
self._connected = False
|
|
logger.error("dingtalk ws thread exited prematurely")
|
|
return
|
|
await asyncio.sleep(interval)
|
|
elapsed += interval
|
|
|
|
async def stop(self) -> None:
|
|
self._connected = False
|
|
if self._stream_client and hasattr(self._stream_client, "close"):
|
|
self._stream_client.close()
|
|
|
|
def _on_dingtalk_event(self, data: dict) -> None:
|
|
raw = self._event_to_raw(data)
|
|
if self._loop and self._on_message:
|
|
try:
|
|
future = asyncio.run_coroutine_threadsafe(self._on_message(raw), self._loop)
|
|
future.add_done_callback(self._callback_done)
|
|
except RuntimeError:
|
|
logger.warning("dingtalk ws bridge: event loop is closed")
|
|
|
|
def _event_to_raw(self, data: dict) -> dict:
|
|
return {
|
|
"channel_type": "dingtalk",
|
|
"header": {"event_id": data.get("headers", {}).get("eventId", "")},
|
|
"event": {
|
|
"message": {
|
|
"message_id": data.get("data", {}).get("messageId", ""),
|
|
"chat_id": data.get("data", {}).get("conversationId", ""),
|
|
"chat_type": data.get("data", {}).get("conversationType", ""),
|
|
"message_type": data.get("data", {}).get("msgtype", "text"),
|
|
"content": data.get("data", {}).get("text", {}).get("content", ""),
|
|
},
|
|
"sender": {
|
|
"sender_id": {
|
|
"user_id": data.get("data", {}).get("senderId", ""),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
@staticmethod
|
|
def _callback_done(future):
|
|
try:
|
|
future.result()
|
|
except Exception:
|
|
logger.exception("ws→asyncio bridge error")
|