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
+5 -97
View File
@@ -1,28 +1,24 @@
import copy
import json
import re
from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.parse import quote, unquote
from app.domain.context import MediaInfo, Context
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.modules.slack.slack import Slack
from app.schemas import (
CommandRegisterEventData,
CommingMessage,
MessageChannel,
MessageResponse,
Notification,
)
from app.schemas.types import ChainEventType, ModuleType
from app.foundation.collections import DictUtils
from app.schemas.types import ModuleType
register_channel_admin_resolver(
@@ -31,7 +27,9 @@ register_channel_admin_resolver(
)
class SlackModule(_ModuleBase, _MessageBase[Slack]):
class SlackModule(_MessageChannelModuleBase[Slack]):
# 管理员配置键,与渠道 resolver 保持一致
_admin_config_key = "SLACK_ADMINS"
PROCESSING_REACTION = "eyes"
_AUDIO_SUFFIXES = (
".mp3",
@@ -88,51 +86,9 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
except Exception as err:
logger.error(f"停止Slack模块实例失败:{err}")
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"Slack {name} 未就绪"
return True, ""
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass
@staticmethod
def _get_admins(config: Optional[dict]) -> List[str]:
"""
解析 Slack 管理员配置,兼容逗号分隔和首尾空白。
"""
return [
admin.strip()
for admin in str((config or {}).get("SLACK_ADMINS") or "").split(",")
if admin.strip()
]
@classmethod
def _should_reject_admin_command(
cls,
config: Optional[dict],
*user_ids: Optional[Union[str, int]],
) -> bool:
"""
判断 Slack 命令或命令型按钮回调是否应因非管理员身份被拒绝。
"""
admins = cls._get_admins(config)
if not admins:
return False
candidates = [
str(user_id).strip()
for user_id in user_ids
if user_id is not None and str(user_id).strip()
]
return not any(candidate in admins for candidate in candidates)
@staticmethod
def _send_admin_denied(client: Optional[Slack], userid: Optional[Union[str, int]]) -> None:
"""
@@ -688,54 +644,6 @@ class SlackModule(_ModuleBase, _MessageBase[Slack]):
return True
return False
def register_commands(self, commands: Dict[str, dict]) -> None:
"""
注册命令,实现这个函数接收系统可用的命令菜单。
:param commands: 命令字典
"""
for client_config in self.get_configs().values():
client = self.get_instance(client_config.name)
if not client:
continue
scoped_commands = copy.deepcopy(commands)
event = eventmanager.send_event(
ChainEventType.CommandRegister,
CommandRegisterEventData(
commands=scoped_commands,
origin="Slack",
service=client_config.name,
),
)
if event and event.event_data:
event_data: CommandRegisterEventData = event.event_data
if event_data.cancel:
client.delete_commands()
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_commands()
filtered_scoped_commands = DictUtils.filter_keys_to_subset(
scoped_commands,
commands,
)
if not filtered_scoped_commands:
logger.debug("Filtered commands are empty, skipping registration.")
client.delete_commands()
continue
if filtered_scoped_commands != commands:
logger.debug(
f"Command set has changed, Updating new commands: {filtered_scoped_commands}"
)
client.register_commands(filtered_scoped_commands)
def mark_message_processing_started(
self,
channel: MessageChannel,