ForcePilot/backend/package/yuxi/channel/channels/feishu/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

122 lines
4.0 KiB
Python

from __future__ import annotations
import asyncio
import logging
import threading
import lark_oapi as lark
from lark_oapi.api.im.v1 import P2ImMessageReceiveV1
logger = logging.getLogger(__name__)
class FeishuWsConnection:
def __init__(self, *, app_id: str, app_secret: str) -> None:
self._app_id = app_id
self._app_secret = app_secret
self._ws_client: lark.ws.Client | None = 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 "feishu"
@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
event_handler = (
lark.EventDispatcherHandler.builder("", "").register_p2_im_message_receive_v1(self._on_ws_event).build()
)
self._ws_client = lark.ws.Client(
self._app_id,
self._app_secret,
event_handler=event_handler,
log_level=lark.LogLevel.INFO,
)
self._thread = threading.Thread(target=self._run_ws_client, daemon=True, name="feishu-ws")
self._thread.start()
await self._wait_for_connection(timeout=10.0)
logger.info(
"feishu ws started, app_id=%s",
self._app_id[:4] + "..." if self._app_id else "N/A",
)
def _run_ws_client(self) -> None:
try:
self._connected = True
self._ws_client.start()
except Exception:
self._connected = False
logger.exception("feishu 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("feishu ws thread exited prematurely")
return
await asyncio.sleep(interval)
elapsed += interval
async def stop(self) -> None:
self._connected = False
logger.warning("feishu ws stop: SDK does not provide stop(), daemon thread will exit with process")
def _on_ws_event(self, data: P2ImMessageReceiveV1) -> 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("feishu ws bridge: event loop is closed")
def _event_to_raw(self, data: P2ImMessageReceiveV1) -> dict:
msg = data.event.message
sender = data.event.sender
sender_id = sender.sender_id if sender else None
return {
"channel_type": "feishu",
"header": {"event_id": getattr(data.header, "event_id", "") or ""},
"event": {
"message": {
"message_id": getattr(msg, "message_id", "") or "",
"chat_id": getattr(msg, "chat_id", "") or "",
"chat_type": getattr(msg, "chat_type", "") or "",
"message_type": getattr(msg, "message_type", "") or "",
"content": getattr(msg, "content", "") or "",
},
"sender": {
"sender_id": {
"user_id": getattr(sender_id, "user_id", "") or "",
"open_id": getattr(sender_id, "open_id", "") or "",
},
},
},
}
@staticmethod
def _callback_done(future):
try:
future.result()
except Exception:
logger.exception("ws→asyncio bridge error")