diff --git a/README.md b/README.md index 0642f57..8e5f1de 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) @@ -144,6 +144,9 @@ _✨ 一个基于AstrBot的智能群聊分析插件,能够生成精美的群 ### v1.8.0 - 修复表情统计情况 +### v1.9.0 +- 生成 24h 时间段活跃情况分析 + ## 许可证 MIT License diff --git a/main.py b/main.py index 1c6452b..493f749 100644 --- a/main.py +++ b/main.py @@ -216,6 +216,8 @@ class QQGroupDailyAnalysis(Star): logger.error(f"群分析失败: {e}", exc_info=True) yield event.plain_result(f"❌ 分析失败: {str(e)}。请检查网络连接和LLM配置,或联系管理员") + + @filter.command("设置格式") @filter.permission_type(PermissionType.ADMIN) async def set_output_format(self, event: AiocqhttpMessageEvent, format_type: str = ""): @@ -366,7 +368,7 @@ class QQGroupDailyAnalysis(Star): • 最大查询轮数: {max_rounds} 💡 可用命令: enable, disable, status, reload, test -💡 支持的输出格式: image, text, pdf +💡 支持的输出格式: image, text, pdf (图片和PDF包含活跃度可视化) 💡 其他命令: /设置格式, /安装PDF""") diff --git a/metadata.yaml b/metadata.yaml index f0d50db..1f66982 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -13,6 +13,6 @@ help: | # 插件的帮助信息 命令: /群分析 [天数] - 分析群聊活动 /分析设置 [操作] - 管理设置(enable/disable/status/test) -version: v1.8.0 # 插件版本号。格式:v1.1.1 或者 v1.1 +version: v1.9.0 # 插件版本号。格式:v1.1.1 或者 v1.1 author: SXP-Simon # 作者 repo: https://github.com/SXP-Simon/astrbot-qq-group-daily-analysis # 插件的仓库地址 diff --git a/src/core/message_handler.py b/src/core/message_handler.py index 75ee04e..0d51d82 100644 --- a/src/core/message_handler.py +++ b/src/core/message_handler.py @@ -8,7 +8,8 @@ from datetime import datetime, timedelta from typing import List, Dict, Optional from collections import defaultdict from astrbot.api import logger -from ...src.models.data_models import GroupStatistics, TokenUsage, EmojiStatistics +from ...src.models.data_models import GroupStatistics, TokenUsage, EmojiStatistics, ActivityVisualization +from ...src.visualization.activity_charts import ActivityVisualizer class MessageHandler: @@ -16,6 +17,7 @@ class MessageHandler: def __init__(self, config_manager): self.config_manager = config_manager + self.activity_visualizer = ActivityVisualizer() self.bot_qq_id = None async def set_bot_qq_id(self, bot_instance): @@ -189,6 +191,9 @@ class MessageHandler: most_active_hour = max(hour_counts.items(), key=lambda x: x[1])[0] if hour_counts else 0 most_active_period = f"{most_active_hour:02d}:00-{(most_active_hour+1)%24:02d}:00" + # 生成活跃度可视化数据 + activity_visualization = self.activity_visualizer.generate_activity_visualization(messages) + return GroupStatistics( message_count=len(messages), total_characters=total_chars, @@ -197,5 +202,6 @@ class MessageHandler: golden_quotes=[], emoji_count=emoji_statistics.total_emoji_count, # 保持向后兼容 emoji_statistics=emoji_statistics, + activity_visualization=activity_visualization, token_usage=TokenUsage() ) \ No newline at end of file diff --git a/src/models/data_models.py b/src/models/data_models.py index 9f00cc3..c6210c3 100644 --- a/src/models/data_models.py +++ b/src/models/data_models.py @@ -57,6 +57,16 @@ class EmojiStatistics: return self.face_count + self.mface_count + self.bface_count + self.sface_count + self.other_emoji_count +@dataclass +class ActivityVisualization: + """活跃度可视化数据结构""" + hourly_activity: dict = field(default_factory=dict) # {hour: count} + daily_activity: dict = field(default_factory=dict) # {date: count} + user_activity_ranking: list = field(default_factory=list) # 用户活跃度排行 + peak_hours: list = field(default_factory=list) # 高峰时段 + activity_heatmap_data: dict = field(default_factory=dict) # 热力图数据 + + @dataclass class GroupStatistics: """群聊统计数据结构""" @@ -67,4 +77,5 @@ class GroupStatistics: golden_quotes: List[GoldenQuote] emoji_count: int # 保持向后兼容 emoji_statistics: EmojiStatistics = field(default_factory=EmojiStatistics) + activity_visualization: ActivityVisualization = field(default_factory=ActivityVisualization) token_usage: TokenUsage = field(default_factory=TokenUsage) \ No newline at end of file diff --git a/src/reports/generators.py b/src/reports/generators.py index 86fd3b4..240bba6 100644 --- a/src/reports/generators.py +++ b/src/reports/generators.py @@ -10,6 +10,7 @@ from typing import Dict, Optional from pathlib import Path from astrbot.api import logger from .templates import HTMLTemplates +from ..visualization.activity_charts import ActivityVisualizer class ReportGenerator: @@ -17,6 +18,7 @@ class ReportGenerator: def __init__(self, config_manager): self.config_manager = config_manager + self.activity_visualizer = ActivityVisualizer() async def generate_image_report(self, analysis_result: Dict, group_id: str, html_render_func) -> Optional[str]: """生成图片格式的分析报告""" @@ -32,6 +34,8 @@ class ReportGenerator: logger.error(f"生成图片报告失败: {e}") return None + + async def generate_pdf_report(self, analysis_result: Dict, group_id: str) -> Optional[str]: """生成PDF格式的分析报告""" try: @@ -113,6 +117,7 @@ class ReportGenerator: stats = analysis_result["statistics"] topics = analysis_result["topics"] user_titles = analysis_result["user_titles"] + activity_viz = stats.activity_visualization # 构建话题HTML topics_html = "" @@ -166,6 +171,11 @@ class ReportGenerator: """ + # 生成活跃度可视化HTML + hourly_chart_html = self.activity_visualizer.generate_hourly_chart_html( + activity_viz.hourly_activity + ) + # 返回扁平化的渲染数据 return { "current_date": datetime.now().strftime('%Y年%m月%d日'), @@ -178,11 +188,15 @@ class ReportGenerator: "topics_html": topics_html, "titles_html": titles_html, "quotes_html": quotes_html, + "hourly_chart_html": hourly_chart_html, "total_tokens": stats.token_usage.total_tokens, "prompt_tokens": stats.token_usage.prompt_tokens, "completion_tokens": stats.token_usage.completion_tokens } + + + def _render_html_template(self, template: str, data: Dict, use_jinja_style: bool = False) -> str: """HTML模板渲染,支持两种占位符格式 diff --git a/src/reports/templates.py b/src/reports/templates.py index 4077f46..1266f10 100644 --- a/src/reports/templates.py +++ b/src/reports/templates.py @@ -66,6 +66,103 @@ class HTMLTemplates: .quote-author { font-size: 0.9em; color: #4299e1; font-weight: 600; margin-bottom: 8px; text-align: right; } .quote-reason { font-size: 0.8em; color: #666666; font-style: normal; background: rgba(66, 153, 225, 0.1); padding: 8px 12px; border-radius: 12px; border-left: 3px solid #4299e1; } .footer { background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%); color: #ffffff; text-align: center; padding: 32px; font-size: 0.8em; font-weight: 300; letter-spacing: 0.5px; opacity: 0.9; } + + /* 活跃度可视化样式 - 重新设计 */ + .activity-section { + background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); + padding: 40px; + border-radius: 20px; + margin: 40px 0; + border: 1px solid #e2e8f0; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); + } + .activity-chart-container { + background: #ffffff; + padding: 32px; + border-radius: 16px; + border: 1px solid #e2e8f0; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); + } + .chart-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 32px; + padding-bottom: 16px; + border-bottom: 2px solid #f0f2f5; + } + .chart-title { + font-size: 1.4em; + font-weight: 600; + color: #2d3748; + display: flex; + align-items: center; + gap: 12px; + } + .chart-subtitle { + color: #7f8c8d; + font-size: 0.9em; + font-weight: 400; + } + .hour-bar-container { + display: flex; + align-items: center; + margin: 10px 0; + height: 20px; + transition: all 0.2s ease; + } + .hour-bar-container:hover { + background: #f8f9fa; + border-radius: 8px; + padding: 0 8px; + } + .hour-label { + width: 55px; + text-align: left; + color: #4a5568; + font-size: 13px; + font-weight: 500; + flex-shrink: 0; + } + .bar-wrapper { + display: flex; + align-items: center; + flex-grow: 1; + gap: 12px; + min-width: 0; + } + .bar { + height: 10px; + background: linear-gradient(90deg, #4299e1 0%, #667eea 100%); + border-radius: 6px; + transition: all 0.3s ease-out; + display: flex; + justify-content: flex-end; + align-items: center; + overflow: hidden; + box-shadow: 0 2px 8px rgba(66, 153, 225, 0.2); + } + .bar:hover { + transform: scaleY(1.2); + box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3); + } + .hourly-value-outside { + color: #4a5568; + font-size: 12px; + font-weight: 600; + flex-shrink: 0; + min-width: 30px; + text-align: right; + } + .hourly-value-inside { + color: white; + font-size: 11px; + padding: 0 8px; + font-weight: 600; + white-space: nowrap; + text-shadow: 0 1px 2px rgba(0,0,0,0.2); + } + @media (min-width: 1400px) { .container { max-width: 1400px; } .topics-grid { grid-template-columns: repeat(3, 1fr); } .users-grid { grid-template-columns: repeat(3, 1fr); } } @media (max-width: 768px) { body { padding: 10px; } .container { margin: 0; max-width: 100%; } .header { padding: 24px 20px; } .header h1 { font-size: 1.8em; } .content { padding: 20px; } .topics-grid { grid-template-columns: 1fr; } .users-grid { grid-template-columns: 1fr; } .stats-grid { grid-template-columns: 1fr 1fr; gap: 12px; } .stat-card { padding: 20px 16px; } .topic-item { padding: 20px; } .user-title { flex-direction: column; align-items: flex-start; gap: 12px; padding: 16px; min-height: auto; } .user-info { width: 100%; } .user-reason { text-align: left; max-width: none; margin-left: 0; margin-top: 8px; } } @@ -90,6 +187,16 @@ class HTMLTemplates: