945 lines
35 KiB
Markdown
945 lines
35 KiB
Markdown
# ChatPapers 双模式集成架构设计
|
||
|
||
| 字段 | 内容 |
|
||
|------|------|
|
||
| 产品名称 | ChatPapers for Zotero |
|
||
| 文档名称 | 双模式集成架构(方案 A) |
|
||
| 文档版本 | v0.1 |
|
||
| 状态 | 草案 |
|
||
| 目标平台 | Zotero 9(macOS / Windows) |
|
||
| 关联文档 | [双模式 PRD](./DUAL_MODE_PRD.md)、[开发文档](../development.md)、[BanDu 架构参考](./ARCHITECTURE.md) |
|
||
|
||
> **本文档定位**:描述 ChatPapers 插件在保留**文字对话**能力的基础上,扩展**语音伴读**(备课 → 听课 → 语音问答)的架构设计。
|
||
> 产品统一品牌为 **ChatPapers**;两种模式是同一条「与论文对话」产品线的不同交互形态——文字即时聊 vs 语音系统讲。
|
||
|
||
---
|
||
|
||
## 1. 架构概述
|
||
|
||
### 1.1 架构演进
|
||
|
||
```
|
||
v0.2(当前) v1.0(目标)
|
||
───────────────── ─────────────────────────────────
|
||
ChatPapers 插件 ChatPapers 插件(单一 .xpi)
|
||
└── 文字对话 pane ├── 文字对话 pane(已有)
|
||
PDFWorker 纯文本 │ 即时问答 / 总结 / JSON 会话
|
||
LLM 流式 │
|
||
sessions/*.json └── 语音伴读 pane(新增)
|
||
MinerU 段落树
|
||
Lecture 备课 + TTS + 播放器
|
||
SQLite + mp3 缓存
|
||
共享:LLM · PDF 附件 · 笔记 · BibTeX
|
||
```
|
||
|
||
### 1.2 架构风格
|
||
|
||
**Local-first 双模式插件架构**:核心计算与缓存均在用户机器完成;仅将段落文本与用户问题发送至用户配置的 LLM / TTS 服务。无自建后端。
|
||
|
||
### 1.3 设计原则
|
||
|
||
| 原则 | 说明 |
|
||
|------|------|
|
||
| 单一插件、两种对话形态 | 文字对话与语音伴读并存,共享 LLM 与 Zotero 集成 |
|
||
| 模块隔离 | 语音模块不侵入文字对话代码路径 |
|
||
| 备课一次、缓存复用 | Lecture 持久化;attachment ID + 文件哈希为缓存键 |
|
||
| 增量交付 | 摘要 → beats → 逐段 → TTS 分阶段就绪 |
|
||
| UI / 领域 / 集成 分离 | 降低 Zotero API 变动影响 |
|
||
| Grounding 优先 | 生成内容必须可追溯到段落 ID |
|
||
| 优雅降级 | refs 无效、无 timestamps、无 BBT 均有 fallback |
|
||
|
||
### 1.4 系统上下文
|
||
|
||
```
|
||
┌─────────────────┐
|
||
│ 用户 │
|
||
└────────┬────────┘
|
||
│
|
||
┌──────────────▼──────────────┐
|
||
│ Zotero 9(宿主) │
|
||
│ ┌────────────────────────┐ │
|
||
│ │ ChatPapers 插件 │ │
|
||
│ │ ┌──────────┬─────────┐ │ │
|
||
│ │ │文字对话 │语音伴读 │ │ │
|
||
│ │ │ ChatView │LectureUI│ │ │
|
||
│ │ └────┬─────┴────┬────┘ │ │
|
||
│ │ │ Shared │ │ │
|
||
│ │ └─────┬─────┘ │ │
|
||
│ └────────────┼────────────┘ │
|
||
└───────────────┼──────────────┘
|
||
│
|
||
┌─────────────────────────┼─────────────────────────┐
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||
│ Zotero │ │ LLM API │ │ TTS API │
|
||
│ PDFWorker │ │ / Ollama │ │ 火山/Azure等 │
|
||
│ MinerU │ │ (共享) │ │(语音模式) │
|
||
└─────────────┘ └─────────────┘ └─────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────┐
|
||
│ {Profile}/chatpapers/ │
|
||
│ sessions/*.json ← 文字对话 │
|
||
│ lecture/lecture.db ← 语音伴读 │
|
||
│ lecture/audio/ │
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
### 1.5 两种对话形态对比
|
||
|
||
| 维度 | 文字对话(已有) | 语音伴读(新增) |
|
||
|------|------------------|------------------|
|
||
| 用户意图 | 「我现在想问什么」 | 「请 AI 先读完再给我讲」 |
|
||
| 交互 | 打字 → 文字流式回复 | 听课 → 打字提问 → 文字 + 语音回复 |
|
||
| PDF 输入 | PDFWorker 整篇纯文本 | MinerU 结构化段落树 |
|
||
| LLM 用法 | 每轮即时组装 context | 一次性备课 + 按需问答 |
|
||
| 持久化 | `sessions/{itemKey}.json` | `lecture/lecture.db` + mp3 |
|
||
| TTS | 无 | 讲解 + 问答回答均支持 |
|
||
| 笔记 | `ChatPapers:` 前缀 | `ChatPapers 伴读报告:` 前缀 |
|
||
|
||
---
|
||
|
||
## 2. 整体分层架构
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ Presentation Layer(表现层) │
|
||
│ 【文字】chatView.ts · multiChatView.ts · readerPane (Chat sec) │
|
||
│ 【语音】lecturePane.ts · player/ · libraryWindow.ts(P1) │
|
||
│ 【共享】prefs.ts · preferences.xhtml │
|
||
└───────────────────────────────┬──────────────────────────────────┘
|
||
│
|
||
┌───────────────────────────────▼──────────────────────────────────┐
|
||
│ Application Layer(应用层 / 用例) │
|
||
│ 【文字】buildChatMessages · buildMultiChatMessages(已有) │
|
||
│ 【语音】PrepareLectureUseCase · PlayUseCase │
|
||
│ AskQuestionUseCase · GenerateReportUseCase │
|
||
└───────────────────────────────┬──────────────────────────────────┘
|
||
│
|
||
┌───────────────────────────────▼──────────────────────────────────┐
|
||
│ Domain Layer(领域层) │
|
||
│ 【文字】ChatMessage · ExtractResult(已有) │
|
||
│ 【语音】PaperCache · Paragraph · Chapter · Lecture · Beat │
|
||
│ ParagraphExplanation · QARecord · Progress · Highlight │
|
||
│ LecturePreparationState · PlayerState │
|
||
└───────────────────────────────┬──────────────────────────────────┘
|
||
│
|
||
┌───────────────────────────────▼──────────────────────────────────┐
|
||
│ Infrastructure Layer(基础设施层) │
|
||
│ ┌─ 共享 ─────────────────────────────────────────────────────┐ │
|
||
│ │ llm/client · llm/providers · zotero/notes · zotero/citeExport│ │
|
||
│ │ pdf/extractor (附件定位) · pdf/selection · utils/* │ │
|
||
│ └────────────────────────────────────────────────────────────┘ │
|
||
│ ┌─ 文字专属 ─────────────────────────────────────────────────┐ │
|
||
│ │ pdf/context · pdf/multiContext · storage/sessions │ │
|
||
│ └────────────────────────────────────────────────────────────┘ │
|
||
│ ┌─ 语音专属 ─────────────────────────────────────────────────┐ │
|
||
│ │ lecture/infrastructure/pdf (MinerU) · tts/ · storage/sqlite │ │
|
||
│ │ lecture/infrastructure/rag · audioStorage · promptRegistry │ │
|
||
│ └────────────────────────────────────────────────────────────┘ │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 现有代码映射
|
||
|
||
### 3.1 共享层(已实现,语音模式直接复用)
|
||
|
||
| 模块路径 | 职责 | 文字对话 | 语音伴读 |
|
||
|----------|------|:--------:|:--------:|
|
||
| `src/hooks.ts` | 生命周期、注册入口 | ✓ | 扩展注册 |
|
||
| `src/modules/ui/readerPane.ts` | Item pane section 注册 | ✓ | 扩展第二 section |
|
||
| `src/modules/llm/client.ts` | LLM 流式请求 | ✓ | ✓ 备课 + 问答 |
|
||
| `src/modules/llm/providers.ts` | Provider 预设 | ✓ | ✓ 只读共享 prefs |
|
||
| `src/modules/llm/types.ts` | ChatMessage 等类型 | ✓ | 问答层可复用类型 |
|
||
| `src/modules/pdf/extractor.ts` | PDF 附件定位 | ✓ | ✓ 路径 + 扩展 hash |
|
||
| `src/modules/pdf/selection.ts` | Reader 选区 | ✓ | ✓ 「解释这句」 |
|
||
| `src/modules/zotero/notes.ts` | 子笔记创建 | ✓ | ✓ 伴读报告 |
|
||
| `src/modules/zotero/citeExport.ts` | BibTeX 导出 | ✓ | ✓ 参考文献卡片 |
|
||
| `src/utils/markdown.ts` | Markdown 渲染 | ✓ | ✓ 报告预览 |
|
||
| `src/utils/prefs.ts` | Preference 读写 | ✓ | ✓ 扩展 TTS prefs |
|
||
| `src/utils/abort.ts` | 请求取消 | ✓ | ✓ |
|
||
|
||
### 3.2 文字对话专属(已实现,语音模式不得引用)
|
||
|
||
| 模块路径 | 职责 |
|
||
|----------|------|
|
||
| `src/modules/ui/chatView.ts` | 文字对话 UI |
|
||
| `src/modules/ui/multiChatView.ts` | 多 PDF 联合对话 |
|
||
| `src/modules/pdf/context.ts` | 单篇 context 组装 |
|
||
| `src/modules/pdf/multiContext.ts` | 多篇 context 组装 |
|
||
| `src/modules/llm/prompts.ts` | 文字对话 system / summary prompt |
|
||
| `src/modules/llm/history.ts` | 论文材料持久化到消息历史 |
|
||
| `src/modules/storage/sessions.ts` | JSON 会话文件 |
|
||
|
||
### 3.3 语音伴读新建(`src/modules/lecture/`)
|
||
|
||
| 模块 | 职责 |
|
||
|------|------|
|
||
| `lecture/ui/lecturePane.ts` | Item pane「语音伴读」section |
|
||
| `lecture/ui/playerController.ts` | 播放状态机 + `<audio>` |
|
||
| `lecture/ui/unitList.ts` | beats / 段落列表 |
|
||
| `lecture/ui/qaPanel.ts` | 锚定提问 + 流式 + 重播 |
|
||
| `lecture/application/prepareLecture.ts` | 备课流水线编排 |
|
||
| `lecture/application/playLecture.ts` | 播放 / 跳播 / 断点 |
|
||
| `lecture/application/askQuestion.ts` | 锚定问答 + RAG |
|
||
| `lecture/application/generateReport.ts` | 报告组装 + Note 回写 |
|
||
| `lecture/domain/*` | 领域实体与状态机 |
|
||
| `lecture/infrastructure/pdf/parser.ts` | MinerU 封装 |
|
||
| `lecture/infrastructure/pdf/fileHash.ts` | SHA-256 |
|
||
| `lecture/infrastructure/tts/client.ts` | TTS 多提供商 |
|
||
| `lecture/infrastructure/tts/providers.ts` | TTS 预设 |
|
||
| `lecture/infrastructure/storage/repository.ts` | SQLite CRUD |
|
||
| `lecture/infrastructure/storage/audioStorage.ts` | mp3 路径管理 |
|
||
| `lecture/infrastructure/rag/index.ts` | FTS5 段落检索 |
|
||
| `lecture/infrastructure/llm/lecturePrompts.ts` | 备课 / beats / 三层讲解 prompt |
|
||
| `lecture/infrastructure/events.ts` | EventBus |
|
||
|
||
---
|
||
|
||
## 4. 依赖规则
|
||
|
||
```
|
||
┌─────────────┐
|
||
│ utils/ │
|
||
└──────┬──────┘
|
||
│
|
||
┌────────────────┼────────────────┐
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||
│ llm/ │ │ pdf/ext │ │ zotero/ │
|
||
└────┬─────┘ └────┬─────┘ └────┬─────┘
|
||
│ │ │
|
||
┌────┴───────┐ │ ┌────┴─────┐
|
||
│ │ │ │ │
|
||
▼ ▼ ▼ ▼ ▼
|
||
chatView multiChat selection notes citeExport
|
||
(文字 UI) (文字 UI) (共享)
|
||
|
||
lecture/* ──可引用──► llm/, pdf/extractor, pdf/selection, zotero/, utils/
|
||
|
||
chatView / multiChat / context / sessions ──禁止引用──► lecture/*
|
||
lecture/* ──禁止引用──► chatView, context, sessions, multiContext
|
||
```
|
||
|
||
**运行时隔离**:两个 pane 的 `onAsyncRender` 各自实例化 View,异常在 View 边界捕获,不向上冒泡至 `hooks.ts` 导致整个插件失效。
|
||
|
||
---
|
||
|
||
## 5. 启动与注册流程
|
||
|
||
```typescript
|
||
// hooks.ts(目标形态,伪代码)
|
||
async function onStartup() {
|
||
await Promise.all([Zotero.initializationPromise, ...]);
|
||
|
||
initLocale();
|
||
registerPrefs(); // 扩展 TTS / 教师风格分组
|
||
registerChatPane(); // 已有:文字对话 section
|
||
registerLecturePane(); // 新增:语音伴读 section
|
||
registerReaderSelectionHook();
|
||
|
||
addon.data.initialized = true;
|
||
}
|
||
```
|
||
|
||
```typescript
|
||
// readerPane.ts 扩展策略
|
||
export function registerChatPane() { /* 现有,不动 */ }
|
||
|
||
export function registerLecturePane() {
|
||
Zotero.ItemPaneManager.registerSection({
|
||
paneID: "chatpapers-lecture",
|
||
pluginID: config.addonID,
|
||
header: { l10nID: "chatpapers-item-section-lecture-head", ... },
|
||
onItemChange: ({ item, setEnabled }) => {
|
||
setEnabled(Boolean(item && findPdfAttachment(item)));
|
||
},
|
||
onAsyncRender: async ({ body, item }) => {
|
||
const view = new LecturePaneView(doc, body, item);
|
||
await view.mount();
|
||
},
|
||
// ...
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 语音伴读模块详细设计
|
||
|
||
### 6.1 Zotero 集成适配(复用 + 薄封装)
|
||
|
||
语音模块不新建独立 Adapter 文件树,而是在 `lecture/infrastructure/zotero/` 做薄封装,内部调用已有模块:
|
||
|
||
| 接口 | 实现 |
|
||
|------|------|
|
||
| `getPdfAttachment(item)` | 委托 `findPdfAttachment` |
|
||
| `computeFileHash(path)` | 新增:`IOUtils.read` + SubtleCrypto / nsICryptoHash |
|
||
| `openPdfAtPage(itemId, page)` | 新增:Zotero.Reader API |
|
||
| `createLectureNote(item, markdown)` | 委托 `createChildNote`,标题 `ChatPapers 伴读报告: {title}` |
|
||
| `exportBibtex(item)` | 委托 `citeExport.formatCiteText` |
|
||
|
||
### 6.2 PdfParser(MinerU)
|
||
|
||
**与文字对话 PDF 路径分离**:文字对话继续 `PDFWorker.getFullText`;语音伴读走 MinerU。
|
||
|
||
**输入**:PDF 本地路径(来自 attachment)
|
||
**输出**:
|
||
|
||
```typescript
|
||
interface ParseResult {
|
||
chapters: Chapter[];
|
||
paragraphs: Paragraph[];
|
||
metadata: { pageCount: number; detectedLanguage?: "en" | "zh" };
|
||
}
|
||
|
||
interface Paragraph {
|
||
id: string; // "p-0017"
|
||
chapterId: string;
|
||
orderIndex: number;
|
||
text: string;
|
||
page: number;
|
||
language?: "en" | "zh";
|
||
bulletPoints?: string[];
|
||
}
|
||
```
|
||
|
||
**失败策略**:
|
||
|
||
| 错误类型 | UI 文案 |
|
||
|----------|---------|
|
||
| `ScannedPdfError` | 扫描版 PDF,请先 OCR |
|
||
| `ParseTimeoutError` | 解析超时,请重试 |
|
||
| `MinerUNotFoundError` | 未检测到 MinerU,见设置页安装说明 |
|
||
|
||
### 6.3 LLM 扩展(共享 client,专属 prompt)
|
||
|
||
`llm/client.ts` 保持通用 `chatStream`;语音备课在 `lecture/infrastructure/llm/lecturePrompts.ts` 定义模板,通过 client 调用:
|
||
|
||
| 方法 | 位置 | 用途 |
|
||
|------|------|------|
|
||
| `chatStream` | `llm/client.ts`(已有) | 文字对话 + 语音问答 |
|
||
| `generateSummary` | `lecture/.../lectureLlm.ts` | 全文摘要四字段 |
|
||
| `generateBeats` | 同上 | 8–15 beats + refs[] |
|
||
| `generateParagraphExplanation` | 同上 | 三层讲解 |
|
||
| `generateSentenceExplanation` | 同上 | 逐句讲解 |
|
||
| `answerWithRag` | `lecture/application/askQuestion.ts` | 锚定问答 |
|
||
|
||
**配置**:语音备课 temperature 0.2–0.4;与文字对话共用 `getRuntimeConfig()`,不新增 LLM prefs 键。
|
||
|
||
### 6.4 TtsClient
|
||
|
||
```typescript
|
||
interface TtsResult {
|
||
audioPath: string;
|
||
timestamps: WordTimestamp[];
|
||
durationMs: number;
|
||
}
|
||
|
||
interface WordTimestamp {
|
||
text: string;
|
||
startMs: number;
|
||
endMs: number;
|
||
}
|
||
|
||
interface TtsProvider {
|
||
name: string;
|
||
synthesize(text: string, options: TtsOptions): Promise<TtsResult>;
|
||
supportsTimestamps: boolean;
|
||
}
|
||
```
|
||
|
||
**高亮降级链**:`WORD` → `SENTENCE` → `PARAGRAPH` → `NONE`(仅进度条)
|
||
|
||
**Prefs 键(建议)**:`extensions.zotero.chatpapers.ttsProvider` / `ttsApiKey` / `ttsVoice`
|
||
|
||
### 6.5 RagIndex
|
||
|
||
```typescript
|
||
interface RagIndex {
|
||
build(paperId: string, paragraphs: Paragraph[]): Promise<void>;
|
||
search(paperId: string, query: string, topK: number): Paragraph[];
|
||
}
|
||
```
|
||
|
||
一期 SQLite FTS5,不引入 embedding。索引在解析完成后构建,与 `paragraph_fts` 表同步。
|
||
|
||
### 6.6 PrepareLectureUseCase
|
||
|
||
**职责**:编排备课流水线;发出增量就绪事件;保证同一 paper 互斥(备课锁)。
|
||
|
||
**伪流程**:
|
||
|
||
```
|
||
start(paperId):
|
||
if lecture_status != idle && != failed: return // 防重入
|
||
set status → parsing
|
||
paragraphs ← PdfParser.parse(path)
|
||
persist paragraphs + FTS
|
||
set status → summarizing
|
||
summary ← Llm.generateSummary(paragraphs)
|
||
persist lecture.summary
|
||
set status → beats_generating
|
||
beats ← Llm.generateBeats(summary, paragraphIndex)
|
||
beats ← validateRefs(beats, paragraphIds)
|
||
persist beats
|
||
emit lecture:beats_ready // UI 可开整体讲解
|
||
set status → paragraphs_generating
|
||
for batch in paragraphs.chunk(5):
|
||
explanations ← Llm.generateParagraphExplanation(batch)
|
||
persist + emit lecture:paragraph_ready
|
||
set status → tts_generating
|
||
for unit in beats + explanations:
|
||
audio ← Tts.synthesize(unit.script)
|
||
persist audio_path + emit tts:unit_ready
|
||
set status → ready
|
||
```
|
||
|
||
### 6.7 PlayUseCase + PlayerController
|
||
|
||
```typescript
|
||
interface PlayerState {
|
||
mode: "overview" | "paragraph";
|
||
currentUnitId: string;
|
||
isPlaying: boolean;
|
||
playbackRate: 1 | 1.5 | 2;
|
||
highlightCursorMs: number;
|
||
}
|
||
```
|
||
|
||
**跳播**:`currentUnitId` 变更 → 加载 mp3 → `audio.currentTime = 0` → 更新 `progress` 表。
|
||
|
||
**断点续播**:pane 打开时读 `progress.last_unit_id` + `last_position_ms`。
|
||
|
||
### 6.8 AskQuestionUseCase
|
||
|
||
与文字对话的差异:
|
||
|
||
| 项 | 文字对话 | 语音伴读问答 |
|
||
|----|----------|--------------|
|
||
| 上下文 | 整篇 PDF 文本(可能截断) | 锚点 + 摘要 + RAG topK=5 |
|
||
| 存储 | sessions JSON | qa_record 表 |
|
||
| 输出 | 仅文字流 | 文字流 + TTS mp3 |
|
||
| 锚点 | 选区字符串 | beat / paragraph / sentence / global |
|
||
|
||
```typescript
|
||
interface AskContext {
|
||
question: string;
|
||
anchor: {
|
||
type: "beat" | "paragraph" | "sentence" | "global";
|
||
id?: string;
|
||
text?: string;
|
||
explanation?: string;
|
||
};
|
||
summary: SummaryJson;
|
||
ragParagraphs: Paragraph[];
|
||
paperTitle: string;
|
||
}
|
||
```
|
||
|
||
### 6.9 GenerateReportUseCase
|
||
|
||
```
|
||
ReportBuilder.build(paperId):
|
||
summary ← lecture.summary_json
|
||
progress ← progress.listened_units_json
|
||
qas ← qa_record ORDER BY created_at
|
||
highlights ← highlight JOIN paragraph
|
||
bibtex ← citeExport
|
||
|
||
MarkdownRenderer → createLectureNote(parentItem, markdown)
|
||
```
|
||
|
||
**Note 策略**:同名 `ChatPapers 伴读报告: {title}` 存在则更新,避免笔记爆炸。
|
||
|
||
---
|
||
|
||
## 7. 数据模型
|
||
|
||
### 7.1 存储目录
|
||
|
||
```
|
||
{ZoteroProfile}/chatpapers/
|
||
├── sessions/ # 文字对话(已有)
|
||
│ └── {itemKey}-{attachmentKey}.json
|
||
└── lecture/ # 语音伴读(新增)
|
||
├── lecture.db # SQLite
|
||
└── audio/
|
||
└── {cacheKey}/
|
||
├── beat-{id}.mp3
|
||
├── para-{id}.mp3
|
||
└── qa-{id}.mp3
|
||
```
|
||
|
||
### 7.2 缓存键
|
||
|
||
```
|
||
cacheKey = SHA256(attachmentId + "|" + fileHash)
|
||
|
||
失效条件:
|
||
• fileHash 变化
|
||
• 用户强制重新备课
|
||
• lecture prompt_version major 升级(可选)
|
||
```
|
||
|
||
### 7.3 ER 关系
|
||
|
||
```
|
||
PaperCache 1──* Paragraph
|
||
PaperCache 1──1 Lecture
|
||
Lecture 1──* Beat
|
||
Lecture 1──* ParagraphExplanation
|
||
PaperCache 1──* QARecord
|
||
PaperCache 1──1 Progress
|
||
PaperCache 1──* Highlight
|
||
```
|
||
|
||
### 7.4 SQLite 表结构
|
||
|
||
```sql
|
||
-- 论文缓存根表
|
||
CREATE TABLE paper_cache (
|
||
id TEXT PRIMARY KEY,
|
||
attachment_id TEXT NOT NULL,
|
||
item_id TEXT NOT NULL,
|
||
file_hash TEXT NOT NULL,
|
||
file_path TEXT NOT NULL,
|
||
title TEXT,
|
||
language TEXT,
|
||
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,
|
||
prompt_version TEXT NOT NULL,
|
||
created_at INTEGER NOT NULL
|
||
);
|
||
|
||
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,
|
||
script TEXT NOT NULL,
|
||
refs_json TEXT NOT NULL,
|
||
audio_path TEXT,
|
||
timestamps_json TEXT,
|
||
tts_status TEXT NOT NULL
|
||
);
|
||
|
||
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,
|
||
last_unit_id TEXT,
|
||
last_position_ms INTEGER DEFAULT 0,
|
||
listened_units_json TEXT,
|
||
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,
|
||
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)
|
||
);
|
||
|
||
CREATE VIRTUAL TABLE paragraph_fts USING fts5(
|
||
paragraph_id,
|
||
paper_id,
|
||
text,
|
||
tokenize='unicode61'
|
||
);
|
||
```
|
||
|
||
### 7.5 Beat refs 校验
|
||
|
||
```
|
||
输入: beats[], validParagraphIds (Set)
|
||
对每个 beat:
|
||
beat.refs = beat.refs.filter(id => validParagraphIds.has(id))
|
||
if beat.refs.isEmpty:
|
||
beat.degraded = true
|
||
输出: beats[]
|
||
```
|
||
|
||
### 7.6 摘要 JSON
|
||
|
||
```json
|
||
{
|
||
"problem": "…",
|
||
"method": "…",
|
||
"result": "…",
|
||
"limitation": "…"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 核心流水线
|
||
|
||
### 8.1 语音伴读端到端
|
||
|
||
```
|
||
PDF attachment path
|
||
→ findPdfAttachment + computeFileHash
|
||
→ [MinerU PdfParser] → paragraphs[] → SQLite + FTS
|
||
→ [generateSummary] → lecture.summary
|
||
→ [generateBeats] → refs 校验 → SQLite
|
||
→ emit lecture:beats_ready ──► 整体讲解可播(文本)
|
||
→ [generateParagraphExplanation] × N → SQLite
|
||
→ emit lecture:paragraph_ready
|
||
→ [Tts.synthesize] × units → mp3 → SQLite
|
||
→ emit tts:unit_ready ──► 播放器加载音频
|
||
→ 用户提问 → [AskQuestion + RAG] → 文字流 + TTS
|
||
→ [GenerateReport] → Zotero Note
|
||
```
|
||
|
||
### 8.2 增量备课时序
|
||
|
||
```
|
||
时间 ──────────────────────────────────────────────►
|
||
|
||
解析 ████████
|
||
摘要 ███
|
||
beats ██████ ← beats_ready(~1-2min,可先听文本/TTS排队)
|
||
逐段讲解 ████████████████████
|
||
TTS(beats) ████████
|
||
TTS(段落) █████████████████
|
||
```
|
||
|
||
### 8.3 文字对话路径(不变)
|
||
|
||
```
|
||
Item → findPdfAttachment → PDFWorker.getFullText
|
||
→ buildChatMessages → chatStream → ChatView 渲染
|
||
→ saveSession (JSON)
|
||
→ createChildNote(可选)
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 状态机
|
||
|
||
### 9.1 备课状态(lecture_status)
|
||
|
||
```
|
||
idle → parsing → parsed → summarizing → beats_generating
|
||
→ paragraphs_generating (batch loop) → tts_generating → ready
|
||
任何阶段 ──error──► failed(可从 failed 或任意已完成阶段 resume)
|
||
```
|
||
|
||
**可恢复**:每阶段结束写库;重启后读 `lecture_status`,跳过已完成步骤。
|
||
|
||
### 9.2 播放器状态
|
||
|
||
见 §6.7。pane `onDestroy` 时持久化 progress,释放 `<audio>` 引用。
|
||
|
||
---
|
||
|
||
## 10. 内部事件(EventBus)
|
||
|
||
| 事件 | 载荷 | 订阅者 |
|
||
|------|------|--------|
|
||
| `parse:progress` | `{ paperId, percent, message }` | LecturePane |
|
||
| `lecture:beats_ready` | `{ paperId, beatCount }` | LecturePane, Player |
|
||
| `lecture:paragraph_ready` | `{ paperId, paragraphId }` | LecturePane |
|
||
| `tts:unit_ready` | `{ paperId, unitId, audioPath }` | Player |
|
||
| `lecture:failed` | `{ paperId, error }` | LecturePane |
|
||
| `qa:chunk` | `{ textDelta }` | QAPanel |
|
||
| `qa:complete` | `{ qaRecordId, audioPath? }` | QAPanel |
|
||
|
||
EventBus 实例按 `paperId` 或全局单例均可;MVP 用模块级 `EventTarget` 即可。
|
||
|
||
**长时间任务**:
|
||
- MinerU:子进程(不阻塞 UI)
|
||
- LLM / TTS:`async fetch` + AbortController
|
||
- SQLite 写:单线程队列,避免并发锁
|
||
|
||
---
|
||
|
||
## 11. 设置与 Preference 设计
|
||
|
||
### 11.1 分组结构(preferences.xhtml)
|
||
|
||
```
|
||
ChatPapers 设置
|
||
├── 通用 · LLM ← 两模式共享
|
||
│ Provider / Base URL / API Key / Model / Temperature
|
||
├── 文字对话 ← 已有
|
||
│ System Prompt / Summary Prompt / Answer Language
|
||
└── 语音伴读 ← 新增
|
||
TTS Provider / API Key / Voice
|
||
教师风格(简洁 / 详细)
|
||
MinerU 路径(若需)
|
||
数据流向说明(静态文案)
|
||
```
|
||
|
||
### 11.2 Prefs 键命名
|
||
|
||
沿用 `extensions.zotero.chatpapers.*` 前缀,不新增 addon ID:
|
||
|
||
| 键 | 模式 | 说明 |
|
||
|----|------|------|
|
||
| `provider` | 共享 | 已有 |
|
||
| `apiKey` | 共享 | 已有 |
|
||
| `systemPrompt` | 文字 | 已有 |
|
||
| `ttsProvider` | 语音 | 新增 |
|
||
| `ttsApiKey` | 语音 | 新增 |
|
||
| `ttsVoice` | 语音 | 新增 |
|
||
| `lectureTeacherStyle` | 语音 | 新增 |
|
||
| `mineruPath` | 语音 | 新增,可选 |
|
||
|
||
---
|
||
|
||
## 12. 目标工程结构
|
||
|
||
在 [development.md](../development.md) 现有结构基础上扩展:
|
||
|
||
```text
|
||
src/
|
||
├── hooks.ts
|
||
├── modules/
|
||
│ ├── ui/
|
||
│ │ ├── readerPane.ts # 注册 Chat + Lecture 两个 section
|
||
│ │ ├── chatView.ts # 文字对话(不动)
|
||
│ │ ├── multiChatView.ts # 文字多 PDF(不动)
|
||
│ │ └── prefs.ts
|
||
│ ├── pdf/
|
||
│ │ ├── extractor.ts # 共享:附件定位
|
||
│ │ ├── context.ts # 文字专属
|
||
│ │ ├── multiContext.ts # 文字专属
|
||
│ │ └── selection.ts # 共享
|
||
│ ├── llm/ # 共享 client + providers
|
||
│ ├── storage/
|
||
│ │ └── sessions.ts # 文字专属
|
||
│ ├── zotero/ # 共享
|
||
│ └── lecture/ # ★ 语音伴读(新建)
|
||
│ ├── ui/
|
||
│ │ ├── lecturePane.ts
|
||
│ │ ├── playerController.ts
|
||
│ │ ├── unitList.ts
|
||
│ │ └── qaPanel.ts
|
||
│ ├── application/
|
||
│ │ ├── prepareLecture.ts
|
||
│ │ ├── playLecture.ts
|
||
│ │ ├── askQuestion.ts
|
||
│ │ └── generateReport.ts
|
||
│ ├── domain/
|
||
│ │ ├── types.ts
|
||
│ │ ├── lectureState.ts
|
||
│ │ └── playerState.ts
|
||
│ └── infrastructure/
|
||
│ ├── pdf/
|
||
│ │ ├── parser.ts
|
||
│ │ └── fileHash.ts
|
||
│ ├── tts/
|
||
│ │ ├── client.ts
|
||
│ │ └── providers.ts
|
||
│ ├── storage/
|
||
│ │ ├── repository.ts
|
||
│ │ ├── schema.ts
|
||
│ │ └── audioStorage.ts
|
||
│ ├── rag/
|
||
│ │ └── index.ts
|
||
│ ├── llm/
|
||
│ │ ├── lectureLlm.ts
|
||
│ │ └── lecturePrompts.ts
|
||
│ ├── zotero/
|
||
│ │ └── adapter.ts
|
||
│ └── events.ts
|
||
```
|
||
|
||
**样式**:`addon/content/chatpapers.css` 增加 `.chatpapers-lecture-*` 前缀类,与文字对话样式并列。
|
||
|
||
**i18n**:`addon/locale/*/chatpapers.ftl` 增加 `item-section-lecture-*` 等键。
|
||
|
||
---
|
||
|
||
## 13. 外部集成
|
||
|
||
### 13.1 LLM(共享)
|
||
|
||
| 提供商 | 协议 | 文字对话 | 语音伴读 |
|
||
|--------|------|:--------:|:--------:|
|
||
| OpenAI 兼容 | HTTPS SSE | ✓ | ✓ 备课 + 问答 |
|
||
| Ollama | localhost | ✓ | ✓ |
|
||
|
||
### 13.2 TTS(语音专属)
|
||
|
||
| 提供商 | 选型维度 |
|
||
|--------|----------|
|
||
| 火山 / Azure / MiniMax / ElevenLabs | 中文自然度、价格、word timestamps |
|
||
|
||
### 13.3 MinerU(语音专属)
|
||
|
||
- 集成:子进程 CLI,stdout JSON
|
||
- 分发:Phase 0 决定「随插件文档安装」vs「用户自装 Python 环境」
|
||
|
||
### 13.4 Better BibTeX(共享,可选)
|
||
|
||
语音报告参考文献卡片复用 `citeExport`;逻辑已在 Chat 多 PDF 模式验证。
|
||
|
||
---
|
||
|
||
## 14. 安全与隐私
|
||
|
||
| 项 | 文字对话 | 语音伴读 |
|
||
|----|----------|----------|
|
||
| PDF 文件 | 不上传 | 不上传 |
|
||
| 发往 LLM | 正文 text(可能截断) | 段落 text + 摘要 + 问题 |
|
||
| 发往 TTS | — | 讲解/回答中文文本 |
|
||
| API Key | Zotero prefs | LLM 共享;TTS 独立 pref |
|
||
| 日志 | 不记录 Key | 不记录 Key;段落正文可截断 |
|
||
|
||
设置页「数据流向」分组须同时覆盖两种模式。
|
||
|
||
---
|
||
|
||
## 15. Phase 0 技术验证(Go / No-Go)
|
||
|
||
| # | 验证项 | 通过标准 | 阻塞 MVP |
|
||
|---|--------|----------|----------|
|
||
| P0-1 | 第二 Item pane 与 Chat 并存 | 切换 tab 互不影响 | 是 |
|
||
| P0-2 | MinerU 解析 3 篇论文 | 段落可用率 ≥ 80% | 是 |
|
||
| P0-3 | Zotero 9 插件内 SQLite CRUD | lecture.db 读写正常 | 是 |
|
||
| P0-4 | `<audio>` 10 min+ 播放 | 无泄漏 / 崩溃 | 是 |
|
||
| P0-5 | TTS 中文样本 | 自然度可接受 | 是 |
|
||
| P0-6 | TTS timestamps | 有则记录;无则句子级降级 | 否 |
|
||
| P0-7 | Chat 模式回归 | 对话 / 总结 / 存笔记不受影响 | 是 |
|
||
|
||
P0-1、P0-7 验证**双模式隔离**;P0-2 ~ P0-6 验证**语音链路**。
|
||
|
||
---
|
||
|
||
## 16. 性能与扩展
|
||
|
||
### 16.1 长论文策略
|
||
|
||
| 页数 | 策略 |
|
||
|------|------|
|
||
| ≤ 30 | 全量逐段备课 |
|
||
| 30–80 | 逐段 lazy:按章节或用户触发 |
|
||
| > 80 | 默认仅整体讲解;逐段需用户确认 |
|
||
|
||
### 16.2 并发控制
|
||
|
||
- 备课 LLM:beats 小批量并行 ≤ 3
|
||
- TTS:队列化,beats 优先
|
||
- 同一 paper 备课互斥锁
|
||
- SQLite 写串行队列
|
||
|
||
### 16.3 伴读库窗口(P1)
|
||
|
||
`libraryWindow.ts` 扫描 `paper_cache` 表 + Zotero Items;>5000 items 时仅索引「有过备课记录」的 attachment。
|
||
|
||
---
|
||
|
||
## 17. 可观测性
|
||
|
||
| 类型 | 内容 |
|
||
|------|------|
|
||
| 结构化日志 | `mode: "lecture"`, paperId, stage, durationMs, errorCode |
|
||
| 用户可见进度 | 解析 %、beats n/total、TTS n/total |
|
||
| 调试导出 | 单篇 metrics JSON(开发者菜单,可选) |
|
||
|
||
日志前缀建议 `[ChatPapers:Lecture]`,与 `[ChatPapers:Chat]` 区分。
|
||
|
||
---
|
||
|
||
## 18. 风险与对策
|
||
|
||
| 风险 | 对策 |
|
||
|------|------|
|
||
| 语音模块影响文字对话稳定性 | 依赖隔离 + pane 级 try/catch + P0-7 回归 |
|
||
| MinerU 打包复杂 | Phase 0 验证;文档化安装 |
|
||
| refs 幻觉 | 校验器 + degraded beat |
|
||
| TTS 无 timestamps | 高亮降级链 |
|
||
| SQLite 在插件环境不可用 | Phase 0 备选 sql.js / JSON 分文件 |
|
||
| 双 pane 内存占用 | onDestroy 释放 audio、取消进行中的 fetch |
|
||
|
||
---
|
||
|
||
## 19. 开放问题
|
||
|
||
| # | 问题 | 影响 |
|
||
|---|------|------|
|
||
| 1 | SQLite:better-sqlite3 vs sql.js vs 纯 JSON | 存储层选型 |
|
||
| 2 | MinerU 分发方式 | 安装体验 |
|
||
| 3 | 问答 TTS:流式 vs 整段合成 | AskQuestion 复杂度 |
|
||
| 4 | 文字对话是否未来支持 TTS 朗读回复 | v1.1+ 可统一 TtsClient |
|
||
| 5 | Chat 会话 → 语音问答上下文导入 | v1.1 产品决策 |
|
||
|
||
---
|
||
|
||
## 20. 文档关系
|
||
|
||
```
|
||
docs/requirements.md ChatPapers 文字对话需求(不变)
|
||
docs/development.md 现有工程与工具链(不变)
|
||
docs/newidea/
|
||
├── PLAN.md / PRD.md BanDu 原始策划(参考)
|
||
├── ARCHITECTURE.md 独立插件架构(参考)
|
||
├── DUAL_MODE_PRD.md ★ 双模式集成需求
|
||
└── DUAL_MODE_ARCHITECTURE.md ★ 本文:双模式集成架构
|
||
```
|
||
|
||
**实施顺序**:需求看 DUAL_MODE_PRD.md → 架构看本文 → 领域细节参考 ARCHITECTURE.md §4–§6。
|
||
|
||
---
|
||
|
||
## 21. 文档修订记录
|
||
|
||
| 版本 | 日期 | 说明 |
|
||
|------|------|------|
|
||
| v0.1 | 2026-08-29 | 初稿:ChatPapers 双模式集成架构;语音模块命名 `lecture/` |
|