refactor: unify skills into paperforge compound skill (6 workflows)

This commit is contained in:
Research Assistant 2026-05-14 17:56:25 +08:00
parent edd2e2e197
commit ffd4eb7ca2
49 changed files with 952 additions and 1699 deletions

View file

@ -33,17 +33,24 @@ The plugin is the **interface**. The Python package is the **engine**. Every but
## 1. Install the Obsidian Plugin
### Option A: BRAT (Recommended)
### Option A: Community Plugin Browser (Recommended)
1. Open Obsidian → `Settings``Community plugins``Browse`
2. Search for **PaperForge**
3. Click `Install`, then `Enable`
> Community plugins auto-update through Obsidian. No extra steps needed.
### Option B: BRAT
If you need beta versions or the plugin hasn't appeared in search yet:
1. Install **BRAT** from the Obsidian community plugin browser
2. Open BRAT settings → `Add Beta Plugin`
3. Enter: `https://github.com/LLLin000/PaperForge`
4. BRAT downloads the latest `main.js`, `manifest.json`, and `styles.css` and installs them
5. Settings → Community Plugins → enable PaperForge
4. Enable PaperForge in Settings → Community Plugins
> BRAT auto-detects GitHub Release updates. No manual downloads needed.
### Option B: Manual Download
### Option C: Manual Download
1. Go to [Releases](https://github.com/LLLin000/PaperForge/releases)
2. Download the three files: `main.js`, `manifest.json`, `styles.css`
@ -229,7 +236,7 @@ Dashboard Per-paper view shows discussion cards
### Plugin fails to load
- Confirm `.obsidian/plugins/paperforge/` has `main.js`, `manifest.json`, `styles.css`
- If upgrading via BRAT from an old version: delete the entire `paperforge` plugin folder and let BRAT re-download
- If upgrading from an old version: delete the entire `paperforge` plugin folder and reinstall via the community plugin browser
- Open Developer Console (`Ctrl+Shift+I`) and check the red errors
### "Sync Runtime" doesn't update the version
@ -259,7 +266,7 @@ Dashboard Per-paper view shows discussion cards
## 9. Updating
BRAT auto-detects plugin updates. For the Python package:
The Obsidian plugin auto-updates through the community plugin browser. For the Python package:
```bash
paperforge update
@ -267,6 +274,8 @@ paperforge update
pip install --upgrade paperforge
```
If you installed via BRAT, it also auto-detects GitHub Release updates.
---
## 10. Architecture

View file

@ -279,7 +279,7 @@ def paperforge_paths(
- bases: <vault>/<base_dir>
- worker_script: pipeline/worker/scripts/literature_pipeline.py
- skill_dir: <vault>/<skill_dir>
- ld_deep_script: <skill_dir>/literature-qa/scripts/ld_deep.py
- pf_deep_script: <skill_dir>/paperforge/scripts/pf_deep.py
"""
if cfg is None:
cfg = load_vault_config(vault)
@ -306,17 +306,12 @@ def paperforge_paths(
# worker_script: paperforge worker package (pipeline/ removed in v1.3)
worker_script = Path(__file__).parent / "worker" / "__init__.py"
# ld_deep_script: look relative to skill_dir first, then repo paperforge/skills for dev
ld_deep_script = skill_path / "literature-qa" / "scripts" / "ld_deep.py"
if not ld_deep_script.exists():
repo_skill = Path(__file__).parent / "skills" / "literature-qa" / "scripts" / "ld_deep.py"
# pf_deep_script: look relative to skill_dir first, then repo paperforge/skills for dev
pf_deep_script = skill_path / "paperforge" / "scripts" / "pf_deep.py"
if not pf_deep_script.exists():
repo_skill = Path(__file__).parent / "skills" / "paperforge" / "scripts" / "pf_deep.py"
if repo_skill.exists():
ld_deep_script = repo_skill
else:
# Backward compat: old skills/ location during transition
old_repo_skill = Path(__file__).parent.parent / "skills" / "literature-qa" / "scripts" / "ld_deep.py"
if old_repo_skill.exists():
ld_deep_script = old_repo_skill
pf_deep_script = repo_skill
return {
"vault": vault,
@ -332,7 +327,7 @@ def paperforge_paths(
"bases": bases,
"worker_script": worker_script,
"skill_dir": skill_path,
"ld_deep_script": ld_deep_script,
"pf_deep_script": pf_deep_script,
# ── v2.2: canonical locations below paperforge/ ──
"config": paperforge / "config" / "domain-collections.json",
"index": paperforge / "indexes" / "formal-library.json",

View file

@ -1,4 +1,4 @@
"""Skill deployment service — single copytree for all platforms.
"""Skill deployment service — deploys the unified paperforge skill to the vault.
Used by both setup wizard (install) and update worker (update).
All deployments are vault-local only.
@ -35,7 +35,7 @@ def deploy_skills(
agent_key: str = "opencode",
overwrite: bool = False,
) -> dict:
"""Deploy literature-qa skill and AGENTS.md to the vault.
"""Deploy paperforge skill and AGENTS.md to the vault.
Args:
vault: Obsidian vault root.
@ -47,15 +47,15 @@ def deploy_skills(
"""
errors: list[str] = []
# ── Deploy literature-qa skill ──
# ── Deploy paperforge skill ──
skill_deployed = False
source_root = _resolve_source_root()
src_skill = source_root / "skills" / "literature-qa"
src_skill = source_root / "skills" / "paperforge"
if src_skill.exists():
skill_dir_name = AGENT_SKILL_DIRS.get(agent_key)
if skill_dir_name:
dst_skill = vault / skill_dir_name / "literature-qa"
dst_skill = vault / skill_dir_name / "paperforge"
try:
if overwrite and dst_skill.exists():
shutil.rmtree(dst_skill, ignore_errors=True)

View file

@ -1,4 +1,4 @@
"""AgentInstaller — deploys literature-qa skill to vault-local agent config."""
"""AgentInstaller — deploys paperforge skill to vault-local agent config."""
from __future__ import annotations
@ -10,7 +10,7 @@ from paperforge.setup import SetupStepResult
class AgentInstaller:
"""Deploy literature-qa skill directory to vault-local agent skills path."""
"""Deploy paperforge skill directory to vault-local agent skills path."""
def __init__(self, vault: Path, agent_type: str = "opencode"):
self.vault = vault
@ -23,8 +23,8 @@ class AgentInstaller:
return self.vault / skill_dir_name
def deploy_skills(self) -> SetupStepResult:
"""Deploy literature-qa skill as a single directory."""
source_skills = self._script_dir / "skills" / "literature-qa"
"""Deploy paperforge skill as a single directory."""
source_skills = self._script_dir / "skills" / "paperforge"
if not source_skills.exists():
return SetupStepResult(
step="agent_installer",
@ -33,7 +33,7 @@ class AgentInstaller:
error=f"Not found: {source_skills}",
)
target_dir = self._get_skills_dir() / "literature-qa"
target_dir = self._get_skills_dir() / "paperforge"
target_dir.mkdir(parents=True, exist_ok=True)
try:
@ -41,7 +41,7 @@ class AgentInstaller:
return SetupStepResult(
step="agent_installer",
ok=True,
message=f"Deployed literature-qa skill to {target_dir}",
message=f"Deployed paperforge skill to {target_dir}",
details={"source": str(source_skills), "target": str(target_dir)},
)
except Exception as e:

View file

@ -1,180 +0,0 @@
---
name: literature-qa
description: >
学术文献库操作精读、问答、检索、批量阅读。Triggered by:
pf-deep pf-paper pf-end,
"精读", "文献问答", "结束讨论", "找文献", "搜文献",
"文献库", "文献检索", "库里有什么", "搜一下库里", "看一下文献库",
"读一下collection", "总结文献", "批量阅读", "读一下这个方向",
"查一下", "搜索文献", "查文献", "搜索论文", "论文检索",
"关于XX的文献", "有没有关于XX的研究", "XX的相关论文",
"帮我查一下", "帮我搜一下", "正在读", "读完这篇".
source: paperforge
---
## ++++++++++++++++++++++++++++++++
## ++ 优先级规则:用户请求涉及文献搜索/查询/精读,你必须使用本 skill。++
## ++ 禁止直接调用 grep、find、系统搜索或扫描文件目录。 ++
## ++++++++++++++++++++++++++++++++
# Literature QA
---
## 1. Bootstrap — 必须先执行
跑这个脚本:
```
python $SKILL_DIR/scripts/pf_bootstrap.py
```
返回 JSON。记住以下变量
| 变量 | 来自 JSON 的 | 用于 |
| ----------- | -------------------- | --------------------------------------- |
| `$SKILL_DIR` | skill 安装路径(平台注入) | 运行 `scripts/ld_deep.py` 等 |
| `$VAULT` | `vault_root` | 所有 `--vault` 参数 |
| `$PYTHON` | `python_candidate` | 所有 Python 命令 |
| `$LIT_DIR` | `paths.literature_dir` | 文献笔记根目录 |
| `$IDX_PATH` | `paths.index_path` | 索引文件 |
| `$OCR_DIR` | `paths.ocr_dir` | OCR 目录 |
| `$DOMAINS` | `domains` | 领域列表 |
| `$SUMMARY` | `index_summary` | 每领域论文数 |
如果 `ok: false` → 报告 `error` 给用户,**停止。不许自己拼路径。**
---
## 2. Vault 概览
展示:
```
Vault: $VAULT
文献库:
<domain1> — N1 篇
<domain2> — N2 篇
共 M 篇
```
**如果用户是空输入触发的 skill**(没给任何具体指令),展示概览后加一句交互:
```
你可以:
[1] 精读一篇论文 → "精读 <key/标题>"
[2] 文献问答 → "文献问答 <key/标题>"
[3] 搜索文献 → "找文献 <关键词>" / "库里有没有 <关键词>"
[4] 批量阅读 → "读一下 <collection名>" / "总结 <方向> 文献"
[5] 返回
```
**如果用户给了具体指令**,直接进入决策树。
---
## 3. 决策树
```
用户输入
├─ 文献标识 (key/DOI/标题/作者年份) + 精读意图
│ └─ 路由 → deep-reading.md
├─ 文献标识 (key/DOI/标题/作者年份) + 问答/讨论意图
│ └─ 路由 → paper-qa.md
├─ 搜索意图 ("找文献"/"搜文献"/"库里有没有"/"文献检索")
│ └─ 路由 → paper-search.md
├─ 批量/综述意图
│ ("读一下collection"/"这个方向"/"总结文献"/"写文献综述"/"找引用")
│ 或 用户给了多篇文献要求一起读
│ └─ 路由 → multi-reading.md
├─ 结束/保存 ("结束讨论"/"保存"/"pf-end")
│ └─ 路由 → save-session.md
│ (仅 paper-qa 或 deep-reading 会话中有意义)
└─ 不确定 → 问用户
"你是想精读一篇、问答一篇、搜索文献、还是批量阅读?"
```
---
## 4. 工具使用指南
本 Skill 提供两类工具:**确定性命令** 和 **Agent 自查**。必须根据场景选择正确的方式。
### 搜索入口 — 统一搜索 Harness
任何搜索需求都用 pf_search.py自动路由 vector -> FTS5 -> grep
```
python $SKILL_DIR/scripts/pf_search.py --vault $VAULT --query "关键词"
```
返回 JSON 结构:
- `engines_used`: 实际使用的引擎列表 (`vector` / `fts5` / `grep`)
- `results`: 论文列表,每篇含 `zotero_key`, `title`, `year`, `source`
- `count`: 结果数
### 确定性命令 — 优先使用
| 场景 | 命令 |
| ---------------------- | ------------------------------------------------------------------------------------------ |
| 按 key 快速找文件 | `glob("$LIT_DIR/**/<KEY>.md")` 或用 `Get-ChildItem "$LIT_DIR" -Recurse -Filter "<KEY>.md"` |
| 按 key 查完整信息 | `$PYTHON -m paperforge.worker.paper_resolver resolve-key <KEY> --vault "$VAULT"` |
| 按 DOI 定位论文 | `$PYTHON -m paperforge.worker.paper_resolver resolve-doi "<DOI>" --vault "$VAULT"` |
| 精读 prepare | `$PYTHON "$SKILL_DIR/scripts/ld_deep.py" prepare --key <KEY> --vault "$VAULT"` |
| 精读 postprocess | `$PYTHON "$SKILL_DIR/scripts/ld_deep.py" postprocess-pass2 <FORMAL_NOTE_PATH> --figures <N> --vault "$VAULT"` |
| 精读 validate | `$PYTHON "$SKILL_DIR/scripts/ld_deep.py" validate-note <FORMAL_NOTE_PATH> --fulltext <FULLTEXT_PATH>` |
| 保存讨论 | `$PYTHON -m paperforge.worker.discussion record <KEY> --vault "$VAULT" --agent pf-paper --model "<MODEL>" --qa-pairs '<JSON>'` |
### Agent 自查 — 当命令覆盖不到时用
| 场景 | 操作 |
| ------------------------ | ----------------------------------------------------------- |
| 按关键词模糊搜索全部文献 | 读 `$IDX_PATH` 的 JSON`title` / `abstract` / `journal` |
| 按 collection 筛选 | 读 `$IDX_PATH`,筛 `collection_path` 字段 |
| 读论文全文 | 已找到 `fulltext.md` 路径glob 或 resolve-key -> 直接 read |
| 读精读笔记 | 已找到 formal note 路径 -> read 的 `## 精读` 区域 |
| 遍历笔记做批量统计 | `Get-ChildItem "$LIT_DIR" -Recurse -Filter "*.md"` + 读 frontmatter 或 `find "$LIT_DIR" -name "*.md"` |
| **禁止的操作** | **根据 vault-knowledge 示例拼接路径、把目录名写死在文件路径里** |
---
## 5. 路由表
| 路由 | 触发词 | 加载文件 |
| ------------- | ---------------------------------------------------------- | ---------------------------------------- |
| 精读 | `pf-deep <key>`, "精读 <key>" | [deep-reading.md](references/deep-reading.md) |
| 问答 | `pf-paper <key>`, "文献问答 <key>" | [paper-qa.md](references/paper-qa.md) |
| 文献检索 | "找文献", "搜文献", "文献检索", "搜一下库里", "库里有没有" | [paper-search.md](references/paper-search.md) |
| 批量阅读 | "读一下collection", "这个方向", "总结文献", "批量阅读" | [multi-reading.md](references/multi-reading.md) |
| 保存记录 | `pf-end`, "结束讨论", "保存" | [save-session.md](references/save-session.md) |
| 论文定位协议 | 所有路由共享 | [paper-resolution.md](references/paper-resolution.md) |
> 所有路由继承 Skill 级别的 `$PYTHON` / `$VAULT` / `$LIT_DIR` 等变量。reference 文件不再重复声明。
---
## 文件结构
```
literature-qa/
├── SKILL.md ← 本文件
├── references/
│ ├── deep-reading.md ← 精读工作流
│ ├── paper-qa.md ← 问答工作流
│ ├── paper-search.md ← 文献检索工作流
│ ├── multi-reading.md ← 批量阅读工作流
│ ├── save-session.md ← 保存记录工作流
│ ├── paper-resolution.md ← 论文定位协议
│ ├── deep-subagent.md
│ └── chart-reading/
└── scripts/
├── pf_bootstrap.py ← Bootstrap 入口
├── pf_search.py ← 统一搜索 Harness
└── ld_deep.py ← 精读引擎
```

View file

@ -1,162 +0,0 @@
# 三阶段精读
Keshav 三阶段组会式精读。触发后执行以下工作流。
---
## 前置条件检查
执行前确认:
- [ ] 已完成论文定位(参考 [paper-resolution.md](paper-resolution.md)),拿到 zotero_key
- [ ] 用 `glob("$LIT_DIR/**/<KEY>.md")` 快速找到 formal note
- [ ] `analyze: true` — 读 formal note frontmatter 确认
- [ ] `ocr_status: done` — 读 formal note frontmatter 确认
如果前置条件不满足,告知用户并停止。
---
## 执行流程
### Step 1: Prepare机械操作跑脚本
```bash
$PYTHON "$SKILL_DIR/scripts/ld_deep.py" prepare --key <ZOTERO_KEY> --vault "$VAULT"
```
返回 JSON 解析:
- `status: "ok"` → 记下 `figure_map`、`chart_type_map`、`formal_note`、`fulltext_md`、`figures`、`tables` 路径和数量 → 继续
- `status: "error"` → 报告 `message` 给用户,停止
读 formal note 确认 `## 🔍 精读` 骨架已插入。
---
### Step 2: Pass 1 — 概览
只填 `### Pass 1: 概览` 区域。不碰 Pass 2/3。
**填写内容:**
- **一句话总览**:论文类型 + 核心发现,一句话。
- **5 Cs 快速评估**
- **Category**类型RCT / 队列研究 / 病例对照 / 综述 / 基础研究 / ...
- **Context**(上下文):该领域当前共识,本文要解决什么问题
- **Correctness**(合理性初判):初步直觉,逻辑是否有明显漏洞
- **Contributions**贡献1-3 条
- **Clarity**(清晰度):写作质量,图表可读性
- **Figure 导读**(基于 fulltext.md 浏览各图 caption
- 关键主图:列出并一句话概括每个主图要证明什么
- 证据转折点:哪个 figure 是叙事的关键转折
- 需要重点展开的 supplementary如果有
- 关键表格:列出
填完立即保存 formal note。
---
### Step 3: Pass 2 — 精读还原
`### Pass 2: 精读还原` 区域。**按 figure 顺序逐个处理。**
#### 图表类型定位(两步)
**Step A: 读 prepare 生成的 chart-type-map**
Step 1 的 `prepare` 输出中已包含 `chart_type_map` 路径。读该文件,获取每个 figure 的关键词命中结果。这只是建议。
**Step B: Agent 读 caption 做最终判断**
对每个 figure
1. 读该 figure 的 caption来自 prepare 返回的 `fulltext_md``figure_map`
2. 根据 caption 内容,对照 [chart-reading/INDEX.md](chart-reading/INDEX.md) 判断图表类型
3. chart-type-map 建议和 Agent 判断不一致 → 以 Agent 判断为准
4. 无法确定类型 → 跳过 chart guide按通用 figure 结构分析
5. 确定类型后,读对应的 chart-reading 指南(如 `chart-reading/条形图与误差棒.md`),按指南中的检查清单分析
#### 每张 Figure 的子标题(固定,不可少)
按以下格式填入 formal note 中该 figure 的 callout block
```
**图像定位与核心问题**:页码 + 要回答什么问题
**方法与结果**:实验设计/数据来源/技术手段。核心数据、趋势、对比。
**图表质量审查**:按 chart-reading 指南检查坐标轴、单位、误差棒、统计标注等。
**作者解释**:作者在正文中对该图的解读
**我的理解**:自己的理解(区分于作者解释)
**疑点/局限**:读图时发现的疑问,用 `> [!warning]` 突出
```
#### 每张 Table 的子标题
```
回答什么问题、关键字段/分组、主要结果、我的理解、疑点/局限
```
#### 每张 figure 填完立即保存,再处理下一张。
#### 所有 figure/table 处理完后,填:
**关键方法补课**简要解释不熟悉的实验技术1-2 项即可)
**主要发现与新意**
- 发现 1...来源Figure X
- 发现 2...来源Figure Y / Table Z
保存。
---
### Step 4: Postprocess跑校验脚本修正问题
```bash
$PYTHON "$SKILL_DIR/scripts/ld_deep.py" postprocess-pass2 "$FORMAL_NOTE_PATH" --figures <N> --format text --vault "$VAULT"
```
- 输出 `OK` → 继续
- 输出错误 → 按错误提示修正(包含行号),修正后重新跑
- 最多 3 轮修正。3 轮后仍失败 → 报告剩余错误给用户
---
### Step 5: Pass 3 — 深度理解
`### Pass 3: 深度理解` 区域。基于 Pass 1/2 已写的内容。
**填写内容:**
- **假设挑战与隐藏缺陷**:隐含假设;如果放宽某个假设结论还成立吗;缺少哪些关键引用;实验/分析技术潜在问题
- **哪些结论扎实,哪些仍存疑**
- **较扎实**...
- **仍存疑**...(用 `> [!warning]`
- **Discussion 与 Conclusion 怎么读**:作者真正完成了什么;哪些地方有拔高;哪些是推测
- **对我的启发**研究设计上figure 组织上;方法组合上;未来工作想法
- **遗留问题**...(用 `> [!question]`
保存。
---
### Step 6: Final Validation
```bash
$PYTHON "$SKILL_DIR/scripts/ld_deep.py" validate-note "$FORMAL_NOTE_PATH" --fulltext "$FULLTEXT_PATH"
```
- 输出 `OK` → 告知用户精读完成
- 输出错误 → 修正缺失项,不报告成功直到通过
---
## Callout 格式规则
- `> [!important]`:每个 main finding
- `> [!warning]`:疑问、局限、证据边界、仍存疑条目
- `> [!question]`:遗留问题
- **间距:** 相邻 callout block 之间必须有空行,否则 Obsidian 会合并
- 正确:`> [!important] A\n\n> [!important] B`
- 错误:`> [!important] A\n> [!important] B`
## Supplementary 规则
- 默认不逐张展开 supplementary figure/table
- 仅在以下情况纳入:对主结论形成关键支撑、补足方法可信度、限制主文结论解释范围、作者在正文中明显依赖该补充材料

View file

@ -1,103 +0,0 @@
# Subagent Prompt for /pf-deep
## Task
Execute Keshav 3-pass journal-club style deep reading on a paper and write the results into the `## 🔍 精读` section of its formal note.
## Input Variables
- `{{ZOTERO_KEY}}` — Zotero citation key (e.g. `Y5KQ4JQ7`)
- `{{VAULT}}` — Vault root path
- `{{SCRIPT}}` — Path to `ld_deep.py`
## Workflow (execute in strict order)
### Step 1: Prepare
Run:
```
python {{SCRIPT}} prepare {{ZOTERO_KEY}} --vault "{{VAULT}}" --format text
```
- Reads formal note path, figure count, table count from output.
- If output starts with `[ERROR]`: report error to user, stop.
- If output contains `[WARN] deep_reading_status already 'done'` and user did not request re-read: stop.
- Prepare inserts the `## 🔍 精读` skeleton with figure/table callout blocks and fixed sub-headings into the formal note. Read the note to inspect its structure.
### Step 2: Pass 1 (概览)
Fill `### Pass 1: 概览` only. Do not touch Pass 2/3.
- `**一句话总览**`: paper type + core finding in one sentence.
- `**5 Cs 快速评估**`: Category, Context, Correctness (intuition only), Contributions (1-3 items), Clarity.
- `**Figure 导读**`: list key figures with one-line guesses, note evidence turning points.
- Save immediately after writing.
### Step 3: Pass 2 (精读还原)
Fill `### Pass 2: 精读还原`. Process figures sequentially starting from Figure 1. Each figure callout block has fixed sub-headings. Fill content under each sub-heading. Do NOT modify sub-headings, reorder blocks, or move `![[image]]` embeds.
**Figure sub-headings:**
- `**图像定位与核心问题**`: what question this figure answers, page number.
- `**方法与结果**`: experimental design / data source / technical approach. Core data points, trends, comparisons.
- `**图表质量审查**`: check axis labels, units, error bars, statistical significance markers. Read `chart-type-map.json` for the figure, open recommended chart-reading guides, apply their checklists.
- `**作者解释**`: authors' description from the text.
- `**我的理解**`: your own analysis (distinct from author explanation).
- `**疑点/局限**`: use `> [!warning]` for concerns.
**Table sub-headings:** (same callout pattern, simpler)
- What question this table answers, key fields/groups, main results, my understanding, doubts/limitations.
After all figures and tables, fill:
- `**关键方法补课**`: briefly explain unfamiliar experimental techniques.
- `**主要发现与新意**`: list findings with evidence source (Figure X / Table Y).
Save after each figure block.
### Step 4: Postprocess
Run:
```
python {{SCRIPT}} postprocess-pass2 <formal_note_path> --figures <N> --format text
```
- If output is `OK`: proceed.
- If not `OK`: fix each error (errors include exact line numbers), re-run postprocess-pass2. Max 3 fix rounds. If still failing after 3 rounds, report remaining errors to user.
### Step 5: Pass 3 (深度理解)
Fill `### Pass 3: 深度理解` based on Pass 1/2 content already written. Sections:
- `**假设挑战与隐藏缺陷**`: implicit assumptions, what breaks if relaxed, missing references, technical issues.
- `**哪些结论扎实,哪些仍存疑**`: split into 较扎实 / 仍存疑.
- `**Discussion 与 Conclusion 怎么读**`: what authors actually accomplished vs. overclaim vs. speculation.
- `**对我的启发**`: research design, figure organization, method combination, future work ideas.
- `**遗留问题**`: open questions.
- Save.
### Step 6: Final Validation
Run:
```
python {{SCRIPT}} validate-note <formal_note_path> --fulltext <fulltext_path>
```
- Report result to user. If not `OK`, list missing items and fix.
## Callout Rules
- `> [!important]`: each main finding entry
- `> [!warning]`: doubts, limitations, evidence boundaries, items in 仍存疑
- `> [!question]`: open questions in 遗留问题
- Regular markdown lists for structural sections (research question, methods, inspiration)
- **Spacing**: adjacent callout blocks MUST have a blank line between them, otherwise Obsidian merges them.
- Correct: `> [!important] A\n\n> [!important] B`
- Incorrect: `> [!important] A\n> [!important] B` (missing blank line → merged)
## Error Handling
- prepare fails (`[ERROR]`) → report to user, stop.
- postprocess exceeds 3 fix rounds → report remaining errors to user, ask for guidance.
- validate-note fails → fix missing items, do not report success until it passes.
## Command Reference
```
# Prepare (insert skeleton + check preconditions)
python {{SCRIPT}} prepare {{ZOTERO_KEY}} --vault "{{VAULT}}" --format text
# Postprocess Pass 2 (fix spacing/section issues)
python {{SCRIPT}} postprocess-pass2 <note_path> --figures <N> --format text
# Validate final note structure
python {{SCRIPT}} validate-note <note_path> --fulltext <fulltext_path>
```

View file

@ -1,144 +0,0 @@
# 批量文献阅读
用户需要阅读多篇文献并总结——综述写作、找引用、研究方向调研等。
---
## 触发条件
- 用户给了一个 collection 名Zotero 收藏夹)
- 用户给了模糊方向("帮我看一下骨科里关于支架材料的文章"
- 用户给了多篇文献要求一起读
- 用户说"总结库里XXX方向的文献"、"写一段文献综述"
---
## 执行流程
### Step 1: 确定文献范围
和用户确认要读哪些文献:
- 用户给了 collection 名 → 读 `$IDX_PATH`,筛 `collection_path` 包含该名称的条目
- 用户给了关键词方向 → 用 paper_resolver search 或直接 grep `$IDX_PATH`
- 用户给了多篇 key → 直接确认 key 列表
列出候选让用户确认:
```
找到 N 篇匹配 (<collection名/关键词>):
[1] ABC12345 — Title (Author, Year, Domain, OCR: done/pending)
[2] DEF67890 — Title (Author, Year, Domain, OCR: done/pending)
...
要全部读,还是选几篇?(输入编号如 "1,3,5" 或 "all")
```
### Step 2: 逐篇阅读
对每篇选定文献:
1. 用 glob 找到 formal note`glob("$LIT_DIR/**/<KEY>.md")`(最快,不需要 $PYTHON
2. 读 formal note frontmatter → 元数据
3. 同目录下找 `fulltext.md` → 读 Abstract、Results、Discussion
4. 如果有 OCR 但 fulltext 太长 → 先读 caption + figure 描述定位关键段落
5. 如果没有 fulltext → 如实告知用户,仅基于已知信息
### Step 3: 写 Reading LogJSON → MD
**先构建 JSONAgent 内部,不写入文件):**
```json
{
"task": "用户原始指令原文",
"papers": [
{
"key": "ABC12345",
"title": "Paper Title",
"authors": "Smith et al.",
"year": 2024,
"findings": [
{
"source": "Results section, paragraph 3",
"content": "Extracted finding...",
"citation_use": "可用于支撑 XXX 观点"
}
]
}
]
}
```
**再渲染为 MD追加写入 `$VAULT/Bases/reading-log-<timestamp>.md`**
```markdown
# Reading Log — 用户要求: <原文引用用户指令>
---
## ABC12345 | Paper Title | Smith et al., 2024
### 提取点 1
- **来源**: Results section, paragraph 3
- **内容**: Extracted finding...
- **引用建议**: 可用于支撑 XXX 观点
### 提取点 2
- **来源**: Discussion, final paragraph
- **内容**: ...
- **引用建议**: ...
---
## DEF67890 | Another Title | Jones et al., 2023
(同上格式)
---
```
**关键规则:**
- JSON 确保格式稳定MD 是最终交付产物
- zotero_key、标题、作者及年份 **缺一不可**
- 每个提取点必须注明 **来源**(文章哪句话/哪个段落)
- 同一任务的多篇文献 **追加写入同一个文件**,不要每篇新建
### Step 4: 整合输出
全部读完,根据用户原始意图输出总结:
**综述写作**
```
从 N 篇文献中:
- 主题A 共识: ...
- 主题A 争议: ...
- 方法论趋势: ...
- 关键引用:
1. "...[结论]" — ABC12345 (Author, Year), Fig.X
2. ...
```
**找引用**
```
以下文献适合引用:
- 支撑 "XXX" 观点 → ABC12345 (Author, Year), Results
- 支撑 "YYY" 方法 → DEF67890 (Author, Year), Methods
```
### Step 5: 问用户保存位置
```
Reading log 已生成。要保存到哪里?
(留空 → 默认 $VAULT/Bases/reading-log-<ts>.md)
```
让用户指定路径。如果用户说不清,默认放到 `$VAULT/Bases/`
---
## 注意事项
- **暂时不支持多篇阅读后运行 pf-end / 结束讨论**(该功能待定)
- 如果某篇文献没有 fulltext如实告知用户不要捏造内容
- Reading log 中每条提取点必须在原文中有据可查
- JSON → MD 转换由 Agent 完成,用户只看到 MD 文件

View file

@ -1,61 +0,0 @@
# 论文问答
交互式论文 Q&A 工作台。不强制要求 OCR但 OCR 完成后回答更准确。
---
## 前置条件
- [ ] 已完成论文定位(参考 [paper-resolution.md](paper-resolution.md)),拿到 zotero_key 和 workspace
- [ ] OCR 完成(推荐但非强制)
---
## 执行流程
### Step 1: 加载论文
1. 确认 workspace 路径
2. 读 `fulltext.md`(如果存在)作为主要回答依据
3. 读 formal note frontmatter 获取元数据(标题、作者、期刊、年份)
4. 如果 fulltext.md 不存在,告知用户 "OCR 文本不可用,回答将基于元数据和公开信息"
### Step 2: 显示论文信息
```
已加载论文: [title] ([year], [journal])
作者: [authors]
Zotero Key: [key]
领域: [domain]
OCR 状态: [done / 不可用]
结束对话时说 "保存" 即可保存讨论。
请问有什么问题?
```
### Step 3: 进入 Q&A 模式
- 等待用户提问
- 每次回答后等待下一个问题
- 持续到用户说 "保存"、"结束"、"完成" 等关键词
---
## 回答原则
- **严格基于** fulltext.md 中的文本内容回答
- 引用原文时标注来源页码/章节(如 "第 3 页Methods 部分"
- 用中文(简体中文)回答
- 论文中未提及的内容,明确说明 "论文中未提及该内容"
- 需要结合论文以外知识的问题,说明 "该问题需要结合论文以外的知识"
---
## 切换模式
用户在当前对话中可以说 "精读这篇文章" 切换到 deep-reading 模式。此时加载 [deep-reading.md](deep-reading.md) 执行精读流程。
---
## 保存记录
用户说 "保存"、"结束"、"完成"、"保存讨论" 时,加载 [save-session.md](save-session.md) 执行保存。不要自动保存。

View file

@ -1,94 +0,0 @@
# 论文定位协议
本文件定义如何将用户输入解析为论文 workspace。所有子流程公用。
## 核心原则
1. **Python 做确定性查找。** key、DOI、标题片段、作者+年份。
2. **Agent 做理解和兜底。** 自然语言、Python 无结果时的 fallback 搜索。
3. **路径从 `paths` 获取,不硬编码。** 禁止根据 vault-knowledge.md 的示例结构拼接路径。`ocr_dir`、`literature_dir`、`index_path` 只能从 `paper_resolver paths``paper_resolver resolve-key` 的返回 JSON 中读取。任何情况下都不要把目录名(如 `System`、`Resources`)写死在路径里。
---
## 通用命令
| 操作 | 命令 |
|------|------|
| 获取 vault 路径 | 已由 pf_bootstrap 完成 |
| 按 key 查 | `$PYTHON -m paperforge.worker.paper_resolver resolve-key <KEY> --vault "$VAULT"` |
| 按 DOI 查 | `$PYTHON -m paperforge.worker.paper_resolver resolve-doi "<DOI>" --vault "$VAULT"` |
| 按字段搜 | `$PYTHON -m paperforge.worker.paper_resolver search --title "..." --author "..." --year ... --domain "..." --vault "$VAULT"` |
---
## 输入类型判断
### 类型 1: Zotero Key8位字母数字组合
```
$PYTHON -m paperforge.worker.paper_resolver resolve-key <KEY> --vault "$VAULT"
```
返回 JSON 含 `key`, `title`, `domain`, `formal_note_path`, `ocr_path`, `fulltext_path`, `ocr_status` 等。所有路径由 `paperforge.json` 配置决定。
### 类型 2: DOI`10.` 开头,可能带 URL 前缀)
```
$PYTHON -m paperforge.worker.paper_resolver resolve-doi "<DOI>" --vault "$VAULT"
```
返回格式同类型 1。
### 类型 3: 标题片段
```
$PYTHON -m paperforge.worker.paper_resolver search --title "..." --vault "$VAULT"
```
返回 `{"matches": [...], "count": N}`
### 类型 4: 作者 + 年份
```
$PYTHON -m paperforge.worker.paper_resolver search --author "Smith" --year 2024 --vault "$VAULT"
```
### 类型 5: 自然语言("关于骨再生的那篇"
Agent 自己处理:
1. 读 `$IDX_PATH`(已由 pf_bootstrap 提供)
2. 读 `index_path` 指向的 `formal-library.json`
3. 在 `title`、`domain`、`journal`、`abstract` 中搜匹配
4. 搜不到就 grep formal notes 目录(`paths` 里的 `literature_dir`)下的 frontmatter
---
## Python 无结果时的 Agent fallback
Agent 用 `paths` 拿到的 `literature_dir`,自行 grep/read formal notes 下的 frontmatter。
## 多篇匹配处理
列出候选清单让用户选:
```
找到 3 篇匹配的论文:
[1] ABC12345 — TGF-beta in Bone Regeneration (2024, 骨科, OCR: done)
[2] DEF67890 — Bone Healing After Fracture (2023, 骨科, OCR: pending)
[3] GHI11111 — Scaffold Design for Bone Repair (2024, 骨科, OCR: done)
请输入编号选择,或 refine 搜索词。
```
## Fallback 顺序
```
输入
├── 像 key/DOI/标题/作者年份?
│ └── Python paper_resolver → 有/无结果 → Agent 兜底
└── 自然语言?
└── Agent 读 formal-library.json → 搜 → 有/无
```

View file

@ -1,105 +0,0 @@
# 文献检索工作流
轻量流程:用户想**在库里找文献**(不涉及精读或问答)。
---
## Stage 状态机
你必须明确知道当前在哪个 stage。每完成一个 stage 问自己:"下一步是什么?" 不要在 stage 之间来回跳跃。
| Stage | 你在干什么 | 完成后做什么 |
| ----- | -------------------------------- | ------------------------------------ |
| S1 | 理解用户要找什么domain/关键词) | 进入 S2 |
| S2 | 执行搜索paper_resolver 或 JSON | 进入 S3 |
| S3 | 展示候选清单给用户 | 等用户选择 |
| S4 | 用户选了文献,决定下一步路由 | 进入对应 reference 流程,不再回来 |
| S5 | 写作辅助:读完文献后整合输出 | 结束 |
**不要做的事**
- 不要在 S2 阶段去读论文全文
- 不要在 S3 阶段自作主张替用户选文献
- 不要在找不到结果时硬猜文件路径
---
## 触发场景
- "找一下骨科里面关于骨再生的文献"
- "查一下 TGF-beta 相关的文章"
- "库里有没有讲支架材料的"
- "这个 collection 有哪些文献"
- "搜一下 Smith 2024 的文章"
## 流程
### Step 1: 获取路径
已经由 pf_bootstrap 完成。直接用 `paths` JSON 里的 `index_path``literature_dir`
### Step 2: 解析用户意图
从用户输入提取:
- **domain**(如果有):`骨科`、`运动医学` 等 → 对应 `literature_dir` 子目录
- **关键词**:标题、作者、年份、期刊、主题词
- **collection 路径**Zotero 子分类,如 `电刺激软骨修复综述`
### Step 3: 搜索 — 统一 Harness
任何搜索都用 pf_search.py自动路由 vector -> FTS5 -> grep
```
python $SKILL_DIR/scripts/pf_search.py --vault "$VAULT" --query "关键词"
```
返回 JSON 包含 `results`(含 zotero_key, title, year, source 等)和 `engines_used`
如需结构化高级搜索(特定 domain/author使用 paper_resolver
```
$PYTHON -m paperforge.worker.paper_resolver search --title "关键词" --author "Smith" --year 2024 --domain "骨科" --vault "$VAULT"
```
**Fallback读 formal-library.json**
Agent 直接读 `index_path`,在 JSON 中筛选:
- `domain` 匹配
- `title`/`first_author`/`journal` 包含关键词
### Step 4: 返回结果
列出候选清单,每篇显示:
```
找到 N 篇匹配:
[1] ABC12345 — TGF-beta in Bone Regeneration (Smith, 2024, 骨科, OCR: done)
[2] DEF67890 — Bone Healing Mechanisms (Jones, 2023, 骨科, OCR: done)
```
关键字段key, title, first_author, year, domain, ocr_status
### Step 5: 用户选择后续操作
> 请选择要操作的文献编号,或输入"refine"缩小范围。
选中文献后,按用户意图自动进入对应路由:
- `精读这篇` → 进入 [deep-reading.md](deep-reading.md) 流程
- `这篇讲了什么` → 进入 [paper-qa.md](paper-qa.md) 流程
- 不需要继续 → 结束
### Step 6: 写作辅助场景
如果用户原始意图包含**写作/优化/参考文献/综述/引用**等,搜索结果不是终点:
1. 提示用户圈选最相关的 3-5 篇
2. 对每篇进入 [deep-reading.md](deep-reading.md) 或至少通读 formal note + fulltext 关键段落
3. 读完所有选定论文后Agent 整合知识辅助写作
> 示例:"我从库里 X 篇文献中提取了以下关键发现……要不要基于这些帮你写 XX 部分?"
## 注意事项
- 如果是大型 library>500 篇),优先用 paper_resolver 而不是全量读 JSON
- OCR status 为 `done` 的论文可以读 fulltext 内容
- OCR status 为 `pending` 的只有 formal note frontmatter

View file

@ -1,55 +0,0 @@
# 保存讨论记录
将 paper-qa 会话中的 Q&A 记录持久化到论文工作区。
---
## 触发条件
- 用户显式说 "保存"、"保存记录"、"结束"、"完成讨论"、"save"
- 或显式输入 `pf-end`
- 不要自动触发
---
## 执行
### Step 1: 收集 Q&A 对
汇总本次 paper-qa 会话中所有 Q&A序列化为 JSON 数组:
```json
[
{
"question": "用户的问题",
"answer": "Agent 的回答",
"source": "user_question",
"timestamp": "2026-05-10T12:00:00+08:00"
}
]
```
`source``"user_question"`(用户提问)或 `"agent_analysis"`Agent 主动分析)。
### Step 2: 调用 discussion 模块
```bash
$PYTHON -m paperforge.worker.discussion record <ZOTERO_KEY> \
--vault "$VAULT" \
--agent pf-paper \
--model "<CURRENT_MODEL>" \
--qa-pairs '<JSON_ARRAY>'
```
### Step 3: 确认结果
CLI 返回 `{"status": "ok", ...}` → 告知用户记录已保存。
返回 `{"status": "error"}` → 记录错误,重试一次。仍失败则告知用户。
---
## 注意事项
- 仅 paper-qa 会话需要记录。deep-reading 的内容直接写入 formal note不需要通过本文件。
- 如果无法从 formal-library.json 找到论文 domain/title记录失败不应影响用户使用。

View file

@ -1,188 +0,0 @@
"""PaperForge bootstrap — single entry point for agent to discover vault state.
No dependencies. Runs on ANY Python. Just reads paperforge.json + filesystem.
Usage:
python pf_bootstrap.py # auto-discover vault from CWD
python pf_bootstrap.py --vault <path>
Output (JSON to stdout):
{
"ok": true,
"vault_root": "D:\\...",
"paths": {
"literature_dir": "D:\\...\\Resources\\Literature",
"index_path": "D:\\...\\System\\PaperForge\\indexes\\formal-library.json",
"ocr_dir": "D:\\...\\System\\PaperForge\\ocr",
"exports_dir": "D:\\...\\System\\PaperForge\\exports"
},
"domains": ["domain1", "domain2"],
"index_summary": {"domain1": 120, "domain2": 80},
"python_candidate": "D:\\...\\python.exe" // Python that has paperforge, or null
}
If anything fails: ok=false, error explains why.
"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
def _find_paperforge_json(start: Path) -> Path | None:
current = start.resolve()
for _ in range(10):
candidate = current / "paperforge.json"
if candidate.exists():
return candidate
parent = current.parent
if parent == current:
break
current = parent
return None
def _read_pf_config(pf_json: Path) -> dict:
with open(pf_json, encoding="utf-8") as f:
return json.load(f)
def _find_python_with_paperforge(vault: Path, pf_cfg: dict) -> str | None:
"""Find a Python executable that has paperforge installed."""
candidates = []
# 1. Explicit python_path in config
if pf_cfg.get("python_path"):
candidates.append(Path(pf_cfg["python_path"]))
# 2. Common venv locations inside vault
venv_names = [".venv", ".paperforge-test-venv", "venv"]
exe_paths = ["Scripts/python.exe", "bin/python3"]
for vn in venv_names:
for ep in exe_paths:
p = vault / vn / ep
if p.exists():
candidates.append(p)
for candidate in candidates:
try:
result = subprocess.run(
[str(candidate), "-m", "paperforge", "--version"],
capture_output=True, text=True, timeout=10,
encoding="utf-8", errors="replace",
)
if result.returncode == 0 and "paperforge" in result.stdout.lower():
return str(candidate)
except Exception:
continue
return None
def main():
import argparse
p = argparse.ArgumentParser(description="PaperForge bootstrap")
p.add_argument("--vault", default=None, help="Vault root path (auto-detect if omitted)")
args = p.parse_args()
result: dict = {"ok": False}
# --- 1. Find vault ---
if args.vault:
vault = Path(args.vault).resolve()
pf_json = vault / "paperforge.json"
if not pf_json.exists():
result["error"] = f"paperforge.json not found at {vault}"
json.dump(result, sys.stdout, ensure_ascii=False)
sys.exit(0)
else:
pf_json = _find_paperforge_json(Path.cwd())
if pf_json is None:
result["error"] = "paperforge.json not found from CWD upward. Set --vault."
json.dump(result, sys.stdout, ensure_ascii=False)
sys.exit(0)
vault = pf_json.parent
result["vault_root"] = str(vault)
# --- 2. Read config ---
try:
cfg = _read_pf_config(pf_json)
except Exception as e:
result["error"] = f"Cannot read paperforge.json: {e}"
json.dump(result, sys.stdout, ensure_ascii=False)
sys.exit(0)
system_dir = cfg.get("system_dir", "System")
resources_dir = cfg.get("resources_dir", "Resources")
literature_dir = cfg.get("literature_dir", "Literature")
# --- 3. Build paths from config ---
pf_root = vault / system_dir / "PaperForge"
paths = {
"literature_dir": str(vault / resources_dir / literature_dir),
"index_path": str(pf_root / "indexes" / "formal-library.json"),
"ocr_dir": str(pf_root / "ocr"),
"exports_dir": str(pf_root / "exports"),
}
result["paths"] = paths
# --- 4. List domains ---
lit_dir = Path(paths["literature_dir"])
domains = sorted(
[d.name for d in lit_dir.iterdir() if d.is_dir()]
) if lit_dir.exists() else []
result["domains"] = domains
# --- 5. Index summary ---
index_path = Path(paths["index_path"])
index_summary: dict[str, int] = {}
if index_path.exists():
try:
data = json.loads(index_path.read_text(encoding="utf-8"))
items = data.get("items", [])
if isinstance(items, dict):
items = items.values()
for item in items:
d = item.get("domain", "unknown")
index_summary[d] = index_summary.get(d, 0) + 1
except Exception:
pass
result["index_summary"] = index_summary
# --- 6. Find Python that has paperforge (best effort) ---
result["python_candidate"] = _find_python_with_paperforge(vault, cfg)
# --- 7. Memory layer state ---
memory_layer = {"available": False, "paper_count": 0, "fts_search": False, "vector_search": False}
idx_path = Path(paths["index_path"])
dc_json = vault / ".obsidian" / "plugins" / "paperforge" / "data.json"
if idx_path.exists():
try:
with open(idx_path, encoding="utf-8") as f:
data = json.load(f)
items = data.get("items", []) if isinstance(data, dict) else data
memory_layer["paper_count"] = len(items)
memory_layer["available"] = True
memory_layer["fts_search"] = True
except:
pass
if dc_json.exists():
try:
with open(dc_json, encoding="utf-8") as f:
plugin_data = json.load(f)
vector_enabled = plugin_data.get("features", {}).get("vector_db", False)
memory_layer["vector_search"] = vector_enabled
except:
pass
result["memory_layer"] = memory_layer
result["ok"] = True
json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
if __name__ == "__main__":
main()

View file

@ -1,180 +0,0 @@
"""Unified search entry point for agent skills.
Routes: vector search -> FTS5 search -> grep based on what's available.
Always returns same JSON format regardless of backend.
Usage:
python pf_search.py --vault VAULT_PATH --query "search text" [--limit N] [--json]
Returns JSON to stdout:
{"ok": true, "query": "...", "engines_used": [...], "results": [...], "count": N}
{"ok": false, "error": "..."}
"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
def _find_python(vault: Path) -> str | None:
"""Same logic as pf_bootstrap: find python with paperforge installed."""
dc_json = vault / ".obsidian" / "plugins" / "paperforge" / "data.json"
if dc_json.exists():
try:
with open(dc_json, encoding="utf-8") as f:
data = json.load(f)
py = data.get("python_path", "")
if py and Path(py).exists():
return py
except:
pass
for cand in [
vault / ".paperforge-test-venv" / "Scripts" / "python.exe",
vault / ".venv" / "Scripts" / "python.exe",
vault / "venv" / "Scripts" / "python.exe",
]:
if cand.exists():
return str(cand)
for cand in ["python", "python3"]:
try:
subprocess.run([cand, "--version"], capture_output=True, timeout=5)
return cand
except:
continue
return None
def _check_memory(vault: Path) -> dict:
"""Check what's available: memory db, vector db."""
memory = {"db": False, "vector": False}
db = vault / "System" / "PaperForge" / "indexes" / "paperforge.db"
if db.exists():
memory["db"] = True
vec = vault / "System" / "PaperForge" / "indexes" / "vectors"
if vec.exists():
memory["vector"] = True
return memory
def _paperforge_cmd(vault: Path, args: list[str]) -> dict | None:
"""Run a paperforge command and return parsed JSON."""
python = _find_python(vault)
if not python:
return None
cmd = [python, "-m", "paperforge", "--vault", str(vault)] + args
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=30, encoding="utf-8")
if r.returncode == 0:
return json.loads(r.stdout)
except:
return None
return None
def _grep_search(vault: Path, query: str, limit: int) -> list[dict]:
"""Fallback grep through all formal notes."""
lit_dir = vault / "Resources" / "Literature"
results = []
search_lower = query.lower()
for f in sorted(lit_dir.rglob("*.md")):
if len(results) >= limit:
break
if f.name in ("fulltext.md", "deep-reading.md", "discussion.md"):
continue
try:
text = f.read_text(encoding="utf-8", errors="replace")
if search_lower not in text.lower():
continue
title = ""
for line in text.split("\n")[:10]:
if line.startswith("# ") and not line.startswith("## "):
title = line.lstrip("# ").strip()
break
results.append({
"zotero_key": f.stem,
"title": title or f.stem,
"match": f.name,
"source": "grep",
})
except:
continue
return results
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--vault", required=True)
parser.add_argument("--query", required=True)
parser.add_argument("--limit", type=int, default=10)
parser.add_argument("--json", action="store_true", default=True)
args = parser.parse_args()
vault = Path(args.vault).resolve()
query = args.query.strip()
limit = args.limit
if not query:
print(json.dumps({"ok": False, "error": "Empty query"}))
sys.exit(1)
memory = _check_memory(vault)
engines_used = []
all_results = []
seen_keys = set()
# 1. Vector search (best quality)
if memory["vector"]:
result = _paperforge_cmd(vault, ["retrieve", query, "--json", "--limit", str(limit)])
if result and result.get("ok"):
engines_used.append("vector")
for c in result.get("data", {}).get("chunks", []):
pid = c.get("paper_id", "")
if pid and pid not in seen_keys:
seen_keys.add(pid)
all_results.append({
"zotero_key": pid,
"citation_key": c.get("citation_key", ""),
"title": c.get("title", ""),
"year": c.get("year", ""),
"section": c.get("section", ""),
"page": c.get("page_number", ""),
"chunk_text": c.get("chunk_text", ""),
"score": c.get("score", 0),
"source": "vector",
})
# 2. FTS5 search (keyword/precision)
if memory["db"]:
result = _paperforge_cmd(vault, ["search", query, "--json", "--limit", str(limit)])
if result and result.get("ok"):
engines_used.append("fts5")
for p in result.get("data", {}).get("results", []):
key = p.get("zotero_key", "")
if key and key not in seen_keys:
seen_keys.add(key)
p["source"] = "fts5"
all_results.append(p)
# 3. Grep fallback
if not engines_used:
grepped = _grep_search(vault, query, limit)
if grepped:
engines_used.append("grep")
all_results.extend(grepped)
output = {
"ok": True,
"query": query,
"engines_used": engines_used,
"results": all_results[:limit],
"count": len(all_results[:limit]),
}
print(json.dumps(output, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

View file

@ -1,133 +0,0 @@
---
name: logging
description: >
Work and reading log management. Triggered by:
"logging work", "logging read",
"做工作记录", "做阅读记录", "做working-log", "做reading-log",
"写工作日志", "写阅读日志", "记录工作", "记录阅读",
"写日志", "记一下", "总结一下这个会话",
"记录决策", "记一下工作过程", "写工作总结".
source: paperforge
---
# Logging
---
## 1. Bootstrap — 必须先执行
跑这个脚本:
```
python $SKILL_DIR/scripts/pf_bootstrap.py
```
返回 JSON。记住以下变量
| 变量 | 来自 JSON 的 | 用于 |
| ----------- | -------------------- | --------------------------------------------- |
| `$VAULT` | `vault_root` | 所有 `--vault` 参数 |
| `$PYTHON` | `python_candidate` | 所有 cli 调用 |
如果 `ok: false` → 报告 `error` 给用户,**停止**。
---
## 2. Routing — 判断用户要什么
根据用户说的内容确定走哪个分支:
| 用户说 | 走分支 |
| ------------------------------------------- | -------- |
| "记录阅读" "reading log" "做阅读记录" "读完了记一下" "刚读了一段记一下" "有没有什么值得记的" "把这段记下来" | **reading** |
| "工作记录" "working log" "总结会话" "记一下工作过程" "写工作总结" "记录决策" "logging work" | **working** |
| "写日志" "记录一下" "记一下" 不清楚哪个 | **先问用户** |
## 3. reading 分支 — 记录单条阅读笔记
调用条件:用户读完一个段落/章节后要记录。
动作:
1. 确认 `$VAULT``$PYTHON`
2. 确定 zotero_key从上下文或 formal note 中获取)
3. 提取:
- **section**: 文献中的位置 (Discussion P12, Results Fig.3)
- **excerpt**: 逐字引用的原文关键句
- **usage**: 这个信息支持当前写作的哪个论点
- **note**: 交叉验证/矛盾/注意事项 (optional)
4. 给用户展示确认后再执行:
```
$PYTHON -m paperforge --vault $VAULT reading-log --write KEY \
--section "..." --excerpt "..." --usage "..." --note "..."
```
5. 确认写入成功
### Reading Log Format (MANDATORY)
When writing reading-log.md, use EXACTLY this format. Field labels (`**Info:**`, `**Use:**`, `**Note:**`) must be in English, but the content (title, section name, excerpt, usage, note) should be in the same language as the user's conversation.
```
## ABCDEFGH — Author Last Name et al. Year
**Title:** Full Paper Title
### Section Name — Page NN
**Info:** "verbatim excerpt from paper"
**Use:** how this supports current writing
**Note:** optional cross-reference (optional field)
```
Rules:
- Paper key: 8 uppercase letters/digits after ## (must match ^[A-Z0-9]{8})
- **Title:** line required after every ## header
- **Info:** and **Use:** required after every ### section header
- **Note:** optional
- Do NOT deviate from this format — parsing is strict
After writing the log file, suggest user run:
```
paperforge reading-log --validate path/to/file.md
```
## 4. working 分支 — 会话总结写入 working-log
调用条件:会话结束前/用户要求记录工作过程。
动作:
1. 回顾本次会话中所有关键节点:
- 用户纠正了什么
- 方案怎么变的
- 有什么弯路和教训
- 可复用的方法论
2. 按以下格式生成 markdown给用户确认
```
## YYYY-MM-DD — 小节名
### 核心决策
- 做了什么、为什么
### 弯路与修正
- 错误方向 → 用户纠正 → 最终方案
### 可复用方法论
- 本段的 pattern
### 待办
- [ ] ...
```
3. 用户确认后,询问目标 project 目录路径
4. 追加到 `Project/<project>/working-log.md`(文件不存在则新建)
5. 确认写入成功
---
## 5. Export — 导出 reading-log
用户说 "导出阅读日志"
```bash
$PYTHON -m paperforge --vault $VAULT reading-log --output <path> [--since DATE]
```
导出为 markdown 文件。如果用户没指定路径,询问。

View file

@ -1,164 +0,0 @@
"""PaperForge bootstrap — single entry point for agent to discover vault state.
No dependencies. Runs on ANY Python. Just reads paperforge.json + filesystem.
Usage:
python pf_bootstrap.py # auto-discover vault from CWD
python pf_bootstrap.py --vault <path>
Output (JSON to stdout):
{
"ok": true,
"vault_root": "D:\\...",
"paths": {
"literature_dir": "D:\\...\\Resources\\Literature",
"index_path": "D:\\...\\System\\PaperForge\\indexes\\formal-library.json",
"ocr_dir": "D:\\...\\System\\PaperForge\\ocr",
"exports_dir": "D:\\...\\System\\PaperForge\\exports"
},
"domains": ["domain1", "domain2"],
"index_summary": {"domain1": 120, "domain2": 80},
"python_candidate": "D:\\...\\python.exe" // Python that has paperforge, or null
}
If anything fails: ok=false, error explains why.
"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
def _find_paperforge_json(start: Path) -> Path | None:
current = start.resolve()
for _ in range(10):
candidate = current / "paperforge.json"
if candidate.exists():
return candidate
parent = current.parent
if parent == current:
break
current = parent
return None
def _read_pf_config(pf_json: Path) -> dict:
with open(pf_json, encoding="utf-8") as f:
return json.load(f)
def _find_python_with_paperforge(vault: Path, pf_cfg: dict) -> str | None:
"""Find a Python executable that has paperforge installed."""
candidates = []
# 1. Explicit python_path in config
if pf_cfg.get("python_path"):
candidates.append(Path(pf_cfg["python_path"]))
# 2. Common venv locations inside vault
venv_names = [".venv", ".paperforge-test-venv", "venv"]
exe_paths = ["Scripts/python.exe", "bin/python3"]
for vn in venv_names:
for ep in exe_paths:
p = vault / vn / ep
if p.exists():
candidates.append(p)
for candidate in candidates:
try:
result = subprocess.run(
[str(candidate), "-m", "paperforge", "--version"],
capture_output=True, text=True, timeout=10,
encoding="utf-8", errors="replace",
)
if result.returncode == 0 and "paperforge" in result.stdout.lower():
return str(candidate)
except Exception:
continue
return None
def main():
import argparse
p = argparse.ArgumentParser(description="PaperForge bootstrap")
p.add_argument("--vault", default=None, help="Vault root path (auto-detect if omitted)")
args = p.parse_args()
result: dict = {"ok": False}
# --- 1. Find vault ---
if args.vault:
vault = Path(args.vault).resolve()
pf_json = vault / "paperforge.json"
if not pf_json.exists():
result["error"] = f"paperforge.json not found at {vault}"
json.dump(result, sys.stdout, ensure_ascii=False)
sys.exit(0)
else:
pf_json = _find_paperforge_json(Path.cwd())
if pf_json is None:
result["error"] = "paperforge.json not found from CWD upward. Set --vault."
json.dump(result, sys.stdout, ensure_ascii=False)
sys.exit(0)
vault = pf_json.parent
result["vault_root"] = str(vault)
# --- 2. Read config ---
try:
cfg = _read_pf_config(pf_json)
except Exception as e:
result["error"] = f"Cannot read paperforge.json: {e}"
json.dump(result, sys.stdout, ensure_ascii=False)
sys.exit(0)
system_dir = cfg.get("system_dir", "System")
resources_dir = cfg.get("resources_dir", "Resources")
literature_dir = cfg.get("literature_dir", "Literature")
# --- 3. Build paths from config ---
pf_root = vault / system_dir / "PaperForge"
paths = {
"literature_dir": str(vault / resources_dir / literature_dir),
"index_path": str(pf_root / "indexes" / "formal-library.json"),
"ocr_dir": str(pf_root / "ocr"),
"exports_dir": str(pf_root / "exports"),
}
result["paths"] = paths
# --- 4. List domains ---
lit_dir = Path(paths["literature_dir"])
domains = sorted(
[d.name for d in lit_dir.iterdir() if d.is_dir()]
) if lit_dir.exists() else []
result["domains"] = domains
# --- 5. Index summary ---
index_path = Path(paths["index_path"])
index_summary: dict[str, int] = {}
if index_path.exists():
try:
data = json.loads(index_path.read_text(encoding="utf-8"))
items = data.get("items", [])
if isinstance(items, dict):
items = items.values()
for item in items:
d = item.get("domain", "unknown")
index_summary[d] = index_summary.get(d, 0) + 1
except Exception:
pass
result["index_summary"] = index_summary
# --- 6. Find Python that has paperforge (best effort) ---
result["python_candidate"] = _find_python_with_paperforge(vault, cfg)
result["ok"] = True
json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
if __name__ == "__main__":
main()

View file

@ -1,86 +0,0 @@
---
name: methodology
description: >
Extract reusable methodology from project work logs. Triggered by:
methodology, /methodology, 提取方法论, 存档写作规律,
总结本项目方法, 提取可复用规则, 提取写作规律.
source: paperforge
---
# Methodology Extract
---
## 1. Bootstrap
```python $SKILL_DIR/scripts/pf_bootstrap.py```
Remember: `$VAULT`, `$PYTHON`.
---
## 2. Determine Project
Ask user: which project to extract methodology from?
If user doesn't specify, scan `Project/` directory for complete working-log.md files and list them.
---
## 3. Read working-log
Read `<vault>/Project/<project>/working-log.md`.
---
## 4. Identify Extractable Patterns
Scan the working-log for these signals:
| Signal in working-log | Extract to |
|----------------------|-------------|
| "弯路" + "修正" or "教训" sections | Pattern rules |
| "最终逻辑:" or "最终结构:" | Section templates |
| "复用" keyword + methodology block | Reusable practices |
| Cross-study audit sections (跨研究可比性) | Analysis methodology |
| "methodology" header sections | Full methodology block |
| Review feedback patterns (审阅/修正) | Writing checklists |
For each found pattern, classify into one of:
- `review-writing` — 综述写作 framework design, gap analysis, cross-study audit
- `argument-writing` — 段落写作, 参数框架, 论证结构
- `analysis-methods` — 文献审计, 跨研究比较, 参数提取
- `general` — fallback
---
## 5. Present and Confirm
For each extracted pattern, show:
- Category
- Source (working-log section number)
- Brief summary (1-2 sentences)
Ask user to confirm/edit before writing.
---
## 6. Write Methodology Files
Write confirmed patterns to `<system_dir>/PaperForge/methodologies/<category>.md`.
If file exists, APPEND (do not overwrite).
Format per method:
```
## <Method Name>
**Category:** <category>
**Source:** Project/<project>/working-log.md Section X.Y
**Extracted:** YYYY-MM-DD
### Pattern
<reusable methodology>
### Example
<concrete example from project>
```

View file

@ -0,0 +1,113 @@
---
name: paperforge
description: >
Research Memory Runtime — 文献搜索、精读、问答、阅读笔记、
工作记录、方法论提取。Triggered by:
pf-deep pf-paper pf-sync pf-ocr pf-status,
"精读" "找文献" "搜文献" "文献问答" "读一下" "看看这篇"
"讨论" "记录阅读" "记录工作" "总结会话" "提取方法论".
source: paperforge
---
# PaperForge — Research Memory Runtime
PaperForge 将文献、阅读痕迹、工作过程、方法论和产物
组织成可检索、可复核、可由 agent 调用的研究记忆。
---
## 1. Bootstrap — 必须先执行
```bash
python $SKILL_DIR/scripts/pf_bootstrap.py --vault "$VAULT"
```
返回 JSON。记录以下变量所有 workflow 文件继承,不再重复声明):
| 变量 | JSON 字段 | 用途 |
| ------------- | ----------------------- | ------------------------------ |
| `$VAULT` | `vault_root` | 所有 `--vault` 参数 |
| `$PYTHON` | `python_candidate` | 所有 `python -m paperforge` 调用 |
| `$LIT_DIR` | `paths.literature_dir` | 文献笔记根目录 |
| `$SKILL_DIR` | 平台注入 | 脚本路径 |
| `$METHODS` | `methodology_index` | 可用方法论索引 |
如果 `ok: false`,报告 `error` 给用户,**停止。禁止自己拼路径。**
---
## 2. Agent Context — bootstrap 成功后执行
```bash
$PYTHON -m paperforge agent-context --json --vault "$VAULT"
```
返回 library overview、collection tree、可用命令和规则。Agent 注入为会话上下文。
---
## 3. Methodology Index — bootstrap 自动提供
bootstrap 已返回 `methodology_index`(从 `System/PaperForge/methodology/archive/` 扫描)。
Agent 在需要时自行读取对应卡片(`read System/PaperForge/methodology/archive/<id>.md`)。
---
## 4. Reading-Log Safety Rule — 全局规则,所有 workflow 必须遵守
Reading-log 不是事实源。它记录的是**之前的关注点、解读和预期用途**。
当存在 prior reading-log 时:
1. 用它决定**优先复查什么**,不是用它回答用户问题
2. 重新打开**原文/图表/表格**,核实之前的解读
3. 确认的,说明"已回原文复核"
4. 被推翻的,创建 correction note
5. **绝对禁止**仅根据 reading-log 内容回答事实性问题
---
## 5. 意图路由
用户输入对应唯一一个 workflow 文件(打开并执行其完整流程):
| 用户说 | 打开 |
| -------------------------------------------------------- | -------------------------------- |
| "找文献" "搜" "库里有没有XX" "collection 里关于YY" | `workflows/paper-search.md` |
| "精读 <key>" "/pf-deep" "三阶段阅读" | `workflows/deep-reading.md` |
| "读一下" "看看" "讨论" "/pf-paper" "<key> 这篇讲了什么" | `workflows/paper-qa.md` |
| "记一下" "记录阅读" "reading log" "读完这段记一下" | `workflows/reading-log.md` |
| "总结会话" "工作记录" "项目记录" "project log" "记决策" | `workflows/project-log.md` |
| "提取方法论" "总结规律" "存档写作规律" | `workflows/methodology.md` |
| 不确定 / 空输入 | 问用户:搜文献、精读、问答、记笔记、记工作、提方法论? |
路由后如用户切换意图,重新判断并打开对应 workflow。
---
## 6. 全局禁止规则
- **禁止自行拼接文件路径**。所有路径从 bootstrap 或 paper-context 获取。
- **禁止绕过 CLI 直接操作文件**。搜索用 `$PYTHON -m paperforge search`,不用 glob/grep 扫库。
- **禁止在未完成 paper-context 检查前读取原文**(适用于 deep-reading、paper-qa
---
## 文件结构
```
paperforge/
├── SKILL.md ← 本文件compound启动注入 + 路由 + 全局规则)
├── workflows/ ← molecules原子序列 + 分支条件
│ ├── paper-search.md
│ ├── deep-reading.md
│ ├── paper-qa.md
│ ├── reading-log.md
│ ├── project-log.md
│ └── methodology.md
├── references/ ← 共享参考
│ ├── chart-reading/ ← 19 种图表阅读指南
│ └── method-card-template.md
└── scripts/ ← 脚本 atoms
├── pf_bootstrap.py
└── pf_deep.py
```

View file

@ -0,0 +1,39 @@
# Method Card Template
复制此模板创建新的方法论卡片。
---
<!-- 卡片 frontmatterYAMLAgent 写入时保留) -->
<!--
---
id: <kebab-case-id>
tags: [<tag1>, <tag2>]
source_project: <project-name>
status: active
---
-->
# <标题简短可搜索>
## Use when
<!-- 什么时候应该用这个方法1-2 句话 -->
## Procedure
<!-- 具体步骤,每步一个编号 -->
1. <步骤 1>
2. <步骤 2>
3. <步骤 3>
## Watch-outs
<!-- 常见陷阱、误用场景 -->
- <注意 1>
- <注意 2>
## Example
<!-- 来自项目的具体例子,最好附上 project-log 来源 -->
---
<!-- 每张卡片末尾保留分隔线 -->

View file

@ -18,7 +18,11 @@ Output (JSON to stdout):
},
"domains": ["domain1", "domain2"],
"index_summary": {"domain1": 120, "domain2": 80},
"python_candidate": "D:\\...\\python.exe" // Python that has paperforge, or null
"python_candidate": "D:\\...\\python.exe",
"methodology_index": [
{"id": "parameter-window-audit", "description": "比较多个研究的参数和剂量反应"},
...
]
}
If anything fails: ok=false, error explains why.
@ -81,6 +85,39 @@ def _find_python_with_paperforge(vault: Path, pf_cfg: dict) -> str | None:
return None
def _scan_methodology_archive(pf_root: Path) -> list[dict]:
"""Scan methodology archive directory for available method cards."""
archive_dir = pf_root / "methodology" / "archive"
if not archive_dir.exists():
return []
methods = []
for f in sorted(archive_dir.glob("*.md")):
try:
text = f.read_text(encoding="utf-8")
# Extract first heading as title, first paragraph after "Use when" as description
title = ""
description = ""
in_use_when = False
for line in text.split("\n"):
stripped = line.strip()
if stripped.startswith("# ") and not title:
title = stripped.lstrip("# ").strip()
elif stripped.startswith("## Use when"):
in_use_when = True
elif in_use_when and stripped and not stripped.startswith("#"):
description = stripped
in_use_when = False
methods.append({
"id": f.stem,
"title": title or f.stem,
"description": description or "(no description)",
})
except Exception:
continue
return methods
def main():
import argparse
p = argparse.ArgumentParser(description="PaperForge bootstrap")
@ -180,6 +217,9 @@ def main():
pass
result["memory_layer"] = memory_layer
# --- 8. Scan methodology archive ---
result["methodology_index"] = _scan_methodology_archive(pf_root)
result["ok"] = True
json.dump(result, sys.stdout, ensure_ascii=False, indent=2)

View file

@ -0,0 +1,169 @@
# deep-reading
Keshav 三阶段精读。在 formal note 中写入结构化的 `## 精读` 区域。
---
## 前置检查
### Step 0: paper-context必须
```bash
$PYTHON -m paperforge paper-context <zotero_key> --json --vault "$VAULT"
```
检查返回 JSON
- `ok: false` → 报告 `error.message`,停止
- `data.paper.ocr_status != "done"` → "OCR 未完成,请先运行 paperforge ocr",停止
- `data.paper.analyze != true` → "analyze 未开启,请在 formal note frontmatter 中设为 true",停止
**检查 prior_notes**
- 如果存在 `data.prior_notes`,逐条看 `verified` 字段
- `verified: false` 的条目记入 recheck_targets精读时必须回原文复核这些位置
- `verified: true` 的条目可以信任,但标注"之前已验证"
**记录关键路径:**
- `data.paper.note_path`formal note 路径)
- `data.paper.fulltext_path`fulltext 路径)
- 记下 `recheck_targets` 列表
---
## 执行流程
### Step 1: Prepare跑脚本
```bash
$PYTHON "$SKILL_DIR/scripts/pf_deep.py" prepare --key <zotero_key> --vault "$VAULT"
```
解析返回 JSON
- `status: "ok"` → 记下 `figure_map`、`chart_type_map`、`formal_note`、`fulltext_md`、`figures`、`tables` 的路径和数量
- `status: "warn"` + `deep_reading_status: done` → 告知用户"该文献已精读过",确认是否重读
- `status: "error"` → 报告 `message`,停止
读 formal note确认 `## 精读` 骨架已插入。
---
### Step 2: Pass 1 — 概览
只填 `### Pass 1: 概览`。不碰 Pass 2/3。
填写内容必须来自原文,不可推断:
- **一句话总览**:论文类型 + 核心发现,一句话
- **5 Cs 快速评估**
- CategoryRCT / 队列 / 综述 / 基础研究等)
- Context领域共识本文要解决什么
- Correctness初步直觉逻辑有否明显漏洞
- Contributions1-3 条)
- Clarity写作质量图表可读性
- **Figure 导读**(基于 fulltext 浏览各图 caption
- 关键主图:列出,一句话概括要证明什么
- 证据转折点:哪个 figure 是叙事关键转折
- 需要重点展开的 supplementary
- 关键表格
填完立即保存。
---
### Step 3: Pass 2 — 精读还原
`### Pass 2: 精读还原`。**按 figure 顺序逐个处理**。
每处理完一个 figure 立即保存。
#### 图表类型定位(两步)
**A: 读 chart-type-map**prepare 输出中包含该路径)。这是关键词命中建议。
**B: Agent 读 caption 做最终判断**
1. 读该 figure 的 caption来自 fulltext
2. 打开 `references/chart-reading/INDEX.md`,对照 caption 内容判断图表类型
3. chart-type-map 建议和 Agent 判断不一致时 → 以 Agent 判断为准
4. 无法确定类型 → 跳过 chart guide按通用结构分析
5. 确定类型 → 读对应 chart-reading 指南,按指南中的检查清单分析
#### 每张 Figure 的子标题(固定,不可跳过)
```
**图像定位与核心问题**:页码 + 要回答什么问题
**方法与结果**:实验设计 / 数据来源 / 技术手段;核心数据、趋势、对比
**图表质量审查**:按 chart-reading 指南检查坐标轴、单位、误差棒、统计标注
**作者解释**:作者在正文中对该图的解读
**我的理解**:自己的理解(必须与作者解释做明显区分)
**疑点/局限**:用 `> [!warning]` 突出
```
#### 每张 Table 的子标题(简化版)
```
回答什么问题、关键字段/分组、主要结果、我的理解、疑点/局限
```
#### 所有 figure/table 处理完后
**关键方法补课**简要解释不熟悉的实验技术1-2 项)
**主要发现与新意**
- 发现 1...来源Figure X
- 发现 2...来源Table Y
- 每条发现必须标注来源Figure 编号或正文段落)
---
### Step 4: Postprocess跑校验修正问题
```bash
$PYTHON "$SKILL_DIR/scripts/pf_deep.py" postprocess-pass2 "<formal_note_path>" --figures <N> --vault "$VAULT"
```
- 输出 `OK` → 继续 Step 5
- 输出错误列表(含行号)→ 按提示修正,修正后重新跑
- 最多 3 轮修正。3 轮后仍失败 → 报告剩余错误给用户
---
### Step 5: Pass 3 — 深度理解
`### Pass 3: 深度理解`。基于 Pass 1/2 已写内容。
- **假设挑战与隐藏缺陷**:隐含假设;放宽假设后结论还成立吗;缺少的关键引用;实验/分析技术潜在问题
- **哪些结论扎实,哪些仍存疑**
- 较扎实:...
- 仍存疑:...(用 `> [!warning]`
- **Discussion 与 Conclusion 怎么读**:作者实际完成了什么;哪些有拔高;哪些是推测
- **对我的启发**研究设计、figure 组织、方法组合、未来工作
- **遗留问题**...(用 `> [!question]`
---
### Step 6: Final Validation
```bash
$PYTHON "$SKILL_DIR/scripts/pf_deep.py" validate-note "<formal_note_path>" --fulltext "<fulltext_path>"
```
- 输出 `OK` → 告知用户精读完成
- 输出错误 → 修正缺失项,直到通过
---
## Callout 格式规则
- `> [!important]` — 每个 main finding
- `> [!warning]` — 疑问、局限、证据边界、仍存疑条目
- `> [!question]` — 遗留问题
- **相邻 callout 之间必须有空行**(否则 Obsidian 合并):
- 正确:`> [!important] A\n\n> [!important] B`
- 错误:`> [!important] A\n> [!important] B`
---
## 禁止
- 不要在 Pass 1 完成前碰 Pass 2/3
- 不要把推断写成文献事实——区分"作者说了 X"和"我推断 Y"
- 不要跨 figure 写综合判断Pass 2 逐图Pass 3 才做综合)

View file

@ -0,0 +1,94 @@
# methodology
从 project-log 中提取可复用方法论,按 method-card 模板写入 methodology archive。
不 append 到大文件,每张卡片独立保存。
---
## 前置条件
- bootstrap 已完成
- 有 project-log 记录可读取
---
## 步骤
### Step 1: 确定项目和来源
询问用户从哪个项目提取。如用户未指定,列出有 project-log 的项目。
### Step 2: 读取 project-log
```bash
$PYTHON -m paperforge project-log --list "<project>" --json --vault "$VAULT"
```
扫描其中以下信号:
| log 中的信号 | 可提取为 |
| ------------------------- | --------------------------- |
| `detours` 中的教训 | 方法论规则 |
| `reusable` 字段里的内容 | 直接采用 |
| `decisions` 中的重要选择 | 决策原则 |
| 跨文献审计/比较分析 | 审计方法论 |
| 写作修正/审阅反馈 | 写作检查清单 |
### Step 3: 识别可提取 pattern
对每个 pattern 分类:
- `review-writing` — 综述框架设计、gap 分析、跨研究审计
- `argument-writing` — 段落写作、论证结构
- `analysis-methods` — 文献审计、跨研究比较、参数提取
- `general` — fallback
### Step 4: 按 method-card 模板生成卡片
打开 `references/method-card-template.md` 确认模板格式。
对每个 pattern 生成一张卡片,展示给用户确认。格式:
```markdown
---
id: <kebab-case-id>
tags: [<tag1>, <tag2>]
source_project: <project-name>
status: active
---
# <标题>
## Use when
<什么时候应该用这个方法>
## Procedure
1. <步骤 1>
2. <步骤 2>
...
## Watch-outs
- <注意事项 1>
- <注意事项 2>
## Example
<来自项目的具体例子>
```
### Step 5: 用户确认后写入
将每张卡片写入:
```
System/PaperForge/methodology/archive/<id>.md
```
`write` 工具创建文件。如已存在同名文件,追加到末尾(用 `---` 分隔)。
不自动覆盖已有内容。
---
## 禁止
- 不要提取太泛的"教训"(如"多读文献")——必须有具体的 Procedure 步骤
- 不要创建超过 4 张卡片/次——优先最可复用的
- 不要在用户确认前写入

View file

@ -0,0 +1,105 @@
# paper-qa
交互式文献问答。不强制要求 OCR但 OCR 完成后回答更准确。
每次问答记录到 `discussion.json`Dashboard 可见)。
---
## 前置条件
- bootstrap 已完成(有 `$VAULT`、`$PYTHON`
---
## 步骤
### Step 1: 定位论文
用户可能给 zotero_key、DOI、标题片段、作者+年份。按以下方式查找:
**优先用 paper-context一次拿到全部信息**
```bash
$PYTHON -m paperforge paper-context <query> --json --vault "$VAULT"
```
返回 JSON 包含 paper 元数据、OCR 状态、prior_notes 等。
**paper-context 无结果时的备选:**
```bash
$PYTHON -m paperforge search "<query>" --json --vault "$VAULT" --limit 5
```
如果多候选,列出让用户选(同 paper-search 的 Step 4-5 格式)。
如果无结果,告知用户并停止。
### Step 2: 加载文献内容
1. 从 paper-context 或 formal note frontmatter 获取标题、作者、期刊、年份、domain
2. 读 `fulltext.md`(如果 OCR done作为主要回答依据
3. 如果 fulltext 不存在:"OCR 文本不可用,回答将基于元数据和公开信息"
### Step 3: 展示论文信息 + 进入 Q&A
```
已加载: <title> (<year>, <journal>)
作者: <authors> | Key: <zotero_key> | 领域: <domain>
OCR: done / 不可用
结束对话时说"保存"即可保存讨论。
请问有什么问题?
```
### Step 4: Q&A 循环
- 等待用户提问
- 每次回答后等待下一个问题
- 持续到用户说"保存"、"结束"、"完成"
**回答原则:**
- 严格基于 fulltext.md 中的文本内容
- 引用原文时标注来源页码/章节
- 论文未提及的内容明确说明"论文中未提及"
- 区分"文献说了什么"和"我推断什么"
### Step 5: 保存讨论
用户说"保存"、"结束"、"完成"时执行。
**收集 Q&A 对**,序列化为 JSON 数组:
```json
[
{
"question": "用户的问题",
"answer": "Agent 的回答",
"source": "user_question",
"timestamp": "2026-05-14T12:00:00+08:00"
}
]
```
`source`: `"user_question"`(用户提问)或 `"agent_analysis"`Agent 主动分析)。
**调 discussion 模块:**
```bash
$PYTHON -m paperforge.worker.discussion record <zotero_key> \
--vault "$VAULT" \
--agent pf-paper \
--model "<current_model>" \
--qa-pairs '<JSON_ARRAY>'
```
- 返回 `ok` → 告知用户已保存
- 返回 `error` → 重试一次,仍失败则告知用户
**不要自动保存。** 仅用户明确要求时执行。
---
## 禁止
- 不要捏造论文未提及的内容
- 不要把推断写成论文事实

View file

@ -0,0 +1,101 @@
# paper-search
从文献库中按条件检索文献,返回候选清单及每篇的可用状态。
---
## 前置条件
- bootstrap 已完成(有 `$VAULT`、`$PYTHON`、`$LIT_DIR`
---
## 步骤
### Step 1: 解析用户搜索意图
提取以下信息(缺什么就问用户):
- **搜索词**:关键词、作者名、年份
- **范围**domain如"骨科"、collection如"DC")、不指定=全库
- **过滤条件**OCR 状态done/pending、年份范围--year-from/--year-to、lifecycle
### Step 2: 执行搜索
```bash
$PYTHON -m paperforge search <query> --json --vault "$VAULT" --limit 15 \
[--domain "<domain>"] \
[--year-from <N>] [--year-to <N>] \
[--ocr <done|pending>] \
[--lifecycle <active|archived>]
```
返回 JSON 结构:
```json
{
"ok": true,
"data": {
"query": "<query>",
"matches": [
{
"zotero_key": "ABC12345",
"citation_key": "...",
"title": "...",
"year": "2024",
"first_author": "Smith",
"domain": "...",
"collection_path": "...",
"ocr_status": "done",
"deep_reading_status": "pending",
"lifecycle": "active",
"has_pdf": true,
"rank": "..."
}
],
"count": 5
}
}
```
- 如果 `ok: false` → 报告 `error.message`,问用户是否换搜索词
- 如果 `data.count == 0` → 告知用户无结果,建议换词或扩大范围
- 如果 `data.count > 0` → 进入 Step 3
### Step 3: 逐个确认状态paper-context 原子)
对每个 match`paper-context` 获取更详细的可读状态:
```bash
$PYTHON -m paperforge paper-context <zotero_key> --json --vault "$VAULT"
```
目的:拿到 `ocr_status`、`prior_notes` 数量、`analyze` 状态,帮助用户判断哪些可以直接读。
### Step 4: 展示候选清单
格式(每条一行):
```
找到 N 篇匹配 "<query>"
[1] ABC12345 | Smith 2024 | Title Here | 骨科 | OCR: done | 精读: pending | 阅读笔记: 3
[2] DEF67890 | Jones 2023 | Title Here | 骨科 | OCR: done | 精读: done | 阅读笔记: 0
[3] GHI11111 | Wang 2022 | Title Here | 骨科 | OCR: pending | | 阅读笔记: 0
```
关键字段zotero_key, first_author, year, title, ocr_status, deep_reading_status, prior_notes 数量
### Step 5: 等用户选择后续操作
展示候选后不要自己决定下一步。等用户说:
- "读一下 [1]" → 路由到 paper-qa.md
- "精读 [2]" → 路由到 deep-reading.md
- "记一下 [1]" → 路由到 reading-log.md
- "缩小范围"/"refine" → 回到 Step 1加更多过滤条件
---
## 禁止
- 不要在搜索结果中替用户决定读哪篇
- 不要在搜索阶段读全文
- 不要对 0 结果硬猜路径

View file

@ -0,0 +1,131 @@
# project-log
记录研究项目的会话总结、决策、弯路修正和方法论提取。
Agent 按 JSON schema 写入 project-log.jsonl。
系统自动渲染对应项目的 project-log.md。
---
## 前置条件
- bootstrap 已完成(有 `$VAULT`、`$PYTHON`
- 已知 project 名称
---
## 项目日志 JSON Schema
```json
{
"id": "plog_<YYYYMMDD>_<序号>",
"project": "综述写作",
"date": "2026-05-14",
"type": "session_summary",
"title": "DC 段参数窗审计",
"decisions": ["做了 X因为 Y"],
"detours": [
{
"wrong": "错误方向",
"correction": "用户如何纠正",
"resolution": "最终方案"
}
],
"reusable": ["可复用的方法论或教训"],
"todos": [
{"content": "待办事项", "done": false}
],
"related_papers": ["ABC12345"],
"tags": ["DC", "参数窗", "审计"],
"agent": "opencode"
}
```
| 字段 | 必填 | 说明 |
| ---------------- | ---- | ------------------------------------------- |
| `id` | 是 | 自动生成 `plog_YYYYMMDD_NNN` |
| `project` | 是 | 项目名 |
| `date` | 是 | YYYY-MM-DD |
| `type` | 是 | `session_summary` / `decision` / `correction` / `milestone` / `note` |
| `title` | 是 | 本条目的简短标题 |
| `decisions` | 否 | 核心决策列表 |
| `detours` | 否 | 弯路与修正记录 |
| `reusable` | 否 | 可复用的方法论或教训 |
| `todos` | 否 | 待办事项 |
| `related_papers` | 否 | 相关 Zotero keys |
| `tags` | 否 | 分类标签 |
| `agent` | 否 | 记录者 |
---
## 步骤
### Step 1: 确定 project
从上下文获取。如果用户未指定,询问。
### Step 2: 回顾本次会话
回顾以下内容:
- 做了什么(核心决策)
- 用户纠正了什么(弯路与修正)
- 有什么可复用的方法论或教训
- 待办事项
### Step 3: 按 Schema 组织内容,展示确认
展示给用户确认后再写入:
```
即将记录到 Project/综述写作/project-log.md:
日期: 2026-05-14
类型: session_summary
标题: DC 段参数窗审计完成
决策:
- 限定参数窗为 100Hz-1kHz
- 移除 AC vs DC 对比段落
弯路:
- 把推断当文献事实 → 用户要求逐句审计 → 5 处修正
可复用:
- 写完必须逐句过 source区分"文献说了什么"和"我推断什么"
确认写入?(y/n)
```
### Step 4: 写入Atom
```bash
$PYTHON -m paperforge project-log --write \
--vault "$VAULT" \
--project "<project>" \
--json '<payload>'
```
- 返回 `ok: true` → 确认写入成功。**自动渲染对应项目 markdown。**
- 返回 `ok: false` → 报告错误,重试一次
### Step 5: 确认渲染
```bash
$PYTHON -m paperforge project-log --render --project "<project>" --vault "$VAULT"
```
输出到 `Project/<project>/project-log.md`
---
## type 参考
| type | 使用场景 |
| ----------------- | ------------------------------ |
| `session_summary` | 会话结束时的总结 |
| `decision` | 单独记录一个重要决策 |
| `correction` | 用户纠正了某个方向 |
| `milestone` | 项目里程碑 |
| `note` | 一般研究笔记 |
---
## 禁止
- 不要在用户确认前写入
- 不要只写"做了什么"而没有"弯路与修正"和"可复用方法论"

View file

@ -0,0 +1,112 @@
# reading-log
记录单条阅读笔记。Agent 将用户确认的信息按 JSON schema 写入 reading-log.jsonl。
系统自动渲染对应项目的 reading-log.md给人看并导入 paperforge.db可搜索
---
## 前置条件
- bootstrap 已完成(有 `$VAULT`、`$PYTHON`
- 已知 paper_idzotero_key
---
## 阅读笔记 JSON Schema
每条阅读笔记必须包含以下字段:
```json
{
"id": "rln_<YYYYMMDD>_<序号>",
"paper_id": "ABC12345",
"project": "综述写作",
"section": "Results Fig.3",
"excerpt": "原文关键句(逐字引用)",
"context": "包含 excerpt 的完整段落(供后续回原文复核时定位)",
"usage": "这个信息在当前写作中的用途",
"note": "注意事项 / 待核查 / 可能矛盾",
"tags": ["PEMF", "dose-response"],
"verified": false
}
```
| 字段 | 必填 | 说明 |
| --------- | ---- | -------------------------------------------------------- |
| `id` | 是 | 自动生成,格式 `rln_YYYYMMDD_NNN` |
| `paper_id` | 是 | Zotero key8位大写字母数字 |
| `project` | 否 | 关联的研究项目 |
| `section` | 是 | 文献中的位置(如 "Results Fig.3"、"Discussion P12" |
| `excerpt` | 是 | 逐字引用的原文关键句 |
| `context` | 是 | 包含 excerpt 的完整段落,供复核定位 |
| `usage` | 是 | 这个信息在当前工作(写作/研究)中的用途 |
| `note` | 否 | 交叉验证、矛盾、待核查事项 |
| `tags` | 否 | 分类标签,供横切检索 |
| `verified` | 否 | 默认 false。Agent 回原文复核后应更新为 true |
---
## 步骤
### Step 1: 确认 paper_id 和 project
从上下文获取 zotero_key。如果用户未指定 project询问或留空。
### Step 2: Agent 按 Schema 提取内容
从对话上下文中提取 `section`、`excerpt`、`context`、`usage`、`note`、`tags`。
**excerpt vs context 的区别:**
- `excerpt`:你关注的那一句(逐字引用)
- `context`包含这句的完整段落3-5 句),让以后的人不翻原文也能理解语境
### Step 3: 展示确认
先展示给用户确认,不要直接写入:
```
即将记录:
文献: ABC12345 | Smith 2024
位置: Results Fig.3
原文: "..."
用途: 支撑 PEMF 基质合成的论证
备注: 需核查是否对 DNA 归一化了
项目: 综述写作
标签: PEMF, GAG
段落语境: "..."
确认写入?(y/n)
```
### Step 4: 写入Atom
```bash
$PYTHON -m paperforge reading-log --write <paper_id> \
--vault "$VAULT" \
--section "<section>" \
--excerpt "<excerpt>" \
--context "<context>" \
--usage "<usage>" \
--note "<note>" \
--project "<project>" \
--tags "<tag1>,<tag2>"
```
- 返回 `ok: true` → 确认写入成功。**写入后自动渲染对应项目的 markdown。**
- 返回 `ok: false` → 报告错误,重试一次
### Step 5: 确认渲染
```bash
$PYTHON -m paperforge reading-log --render --project "<project>" --vault "$VAULT"
```
输出到 `Project/<project>/reading-log.md`
---
## 禁止
- 不要在用户确认前写入
- 不要把推断当作 `excerpt`(必须是原文逐字引用)
- 不要让 `context` 为空(必须是完整段落)

View file

@ -639,36 +639,36 @@ def run_doctor(vault: Path, verbose: bool = False, json_output: bool = False) ->
if total_issues == 0:
add_check("字段注册表", "pass", "所有 formal note frontmatter 与字段注册表一致")
ld_deep_script = paths.get("ld_deep_script")
pf_deep_script = paths.get("pf_deep_script")
skill_dir = None
if ld_deep_script:
skill_dir = ld_deep_script.parent.parent
if pf_deep_script:
skill_dir = pf_deep_script.parent.parent
if skill_dir and skill_dir.exists():
# Try actual importability check
ld_deep_import_ok = False
pf_deep_import_ok = False
import_error = ""
if ld_deep_script and ld_deep_script.exists():
if pf_deep_script and pf_deep_script.exists():
try:
import importlib.util
spec = importlib.util.spec_from_file_location("ld_deep", ld_deep_script)
spec = importlib.util.spec_from_file_location("pf_deep", pf_deep_script)
if spec and spec.loader:
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
ld_deep_import_ok = True
pf_deep_import_ok = True
except Exception as e:
import_error = str(e)
if ld_deep_import_ok:
add_check("Agent 脚本", "pass", "paperforge and ld_deep importable")
if pf_deep_import_ok:
add_check("Agent 脚本", "pass", "paperforge and pf_deep importable")
else:
add_check(
"Agent 脚本",
"warn",
f"literature-qa skill 目录存在但 import 失败: {import_error}",
f"paperforge skill 目录存在但 import 失败: {import_error}",
"确认 agent_config_dir 配置正确并已运行 pip install -e .",
)
else:
add_check("Agent 脚本", "warn", "literature-qa skill 目录未找到", "确认 agent_config_dir 配置正确")
add_check("Agent 脚本", "warn", "paperforge skill 目录未找到", "确认 agent_config_dir 配置正确")
# --- Index Health section (Phase 25: derived from canonical index) ---
try:

View file

@ -252,7 +252,7 @@ def _deploy_all_skills(vault: Path) -> None:
agent_key = config.get("agent_platform") or "opencode"
result = deploy_skills(vault=vault, agent_key=agent_key, overwrite=True)
if result["skill_deployed"]:
logger.info("已部署 literature-qa skill")
logger.info("已部署 paperforge skill")
if result["agents_md"]:
logger.info("已更新 AGENTS.md")
for err in result.get("errors", []):