refactor(chain): 处理链功能域 mixin 化,清理未使用导入并根治兼容层循环导入

- ChainBase 拆分为 RecognitionMixin/MessageProcessingMixin/NotificationMixin
- TransferChain 拆分为 7 个功能 mixin(_mixins.py),SubscribeChain 音乐订阅域拆出 _music.py
- 斜杠命令交互四件套收敛为 InteractionChainMixin 委托,会话管理器移至 application 层,chain 层不再 re-export
- 模块基础类收敛到 app/modules/_base(notification/mediaserver 语义重命名)
- 清理 app/chain/__init__.py 24 个未使用导入,修正 49 处测试 patch 目标到实际命名空间
- 兼容层 legacy 符号不再并入 __all__,根治 schemas 初始化反向拉起 application.transfer 的循环导入
- 修复 bangumi 集数为字符串时 set_bangumi_info 抛 TypeError
- 新增重复代码等架构门禁测试;capability 清单校验排除下划线内部目录
This commit is contained in:
jxxghp
2026-08-16 16:30:16 +08:00
parent 24671f8f18
commit 7e851dbfa7
102 changed files with 6041 additions and 5888 deletions
+24 -93
View File
@@ -1,4 +1,3 @@
import copy
import json
import re
import xml.dom.minidom
@@ -6,21 +5,19 @@ from typing import Optional, Union, List, Tuple, Any, Dict
from urllib.parse import quote
from app.domain.context import Context, MediaInfo
from app.runtime.events import eventmanager
from app.application.messaging.agent import (
matches_channel_admin,
register_channel_admin_resolver,
resolve_config_principal_ids,
)
from app.runtime.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules._base import _MessageChannelModuleBase
from app.adapters.external.wechat_crypt import WXBizMsgCrypt
from app.modules.wechat.wechat import WeChat
from app.modules.wechat.wechatbot import WeChatBot
from app.schemas import MessageChannel, CommingMessage, Notification, CommandRegisterEventData
from app.schemas.types import ModuleType, ChainEventType
from app.schemas import MessageChannel, CommingMessage, Notification
from app.schemas.types import ModuleType
from app.foundation.dom import DomUtils
from app.foundation.collections import DictUtils
def _resolve_wechat_admin_ids(config: Optional[dict]) -> set[str]:
@@ -34,7 +31,12 @@ def _resolve_wechat_admin_ids(config: Optional[dict]) -> set[str]:
register_channel_admin_resolver(MessageChannel.Wechat, _resolve_wechat_admin_ids)
class WechatModule(_ModuleBase, _MessageBase[WeChat]):
class WechatModule(_MessageChannelModuleBase[WeChat]):
# 管理员配置键,与渠道 resolver 保持一致
_admin_config_key = "WECHAT_ADMINS"
# 命令注册事件源标识固定为 WeChat(get_name 为“企业微信”)
_command_origin = "WeChat"
def init_module(self) -> None:
"""
@@ -82,51 +84,12 @@ class WechatModule(_ModuleBase, _MessageBase[WeChat]):
def _is_bot_mode(config: dict) -> bool:
return (config or {}).get("WECHAT_MODE", "app") == "bot"
@staticmethod
def _get_admins(config: Optional[dict]) -> List[str]:
"""
解析企业微信管理员配置,兼容逗号分隔和首尾空白。
"""
return [
admin.strip()
for admin in str((config or {}).get("WECHAT_ADMINS") or "").split(",")
if admin.strip()
]
@classmethod
def _should_reject_admin_command(
cls, config: Optional[dict], user_id: Optional[str]
) -> bool:
"""
判断企业微信菜单或斜杠命令是否应因非管理员身份被拒绝。
"""
admins = cls._get_admins(config)
if not admins:
return False
return not matches_channel_admin(
MessageChannel.Wechat,
config,
user_id,
)
@classmethod
def _create_client(cls, conf):
if cls._is_bot_mode(conf.config):
return WeChatBot(name=conf.name, **conf.config)
return WeChat(name=conf.name, **conf.config)
def test(self) -> Optional[Tuple[bool, str]]:
"""
测试模块连接性
"""
if not self.get_instances():
return None
for name, client in self.get_instances().items():
state = client.get_state()
if not state:
return False, f"企业微信 {name} 未就绪"
return True, ""
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass
@@ -457,54 +420,22 @@ class WechatModule(_ModuleBase, _MessageBase[WeChat]):
client.send_torrents_msg(title=message.title, torrents=torrents,
userid=message.userid, link=message.link)
def register_commands(self, commands: Dict[str, dict]):
def _commands_enabled(self, config: Optional[dict]) -> bool:
"""
注册命令,实现这个函数接收系统可用的命令菜单
:param commands: 命令字典
菜单注册前置条件:智能机器人模式无传统菜单,缺少解密参数时无法调用菜单 API。
"""
for client_config in self.get_configs().values():
if self._is_bot_mode(client_config.config):
logger.debug(f"{client_config.name} 为智能机器人模式,跳过传统菜单初始化")
continue
# 如果没有配置消息解密相关参数,则也没有必要进行菜单初始化
if not client_config.config.get("WECHAT_ENCODING_AESKEY") or not client_config.config.get("WECHAT_TOKEN"):
logger.debug(f"{client_config.name} 缺少消息解密参数,跳过后续菜单初始化")
continue
if self._is_bot_mode(config):
logger.debug("智能机器人模式,跳过传统菜单初始化")
return False
if not config.get("WECHAT_ENCODING_AESKEY") or not config.get("WECHAT_TOKEN"):
logger.debug("缺少消息解密参数,跳过菜单初始化")
return False
return True
client = self.get_instance(client_config.name)
if not client:
continue
def _delete_commands(self, client) -> None:
"""企业微信使用自定义菜单 API 清理命令。"""
client.delete_menus()
# 触发事件,允许调整命令数据,这里需要进行深复制,避免实例共享
scoped_commands = copy.deepcopy(commands)
event = eventmanager.send_event(
ChainEventType.CommandRegister,
CommandRegisterEventData(commands=scoped_commands, origin="WeChat", service=client_config.name)
)
# 如果事件返回有效的 event_data,使用事件中调整后的命令
if event and event.event_data:
event_data: CommandRegisterEventData = event.event_data
# 如果事件被取消,跳过命令注册,并清理菜单
if event_data.cancel:
client.delete_menus()
logger.debug(
f"Command registration for {client_config.name} canceled by event: {event_data.source}"
)
continue
scoped_commands = event_data.commands or {}
if not scoped_commands:
logger.debug("Filtered commands are empty, skipping registration.")
client.delete_menus()
# scoped_commands 必须是 commands 的子集
filtered_scoped_commands = DictUtils.filter_keys_to_subset(scoped_commands, commands)
# 如果 filtered_scoped_commands 为空,则跳过注册
if not filtered_scoped_commands:
logger.debug("Filtered commands are empty, skipping registration.")
client.delete_menus()
continue
# 对比调整后的命令与当前命令
if filtered_scoped_commands != commands:
logger.debug(f"Command set has changed, Updating new commands: {filtered_scoped_commands}")
client.create_menus(filtered_scoped_commands)
def _apply_commands(self, client, commands: Dict[str, dict]) -> None:
"""企业微信使用自定义菜单 API 注册命令。"""
client.create_menus(commands)