feat: complete DDD Phase 2 - add domain services, infrastructure layers, and shared components

- domain/value_objects: Add Topic, UserTitle, GoldenQuote, Statistics value objects
- domain/services: Add StatisticsCalculator, ReportGenerator domain services
- domain/exceptions: Add comprehensive domain exception hierarchy
- infrastructure/persistence: Add HistoryRepository for data storage
- infrastructure/llm: Add LLMClient wrapper for AstrBot providers
- infrastructure/config: Add ConfigManager for centralized configuration
- infrastructure/resilience: Add CircuitBreaker, RateLimiter, retry utilities
- application: Add SchedulingService, ReportingService application services
- shared: Add constants and TraceContext for request tracing

All imports verified in Docker container.
This commit is contained in:
SXP-Simon
2026-02-08 14:39:19 +08:00
parent 8f1878356d
commit 7ef58f9d53
27 changed files with 3379 additions and 3 deletions
+11
View File
@@ -0,0 +1,11 @@
"""
Shared Module - Common utilities and constants
"""
from .constants import *
from .trace_context import TraceContext
__all__ = [
"TraceContext",
# Constants are exported via *
]
+91
View File
@@ -0,0 +1,91 @@
"""
Constants - Shared constants used across the plugin
"""
# Plugin metadata
PLUGIN_NAME = "astrbot_plugin_qq_group_daily_analysis"
PLUGIN_VERSION = "2.0.0"
# Platform identifiers
PLATFORM_ONEBOT = "onebot"
PLATFORM_TELEGRAM = "telegram"
PLATFORM_DISCORD = "discord"
PLATFORM_SLACK = "slack"
PLATFORM_LARK = "lark"
SUPPORTED_PLATFORMS = [
PLATFORM_ONEBOT,
# Future platforms
# PLATFORM_TELEGRAM,
# PLATFORM_DISCORD,
# PLATFORM_SLACK,
# PLATFORM_LARK,
]
# Analysis defaults
DEFAULT_MAX_TOPICS = 5
DEFAULT_MAX_USER_TITLES = 10
DEFAULT_MAX_GOLDEN_QUOTES = 5
DEFAULT_MIN_MESSAGES = 50
DEFAULT_MAX_TOKENS = 2000
# Time periods
HOUR_RANGES = {
"morning": (6, 12),
"afternoon": (12, 18),
"evening": (18, 24),
"night": (0, 6),
}
# Report formats
REPORT_FORMAT_TEXT = "text"
REPORT_FORMAT_MARKDOWN = "markdown"
REPORT_FORMAT_IMAGE = "image"
REPORT_FORMAT_HTML = "html"
# Message content types
CONTENT_TYPE_TEXT = "text"
CONTENT_TYPE_IMAGE = "image"
CONTENT_TYPE_EMOJI = "emoji"
CONTENT_TYPE_STICKER = "sticker"
CONTENT_TYPE_FILE = "file"
CONTENT_TYPE_AUDIO = "audio"
CONTENT_TYPE_VIDEO = "video"
CONTENT_TYPE_REPLY = "reply"
CONTENT_TYPE_AT = "at"
CONTENT_TYPE_UNKNOWN = "unknown"
# Analysis task states
TASK_STATE_PENDING = "pending"
TASK_STATE_RUNNING = "running"
TASK_STATE_COMPLETED = "completed"
TASK_STATE_FAILED = "failed"
TASK_STATE_CANCELLED = "cancelled"
# Error codes
ERROR_INSUFFICIENT_DATA = "INSUFFICIENT_DATA"
ERROR_LLM_FAILED = "LLM_FAILED"
ERROR_PLATFORM_ERROR = "PLATFORM_ERROR"
ERROR_CONFIG_ERROR = "CONFIG_ERROR"
ERROR_TIMEOUT = "TIMEOUT"
# Cache TTL (in seconds)
CACHE_TTL_SHORT = 60 # 1 minute
CACHE_TTL_MEDIUM = 300 # 5 minutes
CACHE_TTL_LONG = 3600 # 1 hour
CACHE_TTL_DAY = 86400 # 24 hours
# Rate limiting defaults
RATE_LIMIT_LLM_CALLS = 10 # calls per minute
RATE_LIMIT_API_CALLS = 60 # calls per minute
RATE_LIMIT_BURST = 5 # burst size
# Retry defaults
RETRY_MAX_ATTEMPTS = 3
RETRY_BASE_DELAY = 1.0
RETRY_MAX_DELAY = 30.0
# File paths
HISTORY_DIR = "history"
CACHE_DIR = "cache"
TEMP_DIR = "temp"
+161
View File
@@ -0,0 +1,161 @@
"""
Trace Context - Request tracing and correlation
Provides context for tracking requests across the plugin.
"""
import uuid
from contextvars import ContextVar
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, Optional
# Context variable for current trace
_current_trace: ContextVar[Optional["TraceContext"]] = ContextVar(
"current_trace", default=None
)
@dataclass
class TraceContext:
"""
Context for tracing requests through the plugin.
Provides correlation IDs and timing information for debugging
and monitoring.
"""
trace_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
group_id: str = ""
platform: str = ""
operation: str = ""
start_time: datetime = field(default_factory=datetime.now)
metadata: Dict[str, Any] = field(default_factory=dict)
# Timing data
_checkpoints: Dict[str, datetime] = field(default_factory=dict, init=False)
def checkpoint(self, name: str) -> None:
"""
Record a timing checkpoint.
Args:
name: Checkpoint name
"""
self._checkpoints[name] = datetime.now()
def elapsed_ms(self, from_checkpoint: Optional[str] = None) -> float:
"""
Get elapsed time in milliseconds.
Args:
from_checkpoint: Optional checkpoint to measure from
Returns:
Elapsed time in milliseconds
"""
start = self.start_time
if from_checkpoint and from_checkpoint in self._checkpoints:
start = self._checkpoints[from_checkpoint]
delta = datetime.now() - start
return delta.total_seconds() * 1000
def to_dict(self) -> Dict[str, Any]:
"""Convert trace context to dictionary."""
return {
"trace_id": self.trace_id,
"group_id": self.group_id,
"platform": self.platform,
"operation": self.operation,
"start_time": self.start_time.isoformat(),
"elapsed_ms": self.elapsed_ms(),
"metadata": self.metadata,
"checkpoints": {k: v.isoformat() for k, v in self._checkpoints.items()},
}
def __enter__(self) -> "TraceContext":
"""Enter context manager."""
_current_trace.set(self)
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
"""Exit context manager."""
_current_trace.set(None)
@classmethod
def current(cls) -> Optional["TraceContext"]:
"""Get the current trace context."""
return _current_trace.get()
@classmethod
def get_or_create(
cls,
group_id: str = "",
platform: str = "",
operation: str = "",
) -> "TraceContext":
"""
Get current trace or create a new one.
Args:
group_id: Group identifier
platform: Platform name
operation: Operation name
Returns:
TraceContext instance
"""
current = cls.current()
if current:
return current
return cls(
group_id=group_id,
platform=platform,
operation=operation,
)
def get_trace_id() -> str:
"""
Get current trace ID or generate a new one.
Returns:
Trace ID string
"""
trace = TraceContext.current()
if trace:
return trace.trace_id
return str(uuid.uuid4())[:8]
def with_trace(
group_id: str = "",
platform: str = "",
operation: str = "",
):
"""
Decorator to add trace context to a function.
Args:
group_id: Group identifier
platform: Platform name
operation: Operation name
Returns:
Decorated function
"""
def decorator(func):
async def wrapper(*args, **kwargs):
with TraceContext(
group_id=group_id,
platform=platform,
operation=operation or func.__name__,
):
return await func(*args, **kwargs)
return wrapper
return decorator