mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 20:01:04 +00:00
feat: add DDD architecture layers (domain, infrastructure, application)
- Domain layer: UnifiedMessage, PlatformCapabilities, repository interfaces - Infrastructure layer: PlatformAdapter base, OneBotAdapter, factory - Application layer: AnalysisOrchestrator, MessageConverter This provides cross-platform abstraction for group analysis plugin.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Application Layer - Orchestration and Use Cases
|
||||
from .analysis_orchestrator import AnalysisOrchestrator
|
||||
from .message_converter import MessageConverter
|
||||
|
||||
__all__ = ["AnalysisOrchestrator", "MessageConverter"]
|
||||
@@ -0,0 +1,227 @@
|
||||
"""
|
||||
Analysis Orchestrator - Application layer coordinator
|
||||
|
||||
This orchestrator bridges the new DDD architecture with the existing
|
||||
analysis logic, providing a gradual migration path.
|
||||
|
||||
Architecture Decision:
|
||||
- The orchestrator uses PlatformAdapter for message fetching (new DDD way)
|
||||
- But delegates to existing analyzers for LLM analysis (preserving working code)
|
||||
- MessageConverter provides bidirectional conversion for compatibility
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
from astrbot.api import logger
|
||||
|
||||
from ..domain.value_objects.unified_message import UnifiedMessage
|
||||
from ..domain.value_objects.platform_capabilities import PlatformCapabilities
|
||||
from ..infrastructure.platform import PlatformAdapter, PlatformAdapterFactory
|
||||
from .message_converter import MessageConverter
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalysisConfig:
|
||||
"""Configuration for analysis operation"""
|
||||
days: int = 1
|
||||
max_messages: int = 1000
|
||||
min_messages_threshold: int = 10
|
||||
output_format: str = "image"
|
||||
|
||||
|
||||
class AnalysisOrchestrator:
|
||||
"""
|
||||
Analysis orchestrator - coordinates the analysis workflow.
|
||||
|
||||
Responsibilities:
|
||||
1. Use PlatformAdapter to fetch messages (DDD approach)
|
||||
2. Convert messages for compatibility with existing analyzers
|
||||
3. Coordinate analysis flow
|
||||
4. Provide platform capability checks
|
||||
|
||||
This class serves as the bridge between:
|
||||
- New DDD infrastructure (PlatformAdapter, UnifiedMessage)
|
||||
- Existing analysis logic (MessageHandler, LLMAnalyzer, etc.)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
adapter: PlatformAdapter,
|
||||
config: AnalysisConfig = None,
|
||||
):
|
||||
"""
|
||||
Initialize the orchestrator.
|
||||
|
||||
Args:
|
||||
adapter: Platform adapter for message operations
|
||||
config: Analysis configuration
|
||||
"""
|
||||
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"]:
|
||||
"""
|
||||
Factory method to create orchestrator for a specific platform.
|
||||
|
||||
Args:
|
||||
platform_name: Platform name (e.g., "aiocqhttp", "telegram")
|
||||
bot_instance: Bot instance from AstrBot
|
||||
config: Platform-specific config
|
||||
analysis_config: Analysis configuration
|
||||
|
||||
Returns:
|
||||
AnalysisOrchestrator or None if platform not supported
|
||||
"""
|
||||
adapter = PlatformAdapterFactory.create(platform_name, bot_instance, config)
|
||||
if adapter is None:
|
||||
logger.warning(f"Platform '{platform_name}' not supported for analysis")
|
||||
return None
|
||||
|
||||
return cls(adapter, analysis_config)
|
||||
|
||||
def get_capabilities(self) -> PlatformCapabilities:
|
||||
"""Get platform capabilities."""
|
||||
return self.adapter.get_capabilities()
|
||||
|
||||
def can_analyze(self) -> bool:
|
||||
"""Check if the platform supports analysis."""
|
||||
return self.adapter.get_capabilities().can_analyze()
|
||||
|
||||
def can_send_report(self, format: str = "image") -> bool:
|
||||
"""Check if the platform can send reports in the specified format."""
|
||||
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]:
|
||||
"""
|
||||
Fetch messages using the platform adapter.
|
||||
|
||||
Args:
|
||||
group_id: Group ID to fetch messages from
|
||||
days: Number of days (defaults to config)
|
||||
max_count: Maximum message count (defaults to config)
|
||||
|
||||
Returns:
|
||||
List of UnifiedMessage
|
||||
"""
|
||||
days = days or self.config.days
|
||||
max_count = max_count or self.config.max_messages
|
||||
|
||||
# Apply platform capability limits
|
||||
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"Platform limits: requested {days} days, "
|
||||
f"using {effective_days} 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]:
|
||||
"""
|
||||
Fetch messages and convert to raw dict format.
|
||||
|
||||
This provides backward compatibility with existing analyzers
|
||||
that expect raw dict messages.
|
||||
|
||||
Args:
|
||||
group_id: Group ID to fetch messages from
|
||||
days: Number of days
|
||||
max_count: Maximum message count
|
||||
|
||||
Returns:
|
||||
List of raw message dicts (OneBot format)
|
||||
"""
|
||||
unified_messages = await self.fetch_messages(group_id, days, max_count)
|
||||
return MessageConverter.batch_to_onebot(unified_messages)
|
||||
|
||||
async def get_group_info(self, group_id: str):
|
||||
"""Get group information."""
|
||||
return await self.adapter.get_group_info(group_id)
|
||||
|
||||
async def get_member_avatars(
|
||||
self,
|
||||
user_ids: List[str],
|
||||
size: int = 100,
|
||||
) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
Batch get user avatar URLs.
|
||||
|
||||
Args:
|
||||
user_ids: List of user IDs
|
||||
size: Avatar size
|
||||
|
||||
Returns:
|
||||
Dict mapping user_id to avatar URL (or None)
|
||||
"""
|
||||
return await self.adapter.batch_get_avatar_urls(user_ids, size)
|
||||
|
||||
async def send_text(self, group_id: str, text: str) -> bool:
|
||||
"""Send text message to group."""
|
||||
return await self.adapter.send_text(group_id, text)
|
||||
|
||||
async def send_image(
|
||||
self,
|
||||
group_id: str,
|
||||
image_path: str,
|
||||
caption: str = "",
|
||||
) -> bool:
|
||||
"""Send image to group."""
|
||||
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:
|
||||
"""Send file to group."""
|
||||
return await self.adapter.send_file(group_id, file_path, filename)
|
||||
|
||||
def validate_message_count(self, messages: List[UnifiedMessage]) -> bool:
|
||||
"""
|
||||
Check if message count meets minimum threshold.
|
||||
|
||||
Args:
|
||||
messages: List of messages
|
||||
|
||||
Returns:
|
||||
True if count is sufficient
|
||||
"""
|
||||
return len(messages) >= self.config.min_messages_threshold
|
||||
|
||||
def get_analysis_text(self, messages: List[UnifiedMessage]) -> str:
|
||||
"""
|
||||
Convert messages to analysis text format for LLM.
|
||||
|
||||
Args:
|
||||
messages: List of UnifiedMessage
|
||||
|
||||
Returns:
|
||||
Formatted text for LLM analysis
|
||||
"""
|
||||
return MessageConverter.unified_to_analysis_text(messages)
|
||||
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
Message Converter - Bridges raw platform messages to UnifiedMessage
|
||||
|
||||
This module provides backward compatibility by converting between
|
||||
raw platform message formats and the new UnifiedMessage format.
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ..domain.value_objects.unified_message import (
|
||||
UnifiedMessage,
|
||||
MessageContent,
|
||||
MessageContentType,
|
||||
)
|
||||
|
||||
|
||||
class MessageConverter:
|
||||
"""
|
||||
Converts between raw platform messages and UnifiedMessage format.
|
||||
|
||||
This provides a migration path: existing code can continue using
|
||||
raw dicts while new code uses UnifiedMessage.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_onebot_message(raw_msg: dict, group_id: str) -> Optional[UnifiedMessage]:
|
||||
"""
|
||||
Convert OneBot v11 raw message to UnifiedMessage.
|
||||
|
||||
Args:
|
||||
raw_msg: Raw message dict from OneBot API
|
||||
group_id: Group ID
|
||||
|
||||
Returns:
|
||||
UnifiedMessage or None if conversion fails
|
||||
"""
|
||||
try:
|
||||
sender = raw_msg.get("sender", {})
|
||||
message_chain = raw_msg.get("message", [])
|
||||
|
||||
# Handle string message format
|
||||
if isinstance(message_chain, str):
|
||||
message_chain = [{"type": "text", "data": {"text": message_chain}}]
|
||||
|
||||
contents = []
|
||||
text_parts = []
|
||||
|
||||
for seg in message_chain:
|
||||
seg_type = seg.get("type", "")
|
||||
seg_data = seg.get("data", {})
|
||||
|
||||
if seg_type == "text":
|
||||
text = seg_data.get("text", "")
|
||||
text_parts.append(text)
|
||||
contents.append(MessageContent(type=MessageContentType.TEXT, text=text))
|
||||
|
||||
elif seg_type == "image":
|
||||
contents.append(MessageContent(
|
||||
type=MessageContentType.IMAGE,
|
||||
url=seg_data.get("url", seg_data.get("file", ""))
|
||||
))
|
||||
|
||||
elif seg_type == "at":
|
||||
contents.append(MessageContent(
|
||||
type=MessageContentType.AT,
|
||||
at_user_id=str(seg_data.get("qq", ""))
|
||||
))
|
||||
|
||||
elif seg_type in ("face", "mface", "bface", "sface"):
|
||||
contents.append(MessageContent(
|
||||
type=MessageContentType.EMOJI,
|
||||
emoji_id=str(seg_data.get("id", "")),
|
||||
raw_data={"face_type": seg_type}
|
||||
))
|
||||
|
||||
elif seg_type == "reply":
|
||||
contents.append(MessageContent(
|
||||
type=MessageContentType.REPLY,
|
||||
raw_data={"reply_id": seg_data.get("id", "")}
|
||||
))
|
||||
|
||||
elif seg_type == "forward":
|
||||
contents.append(MessageContent(
|
||||
type=MessageContentType.FORWARD,
|
||||
raw_data=seg_data
|
||||
))
|
||||
|
||||
elif seg_type == "record":
|
||||
contents.append(MessageContent(
|
||||
type=MessageContentType.VOICE,
|
||||
url=seg_data.get("url", seg_data.get("file", ""))
|
||||
))
|
||||
|
||||
elif seg_type == "video":
|
||||
contents.append(MessageContent(
|
||||
type=MessageContentType.VIDEO,
|
||||
url=seg_data.get("url", seg_data.get("file", ""))
|
||||
))
|
||||
|
||||
else:
|
||||
contents.append(MessageContent(
|
||||
type=MessageContentType.UNKNOWN,
|
||||
raw_data=seg
|
||||
))
|
||||
|
||||
# Extract reply_to from contents
|
||||
reply_to = None
|
||||
for c in contents:
|
||||
if c.type == MessageContentType.REPLY and c.raw_data:
|
||||
reply_to = str(c.raw_data.get("reply_id", ""))
|
||||
break
|
||||
|
||||
return UnifiedMessage(
|
||||
message_id=str(raw_msg.get("message_id", "")),
|
||||
sender_id=str(sender.get("user_id", "")),
|
||||
sender_name=sender.get("nickname", ""),
|
||||
sender_card=sender.get("card", "") or None,
|
||||
group_id=group_id,
|
||||
text_content="".join(text_parts),
|
||||
contents=tuple(contents),
|
||||
timestamp=raw_msg.get("time", 0),
|
||||
platform="onebot",
|
||||
reply_to_id=reply_to,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def to_onebot_message(unified: UnifiedMessage) -> dict:
|
||||
"""
|
||||
Convert UnifiedMessage back to OneBot v11 raw format.
|
||||
|
||||
For backward compatibility with existing code that expects raw dicts.
|
||||
"""
|
||||
message_chain = []
|
||||
|
||||
for content in unified.contents:
|
||||
if content.type == MessageContentType.TEXT:
|
||||
message_chain.append({"type": "text", "data": {"text": content.text}})
|
||||
elif content.type == MessageContentType.IMAGE:
|
||||
message_chain.append({"type": "image", "data": {"url": content.url}})
|
||||
elif content.type == MessageContentType.AT:
|
||||
message_chain.append({"type": "at", "data": {"qq": content.at_user_id}})
|
||||
elif content.type == MessageContentType.EMOJI:
|
||||
face_type = content.raw_data.get("face_type", "face") if content.raw_data else "face"
|
||||
message_chain.append({"type": face_type, "data": {"id": content.emoji_id}})
|
||||
elif content.type == MessageContentType.REPLY:
|
||||
reply_id = content.raw_data.get("reply_id", "") if content.raw_data else ""
|
||||
message_chain.append({"type": "reply", "data": {"id": reply_id}})
|
||||
elif content.type == MessageContentType.VOICE:
|
||||
message_chain.append({"type": "record", "data": {"url": content.url}})
|
||||
elif content.type == MessageContentType.VIDEO:
|
||||
message_chain.append({"type": "video", "data": {"url": content.url}})
|
||||
|
||||
return {
|
||||
"message_id": unified.message_id,
|
||||
"sender": {
|
||||
"user_id": unified.sender_id,
|
||||
"nickname": unified.sender_name,
|
||||
"card": unified.sender_card or "",
|
||||
},
|
||||
"group_id": unified.group_id,
|
||||
"message": message_chain,
|
||||
"time": unified.timestamp,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def batch_from_onebot(raw_messages: List[dict], group_id: str) -> List[UnifiedMessage]:
|
||||
"""Convert a batch of OneBot messages to UnifiedMessage list."""
|
||||
result = []
|
||||
for raw_msg in raw_messages:
|
||||
unified = MessageConverter.from_onebot_message(raw_msg, group_id)
|
||||
if unified:
|
||||
result.append(unified)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def batch_to_onebot(unified_messages: List[UnifiedMessage]) -> List[dict]:
|
||||
"""Convert a batch of UnifiedMessage to OneBot raw format."""
|
||||
return [MessageConverter.to_onebot_message(msg) for msg in unified_messages]
|
||||
|
||||
@staticmethod
|
||||
def unified_to_analysis_text(messages: List[UnifiedMessage]) -> str:
|
||||
"""
|
||||
Convert UnifiedMessage list to analysis text format for LLM.
|
||||
|
||||
This is the format expected by the existing LLM analyzers.
|
||||
"""
|
||||
lines = []
|
||||
for msg in messages:
|
||||
if msg.has_text():
|
||||
lines.append(msg.to_analysis_format())
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user