ForcePilot/backend/package/yuxi/channel/channels/dingtalk/ws_connection.py
Kris 9a8a27bf36 feat(channel): 新增渠道网关模块完整实现
本次提交新增了完整的多渠道消息网关系统,包括:
1. 支持飞书、钉钉、Web、Hook 四种渠道的适配器与配置
2. 领域模型层:消息、会话、绑定、出箱等核心实体
3. 应用服务层:管道、中间件、DTO 与业务逻辑
4. 基础设施层:持久化、过滤器、队列等端口实现
5. 接口层:REST API、SSE、WebSocket 通信端点
6. 前端页面与路由配置,添加渠道管理菜单
7. 新增相关依赖包与 docker-compose 部署配置
2026-05-30 21:53:09 +08:00

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")