diff --git a/AGENTS.md b/AGENTS.md index c41328d2..179b0b67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 数据目录已链接到 /Zotero +[ ] Better BibTeX 已按下方步骤导出 JSON 到 /PaperForge/exports/ ``` +> 安装向导是增量式的:如果你选择的 Vault 或目录里已经有文件,PaperForge 只会补充缺失的目录和文件,不会删除已有内容。 + ### Better BibTeX 自动导出配置 -1. Zotero → Edit → Preferences → Better BibTeX -2. 勾选 **"Keep updated"**(自动导出) -3. 选择导出格式:**Better BibLaTeX** 或 **Better BibTeX** -4. 导出路径设置为:`{你的Vault路径}//PaperForge/exports/library.json` -5. 点击 OK,JSON 文件会自动生成并保持同步 +这一步应在安装向导完成之后再做,因为 `exports/` 目录需要先由安装流程创建。 + +1. 打开 Zotero +2. 对你要同步的文献库或分类右键 → `导出...` +3. 选择格式:**Better BibTeX JSON** +4. 勾选 **"Keep updated"**(自动导出) +5. 保存到:`{你的Vault路径}//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 导出到 `/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. 打开 `//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 中打开正式笔记,找到 `## 🔍 精读` 区域,精读已完成。 diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 63895ebb..0147baaa 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -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 # 查看系统状态 ``` --- diff --git a/docs/ai-agent-setup-guide-zh.md b/docs/ai-agent-setup-guide-zh.md index 6b4baa1a..52070395 100644 --- a/docs/ai-agent-setup-guide-zh.md +++ b/docs/ai-agent-setup-guide-zh.md @@ -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 里的 /PaperForge/exports/ +> - 对要同步的文献库或分类右键 → 导出 +> - 格式选 Better BibTeX JSON > - 必须勾选 "保持更新" +> - 保存到 Vault 里的 /PaperForge/exports/ > > **2. 在 Obsidian 里启用 PaperForge 插件** > - 设置 → 社区插件 → 已安装 → PaperForge → 启用 -> - Ctrl+P 输入 "PaperForge" +> - Ctrl+P 输入 "PaperForge",打开 Dashboard > -> **3. 若跳过了 PaddleOCR Key** +> **3. 同步文献** +> - 在 Dashboard 中点击 `Sync Library` +> +> **4. 若跳过了 PaddleOCR Key** > - 在 /PaperForge/.env 里添加: > PADDLEOCR_API_TOKEN=<你的key> diff --git a/docs/ai-agent-setup-guide.md b/docs/ai-agent-setup-guide.md index b0b7361b..c151b1b9 100644 --- a/docs/ai-agent-setup-guide.md +++ b/docs/ai-agent-setup-guide.md @@ -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 /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 /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 /PaperForge/.env: > PADDLEOCR_API_TOKEN= diff --git a/docs/setup-guide.md b/docs/setup-guide.md index 7568cb01..b4b0df13 100644 --- a/docs/setup-guide.md +++ b/docs/setup-guide.md @@ -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` 下面 +- 重点关系是: + - `//`:正式文献笔记 + - `//library-records/`:每篇文献的状态跟踪 + - `/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 平台)输入: diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index ce12c2c6..9134a5c7 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -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: '先安装 Zotero:https://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 { `; 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' }); diff --git a/paperforge/setup_wizard.py b/paperforge/setup_wizard.py index 714b42e9..f53e3a74 100644 --- a/paperforge/setup_wizard.py +++ b/paperforge/setup_wizard.py @@ -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 @@ -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), ]: 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 in your AI agent for deep reading") print()