From a66cbe61926230344473fe6281bc9a68ddbd1d85 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 16 Aug 2026 05:45:58 +0800 Subject: [PATCH] =?UTF-8?q?refactor(wechatclawbot):=20=E6=B8=A0=E9=81=93?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=94=B9=E4=B8=BA=E6=A8=A1=E5=9D=97=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=A5=91=E7=BA=A6=EF=BC=8Cendpoint=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=8E=A5=E8=A7=A6=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 收消息方向保持 Telegram 模式:模块自建连接轮询网关,转发本地消息 API - 管理方向(状态/二维码/退出/连通性)封装为 wechatclawbot_status 等模块契约方法, 实例解析与临时客户端回退封闭在模块内部,统一返回 {success, message} dict - 新建 NotificationChain 薄分发链,MessageChain 移除全部 clawbot 方法 - notification endpoint 改为纯 chain 调用,不再获取任何客户端对象 --- app/api/endpoints/notification.py | 39 +++----- app/chain/message.py | 56 +---------- app/chain/notification.py | 114 ++++++++++++++++++++++ app/modules/wechatclawbot/__init__.py | 135 ++++++++++++++++++++++++-- 4 files changed, 254 insertions(+), 90 deletions(-) create mode 100644 app/chain/notification.py diff --git a/app/api/endpoints/notification.py b/app/api/endpoints/notification.py index 4ecf245bc..c1ac448de 100644 --- a/app/api/endpoints/notification.py +++ b/app/api/endpoints/notification.py @@ -4,7 +4,7 @@ from fastapi import Depends from app import schemas from app.api.response import ResponseAPIRouter -from app.chain.message import MessageChain +from app.chain.notification import NotificationChain from app.db.models import User from app.api.deps import get_current_active_superuser @@ -27,23 +27,20 @@ def wechatclawbot_status( _: User = Depends(get_current_active_superuser), ): """查询微信 ClawBot 登录状态和二维码。""" - client, errmsg = MessageChain().get_wechatclawbot_client( + result = NotificationChain().get_wechatclawbot_status( source=source, fallback_source=fallback_source, WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, - allow_temporary=True, + refresh_remote=refresh_remote, + auto_generate_qrcode=auto_generate_qrcode, ) - if not client: - return schemas.Response(success=False, message=errmsg) return schemas.Response( - success=True, - data=client.get_status( - refresh_remote=refresh_remote, - auto_generate_qrcode=auto_generate_qrcode, - ), + success=bool(result.get("success")), + message=result.get("message"), + data=result if result.get("success") else None, ) @@ -62,18 +59,14 @@ def refresh_wechatclawbot_qrcode( _: User = Depends(get_current_active_superuser), ): """刷新微信 ClawBot 二维码。""" - client, errmsg = MessageChain().get_wechatclawbot_client( + result = NotificationChain().refresh_wechatclawbot_qrcode( source=source, fallback_source=fallback_source, WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, - allow_temporary=True, ) - if not client: - return schemas.Response(success=False, message=errmsg) - result = client.refresh_qrcode() return schemas.Response( success=bool(result.get("success")), message=result.get("message"), @@ -96,18 +89,14 @@ def logout_wechatclawbot( _: User = Depends(get_current_active_superuser), ): """退出微信 ClawBot 登录。""" - client, errmsg = MessageChain().get_wechatclawbot_client( + result = NotificationChain().logout_wechatclawbot( source=source, fallback_source=fallback_source, WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, - allow_temporary=True, ) - if not client: - return schemas.Response(success=False, message=errmsg) - result = client.logout() return schemas.Response( success=bool(result.get("success")), message=result.get("message"), @@ -130,19 +119,15 @@ def test_wechatclawbot( _: User = Depends(get_current_active_superuser), ): """测试微信 ClawBot 当前登录态是否可用。""" - client, errmsg = MessageChain().get_wechatclawbot_client( + result = NotificationChain().test_wechatclawbot_connection( source=source, fallback_source=fallback_source, WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, - allow_temporary=True, ) - if not client: - return schemas.Response(success=False, message=errmsg) - state, message = client.test_connection() - return schemas.Response(success=state, message=message) + return schemas.Response(success=bool(result.get("success")), message=result.get("message")) @router.post( @@ -158,7 +143,7 @@ def migrate_wechatclawbot_cache( _: User = Depends(get_current_active_superuser), ): """在通知名称变更时迁移对应的微信 ClawBot 登录缓存。""" - success, message = MessageChain().migrate_wechatclawbot_cache( + success, message = NotificationChain().migrate_wechatclawbot_cache( old_name=old_source, new_name=new_source, cleanup_old=cleanup_old, diff --git a/app/chain/message.py b/app/chain/message.py index 3a9b5d429..d665e9f27 100644 --- a/app/chain/message.py +++ b/app/chain/message.py @@ -1850,58 +1850,4 @@ class MessageChain(ChainBase): return base64.b64decode(payload) except Exception as e: logger.error(e) - return None - - def get_wechatclawbot_client( - self, - source: Optional[str] = None, - fallback_source: Optional[str] = None, - WECHATCLAWBOT_BASE_URL: Optional[str] = None, - WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None, - WECHATCLAWBOT_ADMINS: Optional[str] = None, - WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None, - allow_temporary: bool = False, - ): - """获取已加载的微信 ClawBot 客户端,必要时退回到临时客户端。""" - source_name = str(source or "").strip() or None - - client = self.run_module( - "wechatclawbot_client", - source=source, - fallback_source=fallback_source, - ) - if client: - return client, None - - if allow_temporary: - temp_client = self.run_module( - "wechatclawbot_temp_client", - source=source_name or str(fallback_source or "").strip() or None, - WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, - WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, - WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, - WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, - ) - if temp_client: - return temp_client, None - - if source_name: - return None, f"未找到名为 {source_name} 的微信 ClawBot 通知配置" - return None, "微信 ClawBot 通知未启用或配置尚未保存,请先保存并启用当前渠道" - - def migrate_wechatclawbot_cache( - self, - old_name: str, - new_name: str, - cleanup_old: bool = False, - overwrite: bool = False, - ): - """在通知名称变更时迁移对应的微信 ClawBot 登录缓存。""" - return self.run_module( - "wechatclawbot_migrate_cache", - old_name=old_name, - new_name=new_name, - cleanup_old=cleanup_old, - overwrite=overwrite, - ) - + return None \ No newline at end of file diff --git a/app/chain/notification.py b/app/chain/notification.py new file mode 100644 index 000000000..39c671a07 --- /dev/null +++ b/app/chain/notification.py @@ -0,0 +1,114 @@ +from typing import Any, Dict, Tuple + +from app.chain import ChainBase + + +class NotificationChain(ChainBase): + """ + 通知渠道管理链,仅做模块方法名契约的薄分发,渠道连接与能力全部封闭在模块内部 + """ + + def get_wechatclawbot_status( + self, + source=None, + fallback_source=None, + WECHATCLAWBOT_BASE_URL=None, + WECHATCLAWBOT_DEFAULT_TARGET=None, + WECHATCLAWBOT_ADMINS=None, + WECHATCLAWBOT_POLL_TIMEOUT=None, + refresh_remote: bool = True, + auto_generate_qrcode: bool = True, + ) -> Dict[str, Any]: + """查询微信 ClawBot 登录状态与二维码。""" + result = self.run_module( + "wechatclawbot_status", + source=source, + fallback_source=fallback_source, + WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, + WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, + WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, + WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, + refresh_remote=refresh_remote, + auto_generate_qrcode=auto_generate_qrcode, + ) + return result or {"success": False, "message": "微信 ClawBot 通知未启用或配置尚未保存"} + + def refresh_wechatclawbot_qrcode( + self, + source=None, + fallback_source=None, + WECHATCLAWBOT_BASE_URL=None, + WECHATCLAWBOT_DEFAULT_TARGET=None, + WECHATCLAWBOT_ADMINS=None, + WECHATCLAWBOT_POLL_TIMEOUT=None, + ) -> Dict[str, Any]: + """刷新微信 ClawBot 登录二维码。""" + result = self.run_module( + "wechatclawbot_refresh_qrcode", + source=source, + fallback_source=fallback_source, + WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, + WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, + WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, + WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, + ) + return result or {"success": False, "message": "微信 ClawBot 通知未启用或配置尚未保存"} + + def logout_wechatclawbot( + self, + source=None, + fallback_source=None, + WECHATCLAWBOT_BASE_URL=None, + WECHATCLAWBOT_DEFAULT_TARGET=None, + WECHATCLAWBOT_ADMINS=None, + WECHATCLAWBOT_POLL_TIMEOUT=None, + ) -> Dict[str, Any]: + """退出微信 ClawBot 登录。""" + result = self.run_module( + "wechatclawbot_logout", + source=source, + fallback_source=fallback_source, + WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, + WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, + WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, + WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, + ) + return result or {"success": False, "message": "微信 ClawBot 通知未启用或配置尚未保存"} + + def test_wechatclawbot_connection( + self, + source=None, + fallback_source=None, + WECHATCLAWBOT_BASE_URL=None, + WECHATCLAWBOT_DEFAULT_TARGET=None, + WECHATCLAWBOT_ADMINS=None, + WECHATCLAWBOT_POLL_TIMEOUT=None, + ) -> Dict[str, Any]: + """测试微信 ClawBot 当前登录态是否可用。""" + result = self.run_module( + "wechatclawbot_test_connection", + source=source, + fallback_source=fallback_source, + WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, + WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, + WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, + WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, + ) + return result or {"success": False, "message": "微信 ClawBot 通知未启用或配置尚未保存"} + + def migrate_wechatclawbot_cache( + self, + old_name: str, + new_name: str, + cleanup_old: bool = False, + overwrite: bool = False, + ) -> Tuple[bool, str]: + """在通知名称变更时迁移对应的微信 ClawBot 登录缓存。""" + result = self.run_module( + "wechatclawbot_migrate_cache", + old_name=old_name, + new_name=new_name, + cleanup_old=cleanup_old, + overwrite=overwrite, + ) + return result or (False, "微信 ClawBot 通知未启用") diff --git a/app/modules/wechatclawbot/__init__.py b/app/modules/wechatclawbot/__init__.py index 3067cc3e9..17ba77de2 100644 --- a/app/modules/wechatclawbot/__init__.py +++ b/app/modules/wechatclawbot/__init__.py @@ -1,5 +1,5 @@ import json -from typing import Any, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union from app.runtime.cache import TTLCache from app.domain.context import Context, MediaInfo @@ -83,14 +83,23 @@ class WechatClawBotModule(_ModuleBase, _MessageBase[WechatClawBot]): """初始化模块设置。""" pass - def wechatclawbot_client( + def _resolve_client( self, source: Optional[str] = None, fallback_source: Optional[str] = None, - ): - """按名称解析已加载的微信 ClawBot 客户端,候选名均无配置时返回默认实例。""" + WECHATCLAWBOT_BASE_URL: Optional[str] = None, + WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None, + WECHATCLAWBOT_ADMINS: Optional[str] = None, + WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None, + ) -> Tuple[Optional[Any], Optional[str]]: + """解析微信 ClawBot 客户端实例,返回 (客户端, 错误信息)。 + + 优先使用已加载的配置实例,均无配置时退回到基于表单参数的临时客户端, + 用于未保存配置的扫码状态预览。 + """ source_name = str(source or "").strip() or None fallback_name = str(fallback_source or "").strip() or None + candidate_names = [] for candidate in (fallback_name, source_name): if candidate and candidate not in candidate_names: @@ -102,11 +111,121 @@ class WechatClawBotModule(_ModuleBase, _MessageBase[WechatClawBot]): continue client = self.get_instance(config.name) if client: - return client - return None - return self.get_instance() + return client, None + else: + client = self.get_instance() + if client: + return client, None - def wechatclawbot_temp_client( + temp_client = self._build_temp_client( + source=source_name or fallback_name, + WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, + WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, + WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, + WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, + ) + if temp_client: + return temp_client, None + + if source_name: + return None, f"未找到名为 {source_name} 的微信 ClawBot 通知配置" + return None, "微信 ClawBot 通知未启用或配置尚未保存,请先保存并启用当前渠道" + + def wechatclawbot_status( + self, + source: Optional[str] = None, + fallback_source: Optional[str] = None, + WECHATCLAWBOT_BASE_URL: Optional[str] = None, + WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None, + WECHATCLAWBOT_ADMINS: Optional[str] = None, + WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None, + refresh_remote: bool = True, + auto_generate_qrcode: bool = True, + ) -> Dict[str, Any]: + """查询微信 ClawBot 登录状态与二维码,实例解析全部封闭在模块内部。""" + client, errmsg = self._resolve_client( + source=source, + fallback_source=fallback_source, + WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, + WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, + WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, + WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, + ) + if not client: + return {"success": False, "message": errmsg} + return client.get_status( + refresh_remote=refresh_remote, + auto_generate_qrcode=auto_generate_qrcode, + ) + + def wechatclawbot_refresh_qrcode( + self, + source: Optional[str] = None, + fallback_source: Optional[str] = None, + WECHATCLAWBOT_BASE_URL: Optional[str] = None, + WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None, + WECHATCLAWBOT_ADMINS: Optional[str] = None, + WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None, + ) -> Dict[str, Any]: + """刷新微信 ClawBot 登录二维码。""" + client, errmsg = self._resolve_client( + source=source, + fallback_source=fallback_source, + WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, + WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, + WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, + WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, + ) + if not client: + return {"success": False, "message": errmsg} + return client.refresh_qrcode() + + def wechatclawbot_logout( + self, + source: Optional[str] = None, + fallback_source: Optional[str] = None, + WECHATCLAWBOT_BASE_URL: Optional[str] = None, + WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None, + WECHATCLAWBOT_ADMINS: Optional[str] = None, + WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None, + ) -> Dict[str, Any]: + """退出微信 ClawBot 登录。""" + client, errmsg = self._resolve_client( + source=source, + fallback_source=fallback_source, + WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, + WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, + WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, + WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, + ) + if not client: + return {"success": False, "message": errmsg} + return client.logout() + + def wechatclawbot_test_connection( + self, + source: Optional[str] = None, + fallback_source: Optional[str] = None, + WECHATCLAWBOT_BASE_URL: Optional[str] = None, + WECHATCLAWBOT_DEFAULT_TARGET: Optional[str] = None, + WECHATCLAWBOT_ADMINS: Optional[str] = None, + WECHATCLAWBOT_POLL_TIMEOUT: Optional[int] = None, + ) -> Dict[str, Any]: + """测试微信 ClawBot 当前登录态是否可用。""" + client, errmsg = self._resolve_client( + source=source, + fallback_source=fallback_source, + WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, + WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, + WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, + WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, + ) + if not client: + return {"success": False, "message": errmsg} + state, message = client.test_connection() + return {"success": state, "message": message} + + def _build_temp_client( self, source: Optional[str] = None, WECHATCLAWBOT_BASE_URL: Optional[str] = None,