From ac8ebeffc8f5b28f409c4fa8eff1472fc8db8b98 Mon Sep 17 00:00:00 2001 From: liuwei Date: Thu, 22 Jan 2026 11:20:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A9=E6=9F=9A=E6=A8=A1=E5=9D=97=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/gscore_adapter/__init__.py | 5 + plugins/gscore_adapter/config.toml | 4 + plugins/gscore_adapter/main.py | 259 +++++++++++++++++++++++++++++ requirements.txt | 4 +- 4 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 plugins/gscore_adapter/__init__.py create mode 100644 plugins/gscore_adapter/config.toml create mode 100644 plugins/gscore_adapter/main.py diff --git a/plugins/gscore_adapter/__init__.py b/plugins/gscore_adapter/__init__.py new file mode 100644 index 0000000..880cb1e --- /dev/null +++ b/plugins/gscore_adapter/__init__.py @@ -0,0 +1,5 @@ +from .main import GsCoreAdapterPlugin + + +def get_plugin(): + return GsCoreAdapterPlugin() diff --git a/plugins/gscore_adapter/config.toml b/plugins/gscore_adapter/config.toml new file mode 100644 index 0000000..ea72388 --- /dev/null +++ b/plugins/gscore_adapter/config.toml @@ -0,0 +1,4 @@ +[GsCoreAdapter] +enable = true +gscore_url = "ws://192.168.2.240:8765/ws/abot" + diff --git a/plugins/gscore_adapter/main.py b/plugins/gscore_adapter/main.py new file mode 100644 index 0000000..82091c6 --- /dev/null +++ b/plugins/gscore_adapter/main.py @@ -0,0 +1,259 @@ +from typing import Dict, Any, List, Optional, Tuple +import json +import asyncio +import websockets +import markdown +from bs4 import BeautifulSoup +from loguru import logger as _logger + +from base.plugin_common.message_plugin_interface import MessagePluginInterface +from base.plugin_common.plugin_interface import PluginStatus +from utils.robot_cmd.robot_command import GroupBotManager, PermissionStatus +from utils.decorator.plugin_decorators import plugin_stats_decorator +from wechat_ipad import WechatAPIClient + + +class MessageNode: + def __init__(self, message: str): + self.type = "text" + self.data = message + + def to_dict(self): + return {"type": self.type, "data": self.data} + + +class SenderDict: + def __init__(self, user: dict): + self.age = 0 + self.area = user.get("Country", "未知") + self.card = user.get("NickName", {}).get("string", "") + self.level = "" + self.nickname = user.get("NickName", {}).get("string", "") + self.role = "owner" + self.sex = "男" if user.get("Sex") == 1 else ("女" if user.get("Sex") == 2 else "未知") + self.title = "" + self.user_id = 0 + self.avater = user.get("SmallHeadImgUrl", "") + + def to_dict(self): + return { + "age": self.age, + "area": self.area, + "card": self.card, + "level": self.level, + "nickname": self.nickname, + "role": self.role, + "sex": self.sex, + "title": self.title, + "user_id": self.user_id, + "avater": self.avater, + } + + +class GsCoreAdapterPlugin(MessagePluginInterface): + FEATURE_KEY = "GS_CORE_ADAPTER" + FEATURE_DESCRIPTION = "🌐 早柚核心适配 [早柚, 开启早柚, 关闭早柚, 重连早柚]" + + @property + def name(self) -> str: + return "早柚适配器" + + @property + def version(self) -> str: + return "0.0.1" + + @property + def description(self) -> str: + return "将指令转发至早柚核心并回传处理结果" + + @property + def author(self) -> str: + return "xuangeer" + + @property + def command_prefix(self) -> Optional[str]: + return "" + + @property + def commands(self) -> List[str]: + return self._commands + + @property + def feature_key(self) -> Optional[str]: + return self.FEATURE_KEY + + @property + def feature_description(self) -> Optional[str]: + return self.FEATURE_DESCRIPTION + + def __init__(self): + super().__init__() + self.websocket = None + self.gscore_url = "" + self.bot: Optional[WechatAPIClient] = None + self._commands = ["早柚", "重连早柚"] + self.feature = None + + def initialize(self, context: Dict[str, Any]) -> bool: + self.LOG = _logger + self.event_system = context.get("event_system") + self.feature = self.register_feature() + plugin_cfg = self._config.get("GsCoreAdapter", {}) + self.gscore_url = plugin_cfg.get("gscore_url", "") + self._commands = plugin_cfg.get("command", self._commands) + # 连接早柚核心(未配置时记录提醒) + if not self.gscore_url: + self.LOG.warning(f"[{self.name}] gscore_url 未配置,跳过连接") + else: + asyncio.create_task(self.connect()) + return True + + def start(self) -> bool: + self.status = PluginStatus.RUNNING + return True + + def stop(self) -> bool: + self.status = PluginStatus.STOPPED + return True + + def can_process(self, message: Dict[str, Any]) -> bool: + content = str(message.get("content", "")).strip() + command = content.split(" ")[0] if content else "" + return command in self._commands + + @plugin_stats_decorator(plugin_name="早柚适配器") + async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]: + content = str(message.get("content", "")).strip() + sender = message.get("sender") + roomid = message.get("roomid", "") + gbm = message.get("gbm") + bot: WechatAPIClient = message.get("bot") + command = content.split(" ")[0] + if command in ["重连早柚"]: + admins = GroupBotManager.get_admin_list() + if sender not in admins: + await bot.send_text_message((roomid if roomid else sender), "无管理员权限", sender) + return True, "权限不足" + await self.reconnect() + await bot.send_text_message((roomid if roomid else sender), "重连早柚成功", sender) + return True, "重连成功" + if command == "早柚": + if roomid and gbm and gbm.get_group_permission(roomid, self.feature) == PermissionStatus.DISABLED: + return False, "没有权限" + if self.bot is None: + self.bot = bot + try: + user = await bot.get_contact(sender) + except Exception: + return False, "获取用户信息失败" + msg_text = content[len(command):].strip() + payload = { + "bot_id": "abot", + "sender": SenderDict(user).to_dict(), + "bot_self_id": bot.wxid, + "msg_id": "", + "user_type": "group" if roomid else "direct", + "group_id": roomid or "", + "user_id": sender, + "user_pm": 1 if sender in GroupBotManager.get_admin_list() else 6, + "content": [MessageNode(msg_text).to_dict()], + } + try: + await self.send_message(json.dumps(payload, ensure_ascii=False)) + return True, "发送成功" + except Exception: + return False, "发送失败" + return False, None + + async def send_message(self, message: str): + if self.websocket: + await self.websocket.send(message.encode("utf-8")) + + async def receive_message(self): + if not self.websocket: + return + while True: + try: + message = await self.websocket.recv() + await self.message_handler(message) + except websockets.exceptions.ConnectionClosed as e: + self.LOG.warning(f"[{self.name}] WebSocket连接关闭: {e}") + break + except Exception as e: + self.LOG.exception(f"[{self.name}] 接收消息异常: {e}") + break + await asyncio.sleep(0) + + def parse_markdown(self, md_text: str): + try: + html = markdown.markdown(md_text) + soup = BeautifulSoup(html, "html.parser") + text = soup.get_text() + images = [] + for img in soup.find_all("img"): + img_src = img.get("src") + if img_src: + images.append(img_src) + return text, images + except Exception as e: + self.LOG.exception(f"[{self.name}] Markdown解析失败: {e}") + return md_text, [] + + async def message_handler(self, message: bytes): + if not self.bot: + return + try: + message_str = message.decode("utf-8") + message_json = json.loads(message_str) + except Exception as e: + self.LOG.exception(f"[{self.name}] 解析核心返回失败: {e}") + return + if message_json.get("bot_id") != "abot": + return + target_id = message_json.get("target_id", "") + content_list = message_json.get("content", []) + for msg in content_list: + t = msg.get("type") + if not t: + continue + try: + if t == "node": + data = msg.get("data", []) + for node in data: + if node.get("type") == "text": + await self.bot.send_text_message(target_id, node.get("data", "")) + if node.get("type") in ["image", "b64", "url"]: + jpg = str(node.get("data", "")).replace("base64://", "data:image/jpg;base64,") + await self.bot.send_image_message(target_id, jpg) + if t == "text": + await self.bot.send_text_message(target_id, msg.get("data", "")) + if t == "markdown": + text, images = self.parse_markdown(msg.get("data", "")) + await self.bot.send_text_message(target_id, text) + for img in images: + await self.bot.send_image_message(target_id, img) + if t == "image": + jpg = str(msg.get("data", "")).replace("base64://", "data:image/jpg;base64,") + await self.bot.send_image_message(target_id, jpg) + except Exception as e: + self.LOG.exception(f"[{self.name}] 转发消息失败(type={t}): {e}") + continue + + async def reconnect(self): + await self.close_connection() + await self.connect() + + async def close_connection(self): + if self.websocket: + await self.websocket.close() + self.websocket = None + + async def connect(self): + try: + self.LOG.info(f"[{self.name}] 连接早柚核心: {self.gscore_url}") + self.websocket = await websockets.connect(self.gscore_url, max_size=10**7) + asyncio.create_task(self.receive_message()) + except Exception as e: + self.LOG.error(f"[{self.name}] 连接早柚核心失败: {e}") + return False + return True diff --git a/requirements.txt b/requirements.txt index 4bcd239..26c78c2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -48,4 +48,6 @@ pathlib~=1.0.1 Glances~=4.3.1 aiofiles~=24.1.0 -undetected-chromedriver~=3.5.5 \ No newline at end of file +undetected-chromedriver~=3.5.5 +urllib3~=2.5.0 +websockets~=15.0.1 \ No newline at end of file