feat: headless setup mode, Obsidian plugin, AI agent install guide (v1.4.2)

This commit is contained in:
Research Assistant 2026-04-28 14:17:13 +08:00
parent 1ee235bad2
commit 859fa5ff97
9 changed files with 1078 additions and 15 deletions

View file

@ -16,6 +16,7 @@
- [x] **Phase 17: Dead Code Removal + Pre-Commit** — Clean codebase validated by automated git hooks
- [x] **Phase 18: Documentation + CHANGELOG + UX Polish** — Complete user/maintainer docs, README fix, command naming audit (2 plans)
- [x] **Phase 19: Testing** — E2E pipeline tests, setup wizard tests, `_utils.py` unit tests
- [ ] **Phase 20: Headless Setup & Obsidian Plugin**`paperforge setup --headless`, minimal Obsidian plugin for command palette integration
---
@ -67,12 +68,27 @@ Plans:
2. Setup wizard tests pass validating agent platform detection, vault path resolution, environment checks, and configuration file generation
3. Dedicated unit tests for `_utils.py` cover JSON I/O, YAML helpers, slugify, and journal DB functions — `test_utils_json.py`, `test_utils_yaml.py`, `test_utils_slugify.py`, `test_utils_journal.py`
4. All 205 existing tests continue to pass with zero failures — minimum bar: 205+ tests passing, 0 failures, 0 errors
**Plans**: 3 plans (1 wave)
**Plans**: 3 plans (1 wave) — EXECUTED (317 tests pass, see 19-*-SUMMARY.md)
Plans:
- [ ] 19-01-PLAN.md — _utils.py unit tests (JSON I/O, YAML helpers, slugify, journal DB)
- [ ] 19-02-PLAN.md — E2E pipeline integration tests (selection-sync → index-refresh → OCR queue → formal notes)
- [ ] 19-03-PLAN.md — Setup wizard tests (Agent configs, EnvChecker, path resolution)
- [x] 19-01-PLAN.md — _utils.py unit tests (71 new tests, 4 files)
- [x] 19-02-PLAN.md — E2E pipeline integration tests (13 tests, selection-sync → notes)
- [x] 19-03-PLAN.md — Setup wizard tests (30 tests, Agent configs + EnvChecker)
---
### Phase 20: Headless Setup & Obsidian Plugin
**Goal**: Enable one-command `paperforge setup --headless` for AI agents, and deploy a minimal Obsidian plugin for command palette integration.
**Depends on**: Phase 19 (tested deployment pipeline already exists)
**Requirements**: None (v1.4.2 patch — new milestone document at `.planning/milestones/v1.4.2.md`)
**Success Criteria** (what must be TRUE):
1. `paperforge setup --headless --vault <path> --agent opencode` completes full installation, exit code 0
2. Headless-generated vault structure matches wizard-generated structure (dirs, files, configs)
3. `.obsidian/plugins/paperforge/main.js` and `manifest.json` are deployed and valid
4. `python -m paperforge sync/ocr/status` works with cwd=vault (same env as Obsidian plugin)
5. AGENTS.md contains complete AI Agent self-install guidance section
6. All 317 existing tests pass with zero regressions
**Plans**: No formal PLAN.md — milestone document is authoritative
---
@ -83,12 +99,13 @@ Plans:
| Phase | Milestone | Plans Complete | Status | Completed |
|-------|-----------|----------------|--------|-----------|
| 13. Logging Foundation | v1.4 | 3/3 | Complete | 2026-04-27 |
| 14. Shared Utils Extraction | v1.4 | 0/0 | Not started | — |
| 14. Shared Utils Extraction | v1.4 | 0/0 | Complete | 2026-04-27 |
| 15. Queue Merge | v1.4 | 1/1 | Complete | 2026-04-27 |
| 16. Retry + Progress | v1.4 | 2/2 | Complete | 2026-04-27 |
| 17. Dead Code + Pre-Commit | v1.4 | 1/1 | Complete | 2026-04-27 |
| 18. Docs + CHANGELOG + UX | v1.4 | 2/2 | Complete | 2026-04-27 |
| 19. Testing | v1.4 | 0/3 | Not started | — |
| 19. Testing | v1.4 | 3/3 | Complete | 2026-04-28 |
| 20. Headless Setup & Plugin | v1.4.2 | — | In progress | — |
### Historical Milestones

View file

@ -0,0 +1,272 @@
# Milestone: v1.4.2 — Headless Setup & Obsidian Plugin
**Started:** 2026-04-28
**Status:** In Progress
**Parent:** v1.4 Code Health & UX Hardening
**Phases:** 20 (single phase)
---
## Summary
v1.4.2 是 PaperForge v1.4 系列的第三个补丁版本,聚焦于降低安装门槛和 Obsidian 集成。
**核心交付三件事:**
1. **Headless 安装模式**`paperforge setup --headless` 让 AI Agent 能代替用户完成安装,无需 Textual TUI
2. **最小 Obsidian 插件** — 安装时自动部署到 `.obsidian/plugins/paperforge/`,提供 Ctrl+P 命令面板集成
3. **AI Agent 安装指引** — AGENTS.md 新增 Agent 自安装章节,明确 Agent 应该问用户什么、执行什么命令
**不在此 milestone**
- 多平台 Agent 命令生成Cursor/Copilot/Claude 等)→ v1.5+
- Obsidian 插件的 Base 按钮 / 状态栏面板 / 侧边栏 → v1.5+
- Textual wizard 的 TUI 测试 → v1.5+
---
## Phase 20: Headless Setup + Obsidian Plugin Deployment
**Goal:** 用户(或 AI Agent能用一条命令完成 PaperForge 全量安装,安装后 Obsidian 插件立即可用。
**Depends on:** v1.4.1 baseline
### Tasks
#### 20-01: Headless Setup CLI Entry
**目标:** `paperforge setup --headless` 跳过 Textual走纯 CLI 路径完成安装。
**当前状态:** `cli.py:364-367` 调用 `setup_wizard.main()`,后者硬编码启动 `SetupWizardApp`Textual TUI
**改动:**
1. `cli.py``setup` 子命令新增参数:
```
--headless 跳过 Textual TUI非交互模式
--agent AGENT 选择 Agent 平台 (默认 opencode)
--paddleocr-key KEY PaddleOCR API Key (可选)
--paddleocr-url URL PaddleOCR API URL (可选,有默认值)
--system-dir NAME 系统文件夹名 (默认 99_System)
--resources-dir NAME 资源文件夹名 (默认 03_Resources)
--literature-dir NAME 文献文件夹名 (默认 Literature)
--control-dir NAME 控制文件夹名 (默认 LiteratureControl)
--base-dir NAME Base 文件夹名 (默认 05_Bases)
--zotero-data PATH Zotero 数据目录 (可选,自动检测)
--skip-checks 跳过环境检测 (测试/CI 用)
```
2. `paperforge/setup_wizard.py` — 提取 `_deploy()` 的核心逻辑为独立函数 `headless_deploy(vault, config)`
- 环境检测Python 版本、依赖、Zotero、BBT、JSON 导出)
- 目录创建
- 文件复制workers、skills、commands、docs、AGENTS.md
- .env 创建
- paperforge.json 创建
- pip install -e . (注册 paperforge 命令)
- 文件完整性验证
3. `cli.py` — headless 路径直接调用 `headless_deploy()` 而非 `SetupWizardApp`
**验证:**
- `paperforge setup --headless --vault /tmp/test-vault --skip-checks` 成功完成
- 生成的 vault 结构与 wizard 模式一致
- 返回 exit code 0 成功 / non-zero 失败 + stderr 错误信息
---
#### 20-02: 最小 Obsidian 插件
**目标:** 一个约 50 行的 Obsidian 插件提供命令面板Ctrl+P集成。
**插件设计:**
```
.obsidian/plugins/paperforge/
├── main.js # 插件主体 (~60 行)
└── manifest.json # 插件元数据
```
**`main.js` 功能:**
- 注册 3 个 Obsidian 命令,注入到命令面板:
- `PaperForge: 同步文献并生成笔记` → 执行 `python -m paperforge sync`
- `PaperForge: 运行 OCR` → 执行 `python -m paperforge ocr`
- `PaperForge: 查看状态` → 执行 `python -m paperforge status`
- 通过 `child_process.exec` 调起 Python`cwd` 设为 vault 根目录
- 执行结果通过 `new Notice()` 浮窗提示
- 通过 `this.app.vault.adapter.basePath` 自动获取 vault 路径(无需用户配置)
**`manifest.json` 内容:**
```json
{
"id": "paperforge",
"name": "PaperForge",
"version": "1.4.2",
"minAppVersion": "1.0.0",
"description": "PaperForge — Zotero + Obsidian literature pipeline integration",
"author": "PaperForge",
"authorUrl": "https://github.com/LLLin000/PaperForge",
"isDesktopOnly": true
}
```
**`isDesktopOnly: true`** — 因为需要 `child_process` 调 Python移动端不支持。
**路径处理:**
- 插件不硬编码任何路径 — `basePath` 就是 vault 根
- `paperforge` 命令需要已在 PATH 中(由 pip install 保证)
- Fallback: 如果 `paperforge` 不在 PATH尝试 `python -m paperforge`
- 配置写入 `paperforge.json` 时记录 Python 路径,插件可读取
- `paperforge_paths()` 已处理 `worker_script` / `ld_deep_script` 的 fallback 查找
**部署:** setup wizard 的 deploy 步骤自动复制 `main.js``manifest.json``<vault>/.obsidian/plugins/paperforge/`
**用户启用步骤:** Obsidian → 设置 → 社区插件 → 已安装插件 → PaperForge → 启用
**验证:**
- 安装后 `.obsidian/plugins/paperforge/main.js` 存在且内容正确
- 安装后 `.obsidian/plugins/paperforge/manifest.json` 存在且内容正确
- Obsidian 设置中可见 PaperForge 插件
- Ctrl+P 搜索 "PaperForge" 可看到 3 个命令
---
#### 20-03: AGENTS.md Agent 自安装指引
**目标:** AI Agent 在读取 AGENTS.md 后,能自主执行 PaperForge 安装。
**新增章节内容:**
```markdown
## 12. AI Agent 自安装指南
如果你是一个 AI AgentOpenCode / Cursor / Claude Code 等),
并且用户还没有安装 PaperForge请按以下步骤帮用户完成安装
### 需要向用户确认的信息
1. **Obsidian Vault 路径**(必填)
- 问:"你的 Obsidian Vault 的完整路径是什么?"
- 示例:`D:\Documents\MyVault` 或 `/Users/name/Documents/MyVault`
2. **AI Agent 平台**(必填)
- 问:"你使用哪个 AI Agent"
- 选项opencode / cursor / claude / windsurf / github_copilot / cline / augment / trae
3. **PaddleOCR API Key**(可选,可后续配置)
- 问:"你有 PaddleOCR API Key 吗?(没有可以跳过,后续在 .env 中配置)"
- 获取地址https://paddleocr.baidu.com
### 执行安装
收集到 vault 路径后,运行:
```bash
# pip 安装
pip install git+https://github.com/LLLin000/PaperForge.git
# headless 安装
paperforge setup --headless \
--vault "<用户填的vault路径>" \
--agent <agent平台key> \
[--paddleocr-key "<api key>"]
```
### 安装后告知用户
安装完成后,告知用户:
1. 打开 Obsidian
2. 设置 → 社区插件 → 已安装 → 启用 "PaperForge"
3. Ctrl+P 输入 "PaperForge" 即可使用命令
### 首次使用验证
运行 `paperforge status` 确认系统就绪。
```
---
### 20-04: 路径处理验证
**目标:** 确认 Obsidian 插件在各种环境下能正确找到 paperforge。
**场景矩阵:**
| 场景 | 预期行为 |
| --------------------- | --------------------------------------- |
| pip install 全局安装 | `paperforge sync` 直接可用 |
| pip install --user | 取决于 PATH 是否包含用户 Scripts 目录 |
| pip install -e (dev) | 同全局安装 |
| python -m paperforge | 始终可用(只要 python 在 PATH推荐 |
| Obsidian 以管理员启动 | 环境变量可能不同,需 python -m fallback |
**插件内 fallback 逻辑:**
```javascript
// 优先用 paperforge 命令
exec('paperforge sync', { cwd: vault }, (err, stdout) => {
if (err) {
// Fallback: python -m paperforge
exec('python -m paperforge sync', { cwd: vault }, (err2, stdout2) => {
if (err2) new Notice('[!!] 找不到 paperforge 命令');
else new Notice('[OK] ' + stdout2);
});
} else {
new Notice('[OK] ' + stdout);
}
});
```
**验证:**
- `python -m paperforge status` 在 headless 安装后的 vault 中正常输出
- 插件产生的输出与 CLI 手动执行一致
---
## Success Criteria
- [ ] `paperforge setup --headless --vault <path> --agent opencode` 完成全量安装exit code 0
- [ ] headless 安装生成的 vault 结构与 wizard 模式一致(目录、文件、配置)
- [ ] `.obsidian/plugins/paperforge/main.js``manifest.json` 正确部署
- [ ] Obsidian Ctrl+P 可搜索到 "PaperForge" 命令
- [ ] AGENTS.md 包含完整的 Agent 自安装指引章节
- [ ] 现有 317 个测试全部通过0 回归
- [ ] Headless 模式在缺失必填参数时给出明确错误提示stderr + 非零退出码)
---
## Design Decisions
| 决策项 | 选择 | 理由 |
| -------------------------------- | ---------------------------------- | ---------------------------------------------------------- |
| 插件实现方式 | 本地插件,安装时自动部署 | 无需社区审核无第三方依赖setup 本来就往 vault 复制文件 |
| 插件调用 paperforge 的方式 | `python -m paperforge` + `cwd=vault` | 最可靠,不需要 paperforge 在 PATH |
| 插件功能范围 | 只有命令面板3 个命令) | 先打通最小闭环Base 按钮/状态栏等后续加 |
| 多平台 Agent 命令 | 不在此 milestone | 工程量大(需研究每平台机制),独立做 |
| headless 模式与 wizard 代码复用 | 提取 `headless_deploy()` 共享核心逻辑 | 避免两份 deploy 代码越走越远 |
---
## Risks
| 风险 | 缓解措施 |
| ----------------------------- | ---------------------------------------------------- |
| Obsidian 不在 PATH 的 Python | 插件 fallback 链: paperforge → python -m paperforge |
| vault 路径有空格/中文 | child_process exec cwd 传 Path 对象JS 自动处理 |
| 用户禁用了社区插件 | AGENTS.md 指引中说明需要启用 |
| headless 模式下 Zotero 检测失败 | `--skip-checks` flag 可跳过,不阻塞安装 |
---
## Plan Checklist
- [x] 20-01: Headless Setup CLI Entry
- [ ] 20-01 Implementation
- [x] 20-02: Minimal Obsidian Plugin Design
- [ ] 20-02 Implementation
- [x] 20-03: AGENTS.md Agent Self-Install Guide
- [ ] 20-03 Documentation Update
- [x] 20-04: Path Handling Verification Matrix
- [ ] 20-04 Validation
---
*v1.4.2 Milestone | 2026-04-28*

View file

@ -103,7 +103,18 @@ PaperForge turns your Zotero library into an **AI-ready literature knowledge bas
## Quick Start
### Install
### Install — Let your AI agent do it (Recommended)
Copy and paste this prompt to your AI agent (OpenCode, Cursor, Claude Code, etc.):
```
请帮我安装 PaperForge。参考下面的文档完成安装
https://raw.githubusercontent.com/LLLin000/PaperForge/master/docs/ai-agent-setup-guide.md
```
The agent will ask you 2 questions, then install everything. Done.
### Install — Do it yourself
```bash
pip install git+https://github.com/LLLin000/PaperForge.git
@ -112,11 +123,7 @@ paperforge setup
The interactive wizard handles: agent platform selection, vault directory setup, Zotero data linking, PaddleOCR API key configuration. See [setup-guide.md](docs/setup-guide.md) for a step-by-step walkthrough.
> Windows one-click installer:
> ```powershell
> powershell -c "iwr -Uri https://raw.githubusercontent.com/LLLin000/PaperForge/master/scripts/install-paperforge.ps1 -OutFile install.ps1; ./install.ps1"
> paperforge setup
> ```
> For AI agents reading this: use the headless mode — `paperforge setup --headless`. Full instructions in [docs/ai-agent-setup-guide.md](docs/ai-agent-setup-guide.md).
### Prerequisites

View file

@ -0,0 +1,233 @@
# PaperForge Headless Setup — For LLM Agents
> **IMPORTANT**: Read this file directly. Do NOT use WebFetch or summarization — you will lose critical CLI flags and decision logic.
---
## For Humans
Copy this and paste it to your AI agent:
```
请帮我安装 PaperForge。
参考 https://raw.githubusercontent.com/LLLin000/PaperForge/master/docs/ai-agent-setup-guide.md 完成安装。
```
---
## For LLM Agents
You are installing PaperForge into the user's Obsidian Vault. Follow each step in order. **Do not skip steps.** Report results at each step before proceeding.
---
### Step 0: 收集信息 — 问用户 3 个问题
**必须问。不能猜。不能跳过。**
**Q1: Obsidian Vault 路径**
问:
> 你的 Obsidian Vault 的完整绝对路径是什么?
要求用户提供绝对路径(如 `D:\Documents\医学文献``/Users/name/Documents/MyVault`)。
如果用户说不知道:
> 打开 Obsidian → 左下角点 vault 名称 → "管理 Vault" → 右键 vault → "在系统文件管理器中显示"。把那个文件夹的完整路径发给我。
**Q2: AI Agent 平台**
问:
> 你正在使用哪个 AI Agent
展示列表让用户选:
| Key | Name |
|-----|------|
| `opencode` | OpenCode |
| `cursor` | Cursor |
| `claude` | Claude Code |
| `windsurf` | Windsurf |
| `github_copilot` | GitHub Copilot |
| `cline` | Cline |
| `augment` | Augment |
| `trae` | Trae |
用户不回答则默认 `opencode`
**Q3可选: PaddleOCR API Key**
问:
> 你有 PaddleOCR API Key 吗?
- 有 → 拿到 key 值,记下来
- 没有 → 跳过不追问。获取地址https://paddleocr.baidu.com
---
### Step 1: 检查 Python 版本
```bash
python --version
```
- Python >= 3.10 → 通过,进入 Step 2
- Python < 3.10 或不存在 **停止**告诉用户
> PaperForge 需要 Python 3.10 或更高版本。请从 https://python.org 下载安装后再继续。
>
> 安装时请勾选 "Add Python to PATH"。
---
### Step 2: 安装 paperforge 包
```bash
pip install git+https://github.com/LLLin000/PaperForge.git
```
- 成功 → 告诉用户"paperforge 已安装",进入 Step 3
- 权限错误 → 重试 `pip install --user git+https://github.com/LLLin000/PaperForge.git`
- 其他错误 → 把错误信息展示给用户,**停止**
---
### Step 3: 检测 Zotero
```bash
python -c "from paperforge.setup_wizard import EnvChecker; from pathlib import Path; c = EnvChecker(Path('<vault_path>')); r = c.check_zotero(); print('OK' if r.passed else 'NOT_FOUND'); print(r.detail)"
```
`<vault_path>` 替换为 Step 0 拿到的路径。
- `OK` → 告诉用户 "检测到 Zotero",进入 Step 4
- `NOT_FOUND` → 告诉用户:
> 未检测到 Zotero。请从 https://zotero.org 下载安装。
>
> 安装完成后告诉我,我继续检测。
**等待用户确认后再继续**
---
### Step 4: 检测 Better BibTeX 插件
```bash
python -c "from paperforge.setup_wizard import EnvChecker; from pathlib import Path; c = EnvChecker(Path('<vault_path>')); r = c.check_bbt(); print('OK' if r.passed else 'NOT_FOUND'); print(r.detail)"
```
- `OK` → 告诉用户 "检测到 Better BibTeX",进入 Step 5
- `NOT_FOUND` → 告诉用户:
> 未检测到 Better BibTeX 插件。请按以下步骤安装:
> 1. 下载https://retorque.re/zotero-better-bibtex/
> 2. Zotero → 工具 → 插件 → 齿轮 → Install Plugin From File
> 3. 选择下载的 .xpi 文件 → 重启 Zotero
**等待用户确认后再继续**
---
### Step 5: 创建目录并部署文件
现在一次性完成目录创建和文件部署:
```bash
paperforge setup --headless \
--vault "<vault_path>" \
--agent "<agent_key>" \
--skip-checks
```
**重要**:加 `--skip-checks`,因为 Step 1-4 已经逐项检测过了,不需要重复。
如果有 PaddleOCR Key加上 `--paddleocr-key "<key>"`
示例WindowsOpenCode无 Key
```bash
paperforge setup --headless --vault "D:\Documents\医学文献" --agent opencode --skip-checks
```
**输出解读**
成功时输出类似:
```
[*] Phase 2: Creating directories... [OK] 10 directories ready
[*] Phase 4: Deploying files... [OK] worker scripts / skill files / ...
[*] Phase 5: Creating config files... [OK] .env / paperforge.json
[*] Phase 6: Registering CLI... [OK] paperforge CLI registered
[*] Phase 7: Verifying installation... [OK] All 12 checks passed
```
失败时查看退出码:
| 退出码 | 含义 | 处理 |
|--------|------|------|
| 1 | 找不到 paperforge 包目录 | 包安装不完整,重新 pip install |
| 4 | Worker 脚本缺失 | 同上 |
| 5 | Skill 文件缺失 | 同上 |
| 6 | 文件完整性验证失败 | 检查磁盘空间和写权限 |
---
### Step 6: 验证安装
```bash
paperforge status
```
如果能正常输出状态信息 → 安装成功。
```bash
python -m paperforge status
```
如果 `paperforge` 命令找不到,用这个备选。两者等效。
---
### Step 7: 告诉用户下一步
报告给用户以下信息:
> 安装完成。接下来你需要做:
>
> **在 Obsidian 里启用插件:**
> 1. 打开 Obsidian
> 2. 设置 → 社区插件 → 已安装插件 → 找到 PaperForge → 启用
> 3. Ctrl+P 输入 "PaperForge",可以看到 3 个命令
>
> **配置 Zotero 自动导出(必须):**
> 4. Zotero → 文件 → 导出库 → 格式选 Better BibTeX
> 5. 保存到 Vault 里的 `<system_dir>/PaperForge/exports/`
> 6. 勾选 "保持更新"
>
> **首次使用流程:**
> 7. 在 Zotero 里添加文献 → `paperforge sync`(同步到 Obsidian
> 8. 在 Obsidian 的 library-records 里设置 `do_ocr: true`
> 9. `paperforge ocr`(运行 OCR
> 10. 在 Agent 里输入 `/pf-deep <key>`(精读)
---
## 常见问题
### 用户卡在某个步骤
回到那个步骤重新检测,确认用户已完成后再继续。不要跳过。
### vault 路径有空格
用双引号括起来:`--vault "D:\My Documents\MyVault"`
### macOS/Linux 上的 pip 权限问题
`--user`
```bash
pip install --user git+https://github.com/LLLin000/PaperForge.git
```
### 用户已装过 PaperForgeupgrade 场景)
跳过 Step 0-1直接
```bash
paperforge setup --headless --vault "<path>" --agent "<key>" --skip-checks
```

View file

@ -220,7 +220,65 @@ def build_parser() -> argparse.ArgumentParser:
sub.add_parser("update", help="Update PaperForge to the latest version")
# setup wizard
sub.add_parser("setup", help="Run the setup wizard (Textual-based)")
p_setup = sub.add_parser("setup", help="Run the setup wizard (Textual-based)")
p_setup.add_argument(
"--headless",
action="store_true",
help="Run setup non-interactively (for AI agents or scripts)",
)
p_setup.add_argument(
"--agent",
metavar="AGENT",
default="opencode",
choices=["opencode", "cursor", "claude", "windsurf", "github_copilot", "cline", "augment", "trae"],
help="AI Agent platform (default: opencode)",
)
p_setup.add_argument(
"--paddleocr-key",
metavar="KEY",
help="PaddleOCR API Key",
)
p_setup.add_argument(
"--paddleocr-url",
metavar="URL",
default="https://paddleocr.aistudio-app.com/api/v2/ocr/jobs",
help="PaddleOCR API URL",
)
p_setup.add_argument(
"--system-dir",
metavar="NAME",
help="System directory name (default: 99_System)",
)
p_setup.add_argument(
"--resources-dir",
metavar="NAME",
help="Resources directory name (default: 03_Resources)",
)
p_setup.add_argument(
"--literature-dir",
metavar="NAME",
help="Literature directory name (default: Literature)",
)
p_setup.add_argument(
"--control-dir",
metavar="NAME",
help="Control directory name (default: LiteratureControl)",
)
p_setup.add_argument(
"--base-dir",
metavar="NAME",
help="Base directory name (default: 05_Bases)",
)
p_setup.add_argument(
"--zotero-data",
metavar="PATH",
help="Zotero data directory (auto-detect if omitted)",
)
p_setup.add_argument(
"--skip-checks",
action="store_true",
help="Skip environment checks (for testing/CI)",
)
return parser
@ -362,9 +420,27 @@ def main(argv: list[str] | None = None) -> int:
return run_update(vault)
if args.command == "setup":
from paperforge.setup_wizard import main as run_setup
if getattr(args, "headless", False):
from paperforge.setup_wizard import headless_setup
return run_setup(sys.argv[2:])
return headless_setup(
vault=vault,
agent_key=args.agent,
paddleocr_key=getattr(args, "paddleocr_key", None),
paddleocr_url=getattr(args, "paddleocr_url",
"https://paddleocr.aistudio-app.com/api/v2/ocr/jobs"),
system_dir=getattr(args, "system_dir", None) or "99_System",
resources_dir=getattr(args, "resources_dir", None) or "03_Resources",
literature_dir=getattr(args, "literature_dir", None) or "Literature",
control_dir=getattr(args, "control_dir", None) or "LiteratureControl",
base_dir=getattr(args, "base_dir", None) or "05_Bases",
zotero_data=getattr(args, "zotero_data", None),
skip_checks=getattr(args, "skip_checks", False),
)
else:
from paperforge.setup_wizard import main as run_setup
return run_setup(sys.argv[2:])
print(f"Error: unknown command {args.command}", file=sys.stderr)
return 1

58
paperforge/plugin/main.js Normal file
View file

@ -0,0 +1,58 @@
const { Plugin, Notice } = require('obsidian');
const { exec } = require('child_process');
const COMMANDS = [
{
id: 'paperforge-sync',
name: 'PaperForge: 同步文献并生成笔记',
cmd: 'sync',
okMsg: 'Sync complete',
},
{
id: 'paperforge-ocr',
name: 'PaperForge: 运行 OCR',
cmd: 'ocr',
okMsg: 'OCR started — check status for completion',
},
{
id: 'paperforge-status',
name: 'PaperForge: 查看系统状态',
cmd: 'status',
okMsg: null,
},
];
module.exports = class PaperForgePlugin extends Plugin {
async onload() {
for (const c of COMMANDS) {
this.addCommand({
id: c.id,
name: c.name,
callback: () => this._run(c.cmd, c.okMsg),
});
}
}
_run(subcommand, okMsg) {
const vaultPath = this.app.vault.adapter.basePath;
const cmd = `python -m paperforge ${subcommand}`;
new Notice(`PaperForge: running ${subcommand}...`);
exec(cmd, { cwd: vaultPath, timeout: 300000 }, (err, stdout, stderr) => {
if (err) {
const msg = stderr
? stderr.split('\n').filter(Boolean).slice(-2).join(' | ')
: err.message;
new Notice(`[!!] PaperForge ${subcommand} failed: ${msg}`, 8000);
return;
}
if (okMsg) {
new Notice(`[OK] ${okMsg}`);
} else {
const firstLine = stdout.trim().split('\n')[0] || '';
new Notice(`[OK] ${firstLine.slice(0, 80)}`);
}
});
}
};

View file

@ -0,0 +1,10 @@
{
"id": "paperforge",
"name": "PaperForge",
"version": "1.4.2",
"minAppVersion": "1.0.0",
"description": "PaperForge — Zotero + Obsidian literature pipeline. Sync, OCR, and deep-reading commands in your command palette.",
"author": "PaperForge",
"authorUrl": "https://github.com/LLLin000/PaperForge",
"isDesktopOnly": true
}

View file

@ -1491,6 +1491,394 @@ def _find_vault() -> Path | None:
return None
def headless_setup(
vault: Path,
agent_key: str = "opencode",
paddleocr_key: str | None = None,
paddleocr_url: str = "https://paddleocr.aistudio-app.com/api/v2/ocr/jobs",
system_dir: str = "99_System",
resources_dir: str = "03_Resources",
literature_dir: str = "Literature",
control_dir: str = "LiteratureControl",
base_dir: str = "05_Bases",
zotero_data: str | None = None,
skip_checks: bool = False,
repo_root: Path | None = None,
) -> int:
"""Run PaperForge setup non-interactively (no Textual TUI).
Designed for AI agents and automated scripts. Returns 0 on success,
non-zero on failure with error messages on stderr.
Args:
vault: Path to Obsidian vault root.
agent_key: AI agent platform key (opencode, cursor, claude, etc.)
paddleocr_key: PaddleOCR API token.
paddleocr_url: PaddleOCR API URL.
system_dir: System directory name.
resources_dir: Resources directory name.
literature_dir: Literature subdirectory name.
control_dir: Control subdirectory name.
base_dir: Base directory name.
zotero_data: Zotero data directory (auto-detect if None).
skip_checks: Skip environment validation.
repo_root: Path to PaperForge package root (auto-detect if None).
Returns:
int: 0 on success, non-zero on failure.
"""
vault = Path(vault).expanduser().resolve()
# Determine repo_root (where paperforge package sources live)
if repo_root is None:
wizard_dir = Path(__file__).parent.resolve()
if (wizard_dir / "paperforge" if wizard_dir.name != "paperforge" else False):
_repo = wizard_dir
elif (wizard_dir.parent / "paperforge").exists():
_repo = wizard_dir.parent
elif wizard_dir.name == "paperforge" and (wizard_dir / "__init__.py").exists():
_repo = wizard_dir.parent
else:
_repo = wizard_dir.parent
repo_root = _repo
if not (repo_root / "paperforge").exists():
print(f"Error: cannot find PaperForge package root (tried: {repo_root})", file=sys.stderr)
return 1
# Agent config
agent_config = AGENT_CONFIGS.get(agent_key)
if not agent_config:
print(f"Error: unknown agent platform '{agent_key}'", file=sys.stderr)
return 1
skill_dir = agent_config.get("skill_dir", ".opencode/skills")
print(f"[*] PaperForge headless setup")
print(f" Vault: {vault}")
print(f" Agent: {agent_config['name']}")
print(f" System: {system_dir}")
print(f" Resources: {resources_dir}")
# =========================================================================
# Phase 1: Pre-flight checks (BLOCKING — must pass to continue)
# =========================================================================
checker = EnvChecker(vault)
checker.system_dir = system_dir
if not skip_checks:
print("[*] Phase 1: Pre-flight checks...")
py = checker.check_python()
if not py.passed:
print(f"[FAIL] {py.detail}", file=sys.stderr)
return 2
print(f" [OK] {py.detail}")
deps = checker.check_dependencies()
if not deps.passed:
print(f"[FAIL] {deps.detail}", file=sys.stderr)
print(f"[FIX] pip install requests pymupdf pillow", file=sys.stderr)
return 3
print(f" [OK] {deps.detail}")
# =========================================================================
# Phase 2: Create directories
# =========================================================================
print("[*] Phase 2: Creating directories...")
pf_path = vault / system_dir / "PaperForge"
dirs = [
pf_path / "exports",
pf_path / "ocr",
pf_path / "config",
pf_path / "worker/scripts",
vault / resources_dir / literature_dir,
vault / resources_dir / control_dir / "library-records",
vault / base_dir,
vault / skill_dir / "literature-qa/scripts",
vault / skill_dir / "literature-qa/chart-reading",
vault / ".obsidian" / "plugins" / "paperforge",
]
for d in dirs:
d.mkdir(parents=True, exist_ok=True)
print(f" [OK] {len(dirs)} directories ready")
# =========================================================================
# Phase 3: Informational checks (NON-BLOCKING — warnings only)
# =========================================================================
built_vault_config = {
"system_dir": system_dir,
"resources_dir": resources_dir,
"literature_dir": literature_dir,
"control_dir": control_dir,
"base_dir": base_dir,
}
if not skip_checks:
print("[*] Phase 3: Environment checks (non-blocking)...")
zot = checker.check_zotero()
if zot.passed:
print(f" [OK] Zotero: {zot.detail}")
else:
print(f" [WARN] Zotero not found — install from https://zotero.org")
print(f" {zot.detail}")
bbt = checker.check_bbt()
if bbt.passed:
print(f" [OK] Better BibTeX: {bbt.detail}")
else:
print(f" [WARN] Better BibTeX not found")
print(f" Install from: https://retorque.re/zotero-better-bibtex/")
# JSON export check — only after exports/ dir exists
json_check = checker.check_json()
if json_check.passed:
print(f" [OK] JSON exports: {json_check.detail}")
else:
print(f" [WARN] {json_check.detail}")
zotero_data_hint = zotero_data or "<你的Zotero数据目录>"
if sys.platform == "win32":
print(f" 首次安装需要配置 BBT 自动导出:")
print(f" 1. Zotero → 文件 → 导出库 → Better BibTeX")
print(f" 2. 保存到 vault 的 {system_dir}/PaperForge/exports/")
print(f" 3. 勾选 "保持更新"")
else:
print(f" Configure BBT auto-export to: {system_dir}/PaperForge/exports/")
print(f" 完成后运行: paperforge sync")
# Zotero data directory detection
if zotero_data is None and not skip_checks:
detected = checker._find_zotero()
if detected:
# _find_zotero returns the .exe path; we need the data directory
zotero_home = detected.parent.parent if detected.parent.name == "Zotero" else detected.parent
data_candidate = Path(str(zotero_home)).parent if "Zotero" in str(zotero_home) else zotero_home
# Common Zotero data dir: ~/Zotero on all platforms
home_zotero = Path.home() / "Zotero"
if home_zotero.exists() and (home_zotero / "zotero.sqlite").exists():
zotero_data = str(home_zotero)
print(f" [OK] Zotero data detected: {zotero_data}")
# =========================================================================
# Phase 4: Deploy files
# =========================================================================
print("[*] Phase 4: Deploying files...")
import shutil
# Worker scripts
worker_src = repo_root / "paperforge/worker/sync.py"
worker_dst = pf_path / "worker/scripts/sync.py"
if not worker_src.exists():
print(f"Error: worker script not found: {worker_src}", file=sys.stderr)
return 4
shutil.copy2(worker_src, worker_dst)
for mod in ["ocr.py", "repair.py", "status.py", "deep_reading.py",
"update.py", "base_views.py", "__init__.py",
"_utils.py", "_progress.py", "_retry.py"]:
mod_src = repo_root / "paperforge/worker" / mod
if mod_src.exists():
shutil.copy2(mod_src, pf_path / "worker/scripts" / mod)
print(f" [OK] worker scripts")
# LD deep script
ld_src = repo_root / "paperforge/skills/literature-qa/scripts/ld_deep.py"
ld_dst = vault / skill_dir / "literature-qa/scripts/ld_deep.py"
if ld_src.exists():
shutil.copy2(ld_src, ld_dst)
else:
print(f"Error: ld_deep.py not found: {ld_src}", file=sys.stderr)
return 5
# Subagent prompt
prompt_src = repo_root / "paperforge/skills/literature-qa/prompt_deep_subagent.md"
prompt_dst = vault / skill_dir / "literature-qa/prompt_deep_subagent.md"
if prompt_src.exists():
shutil.copy2(prompt_src, prompt_dst)
print(f" [OK] skill files")
# Chart-reading guides
chart_src = repo_root / "paperforge/skills/literature-qa/chart-reading"
chart_dst = vault / skill_dir / "literature-qa/chart-reading"
if chart_src.exists() and chart_src.is_dir():
for f in chart_src.glob("*.md"):
shutil.copy2(f, chart_dst / f.name)
print(f" [OK] chart-reading guides")
# Command files (OpenCode only)
if agent_key == "opencode":
command_src = repo_root / "command"
command_dst = vault / agent_config.get("command_dir", ".opencode/command")
if command_src.exists() and command_src.is_dir():
command_dst.mkdir(parents=True, exist_ok=True)
for f in command_src.glob("*.md"):
text = f.read_text(encoding="utf-8")
for old, new in [
("<system_dir>", system_dir),
("<resources_dir>", resources_dir),
("<literature_dir>", literature_dir),
("<control_dir>", control_dir),
("<base_dir>", base_dir),
("<skill_dir>", skill_dir),
]:
text = text.replace(old, new)
(command_dst / f.name).write_text(text, encoding="utf-8")
print(f" [OK] OpenCode command files")
# Docs
docs_src = repo_root / "docs"
docs_dst = vault / "docs"
if docs_src.exists() and docs_src.is_dir():
shutil.copytree(docs_src, docs_dst, dirs_exist_ok=True)
print(f" [OK] docs")
# Obsidian plugin
plugin_src = repo_root / "paperforge/plugin"
plugin_dst = vault / ".obsidian" / "plugins" / "paperforge"
if plugin_src.exists() and plugin_src.is_dir():
for f in plugin_src.glob("*"):
shutil.copy2(f, plugin_dst / f.name)
print(f" [OK] Obsidian plugin")
else:
print(f" [WARN] Plugin source not found: {plugin_src}")
# AGENTS.md
agents_src = repo_root / "AGENTS.md"
agents_dst = vault / "AGENTS.md"
if agents_src.exists():
text = agents_src.read_text(encoding="utf-8")
for old, new in [
("<system_dir>", system_dir),
("<resources_dir>", resources_dir),
("<literature_dir>", literature_dir),
("<control_dir>", control_dir),
("<base_dir>", base_dir),
("<skill_dir>", skill_dir),
]:
text = text.replace(old, new)
agents_dst.write_text(text, encoding="utf-8")
# =========================================================================
# Phase 5: Create config files
# =========================================================================
print("[*] Phase 5: Creating config files...")
# .env
if paddleocr_key:
env_content = f"""# PaperForge 配置文件
PADDLEOCR_API_TOKEN={paddleocr_key}
PADDLEOCR_JOB_URL={paddleocr_url}
PADDLEOCR_MODEL=PaddleOCR-VL-1.5
"""
(pf_path / ".env").write_text(env_content, encoding="utf-8")
print(f" [OK] .env")
else:
print(f" [INFO] No PaddleOCR key — create {pf_path}/.env manually")
# Domain config (populated from whatever JSONs already exist in exports/)
domain_config = pf_path / "config" / "domain-collections.json"
if not domain_config.exists():
export_domains = [
{"domain": f.stem, "export_file": f.name, "allowed_collections": []}
for f in sorted((pf_path / "exports").glob("*.json"))
]
domain_config.write_text(
json.dumps({"domains": export_domains}, indent=2, ensure_ascii=False),
encoding="utf-8",
)
print(f" [OK] domain-collections.json")
# paperforge.json
pf_json = vault / "paperforge.json"
existing_config = {}
if pf_json.exists():
try:
existing_config = json.loads(pf_json.read_text(encoding="utf-8"))
except Exception:
existing_config = {}
existing_config.update({
"version": existing_config.get("version", "1.4.2"),
"agent_platform": agent_config.get("name", "OpenCode"),
"agent_key": agent_key,
"skill_dir": skill_dir,
"command_dir": agent_config.get("command_dir", ""),
"system_dir": system_dir,
"resources_dir": resources_dir,
"literature_dir": literature_dir,
"control_dir": control_dir,
"base_dir": base_dir,
"paperforge_path": f"{system_dir}/PaperForge",
"zotero_data_dir": zotero_data or "",
"zotero_link": f"{system_dir}/Zotero",
"vault_config": built_vault_config,
})
pf_json.write_text(json.dumps(existing_config, indent=2, ensure_ascii=False), encoding="utf-8")
print(f" [OK] paperforge.json")
# =========================================================================
# Phase 6: pip install
# =========================================================================
print("[*] Phase 6: Registering paperforge CLI...")
try:
result = subprocess.run(
[sys.executable, "-m", "pip", "install", "-e", str(repo_root)],
capture_output=True, text=True, timeout=120,
)
if result.returncode == 0:
print(f" [OK] paperforge CLI registered")
else:
stderr_short = result.stderr[:200] if result.stderr else ""
print(f" [WARN] pip install: {stderr_short}", file=sys.stderr)
except Exception as e:
print(f" [WARN] pip install skipped: {e}")
# =========================================================================
# Phase 7: Verify
# =========================================================================
print("[*] Phase 7: Verifying installation...")
checks = {
"Worker scripts": worker_dst.exists(),
"Skill scripts": ld_dst.exists(),
"Chart-reading guides": (chart_dst / "INDEX.md").exists() if chart_dst.exists() else True,
"Library records dir": (vault / resources_dir / control_dir / "library-records").exists(),
"Base dir": (vault / base_dir).exists(),
"Exports dir": (pf_path / "exports").exists(),
"OCR dir": (pf_path / "ocr").exists(),
"paperforge.json": pf_json.exists(),
"Obsidian plugin": (vault / ".obsidian" / "plugins" / "paperforge" / "main.js").exists(),
"AGENTS.md": agents_dst.exists(),
}
failed = [k for k, v in checks.items() if not v]
if failed:
print(f"[FAIL] Missing: {', '.join(failed)}", file=sys.stderr)
return 6
print(f" [OK] All {len(checks)} checks passed")
# =========================================================================
# Done
# =========================================================================
print()
print("=" * 60)
print(" PaperForge v1.4.2 installation complete!")
print("=" * 60)
print()
print("Next steps:")
print(" 1. Open Obsidian → Settings → Community Plugins → Enable 'PaperForge'")
print(" 2. Press Ctrl+P and type 'PaperForge' to see CLI commands")
print()
print("First-run workflow:")
print(f" 3. Configure Better BibTeX auto-export to: {system_dir}/PaperForge/exports/")
print(" (Zotero → Export Library → Better BibTeX → Keep Updated)")
print(" 4. Add papers to Zotero, then run: paperforge sync")
print(" 5. Mark papers with do_ocr:true in library-records")
print(" 6. Run: paperforge ocr")
print(" 7. Use /pf-deep <key> in your AI agent for deep reading")
print()
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="PaperForge 安装向导")
parser.add_argument("--vault", type=Path, default=None, help="Vault 路径(可选,默认当前目录)")

View file

@ -53,6 +53,8 @@ paperforge = [
"skills/literature-qa/chart-reading/*.md",
"skills/literature-qa/chart-reading/*",
"command_files/*.md",
"plugin/*.js",
"plugin/*.json",
]
[tool.pytest.ini_options]