From a3d72e43ab5f383933d641b857d379017950c977 Mon Sep 17 00:00:00 2001 From: SXP-Simon Date: Sat, 28 Mar 2026 21:53:01 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E5=88=86=E6=9E=90=E5=90=8D=E5=8D=95=E5=87=86=E5=85=A5=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=99=BD=E5=90=8D=E5=8D=95=E7=BB=95=E8=BF=87?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20=EF=BC=88=E6=83=85=E5=86=B5=E5=A6=82=20#13?= =?UTF-8?q?3=20=E6=89=80=E8=BF=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../services/analysis_application_service.py | 6 +- src/infrastructure/config/config_manager.py | 209 +++++++++++------- src/infrastructure/platform/bot_manager.py | 8 +- .../scheduler/auto_scheduler.py | 20 +- 5 files changed, 147 insertions(+), 97 deletions(-) diff --git a/.gitignore b/.gitignore index a032166..f6e53b6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ scripts/data/avatar/cache.db debug_miku.html scripts/output_all/debug_HatsuneMiku_test_group_*.pdf scripts/output/mock_report_test_group_mock_*.pdf +plugin.log diff --git a/src/application/services/analysis_application_service.py b/src/application/services/analysis_application_service.py index 4000ee7..4e7888e 100644 --- a/src/application/services/analysis_application_service.py +++ b/src/application/services/analysis_application_service.py @@ -138,7 +138,7 @@ class AnalysisApplicationService: logger.warning(f"群 {group_id} 在最近 {days} 天内无消息或无法获取") return {"success": False, "reason": "no_messages"} - # 3. 清理消息 (Filter commands, bot messages, noise) + # 3. 清理消息 (过滤指令、机器人消息、噪声) from ...domain.services.message_cleaner_service import MessageCleanerService cleaner = MessageCleanerService() @@ -192,8 +192,8 @@ class AnalysisApplicationService: chat_quality_review = None total_token_usage = TokenUsage() - # Note: LLMAnalyzer 目前可能只接收 legacy 格式或特定的 UnifiedMessage 适配 - # 暂时转换回 legacy 格式以确保稳定性,直到 LLMAnalyzer 被重构 + # 注意: LLMAnalyzer 目前可能只接收旧版格式或特定的 UnifiedMessage 适配 + # 暂时转换回旧版格式以确保稳定性,直到 LLMAnalyzer 被重构 legacy_messages = self.statistics_service._convert_to_legacy_dict( unified_messages ) diff --git a/src/infrastructure/config/config_manager.py b/src/infrastructure/config/config_manager.py index f2c21e0..d5b3c36 100644 --- a/src/infrastructure/config/config_manager.py +++ b/src/infrastructure/config/config_manager.py @@ -49,6 +49,34 @@ class ConfigManager: """获取群组列表(用于黑白名单)""" return self._get_group("basic").get("group_list", []) + @staticmethod + def _match_group_item(item: str, target: str) -> bool: + """匹配列表项与 UMO/简单群 ID/话题父 ID。""" + item = str(item).strip() + target = str(target).strip() + + target_simple_id = target.split(":")[-1] if ":" in target else target + target_parent_id = ( + target_simple_id.split("#", 1)[0] + if "#" in target_simple_id + else target_simple_id + ) + + if ":" in item: + if item == target: + return True + + if "#" in target_simple_id and ":" in target: + item_prefix, item_tail = item.rsplit(":", 1) + target_prefix, _ = target.rsplit(":", 1) + return item_prefix == target_prefix and item_tail == target_parent_id + return False + + if item == target_simple_id: + return True + + return "#" in target_simple_id and item == target_parent_id + def is_group_allowed(self, group_id_or_umo: str) -> bool: """ 根据配置的白/黑名单判断是否允许在该群聊中使用 @@ -61,47 +89,9 @@ class ConfigManager: if mode == "none": return True - glist = [str(g) for g in self.get_group_list()] - target = str(group_id_or_umo) - - target_simple_id = target.split(":")[-1] if ":" in target else target - target_parent_id = ( - target_simple_id.split("#", 1)[0] - if "#" in target_simple_id - else target_simple_id - ) - - def _is_match( - item: str, - target: str, - target_simple_id: str, - target_parent_id: str, - ) -> bool: - if ":" in item: - if item == target: - return True - - # 允许 Telegram 话题会话通过“父 UMO”命中, - # 例如: item=telegram2:GroupMessage:-1001 - # target=telegram2:GroupMessage:-1001#2264 - if "#" in target_simple_id: - if ":" not in target: - return False - item_prefix, item_tail = item.rsplit(":", 1) - target_prefix, _ = target.rsplit(":", 1) - return ( - item_prefix == target_prefix and item_tail == target_parent_id - ) - return False - if item == target_simple_id: - return True - # 允许 Telegram 话题会话通过父群 ID 命中简单群号白/黑名单 - return "#" in target_simple_id and item == target_parent_id - - is_in_list = any( - _is_match(item, target, target_simple_id, target_parent_id) - for item in glist - ) + glist = [str(g).strip() for g in self.get_group_list()] + target = str(group_id_or_umo).strip() + is_in_list = any(self._match_group_item(item, target) for item in glist) if mode == "whitelist": return is_in_list @@ -110,6 +100,58 @@ class ConfigManager: return True + def is_allowed_by_astr_whitelist( + self, group_id_or_umo: str, astrbot_config: dict | None + ) -> bool: + """ + 根据 Astr 全局白名单设置校验群组访问。 + + 规则与 Astr WhitelistCheckStage 保持一致: + - 白名单未启用 -> 允许 + - 白名单为空 -> 允许 + - 否则,目标必须匹配 UMO 或群组 ID 格式的条目 + """ + if not astrbot_config: + return True + + try: + platform_settings = astrbot_config.get("platform_settings", {}) + enable_whitelist = bool( + platform_settings.get("enable_id_white_list", False) + ) + whitelist = [ + str(item).strip() + for item in platform_settings.get("id_whitelist", []) + if str(item).strip() + ] + + if not enable_whitelist or not whitelist: + return True + + target = str(group_id_or_umo).strip() + return any(self._match_group_item(item, target) for item in whitelist) + except Exception as e: + logger.warning(f"Astr whitelist check failed, deny by default: {e}") + return False + + def is_group_allowed_for_scheduled_task( + self, group_id_or_umo: str, astrbot_config: dict | None = None + ) -> bool: + """定时分析任务的统一准入控制网关。""" + target = str(group_id_or_umo).strip() + + if not self.is_allowed_by_astr_whitelist(target, astrbot_config): + return False + + if not self.is_group_allowed(target): + return False + + return self.is_group_in_filtered_list( + target, + self.get_scheduled_group_list_mode(), + self.get_scheduled_group_list(), + ) + def get_max_messages(self) -> int: """获取最大消息数量""" return self._get_group("basic").get("max_messages", 1000) @@ -136,6 +178,32 @@ class ConfigManager: return val_list return val if isinstance(val, list) else ["09:00"] + def is_auto_analysis_enabled(self) -> bool: + """检查自动分析总开关是否启用 (根据名单模式和列表内容判断)""" + mode = self.get_scheduled_group_list_mode().lower() + lst = self.get_scheduled_group_list() + return (mode == "whitelist" and len(lst) > 0) or (mode == "blacklist") + + def get_scheduled_group_list_mode(self) -> str: + """获取定时分析名单模式 (whitelist/blacklist)""" + return self._get_group("auto_analysis").get( + "scheduled_group_list_mode", "whitelist" + ) + + def set_scheduled_group_list_mode(self, mode: str): + """设置定时分析名单模式""" + self._ensure_group("auto_analysis")["scheduled_group_list_mode"] = mode + self.config.save_config() + + def get_scheduled_group_list(self) -> list[str]: + """获取定时分析目标群列表""" + return self._get_group("auto_analysis").get("scheduled_group_list", []) + + def set_scheduled_group_list(self, groups: list[str]): + """设置定时分析目标群列表""" + self._ensure_group("auto_analysis")["scheduled_group_list"] = groups + self.config.save_config() + def get_enable_auto_analysis(self) -> bool: """ 获取是否启用自动分析(兼容旧接口)。 @@ -378,40 +446,6 @@ class ConfigManager: self._ensure_group("basic")["analysis_days"] = days self.config.save_config() - def set_auto_analysis_time(self, time_val: str | list[str]): - """设置自动分析时间点""" - self._ensure_group("auto_analysis")["auto_analysis_time"] = time_val - self.config.save_config() - - def is_auto_analysis_enabled(self) -> bool: - """ - 判断自动分析功能是否通过名单“按需开启”。 - 逻辑:如果是白名单模式且名单不为空,或者为黑名单模式,则视为开启。 - """ - mode = self.get_scheduled_group_list_mode() - lst = self.get_scheduled_group_list() - return (mode == "whitelist" and len(lst) > 0) or (mode == "blacklist") - - def get_scheduled_group_list_mode(self) -> str: - """获取定时分析名单模式 (whitelist/blacklist)""" - return self._get_group("auto_analysis").get( - "scheduled_group_list_mode", "whitelist" - ) - - def set_scheduled_group_list_mode(self, mode: str): - """设置定时分析名单模式""" - self._ensure_group("auto_analysis")["scheduled_group_list_mode"] = mode - self.config.save_config() - - def get_scheduled_group_list(self) -> list[str]: - """获取定时分析目标群列表""" - return self._get_group("auto_analysis").get("scheduled_group_list", []) - - def set_scheduled_group_list(self, groups: list[str]): - """设置定时分析目标群列表""" - self._ensure_group("auto_analysis")["scheduled_group_list"] = groups - self.config.save_config() - def is_group_in_filtered_list( self, group_umo_or_id: str, mode: str, group_list: list ) -> bool: @@ -420,7 +454,7 @@ class ConfigManager: 逻辑如下: - whitelist 模式: - - 如果列表为空,则视为“此级别未开启”。 + - 如果列表为空,则视为“此级别不开启”。 - 如果不为空,仅在列表中的通过。 - blacklist 模式: - 在列表中的不通过。 @@ -428,13 +462,21 @@ class ConfigManager: """ group_list = [str(x).strip() for x in group_list] target = str(group_umo_or_id).strip() + mode = str(mode).lower() # 兼容 UMO 匹配 (如果列表里写的是 ID,UMO 也能匹配上) def match_umo(umo: str, item: str) -> bool: if umo == item: return True - if ":" in umo and umo.split(":")[-1] == item: - return True + if ":" in umo: + parts = umo.split(":") + simple_id = parts[-1] + # 基础 ID 匹配 + if simple_id == item: + return True + # 兼容 Telegram Topic 匹配 (如果 item 是 Parent ID,命中 Topic ID) + if "#" in simple_id and simple_id.split("#")[0] == item: + return True return False if mode == "whitelist": @@ -442,11 +484,20 @@ class ConfigManager: # 白名单为空:此级别不开启 (按需开启逻辑) return False return any(match_umo(target, x) for x in group_list) - else: # blacklist + elif mode == "blacklist": if not group_list: # 黑名单为空:全通过 return True return not any(match_umo(target, x) for x in group_list) + else: + # 未知模式:默认不通过 (安全策略) + logger.warning(f"未知过滤模式: {mode}, 默认不通过。") + return False + + def set_auto_analysis_time(self, time_val: str | list[str]): + """设置自动分析时间点""" + self._ensure_group("auto_analysis")["auto_analysis_time"] = time_val + self.config.save_config() def set_min_messages_threshold(self, threshold: int): """设置最小消息阈值""" diff --git a/src/infrastructure/platform/bot_manager.py b/src/infrastructure/platform/bot_manager.py index f11baaf..87606dc 100644 --- a/src/infrastructure/platform/bot_manager.py +++ b/src/infrastructure/platform/bot_manager.py @@ -134,7 +134,7 @@ class BotManager: if not bot_client and hasattr(platform, "bot"): bot_client = platform.bot if not bot_client and hasattr(platform, "client"): - # AstrBot v4.14.4 DiscordPlatformAdapter uses 'client' attribute + # AstrBot v4.14.4 DiscordPlatformAdapter 使用 'client' 属性 bot_client = platform.client if bot_client: @@ -348,7 +348,7 @@ class BotManager: platform_id = metadata.get("id") if platform_id: - # KNOWLEDGE DISCOVERY: Log metadata for debugging custom IDs + # 知识点发现: 记录元数据以调试自定义 ID logger.info( f"[群分析插件 BotManager]: Log metadata for debugging custom IDs ,Platform: {platform_id}, Metadata Type: {getattr(metadata, 'type', 'N/A')}, Metadata Name: {getattr(metadata, 'name', 'N/A')}" ) @@ -474,10 +474,10 @@ class BotManager: return str(bot_instance.self_id) elif hasattr(bot_instance, "user_id") and bot_instance.user_id: return str(bot_instance.user_id) - # Discord.py style: client.user.id + # Discord.py 风格: client.user.id elif hasattr(bot_instance, "user") and hasattr(bot_instance.user, "id"): return str(bot_instance.user.id) - # python-telegram-bot style: bot.id + # python-telegram-bot 风格: bot.id elif hasattr(bot_instance, "id") and bot_instance.id: return str(bot_instance.id) return None diff --git a/src/infrastructure/scheduler/auto_scheduler.py b/src/infrastructure/scheduler/auto_scheduler.py index 8a56972..02b3972 100644 --- a/src/infrastructure/scheduler/auto_scheduler.py +++ b/src/infrastructure/scheduler/auto_scheduler.py @@ -49,7 +49,7 @@ class AutoScheduler: self.scheduler_job_ids = [] # 存储已注册的定时任务 ID self.last_executed_target = None # 记录上次执行的具体时间点,防止重复执行 - # Cache: group_id -> group_name (populated lazily) + # 缓存: group_id -> group_name (延迟加载) self._group_name_cache: dict[str, str] = {} self._terminating = False # 终止标志位 @@ -289,9 +289,12 @@ class AutoScheduler: # 获取基础信息 all_groups = await self._get_all_groups() - # 预加载所有配置名单和模式 - sched_list = self.config_manager.get_scheduled_group_list() - sched_list_mode = self.config_manager.get_scheduled_group_list_mode() + astrbot_config = None + if self.plugin_instance and getattr(self.plugin_instance, "context", None): + try: + astrbot_config = self.plugin_instance.context.get_config() + except Exception as e: + logger.warning(f"读取 Astr 全局配置失败,默认拒绝调度目标: {e}") incr_list = self.config_manager.get_incremental_group_list() incr_list_mode = self.config_manager.get_incremental_group_list_mode() @@ -303,13 +306,8 @@ class AutoScheduler: group_id = str(group_id_orig) umo = f"{platform_id}:GroupMessage:{group_id}" - # 1. 准入层判定 (基础黑白名单) - if not self.config_manager.is_group_allowed(umo): - continue - - # 2. 定时层判定 (定时分析黑白名单) - if not self.config_manager.is_group_in_filtered_list( - umo, sched_list_mode, sched_list + if not self.config_manager.is_group_allowed_for_scheduled_task( + umo, astrbot_config ): continue