From c868faf0cf5a4de137cb2fccba719a2bbe4b6ec6 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 13 Sep 2026 14:22:59 +0800 Subject: [PATCH] fix(synologychat): skip unsupported command menu registration --- app/modules/synologychat/__init__.py | 14 +++++++++ .../fixtures/architecture/ruff-baseline.json | 3 -- .../test_synologychat_command_registration.py | 30 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 tests/test_synologychat_command_registration.py diff --git a/app/modules/synologychat/__init__.py b/app/modules/synologychat/__init__.py index e298f401e..1e927781c 100644 --- a/app/modules/synologychat/__init__.py +++ b/app/modules/synologychat/__init__.py @@ -25,6 +25,8 @@ register_channel_admin_resolver( class SynologyChatModule(_MessageChannelModuleBase[SynologyChat]): + """负责 Synology Chat 消息收发、附件解析和管理员命令校验。""" + # 管理员配置键,与渠道 resolver 保持一致 _admin_config_key = "SYNOLOGYCHAT_ADMINS" _IMAGE_SUFFIXES = ( @@ -62,6 +64,7 @@ class SynologyChatModule(_MessageChannelModuleBase[SynologyChat]): @staticmethod def get_name() -> str: + """获取模块显示名称""" return "Synology Chat" @staticmethod @@ -85,10 +88,16 @@ class SynologyChatModule(_MessageChannelModuleBase[SynologyChat]): """ return 5 + def _commands_enabled(self, config: Optional[dict[str, Any]]) -> bool: + """客户端不提供命令菜单注册或删除 API,跳过基类的菜单注册流程。""" + return False + def stop(self): + """客户端没有需要在模块停止时释放的后台资源。""" pass def init_setting(self) -> Tuple[str, Union[str, bool]]: + """实例由通知服务配置管理,不额外声明环境开关。""" pass @staticmethod @@ -167,6 +176,7 @@ class SynologyChatModule(_MessageChannelModuleBase[SynologyChat]): def _extract_images( cls, message: dict ) -> Optional[List[IncomingMessage.MessageImage]]: + """从消息字段和附件中提取并去重图片引用""" images = [] for key in ("file_url", "image_url", "pic_url"): value = message.get(key) @@ -206,6 +216,7 @@ class SynologyChatModule(_MessageChannelModuleBase[SynologyChat]): @classmethod def _extract_audio_refs(cls, message: dict) -> Optional[List[str]]: + """从消息字段和附件中提取并去重音频下载引用""" audio_refs = [] for key in ("audio_url", "voice_url", "file_url"): value = message.get(key) @@ -249,6 +260,7 @@ class SynologyChatModule(_MessageChannelModuleBase[SynologyChat]): @classmethod def _looks_like_image(cls, value: str) -> bool: + """判断 HTTP 地址是否包含支持的图片扩展名""" if not value or not isinstance(value, str): return False lowered = value.lower() @@ -258,6 +270,7 @@ class SynologyChatModule(_MessageChannelModuleBase[SynologyChat]): @classmethod def _looks_like_audio(cls, value: str) -> bool: + """判断 HTTP 地址是否包含支持的音频扩展名""" if not value or not isinstance(value, str): return False lowered = value.lower() @@ -269,6 +282,7 @@ class SynologyChatModule(_MessageChannelModuleBase[SynologyChat]): def _extract_files( cls, message: dict ) -> Optional[List[IncomingMessage.MessageAttachment]]: + """提取普通附件下载引用,排除已归类的图片和音频""" files = [] for key in ("attachments", "files"): raw_value = message.get(key) diff --git a/tests/fixtures/architecture/ruff-baseline.json b/tests/fixtures/architecture/ruff-baseline.json index 9fefd1a70..be7250c3e 100644 --- a/tests/fixtures/architecture/ruff-baseline.json +++ b/tests/fixtures/architecture/ruff-baseline.json @@ -296,9 +296,6 @@ "app/modules/listenbrainz/__init__.py": { "I001": 1 }, - "app/modules/lrclib/__init__.py": { - "I001": 1 - }, "app/modules/navidrome/__init__.py": { "F401": 2, "I001": 1 diff --git a/tests/test_synologychat_command_registration.py b/tests/test_synologychat_command_registration.py new file mode 100644 index 000000000..fd45f7610 --- /dev/null +++ b/tests/test_synologychat_command_registration.py @@ -0,0 +1,30 @@ +"""Synology Chat 不支持命令菜单时的渠道注册回归测试。""" + +from types import SimpleNamespace +from unittest.mock import patch + +import pytest + +from app.modules.synologychat import SynologyChatModule +from app.modules.synologychat.synologychat import SynologyChat + + +@pytest.mark.parametrize("commands", [{}, {"/version": {"description": "当前版本"}}]) +def test_synologychat_skips_command_menu_registration(commands): + """空菜单和非空菜单均应跳过客户端调用及菜单事件,重复注册也不报错。""" + module = SynologyChatModule() + client = SynologyChat.__new__(SynologyChat) + configs = { + name: SimpleNamespace(name=name, config={"SYNOLOGYCHAT_TOKEN": "test-token"}) + for name in ("synology-main", "synology-other") + } + with ( + patch.object(module, "get_configs", return_value=configs), + patch.object(module, "get_instance", return_value=client) as get_instance, + patch("app.modules._base.notification.eventmanager.send_event", return_value=None) as send_event, + ): + module.register_commands(commands) + module.register_commands(commands) + + get_instance.assert_not_called() + send_event.assert_not_called()