mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 05:31:52 +00:00
chore: docs
This commit is contained in:
@@ -32,3 +32,4 @@ 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/**
|
||||
.ace-tool/
|
||||
|
||||
@@ -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 升级带来的兼容性风险。
|
||||
@@ -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` 的一次性延时任务中,复用框架能力。
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
@@ -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. **配置清理**:
|
||||
- 确保所有配置变更向后兼容。
|
||||
|
||||
**验收标准**:
|
||||
- 插件启动无硬编码等待。
|
||||
- 定时任务准确触发。
|
||||
@@ -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 架构体系。
|
||||
|
||||
@@ -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
@@ -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 层导入成功!')
|
||||
"
|
||||
```
|
||||
@@ -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*
|
||||
@@ -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
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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 方案。
|
||||
@@ -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 和向量检索
|
||||
|
||||
但在现在,最重要的是先把这条基础链路跑通。
|
||||
@@ -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-4:compact / 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
|
||||
- 在稳定运行后,再向更复杂的长期认知能力演进
|
||||
|
||||
这也是本项目当前的核心执行策略。
|
||||
@@ -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. 结论
|
||||
|
||||
这套记忆模块的关键,不是给插件“多塞一些历史聊天记录”,而是构建一层真正面向总结任务的认知结构。
|
||||
|
||||
对于当前插件来说,最合适的路线不是推倒重来,而是:
|
||||
|
||||
- 继续保留现有的增量批次机制作为短期记忆原材料;
|
||||
- 在其上增加群级长期记忆库;
|
||||
- 以“群作用域 + 成员作用域”双层结构实现天然分群;
|
||||
- 通过提取、更新、检索、遗忘、安全治理,形成一个可持续演化的闭环。
|
||||
|
||||
如果这套方案落地成功,插件的总结能力会从“会总结消息”提升到“理解这个群的长期叙事和人物结构”,这会是体验层面非常明显的一次升级。
|
||||
Reference in New Issue
Block a user