Files
astrbot_plugin_qq_group_dai…/main.py
T
2025-11-21 21:09:05 +08:00

441 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
QQ群日常分析插件
基于群聊记录生成精美的日常分析报告,包含话题总结、用户画像、统计数据等
重构版本 - 使用模块化架构
"""
import asyncio
from astrbot.api.event import filter
from astrbot.api.star import Context, Star
from astrbot.api import logger, AstrBotConfig
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import (
AiocqhttpMessageEvent,
)
from astrbot.core.message.components import File
from astrbot.core.star.filter.permission import PermissionType
# 导入重构后的模块
from .src.core.config import ConfigManager
from .src.core.bot_manager import BotManager
from .src.reports.generators import ReportGenerator
from .src.scheduler.auto_scheduler import AutoScheduler
from .src.utils.pdf_utils import PDFInstaller
from .src.utils.helpers import MessageAnalyzer
# 全局变量
config_manager = None
bot_manager = None
message_analyzer = None
report_generator = None
auto_scheduler = None
class QQGroupDailyAnalysis(Star):
def __init__(self, context: Context, config: AstrBotConfig):
super().__init__(context)
self.config = config
# 初始化模块化组件
global \
config_manager, \
bot_manager, \
message_analyzer, \
report_generator, \
auto_scheduler
config_manager = ConfigManager(config)
bot_manager = BotManager(config_manager)
bot_manager.set_context(context)
message_analyzer = MessageAnalyzer(context, config_manager, bot_manager)
report_generator = ReportGenerator(config_manager)
auto_scheduler = AutoScheduler(
config_manager,
message_analyzer.message_handler,
message_analyzer,
report_generator,
bot_manager,
self.html_render, # 传入html_render函数
)
# 延迟启动自动调度器,给系统时间初始化
if config_manager.get_enable_auto_analysis():
asyncio.create_task(self._delayed_start_scheduler())
logger.info("QQ群日常分析插件已初始化(模块化版本)")
async def _delayed_start_scheduler(self):
"""延迟启动调度器,给系统时间初始化"""
try:
# 等待10秒让系统完全初始化
await asyncio.sleep(30)
# 初始化所有bot实例
discovered = await bot_manager.initialize_from_config()
if discovered:
platform_count = len(discovered)
logger.info(f"Bot管理器初始化成功,发现 {platform_count} 个适配器")
for platform_id, bot_instance in discovered.items():
logger.info(
f" - 平台 {platform_id}: {type(bot_instance).__name__}"
)
# 启动调度器
await auto_scheduler.start_scheduler()
else:
logger.warning("Bot管理器初始化失败,未发现任何适配器")
status = bot_manager.get_status_info()
logger.info(f"Bot管理器状态: {status}")
except Exception as e:
logger.debug(f"延迟启动调度器失败,可能由于短时间内多次更新插件配置: {e}")
async def terminate(self):
"""插件被卸载/停用时调用,清理资源"""
try:
logger.info("开始清理QQ群日常分析插件资源...")
global \
auto_scheduler, \
bot_manager, \
message_analyzer, \
report_generator, \
config_manager
# 停止自动调度器
if auto_scheduler:
logger.info("正在停止自动调度器...")
await auto_scheduler.stop_scheduler()
logger.info("自动调度器已停止")
# 清理bot管理器资源
# if bot_manager:
# logger.info("正在清理bot管理器资源...")
# # 如果有其他需要清理的资源,可以在这里添加
# # 清理消息分析器资源
# if message_analyzer:
# logger.info("正在清理消息分析器资源...")
# # 如果有其他需要清理的资源,可以在这里添加
# # 清理报告生成器资源
# if report_generator:
# logger.info("正在清理报告生成器资源...")
# # 如果有其他需要清理的资源,可以在这里添加
# 重置全局变量
auto_scheduler = None
bot_manager = None
message_analyzer = None
report_generator = None
config_manager = None
logger.info("QQ群日常分析插件资源清理完成")
except Exception as e:
logger.error(f"插件资源清理失败: {e}")
@filter.command("群分析")
@filter.permission_type(PermissionType.ADMIN)
async def analyze_group_daily(
self, event: AiocqhttpMessageEvent, days: int | None = None
):
"""
分析群聊日常活动
用法: /群分析 [天数]
"""
if not isinstance(event, AiocqhttpMessageEvent):
yield event.plain_result("❌ 此功能仅支持QQ群聊")
return
group_id = event.get_group_id()
if not group_id:
yield event.plain_result("❌ 请在群聊中使用此命令")
return
# 更新bot实例(用于手动命令)
bot_manager.update_from_event(event)
# 检查群组权限
enabled_groups = config_manager.get_enabled_groups()
if enabled_groups and group_id not in enabled_groups:
yield event.plain_result("❌ 此群未启用日常分析功能")
return
# 设置分析天数
analysis_days = (
days if days and 1 <= days <= 7 else config_manager.get_analysis_days()
)
yield event.plain_result(f"🔍 开始分析群聊近{analysis_days}天的活动,请稍候...")
# 调试:输出当前配置
logger.info(f"当前输出格式配置: {config_manager.get_output_format()}")
try:
# 获取该群对应的平台ID和bot实例
platform_id = auto_scheduler._get_platform_id_for_group(group_id)
bot_instance = bot_manager.get_bot_instance(platform_id)
if not bot_instance:
yield event.plain_result(
f"❌ 未找到群 {group_id} 对应的bot实例(平台: {platform_id}"
)
return
# 获取群聊消息
messages = await message_analyzer.message_handler.fetch_group_messages(
bot_instance, group_id, analysis_days, platform_id
)
if not messages:
yield event.plain_result(
"❌ 未找到足够的群聊记录,请确保群内有足够的消息历史"
)
return
# 检查消息数量是否足够分析
min_threshold = config_manager.get_min_messages_threshold()
if len(messages) < min_threshold:
yield event.plain_result(
f"❌ 消息数量不足({len(messages)}条),至少需要{min_threshold}条消息才能进行有效分析"
)
return
yield event.plain_result(
f"📊 已获取{len(messages)}条消息,正在进行智能分析..."
)
# 进行分析 - 传递 unified_msg_origin 以获取正确的 LLM 提供商
analysis_result = await message_analyzer.analyze_messages(
messages, group_id, event.unified_msg_origin
)
# 检查分析结果
if not analysis_result or not analysis_result.get("statistics"):
yield event.plain_result("❌ 分析过程中出现错误,请稍后重试")
return
# 生成报告
output_format = config_manager.get_output_format()
if output_format == "image":
image_url = await report_generator.generate_image_report(
analysis_result, group_id, self.html_render
)
if image_url:
yield event.image_result(image_url)
else:
# 如果图片生成失败,回退到文本报告
logger.warning("图片报告生成失败,回退到文本报告")
text_report = report_generator.generate_text_report(analysis_result)
yield event.plain_result(
f"⚠️ 图片报告生成失败,以下是文本版本:\n\n{text_report}"
)
elif output_format == "pdf":
if not config_manager.pyppeteer_available:
yield event.plain_result(
"❌ PDF 功能不可用,请使用 /安装PDF 命令安装 pyppeteer==1.0.2"
)
return
pdf_path = await report_generator.generate_pdf_report(
analysis_result, group_id
)
if pdf_path:
# 发送 PDF 文件
from pathlib import Path
pdf_file = File(name=Path(pdf_path).name, file=pdf_path)
result = event.make_result()
result.chain.append(pdf_file)
yield result
else:
# 如果 PDF 生成失败,提供详细的错误信息和解决方案
# yield event.plain_result("❌ PDF 报告生成失败")
# yield event.plain_result("🔧 可能的解决方案:")
# yield event.plain_result("1. 使用 /安装PDF 命令重新安装依赖")
# yield event.plain_result("2. 检查网络连接是否正常")
# yield event.plain_result("3. 暂时使用图片格式:/设置格式 image")
# 回退到文本报告
logger.warning("PDF 报告生成失败,回退到文本报告")
text_report = report_generator.generate_text_report(analysis_result)
yield event.plain_result(
f"\n📝 以下是文本版本的分析报告:\n\n{text_report}"
)
else:
text_report = report_generator.generate_text_report(analysis_result)
yield event.plain_result(text_report)
except Exception as e:
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 = ""
):
"""
设置分析报告输出格式
用法: /设置格式 [image|text|pdf]
"""
if not isinstance(event, AiocqhttpMessageEvent):
yield event.plain_result("❌ 此功能仅支持QQ群聊")
return
group_id = event.get_group_id()
if not group_id:
yield event.plain_result("❌ 请在群聊中使用此命令")
return
if not format_type:
current_format = config_manager.get_output_format()
pdf_status = (
"✅" if config_manager.pyppeteer_available else "❌ (需安装 pyppeteer)"
)
yield event.plain_result(f"""📊 当前输出格式: {current_format}
可用格式:
• image - 图片格式 (默认)
• text - 文本格式
• pdf - PDF 格式 {pdf_status}
用法: /设置格式 [格式名称]""")
return
format_type = format_type.lower()
if format_type not in ["image", "text", "pdf"]:
yield event.plain_result("❌ 无效的格式类型,支持: image, text, pdf")
return
if format_type == "pdf" and not config_manager.pyppeteer_available:
yield event.plain_result(
"❌ PDF 格式不可用,请使用 /安装PDF 命令安装 pyppeteer==1.0.2"
)
return
config_manager.set_output_format(format_type)
yield event.plain_result(f"✅ 输出格式已设置为: {format_type}")
@filter.command("安装PDF")
@filter.permission_type(PermissionType.ADMIN)
async def install_pdf_deps(self, event: AiocqhttpMessageEvent):
"""
安装 PDF 功能依赖
用法: /安装PDF
"""
if not isinstance(event, AiocqhttpMessageEvent):
yield event.plain_result("❌ 此功能仅支持QQ群聊")
return
yield event.plain_result("🔄 开始安装 PDF 功能依赖,请稍候...")
try:
# 安装 pyppeteer
result = await PDFInstaller.install_pyppeteer(config_manager)
yield event.plain_result(result)
# 提供系统依赖安装指导
system_deps_result = await PDFInstaller.install_system_deps()
yield event.plain_result(system_deps_result)
except Exception as e:
logger.error(f"安装 PDF 依赖失败: {e}", exc_info=True)
yield event.plain_result(f"❌ 安装过程中出现错误: {str(e)}")
@filter.command("分析设置")
@filter.permission_type(PermissionType.ADMIN)
async def analysis_settings(
self, event: AiocqhttpMessageEvent, action: str = "status"
):
"""
管理分析设置
用法: /分析设置 [enable|disable|status|reload|test]
- enable: 启用当前群的分析功能
- disable: 禁用当前群的分析功能
- status: 查看当前状态
- reload: 重新加载配置并重启定时任务
- test: 测试自动分析功能
"""
if not isinstance(event, AiocqhttpMessageEvent):
yield event.plain_result("❌ 此功能仅支持QQ群聊")
return
group_id = event.get_group_id()
if not group_id:
yield event.plain_result("❌ 请在群聊中使用此命令")
return
if action == "enable":
enabled_groups = config_manager.get_enabled_groups()
if group_id not in enabled_groups:
config_manager.add_enabled_group(group_id)
yield event.plain_result("✅ 已为当前群启用日常分析功能")
# 重新启动定时任务
await auto_scheduler.restart_scheduler()
else:
yield event.plain_result("ℹ️ 当前群已启用日常分析功能")
elif action == "disable":
enabled_groups = config_manager.get_enabled_groups()
if group_id in enabled_groups:
config_manager.remove_enabled_group(group_id)
yield event.plain_result("✅ 已为当前群禁用日常分析功能")
# 重新启动定时任务
await auto_scheduler.restart_scheduler()
else:
yield event.plain_result("ℹ️ 当前群未启用日常分析功能")
elif action == "reload":
# 重新启动定时任务
await auto_scheduler.restart_scheduler()
yield event.plain_result("✅ 已重新加载配置并重启定时任务")
elif action == "test":
# 测试自动分析功能
enabled_groups = config_manager.get_enabled_groups()
if group_id not in enabled_groups:
yield event.plain_result("❌ 请先启用当前群的分析功能")
return
yield event.plain_result("🧪 开始测试自动分析功能...")
# 更新bot实例(用于测试)
bot_manager.update_from_event(event)
# 执行自动分析
try:
await auto_scheduler._perform_auto_analysis_for_group(group_id)
yield event.plain_result("✅ 自动分析测试完成,请查看群消息")
except Exception as e:
yield event.plain_result(f"❌ 自动分析测试失败: {str(e)}")
else: # status
enabled_groups = config_manager.get_enabled_groups()
status = "已启用" if group_id in enabled_groups else "未启用"
auto_status = (
"已启用" if config_manager.get_enable_auto_analysis() else "未启用"
)
auto_time = config_manager.get_auto_analysis_time()
pdf_status = PDFInstaller.get_pdf_status(config_manager)
output_format = config_manager.get_output_format()
min_threshold = config_manager.get_min_messages_threshold()
yield event.plain_result(f"""📊 当前群分析功能状态:
• 群分析功能: {status}
• 自动分析: {auto_status} ({auto_time})
• 输出格式: {output_format}
• PDF 功能: {pdf_status}
• 最小消息数: {min_threshold}
💡 可用命令: enable, disable, status, reload, test
💡 支持的输出格式: image, text, pdf (图片和PDF包含活跃度可视化)
💡 其他命令: /设置格式, /安装PDF""")