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

945 lines
35 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.
# ChatPapers 双模式集成架构设计
| 字段 | 内容 |
|------|------|
| 产品名称 | ChatPapers for Zotero |
| 文档名称 | 双模式集成架构(方案 A) |
| 文档版本 | v0.1 |
| 状态 | 草案 |
| 目标平台 | Zotero 9macOS / 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.tsP1
│ 【共享】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 PdfParserMinerU
**与文字对话 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` | 同上 | 815 beats + refs[] |
| `generateParagraphExplanation` | 同上 | 三层讲解 |
| `generateSentenceExplanation` | 同上 | 逐句讲解 |
| `answerWithRag` | `lecture/application/askQuestion.ts` | 锚定问答 |
**配置**:语音备课 temperature 0.20.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(语音专属)
- 集成:子进程 CLIstdout 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 并发控制
- 备课 LLMbeats 小批量并行 ≤ 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 | SQLitebetter-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/` |