mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
docs: update documentation and project structure
This commit is contained in:
+32
@@ -43,3 +43,35 @@ src/utils/__pycache__/pdf_utils.cpython-311.pyc
|
|||||||
src/visualization/__pycache__/__init__.cpython-311.pyc
|
src/visualization/__pycache__/__init__.cpython-311.pyc
|
||||||
src/visualization/__pycache__/activity_charts.cpython-311.pyc
|
src/visualization/__pycache__/activity_charts.cpython-311.pyc
|
||||||
src/scheduler/__pycache__/retry.cpython-311.pyc
|
src/scheduler/__pycache__/retry.cpython-311.pyc
|
||||||
|
src/utils/__pycache__/__init__.cpython-312.pyc
|
||||||
|
src/utils/__pycache__/helpers.cpython-312.pyc
|
||||||
|
src/utils/__pycache__/resilience.cpython-311.pyc
|
||||||
|
src/utils/__pycache__/trace_context.cpython-311.pyc
|
||||||
|
src/analysis/__pycache__/__init__.cpython-312.pyc
|
||||||
|
src/analysis/__pycache__/llm_analyzer.cpython-312.pyc
|
||||||
|
src/analysis/__pycache__/statistics.cpython-312.pyc
|
||||||
|
src/analysis/analyzers/__pycache__/__init__.cpython-312.pyc
|
||||||
|
src/analysis/analyzers/__pycache__/base_analyzer.cpython-312.pyc
|
||||||
|
src/analysis/analyzers/__pycache__/golden_quote_analyzer.cpython-312.pyc
|
||||||
|
src/analysis/analyzers/__pycache__/topic_analyzer.cpython-312.pyc
|
||||||
|
src/analysis/analyzers/__pycache__/user_title_analyzer.cpython-312.pyc
|
||||||
|
src/analysis/utils/__pycache__/__init__.cpython-312.pyc
|
||||||
|
src/analysis/utils/__pycache__/info_utils.cpython-312.pyc
|
||||||
|
src/analysis/utils/__pycache__/json_utils.cpython-312.pyc
|
||||||
|
src/analysis/utils/__pycache__/llm_utils.cpython-312.pyc
|
||||||
|
src/core/__pycache__/__init__.cpython-312.pyc
|
||||||
|
src/core/__pycache__/config.cpython-312.pyc
|
||||||
|
src/core/__pycache__/message_handler.cpython-312.pyc
|
||||||
|
src/core/__pycache__/message_sender.cpython-312.pyc
|
||||||
|
src/reports/__pycache__/dispatcher.cpython-312.pyc
|
||||||
|
src/scheduler/__pycache__/__init__.cpython-312.pyc
|
||||||
|
src/scheduler/__pycache__/auto_scheduler.cpython-312.pyc
|
||||||
|
src/utils/__pycache__/pdf_utils.cpython-312.pyc
|
||||||
|
src/utils/__pycache__/resilience.cpython-312.pyc
|
||||||
|
src/utils/__pycache__/trace_context.cpython-312.pyc
|
||||||
|
data/cmd_config.json
|
||||||
|
data/t2i_templates/astrbot_powershell.html
|
||||||
|
data/t2i_templates/base.html
|
||||||
|
__pycache__/main.cpython-312.pyc
|
||||||
|
src/core/__pycache__/message_sender.cpython-311.pyc
|
||||||
|
src/reports/__pycache__/dispatcher.cpython-311.pyc
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# 01. 需求分析与现状评估 (Pragmatic Refactoring Edition)
|
||||||
|
|
||||||
|
> **注**: 本文档基于 `06_review.md` 的反馈进行了大幅修正,从"理想化DDD重构"转向"务实框架对齐重构"。
|
||||||
|
|
||||||
|
## 1. 当前现状与实际问题
|
||||||
|
|
||||||
|
经过对代码和 AstrBot 框架的深度对比分析,我们重新定义了核心问题:
|
||||||
|
|
||||||
|
### 1.1 核心架构冲突
|
||||||
|
- **重复造轮子**: 插件内部实现了简陋的定时循环 (`_scheduler_loop`),而忽视了框架提供的 `Context.task_scheduler` (APScheduler)。
|
||||||
|
- **绕过抽象层**: 大量直接调用 `bot.api.call_action`,导致代码与 OneBot v11 协议强耦合,未利用 AstrBot 的 `Context.send_message` 和 `PlatformManager` 抽象。
|
||||||
|
- **上帝类 (God Class)**: `AutoScheduler` (1000+行) 确实职责过重,但问题不在于缺乏 EventBus,而在于缺乏合理的模块拆分(如 `MessageSender`, `ReportDispatcher`)。
|
||||||
|
|
||||||
|
### 1.2 稳定性痛点 (Confirmed)
|
||||||
|
- **多群并发冲击**: 确实存在,但通过 `asyncio.Semaphore` 已有基础控制,缺的是**全局速率限制**。
|
||||||
|
- **LLM 可靠性**: 现有代码已实现多 Provider 和重试,但缺乏**熔断机制 (Circuit Breaker)**,导致单点故障可能拖累整体。
|
||||||
|
- **发送可靠性**: 图片发送失败是高频问题,虽然有 URL/Base64 降级,但逻辑重复散落在各处。
|
||||||
|
|
||||||
|
### 1.3 可观测性缺失
|
||||||
|
- **日志混乱**: 多群并发分析时,日志交织在一起,无法通过 TraceID 串联单次分析的全过程。
|
||||||
|
|
||||||
|
## 2. 重构目标 (Pragmatic Goals)
|
||||||
|
|
||||||
|
本次重构的核心原则是:**回归框架,做减法,补短板**。
|
||||||
|
|
||||||
|
### 2.1 架构对齐 (Alignment)
|
||||||
|
- **废弃**自建的定时循环,转用 `Context.task_scheduler`。
|
||||||
|
- **废弃**直接的 API 调用,尽可能使用 `Context.send_message` 和 `StarTools`。
|
||||||
|
- **利用**框架生命周期钩子 (`OnPlatformLoaded`) 替代硬编码的 `sleep(30)`。
|
||||||
|
|
||||||
|
### 2.2 职责拆分 (Refactoring)
|
||||||
|
不是引入新架构,而是将 `AutoScheduler` 的代码剥离到独立模块:
|
||||||
|
- **`MessageSender`**: 统一处理文本、图片、PDF、合并转发发送,封装 URL->Base64 降级逻辑。
|
||||||
|
- **`ReportDispatcher`**: 负责协调 分析 -> 生成 -> 发送 的流程。
|
||||||
|
- **`BotManager`**: 增强群组发现和 Session 管理能力。
|
||||||
|
|
||||||
|
### 2.3 稳定性增强 (Robustness)
|
||||||
|
- **TraceID**: 使用 `contextvars` 实现零侵入的链路追踪。
|
||||||
|
- **Circuit Breaker**: 在 LLM 调用层增加简单的熔断器(失败计数+冷却)。
|
||||||
|
- **Global Rate Limit**: 全局控制并发请求数。
|
||||||
|
|
||||||
|
## 3. 预期收益
|
||||||
|
- **代码量减少**: 预计减少 ~30% 冗余代码 (主要是 Scheduler 和 Message 发送逻辑)。
|
||||||
|
- **稳定性提升**: 消除 API 超频风险,提升网络抖动时的恢复能力。
|
||||||
|
- **维护性提升**: 遵循框架规范,降低后续 AstrBot 升级带来的兼容性风险。
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# 02. 架构设计 (Architecture Design - Framework Aligned)
|
||||||
|
|
||||||
|
## 1. 总体架构图 (Pragmatic Architecture)
|
||||||
|
|
||||||
|
本架构旨在最大限度复用 AstrBot 框架能力,通过职责拆分(而非分层解耦)来降低 `AutoScheduler` 的复杂度。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "AstrBot Framework (宿主环境)"
|
||||||
|
TaskScheduler["Context.task_scheduler\n(APScheduler)"]
|
||||||
|
Context["Context\n(Session/Event/PlatformManager)"]
|
||||||
|
StarTools["StarTools\n(Message/Image)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Plugin Core (核心逻辑)"
|
||||||
|
Bootstrap["Plugin Bootstrap\n(main.py)"]
|
||||||
|
SchedulerJob["Scheduler Job\n(定时任务回调)"]
|
||||||
|
|
||||||
|
AnalysisOrchestrator["Analysis Orchestrator\n(原有逻辑拆分)"]
|
||||||
|
|
||||||
|
subgraph "Extracted Modules (提取模块)"
|
||||||
|
MessageSender["Message Sender\n(统一发送+降级)"]
|
||||||
|
ReportDispatcher["Report Dispatcher\n(报告生成+分发)"]
|
||||||
|
BotManagerEnh["Bot Manager\n(Session管理+群发现)"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Infrastructure Enhancements (基建增强)"
|
||||||
|
TraceContext["Trace Context\n(contextvars)"]
|
||||||
|
LLMClient["LLM Client\n(CircuitBreaker + RateLimit)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
%% Flow
|
||||||
|
Bootstrap -->|Register Job| TaskScheduler
|
||||||
|
TaskScheduler -->|Trigger| SchedulerJob
|
||||||
|
|
||||||
|
SchedulerJob -->|Set TraceID| TraceContext
|
||||||
|
SchedulerJob -->|Invoke| AnalysisOrchestrator
|
||||||
|
|
||||||
|
AnalysisOrchestrator -->|Get Groups| BotManagerEnh
|
||||||
|
BotManagerEnh -->|Query| Context
|
||||||
|
|
||||||
|
AnalysisOrchestrator -->|Analyze| LLMClient
|
||||||
|
|
||||||
|
AnalysisOrchestrator -->|Generate Report| ReportDispatcher
|
||||||
|
ReportDispatcher -->|Send Report| MessageSender
|
||||||
|
|
||||||
|
MessageSender -->|Use| StarTools
|
||||||
|
MessageSender -->|Fallback| Context
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 核心组件详解
|
||||||
|
|
||||||
|
### 2.1 任务调度 (Scheduler)
|
||||||
|
**不再自建循环**。直接使用 AstrBot 提供的 `Context.task_scheduler` (APScheduler 实例)。
|
||||||
|
- **注册**: 在 `__init__` 或 `OnPlatformLoaded` 中注册 cron job。
|
||||||
|
- **优势**: 自动处理时区、任务持久化(如果配置)、优雅关闭。
|
||||||
|
|
||||||
|
### 2.2 消息发送 (MessageSender)
|
||||||
|
**统一发送入口**。将散落在各处的发送逻辑收敛到 `src/core/message_sender.py`。
|
||||||
|
- **职责**:
|
||||||
|
1. **协议适配**: 优先构建 `AstrMessageEvent` (即使是主动发送,也可构造虚拟 Event),调用 `Context.send_message`。
|
||||||
|
2. **降级策略**: URL发送失败 -> 下载转Base64发送 -> 纯文本回退。
|
||||||
|
3. **合并转发**: 封装 OneBot v11 的 Forward Message 构建细节。
|
||||||
|
- **依赖**: 依赖 `Context` 和 `StarTools`,而非直接依赖 `bot.api`。
|
||||||
|
|
||||||
|
### 2.3 分析编排 (AnalysisOrchestrator / ReportDispatcher)
|
||||||
|
**逻辑拆分**。
|
||||||
|
- `ReportDispatcher`: 接收 `AnalysisResult`,决定调用哪个渲染器(HTML/Text/PDF),并调用 `MessageSender` 发送。
|
||||||
|
- `AnalysisOrchestrator`: 负责并发控制(Semaphore)和错误处理(Partial Failure)。
|
||||||
|
|
||||||
|
### 2.4 可观测性 (TraceContext)
|
||||||
|
**零侵入追踪**。
|
||||||
|
- 使用 `contextvars.ContextVar` 存储 `trace_id`。
|
||||||
|
- 实现 `TraceLogFilter` 自动注入 logging record。
|
||||||
|
- 效果:在 `SchedulerJob` 入口设置一次 ID,后续深层调用的所有 `logger.info` 自动带上 `[trace_id: xxx]`。
|
||||||
|
|
||||||
|
### 2.5 LLM 客户端增强
|
||||||
|
**原地增强**。不重写 `LLMAnalyzer`,而是在 `call_provider_with_retry` 层面增加:
|
||||||
|
- **CircuitBreaker**: 简单的失败计数器 (Windowed Counter)。
|
||||||
|
- **RateLimiter**: `asyncio.Semaphore` 全局控制并发数。
|
||||||
|
|
||||||
|
## 3. 发送流程演进
|
||||||
|
|
||||||
|
### 旧流程 (Current)
|
||||||
|
`AutoScheduler` -> `_send_image_message` -> `bot.api.call_action("send_group_msg")` -> (失败) -> `RetryManager` queue -> `RetryManager` worker -> `bot.api`
|
||||||
|
|
||||||
|
### 新流程 (Proposed)
|
||||||
|
`SchedulerJob` -> `ReportDispatcher` -> `MessageSender.send_image(url)`
|
||||||
|
-> **Attempt 1**: `Context.send_message(image(url))`
|
||||||
|
-> **Fail**: Catch generic exception
|
||||||
|
-> **Attempt 2**: Download -> `Context.send_message(image(base64))`
|
||||||
|
-> **Fail**: Catch exception
|
||||||
|
-> **Fallback**: `MessageSender.send_text(report.text)` (Instant Fallback, no complex queue)
|
||||||
|
|
||||||
|
> **注**: 如果确实需要异步低优先级的重试队列,可以将 `MessageSender` 的失败任务推送到 `Context.task_scheduler` 的一次性延时任务中,复用框架能力。
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 03. 领域模型设计 (Domain Model - Pragmatic Edition)
|
||||||
|
|
||||||
|
> **注**: 本文档已根据 Review 意见简化,移除了复杂的 DDD 聚合根,保留轻量级的数据结构和必要的配置管理。
|
||||||
|
|
||||||
|
## 1. 核心数据结构 (Data Structures)
|
||||||
|
|
||||||
|
保持现有 `src/models/data_models.py` 的精简风格,按需增强。
|
||||||
|
|
||||||
|
### 1.1 `AnalysisContext` (New)
|
||||||
|
用于在一次分析流程中传递上下文信息,替代之前的 `AnalysisTask` 聚合根。
|
||||||
|
* `trace_id: str`: 链路追踪 ID。
|
||||||
|
* `group_id: str`: 目标群号。
|
||||||
|
* `start_time: float`: 开始时间。
|
||||||
|
* `is_manual: bool`: 是否为手动触发。
|
||||||
|
|
||||||
|
### 1.2 `GroupStatistics` (Enhanced)
|
||||||
|
增强现有的统计模型,支持追踪和部分失败记录。
|
||||||
|
* `trace_id: str`: **[New]** 关联的 TraceID。
|
||||||
|
* `partial_failures: List[str]`: **[New]** 记录分析过程中失败的模块 (e.g., ["golden_quote"])。
|
||||||
|
* `...` (Existing fields: message_count, emoji_stats, etc.)
|
||||||
|
|
||||||
|
### 1.3 `LLMRequest` (Value Object)
|
||||||
|
用于规范化 LLM 请求,支持模块化配置。
|
||||||
|
* `module_tag: str`: 业务模块标签 (e.g., "topic", "summary")。
|
||||||
|
* `prompt: str`: 提示词。
|
||||||
|
* `system_prompt: str`: 系统提示词。
|
||||||
|
* `trace_id: str`: 追踪 ID。
|
||||||
|
|
||||||
|
## 2. 配置管理 (Configuration)
|
||||||
|
|
||||||
|
不引入新的 Entity,直接使用增强后的 `ConfigManager`。
|
||||||
|
|
||||||
|
### 2.1 `ConfigManager` (Enhanced)
|
||||||
|
* **LLM Configuration**:
|
||||||
|
* `get_provider_config(module_tag: str) -> dict`: 获取特定模块的 Provider 配置 (Platform, Model, Token)。
|
||||||
|
* 支持回退策略: Module config -> Global config -> Default config。
|
||||||
|
* **Feature Flags**:
|
||||||
|
* `is_module_enabled(module_tag: str) -> bool`: 检查模块开关。
|
||||||
|
|
||||||
|
## 3. 基础设施抽象 (Infrastructure Abstractions)
|
||||||
|
|
||||||
|
仅保留必要的接口定义,避免过度抽象。
|
||||||
|
|
||||||
|
### 3.1 `IMessageSender` (Interface)
|
||||||
|
* `send_msg(group_id: str, message: list | str)`: 统一发送接口。
|
||||||
|
|
||||||
|
### 3.2 `IReportRenderer` (Interface)
|
||||||
|
* `render(data: AnalysisResult, template: str) -> bytes | str`: 渲染接口。
|
||||||
|
|
||||||
|
## 4. 链路追踪 (Traceability)
|
||||||
|
|
||||||
|
使用 Python 标准库 `contextvars` 实现。
|
||||||
|
|
||||||
|
```python
|
||||||
|
# src/utils/trace_context.py
|
||||||
|
import contextvars
|
||||||
|
|
||||||
|
trace_id_var = contextvars.ContextVar("trace_id", default="N/A")
|
||||||
|
|
||||||
|
def get_trace_id() -> str:
|
||||||
|
return trace_id_var.get()
|
||||||
|
|
||||||
|
def set_trace_id(trace_id: str):
|
||||||
|
trace_id_var.set(trace_id)
|
||||||
|
```
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# 04. 基础设施层设计 (Infrastructure Layer - Improved)
|
||||||
|
|
||||||
|
## 1. 链路追踪 (TraceContext)
|
||||||
|
|
||||||
|
使用 `contextvars` 实现零侵入的链路追踪,确保所有日志都能关联到具体的分析任务。
|
||||||
|
|
||||||
|
### 1.1 实现方案
|
||||||
|
|
||||||
|
```python
|
||||||
|
import contextvars
|
||||||
|
from astrbot.api import logger
|
||||||
|
|
||||||
|
_trace_id_ctx = contextvars.ContextVar("trace_id", default="")
|
||||||
|
|
||||||
|
class TraceContext:
|
||||||
|
@staticmethod
|
||||||
|
def set(trace_id: str):
|
||||||
|
return _trace_id_ctx.set(trace_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get() -> str:
|
||||||
|
return _trace_id_ctx.get()
|
||||||
|
|
||||||
|
class TraceLogFilter(logging.Filter):
|
||||||
|
def filter(self, record):
|
||||||
|
trace_id = _trace_id_ctx.get()
|
||||||
|
if trace_id:
|
||||||
|
record.msg = f"[{trace_id}] {record.msg}"
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 在插件初始化时挂载 Filter
|
||||||
|
logger.addFilter(TraceLogFilter())
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. LLM 客户端增强 (Resilient LLM)
|
||||||
|
|
||||||
|
在现有的 `call_provider_with_retry` 基础上,增加 **熔断 (Circuit Breaker)** 和 **限流 (Rate Limiter)**。
|
||||||
|
|
||||||
|
### 2.1 熔断器 (Circuit Breaker)
|
||||||
|
|
||||||
|
防止单点故障拖垮整个流程。
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CircuitBreaker:
|
||||||
|
def __init__(self, failure_threshold=5, recovery_timeout=60):
|
||||||
|
self.failure_count = 0
|
||||||
|
self.state = "CLOSED" # CLOSED, OPEN, HALF_OPEN
|
||||||
|
self.last_failure_time = 0
|
||||||
|
|
||||||
|
def record_failure(self):
|
||||||
|
self.failure_count += 1
|
||||||
|
if self.failure_count >= self.failure_threshold:
|
||||||
|
self.state = "OPEN"
|
||||||
|
self.last_failure_time = time.time()
|
||||||
|
|
||||||
|
def allow_request(self) -> bool:
|
||||||
|
if self.state == "OPEN":
|
||||||
|
if time.time() - self.last_failure_time > self.recovery_timeout:
|
||||||
|
self.state = "HALF_OPEN"
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 全局限流 (Global Rate Limiter)
|
||||||
|
|
||||||
|
使用 `asyncio.Semaphore` 控制并发 LLM 请求数。
|
||||||
|
|
||||||
|
```python
|
||||||
|
# src/core/llm/limiter.py
|
||||||
|
global_llm_semaphore = asyncio.Semaphore(3) # 最大并发 3
|
||||||
|
|
||||||
|
async def call_llm_with_limit(...):
|
||||||
|
async with global_llm_semaphore:
|
||||||
|
return await call_provider_with_retry(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 消息发送增强 (MessageSender)
|
||||||
|
|
||||||
|
### 3.1 统一发送接口
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MessageSender:
|
||||||
|
def __init__(self, context: Context):
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
async def send_image(self, group_id: str, url: str):
|
||||||
|
# 1. 尝试 URL 发送
|
||||||
|
# 2. 失败 -> 尝试 Base64 发送
|
||||||
|
# 3. 失败 -> 发送文本回退
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 离线任务支持
|
||||||
|
|
||||||
|
对于定时任务触发的场景,此时没有 `AstrMessageEvent`。需要手动构建 Session 或使用 `PlatformManager` 获取 Bot 实例直接发送。
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 获取 Bot 实例
|
||||||
|
bot = self.context.platform_manager.get_inst(platform_id)
|
||||||
|
# 构造虚拟 Session
|
||||||
|
session = Session(bot, group_id=group_id)
|
||||||
|
# 发送
|
||||||
|
await self.context.send_message(session, chain)
|
||||||
|
```
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# 05. 重构路线图 (Refactoring Roadmap - Pragmatic Edition)
|
||||||
|
|
||||||
|
> **注**: 本路线图采用渐进式重构策略,优先解决稳定性问题,逐步对齐框架。
|
||||||
|
|
||||||
|
## Phase 1: 轻量级增强 (Lightweight Enhancements)
|
||||||
|
**目标**: 不动架构,仅通过装饰器和 ContextVar 增强系统的可观测性和稳定性。
|
||||||
|
**预估工期**: 1 周
|
||||||
|
|
||||||
|
1. **TraceID 注入**:
|
||||||
|
- 实现 `TraceContext` (contextvars)。
|
||||||
|
- 在 `main.py` 和 `auto_scheduler` 入口处埋点。
|
||||||
|
- 配置 `logging.Filter`。
|
||||||
|
|
||||||
|
2. **LLM 熔断与限流**:
|
||||||
|
- 实现 `CircuitBreaker` 类。
|
||||||
|
- 在 `src/utils/llm_utils.py` 的 `call_provider_with_retry` 中集成熔断器和全局 `Semaphore`。
|
||||||
|
|
||||||
|
3. **修复已知 Bug**:
|
||||||
|
- 修复 `_send_image_message` 中的双重 `return False` 问题。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 日志中包含 `[trace_id]`。
|
||||||
|
- 模拟 LLM 故障时,系统能快速失败并恢复。
|
||||||
|
|
||||||
|
## Phase 2: 核心职责提取 (Core Extraction)
|
||||||
|
**目标**: 将 `AutoScheduler` 的核心逻辑剥离为独立模块。
|
||||||
|
**预估工期**: 1.5 周
|
||||||
|
|
||||||
|
1. **提取 `MessageSender`**:
|
||||||
|
- 创建 `src/core/message_sender.py`。
|
||||||
|
- 迁移图片/文本发送逻辑,实现 URL->Base64 降级。
|
||||||
|
- 在 `main.py` 中替换原有发送逻辑。
|
||||||
|
|
||||||
|
2. **提取 `ReportDispatcher`**:
|
||||||
|
- 创建 `src/reports/dispatcher.py`。
|
||||||
|
- 迁移报告生成和分发逻辑。
|
||||||
|
|
||||||
|
3. **增强 `BotManager`**:
|
||||||
|
- 合并群组发现 (`_get_all_groups`) 逻辑。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- `AutoScheduler` 代码行数减少 40% 以上。
|
||||||
|
- 发送文本和图片功能在各种网络环境下依然稳定。
|
||||||
|
|
||||||
|
## Phase 3: 框架完全对齐 (Framework Alignment)
|
||||||
|
**目标**: 移除自定义调度循环,完全复用 AstrBot 能力。
|
||||||
|
**预估工期**: 1 周
|
||||||
|
|
||||||
|
1. **对接 `Context.task_scheduler`**:
|
||||||
|
- 移除 `_scheduler_loop`。
|
||||||
|
- 使用 `context.task_scheduler.add_job` 注册定时任务。
|
||||||
|
|
||||||
|
2. **生命周期钩子**:
|
||||||
|
- 使用 `OnPlatformLoaded` 事件替代冷启动 sleep。
|
||||||
|
|
||||||
|
3. **配置清理**:
|
||||||
|
- 确保所有配置变更向后兼容。
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 插件启动无硬编码等待。
|
||||||
|
- 定时任务准确触发。
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
# 06. 重构文档审查报告 (Architecture Review)
|
||||||
|
|
||||||
|
> **审查日期**: 2026-02-07
|
||||||
|
> **审查范围**: `docs/01~05` 全部重构文档
|
||||||
|
> **参照基准**: 插件现有代码 (v4.6.9) + AstrBot-master 框架实际 API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 审查总评
|
||||||
|
|
||||||
|
重构文档整体展现了较高的架构设计水平,对现有代码问题的诊断基本准确,DDD + 事件驱动的方向也是合理的工程演进路径。但文档在**与宿主框架的适配**、**实际可行性**、**复杂度收益比**上存在若干需要重新审视的关键问题。
|
||||||
|
|
||||||
|
### 评分概览
|
||||||
|
|
||||||
|
| 维度 | 评分 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 问题诊断准确度 | ⭐⭐⭐⭐☆ | AutoScheduler 上帝类问题判断精准,LLM 脆弱性分析到位 |
|
||||||
|
| 架构方向合理性 | ⭐⭐⭐☆☆ | 方向正确但严重过度设计,未充分利用宿主框架能力 |
|
||||||
|
| 与 AstrBot 框架适配 | ⭐⭐☆☆☆ | 几乎未考虑 AstrBot 已有的事件系统和 API,存在大量重复建设 |
|
||||||
|
| 落地可行性 | ⭐⭐☆☆☆ | 四阶段路线图工期估计不足,缺乏增量验证策略 |
|
||||||
|
| 配置兼容性 | ⭐⭐⭐⭐☆ | 明确提出了配置向后兼容需求,这是正确的 |
|
||||||
|
| 模型设计质量 | ⭐⭐⭐☆☆ | 领域模型合理但偏理想化,与现有数据结构差距大 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 关键架构问题批注
|
||||||
|
|
||||||
|
### 1.1 🔴 [严重] 自建 EventBus 与 AstrBot 框架能力重复
|
||||||
|
|
||||||
|
**文档立场** (02_architecture_design):
|
||||||
|
> 引入事件总线 (EventBus) 作为核心通信机制,解耦各业务模块。
|
||||||
|
|
||||||
|
**实际情况**:
|
||||||
|
AstrBot 框架**已经内置了完整的事件系统**,包括:
|
||||||
|
|
||||||
|
- **EventBus** (`astrbot/core/event_bus.py`) — 基于 `asyncio.Queue` 的事件分发器
|
||||||
|
- **Pipeline 管道** — 洋葱模型的 9 阶段消息处理流水线
|
||||||
|
- **StarHandlerRegistry** — 按 `EventType` + `priority` 分发到插件处理器
|
||||||
|
- **丰富的 EventType 枚举**:
|
||||||
|
- `OnAfterAstrBotLoaded` — 启动后钩子
|
||||||
|
- `OnPlatformLoaded` — 平台加载钩子
|
||||||
|
- `OnAfterMessageSent` — 消息发送后钩子
|
||||||
|
- 以及 LLM 请求/响应拦截等
|
||||||
|
|
||||||
|
**建议**:
|
||||||
|
> ❌ **不应在插件内部自建 AsyncEventBus**。应利用 AstrBot 已有的事件钩子实现解耦。
|
||||||
|
> ✅ 对于插件内部的业务流转(分析→报告→发送),使用**简单的 async 回调链**或**协程编排**即可,无需引入一个完整的发布/订阅系统。
|
||||||
|
> ✅ 如果确实需要插件级别的内部事件(如扩展点),使用轻量的 `Dict[str, List[Callable]]` 注册表即可,不必实现完整的 `DomainEvent` 体系。
|
||||||
|
|
||||||
|
**风险**: 自建 EventBus 会与 AstrBot 的 Pipeline 产生两套事件流,增加调试复杂度,且 `asyncio.create_task` 包裹的 handler 异常容易丢失。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 🔴 [严重] 防腐层 (ACL) 层设计与 AstrBot 平台抽象冲突
|
||||||
|
|
||||||
|
**文档立场** (02, 04):
|
||||||
|
> NapCat ACL (防腐层) — 隔离 Bot 平台差异。
|
||||||
|
|
||||||
|
**实际情况**:
|
||||||
|
AstrBot 框架已经提供了完善的**平台抽象层**:
|
||||||
|
|
||||||
|
- `Context.send_message(session, chain)` — 统一消息发送接口
|
||||||
|
- `PlatformManager.get_insts()` — 获取所有平台实例
|
||||||
|
- `AstrMessageEvent` — 统一消息事件对象,提供 `plain_result()`、`image_result()` 等
|
||||||
|
- `Star.html_render()` — 内置 HTML→图片渲染
|
||||||
|
- `StarTools.send_message()` — 主动消息发送
|
||||||
|
|
||||||
|
**当前插件的问题**:
|
||||||
|
插件绕过了 AstrBot 的抽象层,直接通过 `bot_instance.api.call_action()` 调用 OneBot v11 原始 API。这才是真正应该修复的"防腐层"问题——但修复方向应该是**回归 AstrBot 抽象接口**,而非再建一层 ACL。
|
||||||
|
|
||||||
|
**建议**:
|
||||||
|
> ✅ 自动分析的消息发送应使用 `StarTools.send_message(unified_msg_origin, chain)` 或 `Context.send_message(session, chain)`,而非直接调用 `call_action`。
|
||||||
|
> ✅ `BotManager` 中大量的平台发现、实例缓存逻辑,应尽量复用 `Context.platform_manager`。
|
||||||
|
> ⚠️ 但要注意: 定时任务主动推送消息时没有 `AstrMessageEvent` 上下文,此时需要手动构建 session,这是需要特殊处理的边界场景。建议在 `_delayed_start_scheduler` 中用 `@filter.on_decorating_result` 或 `OnPlatformLoaded` 钩子缓存 session 信息。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.3 🟡 [中等] DDD 领域模型过度设计
|
||||||
|
|
||||||
|
**文档立场** (03_domain_model):
|
||||||
|
> 引入 `AnalysisTask` (聚合根)、`GroupConfig` (实体)、`LLMRequest` (值对象)、`RetryPolicy` (值对象) 等完整 DDD 体系。
|
||||||
|
|
||||||
|
**实际情况**:
|
||||||
|
当前插件的数据模型 (`src/models/data_models.py`) 使用简洁的 `@dataclass`:
|
||||||
|
`SummaryTopic`、`UserTitle`、`GoldenQuote`、`TokenUsage`、`GroupStatistics` 等,共 ~100 行代码。
|
||||||
|
|
||||||
|
这些模型**已经够用**,且与报告生成、LLM 分析紧密配合。引入完整的 DDD 聚合根 + 领域事件体系,对于一个**插件级别**的代码量来说:
|
||||||
|
|
||||||
|
**成本远大于收益**。
|
||||||
|
|
||||||
|
**建议**:
|
||||||
|
> ✅ 保留现有 `@dataclass` 模型,按需增强:
|
||||||
|
> - 给 `GroupStatistics` 增加 `trace_id` 字段(支持日志追踪)
|
||||||
|
> - 给分析结果增加 `partial_failures: List[str]` 字段(支持部分成功)
|
||||||
|
> ❌ 不建议引入 `AnalysisTask` 作为聚合根并承载完整的状态机。对于插件场景,一个 `@dataclass AnalysisContext` 保存本次分析的元信息即可。
|
||||||
|
> ❌ `GroupConfig` 作为独立实体没有必要,`ConfigManager` 已经很好地封装了配置读写。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.4 🟡 [中等] LLM 服务增强方案忽视了 AstrBot Provider 体系
|
||||||
|
|
||||||
|
**文档立场** (03, 04):
|
||||||
|
> 多供应商支持:允许为不同模块(Topic, UserTitle, GoldenQuote)配置不同的 LLM 配置。
|
||||||
|
> 实现 ResilientLLMClient 带 Rate Limiter + Circuit Breaker。
|
||||||
|
|
||||||
|
**实际情况**:
|
||||||
|
**好消息是——插件已经实现了这些功能的大部分!**
|
||||||
|
|
||||||
|
- `ConfigManager` 已有 `get_topic_provider_id()`、`get_user_title_provider_id()`、`get_golden_quote_provider_id()` — **多 Provider 支持已存在**
|
||||||
|
- `llm_utils.py` 中的 `get_provider_id_with_fallback()` 实现了 4 级回退策略 — **Provider 回退已存在**
|
||||||
|
- `llm_utils.py` 中的 `call_provider_with_retry()` 已实现重试 — **重试已存在**
|
||||||
|
- `LLMAnalyzer.analyze_all_concurrent()` 已实现并发分析 + 部分失败隔离 — **部分成功已存在**
|
||||||
|
|
||||||
|
文档似乎是基于**更早版本**的代码做的分析,没有充分反映当前已有的改进。
|
||||||
|
|
||||||
|
**建议**:
|
||||||
|
> ✅ 文档应先做 **现状盘点**,明确哪些能力已具备、哪些还缺失,避免重复建设。
|
||||||
|
> ✅ 真正缺失的是:
|
||||||
|
> - **熔断器 (Circuit Breaker)** — 当前没有,可以用简单的计数器实现,不需要完整的状态机
|
||||||
|
> - **全局 LLM 速率限制** — 当前单次请求有重试,但无全局 QPS 限制
|
||||||
|
> - **结构化超时** — 当前只有 `get_llm_timeout()`,建议按模块区分
|
||||||
|
> ❌ 不需要新建 `ResilientLLMClient` 类。在现有的 `call_provider_with_retry()` 上增强即可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.5 🟡 [中等] TraceID 方案可行但需简化
|
||||||
|
|
||||||
|
**文档立场** (02, 03):
|
||||||
|
> TraceID 格式: `{group_id}-{timestamp}-{uuid}`,所有领域事件都必须携带 TraceID。
|
||||||
|
|
||||||
|
**实际情况**:
|
||||||
|
TraceID 的理念是**正确的**,当前代码在多群并发分析时,日志确实难以区分。但实现不必绑定到 DomainEvent 体系。
|
||||||
|
|
||||||
|
**建议**:
|
||||||
|
> ✅ 使用 Python 标准库的 `contextvars` + `logging.Filter` 实现零侵入的 TraceID 注入:
|
||||||
|
> ```python
|
||||||
|
> import contextvars, uuid
|
||||||
|
> _trace_id: contextvars.ContextVar[str] = contextvars.ContextVar('trace_id', default='')
|
||||||
|
>
|
||||||
|
> class TraceFilter(logging.Filter):
|
||||||
|
> def filter(self, record):
|
||||||
|
> record.trace_id = _trace_id.get('')
|
||||||
|
> return True
|
||||||
|
> ```
|
||||||
|
> 在每次群分析开始时 `_trace_id.set(f"{group_id}-{int(time.time())}")`,所有子协程自动继承。这比在每个函数签名中传递 `trace_id` 参数更优雅。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.6 🟢 [建议] AutoScheduler 拆分策略
|
||||||
|
|
||||||
|
**文档诊断准确**: `AutoScheduler` 确实承担了过多职责 (1003 行代码),包含:
|
||||||
|
1. 定时循环逻辑 (`_scheduler_loop`)
|
||||||
|
2. 群组发现 (`_get_all_groups`)
|
||||||
|
3. 并发编排 (`_run_auto_analysis`)
|
||||||
|
4. 单群分析核心流程 (`_perform_auto_analysis_for_group`)
|
||||||
|
5. 报告发送 (`_send_analysis_report`, `_send_image_message`, `_send_text_message`, `_send_pdf_file`)
|
||||||
|
6. 平台路由 (`get_platform_id_for_group`)
|
||||||
|
|
||||||
|
**但拆分方案应更务实**:
|
||||||
|
|
||||||
|
> ✅ **推荐拆分方案**(非 EventBus 驱动,而是职责提取):
|
||||||
|
>
|
||||||
|
> | 提取目标 | 来源方法 | 新模块 |
|
||||||
|
> |----------|----------|--------|
|
||||||
|
> | `MessageSender` | `_send_image_message`, `_send_text_message`, `_send_pdf_file` | `src/core/message_sender.py` |
|
||||||
|
> | `GroupDiscovery` | `_get_all_groups`, `get_platform_id_for_group` | 合并到 `BotManager` |
|
||||||
|
> | `ReportDispatcher` | `_send_analysis_report` | `src/reports/dispatcher.py` |
|
||||||
|
>
|
||||||
|
> `AutoScheduler` 最终只保留: 定时循环 + 并发编排 + 调用各模块。
|
||||||
|
> 预计从 1003 行缩减到 ~250 行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 文档间一致性问题
|
||||||
|
|
||||||
|
### 2.1 01 与 02 之间的概念跳跃
|
||||||
|
- 01 提出了"上帝类"和"异常处理过度"的问题
|
||||||
|
- 02 直接跳到了完整的 EventBus + DDD 架构
|
||||||
|
- **缺少过渡**: 没有评估"在不引入 EventBus 的前提下,仅通过职责提取能解决多少问题"
|
||||||
|
|
||||||
|
### 2.2 03 领域模型与现有代码断层严重
|
||||||
|
- 文档定义了 `AnalysisTask` 聚合根带 `TaskStatus` 状态机
|
||||||
|
- 现有代码没有任何 `TaskStatus` 枚举或任务状态管理
|
||||||
|
- **迁移成本被低估**: Phase 2 说"将 AutoScheduler 的逻辑拆解为事件处理器",但现有 1003 行的 AutoScheduler 与新设计几乎是**重写**而非渐进迁移
|
||||||
|
|
||||||
|
### 2.3 04 基础设施代码示例存在缺陷
|
||||||
|
- `AsyncEventBus.publish()` 使用 `asyncio.create_task` 且仅在 `_safe_execute` 中 `logger.error`
|
||||||
|
- 问题: `create_task` 的异常如果没有被 `await`,在 Python 3.12+ 不会触发 `unraisable hook`,可能导致**静默丢失错误**
|
||||||
|
- 建议: 至少维护一个 `_pending_tasks: set` 并注册 `task.add_done_callback()` 进行异常日志记录
|
||||||
|
|
||||||
|
### 2.4 05 路线图时间估计缺失
|
||||||
|
- 四个 Phase 都没有时间估计
|
||||||
|
- Phase 2 的"事件驱动迁移"实质上是重写核心流程,至少需要 2-3 周集中开发 + 1 周回归测试
|
||||||
|
- **建议增加**: 每个 Phase 的预估人天、验收标准和回退方案
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 现有代码中被忽视的优点
|
||||||
|
|
||||||
|
文档以问题为导向,但忽视了当前代码中已有的若干良好实践,重构时**不应丢失**:
|
||||||
|
|
||||||
|
| 现有优点 | 所在位置 | 说明 |
|
||||||
|
|----------|----------|------|
|
||||||
|
| 并发控制 + Semaphore | `auto_scheduler.py` L256 | 使用 `asyncio.Semaphore(max_concurrent)` 控制并发数 |
|
||||||
|
| 细粒度 LLM Provider 配置 | `config.py` | 已支持 topic/user_title/golden_quote 独立 Provider |
|
||||||
|
| 4 级 Provider 回退 | `llm_utils.py` | 专用→主→会话→首个可用 |
|
||||||
|
| BaseAnalyzer 模板方法模式 | `base_analyzer.py` | 分析器抽象类设计合理 |
|
||||||
|
| 图片发送三级降级 | `auto_scheduler.py` | URL → Base64 → 文本回退 |
|
||||||
|
| 死信队列 (DLQ) | `retry.py` | RetryManager 已有死信队列 + 文本回退 |
|
||||||
|
| 多平台适配器遍历 | `bot_manager.py` | 自动发现所有 aiocqhttp 实例 |
|
||||||
|
| 配置向后兼容 | `config.py` | get/set 方法带默认值,旧配置平滑迁移 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 推荐的替代重构策略
|
||||||
|
|
||||||
|
鉴于上述分析,建议采用**渐进式务实重构**替代文档中的"大设计上前 (Big Design Up Front)"方案:
|
||||||
|
|
||||||
|
### Phase 1: 轻量增强 (1-2 周)
|
||||||
|
**零架构改动,仅增强现有模块**
|
||||||
|
|
||||||
|
1. **TraceID 注入** — 使用 `contextvars` 全局注入 trace_id 到日志
|
||||||
|
2. **LLM 熔断器** — 在 `call_provider_with_retry()` 中增加简单的失败计数 + 冷却期
|
||||||
|
3. **LLM 全局限流** — 使用 `asyncio.Semaphore` 控制同时发起的 LLM 请求数
|
||||||
|
4. **部分成功增强** — `analyze_all_concurrent` 已支持隔离失败,增加结果标记
|
||||||
|
|
||||||
|
### Phase 2: 职责提取 (1-2 周)
|
||||||
|
**从 AutoScheduler 提取独立模块,不引入 EventBus**
|
||||||
|
|
||||||
|
1. **提取 `MessageSender`** — 统一文本/图片/PDF/合并转发发送逻辑
|
||||||
|
- 优先使用 `StarTools.send_message()` / `Context.send_message()`
|
||||||
|
- 仅在必须使用 OneBot 专有 API 时保留 `call_action` 调用
|
||||||
|
2. **提取 `ReportDispatcher`** — 从分析结果到报告生成到发送的编排逻辑
|
||||||
|
3. **合并群发现逻辑到 `BotManager`**
|
||||||
|
|
||||||
|
### Phase 3: 框架对齐 (1 周)
|
||||||
|
**复用 AstrBot 框架能力**
|
||||||
|
|
||||||
|
1. **使用 AstrBot 定时器** — `Context.task_scheduler` 是 APScheduler 实例,用它替代手写的 `_scheduler_loop`
|
||||||
|
2. **使用 `OnPlatformLoaded` 钩子** — 替代 `_delayed_start_scheduler` 中的 30 秒 sleep
|
||||||
|
3. **使用 `Star.html_render`** — 已经内置,确认当前是否正确使用
|
||||||
|
|
||||||
|
### Phase 4: 可选增强 (按需)
|
||||||
|
1. **插件级事件扩展点** — 如果社区有需求(如 Webhook 推送),用简单的回调注册表实现
|
||||||
|
2. **历史报告存储** — 利用 `Star.put_kv()` 存储分析结果摘要
|
||||||
|
3. **Web Dashboard 集成** — 通过 `Context.register_web_api()` 暴露分析数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 逐文档批注汇总
|
||||||
|
|
||||||
|
### 01_requirements_analysis.md
|
||||||
|
|
||||||
|
| 条目 | 批注 |
|
||||||
|
|------|------|
|
||||||
|
| §1.1 上帝类诊断 | ✅ 准确。AutoScheduler 1003 行确实需要拆分 |
|
||||||
|
| §1.2 异常处理过度 | ✅ 准确。但当前代码已比描述改善不少 (BaseAnalyzer 有结构化异常处理) |
|
||||||
|
| §1.3 NapCat 交互稳定性 | ⚠️ 部分已解决。当前已有 URL→Base64→文本 三级降级 |
|
||||||
|
| §1.4 LLM 瓶颈 | ⚠️ 部分已解决。多 Provider 配置和并发分析已实现 |
|
||||||
|
| §2 重构目标 | 🔴 目标过于宏大。EventBus + DDD + ACL 对插件规模来说过度 |
|
||||||
|
| §3.1 配置兼容 | ✅ 非常正确且重要 |
|
||||||
|
| §3.2 EventBus | 🔴 应复用 AstrBot 事件系统或采用更轻量方案 |
|
||||||
|
| §3.3 TraceID | ✅ 方向正确,建议用 contextvars 实现 |
|
||||||
|
|
||||||
|
### 02_architecture_design.md
|
||||||
|
|
||||||
|
| 条目 | 批注 |
|
||||||
|
|------|------|
|
||||||
|
| 总体架构图 | 🟡 设计精美但与 AstrBot Pipeline 架构有冲突 |
|
||||||
|
| §2.1 EventBus | 🔴 与 AstrBot 内置 EventBus 重复建设 |
|
||||||
|
| §2.2 TraceContext | ✅ 理念正确 |
|
||||||
|
| §2.2 LLMService 多供应商 | ⚠️ 已经实现,文档未反映现状 |
|
||||||
|
| §2.3 AnalysisOrchestrator | ✅ 概念合理,但不必通过事件驱动,直接调用即可 |
|
||||||
|
| §2.3 MessageService | 🟡 概念可取,命名建议改为 MessageSender |
|
||||||
|
| §2.4 TaskQueue | 🟡 当前 RetryManager 已有队列,合并而非新建 |
|
||||||
|
| §3 事件流转 | 🔴 4 个阶段 7 个事件类型 — 对于"定时分析→生成报告→发送"的线性流程来说过度抽象 |
|
||||||
|
| §4 Circuit Breaker | ✅ 这是真正缺失的能力,值得实现 |
|
||||||
|
|
||||||
|
### 03_domain_model.md
|
||||||
|
|
||||||
|
| 条目 | 批注 |
|
||||||
|
|------|------|
|
||||||
|
| AnalysisTask 聚合根 | 🔴 过度设计。用 `@dataclass AnalysisContext(trace_id, group_id, started_at)` 即可 |
|
||||||
|
| GroupConfig 实体 | 🔴 不需要。ConfigManager 已充分封装 |
|
||||||
|
| LLMRequest 值对象 | 🟡 概念有用,但 `module_tag` 已通过 `provider_id_key` 间接实现 |
|
||||||
|
| LLMConfig 值对象 | 🟡 理论上好,但 AstrBot Provider 体系已管理 LLM 配置 |
|
||||||
|
| RetryPolicy 值对象 | ✅ 有用。当前重试参数散落在 ConfigManager 各方法中,统一为一个对象是好的 |
|
||||||
|
| ILLMService 接口 | 🟡 BaseAnalyzer 模板方法模式已经提供了类似抽象 |
|
||||||
|
| IEventBus 接口 | 🔴 不需要自建 |
|
||||||
|
|
||||||
|
### 04_infrastructure_layer.md
|
||||||
|
|
||||||
|
| 条目 | 批注 |
|
||||||
|
|------|------|
|
||||||
|
| AsyncEventBus 实现 | 🔴 不建议。create_task 异常处理有隐患 |
|
||||||
|
| ResilientLLMClient | 🟡 熔断器和限流逻辑有价值,但应增强现有 `call_provider_with_retry` 而非新建类 |
|
||||||
|
| NapCatAdapter 增强 | ✅ 消息获取重试有价值。但应回归 AstrBot 抽象 API |
|
||||||
|
| TaskQueueService | 🟡 RetryManager 已有此能力,应增强而非新建 |
|
||||||
|
|
||||||
|
### 05_refactoring_roadmap.md
|
||||||
|
|
||||||
|
| 条目 | 批注 |
|
||||||
|
|------|------|
|
||||||
|
| Phase 1 基础设施 | 🔴 方向有误。不应以 EventBus 为起点,应以"职责提取"为起点 |
|
||||||
|
| Phase 2 事件驱动迁移 | 🔴 风险高。实质是重写核心流程,不是"迁移" |
|
||||||
|
| Phase 3 配置迁移 | ✅ 配置兼容策略正确 |
|
||||||
|
| Phase 3 移除上帝类 | ✅ 方向正确,但应在 Phase 1 就开始 |
|
||||||
|
| Phase 4 验证 | ✅ 压力测试和长稳测试都是必要的 |
|
||||||
|
| 缺失: 时间估计 | 🔴 四个 Phase 均无时间线 |
|
||||||
|
| 缺失: 回退方案 | 🔴 如果某个 Phase 失败,如何回退? |
|
||||||
|
| 缺失: 功能开关 | 🟡 建议使用 Feature Flag 渐进切换新旧实现 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 最终建议清单
|
||||||
|
|
||||||
|
### 必须做 (P0)
|
||||||
|
|
||||||
|
1. **利用 AstrBot `Context.task_scheduler` (APScheduler) 替代手写定时循环** — 消除 `_scheduler_loop` 中复杂的时间计算和 sleep 逻辑
|
||||||
|
2. **从 AutoScheduler 提取 MessageSender** — 至少减少 400 行代码
|
||||||
|
3. **增加 TraceID** — 基于 `contextvars` 实现,零侵入
|
||||||
|
4. **修复 `_send_image_message` 中的双重 `return False`** — 这是一个实际 bug (auto_scheduler.py 末尾连续两个 `return False`)
|
||||||
|
|
||||||
|
### 应该做 (P1)
|
||||||
|
|
||||||
|
5. **LLM 熔断器** — 简单的失败计数 + 冷却期,在 `call_provider_with_retry()` 层面实现
|
||||||
|
6. **全局 LLM 并发限制** — `asyncio.Semaphore` 控制同时发起的 LLM API 请求数量
|
||||||
|
7. **使用 `OnPlatformLoaded` 钩子** 初始化 Bot 实例,替代 `asyncio.sleep(30)` 的硬编码等待
|
||||||
|
8. **RetryPolicy 数据类** — 统一重试参数
|
||||||
|
|
||||||
|
### 可以做 (P2)
|
||||||
|
|
||||||
|
9. **轻量级插件内事件注册表** — 为未来扩展(Webhook、数据库存储)预留回调接口
|
||||||
|
10. **AnalysisContext 数据类** — 轻量级追踪上下文,而非完整 DDD 聚合根
|
||||||
|
11. **历史分析结果存储** — 利用 `Star.put_kv()` 持久化
|
||||||
|
|
||||||
|
### 不建议做
|
||||||
|
|
||||||
|
12. ❌ 自建 AsyncEventBus + DomainEvent 体系
|
||||||
|
13. ❌ 自建 NapCat ACL 防腐层
|
||||||
|
14. ❌ AnalysisTask 聚合根 + TaskStatus 状态机
|
||||||
|
15. ❌ GroupConfig 独立实体
|
||||||
|
16. ❌ 完整的 ILLMService 接口 (BaseAnalyzer 模板方法已足够)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 结语
|
||||||
|
|
||||||
|
这套重构文档展现了作者对 DDD、事件驱动架构的深入理解,架构设计的**理论水平很高**。但在 AstrBot 插件的具体场景下,需要在"理想架构"和"实际收益"之间找到平衡。
|
||||||
|
|
||||||
|
核心原则:
|
||||||
|
> **一个好的插件架构不是最先进的架构,而是最适合宿主框架的架构。**
|
||||||
|
|
||||||
|
当前代码实际上已经完成了一次不错的模块化重构 (v4.6.9),BaseAnalyzer 模板模式、多 Provider 回退、并发分析等设计都很好。下一步的重点应该是:
|
||||||
|
|
||||||
|
1. **减法** — 从 AutoScheduler 中提取职责
|
||||||
|
2. **对齐** — 尽可能复用 AstrBot 框架能力
|
||||||
|
3. **增强** — 补上真正缺失的熔断、限流、TraceID
|
||||||
|
|
||||||
|
而不是引入一套全新的事件驱动 + DDD 架构体系。
|
||||||
|
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
# TraceID 实现代码示例
|
||||||
|
|
||||||
|
这个文件展示如何在插件中实现 contextvars + logging.Filter 方案。
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
astrbot_plugin_qq_group_daily_analysis/
|
||||||
|
├── src/
|
||||||
|
│ ├── utils/
|
||||||
|
│ │ ├── trace.py # ← 新增:TraceID 相关
|
||||||
|
│ │ └── helpers.py
|
||||||
|
│ ├── core/
|
||||||
|
│ │ └── config.py
|
||||||
|
│ └── ...
|
||||||
|
├── main.py # ← 需要修改:注册 Filter
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. 新增文件:src/utils/trace.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
TraceID 追踪工具模块
|
||||||
|
提供分布式追踪的 trace_id 上下文管理
|
||||||
|
"""
|
||||||
|
|
||||||
|
import contextvars
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from astrbot.api import logger
|
||||||
|
|
||||||
|
# ============ ContextVar 定义 ============
|
||||||
|
_trace_id: contextvars.ContextVar[str] = contextvars.ContextVar(
|
||||||
|
'trace_id',
|
||||||
|
default=''
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============ Filter 实现 ============
|
||||||
|
class TraceIDFilter(logging.Filter):
|
||||||
|
"""
|
||||||
|
将 trace_id 自动注入到日志记录
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
logger.addFilter(TraceIDFilter())
|
||||||
|
# 之后所有日志的 record 对象都会有 trace_id 属性
|
||||||
|
"""
|
||||||
|
|
||||||
|
def filter(self, record):
|
||||||
|
"""添加 trace_id 到日志记录"""
|
||||||
|
trace_id = _trace_id.get('')
|
||||||
|
record.trace_id = trace_id if trace_id else 'no-trace'
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ============ 接口函数 ============
|
||||||
|
def set_trace_id(group_id: str, timestamp: int = None) -> str:
|
||||||
|
"""
|
||||||
|
设置当前协程的 trace_id
|
||||||
|
|
||||||
|
Args:
|
||||||
|
group_id: 群 ID
|
||||||
|
timestamp: 时间戳(如果为 None 则使用当前时间)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
生成的 trace_id 字符串
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> trace_id = set_trace_id("123456789")
|
||||||
|
>>> logger.info("分析开始") # 日志自动包含 trace_id
|
||||||
|
"""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = int(time.time())
|
||||||
|
|
||||||
|
trace_id = f"{group_id}-{timestamp}"
|
||||||
|
_trace_id.set(trace_id)
|
||||||
|
return trace_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_trace_id() -> str:
|
||||||
|
"""获取当前协程的 trace_id"""
|
||||||
|
return _trace_id.get('no-trace')
|
||||||
|
|
||||||
|
|
||||||
|
def clear_trace_id():
|
||||||
|
"""清理当前协程的 trace_id"""
|
||||||
|
_trace_id.set('')
|
||||||
|
|
||||||
|
|
||||||
|
def with_trace_id(group_id: str):
|
||||||
|
"""
|
||||||
|
上下文管理器:自动管理 trace_id 的生命周期
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from src.utils.trace import with_trace_id
|
||||||
|
>>>
|
||||||
|
>>> async def analyze_group(group_id: str):
|
||||||
|
... with with_trace_id(group_id):
|
||||||
|
... logger.info("开始分析") # 自动包含 trace_id
|
||||||
|
... # ...
|
||||||
|
... logger.info("分析完成")
|
||||||
|
... # 退出时自动清理 trace_id
|
||||||
|
"""
|
||||||
|
class TraceContextManager:
|
||||||
|
def __init__(self, group_id):
|
||||||
|
self.group_id = group_id
|
||||||
|
self.trace_id = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.trace_id = set_trace_id(self.group_id)
|
||||||
|
return self.trace_id
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
clear_trace_id()
|
||||||
|
return False
|
||||||
|
|
||||||
|
return TraceContextManager(group_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ============ 初始化函数 ============
|
||||||
|
def setup_trace_logging():
|
||||||
|
"""
|
||||||
|
初始化 TraceID 日志追踪
|
||||||
|
|
||||||
|
在插件启动时调用一次即可:
|
||||||
|
from src.utils.trace import setup_trace_logging
|
||||||
|
setup_trace_logging()
|
||||||
|
"""
|
||||||
|
# 注册 Filter 到 AstrBot logger
|
||||||
|
trace_filter = TraceIDFilter()
|
||||||
|
|
||||||
|
# 检查是否已注册(避免重复注册)
|
||||||
|
for existing_filter in logger.filters:
|
||||||
|
if isinstance(existing_filter, TraceIDFilter):
|
||||||
|
return # 已经注册过了
|
||||||
|
|
||||||
|
logger.addFilter(trace_filter)
|
||||||
|
logger.info("[Trace] TraceID 日志追踪已启用")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 修改文件:main.py(插件主文件)
|
||||||
|
|
||||||
|
在 `__init__` 方法中添加初始化:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# main.py
|
||||||
|
from src.utils.trace import setup_trace_logging
|
||||||
|
|
||||||
|
class QQGroupDailyAnalysis(Star):
|
||||||
|
def __init__(self, context: Context, config: AstrBotConfig):
|
||||||
|
super().__init__(context)
|
||||||
|
# ... 其他初始化代码 ...
|
||||||
|
|
||||||
|
# ← 新增:初始化 TraceID 追踪
|
||||||
|
setup_trace_logging()
|
||||||
|
|
||||||
|
# ... 其他初始化代码 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 修改文件:src/scheduler/auto_scheduler.py
|
||||||
|
|
||||||
|
在群分析方法中使用 TraceID:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# auto_scheduler.py
|
||||||
|
from src.utils.trace import set_trace_id, clear_trace_id, with_trace_id
|
||||||
|
|
||||||
|
class AutoScheduler:
|
||||||
|
# ... 其他方法 ...
|
||||||
|
|
||||||
|
async def _perform_auto_analysis_for_group(self, group_id: str):
|
||||||
|
"""为指定群执行自动分析"""
|
||||||
|
|
||||||
|
# ← 方案 1:使用上下文管理器(推荐)
|
||||||
|
with with_trace_id(group_id):
|
||||||
|
try:
|
||||||
|
logger.info(f"开始为群 {group_id} 执行自动分析")
|
||||||
|
# ... 分析逻辑,所有 logger 调用都自动包含 trace_id ...
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"群 {group_id} 自动分析失败: {e}")
|
||||||
|
|
||||||
|
# ← 方案 2:手动设置/清理(传统方式)
|
||||||
|
# set_trace_id(group_id)
|
||||||
|
# try:
|
||||||
|
# logger.info(f"开始为群 {group_id} 执行自动分析")
|
||||||
|
# # ... 分析逻辑 ...
|
||||||
|
# finally:
|
||||||
|
# clear_trace_id()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 修改文件:src/utils/helpers.py
|
||||||
|
|
||||||
|
在 `MessageAnalyzer` 中使用 TraceID:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# helpers.py
|
||||||
|
from src.utils.trace import with_trace_id
|
||||||
|
|
||||||
|
class MessageAnalyzer:
|
||||||
|
async def analyze_messages(
|
||||||
|
self,
|
||||||
|
messages: list[dict],
|
||||||
|
group_id: str,
|
||||||
|
unified_msg_origin: str = None
|
||||||
|
) -> dict:
|
||||||
|
"""完整的消息分析流程"""
|
||||||
|
|
||||||
|
with with_trace_id(group_id):
|
||||||
|
try:
|
||||||
|
logger.info("开始消息分析")
|
||||||
|
|
||||||
|
# 基础统计
|
||||||
|
statistics = await asyncio.to_thread(...)
|
||||||
|
logger.info(f"统计完成:{len(messages)} 条消息")
|
||||||
|
|
||||||
|
# LLM 分析
|
||||||
|
topics, user_titles, golden_quotes, token_usage = \
|
||||||
|
await self.llm_analyzer.analyze_all_concurrent(...)
|
||||||
|
logger.info(f"LLM 分析完成")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"statistics": statistics,
|
||||||
|
"topics": topics,
|
||||||
|
"user_titles": user_titles,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"消息分析失败: {e}")
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 日志输出效果
|
||||||
|
|
||||||
|
启动后,日志会自动包含 trace_id:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ python main.py
|
||||||
|
|
||||||
|
[10:30:45] [Plug] [INFO ] [auto_scheduler:100]: [123456789-1707292800] 开始为群 123456789 执行自动分析
|
||||||
|
[10:30:45] [Plug] [INFO ] [message_handler:50]: [123456789-1707292800] 开始获取消息
|
||||||
|
[10:30:46] [Plug] [INFO ] [message_handler:100]: [123456789-1707292800] 获取成功,共 256 条消息
|
||||||
|
[10:30:47] [Plug] [INFO ] [helpers:200]: [123456789-1707292800] 开始消息分析
|
||||||
|
[10:30:48] [Plug] [INFO ] [llm_analyzer:300]: [123456789-1707292800] 开始 LLM 分析
|
||||||
|
[10:30:50] [Plug] [INFO ] [llm_analyzer:350]: [123456789-1707292800] LLM 分析完成
|
||||||
|
[10:30:51] [Plug] [INFO ] [report_generator:400]: [123456789-1707292800] 报告生成中
|
||||||
|
[10:30:53] [Plug] [INFO ] [auto_scheduler:450]: [123456789-1707292800] 分析完成,耗时 8s
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 查看日志
|
||||||
|
|
||||||
|
### 终端实时查看
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 看所有日志
|
||||||
|
tail -f logs/astrbot.log
|
||||||
|
|
||||||
|
# 看特定群的日志
|
||||||
|
tail -f logs/astrbot.log | grep "123456789"
|
||||||
|
|
||||||
|
# 或使用 grep 搜索
|
||||||
|
grep "123456789-1707292800" logs/astrbot.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 按 TraceID 追踪完整链路
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取某个 trace_id 的所有日志
|
||||||
|
grep "123456789-1707292800" logs/astrbot.log
|
||||||
|
|
||||||
|
# 按时间戳统计分析耗时
|
||||||
|
# trace_id 格式 {group_id}-{start_timestamp}
|
||||||
|
# 可从日志时间戳对比,计算耗时
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 成本统计
|
||||||
|
|
||||||
|
| 项目 | 工作量 | 难度 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 新增 trace.py | ~80 行 | 低 |
|
||||||
|
| 修改 main.py | 3 行 | 低 |
|
||||||
|
| 修改 auto_scheduler.py | ~10 行 | 低 |
|
||||||
|
| 修改 helpers.py | ~5 行 | 低 |
|
||||||
|
| 修改其他文件 | 可选(向后兼容) | 低 |
|
||||||
|
| **总计** | **~100 行** | **低** |
|
||||||
|
|
||||||
|
## 8. 向后兼容性
|
||||||
|
|
||||||
|
✅ **完全向后兼容**
|
||||||
|
|
||||||
|
- 现有代码无需改动
|
||||||
|
- 新增代码仅在初始化时注册 Filter
|
||||||
|
- 所有日志输出不变,仅在 record 对象中添加 trace_id 字段
|
||||||
|
- 如果日志格式未修改,trace_id 不会显示,但在代码中可以访问
|
||||||
|
|
||||||
|
## 9. 扩展应用
|
||||||
|
|
||||||
|
### 应用 1:性能分析
|
||||||
|
|
||||||
|
```python
|
||||||
|
from src.utils.trace import get_trace_id
|
||||||
|
import time
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
# ... 某个操作 ...
|
||||||
|
elapsed = time.time() - start
|
||||||
|
logger.info(f"操作耗时 {elapsed:.2f}s")
|
||||||
|
# 日志自动包含 trace_id,可后续统计所有群的平均耗时
|
||||||
|
```
|
||||||
|
|
||||||
|
### 应用 2:错误统计
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 找出所有失败的 trace_id
|
||||||
|
grep "ERROR" logs/astrbot.log | awk -F'[]' '{print $2}' | sort | uniq -c
|
||||||
|
|
||||||
|
# 输出:
|
||||||
|
# 3 123456789-1707292800
|
||||||
|
# 2 987654321-1707292801
|
||||||
|
# → 表示这两个分析任务各失败了 3 次和 2 次
|
||||||
|
```
|
||||||
|
|
||||||
|
### 应用 3:分布式追踪
|
||||||
|
|
||||||
|
如果以后扩展到分布式部署,trace_id 可以传递到其他服务:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 跨服务调用时传递 trace_id
|
||||||
|
async def call_external_service(url, data):
|
||||||
|
headers = {"X-Trace-ID": get_trace_id()}
|
||||||
|
return await http_client.post(url, json=data, headers=headers)
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# QQ群日常分析插件 - PDF 功能说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本插件现已支持生成 PDF 格式的群聊分析报告,提供更专业的报告输出格式。PDF 引擎已迁移至 Playwright,提供更好的兼容性。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 📄 **PDF 报告生成**: 将群聊分析结果导出为精美的 PDF 文档
|
||||||
|
- 🎨 **专业排版**: 针对 PDF 打印优化的样式设计
|
||||||
|
- 📊 **完整内容**: 包含所有分析数据(统计信息、话题分析、用户称号、群聊金句)
|
||||||
|
- 🔧 **灵活配置**: 可自定义输出目录、文件名格式以及**自定义浏览器路径**
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
使用 PDF 功能前,需要安装 playwright 库。有以下几种安装方式:
|
||||||
|
|
||||||
|
### 方式一:使用命令安装(推荐)
|
||||||
|
|
||||||
|
在群聊中直接使用安装命令:
|
||||||
|
|
||||||
|
```
|
||||||
|
/安装PDF
|
||||||
|
```
|
||||||
|
|
||||||
|
此命令会自动:
|
||||||
|
- 检查 playwright 是否已安装
|
||||||
|
- 如未安装,自动安装 playwright 库
|
||||||
|
- 检查是否需要下载 Chromium 浏览器(已配置自定义路径则跳过)
|
||||||
|
- 测试 PDF 功能是否正常
|
||||||
|
|
||||||
|
### 方式二:手动安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install playwright
|
||||||
|
playwright install chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 设置输出格式
|
||||||
|
|
||||||
|
使用新增的 `/设置格式` 命令来切换输出格式:
|
||||||
|
|
||||||
|
```
|
||||||
|
/设置格式 pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 生成 PDF 报告
|
||||||
|
|
||||||
|
设置为 PDF 格式后,使用常规的分析命令:
|
||||||
|
|
||||||
|
```
|
||||||
|
/群分析
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 高级配置:自定义浏览器路径
|
||||||
|
|
||||||
|
如果您希望使用系统中已有的 Chrome 或 Edge,或者在特定的路径下运行浏览器,可以在配置中设置 `browser_path`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"browser_path": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 1. Playwright 安装失败
|
||||||
|
|
||||||
|
请确保您的系统网络能够访问官网下载浏览器内核。如果是在受限环境下,建议使用系统的 Chrome/Edge 并配置 `browser_path`。
|
||||||
|
|
||||||
|
### 2. PDF 生成失败
|
||||||
|
|
||||||
|
- 检查 `browser_path` 是否正确且可执行。
|
||||||
|
- 确保系统安装了必要的字体。
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.2.0
|
||||||
|
- 🚀 **Playwright 迁移**: 替换了不稳定的 pyppeteer。
|
||||||
|
- 🛠️ **自定义浏览器**: 支持手动指定 Chrome/Edge 路径。
|
||||||
|
- ⚡ **安装优化**: 自动检测系统浏览器并减少不必要的下载。
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
PDF 生成基于以下技术:
|
||||||
|
- **Playwright**: 现代无头浏览器控制
|
||||||
|
- **HTML/CSS**: 报告模板和样式
|
||||||
|
- **异步处理**: 非阻塞的 PDF 生成过程
|
||||||
Reference in New Issue
Block a user