mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
Merge branch 'paperforge-stabilization'
# Conflicts: # paperforge/memory/vector_db.py
This commit is contained in:
commit
a599cd930b
34 changed files with 920 additions and 1217 deletions
651
AGENTS.md
651
AGENTS.md
|
|
@ -1,627 +1,90 @@
|
|||
# PaperForge - Agent Guide
|
||||
# PaperForge - Agent Operating Guide
|
||||
|
||||
> 本文档面向 **安装完成后的新用户** 和 **AI Agent**。如果还没有安装 PaperForge,请通过 Obsidian 插件市场安装,或查看 [README.md](README.md) 中的安装说明。Docs 版本与 v1.4.18 对应。
|
||||
> 本文档面向 **AI Agent**(OpenCode / Claude Code / GPT / Cursor 等)。终端用户请阅读 [使用教程](docs/getting-started.md)。
|
||||
|
||||
---
|
||||
|
||||
## 0. 安装后检查清单(第一次使用前必做)
|
||||
## 1. 核心不变式
|
||||
|
||||
```
|
||||
[ ] Zotero 已安装 + Better BibTeX 插件已启用
|
||||
[ ] Obsidian 已打开当前 Vault
|
||||
[ ] PaperForge 已安装 (pip install paperforge)
|
||||
[ ] PaddleOCR API Key 已配置(在 .env 中)
|
||||
[ ] 目录结构已创建(安装向导会自动完成)
|
||||
[ ] Zotero 数据目录已链接到 <system_dir>/Zotero
|
||||
[ ] Better BibTeX 已按下方步骤导出 JSON 到 <system_dir>/PaperForge/exports/
|
||||
```
|
||||
|
||||
> 安装向导是增量式的:如果你选择的 Vault 或目录里已经有文件,PaperForge 只会补充缺失的目录和文件,不会删除已有内容。
|
||||
|
||||
### Better BibTeX 自动导出配置
|
||||
|
||||
这一步应在安装向导完成之后再做,因为 `exports/` 目录需要先由安装流程创建。
|
||||
|
||||
1. 打开 Zotero
|
||||
2. 对你要同步的文献库或分类右键 → `导出...`
|
||||
3. 选择格式:**Better BibTeX JSON**
|
||||
4. 勾选 **"Keep updated"**(自动导出)
|
||||
5. 保存到:`{你的Vault路径}/<system_dir>/PaperForge/exports/`
|
||||
6. JSON 文件名会作为 Base 名称,例如 `library.json`、`骨科.json`
|
||||
1. **CLI 是命令真相源。** 所有 skill、workflow、文档命令引用必须对齐 `paperforge/cli.py`。
|
||||
2. **Python 是运行时真相源。** Plugin 只读 canonical 快照,不做业务推断。
|
||||
3. **`paperforge.json` 是路径真相源。** Plugin 和 Python 共享同源路径解析,不硬编码目录名。
|
||||
4. **Agent 机械命令和思考工作流分层。** `/pf-sync` `/pf-ocr` `/pf-status` 是机械执行,`/pf-deep` `/pf-paper` 是思考工作流。
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心架构(v2.1 契约驱动)
|
||||
## 2. 架构边界
|
||||
|
||||
PaperForge 在 v2.1(1.4.17rc4)重构为 **契约驱动架构**,分 5 层:
|
||||
| 层 | 做什么 | 不做什么 |
|
||||
|----|--------|---------|
|
||||
| Plugin JS | 读快照、render UI、`execFile` 调 Python | 不读 SQLite、不推断 runtime 状态 |
|
||||
| Python CLI | 写快照、sync/ocr/repair/embed/memory | 不被 JS 轮询驱动 |
|
||||
| Memory DB | SQLite + FTS5,由 Python 全权重建 | JS 不读 memory DB |
|
||||
| Vector DB | ChromaDB,由 `embed build` 管理 | 不与 memory DB 合并语义 |
|
||||
| Skill/workflow | 路由 → 执行 CLI → 解释结果 | 不绕过 CLI 直操作文件 |
|
||||
|
||||
**运行时快照契约:**
|
||||
|
||||
```
|
||||
CLI/Plugin 调用
|
||||
↓ PFResult 契约 {ok, command, version, data, error}
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ commands/ CLI 分发层 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ adapter/bbt, zotero_paths, frontmatter │ ← 独立可测
|
||||
│ services/sync_service ← 编排适配器 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ core/result, errors, state ← 共享数据契约 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ worker/sync, ocr, status ← 机械劳动 │
|
||||
│ setup/6个类 ← 安装流程 │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ schema/field_registry.yaml ← 字段注册表 │
|
||||
│ doctor/field_validator.py ← 字段校验 │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
Python 写(每次相关 CLI 命令结束时):
|
||||
runtime-health --json → runtime-health.json
|
||||
memory status --json → memory-runtime-state.json
|
||||
embed status --json → vector-runtime-state.json
|
||||
embed build → vector-build-state.json
|
||||
|
||||
| 层级 | 组件 | 触发方式 | 作用 |
|
||||
|------|------|----------|------|
|
||||
| **契约层** | `core/`(PFResult, ErrorCode, 状态机) | 被所有模块引用 | 定义 CLI/Plugin/Worker 之间的数据交换格式 |
|
||||
| **适配器层** | `adapters/`(bbt, zotero_paths, frontmatter) | 被服务层调用 | 封装外部数据格式与 I/O 操作 |
|
||||
| **服务层** | `services/sync_service` | 被 worker 调度 | 编排适配器,实现业务逻辑 |
|
||||
| **Worker 层** | `worker/`(sync, ocr, status, repair 等) | Python CLI | 后台自动化(机械劳动) |
|
||||
| **Agent 层** | `/pf-deep`, `/pf-paper` | 用户手动触发 | 交互式精读(深度思考) |
|
||||
|
||||
**操作速查**:
|
||||
| 你要做什么 | 在终端输入 | 在 OpenCode 输入 |
|
||||
|-----------|-----------|-----------------|
|
||||
| 同步 Zotero 并生成笔记 | `paperforge sync` | `/pf-sync` |
|
||||
| 运行 OCR | `paperforge ocr` | `/pf-ocr` |
|
||||
| 查看精读队列 | `paperforge deep-reading` | `/pf-deep`(精读具体文献) |
|
||||
| 查看系统状态 | `paperforge status` | `/pf-status` |
|
||||
| 修复状态分歧 | `paperforge repair` | (终端操作) |
|
||||
| 验证安装配置 | `paperforge doctor` | (终端操作) |
|
||||
| 查看帮助 | `paperforge --help` | (终端操作) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 完整数据流
|
||||
|
||||
```
|
||||
Zotero 添加文献
|
||||
↓ Better BibTeX 自动导出 JSON
|
||||
<system_dir>/PaperForge/exports/library.json
|
||||
↓ 运行 sync
|
||||
<resources_dir>/<literature_dir>/<domain>/<key> - <Title>.md(正式笔记,含 frontmatter)
|
||||
↓ 用户在正式笔记 frontmatter 中设置 do_ocr: true
|
||||
运行 ocr → <system_dir>/PaperForge/ocr/<key>/
|
||||
↓ 用户在正式笔记 frontmatter 中设置 analyze: true
|
||||
运行 deep-reading(查看队列,确认就绪)
|
||||
↓ 用户执行 Agent 命令
|
||||
/pf-deep <zotero_key>
|
||||
↓ Agent 生成
|
||||
正式笔记中新增 ## 🔍 精读 区域
|
||||
JS 读(同步,不推断):
|
||||
memoryState.getMemoryRuntime()
|
||||
memoryState.getVectorRuntime()
|
||||
memoryState.getRuntimeHealth()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 目录结构(Lite 版,5 个核心目录)
|
||||
## 3. 安全命令惯例
|
||||
|
||||
```
|
||||
{你的Vault根目录}/
|
||||
├── <resources_dir>/
|
||||
│ ├── <literature_dir>/ ← 正式文献笔记(sync 生成,含 frontmatter 状态跟踪)
|
||||
│ │ ├── 骨科/
|
||||
│ │ ├── 运动医学/
|
||||
│ │ └── ...(你的分类)
|
||||
│
|
||||
├── <system_dir>/
|
||||
│ ├── PaperForge/
|
||||
│ │ ├── exports/ ← Better BibTeX 自动导出的 JSON
|
||||
│ │ │ └── library.json
|
||||
│ │ ├── ocr/ ← OCR 结果(每个文献一个子目录)
|
||||
│ │ │ └── ABCDEFG/ ← Zotero key 作为目录名
|
||||
│ │ │ ├── fulltext.md ← OCR 提取的全文
|
||||
│ │ │ ├── images/ ← 图表切割图片
|
||||
│ │ │ ├── meta.json ← OCR 元数据(含 ocr_status)
|
||||
│ │ │ └── figure-map.json ← 图表索引(自动创建)
|
||||
│ │ ├── indexes/ ← 索引缓存(formal-library.json 等)
|
||||
│ │ └── config/ ← 领域-收藏夹映射等配置
|
||||
│ └── Zotero/ ← Junction/Symlink 到 Zotero 数据目录
|
||||
│
|
||||
├── <agent_config_dir>/ ← OpenCode Agent 配置(自动创建)
|
||||
│ └── skills/
|
||||
│ └── literature-qa/ ← 深度阅读 Skill
|
||||
│ ├── scripts/
|
||||
│ │ └── ld_deep.py ← /pf-deep 核心脚本
|
||||
│ ├── prompt_deep_subagent.md ← Agent 精读提示词
|
||||
│ └── chart-reading/ ← 14 种图表阅读指南
|
||||
│
|
||||
├── .env ← API Key 等敏感配置
|
||||
└── AGENTS.md ← 本文件
|
||||
```
|
||||
|
||||
### 各目录作用速查
|
||||
|
||||
| 目录 | 内容 | 谁生成/修改 |
|
||||
|------|------|------------|
|
||||
| `<resources_dir>/<literature_dir>/` | 正式文献笔记(含 frontmatter + 精读内容) | sync 生成,Agent 写入精读 |
|
||||
| `<system_dir>/PaperForge/exports/` | Better BibTeX JSON 导出 | Zotero 自动导出 |
|
||||
| `<system_dir>/PaperForge/ocr/` | OCR 全文 + 图表切割 | ocr worker 生成 |
|
||||
| `<system_dir>/PaperForge/indexes/` | 索引缓存(formal-library.json) | sync 生成 |
|
||||
| `<system_dir>/PaperForge/config/` | 领域-收藏夹映射等 | 用户/安装向导配置 |
|
||||
| `<system_dir>/Zotero/` | Zotero 数据目录的链接 | 安装时手动创建 junction |
|
||||
- 搜索用 `$PYTHON -m paperforge search`,不用 `grep`/`glob` 扫库。
|
||||
- 路径从 bootstrap 或 paper-context 获取,禁止自行拼接。
|
||||
- 未完成 paper-context 检查前不读原文(deep-reading、paper-qa)。
|
||||
- Reading-log 不是事实源,只能用做复查定位。
|
||||
- 未知或拼错的 `/pf-*` 必须提示用户,禁止静默掉进 `project-engineering`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心 Workers(Lite 版,4 个)
|
||||
## 4. 机械 vs 思考路由
|
||||
|
||||
### sync
|
||||
- **作用**:检测 Zotero 中的新条目并生成正式文献笔记
|
||||
- **运行时机**:添加新文献到 Zotero 后,或需要更新笔记格式时
|
||||
- **输出**:
|
||||
- `<resources_dir>/<literature_dir>/<domain>/<key> - <Title>.md`
|
||||
- **示例**:
|
||||
```bash
|
||||
paperforge sync
|
||||
# Legacy (备用):
|
||||
# python -m paperforge sync --vault "{vault路径}"
|
||||
```
|
||||
|
||||
### ocr
|
||||
- **作用**:将 PDF 上传到 PaddleOCR API,提取全文文本和图表
|
||||
- **触发条件**:正式笔记 frontmatter 中 `do_ocr: true`
|
||||
- **输出**:`<system_dir>/PaperForge/ocr/<key>/` 目录
|
||||
- `fulltext.md`:提取的全文(含 `<!-- page N -->` 分页标记)
|
||||
- `images/`:自动切割的图表图片
|
||||
- `meta.json`:OCR 状态(`ocr_status: done/pending/processing/failed`)
|
||||
- `figure-map.json`:图表索引(后续自动生成)
|
||||
- **注意**:OCR 是异步的,大文件可能需要几分钟
|
||||
- **示例**:
|
||||
```bash
|
||||
paperforge ocr
|
||||
# 诊断模式(不运行,仅检查状态)
|
||||
paperforge ocr --diagnose
|
||||
# Legacy (备用):
|
||||
# python -m paperforge ocr --vault "{vault路径}"
|
||||
```
|
||||
|
||||
### deep-reading
|
||||
- **作用**:扫描所有正式笔记,列出 `analyze=true` 且 OCR 完成的文献
|
||||
- **运行时机**:用户想看看哪些文献可以开始精读了
|
||||
- **输出**:控制台表格,显示队列状态
|
||||
- **重要**:这只是**查看队列**,不会自动触发 Agent 精读
|
||||
- **示例**:
|
||||
```bash
|
||||
paperforge deep-reading
|
||||
paperforge deep-reading --verbose # 显示阻塞条目的修复指令
|
||||
# Legacy (备用):
|
||||
# python -m paperforge deep-reading --vault "{vault路径}"
|
||||
```
|
||||
| Route | 类型 | 动作 |
|
||||
|-------|------|------|
|
||||
| `/pf-sync` | 机械 | 执行 `paperforge sync`,解释结果 |
|
||||
| `/pf-ocr` | 机械 | 执行 `paperforge ocr`,解释结果 |
|
||||
| `/pf-status` | 机械 | 执行 `paperforge status --json` 或 `paperforge runtime-health --json`,解读状态 |
|
||||
| `/pf-deep` | 思考 | 打开 `workflows/deep-reading.md` 执行完整流程 |
|
||||
| `/pf-paper` | 思考 | 打开 `workflows/paper-qa.md` 执行完整流程 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Agent 命令(用户手动触发)
|
||||
|
||||
PaperForge 的命令分为两类:
|
||||
|
||||
| 类型 | 命令 | 用途 | 说明 |
|
||||
|------|------|------|------|
|
||||
| **深度思考** | `/pf-deep <key>` | 完整 Keshav 三阶段精读 | **必须 Agent 执行** — 需要理解论文、分析图表、生成 callout |
|
||||
| **深度思考** | `/pf-paper <key>` | 文献问答 | **必须 Agent 执行** — 需要理解内容并写作 |
|
||||
| **机械操作** | `/pf-sync` | 同步 Zotero 并生成笔记 | Agent 可帮你检查状态并执行 |
|
||||
| **机械操作** | `/pf-ocr` | 运行 PDF OCR | Agent 可帮你检查队列并执行 |
|
||||
| **机械操作** | `/pf-status` | 查看系统状态 | Agent 可帮你解读诊断结果 |
|
||||
|
||||
> **双模式调用**:`/pf-sync`、`/pf-ocr`、`/pf-status` 本质上是 CLI 命令的 Agent 包装。你可以在终端直接运行 `paperforge sync/ocr/status`,也可以在 OpenCode 中使用 `/pf-*` 让 Agent 帮你检查前置条件、执行命令、解读输出。
|
||||
|
||||
> **v1.4 新增**:所有命令支持全局 `--verbose` / `-v` 参数(如 `paperforge sync --verbose`),输出 DEBUG 级别的诊断信息到 stderr,不影响 stdout 的正常输出。
|
||||
|
||||
> **v1.4 新增 — auto_analyze_after_ocr**:如果开启了 `paperforge.json` 中的 `auto_analyze_after_ocr`,OCR 完成后 `analyze` 会自动设为 `true`,无需手动修改 formal note frontmatter。
|
||||
|
||||
### 必须 Agent 执行的命令
|
||||
|
||||
#### `/pf-deep <zotero_key>` — 完整精读
|
||||
|
||||
**用途**:完整 Keshav 三阶段精读
|
||||
**前置条件**:OCR 完成 (`ocr_status: done`)
|
||||
|
||||
执行流程:
|
||||
1. **prepare 阶段**(自动):查找正式笔记、检查 OCR、生成 figure-map
|
||||
2. **精读阶段**(Agent 执行):Pass 1 概览 → Pass 2 精读还原 → Pass 3 深度理解
|
||||
3. **验证阶段**(自动):检查 callout 间距、section 完整性
|
||||
|
||||
#### `/pf-paper <zotero_key>` — 文献问答
|
||||
|
||||
**用途**:文献问答(无 OCR 要求)
|
||||
**前置条件**:有正式笔记即可
|
||||
|
||||
---
|
||||
|
||||
## 6. Path Resolution(路径解析)
|
||||
|
||||
PaperForge 支持三种 Better BibTeX 导出路径格式,并统一转换为 Obsidian wikilink:
|
||||
|
||||
### 支持的 BBT 路径格式
|
||||
|
||||
| 格式 | 示例 | 处理方式 |
|
||||
|------|------|----------|
|
||||
| **Absolute Windows** | `D:\Zotero\storage\KEY\file.pdf` | 提取 8 位 KEY,转换为 `storage:KEY/file.pdf` |
|
||||
| **storage: prefix** | `storage:KEY/file.pdf` | 直接透传,仅规范化斜杠 |
|
||||
| **Bare relative** | `KEY/file.pdf` | 自动添加 `storage:` 前缀 |
|
||||
|
||||
### Wikilink 生成规则
|
||||
|
||||
- 所有 PDF 路径在正式笔记中存储为 **Obsidian wikilink** 格式:`[[relative/path/to/file.pdf]]`
|
||||
- 使用正斜杠 `/`(即使 Windows 系统)
|
||||
- 支持中文文件名,无需转义
|
||||
- 示例:`[[99_System/Zotero/storage/KEY/中文论文.pdf]]`
|
||||
|
||||
### Junction / Symlink 设置
|
||||
|
||||
如果 Zotero 数据目录在 Vault 外部,需创建 junction:
|
||||
|
||||
```powershell
|
||||
# 以管理员身份运行 PowerShell
|
||||
New-Item -ItemType Junction -Path "C:\你的Vault\99_System\Zotero" -Target "C:\Users\用户名\Zotero"
|
||||
```
|
||||
|
||||
或 CMD:
|
||||
|
||||
```cmd
|
||||
mklink /J "C:\你的Vault\99_System\Zotero" "C:\Users\用户名\Zotero"
|
||||
```
|
||||
|
||||
运行 `paperforge doctor` 会自动检测 Zotero 位置并推荐正确的 junction 命令。
|
||||
|
||||
### 多附件处理
|
||||
|
||||
当一篇文献有多个 PDF 附件时:
|
||||
- **Main PDF**:title="PDF" 的附件,或最大文件,或第一个 PDF
|
||||
- **Supplementary**:其他 PDF 附件列表,以 wikilink 数组形式存储
|
||||
|
||||
---
|
||||
|
||||
## 7. Frontmatter 字段参考
|
||||
|
||||
### Formal Note(`Literature/<domain>/<key> - <Title>.md`)
|
||||
|
||||
这是**用户控制工作流的核心**。每个文献对应一个 formal note 文件,frontmatter 包含元数据 + 工作流控制字段:
|
||||
|
||||
```yaml
|
||||
---
|
||||
zotero_key: "ABCDEFG" # Zotero citation key(自动生成)
|
||||
domain: "骨科" # 分类领域(对应 Zotero 收藏夹)
|
||||
title: "论文标题"
|
||||
year: 2024
|
||||
doi: "10.xxxx/xxxxx"
|
||||
collection_path: "子分类" # Zotero 子收藏夹路径
|
||||
has_pdf: true # 是否有 PDF 附件(自动生成)
|
||||
pdf_path: "[[99_System/Zotero/storage/KEY/文件名.pdf]]" # Wikilink 格式
|
||||
bbt_path_raw: "D:\\Zotero\\storage\\KEY\\文件名.pdf" # 原始 BBT 路径(调试用)
|
||||
zotero_storage_key: "KEY" # 8 位 Zotero storage key
|
||||
attachment_count: 2 # 附件总数
|
||||
supplementary: # 其他 PDF 附件(wikilink 列表)
|
||||
- "[[99_System/Zotero/storage/KEY/supp1.pdf]]"
|
||||
- "[[99_System/Zotero/storage/KEY/supp2.pdf]]"
|
||||
fulltext_md_path: "[[99_System/PaperForge/ocr/KEY/fulltext.md]]"
|
||||
recommend_analyze: true # 系统推荐精读(有 PDF 时自动设为 true)
|
||||
analyze: false # 【用户控制】是否生成精读?设为 true 触发
|
||||
do_ocr: true # 【用户控制】是否运行 OCR?设为 true 触发
|
||||
ocr_status: "done" # OCR 状态(pending/processing/done/failed)
|
||||
deep_reading_status: "pending" # 精读状态(pending/done)
|
||||
path_error: "" # 路径错误(not_found/invalid/permission_denied)
|
||||
analysis_note: "" # 预留字段
|
||||
---
|
||||
```
|
||||
|
||||
**用户操作方式**:
|
||||
- 在 Obsidian 中打开 formal note 文件
|
||||
- 修改 `analyze: false` → `analyze: true` 标记要精读的文献
|
||||
- 修改 `do_ocr: false` → `do_ocr: true` 触发 OCR
|
||||
- 或使用 Obsidian Base 视图批量操作
|
||||
|
||||
---
|
||||
|
||||
## 8. 第一次使用指南(手把手)
|
||||
|
||||
### Step 1: 完成 Better BibTeX 自动导出
|
||||
|
||||
先按上面的步骤把 JSON 导出到 `<system_dir>/PaperForge/exports/`。这一步没完成前,`sync` 不会读到文献。
|
||||
|
||||
### Step 2: 确认 Zotero 有文献
|
||||
|
||||
确保 Zotero 中已有至少一篇带 PDF 的文献,且 Better BibTeX 已导出 JSON。
|
||||
|
||||
### Step 3: 运行 sync
|
||||
## 5. 测试
|
||||
|
||||
```bash
|
||||
# 在 Vault 根目录执行
|
||||
paperforge sync
|
||||
```
|
||||
# Python
|
||||
python -m pytest tests/unit/ tests/cli/ -v --tb=short
|
||||
|
||||
预期输出:
|
||||
```
|
||||
[INFO] Found 5 new items
|
||||
[INFO] Created 骨科/XXXXXXX.md
|
||||
[INFO] Generated 5 formal notes
|
||||
[INFO] Output: <resources_dir>/<literature_dir>/骨科/XXXXXXX - Title.md
|
||||
...
|
||||
```
|
||||
# JS
|
||||
cd paperforge/plugin && npx vitest run
|
||||
|
||||
### Step 4: 在 Base 视图中标记 OCR
|
||||
|
||||
打开 Obsidian Base 视图,找到该文献,将 `do_ocr` 设为 `true`。
|
||||
|
||||
### Step 5: 运行 OCR
|
||||
|
||||
```bash
|
||||
paperforge ocr
|
||||
```
|
||||
|
||||
等待完成(可能需要几分钟)。
|
||||
|
||||
### Step 6: 在 Base 视图中标记精读
|
||||
|
||||
OCR 完成后,在 Base 视图中找到该文献,将 `analyze` 设为 `true`。
|
||||
|
||||
### Step 7: 执行精读
|
||||
|
||||
先确认队列就绪:
|
||||
```bash
|
||||
paperforge deep-reading
|
||||
```
|
||||
|
||||
然后在 OpenCode Agent 中输入:
|
||||
```
|
||||
/pf-deep XXXXXXX
|
||||
```
|
||||
|
||||
Agent 会自动:
|
||||
1. 准备精读骨架(prepare)
|
||||
2. 逐阶段填写精读内容
|
||||
3. 验证结构完整性
|
||||
|
||||
### Step 8: 查看结果
|
||||
|
||||
在 Obsidian 中打开正式笔记,找到 `## 🔍 精读` 区域,精读已完成。
|
||||
|
||||
---
|
||||
|
||||
## 9. 常用命令速查
|
||||
|
||||
```bash
|
||||
# 检测 Zotero 新条目并生成正式笔记
|
||||
paperforge sync
|
||||
paperforge sync --verbose # 显示详细诊断信息
|
||||
|
||||
|
||||
|
||||
# 运行 OCR(处理 do_ocr=true 的文献)
|
||||
paperforge ocr
|
||||
paperforge ocr --verbose # 显示 OCR 详细日志
|
||||
paperforge ocr --diagnose # 诊断模式,不实际运行
|
||||
paperforge ocr --no-progress # 静默模式,不显示进度条
|
||||
|
||||
# 查看精读队列
|
||||
paperforge deep-reading
|
||||
paperforge deep-reading --verbose # 显示阻塞条目修复指令
|
||||
|
||||
# 修复状态分歧(默认 dry-run)
|
||||
paperforge repair --verbose # 查看三向状态分歧详情
|
||||
paperforge repair --fix # 实际修复(慎用)
|
||||
|
||||
# 查看整体状态
|
||||
paperforge status
|
||||
|
||||
# 验证安装配置
|
||||
paperforge doctor
|
||||
```
|
||||
|
||||
> 如果 `paperforge` 命令未注册,可使用 fallback:
|
||||
> ```bash
|
||||
> python -m paperforge <command>
|
||||
> ```
|
||||
> 例如:`python -m paperforge status`
|
||||
|
||||
### Agent 命令
|
||||
```
|
||||
/pf-deep <zotero_key> # 完整三阶段精读(必须 Agent 执行)
|
||||
/pf-paper <zotero_key> # 文献问答(必须 Agent 执行)
|
||||
/pf-sync # 同步 Zotero(Agent 包装 CLI)
|
||||
/pf-ocr # 运行 OCR(Agent 包装 CLI)
|
||||
/pf-status # 查看状态(Agent 包装 CLI)
|
||||
```
|
||||
|
||||
> 注:`/pf-sync`、`/pf-ocr`、`/pf-status` 与 `paperforge sync/ocr/status` 是同一命令的两种调用方式。在终端直接运行 CLI 即可;在 OpenCode 中使用 `/pf-*` 可以让 Agent 帮你检查前置条件并解读输出。
|
||||
|
||||
### Chart-Reading 指南索引
|
||||
|
||||
`/pf-deep` 精读时会参考 19 种图表类型的阅读指南,按生物医学文献常见度排序。完整索引参见 `chart-reading/INDEX.md`。
|
||||
|
||||
---
|
||||
|
||||
## 10. 常见问题
|
||||
|
||||
### Q: 运行 sync 后没有生成正式笔记?
|
||||
- 检查 Better BibTeX JSON 导出路径是否正确
|
||||
- 检查 JSON 文件是否包含文献数据
|
||||
- 确认 Zotero 中该文献有 citation key
|
||||
|
||||
### Q: OCR 一直显示 pending?
|
||||
- 检查 PaddleOCR API Key 是否配置正确(`.env` 文件)
|
||||
- 检查网络连接
|
||||
- 查看 `<system_dir>/PaperForge/ocr/<key>/meta.json` 中的错误信息
|
||||
|
||||
### Q: /pf-deep 提示 OCR 未完成?
|
||||
- 确认正式笔记 frontmatter 中 `ocr_status: done`
|
||||
- 如 OCR 失败,可重新设置 `do_ocr: true` 再运行 ocr worker
|
||||
|
||||
### Q: Base 视图中 pdf_path 显示为绝对路径?
|
||||
- 这是 Obsidian 渲染问题,数据本身是相对路径
|
||||
- 不影响功能,可忽略
|
||||
|
||||
### Q: 可以批量操作吗?
|
||||
- 可以。使用 Obsidian Base 视图批量修改 `do_ocr` 和 `analyze` 字段
|
||||
- 或使用脚本批量修改 formal note frontmatter
|
||||
|
||||
---
|
||||
|
||||
## 11. 升级与维护
|
||||
|
||||
### 更新 PaperForge 代码
|
||||
|
||||
#### 方式 1:自动更新(推荐)
|
||||
|
||||
```bash
|
||||
# 自动检测安装方式并更新
|
||||
paperforge update
|
||||
```
|
||||
|
||||
系统会自动检测你是通过 pip、git 还是手动安装,并执行对应的更新方式。
|
||||
|
||||
#### 方式 2:Windows 一键脚本
|
||||
|
||||
双击运行 Vault 根目录下的 `scripts/update-paperforge.ps1`:
|
||||
- 自动检测安装方式
|
||||
- 自动执行更新
|
||||
- 无需手动输入命令
|
||||
|
||||
```powershell
|
||||
# 或在 PowerShell 中执行
|
||||
.\scripts\update-paperforge.ps1
|
||||
|
||||
# 强制更新(跳过确认)
|
||||
.\scripts\update-paperforge.ps1 -Force
|
||||
|
||||
# 只检测不更新
|
||||
.\scripts\update-paperforge.ps1 -DryRun
|
||||
```
|
||||
|
||||
#### 方式 3:手动更新
|
||||
|
||||
**推荐:自动更新**
|
||||
```bash
|
||||
paperforge update
|
||||
```
|
||||
系统会自动检测安装方式并执行对应的更新命令。
|
||||
|
||||
**pip 安装用户:**
|
||||
```bash
|
||||
pip install --upgrade paperforge
|
||||
```
|
||||
|
||||
**pip editable / git clone 用户:**
|
||||
```bash
|
||||
cd 你的仓库目录
|
||||
git pull origin master
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
#### 方式 4:手动复制(最后手段)
|
||||
|
||||
```bash
|
||||
cp -r 新下载的代码/* <vault_path>/
|
||||
```
|
||||
|
||||
> [!WARNING] 手动复制容易遗漏文件,建议优先使用自动更新。
|
||||
|
||||
### 备份注意事项
|
||||
- `<resources_dir>/` 和 `<system_dir>/PaperForge/ocr/` 包含你的数据,需备份
|
||||
- `.env` 包含 API Key,不要提交到 git
|
||||
- `<system_dir>/PaperForge/exports/` 可重新生成(由 Zotero 自动导出)
|
||||
|
||||
---
|
||||
|
||||
## 12. 命令迁移说明(v1.1 → v1.2)
|
||||
|
||||
从 v1.2 开始,PaperForge 采用统一的命令接口:
|
||||
|
||||
- **CLI 统一入口**:`paperforge sync`(替代 `selection-sync` + `index-refresh`)、`paperforge ocr`(替代 `ocr run`)
|
||||
- **Agent 统一前缀**:`/pf-deep`、`/pf-paper`、`/pf-ocr`、`/pf-sync`、`/pf-status`(替代 `/LD-*` 和 `/lp-*`)
|
||||
- **Python 包重命名**:`paperforge`(替代 `paperforge_lite`)
|
||||
|
||||
**旧命令仍兼容**:v1.2 继续支持旧命令名(`selection-sync`、`index-refresh`、`ocr run`),但文档已统一使用新命令。
|
||||
|
||||
> **v1.4 新增**:结构化日志(`--verbose`)、自动重试、进度条、代码自动化检查。详细迁移步骤和回滚说明参见 [docs/MIGRATION-v1.2.md](docs/MIGRATION-v1.2.md)(v1.1→v1.2)和 [docs/MIGRATION-v1.4.md](docs/MIGRATION-v1.4.md)(v1.3→v1.4)。
|
||||
|
||||
---
|
||||
|
||||
## 13. 开发者指南(AI Agent 必读)
|
||||
|
||||
### v2.1 新增模块
|
||||
|
||||
v2.1(1.4.17rc4)引入了以下新增模块,开发时请注意:
|
||||
|
||||
| 路径 | 内容 | 注意事项 |
|
||||
|------|------|---------|
|
||||
| `paperforge/core/` | 契约层 — PFResult, ErrorCode, 状态机 | 所有模块都可引用,不循环依赖 |
|
||||
| `paperforge/adapters/` | 适配器 — bbt, zotero_paths, frontmatter | 独立可测,从 `sync.py` 提取 |
|
||||
| `paperforge/services/` | 服务层 — SyncService | 编排适配器,`sync.py` 变调度壳 |
|
||||
| `paperforge/setup/` | 安装层 — 6 个类 | 从 `setup_wizard.py` 拆出 |
|
||||
| `paperforge/schema/` | 字段注册表 — `field_registry.yaml` | 44 字段定义 |
|
||||
| `paperforge/doctor/` | 校验 — `field_validator.py` | `doctor` 命令使用 |
|
||||
|
||||
所有 `--json` 输出统一为 PFResult 格式 `{ok, command, version, data, error}`。修改输出结构时需同时更新 `core/result.py` 和下游 consumer(plugin)。
|
||||
|
||||
### 版本号管理
|
||||
|
||||
PaperForge 版本只在 `paperforge/__init__.py` 一处定义。升版本时不要手动改多个文件,使用自动化脚本:
|
||||
|
||||
```bash
|
||||
# 升 patch 版本(1.4.11 → 1.4.12),自动 git commit + tag
|
||||
python scripts/bump.py patch
|
||||
|
||||
# 升 minor 版本(1.4.11 → 1.5.0)
|
||||
python scripts/bump.py minor
|
||||
|
||||
# 指定版本号
|
||||
python scripts/bump.py 2.0.0
|
||||
|
||||
# 预览(不实际修改)
|
||||
python scripts/bump.py patch --dry-run
|
||||
|
||||
# 只改文件不 commit
|
||||
python scripts/bump.py patch --no-git
|
||||
```
|
||||
|
||||
脚本会自动更新:
|
||||
| 文件 | 字段 |
|
||||
|------|------|
|
||||
| `paperforge/__init__.py` | `__version__` |
|
||||
| `paperforge/plugin/manifest.json` | `version` |
|
||||
| `manifest.json` | `version` |
|
||||
| `paperforge/plugin/versions.json` | 追加 `"version": "minAppVersion"` |
|
||||
|
||||
### 发布 Release 流程
|
||||
|
||||
```bash
|
||||
# 1. 升版本(自动 commit + tag)
|
||||
python scripts/bump.py patch
|
||||
|
||||
# 2. 推送代码和 tag
|
||||
git push && git push --tags
|
||||
|
||||
# 3. 创建 GitHub Release 并上传插件文件
|
||||
gh release create v1.4.12 \
|
||||
--title "v1.4.12" \
|
||||
--notes "简短说明" \
|
||||
paperforge/plugin/main.js \
|
||||
paperforge/plugin/styles.css \
|
||||
paperforge/plugin/manifest.json \
|
||||
paperforge/plugin/versions.json
|
||||
```
|
||||
|
||||
> Obsidian 社区插件要求 Release 附带 `main.js`、`manifest.json`、`styles.css`、`versions.json` 四个单独文件(非 zip)。
|
||||
|
||||
### 插件 i18n
|
||||
|
||||
插件界面通过 `paperforge/plugin/i18n.js` 做中英文适配:
|
||||
- 语言自动检测:读取 Obsidian 语言设置,`zh*` 用中文,其他用英文
|
||||
- 新增文本时,需同时在 `i18n.js` 的 `zh` 和 `en` 两块加 key
|
||||
- 代码中使用 `t('key_name')` 读取翻译
|
||||
- Dashboard 的 `ACTIONS` 标题保持英文(Obsidian 命令面板统一使用英文 ID)
|
||||
|
||||
### pre-commit 检查
|
||||
|
||||
```bash
|
||||
# 提交前运行 ruff lint + format + 一致性审计
|
||||
# Lint
|
||||
ruff check --fix paperforge/ && ruff format paperforge/
|
||||
|
||||
# 运行测试(173 tests)
|
||||
python -m pytest tests/unit/ -q --tb=short
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*PaperForge | 快速开始指南 | 安装后阅读*
|
||||
## 文档地图
|
||||
|
||||
| 受众 | 文件 |
|
||||
|------|------|
|
||||
| 终端用户教程 | [docs/getting-started.md](docs/getting-started.md) |
|
||||
| 故障排除 | [docs/troubleshooting.md](docs/troubleshooting.md) |
|
||||
| 命令参考 | [docs/COMMANDS.md](docs/COMMANDS.md) |
|
||||
| 更新升级 | [docs/update-upgrade.md](docs/update-upgrade.md) |
|
||||
| 架构 | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) |
|
||||
| 维护者 | [docs/maintainer-guide.md](docs/maintainer-guide.md) |
|
||||
| 迁移历史 | [docs/MIGRATION-v1.2.md](docs/MIGRATION-v1.2.md) |
|
||||
|
|
|
|||
228
README.en.md
228
README.en.md
|
|
@ -68,220 +68,34 @@ Plugin v1.5.0 → Python Package v1.5.0 ✓ Matched
|
|||
|
||||
---
|
||||
|
||||
## 3. How Python Interpreter Resolution Works
|
||||
|
||||
PaperForge needs to find a working Python on your system. It searches in this order:
|
||||
|
||||
| Priority | Source | Description |
|
||||
|----------|--------|-------------|
|
||||
| 1 | **Manual override** | Settings → `Custom Python Path`, enter the full path (e.g., `C:\Users\you\...\python.exe`). **This is the most reliable method.** |
|
||||
| 2 | **venv auto-detect** | Scans `.paperforge-test-venv`, `.venv`, `venv` under your vault root |
|
||||
| 3 | **System auto-detect** | Tries `py -3`, `python`, `python3` in order, verifies with `--version` |
|
||||
| 4 | **Fallback** | Defaults to `python` if nothing else works |
|
||||
|
||||
> If you have multiple Python installations (e.g., system 3.9 + self-installed 3.11), **strongly recommend setting a manual path** in settings to avoid hitting the wrong one.
|
||||
>
|
||||
> The **Validate** button in settings immediately tests the resolved interpreter and shows its version.
|
||||
|
||||
---
|
||||
|
||||
## 4. Setup Wizard — What Each Step Means
|
||||
|
||||
Open the plugin settings panel (`Settings` → `Community plugins` → `PaperForge`) and click the **Open Wizard** button. The wizard walks you through configuration. Here's what every step does.
|
||||
|
||||
### 4.1 Vault Path
|
||||
|
||||
Your Obsidian vault root. Auto-detected, usually no need to change.
|
||||
|
||||
### 4.2 AI Agent Platform
|
||||
|
||||
PaperForge's deep reading features run through an AI Agent. The core mechanism is **trigger phrases**, not registered plugins: you type `/pf-deep <key>` directly into the Agent chat, and the Agent recognizes the trigger and loads the `literature-qa` Skill automatically.
|
||||
|
||||
The setup wizard deploys Skill files to the correct location:
|
||||
|
||||
| Agent | Skill location | Trigger example |
|
||||
|-------|---------------|-----------------|
|
||||
| **OpenCode** | `.opencode/skills/` + `.opencode/command/` | `/pf-deep <key>` |
|
||||
| **Claude Code** | `.claude/skills/` | `/pf-deep <key>` |
|
||||
| **Cursor** | `.cursor/skills/` | `/pf-deep <key>` |
|
||||
| **GitHub Copilot** | `.github/skills/` | `/pf-deep <key>` |
|
||||
| **Windsurf** | `.windsurf/skills/` | `/pf-deep <key>` |
|
||||
| **Codex** | `.codex/skills/` | `$pf-deep <key>` |
|
||||
| **Cline** | `.clinerules/` | `/pf-deep <key>` |
|
||||
|
||||
> **Key concept**: `/pf-deep` is NOT a plugin you install on the Agent platform — it's a Skill file deployed inside your Vault. Once the setup wizard copies the files into place, the Agent auto-discovers the triggers on startup. You type the trigger phrase just like any other chat input.
|
||||
|
||||
### 4.3 Directory Names
|
||||
|
||||
The wizard asks what to name several directories. These are for organizing files inside your vault. **Defaults work for most users.**
|
||||
|
||||
| Parameter | Default | Purpose |
|
||||
|-----------|---------|---------|
|
||||
| `system_dir` | `System` | Root for PaperForge internal data. Contains `exports/` (Zotero JSON exports), `ocr/` (OCR results), `config/`. You rarely need to open this manually. |
|
||||
| `resources_dir` | `Resources` | Resources root. Your formal literature notes live under this directory, inside `literature_dir`. |
|
||||
| `literature_dir` | `Literature` | Formal literature notes directory. `paperforge sync` generates frontmatter `.md` notes here. |
|
||||
| `base_dir` | `Bases` | Obsidian Base view definitions. Dashboard filters ("Pending OCR", "Ready to Read", etc.) are stored here. |
|
||||
|
||||
### 4.4 PaddleOCR API Token
|
||||
|
||||
OCR requires a PaddleOCR API key. Configured in `.env`:
|
||||
|
||||
```
|
||||
PADDLEOCR_API_TOKEN=your-api-key
|
||||
```
|
||||
|
||||
The wizard guides you through setting this. You can also edit `.env` later. The OCR URL usually stays at the default.
|
||||
|
||||
### 4.5 Zotero Data Directory
|
||||
|
||||
PaperForge creates a junction (Windows) or symlink (macOS/Linux) linking your Zotero data directory into the vault. This is how Obsidian wikilinks resolve to PDF files.
|
||||
|
||||
The wizard auto-detects your Zotero installation. If detection fails, manually enter the path to your Zotero data directory — the folder that contains the `storage/` subdirectory (not the Zotero executable).
|
||||
|
||||
### 4.6 What Happens During Setup
|
||||
|
||||
After confirming your choices, the wizard automatically:
|
||||
- Creates all needed directory structures
|
||||
- Deploys Agent command files to the correct locations
|
||||
- Installs Obsidian plugin files
|
||||
- Creates the Zotero junction/symlink
|
||||
- Writes `paperforge.json` and `.env`
|
||||
|
||||
The process is **incremental** — if files already exist in the chosen directories, the wizard only adds what's missing and never deletes existing content.
|
||||
|
||||
---
|
||||
|
||||
## 5. First-Time Setup Checklist
|
||||
|
||||
1. **Version match**: Settings → Runtime Status → confirm plugin and Python package match
|
||||
2. **Python path**: Settings → Validate button → confirm it's the Python you want
|
||||
3. **Setup wizard**: Settings → PaperForge → Open Wizard
|
||||
4. **PaddleOCR key**: Enter your API token in `.env` (wizard guides this)
|
||||
5. **Export from Zotero**: Right-click your library → `Export...` → format `Better BibTeX JSON` → check `Keep updated` → save to `<system_dir>/PaperForge/exports/`
|
||||
6. **Run Doctor**: Dashboard → `Run Doctor` → all checks should pass
|
||||
|
||||
---
|
||||
|
||||
## 6. Daily Use
|
||||
|
||||
### Dashboard (Three-Mode Views)
|
||||
|
||||
`Ctrl+P` → `PaperForge: Open Dashboard` opens the control panel with three views:
|
||||
|
||||
| View | Purpose |
|
||||
|------|---------|
|
||||
| **Global** | System homepage: run Sync, OCR, Doctor, and other mechanical operations |
|
||||
| **Collection** | Batch workspace: browse paper queues by domain, batch tagging |
|
||||
| **Per-paper** | Reading companion: `do_ocr` / `analyze` toggle checkboxes, discussion record cards |
|
||||
|
||||
> PDF files in the Dashboard automatically switch to Per-paper mode — no manual switching needed.
|
||||
|
||||
### AI Deep Reading & Q&A (Requires Agent)
|
||||
|
||||
Launch your Agent app and type commands into its chat input. **The more specific you are about the paper (Zotero Key, title, DOI), the faster the Agent locates it.**
|
||||
|
||||
| Route | Command | Does | Trigger examples | Prerequisites |
|
||||
|-------|---------|------|-----------------|--------------|
|
||||
| Deep Read | `/pf-deep <key>` | Keshav three-pass deep reading, writes to formal note | `deep read XX`, `walk me through`, `journal club` | OCR done, analyze: true |
|
||||
| Q&A | `/pf-paper <key>` | Interactive paper Q&A, OCR not required | `take a look at XX`, `what does this paper say` | Formal note exists |
|
||||
| Archive | `/pf-end` | Save current `/pf-paper` Q&A session | `save`, `end discussion` | During `/pf-paper` session |
|
||||
|
||||
### `/pf-end` Details
|
||||
|
||||
- `/pf-end` only applies to `/pf-paper` Q&A sessions. Deep reading (`/pf-deep`) writes directly to the formal note and does not need `/pf-end`.
|
||||
- When executed, two files are created in the paper's workspace:
|
||||
- `discussion.md` — human-readable Q&A discussion record
|
||||
- `discussion.json` — structured Q&A data (with timestamps, source tags)
|
||||
- Dashboard **Per-paper** view automatically displays these as discussion record cards
|
||||
|
||||
> Command prefixes vary by platform (mostly `/`, Codex uses `$`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Full Workflow
|
||||
|
||||
```
|
||||
Add paper to Zotero
|
||||
↓ Better BibTeX auto-exports JSON to exports/
|
||||
Dashboard → Sync Library
|
||||
↓ Generates formal note (in Literature/, with frontmatter metadata)
|
||||
Set do_ocr: true in the note's frontmatter
|
||||
↓
|
||||
Dashboard → Run OCR
|
||||
↓ PaddleOCR extracts full text + figures → ocr/ directory
|
||||
Set analyze: true in the note's frontmatter
|
||||
↓
|
||||
Open Agent → type /pf-deep <zotero_key>
|
||||
↓ Agent performs three-pass deep reading
|
||||
## 🔍 Deep Reading section appears in the note
|
||||
↓ (for additional Q&A)
|
||||
Open Agent → type /pf-paper <zotero_key>
|
||||
↓ Interactive Q&A
|
||||
Type /pf-end to save the discussion record
|
||||
↓
|
||||
Dashboard Per-paper view shows discussion cards
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Troubleshooting
|
||||
|
||||
### Plugin fails to load
|
||||
|
||||
- Confirm `.obsidian/plugins/paperforge/` has `main.js`, `manifest.json`, `styles.css`
|
||||
- If upgrading via BRAT from an old version: delete the entire `paperforge` plugin folder and let BRAT re-download
|
||||
- Open Developer Console (`Ctrl+Shift+I`) and check the red errors
|
||||
|
||||
### "Sync Runtime" doesn't update the version
|
||||
|
||||
- The plugin may be calling a different Python than your terminal. Check Settings → Python path
|
||||
- Try with `--no-cache-dir` to bypass pip cache
|
||||
- Confirm `https://github.com/LLLin000/PaperForge` is reachable
|
||||
|
||||
### OCR stays pending
|
||||
|
||||
- Confirm `.env` has `PADDLEOCR_API_TOKEN`
|
||||
- Run `paperforge ocr --diagnose` to check API connectivity
|
||||
- PDF paths may be broken: run `paperforge repair --fix-paths`
|
||||
|
||||
### No notes generated after sync
|
||||
|
||||
- Is Better BibTeX auto-export configured in Zotero? Are JSON files in `exports/`?
|
||||
- Run `paperforge doctor` to find which step failed
|
||||
|
||||
### /pf-deep command does nothing
|
||||
|
||||
- Make sure you're running it in your Agent app, not a terminal
|
||||
- Confirm OCR is done (`ocr_status: done`)
|
||||
- Confirm `analyze` is set to `true`
|
||||
|
||||
---
|
||||
|
||||
## 9. Updating
|
||||
|
||||
BRAT auto-detects plugin updates. For the Python package:
|
||||
## 3. Quickstart
|
||||
|
||||
```bash
|
||||
paperforge update
|
||||
# or
|
||||
pip install --upgrade paperforge
|
||||
# 1. Export from Zotero (Better BibTeX JSON, Keep updated) to exports/
|
||||
# 2. Sync
|
||||
paperforge sync
|
||||
|
||||
# 3. Mark a paper for OCR in its frontmatter: do_ocr: true
|
||||
# 4. Run OCR
|
||||
paperforge ocr
|
||||
|
||||
# 5. Mark for deep reading: analyze: true
|
||||
# 6. In your Agent chat:
|
||||
/pf-deep <zotero_key>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Architecture
|
||||
## Documentation
|
||||
|
||||
```
|
||||
paperforge/
|
||||
├── core/ Contract layer — PFResult/ErrorCode/state machine
|
||||
├── adapters/ Adapter layer — BBT parsing, paths, frontmatter I/O
|
||||
├── services/ Service layer — SyncService orchestration
|
||||
├── worker/ Worker layer — OCR, status, repair
|
||||
├── commands/ CLI dispatch
|
||||
├── setup/ Setup wizard (directories, agent deployment, Zotero linking)
|
||||
├── plugin/ Obsidian plugin (Dashboard, settings panel)
|
||||
└── schema/ Field registry
|
||||
```
|
||||
| If you want to | Read |
|
||||
| --------------------------------------- | ----------------------------------------- |
|
||||
| Full tutorial, from install to deep read | [Getting Started](docs/getting-started.md) |
|
||||
| Troubleshooting | [Troubleshooting](docs/troubleshooting.md) |
|
||||
| Command reference | [Commands](docs/COMMANDS.md) |
|
||||
| How to update | [Update Guide](docs/update-upgrade.md) |
|
||||
| Architecture / Maintenance / Release | [Architecture](docs/ARCHITECTURE.md) |
|
||||
| AI Agent collaboration | [AGENTS.md](AGENTS.md) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
230
README.md
230
README.md
|
|
@ -75,222 +75,34 @@ Plugin v1.5.0 → Python Package v1.5.0 ✓ Matched
|
|||
|
||||
---
|
||||
|
||||
## 3. How Python Interpreter Resolution Works
|
||||
|
||||
PaperForge needs to find a working Python on your system. It searches in this order:
|
||||
|
||||
| Priority | Source | Description |
|
||||
|----------|--------|-------------|
|
||||
| 1 | **Manual override** | Settings → `Custom Python Path`, enter the full path (e.g., `C:\Users\you\...\python.exe`). **This is the most reliable method.** |
|
||||
| 2 | **venv auto-detect** | Scans `.paperforge-test-venv`, `.venv`, `venv` under your vault root |
|
||||
| 3 | **System auto-detect** | Tries `py -3`, `python`, `python3` in order, verifies with `--version` |
|
||||
| 4 | **Fallback** | Defaults to `python` if nothing else works |
|
||||
|
||||
> If you have multiple Python installations (e.g., system 3.9 + self-installed 3.11), **strongly recommend setting a manual path** in settings to avoid hitting the wrong one.
|
||||
>
|
||||
> The **Validate** button in settings immediately tests the resolved interpreter and shows its version.
|
||||
|
||||
---
|
||||
|
||||
## 4. Setup Wizard — What Each Step Means
|
||||
|
||||
Open the plugin settings panel (`Settings` → `Community plugins` → `PaperForge`) and click the **Open Wizard** button. The wizard walks you through configuration. Here's what every step does.
|
||||
|
||||
### 4.1 Vault Path
|
||||
|
||||
Your Obsidian vault root. Auto-detected, usually no need to change.
|
||||
|
||||
### 4.2 AI Agent Platform
|
||||
|
||||
PaperForge's deep reading features run through an AI Agent. The core mechanism is **trigger phrases**, not registered plugins: you type `/pf-deep <key>` directly into the Agent chat, and the Agent recognizes the trigger and loads the `literature-qa` Skill automatically.
|
||||
|
||||
The setup wizard deploys Skill files to the correct location:
|
||||
|
||||
| Agent | Skill location | Trigger example |
|
||||
|-------|---------------|-----------------|
|
||||
| **OpenCode** | `.opencode/skills/` + `.opencode/command/` | `/pf-deep <key>` |
|
||||
| **Claude Code** | `.claude/skills/` | `/pf-deep <key>` |
|
||||
| **Cursor** | `.cursor/skills/` | `/pf-deep <key>` |
|
||||
| **GitHub Copilot** | `.github/skills/` | `/pf-deep <key>` |
|
||||
| **Windsurf** | `.windsurf/skills/` | `/pf-deep <key>` |
|
||||
| **Codex** | `.codex/skills/` | `$pf-deep <key>` |
|
||||
| **Cline** | `.clinerules/` | `/pf-deep <key>` |
|
||||
|
||||
> **Key concept**: `/pf-deep` is NOT a plugin you install on the Agent platform — it's a Skill file deployed inside your Vault. Once the setup wizard copies the files into place, the Agent auto-discovers the triggers on startup. You type the trigger phrase just like any other chat input.
|
||||
|
||||
### 4.3 Directory Names
|
||||
|
||||
The wizard asks what to name several directories. These are for organizing files inside your vault. **Defaults work for most users.**
|
||||
|
||||
| Parameter | Default | Purpose |
|
||||
|-----------|---------|---------|
|
||||
| `system_dir` | `System` | Root for PaperForge internal data. Contains `exports/` (Zotero JSON exports), `ocr/` (OCR results), `config/`. You rarely need to open this manually. |
|
||||
| `resources_dir` | `Resources` | Resources root. Your formal literature notes live under this directory, inside `literature_dir`. |
|
||||
| `literature_dir` | `Literature` | Formal literature notes directory. `paperforge sync` generates frontmatter `.md` notes here. |
|
||||
| `base_dir` | `Bases` | Obsidian Base view definitions. Dashboard filters ("Pending OCR", "Ready to Read", etc.) are stored here. |
|
||||
|
||||
### 4.4 PaddleOCR API Token
|
||||
|
||||
OCR requires a PaddleOCR API key. Configured in `.env`:
|
||||
|
||||
```
|
||||
PADDLEOCR_API_TOKEN=your-api-key
|
||||
```
|
||||
|
||||
The wizard guides you through setting this. You can also edit `.env` later. The OCR URL usually stays at the default.
|
||||
|
||||
### 4.5 Zotero Data Directory
|
||||
|
||||
PaperForge creates a junction (Windows) or symlink (macOS/Linux) linking your Zotero data directory into the vault. This is how Obsidian wikilinks resolve to PDF files.
|
||||
|
||||
The wizard auto-detects your Zotero installation. If detection fails, manually enter the path to your Zotero data directory — the folder that contains the `storage/` subdirectory (not the Zotero executable).
|
||||
|
||||
### 4.6 What Happens During Setup
|
||||
|
||||
After confirming your choices, the wizard automatically:
|
||||
- Creates all needed directory structures
|
||||
- Deploys Agent command files to the correct locations
|
||||
- Installs Obsidian plugin files
|
||||
- Creates the Zotero junction/symlink
|
||||
- Writes `paperforge.json` and `.env`
|
||||
|
||||
The process is **incremental** — if files already exist in the chosen directories, the wizard only adds what's missing and never deletes existing content.
|
||||
|
||||
---
|
||||
|
||||
## 5. First-Time Setup Checklist
|
||||
|
||||
1. **Version match**: Settings → Runtime Status → confirm plugin and Python package match
|
||||
2. **Python path**: Settings → Validate button → confirm it's the Python you want
|
||||
3. **Setup wizard**: Settings → PaperForge → Open Wizard
|
||||
4. **PaddleOCR key**: Enter your API token in `.env` (wizard guides this)
|
||||
5. **Export from Zotero**: Right-click your library → `Export...` → format `Better BibTeX JSON` → check `Keep updated` → save to `<system_dir>/PaperForge/exports/`
|
||||
6. **Run Doctor**: Dashboard → `Run Doctor` → all checks should pass
|
||||
|
||||
---
|
||||
|
||||
## 6. Daily Use
|
||||
|
||||
### Dashboard (Three-Mode Views)
|
||||
|
||||
`Ctrl+P` → `PaperForge: Open Dashboard` opens the control panel with three views:
|
||||
|
||||
| View | Purpose |
|
||||
|------|---------|
|
||||
| **Global** | System homepage: run Sync, OCR, Doctor, and other mechanical operations |
|
||||
| **Collection** | Batch workspace: browse paper queues by domain, batch tagging |
|
||||
| **Per-paper** | Reading companion: `do_ocr` / `analyze` toggle checkboxes, discussion record cards |
|
||||
|
||||
> PDF files in the Dashboard automatically switch to Per-paper mode — no manual switching needed.
|
||||
|
||||
### AI Deep Reading & Q&A (Requires Agent)
|
||||
|
||||
Launch your Agent app and type commands into its chat input. **The more specific you are about the paper (Zotero Key, title, DOI), the faster the Agent locates it.**
|
||||
|
||||
| Route | Command | Does | Trigger examples | Prerequisites |
|
||||
|-------|---------|------|-----------------|--------------|
|
||||
| Deep Read | `/pf-deep <key>` | Keshav three-pass deep reading, writes to formal note | `deep read XX`, `walk me through`, `journal club` | OCR done, analyze: true |
|
||||
| Q&A | `/pf-paper <key>` | Interactive paper Q&A, OCR not required | `take a look at XX`, `what does this paper say` | Formal note exists |
|
||||
| Archive | `/pf-end` | Save current `/pf-paper` Q&A session | `save`, `end discussion` | During `/pf-paper` session |
|
||||
|
||||
### `/pf-end` Details
|
||||
|
||||
- `/pf-end` only applies to `/pf-paper` Q&A sessions. Deep reading (`/pf-deep`) writes directly to the formal note and does not need `/pf-end`.
|
||||
- When executed, two files are created in the paper's workspace:
|
||||
- `discussion.md` — human-readable Q&A discussion record
|
||||
- `discussion.json` — structured Q&A data (with timestamps, source tags)
|
||||
- Dashboard **Per-paper** view automatically displays these as discussion record cards
|
||||
|
||||
> Command prefixes vary by platform (mostly `/`, Codex uses `$`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Full Workflow
|
||||
|
||||
```
|
||||
Add paper to Zotero
|
||||
↓ Better BibTeX auto-exports JSON to exports/
|
||||
Dashboard → Sync Library
|
||||
↓ Generates formal note (in Literature/, with frontmatter metadata)
|
||||
Set do_ocr: true in the note's frontmatter
|
||||
↓
|
||||
Dashboard → Run OCR
|
||||
↓ PaddleOCR extracts full text + figures → ocr/ directory
|
||||
Set analyze: true in the note's frontmatter
|
||||
↓
|
||||
Open Agent → type /pf-deep <zotero_key>
|
||||
↓ Agent performs three-pass deep reading
|
||||
## 🔍 Deep Reading section appears in the note
|
||||
↓ (for additional Q&A)
|
||||
Open Agent → type /pf-paper <zotero_key>
|
||||
↓ Interactive Q&A
|
||||
Type /pf-end to save the discussion record
|
||||
↓
|
||||
Dashboard Per-paper view shows discussion cards
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Troubleshooting
|
||||
|
||||
### Plugin fails to load
|
||||
|
||||
- Confirm `.obsidian/plugins/paperforge/` has `main.js`, `manifest.json`, `styles.css`
|
||||
- If upgrading from an old version: delete the entire `paperforge` plugin folder and reinstall via the community plugin browser
|
||||
- Open Developer Console (`Ctrl+Shift+I`) and check the red errors
|
||||
|
||||
### "Sync Runtime" doesn't update the version
|
||||
|
||||
- The plugin may be calling a different Python than your terminal. Check Settings → Python path
|
||||
- Try with `--no-cache-dir` to bypass pip cache
|
||||
- Confirm `https://github.com/LLLin000/PaperForge` is reachable
|
||||
|
||||
### OCR stays pending
|
||||
|
||||
- Confirm `.env` has `PADDLEOCR_API_TOKEN`
|
||||
- Run `paperforge ocr --diagnose` to check API connectivity
|
||||
- PDF paths may be broken: run `paperforge repair --fix-paths`
|
||||
|
||||
### No notes generated after sync
|
||||
|
||||
- Is Better BibTeX auto-export configured in Zotero? Are JSON files in `exports/`?
|
||||
- Run `paperforge doctor` to find which step failed
|
||||
|
||||
### /pf-deep command does nothing
|
||||
|
||||
- Make sure you're running it in your Agent app, not a terminal
|
||||
- Confirm OCR is done (`ocr_status: done`)
|
||||
- Confirm `analyze` is set to `true`
|
||||
|
||||
---
|
||||
|
||||
## 9. Updating
|
||||
|
||||
The Obsidian plugin auto-updates through the community plugin browser. For the Python package:
|
||||
## 3. Quickstart
|
||||
|
||||
```bash
|
||||
paperforge update
|
||||
# or
|
||||
pip install --upgrade paperforge
|
||||
```
|
||||
# 1. Export from Zotero (Better BibTeX JSON, Keep updated) to exports/
|
||||
# 2. Sync
|
||||
paperforge sync
|
||||
|
||||
If you installed via BRAT, it also auto-detects GitHub Release updates.
|
||||
# 3. Mark a paper for OCR in its frontmatter: do_ocr: true
|
||||
# 4. Run OCR
|
||||
paperforge ocr
|
||||
|
||||
# 5. Mark for deep reading: analyze: true
|
||||
# 6. In your Agent chat:
|
||||
/pf-deep <zotero_key>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Architecture
|
||||
## 文档导航
|
||||
|
||||
```
|
||||
paperforge/
|
||||
├── core/ Contract layer — PFResult/ErrorCode/state machine
|
||||
├── adapters/ Adapter layer — BBT parsing, paths, frontmatter I/O
|
||||
├── services/ Service layer — SyncService orchestration
|
||||
├── worker/ Worker layer — OCR, status, repair
|
||||
├── commands/ CLI dispatch
|
||||
├── setup/ Setup wizard (directories, agent deployment, Zotero linking)
|
||||
├── plugin/ Obsidian plugin (Dashboard, settings panel)
|
||||
└── schema/ Field registry
|
||||
```
|
||||
| 你想做什么 | 去看 |
|
||||
| ------------------------------------------ | ------------------------------ |
|
||||
| 完整教程,从安装到精读 | [使用教程](docs/getting-started.md) |
|
||||
| 遇到问题了 | [故障排除](docs/troubleshooting.md) |
|
||||
| 查某个命令 | [命令参考](docs/COMMANDS.md) |
|
||||
| 如何升级 | [更新指南](docs/update-upgrade.md) |
|
||||
| 架构 / 维护 / 发布 | [架构文档](docs/ARCHITECTURE.md) |
|
||||
| AI Agent 协作 | [AGENTS.md](AGENTS.md) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
145
docs/COMMANDS.md
145
docs/COMMANDS.md
|
|
@ -1,119 +1,104 @@
|
|||
# PaperForge 命令总览
|
||||
# PaperForge 命令参考
|
||||
|
||||
> PaperForge 完整命令参考。Agent 命令与 CLI 命令对照表。
|
||||
>
|
||||
> 另见:[AGENTS.md](../AGENTS.md) — 完整使用指南与架构说明
|
||||
> 所有 CLI 命令和 Agent 命令速查。
|
||||
|
||||
---
|
||||
|
||||
## 命令矩阵
|
||||
|
||||
| Agent 命令 | CLI 命令 | 说明 | 前置条件 | 示例 |
|
||||
|-----------|---------|------|---------|------|
|
||||
| `/pf-deep` | `paperforge sync` + `paperforge ocr` | 完整三阶段精读(Keshav 法) | OCR 完成 (`ocr_status: done`) | `/pf-deep ABCDEFG` |
|
||||
| `/pf-paper` | `paperforge sync` | 快速摘要与问答工作台 | 正式笔记已生成 | `/pf-paper ABCDEFG` |
|
||||
| `/pf-ocr` | `paperforge ocr` | PDF OCR 文本与图表提取 | PDF 存在 + `do_ocr: true` | `paperforge ocr` |
|
||||
| `/pf-sync` | `paperforge sync` | 同步 Zotero 到文献库 | Better BibTeX JSON 导出 | `paperforge sync --domain 骨科` |
|
||||
| `/pf-status` | `paperforge status` | 查看系统安装与运行状态 | 配置完成 | `paperforge status` |
|
||||
|
||||
> **说明**:Agent 命令(`/pf-*`)在对话窗口中输入,由 AI Agent 执行。CLI 命令在终端中输入,由 Python worker 执行。
|
||||
| Agent 命令 | CLI 命令 | 用途 | 前置条件 |
|
||||
|-----------|---------|------|---------|
|
||||
| `/pf-sync` | `paperforge sync` | 同步 Zotero,生成正式笔记 | BBT JSON 导出 |
|
||||
| `/pf-ocr` | `paperforge ocr` | PDF OCR 文本与图表提取 | `do_ocr: true` |
|
||||
| `/pf-status` | `paperforge status` | 查看系统状态 | 配置完成 |
|
||||
| `/pf-deep <key>` | `paperforge deep-reading` | 三阶段精读 | OCR done + `analyze: true` |
|
||||
| `/pf-paper <key>` | — | 文献问答 | 正式笔记存在 |
|
||||
|
||||
---
|
||||
|
||||
## 快速参考
|
||||
## CLI 命令
|
||||
|
||||
| 命令 | 一句话说明 |
|
||||
|------|-----------|
|
||||
| `/pf-deep <key>` | 对单篇论文进行 figure-by-figure 深度精读 |
|
||||
| `/pf-paper <key>` | 加载论文 OCR 文本,进入问答模式 |
|
||||
| `paperforge ocr` | 处理所有标记 `do_ocr: true` 的文献 |
|
||||
| `paperforge sync` | 检测 Zotero 新条目,直接生成正式笔记 |
|
||||
| `paperforge status` | 检查安装状态、配置、路径连通性 |
|
||||
### `paperforge sync`
|
||||
|
||||
---
|
||||
|
||||
## 工作流参考
|
||||
|
||||
典型使用顺序(首次安装后):
|
||||
|
||||
### 第一步:同步文献
|
||||
```bash
|
||||
paperforge sync
|
||||
paperforge sync # 完整同步
|
||||
paperforge sync --dry-run # 预览
|
||||
paperforge sync --rebuild-index # 强制重建索引
|
||||
paperforge sync --json # JSON 输出
|
||||
```
|
||||
- 检测 Zotero JSON 中的新条目
|
||||
- 生成正式笔记 `<resources_dir>/<literature_dir>/<domain>/<key> - Title.md`
|
||||
|
||||
### 第二步:标记 OCR
|
||||
在 Obsidian 中打开 formal note 文件,将 `do_ocr: false` 改为 `do_ocr: true`。
|
||||
### `paperforge ocr`
|
||||
|
||||
或使用命令行批量修改(高级用户):
|
||||
```bash
|
||||
# 示例:批量标记某领域的所有文献
|
||||
grep -l "do_ocr: false" <resources_dir>/<literature_dir>/骨科/*.md | xargs sed -i 's/do_ocr: false/do_ocr: true/'
|
||||
paperforge ocr # 处理队列
|
||||
paperforge ocr --key ABCDEFG # 处理指定文献
|
||||
paperforge ocr --diagnose # 诊断模式
|
||||
paperforge ocr --json # JSON 输出
|
||||
```
|
||||
|
||||
### 第三步:运行 OCR
|
||||
### `paperforge status`
|
||||
|
||||
```bash
|
||||
paperforge ocr
|
||||
paperforge status # 完整状态
|
||||
paperforge status --json # JSON 输出
|
||||
```
|
||||
- 上传 PDF 到 PaddleOCR API
|
||||
- 提取全文文本和图表
|
||||
- 输出到 `<system_dir>/PaperForge/ocr/<key>/`
|
||||
|
||||
### 第四步:标记精读
|
||||
在 formal note 中将 `analyze: false` 改为 `analyze: true`。
|
||||
### `paperforge doctor`
|
||||
|
||||
### 第五步:执行精读
|
||||
在 Agent 对话中输入:
|
||||
```bash
|
||||
paperforge doctor # 验证安装配置
|
||||
paperforge doctor --json # JSON 输出
|
||||
```
|
||||
/pf-deep ABCDEFG
|
||||
|
||||
### `paperforge repair`
|
||||
|
||||
```bash
|
||||
paperforge repair # 扫描分歧(dry-run)
|
||||
paperforge repair --fix # 修复
|
||||
paperforge repair --fix-paths # 修复 PDF 路径
|
||||
paperforge repair --json # JSON 输出
|
||||
```
|
||||
Agent 自动:
|
||||
1. 检查 OCR 状态和 formal note
|
||||
2. 生成 `## 精读` 骨架
|
||||
3. 逐阶段填写精读内容(Pass 1/2/3)
|
||||
4. 验证结构完整性
|
||||
|
||||
---
|
||||
### `paperforge deep-reading`
|
||||
|
||||
## 命令详情
|
||||
```bash
|
||||
paperforge deep-reading # 查看精读队列
|
||||
paperforge deep-reading --verbose # 含修复指令
|
||||
```
|
||||
|
||||
各命令的详细文档见对应文件:
|
||||
### `paperforge embed`
|
||||
|
||||
- [`command/pf-deep.md`](../command/pf-deep.md) — 深度精读
|
||||
- [`command/pf-paper.md`](../command/pf-paper.md) — 快速摘要
|
||||
- [`command/pf-ocr.md`](../command/pf-ocr.md) — OCR 提取
|
||||
- [`command/pf-sync.md`](../command/pf-sync.md) — 文献同步
|
||||
- [`command/pf-status.md`](../command/pf-status.md) — 状态检查
|
||||
```bash
|
||||
paperforge embed build # 构建向量索引
|
||||
paperforge embed build --resume # 续建
|
||||
paperforge embed status # 查看状态
|
||||
paperforge embed stop # 停止构建
|
||||
```
|
||||
|
||||
---
|
||||
### `paperforge memory`
|
||||
|
||||
## 平台说明
|
||||
```bash
|
||||
paperforge memory build # 构建 memory DB
|
||||
paperforge memory status # 查看状态
|
||||
```
|
||||
|
||||
### OpenCode
|
||||
### `paperforge search`
|
||||
|
||||
- Agent 命令以 `/pf-` 前缀输入,支持文件附件
|
||||
- `/pf-deep` 和 `/pf-paper` 需要 OCR 完成的 PDF 作为上下文
|
||||
- Agent 使用 `paperforge paths --json` 获取 Vault 路径配置
|
||||
- 多篇文章并行精读时使用 `Task` tool 启动 subagent
|
||||
```bash
|
||||
paperforge search "<query>" --json
|
||||
paperforge search "PEMF" --domain 骨科 --ocr done --year-from 2020
|
||||
```
|
||||
|
||||
### Codex
|
||||
### `paperforge runtime-health`
|
||||
|
||||
> **Future**:Codex 平台支持计划开发中。预计采用类似的 `/pf-*` 命令前缀,通过 API 调用 PaperForge CLI。
|
||||
|
||||
### Claude Code
|
||||
|
||||
> **Future**:Claude Code 平台支持计划开发中。预计通过 `@paperforge` 提及或专用工具调用实现集成。
|
||||
```bash
|
||||
paperforge runtime-health --json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [AGENTS.md](../AGENTS.md) — 完整使用指南、架构说明、常见问题
|
||||
- [docs/INSTALLATION.md](INSTALLATION.md) — 安装步骤
|
||||
- [docs/MIGRATION-v1.2.md](MIGRATION-v1.2.md) — v1.1 → v1.2 迁移指南
|
||||
- [docs/ARCHITECTURE.md](ARCHITECTURE.md) — 系统架构与设计决策
|
||||
|
||||
---
|
||||
|
||||
*PaperForge Lite v1.2 | 命令参考文档*
|
||||
- [使用教程](getting-started.md)
|
||||
- [故障排除](troubleshooting.md)
|
||||
- [更新指南](update-upgrade.md)
|
||||
|
|
|
|||
90
docs/getting-started.md
Normal file
90
docs/getting-started.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# PaperForge 使用教程
|
||||
|
||||
> 面向终端用户。如果你刚安装完 PaperForge,从这里开始。
|
||||
|
||||
---
|
||||
|
||||
## 1. 安装后检查
|
||||
|
||||
1. Obsidian 已打开当前 Vault
|
||||
2. PaperForge 插件已在设置中启用
|
||||
3. Python 运行时已确认(设置面板 --> 验证)
|
||||
|
||||
---
|
||||
|
||||
## 2. 配置 Better BibTeX 自动导出
|
||||
|
||||
1. 打开 Zotero
|
||||
2. 对你要同步的文献库或分类右键 -> `导出...`
|
||||
3. 格式选:**Better BibTeX JSON**
|
||||
4. 勾选 **"Keep updated"**
|
||||
5. 保存到:`{你的Vault路径}/<system_dir>/PaperForge/exports/`
|
||||
|
||||
> system_dir 默认是 `System`,例如保存到 `MyVault/System/PaperForge/exports/library.json`
|
||||
|
||||
---
|
||||
|
||||
## 3. 第一次同步
|
||||
|
||||
打开 Dashboard(`Ctrl+P` -> `PaperForge: Open Main Panel`),点击 **Sync Library**。
|
||||
|
||||
或者终端执行:
|
||||
```bash
|
||||
paperforge sync
|
||||
```
|
||||
|
||||
完成后,正式文献笔记出现在 `Resources/Literature/<领域>/` 下。
|
||||
|
||||
---
|
||||
|
||||
## 4. 触发 OCR
|
||||
|
||||
在正式笔记的 frontmatter 里把 `do_ocr` 改为 `true`,然后运行:
|
||||
|
||||
```bash
|
||||
paperforge ocr
|
||||
```
|
||||
|
||||
OCR 结果保存在 `System/PaperForge/ocr/<key>/` 下,包括全文文本和图表。
|
||||
|
||||
---
|
||||
|
||||
## 5. 深度精读
|
||||
|
||||
OCR 完成后,把 formal note 的 `analyze` 改为 `true`。
|
||||
|
||||
确认就绪:
|
||||
```bash
|
||||
paperforge deep-reading
|
||||
```
|
||||
|
||||
在 Agent 中输入 `/pf-deep <zotero_key>`,Agent 会执行 Keshav 三阶段精读,结果直接写入 formal note 的 `## 精读` 区域。
|
||||
|
||||
---
|
||||
|
||||
## 6. 文献问答
|
||||
|
||||
不强制 OCR。直接输入 `/pf-paper <zotero_key>` 进入交互式问答。
|
||||
|
||||
---
|
||||
|
||||
## 7. 日常路径
|
||||
|
||||
```
|
||||
添加文献到 Zotero
|
||||
-> BBT 自动导出到 exports/
|
||||
-> 点 Sync
|
||||
-> 标记 do_ocr: true
|
||||
-> 运行 OCR
|
||||
-> 标记 analyze: true
|
||||
-> /pf-deep <key>
|
||||
-> /pf-paper <key>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关文档
|
||||
|
||||
- [命令参考](COMMANDS.md) — 所有 CLI 和 Agent 命令速查
|
||||
- [故障排除](troubleshooting.md) — 常见问题
|
||||
- [更新指南](update-upgrade.md) — 如何升级 PaperForge
|
||||
74
docs/maintainer-guide.md
Normal file
74
docs/maintainer-guide.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# PaperForge 维护者指南
|
||||
|
||||
> 面向贡献者和维护者。
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
paperforge/
|
||||
├── core/ PFResult 契约层
|
||||
├── adapters/ BBT 解析、frontmatter、路径
|
||||
├── services/ SyncService 编排
|
||||
├── worker/ OCR、sync、status、repair
|
||||
├── memory/ SQLite、ChromaDB、快照
|
||||
├── commands/ CLI 分发
|
||||
├── setup/ 安装向导
|
||||
├── schema/ field_registry.yaml
|
||||
├── doctor/ 字段校验
|
||||
├── plugin/ Obsidian 插件
|
||||
│ ├── main.js UI + 快照读取
|
||||
│ └── src/testable.js 提取的纯函数
|
||||
└── skills/paperforge/ Agent skill 文件
|
||||
```
|
||||
|
||||
## 架构原则
|
||||
|
||||
1. Python 写 canonical 快照,JS 只读
|
||||
2. CLI 是所有命令的真相源
|
||||
3. `paperforge.json` 是路径真相源
|
||||
4. `formal-library.json` 是文献索引真相源
|
||||
|
||||
## 版本号
|
||||
|
||||
版本只在 `paperforge/__init__.py` 定义。升版本:
|
||||
|
||||
```bash
|
||||
python scripts/bump.py patch
|
||||
python scripts/bump.py minor
|
||||
```
|
||||
|
||||
自动更新:`__init__.py`、`manifest.json`、`versions.json`、git tag。
|
||||
|
||||
## 发布
|
||||
|
||||
```bash
|
||||
python scripts/bump.py patch
|
||||
git push && git push --tags
|
||||
gh release create vX.Y.Z \
|
||||
--title "vX.Y.Z" \
|
||||
--notes "说明" \
|
||||
paperforge/plugin/main.js \
|
||||
paperforge/plugin/styles.css \
|
||||
paperforge/plugin/manifest.json \
|
||||
paperforge/plugin/versions.json
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
ruff check --fix paperforge/ && ruff format paperforge/
|
||||
python -m pytest tests/unit/ tests/cli/ -v --tb=short
|
||||
cd paperforge/plugin && npx vitest run
|
||||
```
|
||||
|
||||
## i18n
|
||||
|
||||
插件文案在 `paperforge/plugin/main.js` 的 `LANG` 对象中。中文 keys 在 `LANG.zh`,英文在 `LANG.en`。代码用 `t('key')` 读取。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [架构](ARCHITECTURE.md)
|
||||
- [命令参考](COMMANDS.md)
|
||||
- [AGENTS.md](../AGENTS.md)
|
||||
67
docs/troubleshooting.md
Normal file
67
docs/troubleshooting.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# PaperForge 故障排除
|
||||
|
||||
---
|
||||
|
||||
## 插件无法加载
|
||||
|
||||
确认 `.obsidian/plugins/paperforge/` 下有 `main.js`、`manifest.json`、`styles.css`。
|
||||
|
||||
如果从旧版本升级:删除整个 `paperforge` 插件文件夹,通过社区插件浏览器重新安装。
|
||||
|
||||
打开开发者控制台(`Ctrl+Shift+I`)查看红色错误信息。
|
||||
|
||||
---
|
||||
|
||||
## "Sync Runtime" 不更新版本
|
||||
|
||||
插件可能调用了和终端不同的 Python。检查设置面板中的 Python Path。
|
||||
|
||||
如果仍不行,使用 `--no-cache-dir` 避开 pip 缓存。
|
||||
|
||||
确认 `https://github.com/LLLin000/PaperForge` 可访问。
|
||||
|
||||
---
|
||||
|
||||
## OCR 一直 pending
|
||||
|
||||
1. 确认 `.env` 中有 `PADDLEOCR_API_TOKEN`
|
||||
2. 运行 `paperforge ocr --diagnose` 检查接口连通性
|
||||
3. 如果 PDF 路径有问题,运行 `paperforge repair --fix-paths`
|
||||
|
||||
---
|
||||
|
||||
## 同步后没有生成笔记
|
||||
|
||||
1. Zotero 的 Better BibTeX 自动导出配好了吗?JSON 文件在 `exports/` 下?
|
||||
2. 运行 `paperforge doctor` 定位失败步骤
|
||||
|
||||
---
|
||||
|
||||
## /pf-deep 无反应
|
||||
|
||||
前置条件:`ocr_status: done` 且 `analyze: true`。
|
||||
|
||||
确认队列就绪:
|
||||
```bash
|
||||
paperforge deep-reading --verbose
|
||||
```
|
||||
|
||||
Agent 必须能访问 vault,确认 Agent 配置路径正确。
|
||||
|
||||
---
|
||||
|
||||
## Base 视图中 pdf_path 显示绝对路径
|
||||
|
||||
Obsidian 渲染问题,数据实际是相对路径。不影响功能。
|
||||
|
||||
---
|
||||
|
||||
## 命令参考
|
||||
|
||||
```bash
|
||||
paperforge status # 查看系统状态
|
||||
paperforge doctor # 验证安装配置
|
||||
paperforge repair --fix # 修复状态分歧
|
||||
```
|
||||
|
||||
更多见 [命令参考](COMMANDS.md)。
|
||||
32
docs/update-upgrade.md
Normal file
32
docs/update-upgrade.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# 更新与升级
|
||||
|
||||
---
|
||||
|
||||
## 插件更新
|
||||
|
||||
Obsidian 社区插件浏览器自动更新。BRAT 安装用户同样自动检测更新。
|
||||
|
||||
## Python 包更新
|
||||
|
||||
```bash
|
||||
paperforge update
|
||||
# 或
|
||||
pip install --upgrade paperforge
|
||||
```
|
||||
|
||||
## 备份
|
||||
|
||||
更新前建议备份:
|
||||
- `.env`(API key)
|
||||
- `Resources/` 目录(文献笔记)
|
||||
- `System/PaperForge/ocr/`(OCR 结果)
|
||||
|
||||
## 迁移指南
|
||||
|
||||
大版本升级详见:
|
||||
- [MIGRATION-v1.2.md](MIGRATION-v1.2.md) — v1.1 -> v1.2
|
||||
- [MIGRATION-v1.4.md](MIGRATION-v1.4.md) — v1.3 -> v1.4
|
||||
|
||||
---
|
||||
|
||||
相关问题见 [故障排除](troubleshooting.md)。
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"vault": "<VAULT>",
|
||||
"worker_script": "<VAULT>/System/PaperForge/paperforge/worker",
|
||||
"ld_deep_script": "<VAULT>/.opencode/skills/literature-qa/scripts/ld_deep.py"
|
||||
"pf_deep_script": "<VAULT>/.opencode/skills/paperforge/scripts/pf_deep.py"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ COMMANDS = {
|
|||
"purpose": "Look up one paper's full status and recommended next action",
|
||||
},
|
||||
"search": {
|
||||
"usage": "paperforge search <query> --json [--collection NAME] [--domain NAME] [--ocr done|pending] [--year-from N] [--year-to N] [--limit N]",
|
||||
"purpose": "Full-text search with optional collection/domain/lifecycle filters",
|
||||
"usage": "paperforge search <query> --json [--domain NAME] [--ocr done|pending|failed|processing] [--year-from N] [--year-to N] [--limit N] [--lifecycle indexed|pdf_ready|fulltext_ready|deep_read_done]",
|
||||
"purpose": "Full-text search with optional domain and workflow-state filters",
|
||||
},
|
||||
"retrieve": {
|
||||
"usage": "paperforge retrieve <query> --json [--limit N]",
|
||||
"purpose": "Search OCR fulltext chunks for evidence paragraphs (coming soon)",
|
||||
"purpose": "Search OCR fulltext chunks for evidence paragraphs",
|
||||
},
|
||||
"deep": {
|
||||
"usage": "/pf-deep <zotero_key>",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -73,15 +74,27 @@ def run(args: argparse.Namespace) -> int:
|
|||
import signal
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as exc:
|
||||
result = PFResult(
|
||||
ok=False,
|
||||
command="embed stop",
|
||||
version=PF_VERSION,
|
||||
error=PFError(code=ErrorCode.INTERNAL_ERROR, message=f"Failed to stop embed build: {exc}"),
|
||||
data={"state": "running", "pid": pid},
|
||||
)
|
||||
if args.json:
|
||||
print(result.to_json())
|
||||
else:
|
||||
print(result.error.message, file=sys.stderr)
|
||||
return 1
|
||||
mark_vector_build_state(vault, status="stopping", message="Stop requested")
|
||||
result = PFResult(ok=True, command="embed stop", version=PF_VERSION,
|
||||
data={"state": "stopping" if pid else "idle"})
|
||||
result = PFResult(ok=True, command="embed stop", version=PF_VERSION, data={"state": "stopping", "pid": pid})
|
||||
else:
|
||||
result = PFResult(ok=True, command="embed stop", version=PF_VERSION, data={"state": "idle"})
|
||||
if args.json:
|
||||
print(result.to_json())
|
||||
else:
|
||||
print("Stop requested." if pid else "No active build.")
|
||||
print("Stop requested." if result.data["state"] == "stopping" else "No active build.")
|
||||
return 0
|
||||
|
||||
# Build
|
||||
|
|
|
|||
|
|
@ -331,7 +331,6 @@ def paperforge_paths(
|
|||
# ── v2.2: canonical locations below paperforge/ ──
|
||||
"config": paperforge / "config" / "domain-collections.json",
|
||||
"index": paperforge / "indexes" / "formal-library.json",
|
||||
"memory_db": paperforge / "indexes" / "paperforge.db",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,14 @@ def read_json(path: Path):
|
|||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def write_json_atomic(path: Path, data) -> None:
|
||||
"""Write JSON atomically using a temp file and replace."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(f"{path.suffix}.tmp")
|
||||
tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
tmp.replace(path)
|
||||
|
||||
|
||||
def write_json(path: Path, data) -> None:
|
||||
"""Write data as JSON, creating parent directories as needed."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
write_json_atomic(path, data)
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ from paperforge.config import paperforge_paths
|
|||
def get_memory_db_path(vault: Path) -> Path:
|
||||
"""Return the absolute path to paperforge.db."""
|
||||
paths = paperforge_paths(vault)
|
||||
db_path = paths.get("memory_db")
|
||||
if not db_path:
|
||||
raise FileNotFoundError("memory_db path not configured")
|
||||
return db_path
|
||||
index_path = paths.get("index")
|
||||
if not index_path:
|
||||
raise FileNotFoundError("index path not configured")
|
||||
return index_path.parent / "paperforge.db"
|
||||
|
||||
|
||||
def get_connection(db_path: Path, read_only: bool = False) -> sqlite3.Connection:
|
||||
|
|
|
|||
|
|
@ -110,11 +110,11 @@ def _check_index(vault: Path) -> dict:
|
|||
if version:
|
||||
return _layer("ok", [f"Schema version: {version['value']}"])
|
||||
return _layer("degraded", ["Schema version unknown"],
|
||||
"Rebuild memory DB", "paperforge memory build --force")
|
||||
"Rebuild memory DB", "paperforge memory build")
|
||||
except Exception as e:
|
||||
return _layer("blocked", [f"DB query failed: {e}"],
|
||||
"Rebuild memory DB",
|
||||
"paperforge memory build --force")
|
||||
"paperforge memory build")
|
||||
|
||||
|
||||
def _check_vector(vault: Path) -> dict:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from datetime import datetime, timezone
|
|||
from pathlib import Path
|
||||
|
||||
from paperforge.config import paperforge_paths
|
||||
from paperforge.core.io import write_json_atomic
|
||||
|
||||
|
||||
def _snapshot_dir(vault: Path) -> Path:
|
||||
|
|
@ -31,7 +32,7 @@ def write_memory_runtime(vault: Path, *, paper_count_db: int,
|
|||
"fts_ready": fts_ready,
|
||||
}
|
||||
path = _snapshot_dir(vault) / "memory-runtime-state.json"
|
||||
path.write_text(json.dumps(snap, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
write_json_atomic(path, snap)
|
||||
|
||||
|
||||
def write_vector_runtime(vault: Path, *, enabled: bool, mode: str, model: str,
|
||||
|
|
@ -53,9 +54,9 @@ def write_vector_runtime(vault: Path, *, enabled: bool, mode: str, model: str,
|
|||
"build_state": build_state or {},
|
||||
}
|
||||
path = _snapshot_dir(vault) / "vector-runtime-state.json"
|
||||
path.write_text(json.dumps(snap, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
write_json_atomic(path, snap)
|
||||
|
||||
|
||||
def write_runtime_health(vault: Path, health_data: dict) -> None:
|
||||
path = _snapshot_dir(vault) / "runtime-health.json"
|
||||
path.write_text(json.dumps(health_data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
write_json_atomic(path, health_data)
|
||||
|
|
|
|||
|
|
@ -10,8 +10,38 @@ const memoryState = (() => {
|
|||
const path = require('path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
function readPathConfig(vaultPath) {
|
||||
const pfPath = path.join(vaultPath, 'paperforge.json');
|
||||
const defaults = {
|
||||
system_dir: 'System',
|
||||
resources_dir: 'Resources',
|
||||
literature_dir: 'Literature',
|
||||
base_dir: 'Bases',
|
||||
};
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(pfPath)) {
|
||||
return { ...defaults, _warning: 'paperforge.json not found; using defaults' };
|
||||
}
|
||||
const raw = fs.readFileSync(pfPath, 'utf-8');
|
||||
const data = JSON.parse(raw);
|
||||
const vc = data.vault_config || {};
|
||||
return {
|
||||
system_dir: vc.system_dir || data.system_dir || defaults.system_dir,
|
||||
resources_dir: vc.resources_dir || data.resources_dir || defaults.resources_dir,
|
||||
literature_dir: vc.literature_dir || data.literature_dir || defaults.literature_dir,
|
||||
base_dir: vc.base_dir || data.base_dir || defaults.base_dir,
|
||||
_warning: null,
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn('PaperForge: Failed to read paperforge.json, using defaults', e);
|
||||
return { ...defaults, _warning: 'paperforge.json invalid; using defaults' };
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVaultPaths(vaultPath) {
|
||||
const systemDir = path.join(vaultPath, 'System', 'PaperForge');
|
||||
const cfg = readPathConfig(vaultPath);
|
||||
const systemDir = path.join(vaultPath, cfg.system_dir, 'PaperForge');
|
||||
return {
|
||||
vault: vaultPath,
|
||||
systemDir,
|
||||
|
|
@ -22,8 +52,11 @@ const memoryState = (() => {
|
|||
vectorStatePath: path.join(systemDir, 'indexes', 'vector-runtime-state.json'),
|
||||
healthStatePath: path.join(systemDir, 'indexes', 'runtime-health.json'),
|
||||
buildStatePath: path.join(systemDir, 'indexes', 'vector-build-state.json'),
|
||||
exportsDir: path.join(systemDir, 'exports'),
|
||||
ocrDir: path.join(systemDir, 'ocr'),
|
||||
pluginDataPath: path.join(vaultPath, '.obsidian', 'plugins', 'paperforge', 'data.json'),
|
||||
pfJsonPath: path.join(vaultPath, 'paperforge.json'),
|
||||
configWarning: cfg._warning,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +132,7 @@ const memoryState = (() => {
|
|||
return _cachedPython;
|
||||
}
|
||||
}
|
||||
var sysCandidates = [{path:'python',extraArgs:[]},{path:'python3',extraArgs:[]}];
|
||||
var sysCandidates = [{path:'py',extraArgs:['-3']},{path:'python',extraArgs:[]},{path:'python3',extraArgs:[]}];
|
||||
for (var j = 0; j < sysCandidates.length; j++) {
|
||||
try {
|
||||
var c = sysCandidates[j];
|
||||
|
|
@ -170,6 +203,7 @@ function resolvePythonExecutable(vaultPath, settings, _fs, _execFileSync) {
|
|||
}
|
||||
|
||||
const systemCandidates = [
|
||||
{ path: "py", extraArgs: ["-3"] },
|
||||
{ path: "python", extraArgs: [] },
|
||||
{ path: "python3", extraArgs: [] },
|
||||
];
|
||||
|
|
@ -242,7 +276,7 @@ function buildRuntimeInstallCommand(pythonExe, version, extraArgs) {
|
|||
const gitUrl = `git+https://github.com/LLLin000/PaperForge.git@v${version}`;
|
||||
const pypiArgs = [...extraArgs, "-m", "pip", "install", "--upgrade", pypiPkg];
|
||||
const gitArgs = [...extraArgs, "-m", "pip", "install", "--upgrade", gitUrl];
|
||||
return { cmd: pythonExe, pypiArgs, gitArgs, timeout: 120000 };
|
||||
return { cmd: pythonExe, url: gitUrl.replace('@v', '@'), args: gitArgs, pypiArgs, gitArgs, timeout: 120000 };
|
||||
}
|
||||
|
||||
function parseRuntimeStatus(err, stdout, stderr) {
|
||||
|
|
@ -583,8 +617,9 @@ Object.assign(LANG.en, {
|
|||
validate_notes: 'Notes directory is required',
|
||||
validate_index: 'Index directory is required',
|
||||
validate_base: 'Base directory is required',
|
||||
validate_key: 'PaddleOCR API key is required',
|
||||
validate_zotero: 'Zotero data directory is required',
|
||||
validate_key: 'PaddleOCR API key (optional, needed for OCR)',
|
||||
validate_zotero: 'Zotero data directory (optional, needed for PDF linking)',
|
||||
optional_later: '(can be set later in Settings)',
|
||||
validate_system: 'System directory is required',
|
||||
notice_python_missing: 'Python was not detected. Install Python 3.10+ and add it to PATH.',
|
||||
api_key_set: 'Entered',
|
||||
|
|
@ -736,6 +771,7 @@ Object.assign(LANG.zh, {
|
|||
ocr_privacy_warning: 'OCR 会将 PDF 上传到 PaddleOCR API 进行处理。请不要上传包含敏感信息或无法外传的文献。',
|
||||
ocr_understand: '我了解,继续',
|
||||
install_validating: '正在校验安装环境…',
|
||||
optional_later: '(稍后可在设置中补充)',
|
||||
install_bootstrapping: '未检测到 PaperForge Python 包,正在自动安装…',
|
||||
wizard_safety: '安全说明:如果你选择的目录里已经有文件,安装向导会保留已有内容,只补充缺失的 PaperForge 文件和目录。',
|
||||
|
||||
|
|
@ -1714,6 +1750,37 @@ class PaperForgeStatusView extends ItemView {
|
|||
const action = ACTIONS.find(a => a.id === 'paperforge-sync');
|
||||
if (action) this._runAction(action, globalSyncBtn);
|
||||
});
|
||||
|
||||
const globalOcrBtn = btnsRow.createEl('button', { cls: 'paperforge-contextual-btn' });
|
||||
globalOcrBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u229E' });
|
||||
globalOcrBtn.createEl('span', { text: 'Run OCR' });
|
||||
globalOcrBtn.addEventListener('click', () => {
|
||||
const action = ACTIONS.find(a => a.id === 'paperforge-ocr');
|
||||
if (action) this._runAction(action, globalOcrBtn);
|
||||
});
|
||||
|
||||
const globalDoctorBtn = btnsRow.createEl('button', { cls: 'paperforge-contextual-btn' });
|
||||
globalDoctorBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u2695' });
|
||||
globalDoctorBtn.createEl('span', { text: 'Run Doctor' });
|
||||
globalDoctorBtn.addEventListener('click', () => {
|
||||
const action = ACTIONS.find(a => a.id === 'paperforge-doctor');
|
||||
if (action) this._runAction(action, globalDoctorBtn);
|
||||
});
|
||||
|
||||
const globalStatusBtn = btnsRow.createEl('button', { cls: 'paperforge-contextual-btn' });
|
||||
globalStatusBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u2139' });
|
||||
globalStatusBtn.createEl('span', { text: 'Refresh Status' });
|
||||
globalStatusBtn.addEventListener('click', () => {
|
||||
this._fetchStats();
|
||||
});
|
||||
|
||||
const globalRepairBtn = btnsRow.createEl('button', { cls: 'paperforge-contextual-btn' });
|
||||
globalRepairBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u21BA' });
|
||||
globalRepairBtn.createEl('span', { text: 'Repair Issues' });
|
||||
globalRepairBtn.addEventListener('click', () => {
|
||||
const action = ACTIONS.find(a => a.id === 'paperforge-repair');
|
||||
if (action) this._runAction(action, globalRepairBtn);
|
||||
});
|
||||
}
|
||||
|
||||
/* ── System Status Row helper ── */
|
||||
|
|
@ -3137,15 +3204,32 @@ class PaperForgeSettingTab extends PluginSettingTab {
|
|||
});
|
||||
}
|
||||
|
||||
// --- Section: Memory Layer ---
|
||||
containerEl.createEl('h3', { text: 'Memory Layer' });
|
||||
// --- Section: Advanced (Memory Layer + Vector DB, collapsed by default) ---
|
||||
const advHeader = containerEl.createEl('div', { cls: 'paperforge-collapsible-header' });
|
||||
advHeader.style.cssText = 'cursor:pointer; display:flex; align-items:center; gap:8px; padding:8px 0; user-select:none;';
|
||||
const advArrow = advHeader.createEl('span', { text: '\u25B6', cls: 'paperforge-collapsible-arrow' });
|
||||
advArrow.style.cssText = 'display:inline-block; transition:transform 0.2s; font-size:10px; transform:rotate(90deg);';
|
||||
advHeader.createEl('h3', { text: 'Advanced', cls: 'paperforge-collapsible-title' });
|
||||
advHeader.createEl('span', { text: 'Memory + Vector DB + Embedding', cls: 'paperforge-collapsible-sub' });
|
||||
advHeader.lastChild.style.cssText = 'font-size:11px; color:var(--text-muted); margin-left:8px;';
|
||||
|
||||
const memoryDescEl = containerEl.createEl('div', { cls: 'paperforge-desc-box' });
|
||||
const advContent = containerEl.createEl('div', { cls: 'paperforge-collapsible-content' });
|
||||
advContent.style.display = 'none';
|
||||
|
||||
advHeader.addEventListener('click', () => {
|
||||
const collapsed = advContent.style.display === 'none';
|
||||
advContent.style.display = collapsed ? '' : 'none';
|
||||
advArrow.style.transform = collapsed ? 'rotate(0deg)' : 'rotate(90deg)';
|
||||
});
|
||||
|
||||
// Memory Layer section (inside Advanced)
|
||||
advContent.createEl('h4', { text: 'Memory Layer' });
|
||||
|
||||
const memoryDescEl = advContent.createEl('div', { cls: 'paperforge-desc-box' });
|
||||
memoryDescEl.style.cssText = 'padding:8px 12px; margin:0 0 12px; background:var(--background-secondary); border-radius:4px; font-size:12px; color:var(--text-muted); line-height:1.5;';
|
||||
memoryDescEl.setText(t('feat_memory_desc'));
|
||||
|
||||
// Always-on SQLite status display
|
||||
const statusRow = containerEl.createEl('div', { cls: 'paperforge-memory-status' });
|
||||
const statusRow = advContent.createEl('div', { cls: 'paperforge-memory-status' });
|
||||
statusRow.style.cssText = 'display:flex; align-items:center; padding:8px 12px; margin:8px 0; background:var(--background-secondary); border-radius:4px;';
|
||||
|
||||
const vp = this.app.vault.adapter.basePath;
|
||||
|
|
@ -3159,7 +3243,7 @@ class PaperForgeSettingTab extends PluginSettingTab {
|
|||
}
|
||||
this._renderMemoryStatusText(statusRow, this._memoryStatusText, this._lastSyncTime);
|
||||
|
||||
this._renderVectorSection(containerEl);
|
||||
this._renderVectorSection(advContent);
|
||||
}
|
||||
|
||||
_renderVectorSection(containerEl) {
|
||||
|
|
@ -4363,8 +4447,8 @@ class PaperForgeSetupModal extends Modal {
|
|||
'--base-dir', s.base_dir.trim(),
|
||||
'--agent', s.agent_platform || 'opencode',
|
||||
];
|
||||
setupArgs.push('--zotero-data', s.zotero_data_dir.trim());
|
||||
setupArgs.push('--paddleocr-key', s.paddleocr_api_key.trim());
|
||||
if (s.zotero_data_dir && s.zotero_data_dir.trim()) setupArgs.push('--zotero-data', s.zotero_data_dir.trim());
|
||||
if (s.paddleocr_api_key && s.paddleocr_api_key.trim()) setupArgs.push('--paddleocr-key', s.paddleocr_api_key.trim());
|
||||
|
||||
try {
|
||||
let hasPaperforge = true;
|
||||
|
|
@ -4466,8 +4550,8 @@ class PaperForgeSetupModal extends Modal {
|
|||
if (!s.resources_dir || !s.resources_dir.trim()) errors.push(t('validate_resources'));
|
||||
if (!s.literature_dir || !s.literature_dir.trim()) errors.push(t('validate_notes'));
|
||||
if (!s.base_dir || !s.base_dir.trim()) errors.push(t('validate_base'));
|
||||
if (!s.paddleocr_api_key || !s.paddleocr_api_key.trim()) errors.push(t('validate_key'));
|
||||
if (!s.zotero_data_dir || !s.zotero_data_dir.trim()) errors.push(t('validate_zotero'));
|
||||
if (!s.paddleocr_api_key || !s.paddleocr_api_key.trim()) this._log(' ! ' + t('validate_key') + ' ' + t('optional_later'));
|
||||
if (!s.zotero_data_dir || !s.zotero_data_dir.trim()) this._log(' ! ' + t('validate_zotero') + ' ' + t('optional_later'));
|
||||
return errors;
|
||||
}
|
||||
|
||||
|
|
@ -4618,7 +4702,7 @@ module.exports = class PaperForgePlugin extends Plugin {
|
|||
|
||||
|
||||
/* ── Auto-update PaperForge (deferred — don't slow startup) ── */
|
||||
if (this.settings.auto_update !== false && this.settings.setup_complete) {
|
||||
if (this.settings.auto_update_on_startup === true && this.settings.setup_complete) {
|
||||
setTimeout(() => this._autoUpdate(), 3000);
|
||||
}
|
||||
this._startFilePolling();
|
||||
|
|
@ -4627,14 +4711,22 @@ module.exports = class PaperForgePlugin extends Plugin {
|
|||
(() => {
|
||||
const vp = this.app.vault.adapter.basePath;
|
||||
if (!vp) return;
|
||||
const memSnap = require('path').join(vp, 'System', 'PaperForge', 'indexes', 'memory-runtime-state.json');
|
||||
const runtimePaths = memoryState.resolveVaultPaths(vp);
|
||||
const memSnap = runtimePaths.memoryStatePath;
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync(memSnap)) {
|
||||
// No snapshots — first launch or upgrade. Fire and forget.
|
||||
const py = resolvePythonExecutable(vp, this.settings);
|
||||
const { execFile } = require('node:child_process');
|
||||
const args = [...py.extraArgs, '-m', 'paperforge', '--vault', vp, 'runtime-health', '--json'];
|
||||
execFile(py.path, args, { cwd: vp, timeout: 60000, windowsHide: true }, () => {});
|
||||
const commands = [
|
||||
['runtime-health', '--json'],
|
||||
['memory', 'status', '--json'],
|
||||
['embed', 'status', '--json'],
|
||||
];
|
||||
commands.forEach((cmdArgs) => {
|
||||
const args = [...py.extraArgs, '-m', 'paperforge', '--vault', vp, ...cmdArgs];
|
||||
execFile(py.path, args, { cwd: vp, timeout: 60000, windowsHide: true }, () => {});
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
@ -4692,7 +4784,7 @@ module.exports = class PaperForgePlugin extends Plugin {
|
|||
|
||||
_checkExports(vaultPath, fs, path, exec) {
|
||||
if (this._autoSyncRunning) return;
|
||||
const exportsDir = path.join(vaultPath, 'System', 'PaperForge', 'exports');
|
||||
const exportsDir = memoryState.resolveVaultPaths(vaultPath).exportsDir;
|
||||
if (!fs.existsSync(exportsDir)) return;
|
||||
|
||||
let newestMtime = 0;
|
||||
|
|
@ -4728,7 +4820,7 @@ module.exports = class PaperForgePlugin extends Plugin {
|
|||
try {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const exportsDir = path.join(vaultPath, 'System', 'PaperForge', 'exports');
|
||||
const exportsDir = memoryState.resolveVaultPaths(vaultPath).exportsDir;
|
||||
let newest = 0;
|
||||
fs.readdirSync(exportsDir).forEach(f => {
|
||||
if (!f.endsWith('.json')) return;
|
||||
|
|
@ -4741,7 +4833,7 @@ module.exports = class PaperForgePlugin extends Plugin {
|
|||
|
||||
_checkOcr(vaultPath, fs, path, exec) {
|
||||
if (this._autoSyncRunning) return;
|
||||
const ocrDir = path.join(vaultPath, 'System', 'PaperForge', 'ocr');
|
||||
const ocrDir = memoryState.resolveVaultPaths(vaultPath).ocrDir;
|
||||
if (!fs.existsSync(ocrDir)) return;
|
||||
|
||||
try {
|
||||
|
|
@ -4760,7 +4852,9 @@ module.exports = class PaperForgePlugin extends Plugin {
|
|||
const pyResult = resolvePythonExecutable(vaultPath, this.settings);
|
||||
if (!pyResult.path) { this._autoSyncRunning = false; return; }
|
||||
|
||||
const cmd = `"${pyResult.path}" -m paperforge --vault "${vaultPath}" sync --key "${entry.name}"`;
|
||||
// `paperforge sync` has no `--key` selector. Fall back to a full sync
|
||||
// so OCR state changes still propagate into the canonical index.
|
||||
const cmd = `"${pyResult.path}" -m paperforge --vault "${vaultPath}" sync`;
|
||||
exec(cmd, { timeout: 30000, encoding: 'utf-8' }, () => {
|
||||
this._autoSyncRunning = false;
|
||||
this._memoryStatusText = null;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,56 @@ const fs = require('fs');
|
|||
const path = require('path');
|
||||
const { execFile, spawn } = require('node:child_process');
|
||||
|
||||
function readPathConfig(vaultPath, _fs) {
|
||||
const f = _fs || fs;
|
||||
const pfPath = path.join(vaultPath, 'paperforge.json');
|
||||
const defaults = {
|
||||
system_dir: 'System',
|
||||
resources_dir: 'Resources',
|
||||
literature_dir: 'Literature',
|
||||
base_dir: 'Bases',
|
||||
};
|
||||
|
||||
try {
|
||||
if (!f.existsSync(pfPath)) {
|
||||
return { ...defaults, _warning: 'paperforge.json not found; using defaults' };
|
||||
}
|
||||
const raw = f.readFileSync(pfPath, 'utf-8');
|
||||
const data = JSON.parse(raw);
|
||||
const vc = data.vault_config || {};
|
||||
return {
|
||||
system_dir: vc.system_dir || data.system_dir || defaults.system_dir,
|
||||
resources_dir: vc.resources_dir || data.resources_dir || defaults.resources_dir,
|
||||
literature_dir: vc.literature_dir || data.literature_dir || defaults.literature_dir,
|
||||
base_dir: vc.base_dir || data.base_dir || defaults.base_dir,
|
||||
_warning: null,
|
||||
};
|
||||
} catch {
|
||||
return { ...defaults, _warning: 'paperforge.json invalid; using defaults' };
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRuntimePaths(vaultPath, _fs) {
|
||||
const cfg = readPathConfig(vaultPath, _fs);
|
||||
const systemRoot = path.join(vaultPath, cfg.system_dir, 'PaperForge');
|
||||
return {
|
||||
vault: vaultPath,
|
||||
systemDir: systemRoot,
|
||||
indexesDir: path.join(systemRoot, 'indexes'),
|
||||
logsDir: path.join(systemRoot, 'logs'),
|
||||
dbPath: path.join(systemRoot, 'indexes', 'paperforge.db'),
|
||||
memoryStatePath: path.join(systemRoot, 'indexes', 'memory-runtime-state.json'),
|
||||
vectorStatePath: path.join(systemRoot, 'indexes', 'vector-runtime-state.json'),
|
||||
healthStatePath: path.join(systemRoot, 'indexes', 'runtime-health.json'),
|
||||
buildStatePath: path.join(systemRoot, 'indexes', 'vector-build-state.json'),
|
||||
exportsDir: path.join(systemRoot, 'exports'),
|
||||
ocrDir: path.join(systemRoot, 'ocr'),
|
||||
pluginDataPath: path.join(vaultPath, '.obsidian', 'plugins', 'paperforge', 'data.json'),
|
||||
pfJsonPath: path.join(vaultPath, 'paperforge.json'),
|
||||
configWarning: cfg._warning,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Runtime helpers ──
|
||||
|
||||
function resolvePythonExecutable(vaultPath, settings, _fs, _execFileSync) {
|
||||
|
|
@ -33,6 +83,7 @@ function resolvePythonExecutable(vaultPath, settings, _fs, _execFileSync) {
|
|||
}
|
||||
|
||||
const systemCandidates = [
|
||||
{ path: "py", extraArgs: ["-3"] },
|
||||
{ path: "python", extraArgs: [] },
|
||||
{ path: "python3", extraArgs: [] },
|
||||
];
|
||||
|
|
@ -107,7 +158,7 @@ function buildRuntimeInstallCommand(pythonExe, version, extraArgs) {
|
|||
const gitUrl = `git+https://github.com/LLLin000/PaperForge.git@v${version}`;
|
||||
const pypiArgs = [...extraArgs, "-m", "pip", "install", "--upgrade", pypiPkg];
|
||||
const gitArgs = [...extraArgs, "-m", "pip", "install", "--upgrade", gitUrl];
|
||||
return { cmd: pythonExe, pypiArgs, gitArgs, timeout: 120000 };
|
||||
return { cmd: pythonExe, url: gitUrl.replace('@v', '@'), args: gitArgs, pypiArgs, gitArgs, timeout: 120000 };
|
||||
}
|
||||
|
||||
function parseRuntimeStatus(err, stdout, stderr) {
|
||||
|
|
@ -211,6 +262,8 @@ function shouldRenderVectorReady(vectorDepsOk, embedStatusText) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
readPathConfig,
|
||||
resolveRuntimePaths,
|
||||
resolvePythonExecutable,
|
||||
getPluginVersion,
|
||||
checkRuntimeVersion,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,58 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const {
|
||||
readPathConfig,
|
||||
resolveRuntimePaths,
|
||||
resolvePythonExecutable,
|
||||
getPluginVersion,
|
||||
checkRuntimeVersion,
|
||||
} = await import('../src/testable.js');
|
||||
|
||||
describe('readPathConfig', () => {
|
||||
it('uses vault_config system_dir when present', () => {
|
||||
const mockFs = {
|
||||
existsSync: vi.fn(() => true),
|
||||
readFileSync: vi.fn(() => JSON.stringify({ vault_config: { system_dir: '99_System' } })),
|
||||
};
|
||||
const cfg = readPathConfig('/vault', mockFs);
|
||||
expect(cfg.system_dir).toBe('99_System');
|
||||
expect(cfg._warning).toBeNull();
|
||||
});
|
||||
|
||||
it('falls back to defaults with warning when config is missing', () => {
|
||||
const mockFs = {
|
||||
existsSync: vi.fn(() => false),
|
||||
readFileSync: vi.fn(),
|
||||
};
|
||||
const cfg = readPathConfig('/vault', mockFs);
|
||||
expect(cfg.system_dir).toBe('System');
|
||||
expect(cfg._warning).toContain('using defaults');
|
||||
});
|
||||
|
||||
it('falls back to defaults with warning when config is invalid', () => {
|
||||
const mockFs = {
|
||||
existsSync: vi.fn(() => true),
|
||||
readFileSync: vi.fn(() => '{bad json'),
|
||||
};
|
||||
const cfg = readPathConfig('/vault', mockFs);
|
||||
expect(cfg.system_dir).toBe('System');
|
||||
expect(cfg._warning).toContain('invalid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveRuntimePaths', () => {
|
||||
it('uses configured system_dir for runtime file paths', () => {
|
||||
const mockFs = {
|
||||
existsSync: vi.fn(() => true),
|
||||
readFileSync: vi.fn(() => JSON.stringify({ vault_config: { system_dir: '99_System' } })),
|
||||
};
|
||||
const paths = resolveRuntimePaths('/vault', mockFs);
|
||||
expect(paths.memoryStatePath).toContain('99_System');
|
||||
expect(paths.exportsDir).toContain('99_System');
|
||||
expect(paths.ocrDir).toContain('99_System');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolvePythonExecutable', () => {
|
||||
/** Create injected fs + execFileSync mocks */
|
||||
function mockDeps(existsSyncImpl, execFileSyncImpl) {
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ class SyncService:
|
|||
index_count = 0
|
||||
orphaned = 0
|
||||
flat_cleaned = 0
|
||||
memory_rebuild_error: Exception | None = None
|
||||
|
||||
if not selection_only:
|
||||
from paperforge.worker._domain import load_domain_config
|
||||
|
|
@ -288,12 +289,27 @@ class SyncService:
|
|||
orphaned = self.clean_orphaned_records(exports, paths, json_output=json_output)
|
||||
flat_cleaned = self.clean_flat_notes(paths, json_output=json_output)
|
||||
|
||||
if orphaned > 0 or flat_cleaned > 0:
|
||||
index_count = asset_index.build_index(self.vault, verbose)
|
||||
|
||||
try:
|
||||
from paperforge.memory.builder import build_from_index
|
||||
from paperforge.memory.db import get_memory_db_path
|
||||
|
||||
if get_memory_db_path(self.vault).exists():
|
||||
build_from_index(self.vault)
|
||||
except Exception as exc:
|
||||
memory_rebuild_error = exc
|
||||
logger.error("Failed to rebuild memory DB after sync: %s", exc)
|
||||
|
||||
if not json_output:
|
||||
print(f"index-refresh: {index_count} entries in index")
|
||||
total = sum(1 for _ in paths["literature"].rglob("*.md")) if paths["literature"].exists() else 0
|
||||
print(f"index-refresh: {total} formal note(s) in literature")
|
||||
|
||||
all_errors = list(selection_result.get("errors", []))
|
||||
if memory_rebuild_error is not None:
|
||||
all_errors.append(f"memory rebuild failed: {memory_rebuild_error}")
|
||||
is_ok = selection_result.get("failed", 0) == 0 and len(all_errors) == 0
|
||||
|
||||
result = PFResult(
|
||||
|
|
@ -309,6 +325,18 @@ class SyncService:
|
|||
if _export_code != "ok":
|
||||
result.warnings.append(f"BBT exports: {_export_code}")
|
||||
|
||||
if memory_rebuild_error is not None:
|
||||
result.error = PFError(
|
||||
code=ErrorCode.SYNC_FAILED,
|
||||
message=f"Sync completed but memory rebuild failed: {memory_rebuild_error}",
|
||||
details={"phase": "memory_rebuild", "exception_type": type(memory_rebuild_error).__name__},
|
||||
suggestions=["Run paperforge memory build to rebuild the memory DB from the cleaned canonical index."],
|
||||
)
|
||||
result.next_actions.append({
|
||||
"command": "paperforge memory build",
|
||||
"reason": "Rebuild memory DB so it matches the cleaned canonical index",
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
# ── Legacy passthrough (backward-compat for commands/sync.py) ──
|
||||
|
|
|
|||
|
|
@ -98,7 +98,17 @@ Reading-log 不是事实源。它记录的是**之前的关注点、解读和预
|
|||
|
||||
## 6. 意图路由
|
||||
|
||||
用户输入对应唯一一个 workflow 文件(打开并执行其完整流程):
|
||||
用户输入分两类处理:
|
||||
|
||||
### A. 机械命令(直接执行并解读结果,不走研究 workflow)
|
||||
|
||||
| 用户说 | 动作 |
|
||||
| --- | --- |
|
||||
| `/pf-sync` | 执行 `paperforge sync`,检查结果并解释 |
|
||||
| `/pf-ocr` | 执行 `paperforge ocr`,检查结果并解释 |
|
||||
| `/pf-status` | 执行 `paperforge status --json` 或 `paperforge runtime-health --json`,解读状态 |
|
||||
|
||||
### B. 研究工作流(打开唯一 workflow 文件并执行完整流程)
|
||||
|
||||
| 用户说 | 打开 |
|
||||
| -------------------------------------------------------- | -------------------------------- |
|
||||
|
|
@ -108,10 +118,11 @@ Reading-log 不是事实源。它记录的是**之前的关注点、解读和预
|
|||
| "记一下" "记录阅读" "reading log" "读完这段记一下" | `workflows/reading-log.md` |
|
||||
| "总结会话" "工作记录" "项目记录" "project log" "记决策" | `workflows/project-log.md` |
|
||||
| "提取方法论" "总结规律" "存档写作规律" | `workflows/methodology.md` |
|
||||
| "branch" "代码审查" "feature" "dashboard" "memory layer" "用户反馈" "报错" "安装失败" "Git" "Zotero" "BetterBibTeX" "OCR" "插件" | `workflows/project-engineering.md` |
|
||||
| "branch" "代码审查" "feature" "dashboard" "memory layer" "用户反馈" "报错" "安装失败" "Git" "Zotero" "BetterBibTeX" "插件" | `workflows/project-engineering.md` |
|
||||
| 不确定 / 空输入 | 问用户:搜文献、精读、问答、记笔记、记工作、提方法论? |
|
||||
|
||||
路由后如用户切换意图,重新判断并打开对应 workflow。
|
||||
未知或拼错的 `/pf-*` 命令不要静默掉进 `project-engineering`,必须明确提示用户命令不存在或请澄清意图。
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ $PYTHON "$SKILL_DIR/scripts/pf_deep.py" prepare --key <zotero_key> --vault "$VAU
|
|||
### Step 4: Postprocess(跑校验,修正问题)
|
||||
|
||||
```bash
|
||||
$PYTHON "$SKILL_DIR/scripts/pf_deep.py" postprocess-pass2 "<formal_note_path>" --figures <N> --vault "$VAULT"
|
||||
$PYTHON "$SKILL_DIR/scripts/pf_deep.py" postprocess-pass2 "<formal_note_path>" --figures <N>
|
||||
```
|
||||
|
||||
- 输出 `OK` → 继续 Step 5
|
||||
|
|
|
|||
|
|
@ -21,21 +21,29 @@
|
|||
|
||||
用户可能给 zotero_key、DOI、标题片段、作者+年份。按以下方式查找:
|
||||
|
||||
**优先用 paper-context(一次拿到全部信息):**
|
||||
**如果用户给的是 zotero_key:**
|
||||
|
||||
```bash
|
||||
$PYTHON -m paperforge --vault "$VAULT" paper-context <query> --json
|
||||
$PYTHON -m paperforge --vault "$VAULT" paper-context <zotero_key> --json
|
||||
```
|
||||
|
||||
返回 JSON 包含 paper 元数据、OCR 状态、prior_notes 等。
|
||||
|
||||
**paper-context 无结果时的备选:**
|
||||
**如果用户给的是 DOI、标题片段、作者+年份,先解析成候选:**
|
||||
|
||||
```bash
|
||||
$PYTHON -m paperforge --vault "$VAULT" paper-status "<query>" --json
|
||||
```
|
||||
|
||||
如果 `paper-status` 直接唯一命中,取返回的 `zotero_key` 再调 `paper-context`。
|
||||
|
||||
**paper-status 无法唯一定位时的备选:**
|
||||
|
||||
```bash
|
||||
$PYTHON -m paperforge --vault "$VAULT" search "<query>" --json --limit 5
|
||||
```
|
||||
|
||||
如果多候选,列出让用户选(同 paper-search 的 Step 4-5 格式)。
|
||||
如果多候选,列出让用户选(同 paper-search 的 Step 4-5 格式);用户选定后,再用选中的 `zotero_key` 调 `paper-context`。
|
||||
如果无结果,告知用户并停止。
|
||||
|
||||
### Step 2: 加载文献内容
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
提取以下信息(缺什么就问用户):
|
||||
- **搜索词**:关键词、作者名、年份
|
||||
- **范围**:domain(如"骨科")、collection(如"DC")、不指定=全库
|
||||
- **过滤条件**:OCR 状态(done/pending)、年份范围(--year-from/--year-to)、lifecycle
|
||||
- **范围**:domain(如"骨科")、不指定=全库;如果用户说 collection,只把它当结果展示时的二级筛选条件
|
||||
- **过滤条件**:OCR 状态(done/pending/failed/processing)、年份范围(--year-from/--year-to)、lifecycle
|
||||
|
||||
### Step 2: 执行搜索
|
||||
|
||||
|
|
@ -25,8 +25,8 @@
|
|||
$PYTHON -m paperforge --vault "$VAULT" search <query> --json --limit 15 \
|
||||
[--domain "<domain>"] \
|
||||
[--year-from <N>] [--year-to <N>] \
|
||||
[--ocr <done|pending>] \
|
||||
[--lifecycle <active|archived>]
|
||||
[--ocr <done|pending|failed|processing>] \
|
||||
[--lifecycle <indexed|pdf_ready|fulltext_ready|deep_read_done>]
|
||||
```
|
||||
|
||||
返回 JSON 结构:
|
||||
|
|
@ -46,7 +46,7 @@ $PYTHON -m paperforge --vault "$VAULT" search <query> --json --limit 15 \
|
|||
"collection_path": "...",
|
||||
"ocr_status": "done",
|
||||
"deep_reading_status": "pending",
|
||||
"lifecycle": "active",
|
||||
"lifecycle": "pdf_ready",
|
||||
"has_pdf": true,
|
||||
"rank": "..."
|
||||
}
|
||||
|
|
@ -69,6 +69,7 @@ $PYTHON -m paperforge --vault "$VAULT" paper-context <zotero_key> --json
|
|||
```
|
||||
|
||||
目的:拿到 `ocr_status`、`prior_notes` 数量、`analyze` 状态,帮助用户判断哪些可以直接读。
|
||||
如果用户一开始明确要求 collection 范围,在这一步根据 `collection_path` 做结果后筛选,而不是给 `search` 传不存在的 `--collection` 参数。
|
||||
|
||||
### Step 4: 展示候选清单
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ Agent 按 JSON schema 写入 project-log.jsonl。
|
|||
展示给用户确认后再写入:
|
||||
|
||||
```
|
||||
即将记录到 Project/综述写作/project-log.md:
|
||||
即将记录到 Resources/Projects/综述写作/project-log.md:
|
||||
日期: 2026-05-14
|
||||
类型: session_summary
|
||||
标题: DC 段参数窗审计完成
|
||||
|
|
@ -111,7 +111,7 @@ $PYTHON -m paperforge --vault "$VAULT" project-log --write \
|
|||
$PYTHON -m paperforge --vault "$VAULT" project-log --render --project "<project>"
|
||||
```
|
||||
|
||||
输出到 `Project/<project>/project-log.md`。
|
||||
输出到 `Resources/Projects/<project>/project-log.md`。
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ $PYTHON -m paperforge --vault "$VAULT" reading-log --write <paper_id> \
|
|||
$PYTHON -m paperforge --vault "$VAULT" reading-log --render --project "<project>"
|
||||
```
|
||||
|
||||
输出到 `Project/<project>/reading-log.md`。
|
||||
输出到 `Resources/Projects/<project>/reading-log.md`。
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,46 @@ LOCK_TIMEOUT = 10
|
|||
``filelock.Timeout``."""
|
||||
|
||||
|
||||
def _index_lock(path: Path) -> filelock.FileLock:
|
||||
return filelock.FileLock(path.with_suffix(".json.lock"), timeout=LOCK_TIMEOUT)
|
||||
|
||||
|
||||
def mutate_index(vault: Path, mutator) -> bool:
|
||||
"""Read, mutate, and write the canonical index under one lock."""
|
||||
path = get_index_path(vault)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with _index_lock(path):
|
||||
if not path.exists():
|
||||
return False
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
current = json.load(fh)
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
logger.warning("Corrupt index at %s during mutation: %s", path, exc)
|
||||
return False
|
||||
|
||||
updated = mutator(current)
|
||||
if updated is None:
|
||||
return False
|
||||
|
||||
tmp = tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", delete=False, dir=path.parent)
|
||||
try:
|
||||
json.dump(updated, tmp, ensure_ascii=False, indent=2)
|
||||
tmp.flush()
|
||||
os.fsync(tmp.fileno())
|
||||
tmp.close()
|
||||
os.replace(tmp.name, path)
|
||||
except BaseException:
|
||||
tmp.close()
|
||||
try:
|
||||
os.unlink(tmp.name)
|
||||
except OSError:
|
||||
pass
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Path resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -123,10 +163,7 @@ def atomic_write_index(path: Path, data: dict) -> None:
|
|||
"""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
lock_path = path.with_suffix(".json.lock")
|
||||
lock = filelock.FileLock(lock_path, timeout=LOCK_TIMEOUT)
|
||||
|
||||
with lock:
|
||||
with _index_lock(path):
|
||||
tmp = None
|
||||
try:
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
|
|
@ -581,9 +618,6 @@ def refresh_index_entry(vault: Path, key: str) -> bool:
|
|||
build_index(vault)
|
||||
return False
|
||||
|
||||
# Envelope format — incremental refresh
|
||||
items = existing.get("items", [])
|
||||
|
||||
# Find the export item for the requested key
|
||||
config = load_domain_config(paths)
|
||||
domain_lookup = {entry["export_file"]: entry["domain"] for entry in config["domains"]}
|
||||
|
|
@ -617,19 +651,24 @@ def refresh_index_entry(vault: Path, key: str) -> bool:
|
|||
except Exception:
|
||||
pass # memory DB refresh is best-effort
|
||||
|
||||
replaced = False
|
||||
for i, existing_entry in enumerate(items):
|
||||
if existing_entry.get("zotero_key") == key:
|
||||
items[i] = new_entry
|
||||
replaced = True
|
||||
break
|
||||
if not replaced:
|
||||
items.append(new_entry)
|
||||
def _apply(current):
|
||||
if is_legacy_format(current) or not isinstance(current, dict):
|
||||
return None
|
||||
items = list(current.get("items", []))
|
||||
replaced = False
|
||||
for i, existing_entry in enumerate(items):
|
||||
if existing_entry.get("zotero_key") == key:
|
||||
items[i] = new_entry
|
||||
replaced = True
|
||||
break
|
||||
if not replaced:
|
||||
items.append(new_entry)
|
||||
return build_envelope(items)
|
||||
|
||||
if not mutate_index(vault, _apply):
|
||||
build_index(vault)
|
||||
return False
|
||||
|
||||
# Write back atomically
|
||||
index_path = paths["index"]
|
||||
envelope = build_envelope(items)
|
||||
atomic_write_index(index_path, envelope)
|
||||
logger.info("refresh_index_entry: updated entry %s (%s)", key, found_domain)
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -287,25 +287,6 @@ def run_repair(vault: Path, paths: dict, verbose: bool = False, fix: bool = Fals
|
|||
if new_record_text != record_text:
|
||||
record_path.write_text(new_record_text, encoding="utf-8")
|
||||
fixed_formal_note_primary = True
|
||||
if index_ocr_status is not None:
|
||||
try:
|
||||
index_data = read_json(paths["index"])
|
||||
if isinstance(index_data, dict):
|
||||
idx_items = index_data.get("items", [])
|
||||
elif isinstance(index_data, list):
|
||||
idx_items = index_data
|
||||
else:
|
||||
idx_items = []
|
||||
for entry in idx_items:
|
||||
if isinstance(entry, dict) and entry.get("zotero_key") == zotero_key:
|
||||
entry["ocr_status"] = new_status
|
||||
break
|
||||
if isinstance(index_data, dict):
|
||||
index_data["items"] = idx_items
|
||||
write_json(paths["index"], index_data)
|
||||
fixed_index_entry = True
|
||||
except Exception as e:
|
||||
logger.warning("Failed to update index entry for %s: %s", zotero_key, e)
|
||||
if meta_validated_status is not None and meta_validated_status != "done":
|
||||
if meta_path.exists():
|
||||
try:
|
||||
|
|
@ -328,25 +309,6 @@ def run_repair(vault: Path, paths: dict, verbose: bool = False, fix: bool = Fals
|
|||
if new_record_text != record_text:
|
||||
record_path.write_text(new_record_text, encoding="utf-8")
|
||||
fixed_formal_note_primary = True
|
||||
if index_ocr_status is not None:
|
||||
try:
|
||||
index_data = read_json(paths["index"])
|
||||
if isinstance(index_data, dict):
|
||||
idx_items = index_data.get("items", [])
|
||||
elif isinstance(index_data, list):
|
||||
idx_items = index_data
|
||||
else:
|
||||
idx_items = []
|
||||
for entry in idx_items:
|
||||
if isinstance(entry, dict) and entry.get("zotero_key") == zotero_key:
|
||||
entry["ocr_status"] = new_status
|
||||
break
|
||||
if isinstance(index_data, dict):
|
||||
index_data["items"] = idx_items
|
||||
write_json(paths["index"], index_data)
|
||||
fixed_index_entry = True
|
||||
except Exception as e:
|
||||
logger.warning("Failed to update index entry for %s: %s", zotero_key, e)
|
||||
if meta_path.exists():
|
||||
try:
|
||||
meta = read_json(meta_path)
|
||||
|
|
@ -370,9 +332,14 @@ def run_repair(vault: Path, paths: dict, verbose: bool = False, fix: bool = Fals
|
|||
logger.info("fixed %d files for %s", fixed_count, zotero_key)
|
||||
if fixed_count > 0:
|
||||
try:
|
||||
refresh_index_entry(vault, zotero_key)
|
||||
fixed_index_entry = refresh_index_entry(vault, zotero_key)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to refresh index for %s: %s", zotero_key, e)
|
||||
else:
|
||||
if fixed_index_entry:
|
||||
result["fixed"] += 1
|
||||
else:
|
||||
logger.warning("Failed to update index entry for %s via shared refresh", zotero_key)
|
||||
# Path error detection and repair
|
||||
path_errors = _detect_path_errors(paths, verbose)
|
||||
if path_errors["total"] > 0:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from .test_contract_helpers import (
|
|||
class TestPathsJson:
|
||||
"""Contract: 'paperforge paths --json' returns valid JSON with correct value types."""
|
||||
|
||||
REQUIRED_KEYS = {"vault", "worker_script", "ld_deep_script"}
|
||||
REQUIRED_KEYS = {"vault", "worker_script", "pf_deep_script"}
|
||||
|
||||
def test_paths_json_value_semantics(self, cli_invoker):
|
||||
"""paths --json returns paths with correct value types (all non-empty strings)."""
|
||||
|
|
|
|||
|
|
@ -188,8 +188,8 @@ class TestPaperforgeCommandExamplesInUserDocs:
|
|||
@pytest.mark.parametrize(
|
||||
"doc_key,pattern",
|
||||
[
|
||||
("README", r"paperforge (status|paths)"),
|
||||
("AGENTS", r"paperforge (status|paths)"),
|
||||
("README", r"paperforge (sync|ocr|status)"),
|
||||
("AGENTS", r"paperforge (search|status|sync)"),
|
||||
],
|
||||
)
|
||||
def test_user_doc_contains_paperforge_stable_example(
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ def test_paperforge_paths_returns_exact_keys(tmp_path: Path):
|
|||
"bases",
|
||||
"worker_script",
|
||||
"skill_dir",
|
||||
"ld_deep_script",
|
||||
"pf_deep_script",
|
||||
# ── v2.2: canonical locations below paperforge/ ──
|
||||
"config",
|
||||
"index",
|
||||
|
|
@ -370,8 +370,8 @@ def test_paperforge_paths_includes_worker_script(tmp_path: Path):
|
|||
assert paths["worker_script"].name == "__init__.py"
|
||||
|
||||
|
||||
def test_paperforge_paths_includes_ld_deep_script(tmp_path: Path):
|
||||
"""ld_deep_script key must point to ld_deep.py."""
|
||||
def test_paperforge_paths_includes_pf_deep_script(tmp_path: Path):
|
||||
"""pf_deep_script key must point to pf_deep.py."""
|
||||
from paperforge.config import paperforge_paths
|
||||
|
||||
vault = tmp_path / "vault_ld"
|
||||
|
|
@ -385,8 +385,8 @@ def test_paperforge_paths_includes_ld_deep_script(tmp_path: Path):
|
|||
(vault / ".opencode" / "command").mkdir(parents=True)
|
||||
|
||||
paths = paperforge_paths(vault)
|
||||
assert "ld_deep_script" in paths
|
||||
assert paths["ld_deep_script"].name == "ld_deep.py"
|
||||
assert "pf_deep_script" in paths
|
||||
assert paths["pf_deep_script"].name == "pf_deep.py"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -413,7 +413,7 @@ def test_paths_as_strings_returns_string_values():
|
|||
"bases": Path("/some/vault/05_Bases"),
|
||||
"worker_script": Path("/some/vault/99_System/PaperForge/worker/scripts/literature_pipeline.py"),
|
||||
"skill_dir": Path("/some/vault/.opencode/skills"),
|
||||
"ld_deep_script": Path("/some/vault/.opencode/skills/literature-qa/scripts/ld_deep.py"),
|
||||
"pf_deep_script": Path("/some/vault/.opencode/skills/paperforge/scripts/pf_deep.py"),
|
||||
}
|
||||
|
||||
result = paths_as_strings(paths)
|
||||
|
|
|
|||
|
|
@ -28,3 +28,36 @@ def test_embed_status_includes_build_state(tmp_path):
|
|||
mock_read.return_value = {"status": "running", "current": 3, "total": 10}
|
||||
result_code = run(args)
|
||||
assert result_code == 0
|
||||
|
||||
|
||||
def test_embed_stop_requests_signal_for_running_job(tmp_path):
|
||||
from argparse import Namespace
|
||||
|
||||
vault = tmp_path / "vault"
|
||||
vault.mkdir()
|
||||
args = Namespace(vault_path=vault, embed_subcommand="stop", json=True)
|
||||
|
||||
with patch("paperforge.commands.embed.read_vector_build_state") as mock_read:
|
||||
mock_read.return_value = {"status": "running", "pid": 12345}
|
||||
with patch("paperforge.commands.embed.os.kill") as mock_kill:
|
||||
with patch("paperforge.commands.embed.mark_vector_build_state") as mock_mark:
|
||||
result_code = run(args)
|
||||
|
||||
assert result_code == 0
|
||||
mock_kill.assert_called_once()
|
||||
mock_mark.assert_called_once()
|
||||
|
||||
|
||||
def test_embed_stop_returns_error_when_signal_fails(tmp_path):
|
||||
from argparse import Namespace
|
||||
|
||||
vault = tmp_path / "vault"
|
||||
vault.mkdir()
|
||||
args = Namespace(vault_path=vault, embed_subcommand="stop", json=True)
|
||||
|
||||
with patch("paperforge.commands.embed.read_vector_build_state") as mock_read:
|
||||
mock_read.return_value = {"status": "running", "pid": 12345}
|
||||
with patch("paperforge.commands.embed.os.kill", side_effect=OSError("denied")):
|
||||
result_code = run(args)
|
||||
|
||||
assert result_code == 1
|
||||
|
|
|
|||
33
tests/unit/core/test_io.py
Normal file
33
tests/unit/core/test_io.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from paperforge.core.io import read_json, write_json, write_json_atomic
|
||||
|
||||
|
||||
def test_write_json_atomic_creates_parent_dirs(tmp_path):
|
||||
path = tmp_path / "nested" / "state.json"
|
||||
|
||||
write_json_atomic(path, {"ok": True})
|
||||
|
||||
assert path.exists()
|
||||
assert read_json(path) == {"ok": True}
|
||||
|
||||
|
||||
def test_write_json_atomic_replaces_existing_file(tmp_path):
|
||||
path = tmp_path / "state.json"
|
||||
path.write_text(json.dumps({"old": True}), encoding="utf-8")
|
||||
|
||||
write_json_atomic(path, {"new": True})
|
||||
|
||||
assert read_json(path) == {"new": True}
|
||||
assert not path.with_suffix(".json.tmp").exists()
|
||||
|
||||
|
||||
def test_write_json_uses_atomic_writer(tmp_path):
|
||||
path = tmp_path / "state.json"
|
||||
|
||||
write_json(path, {"value": 1})
|
||||
|
||||
assert read_json(path) == {"value": 1}
|
||||
assert not path.with_suffix(".json.tmp").exists()
|
||||
|
|
@ -15,3 +15,35 @@ def test_refresh_paper_returns_false_for_empty_key():
|
|||
|
||||
def test_refresh_paper_returns_false_for_missing_key():
|
||||
assert refresh_paper(Path("/nonexistent/vault"), {"title": "No Key"}) is False
|
||||
|
||||
|
||||
def test_refresh_paper_rebuild_from_index_removes_stale_rows(tmp_path):
|
||||
from paperforge.memory.builder import build_from_index
|
||||
from paperforge.memory.db import get_connection, get_memory_db_path
|
||||
from paperforge.worker.asset_index import build_envelope, get_index_path
|
||||
|
||||
vault = tmp_path / "vault"
|
||||
vault.mkdir()
|
||||
(vault / "paperforge.json").write_text(
|
||||
'{"vault_config":{"system_dir":"System","resources_dir":"Resources","literature_dir":"Literature","base_dir":"Bases","control_dir":"LiteratureControl"}}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
index_path = get_index_path(vault)
|
||||
index_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
first = [{"zotero_key": "AAA", "title": "Paper A", "domain": "test", "has_pdf": False, "ocr_status": "pending", "analyze": False, "deep_reading_status": "pending", "pdf_path": "", "note_path": "", "main_note_path": "", "fulltext_path": "", "ocr_json_path": "", "ai_path": "", "paper_root": "", "citation_key": "", "doi": "", "journal": "", "first_author": "", "collection_path": "", "collections": [], "authors": [], "abstract": "", "zotero_storage_key": "", "attachment_count": 0, "supplementary": [], "recommend_analyze": False, "path_error": "", "analysis_note": "", "lifecycle": "indexed", "maturity": {"score": 0}, "next_step": "ready"}]
|
||||
index_path.write_text(__import__("json").dumps(build_envelope(first), ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
build_from_index(vault)
|
||||
|
||||
second = []
|
||||
index_path.write_text(__import__("json").dumps(build_envelope(second), ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
build_from_index(vault)
|
||||
|
||||
conn = get_connection(get_memory_db_path(vault), read_only=True)
|
||||
try:
|
||||
count = conn.execute("SELECT COUNT(*) AS c FROM papers").fetchone()["c"]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
assert count == 0
|
||||
|
|
|
|||
Loading…
Reference in a new issue