diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..dee30f0 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,19 @@ +""" +QQ群日常分析插件 - 源代码包 + +本包包含插件的核心实现,采用 DDD (领域驱动设计) 架构: +- application: 应用层 - 编排领域服务,处理用例 +- domain: 领域层 - 核心业务逻辑,平台无关 +- infrastructure: 基础设施层 - 外部服务适配 +- shared: 共享组件 - 跨层使用的工具和常量 + +遗留模块(渐进式迁移中): +- analysis: 分析器实现 +- core: 核心组件 +- reports: 报告生成 +- scheduler: 定时任务 +- utils: 工具函数 +- visualization: 可视化组件 +""" + +__version__ = "2.0.0" diff --git a/src/domain/entities/__init__.py b/src/domain/entities/__init__.py index 663e87d..924d6eb 100644 --- a/src/domain/entities/__init__.py +++ b/src/domain/entities/__init__.py @@ -1,5 +1,36 @@ -# 领域实体 -from .analysis_task import AnalysisTask, TaskStatus -from .analysis_result import GroupAnalysisResult +""" +领域实体 -__all__ = ["AnalysisTask", "TaskStatus", "GroupAnalysisResult"] +该模块导出所有领域实体类,包括: +- AnalysisTask: 分析任务聚合根 +- GroupAnalysisResult: 群聊分析结果实体 +""" + +from .analysis_task import AnalysisTask, TaskStatus +from .analysis_result import ( + GroupAnalysisResult, + SummaryTopic, + UserTitle, + GoldenQuote, + TokenUsage, + EmojiStatistics, + ActivityVisualization, + GroupStatistics, +) + +# 别名,保持向后兼容 +AnalysisResult = GroupAnalysisResult + +__all__ = [ + "AnalysisTask", + "TaskStatus", + "GroupAnalysisResult", + "AnalysisResult", # 别名 + "SummaryTopic", + "UserTitle", + "GoldenQuote", + "TokenUsage", + "EmojiStatistics", + "ActivityVisualization", + "GroupStatistics", +] diff --git a/src/domain/services/__init__.py b/src/domain/services/__init__.py index 0502d22..4abe061 100644 --- a/src/domain/services/__init__.py +++ b/src/domain/services/__init__.py @@ -3,12 +3,30 @@ 该模块导出所有封装核心业务逻辑的领域服务, 用于分析群聊数据。这些服务是平台无关的。 + +服务分类: +- 分析器服务: 话题分析、用户称号分析、金句分析 +- 计算服务: 统计计算 +- 生成服务: 报告生成 """ from .statistics_calculator import StatisticsCalculator from .report_generator import ReportGenerator +from .topic_analyzer import ITopicAnalyzer, TopicAnalyzerAdapter +from .user_title_analyzer import IUserTitleAnalyzer, UserTitleAnalyzerAdapter +from .golden_quote_analyzer import IGoldenQuoteAnalyzer, GoldenQuoteAnalyzerAdapter __all__ = [ + # 统计与报告服务 "StatisticsCalculator", "ReportGenerator", + # 话题分析服务 + "ITopicAnalyzer", + "TopicAnalyzerAdapter", + # 用户称号分析服务 + "IUserTitleAnalyzer", + "UserTitleAnalyzerAdapter", + # 金句分析服务 + "IGoldenQuoteAnalyzer", + "GoldenQuoteAnalyzerAdapter", ] diff --git a/src/domain/services/golden_quote_analyzer.py b/src/domain/services/golden_quote_analyzer.py new file mode 100644 index 0000000..f4e26b4 --- /dev/null +++ b/src/domain/services/golden_quote_analyzer.py @@ -0,0 +1,132 @@ +""" +金句分析领域服务 + +该模块提供平台无关的金句分析服务接口。 +实际分析逻辑委托给 infrastructure 层的具体实现。 + +架构说明: +- 本文件定义领域服务接口和数据转换逻辑 +- 具体的 LLM 调用和消息处理在 src/analysis/analyzers/golden_quote_analyzer.py 中实现 +- 采用渐进式迁移策略,保持与现有代码的兼容性 +""" + +from abc import ABC, abstractmethod +from typing import List, Tuple, TYPE_CHECKING + +from ..value_objects.golden_quote import GoldenQuote +from ..value_objects.unified_message import UnifiedMessage + +if TYPE_CHECKING: + from ..value_objects.statistics import TokenUsage + + +class IGoldenQuoteAnalyzer(ABC): + """ + 金句分析服务接口 + + 定义平台无关的金句分析契约。 + 所有平台的金句分析都应该实现此接口。 + """ + + @abstractmethod + async def analyze( + self, + messages: List[UnifiedMessage], + unified_msg_origin: str = None, + ) -> Tuple[List[GoldenQuote], "TokenUsage"]: + """ + 分析消息中的金句 + + 参数: + messages: 统一格式的消息列表 + unified_msg_origin: 消息来源标识,用于选择 LLM 提供商 + + 返回: + (金句列表, Token 使用统计) + """ + pass + + +class GoldenQuoteAnalyzerAdapter(IGoldenQuoteAnalyzer): + """ + 金句分析服务适配器 + + 将现有的 GoldenQuoteAnalyzer 实现适配为领域服务接口。 + 负责 UnifiedMessage 与原始消息格式之间的转换。 + """ + + def __init__(self, legacy_analyzer): + """ + 初始化适配器 + + 参数: + legacy_analyzer: 现有的 GoldenQuoteAnalyzer 实例 + """ + self._analyzer = legacy_analyzer + + async def analyze( + self, + messages: List[UnifiedMessage], + unified_msg_origin: str = None, + ) -> Tuple[List[GoldenQuote], "TokenUsage"]: + """ + 分析消息中的金句 + + 将 UnifiedMessage 转换为原始格式,调用现有分析器, + 然后将结果转换为领域值对象。 + + 参数: + messages: 统一格式的消息列表 + unified_msg_origin: 消息来源标识 + + 返回: + (金句列表, Token 使用统计) + """ + # 将 UnifiedMessage 转换为原始消息格式 + raw_messages = [self._to_raw_message(msg) for msg in messages] + + # 调用现有分析器 + legacy_quotes, token_usage = await self._analyzer.analyze_golden_quotes( + raw_messages, unified_msg_origin + ) + + # 将结果转换为领域值对象 + quotes = [ + GoldenQuote( + content=q.content, + sender_name=q.sender, + sender_id=str(q.qq) if hasattr(q, 'qq') and q.qq else None, + reason=q.reason, + ) + for q in legacy_quotes + ] + + return quotes, token_usage + + def _to_raw_message(self, msg: UnifiedMessage) -> dict: + """ + 将 UnifiedMessage 转换为原始消息格式 + + 参数: + msg: 统一消息对象 + + 返回: + 原始消息字典 + """ + # 构建消息内容列表 + message_content = [] + if msg.text_content: + message_content.append({ + "type": "text", + "data": {"text": msg.text_content} + }) + + return { + "message_id": msg.message_id, + "time": int(msg.timestamp.timestamp()) if msg.timestamp else 0, + "sender": { + "user_id": msg.sender_id, + "nickname": msg.sender_name, + }, + "message": message_content, + } diff --git a/src/domain/services/topic_analyzer.py b/src/domain/services/topic_analyzer.py new file mode 100644 index 0000000..d7fcd84 --- /dev/null +++ b/src/domain/services/topic_analyzer.py @@ -0,0 +1,131 @@ +""" +话题分析领域服务 + +该模块提供平台无关的话题分析服务接口。 +实际分析逻辑委托给 infrastructure 层的具体实现。 + +架构说明: +- 本文件定义领域服务接口和数据转换逻辑 +- 具体的 LLM 调用和消息处理在 src/analysis/analyzers/topic_analyzer.py 中实现 +- 采用渐进式迁移策略,保持与现有代码的兼容性 +""" + +from abc import ABC, abstractmethod +from typing import List, Tuple, TYPE_CHECKING + +from ..value_objects.topic import Topic +from ..value_objects.unified_message import UnifiedMessage + +if TYPE_CHECKING: + from ..value_objects.statistics import TokenUsage + + +class ITopicAnalyzer(ABC): + """ + 话题分析服务接口 + + 定义平台无关的话题分析契约。 + 所有平台的话题分析都应该实现此接口。 + """ + + @abstractmethod + async def analyze( + self, + messages: List[UnifiedMessage], + unified_msg_origin: str = None, + ) -> Tuple[List[Topic], "TokenUsage"]: + """ + 分析消息中的话题 + + 参数: + messages: 统一格式的消息列表 + unified_msg_origin: 消息来源标识,用于选择 LLM 提供商 + + 返回: + (话题列表, Token 使用统计) + """ + pass + + +class TopicAnalyzerAdapter(ITopicAnalyzer): + """ + 话题分析服务适配器 + + 将现有的 TopicAnalyzer 实现适配为领域服务接口。 + 负责 UnifiedMessage 与原始消息格式之间的转换。 + """ + + def __init__(self, legacy_analyzer): + """ + 初始化适配器 + + 参数: + legacy_analyzer: 现有的 TopicAnalyzer 实例 + """ + self._analyzer = legacy_analyzer + + async def analyze( + self, + messages: List[UnifiedMessage], + unified_msg_origin: str = None, + ) -> Tuple[List[Topic], "TokenUsage"]: + """ + 分析消息中的话题 + + 将 UnifiedMessage 转换为原始格式,调用现有分析器, + 然后将结果转换为领域值对象。 + + 参数: + messages: 统一格式的消息列表 + unified_msg_origin: 消息来源标识 + + 返回: + (话题列表, Token 使用统计) + """ + # 将 UnifiedMessage 转换为原始消息格式 + raw_messages = [self._to_raw_message(msg) for msg in messages] + + # 调用现有分析器 + legacy_topics, token_usage = await self._analyzer.analyze_topics( + raw_messages, unified_msg_origin + ) + + # 将结果转换为领域值对象 + topics = [ + Topic( + name=t.topic, + contributors=t.contributors, + detail=t.detail, + ) + for t in legacy_topics + ] + + return topics, token_usage + + def _to_raw_message(self, msg: UnifiedMessage) -> dict: + """ + 将 UnifiedMessage 转换为原始消息格式 + + 参数: + msg: 统一消息对象 + + 返回: + 原始消息字典 + """ + # 构建消息内容列表 + message_content = [] + if msg.text_content: + message_content.append({ + "type": "text", + "data": {"text": msg.text_content} + }) + + return { + "message_id": msg.message_id, + "time": int(msg.timestamp.timestamp()) if msg.timestamp else 0, + "sender": { + "user_id": msg.sender_id, + "nickname": msg.sender_name, + }, + "message": message_content, + } diff --git a/src/domain/services/user_title_analyzer.py b/src/domain/services/user_title_analyzer.py new file mode 100644 index 0000000..d7c19c9 --- /dev/null +++ b/src/domain/services/user_title_analyzer.py @@ -0,0 +1,141 @@ +""" +用户称号分析领域服务 + +该模块提供平台无关的用户称号分析服务接口。 +实际分析逻辑委托给 infrastructure 层的具体实现。 + +架构说明: +- 本文件定义领域服务接口和数据转换逻辑 +- 具体的 LLM 调用和消息处理在 src/analysis/analyzers/user_title_analyzer.py 中实现 +- 采用渐进式迁移策略,保持与现有代码的兼容性 +""" + +from abc import ABC, abstractmethod +from typing import List, Dict, Tuple, Any, TYPE_CHECKING + +from ..value_objects.user_title import UserTitle +from ..value_objects.unified_message import UnifiedMessage + +if TYPE_CHECKING: + from ..value_objects.statistics import TokenUsage + + +class IUserTitleAnalyzer(ABC): + """ + 用户称号分析服务接口 + + 定义平台无关的用户称号分析契约。 + 所有平台的用户称号分析都应该实现此接口。 + """ + + @abstractmethod + async def analyze( + self, + messages: List[UnifiedMessage], + user_analysis: Dict[str, Any], + unified_msg_origin: str = None, + top_users: List[Dict] = None, + ) -> Tuple[List[UserTitle], "TokenUsage"]: + """ + 分析用户称号 + + 参数: + messages: 统一格式的消息列表 + user_analysis: 用户分析统计数据 + unified_msg_origin: 消息来源标识,用于选择 LLM 提供商 + top_users: 活跃用户列表(可选) + + 返回: + (用户称号列表, Token 使用统计) + """ + pass + + +class UserTitleAnalyzerAdapter(IUserTitleAnalyzer): + """ + 用户称号分析服务适配器 + + 将现有的 UserTitleAnalyzer 实现适配为领域服务接口。 + 负责 UnifiedMessage 与原始消息格式之间的转换。 + """ + + def __init__(self, legacy_analyzer): + """ + 初始化适配器 + + 参数: + legacy_analyzer: 现有的 UserTitleAnalyzer 实例 + """ + self._analyzer = legacy_analyzer + + async def analyze( + self, + messages: List[UnifiedMessage], + user_analysis: Dict[str, Any], + unified_msg_origin: str = None, + top_users: List[Dict] = None, + ) -> Tuple[List[UserTitle], "TokenUsage"]: + """ + 分析用户称号 + + 将 UnifiedMessage 转换为原始格式,调用现有分析器, + 然后将结果转换为领域值对象。 + + 参数: + messages: 统一格式的消息列表 + user_analysis: 用户分析统计数据 + unified_msg_origin: 消息来源标识 + top_users: 活跃用户列表 + + 返回: + (用户称号列表, Token 使用统计) + """ + # 将 UnifiedMessage 转换为原始消息格式 + raw_messages = [self._to_raw_message(msg) for msg in messages] + + # 调用现有分析器 + legacy_titles, token_usage = await self._analyzer.analyze_user_titles( + raw_messages, user_analysis, unified_msg_origin, top_users + ) + + # 将结果转换为领域值对象 + titles = [ + UserTitle( + user_id=str(t.qq), + user_name=t.name, + title=t.title, + mbti=t.mbti, + reason=t.reason, + ) + for t in legacy_titles + ] + + return titles, token_usage + + def _to_raw_message(self, msg: UnifiedMessage) -> dict: + """ + 将 UnifiedMessage 转换为原始消息格式 + + 参数: + msg: 统一消息对象 + + 返回: + 原始消息字典 + """ + # 构建消息内容列表 + message_content = [] + if msg.text_content: + message_content.append({ + "type": "text", + "data": {"text": msg.text_content} + }) + + return { + "message_id": msg.message_id, + "time": int(msg.timestamp.timestamp()) if msg.timestamp else 0, + "sender": { + "user_id": msg.sender_id, + "nickname": msg.sender_name, + }, + "message": message_content, + } diff --git a/src/shared/constants.py b/src/shared/constants.py index cd2f0e1..072eb6c 100644 --- a/src/shared/constants.py +++ b/src/shared/constants.py @@ -2,6 +2,50 @@ 常量 - 插件中使用的共享常量 """ +from enum import Enum + + +class Platform(str, Enum): + """平台枚举类""" + ONEBOT = "onebot" + AIOCQHTTP = "aiocqhttp" + TELEGRAM = "telegram" + DISCORD = "discord" + SLACK = "slack" + LARK = "lark" + + +class TaskStatus(str, Enum): + """任务状态枚举类""" + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + + +class ContentType(str, Enum): + """消息内容类型枚举类""" + TEXT = "text" + IMAGE = "image" + EMOJI = "emoji" + STICKER = "sticker" + FILE = "file" + AUDIO = "audio" + VIDEO = "video" + REPLY = "reply" + AT = "at" + UNKNOWN = "unknown" + + +class ReportFormat(str, Enum): + """报告格式枚举类""" + TEXT = "text" + MARKDOWN = "markdown" + IMAGE = "image" + HTML = "html" + + # 插件元数据 PLUGIN_NAME = "astrbot_plugin_qq_group_daily_analysis" PLUGIN_VERSION = "2.0.0"