diff --git a/_conf_schema.json b/_conf_schema.json index 57a55f4..91236bf 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -117,6 +117,24 @@ "default": 2, "hint": "重试之间的基准等待时间(秒),实际等待时间为基值乘以尝试次数。" }, + "topic_max_tokens": { + "type": "int", + "description": "(兼容部分提供商)话题分析最大 Token 数", + "default": 12288, + "hint": "(兼容部分提供商,实测大部分模型调整后没有明显效果)话题分析时 LLM 能生成的最大 token 数量。当分析内容较多或者分析提示词复杂时,建议适当调大此值以保证输出质量。" + }, + "golden_quote_max_tokens": { + "type": "int", + "description": "(兼容部分提供商)金句分析最大 Token 数", + "default": 4096, + "hint": "(兼容部分提供商,实测大部分模型调整后没有明显效果)金句分析时 LLM 能生成的最大 token 数量。当分析内容较多或者分析提示词复杂时,建议适当调大此值以保证输出质量。" + }, + "user_title_max_tokens": { + "type": "int", + "description": "(兼容部分提供商)用户称号分析最大 Token 数", + "default": 4096, + "hint": "(兼容部分提供商,实测大部分模型调整后没有明显效果)用户称号分析时 LLM 能生成的最大 token 数量。当分析内容较多或者分析提示词复杂时,建议适当调大此值以保证输出质量。" + }, "custom_api_key": { "type": "string", "description": "自定义 LLM 服务 API Key (选填)", diff --git a/src/analysis/analyzers/golden_quote_analyzer.py b/src/analysis/analyzers/golden_quote_analyzer.py index 60ef1d8..5417860 100644 --- a/src/analysis/analyzers/golden_quote_analyzer.py +++ b/src/analysis/analyzers/golden_quote_analyzer.py @@ -29,7 +29,7 @@ class GoldenQuoteAnalyzer(BaseAnalyzer): def get_max_tokens(self) -> int: """获取最大token数""" - return 1500 + return self.config_manager.get_golden_quote_max_tokens() def get_temperature(self) -> float: """获取温度参数""" diff --git a/src/analysis/analyzers/topic_analyzer.py b/src/analysis/analyzers/topic_analyzer.py index a832ccb..8399224 100644 --- a/src/analysis/analyzers/topic_analyzer.py +++ b/src/analysis/analyzers/topic_analyzer.py @@ -29,7 +29,7 @@ class TopicAnalyzer(BaseAnalyzer): def get_max_tokens(self) -> int: """获取最大token数""" - return 10000 + return self.config_manager.get_topic_max_tokens() def get_temperature(self) -> float: """获取温度参数""" diff --git a/src/analysis/analyzers/user_title_analyzer.py b/src/analysis/analyzers/user_title_analyzer.py index ab04d41..127c24d 100644 --- a/src/analysis/analyzers/user_title_analyzer.py +++ b/src/analysis/analyzers/user_title_analyzer.py @@ -26,7 +26,7 @@ class UserTitleAnalyzer(BaseAnalyzer): def get_max_tokens(self) -> int: """获取最大token数""" - return 1500 + return self.config_manager.get_user_title_max_tokens() def get_temperature(self) -> float: """获取温度参数""" @@ -136,22 +136,42 @@ class UserTitleAnalyzer(BaseAnalyzer): logger.error(f"创建用户称号对象失败: {e}") return [] - def prepare_user_data(self, messages: List[Dict], user_analysis: Dict) -> Dict: + def prepare_user_data(self, messages: List[Dict], user_analysis: Dict, top_users: List[Dict] = None) -> Dict: """ 准备用户数据 Args: messages: 群聊消息列表 user_analysis: 用户分析统计 + top_users: 活跃用户列表(从get_top_users获取) Returns: 准备好的用户数据字典 """ try: + # 获取机器人QQ号用于过滤 + bot_qq_id = self.config_manager.get_bot_qq_id() + user_summaries = [] + # 如果提供了top_users列表,只分析这些活跃用户 + if top_users: + logger.info(f"使用get_top_users筛选出的 {len(top_users)} 个活跃用户进行称号分析") + target_user_ids = {str(user['user_id']) for user in top_users} + else: + # 兼容旧逻辑:如果没有提供top_users,则使用所有消息数>=5的用户 + logger.info("未提供活跃用户列表,使用消息数>=5的用户") + target_user_ids = {user_id for user_id, stats in user_analysis.items() + if stats["message_count"] >= 5} + for user_id, stats in user_analysis.items(): - if stats["message_count"] < 5: # 过滤活跃度太低的用户 + # 过滤机器人自己的消息 + if bot_qq_id and str(user_id) == str(bot_qq_id): + logger.debug(f"过滤掉机器人QQ号: {user_id}") + continue + + # 只处理活跃用户 + if user_id not in target_user_ids: continue # 分析用户特征 @@ -164,18 +184,16 @@ class UserTitleAnalyzer(BaseAnalyzer): "qq": int(user_id), "message_count": stats["message_count"], "avg_chars": round(avg_chars, 1), - "emoji_ratio": round(stats["emoji_count"] / stats["message_count"], 2), - "night_ratio": round(night_messages / stats["message_count"], 2), - "reply_ratio": round(stats["reply_count"] / stats["message_count"], 2) + "emoji_ratio": round(stats["emoji_count"] / stats["message_count"], 2) if stats["message_count"] > 0 else 0, + "night_ratio": round(night_messages / stats["message_count"], 2) if stats["message_count"] > 0 else 0, + "reply_ratio": round(stats["reply_count"] / stats["message_count"], 2) if stats["message_count"] > 0 else 0 }) if not user_summaries: return {"user_summaries": []} - # 按消息数量排序,取前N名 - max_user_titles = self.get_max_count() + # 按消息数量排序 user_summaries.sort(key=lambda x: x["message_count"], reverse=True) - user_summaries = user_summaries[:max_user_titles] return {"user_summaries": user_summaries} @@ -183,7 +201,7 @@ class UserTitleAnalyzer(BaseAnalyzer): logger.error(f"准备用户数据失败: {e}") return {"user_summaries": []} - async def analyze_user_titles(self, messages: List[Dict], user_analysis: Dict, umo: str = None) -> Tuple[List[UserTitle], TokenUsage]: + async def analyze_user_titles(self, messages: List[Dict], user_analysis: Dict, umo: str = None, top_users: List[Dict] = None) -> Tuple[List[UserTitle], TokenUsage]: """ 分析用户称号 @@ -191,19 +209,20 @@ class UserTitleAnalyzer(BaseAnalyzer): messages: 群聊消息列表 user_analysis: 用户分析统计 umo: 模型唯一标识符 + top_users: 活跃用户列表(从get_top_users获取,可选) Returns: (用户称号列表, Token使用统计) """ try: - # 准备用户数据 - user_data = self.prepare_user_data(messages, user_analysis) + # 准备用户数据,传入活跃用户列表 + user_data = self.prepare_user_data(messages, user_analysis, top_users) if not user_data["user_summaries"]: logger.info("没有符合条件的用户,返回空结果") return [], TokenUsage() - logger.info(f"开始分析 {len(user_data['user_summaries'])} 个用户的称号") + logger.info(f"开始分析 {len(user_data['user_summaries'])} 个活跃用户的称号") return await self.analyze(user_data, umo) except Exception as e: diff --git a/src/analysis/llm_analyzer.py b/src/analysis/llm_analyzer.py index 973aa05..ebd11f9 100644 --- a/src/analysis/llm_analyzer.py +++ b/src/analysis/llm_analyzer.py @@ -57,7 +57,7 @@ class LLMAnalyzer: logger.error(f"话题分析失败: {e}") return [], TokenUsage() - async def analyze_user_titles(self, messages: List[Dict], user_analysis: Dict, umo: str = None) -> Tuple[List[UserTitle], TokenUsage]: + async def analyze_user_titles(self, messages: List[Dict], user_analysis: Dict, umo: str = None, top_users: List[Dict] = None) -> Tuple[List[UserTitle], TokenUsage]: """ 使用LLM分析用户称号 保持原有接口,委托给专门的UserTitleAnalyzer处理 @@ -66,13 +66,14 @@ class LLMAnalyzer: messages: 群聊消息列表 user_analysis: 用户分析统计 umo: 模型唯一标识符 + top_users: 活跃用户列表(可选) Returns: (用户称号列表, Token使用统计) """ try: logger.info("开始用户称号分析") - return await self.user_title_analyzer.analyze_user_titles(messages, user_analysis, umo) + return await self.user_title_analyzer.analyze_user_titles(messages, user_analysis, umo, top_users) except Exception as e: logger.error(f"用户称号分析失败: {e}") return [], TokenUsage() @@ -96,7 +97,7 @@ class LLMAnalyzer: logger.error(f"金句分析失败: {e}") return [], TokenUsage() - async def analyze_all_concurrent(self, messages: List[Dict], user_analysis: Dict, umo: str = None) -> Tuple[List[SummaryTopic], List[UserTitle], List[GoldenQuote], TokenUsage]: + async def analyze_all_concurrent(self, messages: List[Dict], user_analysis: Dict, umo: str = None, top_users: List[Dict] = None) -> Tuple[List[SummaryTopic], List[UserTitle], List[GoldenQuote], TokenUsage]: """ 并发执行所有分析任务(话题、用户称号、金句) @@ -104,6 +105,7 @@ class LLMAnalyzer: messages: 群聊消息列表 user_analysis: 用户分析统计 umo: 模型唯一标识符 + top_users: 活跃用户列表(可选) Returns: (话题列表, 用户称号列表, 金句列表, 总Token使用统计) @@ -114,7 +116,7 @@ class LLMAnalyzer: # 并发执行三个分析任务 results = await asyncio.gather( self.topic_analyzer.analyze_topics(messages, umo), - self.user_title_analyzer.analyze_user_titles(messages, user_analysis, umo), + self.user_title_analyzer.analyze_user_titles(messages, user_analysis, umo, top_users), self.golden_quote_analyzer.analyze_golden_quotes(messages, umo), return_exceptions=True ) diff --git a/src/analysis/statistics.py b/src/analysis/statistics.py index c1bbb1a..473ac5d 100644 --- a/src/analysis/statistics.py +++ b/src/analysis/statistics.py @@ -17,6 +17,9 @@ class UserAnalyzer: def analyze_users(self, messages: List[Dict]) -> Dict[str, Dict]: """分析用户活跃度""" + # 获取机器人QQ号用于过滤 + bot_qq_id = self.config_manager.get_bot_qq_id() + user_stats = defaultdict(lambda: { "message_count": 0, "char_count": 0, @@ -29,6 +32,11 @@ class UserAnalyzer: for msg in messages: sender = msg.get("sender", {}) user_id = str(sender.get("user_id", "")) + + # 跳过机器人自己的消息,避免进入统计 + if bot_qq_id and user_id == str(bot_qq_id): + continue + nickname = InfoUtils.get_user_nickname(self.config_manager, sender) user_stats[user_id]["message_count"] += 1 @@ -69,8 +77,15 @@ class UserAnalyzer: def get_top_users(self, user_analysis: Dict[str, Dict], limit: int = 10) -> List[Dict]: """获取最活跃的用户""" + # 获取机器人QQ号用于过滤 + bot_qq_id = self.config_manager.get_bot_qq_id() + users = [] for user_id, stats in user_analysis.items(): + # 过滤机器人自己 + if bot_qq_id and str(user_id) == str(bot_qq_id): + continue + users.append({ "user_id": user_id, "nickname": stats["nickname"], diff --git a/src/analysis/utils/llm_utils.py b/src/analysis/utils/llm_utils.py index 381af57..002e9fc 100644 --- a/src/analysis/utils/llm_utils.py +++ b/src/analysis/utils/llm_utils.py @@ -38,7 +38,7 @@ async def call_provider_with_retry(context, config_manager, prompt: str, max_tok for attempt in range(1, retries + 1): try: if custom_api_key and custom_api_base and custom_model: - logger.info(f"使用自定义LLM提供商: {custom_api_base} model={custom_model}") + logger.info(f"使用自定义LLM提供商: {custom_api_base} model={custom_model}, max_tokens={max_tokens}, temperature={temperature}") logger.debug(f"自定义LLM提供商 prompt 长度: {len(prompt) if prompt else 0}") logger.debug(f"自定义LLM提供商 prompt 前100字符: {prompt[:100] if prompt else 'None'}...") @@ -102,7 +102,7 @@ async def call_provider_with_retry(context, config_manager, prompt: str, max_tok if not provider or provider_id == 'unknown': logger.warning(f"获取的提供商不正确 (Provider ID: {provider_id})") - logger.info(f"使用LLM provider: {provider}") + logger.info(f"使用LLM provider: {provider}, max_tokens={max_tokens}, temperature={temperature}") if not provider: logger.error("provider 为空,无法调用 text_chat,直接返回 None") return None diff --git a/src/core/config.py b/src/core/config.py index 954fe06..82fcb9e 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -87,6 +87,18 @@ class ConfigManager: """获取LLM请求重试退避基值(秒),实际退避会乘以尝试次数""" return self.config.get("llm_backoff", 2) + def get_topic_max_tokens(self) -> int: + """获取话题分析最大token数""" + return self.config.get("topic_max_tokens", 12288) + + def get_golden_quote_max_tokens(self) -> int: + """获取金句分析最大token数""" + return self.config.get("golden_quote_max_tokens", 4096) + + def get_user_title_max_tokens(self) -> int: + """获取用户称号分析最大token数""" + return self.config.get("user_title_max_tokens", 4096) + def get_custom_api_key(self) -> str: """获取自定义 LLM 服务的 API Key""" return self.config.get("custom_api_key", "") diff --git a/src/utils/helpers.py b/src/utils/helpers.py index 339d263..b538f77 100644 --- a/src/utils/helpers.py +++ b/src/utils/helpers.py @@ -8,7 +8,7 @@ from ...src.models.data_models import GroupStatistics, SummaryTopic, UserTitle, from ...src.core.message_handler import MessageHandler from ...src.analysis.llm_analyzer import LLMAnalyzer from ...src.analysis.statistics import UserAnalyzer - +from astrbot.api import logger class MessageAnalyzer: """消息分析器 - 整合所有分析功能""" @@ -36,6 +36,11 @@ class MessageAnalyzer: # 用户分析 user_analysis = self.user_analyzer.analyze_users(messages) + + # 获取活跃用户列表 - 使用get_top_users方法,limit从配置中读取 + max_user_titles = self.config_manager.get_max_user_titles() + top_users = self.user_analyzer.get_top_users(user_analysis, limit=max_user_titles) + logger.info(f"获取到 {len(top_users)} 个活跃用户用于称号分析(配置上限: {max_user_titles})") # LLM分析 - 使用并发方式 topics = [] @@ -50,9 +55,9 @@ class MessageAnalyzer: # 如果三个分析都启用,使用并发执行 if topic_enabled and user_title_enabled and golden_quote_enabled: - # 并发执行所有三个分析任务 + # 并发执行所有三个分析任务,传入活跃用户列表 topics, user_titles, golden_quotes, total_token_usage = await self.llm_analyzer.analyze_all_concurrent( - messages, user_analysis, umo=unified_msg_origin + messages, user_analysis, umo=unified_msg_origin, top_users=top_users ) else: # 如果只启用部分分析,则按需执行 @@ -63,7 +68,10 @@ class MessageAnalyzer: total_token_usage.total_tokens += topic_tokens.total_tokens if user_title_enabled: - user_titles, title_tokens = await self.llm_analyzer.analyze_user_titles(messages, user_analysis, umo=unified_msg_origin) + # 传入活跃用户列表 + user_titles, title_tokens = await self.llm_analyzer.analyze_user_titles( + messages, user_analysis, umo=unified_msg_origin, top_users=top_users + ) total_token_usage.prompt_tokens += title_tokens.prompt_tokens total_token_usage.completion_tokens += title_tokens.completion_tokens total_token_usage.total_tokens += title_tokens.total_tokens @@ -86,6 +94,5 @@ class MessageAnalyzer: } except Exception as e: - from astrbot.api import logger logger.error(f"消息分析失败: {e}") - return None \ No newline at end of file + return None