chore: docs

This commit is contained in:
SXP-Simon
2026-04-01 21:23:02 +08:00
parent 05836f64f2
commit 692df4b0c9
20 changed files with 2835 additions and 8273 deletions
+2 -1
View File
@@ -31,4 +31,5 @@ scripts/data/avatar/cache.db
debug_miku.html
scripts/output_all/debug_HatsuneMiku_test_group_*.pdf
scripts/output/mock_report_test_group_mock_*.pdf
astrbot-lark-group-daily-analysis-main/**
astrbot-lark-group-daily-analysis-main/**
.ace-tool/
-45
View File
@@ -1,45 +0,0 @@
# 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 升级带来的兼容性风险。
-96
View File
@@ -1,96 +0,0 @@
# 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` 的一次性延时任务中,复用框架能力。
-65
View File
@@ -1,65 +0,0 @@
# 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)
```
-105
View File
@@ -1,105 +0,0 @@
# 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)
```
-61
View File
@@ -1,61 +0,0 @@
# 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. **配置清理**:
- 确保所有配置变更向后兼容。
**验收标准**:
- 插件启动无硬编码等待。
- 定时任务准确触发。
-361
View File
@@ -1,361 +0,0 @@
# 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 架构体系。
-698
View File
@@ -1,698 +0,0 @@
# 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/
@@ -1,968 +0,0 @@
# 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/
@@ -1,796 +0,0 @@
# 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
-317
View File
@@ -1,317 +0,0 @@
# 11. DDD 重构实施状态文档 (DDD Refactoring Implementation Status)
> **文档日期**: 2026-02-08
> **版本**: v2.1
> **状态**: Phase 2 完成 (100%)
---
## 1. 实施概述
### 1.1 架构决策记录 (ADR)
#### ADR-001: 采用渐进式集成而非完全重构
**背景**: 原计划对所有分析器进行完全重构以使用 UnifiedMessage 格式。
**决策**: 采用渐进式集成方式:
- 新建 DDD 分层结构 (domain/infrastructure/application)
- 现有分析器代码保持不变
- 通过 MessageConverter 提供双向转换
- AnalysisOrchestrator 作为新旧代码的桥梁
**原因**:
1. 现有分析器代码已经稳定运行
2. 完全重构风险高,可能引入新 bug
3. 渐进式迁移允许逐步验证
4. 保持向后兼容性
**后果**:
- 正面:风险低,可逐步迁移
- 负面:短期内存在两套消息格式
---
## 2. 已实现的架构层
### 2.1 领域层 (Domain Layer) ✅ 100%
```
src/domain/
├── __init__.py
├── exceptions.py # 领域异常层次结构
├── entities/
│ ├── __init__.py
│ ├── analysis_task.py # 分析任务聚合根
│ └── analysis_result.py # 分析结果实体
├── value_objects/
│ ├── __init__.py
│ ├── unified_message.py # 统一消息格式 (核心)
│ ├── platform_capabilities.py # 平台能力声明
│ ├── unified_group.py # 统一群组/成员信息
│ ├── topic.py # 话题值对象
│ ├── user_title.py # 用户称号值对象
│ ├── golden_quote.py # 金句值对象
│ └── statistics.py # 统计数据值对象
├── services/ # ✅ 完整实现
│ ├── __init__.py
│ ├── statistics_calculator.py # 统计计算服务
│ ├── report_generator.py # 报告生成服务
│ ├── topic_analyzer.py # 话题分析服务接口与适配器 ✅ NEW
│ ├── user_title_analyzer.py # 用户称号分析服务接口与适配器 ✅ NEW
│ └── golden_quote_analyzer.py # 金句分析服务接口与适配器 ✅ NEW
└── repositories/
├── __init__.py
├── message_repository.py # IMessageRepository, IMessageSender, IGroupInfoRepository
└── avatar_repository.py # IAvatarRepository
```
**新增值对象**:
- `Topic`: 不可变话题值对象,支持 from_dict/to_dict
- `UserTitle`: 不可变用户称号值对象,平台无关的用户ID
- `GoldenQuote`: 不可变金句值对象
- `TokenUsage`, `EmojiStatistics`, `GroupStatistics`, `UserStatistics`: 统计相关值对象
**新增领域服务**:
- `StatisticsCalculator`: 从 UnifiedMessage 计算群聊统计
- `ReportGenerator`: 生成平台无关的分析报告
- `ITopicAnalyzer` + `TopicAnalyzerAdapter`: 话题分析服务接口与适配器
- `IUserTitleAnalyzer` + `UserTitleAnalyzerAdapter`: 用户称号分析服务接口与适配器
- `IGoldenQuoteAnalyzer` + `GoldenQuoteAnalyzerAdapter`: 金句分析服务接口与适配器
**新增领域异常**:
- `DomainException` 基类
- `AnalysisException`, `LLMException`, `PlatformException` 等层次结构
**关键设计**:
- `UnifiedMessage`: 不可变值对象,所有平台消息的统一抽象
- `PlatformCapabilities`: 声明式能力描述,支持运行时能力检查
- Repository 接口:定义平台无关的数据访问契约
### 2.2 基础设施层 (Infrastructure Layer) ✅ 100%
```
src/infrastructure/
├── __init__.py
├── platform/
│ ├── __init__.py
│ ├── base.py # PlatformAdapter 基类
│ ├── factory.py # PlatformAdapterFactory 工厂
│ └── adapters/
│ ├── __init__.py
│ └── onebot_adapter.py # OneBot v11 完整实现
├── persistence/ # ✅ NEW
│ ├── __init__.py
│ └── history_repository.py # 历史记录存储实现
├── llm/ # ✅ NEW
│ ├── __init__.py
│ └── llm_client.py # LLM 客户端封装
├── config/ # ✅ NEW
│ ├── __init__.py
│ └── config_manager.py # 集中配置管理
└── resilience/ # ✅ NEW
├── __init__.py
├── circuit_breaker.py # 熔断器
├── rate_limiter.py # 令牌桶限流器
└── retry.py # 指数退避重试
```
**新增组件**:
- `HistoryRepository`: JSON 文件存储,支持按日期查询历史
- `LLMClient`: 封装 AstrBot 的 LLM provider 系统
- `ConfigManager`: 统一配置访问,支持点号分隔的键路径
- `CircuitBreaker`: 熔断器模式,防止级联故障
- `RateLimiter`: 令牌桶算法限流
- `retry_async`: 指数退避重试装饰器
**关键设计**:
- `PlatformAdapter`: 组合所有 Repository 接口的抽象基类
- `OneBotAdapter`: 完整实现消息获取、发送、群组信息、头像获取
- `PlatformAdapterFactory`: 注册表模式,支持动态添加新平台
**支持的平台**:
- ✅ OneBot v11 (aiocqhttp) - 完整实现
- 🔲 Telegram - 预留接口
- 🔲 Discord - 预留接口
- 🔲 Slack - 预留接口
### 2.3 应用层 (Application Layer) ✅ 100%
```
src/application/
├── __init__.py
├── analysis_orchestrator.py # 分析流程编排器
├── message_converter.py # 消息格式转换器
├── scheduling_service.py # 定时任务服务 ✅ NEW
└── reporting_service.py # 报告服务 ✅ NEW
```
**新增服务**:
- `SchedulingService`: 定时任务管理,支持按时间调度分析
- `ReportingService`: 报告生成和存储协调服务
**关键设计**:
- `AnalysisOrchestrator`:
- 使用 PlatformAdapter 获取消息 (DDD 方式)
- 提供 `fetch_messages_as_raw()` 兼容现有分析器
- 封装平台能力检查逻辑
- `MessageConverter`:
- `from_onebot_message()`: OneBot dict → UnifiedMessage
- `to_onebot_message()`: UnifiedMessage → OneBot dict
- `unified_to_analysis_text()`: 生成 LLM 分析用文本
### 2.4 共享层 (Shared Layer) ✅ NEW
```
src/shared/
├── __init__.py
├── constants.py # 全局常量定义
└── trace_context.py # 请求追踪上下文
```
**新增组件**:
- `constants.py`: 平台标识、任务状态、错误码等常量
- `TraceContext`: 请求追踪,支持 context manager 和装饰器
### 2.5 核心层集成 (Core Layer Integration) ✅
**BotManager 重构**:
- 自动创建 `PlatformAdapter` alongside bot instances
- 新增 `get_adapter()`, `has_adapter()`, `can_analyze()` 方法
- 新增 `_detect_platform_name()` 自动平台检测
- `get_status_info()` 包含 adapter 信息
```python
# 使用示例
adapter = bot_manager.get_adapter(platform_id)
if adapter:
caps = adapter.get_capabilities()
if caps.can_analyze():
messages = await adapter.fetch_messages(group_id, days=1)
```
---
## 3. 与原设计文档的差异
### 3.1 文档 09 vs 实际实现
| 原设计 | 实际实现 | 原因 |
|--------|----------|------|
| 完全重构分析器 | 保持现有分析器 | 风险控制 |
| main.py 使用 AstrMessageEvent | 保持 AiocqhttpMessageEvent | 渐进式迁移 |
| 所有分析使用 UnifiedMessage | 通过 Converter 兼容 | 向后兼容 |
### 3.2 后续迁移路径
1. **Phase 1 (当前)**: DDD 基础架构就位,现有代码不变
2. **Phase 2**: 新功能使用 DDD 架构开发
3. **Phase 3**: 逐步将现有分析器迁移到 UnifiedMessage
4. **Phase 4**: 移除 MessageConverter,完成迁移
---
## 4. 验证状态
### 4.1 Docker 容器验证 ✅
```bash
# 验证命令
docker exec astrbot python -c "
from src.domain.value_objects import UnifiedMessage, PlatformCapabilities
from src.infrastructure.platform import PlatformAdapterFactory
from src.application import AnalysisOrchestrator, MessageConverter
print('All imports successful!')
print(f'Supported platforms: {PlatformAdapterFactory.get_supported_platforms()}')
"
# 输出
All DDD layer imports successful!
Supported platforms: ['aiocqhttp', 'onebot']
```
### 4.2 待验证项
- [ ] 完整分析流程端到端测试
- [ ] OneBotAdapter 消息获取实际测试
- [ ] 报告生成与发送测试
---
## 5. 使用指南
### 5.1 新代码使用 DDD 架构
```python
from src.infrastructure.platform import PlatformAdapterFactory
from src.application import AnalysisOrchestrator, AnalysisConfig
# 创建适配器
adapter = PlatformAdapterFactory.create("aiocqhttp", bot_instance, config)
# 创建编排器
orchestrator = AnalysisOrchestrator(adapter, AnalysisConfig(days=1))
# 检查能力
if orchestrator.can_analyze():
# 获取统一格式消息
messages = await orchestrator.fetch_messages(group_id)
# 或获取原始格式 (兼容现有分析器)
raw_messages = await orchestrator.fetch_messages_as_raw(group_id)
```
### 5.2 现有代码保持不变
现有的 `MessageHandler`, `MessageAnalyzer`, `LLMAnalyzer` 等继续使用原始 dict 格式,无需修改。
---
## 6. Git 提交记录
| Commit | 描述 |
|--------|------|
| `c1d3bf5` | feat: add DDD architecture layers (domain, infrastructure, application) |
| `8d5d95a` | docs: add DDD implementation status and architecture decisions |
| `59ab291` | chore: simplify .gitignore with glob pattern for __pycache__ |
| `62a91a9` | refactor: integrate PlatformAdapterFactory into BotManager |
| `8f18783` | docs: update DDD implementation status to Phase 1 complete |
| `7ef58f9` | feat: complete DDD Phase 2 - domain services, infrastructure layers, shared |
| `39cc318` | feat: 完善 DDD 架构 - 添加领域分析器服务适配层 |
---
## 7. 下一步计划
1.~~将 BotManager 集成 PlatformAdapterFactory~~
2.~~添加 domain/value_objects (Topic, UserTitle, GoldenQuote, Statistics)~~
3.~~添加 domain/services (StatisticsCalculator, ReportGenerator)~~
4.~~添加 infrastructure 子模块 (persistence, llm, config, resilience)~~
5.~~添加 application 服务 (SchedulingService, ReportingService)~~
6.~~添加 shared 组件 (constants, TraceContext)~~
7.~~添加 domain/services 分析器服务接口与适配器~~
8. 🔲 添加更多平台适配器 (Telegram, Discord) - 按需实现
9. 🔲 编写单元测试覆盖 DDD 层
10. 🔲 端到端测试完整分析流程
11. 🔲 逐步迁移现有分析器到 UnifiedMessage 格式
---
## 8. 架构验证
### 8.1 Docker 容器内验证通过 ✅
```bash
docker exec astrbot python -c "
from src.domain.value_objects import UnifiedMessage, PlatformCapabilities, Topic, UserTitle, GoldenQuote
from src.domain.services import StatisticsCalculator, ReportGenerator, ITopicAnalyzer, IUserTitleAnalyzer, IGoldenQuoteAnalyzer
from src.domain.repositories import IMessageRepository, IMessageSender, IGroupInfoRepository
from src.domain.entities import AnalysisTask, AnalysisResult, GroupAnalysisResult
from src.infrastructure.platform import PlatformAdapterFactory
from src.infrastructure.resilience import CircuitBreaker, RateLimiter
from src.application import AnalysisOrchestrator, MessageConverter
from src.shared.constants import Platform, TaskStatus, ContentType, ReportFormat
print('✅ 所有 DDD 层导入成功!')
"
```
-658
View File
@@ -1,658 +0,0 @@
# 平台接入开发指南
本文档说明如何为群聊日报分析插件接入新的消息平台。
## 目录
1. [架构概述](#架构概述)
2. [快速开始](#快速开始)
3. [详细步骤](#详细步骤)
4. [接口说明](#接口说明)
5. [最佳实践](#最佳实践)
6. [示例代码](#示例代码)
7. [测试指南](#测试指南)
---
## 架构概述
本插件采用 DDD(领域驱动设计)架构,通过平台适配器模式实现多平台支持:
```
┌─────────────────────────────────────────────────────────┐
│ 应用层 (Application) │
│ AnalysisOrchestrator │
└─────────────────────────┬───────────────────────────────┘
│ 使用
┌─────────────────────────────────────────────────────────┐
│ 基础设施层 (Infrastructure) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ PlatformAdapter (抽象基类) │ │
│ │ - fetch_messages() │ │
│ │ - send_text/image/file() │ │
│ │ - get_group_info() │ │
│ │ - convert_to_raw_format() │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ ▲ ▲ │
│ │ │ │ │
│ ┌────────┴───┐ ┌──────┴──────┐ ┌────┴────────┐ │
│ │OneBotAdapter│ │DiscordAdapter│ │ 新平台Adapter │ │
│ │ (QQ平台) │ │ (Discord) │ │ (待实现) │ │
│ └────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
```
### 核心组件
| 组件 | 路径 | 说明 |
|------|------|------|
| PlatformAdapter | `src/infrastructure/platform/base.py` | 平台适配器抽象基类 |
| PlatformAdapterFactory | `src/infrastructure/platform/factory.py` | 适配器工厂,管理注册和创建 |
| UnifiedMessage | `src/domain/value_objects/unified_message.py` | 统一消息格式 |
| PlatformCapabilities | `src/domain/value_objects/platform_capabilities.py` | 平台能力声明 |
---
## 快速开始
接入新平台只需 3 步:
### 步骤 1:创建适配器文件
```bash
# 在 adapters 目录下创建新文件
touch src/infrastructure/platform/adapters/your_platform_adapter.py
```
### 步骤 2:实现适配器类
```python
from ..base import PlatformAdapter
from ....domain.value_objects.platform_capabilities import PlatformCapabilities
class YourPlatformAdapter(PlatformAdapter):
def _init_capabilities(self) -> PlatformCapabilities:
return PlatformCapabilities(
platform_name="your_platform",
supports_message_history=True,
# ... 其他能力
)
# 实现所有抽象方法...
```
### 步骤 3:注册适配器
`factory.py``_register_adapters()` 函数中添加:
```python
try:
from .adapters.your_platform_adapter import YourPlatformAdapter
PlatformAdapterFactory.register("your_platform", YourPlatformAdapter)
except ImportError:
pass
```
---
## 详细步骤
### 1. 定义平台能力
首先,明确你的平台支持哪些功能。以 `Discord` 为例:
```python
from ....domain.value_objects.platform_capabilities import PlatformCapabilities
DISCORD_CAPABILITIES = PlatformCapabilities(
platform_name="discord", # 平台标识符
platform_version="api_v10", # 平台版本
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, # 是否支持文件消息
supports_reply_message=True, # 是否支持回复消息
max_text_length=2000, # 最大文本长度
max_image_size_mb=8.0, # 最大图片大小
supports_edit=True, # 是否支持编辑
supports_user_avatar=True, # 是否支持用户头像
supports_group_avatar=True, # 是否支持群头像
avatar_sizes=(16, 32, 64, 128, 256, 512, 1024, 2048, 4096), # 支持的头像尺寸
)
```
### 2. 实现消息获取
消息获取是分析的核心。你需要实现 `fetch_messages` 方法。
**以 Discord 为例:**
```python
async def fetch_messages(
self,
group_id: str,
days: int = 1,
max_count: int = 100,
before_id: Optional[str] = None,
) -> List[UnifiedMessage]:
"""
获取 Discord 频道消息历史
"""
if not discord:
logger.error("未安装 py-cord 库,无法使用 Discord 适配器")
return []
try:
channel_id = int(group_id)
channel = self._discord_client.get_channel(channel_id)
# ... 获取 channel 逻辑 ...
end_time = datetime.now()
start_time = end_time - timedelta(days=days)
messages = []
# 构建 history 参数
history_kwargs = {"limit": max_count, "after": start_time}
if before_id:
# 处理分页
try:
history_kwargs["before"] = discord.Object(id=int(before_id))
except ValueError:
pass
# 获取消息
async for msg in channel.history(**history_kwargs):
# 过滤机器人自己的消息(如果配置了 ID)
if self.bot_user_id and str(msg.author.id) == self.bot_user_id:
continue
unified = self._convert_message(msg, group_id)
if unified:
messages.append(unified)
# 按时间升序排序
messages.sort(key=lambda m: m.timestamp)
return messages
except Exception as e:
logger.error(f"获取 Discord 消息失败: {e}", exc_info=True)
return []
```
### 3. 实现消息转换
将平台原生消息转换为 `UnifiedMessage`。这是解耦的关键。
**以 Discord 为例:**
```python
def _convert_message(self, raw_msg: Any, group_id: str) -> Optional[UnifiedMessage]:
"""
将 Discord 消息转换为统一格式
"""
try:
contents = []
# 1. 文本内容
if raw_msg.content:
contents.append(
MessageContent(type=MessageContentType.TEXT, text=raw_msg.content)
)
# 2. 附件处理 (图片/视频/文件)
for attachment in raw_msg.attachments:
content_type = attachment.content_type or ""
if content_type.startswith("image/"):
contents.append(
MessageContent(
type=MessageContentType.IMAGE, url=attachment.url
)
)
# ... 处理其他类型 ...
# 3. 嵌入内容 (Embeds)
for embed in raw_msg.embeds:
if embed.image:
contents.append(
MessageContent(
type=MessageContentType.IMAGE, url=embed.image.url
)
)
# ...
# 4. 贴纸 (Stickers)
if raw_msg.stickers:
for sticker in raw_msg.stickers:
contents.append(
MessageContent(
type=MessageContentType.IMAGE, # 贴纸视为图片
url=sticker.url,
# ...
)
)
# 构造 UnifiedMessage
return UnifiedMessage(
message_id=str(raw_msg.id),
sender_id=str(raw_msg.author.id),
sender_name=raw_msg.author.name, # 用户名
sender_card=getattr(raw_msg.author, "nick", None) or getattr(raw_msg.author, "global_name", None), # 优先显示服务器昵称
group_id=group_id,
text_content=raw_msg.content, # 用于 LLM 分析的纯文本
contents=tuple(contents),
timestamp=int(raw_msg.created_at.timestamp()),
platform="discord",
reply_to_id=str(raw_msg.reference.message_id) if raw_msg.reference else None,
)
except Exception as e:
logger.error(f"转换 Discord 消息失败: {e}")
return None
```
### 4. 实现原生格式转换
为了保持与现有分析器(如 `MessageHandler`)的向后兼容性,需要实现 `convert_to_raw_format`
```python
def convert_to_raw_format(self, messages: List[UnifiedMessage]) -> List[dict]:
"""
将统一消息格式转换为 OneBot 兼容格式 (用于兼容 MessageHandler)
"""
raw_messages = []
for msg in messages:
# 构造 OneBot 风格的消息字典
raw_msg = {
"message_id": msg.message_id,
"group_id": msg.group_id,
"time": msg.timestamp,
"sender": {
"user_id": msg.sender_id,
"nickname": msg.sender_name,
"card": msg.sender_card,
},
"message": [],
}
# 构造消息链
for content in msg.contents:
if content.type == MessageContentType.TEXT:
raw_msg["message"].append(
{"type": "text", "data": {"text": content.text}}
)
elif content.type == MessageContentType.IMAGE:
raw_msg["message"].append(
{
"type": "image",
"data": {"url": content.url, "file": content.url},
}
)
# ... 其他类型 ...
raw_messages.append(raw_msg)
return raw_messages
```
### 5. 实现消息发送
实现发送文本、图片等功能。
**以 Discord 为例:**
```python
async def send_image(
self,
group_id: str,
image_path: str,
caption: str = "",
) -> bool:
"""发送图片到 Discord 频道"""
# ... 获取 channel ...
try:
# 处理本地文件或 URL
file_to_send = None
if image_path.startswith(("http://", "https://")):
# URL 方式,需要下载图片后作为文件发送
# 因为 Discord 无法访问内部/本地 URL
import aiohttp
from io import BytesIO
async with aiohttp.ClientSession() as session:
async with session.get(image_path) as response:
if response.status == 200:
image_data = await response.read()
# ...
file_to_send = discord.File(BytesIO(image_data), filename="report.png")
else:
# 本地文件
file_to_send = discord.File(image_path)
if file_to_send:
await channel.send(content=caption if caption else None, file=file_to_send)
return True
except Exception as e:
logger.error(f"Discord 发送图片失败: {e}")
return False
```
### 6. 实现群组和成员信息获取
实现 `get_group_info``get_group_list``get_member_list` 等方法,以便插件可以自动发现群组并获取成员信息。
**以 Discord 为例:**
```python
async def get_group_info(self, group_id: str) -> Optional[UnifiedGroup]:
"""获取 Discord 频道信息"""
# ... 获取 channel ...
# 尝试获取 Guild 信息
guild = getattr(channel, "guild", None)
group_name = getattr(channel, "name", str(channel.id))
if guild:
member_count = guild.member_count
owner_id = str(guild.owner_id)
else:
# 私信
member_count = len(getattr(channel, "recipients", [])) + 1
owner_id = None
return UnifiedGroup(
group_id=str(channel.id),
group_name=group_name,
member_count=member_count,
owner_id=owner_id,
create_time=int(channel.created_at.timestamp()),
platform="discord",
)
```
### 7. 实现头像获取
实现 `get_user_avatar_url` 等方法,用于生成报告时的头像显示。
**以 Discord 为例:**
```python
async def get_user_avatar_url(
self,
user_id: str,
size: int = 100,
) -> Optional[str]:
"""获取 Discord 用户头像 URL"""
# ... 获取 user ...
if user:
# 调整 size 到最接近的 2 的幂次方 (Discord 要求)
allowed_sizes = [16, 32, 64, 128, 256, 512, 1024, 2048, 4096]
target_size = min(allowed_sizes, key=lambda x: abs(x - size))
# display_avatar 自动处理默认头像
return user.display_avatar.with_size(target_size).url
return None
```
---
## 接口说明
### PlatformAdapter 必须实现的方法
| 方法 | 说明 | 返回类型 |
|------|------|----------|
| `_init_capabilities()` | 初始化平台能力 | `PlatformCapabilities` |
| `fetch_messages()` | 获取消息历史 | `List[UnifiedMessage]` |
| `convert_to_raw_format()` | 转换为原生格式 | `List[dict]` |
| `send_text()` | 发送文本 | `bool` |
| `send_image()` | 发送图片 | `bool` |
| `send_file()` | 发送文件 | `bool` |
| `get_group_info()` | 获取群组信息 | `Optional[UnifiedGroup]` |
| `get_group_list()` | 获取群组列表 | `List[str]` |
| `get_member_list()` | 获取成员列表 | `List[UnifiedMember]` |
| `get_member_info()` | 获取成员信息 | `Optional[UnifiedMember]` |
| `get_user_avatar_url()` | 获取头像 URL | `Optional[str]` |
| `get_user_avatar_data()` | 获取头像 Base64 | `Optional[str]` |
| `get_group_avatar_url()` | 获取群头像 URL | `Optional[str]` |
| `batch_get_avatar_urls()` | 批量获取头像 | `Dict[str, Optional[str]]` |
### UnifiedMessage 字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `message_id` | `str` | 消息唯一 ID |
| `sender_id` | `str` | 发送者 ID |
| `sender_name` | `str` | 发送者昵称 |
| `sender_card` | `Optional[str]` | 发送者群名片 |
| `group_id` | `str` | 群组 ID |
| `text_content` | `str` | 纯文本内容 |
| `contents` | `Tuple[MessageContent, ...]` | 消息内容列表 |
| `timestamp` | `int` | Unix 时间戳 |
| `platform` | `str` | 平台标识 |
| `reply_to_id` | `Optional[str]` | 回复的消息 ID |
### MessageContentType 枚举
| 类型 | 说明 |
|------|------|
| `TEXT` | 文本 |
| `IMAGE` | 图片 |
| `AT` | @某人 |
| `EMOJI` | 表情 |
| `REPLY` | 回复 |
| `FORWARD` | 转发 |
| `VOICE` | 语音 |
| `VIDEO` | 视频 |
| `FILE` | 文件 |
| `UNKNOWN` | 未知类型 |
---
## 最佳实践
### 1. 不要硬编码平台特定逻辑
**错误做法**
```python
# 在应用层硬编码平台判断
if platform == "qq":
messages = fetch_qq_messages()
elif platform == "discord":
messages = fetch_discord_messages()
```
**正确做法**
```python
# 使用适配器模式
adapter = PlatformAdapterFactory.create(platform_name, bot_instance, config)
messages = await adapter.fetch_messages(group_id, days, max_count)
```
### 2. 使用中文注释
所有代码注释必须使用中文:
```python
def fetch_messages(self, group_id: str, days: int = 1) -> List[UnifiedMessage]:
"""
获取群组消息历史
参数:
group_id: 群组 ID
days: 获取多少天内的消息
返回:
UnifiedMessage 列表
"""
```
### 3. 优雅处理异常
```python
async def fetch_messages(self, ...) -> List[UnifiedMessage]:
try:
# 正常逻辑
return messages
except SpecificError as e:
logger.warning(f"获取消息失败: {e}")
return []
except Exception:
# 不要让异常传播到上层
return []
```
### 4. 过滤机器人自己的消息
```python
# 在 __init__ 中保存机器人 ID
self.bot_user_id = config.get("bot_user_id", "")
# 在 fetch_messages 中过滤
if str(msg.author.id) == self.bot_user_id:
continue
```
### 5. 声明正确的平台能力
如果平台不支持某功能,在 `PlatformCapabilities` 中正确声明:
```python
PlatformCapabilities(
supports_message_history=False, # 不支持历史消息获取
max_message_history_days=0, # 无法获取历史消息
)
```
---
## 示例代码
完整的适配器示例请参考:
- **OneBot 适配器**QQ):`src/infrastructure/platform/adapters/onebot_adapter.py`
- **Discord 适配器**(骨架):`src/infrastructure/platform/adapters/discord_adapter.py`
---
## 测试指南
### 1. 单元测试
为适配器编写单元测试:
```python
# tests/unit/infrastructure/platform/test_your_adapter.py
import pytest
from src.infrastructure.platform.adapters.your_platform_adapter import YourPlatformAdapter
class TestYourPlatformAdapter:
def test_init_capabilities(self):
adapter = YourPlatformAdapter(mock_bot, {})
caps = adapter.get_capabilities()
assert caps.platform_name == "your_platform"
assert caps.supports_message_history == True
@pytest.mark.asyncio
async def test_fetch_messages(self):
adapter = YourPlatformAdapter(mock_bot, {})
messages = await adapter.fetch_messages("group_123", days=1)
assert isinstance(messages, list)
```
### 2. Docker 容器内验证
在 Docker 容器内验证适配器注册:
```bash
docker exec astrbot python -c "
from data.plugins.astrbot_plugin_qq_group_daily_analysis.src.infrastructure.platform import PlatformAdapterFactory
print('支持的平台:', PlatformAdapterFactory.get_supported_platforms())
print('Discord 支持:', PlatformAdapterFactory.is_supported('discord'))
"
```
### 3. 集成测试
确保适配器与 `AnalysisOrchestrator` 正确集成:
```python
from src.application.analysis_orchestrator import AnalysisOrchestrator
orchestrator = AnalysisOrchestrator.create_for_platform(
platform_name="your_platform",
bot_instance=bot,
config={},
)
assert orchestrator is not None
assert orchestrator.can_analyze() == True
```
---
## 常见问题
### Q: 如何处理平台特定的消息类型?
使用 `MessageContentType.UNKNOWN` 并在 `raw_data` 中保存原始数据:
```python
contents.append(MessageContent(
type=MessageContentType.UNKNOWN,
raw_data={"platform_specific_type": "sticker", "data": sticker_data}
))
```
### Q: 如何支持分页获取消息?
使用 `before_id` 参数:
```python
async def fetch_messages(self, ..., before_id: Optional[str] = None):
if before_id:
# 从此消息 ID 之前开始获取
messages = await api.get_history(before=before_id, limit=max_count)
else:
messages = await api.get_history(limit=max_count)
```
### Q: 如何处理不支持的功能?
在能力声明中标记为不支持,并在方法中返回空/默认值:
```python
# 能力声明
PlatformCapabilities(supports_member_list=False)
# 方法实现
async def get_member_list(self, group_id: str) -> List[UnifiedMember]:
return [] # 平台不支持,返回空列表
```
---
## 贡献检查清单
在提交 PR 之前,请确保:
- [ ] 适配器继承自 `PlatformAdapter`
- [ ] 实现了所有抽象方法
- [ ] 在工厂中注册了适配器
- [ ] 所有注释使用中文
- [ ] 编写了单元测试
- [ ] 在 Docker 容器内验证通过
- [ ] 更新了相关文档
---
*最后更新:2026-02-08*
-677
View File
@@ -1,677 +0,0 @@
# AstrBot 日志查看方式研究报告
## 概述
本文档详细说明了 AstrBot 的日志系统架构、查看方式、以及如何在代码中集成日志查看功能。
---
## 1. 日志文件默认位置
### 1.1 日志文件存储位置
根据配置,AstrBot 的日志文件存储在以下位置:
| 日志类型 | 默认位置 | 配置键 | 说明 |
|---------|--------|-------|------|
| **普通日志** | `data/logs/astrbot.log` | `log_file_path` | 主应用日志,记录应用运行信息 |
| **Trace 日志** | `data/logs/astrbot.trace.log` | `trace_log_path` | 链路追踪日志,记录请求跨度信息 |
### 1.2 日志文件配置参数
```python
# 配置文件位置: astrbot/core/config/default.py
DEFAULT_CONFIG = {
"log_level": "INFO", # 日志级别(DEBUG, INFO, WARNING, ERROR, CRITICAL
"log_file_enable": False, # 是否启用文件日志(默认禁用)
"log_file_path": "logs/astrbot.log", # 日志文件相对路径(相对于 data/ 目录)
"log_file_max_mb": 20, # 单个日志文件最大大小(MB
"trace_enable": False, # 是否启用 Trace 记录
"trace_log_enable": False, # 是否启用 Trace 文件日志
"trace_log_path": "logs/astrbot.trace.log", # Trace 日志文件路径
"trace_log_max_mb": 20, # Trace 日志文件最大大小
}
```
### 1.3 日志目录基路径
- **根数据目录**: `data/`
- **日志目录**: `data/logs/`
- **获取方法**Python 代码):
```python
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
log_dir = os.path.join(get_astrbot_data_path(), "logs")
```
### 1.4 日志文件轮转配置
当启用文件日志时,AstrBot 使用 `RotatingFileHandler`
- 最大单个文件大小:`log_file_max_mb`(默认 20MB
- 备份文件数量:3 个
- 超过大小后自动轮转:`astrbot.log.1`, `astrbot.log.2`, `astrbot.log.3`
---
## 2. Dashboard 中的日志查看功能
### 2.1 Dashboard 路由和 API
AstrBot Dashboard 提供以下日志相关的 REST API(代码位置:`astrbot/dashboard/routes/log.py`):
| API 端点 | 方法 | 功能 | 说明 |
|---------|------|------|------|
| `/api/live-log` | GET | 实时日志流 | Server-Sent Events (SSE) 连接,推送实时日志 |
| `/api/log-history` | GET | 日志历史 | 获取缓存的日志历史(JSON 格式) |
| `/api/trace/settings` | GET | Trace 设置查询 | 获取当前 Trace 启用状态 |
| `/api/trace/settings` | POST | Trace 设置更新 | 更新 Trace 启用/禁用状态 |
### 2.2 实时日志流(SSE
#### 连接方式
**前端代码**Vue.js,位置:`dashboard/src/stores/common.js`):
```javascript
fetch('/api/live-log', {
method: 'GET',
headers: {
'Content-Type': 'multipart/form-data',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
cache: 'no-cache',
}).then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
// 处理流式数据...
})
```
#### SSE 消息格式
后端返回的 SSE 消息格式(`astrbot/dashboard/routes/log.py`):
```
id: {timestamp}
data: {json_object}
```
JSON 对象结构:
```json
{
"type": "log",
"level": "INFO",
"data": "[12:34:56] [Core] [INFO] [file.py:123]: Log message",
"time": 1697787296.123456,
"uuid": "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
}
```
#### 日志缓存和重放
- **缓存大小**: 最多 500 条日志(常量 `CACHED_SIZE = 500`
- **缓存数据结构**: `deque(maxlen=500)`(环形缓冲区)
- **浏览器断网重连**: 发送 `Last-Event-ID` 请求头,服务端根据时间戳重放缺失的日志
### 2.3 Dashboard 前端界面
#### 控制台页面(Console
**路由**: `/console`
**组件**: [ConsolePage.vue](dashboard/src/views/ConsolePage.vue)
功能:
- ✅ 实时日志显示(通过 SSE 连接)
- ✅ 日志级别过滤(DEBUG, INFO, WARNING, ERROR, CRITICAL
- ✅ 自动滚动开关
- ✅ pip 包安装界面
日志样式:
```
[12:34:56] [Core] [INFO] [astrbot.py:123]: AstrBot started successfully
[12:34:57] [Plug] [WARN] [plugin.py:45]: Missing dependency
[12:35:00] [Core] [ERRO] [error.py:78]: Connection timeout [v4.14.4]
```
#### 链路追踪页面(Trace
**路由**: `/trace`
**组件**: [TracePage.vue](dashboard/src/views/TracePage.vue)
功能:
- ✅ 实时链路追踪显示
- ✅ Trace 启用/禁用开关
- ✅ Trace 事件详细信息展示
#### 日志显示器组件
**组件**: [ConsoleDisplayer.vue](dashboard/src/components/shared/ConsoleDisplayer.vue)
特性:
- 日志级别色彩标记
- ANSI 颜色代码转换为 HTML 样式
- 日志缓存最多保留 1000 条(configurable
- 自动滚动到最新日志
---
## 3. LogBroker 架构(发布-订阅模式)
### 3.1 LogBroker 类设计
**位置**: `astrbot/core/log.py`
```python
class LogBroker:
"""日志代理类, 用于缓存和分发日志消息"""
def __init__(self):
self.log_cache = deque(maxlen=CACHED_SIZE) # 环形缓冲区
self.subscribers: list[Queue] = [] # 订阅者列表
def register(self) -> Queue:
"""注册新的订阅者,返回一个队列用于接收日志"""
q = Queue(maxsize=CACHED_SIZE + 10)
self.subscribers.append(q)
return q
def unregister(self, q: Queue):
"""取消订阅"""
self.subscribers.remove(q)
def publish(self, log_entry: dict):
"""发布日志到所有订阅者(非阻塞方式)"""
self.log_cache.append(log_entry)
for q in self.subscribers:
try:
q.put_nowait(log_entry)
except asyncio.QueueFull:
pass # 订阅者队列满,丢弃该日志
```
### 3.2 工作流程图
```
日志记录器 (logger)
LogQueueHandler (日志处理器)
LogBroker.publish(log_entry)
├→ 添加到 log_cache(环形缓冲区)
└→ 分发给所有订阅者的队列
├→ Dashboard SSE 连接
├→ Trace 日志记录器
└→ 其他订阅者
```
### 3.3 日志项结构
```python
log_entry = {
"level": "INFO", # 日志级别
"time": 1697787296.123, # Unix 时间戳
"data": "Log message text", # 格式化的日志文本
}
```
### 3.4 在代码中集成 LogBroker
#### 启动应用时初始化
```python
# main.py 或 cmd_run.py
from astrbot.core import LogBroker, LogManager, logger
# 创建日志代理
log_broker = LogBroker()
# 将日志处理器连接到 LogBroker
LogManager.set_queue_handler(logger, log_broker)
# 传递给应用初始化器
core_lifecycle = InitialLoader(db, log_broker)
```
#### 在 Dashboard 中使用
```python
# dashboard/routes/log.py
class LogRoute(Route):
def __init__(self, context: RouteContext, log_broker: LogBroker) -> None:
self.log_broker = log_broker
# 注册 API 路由...
async def log(self) -> QuartResponse:
"""SSE 日志流"""
queue = self.log_broker.register() # 注册订阅者
try:
while True:
message = await queue.get() # 等待日志
yield _format_log_sse(message, current_ts)
finally:
self.log_broker.unregister(queue) # 取消订阅
```
---
## 4. Trace 日志系统
### 4.1 Trace 概念
Trace 日志用于记录**请求的整个链路**,包括:
- 跨度信息(span_id
- 请求发起者(sender_name
- 操作阶段(action
- 自定义字段(fields
### 4.2 Trace 启用配置
**配置项**
```python
"trace_enable": False, # 启用 Trace 记录
"trace_log_enable": False, # 启用 Trace 文件日志
"trace_log_path": "logs/astrbot.trace.log", # Trace 日志文件路径
```
**Dashboard 设置**: `/api/trace/settings` 端点可动态启用/禁用 Trace
### 4.3 使用 TraceSpan 记录链路
**代码位置**: `astrbot/core/utils/trace.py`
```python
from astrbot.core.utils.trace import TraceSpan
# 创建 Trace 跨度
span = TraceSpan(
name="group_analysis",
umo="qq_group",
sender_name="QQGroup:123456789",
message_outline="Daily analysis request"
)
# 记录不同阶段的操作
span.record("start", step="initialization")
span.record("process", data_count=1000)
span.record("end", result_code=200)
```
### 4.4 Trace 日志格式
**发布到 LogBroker**
```json
{
"type": "trace",
"level": "TRACE",
"time": 1697787296.123,
"span_id": "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx",
"name": "group_analysis",
"umo": "qq_group",
"sender_name": "QQGroup:123456789",
"message_outline": "Daily analysis request",
"action": "start",
"fields": {"step": "initialization"}
}
```
**写入文件**(JSON 格式,每行一条):
```json
[2024-01-01 12:34:56] {"type":"trace","span_id":"...","name":"group_analysis",...}
```
### 4.5 Trace 查询方式
1. **Dashboard UI** (`/trace` 路由)
- 实时查看所有 Trace 事件
- 可启用/禁用 Trace 记录
2. **日志文件查询**
- 文件位置:`data/logs/astrbot.trace.log`
- 使用 `jq` 或 Python 解析 NDJSON 格式
3. **span_id 查询** - 用于追踪单个请求
```bash
grep "span_id.*abc123" data/logs/astrbot.trace.log
```
---
## 5. 日志访问方式总结
### 5.1 实时日志查看
| 方式 | 说明 | 适用场景 |
|------|------|---------|
| **Dashboard Console** | 浏览器访问 `http://localhost:6185/#/console` | 实时监控、界面友好 |
| **REST API SSE** | `GET /api/live-log`Server-Sent Events | 集成第三方系统 |
| **文件直接查看** | `tail -f data/logs/astrbot.log` | 服务器终端查看 |
### 5.2 历史日志查看
| 方式 | 说明 | 命令/代码 |
|------|------|---------|
| **Dashboard History** | `GET /api/log-history` 返回缓存的日志 | 返回最近 500 条 |
| **文件查询** | 日志文件存储在 `data/logs/astrbot.log` | `grep` 或编辑器打开 |
| **日志分析** | Python/shell 脚本处理日志文件 | 自定义分析 |
### 5.3 Trace 日志查看
| 方式 | 说明 | 对应 API |
|------|------|---------|
| **Dashboard Trace** | 实时追踪链路,浏览器访问 `/trace` | SSE 推送 |
| **Trace 文件** | `data/logs/astrbot.trace.log`NDJSON 格式) | 离线分析 |
| **span_id 查询** | 按 span_id 追踪单个请求 | `grep` 搜索 |
---
## 6. 插件/群分析日志集成示例
### 6.1 为群分析日志添加 Trace ID
对于"QQ 群日常分析插件"`astrbot_plugin_qq_group_daily_analysis`),可以这样集成日志追踪:
```python
# main.py 或分析模块
from astrbot import logger
from astrbot.core.utils.trace import TraceSpan
class GroupAnalyzer:
def analyze_group(self, group_id: str):
# 创建追踪跨度
span = TraceSpan(
name="group_daily_analysis",
umo="qq_group",
sender_name=f"QQGroup:{group_id}",
message_outline=f"Daily analysis for group {group_id}"
)
try:
span.record("start", group_id=group_id)
logger.info(f"[GroupAnalysis] Starting analysis for group: {group_id}")
# 分析逻辑...
data = self._fetch_messages(group_id)
span.record("fetch_complete", message_count=len(data))
# 处理数据...
result = self._process_data(data)
span.record("process_complete", result_code=200)
logger.info(f"[GroupAnalysis] Analysis complete for {group_id}")
return result
except Exception as e:
span.record("error", error_type=type(e).__name__, error_msg=str(e))
logger.error(f"[GroupAnalysis] Error analyzing group {group_id}: {e}")
raise
```
### 6.2 查询特定群的日志
**在 Dashboard 中**
1. 打开 `/console` 页面
2. 输入日志过滤器(或查看所有日志)
3. 搜索 `GroupAnalysis` 或特定的 group_id
**通过命令行**
```bash
# 查找特定群的日志
grep "QQGroup:123456789" data/logs/astrbot.log
# 或查找 Trace 日志
grep "group_id.*123456789" data/logs/astrbot.trace.log
```
### 6.3 日志格式确保
在日志中需要包含:
- **时间戳**: 自动添加(格式 `HH:MM:SS`
- **日志级别**: DEBUG, INFO, WARNING, ERROR, CRITICAL
- **来源标记**: [Core] 或 [Plug]
- **文件和行号**: 自动添加
- **消息内容**: 手动添加
**示例日志行**
```
[12:34:56] [Plug] [INFO] [group_analyzer.py:145]: [GroupAnalysis] Analysis complete for QQGroup:123456789
```
---
## 7. LogManager 高级配置
### 7.1 配置日志级别
```python
from astrbot.core import LogManager, logger
# 根据配置设置日志级别
config = {
"log_level": "DEBUG",
"log_file_enable": True,
"log_file_path": "logs/astrbot.log",
"log_file_max_mb": 50,
}
LogManager.configure_logger(logger, config)
```
### 7.2 配置 Trace 日志
```python
# 启用 Trace 日志文件
config = {
"trace_enable": True,
"trace_log_enable": True,
"trace_log_path": "logs/astrbot.trace.log",
"trace_log_max_mb": 30,
}
LogManager.configure_trace_logger(config)
```
### 7.3 日志过滤器
LogManager 自动添加以下过滤器:
| 过滤器 | 功能 | 输出示例 |
|-------|------|--------|
| **PluginFilter** | 标记日志来源(Core/Plug | `[Core]` 或 `[Plug]` |
| **FileNameFilter** | 修改文件名格式 | `folder.filename` |
| **LevelNameFilter** | 4 字母缩写 | `DBUG`, `INFO`, `WARN`, `ERRO`, `CRIT` |
| **AstrBotVersionTagFilter** | 在 WARNING 及以上追加版本 | `[v4.14.4]` |
---
## 8. 日志配置管理
### 8.1 配置文件路径
- **配置文件**: `data/cmd_config.json`
- **默认配置**: `astrbot/core/config/default.py`
- **编辑方式**:
1. 直接编辑 JSON 文件
2. 通过 Dashboard 管理面板修改(未来功能)
### 8.2 配置更新
配置更改后,需要重启应用以生效(或通过 API 动态更新)。
### 8.3 环境变量支持
**根目录自定义**(可选):
```bash
export ASTRBOT_ROOT=/path/to/root
# 数据目录将为 /path/to/root/data
```
---
## 9. 代码示例汇总
### 9.1 获取日志记录器
```python
from astrbot.core import logger
# 已配置的全局日志记录器
logger.info("Message")
logger.debug("Debug message")
logger.warning("Warning")
logger.error("Error")
logger.critical("Critical error")
```
### 9.2 创建自定义日志记录器
```python
from astrbot.core import LogManager
# 获取命名日志记录器
plugin_logger = LogManager.GetLogger("my_plugin")
plugin_logger.info("Plugin message")
```
### 9.3 发起 Trace 追踪
```python
from astrbot.core.utils.trace import TraceSpan
span = TraceSpan(
name="custom_operation",
umo="custom_type",
sender_name="CustomOperator",
message_outline="Operation description"
)
span.record("stage1", param1="value1")
span.record("stage2", param2="value2", status="success")
```
### 9.4 订阅日志流(自定义)
```python
import asyncio
from astrbot.core import LogBroker
# 从 LogBroker 获取日志队列
log_broker = app.log_broker # 从应用上下文获取
async def listen_logs():
queue = log_broker.register()
try:
while True:
log_entry = await queue.get()
print(f"[{log_entry['level']}] {log_entry['data']}")
finally:
log_broker.unregister(queue)
# 运行监听器
asyncio.run(listen_logs())
```
---
## 10. 常见问题和最佳实践
### 10.1 为什么文件日志默认禁用?
- 性能考虑(避免磁盘 I/O 开销)
- 容器环境中不需要持久化
- 大多数用户通过 Dashboard 查看日志
### 10.2 启用文件日志的步骤
1. 编辑 `data/cmd_config.json`
```json
{
"log_file_enable": true,
"log_file_path": "logs/astrbot.log",
"log_file_max_mb": 50
}
```
2. 重启 AstrBot 应用
3. 日志将写入 `data/logs/astrbot.log`
### 10.3 日志级别选择
- **DEBUG**: 开发调试,包含所有详细信息
- **INFO**: 生产环境推荐,记录重要事件
- **WARNING**: 只记录警告和错误
- **ERROR**: 仅记录错误
- **CRITICAL**: 仅记录严重错误
### 10.4 Trace ID 用于群分析追踪
对于"QQ 群日常分析"场景:
- 每个群的分析请求都有唯一的 `span_id`
- 可通过此 ID 追踪整个分析流程
- 涉及多群并发时日志清晰分离
**查询示例**
```bash
# 查找 span_id 相关的所有日志
grep "span_id: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" data/logs/astrbot.trace.log
```
### 10.5 性能考虑
- **日志缓存**: 最多 500 条,超出自动淘汰(先进先出)
- **SSE 连接**: 断网自动重连,支持日志补发
- **文件轮转**: 单个文件超过 20MB(可配置)自动轮转
---
## 11. 相关文件速查表
| 功能 | 文件路径 |
|------|--------|
| 日志核心逻辑 | `astrbot/core/log.py` |
| 日志管理器 | `astrbot/core/log.py` (LogManager 类) |
| Trace 系统 | `astrbot/core/utils/trace.py` |
| Dashboard API | `astrbot/dashboard/routes/log.py` |
| 默认配置 | `astrbot/core/config/default.py` |
| 路径工具 | `astrbot/core/utils/astrbot_path.py` |
| Dashboard Console UI | `dashboard/src/views/ConsolePage.vue` |
| 日志显示器组件 | `dashboard/src/components/shared/ConsoleDisplayer.vue` |
| Trace UI | `dashboard/src/views/TracePage.vue` |
| 公共 Store | `dashboard/src/stores/common.js` |
---
## 12. 总结
### 日志查看的完整流程
1. **应用启动**
- LogBroker 初始化
- LogQueueHandler 连接到日志记录器
2. **日志产生**
- 应用或插件调用 `logger.info()` 等方法
- LogQueueHandler 拦截日志记录
3. **日志分发**
- LogBroker.publish() 添加到缓存
- 分发给所有订阅者(Dashboard SSE、Trace 日志等)
4. **用户查看**
- **Dashboard Console**: 实时看到日志
- **REST API**: 获取历史日志或 SSE 流
- **文件**: 直接查看 `data/logs/astrbot.log`
5. **Trace 追踪**
- 创建 TraceSpan 记录请求链路
- Dashboard `/trace` 实时查看
- 或通过 span_id 在文件中查询
### 推荐使用方式
- **开发调试**: Dashboard `/console` 页面
- **生产监控**: 启用文件日志 + 日志收集系统
- **问题诊断**: 通过 span_id 追踪完整请求链路
- **群分析**: 在日志中包含 group_id,便于后续查询
---
*本文档基于 AstrBot v4.14.4 代码分析生成。*
File diff suppressed because it is too large Load Diff
-332
View File
@@ -1,332 +0,0 @@
# 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)
```
+461
View File
@@ -0,0 +1,461 @@
# 14. 群聊记忆模块可执行性复盘与研究记录
## 1. 文档目的
本文档用于沉淀上一轮“群聊记忆模块提案”的后续研究结果,重点回答两个问题:
1. 这套方案在当前插件代码里是否真的能落地?
2. 在 AstrBot 宿主生态下,哪些能力可以直接复用,哪些地方必须调整?
这份文档不再重复完整愿景,而是保留讨论过程中的关键判断、约束、风险和修订方向,作为后续 Phase 1 设计与开发的依据。
## 2. 研究范围
本轮研究从两个方向并行展开:
### 2.1 插件内部可执行性
关注点包括:
- 现有分析链路有哪些真实接入点
- 当前数据模型是否足以支撑长期画像
- KV 持久化是否能承接记忆库
- 调度、并发、回退机制是否会被影响
### 2.2 AstrBot 宿主生态适配性
关注点包括:
- AstrBot 是否已经提供插件级 KV 和数据目录能力
- 宿主是否有可复用的 cron/scheduler 机制
- 会话、人格、UMO、消息历史这些基础设施如何复用
- 哪些宿主能力不适合拿来存长期记忆
## 3. 研究输入
本轮研究主要基于以下资料和代码:
- 群聊记忆总提案:`docs/group_memory_proposal.md`
- 插件应用层编排:`src/application/services/analysis_application_service.py`
- 插件增量存储:`src/infrastructure/persistence/incremental_store.py`
- 插件历史摘要存储:`src/infrastructure/persistence/history_manager.py`
- 插件消息入库:`src/application/services/message_processing_service.py`
- 插件配置管理:`src/infrastructure/config/config_manager.py`
- 插件最终报告生成:`src/domain/services/report_generator.py`
- AstrBot 插件 KV`astrbot/core/utils/plugin_kv_store.py`
- AstrBot 路径工具:`astrbot/core/utils/astrbot_path.py`
- AstrBot 共享偏好:`astrbot/core/utils/shared_preferences.py`
- AstrBot 定时任务管理:`astrbot/core/cron/manager.py`
- AstrBot 会话与 UMO`astrbot/core/platform/message_session.py`
- AstrBot 人格管理:`astrbot/core/persona_mgr.py`
## 4. 当前插件已经具备的“记忆底座”
研究后确认,插件并不是从零开始。
当前已经存在三层非常重要的中间能力:
### 4.1 原始消息层
插件在群消息到达时,已经通过 `MessageProcessingService` 将消息写入宿主的 `message_history_manager`
这意味着:
- 原始聊天记录已经有统一入口
- 记忆系统不需要再自己拦第二份原始消息日志
- 长期记忆应当是“结构化提炼层”,不是“原始历史副本层”
### 4.2 增量批次层
插件已经有成熟的增量分析能力:
- 增量分析时只处理新消息
- 生成 `IncrementalBatch`
- 将话题、金句、用户统计、参与者等中间结果落入 KV
- 最终报告时再按窗口合并
这一层非常适合作为长期记忆的“短期原料层”。
换句话说:
- `IncrementalBatch` 不是长期记忆本身
- 但它天然适合作为长期记忆抽取的输入
### 4.3 最终汇总层
插件在最终报告阶段已经能做:
- 滑动窗口合并
- 用户称号分析
- 质量锐评汇总
- 报告生成与发送
这使得“记忆检索增强最终总结”成为可能。
## 5. 插件侧核心研究结论
### 5.1 方案方向是成立的
结论很明确:
- 方案不是空中楼阁
- 现有插件结构足够支撑“长期记忆层”的加入
- 不需要推倒重写现有分析链路
真正的问题不在于“能不能做”,而在于“第一期要做到什么程度才可控”。
### 5.2 真实接入点只有三个
研究后确认,长期记忆闭环真正适合挂接的位置只有三处:
1. `execute_incremental_analysis()` 成功保存批次之后
2. `execute_incremental_final_report()` 生成最终总结之前
3. `execute_incremental_final_report()` 成功完成之后
它们分别对应:
- 批次后抽取
- 总结前检索
- 总结后刷新
这意味着提案中的记忆闭环应该收敛为:
```text
IncrementalBatch saved
-> extract candidate memories
-> update long-term memory
Final report begins
-> retrieve relevant memory digests
-> inject into summary prompt
Final report succeeds
-> refresh group/member snapshots
```
### 5.3 现在还没有“记忆上下文注入链路”
这是本轮 review 里最重要的结论之一。
现有 `ReportGenerator` 和 LLM 分析链路只关心:
- 统计数据
- 话题
- 用户称号
- 金句
- 聊天质量
但提案里想要的是:
- 群长期画像摘要
- 成员长期画像摘要
- 历史 episode 摘要
这些内容目前没有地方可以传进去。
所以如果要实现“记忆增强总结”,必须先新增一种中间结构,比如:
- `memory_context`
- `memory_digest`
- `retrieved_memory_bundle`
然后把它接进:
- 最终总结 prompt 构建
- 分析结果中间对象
- 可能的报告文案生成逻辑
### 5.4 成员长期画像所需信号还不够丰富
当前插件对用户的统计维度主要包括:
- 消息数
- 字符数
- 回复数
- 表情数
- 活跃时段
这些信号可以支持:
- 活跃用户画像
- 时段偏好
- 轻量角色归纳
但还不足以稳定支撑:
- 话题偏好图谱
- 风格特征
- 关系线索
- 长期行为漂移判断
因此提案需要修正:
- 第一阶段的成员画像只能做轻量版
- 更复杂的画像特征,需要后续新增规则抽取或单独 LLM 画像流程
### 5.5 `thread_scope_id` 暂时不适合作为首期目标
虽然从理论上,Telegram topic、Discord thread、子频道这些都应该成为独立子作用域,但当前插件的稳定主键仍然以 `group_id` 为核心。
研究中发现:
- 消息处理层当前只稳定存群级会话
- 调度层也是按群名单工作
- 增量批次与汇总模型也默认按群聚合
这意味着如果一开始就做 thread 级记忆,会显著扩大改造面:
- 消息侧要持久化更多上下文字段
- 调度侧要知道 thread scope
- 存储层要支持群内多层 scope
- 检索和回写逻辑要区分群画像与 thread 画像
因此结论是:
- 首期先做 `group_scope_id`
- thread / topic scope 作为 Phase 2 或 Phase 3 扩展
## 6. 宿主生态侧核心研究结论
### 6.1 插件 KV 完全可以承接第一版长期记忆
AstrBot 已经提供插件级 KV
- `put_kv_data`
- `get_kv_data`
- `delete_kv_data`
这意味着长期记忆第一版不需要引入新数据库。
但同时也有边界:
- 只适合 JSON 友好结构
- 不适合无限长单 key 写入
- 不提供复杂索引
- 不提供并发控制
因此要像当前 `IncrementalStore` 一样自行设计:
- 主体数据 key
- 索引 key
- 清理策略
- 分片策略
### 6.2 `plugin_data` 目录适合保存大快照或导出物
如果未来长期记忆需要:
- 调试快照
- 大型归档
- 导出文件
- 可能的 embedding 文件
则应当放到:
- `data/plugin_data/{plugin_name}/`
这与宿主规范一致,也有利于备份和迁移。
### 6.3 不应该再自己起一套新的 scheduler
AstrBot 已经有 `cron_manager`,其背后是:
- APScheduler
- DB-backed cron metadata
- 可追踪的执行状态
- 持久化的 next run 信息
因此长期记忆需要的:
- 每日 compact
- 每周深度重写
- 过期记忆清理
都应该挂到宿主 cron,而不是插件里再单独维护一个新定时器。
### 6.4 UMO / MessageSession / persona_manager 都可以复用
宿主已经提供:
- 标准 UMO 格式:`platform_id:message_type:session_id`
- `MessageSession`
- `persona_manager`
- `conversation_manager`
这意味着:
- 群记忆 scope 的主键应直接遵循 UMO 风格
- 总结时的人格选择逻辑不需要自造
- 记忆增强 prompt 应尊重当前会话对应的人格设定
### 6.5 SharedPreferences 不适合作为长期记忆主存储
这轮研究明确确认:
- SharedPreferences 更适合偏好设置
- 不适合作为长期记忆实体的主存储层
原因包括:
- 语义职责不匹配
- 有临时缓存清理机制
- 容易把配置型数据和记忆型数据混杂
正确做法应该是:
- 长期记忆主存储:插件 KV / plugin_data
- 会话人格、服务偏好等:继续走 SharedPreferences / persona_manager
## 7. 研究中沉淀出的主要风险
### 7.1 存储膨胀风险
如果每个群不断累积 episode 和记忆索引,KV 体积会不断扩大。
具体风险:
- episode 列表过长
- 成员画像过多
- 快照长期不重写
- 某些高活跃群写入频率过高
因此 compact 不是优化项,而是必要功能。
### 7.2 并发竞争风险
当前插件已经有较复杂的调度和增量分析流程。
如果长期记忆写入与增量批次写入同时发生,可能出现:
- 同 scope 下索引竞争
- 批次成功但记忆写入失败
- 定时清理与写入冲突
因此 MemoryStore 必须引入自己的 scope 级锁,不能裸写 KV。
### 7.3 画像过拟合风险
如果只用单日或少量消息更新长期画像,容易导致:
- 短期异常被错误固化
- 某次玩梗变成“长期角色”
- 某天沉默被误判为风格变化
因此需要:
- 候选记忆区
- 置信度机制
- 稳定画像与近期 delta 分离
### 7.4 集成链路复杂度风险
长期记忆如果直接改动所有 analyzer,会迅速扩大工程复杂度。
所以首期最稳妥的方案应限制在:
- 只增强最终总结
- 不强制所有分析模块都接入长期记忆
## 8. 研究后形成的关键修订
基于本轮 review,原始提案需要做以下收敛:
### 8.1 目标收敛
原始目标:
- 群级记忆
- 成员长期画像
- 事件记忆
- 多层作用域
- 安全治理
- 遗忘
- 复杂检索
修订后首期目标:
- 群级长期画像
- 成员轻量长期画像
- episode 事件记忆
- 群级作用域
- 基础 compact
- 最终总结增强
### 8.2 技术路线收敛
原始路线允许多种可能:
- KV
- 文件
- 向量库
- 图结构
修订后首期路线明确为:
- 主存储:插件 KV
- 大快照:plugin_data
- 不引入向量库
- 不引入图数据库
### 8.3 集成范围收敛
原始设想偏全链路增强。
修订后:
- 首期只动增量分析后、最终总结前后这三个接入点
- 首期不强行改造所有 analyzer
- 首期以 memory digest 注入最终总结为主
## 9. 当前最合理的 Phase 1 方向
研究后,我们认为最合理的第一阶段是:
### 9.1 做什么
-`group_scope_id` 构建群级记忆空间
-`IncrementalBatch` 抽取 episode 和记忆候选
- 维护 `GroupProfile`
- 维护 `MemberProfileLite`
- 最终总结前检索记忆摘要并增强 prompt
- 定时 compact 和记忆淘汰
### 9.2 不做什么
- 不做 thread/topic scope
- 不做复杂关系图谱
- 不做 embedding 检索
- 不做跨群用户统一画像
- 不做高风险人格推断
## 10. 对后续开发的直接指导
### 10.1 必须优先做的事情
1. 设计 `MemoryStore`
2. 设计 `memory` 配置组
3. 确定 `memory_digest` 在总结链路中的传递方式
4. 定义首期 `MemberProfileLite` 的字段边界
### 10.2 应优先避免的事情
- 一开始就追求“非常聪明”的画像系统
- 让长期记忆深度侵入所有 LLM analyzer
- 引入新的数据库依赖
- 在插件内重复造 scheduler
## 11. 结论
本轮研究最终得到的结论是:
- 群聊记忆模块在当前插件与 AstrBot 生态中是可执行的。
- 现有的增量批次、插件 KV、UMO、人格管理和宿主 cron 都为该方案提供了现实基础。
- 但原始提案的范围偏大,若直接照单全做,工程风险会明显上升。
因此更可取的路线是:
- 保留原提案的长期方向
- 用本轮研究结论收敛首期范围
- 先交付一版“真正能跑、能演化、能清理”的群级记忆系统
后续文档将基于本文的研究结果,给出修订后的 Phase 1 方案。
+653
View File
@@ -0,0 +1,653 @@
# 15. 群聊记忆模块 Phase 1 提案
## 1. 文档定位
本文档是基于以下两份前置文档收敛后的第一阶段设计:
- `docs/group_memory_proposal.md`
- `docs/group_memory_feasibility_review.md`
与总提案不同,本文只回答一个问题:
在当前插件代码与 AstrBot 宿主生态的真实边界下,第一阶段到底做什么、怎么做、做到什么程度算完成。
## 2. Phase 1 目标
### 2.1 目标描述
Phase 1 的目标不是做一个“完美的认知架构”,而是交付一个可运行、可演化、可清理的群级长期记忆系统。
它需要满足以下能力:
- 以群为天然作用域,维护长期记忆空间。
- 能从增量批次中抽取结构化长期记忆候选。
- 能维护群级长期画像。
- 能维护群内成员轻量长期画像。
- 能在最终总结前检索相关长期记忆,并增强总结 prompt。
- 能通过 compact / retention 机制控制存储膨胀。
### 2.2 交付标准
当 Phase 1 完成时,应具备以下可见效果:
- 同一个群的总结开始体现持续的群风格。
- 对同一个成员的描述不再完全依赖当天表现,而是出现轻量长期连续性。
- 总结中可以自然提到与近期历史相关的延续话题。
- 长期记忆不会无限增长,并且支持清理和重建。
## 3. Phase 1 范围
### 3.1 In Scope
- 群级记忆作用域:`group_scope_id`
- 事件型记忆:`Episode`
- 群画像:`GroupProfile`
- 成员轻量画像:`MemberProfileLite`
- 最终总结记忆增强
- 基础 compact / cleanup
- 基础安全过滤
### 3.2 Out of Scope
- thread / topic 级作用域
- 跨群统一成员画像
- embedding / vector retrieval
- 图谱式关系建模
- 高复杂度人格漂移模型
- 大规模原始聊天回放式记忆拼装
## 4. 核心设计原则
### 4.1 首期只做群级作用域
Phase 1 的长期记忆主键统一为:
```text
group_scope_id = "{platform_id}:GroupMessage:{group_id}"
```
说明:
- `platform_id` 使用 AstrBot 的标准平台实例 ID
- `GroupMessage` 保持与 UMO 格式一致
- `group_id` 与插件当前已有调度和增量分析逻辑保持一致
这样可以直接与当前插件工作流兼容。
### 4.2 记忆增强只影响最终总结
首期不要求所有 analyzer 都接入长期记忆。
仅在最终总结链路中引入长期记忆摘要:
- 群画像摘要
- 成员画像摘要
- 相关历史事件摘要
这样可以显著降低改造面。
### 4.3 长期画像只做轻量版
首期成员画像不追求复杂人格建模,只保留插件当前信号源能够稳定支持的维度。
## 5. Phase 1 数据模型
## 5.1 `GroupProfile`
用于描述一个群的长期稳定特征。
建议字段:
```json
{
"scope_id": "qq:GroupMessage:123456",
"summary": "这是一个以插件开发和部署交流为主,同时夹杂大量整活和吐槽的技术群。",
"tone_tags": ["技术讨论", "高频吐槽", "互助答疑"],
"recurring_topics": ["AstrBot", "插件开发", "平台适配", "LLM配置"],
"interaction_style": ["高信息密度", "熟人化交流", "延续性梗较多"],
"core_members": [
{"user_id": "u1", "role": "答疑核心"},
{"user_id": "u2", "role": "整活与吐槽"}
],
"confidence": 0.72,
"updated_at": 1710000000
}
```
### 5.2 `MemberProfileLite`
用于描述一个成员在某个群内的轻量长期画像。
建议字段:
```json
{
"scope_id": "qq:GroupMessage:123456",
"user_id": "789",
"display_name": "Simon",
"activity_traits": ["高频发言", "晚间活跃"],
"topic_preferences": ["插件架构", "调度逻辑", "兼容性问题"],
"style_traits": ["直接", "技术密度高", "偶尔吐槽"],
"role_tags": ["答疑者", "推进者"],
"confidence": 0.64,
"last_active_at": 1710000000,
"updated_at": 1710000000
}
```
注意:
- `style_traits` 在首期只允许用保守表达
- `role_tags` 必须是群内行为角色,不是现实人格标签
### 5.3 `Episode`
用于描述最近一段时间中值得记住的事件型记忆。
建议字段:
```json
{
"memory_id": "ep_xxx",
"scope_id": "qq:GroupMessage:123456",
"time_range": {
"start_ts": 1710000000,
"end_ts": 1710001200
},
"summary": "群里围绕飞书成员权限预热方案持续讨论,最终收敛到先执行缓存预检查的做法。",
"keywords": ["飞书", "权限", "缓存预热"],
"participants": ["u1", "u2", "u3"],
"importance": 0.81,
"source_batch_ids": ["batch_a", "batch_b"],
"expires_at": 1712592000
}
```
### 5.4 `MemorySnapshot`
用于在最终总结阶段快速构建记忆上下文,而不是每次都从零扫描大量条目。
建议字段:
```json
{
"scope_id": "qq:GroupMessage:123456",
"group_profile_digest": "...",
"member_profile_digests": [
{"user_id": "u1", "digest": "..."},
{"user_id": "u2", "digest": "..."}
],
"recent_episode_digest": [
{"memory_id": "ep_1", "digest": "..."},
{"memory_id": "ep_2", "digest": "..."}
],
"updated_at": 1710000000
}
```
## 6. 存储设计
### 6.1 存储介质
Phase 1 采用:
- 主存储:插件 KV
- 大快照与导出:`plugin_data`
不引入新数据库。
### 6.2 KV Key 设计
建议采用如下 key 结构:
```text
mem_group_profile_{scope_id}
mem_member_profile_index_{scope_id}
mem_member_profile_{scope_id}_{user_id}
mem_episode_index_{scope_id}
mem_episode_{scope_id}_{memory_id}
mem_snapshot_{scope_id}
mem_meta_{scope_id}
```
### 6.3 Index 设计
为了避免单 key 无限膨胀,所有列表型结构都采用:
- 单独索引 key
- 单条实体 key
`IncrementalStore` 的模式保持一致。
例如:
- `mem_episode_index_{scope_id}` 存储最近 episode 的 id + timestamp
- `mem_episode_{scope_id}_{memory_id}` 存储单个 episode 正文
### 6.4 并发控制
MemoryStore 必须具备 scope 级锁。
理由:
- 增量分析可能并发触发
- 最终报告与 compact 可能冲突
- KV 本身不提供锁
建议做法:
-`MemoryApplicationService` 内使用 `asyncio.Lock`
- lock key 为 `memory:{scope_id}`
## 7. 配置设计
建议新增 `memory` 配置组:
```yaml
memory:
enabled: true
enable_memory_in_summary: true
enable_group_profile: true
enable_member_profile_lite: true
max_episode_per_group: 120
max_member_profile_count: 200
episode_retention_days: 21
snapshot_refresh_interval_hours: 24
retrieve_episode_top_k: 4
retrieve_member_top_k: 5
min_profile_confidence: 0.45
enable_memory_safety_filter: true
```
## 8. 新增模块设计
## 8.1 `MemoryStore`
位置建议:
```text
src/infrastructure/persistence/memory_store.py
```
职责:
- 保存群画像
- 保存成员轻量画像
- 保存 episode
- 保存 snapshot
- 查询与清理
### 8.2 `MemoryApplicationService`
位置建议:
```text
src/application/services/memory_application_service.py
```
职责:
- 统一封装 extract / update / retrieve / compact
- 维护 scope 级锁
- 控制错误回退策略
### 8.3 `MemoryExtractor`
位置建议:
```text
src/infrastructure/analysis/analyzers/memory_extractor.py
```
职责:
-`IncrementalBatch` 提取 episode candidate
-`IncrementalState` / `analysis_result` 提取画像候选
### 8.4 `MemoryUpdater`
位置建议:
```text
src/domain/services/memory_updater.py
```
职责:
- 合并候选记忆到长期存储
- 做去重、置信度调整、替换和衰减
### 8.5 `MemoryPromptBuilder`
位置建议:
```text
src/domain/services/memory_prompt_builder.py
```
职责:
- 将检索到的记忆拼成有限长度的 `memory_digest`
- 控制 prompt 大小
- 避免冗余或极低置信度记忆进入总结 prompt
## 9. 与现有代码的集成点
### 9.1 插件初始化
`main.py` 中新增:
- `MemoryStore`
- `MemoryApplicationService`
并注入到 `AnalysisApplicationService`
### 9.2 增量分析成功后
`execute_incremental_analysis()` 成功保存 `IncrementalBatch` 后,新增:
```text
await memory_application_service.update_from_batch(batch, platform_id)
```
要求:
- best-effort
- 失败只记录日志
- 不阻塞主流程成功返回
### 9.3 最终报告前
`execute_incremental_final_report()` 中构建最终总结前,新增:
```text
memory_bundle = await memory_application_service.retrieve_for_summary(
scope_id=group_scope_id,
state=state,
)
```
返回内容应至少包括:
- `group_profile_digest`
- `member_profile_digests`
- `episode_digests`
### 9.4 最终总结 prompt 注入
需要在最终总结使用的 prompt 构建中引入:
- 当前窗口摘要
- 记忆摘要
注意:
- 记忆增强只作用于最终总结相关 prompt
- 不强制所有 analyzer 都接入
### 9.5 最终报告成功后
在最终报告成功完成后,再执行:
```text
await memory_application_service.refresh_from_final_result(
scope_id=group_scope_id,
state=state,
analysis_result=analysis_result,
)
```
用于更新:
- `GroupProfile`
- `MemberProfileLite`
- `MemorySnapshot`
## 10. Phase 1 检索设计
### 10.1 检索输入
检索阶段只基于以下信息:
- 当前窗口话题
- 当前活跃成员列表
- 当前窗口关键词
### 10.2 检索目标
检索:
- 最近相关的 3 到 4 条 episode
- 当前最活跃成员对应的画像摘要
- 一个群级画像摘要
### 10.3 检索排序
Phase 1 不做 embedding,相似度以规则为主:
- 关键词重合
- 参与者重合
- 时间接近度
- importance
- confidence
建议排序公式:
```text
score =
keyword_overlap * 0.35 +
participant_overlap * 0.20 +
recency * 0.20 +
importance * 0.15 +
confidence * 0.10
```
## 11. Phase 1 画像更新设计
### 11.1 `GroupProfile` 更新
更新来源:
- 最近 N 个 batch 的 recurring topics
- 最终总结的群体风格描述
- 最近活跃成员构成
更新原则:
- 不做完全覆盖
- 优先保留已有稳定标签
- 对新标签先低置信度写入
### 11.2 `MemberProfileLite` 更新
更新来源:
- 用户活跃统计
- 其参与的话题
- 最终总结里的用户描述
更新原则:
- 标签必须保守
- 需要多次出现才升高置信度
- 允许保留“近期变化”但不立即改写长期画像
### 11.3 `Episode` 更新
更新来源:
- 增量批次中的话题、金句、参与者、关键词
更新原则:
- 同主题且时间相邻的 episode 可以合并
- 长期无引用 episode 自动过期
## 12. 总结 Prompt 设计
### 12.1 Prompt 结构
建议在最终总结 prompt 中新增如下区块:
```text
[当前窗口信息]
...
[群长期画像摘要]
...
[相关历史事件摘要]
...
[活跃成员长期画像摘要]
...
```
### 12.2 使用规则
必须明确约束模型:
- 以当前窗口事实为主
- 长期记忆只做背景增强
- 低置信度信息不得用确定性口吻表达
### 12.3 预期输出增强
记忆增强后,最终总结建议包含以下隐含效果:
- 话题延续性
- 人物连续性
- 群风格辨识度
- 今日反差点
## 13. 安全与治理
### 13.1 敏感信息过滤
在写入长期记忆前,过滤:
- 电话号
- 邮箱
- 地址
- 明显私人身份信息
### 13.2 保守画像原则
禁用以下危险画像方式:
- 现实人格判断
- 心理诊断式标签
- 政治、宗教、健康等敏感推断
### 13.3 低置信度不进最终总结
如果:
- 画像 `confidence` 太低
- episode `importance` 太低
- 内容带明显攻击性或疑似玩梗误导
则不进入最终总结的记忆增强上下文。
## 14. Compact 与 Cleanup 设计
### 14.1 执行方式
通过 AstrBot `cron_manager` 注册:
- 每日轻量 compact
- 每周深度 snapshot 重建
### 14.2 每日 compact 内容
- 删除过期 episode
- 限制最大 episode 数
- 降低长期未更新画像的置信度
### 14.3 每周 snapshot 重建
- 重写 `MemorySnapshot`
- 合并过旧且低价值的 episode
- 修正摘要冗余
## 15. 开发顺序建议
### Step 1
新增:
- `memory_models.py`
- `memory_store.py`
- `memory_application_service.py`
### Step 2
`main.py` 中完成依赖注入。
### Step 3
`execute_incremental_analysis()` 中接入 `update_from_batch()`
### Step 4
`execute_incremental_final_report()` 中接入 `retrieve_for_summary()`
### Step 5
`memory_digest` 接入最终总结 prompt。
### Step 6
在最终报告成功后接入 `refresh_from_final_result()`
### Step 7
接入 `cron_manager` 做 compact / cleanup。
## 16. 测试建议
### 16.1 单元测试
- MemoryStore 的保存、查询、清理
- 画像更新规则
- episode 合并规则
- memory digest 长度控制
### 16.2 集成测试
- 增量批次后能生成记忆
- 最终总结前能正确检索记忆
- 记忆失败不影响主分析结果
- compact 后索引仍然一致
### 16.3 回归重点
- 自动分析调度是否变慢
- 增量分析成功率是否下降
- 现有报告生成是否被破坏
- KV key 数量是否增长过快
## 17. 验收标准
Phase 1 可以判定完成的标准如下:
1. 至少一个群在连续多次增量分析后形成可检索的长期记忆。
2. 最终总结能稳定引用群画像、成员画像或历史 episode 中的一部分信息。
3. 记忆写入失败不会导致分析失败。
4. compact / cleanup 可运行,且能控制记忆条目规模。
5. 不引入新的数据库依赖,不破坏现有调度与报告流程。
## 18. 结论
Phase 1 的核心不是“做一个很强的记忆系统”,而是:
- 把长期记忆作为当前增量分析链路上的第四层结构化沉淀;
- 把它首先用于增强最终总结;
- 以最低风险方式,为后续更复杂的成员画像、关系图谱和 thread 级记忆打基础。
只要这一阶段做稳,后续迭代就会非常自然:
- Phase 2 可以增加更强的画像与漂移判断
- Phase 3 再考虑更细粒度 scope 和向量检索
但在现在,最重要的是先把这条基础链路跑通。
+811
View File
@@ -0,0 +1,811 @@
# 群聊记忆模块 PRD(初版)
## 1. 文档信息
- 项目名称:群聊记忆模块(Group Memory System
- 所属插件:`astrbot_plugin_qq_group_daily_analysis`
- 文档版本:v0.2
- 文档类型:项目 PRD / 执行计划总纲
- 当前状态:立项完成,进入 Phase 1 设计冻结与落地准备阶段
### 1.1 关联文档
- 总提案:`docs/group_memory_proposal.md`
- 可执行性复盘:`docs/group_memory_feasibility_review.md`
- Phase 1 技术提案:`docs/group_memory_phase1_proposal.md`
### 1.2 本文档目的
本文档用于将前置讨论、代码研究、宿主生态 review 和 Phase 1 方案统一收敛为一份项目级 PRD。
它重点回答以下问题:
- 这个项目为什么值得做
- 目标用户是谁,核心价值是什么
- 项目的短期、中期、长期目标分别是什么
- 当前已经取得了哪些阶段性成果
- 接下来应该如何推进,按什么里程碑验收
- 如何定义成功、如何灰度、如何回滚、如何持续演化
## 2. 项目背景
当前插件已经具备较强的群聊分析能力:
- 支持按群抓取消息并做全量分析
- 支持增量分析与滑动窗口汇总
- 支持话题、金句、用户称号、聊天质量等分析模块
- 支持多平台(QQ、Telegram、Discord、Lark 等)接入
但从实际产品体验来看,当前总结仍然主要停留在“当前窗口总结”层面。
具体问题包括:
- 总结对“这个群长期是什么风格”缺乏持续理解
- 同一个成员在不同天的描述缺少连贯性
- 今天的讨论与前几天、前几周的延续关系不容易被识别
- 群聊总结还不够“像这个群自己的总结”
因此,需要在现有分析链路之上,引入一层面向总结任务的长期记忆能力。
## 3. 项目机会与价值判断
### 3.1 产品机会
如果插件能够具备长期记忆,它将不再只是“自动生成日报”的工具,而会逐步演变为:
- 群文化观察器
- 群长期叙事整理器
- 人物连续性总结器
- 社区运营辅助工具
### 3.2 用户价值
用户最终感知到的价值应当是:
- 总结更连续:今天的内容和历史脉络能连起来
- 总结更有群味:不同群的输出风格不再趋同
- 人物更稳定:对同一成员的描述更连贯
- 信息密度更高:不需要翻历史也能理解当日讨论的上下文
### 3.3 工程价值
从工程角度,这个项目还能为插件建立一个可持续复用的长期认知底座,后续可以继续承接:
- 周报 / 月报
- 群文化档案
- 历史事件回顾
- 更强的角色识别与内容推荐
## 4. 项目愿景
### 4.1 长期愿景
将插件从“按天生成总结的分析器”,升级为“对每个群拥有持续认知的总结 Agent”。
### 4.2 产品愿景
让最终输出的群聊总结具备以下特征:
- 有历史脉络
- 有群体气质
- 有人物连续性
- 有更强的辨识度
### 4.3 工程愿景
在不推倒现有插件架构的前提下,构建一个:
- 可增量演进
- 可观测
- 可治理
- 可清理
- 可分阶段扩展
的长期记忆底座。
## 5. 目标用户、场景与干系人
### 5.1 目标用户
主要用户包括:
- 使用插件做群总结的群管理员
- 将群总结作为日报、群档案、协作回顾材料的使用者
- 高活跃社区 / 项目群的维护者
- 希望总结更“有群味”的插件高级用户
### 5.2 核心使用场景
#### 场景 A:日常群总结增强
用户希望机器人生成的总结,不只是“今天聊了什么”,而是:
- 今天的话题与近期历史有什么延续关系
- 哪些人延续了长期角色,哪些人出现了反差
- 今天的群氛围是否符合这个群一贯的风格
#### 场景 B:持续运营型群管理
在技术群、项目群、兴趣群中,管理者希望通过总结快速把握:
- 群近期主线在往哪里演化
- 哪些成员是稳定的活跃核心
- 哪些 recurring topics 值得关注
#### 场景 C:历史回顾与群文化沉淀
当群聊逐步积累后,用户希望总结能形成:
- 群风格档案
- 群内长期角色感知
- 历史高光事件回顾
### 5.3 关键干系人
本项目的关键干系人包括:
- 产品 / 方案负责人:确定目标边界、阶段优先级与质量标准
- 插件开发者:负责设计、实现、测试、治理与持续演进
- 群管理员 / 高级用户:在灰度期间提供真实反馈
- AstrBot 宿主生态:提供 KV、cron、UMO、persona、message history 等底层能力边界
## 6. 用户痛点
当前产品的主要痛点可归纳为:
- 总结偏当期,历史连续性不足
- 对同一成员的描述不够稳定
- 群风格和群文化难以持续沉淀
- 不同群的总结容易趋同,缺乏独特性
- 现有分析结果缺少“长期认知层”
## 7. 项目目标
### 7.1 短期目标(Phase 1
在当前插件与 AstrBot 生态下,先交付一版可运行的群级长期记忆系统。
短期目标包括:
- 按群建立长期记忆空间
- 从增量批次中提取事件型记忆
- 构建群级长期画像
- 构建成员轻量长期画像
- 在最终总结前检索长期记忆并增强 prompt
- 增加 compact / retention 能力
### 7.2 中期目标(Phase 2
让长期记忆开始具备更强的稳定性与演化能力。
中期目标包括:
- 引入画像置信度机制
- 引入稳定画像与近期变化的分层
- 增加更强的画像更新规则
- 增强群内长期关系线索
- 提升记忆检索与摘要质量
### 7.3 长期目标(Phase 3
将长期记忆升级为插件真正的“群聊认知底座”。
长期目标包括:
- 支持更细粒度的 thread / topic scope
- 增加向量检索或更高级的相关性检索
- 形成历史阶段切片能力
- 支持周报、月报、群文化档案等更高阶能力
### 7.4 目标树
本项目的目标树可以概括为:
- 一级目标:让群聊总结具备长期认知能力
- 二级目标 A:让总结具备历史连续性
- 二级目标 B:让总结具备群体风格辨识度
- 二级目标 C:让成员描述具备长期一致性
- 二级目标 D:让工程实现具备可控性、可观测性和可回滚能力
对应的阶段性结果是:
- Phase 1:建立基础长期记忆闭环
- Phase 2:增强画像稳定性与演化能力
- Phase 3:扩展到更强的认知与衍生产品能力
## 8. 非目标
本项目当前阶段明确不追求:
- 跨群统一用户画像
- 复杂人格诊断或现实人格推断
- 图数据库级关系图谱
- 全量原始消息长期镜像
- 一开始就把长期记忆接入所有 analyzer
## 9. 当前阶段性成果
截至当前版本,项目已经完成以下阶段性成果:
### 9.1 已完成的文档成果
- 完成总提案文档,明确长期愿景和整体架构方向
- 完成可执行性复盘,明确插件侧与宿主生态的真实边界
- 完成收敛后的 Phase 1 提案,明确首期范围与技术路线
- 完成项目级 PRD,统一目标、里程碑、成功指标和执行策略
### 9.2 已确认的技术结论
- 当前插件已有增量批次与滑动窗口汇总,适合作为长期记忆原料层
- AstrBot 已提供插件级 KV、`plugin_data`、UMO、persona、cron 等核心能力
- 首期适合采用“群级作用域 + 轻量画像 + episode 记忆 + 最终总结增强”的方案
- 首期不适合引入 thread 级 scope、向量检索和复杂关系图谱
### 9.3 当前尚未开始的内容
- 代码骨架尚未落地
- `MemoryStore` 尚未实现
- `memory_digest` 尚未接入最终总结链路
- compact / cleanup 尚未接入宿主 cron
## 10. 产品方案总览
### 10.1 一句话总结
在现有增量分析链路和最终报告链路之间增加一层长期记忆闭环,使插件可以按群沉淀“事件、画像与风格”,并将其反哺到最终总结中。
### 10.2 总体数据流
```text
原始消息
-> 消息清洗
-> IncrementalBatch
-> 记忆提取
-> 长期记忆更新
最终总结前
-> 记忆检索
-> memory_digest
-> 总结 prompt 增强
总结成功后
-> 群画像 / 成员画像 / 快照刷新
```
### 10.3 核心策略
本项目采用“先骨架、后增强;先群级、后细分;先可控、后复杂”的推进策略。
对应原则为:
- 先接通主链路,再优化画像质量
- 先让记忆增强最终总结,再考虑横向扩散到其他 analyzer
- 先保证 compact、开关、回滚,再考虑更激进的能力扩展
## 11. Phase 1 范围
### 11.1 功能需求
#### FR-1 群级长期记忆空间
系统需要为每个群建立独立的长期记忆命名空间。
首期主键:
```text
group_scope_id = "{platform_id}:GroupMessage:{group_id}"
```
#### FR-2 事件型记忆抽取
系统需要能够从 `IncrementalBatch` 中抽取可复用的 episode 记忆。
#### FR-3 群画像
系统需要维护一个可持续更新的群长期画像,用于描述:
- 群主题倾向
- 群风格
- recurring topics
- 核心成员角色
#### FR-4 成员轻量画像
系统需要维护成员在某个群内的轻量长期画像,用于描述:
- 活跃特征
- 话题偏好
- 风格倾向
- 群内角色标签
#### FR-5 最终总结记忆增强
系统需要在最终总结前检索长期记忆,并将其注入总结 prompt。
#### FR-6 记忆 compact / retention
系统需要定期清理过期 episode、限制索引规模、重建快照。
### 11.2 非功能需求
- 不引入新数据库依赖
- 不显著降低现有自动分析成功率
- 记忆写入失败不能导致主分析失败
- 记忆摘要必须可控,不可无限增大 prompt
- 必须提供基本可观测性与回滚手段
### 11.3 优先级分层
#### P0
- MemoryStore
- Episode 抽取
- GroupProfile
- MemberProfileLite
- memory_digest 注入最终总结
- compact / cleanup
#### P1
- 置信度机制
- 近期变化摘要
- 更细致的成员画像更新规则
- 群画像快照优化
#### P2
- thread / topic scope
- 更高级相似度检索
- 向量化扩展
- 周报 / 月报等衍生能力
## 12. 技术与生态约束
### 12.1 插件内部约束
- 真实接入点主要集中在增量分析后、最终总结前后
- 当前报告与 prompt 链路还没有 memory context 注入口
- 当前成员统计信号不足以支持重画像,因此首期必须做轻量版
- 并发写入需要 scope 级锁,不能直接裸写 KV
### 12.2 AstrBot 宿主约束
- 插件 KV 适合存结构化 JSON 记忆,但需要自行管理索引与膨胀
- `plugin_data` 适合存放大快照、调试导出或未来更重的离线材料
- `cron_manager` 应作为 compact / cleanup 的统一调度入口
- `SharedPreferences` 不应作为长期记忆主存储
- `persona_manager``UMO``MessageSession` 应继续作为会话上下文与人格配置的标准来源
### 12.3 前置依赖与阻塞项
当前进入实现前,仍存在几个必须显式管理的前置依赖:
- `IncrementalBatch` 和最终总结链路的结构必须保持稳定,避免记忆模块依附的输入频繁变化
- `AnalysisApplicationService` 需要预留安全接入点,用于挂载批次后更新、总结前检索和总结后刷新
- 最终总结 prompt 构建链路需要确认可安全接入 `memory_digest`
- 插件配置层需要允许新增 `memory` 配置组
- 宿主 AstrBot 当前版本需要满足插件 KV 和 `cron_manager` 的使用前提
当前已知阻塞项包括:
- 如果 `memory_digest` 无法稳定接入总结 prompt,Phase 1 的核心产品价值会明显下降
- 如果 `MemberProfileLite` 的信号质量不足,成员画像必须以实验能力或降级模式上线
- 如果灰度样本群不足,主观质量提升将难以验证
### 12.4 决策门槛
在进入 PR-1 开发前,需要冻结以下决策:
1. `memory_digest` 的挂接入口和长度上限
2. `MemberProfileLite` 的字段边界,以及哪些字段允许规则生成、哪些字段允许依赖 LLM
3. Phase 1 是否支持“仅群画像 + episode,不启用成员画像”的降级发布模式
4. 灰度阶段的主观评估模板与责任人
5. 手动清理指定群记忆是否纳入首期范围
## 13. 执行计划与里程碑
### 13.1 执行顺序
建议按以下顺序推进:
1. 文档与接口对齐
2. 存储与应用服务骨架
3. 批次后记忆更新
4. 总结前记忆检索
5. Prompt 增强与摘要注入
6. Compact、监控与治理
7. 灰度、复盘与下一阶段规划
### 13.2 建议时间线
以下时间线作为 Phase 1 初版建议节奏,可根据开发资源做微调:
- 第 1 周:冻结 Phase 1 范围、接口草案、关键决策门槛
- 第 2 周:完成 `MemoryStore`、配置接入、基础单元测试
- 第 3 周:打通增量批次后的记忆更新链路
- 第 4 周:打通总结前检索与 `memory_digest` 注入
- 第 5 周:接入 compact / cleanup / observability
- 第 6 周:灰度验证、回归修复、文档收尾
### 13.3 并行工作流建议
建议至少并行为三个工作流:
- 工作流 A:存储与配置
- `MemoryStore`
- `memory` 配置组
- 索引、清理、存储约束
- 工作流 B:写入与更新
- `update_from_batch()`
- `GroupProfile` / `MemberProfileLite` 更新规则
- episode 生成与合并
- 工作流 C:检索与总结增强
- `retrieve_for_summary()`
- `memory_digest`
- prompt 注入与输出回归验证
### 13.4 Milestone 0:方案收敛完成
交付物:
- 总提案
- 可执行性复盘
- Phase 1 技术提案
- PRD 初版
完成标准:
- 项目边界明确
- 首期范围收敛
- 关键技术路线达成一致
### 13.5 Milestone 1:存储与骨架落地
交付物:
- `MemoryStore`
- `memory` 配置组
- `MemoryApplicationService`
- 基础数据模型
完成标准:
- 可以为任意群读写基础长期记忆结构
- 支持 scope 级锁
- 支持基础 cleanup
### 13.6 Milestone 2:批次后记忆更新
交付物:
-`IncrementalBatch` 抽取 episode
- 更新 `GroupProfile`
- 更新 `MemberProfileLite`
完成标准:
- 增量分析后,记忆库中有可观测的新记忆条目
- 写入失败不影响主流程成功
### 13.7 Milestone 3:总结前记忆增强
交付物:
- 记忆检索逻辑
- `memory_digest`
- 最终总结 prompt 注入
完成标准:
- 最终总结可以引用历史 episode 或长期画像信息
- prompt 大小可控
### 13.8 Milestone 4:运维与治理
交付物:
- compact / cleanup cron
- 监控指标
- 清理与重建策略
完成标准:
- 记忆体量受控
- 可观测性到位
- 有回滚与修复路径
## 14. 阶段性成绩定义
为便于项目推进,本项目采用“阶段性成绩”作为每个阶段的验收维度。
### 14.1 文档阶段成绩
- 完成需求收敛
- 完成技术边界确认
- 完成路线图、执行计划和 PRD
### 14.2 首期实现阶段成绩
- 任意目标群可生成至少一种长期记忆条目
- 最终总结可稳定引用长期记忆
- 成员描述开始出现轻量连续性
- compact 机制能控制数据规模
### 14.3 中期演进阶段成绩
- 群画像更加稳定
- 成员画像开始具备阶段差异识别能力
- 历史脉络感明显增强
## 15. 成功指标
### 15.1 产品指标
- 记忆增强总结启用率
- 启用记忆的群占比
- 用户对总结“连续性 / 群味 / 人物稳定性”的主观反馈
### 15.2 系统指标
- 在灰度群连续 7 天样本窗口内,记忆写入成功率 >= 99%
- 在灰度群连续 7 天样本窗口内,记忆检索成功率 >= 99%
- 记忆增强开启后,自动分析总体成功率不低于未开启前 7 天基线
- 在灰度群样本中,单次最终总结的 P95 额外耗时不超过 20%
### 15.3 质量指标
- 被注入 prompt 的低置信度画像比例 <= 5%
- compact 后索引一致性抽样正确率 = 100%
- 单群 episode 数量不超过配置上限,且 7 天内无持续失控增长
### 15.4 阶段门槛指标
- 内部验证阶段:至少 3 个测试群连续运行 3 天无主流程阻断
- 白名单灰度阶段:至少 5 个真实群连续运行 7 天,总结成功率不低于历史基线
- 阶段推进门槛:主观反馈中“连续性 / 群味”正向评价占比 >= 70%
## 16. 可观测性与运营保障
### 16.1 日志与追踪
建议新增以下日志分类:
- memory.extract
- memory.update
- memory.retrieve
- memory.compact
- memory.safety_filter
### 16.2 核心监控点
- 每群记忆条目数量
- 每群 episode 数量
- 每次检索返回条目数
- 每次总结注入的 digest 长度
- compact 执行成功率
### 16.3 运维操作
建议后续提供运维能力:
- 清空指定群记忆
- 清理指定成员画像
- 仅重建最近 N 天记忆
## 17. 发布策略
### 17.1 首发策略
建议采用灰度启用:
- 通过 `memory.enabled` 控制总开关
- 可先仅对白名单群启用
- 可先只启用群画像,不启用成员画像
### 17.2 灰度节奏建议
建议采用三步灰度:
1. 内部验证:只在少量测试群启用群画像与 episode
2. 小范围灰度:对白名单真实用户群启用完整 Phase 1
3. 默认可选:保留开关,允许更多群自主启用
每一阶段进入下一步前,都需要满足:
- 总结成功率没有明显回退
- 记忆体量增长可控
- 用户对总结质量的主观反馈为正
建议采用统一人工评估模板,至少覆盖:
- 历史连续性感知
- 群风格辨识度
- 人物描述稳定性
- 是否出现明显误判或“编造感”
### 17.3 回滚策略
如果出现以下情况,应允许快速关闭记忆增强:
- 总结成功率明显下降
- 写入失败率异常升高
- prompt 长度失控
- 画像内容明显失真
回滚方式:
- 关闭 `memory.enabled`
- 保留已有记忆数据但停止读写
- 必要时执行清理或重建
## 18. 主要风险与缓解
### 18.1 存储膨胀
风险:
- 高活跃群 episode 增长过快
缓解:
- retention 上限
- compact cron
- snapshot 重写
### 18.2 并发竞争
风险:
- 增量分析、总结、compact 同 scope 冲突
缓解:
- scope 级锁
- best-effort 写入
- 索引一致性检查
### 18.3 画像误判
风险:
- 短期行为被固化成长期画像
缓解:
- 首期仅做轻量画像
- 引入保守标签
- 低置信度不进入最终总结
### 18.4 Prompt 污染
风险:
- 记忆注入过长或噪声过大,影响总结质量
缓解:
- digest 限长
- 只注入高相关、高置信度内容
- 记忆增强仅在最终总结使用
### 18.5 宿主兼容性风险
风险:
- 与 AstrBot 的 cron、persona、KV 使用方式不一致,导致后续维护成本升高
缓解:
- 明确继续复用插件 KV
- compact 统一挂载 `cron_manager`
- 作用域继续遵循 UMO 风格
- 人格能力继续通过宿主 `persona_manager` 读取
## 19. 项目实施建议
### 19.1 推荐开发顺序
1. 建立数据模型与 MemoryStore
2. 建立 `memory` 配置组
3. 接入增量分析后的记忆更新
4. 接入总结前记忆检索
5. 接入最终总结 prompt 增强
6. 接入 compact / cleanup
7. 完成监控与回滚能力
### 19.2 推荐 PR 拆分
建议至少拆为以下 PR
- PR-1:数据模型、配置、MemoryStore
- PR-2:批次后记忆更新
- PR-3:总结前记忆检索与 prompt 注入
- PR-4compact / cleanup / observability
- PR-5:文档与运维能力补充
### 19.3 资源假设
当前默认资源假设为:
- 开发资源以插件内部演进为主,不引入单独基础设施项目
- 宿主 AstrBot 不需要为 Phase 1 做重大改造
- 早期验证样本以少量真实群和开发测试群为主
- Phase 1 的主要成本集中在存储骨架、总结链路接入和治理能力
### 19.4 推荐职责划分
建议按以下职责划分推进:
- 方案负责人:冻结边界、维护 PRD、确认灰度门槛
- 存储与应用层负责人:实现 `MemoryStore``MemoryApplicationService`、cleanup
- 总结增强负责人:实现 `memory_digest`、检索、prompt 注入
- 验证与运维负责人:维护灰度名单、跟踪指标、执行回滚与清理
### 19.5 建议阶段 Gate
- Gate A:接口冻结
- `MemoryStore``memory_digest``MemberProfileLite` 字段边界冻结
- Gate B:主链路打通
- 增量批次写入与总结前检索均已跑通
- Gate C:治理到位
- compact、日志、开关、回滚已可用
- Gate D:灰度通过
- 指标与主观反馈满足推进门槛
## 20. Definition of Done
当以下条件同时满足时,Phase 1 可以视为完成:
- 代码链路完整贯通
- 至少一个群能够沉淀长期记忆
- 最终总结稳定引用长期记忆
- 现有分析主流程不被破坏
- compact / retention 已上线
- 日志、指标、开关、回滚策略齐备
补充说明:
- 若成员画像质量暂时未达预期,但群画像 + episode + 记忆增强总结已稳定工作,可允许以“成员画像受限 / 受开关控制”的降级模式发布
- 若 compact / cleanup 未完成,则不得视为 Phase 1 可发布
-`memory_digest` 仍无法稳定接入最终总结链路,则 Phase 1 只能视为内部技术验证,不视为正式交付
## 21. 开放问题
当前仍需在实施前进一步明确的开放问题包括:
- `memory_digest` 最终挂接在哪个总结 prompt 入口最稳妥
- `MemberProfileLite` 中哪些字段可以完全基于规则生成,哪些必须依赖 LLM
- Phase 1 是否需要在配置层支持“仅群画像模式”
- 是否需要在首期就提供手动清理指定群记忆的管理命令
- 灰度阶段的人工评估标准是否需要固定模板
建议按优先级理解这些开放问题:
- P0`memory_digest` 挂点、成员画像边界、降级发布模式
- P1:人工评估模板、管理命令范围
这些问题不会阻止骨架开发启动,但会影响功能边界和 PR 拆分细节。
## 22. 下一步行动
基于当前 PRD,下一步建议立即进入:
1. 明确 `MemoryStore``MemoryApplicationService` 的接口草案
2. 明确 `memory_digest` 的中间结构
3. 明确首期 `MemberProfileLite` 字段边界
4. 输出第一批代码实现任务清单
5. 按 PR-1 启动第一批代码骨架实现
## 23. 结论
群聊记忆模块不是对现有总结功能的“小修小补”,而是一次产品能力层级的升级。
它会把插件从“会分析今天的群聊”推进到“能理解这个群的长期叙事和人物结构”。
当前最正确的推进方式,不是一步做大,而是:
- 先用 PRD 固化目标与边界
- 先完成可执行的 Phase 1
- 在稳定运行后,再向更复杂的长期认知能力演进
这也是本项目当前的核心执行策略。
+908
View File
@@ -0,0 +1,908 @@
# 13. 群聊总结记忆模块提案 (Group Memory Proposal)
## 1. 背景与目标
当前插件已经具备以下能力:
- 以群为单位抓取消息并做一次性全量分析。
- 以群为单位执行增量分析,并将结果沉淀为 `IncrementalBatch`
- 在最终报告阶段,基于窗口内累积数据生成话题、金句、用户称号与聊天质量总结。
这套链路已经很好地解决了“今天聊了什么”的问题,但还没有真正解决“这个群一直在聊什么、这群人分别是什么风格、今天的内容与历史脉络是什么关系”这三个更高阶的问题。
如果我们希望总结更有“群味”,就需要引入一层长期记忆系统,让插件从“按天生成报告的分析器”升级为“对每个群拥有持续认知的总结 Agent”。
本提案的目标是:
- 为插件引入天然分群的长期记忆能力。
- 在群级记忆之下,为每个成员构建长期画像,但画像仅在该群作用域内生效。
- 让最终总结能够引用“近期上下文 + 历史脉络 + 人物画像”,生成更具连续性、辨识度和趣味性的分析结果。
- 复用当前的增量分析与 KV 存储架构,避免另起一套重系统。
- 在设计上预留遗忘、安全、回滚和逐步上线能力。
## 2. 设计原则
本方案借鉴“记忆系统”相关研究的核心思想,但会严格贴合当前插件的实际边界。
### 2.1 作用域优先于全局统一
“天然分群”是第一原则。记忆不是全局共享的,而是默认按群隔离。
- 同一个用户在不同群的画像必须分开。
- 同一平台不同群必须分开。
- 不同平台即使群号相同,也必须分开。
- Telegram 话题子会话、Discord 频道等特殊场景,应允许在群级作用域下继续细分。
### 2.2 记忆不是原始聊天记录的简单堆积
原始消息属于“可回放的数据源”,不是“可直接喂给总结器的记忆”。
记忆层应该只保存经过抽取、压缩、结构化后的高价值信息,包括:
- 群级长期主题与 recurring 梗。
- 成员长期角色、风格、兴趣、关系倾向。
- 对总结真正有帮助的事件片段与证据引用。
- 可供检索和动态拼装的摘要单元。
### 2.3 短期记忆和长期记忆分层
参考当前插件现状,可以将记忆分成两层:
- 短期记忆:当前分析窗口内的消息、增量批次、当日热点。
- 长期记忆:跨天持续存在的群画像、成员画像、稳定话题、长期事件线索。
短期记忆回答“今天发生了什么”,长期记忆回答“这件事在这个群里意味着什么”。
### 2.4 先做可控的文本结构化记忆,再考虑向量化增强
第一阶段不建议直接引入复杂向量数据库。更适合基于当前 KV 架构先实现:
- 结构化事实卡片
- 事件摘要
- 群画像快照
- 成员画像快照
- 检索排序逻辑
等第一版跑稳,再视需要增加 embedding 检索。
### 2.5 安全、遗忘、可审计是基础能力
长期记忆一旦落地,就不再只是“功能增强”,而是“长期持有群内认知”的系统。因此必须一开始就考虑:
- 敏感信息不过度沉淀。
- 可以按群清除、按人清除、按时间淘汰。
- 每条高价值记忆尽量保留来源批次和时间区间。
- 对投毒内容、恶搞内容、短期异常行为有抑制机制。
## 3. 与当前实现的关系
当前代码中,已经存在非常适合作为记忆系统底座的能力:
- `MessageProcessingService` 负责消息进入插件侧存储。
- `AnalysisApplicationService.execute_incremental_analysis()` 负责把新消息切成增量批次。
- `IncrementalBatch` 已经保存了用户统计、话题、金句、参与者等中间结构。
- `IncrementalStore` 已经实现了“按群分桶 + 索引 + 单条 KV”的持久化模式。
- `IncrementalMergeService` 已经实现了“窗口查询 -> 合并聚合 -> 最终报告”的工作流。
这意味着我们不需要新造一套完全独立的 pipeline,而是可以在现有链路上增加一层 `Memory Pipeline`
```mermaid
graph TD
A["原始群消息"] --> B["消息清洗"]
B --> C["增量分析批次 IncrementalBatch"]
C --> D["记忆提取 Memory Extract"]
D --> E["群记忆库 Group Memory Bank"]
E --> F["检索 Memory Retrieve"]
C --> G["最终报告聚合"]
F --> G
G --> H["更具连续性的群聊总结"]
```
核心思路不是替换当前分析流程,而是在“增量批次”和“最终报告”之间增加一个长期记忆闭环。
## 4. 记忆系统总体架构
### 4.1 两条主线
本提案建议把记忆拆成两条并行主线:
1. 群级记忆
2. 群内成员画像
它们共享同一个群作用域,但负责不同层面的认知沉淀。
### 4.2 群级记忆
群级记忆关注“这个群整体是什么样的”。
建议长期维护以下内容:
- 稳定话题带:这个群最近长期反复出现的话题、梗、项目、活动。
- 群体互动风格:高能整活、技术答疑、日常陪聊、吐槽、运营协作等。
- 群体事件线:某个持续数天甚至数周的话题演变。
- 群体角色结构:谁是活跃核心、谁是梗王、谁常带节奏、谁负责答疑。
- 历史高光片段:值得在总结中反复引用的经典事件或金句。
### 4.3 群内成员画像
成员画像关注“这个人在这个群里通常扮演什么角色”。
注意这里的画像必须是“群内画像”,而不是跨群统一人格。
建议维护的维度:
- 基础活跃特征:发言频次、活跃时段、长短文倾向、回复倾向。
- 话题偏好:更常参与哪类讨论。
- 风格特征:认真解答型、吐槽型、整活型、观察型、组织者型等。
- 表达习惯:常用口头禅、格式偏好、语气特征、梗密度。
- 群内角色:提问者、答疑者、情报员、乐子人、记录员等。
- 稳定关系线索:经常和谁互动、常在谁的话题下接话。
- 置信度与更新时间:画像是否稳定、最近是否发生明显漂移。
## 5. 作用域设计:如何做到“天然分群”
### 5.1 记忆命名空间
建议引入统一的记忆作用域 ID,而不是只使用裸 `group_id`
推荐主键:
```text
group_scope_id = "{platform_id}:GroupMessage:{group_id}"
```
对于 Telegram 话题、Discord 线程等子空间,可以继续扩展:
```text
thread_scope_id = "{platform_id}:GroupMessage:{group_id}#{topic_or_thread_id}"
```
### 5.2 分层隔离规则
- 默认所有长期记忆写入 `group_scope_id` 命名空间。
- 如果平台具备更细的子话题结构,可以把 thread 级信息先写入 `thread_scope_id`,再在群级进行抽象汇总。
- 成员画像的主键必须包含群作用域:
```text
member_profile_key = "{group_scope_id}:{sender_id}"
```
这样可以天然保证:
- 同一个 QQ 号在 A 群和 B 群是两份画像。
- 同一个人跨平台不会误合并。
- 同一个 Telegram 父群下不同 topic 可以选择共享群画像,但保留 thread 层差异。
## 6. 记忆分类设计
### 6.1 情景记忆 (Episodic Memory)
情景记忆记录“发生过什么”。
在本插件里,适合存为:
- 每次增量批次提炼出的事件摘要
- 某日总结后的关键结论
- 多日持续话题的阶段性节点
- 高光互动或典型群事件
示例:
```json
{
"memory_id": "episode_xxx",
"scope_id": "qq:GroupMessage:123456",
"type": "episode",
"timestamp": 1710000000,
"summary": "群里围绕 AstrBot 插件的飞书适配展开了长时间讨论,最终确认了权限预热方案。",
"entities": ["飞书", "适配", "权限预热"],
"participants": ["user_a", "user_b", "user_c"],
"importance": 0.83,
"source_batch_ids": ["batch_1", "batch_2"]
}
```
### 6.2 语义记忆 (Semantic Memory)
语义记忆记录“这个群长期是什么样、这个人长期是什么样”。
在本插件里,适合存为:
- 群长期画像
- 成员长期画像
- 稳定话题标签
- 持续关系与角色归纳
示例:
```json
{
"profile_id": "member_xxx",
"scope_id": "qq:GroupMessage:123456:user_789",
"type": "member_profile",
"stable_traits": [
"高频参与插件开发讨论",
"回答偏直接且技术密度高",
"常在别人卡住时补关键实现细节"
],
"topic_preferences": ["插件架构", "LLM", "平台适配"],
"style_signals": ["理性", "高信息密度", "偶尔吐槽"],
"confidence": 0.78,
"updated_at": 1710000000
}
```
### 6.3 轨迹内记忆与跨轨迹记忆
按插件当前实际能力,可以这样映射:
- 轨迹内记忆:本次分析窗口中的原始消息、清洗结果、增量批次、最终聚合状态。
- 跨轨迹记忆:群画像、成员画像、历史事件卡片、长期主题索引。
其中:
- `IncrementalBatch` 更像短期情景记忆的原材料。
- `GroupMemoryBank` 才是真正的跨轨迹长期记忆。
## 7. 核心模块设计
### 7.1 新增模块一览
建议新增以下核心模块:
- `MemoryScopeResolver`
- `MemoryExtractor`
- `MemoryUpdater`
- `MemoryRetriever`
- `MemoryStore`
- `MemoryCompactor`
- `MemorySafetyGuard`
### 7.2 推荐目录结构
建议采用与当前插件一致的分层风格:
```text
src/
application/
services/
memory_application_service.py
domain/
entities/
memory_models.py
services/
memory_compactor.py
memory_retriever.py
memory_updater.py
infrastructure/
persistence/
memory_store.py
analysis/
analyzers/
memory_extractor.py
memory_profile_analyzer.py
```
### 7.3 模块职责
#### MemoryScopeResolver
负责把平台、群号、话题等信息统一转换为记忆命名空间。
#### MemoryExtractor
负责从以下输入中抽取可沉淀的记忆单元:
- 增量批次
- 合并后的窗口状态
- 最终报告结果
- 可选的原始消息片段
提取结果不是最终写库格式,而是标准化的候选记忆:
- `EpisodeCandidate`
- `GroupFactCandidate`
- `MemberTraitCandidate`
- `RelationshipCandidate`
#### MemoryUpdater
负责把候选记忆合并进长期记忆库,处理:
- 去重
- 冲突解决
- 置信度更新
- 漂移检测
- 快照刷新
#### MemoryRetriever
负责在生成最终总结前,根据当前窗口内容检索最相关的长期记忆。
#### MemoryCompactor
负责记忆衰减、归档和淘汰,防止 KV 无限膨胀。
#### MemorySafetyGuard
负责敏感信息过滤、异常记忆降权、对抗投毒与可疑内容隔离。
## 8. 存储模型设计
### 8.1 为什么继续使用 KV
当前插件已经大量使用 `put_kv_data/get_kv_data`,并且 `IncrementalStore` 已经证明这条路在插件规模下是可行的。
因此第一版长期记忆建议继续采用 KV 模式,原因有三点:
- 与现有基础设施完全兼容。
- 开发成本低,迁移风险小。
- 足以支撑“按群索引 + 按条加载 + 定期清理”的模式。
### 8.2 推荐 KV 键设计
```text
mem_group_profile_{scope_id}
mem_member_profile_{scope_id}_{user_id}
mem_episode_index_{scope_id}
mem_episode_{scope_id}_{memory_id}
mem_fact_index_{scope_id}
mem_fact_{scope_id}_{memory_id}
mem_relation_index_{scope_id}
mem_relation_{scope_id}_{pair_id}
mem_snapshot_{scope_id}
mem_meta_{scope_id}
```
### 8.3 推荐实体
#### GroupProfile
群的长期语义画像。
建议字段:
- `scope_id`
- `summary`
- `tone_tags`
- `recurring_topics`
- `group_archetypes`
- `inside_jokes`
- `core_members`
- `recent_focus_shift`
- `confidence`
- `updated_at`
#### MemberProfile
某个成员在某个群内的长期画像。
建议字段:
- `scope_id`
- `user_id`
- `display_name`
- `role_tags`
- `topic_preferences`
- `style_traits`
- `behavior_patterns`
- `relationship_hints`
- `notable_phrases`
- `confidence`
- `stability_score`
- `last_active_at`
- `updated_at`
#### MemoryEpisode
事件型记忆。
建议字段:
- `memory_id`
- `scope_id`
- `time_range`
- `summary`
- `keywords`
- `participants`
- `importance`
- `novelty`
- `source_batch_ids`
- `evidence_refs`
- `expires_at`
#### GroupSnapshot
面向报告生成的快照,属于“为检索优化的缓存层”。
建议字段:
- `scope_id`
- `last_compacted_at`
- `weekly_summary`
- `active_members_digest`
- `recent_episodes_digest`
- `prompt_ready_context`
它的作用是减少每次生成报告前都扫描大量细粒度记忆条目。
## 9. 提取、更新、检索、应用闭环
### 9.1 Extract:从增量批次中抽取记忆
建议把长期记忆抽取的主要入口放在两个时机:
1. 每次 `execute_incremental_analysis()` 成功后
2. 每次 `execute_incremental_final_report()` 成功后
两者职责不同:
- 增量后抽取:更偏事件化、细粒度、低延迟。
- 最终报告后抽取:更偏总结化、语义化、画像刷新。
### 9.2 Update:动态更新而不是覆盖重写
长期画像不应每次由 LLM 全量重写,而应基于已有画像增量更新。
建议采用以下策略:
- 新证据与已有画像一致:提升置信度。
- 新证据与已有画像冲突但频率低:记录为短期漂移,不立即覆盖。
- 新证据持续多次出现:触发画像改写。
- 久未出现的标签:逐步衰减。
例如:
- 某成员偶尔一天高频整活,不应立刻把其画像改成“纯乐子人”。
- 但如果连续多周都以某种风格出现,画像就应逐步收敛。
### 9.3 Retrieve:多因子检索,而不是只看语义相似度
建议检索评分由多个因素共同构成:
```text
retrieval_score =
semantic_similarity * 0.35 +
recency_score * 0.20 +
importance_score * 0.20 +
confidence_score * 0.15 +
coverage_bonus * 0.10
```
含义如下:
- `semantic_similarity`:与当前窗口话题、人名、事件的匹配程度。
- `recency_score`:越近期的记忆越容易被取回。
- `importance_score`:高光事件、稳定画像优先。
- `confidence_score`:低置信度画像不应频繁参与总结。
- `coverage_bonus`:避免所有记忆都聚焦一个人或一个话题。
### 9.4 Application:记忆如何作用到最终总结
长期记忆不应该直接“覆盖”今天的分析结果,而应该作为增强上下文注入给总结器。
建议新增一个最终总结增强 prompt,上下文包括:
- 当前滑动窗口摘要
- 群级长期画像摘要
- 当前热点相关的历史事件片段
- 当前活跃成员的长期画像摘录
生成目标可以新增以下维度:
- 今天的话题与群历史脉络的关系
- 哪些成员表现符合长期画像,哪些表现出现反差
- 这个群今天的“群味”体现在哪里
- 哪些梗或冲突是延续性的,哪些是新出现的
## 10. 成员长期画像设计
### 10.1 为什么不能只靠“用户称号”
当前插件的 `user_title` 更像一次性报告中的“今日称号”,它有趣,但不稳定,也不一定适合作为长期画像。
长期画像与用户称号的区别:
- 用户称号:偏展示、偏当期、强调可读性。
- 成员画像:偏记忆、偏长期、强调连续性和可检索性。
建议两者并存:
- `MemberProfile` 用于长期记忆。
- `UserTitle` 继续用于当日报告展示。
后续可以让 `UserTitle` 参考 `MemberProfile`,使称号更贴近这个人的长期群内形象。
### 10.2 推荐画像维度
建议将成员画像分为五层:
1. 活跃层
2. 主题层
3. 风格层
4. 关系层
5. 稳定性层
#### 活跃层
- 活跃时段
- 发言频率
- 长短文倾向
- 回复比率
#### 主题层
- 常参与话题
- 擅长话题
- 经常引发的话题
#### 风格层
- 理性/感性
- 干货/整活
- 直接/委婉
- 高密度/短促型
#### 关系层
- 高频互动对象
- 常被谁接话
- 常和谁形成固定组合
#### 稳定性层
- 画像置信度
- 最近是否漂移
- 最近一次确认时间
### 10.3 画像更新策略
建议采用“双层画像”:
- `stable_profile`:长期稳定特征
- `recent_profile_delta`:近期变化特征
这样总结时可以自然写出:
- “A 依旧承担技术答疑角色,但今天明显比平时更活跃。”
- “B 平时更偏潜水观察,今天却成了整场讨论的节奏中心。”
这会让总结非常有辨识度。
## 11. 群级长期画像设计
### 11.1 群画像维度
建议维护以下字段:
- `group_identity`: 这个群整体的气质与功能定位
- `recurring_topics`: 长期反复出现的话题
- `interaction_style`: 群体交流风格
- `humor_style`: 梗文化、吐槽风格、常见互动模板
- `coordination_mode`: 是松散闲聊还是任务协作型
- `core_cast`: 群里典型核心成员及其长期分工
- `recent_phase`: 群当前处于什么阶段
### 11.2 群画像的价值
它可以直接增强最终总结的“辨识度”:
- 同样是技术群,有的群偏认真答疑,有的群偏边做边吐槽。
- 同样是闲聊群,有的群是高频玩梗,有的群是生活流水账。
如果没有群画像,LLM 很容易把不同群总结成同一种腔调。
## 12. 与现有流程的集成点
### 12.1 在 `execute_incremental_analysis()` 后接入
建议在成功保存 `IncrementalBatch` 后,异步触发:
```text
IncrementalBatch -> MemoryExtractor.extract_from_batch()
-> MemoryUpdater.apply_candidates()
```
适合提取:
- 事件摘要
- 人物短期行为信号
- 新出现的话题线索
### 12.2 在 `execute_incremental_final_report()` 前接入检索
在构建最终报告增强 prompt 前,执行:
```text
IncrementalState -> MemoryRetriever.retrieve_for_report()
-> prompt_context_builder
```
适合检索:
- 与今日话题相关的历史事件
- 今日活跃成员的长期画像摘要
- 群近期稳定风格
### 12.3 在 `execute_incremental_final_report()` 后接入刷新
最终报告成功后,再做一次高层抽象更新:
```text
AnalysisResult -> MemoryExtractor.extract_from_final_report()
-> MemoryUpdater.refresh_profiles()
```
适合更新:
- 群画像快照
- 成员稳定画像
- 本周阶段性群状态
## 13. Prompt 设计建议
### 13.1 不建议把全部长期记忆原样塞进 Prompt
长期记忆一旦增长,直接拼 prompt 会迅速失控。
建议只注入三类摘要:
- `group_memory_digest`
- `member_profile_digest`
- `related_episode_digest`
### 13.2 推荐增强 Prompt 结构
```text
你正在为一个长期观察过的群生成总结。
[当前窗口事实]
- 今天/本次窗口的核心话题
- 活跃成员
- 金句
[群长期画像]
- 群的长期风格
- 最近持续关注的主题
[相关历史事件]
- 与本次窗口最相关的 3~5 条历史记忆
[成员画像摘录]
- 本次最活跃的若干成员的长期角色与近期变化
请输出:
1. 今日总结
2. 历史脉络
3. 人物动态
4. 群风格点评
```
### 13.3 报告风格控制
长期记忆引入后,最容易出现的问题是“总结越来越像编年史,少了当日鲜活感”。
因此 prompt 里要明确要求:
- 今日窗口事实优先于长期记忆。
- 长期记忆只用于解释和增强,不用于替代当日观察。
- 对低置信度画像使用保守措辞。
## 14. 遗忘与压缩机制
### 14.1 为什么必须遗忘
如果没有遗忘,系统会逐渐出现:
- 存储无限膨胀
- 老梗长期霸榜
- 过期人设难以纠正
- Prompt 上下文越来越脏
### 14.2 建议遗忘策略
#### Episode 衰减
- 普通事件:7 到 30 天后衰减
- 高重要度事件:可延长到 90 天或人工保留
#### 画像标签衰减
- 长时间未被新证据支持的特征逐步降权
-`confidence` 低于阈值时,不再用于总结
#### 快照重写
- 周期性重写 `GroupSnapshot`
- 用新快照替换碎片化旧摘要,降低检索成本
### 14.3 Compaction 周期
建议沿用现有定时任务能力,新增低频后台任务:
- 每日轻量 compact 一次
- 每周做一次深度重写
## 15. 安全与风险控制
### 15.1 主要风险
引入长期记忆后,风险主要来自四类:
- 敏感信息持久化
- 恶意投毒
- 短期异常行为被误认成长期画像
- 旧画像固化带来的刻板化偏差
### 15.2 防护建议
#### 敏感信息过滤
在写入长期记忆前,过滤或脱敏以下内容:
- 账号、手机号、邮箱、住址
- 明显私人隐私
- 高风险身份信息
#### 低置信度隔离
对于只出现一次、措辞极端、带强烈攻击性的候选记忆:
- 不直接写入稳定画像
- 先放入短期候选区
- 需要多次证据支持才转正
#### 证据回链
长期记忆条目建议保留:
- 来源批次 ID
- 首次出现时间
- 最近确认时间
这样有助于后续做调试与问题追踪。
#### 可清理接口
建议未来提供以下运维能力:
- 清空指定群记忆
- 清空指定成员画像
- 仅重建最近 N 天记忆
## 16. 分阶段落地方案
### Phase 1:最小可用版
目标:先跑通“群级长期记忆 + 群内成员画像”的基础闭环。
范围:
- 新增 `MemoryStore`
- 基于增量批次提取简单 episode
- 维护群画像快照
- 维护成员画像快照
- 在最终总结前做检索增强
不做:
- 向量检索
- 跨群身份合并
- 复杂关系图谱
### Phase 2:增强版
目标:让画像更稳定、总结更有连续性。
范围:
- 引入画像置信度与漂移检测
- 引入群内关系线索
- 引入候选记忆区与转正机制
- 引入 compact/forgetting 任务
### Phase 3:高级版
目标:让记忆真正变成“群聊认知底座”。
范围:
- 可选 embedding 检索
- 群内长期梗图谱
- 历史阶段自动切片
- 更细粒度的 thread/topic 层记忆
## 17. 对当前代码的建议改动顺序
建议按最小侵入顺序推进:
1. 新增 `memory_models.py` 定义核心数据结构。
2. 新增 `memory_store.py`,完全参考 `IncrementalStore` 的索引化 KV 模式。
3. 新增 `memory_application_service.py`,封装提取、更新、检索入口。
4.`main.py` 初始化时注入 `MemoryStore``MemoryApplicationService`
5.`AnalysisApplicationService.execute_incremental_analysis()` 成功后调用记忆更新。
6.`AnalysisApplicationService.execute_incremental_final_report()` 中,在最终 LLM 总结前调用记忆检索。
7.`ReportGenerator` 或总结 prompt 构建处新增“记忆增强上下文”。
这样改的优点是:
- 不会破坏当前增量分析主链路。
- 问题定位更容易。
- 可以按开关灰度启用。
## 18. 配置项建议
建议新增独立配置组 `memory`
```yaml
memory:
enabled: true
memory_scope_mode: "group"
max_episode_per_group: 200
max_member_profile_count: 300
episode_retention_days: 30
profile_decay_days: 45
min_profile_confidence: 0.45
retrieve_episode_top_k: 5
retrieve_member_top_k: 6
enable_memory_in_summary: true
enable_member_long_profile: true
enable_memory_safety_filter: true
```
后续如要灰度,还可以支持:
- 仅对白名单群启用记忆
- 仅启用群画像,不启用成员长期画像
## 19. 预期收益
记忆系统上线后,预期收益主要体现在四个方面:
### 19.1 总结更像“这个群”的总结
不再是泛化的日常摘要,而是能体现群长期氛围、 recurring 梗和成员关系的总结。
### 19.2 成员描写更稳定
同一个人不会每天都被总结成完全不同的角色,人物描写会更连贯。
### 19.3 历史脉络更清晰
能够回答:
- “这事是不是昨天就开始了?”
- “这个梗是不是最近一直在延续?”
- “今天谁的表现和平时反差最大?”
### 19.4 后续功能拓展空间更大
有了长期记忆底座,未来可以继续扩展:
- 周报/月报
- 历史事件回顾
- 群文化档案
- 成员成长轨迹
## 20. 非目标与边界
为了避免第一版过重,本提案明确以下内容不作为首期目标:
- 不做跨群统一用户画像。
- 不做全量原始消息长期缓存替代。
- 不做复杂图数据库。
- 不做自动判定现实身份或真实人格。
- 不让长期画像直接决定管理动作或高风险决策。
## 21. 结论
这套记忆模块的关键,不是给插件“多塞一些历史聊天记录”,而是构建一层真正面向总结任务的认知结构。
对于当前插件来说,最合适的路线不是推倒重来,而是:
- 继续保留现有的增量批次机制作为短期记忆原材料;
- 在其上增加群级长期记忆库;
- 以“群作用域 + 成员作用域”双层结构实现天然分群;
- 通过提取、更新、检索、遗忘、安全治理,形成一个可持续演化的闭环。
如果这套方案落地成功,插件的总结能力会从“会总结消息”提升到“理解这个群的长期叙事和人物结构”,这会是体验层面非常明显的一次升级。