diff --git a/README.md b/README.md index 0962575..1074e51 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # QQ群日常分析插件 -[](https://github.com/SXP-Simon/astrbot-qq-group-daily-analysis) +[](https://github.com/SXP-Simon/astrbot-qq-group-daily-analysis) [](https://github.com/AstrBotDevs/AstrBot) [](LICENSE) diff --git a/_conf_schema.json b/_conf_schema.json index e1f21ec..b257ce5 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -69,6 +69,12 @@ "default": 8, "hint": "分析报告中显示的最大用户称号数量" }, + "max_golden_quotes": { + "type": "int", + "description": "最大金句数量", + "default": 5, + "hint": "分析报告中显示的最大金句数量" + }, "max_query_rounds": { "type": "int", "description": "最大消息查询轮数", diff --git a/main.py b/main.py index 2a8225f..4459c01 100644 --- a/main.py +++ b/main.py @@ -13,7 +13,7 @@ import os from datetime import datetime, timedelta from typing import List, Dict, Optional from pathlib import Path -from dataclasses import dataclass +from dataclasses import dataclass, field from collections import defaultdict from astrbot.api.event import filter @@ -63,13 +63,20 @@ class UserTitle: reason: str -@dataclass +@dataclass class GoldenQuote: """群聊金句数据结构""" content: str sender: str reason: str +@dataclass +class TokenUsage: + """Token使用统计""" + prompt_tokens: int = 0 + completion_tokens: int = 0 + total_tokens: int = 0 + @dataclass class GroupStatistics: """群聊统计数据结构""" @@ -79,13 +86,14 @@ class GroupStatistics: most_active_period: str golden_quotes: List[GoldenQuote] emoji_count: int + token_usage: TokenUsage = field(default_factory=TokenUsage) @register( "astrbot_qq_group_daily_analysis", "SXP-Simon", "QQ群日常分析插件 - 生成精美的群聊日常分析报告", - "1.2.0", + "1.3.0", "https://github.com/SXP-Simon/astrbot-qq-group-daily-analysis" ) class QQGroupDailyAnalysis(Star): @@ -106,6 +114,7 @@ class QQGroupDailyAnalysis(Star): self.user_title_analysis_enabled = config.get("user_title_analysis_enabled", True) self.max_topics = config.get("max_topics", 5) self.max_user_titles = config.get("max_user_titles", 8) + self.max_golden_quotes = config.get("max_golden_quotes", 5) self.max_query_rounds = config.get("max_query_rounds", 35) # PDF 相关配置 @@ -637,23 +646,36 @@ class QQGroupDailyAnalysis(Star): """分析消息内容""" # 基础统计 stats = self._calculate_statistics(messages) - + # 用户活跃度分析 user_analysis = self._analyze_users(messages) - + # 话题分析(根据配置决定是否启用) topics = [] + topics_token_usage = TokenUsage() if self.topic_analysis_enabled: - topics = await self._analyze_topics(messages) + topics, topics_token_usage = await self._analyze_topics(messages) # 用户称号分析(根据配置决定是否启用) user_titles = [] + titles_token_usage = TokenUsage() if self.user_title_analysis_enabled: - user_titles = await self._analyze_user_titles(messages, user_analysis) - + user_titles, titles_token_usage = await self._analyze_user_titles(messages, user_analysis) + # 群聊金句分析 - golden_quotes = await self._analyze_golden_quotes(messages) + golden_quotes, quotes_token_usage = await self._analyze_golden_quotes(messages) stats.golden_quotes = golden_quotes + + # 汇总token使用情况 + stats.token_usage.prompt_tokens = (topics_token_usage.prompt_tokens + + titles_token_usage.prompt_tokens + + quotes_token_usage.prompt_tokens) + stats.token_usage.completion_tokens = (topics_token_usage.completion_tokens + + titles_token_usage.completion_tokens + + quotes_token_usage.completion_tokens) + stats.token_usage.total_tokens = (topics_token_usage.total_tokens + + titles_token_usage.total_tokens + + quotes_token_usage.total_tokens) return { "group_id": group_id, @@ -698,7 +720,8 @@ class QQGroupDailyAnalysis(Star): participant_count=len(participants), most_active_period=most_active_period, golden_quotes=[], # 将在后续LLM分析中填充 - emoji_count=emoji_count + emoji_count=emoji_count, + token_usage=TokenUsage() # 初始化为空,将在LLM分析后填充 ) def _analyze_users(self, messages: List[Dict]) -> Dict[str, Dict]: @@ -773,7 +796,7 @@ class QQGroupDailyAnalysis(Star): logger.error(f"获取用户头像失败 {user_id}: {e}") return None - async def _analyze_topics(self, messages: List[Dict]) -> List[SummaryTopic]: + async def _analyze_topics(self, messages: List[Dict]) -> tuple[List[SummaryTopic], TokenUsage]: """使用LLM分析话题""" try: # 提取文本消息 @@ -794,7 +817,7 @@ class QQGroupDailyAnalysis(Star): }) if not text_messages: - return [] + return [], TokenUsage() # # 限制消息数量以避免token过多 # if len(text_messages) > 100: @@ -867,14 +890,22 @@ class QQGroupDailyAnalysis(Star): provider = self.context.get_using_provider() if not provider: logger.warning("未配置LLM提供商,跳过话题分析") - return [] + return [], TokenUsage() response = await provider.text_chat( prompt=prompt, - max_tokens=6000, # 增加token限制以避免响应被截断 + max_tokens=10000, # 增加token限制以避免响应被截断 temperature=0.6 ) + # 提取token使用统计 + token_usage = TokenUsage() + if response.raw_completion and hasattr(response.raw_completion, 'usage'): + usage = response.raw_completion.usage + token_usage.prompt_tokens = usage.prompt_tokens + token_usage.completion_tokens = usage.completion_tokens + token_usage.total_tokens = usage.total_tokens + # 解析响应 if hasattr(response, 'completion_text'): result_text = response.completion_text @@ -938,9 +969,9 @@ class QQGroupDailyAnalysis(Star): logger.debug(f"修复后的JSON: {json_text[:300]}...") topics_data = json.loads(json_text) - topics = [SummaryTopic(**topic) for topic in topics_data[:5]] + topics = [SummaryTopic(**topic) for topic in topics_data[:self.max_topics]] logger.info(f"话题分析成功,解析到 {len(topics)} 个话题") - return topics + return topics, token_usage else: logger.warning(f"话题分析响应中未找到JSON格式,响应内容: {result_text[:200]}...") except json.JSONDecodeError as e: @@ -963,7 +994,7 @@ class QQGroupDailyAnalysis(Star): topic_pattern = r'"topic":\s*"([^"]+)"[^}]*"contributors":\s*\[([^\]]+)\][^}]*"detail":\s*"([^"]*(?:\\.[^"]*)*)"' matches = re.findall(topic_pattern, result_text, re.DOTALL) - for match in matches[:5]: # 最多5个话题 + for match in matches[:self.max_topics]: topic_name = match[0].strip() contributors_str = match[1].strip() detail = match[2].strip() @@ -987,7 +1018,7 @@ class QQGroupDailyAnalysis(Star): if topics: logger.info(f"正则表达式提取成功,获得 {len(topics)} 个话题") - return topics + return topics, token_usage else: # 最后的降级方案 logger.info("正则表达式提取失败,使用默认话题...") @@ -1008,13 +1039,13 @@ class QQGroupDailyAnalysis(Star): except Exception as e: logger.error(f"话题分析处理失败: {e}") - return [] + return [], token_usage except Exception as e: logger.error(f"话题分析失败: {e}") - return [] + return [], TokenUsage() - async def _analyze_user_titles(self, messages: List[Dict], user_analysis: Dict) -> List[UserTitle]: + async def _analyze_user_titles(self, messages: List[Dict], user_analysis: Dict) -> tuple[List[UserTitle], TokenUsage]: """使用LLM分析用户称号""" try: # 准备用户数据 @@ -1039,7 +1070,7 @@ class QQGroupDailyAnalysis(Star): }) if not user_summaries: - return [] + return [], TokenUsage() # 按消息数量排序,取前N名(根据配置) user_summaries.sort(key=lambda x: x["message_count"], reverse=True) @@ -1087,7 +1118,7 @@ class QQGroupDailyAnalysis(Star): provider = self.context.get_using_provider() if not provider: logger.warning("未配置LLM提供商,跳过用户称号分析") - return [] + return [], TokenUsage() response = await provider.text_chat( prompt=prompt, @@ -1095,6 +1126,14 @@ class QQGroupDailyAnalysis(Star): temperature=0.5 ) + # 提取token使用统计 + token_usage = TokenUsage() + if response.raw_completion and hasattr(response.raw_completion, 'usage'): + usage = response.raw_completion.usage + token_usage.prompt_tokens = usage.prompt_tokens + token_usage.completion_tokens = usage.completion_tokens + token_usage.total_tokens = usage.total_tokens + # 解析响应 if hasattr(response, 'completion_text'): result_text = response.completion_text @@ -1107,17 +1146,17 @@ class QQGroupDailyAnalysis(Star): json_match = re.search(r'\[.*\]', result_text, re.DOTALL) if json_match: titles_data = json.loads(json_match.group()) - return [UserTitle(**title) for title in titles_data] + return [UserTitle(**title) for title in titles_data], token_usage except: pass - return [] + return [], token_usage except Exception as e: logger.error(f"用户称号分析失败: {e}") - return [] + return [], TokenUsage() - async def _analyze_golden_quotes(self, messages: List[Dict]) -> List[GoldenQuote]: + async def _analyze_golden_quotes(self, messages: List[Dict]) -> tuple[List[GoldenQuote], TokenUsage]: """使用LLM分析群聊金句""" try: # 提取有趣的文本消息 @@ -1139,7 +1178,7 @@ class QQGroupDailyAnalysis(Star): }) if not interesting_messages: - return [] + return [], TokenUsage() # # 限制消息数量以避免token过多 # if len(interesting_messages) > 50: @@ -1154,10 +1193,10 @@ class QQGroupDailyAnalysis(Star): ]) # 计算金句数量,默认5句,但可以根据配置调整 - max_quotes = min(8, max(3, self.max_topics)) # 根据话题数量调整金句数量,3-8句之间 + self.max_golden_quotes prompt = f""" -请从以下群聊记录中挑选出{max_quotes}句最具冲击力、最令人惊叹的"金句"。这些金句需满足: +请从以下群聊记录中挑选出{self.max_golden_quotes}句最具冲击力、最令人惊叹的"金句"。这些金句需满足: - 核心标准:**逆天的神人发言**,即具备颠覆常识的脑洞、逻辑跳脱的表达或强烈反差感的原创内容 - 典型特征:包含某些争议话题元素、夸张类比、反常规结论、一本正经的"胡说八道"或突破语境的清奇思路,并且具备一定的冲击力,让人印象深刻。 @@ -1187,7 +1226,7 @@ class QQGroupDailyAnalysis(Star): provider = self.context.get_using_provider() if not provider: logger.warning("未配置LLM提供商,跳过金句分析") - return [] + return [], TokenUsage() response = await provider.text_chat( prompt=prompt, @@ -1195,6 +1234,14 @@ class QQGroupDailyAnalysis(Star): temperature=0.7 ) + # 提取token使用统计 + token_usage = TokenUsage() + if response.raw_completion and hasattr(response.raw_completion, 'usage'): + usage = response.raw_completion.usage + token_usage.prompt_tokens = usage.prompt_tokens + token_usage.completion_tokens = usage.completion_tokens + token_usage.total_tokens = usage.total_tokens + # 解析响应 if hasattr(response, 'completion_text'): result_text = response.completion_text @@ -1207,15 +1254,15 @@ class QQGroupDailyAnalysis(Star): json_match = re.search(r'\[.*\]', result_text, re.DOTALL) if json_match: quotes_data = json.loads(json_match.group()) - return [GoldenQuote(**quote) for quote in quotes_data[:5]] + return [GoldenQuote(**quote) for quote in quotes_data[:self.max_golden_quotes]], token_usage except: pass - return [] + return [], token_usage except Exception as e: logger.error(f"金句分析失败: {e}") - return [] + return [], TokenUsage() async def _html_to_pdf(self, html_content: str, output_path: str) -> bool: """将 HTML 内容转换为 PDF 文件""" @@ -1389,7 +1436,7 @@ class QQGroupDailyAnalysis(Star): # 构建金句HTML quotes_html = "" - for quote in stats.golden_quotes[:5]: + for quote in stats.golden_quotes[:self.max_golden_quotes]: quotes_html += f"""