Files
chatpapers/docs/newidea/ARCHITECTURE.md
T
2026-08-29 22:13:55 +08:00

681 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 软件架构设计文档
| 字段 | 内容 |
|------|------|
| 产品名称 | BanDu(伴读)— Zotero 插件 |
| 中文名 | 伴读 |
| 英文名 | BanDu |
| 文档版本 | v0.1 |
| 状态 | 草案 |
| 关联文档 | [PLAN.md](./PLAN.md)、[PRD.md](./PRD.md) |
---
## 1. 架构概述
### 1.1 架构风格
**Local-first 桌面插件架构**:核心计算与缓存均在用户机器完成;仅将段落文本与用户问题发送至用户配置的 LLM / TTS 服务。无自建后端(一期)。
### 1.2 设计原则
| 原则 | 说明 |
|------|------|
| 备课一次、缓存复用 | Lecture 持久化;attachment ID + 文件哈希为缓存键 |
| 增量交付 | 摘要 → beats → 逐段 → TTS 分阶段就绪,缩短 Time-to-Listen |
| UI / 领域 / 集成 分离 | 降低 Zotero API 变动影响 |
| Grounding 优先 | 生成内容必须可追溯到段落 ID |
| 优雅降级 | refs 无效、无 timestamps、无 BBT 均有 fallback |
### 1.3 系统上下文
```
┌─────────────────┐
│ 用户 │
└────────┬────────┘
┌──────────────▼──────────────┐
│ Zotero 7 (宿主) │
│ ┌────────────────────────┐ │
│ │ BanDu 插件 │ │
│ │ UI │ Engine │ Store │ │
│ └─────────┬──────────────┘ │
└────────────┼────────────────┘
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌────────────┐ ┌─────────────┐
│ MinerU │ │ LLM API │ │ TTS API │
│ (本地子进程)│ │ / Ollama │ │ ElevenLabs等 │
└───────────┘ └────────────┘ └─────────────┘
┌───────────┐
│ SQLite + │
│ mp3 文件 │
└───────────┘
```
---
## 2. 逻辑分层
```
┌─────────────────────────────────────────────────────────┐
│ Presentation Layer(表现层) │
│ • BanduPaneItem pane 伴读页) │
│ • LibraryWindow(伴读库) │
│ • SettingsPane(设置) │
│ • PlayerController(播放状态机 + <audio>
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ Application Layer(应用层 / 用例) │
│ • PrepareLectureUseCase(备课编排) │
│ • PlayUseCase(播放 / 跳播 / 断点) │
│ • AskQuestionUseCase(问答 + RAG
│ • GenerateReportUseCase(报告 + Note 回写) │
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ Domain Layer(领域层) │
│ • Paper, Paragraph, Chapter │
│ • Lecture, Beat, ParagraphExplanation │
│ • QARecord, Progress, Highlight │
│ • LecturePreparationState(状态机) │
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ Infrastructure Layer(基础设施层) │
│ • ZoteroAdapterItem / Attachment / Note
│ • PdfParserMinerU 封装) │
│ • LlmClientOpenAI-compatible / Ollama
│ • TtsClient(多提供商 + timestamps
│ • SqliteRepository │
│ • AudioStoragemp3 路径管理) │
│ • RagIndex(段落检索) │
│ • PromptRegistry(模板版本管理) │
└─────────────────────────────────────────────────────────┘
```
---
## 3. 模块说明
### 3.1 ZoteroAdapter
**职责**:封装所有 Zotero 7 API 调用。
| 接口 | 说明 |
|------|------|
| `getPdfAttachment(itemId)` | 返回主 PDF 路径、attachmentId |
| `computeFileHash(path)` | SHA-256 |
| `openPdfAtPage(itemId, page)` | 跳转 PDF 阅读器 |
| `createBanduNote(itemId, markdown)` | 创建伴读子笔记 |
| `getCitationKey(itemId)` | Better BibTeX 可选集成 |
| `listLibraryItems()` | 伴读库数据源 |
**约束**UI 与用例层不得直接调用 `Zotero.*` 全局对象。
### 3.2 PdfParser
**职责**:调用 MinerU(一期),输出规范化段落树。
**输入**PDF 本地路径
**输出**`ParseResult { chapters[], paragraphs[], metadata }`
**段落字段**
```typescript
interface Paragraph {
id: string; // 稳定 ID,如 "p-0017"
chapterId: string;
orderIndex: number;
text: string;
page: number;
language?: 'en' | 'zh';
bulletPoints?: string[]; // >150 词长段拆分要点
}
```
**失败策略**:抛出 typed error`ScannedPdfError`, `ParseTimeoutError`),UI 映射为用户文案。
### 3.3 PrepareLectureUseCase
**职责**:编排备课流水线,驱动增量就绪事件。
详见 §5 流水线与 §6 状态机。
### 3.4 LlmClient
**职责**:统一 LLM 调用;支持 streaming。
| 方法 | 用途 |
|------|------|
| `generateSummary(paragraphs[])` | 全文摘要 |
| `generateBeats(summary, paragraphIndex[])` | 整体讲解 beats |
| `generateParagraphExplanation(para, summary, chapterTitle)` | 三层讲解 |
| `generateSentenceExplanation(sentence, context)` | 逐句讲解 |
| `answerQuestion(context, question)` | 问答 stream |
**配置**provider、baseUrl、apiKey、model、temperature(默认 0.20.4)。
### 3.5 TtsClient
**职责**:文本 → mp3 + timestamps。
```typescript
interface TtsResult {
audioPath: string;
timestamps: WordTimestamp[]; // 可为空
durationMs: number;
}
interface WordTimestamp {
text: string;
startMs: number;
endMs: number;
}
```
**策略**:优先 provider 原生 boundaries;缺失则 `HighlightGranularity = SENTENCE`
### 3.6 RagIndex
**职责**:段落级检索,供问答组装上下文。
**一期实现**SQLite FTS5 或 BM25MVP 可不引入 embedding。
```typescript
interface RagIndex {
build(paragraphs: Paragraph[]): void;
search(query: string, topK: number): Paragraph[];
}
```
### 3.7 SqliteRepository + AudioStorage
**职责**:结构化数据与二进制音频分离存储。
- SQLite`{ZoteroDataDir}/bandu/bandu.db`
- 音频:`{ZoteroDataDir}/bandu/audio/{cacheKey}/{unitId}.mp3`
---
## 4. 数据模型
### 4.1 ER 关系
```
PaperCache 1──* Paragraph
PaperCache 1──1 Lecture
Lecture 1──* Beat
Lecture 1──* ParagraphExplanation
PaperCache 1──* QARecord
PaperCache 1──1 Progress
PaperCache 1──* Highlight
```
### 4.2 表结构(SQLite
```sql
-- 论文缓存根表
CREATE TABLE paper_cache (
id TEXT PRIMARY KEY, -- hash(attachment_id + file_hash)
attachment_id TEXT NOT NULL,
item_id TEXT NOT NULL,
file_hash TEXT NOT NULL,
file_path TEXT NOT NULL,
title TEXT,
language TEXT, -- 'en' | 'zh'
parse_status TEXT NOT NULL, --见状态机
lecture_status TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(attachment_id, file_hash)
);
-- 章节
CREATE TABLE chapter (
id TEXT PRIMARY KEY,
paper_id TEXT NOT NULL REFERENCES paper_cache(id),
title TEXT NOT NULL,
order_index INTEGER NOT NULL
);
-- 段落
CREATE TABLE paragraph (
id TEXT PRIMARY KEY,
paper_id TEXT NOT NULL REFERENCES paper_cache(id),
chapter_id TEXT REFERENCES chapter(id),
order_index INTEGER NOT NULL,
text TEXT NOT NULL,
page INTEGER NOT NULL
);
-- 讲义
CREATE TABLE lecture (
paper_id TEXT PRIMARY KEY REFERENCES paper_cache(id),
summary_json TEXT NOT NULL, -- {problem, method, result, limitation}
prompt_version TEXT NOT NULL,
created_at INTEGER NOT NULL
);
-- 整体讲解 beat
CREATE TABLE beat (
id TEXT PRIMARY KEY,
paper_id TEXT NOT NULL REFERENCES paper_cache(id),
order_index INTEGER NOT NULL,
title TEXT,
type TEXT NOT NULL, -- intro|method|experiment|conclusion|extension
script TEXT NOT NULL,
refs_json TEXT NOT NULL, -- ["p-0012","p-0013"]
audio_path TEXT,
timestamps_json TEXT,
tts_status TEXT NOT NULL -- pending|ready|failed
);
-- 逐段三层讲解
CREATE TABLE paragraph_explanation (
paragraph_id TEXT PRIMARY KEY REFERENCES paragraph(id),
paper_id TEXT NOT NULL REFERENCES paper_cache(id),
layer_what TEXT NOT NULL,
layer_how TEXT NOT NULL,
layer_where TEXT NOT NULL,
audio_path TEXT,
timestamps_json TEXT,
gen_status TEXT NOT NULL,
tts_status TEXT NOT NULL
);
-- 播放进度
CREATE TABLE progress (
paper_id TEXT PRIMARY KEY REFERENCES paper_cache(id),
last_mode TEXT, -- overview|paragraph
last_unit_id TEXT,
last_position_ms INTEGER DEFAULT 0,
listened_units_json TEXT, -- 已听 unit id 列表
updated_at INTEGER NOT NULL
);
-- 问答
CREATE TABLE qa_record (
id TEXT PRIMARY KEY,
paper_id TEXT NOT NULL REFERENCES paper_cache(id),
anchor_type TEXT NOT NULL, -- beat|paragraph|sentence|global
anchor_id TEXT,
question TEXT NOT NULL,
answer TEXT NOT NULL,
audio_path TEXT,
created_at INTEGER NOT NULL
);
-- 重点标记
CREATE TABLE highlight (
paper_id TEXT NOT NULL REFERENCES paper_cache(id),
paragraph_id TEXT NOT NULL REFERENCES paragraph(id),
note TEXT,
created_at INTEGER NOT NULL,
PRIMARY KEY (paper_id, paragraph_id)
);
-- 段落 FTSRAG
CREATE VIRTUAL TABLE paragraph_fts USING fts5(
paragraph_id,
paper_id,
text,
tokenize='unicode61'
);
```
### 4.3 缓存键策略
```
cacheKey = SHA256(attachmentId + "|" + fileHash)
失效条件:
• fileHash 变化(PDF 被替换)
• 用户「强制重新备课」
• prompt_version _major 升级(可选策略:仅影响新生成)
```
### 4.4 Lecture JSON 摘要结构
```json
{
"problem": "…",
"method": "…",
"result": "…",
"limitation": "…"
}
```
### 4.5 Beat refs 校验算法
```
输入: beats[], validParagraphIds (Set)
对每个 beat:
beat.refs = beat.refs.filter(id => validParagraphIds.has(id))
if beat.refs.isEmpty:
beat.degraded = true // UI: 不展示原文高亮
输出: beats[]
```
---
## 5. 核心流水线
### 5.1 端到端数据流
```
PDF path
→ [PdfParser] → paragraphs[] → SQLite
→ [LlmClient.generateSummary] → lecture.summary
→ [LlmClient.generateBeats] → beats[] → refs 校验 → SQLite
→ (event: beats_ready) → UI 可播整体模式
→ [LlmClient.generateParagraphExplanation] × N → SQLite
→ (event: paragraph_batch_ready)
→ [TtsClient.synthesize] × units → mp3 → SQLite
→ (event: unit_audio_ready)
```
### 5.2 增量备课时序
```
时间 ──────────────────────────────────────────────►
解析 ████████
摘要 ███
beats ██████ ← beats_ready~1-2min
逐段讲解 ████████████████████
TTS(beats) ████████ ← 整体模式有声音
TTS(段落) █████████████████
```
### 5.3 问答上下文组装
```
AskContext {
question: string
anchor: { type, id, text, explanation? }
summary: SummaryJson
ragParagraphs: Paragraph[] // topK=5
paperTitle: string
}
Prompt 模板顺序:
系统角色 → 全文摘要 → RAG 段落 → 锚点原文+讲解 → 用户问题
```
### 5.4 报告生成
```
ReportBuilder.build(paperId):
summary ← lecture.summary
progress ← progress.listened_units
qas ← qa_record ORDER BY created_at
highlights ← highlight JOIN paragraph
bibtex ← ZoteroAdapter.getCitationKey + export
MarkdownRenderer → ZoteroAdapter.createBanduNote
```
**报告策略(建议 MVP**:同一 Item 只保留一个「伴读报告」笔记;再次生成时**更新**同名 Note(`BanDu 伴读报告: {title}`),避免笔记爆炸。
---
## 6. 状态机
### 6.1 论文 / 备课状态(lecture_status
```
┌─────────┐
│ idle │ 未开始
└────┬────┘
│ start
┌─────────┐
┌────│ parsing │────┐ fail
│ └────┬────┘ │
│ │ success ▼
│ │ ┌─────────┐
│ └───►│ parsed │
│ └────┬────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ summarizing │
│ └──────┬───────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ beats_generating │
│ └──────┬───────┘
│ │ beats_ready
│ ▼
│ ┌────────────────────┐
│ │ paragraphs_generating │◄──┐
│ └──────────┬─────────┘ │ batch loop
│ │ │
│ └─────────────┘
│ │ all para done
│ ▼
│ ┌────────────────────┐
│ │ tts_generating │
│ └──────────┬─────────┘
│ │
│ ▼
│ ┌──────────────┐
└─────────────►│ ready │
error └──────────────┘
┌─────────┐
│ failed │
└─────────┘
```
**可恢复**:每个阶段完成后持久化;重启插件从 `lecture_status` 继续,跳过已完成步骤。
### 6.2 播放器状态
```
PlayerState {
mode: 'overview' | 'paragraph'
currentUnitId: string
isPlaying: boolean
playbackRate: 1 | 1.5 | 2
highlightCursorMs: number
}
```
**跳播**:切换 `currentUnitId` → 加载对应 mp3 → seek(0) → 更新 Progress。
---
## 7. 接口设计(内部事件)
插件内使用 EventBus 解耦 UI 与长时间任务:
| 事件 | 载荷 | 订阅者 |
|------|------|--------|
| `parse:progress` | `{ percent, message }` | BanduPane |
| `lecture:beats_ready` | `{ paperId, beatCount }` | BanduPane, Player |
| `lecture:paragraph_ready` | `{ paperId, paragraphId }` | BanduPane |
| `tts:unit_ready` | `{ unitId, audioPath }` | Player |
| `lecture:failed` | `{ paperId, error }` | BanduPane |
| `qa:chunk` | `{ textDelta }` | QAPanel |
| `qa:complete` | `{ qaRecordId }` | QAPanel, Report |
**长时间任务**:在 Zotero 主线程外使用 Web Worker 或子进程(MinerU);LLM/TTS 使用 async fetch,避免阻塞 UI。
---
## 8. 外部集成
### 8.1 LLM
| 提供商 | 协议 | 用途 |
|--------|------|------|
| OpenAI 兼容 API | HTTPS SSE | 备课 + 问答 |
| Ollama | localhost HTTP | 全本地备选 |
### 8.2 TTS
| 提供商 | 选型维度 |
|--------|----------|
| 火山 / Azure / MiniMax / ElevenLabs | 中文自然度、价格、word timestamps |
**抽象接口**
```typescript
interface TtsProvider {
name: string;
synthesize(text: string, options: TtsOptions): Promise<TtsResult>;
supportsTimestamps: boolean;
}
```
### 8.3 MinerU
- **集成方式(待 Phase 0 确认)**:子进程 CLIstdin/stdout JSON
- **打包**:插件安装包内含 MinerU 二进制或安装脚本;文档说明 Python 环境依赖
### 8.4 Better BibTeX(可选)
- 检测 `Zotero.BetterBibTeX` 是否存在
- 不存在:从 Item 字段生成简易 BibTeX 或留空字段
---
## 9. 安全与隐私
| 项 | 方案 |
|----|------|
| PDF 文件 | 不离开本地 |
| 发往 LLM 的数据 | 段落 text + 用户问题 + 摘要;设置页明示 |
| API Key | Zotero prefs 加密存储或 OS Keychain |
| 日志 | 不记录 Key、不完整记录段落正文(可截断) |
| Ollama | 默认 localhost,无外网 |
---
## 10. 部署与构建
### 10.1 技术栈
| 层 | 选型 |
|----|------|
| 插件框架 | [zotero-plugin-template](https://github.com/windingwind/zotero-plugin-template) |
| 语言 | TypeScript |
| UI | HTML + CSSFluent 风格贴近 Zotero |
| 音频 | HTML5 `<audio>` |
| 数据库 | better-sqlite3 或 Zotero 内置 SQL(待 POC |
| PDF | MinerUPython,本地 CPU |
### 10.2 目录结构(建议)
```
bandu/
├── addon/
│ ├── bootstrap.js
│ ├── manifest.json
│ └── prefs.js
├── src/
│ ├── ui/
│ │ ├── BanduPane.tsx
│ │ ├── LibraryWindow.tsx
│ │ └── SettingsPane.tsx
│ ├── application/
│ │ ├── PrepareLectureUseCase.ts
│ │ ├── PlayUseCase.ts
│ │ ├── AskQuestionUseCase.ts
│ │ └── GenerateReportUseCase.ts
│ ├── domain/
│ ├── infrastructure/
│ │ ├── zotero/
│ │ ├── pdf/
│ │ ├── llm/
│ │ ├── tts/
│ │ └── storage/
│ └── prompts/
├── tests/
└── docs/
```
### 10.3 版本兼容
- `manifest.json` 声明 `applications.zotero.strict_min_version` / `strict_max_version`
- CI 固定 Zotero 7.x 版本做 smoke test
---
## 11. Phase 0 技术验证(Go / No-Go
| # | 验证项 | 通过标准 | 阻塞 MVP |
|---|--------|----------|----------|
| P0-1 | Item pane 嵌入自定义页 | 可展示 UI、读取当前 Item | 是 |
| P0-2 | 读取 PDF attachment 路径 | 路径有效、哈希可算 | 是 |
| P0-3 | `<audio>` 长音频播放 | 10 min+ 无泄漏/崩溃 | 是 |
| P0-4 | MinerU 解析 3 篇论文 | 段落切分可用率 ≥ 80% | 是 |
| P0-5 | TTS 中文样本 | 自然度可接受 | 是 |
| P0-6 | TTS timestamps | 有则记录格式;无则确认句子级降级 | 否(降级) |
| P0-7 | Zotero Note Markdown | 报告渲染可接受 | 否 |
---
## 12. 性能与扩展
### 12.1 长论文策略
| 页数 | 策略 |
|------|------|
| ≤ 30 | 全量逐段备课 |
| 30–80 | 逐段 lazy:按章节或用户点击生成 |
| > 80 | 默认仅整体讲解;逐段需用户确认(成本提示) |
### 12.2 并发控制
- LLM 备课:beats 串行或小批量并行(≤ 3)避免 rate limit
- TTS:队列化,优先 beats 音频
- 同一 paper 不允许重复触发备课(互斥锁)
---
## 13. 可观测性
| 类型 | 内容 |
|------|------|
| 结构化日志 | paperId, stage, durationMs, errorCode |
| 用户可见进度 | 解析 %、beats 数、段落 i/n、TTS i/n |
| Dogfooding 导出 | 单篇 metrics JSON(可选调试菜单) |
---
## 14. 风险与架构对策
| 风险 | 架构对策 |
|------|----------|
| Zotero API 变更 | Adapter 层隔离;pin 版本 |
| MinerU 打包复杂 | Phase 0 验证;文档化外部依赖安装 |
| refs 幻觉 | 校验器 + degraded beat |
| TTS 无 timestamps | HighlightGranularity 降级 |
| SQLite 锁 | 写操作单线程队列 |
| 备课中断 | 状态机 + 阶段持久化 |
---
## 15. 开放问题
1. SQLite 用 `better-sqlite3` 还是 Zotero `Services.storage` — 待插件 POC 验证 WASM/ native 限制
2. MinerU 随插件分发 vs 用户自行安装 Python 环境
3. 问答 TTS MVP 用流式还是整段合成(影响架构复杂度)
4. 伴读库窗口:全库扫描性能(>5000 items 时的索引策略)
---
## 16. 文档修订记录
| 版本 | 日期 | 说明 |
|------|------|------|
| v0.1 | 2026-08-29 | 自 PLAN.md 拆分为架构初稿 |