35 KiB
ChatPapers 双模式集成架构设计
| 字段 | 内容 |
|---|---|
| 产品名称 | ChatPapers for Zotero |
| 文档名称 | 双模式集成架构(方案 A) |
| 文档版本 | v0.1 |
| 状态 | 草案 |
| 目标平台 | Zotero 9(macOS / Windows) |
| 关联文档 | 双模式 PRD、开发文档、BanDu 架构参考 |
本文档定位:描述 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. 启动与注册流程
// hooks.ts(目标形态,伪代码)
async function onStartup() {
await Promise.all([Zotero.initializationPromise, ...]);
initLocale();
registerPrefs(); // 扩展 TTS / 教师风格分组
registerChatPane(); // 已有:文字对话 section
registerLecturePane(); // 新增:语音伴读 section
registerReaderSelectionHook();
addon.data.initialized = true;
}
// 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)
输出:
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
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
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
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 |
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 表结构
-- 论文缓存根表
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
{
"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 现有结构基础上扩展:
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/ |