refactor(wechatclawbot): 渠道管理改为模块方法契约,endpoint 不再接触客户端对象

- 收消息方向保持 Telegram 模式:模块自建连接轮询网关,转发本地消息 API
- 管理方向(状态/二维码/退出/连通性)封装为 wechatclawbot_status 等模块契约方法,
  实例解析与临时客户端回退封闭在模块内部,统一返回 {success, message} dict
- 新建 NotificationChain 薄分发链,MessageChain 移除全部 clawbot 方法
- notification endpoint 改为纯 chain 调用,不再获取任何客户端对象
This commit is contained in:
jxxghp
2026-08-16 05:45:58 +08:00
parent 6504cb36f5
commit a66cbe6192
4 changed files with 254 additions and 90 deletions
+12 -27
View File
@@ -4,7 +4,7 @@ from fastapi import Depends
from app import schemas from app import schemas
from app.api.response import ResponseAPIRouter 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.db.models import User
from app.api.deps import get_current_active_superuser from app.api.deps import get_current_active_superuser
@@ -27,23 +27,20 @@ def wechatclawbot_status(
_: User = Depends(get_current_active_superuser), _: User = Depends(get_current_active_superuser),
): ):
"""查询微信 ClawBot 登录状态和二维码。""" """查询微信 ClawBot 登录状态和二维码。"""
client, errmsg = MessageChain().get_wechatclawbot_client( result = NotificationChain().get_wechatclawbot_status(
source=source, source=source,
fallback_source=fallback_source, fallback_source=fallback_source,
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, 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( return schemas.Response(
success=True, success=bool(result.get("success")),
data=client.get_status( message=result.get("message"),
refresh_remote=refresh_remote, data=result if result.get("success") else None,
auto_generate_qrcode=auto_generate_qrcode,
),
) )
@@ -62,18 +59,14 @@ def refresh_wechatclawbot_qrcode(
_: User = Depends(get_current_active_superuser), _: User = Depends(get_current_active_superuser),
): ):
"""刷新微信 ClawBot 二维码。""" """刷新微信 ClawBot 二维码。"""
client, errmsg = MessageChain().get_wechatclawbot_client( result = NotificationChain().refresh_wechatclawbot_qrcode(
source=source, source=source,
fallback_source=fallback_source, fallback_source=fallback_source,
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, 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( return schemas.Response(
success=bool(result.get("success")), success=bool(result.get("success")),
message=result.get("message"), message=result.get("message"),
@@ -96,18 +89,14 @@ def logout_wechatclawbot(
_: User = Depends(get_current_active_superuser), _: User = Depends(get_current_active_superuser),
): ):
"""退出微信 ClawBot 登录。""" """退出微信 ClawBot 登录。"""
client, errmsg = MessageChain().get_wechatclawbot_client( result = NotificationChain().logout_wechatclawbot(
source=source, source=source,
fallback_source=fallback_source, fallback_source=fallback_source,
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, 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( return schemas.Response(
success=bool(result.get("success")), success=bool(result.get("success")),
message=result.get("message"), message=result.get("message"),
@@ -130,19 +119,15 @@ def test_wechatclawbot(
_: User = Depends(get_current_active_superuser), _: User = Depends(get_current_active_superuser),
): ):
"""测试微信 ClawBot 当前登录态是否可用。""" """测试微信 ClawBot 当前登录态是否可用。"""
client, errmsg = MessageChain().get_wechatclawbot_client( result = NotificationChain().test_wechatclawbot_connection(
source=source, source=source,
fallback_source=fallback_source, fallback_source=fallback_source,
WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL, WECHATCLAWBOT_BASE_URL=WECHATCLAWBOT_BASE_URL,
WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET, WECHATCLAWBOT_DEFAULT_TARGET=WECHATCLAWBOT_DEFAULT_TARGET,
WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS, WECHATCLAWBOT_ADMINS=WECHATCLAWBOT_ADMINS,
WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT, WECHATCLAWBOT_POLL_TIMEOUT=WECHATCLAWBOT_POLL_TIMEOUT,
allow_temporary=True,
) )
if not client: return schemas.Response(success=bool(result.get("success")), message=result.get("message"))
return schemas.Response(success=False, message=errmsg)
state, message = client.test_connection()
return schemas.Response(success=state, message=message)
@router.post( @router.post(
@@ -158,7 +143,7 @@ def migrate_wechatclawbot_cache(
_: User = Depends(get_current_active_superuser), _: User = Depends(get_current_active_superuser),
): ):
"""在通知名称变更时迁移对应的微信 ClawBot 登录缓存。""" """在通知名称变更时迁移对应的微信 ClawBot 登录缓存。"""
success, message = MessageChain().migrate_wechatclawbot_cache( success, message = NotificationChain().migrate_wechatclawbot_cache(
old_name=old_source, old_name=old_source,
new_name=new_source, new_name=new_source,
cleanup_old=cleanup_old, cleanup_old=cleanup_old,
-54
View File
@@ -1851,57 +1851,3 @@ class MessageChain(ChainBase):
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
return None 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,
)
+114
View File
@@ -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 通知未启用")
+127 -8
View File
@@ -1,5 +1,5 @@
import json 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.runtime.cache import TTLCache
from app.domain.context import Context, MediaInfo from app.domain.context import Context, MediaInfo
@@ -83,14 +83,23 @@ class WechatClawBotModule(_ModuleBase, _MessageBase[WechatClawBot]):
"""初始化模块设置。""" """初始化模块设置。"""
pass pass
def wechatclawbot_client( def _resolve_client(
self, self,
source: Optional[str] = None, source: Optional[str] = None,
fallback_source: Optional[str] = None, fallback_source: Optional[str] = None,
): WECHATCLAWBOT_BASE_URL: Optional[str] = None,
"""按名称解析已加载的微信 ClawBot 客户端,候选名均无配置时返回默认实例。""" 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 source_name = str(source or "").strip() or None
fallback_name = str(fallback_source or "").strip() or None fallback_name = str(fallback_source or "").strip() or None
candidate_names = [] candidate_names = []
for candidate in (fallback_name, source_name): for candidate in (fallback_name, source_name):
if candidate and candidate not in candidate_names: if candidate and candidate not in candidate_names:
@@ -102,11 +111,121 @@ class WechatClawBotModule(_ModuleBase, _MessageBase[WechatClawBot]):
continue continue
client = self.get_instance(config.name) client = self.get_instance(config.name)
if client: if client:
return client return client, None
return None else:
return self.get_instance() 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, self,
source: Optional[str] = None, source: Optional[str] = None,
WECHATCLAWBOT_BASE_URL: Optional[str] = None, WECHATCLAWBOT_BASE_URL: Optional[str] = None,