mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
docs: code structure for improved readability and scalability
This commit is contained in:
@@ -0,0 +1,698 @@
|
||||
# 07. DDD 重构分析报告 (Domain-Driven Design Refactoring Analysis)
|
||||
|
||||
> **分析日期**: 2026-02-08
|
||||
> **分析范围**: 插件完整代码库 + 现有文档 (01-06)
|
||||
> **分析目的**: 基于 DDD 范式,结合现有代码现状和 06_review.md 的务实建议,制定可落地的重构方案
|
||||
|
||||
---
|
||||
|
||||
## 1. 执行摘要 (Executive Summary)
|
||||
|
||||
### 1.1 当前状态评估
|
||||
|
||||
经过对代码库的深度分析,本插件已经完成了一次显著的重构(基于 06_review.md 的建议),当前架构状态:
|
||||
|
||||
| 维度 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| **模块拆分** | ✅ 已完成 | AutoScheduler 已从 1000+ 行精简至 517 行 |
|
||||
| **职责分离** | ✅ 已完成 | MessageSender、ReportDispatcher、BotManager 已独立 |
|
||||
| **TraceID** | ✅ 已实现 | 使用 contextvars 实现零侵入链路追踪 |
|
||||
| **熔断器** | ✅ 已实现 | CircuitBreaker + GlobalRateLimiter 已就绪 |
|
||||
| **框架对齐** | ✅ 已完成 | 使用 Context.cron_manager + OnPlatformLoaded 钩子 |
|
||||
| **DDD 分层** | ⚠️ 部分 | 有模块划分但未严格遵循 DDD 分层架构 |
|
||||
|
||||
### 1.2 核心结论
|
||||
|
||||
**本插件已完成 06_review.md 建议的务实重构,代码质量良好。**
|
||||
|
||||
进一步的 DDD 重构应聚焦于:
|
||||
1. **领域边界明确化** - 定义清晰的限界上下文
|
||||
2. **领域模型增强** - 引入轻量级 DDD 战术模式
|
||||
3. **依赖方向规范** - 确保依赖指向领域层
|
||||
4. **可测试性提升** - 通过接口抽象支持单元测试
|
||||
|
||||
---
|
||||
|
||||
## 2. 现有架构分析
|
||||
|
||||
### 2.1 当前目录结构
|
||||
|
||||
```
|
||||
astrbot_plugin_qq_group_daily_analysis/
|
||||
├── main.py # 插件入口 (Application Layer - Controller)
|
||||
├── src/
|
||||
│ ├── core/ # 核心模块
|
||||
│ │ ├── bot_manager.py # Bot 实例管理 (Infrastructure)
|
||||
│ │ ├── config.py # 配置管理 (Infrastructure)
|
||||
│ │ ├── history_manager.py # 历史记录管理 (Infrastructure)
|
||||
│ │ ├── message_handler.py # 消息处理 (Application Service)
|
||||
│ │ └── message_sender.py # 消息发送 (Infrastructure)
|
||||
│ ├── scheduler/ # 调度模块
|
||||
│ │ ├── auto_scheduler.py # 自动调度器 (Application Service)
|
||||
│ │ └── retry.py # 重试管理器 (Infrastructure)
|
||||
│ ├── analysis/ # 分析模块
|
||||
│ │ ├── llm_analyzer.py # LLM 分析协调器 (Application Service)
|
||||
│ │ ├── analyzers/ # 具体分析器 (Domain Services)
|
||||
│ │ │ ├── base_analyzer.py
|
||||
│ │ │ ├── topic_analyzer.py
|
||||
│ │ │ ├── user_title_analyzer.py
|
||||
│ │ │ └── golden_quote_analyzer.py
|
||||
│ │ └── utils/ # 分析工具
|
||||
│ │ ├── json_utils.py
|
||||
│ │ └── llm_utils.py
|
||||
│ ├── models/ # 数据模型
|
||||
│ │ └── data_models.py # 数据结构定义 (Domain Models)
|
||||
│ ├── reports/ # 报告模块
|
||||
│ │ ├── dispatcher.py # 报告分发器 (Application Service)
|
||||
│ │ ├── generators.py # 报告生成器 (Domain Service)
|
||||
│ │ └── templates/ # 报告模板 (Infrastructure)
|
||||
│ ├── visualization/ # 可视化模块
|
||||
│ │ └── activity_charts.py # 活跃度图表 (Domain Service)
|
||||
│ └── utils/ # 工具模块
|
||||
│ ├── helpers.py # 辅助函数
|
||||
│ ├── pdf_utils.py # PDF 工具 (Infrastructure)
|
||||
│ ├── resilience.py # 熔断器/限流器 (Infrastructure)
|
||||
│ └── trace_context.py # 链路追踪 (Infrastructure)
|
||||
└── docs/ # 文档
|
||||
```
|
||||
|
||||
### 2.2 当前架构优点
|
||||
|
||||
| 优点 | 体现 |
|
||||
|------|------|
|
||||
| **模块化清晰** | 按功能划分目录:core、scheduler、analysis、reports、visualization |
|
||||
| **职责单一** | MessageSender 只负责发送,ReportDispatcher 只负责分发 |
|
||||
| **可观测性** | TraceContext + TraceLogFilter 实现全链路追踪 |
|
||||
| **弹性设计** | CircuitBreaker + GlobalRateLimiter + RetryManager |
|
||||
| **框架对齐** | 使用 AstrBot 的 cron_manager、platform_manager、put_kv_data |
|
||||
| **并发控制** | asyncio.Semaphore 控制并发,asyncio.gather 并行执行 |
|
||||
|
||||
### 2.3 当前架构不足 (DDD 视角)
|
||||
|
||||
| 不足 | 说明 | 影响 |
|
||||
|------|------|------|
|
||||
| **领域边界模糊** | 没有明确的限界上下文定义 | 模块间耦合度难以评估 |
|
||||
| **贫血模型** | data_models.py 只有数据结构,无行为 | 业务逻辑分散在 Service 中 |
|
||||
| **依赖方向混乱** | Application 层直接依赖 Infrastructure | 测试困难,替换实现困难 |
|
||||
| **缺少聚合根** | 没有定义实体的一致性边界 | 状态管理分散 |
|
||||
| **接口抽象不足** | 直接依赖具体实现类 | Mock 测试困难 |
|
||||
|
||||
---
|
||||
|
||||
## 3. DDD 重构方案
|
||||
|
||||
### 3.1 限界上下文识别 (Bounded Contexts)
|
||||
|
||||
基于业务能力分析,本插件包含以下限界上下文:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 群聊日常分析插件 (Plugin) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 调度上下文 │ │ 分析上下文 │ │ 报告上下文 │ │
|
||||
│ │ (Scheduling) │ │ (Analysis) │ │ (Reporting) │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ - 定时任务 │ │ - 话题分析 │ │ - 报告生成 │ │
|
||||
│ │ - 并发控制 │ │ - 用户画像 │ │ - 格式转换 │ │
|
||||
│ │ - 任务编排 │ │ - 金句提取 │ │ - 消息发送 │ │
|
||||
│ │ │ │ - 统计计算 │ │ - 重试机制 │ │
|
||||
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
|
||||
│ │ │ │ │
|
||||
│ └────────────────────┼────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────┴─────────────────────────────┐ │
|
||||
│ │ 共享内核 (Shared Kernel) │ │
|
||||
│ │ - 配置管理 (ConfigManager) │ │
|
||||
│ │ - Bot 管理 (BotManager) │ │
|
||||
│ │ - 链路追踪 (TraceContext) │ │
|
||||
│ │ - 弹性组件 (CircuitBreaker, RateLimiter) │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 DDD 分层架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Interface Layer │
|
||||
│ (接口层 / 用户界面) │
|
||||
│ main.py - 命令处理、事件响应、框架集成 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ (应用层 / 用例) │
|
||||
│ - AnalysisOrchestrator: 分析流程编排 │
|
||||
│ - SchedulingService: 定时任务管理 │
|
||||
│ - ReportingService: 报告生成与分发 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Domain Layer │
|
||||
│ (领域层 / 核心业务) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Entities (实体) │ │
|
||||
│ │ - AnalysisTask: 分析任务实体 │ │
|
||||
│ │ - GroupAnalysisResult: 群分析结果实体 │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Value Objects (值对象) │ │
|
||||
│ │ - SummaryTopic, UserTitle, GoldenQuote │ │
|
||||
│ │ - GroupStatistics, TokenUsage │ │
|
||||
│ │ - AnalysisContext, RetryPolicy │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Domain Services (领域服务) │ │
|
||||
│ │ - TopicAnalyzer, UserTitleAnalyzer, GoldenQuoteAnalyzer│ │
|
||||
│ │ - StatisticsCalculator, ActivityVisualizer │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Repository Interfaces (仓储接口) │ │
|
||||
│ │ - IAnalysisHistoryRepository │ │
|
||||
│ │ - IMessageRepository │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Infrastructure Layer │
|
||||
│ (基础设施层 / 实现) │
|
||||
│ - KVAnalysisHistoryRepository: 使用 AstrBot KV 存储 │
|
||||
│ - OneBotMessageRepository: 通过 OneBot API 获取消息 │
|
||||
│ - MessageSender: 消息发送实现 │
|
||||
│ - LLMClient: LLM API 调用实现 │
|
||||
│ - ConfigManager: 配置读写实现 │
|
||||
│ - BotManager: Bot 实例管理实现 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.3 目标目录结构
|
||||
|
||||
```
|
||||
astrbot_plugin_qq_group_daily_analysis/
|
||||
├── main.py # Interface Layer
|
||||
├── src/
|
||||
│ ├── application/ # Application Layer
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── analysis_orchestrator.py # 分析流程编排
|
||||
│ │ ├── scheduling_service.py # 定时任务服务
|
||||
│ │ └── reporting_service.py # 报告服务
|
||||
│ │
|
||||
│ ├── domain/ # Domain Layer
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── entities/ # 实体
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── analysis_task.py # 分析任务实体
|
||||
│ │ │ └── analysis_result.py # 分析结果实体
|
||||
│ │ ├── value_objects/ # 值对象
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── topic.py # SummaryTopic
|
||||
│ │ │ ├── user_title.py # UserTitle
|
||||
│ │ │ ├── golden_quote.py # GoldenQuote
|
||||
│ │ │ ├── statistics.py # GroupStatistics, TokenUsage
|
||||
│ │ │ └── analysis_context.py # AnalysisContext
|
||||
│ │ ├── services/ # 领域服务
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── topic_analyzer.py
|
||||
│ │ │ ├── user_title_analyzer.py
|
||||
│ │ │ ├── golden_quote_analyzer.py
|
||||
│ │ │ ├── statistics_calculator.py
|
||||
│ │ │ └── report_generator.py
|
||||
│ │ └── repositories/ # 仓储接口
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── analysis_history_repository.py
|
||||
│ │ └── message_repository.py
|
||||
│ │
|
||||
│ ├── infrastructure/ # Infrastructure Layer
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── persistence/ # 持久化实现
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ └── kv_analysis_history_repository.py
|
||||
│ │ ├── messaging/ # 消息通信
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── onebot_message_repository.py
|
||||
│ │ │ ├── message_sender.py
|
||||
│ │ │ └── retry_manager.py
|
||||
│ │ ├── llm/ # LLM 集成
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── llm_client.py
|
||||
│ │ │ └── llm_utils.py
|
||||
│ │ ├── bot/ # Bot 管理
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ └── bot_manager.py
|
||||
│ │ ├── config/ # 配置管理
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ └── config_manager.py
|
||||
│ │ └── resilience/ # 弹性组件
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── circuit_breaker.py
|
||||
│ │ ├── rate_limiter.py
|
||||
│ │ └── trace_context.py
|
||||
│ │
|
||||
│ └── shared/ # 共享内核
|
||||
│ ├── __init__.py
|
||||
│ └── exceptions.py # 自定义异常
|
||||
│
|
||||
├── tests/ # 测试
|
||||
│ ├── unit/
|
||||
│ │ ├── domain/
|
||||
│ │ └── application/
|
||||
│ └── integration/
|
||||
│
|
||||
└── docs/ # 文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 领域模型设计
|
||||
|
||||
### 4.1 实体 (Entities)
|
||||
|
||||
#### 4.1.1 AnalysisTask (分析任务)
|
||||
|
||||
```python
|
||||
# src/domain/entities/analysis_task.py
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
import time
|
||||
import uuid
|
||||
|
||||
class TaskStatus(Enum):
|
||||
PENDING = "pending"
|
||||
FETCHING_MESSAGES = "fetching_messages"
|
||||
ANALYZING = "analyzing"
|
||||
GENERATING_REPORT = "generating_report"
|
||||
SENDING = "sending"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
@dataclass
|
||||
class AnalysisTask:
|
||||
"""
|
||||
分析任务实体 - 聚合根
|
||||
封装单次群聊分析的完整生命周期
|
||||
"""
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
||||
group_id: str = ""
|
||||
platform_id: str = ""
|
||||
trace_id: str = ""
|
||||
status: TaskStatus = TaskStatus.PENDING
|
||||
is_manual: bool = False
|
||||
|
||||
# 时间戳
|
||||
created_at: float = field(default_factory=time.time)
|
||||
started_at: Optional[float] = None
|
||||
completed_at: Optional[float] = None
|
||||
|
||||
# 结果引用
|
||||
result_id: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
# 业务方法
|
||||
def start(self):
|
||||
"""开始执行任务"""
|
||||
if self.status != TaskStatus.PENDING:
|
||||
raise ValueError(f"Cannot start task in {self.status} status")
|
||||
self.status = TaskStatus.FETCHING_MESSAGES
|
||||
self.started_at = time.time()
|
||||
|
||||
def advance_to(self, status: TaskStatus):
|
||||
"""推进任务状态"""
|
||||
self.status = status
|
||||
|
||||
def complete(self, result_id: str):
|
||||
"""完成任务"""
|
||||
self.status = TaskStatus.COMPLETED
|
||||
self.result_id = result_id
|
||||
self.completed_at = time.time()
|
||||
|
||||
def fail(self, error: str):
|
||||
"""标记失败"""
|
||||
self.status = TaskStatus.FAILED
|
||||
self.error_message = error
|
||||
self.completed_at = time.time()
|
||||
|
||||
@property
|
||||
def duration(self) -> Optional[float]:
|
||||
"""任务耗时"""
|
||||
if self.started_at and self.completed_at:
|
||||
return self.completed_at - self.started_at
|
||||
return None
|
||||
```
|
||||
|
||||
#### 4.1.2 GroupAnalysisResult (分析结果)
|
||||
|
||||
```python
|
||||
# src/domain/entities/analysis_result.py
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
import time
|
||||
|
||||
@dataclass
|
||||
class GroupAnalysisResult:
|
||||
"""
|
||||
群分析结果实体
|
||||
聚合所有分析产出
|
||||
"""
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
|
||||
group_id: str = ""
|
||||
trace_id: str = ""
|
||||
|
||||
# 分析结果
|
||||
statistics: Optional['GroupStatistics'] = None
|
||||
topics: List['SummaryTopic'] = field(default_factory=list)
|
||||
user_titles: List['UserTitle'] = field(default_factory=list)
|
||||
golden_quotes: List['GoldenQuote'] = field(default_factory=list)
|
||||
|
||||
# 元数据
|
||||
message_count: int = 0
|
||||
analysis_days: int = 1
|
||||
created_at: float = field(default_factory=time.time)
|
||||
|
||||
# 部分失败记录
|
||||
partial_failures: List[str] = field(default_factory=list)
|
||||
|
||||
def add_partial_failure(self, module: str):
|
||||
"""记录部分失败"""
|
||||
if module not in self.partial_failures:
|
||||
self.partial_failures.append(module)
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
"""检查是否完整"""
|
||||
return len(self.partial_failures) == 0
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为字典(用于报告生成)"""
|
||||
return {
|
||||
"statistics": self.statistics,
|
||||
"topics": self.topics,
|
||||
"user_titles": self.user_titles,
|
||||
"golden_quotes": self.golden_quotes,
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 值对象 (Value Objects)
|
||||
|
||||
当前 `data_models.py` 中的类已经是良好的值对象设计,保持不变:
|
||||
|
||||
- `SummaryTopic` - 话题摘要
|
||||
- `UserTitle` - 用户称号
|
||||
- `GoldenQuote` - 金句
|
||||
- `GroupStatistics` - 群统计
|
||||
- `TokenUsage` - Token 使用量
|
||||
- `EmojiStatistics` - 表情统计
|
||||
- `ActivityVisualization` - 活跃度可视化
|
||||
|
||||
新增值对象:
|
||||
|
||||
```python
|
||||
# src/domain/value_objects/analysis_context.py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AnalysisContext:
|
||||
"""
|
||||
分析上下文值对象
|
||||
封装单次分析的元信息
|
||||
"""
|
||||
trace_id: str
|
||||
group_id: str
|
||||
platform_id: str
|
||||
analysis_days: int
|
||||
is_manual: bool
|
||||
unified_msg_origin: str = ""
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RetryPolicy:
|
||||
"""
|
||||
重试策略值对象
|
||||
"""
|
||||
max_retries: int = 3
|
||||
base_delay: float = 5.0
|
||||
max_delay: float = 60.0
|
||||
exponential_base: float = 2.0
|
||||
jitter_range: tuple = (1.0, 5.0)
|
||||
```
|
||||
|
||||
### 4.3 仓储接口 (Repository Interfaces)
|
||||
|
||||
```python
|
||||
# src/domain/repositories/analysis_history_repository.py
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, List
|
||||
|
||||
class IAnalysisHistoryRepository(ABC):
|
||||
"""分析历史仓储接口"""
|
||||
|
||||
@abstractmethod
|
||||
async def save(self, group_id: str, result: 'GroupAnalysisResult') -> bool:
|
||||
"""保存分析结果"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get(self, group_id: str, date_str: str, time_str: str) -> Optional[dict]:
|
||||
"""获取指定时间的分析结果"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def exists(self, group_id: str, date_str: str, time_str: str) -> bool:
|
||||
"""检查是否存在分析记录"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_by_group(self, group_id: str, limit: int = 10) -> List[dict]:
|
||||
"""列出群的历史分析"""
|
||||
pass
|
||||
|
||||
|
||||
# src/domain/repositories/message_repository.py
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
class IMessageRepository(ABC):
|
||||
"""消息仓储接口"""
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_messages(
|
||||
self,
|
||||
group_id: str,
|
||||
days: int,
|
||||
platform_id: str
|
||||
) -> List[dict]:
|
||||
"""获取群消息"""
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 重构路线图
|
||||
|
||||
### Phase 0: 准备工作 (1 天)
|
||||
|
||||
**目标**: 建立重构基础设施
|
||||
|
||||
1. **创建目录结构**
|
||||
- 按照 3.3 节创建新的目录结构
|
||||
- 保留原有文件,采用渐进式迁移
|
||||
|
||||
2. **定义接口**
|
||||
- 创建 `IAnalysisHistoryRepository` 接口
|
||||
- 创建 `IMessageRepository` 接口
|
||||
- 创建 `IMessageSender` 接口
|
||||
|
||||
3. **添加共享异常**
|
||||
```python
|
||||
# src/shared/exceptions.py
|
||||
class AnalysisError(Exception): pass
|
||||
class MessageFetchError(AnalysisError): pass
|
||||
class LLMError(AnalysisError): pass
|
||||
class ReportGenerationError(AnalysisError): pass
|
||||
class MessageSendError(AnalysisError): pass
|
||||
```
|
||||
|
||||
### Phase 1: 领域层提取 (3-4 天)
|
||||
|
||||
**目标**: 将核心业务逻辑迁移到领域层
|
||||
|
||||
1. **迁移值对象**
|
||||
- 将 `data_models.py` 拆分到 `domain/value_objects/`
|
||||
- 添加 `AnalysisContext` 和 `RetryPolicy`
|
||||
|
||||
2. **创建实体**
|
||||
- 实现 `AnalysisTask` 实体
|
||||
- 实现 `GroupAnalysisResult` 实体
|
||||
|
||||
3. **迁移领域服务**
|
||||
- 将 `analyzers/` 移动到 `domain/services/`
|
||||
- 将 `ActivityVisualizer` 移动到 `domain/services/`
|
||||
- 确保领域服务不依赖基础设施
|
||||
|
||||
**验收标准**:
|
||||
- 领域层代码无外部依赖(除 Python 标准库和 dataclasses)
|
||||
- 所有领域逻辑可独立测试
|
||||
|
||||
### Phase 2: 基础设施层实现 (2-3 天)
|
||||
|
||||
**目标**: 实现仓储和外部集成
|
||||
|
||||
1. **实现仓储**
|
||||
- `KVAnalysisHistoryRepository` 实现 `IAnalysisHistoryRepository`
|
||||
- `OneBotMessageRepository` 实现 `IMessageRepository`
|
||||
|
||||
2. **迁移基础设施组件**
|
||||
- 将 `message_sender.py` 移动到 `infrastructure/messaging/`
|
||||
- 将 `bot_manager.py` 移动到 `infrastructure/bot/`
|
||||
- 将 `config.py` 移动到 `infrastructure/config/`
|
||||
- 将 `resilience.py` 拆分到 `infrastructure/resilience/`
|
||||
|
||||
3. **LLM 客户端封装**
|
||||
- 创建 `LLMClient` 封装 LLM 调用
|
||||
- 集成熔断器和限流器
|
||||
|
||||
**验收标准**:
|
||||
- 基础设施层实现所有仓储接口
|
||||
- 可通过 Mock 替换任意基础设施组件
|
||||
|
||||
### Phase 3: 应用层重构 (2-3 天)
|
||||
|
||||
**目标**: 实现用例编排
|
||||
|
||||
1. **创建应用服务**
|
||||
- `AnalysisOrchestrator`: 编排完整分析流程
|
||||
- `SchedulingService`: 管理定时任务
|
||||
- `ReportingService`: 处理报告生成和发送
|
||||
|
||||
2. **依赖注入**
|
||||
- 应用服务通过构造函数注入仓储接口
|
||||
- 使用接口而非具体实现
|
||||
|
||||
3. **重构 AutoScheduler**
|
||||
- 将业务逻辑委托给 `AnalysisOrchestrator`
|
||||
- AutoScheduler 仅保留调度职责
|
||||
|
||||
**验收标准**:
|
||||
- 应用层仅依赖领域层接口
|
||||
- 可为应用服务编写单元测试
|
||||
|
||||
### Phase 4: 接口层对接 (1-2 天)
|
||||
|
||||
**目标**: 更新入口点
|
||||
|
||||
1. **重构 main.py**
|
||||
- 使用依赖注入组装各层
|
||||
- 命令处理器委托给应用服务
|
||||
|
||||
2. **保持向后兼容**
|
||||
- 确保所有命令功能不变
|
||||
- 配置格式保持兼容
|
||||
|
||||
**验收标准**:
|
||||
- 所有现有功能正常工作
|
||||
- 通过 `ruff check` 和 `ruff format`
|
||||
|
||||
### Phase 5: 测试与文档 (2 天)
|
||||
|
||||
**目标**: 补充测试和更新文档
|
||||
|
||||
1. **单元测试**
|
||||
- 领域实体测试
|
||||
- 领域服务测试
|
||||
- 应用服务测试(Mock 仓储)
|
||||
|
||||
2. **集成测试**
|
||||
- 端到端分析流程测试
|
||||
|
||||
3. **文档更新**
|
||||
- 更新 README
|
||||
- 更新架构文档
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险评估与缓解
|
||||
|
||||
| 风险 | 可能性 | 影响 | 缓解措施 |
|
||||
|------|--------|------|----------|
|
||||
| 重构引入回归 Bug | 中 | 高 | 渐进式迁移,保留原有代码直到验证完成 |
|
||||
| 过度设计 | 中 | 中 | 遵循 YAGNI,仅实现必要的抽象 |
|
||||
| 性能下降 | 低 | 中 | 保持异步架构,避免不必要的对象创建 |
|
||||
| 团队学习曲线 | 中 | 低 | 提供清晰的文档和代码示例 |
|
||||
| 与 AstrBot 框架冲突 | 低 | 高 | 继续遵循 06_review.md 的框架对齐原则 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 不建议实施的方案
|
||||
|
||||
基于 06_review.md 的分析和当前代码状态,以下方案**不建议**实施:
|
||||
|
||||
| 方案 | 原因 |
|
||||
|------|------|
|
||||
| 自建 EventBus | AstrBot 已有事件系统,自建会造成重复 |
|
||||
| 完整 CQRS 模式 | 插件规模不足以支撑 CQRS 复杂度 |
|
||||
| 独立数据库 | 应使用 AstrBot 的 KV 存储 |
|
||||
| 复杂的 Saga 模式 | 当前流程足够简单,不需要分布式事务 |
|
||||
| 领域事件 + 事件溯源 | 过度设计,简单的状态机即可 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 总结与建议
|
||||
|
||||
### 8.1 当前状态
|
||||
|
||||
本插件已经完成了一次成功的务实重构,代码质量显著提升:
|
||||
- ✅ AutoScheduler 精简化
|
||||
- ✅ MessageSender、ReportDispatcher 独立
|
||||
- ✅ TraceContext 链路追踪
|
||||
- ✅ CircuitBreaker + RateLimiter 弹性设计
|
||||
- ✅ 框架 API 对齐
|
||||
|
||||
### 8.2 DDD 重构建议
|
||||
|
||||
**推荐程度**: ⭐⭐⭐ (可选,非必须)
|
||||
|
||||
当前代码已经足够好用。DDD 重构的主要收益是:
|
||||
1. **更好的可测试性** - 通过接口抽象支持 Mock
|
||||
2. **更清晰的边界** - 领域层与基础设施层分离
|
||||
3. **更好的可扩展性** - 新增分析器更容易
|
||||
|
||||
**建议优先级**:
|
||||
1. **P0 (推荐)**: 定义仓储接口,提升可测试性
|
||||
2. **P1 (可选)**: 引入 AnalysisTask 实体,统一状态管理
|
||||
3. **P2 (可选)**: 完整的目录结构重组
|
||||
|
||||
### 8.3 下一步行动
|
||||
|
||||
如果决定进行 DDD 重构:
|
||||
1. 从 Phase 0 开始,先建立接口定义
|
||||
2. 采用渐进式迁移,不要一次性重写
|
||||
3. 每个 Phase 完成后进行功能验证
|
||||
4. 保持与 AstrBot 框架的对齐
|
||||
|
||||
如果决定维持现状:
|
||||
1. 当前架构已足够支撑业务需求
|
||||
2. 可以在需要时逐步引入 DDD 元素
|
||||
3. 重点关注功能迭代而非架构重构
|
||||
|
||||
---
|
||||
|
||||
## 附录 A: 术语表
|
||||
|
||||
| 术语 | 定义 |
|
||||
|------|------|
|
||||
| **限界上下文 (Bounded Context)** | 领域模型的边界,定义了模型的适用范围 |
|
||||
| **实体 (Entity)** | 具有唯一标识的领域对象,生命周期内身份不变 |
|
||||
| **值对象 (Value Object)** | 无唯一标识的领域对象,通过属性值定义 |
|
||||
| **聚合根 (Aggregate Root)** | 聚合的入口点,保证聚合内的一致性 |
|
||||
| **领域服务 (Domain Service)** | 无状态的领域逻辑,不属于任何实体 |
|
||||
| **仓储 (Repository)** | 领域对象的持久化抽象 |
|
||||
| **应用服务 (Application Service)** | 用例编排,协调领域对象完成业务流程 |
|
||||
|
||||
## 附录 B: 参考资料
|
||||
|
||||
1. Eric Evans - *Domain-Driven Design: Tackling Complexity in the Heart of Software*
|
||||
2. Vaughn Vernon - *Implementing Domain-Driven Design*
|
||||
3. 06_review.md - 本项目的务实重构审查报告
|
||||
4. AstrBot 官方文档 - https://astrbot.app/
|
||||
@@ -0,0 +1,968 @@
|
||||
# 08. 跨平台解耦调研报告 (Cross-Platform Decoupling Analysis)
|
||||
|
||||
> **调研日期**: 2026-02-08
|
||||
> **调研范围**: AstrBot 平台抽象层 + 插件 QQ 硬编码分析
|
||||
> **调研目的**: 分析如何将插件从 QQ 专属改造为跨平台通用插件
|
||||
|
||||
---
|
||||
|
||||
## 1. 执行摘要
|
||||
|
||||
### 1.1 当前问题
|
||||
|
||||
本插件 (`astrbot_plugin_qq_group_daily_analysis`) 当前存在严重的平台耦合问题:
|
||||
|
||||
| 问题类型 | 数量 | 影响 |
|
||||
|----------|------|------|
|
||||
| **直接导入 aiocqhttp** | 1 处 | 插件无法在非 QQ 平台加载 |
|
||||
| **AiocqhttpMessageEvent 类型检查** | 12+ 处 | 所有命令仅限 QQ 平台 |
|
||||
| **OneBot API 调用 (call_action)** | 8+ 处 | 消息获取/发送依赖 OneBot |
|
||||
| **QQ 特定数据结构** | 15+ 处 | 消息格式、表情类型等 |
|
||||
| **QQ 号相关逻辑** | 10+ 处 | bot_qq_id、self_id 等 |
|
||||
|
||||
### 1.2 核心发现
|
||||
|
||||
**AstrBot 已提供完善的跨平台抽象层**,支持 12+ 个平台:
|
||||
|
||||
| 平台 | 适配器 | 消息历史支持 |
|
||||
|------|--------|--------------|
|
||||
| QQ (OneBot v11) | `aiocqhttp` | ✅ `get_group_msg_history` |
|
||||
| QQ 官方 | `qqofficial` | ❌ 不支持 |
|
||||
| Telegram | `telegram` | ✅ 可通过 API 获取 |
|
||||
| Discord | `discord` | ✅ 可通过 API 获取 |
|
||||
| Slack | `slack` | ✅ 可通过 API 获取 |
|
||||
| 飞书 | `lark` | ✅ 可通过 API 获取 |
|
||||
| 钉钉 | `dingtalk` | ⚠️ 有限支持 |
|
||||
| 企业微信 | `wecom` | ⚠️ 有限支持 |
|
||||
| Misskey | `misskey` | ✅ 可通过 API 获取 |
|
||||
| Satori | `satori` | 取决于实现 |
|
||||
| 微信公众号 | `weixin_offacc` | ❌ 不支持 |
|
||||
| WebChat | `webchat` | ❌ 不支持 |
|
||||
|
||||
### 1.3 解耦可行性评估
|
||||
|
||||
| 维度 | 评估 | 说明 |
|
||||
|------|------|------|
|
||||
| **技术可行性** | ✅ 高 | AstrBot 已有完善的平台抽象 |
|
||||
| **工作量** | 中等 | 约 3-5 天工作量 |
|
||||
| **风险** | 低 | 渐进式重构,可保持兼容 |
|
||||
| **收益** | 高 | 支持 5+ 主流平台 |
|
||||
|
||||
---
|
||||
|
||||
## 2. AstrBot 平台抽象层分析
|
||||
|
||||
### 2.1 核心架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AstrBot Core │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Platform Abstraction │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
||||
│ │ │ Platform │ │ AstrMessage │ │ MessageType │ │ │
|
||||
│ │ │ (Abstract) │ │ Event │ │ (Enum) │ │ │
|
||||
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
||||
│ │ │ AstrBotMsg │ │ Group │ │MessageMember │ │ │
|
||||
│ │ │ (Model) │ │ (Model) │ │ (Model) │ │ │
|
||||
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Platform Adapters │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||
│ │ │aiocqhttp│ │telegram│ │discord │ │ slack │ │ lark │ │ │
|
||||
│ │ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||
│ │ │dingtalk│ │ wecom │ │misskey │ │ satori │ │ webchat│ │ │
|
||||
│ │ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 核心抽象类
|
||||
|
||||
#### 2.2.1 Platform (平台基类)
|
||||
|
||||
**文件**: `astrbot/core/platform/platform.py`
|
||||
|
||||
```python
|
||||
class Platform(abc.ABC):
|
||||
"""平台适配器基类"""
|
||||
|
||||
def __init__(self, config: dict, event_queue: Queue):
|
||||
self.config = config
|
||||
self._event_queue = event_queue
|
||||
self.client_self_id = uuid.uuid4().hex
|
||||
|
||||
@abc.abstractmethod
|
||||
def run(self) -> Coroutine[Any, Any, None]:
|
||||
"""启动平台"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def meta(self) -> PlatformMetadata:
|
||||
"""获取平台元数据"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def send_by_session(self, session: MessageSesion, message_chain: MessageChain):
|
||||
"""通过会话发送消息(跨平台统一接口)"""
|
||||
pass
|
||||
|
||||
def commit_event(self, event: AstrMessageEvent):
|
||||
"""提交事件到事件队列"""
|
||||
self._event_queue.put_nowait(event)
|
||||
|
||||
def get_client(self):
|
||||
"""获取平台客户端对象"""
|
||||
pass
|
||||
```
|
||||
|
||||
#### 2.2.2 AstrMessageEvent (消息事件基类)
|
||||
|
||||
**文件**: `astrbot/core/platform/astr_message_event.py`
|
||||
|
||||
```python
|
||||
class AstrMessageEvent(abc.ABC):
|
||||
"""统一消息事件基类 - 所有平台事件的父类"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message_str: str,
|
||||
message_obj: AstrBotMessage,
|
||||
platform_meta: PlatformMetadata,
|
||||
session_id: str,
|
||||
):
|
||||
self.message_str = message_str # 纯文本消息
|
||||
self.message_obj = message_obj # 完整消息对象
|
||||
self.platform_meta = platform_meta # 平台元数据
|
||||
self.session = MessageSession(...) # 会话信息
|
||||
|
||||
# 统一的跨平台方法
|
||||
def get_platform_name(self) -> str: # 获取平台类型
|
||||
def get_platform_id(self) -> str: # 获取平台实例ID
|
||||
def get_message_str(self) -> str: # 获取消息文本
|
||||
def get_message_type(self) -> MessageType: # 获取消息类型
|
||||
def get_group_id(self) -> str: # 获取群组ID
|
||||
def get_self_id(self) -> str: # 获取机器人ID
|
||||
def get_sender_id(self) -> str: # 获取发送者ID
|
||||
def get_sender_name(self) -> str: # 获取发送者名称
|
||||
|
||||
# 统一的发送方法
|
||||
async def send(self, message: MessageChain):
|
||||
"""发送消息(由子类实现具体逻辑)"""
|
||||
pass
|
||||
|
||||
async def get_group(self, group_id: str = None) -> Group | None:
|
||||
"""获取群组信息(由支持的平台实现)"""
|
||||
pass
|
||||
```
|
||||
|
||||
#### 2.2.3 AstrBotMessage (统一消息模型)
|
||||
|
||||
**文件**: `astrbot/core/platform/astrbot_message.py`
|
||||
|
||||
```python
|
||||
class AstrBotMessage:
|
||||
"""AstrBot 统一消息对象"""
|
||||
|
||||
type: MessageType # 消息类型 (GROUP_MESSAGE, FRIEND_MESSAGE, OTHER)
|
||||
self_id: str # 机器人ID
|
||||
session_id: str # 会话ID
|
||||
message_id: str # 消息ID
|
||||
group: Group | None # 群组信息
|
||||
sender: MessageMember # 发送者信息
|
||||
message: list[BaseMessageComponent] # 消息链
|
||||
message_str: str # 纯文本消息
|
||||
raw_message: object # 原始消息对象
|
||||
timestamp: int # 时间戳
|
||||
|
||||
class MessageMember:
|
||||
user_id: str # 用户ID (平台无关)
|
||||
nickname: str | None # 昵称
|
||||
|
||||
class Group:
|
||||
group_id: str # 群组ID (平台无关)
|
||||
group_name: str | None # 群名称
|
||||
group_owner: str | None # 群主ID
|
||||
group_admins: list[str] # 管理员ID列表
|
||||
members: list[MessageMember] # 群成员列表
|
||||
```
|
||||
|
||||
#### 2.2.4 MessageType (消息类型枚举)
|
||||
|
||||
**文件**: `astrbot/core/platform/message_type.py`
|
||||
|
||||
```python
|
||||
class MessageType(Enum):
|
||||
GROUP_MESSAGE = "GroupMessage" # 群组消息
|
||||
FRIEND_MESSAGE = "FriendMessage" # 私聊消息
|
||||
OTHER_MESSAGE = "OtherMessage" # 其他消息
|
||||
```
|
||||
|
||||
### 2.3 各平台适配器对比
|
||||
|
||||
| 平台 | 事件类 | 消息获取方法 | 消息发送方法 | 群信息获取 |
|
||||
|------|--------|--------------|--------------|------------|
|
||||
| aiocqhttp | `AiocqhttpMessageEvent` | `call_action("get_group_msg_history")` | `send_group_msg` | `get_group_info` |
|
||||
| telegram | `TelegramPlatformEvent` | `get_chat_history()` | `send_message()` | `get_chat()` |
|
||||
| discord | `DiscordPlatformEvent` | `channel.history()` | `channel.send()` | `get_channel()` |
|
||||
| slack | `SlackMessageEvent` | `conversations_history()` | `chat_postMessage()` | `conversations_info()` |
|
||||
| lark | `LarkMessageEvent` | 飞书 API | 飞书 API | 飞书 API |
|
||||
|
||||
### 2.4 平台检测与适配模式
|
||||
|
||||
**正确的跨平台写法**:
|
||||
|
||||
```python
|
||||
# ✅ 推荐:使用基类 AstrMessageEvent
|
||||
from astrbot.api.event import AstrMessageEvent
|
||||
|
||||
@filter.command("分析")
|
||||
async def analyze(self, event: AstrMessageEvent):
|
||||
# 使用统一接口
|
||||
group_id = event.get_group_id()
|
||||
platform = event.get_platform_name()
|
||||
|
||||
# 根据平台选择策略
|
||||
if platform == "aiocqhttp":
|
||||
messages = await self._fetch_qq_messages(event)
|
||||
elif platform == "telegram":
|
||||
messages = await self._fetch_telegram_messages(event)
|
||||
elif platform == "discord":
|
||||
messages = await self._fetch_discord_messages(event)
|
||||
else:
|
||||
yield event.plain_result(f"❌ 平台 {platform} 暂不支持消息历史获取")
|
||||
return
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 插件 QQ 硬编码清单
|
||||
|
||||
### 3.1 硬编码分类汇总
|
||||
|
||||
| 类别 | 文件 | 行数 | 严重程度 | 解耦难度 |
|
||||
|------|------|------|----------|----------|
|
||||
| 直接导入 aiocqhttp | `main.py` | 14-16 | 🔴 高 | 低 |
|
||||
| 类型检查 AiocqhttpMessageEvent | `main.py` | 多处 | 🔴 高 | 低 |
|
||||
| OneBot API 调用 | `message_handler.py` | 90-110 | 🔴 高 | 中 |
|
||||
| OneBot API 调用 | `auto_scheduler.py` | 85-90 | 🔴 高 | 中 |
|
||||
| QQ 表情类型处理 | `message_handler.py` | 206-256 | 🟡 中 | 中 |
|
||||
| QQ 号相关逻辑 | `bot_manager.py` | 多处 | 🟡 中 | 低 |
|
||||
| 消息格式假设 | `message_handler.py` | 133-147 | 🟡 中 | 中 |
|
||||
| 错误码检查 | 多文件 | 多处 | 🟢 低 | 低 |
|
||||
|
||||
### 3.2 详细硬编码清单
|
||||
|
||||
#### 3.2.1 main.py - 入口文件
|
||||
|
||||
```python
|
||||
# 🔴 硬编码 1: 直接导入 aiocqhttp 事件类
|
||||
# 行 14-16
|
||||
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import (
|
||||
AiocqhttpMessageEvent,
|
||||
)
|
||||
|
||||
# 🔴 硬编码 2-13: 所有命令都限制 QQ 平台
|
||||
# 行 122, 128-129
|
||||
async def analyze_group_daily(self, event: AiocqhttpMessageEvent, ...):
|
||||
if not isinstance(event, AiocqhttpMessageEvent):
|
||||
yield event.plain_result("❌ 此功能仅支持QQ群聊")
|
||||
return
|
||||
|
||||
# 同样的模式在以下命令中重复:
|
||||
# - set_output_format (行 330, 336-338)
|
||||
# - set_report_template (行 377, 383-385)
|
||||
# - view_templates (行 447, 452-454)
|
||||
# - install_pdf_deps (行 533, 538-540)
|
||||
# - analysis_settings (行 556, 567-569)
|
||||
|
||||
# 🔴 硬编码 14: 直接调用 OneBot API 发送消息
|
||||
# 行 218-225
|
||||
if hasattr(bot_instance, "api") and hasattr(bot_instance.api, "call_action"):
|
||||
await bot_instance.api.call_action(
|
||||
"send_group_msg",
|
||||
group_id=int(group_id),
|
||||
message=message_chain,
|
||||
)
|
||||
```
|
||||
|
||||
#### 3.2.2 message_handler.py - 消息处理
|
||||
|
||||
```python
|
||||
# 🔴 硬编码 15: QQ 号提取逻辑
|
||||
# 行 40-48
|
||||
def _extract_bot_qq_id_from_instance(self, bot_instance):
|
||||
"""从bot实例中提取QQ号(单个)"""
|
||||
if hasattr(bot_instance, "self_id") and bot_instance.self_id:
|
||||
return str(bot_instance.self_id)
|
||||
elif hasattr(bot_instance, "qq") and bot_instance.qq:
|
||||
return str(bot_instance.qq)
|
||||
...
|
||||
|
||||
# 🔴 硬编码 16: OneBot API 调用获取消息历史
|
||||
# 行 90-111
|
||||
if hasattr(bot_instance, "call_action"):
|
||||
result = await bot_instance.call_action(
|
||||
"get_group_msg_history", **payloads
|
||||
)
|
||||
elif hasattr(bot_instance, "api"):
|
||||
# QQ 官方 bot (botClient) 不支持历史消息
|
||||
logger.error("检测到 QQ 官方 Bot,官方 API 不支持获取历史消息")
|
||||
return []
|
||||
|
||||
# 🟡 硬编码 17: QQ 消息格式假设
|
||||
# 行 124-147
|
||||
round_messages = result.get("messages", [])
|
||||
for msg in round_messages:
|
||||
msg_time = datetime.fromtimestamp(msg.get("time", 0))
|
||||
sender_id = str(msg.get("sender", {}).get("user_id", ""))
|
||||
|
||||
# 🟡 硬编码 18-22: QQ 特定表情类型处理
|
||||
# 行 206-256
|
||||
elif content.get("type") == "face": # QQ基础表情
|
||||
emoji_statistics.face_count += 1
|
||||
elif content.get("type") == "mface": # 动画表情/魔法表情
|
||||
emoji_statistics.mface_count += 1
|
||||
elif content.get("type") == "bface": # 超级表情
|
||||
emoji_statistics.bface_count += 1
|
||||
elif content.get("type") == "sface": # 小表情
|
||||
emoji_statistics.sface_count += 1
|
||||
```
|
||||
|
||||
#### 3.2.3 auto_scheduler.py - 自动调度
|
||||
|
||||
```python
|
||||
# 🔴 硬编码 23: OneBot API 调用获取群信息
|
||||
# 行 85-92
|
||||
if hasattr(bot_instance, "call_action"):
|
||||
result = await bot_instance.call_action(
|
||||
"get_group_info", group_id=int(group_id)
|
||||
)
|
||||
|
||||
# 🟡 硬编码 24: OneBot 错误码检查
|
||||
# 行 100-107
|
||||
if "retcode=1200" in error_msg or "消息undefined不存在" in error_msg:
|
||||
logger.warning(f"群 {group_id} 机器人不在此群中")
|
||||
|
||||
# 🔴 硬编码 25: OneBot API 获取群列表
|
||||
# 行 478-479
|
||||
result = await call_action_func("get_group_list")
|
||||
```
|
||||
|
||||
#### 3.2.4 bot_manager.py - Bot 管理
|
||||
|
||||
```python
|
||||
# 🟡 硬编码 26-28: QQ 号相关属性和方法
|
||||
# 行 18, 39-47, 80-82
|
||||
self._bot_qq_ids = [] # 命名暗示 QQ 专属
|
||||
|
||||
def set_bot_qq_ids(self, bot_qq_ids):
|
||||
"""设置bot QQ号(支持单个QQ号或QQ号列表)"""
|
||||
|
||||
def has_bot_qq_id(self) -> bool:
|
||||
"""检查是否有配置的bot QQ号"""
|
||||
|
||||
# 🟡 硬编码 29: 平台检查硬编码
|
||||
# 行 151-154
|
||||
if hasattr(event, "get_platform_name") and event.get_platform_name() != "aiocqhttp":
|
||||
return False
|
||||
```
|
||||
|
||||
#### 3.2.5 message_sender.py - 消息发送
|
||||
|
||||
```python
|
||||
# 🔴 硬编码 30-32: OneBot API 调用发送消息
|
||||
# 行 38-40, 73-75, 117-119
|
||||
await bot.api.call_action("send_group_msg", group_id=group_id, message=...)
|
||||
```
|
||||
|
||||
#### 3.2.6 retry.py - 重试管理
|
||||
|
||||
```python
|
||||
# 🔴 硬编码 33-34: OneBot API 调用
|
||||
# 行 192-206
|
||||
if hasattr(bot, "api") and hasattr(bot.api, "call_action"):
|
||||
result = await bot.api.call_action(
|
||||
"send_group_msg", group_id=int(task.group_id), message=message
|
||||
)
|
||||
|
||||
# 行 296-304
|
||||
await bot.api.call_action(
|
||||
"send_group_forward_msg",
|
||||
group_id=int(task.group_id),
|
||||
messages=nodes,
|
||||
)
|
||||
```
|
||||
|
||||
### 3.3 硬编码影响分析
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 硬编码影响链 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ main.py │
|
||||
│ └── import AiocqhttpMessageEvent ──────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ 所有命令处理器 │ │
|
||||
│ (12个命令全部限制 QQ) │ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ message_handler.py │ │
|
||||
│ └── call_action("get_group_msg_history") ──────────────────┤ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ auto_scheduler.py │ │
|
||||
│ └── call_action("get_group_info") ─────────────────────────┤ │
|
||||
│ └── call_action("get_group_list") ─────────────────────────┤ │
|
||||
│ │ │ │
|
||||
│ ▼ │ │
|
||||
│ message_sender.py / retry.py │ │
|
||||
│ └── call_action("send_group_msg") ─────────────────────────┘ │
|
||||
│ └── call_action("send_group_forward_msg") │
|
||||
│ │
|
||||
│ 结果: 插件完全无法在非 QQ 平台使用 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 跨平台解耦方案
|
||||
|
||||
### 4.1 设计目标
|
||||
|
||||
1. **平台无关的核心逻辑** - 分析、报告生成与平台解耦
|
||||
2. **可插拔的平台适配器** - 每个平台独立的消息获取/发送实现
|
||||
3. **渐进式迁移** - 保持 QQ 功能完整,逐步添加其他平台
|
||||
4. **统一的接口抽象** - 定义清晰的平台能力接口
|
||||
|
||||
### 4.2 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Plugin Architecture (目标) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Application Layer │ │
|
||||
│ │ main.py - 使用 AstrMessageEvent 基类 │ │
|
||||
│ │ - 命令处理器接受所有平台事件 │ │
|
||||
│ │ - 根据平台能力选择处理策略 │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Domain Layer │ │
|
||||
│ │ - MessageAnalyzer (平台无关) │ │
|
||||
│ │ - ReportGenerator (平台无关) │ │
|
||||
│ │ - LLMAnalyzer (平台无关) │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Platform Abstraction Layer │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ IPlatformMessageRepository (Interface) │ │ │
|
||||
│ │ │ - fetch_messages(group_id, days) -> List[Msg] │ │ │
|
||||
│ │ │ - get_group_info(group_id) -> GroupInfo │ │ │
|
||||
│ │ │ - get_group_list() -> List[str] │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ IPlatformMessageSender (Interface) │ │ │
|
||||
│ │ │ - send_text(group_id, text) -> bool │ │ │
|
||||
│ │ │ - send_image(group_id, image) -> bool │ │ │
|
||||
│ │ │ - send_file(group_id, file) -> bool │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ PlatformCapabilities (Value Object) │ │ │
|
||||
│ │ │ - supports_message_history: bool │ │ │
|
||||
│ │ │ - supports_group_list: bool │ │ │
|
||||
│ │ │ - supports_file_upload: bool │ │ │
|
||||
│ │ │ - supports_forward_message: bool │ │ │
|
||||
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Platform Implementations │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
|
||||
│ │ │ OneBot │ │ Telegram │ │ Discord │ ... │ │
|
||||
│ │ │ Adapter │ │ Adapter │ │ Adapter │ │ │
|
||||
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.3 接口定义
|
||||
|
||||
#### 4.3.1 平台消息仓储接口
|
||||
|
||||
```python
|
||||
# src/platform/interfaces.py
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
@dataclass
|
||||
class UnifiedMessage:
|
||||
"""统一消息格式"""
|
||||
message_id: str
|
||||
sender_id: str
|
||||
sender_name: str
|
||||
content: str # 纯文本内容
|
||||
raw_content: list # 原始消息链
|
||||
timestamp: int
|
||||
message_type: str # text, image, file, etc.
|
||||
|
||||
@dataclass
|
||||
class UnifiedGroup:
|
||||
"""统一群组格式"""
|
||||
group_id: str
|
||||
group_name: str
|
||||
member_count: int
|
||||
owner_id: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class PlatformCapabilities:
|
||||
"""平台能力描述"""
|
||||
platform_name: str
|
||||
supports_message_history: bool = False
|
||||
supports_group_list: bool = False
|
||||
supports_group_info: bool = False
|
||||
supports_file_upload: bool = False
|
||||
supports_forward_message: bool = False
|
||||
max_message_history_days: int = 0
|
||||
|
||||
class IPlatformMessageRepository(ABC):
|
||||
"""平台消息仓储接口"""
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_messages(
|
||||
self,
|
||||
group_id: str,
|
||||
days: int,
|
||||
max_count: int = 1000
|
||||
) -> List[UnifiedMessage]:
|
||||
"""获取群消息历史"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_group_info(self, group_id: str) -> Optional[UnifiedGroup]:
|
||||
"""获取群信息"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_group_list(self) -> List[str]:
|
||||
"""获取群列表"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_capabilities(self) -> PlatformCapabilities:
|
||||
"""获取平台能力"""
|
||||
pass
|
||||
|
||||
class IPlatformMessageSender(ABC):
|
||||
"""平台消息发送接口"""
|
||||
|
||||
@abstractmethod
|
||||
async def send_text(self, group_id: str, text: str) -> bool:
|
||||
"""发送文本消息"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def send_image(self, group_id: str, image_url: str) -> bool:
|
||||
"""发送图片消息"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def send_file(self, group_id: str, file_path: str) -> bool:
|
||||
"""发送文件"""
|
||||
pass
|
||||
```
|
||||
|
||||
#### 4.3.2 OneBot 实现示例
|
||||
|
||||
```python
|
||||
# src/platform/adapters/onebot_adapter.py
|
||||
from ..interfaces import (
|
||||
IPlatformMessageRepository,
|
||||
IPlatformMessageSender,
|
||||
UnifiedMessage,
|
||||
UnifiedGroup,
|
||||
PlatformCapabilities,
|
||||
)
|
||||
|
||||
class OneBotMessageRepository(IPlatformMessageRepository):
|
||||
"""OneBot v11 消息仓储实现"""
|
||||
|
||||
def __init__(self, bot_instance):
|
||||
self.bot = bot_instance
|
||||
|
||||
async def fetch_messages(
|
||||
self, group_id: str, days: int, max_count: int = 1000
|
||||
) -> List[UnifiedMessage]:
|
||||
"""通过 get_group_msg_history 获取消息"""
|
||||
if not hasattr(self.bot, "call_action"):
|
||||
return []
|
||||
|
||||
try:
|
||||
result = await self.bot.call_action(
|
||||
"get_group_msg_history",
|
||||
group_id=int(group_id),
|
||||
count=max_count,
|
||||
)
|
||||
|
||||
messages = []
|
||||
for msg in result.get("messages", []):
|
||||
# 转换为统一格式
|
||||
unified = self._convert_message(msg)
|
||||
if unified:
|
||||
messages.append(unified)
|
||||
return messages
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"OneBot fetch_messages failed: {e}")
|
||||
return []
|
||||
|
||||
def _convert_message(self, raw_msg: dict) -> UnifiedMessage:
|
||||
"""将 OneBot 消息转换为统一格式"""
|
||||
sender = raw_msg.get("sender", {})
|
||||
|
||||
# 提取纯文本内容
|
||||
text_parts = []
|
||||
for seg in raw_msg.get("message", []):
|
||||
if seg.get("type") == "text":
|
||||
text_parts.append(seg.get("data", {}).get("text", ""))
|
||||
|
||||
return UnifiedMessage(
|
||||
message_id=str(raw_msg.get("message_id", "")),
|
||||
sender_id=str(sender.get("user_id", "")),
|
||||
sender_name=sender.get("nickname", "") or sender.get("card", ""),
|
||||
content="".join(text_parts),
|
||||
raw_content=raw_msg.get("message", []),
|
||||
timestamp=raw_msg.get("time", 0),
|
||||
message_type="mixed",
|
||||
)
|
||||
|
||||
def get_capabilities(self) -> PlatformCapabilities:
|
||||
return PlatformCapabilities(
|
||||
platform_name="onebot",
|
||||
supports_message_history=True,
|
||||
supports_group_list=True,
|
||||
supports_group_info=True,
|
||||
supports_file_upload=True,
|
||||
supports_forward_message=True,
|
||||
max_message_history_days=7,
|
||||
)
|
||||
```
|
||||
|
||||
#### 4.3.3 Telegram 实现示例
|
||||
|
||||
```python
|
||||
# src/platform/adapters/telegram_adapter.py
|
||||
class TelegramMessageRepository(IPlatformMessageRepository):
|
||||
"""Telegram 消息仓储实现"""
|
||||
|
||||
def __init__(self, bot_client):
|
||||
self.bot = bot_client
|
||||
|
||||
async def fetch_messages(
|
||||
self, group_id: str, days: int, max_count: int = 1000
|
||||
) -> List[UnifiedMessage]:
|
||||
"""通过 Telegram API 获取消息历史"""
|
||||
try:
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Telegram 使用 chat_id
|
||||
chat_id = int(group_id)
|
||||
|
||||
# 获取消息历史 (需要 bot 有读取历史的权限)
|
||||
messages = []
|
||||
async for message in self.bot.get_chat_history(
|
||||
chat_id=chat_id,
|
||||
limit=max_count,
|
||||
):
|
||||
# 过滤时间范围
|
||||
msg_time = message.date
|
||||
if msg_time < datetime.now() - timedelta(days=days):
|
||||
break
|
||||
|
||||
unified = self._convert_message(message)
|
||||
if unified:
|
||||
messages.append(unified)
|
||||
|
||||
return messages
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Telegram fetch_messages failed: {e}")
|
||||
return []
|
||||
|
||||
def get_capabilities(self) -> PlatformCapabilities:
|
||||
return PlatformCapabilities(
|
||||
platform_name="telegram",
|
||||
supports_message_history=True,
|
||||
supports_group_list=True,
|
||||
supports_group_info=True,
|
||||
supports_file_upload=True,
|
||||
supports_forward_message=True,
|
||||
max_message_history_days=30,
|
||||
)
|
||||
```
|
||||
|
||||
### 4.4 平台适配器工厂
|
||||
|
||||
```python
|
||||
# src/platform/factory.py
|
||||
from typing import Optional
|
||||
from .interfaces import IPlatformMessageRepository, IPlatformMessageSender
|
||||
from .adapters.onebot_adapter import OneBotMessageRepository, OneBotMessageSender
|
||||
from .adapters.telegram_adapter import TelegramMessageRepository, TelegramMessageSender
|
||||
from .adapters.discord_adapter import DiscordMessageRepository, DiscordMessageSender
|
||||
|
||||
class PlatformAdapterFactory:
|
||||
"""平台适配器工厂"""
|
||||
|
||||
@staticmethod
|
||||
def create_repository(
|
||||
platform_name: str,
|
||||
bot_instance
|
||||
) -> Optional[IPlatformMessageRepository]:
|
||||
"""根据平台类型创建消息仓储"""
|
||||
|
||||
adapters = {
|
||||
"aiocqhttp": OneBotMessageRepository,
|
||||
"telegram": TelegramMessageRepository,
|
||||
"discord": DiscordMessageRepository,
|
||||
"slack": SlackMessageRepository,
|
||||
"lark": LarkMessageRepository,
|
||||
}
|
||||
|
||||
adapter_class = adapters.get(platform_name)
|
||||
if adapter_class:
|
||||
return adapter_class(bot_instance)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def create_sender(
|
||||
platform_name: str,
|
||||
bot_instance
|
||||
) -> Optional[IPlatformMessageSender]:
|
||||
"""根据平台类型创建消息发送器"""
|
||||
|
||||
senders = {
|
||||
"aiocqhttp": OneBotMessageSender,
|
||||
"telegram": TelegramMessageSender,
|
||||
"discord": DiscordMessageSender,
|
||||
"slack": SlackMessageSender,
|
||||
"lark": LarkMessageSender,
|
||||
}
|
||||
|
||||
sender_class = senders.get(platform_name)
|
||||
if sender_class:
|
||||
return sender_class(bot_instance)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_supported_platforms() -> list[str]:
|
||||
"""获取支持的平台列表"""
|
||||
return ["aiocqhttp", "telegram", "discord", "slack", "lark"]
|
||||
```
|
||||
|
||||
### 4.5 重构后的命令处理器
|
||||
|
||||
```python
|
||||
# main.py (重构后)
|
||||
from astrbot.api.event import AstrMessageEvent # 使用基类
|
||||
|
||||
class GroupDailyAnalysis(Star): # 改名,去掉 QQ 前缀
|
||||
|
||||
@filter.command("群分析")
|
||||
@filter.permission_type(PermissionType.ADMIN)
|
||||
async def analyze_group_daily(
|
||||
self, event: AstrMessageEvent, days: int | None = None # 使用基类
|
||||
):
|
||||
"""分析群聊日常活动 - 跨平台支持"""
|
||||
|
||||
group_id = event.get_group_id()
|
||||
if not group_id:
|
||||
yield event.plain_result("❌ 请在群聊中使用此命令")
|
||||
return
|
||||
|
||||
platform_name = event.get_platform_name()
|
||||
|
||||
# 获取平台适配器
|
||||
repository = self._get_repository_for_platform(platform_name, event)
|
||||
if not repository:
|
||||
yield event.plain_result(f"❌ 平台 {platform_name} 暂不支持此功能")
|
||||
return
|
||||
|
||||
# 检查平台能力
|
||||
capabilities = repository.get_capabilities()
|
||||
if not capabilities.supports_message_history:
|
||||
yield event.plain_result(
|
||||
f"❌ 平台 {platform_name} 不支持获取消息历史"
|
||||
)
|
||||
return
|
||||
|
||||
# 使用统一接口获取消息
|
||||
messages = await repository.fetch_messages(group_id, days or 1)
|
||||
|
||||
if not messages:
|
||||
yield event.plain_result("❌ 未找到足够的消息记录")
|
||||
return
|
||||
|
||||
# 后续分析逻辑不变...
|
||||
yield event.plain_result(f"📊 已获取 {len(messages)} 条消息,正在分析...")
|
||||
|
||||
# 分析和报告生成使用统一的消息格式
|
||||
analysis_result = await self.message_analyzer.analyze_unified_messages(
|
||||
messages, group_id, event.unified_msg_origin
|
||||
)
|
||||
|
||||
# 发送报告
|
||||
await self._send_report(event, analysis_result)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 执行路线图
|
||||
|
||||
### Phase 0: 准备工作 (0.5 天)
|
||||
|
||||
**目标**: 建立基础设施
|
||||
|
||||
| 任务 | 说明 |
|
||||
|------|------|
|
||||
| 创建 `src/platform/` 目录 | 平台抽象层 |
|
||||
| 定义接口文件 | `interfaces.py` |
|
||||
| 创建工厂类 | `factory.py` |
|
||||
|
||||
### Phase 1: OneBot 适配器 (1 天)
|
||||
|
||||
**目标**: 将现有 QQ 逻辑封装为适配器
|
||||
|
||||
| 任务 | 说明 |
|
||||
|------|------|
|
||||
| 实现 `OneBotMessageRepository` | 封装 `get_group_msg_history` |
|
||||
| 实现 `OneBotMessageSender` | 封装消息发送 |
|
||||
| 添加消息格式转换 | OneBot → UnifiedMessage |
|
||||
| 单元测试 | 确保功能不变 |
|
||||
|
||||
### Phase 2: 核心逻辑解耦 (1 天)
|
||||
|
||||
**目标**: 使核心逻辑平台无关
|
||||
|
||||
| 任务 | 说明 |
|
||||
|------|------|
|
||||
| 修改 `MessageHandler` | 使用 `UnifiedMessage` |
|
||||
| 修改 `MessageAnalyzer` | 移除平台假设 |
|
||||
| 修改 `AutoScheduler` | 使用适配器工厂 |
|
||||
| 修改 `BotManager` | 重命名 QQ 相关方法 |
|
||||
|
||||
### Phase 3: main.py 重构 (0.5 天)
|
||||
|
||||
**目标**: 使命令处理器跨平台
|
||||
|
||||
| 任务 | 说明 |
|
||||
|------|------|
|
||||
| 移除 `AiocqhttpMessageEvent` 导入 | 使用基类 |
|
||||
| 移除类型检查 | 改用能力检查 |
|
||||
| 添加平台适配器选择逻辑 | 根据 `platform_name` |
|
||||
| 更新错误消息 | 更通用的提示 |
|
||||
|
||||
### Phase 4: 添加 Telegram 支持 (1 天)
|
||||
|
||||
**目标**: 验证跨平台架构
|
||||
|
||||
| 任务 | 说明 |
|
||||
|------|------|
|
||||
| 实现 `TelegramMessageRepository` | 使用 python-telegram-bot |
|
||||
| 实现 `TelegramMessageSender` | |
|
||||
| 测试 Telegram 群分析 | 端到端验证 |
|
||||
|
||||
### Phase 5: 添加更多平台 (可选)
|
||||
|
||||
| 平台 | 优先级 | 工作量 |
|
||||
|------|--------|--------|
|
||||
| Discord | P1 | 1 天 |
|
||||
| Slack | P2 | 1 天 |
|
||||
| 飞书 | P2 | 1 天 |
|
||||
| 钉钉 | P3 | 1 天 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与缓解
|
||||
|
||||
| 风险 | 可能性 | 影响 | 缓解措施 |
|
||||
|------|--------|------|----------|
|
||||
| 平台 API 差异大 | 高 | 中 | 统一消息格式 + 能力检查 |
|
||||
| 消息历史获取受限 | 高 | 高 | 明确标注平台能力,提供降级方案 |
|
||||
| 表情/特殊消息处理 | 中 | 低 | 只提取文本内容进行分析 |
|
||||
| 测试覆盖不足 | 中 | 中 | 为每个适配器编写集成测试 |
|
||||
| 性能差异 | 低 | 低 | 异步处理 + 缓存 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 总结
|
||||
|
||||
### 7.1 关键发现
|
||||
|
||||
1. **AstrBot 已有完善的平台抽象** - 不需要自建抽象层
|
||||
2. **插件硬编码严重但可解耦** - 约 34 处需要修改
|
||||
3. **核心分析逻辑平台无关** - LLM 分析、报告生成不受影响
|
||||
4. **渐进式迁移可行** - 可以保持 QQ 功能同时添加新平台
|
||||
|
||||
### 7.2 建议优先级
|
||||
|
||||
| 优先级 | 任务 | 收益 |
|
||||
|--------|------|------|
|
||||
| **P0** | 定义平台抽象接口 | 架构基础 |
|
||||
| **P0** | 封装 OneBot 适配器 | 保持现有功能 |
|
||||
| **P1** | 重构 main.py 使用基类 | 解除平台限制 |
|
||||
| **P1** | 添加 Telegram 支持 | 验证架构 |
|
||||
| **P2** | 添加 Discord 支持 | 扩大用户群 |
|
||||
|
||||
### 7.3 预期成果
|
||||
|
||||
- ✅ 插件可在 5+ 主流平台运行
|
||||
- ✅ 新增平台只需实现适配器接口
|
||||
- ✅ 核心逻辑无需修改
|
||||
- ✅ 保持与 AstrBot 框架的对齐
|
||||
|
||||
---
|
||||
|
||||
## 附录 A: 平台 API 对比
|
||||
|
||||
| 功能 | OneBot v11 | Telegram | Discord | Slack |
|
||||
|------|------------|----------|---------|-------|
|
||||
| 获取消息历史 | `get_group_msg_history` | `get_chat_history` | `channel.history()` | `conversations.history` |
|
||||
| 获取群信息 | `get_group_info` | `get_chat` | `get_channel` | `conversations.info` |
|
||||
| 获取群列表 | `get_group_list` | `get_my_commands` | `guilds` | `conversations.list` |
|
||||
| 发送文本 | `send_group_msg` | `send_message` | `channel.send` | `chat.postMessage` |
|
||||
| 发送图片 | `[CQ:image]` | `send_photo` | `channel.send(file=)` | `files.upload` |
|
||||
| 发送文件 | `[CQ:file]` | `send_document` | `channel.send(file=)` | `files.upload` |
|
||||
| 转发消息 | `send_group_forward_msg` | N/A | N/A | N/A |
|
||||
|
||||
## 附录 B: 参考资料
|
||||
|
||||
1. AstrBot 官方文档 - https://astrbot.app/
|
||||
2. OneBot v11 标准 - https://github.com/botuniverse/onebot-11
|
||||
3. python-telegram-bot - https://python-telegram-bot.org/
|
||||
4. Pycord (Discord) - https://pycord.dev/
|
||||
5. Slack SDK - https://slack.dev/python-slack-sdk/
|
||||
@@ -0,0 +1,796 @@
|
||||
# 09. DDD + 跨平台重构完整方案 (DDD & Cross-Platform Refactoring Complete Guide)
|
||||
|
||||
> **文档日期**: 2026-02-08
|
||||
> **版本**: v1.0
|
||||
> **整合文档**: 07_ddd_refactoring_analysis.md + 08_cross_platform_decoupling_analysis.md
|
||||
> **目的**: 提供完整的 DDD 重构方案,同时解决跨平台解耦问题
|
||||
|
||||
---
|
||||
|
||||
## 1. 执行摘要
|
||||
|
||||
### 1.1 问题分析
|
||||
|
||||
本插件存在两个核心问题需要同时解决:
|
||||
|
||||
| 问题 | 现状 | 影响 |
|
||||
|------|------|------|
|
||||
| **平台耦合** | 34 处 QQ 硬编码 | 完全无法在其他平台使用 |
|
||||
| **架构分层混乱** | 业务逻辑与基础设施混合 | 测试困难,维护成本高 |
|
||||
|
||||
### 1.2 解决方案概述
|
||||
|
||||
采用 **DDD 分层架构 + 平台适配器模式**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Interface Layer (main.py) │
|
||||
│ - 使用 AstrMessageEvent 基类,不再限制 QQ 平台 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ - AnalysisOrchestrator: 分析流程编排 │
|
||||
│ - SchedulingService: 定时任务管理 │
|
||||
│ - ReportingService: 报告生成与分发 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Domain Layer (平台无关) │
|
||||
│ - Entities: AnalysisTask, GroupAnalysisResult │
|
||||
│ - Value Objects: UnifiedMessage, PlatformCapabilities │
|
||||
│ - Domain Services: TopicAnalyzer, StatisticsCalculator, etc. │
|
||||
│ - Repository Interfaces: IMessageRepository, IMessageSender │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Infrastructure Layer │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Platform Adapters (实现 Repository Interfaces) │ │
|
||||
│ │ - OneBotAdapter (QQ) │ │
|
||||
│ │ - TelegramAdapter │ │
|
||||
│ │ - DiscordAdapter │ │
|
||||
│ │ - SlackAdapter │ │
|
||||
│ │ - LarkAdapter (飞书) │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ - LLMClient, ConfigManager, CircuitBreaker, etc. │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.3 预期收益
|
||||
|
||||
| 收益 | 量化 |
|
||||
|------|------|
|
||||
| 跨平台支持 | 从 1 个平台扩展到 5+ 个平台 |
|
||||
| 可测试性 | 单元测试覆盖率可达 80%+ |
|
||||
| 可维护性 | 新增平台只需 1 天工作量 |
|
||||
| 可扩展性 | 新增分析器只需实现接口 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 限界上下文 (Bounded Contexts)
|
||||
|
||||
### 2.1 上下文划分
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 群聊日常分析插件 (Plugin) │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ 调度上下文 │ │ 分析上下文 │ │ 报告上下文 │ │ 平台上下文 │ │
|
||||
│ │ (Scheduling)│ │ (Analysis) │ │ (Reporting) │ │ (Platform) │ │
|
||||
│ │ │ │ │ │ │ │ [核心] │ │
|
||||
│ │ - 定时任务 │ │ - 话题分析 │ │ - 报告生成 │ │ - 消息获取 │ │
|
||||
│ │ - 并发控制 │ │ - 用户画像 │ │ - 格式转换 │ │ - 消息发送 │ │
|
||||
│ │ - 任务编排 │ │ - 金句提取 │ │ - 重试机制 │ │ - 群组信息 │ │
|
||||
│ │ │ │ - 统计计算 │ │ │ │ - 平台能力 │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ └────────────────┴────────────────┴────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────────┴─────────────────────────────────┐ │
|
||||
│ │ 共享内核 (Shared Kernel) │ │
|
||||
│ │ - ConfigManager: 配置管理 │ │
|
||||
│ │ - TraceContext: 链路追踪 │ │
|
||||
│ │ - CircuitBreaker, RateLimiter: 弹性组件 │ │
|
||||
│ │ - UnifiedMessage: 统一消息模型 [跨平台核心] │ │
|
||||
│ │ - PlatformCapabilities: 平台能力描述 [跨平台核心] │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 上下文职责
|
||||
|
||||
| 上下文 | 职责 | 关键组件 |
|
||||
|--------|------|----------|
|
||||
| **调度** | 定时任务、并发控制、任务编排 | SchedulingService, AutoScheduler |
|
||||
| **分析** | 话题分析、用户画像、金句提取、统计 | TopicAnalyzer, UserTitleAnalyzer, StatisticsCalculator |
|
||||
| **报告** | 报告生成、格式转换、发送、重试 | ReportGenerator, ReportDispatcher, RetryManager |
|
||||
| **平台** | 消息获取、消息发送、群组信息 | PlatformAdapter, IMessageRepository, IMessageSender |
|
||||
|
||||
---
|
||||
|
||||
## 3. 领域模型设计
|
||||
|
||||
### 3.1 统一消息模型 (UnifiedMessage)
|
||||
|
||||
**这是跨平台的核心抽象**,所有平台的消息都会被转换为此格式:
|
||||
|
||||
```python
|
||||
# src/domain/value_objects/unified_message.py
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any
|
||||
from enum import Enum
|
||||
|
||||
class MessageContentType(Enum):
|
||||
TEXT = "text"
|
||||
IMAGE = "image"
|
||||
FILE = "file"
|
||||
EMOJI = "emoji"
|
||||
REPLY = "reply"
|
||||
FORWARD = "forward"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MessageContent:
|
||||
"""消息内容值对象"""
|
||||
type: MessageContentType
|
||||
text: str = ""
|
||||
url: str = ""
|
||||
emoji_id: str = ""
|
||||
raw_data: Any = None # 保留原始数据用于调试
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UnifiedMessage:
|
||||
"""
|
||||
统一消息格式 - 跨平台核心值对象
|
||||
|
||||
设计原则:
|
||||
1. 只保留分析需要的字段
|
||||
2. 使用平台无关的类型
|
||||
3. 不可变 (frozen=True)
|
||||
"""
|
||||
message_id: str
|
||||
sender_id: str
|
||||
sender_name: str
|
||||
group_id: str
|
||||
text_content: str # 纯文本,用于 LLM 分析
|
||||
contents: tuple[MessageContent, ...] # 完整消息链
|
||||
timestamp: int
|
||||
platform: str # 来源平台标识
|
||||
reply_to: Optional[str] = None
|
||||
|
||||
def has_text(self) -> bool:
|
||||
return bool(self.text_content.strip())
|
||||
|
||||
def get_emoji_count(self) -> int:
|
||||
return sum(1 for c in self.contents if c.type == MessageContentType.EMOJI)
|
||||
```
|
||||
|
||||
### 3.2 平台能力描述 (PlatformCapabilities)
|
||||
|
||||
```python
|
||||
# src/domain/value_objects/platform_capabilities.py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlatformCapabilities:
|
||||
"""
|
||||
平台能力描述 - 用于运行时决策
|
||||
|
||||
每个平台适配器必须声明自己的能力,
|
||||
应用层根据能力决定是否可以执行某些操作。
|
||||
"""
|
||||
platform_name: str
|
||||
|
||||
# 消息获取能力
|
||||
supports_message_history: bool = False
|
||||
max_message_history_days: int = 0
|
||||
max_message_count: int = 0
|
||||
|
||||
# 群组信息能力
|
||||
supports_group_list: bool = False
|
||||
supports_group_info: bool = False
|
||||
supports_member_list: bool = False
|
||||
|
||||
# 消息发送能力
|
||||
supports_text_message: bool = True
|
||||
supports_image_message: bool = True
|
||||
supports_file_message: bool = False
|
||||
supports_forward_message: bool = False
|
||||
|
||||
def can_analyze(self) -> bool:
|
||||
"""是否支持群聊分析"""
|
||||
return self.supports_message_history and self.max_message_history_days > 0
|
||||
|
||||
|
||||
# 预定义平台能力
|
||||
ONEBOT_CAPABILITIES = PlatformCapabilities(
|
||||
platform_name="onebot",
|
||||
supports_message_history=True, max_message_history_days=7, max_message_count=10000,
|
||||
supports_group_list=True, supports_group_info=True, supports_member_list=True,
|
||||
supports_text_message=True, supports_image_message=True,
|
||||
supports_file_message=True, supports_forward_message=True,
|
||||
)
|
||||
|
||||
TELEGRAM_CAPABILITIES = PlatformCapabilities(
|
||||
platform_name="telegram",
|
||||
supports_message_history=True, max_message_history_days=30, max_message_count=10000,
|
||||
supports_group_list=True, supports_group_info=True, supports_member_list=True,
|
||||
supports_text_message=True, supports_image_message=True, supports_file_message=True,
|
||||
)
|
||||
|
||||
DISCORD_CAPABILITIES = PlatformCapabilities(
|
||||
platform_name="discord",
|
||||
supports_message_history=True, max_message_history_days=30, max_message_count=10000,
|
||||
supports_group_list=True, supports_group_info=True, supports_member_list=True,
|
||||
supports_text_message=True, supports_image_message=True, supports_file_message=True,
|
||||
)
|
||||
```
|
||||
|
||||
### 3.3 仓储接口
|
||||
|
||||
```python
|
||||
# src/domain/repositories/message_repository.py
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
from ..value_objects.unified_message import UnifiedMessage
|
||||
from ..value_objects.platform_capabilities import PlatformCapabilities
|
||||
|
||||
class IMessageRepository(ABC):
|
||||
"""消息仓储接口 - 每个平台适配器必须实现"""
|
||||
|
||||
@abstractmethod
|
||||
async def fetch_messages(
|
||||
self, group_id: str, days: int, max_count: int = 1000
|
||||
) -> List[UnifiedMessage]:
|
||||
"""获取群组历史消息,返回统一格式"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_capabilities(self) -> PlatformCapabilities:
|
||||
"""获取平台能力描述"""
|
||||
pass
|
||||
|
||||
|
||||
# src/domain/repositories/message_sender.py
|
||||
class IMessageSender(ABC):
|
||||
"""消息发送接口"""
|
||||
|
||||
@abstractmethod
|
||||
async def send_text(self, group_id: str, text: str) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def send_image(self, group_id: str, image_url: str, caption: str = "") -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def send_file(self, group_id: str, file_path: str) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
# src/domain/repositories/group_info_repository.py
|
||||
@dataclass
|
||||
class UnifiedGroup:
|
||||
group_id: str
|
||||
group_name: str
|
||||
member_count: int
|
||||
owner_id: Optional[str] = None
|
||||
|
||||
class IGroupInfoRepository(ABC):
|
||||
"""群组信息仓储接口"""
|
||||
|
||||
@abstractmethod
|
||||
async def get_group_info(self, group_id: str) -> Optional[UnifiedGroup]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_group_list(self) -> List[str]:
|
||||
pass
|
||||
```
|
||||
|
||||
### 3.4 实体设计
|
||||
|
||||
```python
|
||||
# src/domain/entities/analysis_task.py
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
import time, uuid
|
||||
|
||||
class TaskStatus(Enum):
|
||||
PENDING = "pending"
|
||||
CHECKING_PLATFORM = "checking_platform"
|
||||
FETCHING_MESSAGES = "fetching_messages"
|
||||
ANALYZING = "analyzing"
|
||||
GENERATING_REPORT = "generating_report"
|
||||
SENDING = "sending"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
UNSUPPORTED_PLATFORM = "unsupported_platform"
|
||||
|
||||
@dataclass
|
||||
class AnalysisTask:
|
||||
"""分析任务实体 - 聚合根"""
|
||||
id: str = field(default_factory=lambda: uuid.uuid4().hex[:8])
|
||||
group_id: str = ""
|
||||
platform_name: str = ""
|
||||
trace_id: str = ""
|
||||
status: TaskStatus = TaskStatus.PENDING
|
||||
is_manual: bool = False
|
||||
created_at: float = field(default_factory=time.time)
|
||||
started_at: Optional[float] = None
|
||||
completed_at: Optional[float] = None
|
||||
result_id: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
def start(self, capabilities: PlatformCapabilities) -> bool:
|
||||
"""开始任务,验证平台能力"""
|
||||
if not capabilities.can_analyze():
|
||||
self.status = TaskStatus.UNSUPPORTED_PLATFORM
|
||||
self.error_message = f"Platform {capabilities.platform_name} does not support analysis"
|
||||
return False
|
||||
self.status = TaskStatus.FETCHING_MESSAGES
|
||||
self.started_at = time.time()
|
||||
return True
|
||||
|
||||
def complete(self, result_id: str):
|
||||
self.status = TaskStatus.COMPLETED
|
||||
self.result_id = result_id
|
||||
self.completed_at = time.time()
|
||||
|
||||
def fail(self, error: str):
|
||||
self.status = TaskStatus.FAILED
|
||||
self.error_message = error
|
||||
self.completed_at = time.time()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 平台适配器设计
|
||||
|
||||
### 4.1 适配器架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PlatformAdapter (Base) │
|
||||
│ - message_repository: IMessageRepository │
|
||||
│ - message_sender: IMessageSender │
|
||||
│ - group_info_repository: IGroupInfoRepository │
|
||||
│ - capabilities: PlatformCapabilities │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
△
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
│ │ │
|
||||
┌───────┴───────┐ ┌─────────┴─────────┐ ┌───────┴───────┐
|
||||
│ OneBotAdapter │ │ TelegramAdapter │ │ DiscordAdapter │
|
||||
│ │ │ │ │ │
|
||||
│ - call_action │ │ - python-telegram │ │ - pycord │
|
||||
│ - OneBot v11 │ │ -bot API │ │ - Discord API │
|
||||
└───────────────┘ └───────────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
### 4.2 OneBot 适配器实现
|
||||
|
||||
```python
|
||||
# src/infrastructure/platform/adapters/onebot_adapter.py
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional, Any
|
||||
from astrbot.api import logger
|
||||
|
||||
from ....domain.repositories.message_repository import IMessageRepository
|
||||
from ....domain.value_objects.unified_message import UnifiedMessage, MessageContent, MessageContentType
|
||||
from ....domain.value_objects.platform_capabilities import PlatformCapabilities, ONEBOT_CAPABILITIES
|
||||
|
||||
class OneBotMessageRepository(IMessageRepository):
|
||||
"""OneBot v11 消息仓储实现"""
|
||||
|
||||
def __init__(self, bot_instance: Any, bot_self_ids: List[str] = None):
|
||||
self.bot = bot_instance
|
||||
self.bot_self_ids = bot_self_ids or []
|
||||
|
||||
async def fetch_messages(self, group_id: str, days: int, max_count: int = 1000) -> List[UnifiedMessage]:
|
||||
if not hasattr(self.bot, "call_action"):
|
||||
return []
|
||||
|
||||
try:
|
||||
result = await self.bot.call_action(
|
||||
"get_group_msg_history",
|
||||
group_id=int(group_id),
|
||||
count=max_count,
|
||||
)
|
||||
|
||||
if not result or "messages" not in result:
|
||||
return []
|
||||
|
||||
end_time = datetime.now()
|
||||
start_time = end_time - timedelta(days=days)
|
||||
|
||||
messages = []
|
||||
for raw_msg in result.get("messages", []):
|
||||
msg_time = datetime.fromtimestamp(raw_msg.get("time", 0))
|
||||
if not (start_time <= msg_time <= end_time):
|
||||
continue
|
||||
|
||||
sender_id = str(raw_msg.get("sender", {}).get("user_id", ""))
|
||||
if sender_id in self.bot_self_ids:
|
||||
continue
|
||||
|
||||
unified = self._convert_message(raw_msg, group_id)
|
||||
if unified:
|
||||
messages.append(unified)
|
||||
|
||||
return messages
|
||||
|
||||
except Exception as e:
|
||||
if "retcode=1200" in str(e):
|
||||
logger.warning(f"Bot not in group {group_id}")
|
||||
else:
|
||||
logger.error(f"OneBot fetch_messages failed: {e}")
|
||||
return []
|
||||
|
||||
def _convert_message(self, raw_msg: dict, group_id: str) -> Optional[UnifiedMessage]:
|
||||
"""将 OneBot 消息转换为统一格式"""
|
||||
try:
|
||||
sender = raw_msg.get("sender", {})
|
||||
message_chain = raw_msg.get("message", [])
|
||||
|
||||
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", "")))
|
||||
elif seg_type in ("face", "mface", "bface", "sface"):
|
||||
contents.append(MessageContent(type=MessageContentType.EMOJI, emoji_id=str(seg_data.get("id", ""))))
|
||||
else:
|
||||
contents.append(MessageContent(type=MessageContentType.UNKNOWN, raw_data=seg))
|
||||
|
||||
return UnifiedMessage(
|
||||
message_id=str(raw_msg.get("message_id", "")),
|
||||
sender_id=str(sender.get("user_id", "")),
|
||||
sender_name=sender.get("nickname", "") or sender.get("card", ""),
|
||||
group_id=group_id,
|
||||
text_content="".join(text_parts),
|
||||
contents=tuple(contents),
|
||||
timestamp=raw_msg.get("time", 0),
|
||||
platform="onebot",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to convert message: {e}")
|
||||
return None
|
||||
|
||||
def get_capabilities(self) -> PlatformCapabilities:
|
||||
return ONEBOT_CAPABILITIES
|
||||
```
|
||||
|
||||
### 4.3 适配器工厂
|
||||
|
||||
```python
|
||||
# src/infrastructure/platform/factory.py
|
||||
from typing import Optional, Any
|
||||
|
||||
class PlatformAdapterFactory:
|
||||
"""平台适配器工厂"""
|
||||
|
||||
_adapters = {
|
||||
"aiocqhttp": OneBotAdapter,
|
||||
# "telegram": TelegramAdapter,
|
||||
# "discord": DiscordAdapter,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create(cls, platform_name: str, bot_instance: Any, config: dict = None) -> Optional[PlatformAdapter]:
|
||||
adapter_class = cls._adapters.get(platform_name)
|
||||
if adapter_class is None:
|
||||
return None
|
||||
return adapter_class(bot_instance, config)
|
||||
|
||||
@classmethod
|
||||
def get_supported_platforms(cls) -> list[str]:
|
||||
return list(cls._adapters.keys())
|
||||
|
||||
@classmethod
|
||||
def is_supported(cls, platform_name: str) -> bool:
|
||||
return platform_name in cls._adapters
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 应用层设计
|
||||
|
||||
### 5.1 分析编排器
|
||||
|
||||
```python
|
||||
# src/application/analysis_orchestrator.py
|
||||
from typing import Optional
|
||||
import asyncio
|
||||
from astrbot.api import logger
|
||||
|
||||
from ..domain.entities.analysis_task import AnalysisTask, TaskStatus
|
||||
from ..domain.entities.analysis_result import GroupAnalysisResult
|
||||
from ..domain.repositories.message_repository import IMessageRepository
|
||||
|
||||
class AnalysisOrchestrator:
|
||||
"""
|
||||
分析流程编排器 - 应用层核心
|
||||
|
||||
职责:协调消息获取、分析、报告生成
|
||||
特点:平台无关,通过仓储接口与基础设施层交互
|
||||
"""
|
||||
|
||||
def __init__(self, config_manager, llm_client, history_repository):
|
||||
self.config_manager = config_manager
|
||||
self.llm_client = llm_client
|
||||
self.history_repository = history_repository
|
||||
|
||||
# 领域服务
|
||||
self.statistics_calculator = StatisticsCalculator()
|
||||
self.topic_analyzer = TopicAnalyzer(llm_client)
|
||||
self.user_title_analyzer = UserTitleAnalyzer(llm_client)
|
||||
self.golden_quote_analyzer = GoldenQuoteAnalyzer(llm_client)
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
task: AnalysisTask,
|
||||
message_repository: IMessageRepository,
|
||||
unified_msg_origin: str = "",
|
||||
) -> Optional[GroupAnalysisResult]:
|
||||
"""执行分析任务"""
|
||||
try:
|
||||
# 1. 验证平台能力
|
||||
capabilities = message_repository.get_capabilities()
|
||||
if not task.start(capabilities):
|
||||
logger.warning(f"Platform {capabilities.platform_name} not supported")
|
||||
return None
|
||||
|
||||
# 2. 获取消息
|
||||
days = self.config_manager.get_analysis_days()
|
||||
max_count = self.config_manager.get_max_messages()
|
||||
|
||||
messages = await message_repository.fetch_messages(task.group_id, days, max_count)
|
||||
|
||||
if not messages:
|
||||
task.fail("No messages found")
|
||||
return None
|
||||
|
||||
min_threshold = self.config_manager.get_min_messages_threshold()
|
||||
if len(messages) < min_threshold:
|
||||
task.fail(f"Not enough messages: {len(messages)} < {min_threshold}")
|
||||
return None
|
||||
|
||||
# 3. 执行分析
|
||||
task.advance_to(TaskStatus.ANALYZING)
|
||||
|
||||
result = GroupAnalysisResult(
|
||||
group_id=task.group_id,
|
||||
trace_id=task.trace_id,
|
||||
message_count=len(messages),
|
||||
)
|
||||
|
||||
# 统计计算 (本地)
|
||||
result.statistics = self.statistics_calculator.calculate(messages)
|
||||
|
||||
# LLM 分析 (并行)
|
||||
topics, titles, quotes = await asyncio.gather(
|
||||
self.topic_analyzer.analyze(messages, unified_msg_origin),
|
||||
self.user_title_analyzer.analyze(messages, unified_msg_origin),
|
||||
self.golden_quote_analyzer.analyze(messages, unified_msg_origin),
|
||||
return_exceptions=True
|
||||
)
|
||||
|
||||
# 处理结果
|
||||
if not isinstance(topics, Exception):
|
||||
result.topics = topics
|
||||
if not isinstance(titles, Exception):
|
||||
result.user_titles = titles
|
||||
if not isinstance(quotes, Exception):
|
||||
result.golden_quotes = quotes
|
||||
|
||||
# 4. 保存并完成
|
||||
await self.history_repository.save(task.group_id, result)
|
||||
task.complete(result.id)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
task.fail(str(e))
|
||||
logger.error(f"Analysis failed: {e}", exc_info=True)
|
||||
return None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 目标目录结构
|
||||
|
||||
```
|
||||
astrbot_plugin_group_daily_analysis/ # 去掉 QQ 前缀
|
||||
├── main.py # Interface Layer
|
||||
├── src/
|
||||
│ ├── application/ # Application Layer
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── analysis_orchestrator.py # 分析流程编排
|
||||
│ │ ├── scheduling_service.py # 定时任务服务
|
||||
│ │ └── reporting_service.py # 报告服务
|
||||
│ │
|
||||
│ ├── domain/ # Domain Layer
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── entities/
|
||||
│ │ │ ├── analysis_task.py
|
||||
│ │ │ └── analysis_result.py
|
||||
│ │ ├── value_objects/
|
||||
│ │ │ ├── unified_message.py # 统一消息格式
|
||||
│ │ │ ├── platform_capabilities.py # 平台能力
|
||||
│ │ │ ├── topic.py, user_title.py, etc.
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── topic_analyzer.py
|
||||
│ │ │ ├── statistics_calculator.py
|
||||
│ │ │ └── report_generator.py
|
||||
│ │ └── repositories/
|
||||
│ │ ├── message_repository.py # IMessageRepository
|
||||
│ │ ├── message_sender.py # IMessageSender
|
||||
│ │ └── group_info_repository.py # IGroupInfoRepository
|
||||
│ │
|
||||
│ ├── infrastructure/ # Infrastructure Layer
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── platform/
|
||||
│ │ │ ├── factory.py # 适配器工厂
|
||||
│ │ │ ├── base.py # 适配器基类
|
||||
│ │ │ └── adapters/
|
||||
│ │ │ ├── onebot_adapter.py # OneBot (QQ)
|
||||
│ │ │ ├── telegram_adapter.py
|
||||
│ │ │ ├── discord_adapter.py
|
||||
│ │ │ └── slack_adapter.py
|
||||
│ │ ├── persistence/
|
||||
│ │ ├── llm/
|
||||
│ │ ├── config/
|
||||
│ │ └── resilience/
|
||||
│ │
|
||||
│ └── shared/
|
||||
│ ├── exceptions.py
|
||||
│ └── constants.py
|
||||
│
|
||||
├── tests/
|
||||
└── docs/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 重构路线图
|
||||
|
||||
### 总览
|
||||
|
||||
| Phase | 内容 | 工作量 | 优先级 |
|
||||
|-------|------|--------|--------|
|
||||
| Phase 0 | 准备工作:目录结构、接口定义 | 1 天 | P0 |
|
||||
| Phase 1 | 平台适配器:OneBot 实现 | 2-3 天 | P0 |
|
||||
| Phase 2 | 领域层:值对象、实体、服务 | 2-3 天 | P0 |
|
||||
| Phase 3 | 应用层:编排器、服务 | 2-3 天 | P0 |
|
||||
| Phase 4 | 接口层:main.py 重构 | 1-2 天 | P0 |
|
||||
| Phase 5 | 新平台:Telegram、Discord | 每平台 1 天 | P1 |
|
||||
| Phase 6 | 测试与文档 | 2 天 | P1 |
|
||||
| **总计** | | **12-16 天** | |
|
||||
|
||||
### Phase 0: 准备工作 (1 天)
|
||||
|
||||
```bash
|
||||
# 创建目录结构
|
||||
mkdir -p src/{application,domain/{entities,value_objects,services,repositories},infrastructure/{platform/adapters,persistence,llm,config,resilience},shared}
|
||||
```
|
||||
|
||||
**任务清单**:
|
||||
- [ ] 创建目录结构
|
||||
- [ ] 定义 UnifiedMessage 值对象
|
||||
- [ ] 定义 PlatformCapabilities 值对象
|
||||
- [ ] 定义 IMessageRepository 接口
|
||||
- [ ] 定义 IMessageSender 接口
|
||||
- [ ] 定义 IGroupInfoRepository 接口
|
||||
- [ ] 定义共享异常类
|
||||
|
||||
### Phase 1: 平台适配器 (2-3 天)
|
||||
|
||||
**任务清单**:
|
||||
- [ ] 实现 OneBotMessageRepository
|
||||
- [ ] 实现 OneBotMessageSender
|
||||
- [ ] 实现 OneBotGroupInfoRepository
|
||||
- [ ] 实现 OneBotAdapter (组合)
|
||||
- [ ] 实现 PlatformAdapterFactory
|
||||
- [ ] 编写单元测试
|
||||
|
||||
### Phase 2: 领域层重构 (2-3 天)
|
||||
|
||||
**任务清单**:
|
||||
- [ ] 迁移 data_models.py 到 value_objects/
|
||||
- [ ] 实现 AnalysisTask 实体
|
||||
- [ ] 实现 GroupAnalysisResult 实体
|
||||
- [ ] 重构分析器使用 UnifiedMessage
|
||||
- [ ] 重构 StatisticsCalculator
|
||||
|
||||
### Phase 3: 应用层重构 (2-3 天)
|
||||
|
||||
**任务清单**:
|
||||
- [ ] 实现 AnalysisOrchestrator
|
||||
- [ ] 实现 SchedulingService
|
||||
- [ ] 实现 ReportingService
|
||||
- [ ] 重构 AutoScheduler
|
||||
|
||||
### Phase 4: 接口层重构 (1-2 天)
|
||||
|
||||
**任务清单**:
|
||||
- [ ] 移除 AiocqhttpMessageEvent 导入
|
||||
- [ ] 使用 AstrMessageEvent 基类
|
||||
- [ ] 添加平台适配器选择逻辑
|
||||
- [ ] 更新错误消息
|
||||
|
||||
### Phase 5: 新平台支持 (每平台 1 天)
|
||||
|
||||
**Telegram 适配器**:
|
||||
- [ ] TelegramMessageRepository
|
||||
- [ ] TelegramMessageSender
|
||||
- [ ] TelegramAdapter
|
||||
|
||||
**Discord 适配器**:
|
||||
- [ ] DiscordMessageRepository
|
||||
- [ ] DiscordMessageSender
|
||||
- [ ] DiscordAdapter
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险与缓解
|
||||
|
||||
| 风险 | 可能性 | 影响 | 缓解措施 |
|
||||
|------|--------|------|----------|
|
||||
| 重构引入回归 | 中 | 高 | 渐进式迁移,保留原代码 |
|
||||
| 平台 API 差异 | 高 | 中 | 统一消息格式 + 能力检查 |
|
||||
| 消息历史受限 | 高 | 高 | 明确能力声明,提供降级 |
|
||||
| 过度设计 | 中 | 中 | YAGNI 原则 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 总结
|
||||
|
||||
### 9.1 核心设计决策
|
||||
|
||||
1. **UnifiedMessage** - 跨平台消息抽象,领域层只处理此格式
|
||||
2. **PlatformCapabilities** - 平台能力声明,运行时决策
|
||||
3. **IMessageRepository** - 消息获取抽象,每个平台实现
|
||||
4. **PlatformAdapterFactory** - 适配器工厂,统一创建入口
|
||||
|
||||
### 9.2 收益总结
|
||||
|
||||
| 维度 | 重构前 | 重构后 |
|
||||
|------|--------|--------|
|
||||
| 平台支持 | 仅 QQ | QQ + Telegram + Discord + ... |
|
||||
| 可测试性 | 困难 | 80%+ 覆盖率 |
|
||||
| 新增平台 | N/A | 1 天 |
|
||||
| 新增分析器 | 需要了解 QQ API | 只需实现接口 |
|
||||
|
||||
### 9.3 参考文档
|
||||
|
||||
- 07_ddd_refactoring_analysis.md - 原 DDD 分析
|
||||
- 08_cross_platform_decoupling_analysis.md - 跨平台调研
|
||||
- 06_review.md - 务实重构审查
|
||||
|
||||
---
|
||||
|
||||
## 附录: 平台能力对比
|
||||
|
||||
| 功能 | OneBot | Telegram | Discord | Slack | 飞书 |
|
||||
|------|--------|----------|---------|-------|------|
|
||||
| 消息历史 | ✅ 7天 | ✅ 30天 | ✅ 30天 | ✅ 30天 | ✅ 30天 |
|
||||
| 群列表 | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 发送图片 | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 发送文件 | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 转发消息 | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| 消息回应 | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user