diff --git a/.gitignore b/.gitignore index 46f4eed..7cb575a 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,35 @@ src/utils/__pycache__/pdf_utils.cpython-311.pyc src/visualization/__pycache__/__init__.cpython-311.pyc src/visualization/__pycache__/activity_charts.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 diff --git a/docs/01_requirements_analysis.md b/docs/01_requirements_analysis.md new file mode 100644 index 0000000..4fce0e9 --- /dev/null +++ b/docs/01_requirements_analysis.md @@ -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 升级带来的兼容性风险。 diff --git a/docs/02_architecture_design.md b/docs/02_architecture_design.md new file mode 100644 index 0000000..9ec278b --- /dev/null +++ b/docs/02_architecture_design.md @@ -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` 的一次性延时任务中,复用框架能力。 diff --git a/docs/03_domain_model.md b/docs/03_domain_model.md new file mode 100644 index 0000000..24b144a --- /dev/null +++ b/docs/03_domain_model.md @@ -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) +``` diff --git a/docs/04_infrastructure_layer.md b/docs/04_infrastructure_layer.md new file mode 100644 index 0000000..4ba14ad --- /dev/null +++ b/docs/04_infrastructure_layer.md @@ -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) +``` diff --git a/docs/05_refactoring_roadmap.md b/docs/05_refactoring_roadmap.md new file mode 100644 index 0000000..7b24c2f --- /dev/null +++ b/docs/05_refactoring_roadmap.md @@ -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. **配置清理**: + - 确保所有配置变更向后兼容。 + +**验收标准**: +- 插件启动无硬编码等待。 +- 定时任务准确触发。 diff --git a/docs/06_review.md b/docs/06_review.md new file mode 100644 index 0000000..b120370 --- /dev/null +++ b/docs/06_review.md @@ -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 架构体系。 + diff --git a/docs/TRACEID_IMPLEMENTATION.md b/docs/TRACEID_IMPLEMENTATION.md new file mode 100644 index 0000000..a1711bd --- /dev/null +++ b/docs/TRACEID_IMPLEMENTATION.md @@ -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) +``` + diff --git a/docs/pdf_feature_guide.md b/docs/pdf_feature_guide.md new file mode 100644 index 0000000..b6653be --- /dev/null +++ b/docs/pdf_feature_guide.md @@ -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 生成过程