fix: overhaul install flow clarity and non-destructive incremental setup

- Move Better BibTeX auto-export to post-install step (exports dir created by setup)
- Clarify wizard overview: show resources_dir/literature_dir/control_dir hierarchy
- Fix post-install flow order: BBT export → enable plugin → dashboard → sync
- Split do_ocr and analyze marks into separate steps (OCR before deep-reading)
- Mark all literature flags via Base views, not manual file editing
- Make installer incremental: preserve existing files, only create missing ones
- Remove destructive junction/link deletion in TUI wizard
- Merge .env instead of overwriting; skip overwrite for AGENTS.md, docs, plugin
- Update TUI, headless, and plugin completion output to match corrected flow
- Add regression test for non-destructive setup behavior
This commit is contained in:
Research Assistant 2026-05-06 14:57:29 +08:00
parent 1d52d94445
commit 29edb2d811
7 changed files with 353 additions and 189 deletions

View file

@ -8,21 +8,26 @@
```
[ ] Zotero 已安装 + Better BibTeX 插件已启用
[ ] Better BibTeX 已配置自动导出 JSON见下方配置
[ ] Obsidian 已打开当前 Vault
[ ] PaperForge 已安装 (pip install git+https://github.com/LLLin000/PaperForge.git)
[ ] PaddleOCR API Key 已配置(在 .env 中)
[ ] 目录结构已创建(安装向导会自动完成)
[ ] Zotero 数据目录已链接到 <system_dir>/Zotero
[ ] Better BibTeX 已按下方步骤导出 JSON 到 <system_dir>/PaperForge/exports/
```
> 安装向导是增量式的:如果你选择的 Vault 或目录里已经有文件PaperForge 只会补充缺失的目录和文件,不会删除已有内容。
### Better BibTeX 自动导出配置
1. Zotero → Edit → Preferences → Better BibTeX
2. 勾选 **"Keep updated"**(自动导出)
3. 选择导出格式:**Better BibLaTeX** 或 **Better BibTeX**
4. 导出路径设置为:`{你的Vault路径}/<system_dir>/PaperForge/exports/library.json`
5. 点击 OKJSON 文件会自动生成并保持同步
这一步应在安装向导完成之后再做,因为 `exports/` 目录需要先由安装流程创建。
1. 打开 Zotero
2. 对你要同步的文献库或分类右键 → `导出...`
3. 选择格式:**Better BibTeX JSON**
4. 勾选 **"Keep updated"**(自动导出)
5. 保存到:`{你的Vault路径}/<system_dir>/PaperForge/exports/`
6. JSON 文件名会作为 Base 名称,例如 `library.json`、`骨科.json`
---
@ -330,11 +335,15 @@ pdf_link: "[[99_System/Zotero/storage/KEY/文件名.pdf]]"
## 8. 第一次使用指南(手把手)
### Step 1: 确认 Zotero 有文献
### Step 1: 完成 Better BibTeX 自动导出
先按上面的步骤把 JSON 导出到 `<system_dir>/PaperForge/exports/`。这一步没完成前,`sync` 不会读到文献。
### Step 2: 确认 Zotero 有文献
确保 Zotero 中已有至少一篇带 PDF 的文献,且 Better BibTeX 已导出 JSON。
### Step 2: 运行 sync
### Step 3: 运行 sync
```bash
# 在 Vault 根目录执行
@ -354,15 +363,11 @@ paperforge sync
> - `paperforge sync --selection` — 仅同步 Zotero 到 library-records
> - `paperforge sync --index` — 仅根据现有 library-records 生成正式笔记
### Step 3: 标记要精读的文献
### Step 4: 在 Base 视图中标记 OCR
在 Obsidian 中:
1. 打开 `<resources_dir>/<control_dir>/library-records/骨科/XXXXXXX.md`
2. 将 `do_ocr: false` 改为 `do_ocr: true`
3. 将 `analyze: false` 改为 `analyze: true`
4. 保存文件
打开 Obsidian Base 视图,找到该文献,将 `do_ocr` 设为 `true`
### Step 4: 运行 OCR
### Step 5: 运行 OCR
```bash
paperforge ocr
@ -370,21 +375,18 @@ paperforge ocr
等待完成(可能需要几分钟)。
### Step 5: 检查 OCR 状态
### Step 6: 在 Base 视图中标记精读
OCR 完成后,在 Base 视图中找到该文献,将 `analyze` 设为 `true`
### Step 7: 执行精读
先确认队列就绪:
```bash
paperforge deep-reading
```
预期输出:
```
## 就绪 (1 篇) — OCR 完成
- `XXXXXXX` | 骨科 | 论文标题
```
### Step 6: 执行精读
在 OpenCode Agent 中输入:
然后在 OpenCode Agent 中输入:
```
/pf-deep XXXXXXX
```
@ -394,7 +396,7 @@ Agent 会自动:
2. 逐阶段填写精读内容
3. 验证结构完整性
### Step 7: 查看结果
### Step 8: 查看结果
在 Obsidian 中打开正式笔记,找到 `## 🔍 精读` 区域,精读已完成。

View file

@ -11,6 +11,8 @@
- Obsidian
- PaddleOCR API Key[申请地址](https://paddleocr.baidu.com)
> 安装向导是**增量式**的:如果你选择的 Vault 或目录里已经有文件PaperForge 只会创建缺失的目录和文件,不会删除已有内容。
---
## 安装推荐方式pip
@ -19,29 +21,44 @@
# 1. 安装
pip install git+https://github.com/LLLin000/PaperForge.git
# 2. 运行安装向导
# 2. 运行安装向导(会把插件文件部署到当前 Vault
paperforge setup
```
向导会引导你完成 Vault 配置、Zotero 链接、API Key 设置。
向导会引导你完成 Vault 配置、Zotero 链接、API Key 设置,并把插件文件部署到 `.obsidian/plugins/paperforge/`
安装向导完成后,才进入 Obsidian 启用插件;启用之前不能打开 Dashboard。
---
## Better BibTeX 配置
1. Zotero → Edit → Preferences → Better BibTeX
2. 勾选 **"Keep updated"**
3. 格式Better BibLaTeX
4. 导出路径:`{你的Vault}/[system_dir]/PaperForge/exports/library.json`
5. 点击 OK
这一步在 `paperforge setup` 完成之后再做,因为导出目录要先由安装向导创建。
1. 打开 Zotero
2. 对你要同步的库或分类右键 → `Export...` / `导出...`
3. 格式选择 **Better BibTeX JSON**
4. 勾选 **"Keep updated"**
5. 导出到:`{你的Vault}/[system_dir]/PaperForge/exports/`
6. JSON 文件名会作为 Base 名称,例如:`library.json`、`骨科.json`
---
## 验证安装
```powershell
paperforge status # 确认 setup 已经把 PaperForge 部署到当前 Vault
```
然后在 Obsidian 中:
1. `Settings -> Community Plugins -> Installed -> PaperForge -> Enable`
2. 按 `Ctrl+P` 搜索 `PaperForge`
之后再执行:
```powershell
paperforge doctor # 诊断所有组件
paperforge status # 查看系统状态
```
---

View file

@ -177,6 +177,8 @@ python -c "from paperforge.setup_wizard import EnvChecker; from pathlib import P
### Step 5: 创建目录并部署文件
重要安全规则:这个安装流程必须是增量式的。如果目标 Vault 或所选目录里已经有文件PaperForge 只能创建缺失的目录和文件,不能覆盖已有内容。
把 Step 0 收集到的所有信息拼成一条命令:
```bash
@ -238,20 +240,24 @@ python -m paperforge status
### Step 7: 告诉用户下一步
> 安装完成。接下来你需要做 3 件事:
> 安装完成。接下来你需要做 4 件事:
>
> **1. 配置 Zotero 自动导出 JSON必须**
> PaperForge 的数据来源,不做这一步 sync 无法工作:
> - 打开 Zotero
> - 文件 → 导出库 → 格式选 Better BibTeX
> - 保存到 Vault 里的 <system_dir>/PaperForge/exports/
> - 对要同步的文献库或分类右键 → 导出
> - 格式选 Better BibTeX JSON
> - 必须勾选 "保持更新"
> - 保存到 Vault 里的 <system_dir>/PaperForge/exports/
>
> **2. 在 Obsidian 里启用 PaperForge 插件**
> - 设置 → 社区插件 → 已安装 → PaperForge → 启用
> - Ctrl+P 输入 "PaperForge"
> - Ctrl+P 输入 "PaperForge",打开 Dashboard
>
> **3. 若跳过了 PaddleOCR Key**
> **3. 同步文献**
> - 在 Dashboard 中点击 `Sync Library`
>
> **4. 若跳过了 PaddleOCR Key**
> - 在 <system_dir>/PaperForge/.env 里添加:
> PADDLEOCR_API_TOKEN=<你的key>

View file

@ -177,6 +177,8 @@ python -c "from paperforge.setup_wizard import EnvChecker; from pathlib import P
### Step 5: Create directories and deploy files
Important safety rule: this setup flow is additive. If the target vault or selected directories already contain files, PaperForge should create only missing directories/files and preserve existing content.
Assemble one command from all Step 0 values:
```bash
@ -243,15 +245,19 @@ python -m paperforge status
> **1. Configure Zotero auto-export JSON (required)**
> This is PaperForge's data source. Sync won't work without it:
> - Open Zotero
> - File → Export Library → Format: Better BibTeX
> - Save to <system_dir>/PaperForge/exports/ in your vault
> - Right-click the library or collection you want to sync → Export
> - Format: Better BibTeX JSON
> - Must check "Keep Updated"
> - Save to <system_dir>/PaperForge/exports/ in your vault
>
> **2. Enable PaperForge plugin in Obsidian**
> - Settings → Community Plugins → Installed → PaperForge → Enable
> - Ctrl+P, type "PaperForge"
> - Ctrl+P, type "PaperForge" and open the dashboard
>
> **3. If you skipped PaddleOCR Key**
> **3. Sync literature**
> - In the dashboard, click `Sync Library`
>
> **4. If you skipped PaddleOCR Key**
> - Add to <system_dir>/PaperForge/.env:
> PADDLEOCR_API_TOKEN=<your key>

View file

@ -44,6 +44,8 @@
2. Zotero 中已有至少一篇带 PDF 附件的文献
3. Better BibTeX 插件已安装Zotero → 工具 → 插件 → 搜索 Better BibTeX
> 安装向导是**增量式**的:如果你选择的 Vault 或目录里已经有文件PaperForge 只会创建缺失的目录和文件,不会删除已有内容。
---
## 2. 安装 PaperForge
@ -93,47 +95,45 @@ paperforge setup
你会看到一个图形化界面(终端内的交互式界面)。
这一步会把 PaperForge 需要的文件部署到当前 Vault包括 `.obsidian/plugins/paperforge/`。在这一步完成之前Obsidian 里还不能启用插件,也不能打开 Dashboard。
### 3.2 向导步骤详解
**第 1 步:选择 Agent 平台**
- 选择你用哪个 AI 编程工具OpenCode / Cursor / Claude Code / Windsurf / GitHub Copilot / Cline / Augment / Trae
- 这决定了后续脚本部署的位置
**第 1 步:概览**
- 向导会先展示目录层级,帮助你理解哪些内容在 Vault 根目录,哪些内容在 `resources_dir` 下面
- 重点关系是:
- `<resources_dir>/<literature_dir>/`:正式文献笔记
- `<resources_dir>/<control_dir>/library-records/`:每篇文献的状态跟踪
- `<system_dir>/PaperForge/exports/`Better BibTeX JSON 导出目录
- 这一步也会明确说明:如果目标目录里已有文件,安装只做增量创建,不覆盖已有内容
**第 2 步:目录配置**
- 在这里设置目录名称,而不是手动创建目录
- 默认结构如下:
- `99_System`系统文件、OCR 结果、exports、worker
- `03_Resources`:用户文献数据根目录
- `03_Resources/Literature`:正式文献笔记
- `03_Resources/LiteratureControl/library-records`:状态卡片
- `05_Bases`Obsidian Base 视图
**第 3 步:平台与密钥**
- 选择 Agent 平台OpenCode / Cursor / Claude Code / Windsurf / GitHub Copilot / Cline / Augment / Trae
- **如果不确定,选 OpenCode**
- 填写 PaddleOCR API Key
- 可选填写 Zotero 数据目录,便于自动定位 PDF
**第 2 步:配置 Vault 路径**
- Vault 是你的 Obsidian 笔记库根目录
- 如果已经在 Obsidian 中打开了一个 vault直接输入该目录路径
- 如果还没有 vault建议先创建一个空目录
**第 3 步:配置目录名称**
- **系统目录system_dir**PaperForge 内部文件存放的目录,默认 `99_System`
- 包含OCR 结果、Zotero 导出、配置文件
- 建议保持默认,或改为 `System`
- **资源目录resources_dir**:文献笔记存放的目录,默认 `03_Resources`
- 包含文献索引、library-records
- 建议保持默认
**第 4 步:配置 Zotero 数据目录**
- 向导会自动检测 Zotero 数据目录位置
- 如果自动检测失败,手动输入路径:
- Windows 默认:`C:\Users\你的用户名\Zotero`
- macOS 默认:`~/Zotero`
- 向导会自动创建 junction/symlink 链接到 vault 内
**第 5 步:配置 PaddleOCR API Key**
- 输入从 https://paddleocr.baidu.com 获取的 API Token
- API URL 保持默认即可:`https://paddleocr.aistudio-app.com/api/v2/ocr/jobs`
**第 6 步:一键部署**
**第 4 步:安装**
- 向导会自动:
1. 创建 vault 目录结构
2. 复制 worker 脚本
3. 复制精读脚本ld_deep.py
4. 复制图表阅读指南
5. 复制 Agent 命令文件
6. 生成 `.env` 配置文件
7. 生成 `paperforge.json`
8. 验证文件完整性
1. 创建缺失目录
2. 部署 worker、技能和插件文件
3. 写入 `paperforge.json`
4. 创建或补充 `.env`
5. 验证安装结果
**第 5 步:完成**
- 先回到 Obsidian 启用插件:`Settings -> Community Plugins -> Installed -> PaperForge -> Enable`
- 然后再配置 Better BibTeX 自动导出
- 最后才是打开 Dashboard、运行 `Sync Library`、运行 OCR
### 3.3 安装后文件结构
@ -156,14 +156,13 @@ your-vault/
│ └── .env # PaddleOCR API Key
├── [resources_dir]/ # 你自定义的资源目录
│ └── [literature_dir]/ # 文献笔记
│ └── [domain]/ # 按 Zotero 分类
│ └── [key] - [Title].md # 正式文献笔记
├── [control_dir]/ # 文献状态控制
│ └── library-records/
│ └── [domain]/
│ └── [key].md # 单条文献状态
│ ├── [literature_dir]/ # 正式文献笔记
│ │ └── [domain]/ # 按 Zotero 分类
│ │ └── [key] - [Title].md # 正式文献笔记
│ └── [control_dir]/ # 文献状态控制
│ └── library-records/
│ └── [domain]/
│ └── [key].md # 单条文献状态
├── [base_dir]/ # Obsidian Base 视图
│ └── [domain].base # 每个分类一个 Base
@ -185,17 +184,20 @@ your-vault/
### 4.1 配置 Better BibTeX 自动导出
这一步要在安装向导完成后再做,因为 `exports/` 目录要先由安装流程创建。
这一步是关键——PaperForge 通过 Better BibTeX 的自动导出 JSON 来感知 Zotero 中的文献变化。
1. 打开 Zotero → Edit → Preferences → Better BibTeX
2. **勾选 "Keep updated"**(自动导出)
3. **导出格式**:选择 **Better BibLaTeX**
4. **导出路径**设置为:
1. 打开 Zotero
2. 对你要同步的文献库或分类右键 → **Export... / 导出...**
3. **导出格式**:选择 **Better BibTeX JSON**
4. **勾选 "Keep updated"**(自动导出)
5. **导出路径**设置为:
```
{你的Vault路径}/[system_dir]/PaperForge/exports/library.json
```
例如:`C:\MyVault\99_System\PaperForge\exports\library.json`
5. **点击 OK 保存**
6. **点击保存**
### 4.2 按分类导出(可选)
@ -279,18 +281,9 @@ paperforge sync
- `[base_dir]/` 下有了 `.base` 文件Base 视图)
- 在 Obsidian 的 Base 插件中可以看到控制面板
### 6.3 标记要精读的文献
### 6.3 在 Base 视图中标记 OCR
在 Obsidian 中找到 `library-records/[domain]/[key].md`,修改 frontmatter
```yaml
---
do_ocr: true # 需要 OCR 提取全文
analyze: true # 需要精读
---
```
保存后,该文献会进入 OCR 队列。
在 Obsidian Base 视图中找到该文献,将 `do_ocr` 设为 `true`。保存后该文献会进入 OCR 队列。
### 6.4 运行 OCR
@ -318,7 +311,11 @@ ocr: updated 2 records
- `[system_dir]/PaperForge/ocr/[key]/meta.json` — OCR 元数据
- `[system_dir]/PaperForge/ocr/[key]/figure-map.json` — 图表索引
### 6.5 执行精读
### 6.5 在 Base 视图中标记精读
OCR 完成后,在 Obsidian Base 视图中找到该文献,将 `analyze` 设为 `true`
### 6.6 执行精读
在 OpenCode 中(或你选择的 Agent 平台)输入:

View file

@ -26,7 +26,7 @@ Object.assign(LANG.en, {
setup_done: 'PaperForge environment is ready',
setup_pending: 'Not installed yet. Finish the preparation items below, then open the wizard.',
section_prep: 'Preparation',
section_prep_desc: 'Before first use, finish these 5 preparation items, then open the setup wizard:',
section_prep_desc: 'Before first use, finish these 4 preparation items. Better BibTeX auto-export is configured after setup:',
section_guide: 'How To Use',
section_config: 'Current Configuration',
prep_python_desc: 'Python must be available from the command line. If you are not sure, click below to auto-detect.',
@ -38,7 +38,7 @@ Object.assign(LANG.en, {
prep_key_desc: 'Get your API key from https://aistudio.baidu.com/paddleocr',
guide_open: 'Open Main Panel',
guide_open_desc: 'Press Ctrl+P and run "PaperForge: Open Main Panel", or click the PaperForge icon in the left sidebar.',
guide_sync_desc: 'In the main panel, click Sync Library to import papers from Zotero into Obsidian and generate notes automatically.',
guide_sync_desc: 'After Better BibTeX JSON export is configured, click Sync Library to import papers from Zotero into Obsidian and generate notes automatically.',
guide_ocr_desc: 'In the main panel, click Run OCR to extract full text and figures from PDFs for later reading and analysis.',
btn_install: 'Open Setup Wizard',
btn_install_desc: 'Check whether the environment is ready, then open the step-by-step setup wizard',
@ -46,12 +46,13 @@ Object.assign(LANG.en, {
wizard_step2: 'Directory Setup',
wizard_step3: 'Platform & Keys',
wizard_intro: 'This wizard walks you through the full setup. In most cases, the default values are fine to keep.',
wizard_dir_hint: 'The resources directory is the main folder for your literature data. PaperForge will create these folders inside it:',
wizard_dir_sub_hint: 'Sub-folders under the resources directory:',
wizard_sys_hint: 'System folders stored at the vault root:',
wizard_dir_hint: 'PaperForge stores user-facing literature data under the resources directory. These folders will live there:',
wizard_dir_sub_hint: 'Resolved folder preview based on the names below:',
wizard_sys_hint: 'These folders live at the vault root, outside the resources directory:',
wizard_agent_hint: 'Choose the AI agent platform you use most often. PaperForge will place the matching command and skill files in the correct location.',
wizard_keys_hint: 'Enter your PaddleOCR API key below. If you want PaperForge to auto-locate Zotero PDFs, you can also fill in the Zotero data directory.',
wizard_preview: 'After installation, system files and agent config stay at the vault root, while literature data is stored under the resources directory.',
wizard_preview: 'After installation, system files stay at the vault root while literature data stays under the resources directory.',
wizard_safety: 'Safety: if the selected folders already contain files, setup preserves existing files and only creates missing PaperForge folders and files.',
field_zotero_placeholder: 'Optional. Helps auto-locate PDF attachments in Zotero storage',
install_btn: 'Start Install',
install_validating: 'Validating setup...',
@ -63,8 +64,9 @@ Object.assign(LANG.en, {
complete_next: 'Recommended next steps',
complete_step1_desc: 'Press Ctrl+P and run "PaperForge: Open Main Panel", or click the PaperForge icon in the left sidebar.',
complete_step2_desc: 'In the main panel, click Sync Library to bring papers from Zotero into Obsidian and generate notes.',
complete_step3_desc: 'In the main panel, click Run OCR to extract full text and figures from PDFs.',
complete_step3_desc: 'In the Obsidian Base view, mark do_ocr:true on papers, then run OCR in the main panel.',
complete_step4: 'Configure Better BibTeX Auto-export',
complete_step4_desc: 'In Zotero, right-click the library or collection you want to sync -> Export -> Better BibTeX JSON -> enable "Keep updated".',
complete_export_path: 'Save Better BibTeX JSON exports into:',
nav_prev: 'Back',
nav_next: 'Next',
@ -86,7 +88,7 @@ Object.assign(LANG.zh, {
desc: 'Obsidian + Zotero 文献管理流水线。自动同步文献、生成笔记、OCR 提取全文,一站式完成文献整理与精读。',
setup_done: 'PaperForge 环境已准备完成',
setup_pending: '尚未完成安装。请先完成下面的准备,再打开安装向导。',
section_prep_desc: '第一次使用前,请先完成下面 5 项准备。完成后再打开安装向导',
section_prep_desc: '第一次使用前,请先完成下面 4 项准备。Better BibTeX 自动导出放在安装完成后配置',
prep_python_desc: '确认系统可以直接运行 Python 命令。如果不确定,点击下方按钮自动检测。',
prep_zotero_desc: '先安装 Zoterohttps://www.zotero.org',
prep_bbt_desc: '在 Zotero 中依次打开:工具 -> 插件 -> 安装 Better BibTeX。',
@ -95,19 +97,20 @@ Object.assign(LANG.zh, {
prep_export_path_label: '请把导出的 JSON 文件保存到这个文件夹:',
guide_open: '打开主面板',
guide_open_desc: '按 Ctrl+P输入 PaperForge: 打开主面板;或点击左侧的 PaperForge 图标。',
guide_sync_desc: '在主面板点击 Sync Library把 Zotero 中的文献同步到 Obsidian并自动生成笔记。',
guide_sync_desc: '先完成 Better BibTeX JSON 自动导出配置,再在主面板点击 Sync Library把 Zotero 文献同步到 Obsidian 并自动生成笔记。',
guide_ocr_desc: '在主面板点击 Run OCR从 PDF 中提取全文和图表,供后续精读和分析使用。',
btn_install_desc: '先检查环境是否就绪,再打开分步安装向导',
btn_reconfig_desc: '重新打开安装向导,修改目录、平台或密钥配置',
wizard_step2: '目录配置',
wizard_step3: '平台与密钥',
wizard_intro: '这个向导会一步步帮你完成安装。大部分选项保持默认即可,安装时会自动创建所需目录。',
wizard_dir_hint: '资源目录是文献数据的主目录。PaperForge 会在里面创建以下子目录',
wizard_dir_sub_hint: '资源目录中的子目录',
wizard_sys_hint: '位于 Vault 根目录的系统文件',
wizard_dir_hint: '资源目录用于存放用户可见的文献数据。下面这些目录都会位于资源目录内部',
wizard_dir_sub_hint: '根据下面的目录名,最终路径会是',
wizard_sys_hint: '这些目录位于 Vault 根目录,不属于资源目录',
wizard_agent_hint: '选择你平时使用的 AI Agent 平台。安装完成后PaperForge 会把对应的命令和技能文件放到正确位置。',
wizard_keys_hint: '下面填写 PaddleOCR API 密钥;如果你希望自动定位 Zotero 中的 PDF也可以补充 Zotero 数据目录。',
wizard_preview: '安装后:系统文件和 Agent 配置位于 Vault 根目录,文献数据统一放在资源目录内。以后仍可在设置页修改。',
wizard_preview: '安装后:系统文件位于 Vault 根目录,文献数据统一放在资源目录内。以后仍可在设置页修改。',
wizard_safety: '安全说明:如果你选择的目录里已经有文件,安装向导会保留已有内容,只补充缺失的 PaperForge 文件和目录。',
field_zotero_placeholder: '可选。填写后可帮助自动定位 Zotero 存储中的 PDF 附件',
install_validating: '正在校验安装环境...',
install_bootstrapping: '未检测到 PaperForge Python 包,正在自动安装...',
@ -117,9 +120,10 @@ Object.assign(LANG.zh, {
complete_summary: '已保存的安装配置',
complete_next: '建议下一步',
complete_step1_desc: '按 Ctrl+P输入 PaperForge: 打开主面板;或点击左侧的 PaperForge 图标。',
complete_step2_desc: '在主面板点击 Sync Library把 Zotero 文献同步到 Obsidian并自动生成笔记。',
complete_step3_desc: '在主面板点击 Run OCR,从 PDF 中提取全文和图表。',
complete_step2_desc: '在主面板点击 Sync Library把 Zotero 文献同步到 Obsidian 并自动生成笔记。',
complete_step3_desc: '在 Obsidian Base 视图中将文献的 do_ocr 设为 true然后在主面板点击 Run OCR。',
complete_step4: '配置 Better BibTeX 自动导出',
complete_step4_desc: '在 Zotero 中右键要同步的文献库或分类 -> 导出 -> 选择 Better BibTeX JSON -> 勾选“保持更新”。',
complete_export_path: 'Better BibTeX 导出的 JSON 文件请保存到:',
nav_prev: '上一步',
nav_next: '下一步',
@ -1548,17 +1552,12 @@ class PaperForgeSettingTab extends PluginSettingTab {
['prep_python', 'prep_python_desc'],
['prep_zotero', 'prep_zotero_desc'],
['prep_bbt', 'prep_bbt_desc'],
['prep_export', 'prep_export_desc'],
['prep_key', 'prep_key_desc'],
];
for (const [kTitle, kDesc] of prepData) {
const row = prep.createEl('div', { cls: 'paperforge-guide-item' });
row.createEl('strong', { text: t(kTitle) });
row.createEl('span', { text: ' — ' + t(kDesc) });
if (kTitle === 'prep_export') {
const expRow = prep.createEl('div', { cls: 'paperforge-guide-item' });
expRow.createEl('span', { text: `${t('prep_export_path_label')} ${vaultPath}/${this._pfConfig.system_dir}/PaperForge/exports/` });
}
}
/* ── Pre-check status area ── */
@ -1805,6 +1804,21 @@ class PaperForgeSetupModal extends Modal {
</div>`;
el.createEl('p', { text: t('wizard_preview'), cls: 'paperforge-modal-hint' });
el.createEl('p', { text: t('wizard_safety'), cls: 'paperforge-modal-hint' });
const summary = el.createEl('div', { cls: 'paperforge-summary' });
const overviewItems = [
{ label: t('dir_resources'), val: `${vault}/${s.resources_dir || '03_Resources'}` },
{ label: t('dir_notes'), val: `${vault}/${s.resources_dir || '03_Resources'}/${s.literature_dir || 'Literature'}` },
{ label: t('dir_index'), val: `${vault}/${s.resources_dir || '03_Resources'}/${s.control_dir || 'LiteratureControl'}` },
{ label: t('dir_base'), val: `${vault}/${s.base_dir || '05_Bases'}` },
{ label: t('dir_system'), val: `${vault}/${s.system_dir || '99_System'}` },
];
for (const item of overviewItems) {
const row = summary.createEl('div', { cls: 'paperforge-summary-row' });
row.createEl('span', { cls: 'paperforge-summary-label', text: item.label });
row.createEl('span', { cls: 'paperforge-summary-value', text: item.val });
}
}
/* ── Step 2: Directory Config (editable) ── */
@ -1830,6 +1844,21 @@ class PaperForgeSetupModal extends Modal {
this._modalInput(el, t('dir_system'), 'system_dir', s.system_dir, 'System');
this._modalInput(el, t('dir_base'), 'base_dir', s.base_dir, 'Base');
el.createEl('p', { text: t('wizard_safety'), cls: 'paperforge-modal-hint' });
const preview = el.createEl('div', { cls: 'paperforge-summary' });
const previewItems = [
{ label: t('dir_resources'), val: `${vault}/${s.resources_dir || ''}` },
{ label: t('dir_notes'), val: `${vault}/${s.resources_dir || ''}/${s.literature_dir || ''}` },
{ label: t('dir_index'), val: `${vault}/${s.resources_dir || ''}/${s.control_dir || ''}` },
{ label: t('dir_system'), val: `${vault}/${s.system_dir || ''}` },
{ label: t('dir_base'), val: `${vault}/${s.base_dir || ''}` },
];
for (const item of previewItems) {
const row = preview.createEl('div', { cls: 'paperforge-summary-row' });
row.createEl('span', { cls: 'paperforge-summary-label', text: item.label });
row.createEl('span', { cls: 'paperforge-summary-value', text: item.val });
}
}
/* ── Step 3: Keys, Zotero & Agent ── */
@ -2076,11 +2105,11 @@ class PaperForgeSetupModal extends Modal {
el.createEl('h3', { text: t('complete_next') });
const nextList = el.createEl('div', { cls: 'paperforge-nextsteps' });
const steps = [
[t('complete_step4'), t('complete_step4_desc')],
['', `${t('complete_export_path')} ${vault}/${s.system_dir}/PaperForge/exports/`],
[t('complete_step1'), t('complete_step1_desc')],
[t('complete_step2'), t('complete_step2_desc')],
[t('complete_step3'), t('complete_step3_desc')],
[t('complete_step4'), t('complete_step4_desc')],
['', `${t('complete_export_path')} ${vault}/${s.system_dir}/PaperForge/exports/`],
];
for (const [title, desc] of steps) {
const item = nextList.createEl('div', { cls: 'paperforge-nextstep-item' });

View file

@ -790,15 +790,12 @@ class ZoteroStep(StepScreen):
system_dir = getattr(self.app, "vault_config", {}).get("system_dir", "99_System")
junction_path = vault / system_dir / "Zotero"
# Remove existing
if junction_path.exists() or junction_path.is_symlink():
try:
if sys.platform == "win32":
subprocess.run(["cmd", "/c", "rmdir", str(junction_path)], check=True, capture_output=True)
else:
junction_path.unlink()
except Exception:
pass
self.app.zotero_data_dir = str(zotero_data)
self.app.zotero_link = str(junction_path)
self.set_status("已存在 Zotero 目录/链接,安装向导不会覆盖已有内容", True)
self.app.post_message(StepPassed(self.step_idx))
return
try:
junction_path.parent.mkdir(parents=True, exist_ok=True)
@ -867,8 +864,8 @@ class JsonStep(StepScreen):
**Better BibTeX 自动导出** PaperForge 的数据来源
**配置步骤**
1. Zotero 文件 导出库...
2. 格式选择 **Better BibTeX**
1. Zotero 对要同步的文献库或分类右键 **导出...**
2. 格式选择 **Better BibTeX JSON**
3. 保存到`{system_dir}/PaperForge/exports/`
4. **[必须] 勾选 "保持更新"** 这是自动同步的关键
@ -1058,7 +1055,7 @@ class DeployStep(StepScreen):
if worker_src.exists():
import shutil
shutil.copy2(worker_src, worker_dst)
_copy_file_incremental(worker_src, worker_dst)
# Also copy other worker modules
for mod in [
"ocr.py",
@ -1071,13 +1068,13 @@ class DeployStep(StepScreen):
]:
mod_src = repo_root / "paperforge/worker" / mod
if mod_src.exists():
shutil.copy2(mod_src, pf_path / "worker/scripts" / mod)
_copy_file_incremental(mod_src, pf_path / "worker/scripts" / mod)
else:
# Fallback to old pipeline location for backward compatibility
worker_src = repo_root / "pipeline/worker/scripts/literature_pipeline.py"
worker_dst = pf_path / "worker/scripts/literature_pipeline.py"
if worker_src.exists():
shutil.copy2(worker_src, worker_dst)
_copy_file_incremental(worker_src, worker_dst)
else:
self.set_status(f"错误:找不到 worker 脚本: {worker_src}", False)
return False
@ -1088,7 +1085,7 @@ class DeployStep(StepScreen):
ld_src = repo_root / "skills/literature-qa/scripts/ld_deep.py"
ld_dst = vault / skill_dir / "literature-qa/scripts/ld_deep.py"
if ld_src.exists():
shutil.copy2(ld_src, ld_dst)
_copy_file_incremental(ld_src, ld_dst)
else:
self.set_status(f"错误:找不到 ld_deep.py: {ld_src}", False)
return False
@ -1099,7 +1096,7 @@ class DeployStep(StepScreen):
prompt_src = repo_root / "skills/literature-qa/prompt_deep_subagent.md"
prompt_dst = vault / skill_dir / "literature-qa/prompt_deep_subagent.md"
if prompt_src.exists():
shutil.copy2(prompt_src, prompt_dst)
_copy_file_incremental(prompt_src, prompt_dst)
else:
self.set_status(f"错误:找不到 prompt_deep_subagent.md: {prompt_src}", False)
return False
@ -1111,7 +1108,7 @@ class DeployStep(StepScreen):
chart_dst = vault / skill_dir / "literature-qa/chart-reading"
if chart_src.exists() and chart_src.is_dir():
for f in chart_src.glob("*.md"):
shutil.copy2(f, chart_dst / f.name)
_copy_file_incremental(f, chart_dst / f.name)
# Copy OpenCode command files when the target platform supports them.
if getattr(self.app, "agent_key", "") == "opencode":
@ -1123,15 +1120,20 @@ class DeployStep(StepScreen):
command_dst.mkdir(parents=True, exist_ok=True)
for f in command_src.glob("*.md"):
text = apply_user_paths(f.read_text(encoding="utf-8"), skill_dir)
(command_dst / f.name).write_text(text, encoding="utf-8")
_write_text_incremental(command_dst / f.name, text)
# Copy user-facing docs. AGENTS.md is regenerated below with the chosen paths.
docs_src = repo_root / "docs"
docs_dst = vault / "docs"
if docs_src.exists() and docs_src.is_dir():
shutil.copytree(docs_src, docs_dst, dirs_exist_ok=True)
for doc in docs_dst.rglob("*.md"):
doc.write_text(apply_user_paths(doc.read_text(encoding="utf-8"), skill_dir), encoding="utf-8")
for src_doc in docs_src.rglob("*.md"):
rel = src_doc.relative_to(docs_src)
text = apply_user_paths(src_doc.read_text(encoding="utf-8"), skill_dir)
_write_text_incremental(docs_dst / rel, text)
for src_other in docs_src.rglob("*"):
if src_other.is_dir() or src_other.suffix.lower() == ".md":
continue
_copy_file_incremental(src_other, docs_dst / src_other.relative_to(docs_src))
# 5. 创建 .env放到 PaperForge 目录下)
from textual.widgets import Input
@ -1161,7 +1163,15 @@ PADDLEOCR_MODEL=PaddleOCR-VL-1.5
# Zotero data directory selected during setup
ZOTERO_DATA_DIR={getattr(self.app, 'zotero_data_dir', '')}
"""
env_path.write_text(env_content, encoding="utf-8")
_merge_env_incremental(
env_path,
{
"PADDLEOCR_API_TOKEN": api_key,
"PADDLEOCR_JOB_URL": api_url,
"PADDLEOCR_MODEL": "PaddleOCR-VL-1.5",
"ZOTERO_DATA_DIR": getattr(self.app, "zotero_data_dir", ""),
},
)
# Create a minimal domain mapping. The worker will keep this usable even
# before JSON exports exist, and will infer domains from export filenames.
@ -1214,7 +1224,7 @@ ZOTERO_DATA_DIR={getattr(self.app, 'zotero_data_dir', '')}
agents_dst = vault / "AGENTS.md"
if agents_src.exists():
agents_text = apply_user_paths(agents_src.read_text(encoding="utf-8"), skill_dir)
agents_dst.write_text(agents_text, encoding="utf-8")
_write_text_incremental(agents_dst, agents_text)
# 7. 验证文件完整性
self.set_status("验证文件完整性...", None)
@ -1286,7 +1296,17 @@ PaperForge 已完成安装和初始化。以下是立即开始使用的步骤:
### 首次使用步骤:
**1. 同步 Zotero 文献并生成正式笔记**
**1. 配置 Better BibTeX 自动导出**
1. Zotero 对要同步的文献库或分类右键 导出
2. 格式选择 **Better BibTeX JSON**
3. 保存到`{system_dir}/PaperForge/exports/`
4. **必须勾选 "保持更新"**
**2. Obsidian 里启用插件并打开 Dashboard**
1. `Settings -> Community Plugins -> Installed -> PaperForge -> Enable`
2. `Ctrl+P` 搜索 `PaperForge`
**3. 同步 Zotero 文献并生成正式笔记**
```bash
paperforge sync
```
@ -1297,12 +1317,10 @@ paperforge sync --selection # 仅同步 Zotero 到 library-records
paperforge sync --index # 仅根据现有 library-records 生成正式笔记
```
**2. 标记精读文献**
Obsidian 中打开 library-records 文件设置
- `do_ocr: true`
- `analyze: true`
**4. Base 视图中标记 OCR**
打开 Obsidian Base 视图找到该文献 `do_ocr` 设为 `true`
**3. 运行 OCR**
**5. 运行 OCR**
```bash
paperforge ocr
```
@ -1312,7 +1330,10 @@ paperforge ocr
paperforge ocr --diagnose
```
**4. 执行精读**
**6. Base 视图中标记精读**
OCR 完成后 Base 视图中找到该文献 `analyze` 设为 `true`
**7. 执行精读**
OpenCode Agent 中输入
```
/pf-deep <zotero_key>
@ -1598,6 +1619,80 @@ def _substitute_vars(
return text
def _copy_file_incremental(src: Path, dst: Path) -> bool:
"""Copy a file only when the destination is missing."""
dst.parent.mkdir(parents=True, exist_ok=True)
if dst.exists():
return False
shutil.copy2(src, dst)
return True
def _write_text_incremental(dst: Path, text: str) -> bool:
"""Write a text file only when the destination is missing."""
dst.parent.mkdir(parents=True, exist_ok=True)
if dst.exists():
return False
dst.write_text(text, encoding="utf-8")
return True
def _copy_tree_incremental(src_dir: Path, dst_dir: Path) -> tuple[int, int]:
"""Copy an entire tree without overwriting existing files."""
created = 0
skipped = 0
for src in src_dir.rglob("*"):
rel = src.relative_to(src_dir)
dst = dst_dir / rel
if src.is_dir():
dst.mkdir(parents=True, exist_ok=True)
continue
if _copy_file_incremental(src, dst):
created += 1
else:
skipped += 1
return created, skipped
def _merge_env_incremental(env_path: Path, values: dict[str, str]) -> str:
"""Create .env if missing, otherwise append only missing keys."""
lines = [
"# PaperForge configuration",
f"PADDLEOCR_API_TOKEN={values['PADDLEOCR_API_TOKEN']}",
f"PADDLEOCR_JOB_URL={values['PADDLEOCR_JOB_URL']}",
f"PADDLEOCR_MODEL={values['PADDLEOCR_MODEL']}",
]
if "ZOTERO_DATA_DIR" in values:
lines.append(f"ZOTERO_DATA_DIR={values['ZOTERO_DATA_DIR']}")
if not env_path.exists():
env_path.parent.mkdir(parents=True, exist_ok=True)
env_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return "created"
existing_text = env_path.read_text(encoding="utf-8")
existing_keys = {
line.split("=", 1)[0].strip()
for line in existing_text.splitlines()
if line.strip() and not line.lstrip().startswith("#") and "=" in line
}
missing_lines = [
f"{key}={value}"
for key, value in values.items()
if key not in existing_keys
]
if not missing_lines:
return "preserved"
suffix = "\n".join(missing_lines) + "\n"
if existing_text and not existing_text.endswith("\n"):
existing_text += "\n"
if existing_text.strip():
existing_text += "\n"
env_path.write_text(existing_text + suffix, encoding="utf-8")
return "extended"
def _deploy_skill_directory(
vault: Path,
skill_dir: str,
@ -1628,26 +1723,25 @@ def _deploy_skill_directory(
if deep_src.exists():
text = deep_src.read_text(encoding="utf-8")
text = _substitute_vars(text, system_dir, resources_dir, literature_dir, control_dir, base_dir, skill_dir, prefix)
(pf_deep_dst / "SKILL.md").write_text(text, encoding="utf-8")
_write_text_incremental(pf_deep_dst / "SKILL.md", text)
imported.append("pf-deep")
# ld_deep.py
ld_src = src_scripts / "ld_deep.py"
ld_dst = pf_deep_dst / "scripts" / "ld_deep.py"
if ld_src.exists():
ld_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(ld_src, ld_dst)
_copy_file_incremental(ld_src, ld_dst)
# subagent prompt
if src_prompt.exists():
shutil.copy2(src_prompt, pf_deep_dst / "prompt_deep_subagent.md")
_copy_file_incremental(src_prompt, pf_deep_dst / "prompt_deep_subagent.md")
# chart-reading guides
if src_charts.exists() and src_charts.is_dir():
chart_dst = pf_deep_dst / "chart-reading"
chart_dst.mkdir(parents=True, exist_ok=True)
for f in src_charts.glob("*.md"):
shutil.copy2(f, chart_dst / f.name)
_copy_file_incremental(f, chart_dst / f.name)
# --- pf-paper: lightweight paper Q&A skill ---
pf_paper_dst = vault / skill_dir / "pf-paper"
@ -1657,7 +1751,7 @@ def _deploy_skill_directory(
if paper_src.exists():
text = paper_src.read_text(encoding="utf-8")
text = _substitute_vars(text, system_dir, resources_dir, literature_dir, control_dir, base_dir, skill_dir, prefix)
(pf_paper_dst / "SKILL.md").write_text(text, encoding="utf-8")
_write_text_incremental(pf_paper_dst / "SKILL.md", text)
imported.append("pf-paper")
return imported
@ -1687,7 +1781,7 @@ def _deploy_flat_command(
for f in command_src.glob("pf-*.md"):
text = f.read_text(encoding="utf-8")
text = _substitute_vars(text, system_dir, resources_dir, literature_dir, control_dir, base_dir, skill_dir)
(command_dst / f.name).write_text(text, encoding="utf-8")
_write_text_incremental(command_dst / f.name, text)
imported.append(f.stem)
return imported
@ -1715,14 +1809,13 @@ def _deploy_rules_file(
pf_deep_dst.mkdir(parents=True, exist_ok=True)
ld_src = src_scripts / "ld_deep.py"
if ld_src.exists():
(pf_deep_dst / "scripts").mkdir(parents=True, exist_ok=True)
shutil.copy2(ld_src, pf_deep_dst / "scripts" / "ld_deep.py")
_copy_file_incremental(ld_src, pf_deep_dst / "scripts" / "ld_deep.py")
if src_prompt.exists():
shutil.copy2(src_prompt, pf_deep_dst / "prompt_deep_subagent.md")
_copy_file_incremental(src_prompt, pf_deep_dst / "prompt_deep_subagent.md")
if src_charts.exists() and src_charts.is_dir():
(pf_deep_dst / "chart-reading").mkdir(parents=True, exist_ok=True)
for f in src_charts.glob("*.md"):
shutil.copy2(f, pf_deep_dst / "chart-reading" / f.name)
_copy_file_incremental(f, pf_deep_dst / "chart-reading" / f.name)
imported.append("clinerules")
return imported
@ -1898,9 +1991,10 @@ def headless_setup(
zotero_data_hint = zotero_data or "<你的Zotero数据目录>"
if sys.platform == "win32":
print(f" 首次安装需要配置 BBT 自动导出:")
print(f" 1. Zotero → 文件 → 导出库 → Better BibTeX")
print(f" 2. 保存到 vault 的 {system_dir}/PaperForge/exports/")
print(f' 3. 勾选 "保持更新"')
print(f" 1. 在 Zotero 中右键要同步的文献库或分类 → 导出")
print(f" 2. 格式选择 Better BibTeX JSON")
print(f" 3. 保存到 vault 的 {system_dir}/PaperForge/exports/")
print(f' 4. 勾选 "保持更新"')
else:
print(f" Configure BBT auto-export to: {system_dir}/PaperForge/exports/")
print(f" 完成后运行: paperforge sync")
@ -1931,13 +2025,13 @@ def headless_setup(
print(f"Error: worker script not found: {worker_src}", file=sys.stderr)
return 4
shutil.copy2(worker_src, worker_dst)
_copy_file_incremental(worker_src, worker_dst)
for mod in ["ocr.py", "repair.py", "status.py", "deep_reading.py",
"update.py", "base_views.py", "__init__.py",
"_utils.py", "_progress.py", "_retry.py"]:
mod_src = repo_root / "paperforge/worker" / mod
if mod_src.exists():
shutil.copy2(mod_src, pf_path / "worker/scripts" / mod)
_copy_file_incremental(mod_src, pf_path / "worker/scripts" / mod)
print(f" [OK] worker scripts")
# Deploy skills based on agent format
@ -1989,16 +2083,23 @@ def headless_setup(
docs_src = repo_root / "docs"
docs_dst = vault / "docs"
if docs_src.exists() and docs_src.is_dir():
shutil.copytree(docs_src, docs_dst, dirs_exist_ok=True)
print(f" [OK] docs")
created, skipped = _copy_tree_incremental(docs_src, docs_dst)
print(f" [OK] docs (created {created}, preserved {skipped})")
else:
print(f" [WARN] docs source not found: {docs_src}")
# Obsidian plugin
plugin_src = repo_root / "paperforge/plugin"
plugin_dst = vault / ".obsidian" / "plugins" / "paperforge"
if plugin_src.exists() and plugin_src.is_dir():
created = 0
skipped = 0
for f in plugin_src.glob("*"):
shutil.copy2(f, plugin_dst / f.name)
print(f" [OK] Obsidian plugin")
if _copy_file_incremental(f, plugin_dst / f.name):
created += 1
else:
skipped += 1
print(f" [OK] Obsidian plugin (created {created}, preserved {skipped})")
else:
print(f" [WARN] Plugin source not found: {plugin_src}")
@ -2016,7 +2117,7 @@ def headless_setup(
("<skill_dir>", skill_dir),
]:
text = text.replace(old, new)
agents_dst.write_text(text, encoding="utf-8")
_write_text_incremental(agents_dst, text)
print(f" [OK] AGENTS.md")
else:
print(f" [WARN] AGENTS.md source not found; skipping")
@ -2028,13 +2129,16 @@ def headless_setup(
# .env
if paddleocr_key:
env_content = f"""# PaperForge 配置文件
PADDLEOCR_API_TOKEN={paddleocr_key}
PADDLEOCR_JOB_URL={paddleocr_url}
PADDLEOCR_MODEL=PaddleOCR-VL-1.5
"""
(pf_path / ".env").write_text(env_content, encoding="utf-8")
print(f" [OK] .env")
env_status = _merge_env_incremental(
pf_path / ".env",
{
"PADDLEOCR_API_TOKEN": paddleocr_key,
"PADDLEOCR_JOB_URL": paddleocr_url,
"PADDLEOCR_MODEL": "PaddleOCR-VL-1.5",
"ZOTERO_DATA_DIR": zotero_data or "",
},
)
print(f" [OK] .env ({env_status})")
else:
print(f" [INFO] No PaddleOCR key — create {pf_path}/.env manually")
@ -2130,16 +2234,19 @@ PADDLEOCR_MODEL=PaddleOCR-VL-1.5
print(f" PaperForge v{__version__} installation complete!")
print("=" * 60)
print()
print("Safety note:")
print(" Existing files in the target vault were preserved; setup only created missing files and folders.")
print()
print("Next steps:")
print(" 1. Open Obsidian → Settings → Community Plugins → Enable 'PaperForge'")
print(" 2. Press Ctrl+P and type 'PaperForge' to see CLI commands")
print(f" 1. In Zotero, export the library or a collection as Better BibTeX JSON into: {vault / system_dir / 'PaperForge' / 'exports'}")
print(" Enable 'Keep updated' so Zotero keeps the JSON in sync.")
print(" 2. Open Obsidian → Settings → Community Plugins → Enable 'PaperForge'")
print(" 3. Press Ctrl+P and type 'PaperForge' to open the dashboard")
print()
print("First-run workflow:")
print(f" 3. Configure Better BibTeX auto-export to: {system_dir}/PaperForge/exports/")
print(" (Zotero → Export Library → Better BibTeX → Keep Updated)")
print(" 4. Add papers to Zotero, then run: paperforge sync")
print(" 5. Mark papers with do_ocr:true in library-records")
print(" 6. Run: paperforge ocr")
print(" 5. In Obsidian Base view, mark do_ocr:true, then run: paperforge ocr")
print(" 6. After OCR done, mark analyze:true in Base view")
print(" 7. Use /pf-deep <key> in your AI agent for deep reading")
print()