refactor(DDD): 整理架构

This commit is contained in:
SXP-Simon
2026-02-09 12:47:43 +08:00
parent 0b096351d5
commit 2005f0633f
67 changed files with 1149 additions and 2490 deletions
-3
View File
@@ -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",
-251
View File
@@ -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,
}