mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 17:00:23 +00:00
feat: headless setup mode, Obsidian plugin, AI agent install guide (v1.4.2)
This commit is contained in:
parent
596faec4ec
commit
2ca557bb9e
9 changed files with 1078 additions and 15 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
272
.planning/milestones/v1.4.2.md
Normal file
272
.planning/milestones/v1.4.2.md
Normal 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 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 <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*
|
||||
19
README.md
19
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
|
||||
|
||||
|
|
|
|||
233
docs/ai-agent-setup-guide.md
Normal file
233
docs/ai-agent-setup-guide.md
Normal 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>"`。
|
||||
|
||||
示例(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 里的 `<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
|
||||
```
|
||||
|
||||
### 用户已装过 PaperForge(upgrade 场景)
|
||||
|
||||
跳过 Step 0-1,直接:
|
||||
```bash
|
||||
paperforge setup --headless --vault "<path>" --agent "<key>" --skip-checks
|
||||
```
|
||||
|
|
@ -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
58
paperforge/plugin/main.js
Normal 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)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
10
paperforge/plugin/manifest.json
Normal file
10
paperforge/plugin/manifest.json
Normal 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
|
||||
}
|
||||
|
|
@ -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 路径(可选,默认当前目录)")
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue