增加语音对话功能
This commit is contained in:
@@ -0,0 +1,680 @@
|
||||
# 软件架构设计文档
|
||||
|
||||
| 字段 | 内容 |
|
||||
|------|------|
|
||||
| 产品名称 | 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(表现层) │
|
||||
│ • 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 }`
|
||||
|
||||
**段落字段**:
|
||||
|
||||
```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.2–0.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 或 BM25;MVP 可不引入 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)
|
||||
);
|
||||
|
||||
-- 段落 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 摘要结构
|
||||
|
||||
```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 确认)**:子进程 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](https://github.com/windingwind/zotero-plugin-template) |
|
||||
| 语言 | TypeScript |
|
||||
| UI | HTML + CSS(Fluent 风格贴近 Zotero) |
|
||||
| 音频 | HTML5 `<audio>` |
|
||||
| 数据库 | better-sqlite3 或 Zotero 内置 SQL(待 POC) |
|
||||
| PDF | 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. 开放问题
|
||||
|
||||
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 拆分为架构初稿 |
|
||||
Reference in New Issue
Block a user