2026-04-21 12:50:17 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Interactive installer for the Literature Workflow."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import shutil
|
|
|
|
|
import subprocess
|
|
|
|
|
import platform
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Colors:
|
|
|
|
|
HEADER = '\033[95m'
|
|
|
|
|
OKBLUE = '\033[94m'
|
|
|
|
|
OKCYAN = '\033[96m'
|
|
|
|
|
OKGREEN = '\033[92m'
|
|
|
|
|
WARNING = '\033[93m'
|
|
|
|
|
FAIL = '\033[91m'
|
|
|
|
|
ENDC = '\033[0m'
|
|
|
|
|
BOLD = '\033[1m'
|
|
|
|
|
|
|
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
# Agent platform configurations
|
|
|
|
|
AGENT_CONFIGS = {
|
|
|
|
|
"opencode": {
|
|
|
|
|
"name": "OpenCode",
|
|
|
|
|
"skill_dir": ".opencode/skills",
|
|
|
|
|
"config_file": None,
|
|
|
|
|
},
|
|
|
|
|
"claude": {
|
|
|
|
|
"name": "Claude Code",
|
|
|
|
|
"skill_dir": ".claude/skills",
|
|
|
|
|
"config_file": ".claude/skills.json",
|
|
|
|
|
},
|
|
|
|
|
"cursor": {
|
|
|
|
|
"name": "Cursor",
|
|
|
|
|
"skill_dir": ".cursor/skills",
|
|
|
|
|
"config_file": ".cursor/settings.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,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def select_agent() -> tuple[str, dict]:
|
|
|
|
|
"""Ask user to select their AI agent platform."""
|
|
|
|
|
print_header("Step 0: Agent Platform Selection")
|
|
|
|
|
print("Which AI agent do you use with this vault?")
|
|
|
|
|
print("(This determines where skill files and configurations are placed)\n")
|
|
|
|
|
|
|
|
|
|
agents = list(AGENT_CONFIGS.items())
|
|
|
|
|
for i, (key, cfg) in enumerate(agents, 1):
|
|
|
|
|
print(f" {i}. {cfg['name']} ({key})")
|
|
|
|
|
print(f" {len(agents) + 1}. Other (custom)")
|
|
|
|
|
|
|
|
|
|
choice = ask("Select agent", default="1")
|
|
|
|
|
try:
|
|
|
|
|
idx = int(choice) - 1
|
|
|
|
|
if 0 <= idx < len(agents):
|
|
|
|
|
return agents[idx]
|
|
|
|
|
elif idx == len(agents):
|
|
|
|
|
# Custom agent
|
|
|
|
|
custom_name = ask("Enter agent name")
|
|
|
|
|
custom_dir = ask("Enter skill directory (relative to vault)", default=".custom/skills")
|
|
|
|
|
return "custom", {
|
|
|
|
|
"name": custom_name,
|
|
|
|
|
"skill_dir": custom_dir,
|
|
|
|
|
"config_file": None,
|
|
|
|
|
}
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# Default to OpenCode
|
|
|
|
|
print_warning("Invalid choice, defaulting to OpenCode")
|
|
|
|
|
return "opencode", AGENT_CONFIGS["opencode"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def configure_vault_paths(vault_path: Path) -> dict:
|
2026-04-21 14:10:40 +00:00
|
|
|
"""Ask user for PaperForge-specific directory structure preferences."""
|
2026-04-21 13:12:12 +00:00
|
|
|
print_header("Step 0.5: Vault Directory Configuration")
|
2026-04-21 14:10:40 +00:00
|
|
|
print("Configure PaperForge directory structure (press Enter to accept defaults):\n")
|
|
|
|
|
print("Note: 00_Inbox, 04_Archives, 05_Bases, 06_AI_Wiki are your personal PARA folders.")
|
|
|
|
|
print(" PaperForge will NOT create them.\n")
|
2026-04-21 13:12:12 +00:00
|
|
|
|
|
|
|
|
paths = {
|
|
|
|
|
"system_dir": ask("System folder name", default="99_System"),
|
|
|
|
|
"resources_dir": ask("Resources folder name", default="03_Resources"),
|
2026-04-21 14:10:40 +00:00
|
|
|
"literature_dir": ask("Literature subfolder (for generated notes)", default="Literature"),
|
2026-04-21 13:12:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Build derived paths
|
|
|
|
|
paths["pipeline_path"] = f"{paths['system_dir']}/LiteraturePipeline"
|
|
|
|
|
paths["template_path"] = f"{paths['system_dir']}/Template"
|
2026-04-21 14:10:40 +00:00
|
|
|
paths["literature_path"] = f"{paths['resources_dir']}/{paths['literature_dir']}"
|
|
|
|
|
|
|
|
|
|
return paths
|
2026-04-21 13:12:12 +00:00
|
|
|
|
|
|
|
|
return paths
|
|
|
|
|
|
|
|
|
|
|
2026-04-21 12:50:17 +00:00
|
|
|
def print_header(text: str) -> None:
|
|
|
|
|
print(f"\n{Colors.HEADER}{'='*60}{Colors.ENDC}")
|
|
|
|
|
print(f"{Colors.BOLD}{text}{Colors.ENDC}")
|
|
|
|
|
print(f"{Colors.HEADER}{'='*60}{Colors.ENDC}\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def print_success(text: str) -> None:
|
|
|
|
|
print(f"{Colors.OKGREEN}[OK]{Colors.ENDC} {text}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def print_warning(text: str) -> None:
|
|
|
|
|
print(f"{Colors.WARNING}[WARN]{Colors.ENDC} {text}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def print_error(text: str) -> None:
|
|
|
|
|
print(f"{Colors.FAIL}[ERROR]{Colors.ENDC} {text}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ask(question: str, default: Optional[str] = None) -> str:
|
|
|
|
|
"""Ask user a question with optional default."""
|
|
|
|
|
if default:
|
|
|
|
|
prompt = f"{question} [{default}]: "
|
|
|
|
|
else:
|
|
|
|
|
prompt = f"{question}: "
|
|
|
|
|
|
|
|
|
|
answer = input(prompt).strip()
|
|
|
|
|
if not answer and default:
|
|
|
|
|
return default
|
|
|
|
|
return answer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ask_yes_no(question: str, default: bool = False) -> bool:
|
|
|
|
|
"""Ask a yes/no question."""
|
|
|
|
|
suffix = " [Y/n]: " if default else " [y/N]: "
|
|
|
|
|
answer = input(f"{question}{suffix}").strip().lower()
|
|
|
|
|
if not answer:
|
|
|
|
|
return default
|
|
|
|
|
return answer in ('y', 'yes', 'true', '1')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def detect_zotero_path() -> Optional[Path]:
|
|
|
|
|
"""Auto-detect Zotero data directory."""
|
|
|
|
|
system = platform.system()
|
|
|
|
|
|
|
|
|
|
if system == "Windows":
|
|
|
|
|
# Check common locations
|
|
|
|
|
home = Path.home()
|
|
|
|
|
candidates = [
|
|
|
|
|
home / "Zotero",
|
|
|
|
|
home / "AppData" / "Roaming" / "Zotero" / "Zotero",
|
|
|
|
|
Path("C:/Users") / os.environ.get("USERNAME", "") / "Zotero",
|
|
|
|
|
]
|
|
|
|
|
elif system == "Darwin": # macOS
|
|
|
|
|
home = Path.home()
|
|
|
|
|
candidates = [
|
|
|
|
|
home / "Zotero",
|
|
|
|
|
home / "Library" / "Application Support" / "Zotero",
|
|
|
|
|
]
|
|
|
|
|
else: # Linux
|
|
|
|
|
home = Path.home()
|
|
|
|
|
candidates = [
|
|
|
|
|
home / "Zotero",
|
|
|
|
|
home / ".zotero",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for candidate in candidates:
|
|
|
|
|
if candidate.exists() and (candidate / "zotero.sqlite").exists():
|
|
|
|
|
return candidate
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_junction(source: Path, target: Path) -> bool:
|
|
|
|
|
"""Create junction/symlink from source to target."""
|
|
|
|
|
system = platform.system()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
if system == "Windows":
|
|
|
|
|
subprocess.run(
|
|
|
|
|
["cmd", "/c", "mklink", "/J", str(source), str(target)],
|
|
|
|
|
check=True,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
shell=False,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
source.symlink_to(target, target_is_directory=True)
|
|
|
|
|
return True
|
|
|
|
|
except (subprocess.CalledProcessError, OSError) as e:
|
|
|
|
|
print_error(f"Failed to create junction: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_python_deps() -> list[str]:
|
|
|
|
|
"""Check if required Python packages are installed."""
|
|
|
|
|
required = ["requests", "pymupdf", "PIL", "pytest"]
|
|
|
|
|
missing = []
|
|
|
|
|
|
|
|
|
|
for package in required:
|
|
|
|
|
try:
|
|
|
|
|
__import__(package.lower().replace("pil", "PIL"))
|
|
|
|
|
except ImportError:
|
|
|
|
|
missing.append(package)
|
|
|
|
|
|
|
|
|
|
return missing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def install_deps(deps: list[str]) -> bool:
|
|
|
|
|
"""Install missing Python dependencies."""
|
|
|
|
|
if not deps:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
print(f"Installing dependencies: {', '.join(deps)}")
|
|
|
|
|
try:
|
|
|
|
|
subprocess.run(
|
|
|
|
|
[sys.executable, "-m", "pip", "install"] + deps,
|
|
|
|
|
check=True,
|
|
|
|
|
)
|
|
|
|
|
return True
|
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
|
print_error(f"Failed to install dependencies: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
def create_directory_structure(vault_path: Path, paths: dict) -> None:
|
2026-04-21 14:10:40 +00:00
|
|
|
"""Create PaperForge directory structure with configurable paths."""
|
2026-04-21 12:50:17 +00:00
|
|
|
dirs = [
|
2026-04-21 14:10:40 +00:00
|
|
|
# Core pipeline directories
|
2026-04-21 13:12:12 +00:00
|
|
|
f"{paths['pipeline_path']}/ocr",
|
|
|
|
|
f"{paths['pipeline_path']}/worker/scripts",
|
2026-04-21 14:10:40 +00:00
|
|
|
f"{paths['pipeline_path']}/worker/tests",
|
|
|
|
|
f"{paths['pipeline_path']}/indexes",
|
|
|
|
|
f"{paths['pipeline_path']}/exports",
|
|
|
|
|
|
|
|
|
|
# Template directories
|
2026-04-21 13:12:12 +00:00
|
|
|
f"{paths['template_path']}",
|
2026-04-21 14:10:40 +00:00
|
|
|
|
|
|
|
|
# Zotero junction point
|
|
|
|
|
f"{paths['system_dir']}/Zotero",
|
|
|
|
|
|
2026-04-21 14:44:09 +00:00
|
|
|
# Library records (for selection_sync state)
|
2026-04-21 14:10:40 +00:00
|
|
|
f"03_Resources/LiteratureControl/library-records",
|
2026-04-21 12:50:17 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for d in dirs:
|
|
|
|
|
(vault_path / d).mkdir(parents=True, exist_ok=True)
|
2026-04-21 14:44:09 +00:00
|
|
|
print_success(f"Created: {d}")
|
2026-04-21 12:50:17 +00:00
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
print_success(f"Directory structure created ({len(dirs)} folders)")
|
2026-04-21 12:50:17 +00:00
|
|
|
|
|
|
|
|
|
2026-04-21 14:10:40 +00:00
|
|
|
def create_env_file(vault_path: Path, config: dict, paths: dict) -> None:
|
2026-04-21 12:50:17 +00:00
|
|
|
"""Create .env configuration file."""
|
|
|
|
|
env_path = vault_path / ".env"
|
|
|
|
|
|
|
|
|
|
lines = [
|
2026-04-21 14:10:40 +00:00
|
|
|
"# PaperForge Configuration",
|
2026-04-21 12:50:17 +00:00
|
|
|
f"ZOTERO_DATA_DIR={config['zotero_path']}",
|
|
|
|
|
f"ZOTERO_STORAGE_DIR={config.get('storage_path', config['zotero_path'])}",
|
|
|
|
|
f"PADDLEOCR_API_KEY={config['ocr_api_key']}",
|
|
|
|
|
"PADDLEOCR_API_URL=https://paddleocr.baidu.com/api/v1/ocr",
|
|
|
|
|
"",
|
2026-04-21 14:10:40 +00:00
|
|
|
"# Agent Configuration",
|
|
|
|
|
f"PAPERFORGE_AGENT={config['agent']}",
|
|
|
|
|
f"PAPERFORGE_AGENT_NAME={config['agent_name']}",
|
|
|
|
|
f"PAPERFORGE_SKILL_DIR={config['skill_dir']}",
|
|
|
|
|
"",
|
|
|
|
|
"# Path Configuration",
|
|
|
|
|
f"PAPERFORGE_SYSTEM_DIR={paths['system_dir']}",
|
|
|
|
|
f"PAPERFORGE_PIPELINE_PATH={paths['pipeline_path']}",
|
|
|
|
|
f"PAPERFORGE_TEMPLATE_PATH={paths['template_path']}",
|
|
|
|
|
f"PAPERFORGE_VAULT_PATH={vault_path}",
|
2026-04-21 12:50:17 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
env_path.write_text("\n".join(lines), encoding="utf-8")
|
|
|
|
|
print_success(f"Configuration saved to {env_path}")
|
|
|
|
|
|
|
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
def create_agents_md(vault_path: Path, config: dict, paths: dict, agent_config: dict) -> None:
|
2026-04-21 12:50:17 +00:00
|
|
|
"""Create generic AGENTS.md template."""
|
|
|
|
|
agents_path = vault_path / "AGENTS.md"
|
|
|
|
|
|
|
|
|
|
if agents_path.exists():
|
|
|
|
|
if not ask_yes_no("AGENTS.md already exists. Overwrite?", default=False):
|
|
|
|
|
print_warning("Skipping AGENTS.md creation")
|
|
|
|
|
return
|
|
|
|
|
|
2026-04-21 14:10:40 +00:00
|
|
|
content = f"""# Agent Guide for PaperForge Literature Workflow
|
2026-04-21 12:50:17 +00:00
|
|
|
|
2026-04-21 14:10:40 +00:00
|
|
|
This vault uses PaperForge -- an automated literature research pipeline.
|
2026-04-21 12:50:17 +00:00
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
## Agent Configuration
|
2026-04-21 12:50:17 +00:00
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
- **Platform**: {agent_config['name']}
|
|
|
|
|
- **Skill Directory**: `{agent_config['skill_dir']}`
|
|
|
|
|
- **Config File**: {agent_config.get('config_file', 'None')}
|
2026-04-21 12:50:17 +00:00
|
|
|
|
2026-04-21 14:10:40 +00:00
|
|
|
## PaperForge Directory Structure
|
|
|
|
|
|
|
|
|
|
PaperForge manages the following directories (do NOT rename):
|
2026-04-21 12:50:17 +00:00
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
```
|
|
|
|
|
{paths['system_dir']}/
|
2026-04-21 14:10:40 +00:00
|
|
|
LiteraturePipeline/ # Core pipeline (managed by PaperForge)
|
|
|
|
|
candidates/ # Candidate literature
|
|
|
|
|
inbox/ # Search result inbox
|
|
|
|
|
archive/ # Archived events
|
|
|
|
|
exports/ # Better BibTeX JSON exports
|
|
|
|
|
indexes/ # Generated indices
|
|
|
|
|
ocr/ # OCR outputs
|
|
|
|
|
<zotero_key>/ # fulltext.md + images/
|
|
|
|
|
search/ # Search tasks & results
|
|
|
|
|
tasks/ # Search configurations
|
|
|
|
|
results/ # Result cache
|
|
|
|
|
writeback/ # Writeback queue & logs
|
|
|
|
|
worker/ # Worker scripts
|
|
|
|
|
scripts/ # literature_pipeline.py
|
|
|
|
|
tests/ # Test files
|
|
|
|
|
Template/ # Templates
|
|
|
|
|
文献阅读.md # Literature note template
|
|
|
|
|
科研读图指南.md # Chart reading guide index
|
|
|
|
|
读图指南/ # 14 chart type guides
|
|
|
|
|
Zotero/ # Zotero data (junction)
|
|
|
|
|
zotero.sqlite # Linked via junction/symlink
|
|
|
|
|
|
|
|
|
|
03_Resources/
|
|
|
|
|
Literature/ # Your literature notes (generated by index-refresh)
|
|
|
|
|
<domain>/ # e.g., 骨科/, 运动医学/
|
|
|
|
|
LiteratureControl/ # PaperForge control files
|
|
|
|
|
library-records/ # Library tracking (analyze/ocr/deep flags)
|
2026-04-21 13:12:12 +00:00
|
|
|
```
|
|
|
|
|
|
2026-04-21 14:10:40 +00:00
|
|
|
Your personal PARA folders (00_Inbox, 01_Projects, 02_Areas, 04_Archives, 05_Bases, 06_AI_Wiki) are NOT managed by PaperForge.
|
|
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
## 1. Environment & Tech Stack
|
2026-04-21 12:50:17 +00:00
|
|
|
|
|
|
|
|
- **Platform**: Obsidian (Knowledge Management)
|
2026-04-21 14:44:09 +00:00
|
|
|
- **Reference Manager**: Zotero (Better BibTeX auto-export)
|
2026-04-21 14:10:40 +00:00
|
|
|
- **OCR**: PaddleOCR-VL API
|
2026-04-21 12:50:17 +00:00
|
|
|
- **Primary Language**: Simplified Chinese (简体中文)
|
2026-04-21 14:10:40 +00:00
|
|
|
- **Scripting**: Python 3.10+
|
|
|
|
|
|
|
|
|
|
## 2. Workflows
|
|
|
|
|
|
2026-04-21 14:44:09 +00:00
|
|
|
### Phase 1: Index & Notes
|
|
|
|
|
Better BibTeX auto-export JSON → selection_sync + index_refresh → formal-library.json + 03_Resources/Literature/<domain>/*.md
|
2026-04-21 14:10:40 +00:00
|
|
|
|
2026-04-21 14:44:09 +00:00
|
|
|
### Phase 2: Deep Processing
|
|
|
|
|
Library records flagged for analyze → OCR (PaddleOCR) → LD-deep agent → ## 🔍 精读 section
|
2026-04-21 12:50:17 +00:00
|
|
|
|
2026-04-21 14:10:40 +00:00
|
|
|
## 3. Available Functions
|
2026-04-21 12:50:17 +00:00
|
|
|
|
2026-04-21 14:10:40 +00:00
|
|
|
- **Quick Lookup** - Search and preview literature (single or batch)
|
|
|
|
|
- **Deep Reading** - Keshav three-pass reading method (ensure-scaffold + fill)
|
|
|
|
|
- **Queue Processing** - Batch process papers awaiting deep reading
|
|
|
|
|
- **Figure Analysis** - Extract and analyze figures/tables from OCR
|
2026-04-21 12:50:17 +00:00
|
|
|
|
2026-04-21 14:10:40 +00:00
|
|
|
## 4. Style Guidelines
|
2026-04-21 12:50:17 +00:00
|
|
|
|
|
|
|
|
### Markdown & Note Structure
|
2026-04-21 13:12:12 +00:00
|
|
|
Follow the template in `{paths['template_path']}/文献阅读.md`.
|
2026-04-21 12:50:17 +00:00
|
|
|
|
|
|
|
|
**Frontmatter (YAML) is MANDATORY:**
|
|
|
|
|
```yaml
|
|
|
|
|
---
|
|
|
|
|
title: " {{{{Title}}}} "
|
|
|
|
|
year: {{{{Year}}}}
|
|
|
|
|
type: {{{{Type}}}}
|
|
|
|
|
journal: " {{{{Journal}}}} "
|
|
|
|
|
category: {{{{Category}}}}
|
|
|
|
|
tags:
|
|
|
|
|
- 文献阅读
|
|
|
|
|
- {{{{Subject_Tag}}}}
|
|
|
|
|
---
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Output Language
|
|
|
|
|
- **Output**: Simplified Chinese (简体中文) ONLY, unless asked otherwise.
|
|
|
|
|
- **Search Terms**: English (for PubMed), but explain in Chinese.
|
|
|
|
|
|
2026-04-21 14:10:40 +00:00
|
|
|
## 5. Interaction Rules
|
2026-04-21 12:50:17 +00:00
|
|
|
|
|
|
|
|
### Protocol
|
|
|
|
|
1. **No Hallucinations**: Never invent PMIDs or citations.
|
|
|
|
|
2. **User Confirmation**:
|
|
|
|
|
- Confirm **Search Strategy** before executing deep searches.
|
|
|
|
|
- Confirm **Target Collection** before importing to Zotero.
|
|
|
|
|
3. **Session Awareness**: Track context across sessions.
|
|
|
|
|
|
|
|
|
|
## 6. Configuration
|
|
|
|
|
|
|
|
|
|
- Zotero Data: `{zotero_path}`
|
|
|
|
|
- Zotero Storage: `{storage_path}`
|
|
|
|
|
- Vault Path: `{vault_path}`
|
2026-04-21 14:10:40 +00:00
|
|
|
- System Dir: `{paths['system_dir']}`
|
|
|
|
|
- Pipeline Path: `{paths['pipeline_path']}`
|
2026-04-21 12:50:17 +00:00
|
|
|
- OCR API: PaddleOCR
|
2026-04-21 13:12:12 +00:00
|
|
|
- Skill Dir: `{agent_config['skill_dir']}`
|
2026-04-21 12:50:17 +00:00
|
|
|
|
2026-04-21 14:10:40 +00:00
|
|
|
Generated by PaperForge setup.py on {platform.system()}
|
2026-04-21 12:50:17 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
agents_path.write_text(content, encoding="utf-8")
|
|
|
|
|
print_success(f"AGENTS.md created at {agents_path}")
|
|
|
|
|
|
|
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
def deploy_workflow_scripts(vault_path: Path, agent_key: str, agent_config: dict, paths: dict) -> bool:
|
2026-04-21 12:59:14 +00:00
|
|
|
"""Deploy workflow scripts from repo to vault.
|
|
|
|
|
|
|
|
|
|
This copies the core pipeline code from the repository into the user's vault,
|
|
|
|
|
ensuring the latest scripts are available while keeping private data (.env, API keys) separate.
|
|
|
|
|
"""
|
|
|
|
|
print_header("Step 4.5: Deploying Workflow Scripts")
|
|
|
|
|
|
|
|
|
|
# Determine repo root (where this script is located)
|
|
|
|
|
repo_root = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
# Get agent-specific skill directory
|
|
|
|
|
skill_dir = agent_config.get("skill_dir", ".opencode/skills")
|
|
|
|
|
|
2026-04-21 12:59:14 +00:00
|
|
|
# Files to deploy: (source_relative_path, dest_relative_path)
|
|
|
|
|
deployments = [
|
|
|
|
|
# OCR pipeline worker
|
2026-04-21 14:17:45 +00:00
|
|
|
(f"pipeline/worker/scripts/literature_pipeline.py",
|
2026-04-21 13:12:12 +00:00
|
|
|
f"{paths['pipeline_path']}/worker/scripts/literature_pipeline.py"),
|
2026-04-21 12:59:14 +00:00
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
# Deep reading scripts (into agent-specific skill dir)
|
2026-04-21 14:17:45 +00:00
|
|
|
(f"skills/literature-qa/scripts/ld_deep.py",
|
2026-04-21 13:12:12 +00:00
|
|
|
f"{skill_dir}/literature-qa/scripts/ld_deep.py"),
|
2026-04-21 12:59:14 +00:00
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
# Subagent prompt (into agent-specific skill dir)
|
2026-04-21 14:17:45 +00:00
|
|
|
(f"skills/literature-qa/prompt_deep_subagent.md",
|
2026-04-21 13:12:12 +00:00
|
|
|
f"{skill_dir}/literature-qa/prompt_deep_subagent.md"),
|
2026-04-21 12:59:14 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
success_count = 0
|
|
|
|
|
fail_count = 0
|
|
|
|
|
|
|
|
|
|
for src_rel, dst_rel in deployments:
|
|
|
|
|
src_path = repo_root / src_rel
|
|
|
|
|
dst_path = vault_path / dst_rel
|
|
|
|
|
|
|
|
|
|
if not src_path.exists():
|
|
|
|
|
print_warning(f"Source file not found (skipping): {src_rel}")
|
|
|
|
|
fail_count += 1
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Ensure parent directory exists
|
|
|
|
|
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
# Copy file
|
|
|
|
|
shutil.copy2(src_path, dst_path)
|
2026-04-21 13:12:12 +00:00
|
|
|
print_success(f"Deployed: {dst_rel}")
|
2026-04-21 12:59:14 +00:00
|
|
|
success_count += 1
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print_error(f"Failed to deploy {src_rel}: {e}")
|
|
|
|
|
fail_count += 1
|
|
|
|
|
|
2026-04-21 15:00:50 +00:00
|
|
|
# Deploy chart reading guides (into literature-qa skill as references)
|
|
|
|
|
chart_guide_src = repo_root / "skills/literature-qa/chart-reading"
|
|
|
|
|
chart_guide_dst = vault_path / skill_dir / "literature-qa/chart-reading"
|
2026-04-21 12:59:14 +00:00
|
|
|
|
|
|
|
|
if chart_guide_src.exists() and chart_guide_src.is_dir():
|
|
|
|
|
chart_files = list(chart_guide_src.glob("*.md"))
|
|
|
|
|
if chart_files:
|
|
|
|
|
chart_guide_dst.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
for chart_file in chart_files:
|
|
|
|
|
try:
|
|
|
|
|
dst_file = chart_guide_dst / chart_file.name
|
|
|
|
|
shutil.copy2(chart_file, dst_file)
|
|
|
|
|
success_count += 1
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print_error(f"Failed to deploy chart guide {chart_file.name}: {e}")
|
|
|
|
|
fail_count += 1
|
|
|
|
|
print_success(f"Deployed {len(chart_files)} chart reading guides")
|
|
|
|
|
|
2026-04-21 14:11:48 +00:00
|
|
|
# Deploy template files
|
|
|
|
|
template_files = [
|
2026-04-21 14:17:45 +00:00
|
|
|
("templates/科研读图指南.md",
|
2026-04-21 14:11:48 +00:00
|
|
|
f"{paths['template_path']}/科研读图指南.md"),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for src_rel, dst_rel in template_files:
|
|
|
|
|
src_path = repo_root / src_rel
|
|
|
|
|
dst_path = vault_path / dst_rel
|
|
|
|
|
|
|
|
|
|
if src_path.exists():
|
|
|
|
|
try:
|
|
|
|
|
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
shutil.copy2(src_path, dst_path)
|
|
|
|
|
print_success(f"Deployed template: {src_rel}")
|
|
|
|
|
success_count += 1
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print_error(f"Failed to deploy template {src_rel}: {e}")
|
|
|
|
|
fail_count += 1
|
|
|
|
|
else:
|
|
|
|
|
print_warning(f"Template not found (skipping): {src_rel}")
|
|
|
|
|
fail_count += 1
|
|
|
|
|
|
2026-04-21 12:59:14 +00:00
|
|
|
print(f"\nDeployment summary: {success_count} succeeded, {fail_count} failed")
|
|
|
|
|
return fail_count == 0
|
|
|
|
|
|
|
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
def validate_setup(vault_path: Path, config: dict, paths: dict) -> list[str]:
|
2026-04-21 12:50:17 +00:00
|
|
|
"""Validate the setup and return issues."""
|
|
|
|
|
issues = []
|
|
|
|
|
|
|
|
|
|
# Check Zotero SQLite
|
|
|
|
|
zotero_db = Path(config['zotero_path']) / "zotero.sqlite"
|
|
|
|
|
if not zotero_db.exists():
|
|
|
|
|
issues.append(f"Zotero database not found: {zotero_db}")
|
|
|
|
|
else:
|
|
|
|
|
print_success("Zotero database accessible")
|
|
|
|
|
|
|
|
|
|
# Check directory structure
|
|
|
|
|
required_dirs = [
|
2026-04-21 13:12:12 +00:00
|
|
|
f"{paths['pipeline_path']}/ocr",
|
|
|
|
|
f"{paths['pipeline_path']}/worker/scripts",
|
2026-04-21 14:10:40 +00:00
|
|
|
f"{paths['pipeline_path']}/indexes",
|
|
|
|
|
f"03_Resources/LiteratureControl/library-records",
|
2026-04-21 12:50:17 +00:00
|
|
|
]
|
|
|
|
|
for d in required_dirs:
|
|
|
|
|
if not (vault_path / d).exists():
|
|
|
|
|
issues.append(f"Missing directory: {d}")
|
|
|
|
|
|
|
|
|
|
if not issues:
|
|
|
|
|
print_success("Directory structure correct")
|
|
|
|
|
|
|
|
|
|
# Check AGENTS.md
|
|
|
|
|
if not (vault_path / "AGENTS.md").exists():
|
|
|
|
|
issues.append("AGENTS.md missing")
|
|
|
|
|
else:
|
|
|
|
|
print_success("AGENTS.md exists")
|
|
|
|
|
|
|
|
|
|
# Check .env
|
|
|
|
|
if not (vault_path / ".env").exists():
|
|
|
|
|
issues.append(".env configuration missing")
|
|
|
|
|
else:
|
|
|
|
|
print_success("Configuration file exists")
|
|
|
|
|
|
|
|
|
|
return issues
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
"""Main installer entry point."""
|
2026-04-21 13:14:25 +00:00
|
|
|
# Show welcome screen
|
|
|
|
|
try:
|
|
|
|
|
from welcome import show_welcome, show_install_menu
|
|
|
|
|
show_welcome()
|
|
|
|
|
show_install_menu()
|
|
|
|
|
except ImportError:
|
|
|
|
|
print_header("Literature Workflow Installer")
|
|
|
|
|
|
2026-04-21 12:50:17 +00:00
|
|
|
print("This script will help you configure the literature research pipeline.\n")
|
|
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
# Step 0: Select agent platform
|
|
|
|
|
agent_key, agent_config = select_agent()
|
|
|
|
|
print_success(f"Selected agent: {agent_config['name']}")
|
|
|
|
|
|
|
|
|
|
# Step 0.5: Configure vault paths
|
|
|
|
|
print_header("Step 0.5: Vault Configuration")
|
2026-04-21 12:50:17 +00:00
|
|
|
vault_path_str = ask(
|
|
|
|
|
"Where is your Obsidian vault located?",
|
|
|
|
|
default=str(Path.cwd()),
|
|
|
|
|
)
|
|
|
|
|
vault_path = Path(vault_path_str).resolve()
|
|
|
|
|
|
|
|
|
|
if not vault_path.exists():
|
|
|
|
|
print_error(f"Vault path does not exist: {vault_path}")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
print_success(f"Using vault: {vault_path}")
|
|
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
# Configure directory structure
|
|
|
|
|
paths = configure_vault_paths(vault_path)
|
|
|
|
|
print_success("Directory structure configured")
|
|
|
|
|
|
2026-04-21 12:50:17 +00:00
|
|
|
# Step 1: Check Python deps
|
|
|
|
|
print_header("Step 1: Checking Python Dependencies")
|
|
|
|
|
missing_deps = check_python_deps()
|
|
|
|
|
if missing_deps:
|
|
|
|
|
print_warning(f"Missing packages: {', '.join(missing_deps)}")
|
|
|
|
|
if ask_yes_no("Install now?", default=True):
|
|
|
|
|
if not install_deps(missing_deps):
|
|
|
|
|
return 1
|
|
|
|
|
else:
|
|
|
|
|
print_error("Required packages must be installed to continue")
|
|
|
|
|
return 1
|
|
|
|
|
else:
|
|
|
|
|
print_success("All dependencies installed")
|
|
|
|
|
|
|
|
|
|
# Step 2: Detect/Ask for Zotero path
|
|
|
|
|
print_header("Step 2: Zotero Configuration")
|
|
|
|
|
detected_zotero = detect_zotero_path()
|
|
|
|
|
|
|
|
|
|
if detected_zotero:
|
|
|
|
|
print_success(f"Detected Zotero at: {detected_zotero}")
|
|
|
|
|
if ask_yes_no("Use this path?", default=True):
|
|
|
|
|
zotero_path = detected_zotero
|
|
|
|
|
else:
|
|
|
|
|
zotero_path = Path(ask("Enter Zotero data directory:"))
|
|
|
|
|
else:
|
|
|
|
|
print_warning("Could not auto-detect Zotero")
|
|
|
|
|
zotero_path = Path(ask("Enter Zotero data directory (contains zotero.sqlite):"))
|
|
|
|
|
|
|
|
|
|
if not (zotero_path / "zotero.sqlite").exists():
|
|
|
|
|
print_error(f"zotero.sqlite not found in {zotero_path}")
|
|
|
|
|
print("Please ensure Zotero is installed and the path is correct.")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
# Step 3: Storage path
|
|
|
|
|
storage_path = zotero_path
|
|
|
|
|
if ask_yes_no("Is your Zotero storage directory in a different location?", default=False):
|
|
|
|
|
storage_path = Path(ask("Enter Zotero storage directory:"))
|
|
|
|
|
|
|
|
|
|
# Step 4: OCR API Key
|
|
|
|
|
print_header("Step 3: OCR Configuration")
|
|
|
|
|
ocr_api_key = ask("Enter your PaddleOCR API key:")
|
|
|
|
|
if not ocr_api_key:
|
|
|
|
|
print_warning("No API key provided. OCR features will not work.")
|
|
|
|
|
|
|
|
|
|
# Step 5: Create directories
|
|
|
|
|
print_header("Step 4: Creating Directory Structure")
|
2026-04-21 13:12:12 +00:00
|
|
|
create_directory_structure(vault_path, paths)
|
2026-04-21 12:50:17 +00:00
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
# Step 6: Deploy workflow scripts
|
|
|
|
|
deploy_workflow_scripts(vault_path, agent_key, agent_config, paths)
|
2026-04-21 12:59:14 +00:00
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
# Step 7: Create junction or config
|
2026-04-21 12:50:17 +00:00
|
|
|
print_header("Step 5: Configuring Zotero Integration")
|
2026-04-21 13:12:12 +00:00
|
|
|
zotero_link = vault_path / paths["system_dir"] / "Zotero"
|
2026-04-21 12:50:17 +00:00
|
|
|
|
|
|
|
|
if zotero_link.exists() or zotero_link.is_symlink():
|
|
|
|
|
print_warning("Zotero link already exists")
|
|
|
|
|
else:
|
|
|
|
|
if create_junction(zotero_link, zotero_path):
|
|
|
|
|
print_success("Zotero junction created")
|
|
|
|
|
else:
|
|
|
|
|
print_warning("Failed to create junction, will use config file instead")
|
|
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
# Step 8: Save configuration
|
2026-04-21 12:50:17 +00:00
|
|
|
print_header("Step 6: Saving Configuration")
|
|
|
|
|
config = {
|
|
|
|
|
"zotero_path": str(zotero_path),
|
|
|
|
|
"storage_path": str(storage_path),
|
|
|
|
|
"ocr_api_key": ocr_api_key,
|
2026-04-21 13:12:12 +00:00
|
|
|
"agent": agent_key,
|
|
|
|
|
"agent_name": agent_config["name"],
|
|
|
|
|
"skill_dir": agent_config["skill_dir"],
|
2026-04-21 12:50:17 +00:00
|
|
|
}
|
2026-04-21 14:10:40 +00:00
|
|
|
create_env_file(vault_path, config, paths)
|
2026-04-21 13:12:12 +00:00
|
|
|
create_agents_md(vault_path, config, paths, agent_config)
|
2026-04-21 12:50:17 +00:00
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
# Step 9: Validation
|
2026-04-21 12:50:17 +00:00
|
|
|
print_header("Step 7: Validating Setup")
|
2026-04-21 13:12:12 +00:00
|
|
|
issues = validate_setup(vault_path, config, paths)
|
2026-04-21 12:50:17 +00:00
|
|
|
|
|
|
|
|
if issues:
|
|
|
|
|
print_error("\nValidation failed with the following issues:")
|
|
|
|
|
for issue in issues:
|
|
|
|
|
print(f" - {issue}")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
print_header("Installation Complete!")
|
|
|
|
|
print(f"""
|
|
|
|
|
{Colors.OKGREEN}Your literature workflow is ready to use!{Colors.ENDC}
|
|
|
|
|
|
2026-04-21 13:12:12 +00:00
|
|
|
Configuration Summary:
|
|
|
|
|
- Agent: {agent_config['name']}
|
|
|
|
|
- Skill Directory: {agent_config['skill_dir']}
|
|
|
|
|
- System Folder: {paths['system_dir']}
|
|
|
|
|
- Literature Path: {paths['literature_path']}
|
|
|
|
|
|
2026-04-21 12:50:17 +00:00
|
|
|
Next steps:
|
|
|
|
|
1. Open Obsidian and ensure your vault is loaded
|
|
|
|
|
2. Index your library: Run the index-refresh worker
|
|
|
|
|
3. Queue papers for analysis in the Base system
|
|
|
|
|
4. Run OCR on queued papers
|
|
|
|
|
5. Start deep reading with /LD-deep <zotero_key>
|
|
|
|
|
|
|
|
|
|
For detailed usage, see the documentation in docs/.
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
try:
|
|
|
|
|
sys.exit(main())
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
print("\n\nInstallation cancelled by user.")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print_error(f"Unexpected error: {e}")
|
|
|
|
|
sys.exit(1)
|