mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 20:01:04 +00:00
refactor(DDD): 整理架构
This commit is contained in:
@@ -1,11 +1,8 @@
|
||||
# 应用层 - 编排和用例
|
||||
from .analysis_orchestrator import AnalysisOrchestrator
|
||||
from .message_converter import MessageConverter
|
||||
from .reporting_service import ReportingService
|
||||
from .scheduling_service import SchedulingService
|
||||
|
||||
__all__ = [
|
||||
"AnalysisOrchestrator",
|
||||
"MessageConverter",
|
||||
"SchedulingService",
|
||||
"ReportingService",
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
"""
|
||||
分析编排器 - 应用层协调器
|
||||
|
||||
此编排器连接新的 DDD 架构与现有的分析逻辑,提供渐进式迁移路径。
|
||||
|
||||
架构决策:
|
||||
- 编排器使用 PlatformAdapter 获取消息(新的 DDD 方式)
|
||||
- 但将 LLM 分析委托给现有分析器(保留已工作的代码)
|
||||
- MessageConverter 提供双向转换以保持兼容性
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
from ..domain.value_objects.platform_capabilities import PlatformCapabilities
|
||||
from ..domain.value_objects.unified_message import UnifiedMessage
|
||||
from ..infrastructure.platform import PlatformAdapter, PlatformAdapterFactory
|
||||
from ..utils.logger import logger
|
||||
from .message_converter import MessageConverter
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalysisConfig:
|
||||
"""分析操作配置"""
|
||||
|
||||
days: int = 1
|
||||
max_messages: int = 1000
|
||||
min_messages_threshold: int = 10
|
||||
output_format: str = "image"
|
||||
|
||||
|
||||
class AnalysisOrchestrator:
|
||||
"""
|
||||
分析编排器 - 协调分析工作流。
|
||||
|
||||
职责:
|
||||
1. 使用 PlatformAdapter 获取消息(DDD 方式)
|
||||
2. 转换消息以兼容现有分析器
|
||||
3. 协调分析流程
|
||||
4. 提供平台能力检查
|
||||
|
||||
此类作为以下组件之间的桥梁:
|
||||
- 新的 DDD 基础设施(PlatformAdapter, UnifiedMessage)
|
||||
- 现有分析逻辑(MessageHandler, LLMAnalyzer 等)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
adapter: PlatformAdapter,
|
||||
config: AnalysisConfig = None,
|
||||
):
|
||||
"""
|
||||
初始化编排器。
|
||||
|
||||
参数:
|
||||
adapter: 用于消息操作的平台适配器
|
||||
config: 分析配置
|
||||
"""
|
||||
self.adapter = adapter
|
||||
self.config = config or AnalysisConfig()
|
||||
|
||||
@classmethod
|
||||
def create_for_platform(
|
||||
cls,
|
||||
platform_name: str,
|
||||
bot_instance: Any,
|
||||
config: dict = None,
|
||||
analysis_config: AnalysisConfig = None,
|
||||
) -> Optional["AnalysisOrchestrator"]:
|
||||
"""
|
||||
工厂方法 - 为特定平台创建编排器。
|
||||
|
||||
参数:
|
||||
platform_name: 平台名称(如 "aiocqhttp", "telegram")
|
||||
bot_instance: 来自 AstrBot 的 bot 实例
|
||||
config: 平台特定配置
|
||||
analysis_config: 分析配置
|
||||
|
||||
返回:
|
||||
AnalysisOrchestrator 或 None(如果平台不支持)
|
||||
"""
|
||||
adapter = PlatformAdapterFactory.create(platform_name, bot_instance, config)
|
||||
if adapter is None:
|
||||
logger.warning(f"平台 '{platform_name}' 不支持分析功能")
|
||||
return None
|
||||
|
||||
return cls(adapter, analysis_config)
|
||||
|
||||
def get_capabilities(self) -> PlatformCapabilities:
|
||||
"""获取平台能力。"""
|
||||
return self.adapter.get_capabilities()
|
||||
|
||||
def can_analyze(self) -> bool:
|
||||
"""检查平台是否支持分析。"""
|
||||
return self.adapter.get_capabilities().can_analyze()
|
||||
|
||||
def can_send_report(self, format: str = "image") -> bool:
|
||||
"""检查平台是否能发送指定格式的报告。"""
|
||||
return self.adapter.get_capabilities().can_send_report(format)
|
||||
|
||||
async def fetch_messages(
|
||||
self,
|
||||
group_id: str,
|
||||
days: int = None,
|
||||
max_count: int = None,
|
||||
) -> list[UnifiedMessage]:
|
||||
"""
|
||||
使用平台适配器获取消息。
|
||||
|
||||
参数:
|
||||
group_id: 要获取消息的群组 ID
|
||||
days: 天数(默认使用配置值)
|
||||
max_count: 最大消息数量(默认使用配置值)
|
||||
|
||||
返回:
|
||||
UnifiedMessage 列表
|
||||
"""
|
||||
days = days or self.config.days
|
||||
max_count = max_count or self.config.max_messages
|
||||
|
||||
# 应用平台能力限制
|
||||
caps = self.adapter.get_capabilities()
|
||||
effective_days = caps.get_effective_days(days)
|
||||
effective_count = caps.get_effective_count(max_count)
|
||||
|
||||
if effective_days < days:
|
||||
logger.info(f"平台限制:请求 {days} 天,实际使用 {effective_days} 天")
|
||||
|
||||
return await self.adapter.fetch_messages(
|
||||
group_id=group_id,
|
||||
days=effective_days,
|
||||
max_count=effective_count,
|
||||
)
|
||||
|
||||
async def fetch_messages_as_raw(
|
||||
self,
|
||||
group_id: str,
|
||||
days: int = None,
|
||||
max_count: int = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
获取消息并转换为原始字典格式。
|
||||
|
||||
此方法提供与现有分析器的向后兼容性,
|
||||
这些分析器期望原始字典格式的消息。
|
||||
|
||||
参数:
|
||||
group_id: 要获取消息的群组 ID
|
||||
days: 天数
|
||||
max_count: 最大消息数量
|
||||
|
||||
返回:
|
||||
原始消息字典列表(通用格式,由适配器决定具体格式)
|
||||
"""
|
||||
# unified_messages = await self.fetch_messages(group_id, days, max_count)
|
||||
#
|
||||
# # 如果适配器实现了 convert_to_raw_format,则使用它
|
||||
# if hasattr(self.adapter, "convert_to_raw_format"):
|
||||
# return self.adapter.convert_to_raw_format(unified_messages)
|
||||
#
|
||||
# # 默认回退逻辑:手动转换
|
||||
# # 这可能不完美,但能保证基本的向后兼容性
|
||||
# return [
|
||||
# {
|
||||
# "message_id": msg.message_id,
|
||||
# "group_id": msg.group_id,
|
||||
# "sender": {
|
||||
# "user_id": msg.sender_id,
|
||||
# "nickname": msg.sender_name,
|
||||
# "card": msg.sender_card
|
||||
# },
|
||||
# "time": msg.timestamp,
|
||||
# "message": msg.text_content, # 简化处理
|
||||
# "raw_message": msg.text_content
|
||||
# }
|
||||
# for msg in unified_messages
|
||||
# ]
|
||||
|
||||
# 暂时直接使用适配器获取 raw 格式,如果适配器支持
|
||||
# 这是为了确保现有逻辑完全兼容,因为 convert_to_raw_format 可能有损
|
||||
# 但我们希望尽可能使用新的 fetch_messages
|
||||
|
||||
unified_messages = await self.fetch_messages(group_id, days, max_count)
|
||||
return self.adapter.convert_to_raw_format(unified_messages)
|
||||
|
||||
async def get_group_info(self, group_id: str):
|
||||
"""获取群组信息。"""
|
||||
return await self.adapter.get_group_info(group_id)
|
||||
|
||||
async def get_member_avatars(
|
||||
self,
|
||||
user_ids: list[str],
|
||||
size: int = 100,
|
||||
) -> dict[str, str | None]:
|
||||
"""
|
||||
批量获取用户头像 URL。
|
||||
|
||||
参数:
|
||||
user_ids: 用户 ID 列表
|
||||
size: 头像尺寸
|
||||
|
||||
返回:
|
||||
用户 ID 到头像 URL 的映射字典(URL 可能为 None)
|
||||
"""
|
||||
return await self.adapter.batch_get_avatar_urls(user_ids, size)
|
||||
|
||||
async def send_text(self, group_id: str, text: str) -> bool:
|
||||
"""发送文本消息到群组。"""
|
||||
return await self.adapter.send_text(group_id, text)
|
||||
|
||||
async def send_image(
|
||||
self,
|
||||
group_id: str,
|
||||
image_path: str,
|
||||
caption: str = "",
|
||||
) -> bool:
|
||||
"""发送图片到群组。"""
|
||||
return await self.adapter.send_image(group_id, image_path, caption)
|
||||
|
||||
async def send_file(
|
||||
self,
|
||||
group_id: str,
|
||||
file_path: str,
|
||||
filename: str = None,
|
||||
) -> bool:
|
||||
"""发送文件到群组。"""
|
||||
return await self.adapter.send_file(group_id, file_path, filename)
|
||||
|
||||
def validate_message_count(self, messages: list[UnifiedMessage]) -> bool:
|
||||
"""
|
||||
检查消息数量是否达到最小阈值。
|
||||
|
||||
参数:
|
||||
messages: 消息列表
|
||||
|
||||
返回:
|
||||
如果数量足够返回 True
|
||||
"""
|
||||
return len(messages) >= self.config.min_messages_threshold
|
||||
|
||||
def get_analysis_text(self, messages: list[UnifiedMessage]) -> str:
|
||||
"""
|
||||
将消息转换为 LLM 分析文本格式。
|
||||
|
||||
参数:
|
||||
messages: UnifiedMessage 列表
|
||||
|
||||
返回:
|
||||
格式化的 LLM 分析文本
|
||||
"""
|
||||
return MessageConverter.unified_to_analysis_text(messages)
|
||||
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
分析应用服务 - 应用层
|
||||
实现“每日群聊分析并生成报告”的核心用例。
|
||||
负责协调领域服务、基础设施适配器及持久化层。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from ...utils.logger import logger
|
||||
from ..domain.models.data_models import TokenUsage
|
||||
from ..domain.repositories.analysis_repository import IAnalysisProvider
|
||||
from ..domain.repositories.report_repository import IReportGenerator
|
||||
from ..domain.services.analysis_domain_service import AnalysisDomainService
|
||||
from ..domain.services.statistics_service import StatisticsService
|
||||
|
||||
|
||||
class AnalysisApplicationService:
|
||||
"""分析应用服务 - 协调业务流程"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_manager: Any,
|
||||
bot_manager: Any,
|
||||
history_manager: Any,
|
||||
report_generator: IReportGenerator,
|
||||
llm_analyzer: IAnalysisProvider,
|
||||
statistics_service: StatisticsService,
|
||||
analysis_domain_service: AnalysisDomainService,
|
||||
):
|
||||
self.config_manager = config_manager
|
||||
self.bot_manager = bot_manager
|
||||
self.history_manager = history_manager
|
||||
self.report_generator = report_generator
|
||||
self.llm_analyzer = llm_analyzer
|
||||
self.statistics_service = statistics_service
|
||||
self.analysis_domain_service = analysis_domain_service
|
||||
|
||||
async def execute_daily_analysis(
|
||||
self, group_id: str, platform_id: str | None = None, manual: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
执行每日分析用例。
|
||||
|
||||
流程:
|
||||
1. 获取适配器
|
||||
2. 拉取消息 (Infrastructure)
|
||||
3. 基础统计 (Domain Service)
|
||||
4. 用户分析 (Domain Service)
|
||||
5. LLM 语义分析 (Infrastructure/Analysis Bridge)
|
||||
6. 生成报告 (Visualization/Infrastructure)
|
||||
7. 持久化摘要 (Persistence)
|
||||
8. 返回结果
|
||||
"""
|
||||
logger.info(f"开始执行分析用例: 群 {group_id}, 平台 {platform_id or '默认'}")
|
||||
|
||||
# 1. 获取适配器
|
||||
adapter = self.bot_manager.get_adapter(platform_id)
|
||||
if not adapter:
|
||||
raise ValueError(f"未找到平台 {platform_id} 的适配器")
|
||||
|
||||
# 2. 拉取消息
|
||||
days = self.config_manager.get_analysis_days()
|
||||
max_count = self.config_manager.get_max_messages()
|
||||
|
||||
unified_messages = await adapter.fetch_messages(
|
||||
group_id=group_id, days=days, max_count=max_count
|
||||
)
|
||||
|
||||
if not unified_messages:
|
||||
logger.warning(f"群 {group_id} 在最近 {days} 天内无消息或无法获取")
|
||||
return {"success": False, "reason": "no_messages"}
|
||||
|
||||
# 检查最小消息阈值
|
||||
if (
|
||||
len(unified_messages) < self.config_manager.get_min_messages_threshold()
|
||||
and not manual
|
||||
):
|
||||
logger.info(
|
||||
f"群 {group_id} 消息数 ({len(unified_messages)}) 未达到自动分析阈值"
|
||||
)
|
||||
return {"success": False, "reason": "below_threshold"}
|
||||
|
||||
# 3. 基础统计 (Domain Service)
|
||||
statistics = await asyncio.to_thread(
|
||||
self.statistics_service.calculate_group_statistics, unified_messages
|
||||
)
|
||||
|
||||
# 4. 用户分析 (Domain Service)
|
||||
bot_self_ids = self.config_manager.get_bot_self_ids()
|
||||
user_activity = await asyncio.to_thread(
|
||||
self.analysis_domain_service.analyze_user_activity,
|
||||
unified_messages,
|
||||
bot_self_ids,
|
||||
)
|
||||
|
||||
max_user_titles = self.config_manager.get_max_user_titles()
|
||||
top_users = self.analysis_domain_service.get_top_users(
|
||||
user_activity, limit=max_user_titles
|
||||
)
|
||||
|
||||
# 5. LLM 语义分析 (为了保持兼容,目前直接传 UnifiedMessage,后续如需传 raw dict 再加转换)
|
||||
# LLMAnalyzer 内部可能已经处理了转换(见之前代码)
|
||||
topic_enabled = self.config_manager.get_topic_analysis_enabled()
|
||||
user_title_enabled = self.config_manager.get_user_title_analysis_enabled()
|
||||
golden_quote_enabled = self.config_manager.get_golden_quote_analysis_enabled()
|
||||
|
||||
topics = []
|
||||
user_titles = []
|
||||
golden_quotes = []
|
||||
total_token_usage = TokenUsage()
|
||||
|
||||
# Note: LLMAnalyzer 目前可能只接收 legacy 格式或特定的 UnifiedMessage 适配
|
||||
# 暂时转换回 legacy 格式以确保稳定性,直到 LLMAnalyzer 被重构
|
||||
legacy_messages = self.statistics_service._convert_to_legacy_dict(
|
||||
unified_messages
|
||||
)
|
||||
|
||||
unified_msg_origin = (
|
||||
f"{platform_id}:GroupMessage:{group_id}" if platform_id else group_id
|
||||
)
|
||||
|
||||
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(
|
||||
legacy_messages,
|
||||
user_activity,
|
||||
umo=unified_msg_origin,
|
||||
top_users=top_users,
|
||||
)
|
||||
else:
|
||||
# 按需串行执行 (略,实际实现可补全或合并)
|
||||
pass
|
||||
|
||||
# 回填结果
|
||||
statistics.golden_quotes = golden_quotes
|
||||
statistics.token_usage = total_token_usage
|
||||
|
||||
analysis_result = {
|
||||
"statistics": statistics,
|
||||
"topics": topics,
|
||||
"user_titles": user_titles,
|
||||
"user_analysis": user_activity,
|
||||
}
|
||||
|
||||
# 6. 持久化摘要 (Persistence)
|
||||
await self.history_manager.save_analysis(group_id, analysis_result)
|
||||
|
||||
# 7. 生成报告并发送 (应用层编排发送动作)
|
||||
# 这里由调用方处理发送,本服务只返回分析结果和可能的视觉产物
|
||||
return {
|
||||
"success": True,
|
||||
"analysis_result": analysis_result,
|
||||
"messages_count": len(unified_messages),
|
||||
"adapter": adapter,
|
||||
}
|
||||
Reference in New Issue
Block a user