diff --git a/AGENTS.md b/AGENTS.md index bb0ac973..c41328d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # PaperForge - Agent Guide -> 本文档面向 **安装完成后的新用户** 和 **AI Agent**。安装步骤见 [setup-guide.md](docs/setup-guide.md) 或快速版 [INSTALLATION.md](docs/INSTALLATION.md)。 +> 本文档面向 **安装完成后的新用户** 和 **AI Agent**。安装步骤见 [setup-guide.md](docs/setup-guide.md) 或快速版 [INSTALLATION.md](docs/INSTALLATION.md)。Docs 版本与 v1.4.13 对应。 --- @@ -12,7 +12,7 @@ [ ] Obsidian 已打开当前 Vault [ ] PaperForge 已安装 (pip install git+https://github.com/LLLin000/PaperForge.git) [ ] PaddleOCR API Key 已配置(在 .env 中) -[ ] 目录结构已创建(setup.py 会自动完成) +[ ] 目录结构已创建(安装向导会自动完成) [ ] Zotero 数据目录已链接到 /Zotero ``` @@ -32,7 +32,7 @@ PaperForge 采用 **两层设计**: | 层级 | 组件 | 触发方式 | 作用 | |------|------|----------|------| -| **Worker 层** | `literature_pipeline.py`(4 个 workers) | Python CLI | 后台自动化 | +| **Worker 层** | `paperforge/worker/`(7 个 worker 模块) | Python CLI | 后台自动化 | | **Agent 层** | `/pf-deep`, `/pf-paper` 命令 | 用户手动触发 | 交互式精读 | **关键区别**: @@ -93,16 +93,16 @@ Zotero 添加文献 │ ├── / │ ├── PaperForge/ -│ │ ├── exports/ ← Better BibTeX 自动导出的 JSON -│ │ │ └── library.json -│ │ ├── ocr/ ← OCR 结果(每个文献一个子目录) -│ │ │ └── ABCDEFG/ ← Zotero key 作为目录名 -│ │ │ ├── fulltext.md ← OCR 提取的全文 -│ │ │ ├── images/ ← 图表切割图片 -│ │ │ ├── meta.json ← OCR 元数据(含 ocr_status) -│ │ │ └── figure-map.json ← 图表索引(自动创建) -│ │ └── worker/scripts/ -│ │ └── literature_pipeline.py ← 核心脚本 +│ │ ├── exports/ ← Better BibTeX 自动导出的 JSON +│ │ │ └── library.json +│ │ ├── ocr/ ← OCR 结果(每个文献一个子目录) +│ │ │ └── ABCDEFG/ ← Zotero key 作为目录名 +│ │ │ ├── fulltext.md ← OCR 提取的全文 +│ │ │ ├── images/ ← 图表切割图片 +│ │ │ ├── meta.json ← OCR 元数据(含 ocr_status) +│ │ │ └── figure-map.json ← 图表索引(自动创建) +│ │ ├── indexes/ ← 索引缓存(formal-library.json 等) +│ │ └── config/ ← 领域-收藏夹映射等配置 │ └── Zotero/ ← Junction/Symlink 到 Zotero 数据目录 │ ├── / ← OpenCode Agent 配置(自动创建) @@ -125,6 +125,10 @@ Zotero 添加文献 | `//library-records/` | 文献状态跟踪(analyze, ocr_status 等) | sync(selection-sync 阶段)生成,用户修改状态 | | `/PaperForge/exports/` | Better BibTeX JSON 导出 | Zotero 自动导出 | | `/PaperForge/ocr/` | OCR 全文 + 图表切割 | ocr worker 生成 | +| `/PaperForge/indexes/` | 索引缓存(formal-library.json) | sync 生成 | +| `/PaperForge/config/` | 领域-收藏夹映射等 | 用户/安装向导配置 | +| `/PaperForge/indexes/` | 索引缓存(formal-library.json) | sync 生成 | +| `/PaperForge/config/` | 领域-收藏夹映射等 | 用户/安装向导配置 | | `/Zotero/` | Zotero 数据目录的链接 | 安装时手动创建 junction | --- @@ -145,10 +149,8 @@ Zotero 添加文献 # 仅根据现有 library-records 生成正式笔记 paperforge sync --index # Legacy (备用): - # python /PaperForge/worker/scripts/literature_pipeline.py \ - # --vault "{vault路径}" selection-sync - # python /PaperForge/worker/scripts/literature_pipeline.py \ - # --vault "{vault路径}" index-refresh + # python -m paperforge sync --selection --vault "{vault路径}" + # python -m paperforge sync --index --vault "{vault路径}" ``` ### ocr @@ -166,8 +168,7 @@ Zotero 添加文献 # 诊断模式(不运行,仅检查状态) paperforge ocr --diagnose # Legacy (备用): - # python /PaperForge/worker/scripts/literature_pipeline.py \ - # --vault "{vault路径}" ocr + # python -m paperforge ocr --vault "{vault路径}" ``` ### deep-reading @@ -180,8 +181,7 @@ Zotero 添加文献 paperforge deep-reading paperforge deep-reading --verbose # 显示阻塞条目的修复指令 # Legacy (备用): - # python /PaperForge/worker/scripts/literature_pipeline.py \ - # --vault "{vault路径}" deep-reading + # python -m paperforge deep-reading --vault "{vault路径}" ``` --- @@ -328,7 +328,7 @@ pdf_link: "[[99_System/Zotero/storage/KEY/文件名.pdf]]" --- -## 7. 第一次使用指南(手把手) +## 8. 第一次使用指南(手把手) ### Step 1: 确认 Zotero 有文献 @@ -400,7 +400,7 @@ Agent 会自动: --- -## 8. 常用命令速查 +## 9. 常用命令速查 ```bash # 检测 Zotero 新条目并生成正式笔记 @@ -459,7 +459,7 @@ paperforge doctor --- -## 9. 常见问题 +## 10. 常见问题 ### Q: 运行 sync 后没有生成 library-records? - 检查 Better BibTeX JSON 导出路径是否正确 @@ -485,7 +485,7 @@ paperforge doctor --- -## 10. 升级与维护 +## 11. 升级与维护 ### 更新 PaperForge 代码 @@ -551,7 +551,7 @@ cp -r 新下载的代码/* / --- -## 11. 命令迁移说明(v1.1 → v1.2) +## 12. 命令迁移说明(v1.1 → v1.2) 从 v1.2 开始,PaperForge 采用统一的命令接口: @@ -565,7 +565,7 @@ cp -r 新下载的代码/* / --- -## 12. 开发者指南(AI Agent 必读) +## 13. 开发者指南(AI Agent 必读) ### 版本号管理 @@ -630,7 +630,7 @@ gh release create v1.4.12 \ # 提交前运行 ruff lint + format + 一致性审计 ruff check --fix paperforge/ && ruff format paperforge/ -# 运行测试(317 tests) +# 运行测试(462 tests) pytest tests/ -q --tb=short ``` diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 05609242..41c31fca 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1416,6 +1416,7 @@ class PaperForgeSettingTab extends PluginSettingTab { // Try common install locations const progFiles = process.env.ProgramFiles || ''; const localAppData = process.env.LOCALAPPDATA || ''; + const home = process.env.USERPROFILE || process.env.HOME || ''; const zotInstallDirs = [ path.join(progFiles, 'Zotero'), path.join(progFiles, '(x86)', 'Zotero'), diff --git a/setup_wizard.py b/setup_wizard.py deleted file mode 100644 index d6254a5d..00000000 --- a/setup_wizard.py +++ /dev/null @@ -1,1521 +0,0 @@ -#!/usr/bin/env python3 -""" -PaperForge Setup Wizard (Textual Step-by-Step) -==================================================== -基于 Textual ContentSwitcher + Tree + ProgressBar 的步骤向导。 - -Usage: - python setup_wizard.py --vault /path/to/vault -""" - -from __future__ import annotations - -import argparse -import json -import os -import platform -import subprocess -import sys -import webbrowser -from pathlib import Path - -if sys.platform == "win32": - import winreg -else: - winreg = None - -from textual.app import App, ComposeResult -from textual.containers import Container, Horizontal, Vertical -from textual.message import Message -from textual.reactive import reactive -from textual.widgets import ( - Button, - ContentSwitcher, - Footer, - Header, - Markdown, - ProgressBar, - Static, - Tree, -) - -# ============================================================================= -# Agent Platform Configurations -# ============================================================================= - -AGENT_CONFIGS = { - "opencode": { - "name": "OpenCode", - "skill_dir": ".opencode/skills", - "command_dir": ".opencode/command", - "config_file": None, - }, - "cursor": {"name": "Cursor", "skill_dir": ".cursor/skills", "config_file": ".cursor/settings.json"}, - "claude": {"name": "Claude Code", "skill_dir": ".claude/skills", "config_file": ".claude/skills.json"}, - "windsurf": {"name": "Windsurf", "skill_dir": ".windsurf/skills", "config_file": None}, - "github_copilot": { - "name": "GitHub Copilot", - "skill_dir": ".github/skills", - "config_file": ".github/copilot-instructions.md", - }, - "cline": {"name": "Cline", "skill_dir": ".clinerules/skills", "config_file": ".clinerules"}, - "augment": {"name": "Augment", "skill_dir": ".augment/skills", "config_file": None}, - "trae": {"name": "Trae", "skill_dir": ".trae/skills", "config_file": None}, -} - - -# ============================================================================= -# Detection Logic (unchanged from previous version) -# ============================================================================= - - -class CheckResult: - def __init__(self, name: str): - self.name = name - self.passed = False - self.detail = "" - self.action_required = False - - -class EnvChecker: - """环境检测器""" - - def __init__(self, vault: Path): - self.vault = vault - self.manual_zotero_path: Path | None = None - self.system_dir: str = "99_System" # 可由用户自定义 - self.results: dict[str, CheckResult] = { - "python": CheckResult("Python 版本"), - "vault": CheckResult("Vault 结构"), - "zotero": CheckResult("Zotero 安装"), - "bbt": CheckResult("Better BibTeX"), - "json": CheckResult("JSON 导出"), - } - - def get_exports_dir(self) -> Path: - """Get exports directory based on user config.""" - return self.vault / self.system_dir / "PaperForge" / "exports" - - def check_python(self) -> CheckResult: - r = self.results["python"] - v = sys.version_info - if v >= (3, 8): - r.passed = True - r.detail = f"Python {v.major}.{v.minor}.{v.micro}" - else: - r.passed = False - r.detail = f"Python {v.major}.{v.minor}.{v.micro} (需要 >= 3.8)" - r.action_required = True - return r - - def check_dependencies(self) -> CheckResult: - r = CheckResult("Python 依赖") - required = {"requests": "requests", "pymupdf": "fitz", "PIL": "PIL"} - missing = [] - for pkg, import_name in required.items(): - try: - __import__(import_name) - except ImportError: - missing.append(pkg) - if not missing: - r.passed = True - r.detail = "所有依赖已安装 (requests, pymupdf, pillow)" - else: - r.passed = False - r.detail = f"缺少依赖: {', '.join(missing)}" - r.action_required = True - return r - - def install_dependencies(self) -> bool: - deps = ["requests", "pymupdf", "pillow"] - try: - subprocess.run([sys.executable, "-m", "pip", "install"] + deps, check=True, capture_output=True) - return True - except subprocess.CalledProcessError: - return False - - def check_vault(self) -> CheckResult: - r = self.results["vault"] - required = [ - f"{self.system_dir}/PaperForge/exports", - f"{self.system_dir}/PaperForge/ocr", - ] - missing = [rel for rel in required if not (self.vault / rel).exists()] - if not missing: - r.passed = True - r.detail = "所有必要目录已就绪" - else: - r.passed = False - r.detail = f"缺少: {', '.join(missing)}" - r.action_required = True - return r - - def _find_zotero(self, manual_path: Path | None = None) -> Path | None: - # 如果提供了手动路径,优先使用 - if manual_path and manual_path.exists(): - return manual_path - - system = platform.system() - if system == "Windows": - # ...existing detection code... - # 1. 注册表检测 (HKEY_LOCAL_MACHINE) - try: - with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Zotero") as key: - install_dir, _ = winreg.QueryValueEx(key, "InstallDir") - path = Path(install_dir) / "zotero.exe" - if path.exists(): - return path - except (FileNotFoundError, OSError): - pass - # 2. 注册表检测 (HKEY_CURRENT_USER - 用户级安装) - try: - with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"SOFTWARE\Zotero") as key: - install_dir, _ = winreg.QueryValueEx(key, "InstallDir") - path = Path(install_dir) / "zotero.exe" - if path.exists(): - return path - except (FileNotFoundError, OSError): - pass - # 3. 常见安装路径检测 - search_paths = [ - Path(os.environ.get("PROGRAMFILES", r"C:\Program Files")) / "Zotero" / "zotero.exe", - Path(os.environ.get("PROGRAMFILES(X86)", r"C:\Program Files (x86)")) / "Zotero" / "zotero.exe", - Path(os.environ.get("LOCALAPPDATA", r"C:\Users\%USERNAME%\AppData\Local")) / "Zotero" / "zotero.exe", - Path.home() / "AppData" / "Local" / "Zotero" / "zotero.exe", - Path.home() / "scoop" / "apps" / "zotero" / "current" / "zotero.exe", # Scoop安装 - ] - for p in search_paths: - if p.exists(): - return p - # 4. 通过 where 命令检测 - try: - result = subprocess.run(["where", "zotero"], capture_output=True, text=True, timeout=5) - if result.returncode == 0: - for line in result.stdout.strip().split("\n"): - p = Path(line.strip()) - if p.exists(): - return p - except Exception: - pass - elif system == "Darwin": - search_paths = [ - Path("/Applications/Zotero.app/Contents/MacOS/zotero"), - Path.home() / "Applications" / "Zotero.app" / "Contents" / "MacOS" / "zotero", - ] - for p in search_paths: - if p.exists(): - return p - # 通过 which 检测 - try: - result = subprocess.run(["which", "zotero"], capture_output=True, text=True, timeout=5) - if result.returncode == 0: - return Path(result.stdout.strip()) - except Exception: - pass - else: - # Linux - search_paths = [ - Path.home() / ".local" / "share" / "zotero" / "zotero", - Path("/usr/bin/zotero"), - Path("/usr/local/bin/zotero"), - Path("/snap/bin/zotero"), - ] - for p in search_paths: - if p.exists(): - return p - try: - result = subprocess.run(["which", "zotero"], capture_output=True, text=True, timeout=5) - if result.returncode == 0: - return Path(result.stdout.strip()) - except Exception: - pass - return None - - def check_zotero(self) -> CheckResult: - r = self.results["zotero"] - path = self._find_zotero() - if path: - r.passed = True - r.detail = str(path) - else: - r.passed = False - r.detail = "未找到 Zotero" - r.action_required = True - return r - - def check_bbt(self) -> CheckResult: - r = self.results["bbt"] - system = platform.system() - bbt_found = False - bbt_path = None - - if system == "Windows": - appdata = os.environ.get("APPDATA", "") - if appdata: - profiles = Path(appdata) / "Zotero" / "Zotero" / "Profiles" - if profiles.exists(): - for profile in profiles.iterdir(): - if profile.is_dir(): - ext_dir = profile / "extensions" - if ext_dir.exists(): - for ext in ext_dir.iterdir(): - if "better-bibtex" in ext.name.lower() or "betterbibtex" in ext.name.lower(): - bbt_found = True - bbt_path = ext - break - elif system == "Darwin": - profiles = Path.home() / "Library" / "Application Support" / "Zotero" / "Profiles" - if profiles.exists(): - for profile in profiles.iterdir(): - ext_dir = profile / "extensions" - if ext_dir.exists(): - for ext in ext_dir.iterdir(): - if "better-bibtex" in ext.name.lower(): - bbt_found = True - bbt_path = ext - break - else: - profiles = Path.home() / ".zotero" / "zotero" / "Profiles" - if profiles.exists(): - for profile in profiles.iterdir(): - ext_dir = profile / "extensions" - if ext_dir.exists(): - for ext in ext_dir.iterdir(): - if "better-bibtex" in ext.name.lower(): - bbt_found = True - bbt_path = ext - break - - if bbt_found: - r.passed = True - r.detail = bbt_path.name if bbt_path else "Better BibTeX" - else: - r.passed = False - r.detail = "未找到 Better BibTeX 插件" - r.action_required = True - return r - - def check_json(self) -> CheckResult: - r = self.results["json"] - exports_dir = self.get_exports_dir() - if not exports_dir.exists(): - r.passed = False - r.detail = f"导出目录不存在: {exports_dir}" - r.action_required = True - return r - - json_files = list(exports_dir.glob("*.json")) - if not json_files: - r.passed = False - r.detail = "未找到 JSON 导出文件" - r.action_required = True - return r - - valid = [] - for jf in json_files: - try: - data = json.loads(jf.read_text(encoding="utf-8")) - # Better BibTeX JSON 是 dict 格式(含 items),也兼容 list 格式 - if isinstance(data, dict) and data.get("items") or isinstance(data, list) and len(data) > 0: - valid.append(jf.name) - except Exception: - pass - - if valid: - r.passed = True - r.detail = f"找到 {len(valid)} 个有效 JSON" - else: - r.passed = False - r.detail = "JSON 文件格式无效" - r.action_required = True - return r - - -# ============================================================================= -# Step Screens -# ============================================================================= - -STEP_TITLES = [ - "欢迎使用 PaperForge", - "选择 AI Agent 平台", - "检查 Python 与依赖", - "检查 Vault 结构", - "安装 Zotero 与链接", - "安装 Better BibTeX", - "配置 JSON 导出", - "部署工作流脚本", - "安装完成", -] - -STEP_IDS = [f"step-{i}" for i in range(len(STEP_TITLES))] - - -class StepScreen(Static): - """单个步骤页面基类""" - - def __init__(self, step_id: str, checker: EnvChecker, **kwargs): - kwargs.setdefault("id", step_id) - super().__init__(**kwargs) - self.step_id = step_id - self.checker = checker - self.step_idx = int(step_id.split("-")[1]) - - def compose(self) -> ComposeResult: - yield Static(f"## {STEP_TITLES[self.step_idx]}", classes="step-title") - yield Static("", id=f"{self.step_id}-status", classes="status-bar") - - def set_status(self, text: str, success: bool | None = None) -> None: - status = self.query_one(f"#{self.step_id}-status", Static) - if success is True: - status.update(f"[green]✓ {text}[/]") - elif success is False: - status.update(f"[red]✗ {text}[/]") - else: - status.update(text) - - -class WelcomeStep(StepScreen): - """Step 0: 欢迎页""" - - def compose(self) -> ComposeResult: - yield from super().compose() - yield Static( - r""" - ______ ___ ______ _________________ ___________ _____ _____ - | ___ \/ _ \ | ___ \ ___| ___ \ ___| _ | ___ \ __ \| ___| - | |_/ / /_\ \| |_/ / |__ | |_/ / |_ | | | | |_/ / | \/| |__ - | __/| _ || __/| __|| /| _| | | | | /| | __ | __| - | | | | | || | | |___| |\ \| | \ \_/ / |\ \| |_\ \| |___ - \_| \_| |_/\_| \____/\_| \_\_| \___/\_| \_|\____/\____/ - - [+] Forge Your Knowledge Into Power [+] - """, - classes="logo", - ) - yield Markdown(""" -**PaperForge** 是一个连接 Zotero 与 Obsidian 的文献工作流工具。 - -安装向导将引导你完成以下配置: - -1. 确认 Python 版本 (>= 3.8) -2. 配置 Vault 目录结构 -3. 创建 Zotero 数据链接 -4. 安装 Better BibTeX 插件 -5. 配置 JSON 自动导出 -6. 部署工作流文件 - -点击 **开始安装** 继续。 - """) - yield Button("▶ 开始安装", id="btn-start", variant="primary") - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "btn-start": - self.app.post_message(StepPassed(self.step_idx)) - - -class AgentPlatformStep(StepScreen): - """Step 1: 选择 AI Agent 平台""" - - def compose(self) -> ComposeResult: - yield from super().compose() - yield Markdown(""" -PaperForge 需要知道你使用哪个 **AI Agent** 来执行精读命令。 - -这将决定 Skill 文件安装到哪里: -- **OpenCode** -> `.opencode/skills/` -- **Cursor** -> `.cursor/skills/` -- **Claude Code** -> `.claude/skills/` -- 其他... - -选择你的 Agent 平台: - """) - for i, (key, cfg) in enumerate(AGENT_CONFIGS.items()): - yield Button( - f"{i+1}. {cfg['name']}", - id=f"btn-agent-{key}", - variant="default", - ) - - def on_button_pressed(self, event: Button.Pressed) -> None: - btn_id = event.button.id - if btn_id and btn_id.startswith("btn-agent-"): - agent_key = btn_id.replace("btn-agent-", "") - cfg = AGENT_CONFIGS.get(agent_key) - if cfg: - # 保存选择到 app - self.app.agent_config = cfg - self.app.agent_key = agent_key - self.set_status(f"已选择: {cfg['name']}", True) - self.app.post_message(StepPassed(self.step_idx)) - - -class PythonStep(StepScreen): - """Step 2: Python 版本与依赖""" - - def compose(self) -> ComposeResult: - yield from super().compose() - yield Markdown(""" -PaperForge 需要 **Python 3.8+** 以及以下 Python 包: -- `requests` — HTTP 请求 -- `pymupdf` — PDF 处理 -- `pillow` — 图像处理 - -点击 **一键检测** 检查环境。 - """) - yield Horizontal( - Button("🔍 一键检测", id="btn-check-python", variant="primary"), - Button("📦 安装依赖", id="btn-install-deps", variant="default"), - Button("⬇ 下载 Python", id="btn-dl-python", variant="default"), - id="btn-row", - ) - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "btn-check-python": - py_result = self.checker.check_python() - self.set_status(f"Python: {py_result.detail}", py_result.passed) - if py_result.passed: - dep_result = self.checker.check_dependencies() - self.set_status(f"依赖: {dep_result.detail}", dep_result.passed) - if dep_result.passed: - self.app.post_message(StepPassed(self.step_idx)) - elif event.button.id == "btn-install-deps": - if self.checker.install_dependencies(): - self.set_status("依赖安装成功", True) - # 重新检测 - dep_result = self.checker.check_dependencies() - if dep_result.passed: - self.app.post_message(StepPassed(self.step_idx)) - else: - self.set_status("依赖安装失败,请手动运行: pip install requests pymupdf pillow", False) - elif event.button.id == "btn-dl-python": - webbrowser.open("https://www.python.org/downloads/") - - -class VaultStep(StepScreen): - """Step 3: Vault 目录结构配置""" - - def __init__(self, step_id: str, checker: EnvChecker, vault: str = "", **kwargs): - kwargs.setdefault("id", step_id) - super().__init__(step_id=step_id, checker=checker, **kwargs) - self.step_id = step_id - self.checker = checker - self.step_idx = int(step_id.split("-")[1]) - self._vault = vault - - def compose(self) -> ComposeResult: - yield from super().compose() - yield Markdown(""" -PaperForge 需要知道你的 **Obsidian Vault 位置**,以及你想要的目录结构。 - -你可以保留默认名称,也可以自定义。 - """) - from textual.widgets import Input - - yield Static("Obsidian Vault 路径 (绝对路径):", classes="step-title") - yield Input(value=self._vault, placeholder="D:\\Documents\\MyVault", id="input-vault-path") - yield Static("", id="vault-error", classes="status-bar") - yield Markdown(""" ---- - -**默认目录结构:** -``` -Obsidian Vault/ -├── 资源文件夹/ ← 文献笔记和资源 -│ ├── 文献子文件夹/ ← 存放正式文献卡片 -│ └── 文献索引/ ← 状态跟踪 -└── 系统文件夹/ ← 系统文件 - └── PaperForge/ ← 导出和 OCR -``` - -修改下方名称(留空使用默认值): - """) - yield Static("系统文件夹名称:", classes="step-title") - yield Input(value="99_System", id="input-system-dir") - yield Static("资源文件夹名称:", classes="step-title") - yield Input(value="03_Resources", id="input-resources-dir") - yield Static("文献子文件夹名称:", classes="step-title") - yield Input(value="Literature", id="input-literature-dir") - yield Static("文献索引文件夹名称:", classes="step-title") - yield Input(value="LiteratureControl", id="input-control-dir") - yield Static("Obsidian Base 文件夹名称:", classes="step-title") - yield Input(value="05_Bases", id="input-base-dir") - yield Horizontal( - Button("✓ 确认并创建目录", id="btn-setup-vault", variant="primary"), - id="btn-row", - ) - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "btn-setup-vault": - from textual.widgets import Input - - # 获取 Vault 路径 - vault_input = self.query_one("#input-vault-path", Input).value.strip() - if not vault_input: - self.query_one("#vault-error", Static).update("[red]请填写 Obsidian Vault 的绝对路径[/]") - return - - vault_path = Path(vault_input) - if not vault_path.exists(): - self.query_one("#vault-error", Static).update(f"[red]该目录不存在: {vault_path}[/]") - return - - # 更新 app 的 vault 路径 - self.app.vault = vault_path - self.checker.vault = vault_path - - # 获取目录名称 - system_dir = self.query_one("#input-system-dir", Input).value.strip() or "99_System" - resources_dir = self.query_one("#input-resources-dir", Input).value.strip() or "03_Resources" - literature_dir = self.query_one("#input-literature-dir", Input).value.strip() or "Literature" - control_dir = self.query_one("#input-control-dir", Input).value.strip() or "LiteratureControl" - base_dir = self.query_one("#input-base-dir", Input).value.strip() or "05_Bases" - - # 解析路径(支持名称、相对路径、绝对路径) - def resolve_path(base: Path, path_str: str) -> Path: - p = Path(path_str) - if p.is_absolute(): - return p - else: - return base / p - - system_path = resolve_path(vault_path, system_dir) - resources_path = resolve_path(vault_path, resources_dir) - base_path = resolve_path(vault_path, base_dir) - - # 保存配置 - self.app.vault_config = { - "vault_path": str(vault_path), - "system_dir": system_dir, - "resources_dir": resources_dir, - "literature_dir": literature_dir, - "control_dir": control_dir, - "base_dir": base_dir, - "paperforge_path": str(system_path / "PaperForge"), - "literature_path": str(resources_path / literature_dir), - "base_path": str(base_path), - } - - # 创建目录 - dirs_to_create = [ - resources_path / control_dir / "library-records", - base_path, - system_path / "PaperForge" / "exports", - system_path / "PaperForge" / "ocr", - ] - - created = [] - for d in dirs_to_create: - d.mkdir(parents=True, exist_ok=True) - created.append(str(d.relative_to(vault_path))) - - # 同步更新 checker - self.checker.system_dir = system_dir - - self.query_one("#vault-error", Static).update("") - self.set_status(f"已创建 {len(created)} 个目录", True) - self.app.post_message(StepPassed(self.step_idx)) - - -class ZoteroStep(StepScreen): - """Step 4: Zotero 数据目录链接""" - - def compose(self) -> ComposeResult: - yield from super().compose() - # 自动检测 Zotero 数据目录 - detected = self._detect_zotero_data() - default_value = str(detected) if detected else "" - - yield Markdown(""" -**Zotero 数据目录**存放了你的文献数据库和 PDF 附件。 - -向导将创建目录链接,让 PaperForge 能读取你的 PDF: -``` -你的 Vault/ - └── [系统目录]/ - └── Zotero/ ← 链接(自动创建) - ↓ junction - 你的 Zotero 数据目录/ ← 你填这里 - ├── zotero.sqlite - └── storage/ -``` - -**请填写你的 Zotero 数据目录路径:** -- 这是 Zotero 存放数据库的地方,不是 Vault 里的路径 -- Windows 通常是 `C:/Users/你的用户名/Zotero` -- macOS 通常是 `~/Zotero` - -> ⚠️ **不要**填 Vault 里面的路径,也不要在这里创建新文件夹 - """) - from textual.widgets import Input - - username = os.environ.get("USERNAME", os.environ.get("USER", "YourName")) - yield Static("Zotero 数据目录:", classes="step-title") - yield Input( - value=default_value, - placeholder=f"C:/Users/{username}/Zotero", - id="input-zotero-data", - ) - yield Horizontal( - Button("🔗 创建目录链接", id="btn-link-zotero", variant="primary"), - Button("⬇ 下载 Zotero", id="btn-dl-zotero", variant="default"), - id="btn-row", - ) - - def _detect_zotero_data(self) -> Path | None: - """Build default Zotero data path from current username.""" - home = Path.home() - # Default: C:/Users//Zotero (Windows) or ~/Zotero (Unix) - default = home / "Zotero" - if default.exists() and (default / "zotero.sqlite").exists(): - return default - return default # Return anyway as default value - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "btn-link-zotero": - from textual.widgets import Input - - path_str = self.query_one("#input-zotero-data", Input).value.strip() - if not path_str: - self.set_status("请填写 Zotero 数据目录路径", False) - return - - zotero_data = Path(path_str) - if not zotero_data.exists(): - self.set_status("路径不存在,请检查路径是否正确", False) - return - if not (zotero_data / "zotero.sqlite").exists(): - self.set_status("未找到 zotero.sqlite,请确认这是 Zotero 数据目录", False) - return - if not (zotero_data / "storage").exists(): - self.set_status("未找到 storage 文件夹,请确认这是 Zotero 数据目录", False) - return - - # 检查是否在 Vault 内部 - vault = self.checker.vault - is_inside_vault = False - try: - zotero_data.resolve().relative_to(vault.resolve()) - is_inside_vault = True - except ValueError: - pass - - if is_inside_vault: - # 在 Vault 内部,直接通过 - self.app.zotero_data_dir = str(zotero_data) - self.set_status("Zotero 数据目录已确认", True) - self.app.post_message(StepPassed(self.step_idx)) - return - - # 在 Vault 外部,创建 Junction - system_dir = getattr(self.app, "vault_config", {}).get("system_dir", "99_System") - junction_path = vault / system_dir / "Zotero" - - # Remove existing - if junction_path.exists() or junction_path.is_symlink(): - try: - if sys.platform == "win32": - subprocess.run(["cmd", "/c", "rmdir", str(junction_path)], check=True, capture_output=True) - else: - junction_path.unlink() - except Exception: - pass - - try: - junction_path.parent.mkdir(parents=True, exist_ok=True) - if sys.platform == "win32": - subprocess.run( - ["cmd", "/c", "mklink", "/J", str(junction_path), str(zotero_data)], - check=True, - capture_output=True, - shell=False, - ) - else: - junction_path.symlink_to(zotero_data, target_is_directory=True) - self.app.zotero_data_dir = str(zotero_data) - self.app.zotero_link = str(junction_path) - self.set_status("链接已创建", True) - self.app.post_message(StepPassed(self.step_idx)) - except Exception as e: - self.set_status(f"创建链接失败: {e}", False) - - elif event.button.id == "btn-dl-zotero": - webbrowser.open("https://www.zotero.org/download/") - - -class BBTStep(StepScreen): - """Step 5: Better BibTeX""" - - def compose(self) -> ComposeResult: - yield from super().compose() - yield Markdown(""" -**Better BibTeX (BBT)** 是 Zotero 的插件,用于生成 citation key 和自动导出 JSON。 - -**安装步骤:** -1. 下载 BBT 插件 -2. Zotero → 工具 → 插件 -3. 齿轮图标 → Install Plugin From File... -4. 选择下载的 `.xpi` 文件 → 重启 Zotero - -安装完成后点击 **检测**。 - """) - yield Horizontal( - Button("🔍 自动检测", id="btn-check-bbt", variant="primary"), - Button("⬇ 下载 BBT", id="btn-dl-bbt", variant="default"), - Button("📷 查看安装截图", id="btn-img-bbt", variant="default"), - id="btn-row", - ) - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "btn-check-bbt": - result = self.checker.check_bbt() - self.set_status(result.detail, result.passed) - if result.passed: - self.app.post_message(StepPassed(self.step_idx)) - elif event.button.id == "btn-dl-bbt": - webbrowser.open("https://retorque.re/zotero-better-bibtex/") - elif event.button.id == "btn-img-bbt": - self.app.open_screenshot("bbt-install.png") - - -class JsonStep(StepScreen): - """Step 5: JSON 导出配置""" - - def compose(self) -> ComposeResult: - yield from super().compose() - system_dir = getattr(self.app, "vault_config", {}).get("system_dir", "99_System") - yield Markdown(f""" -**Better BibTeX 自动导出**是 PaperForge 的数据来源。 - -**配置步骤:** -1. Zotero → 文件 → 导出库... -2. 格式选择 **Better BibTeX** -3. 保存到:`{system_dir}/PaperForge/exports/` -4. **[必须] 勾选 "保持更新"** — 这是自动同步的关键! - -> ⚠️ **重要**:如果不勾选"保持更新",Zotero 新增文献后 PaperForge 不会自动发现,需要每次手动重新导出。 - -📁 **子分类与 Base 管理** - -你可以根据 Zotero 收藏夹结构,灵活决定如何导出: - -**方案 A:分开管理(推荐)** -为每个子分类分别创建 JSON: -``` -Zotero 收藏夹结构 -├── 骨科 -│ ├── 关节外科 → 导出为 orthopedic-joint.json -│ └── 脊柱外科 → 导出为 orthopedic-spine.json -└── 运动医学 - └── 膝盖损伤 → 导出为 sports-knee.json -``` -每个 JSON 对应一个独立的 Obsidian Base 视图,可分别配置 OCR 和精读队列。 - -**方案 B:统一管理** -直接导出整个父级收藏夹: -``` -骨科(含所有子分类) → 导出为 orthopedic.json -``` -适合分类较少、希望统一查看的场景。 - -> 💡 建议:先试用方案 A,分类多了再调整。 - """) - yield Horizontal( - Button("🔍 自动检测", id="btn-check-json", variant="primary"), - id="btn-row", - ) - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "btn-check-json": - result = self.checker.check_json() - self.set_status(result.detail, result.passed) - if result.passed: - self.app.post_message(StepPassed(self.step_idx)) - - -class DeployStep(StepScreen): - """Step 7: 部署脚本和配置""" - - def compose(self) -> ComposeResult: - yield from super().compose() - yield Markdown(""" -**最后一步:部署工作流脚本和配置文件。** - -向导将自动完成以下操作: -1. **复制脚本** — 将 pipeline worker 脚本部署到你的 Vault -2. **创建 .env** — 配置文件存放 API Key(PaddleOCR 等) -3. **创建 paperforge.json** — 版本和路径配置 -4. **安装命令** — 为 Agent 添加快捷命令 - -这些操作不会覆盖你的数据文件。 - """) - from textual.widgets import Input - - yield Static("PaddleOCR API Key:", classes="step-title") - yield Input(placeholder="粘贴你的 PaddleOCR API Key", id="input-api-key") - yield Static("PaddleOCR API URL:", classes="step-title") - yield Input( - value="https://paddleocr.aistudio-app.com/api/v2/ocr/jobs", - placeholder="https://paddleocr.aistudio-app.com/api/v2/ocr/jobs", - id="input-api-url", - ) - yield Horizontal( - Button("🚀 一键部署", id="btn-deploy", variant="primary"), - id="btn-row", - ) - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "btn-deploy": - # 安全检查:前面步骤必须全部通过 - step_states = self.app.step_states - required_steps = [ - (1, "选择 Agent 平台"), - (2, "Python 环境检查"), - (3, "Vault 目录配置"), - (4, "Zotero 数据目录链接"), - (5, "Better BibTeX 插件"), - (6, "JSON 导出配置"), - ] - incomplete = [] - for idx, name in required_steps: - if not step_states[idx]: - incomplete.append(f"步骤 {idx}: {name}") - - if incomplete: - self.set_status( - "[无法部署] 以下步骤未完成:\n" + "\n".join(incomplete) + "\n请先返回并完成上述步骤", False - ) - return - - self.set_status("正在部署...", None) - success = self._deploy() - if success: - self.set_status("部署完成!", True) - self.app.post_message(StepPassed(self.step_idx)) - else: - self.set_status("部署过程中出现错误,请检查上方日志", False) - - def _deploy(self) -> bool: - """Deploy scripts and create config files.""" - vault = self.checker.vault - vault_config = getattr(self.app, "vault_config", {}) - system_dir = vault_config.get("system_dir", "99_System") - resources_dir = vault_config.get("resources_dir", "03_Resources") - literature_dir = vault_config.get("literature_dir", "Literature") - control_dir = vault_config.get("control_dir", "LiteratureControl") - base_dir = vault_config.get("base_dir", "05_Bases") - - def apply_user_paths(text: str, skill_dir_value: str = "") -> str: - agent_config_dir = str(Path(skill_dir_value or ".opencode/skills").parent).replace("\\", "/") - replacements = { - "": system_dir, - "": resources_dir, - "": literature_dir, - "": control_dir, - "": base_dir, - "": skill_dir_value, - "": agent_config_dir, - "99_System/PaperForge": f"{system_dir}/PaperForge", - "99_System\\PaperForge": f"{system_dir}\\PaperForge", - "99_System/Zotero": f"{system_dir}/Zotero", - "99_System\\Zotero": f"{system_dir}\\Zotero", - "03_Resources/LiteratureControl": f"{resources_dir}/{control_dir}", - "03_Resources\\LiteratureControl": f"{resources_dir}\\{control_dir}", - "03_Resources/Literature": f"{resources_dir}/{literature_dir}", - "03_Resources\\Literature": f"{resources_dir}\\{literature_dir}", - ".opencode/skills": skill_dir_value or ".opencode/skills", - ".opencode\\skills": (skill_dir_value or ".opencode/skills").replace("/", "\\"), - } - for old, new in replacements.items(): - text = text.replace(old, new) - return text - - # 1. 获取 agent 配置 - agent_config = getattr(self.app, "agent_config", None) - if not agent_config: - self.set_status("错误:未选择 Agent 平台", False) - return False - - skill_dir = agent_config.get("skill_dir", ".opencode/skills") - - # 2. 确定安装包根目录(wizard 所在目录的父目录) - wizard_dir = Path(__file__).parent.resolve() - # 如果 wizard 在 github-release/ 下,repo_root 就是 github-release/ - # 如果 wizard 在 scripts/ 下,repo_root 是父目录 - if (wizard_dir / "paperforge").exists(): - repo_root = wizard_dir - elif (wizard_dir.parent / "paperforge").exists(): - repo_root = wizard_dir.parent - elif (wizard_dir / "pipeline").exists(): - repo_root = wizard_dir - elif (wizard_dir.parent / "pipeline").exists(): - repo_root = wizard_dir.parent - else: - self.set_status(f"错误:找不到安装包文件。请在 PaperForge 解压目录下运行此向导。当前: {wizard_dir}", False) - return False - - # 3. 创建目录(使用用户自定义路径) - 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", - ] - for d in dirs: - d.mkdir(parents=True, exist_ok=True) - - # 4. 复制脚本(从安装包到 Vault) - import shutil - - # Copy worker modules to PaperForge/worker/scripts/ - worker_src = repo_root / "paperforge/worker/sync.py" - worker_dst = pf_path / "worker/scripts/sync.py" - if worker_src.exists(): - import shutil - - shutil.copy2(worker_src, worker_dst) - # Also copy other worker modules - for mod in [ - "ocr.py", - "repair.py", - "status.py", - "deep_reading.py", - "update.py", - "base_views.py", - "__init__.py", - ]: - mod_src = repo_root / "paperforge/worker" / mod - if mod_src.exists(): - shutil.copy2(mod_src, pf_path / "worker/scripts" / mod) - else: - # Fallback to old pipeline location for backward compatibility - worker_src = repo_root / "pipeline/worker/scripts/literature_pipeline.py" - worker_dst = pf_path / "worker/scripts/literature_pipeline.py" - if worker_src.exists(): - shutil.copy2(worker_src, worker_dst) - else: - self.set_status(f"错误:找不到 worker 脚本: {worker_src}", False) - return False - - # Copy ld_deep.py (prefer paperforge/skills, fallback to skills/) - ld_src = repo_root / "paperforge/skills/literature-qa/scripts/ld_deep.py" - if not ld_src.exists(): - ld_src = repo_root / "skills/literature-qa/scripts/ld_deep.py" - ld_dst = vault / skill_dir / "literature-qa/scripts/ld_deep.py" - if ld_src.exists(): - shutil.copy2(ld_src, ld_dst) - else: - self.set_status(f"错误:找不到 ld_deep.py: {ld_src}", False) - return False - - # Copy subagent prompt (prefer paperforge/skills, fallback to skills/) - prompt_src = repo_root / "paperforge/skills/literature-qa/prompt_deep_subagent.md" - if not prompt_src.exists(): - prompt_src = repo_root / "skills/literature-qa/prompt_deep_subagent.md" - prompt_dst = vault / skill_dir / "literature-qa/prompt_deep_subagent.md" - if prompt_src.exists(): - shutil.copy2(prompt_src, prompt_dst) - else: - self.set_status(f"错误:找不到 prompt_deep_subagent.md: {prompt_src}", False) - return False - - # Copy chart-reading guides (prefer paperforge/skills, fallback to skills/) - chart_src = repo_root / "paperforge/skills/literature-qa/chart-reading" - if not chart_src.exists(): - chart_src = repo_root / "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) - - # Copy OpenCode command files when the target platform supports them. - if getattr(self.app, "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 = apply_user_paths(f.read_text(encoding="utf-8"), skill_dir) - (command_dst / f.name).write_text(text, encoding="utf-8") - - # Copy user-facing docs. AGENTS.md is regenerated below with the chosen paths. - docs_src = repo_root / "docs" - docs_dst = vault / "docs" - if docs_src.exists() and docs_src.is_dir(): - shutil.copytree(docs_src, docs_dst, dirs_exist_ok=True) - for doc in docs_dst.rglob("*.md"): - doc.write_text(apply_user_paths(doc.read_text(encoding="utf-8"), skill_dir), encoding="utf-8") - - # 5. 创建 .env(放到 PaperForge 目录下) - from textual.widgets import Input - - api_key = self.query_one("#input-api-key", Input).value.strip() - api_url = ( - self.query_one("#input-api-url", Input).value.strip() - or "https://paddleocr.aistudio-app.com/api/v2/ocr/jobs" - ) - - if not api_key: - self.set_status("请填写 PaddleOCR API Key", False) - return False - - # 把 .env 放在 PaperForge 目录下 - env_path = pf_path / ".env" - env_content = f"""# PaperForge 配置文件 -# PaddleOCR API Token(从 https://paddleocr.baidu.com 获取) -PADDLEOCR_API_TOKEN={api_key} - -# PaddleOCR API 地址 -PADDLEOCR_JOB_URL={api_url} - -# PaddleOCR 模型(通常不需要修改) -PADDLEOCR_MODEL=PaddleOCR-VL-1.5 - -# Zotero data directory selected during setup -ZOTERO_DATA_DIR={getattr(self.app, 'zotero_data_dir', '')} -""" - env_path.write_text(env_content, encoding="utf-8") - - # Create a minimal domain mapping. The worker will keep this usable even - # before JSON exports exist, and will infer domains from export filenames. - 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", - ) - - # 6. 创建 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.2.0"), - "agent_platform": agent_config.get("name", "OpenCode"), - "agent_key": getattr(self.app, "agent_key", "opencode"), - "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": getattr(self.app, "zotero_data_dir", ""), - "zotero_link": getattr(self.app, "zotero_link", f"{system_dir}/Zotero"), - "vault_config": { - "system_dir": system_dir, - "resources_dir": resources_dir, - "literature_dir": literature_dir, - "control_dir": control_dir, - "base_dir": base_dir, - }, - } - ) - pf_json.write_text(json.dumps(existing_config, indent=2, ensure_ascii=False), encoding="utf-8") - - agents_src = repo_root / "AGENTS.md" - agents_dst = vault / "AGENTS.md" - if agents_src.exists(): - agents_text = apply_user_paths(agents_src.read_text(encoding="utf-8"), skill_dir) - agents_dst.write_text(agents_text, encoding="utf-8") - - # 7. 验证文件完整性 - self.set_status("验证文件完整性...", None) - checks = { - "Worker 脚本": worker_dst.exists(), - "精读脚本": ld_dst.exists(), - "精读提示词": prompt_dst.exists(), - "目录结构": (vault / resources_dir / control_dir / "library-records").exists(), - "Base 目录": (vault / base_dir).exists(), - "分类配置": domain_config.exists(), - "导出目录": (pf_path / "exports").exists(), - "OCR 目录": (pf_path / "ocr").exists(), - } - - missing = [k for k, v in checks.items() if not v] - if missing: - self.set_status(f"验证失败: {', '.join(missing)}", False) - return False - - self.set_status("文件验证通过,初始化系统...", True) - - # 8. 运行初始化命令(模拟测试) - try: - # 测试 selection-sync(会报错因为没 JSON,但测试脚本能否运行) - result = subprocess.run( - [sys.executable, str(worker_dst), "--vault", str(vault), "status"], - capture_output=True, - text=True, - timeout=10, - ) - if result.returncode == 0: - self.set_status("工作流脚本运行正常", True) - else: - self.set_status(f"脚本测试警告: {result.stderr[:100]}", None) - except Exception as e: - self.set_status(f"脚本测试跳过: {e}", None) - - # 9. 安装 PaperForge 工具包(让 paperforge 命令全局可用) - self.set_status("安装 PaperForge 工具包...", None) - try: - result = subprocess.run( - [sys.executable, "-m", "pip", "install", "-e", str(repo_root)], - capture_output=True, - text=True, - timeout=60, - ) - if result.returncode == 0: - self.set_status("PaperForge 工具包安装完成(paperforge 命令已全局注册)", True) - else: - stderr = result.stderr[:200] if result.stderr else "" - self.set_status(f"pip install 警告(paperforge 命令可能需要手动注册): {stderr}", None) - except Exception as e: - self.set_status(f"pip install 跳过: {e}", None) - - return True - - -class DoneStep(StepScreen): - """Step 8: 完成页""" - - def compose(self) -> ComposeResult: - yield from super().compose() - vault_config = getattr(self.app, "vault_config", {}) - system_dir = vault_config.get("system_dir", "99_System") - worker_cmd = "python -m paperforge sync --vault ." - yield Markdown(f""" -## 安装完成! - -PaperForge 已完成安装和初始化。以下是立即开始使用的步骤: - -### 首次使用步骤: - -**1. 同步 Zotero 文献并生成正式笔记** -```bash -paperforge sync -``` - -如需分阶段执行: -```bash -paperforge sync --selection # 仅同步 Zotero 到 library-records -paperforge sync --index # 仅根据现有 library-records 生成正式笔记 -``` - -**2. 标记精读文献** -在 Obsidian 中打开 library-records 文件,设置: -- `do_ocr: true` -- `analyze: true` - -**3. 运行 OCR** -```bash -paperforge ocr -``` - -如需诊断模式(不实际运行): -```bash -paperforge ocr --diagnose -``` - -**4. 执行精读** -在 OpenCode Agent 中输入: -``` -/pf-deep -``` - -### 已安装的 Agent 命令 - -安装向导已自动将以下命令安装到你的 Agent 中: - -**精读命令:** -| 命令 | 作用 | -|------|------| -| `/pf-deep ` | 完整三阶段精读 | -| `/pf-paper ` | 快速摘要 | - -**Worker 快捷命令:** -| 命令 | 作用 | -|------|------| -| `/pf-sync` | 同步 Zotero 文献并生成正式笔记 | -| `/pf-ocr` | 运行 PDF OCR | -| `/pf-status` | 查看工作流状态 | - -**paperforge 命令(推荐):** -```bash -paperforge status # 查看状态 -paperforge sync # 同步文献并生成笔记 -paperforge ocr # 运行 OCR -paperforge deep-reading # 查看精读队列 -``` - -> **命令迁移说明(v1.1 → v1.2)**:旧命令 `paperforge selection-sync`、`paperforge index-refresh`、`paperforge ocr run` 以及 `/LD-deep`、`/LD-paper`、 `/lp-*` 在 v1.2 中仍兼容,但文档已统一使用新命令名。 - -**Python 脚本命令(备用):** -```bash -{worker_cmd} -``` -| 命令 | 作用 | -|------|------| -| `selection-sync` | 检测新文献 | -| `index-refresh` | 生成正式笔记 | -| `ocr` | PDF OCR | -| `deep-reading` | 查看精读队列 | -| `update` | 检查更新 | - -### 详细文档 - -- 安装后指南:`AGENTS.md` -- GitHub: https://github.com/LLLin000/PaperForge - """) - yield Horizontal( - Button("📖 打开详细指南", id="btn-open-guide", variant="primary"), - Button("🔄 重新检测", id="btn-restart", variant="default"), - id="btn-row", - ) - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "btn-open-guide": - guide_path = self.checker.vault / "docs" / "setup-guide.md" - if guide_path.exists(): - if sys.platform == "win32": - os.startfile(str(guide_path)) - elif sys.platform == "darwin": - subprocess.run(["open", str(guide_path)]) - else: - subprocess.run(["xdg-open", str(guide_path)]) - else: - webbrowser.open("https://github.com/LLLin000/PaperForge/blob/master/docs/setup-guide.md") - elif event.button.id == "btn-restart": - self.app.post_message(RestartWizard()) - - -# ============================================================================= -# Custom Messages -# ============================================================================= - - -class StepPassed(Message): - """步骤通过消息""" - - def __init__(self, step_idx: int): - super().__init__() - self.step_idx = step_idx - - -class RestartWizard(Message): - """重新开始消息""" - - def __init__(self): - super().__init__() - - -# ============================================================================= -# Main App -# ============================================================================= - - -class SetupWizardApp(App): - """PaperForge 安装向导主应用""" - - CSS = """ - Screen { align: center middle; } - - .wizard-container { width: 95%; height: 95%; border: solid green; } - - .sidebar { width: 25%; height: 100%; border: solid gray; padding: 1; } - .sidebar-title { text-align: center; text-style: bold; color: cyan; padding: 1; } - .step-tree { height: 1fr; } - - .main-area { width: 75%; height: 100%; padding: 1; } - .progress-area { height: auto; padding: 0 1; } - .content-area { height: 1fr; border: solid blue; padding: 1; overflow-y: auto; } - - .logo { text-align: center; color: ansi_bright_cyan; text-style: bold; height: auto; } - .step-title { text-style: bold; color: yellow; } - .status-bar { height: auto; padding: 1; } - - .step-content { padding: 1; } - .step-content Markdown { padding: 0 1; } - - #btn-row { height: auto; padding: 1; } - #btn-row Button { margin: 0 1; } - - .done { color: green; } - .current { color: yellow; text-style: bold; } - .pending { color: gray; } - """ - - BINDINGS = [ - ("q", "quit", "退出"), - ("n", "next_step", "下一步"), - ("p", "prev_step", "上一步"), - ] - - current_step = reactive(0) - step_states = reactive([False] * len(STEP_TITLES)) - - def __init__(self, vault: Path): - super().__init__() - self.vault = vault - self.checker = EnvChecker(vault) - self.step_screens: dict[str, StepScreen] = {} - - def compose(self) -> ComposeResult: - yield Header(show_clock=False) - - with Container(classes="wizard-container"): - with Horizontal(): - # 左侧:步骤导航树 - with Vertical(classes="sidebar"): - yield Static("安装步骤", classes="sidebar-title") - tree = Tree("PaperForge", id="step-tree", classes="step-tree") - for i, title in enumerate(STEP_TITLES): - tree.root.add_leaf(f"{i}. {title}") - yield tree - - # 右侧:主内容区 - with Vertical(classes="main-area"): - # 进度条 - with Container(classes="progress-area"): - yield ProgressBar(total=len(STEP_TITLES), show_eta=False, id="progress") - yield Static("Step 0 / 6", id="progress-text", classes="progress-text") - - # 内容切换器 - with ContentSwitcher(id="content-switcher", classes="content-area"): - screens = [ - WelcomeStep("step-0", self.checker), - AgentPlatformStep("step-1", self.checker), - PythonStep("step-2", self.checker), - VaultStep("step-3", self.checker, vault=str(self.vault)), - ZoteroStep("step-4", self.checker), - BBTStep("step-5", self.checker), - JsonStep("step-6", self.checker), - DeployStep("step-7", self.checker), - DoneStep("step-8", self.checker), - ] - for screen in screens: - self.step_screens[screen.step_id] = screen - yield screen - - yield Footer() - - def on_mount(self) -> None: - self._update_step_display() - - def watch_current_step(self, step: int) -> None: - self._update_step_display() - - def watch_step_states(self, states: list[bool]) -> None: - self._update_step_display() - - def _update_step_display(self) -> None: - # 更新 ContentSwitcher - switcher = self.query_one("#content-switcher", ContentSwitcher) - switcher.current = f"step-{self.current_step}" - - # 更新进度条 - progress = self.query_one("#progress", ProgressBar) - progress.advance(self.current_step - progress.progress) - - progress_text = self.query_one("#progress-text", Static) - progress_text.update(f"Step {self.current_step} / {len(STEP_TITLES) - 1}: {STEP_TITLES[self.current_step]}") - - # 更新 Tree 高亮 - 区分已完成、跳过、当前、待处理 - tree = self.query_one("#step-tree", Tree) - for i, node in enumerate(tree.root.children): - if self.step_states[i]: - # 已完成:绿色勾 - node.label = f"[green]✓ {i}. {STEP_TITLES[i]}[/]" - elif i == self.current_step: - # 当前:黄色箭头 - node.label = f"[yellow]▶ {i}. {STEP_TITLES[i]}[/]" - elif i < self.current_step and not self.step_states[i]: - # 跳过(已访问但未完成):灰色跳过标记 - node.label = f"[gray]↷ {i}. {STEP_TITLES[i]}[/]" - else: - # 待处理:灰色圆圈 - node.label = f"[gray]○ {i}. {STEP_TITLES[i]}[/]" - - def action_next_step(self) -> None: - if self.current_step < len(STEP_TITLES) - 1: - self.current_step += 1 - - def action_prev_step(self) -> None: - if self.current_step > 0: - self.current_step -= 1 - - def on_step_passed(self, message: StepPassed) -> None: - """步骤通过,标记完成并自动前进""" - self.step_states[message.step_idx] = True - if message.step_idx < len(STEP_TITLES) - 1: - self.current_step = message.step_idx + 1 - - def on_restart_wizard(self) -> None: - """重新开始""" - self.current_step = 0 - self.step_states = [False] * len(STEP_TITLES) - for screen in self.step_screens.values(): - if hasattr(screen, "set_status"): - screen.set_status("") - - def open_screenshot(self, filename: str) -> None: - """打开截图""" - img_path = self.vault / "docs" / "images" / filename - if img_path.exists(): - if sys.platform == "win32": - os.startfile(str(img_path)) - elif sys.platform == "darwin": - subprocess.run(["open", str(img_path)]) - else: - subprocess.run(["xdg-open", str(img_path)]) - else: - # 截图不存在,打开在线文档 - webbrowser.open(f"https://github.com/LLLin000/PaperForge/blob/master/docs/images/{filename}") - - -# ============================================================================= -# Entry -# ============================================================================= - - -def _find_vault() -> Path | None: - """Find vault by looking for paperforge.json in current or parent dirs.""" - current = Path(".").resolve() - for path in [current, *current.parents]: - if (path / "paperforge.json").exists(): - return path - return None - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description="PaperForge 安装向导") - parser.add_argument("--vault", type=Path, default=None, help="Vault 路径(可选,默认当前目录)") - args = parser.parse_args(argv) - - if args.vault: - vault = args.vault.resolve() - else: - # 默认使用当前目录 - vault = Path(".").resolve() - - app = SetupWizardApp(vault) - app.run() - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/token-usage-output.txt b/token-usage-output.txt deleted file mode 100644 index 2b723346..00000000 --- a/token-usage-output.txt +++ /dev/null @@ -1,205 +0,0 @@ -═══════════════════════════════════════════════════════════════════════════ -Token Analysis: Session ses_22d6d418fffeTvHERtaFMo52d7 -Model: deepseek-v4-pro -═══════════════════════════════════════════════════════════════════════════ - -WARNINGS -─────────────────────────────────────────────────── -- Pricing for 'deepseek/deepseek-v4-pro' was not found in models.json. Cost estimates use the default fallback rates ($1/M input, $3/M output, no cache pricing). - -TOKEN BREAKDOWN BY CATEGORY -───────────────────────────────────────────────────────────────────────── -Estimated using tokenizer analysis of message content: - -Input Categories: - SYSTEM ██████████░░░░░░░░░░░░░░░░░░░░ 33.1% (42,306) - USER ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.1% (87) - TOOLS ████████████████████░░░░░░░░░░ 66.8% (85,373) - - Subtotal: 127,766 estimated input tokens - Note: inferred system/overhead values are heuristic estimates from API telemetry. - -Output Categories: - REASONING ██████████████████████████████ 100.0% (2,621) - - Subtotal: 2,621 estimated output tokens - -Local Total: 130,387 tokens (estimated) - -TOOL USAGE BREAKDOWN -───────────────────────────────────────────────────────────────────────── -read ████████████████████████████░░ 92.7% (79,168) 17x -glob ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 6.1% (5,248) 4x -bash ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.9% (767) 2x -grep ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0.2% (190) 2x - -TOP CONTRIBUTORS -───────────────────────────────────────────────────────────────────────── -• read 79,168 tokens (60.7%) -• System (inferred from API telemetry) 42,306 tokens (32.4%) -• glob 5,248 tokens (4.0%) -• Reasoning#6 1,373 tokens (1.1%) -• Reasoning#8 832 tokens (0.6%) - -═══════════════════════════════════════════════════════════════════════════ -MOST RECENT API CALL -───────────────────────────────────────────────────────────────────────── - -Raw telemetry from last API response: - Input (fresh): 2,582 tokens - Cache read: 125,184 tokens - Output: 249 tokens - Reasoning: 89 tokens - Provider total: 128,104 tokens - Cost: $0.0238 - ───────────────────────────────────── - Total: 128,104 tokens - -═══════════════════════════════════════════════════════════════════════════ -SESSION TOTALS (All 7 API calls) -───────────────────────────────────────────────────────────────────────── - -Assistant messages observed: 8 (structural count) - -Total tokens processed across the entire session (for cost calculation): - - Input tokens: 123,385 (fresh tokens across all calls) - Cache read: 510,464 (cached tokens across all calls) - Cache write: 0 (tokens written to cache) - Output tokens: 1,683 (all model responses) - Reasoning tokens: 1,789 (thinking/reasoning) - ───────────────────────────────────── - Session Total: 637,321 tokens (for billing) - Cache read calls: 7 / 7 - Cache write calls: 0 / 7 - -═══════════════════════════════════════════════════════════════════════════ -SESSION COST -───────────────────────────────────────────────────────────────────────── - -Token usage breakdown: - Input tokens: 123,385 - Output tokens: 1,683 - Reasoning tokens: 1,789 - Cache read: 510,464 - -───────────────────────────────────────────────────────────────────────── -ACTUAL COST (from API): $0.3008 -Estimated cost: $0.1338 (-55.5% diff) - -Note: Actual cost from OpenCode includes provider-specific pricing -and 200K+ context adjustments. - -══════════════════════════════════════════════════════════════════════════ -AVAILABLE SKILLS (always-available context) -───────────────────────────────────────────────────────────────────────── - -These skills were recovered from the skill tool metadata available to this session. - - Skill Description Tokens - ─────────────────────────────────────────────────────────────────────── - defuddle Extract clean markdown content from web page… ~78 - academic-writing-dist… Analyze high-quality academic articles to ex… ~66 - gsd-oc-select-model Interactive model selection workflow with pa… ~49 - finishing-a-developme… Use when implementation is complete, all tes… ~46 - git-master Git expert. Detects commit styles, splits at… ~46 - frontend-ui-ux Designer-turned-developer persona. Crafts st… ~42 - brainstorming You MUST use this before any creative work -… ~39 - huashu-nuwa 女娲造人:输入人名/主题/甚至只是模糊需求,自动深度调研→思维框架提炼→生成可运行的人物… ~39 - dispatching-parallel-… Use when facing 2+ independent tasks that ca… ~28 - executing-plans Use when you have a written implementation p… ~25 - ─────────────────────────────────────────────────────────────────────── - Total: ~458 tokens (10 skills available) - - Note: Full skill tool description is ~1,346 tokens (includes boilerplate). - -═══════════════════════════════════════════════════════════════════════════ -AVAILABLE SUBAGENTS (in task tool definition) -───────────────────────────────────────────────────────────────────────── - -These subagents are embedded in the task tool description and consume tokens on every API call. - - Subagent Description Tokens - ─────────────────────────────────────────────────────────────────────── - explore Fast agent specialized for exploring codebases. U… ~105 - gsd-doc-synthesizer Synthesizes classified planning docs into a singl… ~85 - gsd-domain-researcher Researches the business domain and real-world app… ~80 - gsd-eval-auditor Retroactive audit of an implemented AI phase's ev… ~80 - gsd-eval-planner Designs a structured evaluation strategy for an A… ~76 - gsd-ai-researcher Researches a chosen AI framework's official docs … ~73 - gsd-debug-session-man… Manages multi-cycle /gsd-debug checkpoint and con… ~69 - gsd-doc-classifier Classifies a single planning document as ADR, PRD… ~68 - gsd-framework-selector Presents an interactive decision matrix to surfac… ~61 - gsd-project-researcher Researches domain ecosystem before roadmap creati… ~51 - gsd-codebase-mapper Explores codebase and writes structured analysis … ~49 - gsd-ui-researcher Produces UI-SPEC.md design contract for frontend … ~47 - gsd-ui-checker Validates UI-SPEC.md design contracts against 6 q… ~46 - gsd-code-fixer Applies fixes to code review findings from REVIEW… ~45 - gsd-pattern-mapper Analyzes codebase for existing patterns and produ… ~45 - gsd-ui-auditor Retroactive 6-pillar visual audit of implemented … ~43 - gsd-code-reviewer Reviews source files for bugs, security issues, a… ~42 - gsd-doc-writer Writes and updates project documentation. Spawned… ~41 - gsd-phase-researcher Researches how to implement a phase before planni… ~41 - gsd-security-auditor Verifies threat mitigations from PLAN.md threat m… ~41 - gsd-executor Executes GSD plans with atomic commits, deviation… ~38 - gsd-research-synthesi… Synthesizes research outputs from parallel resear… ~38 - gsd-user-profiler Analyzes extracted session messages across 8 beha… ~38 - gsd-verifier Verifies phase goal achievement through goal-back… ~38 - gsd-plan-checker Verifies plans will achieve phase goal before exe… ~37 - gsd-roadmapper Creates project roadmaps with phase breakdown, re… ~37 - gsd-planner Creates executable phase plans with task breakdow… ~35 - gsd-advisor-researcher Researches a single gray area decision and return… ~34 - gsd-debugger Investigates bugs using scientific method, manage… ~34 - gsd-assumptions-analy… Deeply analyzes codebase for a phase and returns … ~33 - gsd-integration-check… Verifies cross-phase integration and E2E flows. C… ~32 - general General-purpose agent for researching complex que… ~28 - gsd-doc-verifier Verifies factual claims in generated docs against… ~28 - gsd-intel-updater Analyzes codebase and writes structured intel fil… ~25 - gsd-nyquist-auditor Fills Nyquist validation gaps by generating tests… ~25 - ─────────────────────────────────────────────────────────────────────── - Total: ~1,688 tokens (35 subagents available) - - Note: Full task tool description is ~2,540 tokens (includes instructions/examples). - -═══════════════════════════════════════════════════════════════════════════ -TOOL DEFINITION COSTS (Estimated from argument analysis) -───────────────────────────────────────────────────────────────────────── - - Tool Est. Tokens Args Complexity - ─────────────────────────────────────────────────────────────────── - bash ~370 3 simple - grep ~370 3 simple - read ~340 2 simple - glob ~340 2 simple - tokenscope ~310 1 simple - ─────────────────────────────────────────────────────────────────── - Total: ~ 1,730 tokens (5 enabled tools) - - Note: Estimates inferred from tool call arguments in this session. - Actual schema tokens may vary +/-20%. - -═══════════════════════════════════════════════════════════════════════════ -CACHE EFFICIENCY -───────────────────────────────────────────────────────────────────────── - - Token Distribution: - Cache Read: 510,464 tokens ████████████████████████░░░░░░ 80.5% - Fresh Input: 123,385 tokens ██████░░░░░░░░░░░░░░░░░░░░░░░░ 19.5% - ─────────────────────────────────────────────────────────────────── - Cache Hit Rate: 80.5% (cache read / (cache read + fresh input)) - - Cost Analysis (deepseek-v4-pro @ $1.00/M input, $0.00/M cache read, $0.00/M cache write): - Without caching: $0.6338 (633,849 tokens x $1.00/M) - With caching: $0.1234 (fresh x $1.00/M + cache read x $0.00/M + cache write x $0.00/M) - ─────────────────────────────────────────────────────────────────── - Cost Savings: $0.5105 (80.5% reduction) - Effective Rate: $0.19/M tokens (vs. $1.00/M standard) - -═══════════════════════════════════════════════════════════════════════════ -SUMMARY -───────────────────────────────────────────────────────────────────────── - - Cost Tokens API Calls - Session: $ 0.3008 637,321 7 - -═══════════════════════════════════════════════════════════════════════════ \ No newline at end of file