diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index bdd6456c..0244df63 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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 --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 diff --git a/.planning/milestones/v1.4.2.md b/.planning/milestones/v1.4.2.md new file mode 100644 index 00000000..353eab1f --- /dev/null +++ b/.planning/milestones/v1.4.2.md @@ -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` 到 `/.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 Agent(OpenCode / 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 \ + [--paddleocr-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 --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* diff --git a/README.md b/README.md index 25b0ccd7..b1516c0d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/ai-agent-setup-guide.md b/docs/ai-agent-setup-guide.md new file mode 100644 index 00000000..d5cd4336 --- /dev/null +++ b/docs/ai-agent-setup-guide.md @@ -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('')); r = c.check_zotero(); print('OK' if r.passed else 'NOT_FOUND'); print(r.detail)" +``` + +把 `` 替换为 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('')); 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 "" \ + --agent "" \ + --skip-checks +``` + +**重要**:加 `--skip-checks`,因为 Step 1-4 已经逐项检测过了,不需要重复。 + +如果有 PaddleOCR Key,加上 `--paddleocr-key ""`。 + +示例(Windows,OpenCode,无 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 里的 `/PaperForge/exports/` +> 6. 勾选 "保持更新" +> +> **首次使用流程:** +> 7. 在 Zotero 里添加文献 → `paperforge sync`(同步到 Obsidian) +> 8. 在 Obsidian 的 library-records 里设置 `do_ocr: true` +> 9. `paperforge ocr`(运行 OCR) +> 10. 在 Agent 里输入 `/pf-deep `(精读) + +--- + +## 常见问题 + +### 用户卡在某个步骤 + +回到那个步骤重新检测,确认用户已完成后再继续。不要跳过。 + +### vault 路径有空格 + +用双引号括起来:`--vault "D:\My Documents\MyVault"` + +### macOS/Linux 上的 pip 权限问题 + +加 `--user`: +```bash +pip install --user git+https://github.com/LLLin000/PaperForge.git +``` + +### 用户已装过 PaperForge(upgrade 场景) + +跳过 Step 0-1,直接: +```bash +paperforge setup --headless --vault "" --agent "" --skip-checks +``` diff --git a/paperforge/cli.py b/paperforge/cli.py index 1aa78fcb..5516d4a5 100644 --- a/paperforge/cli.py +++ b/paperforge/cli.py @@ -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 diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js new file mode 100644 index 00000000..c9f2000b --- /dev/null +++ b/paperforge/plugin/main.js @@ -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)}`); + } + }); + } +}; diff --git a/paperforge/plugin/manifest.json b/paperforge/plugin/manifest.json new file mode 100644 index 00000000..ff76aba6 --- /dev/null +++ b/paperforge/plugin/manifest.json @@ -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 +} diff --git a/paperforge/setup_wizard.py b/paperforge/setup_wizard.py index 9b22c216..465d176b 100644 --- a/paperforge/setup_wizard.py +++ b/paperforge/setup_wizard.py @@ -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), + ("", resources_dir), + ("", literature_dir), + ("", control_dir), + ("", base_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), + ("", resources_dir), + ("", literature_dir), + ("", control_dir), + ("", base_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 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 路径(可选,默认当前目录)") diff --git a/pyproject.toml b/pyproject.toml index 71e48b39..754fb42f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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]