23 KiB
软件架构设计文档
| 字段 | 内容 |
|---|---|
| 产品名称 | BanDu(伴读)— Zotero 插件 |
| 中文名 | 伴读 |
| 英文名 | BanDu |
| 文档版本 | v0.1 |
| 状态 | 草案 |
| 关联文档 | PLAN.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(表现层) │
│ • BanduPane(Item 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(基础设施层) │
│ • ZoteroAdapter(Item / Attachment / Note) │
│ • PdfParser(MinerU 封装) │
│ • LlmClient(OpenAI-compatible / Ollama) │
│ • TtsClient(多提供商 + timestamps) │
│ • SqliteRepository │
│ • AudioStorage(mp3 路径管理) │
│ • 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 }
段落字段:
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.2–0.4)。
3.5 TtsClient
职责:文本 → mp3 + timestamps。
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 或 BM25;MVP 可不引入 embedding。
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)
-- 论文缓存根表
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)
);
-- 段落 FTS(RAG)
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 摘要结构
{
"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 |
抽象接口:
interface TtsProvider {
name: string;
synthesize(text: string, options: TtsOptions): Promise<TtsResult>;
supportsTimestamps: boolean;
}
8.3 MinerU
- 集成方式(待 Phase 0 确认):子进程 CLI,stdin/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 |
| 语言 | TypeScript |
| UI | HTML + CSS(Fluent 风格贴近 Zotero) |
| 音频 | HTML5 <audio> |
| 数据库 | better-sqlite3 或 Zotero 内置 SQL(待 POC) |
| MinerU(Python,本地 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. 开放问题
- SQLite 用
better-sqlite3还是 ZoteroServices.storage— 待插件 POC 验证 WASM/ native 限制 - MinerU 随插件分发 vs 用户自行安装 Python 环境
- 问答 TTS MVP 用流式还是整段合成(影响架构复杂度)
- 伴读库窗口:全库扫描性能(>5000 items 时的索引策略)
16. 文档修订记录
| 版本 | 日期 | 说明 |
|---|---|---|
| v0.1 | 2026-08-29 | 自 PLAN.md 拆分为架构初稿 |