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

23 KiB
Raw Permalink Blame History

软件架构设计文档

字段 内容
产品名称 BanDu(伴读)— Zotero 插件
中文名 伴读
英文名 BanDu
文档版本 v0.1
状态 草案
关联文档 PLAN.mdPRD.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 }

段落字段

interface Paragraph {
  id: string;           // 稳定 ID,如 "p-0017"
  chapterId: string;
  orderIndex: number;
  text: string;
  page: number;
  language?: 'en' | 'zh';
  bulletPoints?: string[];  // >150 词长段拆分要点
}

失败策略:抛出 typed errorScannedPdfError, 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。

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。

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)
);

-- 段落 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 摘要结构

{
  "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 只保留一个「伴读报告」笔记;再次生成时更新同名 NoteBanDu 伴读报告: {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 确认):子进程 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
语言 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 全量逐段备课
3080 逐段 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 拆分为架构初稿