diff --git a/docs/superpowers/plans/2026-06-01-ocr-redo-single-source.md b/docs/superpowers/plans/2026-06-01-ocr-redo-single-source.md new file mode 100644 index 00000000..be63a369 --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-ocr-redo-single-source.md @@ -0,0 +1,59 @@ +# OCR Redo Single-Source Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Convert OCR redo into an immediate rerun workflow and remove workspace `fulltext.md` as a second truth source. + +**Architecture:** Reuse the existing OCR worker as the execution engine, but add a redo preflight that clears stale outputs, targets selected keys only, and writes `ocr_redo` back to `false` only on success. Route all `fulltext_path` consumers to canonical OCR storage. + +**Tech Stack:** Python workers/CLI, pytest, Obsidian frontmatter paths. + +--- + +### Task 1: Remove Workspace Fulltext As Truth Source + +**Files:** +- Modify: `paperforge/worker/asset_index.py` +- Modify: `paperforge/worker/sync.py` +- Test: `tests/test_sync.py` + +- [ ] Delete stale workspace `fulltext.md` during migration/index refresh. +- [ ] Stop bridging OCR fulltext into workspace. +- [ ] Point `entry["fulltext_path"]` at canonical OCR fulltext. +- [ ] Update sync test to expect deletion instead of refresh. + +### Task 2: Make OCR Redo Closed-Loop + +**Files:** +- Modify: `paperforge/worker/ocr.py` +- Modify: `paperforge/commands/ocr.py` +- Test: `tests/test_ocr.py` + +- [ ] Add redo preflight to clear old OCR output and stale workspace fulltext. +- [ ] Force selected notes to `do_ocr: true`, `ocr_status: pending`, `ocr_redo: true` before rerun. +- [ ] Extend `run_ocr()` to accept selected keys. +- [ ] Call OCR directly from `ocr redo`. +- [ ] After rerun, set `ocr_redo: false` only for successful keys. +- [ ] Leave failed/pending keys at `ocr_redo: true`. + +### Task 3: Verify End-to-End Behavior + +**Files:** +- Test: `tests/test_ocr.py` + +- [ ] Cover dry-run no-op behavior. +- [ ] Cover invalid key rejection. +- [ ] Cover success path: delete workspace fulltext, rerun selected key, write back `ocr_redo: false`. +- [ ] Cover incomplete rerun path: preserve `ocr_redo: true`. + +### Task 4: Run Regression Suite + +**Files:** +- Test: `tests/test_ocr_rendering.py` +- Test: `tests/test_ocr_layout_zones.py` +- Test: `tests/test_ocr_body_spine.py` +- Test: `tests/test_sync.py` +- Test: `tests/test_asset_index.py` + +- [ ] Run targeted OCR redo tests first. +- [ ] Run full regression suite for OCR/sync/asset index. diff --git a/docs/superpowers/plans/2026-06-01-plugin-release-notes-and-manual.md b/docs/superpowers/plans/2026-06-01-plugin-release-notes-and-manual.md new file mode 100644 index 00000000..2caef761 --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-plugin-release-notes-and-manual.md @@ -0,0 +1,420 @@ +# Plugin Release Notes & Manual — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a `更新与手册` settings tab with versioned Chinese release notes and external manual links, plus an auto-popup on version change. + +**Architecture:** Release notes stored as JSON imported by esbuild into settings.ts. New tab renders collapsible version cards. `main.ts` onload compares `manifest.version` against `settings.last_seen_version` and shows a Modal on mismatch. + +**Tech Stack:** TypeScript (Obsidian plugin API), JSON, esbuild bundling + +--- + +### Task 1: Release Notes Data File + +**Files:** +- Create: `paperforge/plugin/src/release-notes.json` +- Modify: `paperforge/plugin/src/constants.ts:70-92` + +- [ ] **Step 1: Create `release-notes.json` with version 1.5.15 entry** + +Write `paperforge/plugin/src/release-notes.json`: + +```json +{ + "versions": [ + { + "version": "1.5.15", + "date": "2026-06-01", + "title": "OCR 重新设计 — 全文单一真相源 + Redo 闭环 + 阅读顺序修复", + "breaking_or_migration": [ + "fulltext 正文现在统一位于 System/PaperForge/ocr//fulltext.md,工作区 Resources/.../fulltext.md 不再保留为正文副本", + "OCR Redo 现在会立即重跑 OCR(闭环执行),不再只是标记状态后需要手动再跑一次" + ], + "new_features": [ + "新增 OCR Redo 闭环执行:勾选重做后自动删除旧产物 → 强制 do_ocr → 立即重跑 OCR → 成功后写回 ocr_redo:false", + "设置页新增「更新与手册」标签页,可随时查看版本更新记录与使用手册链接", + "首次更新时自动弹出更新说明" + ], + "fixes": [ + "修复 OCR 双栏阅读顺序混乱:2.5 / Discussion / Introduction 等章节标题和正文错位", + "修复 Kwon et al. 等叙述句被误判为参考文献,导致正文段落被提前截断并插入 Discussion", + "修复 Fig.3 / Fig.4 等页首图注和图块被重排逻辑整体后移,导致图文断裂", + "修复首页(page 1)Abstract / Introduction 排序错乱,METHODS 行跑到 Introduction 后", + "修复并排 panel(如 Fig.2 的左右两块)未合并为单张 composite figure,被拆成两张独立图片", + "修复 redo OCR 后工作区 fulltext 未被同步覆盖,导致用户看到的仍是旧乱序版本", + "Dashboard 现在可以识别 System/PaperForge/ocr//fulltext.md 路径,打开全文后能切到对应 paper 视图" + ], + "recommended_actions": [ + "如果论文是此版本前完成的 OCR,建议对重要论文执行一次「重做OCR」(在 Base 视图勾选 ocr_redo 后点 ribbon Redo 按钮),以修复阅读顺序问题", + "打开全文请直接使用 Dashboard 的「打开全文」按钮,正文已经统一迁至 System/PaperForge/ocr/ 下" + ] + } + ] +} +``` + +- [ ] **Step 2: Add `last_seen_version` to `PaperForgeSettings` interface** + +In `paperforge/plugin/src/constants.ts`, after line 90: + +```typescript + last_seen_version: string; +``` + +- [ ] **Step 3: Add default value to `DEFAULT_SETTINGS`** + +In `paperforge/plugin/src/constants.ts`, after line 116: + +```typescript + last_seen_version: "", +``` + +- [ ] **Step 4: Commit** + +```bash +git add paperforge/plugin/src/release-notes.json paperforge/plugin/src/constants.ts +git commit -m "feat: add release-notes data and last_seen_version setting field" +``` + +--- + +### Task 2: Settings Tab – 更新与手册 + +**Files:** +- Modify: `paperforge/plugin/src/settings.ts` + +- [ ] **Step 1: Import release notes and add tab to tab bar** + +At top of `settings.ts`, add import: + +```typescript +import releaseNotesData from "./release-notes.json"; +``` + +In `display()` method (around line 92-94), add the third tab: + +```typescript +const tabs = [ + { id: "setup", label: t("tab_setup") || "安装" }, + { id: "features", label: t("tab_features") || "功能" }, + { id: "release-notes", label: "更新与手册" }, +]; +``` + +In the render switch (around line 117-121): + +```typescript +if (this.activeTab === "setup") { + this._renderSetupTab(tabContents.setup); +} else if (this.activeTab === "features") { + this._renderFeaturesTab(tabContents.features); +} else { + this._renderReleaseNotesTab(tabContents["release-notes"]); +} +``` + +- [ ] **Step 2: Implement `_renderReleaseNotesTab()`** + +Add new method after `_renderFeaturesTab()`: + +```typescript +_renderReleaseNotesTab(containerEl: HTMLElement) { + containerEl.createEl("h2", { text: "更新与手册" }); + + // --- Version Log --- + containerEl.createEl("h3", { text: "版本更新记录" }); + + const versions = (releaseNotesData as any).versions || []; + for (const ver of versions) { + const card = containerEl.createEl("div", { cls: "paperforge-release-card" }); + + // Header row + const header = card.createEl("div", { cls: "paperforge-release-header" }); + header.createEl("strong", { text: `v${ver.version} — ${ver.title}` }); + header.createEl("span", { cls: "paperforge-release-date", text: ` (${ver.date})` }); + + // Breaking / Migration + if (ver.breaking_or_migration && ver.breaking_or_migration.length > 0) { + const section = card.createEl("div", { cls: "paperforge-release-section" }); + section.createEl("div", { cls: "paperforge-release-label", text: "行为变更 / 迁移注意" }); + for (const item of ver.breaking_or_migration) { + section.createEl("div", { cls: "paperforge-release-item", text: `• ${item}` }); + } + } + + // New Features + if (ver.new_features && ver.new_features.length > 0) { + const section = card.createEl("div", { cls: "paperforge-release-section" }); + section.createEl("div", { cls: "paperforge-release-label", text: "新功能" }); + for (const item of ver.new_features) { + section.createEl("div", { cls: "paperforge-release-item", text: `• ${item}` }); + } + } + + // Fixes + if (ver.fixes && ver.fixes.length > 0) { + const section = card.createEl("div", { cls: "paperforge-release-section" }); + section.createEl("div", { cls: "paperforge-release-label", text: "修复" }); + for (const item of ver.fixes) { + section.createEl("div", { cls: "paperforge-release-item", text: `• ${item}` }); + } + } + + // Recommended Actions + if (ver.recommended_actions && ver.recommended_actions.length > 0) { + const section = card.createEl("div", { cls: "paperforge-release-section" }); + section.createEl("div", { cls: "paperforge-release-label", text: "建议操作" }); + for (const item of ver.recommended_actions) { + section.createEl("div", { cls: "paperforge-release-item", text: `• ${item}` }); + } + } + } + + // --- Manual Link --- + containerEl.createEl("h3", { text: "使用手册" }); + const manualSection = containerEl.createEl("div", { cls: "paperforge-manual-links" }); + const manualLink = manualSection.createEl("a", { + text: "→ 查看完整使用手册(GitHub)", + href: "https://github.com/LLLin000/PaperForge/blob/main/docs/user-manual.md", + }); + manualLink.setAttr("target", "_blank"); +} +``` + +- [ ] **Step 3: Add inline CSS for release notes** + +In the CSS block in `display()` (around line 73-87), add: + +```css +.paperforge-release-card { border: 1px solid var(--background-modifier-border); border-radius: 6px; padding: 12px; margin-bottom: 12px; } +.paperforge-release-header { margin-bottom: 8px; } +.paperforge-release-date { color: var(--text-muted); font-size: 12px; } +.paperforge-release-section { margin-bottom: 6px; } +.paperforge-release-label { font-weight: 600; color: var(--text-accent); margin-bottom: 2px; font-size: 13px; } +.paperforge-release-item { font-size: 13px; margin-left: 8px; color: var(--text-muted); } +.paperforge-manual-links { margin-top: 8px; } +.paperforge-manual-links a { color: var(--text-accent); } +``` + +- [ ] **Step 4: Build and verify** + +```bash +npm run build +``` + +Check that `main.js` contains the release notes JSON inlined. + +- [ ] **Step 5: Commit** + +```bash +git add paperforge/plugin/src/settings.ts paperforge/plugin/src/release-notes.json +git commit -m "feat: add 更新与手册 settings tab with release notes and manual links" +``` + +--- + +### Task 3: Auto-Popup on Version Change + +**Files:** +- Modify: `paperforge/plugin/src/main.ts` + +- [ ] **Step 1: Add `_checkReleaseNotes()` method in main.ts** + +After `_firstLaunchSnapshotMigration()` method, add: + +```typescript +private _checkReleaseNotes() { + const currentVersion = this.manifest.version; + const seen = this.settings.last_seen_version; + if (seen === currentVersion) return; + + const releaseNotesData = require("./release-notes.json"); + const versions = releaseNotesData.versions || []; + const currentEntry = versions.find((v: any) => v.version === currentVersion); + + const { Modal } = require("obsidian"); + class ReleaseNotesModal extends Modal { + constructor(app: any, entry: any) { + super(app); + this._entry = entry; + } + private _entry: any; + onOpen() { + const { contentEl } = this; + contentEl.createEl("h2", { text: `PaperForge v${currentVersion} 更新说明` }); + if (this._entry) { + contentEl.createEl("p", { text: this._entry.title, cls: "paperforge-modal-subtitle" }); + if (this._entry.breaking_or_migration && this._entry.breaking_or_migration.length > 0) { + contentEl.createEl("h4", { text: "行为变更 / 迁移注意" }); + for (const item of this._entry.breaking_or_migration) { + contentEl.createEl("p", { text: `• ${item}`, cls: "paperforge-modal-item" }); + } + } + if (this._entry.recommended_actions && this._entry.recommended_actions.length > 0) { + contentEl.createEl("h4", { text: "建议操作" }); + for (const item of this._entry.recommended_actions) { + contentEl.createEl("p", { text: `• ${item}`, cls: "paperforge-modal-item" }); + } + } + } else { + contentEl.createEl("p", { text: "请前往设置 → 更新与手册 查看完整更新记录。" }); + } + new (require("obsidian").Setting)(contentEl) + .addButton(btn => btn.setButtonText("知道了").setCta().onClick(() => { + this.close(); + })); + } + onClose() { + const { contentEl } = this; + contentEl.empty(); + } + } + + new ReleaseNotesModal(this.app, currentEntry).open(); + this.settings.last_seen_version = currentVersion; + this.saveSettings(); +} +``` + +- [ ] **Step 2: Call `_checkReleaseNotes()` in `onload()`** + +In `onload()`, after `this._firstLaunchSnapshotMigration()`, add: + +```typescript +this._checkReleaseNotes(); +``` + +- [ ] **Step 3: Build, deploy, and verify** + +```bash +npm run build +Copy-Item main.js manifest.json styles.css -Destination ".obsidian/plugins/paperforge/" -Force +``` + +- [ ] **Step 4: Commit** + +```bash +git add paperforge/plugin/src/main.ts +git commit -m "feat: auto-popup release notes modal on plugin version change" +``` + +--- + +### Task 4: User Manual Stub + +**Files:** +- Create: `docs/user-manual.md` + +- [ ] **Step 1: Write minimal Chinese user manual** + +Create `docs/user-manual.md`: + +```markdown +# PaperForge 使用手册 + +> 版本:1.5.15+ + +## 快速开始 + +1. 安装 PaperForge 插件并完成设置向导 +2. 在 Zotero 中配置 Better BibTeX 自动导出 +3. 打开 PaperForge Dashboard,点击 Sync 同步文献 +4. 对需要全文的论文,确保 do_ocr 为 true,点击 Run OCR +5. OCR 完成后即可在 Dashboard 中打开全文、进行精读 + +## 核心概念 + +### fulltext(全文) + +每篇论文的 OCR 正文统一存放在: +`System/PaperForge/ocr//fulltext.md` + +这是唯一的正文真相源。Dashboard 的「打开全文」按钮会直接定位到此文件。 + +### OCR Redo(重做 OCR) + +当你需要重新提取某篇论文的全文时: + +1. 在领域 Base 的「重做OCR」视图中勾选 `ocr_redo` +2. 点击 ribbon 上的 Redo OCR 按钮 +3. 系统会自动:删除旧产物 → 重新上传 PDF → 等待 OCR → 生成新全文 + +完成后 `ocr_redo` 自动变为 `false`。如果失败,标志会保持 `true`,可以再次重试。 + +### Dashboard 识别 + +Dashboard 会自动识别你当前打开的文件所属的论文。支持识别: +- 工作区笔记(`Resources/Literature// - Title/.md`) +- OCR 全文(`System/PaperForge/ocr//fulltext.md`) +- PDF 文件(通过 wikilink 匹配) + +## 常用工作流 + +### 新增论文 + +1. Zotero 中添加文献 → 自动导出 +2. Dashboard → Sync +3. 在 Base 视图中确认 do_ocr 为 true +4. Dashboard → Run OCR +5. OCR 完成后 Dashboard 自动显示「打开全文」按钮 + +### 精读论文 + +1. Dashboard 中找到目标论文 → 点击打开全文 +2. 在 Agent 中输入 `/pf-deep ` 开始精读 +3. 精读完成后 Dashboard 状态自动更新 + +## 目录结构 + +``` +Vault/ +├── System/PaperForge/ +│ ├── ocr// ← OCR 全文、图片、原始 JSON +│ ├── exports/ ← Zotero 导出 +│ └── indexes/ ← 文献索引 +├── Resources/Literature/ +│ └── / - Title/ ← 论文工作区 +│ ├── .md ← 主笔记 +│ └── ai/ ← AI 产物 +├── Bases/ ← Base 视图 +└── .obsidian/plugins/paperforge/ ← 插件 +``` +``` + +- [ ] **Step 2: Commit** + +```bash +git add docs/user-manual.md +git commit -m "docs: add Chinese user manual stub" +``` + +--- + +### Task 5: Run Verification + +- [ ] **Step 1: Build plugin** + +```bash +cd paperforge/plugin && npm run build +``` + +- [ ] **Step 2: Run Python regression** + +```bash +pytest tests/test_sync.py tests/test_ocr.py tests/test_ocr_rendering.py tests/test_ocr_layout_zones.py tests/test_ocr_body_spine.py tests/test_asset_index.py +``` +Expected: all pass + +- [ ] **Step 3: Run plugin vitest** + +```bash +cd paperforge/plugin && npx vitest run tests/zotero-path.test.ts +``` +Expected: 2 passed + +- [ ] **Step 4: Commit any fixes from verification** + +```bash +git commit -m "chore: post-implementation verification" +``` diff --git a/docs/superpowers/specs/2026-06-01-ocr-redo-single-source-design.md b/docs/superpowers/specs/2026-06-01-ocr-redo-single-source-design.md new file mode 100644 index 00000000..996b2f38 --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-ocr-redo-single-source-design.md @@ -0,0 +1,45 @@ +# OCR Redo Single-Source Fulltext Design + +> **Status:** Approved +> **Date:** 2026-06-01 + +## Goal + +Make OCR redo a one-command closed loop and remove the duplicated workspace `fulltext.md` as a truth source. + +## Decisions + +1. Canonical OCR fulltext lives only at `System/PaperForge/ocr//fulltext.md`. +2. Workspace `Resources/.../fulltext.md` is deleted and no longer regenerated. +3. `paperforge ocr redo` immediately reruns OCR instead of only resetting state. +4. `ocr_redo` is a one-shot trigger: + - stays `true` while redo is still pending/failed + - flips to `false` only after successful OCR completion + +## Workflow + +For each note with `ocr_redo: true`: + +1. Delete stale workspace `fulltext.md` if present. +2. Delete old OCR output directory under `System/PaperForge/ocr//`. +3. Force note frontmatter to: + - `do_ocr: true` + - `ocr_status: pending` + - `fulltext_md_path: ""` + - `ocr_redo: true` +4. Invoke OCR code immediately for the selected keys only. +5. After OCR returns: + - success -> `ocr_status: done`, `ocr_redo: false` + - pending/failed -> keep `ocr_redo: true` + +## Single-Source Fulltext Contract + +- `asset_index` advertises canonical OCR `fulltext.md` as `fulltext_path` +- sync/migration no longer bridges OCR fulltext into workspace +- stale workspace copies should be deleted opportunistically when encountered + +## Non-Goals + +- No soft links +- No workspace proxy `fulltext.md` +- No background redo queue separate from the command diff --git a/docs/superpowers/specs/2026-06-01-plugin-release-notes-and-manual-design.md b/docs/superpowers/specs/2026-06-01-plugin-release-notes-and-manual-design.md new file mode 100644 index 00000000..d130153b --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-plugin-release-notes-and-manual-design.md @@ -0,0 +1,54 @@ +# Plugin Release Notes & Manual — Design Spec + +> **Status:** Approved +> **Date:** 2026-06-01 + +## Goal + +Add a settings tab and auto-popup for versioned release notes and user manual access, in Chinese first. + +## Design + +### 1. Release Notes Data + +- `paperforge/plugin/src/release-notes.json` +- Array of versioned entries, each with: + - `version`, `date`, `title` + - `breaking_or_migration` (string[]) + - `new_features` (string[]) + - `fixes` (string[]) + - `recommended_actions` (string[]) + +### 2. Settings Tab + +- New tab `更新与手册` alongside existing `安装` and `功能` +- Upper section: version log cards, collapsible, newest first +- Lower section: links to external docs (`docs/user-manual.md`) + +### 3. Auto-Popup on Version Change + +- Trigger: `plugin.manifest.version !== settings.last_seen_version` +- Modal with version title + migration items + recommended actions +- `知道了` button writes `last_seen_version` to persistent settings + +### 4. Version Tracking + +- `last_seen_version: string` field added to `PaperForgeSettings` +- Compared on plugin `onload()` +- Persisted through `saveSettings()` + +### 5. Deployment + +- `release-notes.json` copied to plugin output dir alongside `main.js` +- esbuild config updated with a copy step + +### 6. Manual + +- `docs/user-manual.md` in repo +- Plugin links to it externally; no inline rendering + +## Non-Goals + +- No English release notes in this phase +- No inline Markdown rendering for manual +- No server-side release notes fetching diff --git a/docs/user-manual.md b/docs/user-manual.md new file mode 100644 index 00000000..349b081a --- /dev/null +++ b/docs/user-manual.md @@ -0,0 +1,403 @@ +# PaperForge 使用手册 + +> 版本:1.5.15+ + +--- + +## 一、PaperForge 是什么 + +PaperForge 是一个 Obsidian 插件,把你 Vault 变成文献研究中心。它可以: + +1. 从 Zotero 自动同步文献目录,为每篇论文生成规范的 Obsidian 笔记 +2. 把 PDF 上传到 PaddleOCR,提取全文 Markdown 和图片 +3. 在 Dashboard 中查看全文、跟踪论文处理状态、重做 OCR +4. 与 AI Agent(OpenCode / Claude Code / Cursor 等)协作,进行深度阅读、文献搜索、方法论提取 +5. 基于全文建立记忆层(FTS 全文搜索 + 向量语义搜索 + 结构化索引) + +--- + +## 二、安装与初始化 + +### 2.1 前置条件 + +你需要先确保系统中有以下工具: + +- **Zotero 7**(文献管理软件) +- **Better BibTeX 插件**(Zotero 的导出插件) +- **Python 3.10+** +- **PaddleOCR API Token**(百度飞桨 OCR 服务) + +### 2.2 安装 Guide + +1. 下载 PaperForge 插件,将 `main.js`、`manifest.json`、`styles.css` 放入 Vault 的 `.obsidian/plugins/paperforge/` 目录 +2. 在 Obsidian 的 Community Plugins 中搜索并启用 PaperForge +3. 打开 PaperForge 设置: + +在 Obsidian 左侧 Ribbon 点击 PaperForge 图标,或使用命令面板搜索 `PaperForge: Dashboard`。 + +#### 步骤 1:安装标签页中配置 + +打开设置后默认在「安装」标签页。你会看到: + +- **Runtime 状态**:显示插件版本和 Python 包的版本是否匹配 +- **Python 解释器**:系统会自动检测。你也可以手动指定路径(如 `C:\Python310\python.exe`) +- **PaddleOCR API Key**:填写你从百度飞桨申请的 API Token +- **安装向导**:首次安装点击 `开始安装`,会自动检测环境并引导你完成配置 + +安装向导会自动帮你: + +- 在 Vault 中创建 PaperForge 目录结构(`System/PaperForge/`、`Resources/Literature/`、`Bases/` 等) +- 检测 Python 环境 +- 检测 Zotero 和 Better BibTeX +- 配置 Zotero 数据目录 +- 选择你的 AI Agent 平台(OpenCode / Claude Code / Cursor 等) +- **部署 Agent Skill 文件**:向导会根据你选择的平台,自动将 PaperForge Skill 部署到对应的 Vault 目录 + +#### 步骤 2:Skills 查看 + +在「功能」标签页的 Skills 区域,你可以看到哪些 Skill 已经部署到你的 Vault 中。每个 Skill 对应一个 SKILL.md 文件。你可以在这里关闭你不希望 Agent 使用的 Skill。这一页只是查看和管理,实际部署是在安装向导中完成的。 + +### 2.3 配置 Better BibTeX 自动导出 + +在 Zotero 中安装 Better BibTeX 后,你需要为每个领域配置一个自动导出。 + +**核心概念**:一个 Zotero Collection(子分类文件夹)对应一个 Base 视图文件。比如你在 Zotero 中有一个名为"骨科"的子分类文件夹,导出这个 Collection 后,Obsidian 中会自动生成 `骨科.base`。这个 Base 文件包含的就是该 Collection 下所有子文件夹里的论文,并且会随 Zotero 的修改实时同步。 + +**具体操作**: + +1. 在 Zotero 左侧面板,右键点击你想导出的 Collection(如"骨科") +2. 选择 `导出 Collection...`(Export Collection...) +3. 格式选择:**Better BibTeX JSON**(这是唯一支持的格式) +4. 勾选 `保持自动更新`(Keep updated) +5. 导出路径:选择 Vault 中的 `System/PaperForge/exports/` + +**实际效果**: +- 导出的 JSON 文件名自动使用 Collection 名称 +- Obsidian 的 `Bases/` 目录下会自动生成同名的 `.base` 文件 +- Base 文件中的论文列表是该 Collection 下所有子文件夹中的文献 +- 一次配置,永久自动同步:每次 Zotero 中新增/删除/修改文献,JSON 和 Base 都会刷新 + +**不需要每加一篇论文就导一次**:自动导出会持续保持同步。 + +### 2.4 第一次 Sync + +打开 Dashboard,点击左侧的 `Sync Library` 按钮。 + +Sync 做了什么: + +1. 读取 `System/PaperForge/exports/` 下的 JSON 导出文件 +2. 为每篇论文生成 Obsidian 笔记(`Resources/Literature// - Title/.md`) +3. 建立文献索引(`System/PaperForge/indexes/formal-library.json`) +4. 重建 FTS 记忆数据库 + +--- + +## 三、Dashboard 完全指南 + +Dashboard 是 PaperForge 的核心界面。它有 **三种模式**,根据你当前在 Obsidian 中打开的文件自动切换。 + +### 3.1 全局模式(Global Mode) + +当你没有打开任何特定论文时,Dashboard 显示全局面板。 + +**Library Snapshot**:显示你的文献库总览 + +- `papers`:总论文数 +- `PDFs ready`:有 PDF 的论文数 +- `OCR done`:已完成 OCR 的论文数 +- `deep-read done`:已完成深度阅读的论文数 + +**System Status**:系统健康状态指示灯 + +| 组件 | 含义 | +|------|------| +| Runtime | 插件版本和 Python CLI 版本是否匹配 | +| Index | 文献索引是否存在且有效 | +| Zotero Export | 是否有 Zotero 导出文件 | +| OCR Token | PaddleOCR API Key 是否已配置 | +| Memory Layer | 记忆层(FTS + 索引)是否健康 | + +**需要处理**:如果有任何状态异常,Dashboard 会列出问题并提供 `Run Doctor` 和 `Repair Issues` 按钮。 + +**Start Working**:提供快捷操作按钮 + +- `Sync Library`:同步 Zotero → PaperForge +- `Run OCR`:开始 OCR 队列处理 +- `Redo OCR`:执行一次重做 OCR +- `Run Doctor`:运行诊断 +- `Repair Issues`:修复状态不一致 + +### 3.2 论文模式(Paper Mode) + +当你打开一篇论文的工作区笔记(`.md`)或 PDF 时,Dashboard 自动切换到此模式。 + +**论文元数据**:标题、作者、年份 + +**状态条**:三个圆形指示器 + +| 指示器 | 含义 | 状态 | +|--------|------|------| +| PDF | 论文是否有 PDF | ✓(有) / ○(无) | +| OCR | OCR 处理状态 | ✓ done / ○ pending / ✗ failed | +| 精读 | 深度阅读是否完成 | ✓ done / ○ pending | + +**快捷按钮** + +| 按钮 | 功能 | +|------|------| +| 打开 PDF | 在 Obsidian 中打开 PDF | +| 打开全文 | 打开 OCR 提取的全文 Markdown | + +**文章概览(Paper Overview)**:从笔记的 `## 精读` 区域中自动提取摘要。如果还没有精读,显示提示信息。 + +**下一步推荐(Next Step)**:根据当前论文的处理状态,智能推荐下一步操作: + +- `Sync Needed`:论文尚未同步 → 点击 Sync +- `OCR Needed`:PDF 已就绪,全文未提取 → 点击 OCR +- `Ready for Deep Reading`:全文就绪 → 复制 `/pf-deep ` 命令到 Agent +- `All Set`:所有流程完成 + +**最近讨论**:显示 `ai/discussion.md` 中最新的问答记录。 + +**技术详情**(折叠区):显示更多内部状态和两个重要的勾选框: + +- `do_ocr`:将此论文加入 OCR 队列 +- `analyze`:标记此论文需要深度阅读 +- `Fulltext Path`:OCR 全文本的完整 Vault 路径 + +### 3.3 领域模式(Collection Mode) + +当你打开某个领域的 `.base` 文件时,Dashboard 切换到此模式。 + +**Workflow Overview**:该领域的漏斗图,显示 Total → PDF Ready → OCR Done → Deep Read 的数量。 + +**OCR Pipeline**:彩色进度条,一眼看清该领域 OCR 的 `Pending / Processing / Done / Attention` 分布。 + +**快捷操作**:`Run OCR`、`Sync Library`、`Redo OCR`(仅影响该领域的论文)。 + +--- + +## 四、Base 视图完全指南 + +每个领域都有对应的 Base 文件(`Bases/.base`)。Base 是 Obsidian 的数据库视图,可以像表格一样浏览、排序、筛选文献。 + +### 4.1 四个标准面板 + +| 面板 | 筛选条件 | 用途 | +|------|---------|------| +| **控制面板** | 无筛选 | 显示该领域全部论文的完整信息。包括 `has_pdf`、`do_ocr`、`analyze`、`ocr_status`、`deep_reading_status` 等所有 workflow 列 | +| **待 OCR** | `do_ocr == true && ocr_status == "pending"` | 列出需要 OCR 处理的论文。勾选这里的 `do_ocr`,下次 Run OCR 就会处理 | +| **待深度阅读** | `analyze == true && ocr_status == "done" && deep_reading_status == "pending"` | 列出 OCR 已完成但未精读的论文 | +| **Redo OCR** | `ocr_status == "done"` | 列出所有 OCR 已完成的论文。勾选 `ocr_redo`,点 Redo 按钮即可重新提取全文 | + +### 4.2 各列的含义 + +| 列名 | 含义 | 谁控制 | +|------|------|--------| +| `title` | 论文标题 | Zotero 自动 | +| `year` | 发表年份 | Zotero 自动 | +| `first_author` | 第一作者 | Zotero 自动 | +| `journal` | 期刊名 | Zotero 自动 | +| `impact_factor` | 影响因子 | 自动推算 | +| `has_pdf` | 是否有 PDF | 自动检测 Zotero 附件 | +| `do_ocr` | 是否提交 OCR | **用户可修改**(勾选 = 下次 Run OCR 时会处理这篇) | +| `analyze` | 是否标记精读 | **用户可修改**(勾选 = 准备好让 Agent 精读这篇) | +| `ocr_status` | OCR 处理状态 | 系统自动:`pending` → `processing` → `done` / `failed` | +| `deep_reading_status` | 深度阅读状态 | 系统自动:`pending` → `done` | +| `ocr_redo` | 是否标记重做 | **用户可修改**(勾选 = 下次 Redo OCR 时处理这篇) | +| `ocr_time` | OCR 完成时间 | 系统自动 | +| `pdf_path` | PDF 链接 | 系统自动 | +| `fulltext_md_path` | 全文路径 | 系统自动 | +| `collection_path` | Zotero 中的 Collection 路径 | Zotero 自动 | +| `collection_tags` | Collection 标签 | Zotero 自动 | + +### 4.3 用户可手动修改的字段 + +在整个 frontmatter 中,**以下字段是用户可以手动修改的**,其余全部由系统自动维护: + +- `do_ocr`:勾选后,论文进入 OCR 队列 +- `analyze`:勾选后,论文标记为"待深度阅读" +- `ocr_redo`:勾选后,论文标记为"需要重做 OCR" +- `tags`:标签列表,系统默认写入 `文献阅读` 和领域名,你可以随意增删,系统不会覆盖你的修改 + +> **警告**:不要手动改 `ocr_status`、`deep_reading_status`、`fulltext_md_path`、`pdf_path` 等系统字段。这些会被 sync 覆盖。 + +### 4.4 `tags` 和 `collection_path` 的来源 + +- `tags`:从 Zotero 的标签同步,同时系统默认写入 `文献阅读` 和领域名。**你可以在 Obsidian 中增删 tags,系统不会覆盖你加的 tag** +- `collection_path`:来自 Zotero 的 Collection 层级。例如 Zotero 中 `骨科 → 软骨修复 → 生物反应器` 的文章,其 `collection_path` 就是 `骨科|软骨修复|生物反应器` +- `collection_tags`:Collection 路径中每一级作为标签存入 + +--- + +## 五、完整工作流 + +### 5.1 新增论文的标准流程 + +``` +Zotero 添加文献 → 自动导出 JSON → Sync → 勾选 do_ocr → Run OCR → 打开全文 → Agent 精读 +``` + +1. **在 Zotero 中添加文献**:通过 DOI、PMID 或网页抓取 +2. **Better BibTeX 自动导出**:JSON 文件自动更新(因为你已经配置了自动导出) +3. **Dashboard → Sync**:读取最新的 JSON 导出,生成/更新 Obsidian 笔记 +4. **勾选 do_ocr**:在 Base 的「待 OCR」面板中勾选 `do_ocr`,或者直接在论文笔记的 frontmatter 中手动设为 `true` +5. **Dashboard → Run OCR**:提交 OCR 任务,等待完成 +6. **打开全文**:OCR 完成后,Dashboard 显示 `打开全文` 按钮 +7. **Agent 精读**:在 Agent 中调用 PaperForge Skill,让 Agent 读取全文 + +### 5.2 重建全文(Redo OCR)的标准流程 + +当你需要重新提取某篇论文的全文时: + +1. 在 Base 的「Redo OCR」面板中找到目标论文 +2. 勾选 `ocr_redo` +3. 在 Dashboard 或 Ribbon 点击 `Redo OCR` +4. 系统自动: + - 删除旧的 OCR 产物 + - 删除工作区旧 `fulltext.md` + - 强制 `do_ocr: true` + - 立即重跑 OCR + - 成功后写回 `ocr_redo: false` + +**注意**:如果 OCR 失败,`ocr_redo` 会保持 `true`,方便你再次尝试。 + +### 5.3 搜索与查找论文 + +有三种搜索方式,覆盖面不同: + +| 方式 | 搜索范围 | 在哪里用 | +|------|---------|---------| +| Base 面板的搜索框 | 标题、作者 | Obsidian 中直接搜 | +| 调用 PaperForge Skill | 标题、摘要、作者、全文 | Agent 中调用 | +| `paperforge search` CLI | 标题、摘要、作者、期刊 | 命令行 | + +--- + +## 六、记忆层(Memory Layer) + +记忆层是 PaperForge 的"大脑",由两部分组成: + +### 6.1 结构化记忆(Memory DB) + +- 基于 **SQLite + FTS5**(全文搜索引擎) +- 存储每篇论文的元数据(标题、作者、摘要、期刊、领域、Collection 等) +- 支持精确的字段查询(按作者、年份、领域、OCR 状态等) +- 每次 `Sync` 后自动重建 + +### 6.2 向量语义搜索(Vector DB)【可选功能】 + +> 向量数据库是**可选择性开启**的。即使没有开启,Agent 仍然可以使用 FTS 元数据搜索来查找论文。 + +- 基于 **ChromaDB**(向量数据库) +- 将所有已完成 OCR 的论文全文切分成段落,生成向量 +- 支持**语义搜索**:即使用自然语言描述一个概念(如 "75 Hz 电刺激对软骨细胞的分化影�"),ChromaDB 也能在正文中找到匹配的段落 +- 这是检索论文最强大的方式——它搜索的不只是标题和摘要,而是正文的所有内容 + +**开启方式**: + +1. 在插件设置页的「功能」标签页中,找到"向量数据库"区域 +2. 勾选开启,可选配置 embedding 模型(如 OpenAI embedding API、本地 sentence-transformers 等) +3. 点击"构建向量数据库" +4. 构建完成后,Agent 就可以使用语义搜索在正文 Methods/Results 中匹配概念 + +--- + +## 七、与 Agent 协作 + +### 7.1 PaperForge Skill 是什么 + +PaperForge Skill 是部署到 Vault 中的 Agent 技能文件(`SKILL.md`)。它做的事情是: + +1. **让 Agent 知道你的文献库存在**:Agent 会通过 bootstrap 脚本获取 Vault 路径、Python 位置、文献目录 +2. **给 Agent 提供搜索工具**:Agent 会使用 `paperforge search`(元数据搜索)和 `paperforge retrieve`(语义搜索)来查找论文 +3. **规范 Agent 的文献操作行为**:Agent 不会用 `grep` 乱搜文件、不会自己拼路径、不会只看摘要就回答事实性问题 +4. **定义 Agent 的研究工作流**:精读论文、搜索文献、文献问答、记录问答、提取方法论等 + +### 7.2 Agent 读了这个 Skill 会做什么 + +当 Agent 加载 PaperForge Skill 后,它会: + +1. 执行 `pf_bootstrap.py` 获取路径和配置 +2. 执行 `agent-context` 获取文献库概览 +3. 检查 `runtime-health` 确认系统健康 +4. 根据你的输入判断意图(搜文献、读论文、找证据、保存笔记、提取方法论) +5. 路由到对应的 workflow 执行 + +### 7.3 所有 Agent 能力详解 + +当你安装了 PaperForge Skill 后,Agent 可以直接响应如下自然语言意图: + +| 你说什么 | Agent 会做什么 | +|---------|---------------| +| "找文献"、"搜文献"、"找一下"、"搜一下" | 搜索论文目录,返回候选列表 | +| "库里有什么"、"collection 里" | 列举领域/Collection 中的论文 | +| "读一下这篇"、"看看这篇"、"这篇论文" | 打开指定论文进行阅读 | +| "找证据"、"找支持"、"找依据"、"找参数" | 检索全文中的具体证据和方法 | +| "精读"、"/pf-deep" | 三阶段深度阅读 | +| "记一下"、"保存这次"、"记录一下" | 保存当前会话的阅读笔记 | +| "提取方法论" | 从论文中提取可复用的研究方法卡片 | +| "/pf-paper " | 快速定位并阅读已知论文 | + +### 7.4 如果 Agent 没有自动读取 Skill 怎么办 + +- **通用方法**:在对话中直接写 `调用 paperforge skill`,或告诉 Agent "读取 vault 中 .opencode/skills/paperforge/SKILL.md"(路径根据你的平台调整) +- 如果 Agent 仍然无法加载,检查 Vault 的对应 skills 文件夹(如 `.opencode/skills/`)下是否存在 `paperforge/` 目录 +- 如果不存在,在插件设置页重新运行一次安装向导,或者手动将 Skill 文件复制进去 +- 重启你的 CLI / Agent 客户端后重试 + +**确认 Agent 已加载 Skill 的标志**:Agent 会先运行 `pf_bootstrap.py` 并输出路径变量,而不是直接用 `grep` 搜文件。 + +### 7.5 Agent 使用的搜索工具 + +Agent 有三种搜索武器,不会只用一种: + +| 工具 | 命令 | 搜索范围 | +|------|------|---------| +| 元数据搜索 | `paperforge search "query"` | 标题、摘要、作者、期刊、领域、Collection 路径 | +| 语义全文搜索 | `paperforge retrieve "query"` | OCR 正文全部内容(需开启向量 DB) | +| Collection/领域列举 | `paperforge context --collection/--domain` | 指定 Collection 下的完整论文列表 | + +**Agent 禁止手动 `grep` / `rg` / `glob` 搜文件**。这是 Skill 的硬性规则。如果 Agent 试图这样做,说明它没有正确加载 Skill,你需要提醒它。 + +### 7.6 Agent 的意图路由顺序 + +Agent 按以下优先级判断你要做什么: + +1. 机械命令(`/pf-sync`、`/pf-ocr`、`/pf-status`) → 直接执行 +2. 命令别名(`/pf-deep` → 深度分析,`/pf-paper` → 快速阅读) +3. 你要保存/归档 → 执行 capture +4. 你给定了明确论文 → 执行阅读 +5. 你要找论文列表 → 执行检索 +6. 你要找具体证据 → 执行证据查找 +7. 意图不清 → Agent 会问你两个问题澄清 + +--- + +## 八、目录结构参考 + +``` +Vault/ +├── System/ +│ └── PaperForge/ +│ ├── ocr// ← OCR 产物 +│ │ ├── fulltext.md ← 唯一的正文真相源 +│ │ ├── json/result.json ← OCR 原始结果 +│ │ ├── images/ ← 提取的图片 +│ │ ├── pages/ ← 页缓存 +│ │ └── meta.json ← OCR 进度 +│ ├── exports/ ← Better BibTeX JSON 导出文件 +│ └── indexes/ ← 文献索引 +│ └── formal-library.json +├── Resources/ +│ └── Literature/ ← 论文工作区 +│ └── / - / +│ ├── <ZoteroKey>.md ← 主笔记(frontmatter + 精读内容) +│ └── ai/ ← AI 产物 +│ └── discussion.md ← Agent 对话记录 +├── Bases/ ← Obsidian Base 视图 +│ └── <domain>.base +├── <平台相关>skills/paperforge/ ← Agent Skill 文件 +│ └── SKILL.md +└── .obsidian/plugins/paperforge/ ← 插件本体 +``` diff --git a/paperforge/commands/ocr.py b/paperforge/commands/ocr.py index 01e7a7fc..7fa5e2a6 100644 --- a/paperforge/commands/ocr.py +++ b/paperforge/commands/ocr.py @@ -127,10 +127,10 @@ def _get_run_ocr(): def _run_ocr_redo(vault: Path, dry_run: bool = False, verbose: bool = False, no_progress: bool = False) -> int: - """Scan for papers with ocr_redo: true, reset their OCR state.""" + """Scan for papers with ocr_redo: true, reset and immediately rerun OCR.""" from paperforge.worker.ocr import ocr_redo_papers - return ocr_redo_papers(vault, dry_run=dry_run, verbose=verbose) + return ocr_redo_papers(vault, dry_run=dry_run, verbose=verbose, no_progress=no_progress) def run(args: argparse.Namespace) -> int: @@ -171,7 +171,13 @@ def run(args: argparse.Namespace) -> int: logger.info("Processing specific key: %s", key) run_ocr = _get_run_ocr() - exit_code = run_ocr(vault, verbose=getattr(args, "verbose", False), no_progress=getattr(args, "no_progress", False)) + selected_keys = {key} if key else None + exit_code = run_ocr( + vault, + verbose=getattr(args, "verbose", False), + no_progress=getattr(args, "no_progress", False), + selected_keys=selected_keys, + ) if json_output: queue_data = _collect_ocr_queue_data(vault) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index d96324df..d3cbd956 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1,11 +1,11 @@ -"use strict";var ut=Object.create;var Se=Object.defineProperty;var ht=Object.getOwnPropertyDescriptor;var gt=Object.getOwnPropertyNames;var ft=Object.getPrototypeOf,_t=Object.prototype.hasOwnProperty;var mt=(h,g)=>{for(var e in g)Se(h,e,{get:g[e],enumerable:!0})},Ue=(h,g,e,t)=>{if(g&&typeof g=="object"||typeof g=="function")for(let s of gt(g))!_t.call(h,s)&&s!==e&&Se(h,s,{get:()=>g[s],enumerable:!(t=ht(g,s))||t.enumerable});return h};var j=(h,g,e)=>(e=h!=null?ut(ft(h)):{},Ue(g||!h||!h.__esModule?Se(e,"default",{value:h,enumerable:!0}):e,h)),yt=h=>Ue(Se({},"__esModule",{value:!0}),h);var Et={};mt(Et,{default:()=>Re});module.exports=yt(Et);var V=require("obsidian"),I=j(require("fs")),me=j(require("path")),se=require("child_process");var pe="paperforge-status",xe="paperforge",Ge='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path><line x1="8" y1="7" x2="16" y2="7"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>',W=[{id:"paperforge-sync",title:"Sync Library",desc:"Pull new references from Zotero and generate literature notes",icon:"\u21BB",cmd:"sync",okMsg:"Sync complete"},{id:"paperforge-ocr",title:"Run OCR",desc:"Extract full text and figures from PDFs via PaddleOCR",icon:"\u229E",cmd:"ocr",okMsg:"OCR started"},{id:"paperforge-doctor",title:"Run Doctor",desc:"Verify PaperForge setup \u2014 check configs, Zotero, paths, and index health",icon:"\u2695",cmd:"doctor",okMsg:"Doctor complete"},{id:"paperforge-repair",title:"Repair Issues",desc:"Fix three-way state divergence, path errors, and rebuild index",icon:"\u21BA",cmd:"repair",args:["--fix","--fix-paths"],okMsg:"Repair complete"},{id:"paperforge-ocr-redo",title:"\u91CD\u505AOCR",desc:"Re-run OCR for papers marked ocr_redo: true",icon:"\u21BA",cmd:"ocr",args:["redo"],okMsg:"OCR redo started"}],Ee={vault_path:"",setup_complete:!1,auto_update:!0,auto_update_on_startup:!0,agent_platform:"opencode",language:"",paddleocr_api_key:"",zotero_data_dir:"",python_path:"",features:{memory_layer:!0,vector_db:!1},selected_skill_platform:"opencode",vector_db_api_key:"",vector_db_api_base:"",vector_db_api_model:"text-embedding-3-small",frozen_skills:{},system_dir:"",resources_dir:"",literature_dir:"",base_dir:""};function Xe(h,g){if(!g||!g.note_path)return g;let e=h.vault.getAbstractFileByPath(g.note_path);if(!e)return g;let t=h.metadataCache.getFileCache(e),s=t&&t.frontmatter;if(!s)return g;let n={...g};for(let a of["do_ocr","analyze","ocr_status","deep_reading_status"])Object.prototype.hasOwnProperty.call(s,a)&&(n[a]=s[a]);return n}function Oe(h,g){return h&&{...h,...g}}var Ie={en:{action_running:"Running ",api_key_missing:"Missing",api_key_set:"Entered",btn_install:"Open Setup Wizard",btn_install_desc:"Check whether the environment is ready, then open the step-by-step setup wizard",btn_reconfig:"Reconfigure",btn_reconfig_desc:"Open the setup wizard again to change directories, platform, or API keys",btn_validate:"Validate",check_bbt_fail:"Not detected",check_bbt_ok:"Installed",check_python_fail:"Not found",check_python_ok:"Ready",check_zotero_fail:"Not detected",check_zotero_ok:"Found",complete_export_path:"Save Better BibTeX JSON exports into:",complete_next:"Recommended next steps",complete_step1:"Open Dashboard",complete_step1_desc:'Press Ctrl+P and run "PaperForge: Open Main Panel", or click the PaperForge icon in the left sidebar.',complete_step2:"Sync Literature",complete_step2_desc:"In the main panel, click Sync Library to bring papers from Zotero into Obsidian and generate notes.",complete_step3:"Run OCR",complete_step3_desc:"In the Obsidian Base view, mark do_ocr:true on papers, then run OCR in the main panel.",complete_step4:"Configure Better BibTeX Auto-export",complete_step4_desc:'In Zotero, right-click the library or collection you want to sync -> Export -> Better BibTeX JSON -> enable "Keep updated".',complete_summary:"Saved Configuration",complete_title:"Setup Complete",copied:"Copied!",copy_pf_deep_cmd:"Copy /pf-deep Command",dashboard_drift_warning:"PaperForge CLI (v{0}) differs from plugin (v{1}). Open Settings \u2192 Runtime Health to sync.",deep_reading_not_found:"Deep reading file not found",desc:"Obsidian + Zotero literature pipeline. Sync papers, generate notes, run OCR, and read deeply in one place.",dir_base:"Base Dir",dir_index:"Index Dir",dir_notes:"Notes Dir",dir_resources:"Resource Dir",dir_system:"System Dir",dir_vault:"Vault Path",error_copied:"Copied!",error_copy_diagnostic:"Copy diagnostic",feat_agent_platform:"Agent Platform",feat_agent_platform_desc:"Select which agent platform to manage skills for.",feat_api_base_url:"API Base URL",feat_api_base_url_desc:"Custom OpenAI-compatible API endpoint. Leave empty for default.",feat_api_model:"API Model",feat_api_model_desc:"Embedding model name for this endpoint.",feat_build_btn:"Build",feat_build_complete:"Vector build complete.",feat_build_failed:"Build failed. See terminal output.",feat_building:"Building...",feat_cache_remove_failed:"Failed: {0}",feat_cache_removed:"Model cache removed.",feat_checking:"Checking...",feat_checking_btn:"Checking...",feat_deps_checking:"Checking dependencies...",feat_deps_missing:"Dependencies not installed. Required: chromadb, openai.",feat_enter_key:"Enter a valid OpenAI API key.",feat_install_btn:"Install",feat_install_deps:"Install Dependencies",feat_install_deps_desc:"pip install chromadb openai (~35MB).",feat_install_done:"Dependencies installed. Building vectors...",feat_install_failed:"Install failed: ",feat_installing:"Installing...",feat_installing_pkgs:"Installing {pkgs}...",feat_key_rejected:"API key rejected.",feat_memory_desc:"The Memory Layer is the core data engine of PaperForge, powered by SQLite. It integrates literature metadata (papers, assets, aliases, reading events), provides FTS5 metadata search across titles, abstracts, authors, domains, and collections, and powers agent-context and paper-status. Always active \u2014 no toggle needed.",feat_memory_rebuild_btn:"Rebuild",feat_memory_rebuild_done:"Memory DB rebuilt.",feat_memory_rebuild_failed:"Rebuild failed.",feat_memory_rebuilding:"Rebuilding...",feat_model_changed_warn:"Model changed ({0} -> {1}). Existing vectors are incompatible \u2014 rebuild required.",feat_network_error:"Network error: ",feat_no_python:"No Python found. Check Installation tab.",feat_not_cached:"Not cached",feat_openai_key:"OpenAI API Key",feat_openai_key_desc:"Used for API embedding calls. Model is defined below.",feat_output_copied:"Output copied to clipboard.",feat_rebuild_btn:"Rebuild",feat_rebuild_vectors:"Rebuild Vectors",feat_rebuild_vectors_changed:"Model changed \u2014 rebuild to update all vectors.",feat_rebuild_vectors_desc:"Rebuild all OCR fulltext vectors. Required after model or mode change.",feat_removing:"Removing...",feat_retry_btn:"Retry",feat_skills_desc:"Manage and enable/disable agent skills installed in your vault. Each row corresponds to a SKILL.md file \u2014 toggle off to prevent the agent from auto-invoking that skill.",feat_skills_system:"System Skills ship with PaperForge and are updated alongside PaperForge.",feat_skills_user:"User Skills are custom skills you install from community or create yourself.",feat_uninstall_btn:"Uninstall",feat_valid_key:"API key valid.",feat_vector_config_label:"Vector Settings",feat_vector_corrupted:"Vector index corrupted \u2014 needs force rebuild.",feat_vector_desc:"Vector Database enables semantic search across OCR-extracted fulltext via API embedding. Documents are split into chunks, embedded via OpenAI-compatible API, and stored in ChromaDB.",feat_vector_enable:"Enable Vector Retrieval",feat_vector_enable_desc:"Semantic search across OCR fulltext. Requires: pip install openai chromadb (~35MB).",feat_vector_rebuild_force_btn:"Force Rebuild",feat_verify:"Verify",feat_verify_btn:"Verify",field_paddleocr:"PaddleOCR API Key",field_python_custom:"Custom Path",field_python_interp:"Python Interpreter",field_zotero_data:"Zotero Data Dir",field_zotero_placeholder:"Required. Path to Zotero data directory for PDF attachment resolution.",guide_ocr:"Run OCR",guide_ocr_desc:"In the main panel, click Run OCR to extract full text and figures from PDFs for later reading and analysis.",guide_open:"Open Main Panel",guide_open_desc:'Press Ctrl+P and run "PaperForge: Open Main Panel", or click the PaperForge icon in the left sidebar.',guide_sync:"Sync Literature",guide_sync_desc:"After Better BibTeX JSON export is configured, click Sync Library to import papers from Zotero into Obsidian and generate notes automatically.",header_title:"PaperForge",install_bootstrapping:"PaperForge Python package not found. Installing automatically...",install_btn:"Start Install",install_btn_retry:"Retry",install_btn_running:"Installing...",install_complete:"Installation complete!",install_failed:"Installation failed: ",install_validating:"Validating setup...",jump_to_deep_reading:"Open Deep Reading",label_agent:"Agent Platform",nav_close:"Close",nav_next:"Next",nav_prev:"Back",not_set:"Not entered",notice_check_fail:"Missing: ",notice_python_missing:"Python was not detected. Install Python 3.10+ and add it to PATH.",ocr_privacy_title:"OCR Privacy Notice",ocr_privacy_warning:"OCR will upload PDFs to the PaddleOCR API. Do not upload sensitive or confidential documents.",ocr_queue_add:"Add to OCR Queue",ocr_queue_added:"Added to OCR queue",ocr_queue_remove:"Remove from OCR Queue",ocr_queue_removed:"Removed from OCR queue",ocr_understand:"I understand, continue",optional_later:"(can be set later in Settings)",orphan_delete_failed:"Prune failed",orphan_delete_selected:"Delete {count} selected",orphan_deleted:"Deleted {count} orphan workspace(s)",orphan_desc:"These papers are no longer in your Zotero library.",orphan_deselect_all:"Deselect all",orphan_explain:"Removed from Zotero. Workspace files remain on disk.",orphan_keep_all:"Keep all",orphan_none_selected:"No papers selected for deletion",orphan_select_all:"Select all",orphan_title:"Found {count} orphan paper(s)",panel_actions:"Quick Actions",prep_bbt:"Better BibTeX",prep_bbt_desc:"In Zotero: Tools -> Add-ons -> install Better BibTeX.",prep_export:"Better BibTeX Auto-export",prep_export_desc:'In Zotero, right-click the collection you want to sync -> Export Collection -> BetterBibTeX JSON -> enable "Keep updated" -> save the JSON file into the exports folder shown below. Obsidian Base views will use the JSON filename as the Base name:',prep_export_path_label:"Save the exported JSON file into this folder:",prep_key:"PaddleOCR Key",prep_key_desc:"Get your API key from https://aistudio.baidu.com/paddleocr",prep_python:"Python 3.10+",prep_python_desc:"Python must be available from the command line. If you are not sure, click below to auto-detect.",prep_zotero:"Zotero Desktop",prep_zotero_desc:"Install Zotero from https://www.zotero.org",run_in_agent:"Run in {0}",runtime_health:"Runtime Health",runtime_health_checking:"Checking...",runtime_health_desc:"Check whether the installed paperforge Python package matches the plugin version and whether the deployed skill contract is current.",runtime_health_match:"Match",runtime_health_mismatch:"Mismatch",runtime_health_package_ver:"Python package v{0}",runtime_health_plugin_ver:"Plugin v{0}",runtime_health_sync:"Sync Runtime",runtime_health_sync_done:"Runtime synced to v{0}",runtime_health_sync_fail:"Sync failed: {0}",runtime_health_syncing:"Syncing...",section_config:"Current Configuration",section_guide:"How To Use",section_prep:"Preparation",section_prep_desc:"Before first use, finish these 4 preparation items. Better BibTeX auto-export is configured after setup:",setup_done:"PaperForge environment is ready",setup_pending:"Not installed yet. Finish the preparation items below, then open the wizard.",tab_features:"Features",tab_setup:"Installation",validate_base:"Base directory is required",validate_fail:"Please complete the required fields below",validate_index:"Index directory is required",validate_key:"PaddleOCR API key (optional, needed for OCR)",validate_notes:"Notes directory is required",validate_resources:"Resources directory is required",validate_system:"System directory is required",validate_vault:"Vault path is required",validate_zotero:"Zotero data directory (optional, needed for PDF linking)",wizard_agent_hint:"Choose the AI agent platform you use most often. PaperForge will place the matching command and skill files in the correct location.",wizard_dir_hint:"PaperForge stores user-facing literature data under the resources directory. These folders will live there:",wizard_dir_sub_hint:"Resolved folder preview based on the names below:",wizard_intro:"This wizard walks you through the full setup. In most cases, the default values are fine to keep.",wizard_keys_hint:"Enter your PaddleOCR API key below. If you want PaperForge to auto-locate Zotero PDFs, you can also fill in the Zotero data directory.",wizard_preview:"After installation, system files stay at the vault root while literature data stays under the resources directory.",wizard_safety:"Safety: if the selected folders already contain files, setup preserves existing files and only creates missing PaperForge folders and files.",wizard_step1:"Overview",wizard_step2:"Directory Setup",wizard_step3:"Platform & Keys",wizard_step4:"Install",wizard_step5:"Done",wizard_skip_ocr_desc:"OCR will not be available until you configure a valid PaddleOCR API key. You can continue setup now and configure it later in Settings.",wizard_skip_ocr_continue:"Continue without OCR key",wizard_skip_ocr_back:"Back to configure",wizard_api_hint_skip:"OCR key is optional \u2014 you may skip it and configure later.",wizard_sys_hint:"These folders live at the vault root, outside the resources directory:",wizard_title:"PaperForge Setup Wizard"},zh:{action_running:"\u6B63\u5728\u6267\u884C ",api_key_missing:"\u672A\u914D\u7F6E \u2717",api_key_set:"\u5DF2\u914D\u7F6E \u2713",btn_install:"\u6253\u5F00\u5B89\u88C5\u5411\u5BFC",btn_install_desc:"\u81EA\u52A8\u68C0\u6D4B Python + \u524D\u7F6E\u73AF\u5883\uFF0C\u901A\u8FC7\u540E\u6253\u5F00\u5206\u6B65\u5B89\u88C5\u5411\u5BFC",btn_reconfig:"\u91CD\u65B0\u914D\u7F6E",btn_reconfig_desc:"\u91CD\u65B0\u8FD0\u884C\u5B89\u88C5\u5411\u5BFC\uFF0C\u4FEE\u6539\u76EE\u5F55\u6216\u5BC6\u94A5\u914D\u7F6E",btn_validate:"\u9A8C\u8BC1",check_bbt_fail:"\u672A\u68C0\u6D4B\u5230",check_bbt_ok:"\u5DF2\u5B89\u88C5",check_python_fail:"\u672A\u5B89\u88C5",check_python_ok:"\u5DF2\u5C31\u7EEA",check_zotero_fail:"\u672A\u68C0\u6D4B\u5230",check_zotero_ok:"\u5DF2\u5B89\u88C5",complete_next:"\u4E0B\u4E00\u6B65\u64CD\u4F5C",complete_step1:"\u6253\u5F00 PaperForge Dashboard",complete_step1_desc:"Ctrl+P \u2192 \u8F93\u5165 PaperForge: Open Dashboard\uFF0C\u6216\u70B9\u5DE6\u4FA7\u4E66\u672C\u56FE\u6807",complete_step2:"\u540C\u6B65\u6587\u732E",complete_step2_desc:"Dashboard \u4E2D\u70B9 Sync Library\uFF0C\u4ECE Zotero \u62C9\u53D6\u6587\u732E\u751F\u6210\u7B14\u8BB0",complete_step3:"\u8FD0\u884C OCR",complete_step3_desc:"Dashboard \u4E2D\u70B9 Run OCR\uFF0C\u63D0\u53D6 PDF \u5168\u6587\u4E0E\u56FE\u8868",complete_step4:"\u914D\u7F6E BBT \u81EA\u52A8\u5BFC\u51FA",complete_summary:"\u5F53\u524D\u5B8C\u6574\u914D\u7F6E",complete_title:"\u2713 PaperForge \u5B89\u88C5\u5B8C\u6210",copied:"\u5DF2\u590D\u5236\uFF01",copy_pf_deep_cmd:"\u590D\u5236 /pf-deep \u547D\u4EE4",dashboard_drift_warning:'\u63D2\u4EF6\u7248\u672C\u4E0E Python \u8FD0\u884C\u65F6\u7248\u672C\u4E0D\u5339\u914D\u3002\u8BF7\u5728\u8BBE\u7F6E\u4E2D\u70B9\u51FB"\u540C\u6B65\u8FD0\u884C\u65F6"\u3002',deep_reading_not_found:"\u7CBE\u8BFB\u6587\u4EF6\u672A\u627E\u5230",desc:"Obsidian + Zotero \u6587\u732E\u7BA1\u7406\u6D41\u6C34\u7EBF\u3002\u81EA\u52A8\u540C\u6B65\u6587\u732E\u3001\u751F\u6210\u7B14\u8BB0\u3001OCR \u63D0\u53D6\u5168\u6587\uFF0C\u4E00\u7AD9\u5F0F\u6587\u732E\u7CBE\u8BFB\u5DE5\u4F5C\u6D41\u3002",dir_base:"Base \u76EE\u5F55",dir_index:"\u7D22\u5F15\u76EE\u5F55",dir_notes:"\u6B63\u6587\u76EE\u5F55",dir_resources:"\u8D44\u6E90\u76EE\u5F55",dir_system:"\u7CFB\u7EDF\u76EE\u5F55",dir_vault:"Vault \u8DEF\u5F84",error_copied:"\u5DF2\u590D\u5236\uFF01",error_copy_diagnostic:"\u590D\u5236\u8BCA\u65AD\u4FE1\u606F",feat_agent_platform:"Agent \u5E73\u53F0",feat_agent_platform_desc:"\u9009\u62E9\u8981\u7BA1\u7406\u7684 Agent \u5E73\u53F0\u3002",feat_api_base_url:"API \u5730\u5740",feat_api_base_url_desc:"\u81EA\u5B9A\u4E49 OpenAI \u517C\u5BB9 API \u7AEF\u70B9\u3002\u7559\u7A7A\u4F7F\u7528\u9ED8\u8BA4\u5730\u5740\u3002",feat_api_model:"API \u6A21\u578B",feat_api_model_desc:"\u8BE5\u7AEF\u70B9\u4F7F\u7528\u7684\u5D4C\u5165\u6A21\u578B\u540D\u79F0\u3002",feat_build_btn:"\u6784\u5EFA",feat_build_complete:"\u5411\u91CF\u6784\u5EFA\u5B8C\u6210\u3002",feat_build_failed:"\u6784\u5EFA\u5931\u8D25\u3002\u8BF7\u67E5\u770B\u7EC8\u7AEF\u8F93\u51FA\u3002",feat_building:"\u6784\u5EFA\u4E2D\u2026",feat_cache_remove_failed:"\u5931\u8D25\uFF1A{0}",feat_cache_removed:"\u6A21\u578B\u7F13\u5B58\u5DF2\u6E05\u9664\u3002",feat_checking:"\u68C0\u6D4B\u4E2D\u2026",feat_checking_btn:"\u68C0\u6D4B\u4E2D\u2026",feat_deps_checking:"\u6B63\u5728\u68C0\u6D4B\u4F9D\u8D56\u2026",feat_deps_missing:"\u4F9D\u8D56\u672A\u5B89\u88C5\u3002\u9700\u8981\uFF1Achromadb, openai\u3002",feat_enter_key:"\u8BF7\u8F93\u5165\u6709\u6548\u7684 OpenAI API Key\u3002",feat_install_btn:"\u5B89\u88C5",feat_install_deps:"\u5B89\u88C5\u4F9D\u8D56",feat_install_done:"\u4F9D\u8D56\u5DF2\u5B89\u88C5\u3002\u6B63\u5728\u6784\u5EFA\u5411\u91CF\u2026",feat_install_failed:"\u5B89\u88C5\u5931\u8D25\uFF1A",feat_installing:"\u5B89\u88C5\u4E2D\u2026",feat_installing_pkgs:"\u6B63\u5728\u5B89\u88C5 {pkgs}...",feat_key_rejected:"API Key \u88AB\u62D2\u7EDD\u3002",feat_memory_desc:"\u8BB0\u5FC6\u5C42\u662F PaperForge \u7684\u6838\u5FC3\u6570\u636E\u5F15\u64CE\uFF0C\u57FA\u4E8E SQLite \u6784\u5EFA\u3002\u5B83\u6574\u5408\u4E86\u6587\u732E\u5143\u6570\u636E\uFF08\u8BBA\u6587\u3001\u8D44\u6E90\u6587\u4EF6\u3001\u522B\u540D\u3001\u9605\u8BFB\u4E8B\u4EF6\uFF09\uFF0C\u652F\u6301 FTS5 \u5143\u6570\u636E\u68C0\u7D22\uFF08\u6807\u9898\u3001\u6458\u8981\u3001\u4F5C\u8005\u3001domain\u3001collection\uFF09\uFF0C\u5E76\u4E3A agent-context \u548C paper-status \u547D\u4EE4\u63D0\u4F9B\u6570\u636E\u652F\u6491\u3002\u59CB\u7EC8\u8FD0\u884C\uFF0C\u65E0\u9700\u624B\u52A8\u5F00\u542F\u3002",feat_memory_rebuild_btn:"\u91CD\u5EFA\u6570\u636E\u5E93",feat_memory_rebuild_done:"\u8BB0\u5FC6\u6570\u636E\u5E93\u91CD\u5EFA\u5B8C\u6210\u3002",feat_memory_rebuild_failed:"\u91CD\u5EFA\u5931\u8D25\u3002",feat_memory_rebuilding:"\u91CD\u5EFA\u4E2D\u2026",feat_model:"\u6A21\u578B",feat_model_changed_warn:"\u6A21\u578B\u5DF2\u66F4\u6362\uFF08{0} -> {1}\uFF09\u3002\u5DF2\u6709\u5411\u91CF\u4E0D\u517C\u5BB9\u2014\u2014\u9700\u8981\u91CD\u5EFA\u3002",feat_network_error:"\u7F51\u7EDC\u9519\u8BEF\uFF1A",feat_no_python:"\u672A\u627E\u5230 Python\u3002\u8BF7\u67E5\u770B\u5B89\u88C5\u6807\u7B7E\u9875\u3002",feat_not_cached:"\u672A\u7F13\u5B58",feat_openai_key:"OpenAI API Key",feat_openai_key_desc:"\u7528\u4E8E API \u5D4C\u5165\u8C03\u7528\uFF0C\u6A21\u578B\u5728\u4E0B\u65B9\u5B9A\u4E49\u3002",feat_output_copied:"\u8F93\u51FA\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F\u3002",feat_rebuild_btn:"\u91CD\u5EFA",feat_rebuild_vectors:"\u91CD\u5EFA\u5411\u91CF",feat_rebuild_vectors_changed:"\u6A21\u578B\u5DF2\u66F4\u6362 \u2014 \u9700\u8981\u91CD\u5EFA\u5411\u91CF\u3002",feat_rebuild_vectors_desc:"\u91CD\u5EFA\u6240\u6709 OCR \u5168\u6587\u5411\u91CF\u3002\u66F4\u6362\u6A21\u578B\u6216\u6A21\u5F0F\u540E\u9700\u8981\u91CD\u5EFA\u3002",feat_removing:"\u5220\u9664\u4E2D\u2026",feat_retry_btn:"\u91CD\u8BD5",feat_skills_desc:"\u7BA1\u7406 Vault \u4E2D\u5DF2\u5B89\u88C5\u7684 Agent \u6280\u80FD\u3002\u6BCF\u884C\u5BF9\u5E94\u4E00\u4E2A SKILL.md \u6587\u4EF6\uFF0C\u5173\u95ED\u5F00\u5173\u53EF\u963B\u6B62 Agent \u81EA\u52A8\u8C03\u7528\u8BE5\u6280\u80FD\u3002",feat_skills_system:"\u7CFB\u7EDF\u6280\u80FD\u968F PaperForge \u4E00\u540C\u53D1\u5E03\uFF0C\u4F1A\u8DDF\u968F PaperForge \u7248\u672C\u66F4\u65B0\u3002",feat_skills_user:"\u7528\u6237\u6280\u80FD\u662F\u4F60\u81EA\u884C\u5B89\u88C5\u6216\u521B\u5EFA\u7684\u81EA\u5B9A\u4E49\u6280\u80FD\u3002",feat_uninstall_btn:"\u5378\u8F7D",feat_valid_key:"API Key \u6709\u6548\u3002",feat_vector_config_label:"\u5411\u91CF\u5E93\u914D\u7F6E",feat_vector_corrupted:"\u5411\u91CF\u7D22\u5F15\u5DF2\u635F\u574F \u2014 \u9700\u8981\u5F3A\u5236\u91CD\u5EFA\u3002",feat_vector_desc:"\u5411\u91CF\u6570\u636E\u5E93\u901A\u8FC7\u5D4C\u5165\u6A21\u578B\u5B9E\u73B0 OCR \u5168\u6587\u7684\u8BED\u4E49\u641C\u7D22\u3002\u6587\u6863\u88AB\u5207\u5206\u4E3A\u6587\u672C\u5757\uFF08chunk\uFF09\uFF0C\u7F16\u7801\u4E3A\u5411\u91CF\u5B58\u5165 ChromaDB\u3002\u652F\u6301\u672C\u5730\u6A21\u578B\uFF08\u514D\u8D39\uFF0CCPU \u8FD0\u884C\uFF09\u6216 OpenAI API\uFF08\u4ED8\u8D39\uFF0C\u66F4\u5FEB\u901F\uFF09\u3002",feat_vector_enable:"\u542F\u7528\u5411\u91CF\u68C0\u7D22",feat_vector_enable_desc:"\u5BF9 OCR \u5168\u6587\u8FDB\u884C\u8BED\u4E49\u641C\u7D22\u3002\u9700\u5B89\u88C5: pip install chromadb sentence-transformers openai (~500MB)\u3002",feat_vector_rebuild_force_btn:"\u5F3A\u5236\u91CD\u5EFA",feat_verify:"\u9A8C\u8BC1",feat_verify_btn:"\u9A8C\u8BC1",field_paddleocr:"PaddleOCR API \u5BC6\u94A5",field_python_custom:"\u81EA\u5B9A\u4E49 Python \u8DEF\u5F84",field_python_interp:"\u5F53\u524D Python \u89E3\u91CA\u5668",field_zotero_data:"Zotero \u6570\u636E\u76EE\u5F55",field_zotero_placeholder:"\u53EF\u9009\uFF0C\u7528\u4E8E\u81EA\u52A8\u68C0\u6D4B PDF",guide_ocr:"\u8FD0\u884C OCR",guide_ocr_desc:"Dashboard \u4E2D\u70B9 Run OCR\uFF0C\u63D0\u53D6 PDF \u5168\u6587\u4E0E\u56FE\u8868",guide_open:"\u6253\u5F00 Dashboard",guide_open_desc:"Ctrl+P \u2192 \u8F93\u5165 PaperForge: Open Dashboard\uFF0C\u6216\u70B9\u5DE6\u4FA7\u4E66\u672C\u56FE\u6807",guide_sync:"\u540C\u6B65\u6587\u732E",guide_sync_desc:"Dashboard \u4E2D\u70B9 Sync Library\uFF0C\u4ECE Zotero \u62C9\u53D6\u6587\u732E\u751F\u6210\u7B14\u8BB0",header_title:"PaperForge",install_bootstrapping:"\u672A\u68C0\u6D4B\u5230 PaperForge Python \u5305\uFF0C\u6B63\u5728\u81EA\u52A8\u5B89\u88C5\u2026",install_btn:"\u5F00\u59CB\u5B89\u88C5",install_btn_retry:"\u91CD\u8BD5",install_btn_running:"\u6B63\u5728\u5B89\u88C5...",install_complete:"\u2713 \u5B89\u88C5\u5B8C\u6210\uFF01",install_failed:"\u2717 \u5B89\u88C5\u5931\u8D25\uFF1A",install_validating:"\u6B63\u5728\u6821\u9A8C\u5B89\u88C5\u73AF\u5883\u2026",jump_to_deep_reading:"\u8DF3\u8F6C\u5230\u7CBE\u8BFB",label_agent:"Agent \u5E73\u53F0",nav_close:"\u5173\u95ED",nav_next:"\u4E0B\u4E00\u6B65 \u2192",nav_prev:"\u2190 \u4E0A\u4E00\u6B65",no_pending_ocr:"\u6240\u6709 OCR \u4EFB\u52A1\u5DF2\u5B8C\u6210",not_set:"\u672A\u8BBE\u7F6E",notice_check_fail:"\u672A\u901A\u8FC7: ",notice_python_missing:"Python \u672A\u68C0\u6D4B\u5230\uFF0C\u8BF7\u5148\u5B89\u88C5 Python 3.10+ \u5E76\u52A0\u5165 PATH",ocr_privacy_title:"OCR \u9690\u79C1\u63D0\u793A",ocr_privacy_warning:"OCR \u4F1A\u5C06 PDF \u4E0A\u4F20\u5230 PaddleOCR API \u8FDB\u884C\u5904\u7406\u3002\u8BF7\u4E0D\u8981\u4E0A\u4F20\u5305\u542B\u654F\u611F\u4FE1\u606F\u6216\u65E0\u6CD5\u5916\u4F20\u7684\u6587\u732E\u3002",ocr_queue_add:"\u52A0\u5165 OCR \u961F\u5217",ocr_queue_added:"\u5DF2\u52A0\u5165 OCR \u961F\u5217",ocr_queue_remove:"\u79FB\u51FA OCR \u961F\u5217",ocr_queue_removed:"\u5DF2\u79FB\u51FA OCR \u961F\u5217",ocr_understand:"\u6211\u4E86\u89E3\uFF0C\u7EE7\u7EED",optional_later:"\uFF08\u7A0D\u540E\u53EF\u5728\u8BBE\u7F6E\u4E2D\u8865\u5145\uFF09",orphan_delete_failed:"\u6E05\u7406\u5931\u8D25",orphan_delete_selected:"\u5220\u9664 {count} \u7BC7",orphan_deleted:"\u5DF2\u5220\u9664 {count} \u7BC7\u6B8B\u7559\u6587\u732E",orphan_desc:"\u8FD9\u4E9B\u6587\u732E\u5DF2\u4ECE Zotero \u4E2D\u79FB\u9664\u3002",orphan_deselect_all:"\u53D6\u6D88\u5168\u9009",orphan_explain:"\u5DF2\u4ECE Zotero \u4E2D\u79FB\u9664\u3002\u5DE5\u4F5C\u533A\u6587\u4EF6\u4ECD\u4FDD\u7559\u5728\u78C1\u76D8\u4E0A\u3002",orphan_keep_all:"\u4FDD\u7559\u5168\u90E8",orphan_none_selected:"\u672A\u9009\u62E9\u4EFB\u4F55\u6587\u732E",orphan_select_all:"\u5168\u9009",orphan_title:"\u53D1\u73B0 {count} \u7BC7\u6B8B\u7559\u6587\u732E",panel_actions:"\u5FEB\u6377\u64CD\u4F5C",prep_bbt:"Better BibTeX",prep_bbt_desc:"Zotero \u2192 \u5DE5\u5177 \u2192 \u63D2\u4EF6 \u2192 \u5B89\u88C5 Better BibTeX",prep_export:"BBT \u81EA\u52A8\u5BFC\u51FA",prep_export_desc:"\u53F3\u952E\u6587\u732E\u5B50\u5206\u7C7B \u2192 \u5BFC\u51FA\u5206\u7C7B \u2192 BetterBibTeX JSON \u2192 \u52FE\u9009\u4FDD\u6301\u66F4\u65B0 \u2192 \u5BFC\u51FA\u5230\uFF08JSON \u6587\u4EF6\u540D\u5373\u4E3A Base \u540D\uFF09\uFF1A",prep_key:"PaddleOCR Key",prep_key_desc:"\u5728 https://aistudio.baidu.com/paddleocr \u83B7\u53D6 API Key",prep_python:"Python 3.10+",prep_python_desc:"\u786E\u4FDD Python \u53EF\u547D\u4EE4\u884C\u8C03\u7528\u3002\u70B9\u51FB\u4E0B\u65B9\u6309\u94AE\u81EA\u52A8\u68C0\u6D4B\u3002",prep_zotero:"Zotero \u684C\u9762\u7248",prep_zotero_desc:"\u5B89\u88C5 Zotero (https://www.zotero.org)",run_in_agent:"\u5728 {0} \u4E2D\u8FD0\u884C",runtime_health:"\u8FD0\u884C\u65F6\u72B6\u6001",runtime_health_checking:"\u6B63\u5728\u68C0\u6D4B\u2026",runtime_health_desc:"\u68C0\u67E5\u63D2\u4EF6\u4E0E Python \u8FD0\u884C\u65F6\u7248\u672C\u7684\u5339\u914D\u60C5\u51B5\uFF0C\u5E76\u786E\u8BA4\u5DF2\u90E8\u7F72\u7684 skill contract \u662F\u5426\u4E3A\u5F53\u524D\u7248\u672C\u3002",runtime_health_match:"\u5339\u914D",runtime_health_mismatch:"\u4E0D\u5339\u914D",runtime_health_package_ver:"Python \u5305 v{0}",runtime_health_plugin_ver:"\u63D2\u4EF6 v{0}",runtime_health_sync:"\u540C\u6B65\u8FD0\u884C\u65F6",runtime_health_sync_done:"\u8FD0\u884C\u65F6\u5DF2\u540C\u6B65\u81F3 v{0}",runtime_health_sync_fail:"\u8FD0\u884C\u65F6\u540C\u6B65\u5931\u8D25\uFF1A{0}",runtime_health_syncing:"\u6B63\u5728\u540C\u6B65\u2026",section_config:"\u5F53\u524D\u914D\u7F6E",section_guide:"\u64CD\u4F5C\u65B9\u5F0F",section_prep:"\u5B89\u88C5\u51C6\u5907",section_prep_desc:"\u9996\u6B21\u4F7F\u7528\u524D\uFF0C\u8BF7\u4F9D\u6B21\u5B8C\u6210\u4EE5\u4E0B\u51C6\u5907\uFF1A",setup_done:"\u2713 PaperForge \u73AF\u5883\u5DF2\u914D\u7F6E\u5B8C\u6210",setup_pending:"\u5C1A\u672A\u5B89\u88C5\uFF0C\u5B8C\u6210\u5B89\u88C5\u51C6\u5907\u540E\u70B9\u51FB\u5B89\u88C5\u5411\u5BFC",tab_features:"\u529F\u80FD",tab_setup:"\u5B89\u88C5",validate_base:"Base \u76EE\u5F55\u672A\u586B\u5199",validate_fail:"\u914D\u7F6E\u9A8C\u8BC1\u5931\u8D25",validate_index:"\u7D22\u5F15\u76EE\u5F55\u672A\u586B\u5199",validate_key:"PaddleOCR API \u5BC6\u94A5\u672A\u586B\u5199",validate_notes:"\u6B63\u6587\u76EE\u5F55\u672A\u586B\u5199",validate_resources:"\u8D44\u6E90\u76EE\u5F55\u672A\u586B\u5199",validate_system:"\u7CFB\u7EDF\u76EE\u5F55\u672A\u586B\u5199",validate_vault:"Vault \u8DEF\u5F84\u672A\u586B\u5199",validate_zotero:"Zotero \u6570\u636E\u76EE\u5F55\u4E3A\u5FC5\u586B\u9879",wizard_agent_hint:"\u9009\u62E9\u4F60\u4F7F\u7528\u7684 AI Agent \u5E73\u53F0\uFF0C\u5B89\u88C5\u65F6\u5C06\u6309\u5BF9\u5E94\u683C\u5F0F\u90E8\u7F72\u6280\u80FD\u6587\u4EF6\uFF1A",wizard_dir_hint:"\u8D44\u6E90\u76EE\u5F55\u662F\u6587\u732E\u6570\u636E\u7684\u7EDF\u4E00\u6839\u76EE\u5F55\uFF0C\u4EE5\u4E0B\u5B50\u76EE\u5F55\u5C06\u521B\u5EFA\u5728\u5176\u5185\u90E8\uFF1A",wizard_dir_sub_hint:"\u8D44\u6E90\u76EE\u5F55\u5185\u7684\u4E24\u4E2A\u5B50\u76EE\u5F55\uFF1A",wizard_intro:"\u672C\u5411\u5BFC\u5C06\u5F15\u5BFC\u60A8\u5B8C\u6210 PaperForge \u73AF\u5883\u7684\u5B8C\u6574\u914D\u7F6E\u3002\u5B89\u88C5\u8FC7\u7A0B\u4F1A\u81EA\u52A8\u521B\u5EFA\u6240\u6709\u76EE\u5F55\u7ED3\u6784\uFF0C\u65E0\u9700\u624B\u52A8\u64CD\u4F5C\u3002",wizard_keys_hint:"\u4EE5\u4E0B\u4E3A API \u5BC6\u94A5\u4E0E Zotero \u914D\u7F6E\uFF1A",wizard_preview:"\u7CFB\u7EDF\u6587\u4EF6\u548C Agent \u914D\u7F6E\u4F4D\u4E8E Vault \u6839\u76EE\u5F55\u4E0B\u3002\u6587\u732E\u6570\u636E\uFF08\u6B63\u6587\u3001\u7D22\u5F15\uFF09\u7EDF\u4E00\u5B58\u653E\u5728\u8D44\u6E90\u76EE\u5F55\u5185\u3002\u5B89\u88C5\u540E\u4ECD\u53EF\u5728\u8BBE\u7F6E\u4E2D\u4FEE\u6539\u3002",wizard_safety:"\u5B89\u5168\u8BF4\u660E\uFF1A\u5982\u679C\u4F60\u9009\u62E9\u7684\u76EE\u5F55\u91CC\u5DF2\u7ECF\u6709\u6587\u4EF6\uFF0C\u5B89\u88C5\u5411\u5BFC\u4F1A\u4FDD\u7559\u5DF2\u6709\u5185\u5BB9\uFF0C\u53EA\u8865\u5145\u7F3A\u5931\u7684 PaperForge \u6587\u4EF6\u548C\u76EE\u5F55\u3002",wizard_step1:"\u6982\u89C8",wizard_step2:"\u76EE\u5F55",wizard_step3:"Agent",wizard_step4:"\u5B89\u88C5",wizard_step5:"\u5B8C\u6210",wizard_skip_ocr_desc:"OCR \u529F\u80FD\u5728\u914D\u7F6E\u6709\u6548\u7684 PaddleOCR API \u5BC6\u94A5\u4E4B\u524D\u4E0D\u53EF\u7528\u3002\u60A8\u53EF\u4EE5\u7EE7\u7EED\u5B8C\u6210\u8BBE\u7F6E\uFF0C\u7A0D\u540E\u5728\u8BBE\u7F6E\u4E2D\u914D\u7F6E\u3002",wizard_skip_ocr_continue:"\u7EE7\u7EED\uFF0C\u7A0D\u540E\u914D\u7F6E\u5BC6\u94A5",wizard_skip_ocr_back:"\u8FD4\u56DE\u914D\u7F6E",wizard_api_hint_skip:"OCR \u5BC6\u94A5\u4E3A\u9009\u586B\u9879 \u2014 \u53EF\u8DF3\u8FC7\uFF0C\u7A0D\u540E\u5728\u8BBE\u7F6E\u4E2D\u914D\u7F6E\u3002",wizard_sys_hint:"\u72EC\u7ACB\u4E8E\u8D44\u6E90\u76EE\u5F55\u7684\u7CFB\u7EDF\u6587\u4EF6\uFF1A",wizard_title:"PaperForge \u5B89\u88C5\u5411\u5BFC"}},Le=null;function bt(h){try{let g=h.vault;if(typeof g.getConfig=="function"){let e=g.getConfig("language");if(e&&String(e).startsWith("zh"))return"zh"}}catch(g){}try{if(typeof localStorage!="undefined"){let g=localStorage.getItem("language");if(g&&String(g).startsWith("zh"))return"zh"}}catch(g){}return"en"}function Ye(h){Le=bt(h)==="zh"?Ie.zh:Ie.en}function i(h){return Le&&Le[h]||Ie.en[h]||h}var w=require("obsidian"),O=j(require("fs")),z=j(require("path")),dt=j(require("os")),H=require("child_process");var Y=j(require("fs")),X=j(require("path")),et=j(require("os")),ne=require("child_process"),Me=null,Qe=!1;function A(h,g,e,t){let s=e||Y,n=t||ne.execFileSync;if(g&&g.python_path&&g.python_path.trim()){let o=g.python_path.trim();if(s.existsSync(o))return{path:o,source:"manual",extraArgs:[]}}let a=[X.join(h,".paperforge-test-venv","Scripts","python.exe"),X.join(h,".venv","Scripts","python.exe"),X.join(h,"venv","Scripts","python.exe")];for(let o of a)try{if(s.existsSync(o))return{path:o,source:"auto-detected",extraArgs:[]}}catch(l){}let r=[{path:"py",extraArgs:["-3"]},{path:"python",extraArgs:[]},{path:"python3",extraArgs:[]}];for(let o of r)try{let l=n(o.path,[...o.extraArgs,"--version"],{encoding:"utf-8",timeout:5e3,windowsHide:!0});if(l&&l.toLowerCase().includes("python"))return{path:o.path,source:"auto-detected",extraArgs:o.extraArgs}}catch(l){}return{path:"python",source:"auto-detected",extraArgs:[]}}function tt(h,g,e,t,s){t===void 0&&(t=1e4);let n=s||ne.execFile;return new Promise(a=>{n(h,["-c","import paperforge; print(paperforge.__version__)"],{cwd:e,timeout:t},(r,o)=>{if(r){a({status:"not-installed",pyVersion:null,pluginVersion:g,error:r.message});return}let l=o&&o.trim()||null;a(l===g?{status:"match",pyVersion:l,pluginVersion:g,error:null}:{status:"mismatch",pyVersion:l,pluginVersion:g,error:null})})})}function st(h,g,e){e===void 0&&(e=[]);let t=`paperforge==${g}`,s=`git+https://github.com/LLLin000/PaperForge.git@${g}`,n=[...e,"-m","pip","install","--upgrade",t],a=[...e,"-m","pip","install","--upgrade",s];return{cmd:h,url:s,args:a,pypiArgs:n,gitArgs:a,timeout:12e4}}function rt(h,g,e,t,s,n){let a=s||ne.spawn;return new Promise(r=>{let o=Date.now(),l={cwd:e,timeout:t,windowsHide:!0};n&&(l.env=n);let c=a(h,g,l),p=[],u=[];c.stdout.on("data",d=>{p.push(d.toString("utf-8"))}),c.stderr.on("data",d=>{u.push(d.toString("utf-8"))}),c.on("close",d=>{r({stdout:p.join(""),stderr:u.join(""),exitCode:d,elapsed:Date.now()-o})}),c.on("error",d=>{r({stdout:p.join(""),stderr:u.join("")+` -`+d.message,exitCode:-1,elapsed:Date.now()-o})})})}function Ne(){if(Qe)return Me;Qe=!0;try{let h;if(process.platform==="win32"){let g=process.env.ComSpec||"C:\\Windows\\System32\\cmd.exe";h=(0,ne.execFileSync)(g,["/c","where","git"],{timeout:5e3,windowsHide:!0,encoding:"utf-8"})}else h=(0,ne.execFileSync)("which",["git"],{timeout:5e3,encoding:"utf-8"});if(h){let g=h.split(` -`)[0].trim();g&&(Me=X.dirname(g))}}catch(h){}return Me}function de(){let h={...process.env},g=process.platform,e=et.homedir(),t=[],s=Ne();s&&t.push(s),g==="darwin"?t.push("/opt/homebrew/bin","/usr/local/bin","/usr/bin",`${e}/.local/bin`):g==="linux"&&t.push("/usr/local/bin","/usr/bin",`${e}/.local/bin`);let n=h.PATH||"";return h.PATH=[...t,n].filter(Boolean).join(X.delimiter),h}function nt(h){return String(h).toLowerCase().replace(/[^a-z0-9]/g,"").includes("betterbibtex")}function ze(h){if(!h)return!1;try{if(!Y.existsSync(h))return!1;for(let g of Y.readdirSync(h))if(nt(g))return!0}catch(g){}return!1}function Ce(h){if(!h)return!1;try{if(!Y.existsSync(h))return!1;for(let g of Y.readdirSync(h)){let e=X.join(h,g,"extensions");try{if(!Y.existsSync(e))continue;for(let t of Y.readdirSync(e))if(nt(t))return!0}catch(t){}}}catch(g){}return!1}var ue=j(require("fs")),M=j(require("path")),at=require("child_process"),Q=null;function vt(h,g){let e=g||ue,t=M.join(h,"paperforge.json"),s={system_dir:"System",resources_dir:"Resources",literature_dir:"Literature",base_dir:"Bases"};try{if(!e.existsSync(t))return{...s,_warning:"paperforge.json not found; using defaults"};let n=e.readFileSync(t,"utf-8"),a=JSON.parse(n),r=a.vault_config||{};return{system_dir:r.system_dir||a.system_dir||s.system_dir,resources_dir:r.resources_dir||a.resources_dir||s.resources_dir,literature_dir:r.literature_dir||a.literature_dir||s.literature_dir,base_dir:r.base_dir||a.base_dir||s.base_dir,_warning:null}}catch(n){return console.warn("PaperForge: Failed to read paperforge.json, using defaults",n),{...s,_warning:"paperforge.json invalid; using defaults"}}}function ee(h,g){let e=vt(h,g),t=M.join(h,e.system_dir,"PaperForge");return{vault:h,systemDir:t,indexesDir:M.join(t,"indexes"),logsDir:M.join(t,"logs"),dbPath:M.join(t,"indexes","paperforge.db"),memoryStatePath:M.join(t,"indexes","memory-runtime-state.json"),vectorStatePath:M.join(t,"indexes","vector-runtime-state.json"),healthStatePath:M.join(t,"indexes","runtime-health.json"),buildStatePath:M.join(t,"indexes","vector-build-state.json"),orphanStatePath:M.join(t,"indexes","sync-orphan-state.json"),exportsDir:M.join(t,"exports"),ocrDir:M.join(t,"ocr"),pluginDataPath:M.join(h,".obsidian","plugins","paperforge","data.json"),pfJsonPath:M.join(h,"paperforge.json"),configWarning:e._warning}}function $e(h){try{return ue.existsSync(h)?JSON.parse(ue.readFileSync(h,"utf-8")):null}catch(g){return null}}function xt(h){let g=ee(h);return $e(g.memoryStatePath)}function ke(h){let g=ee(h);return $e(g.vectorStatePath)}function je(h){let g=ee(h);return $e(g.healthStatePath)}function it(h){var e;let g=je(h);return!!(g&&((e=g.summary)==null?void 0:e.status)==="ok")}function Fe(h){let g=xt(h);return!g||g.paper_count_db===0?"DB not found. Run paperforge memory build.":"Papers: "+g.paper_count_db+" | "+(g.fresh?"fresh":"stale")}function fe(h){let g=ke(h);return g?g.healthy===!1?"Vector index unreadable - rebuild required":"Chunks: "+g.chunk_count+" | "+g.model+" | "+g.mode:"Status unavailable"}function ae(h,g){if(Q)return Q;if(g&&g.python_path&&g.python_path.trim()){let s=g.python_path.trim();if(ue.existsSync(s))return Q={path:s,source:"manual",extraArgs:[]},Q}let e=[M.join(h,".paperforge-test-venv","Scripts","python.exe"),M.join(h,".venv","Scripts","python.exe"),M.join(h,"venv","Scripts","python.exe")];for(let s=0;s<e.length;s++)if(ue.existsSync(e[s]))return Q={path:e[s],source:"auto-detected",extraArgs:[]},Q;let t=[{path:"py",extraArgs:["-3"]},{path:"python",extraArgs:[]},{path:"python3",extraArgs:[]}];for(let s=0;s<t.length;s++)try{let n=t[s],a=(0,at.execFileSync)(n.path,n.extraArgs.concat(["--version"]),{encoding:"utf-8",timeout:5e3,windowsHide:!0});if(a&&a.toLowerCase().indexOf("python")!==-1)return Q={path:n.path,source:"auto-detected",extraArgs:n.extraArgs},Q}catch(n){}return Q={path:"python",source:"auto-detected",extraArgs:[]},Q}function He(h,g,e){return!h||typeof h!="object"||!Object.prototype.hasOwnProperty.call(h,g)?!!e:!!h[g]}function ot(h,g,e){let t=!He(h,g,e);return h&&typeof h=="object"&&(h[g]=t),t}var Z=require("obsidian"),U=j(require("fs")),ct=j(require("path")),pt=j(require("https")),Pe=require("child_process");function lt(h,g){return!g||!g.trim()?{blocked:!0,reason:"zotero"}:h?{blocked:!1}:{blocked:!0,reason:"ocr"}}var Ve=class extends Z.Modal{constructor(e,t,s,n){super(e);this._rowEls=[];this.orphans=t.map((a,r)=>({...a,_selected:!0,_idx:r})),this.vaultPath=s,this.py=n}_updateUI(){let e=this.orphans.filter(t=>t._selected);this._countEl.setText(i("orphan_delete_selected").replace("{count}",String(e.length))),this._selectAllBtn.setText(e.length===this.orphans.length?i("orphan_deselect_all"):i("orphan_select_all"));for(let t of this.orphans){let s=this._rowEls[t._idx];s&&s.toggleClass("paperforge-orphan-dimmed",!t._selected)}}onOpen(){let{contentEl:e}=this;e.addClass("paperforge-modal"),e.createEl("h2",{text:i("orphan_title").replace("{count}",String(this.orphans.length))}),e.createEl("p",{cls:"paperforge-modal-desc",text:i("orphan_desc")}),this._rowEls=[];let t=e.createEl("div",{cls:"paperforge-orphan-list"});for(let n of this.orphans){let a=t.createEl("div",{cls:"paperforge-orphan-row"+(n._selected?"":" paperforge-orphan-dimmed")});this._rowEls.push(a);let r=a.createEl("div",{cls:"paperforge-orphan-info"}),o=r.createEl("div",{cls:"paperforge-orphan-header"});o.createEl("span",{cls:"paperforge-orphan-key",text:n.citation_key||n.key});let l=o.createEl("span",{cls:"paperforge-orphan-tags"});l.createEl("span",{cls:"paperforge-tag "+(n.has_pdf?"tag-pdf":"tag-nopdf"),text:n.has_pdf?"PDF":"no PDF"}),n.collection_path&&l.createEl("span",{cls:"paperforge-tag tag-collection",text:n.collection_path}),n.title&&r.createEl("div",{cls:"paperforge-orphan-title",text:n.title});let c=[];n.authors&&c.push(n.authors),n.year&&c.push(n.year),c.length>0&&r.createEl("div",{cls:"paperforge-orphan-meta",text:c.join(" \xB7 ")}),r.createEl("div",{cls:"paperforge-orphan-explain",text:i("orphan_explain")}),a.addEventListener("click",()=>{n._selected=!n._selected,this._updateUI()})}let s=e.createEl("div",{cls:"paperforge-modal-actions"});this._selectAllBtn=s.createEl("button",{cls:"paperforge-step-btn",text:"Deselect all"}),this._selectAllBtn.addEventListener("click",()=>{let n=this.orphans.every(a=>a._selected);for(let a of this.orphans)a._selected=!n;this._updateUI()}),this._countEl=s.createEl("button",{cls:"paperforge-step-btn mod-cta",text:"Delete "+this.orphans.length+" selected"}),s.createEl("button",{cls:"paperforge-step-btn",text:"Keep all"}).addEventListener("click",()=>this.close()),this._countEl.addEventListener("click",()=>{let n=this.orphans.filter(r=>r._selected);if(n.length===0){new Z.Notice(i("orphan_none_selected"));return}if(this._countEl.setText("Deleting..."),this._countEl.setAttr("disabled",""),this._selectAllBtn.setAttr("disabled",""),!this.py||!this.py.path){new Z.Notice("PaperForge: Python not found"),this.close();return}let a=n.map(r=>r.key);(0,Pe.execFile)(this.py.path,[...this.py.extraArgs,"-m","paperforge","--vault",this.vaultPath,"prune","--force","--json",...a],{cwd:this.vaultPath,timeout:6e4},(r,o)=>{if(r){new Z.Notice("PaperForge: prune failed"),this.close();return}try{let l=JSON.parse(o),c=l.data&&l.data.deleted||[];new Z.Notice("Deleted "+c.length+" orphan workspace(s)")}catch(l){new Z.Notice("PaperForge: prune done")}this.close()})})}onClose(){this.contentEl.empty()}};function Te(h,g,e){console.log("[PF] checkOrphanState called");try{let s=ee(e).orphanStatePath;if(!U.existsSync(s)){console.log("[PF] orphan file NOT FOUND");return}console.log("[PF] orphan file FOUND");let n=U.readFileSync(s,"utf-8"),r=JSON.parse(n).orphans||[];if(console.log("[PF] orphans count:",r.length),r.length===0)return;let o=ae(e,g.settings);console.log("[PF] py.path:",o?o.path:"null"),new Ve(h,r,e,o).open(),U.unlinkSync(s),console.log("[PF] orphan file cleaned")}catch(t){console.log("[PF] checkOrphanState exception:",t.message||t)}}var we=class extends Z.Modal{constructor(e,t){super(e);this._pendingSave=null;this._showSkipConfirm=!1;this.plugin=t,this._step=1}onOpen(){this._render()}onClose(){this.contentEl.empty()}_render(){let{contentEl:e}=this;e.empty(),e.addClass("paperforge-modal"),this._renderStepIndicator(),this._renderStepContent(),this._renderNavigation()}_renderStepIndicator(){let e=[i("wizard_step1"),i("wizard_step2"),i("wizard_step3"),i("wizard_step4"),i("wizard_step5")],t=this.contentEl.createEl("div",{cls:"paperforge-step-bar"});e.forEach((s,n)=>{let a=n+1,r=t.createEl("div",{cls:`paperforge-step-dot ${a===this._step?"active":""} ${a<this._step?"done":""}`});r.createEl("span",{cls:"paperforge-step-num",text:`${a}`}),r.createEl("span",{cls:"paperforge-step-label",text:s})})}_renderStepContent(){let e=this.contentEl.createEl("div",{cls:"paperforge-step-content"});switch(this._step){case 1:this._stepOverview(e);break;case 2:this._stepDirectories(e);break;case 3:this._stepKeys(e);break;case 4:this._stepInstall(e);break;case 5:this._stepComplete(e);break}}_renderNavigation(){let e=this.contentEl.createEl("div",{cls:"paperforge-step-nav"});this._step>1&&e.createEl("button",{cls:"paperforge-step-btn",text:i("nav_prev")}).addEventListener("click",()=>{this._step--,this._showSkipConfirm=!1,this._render()}),this._step<5?e.createEl("button",{cls:"paperforge-step-btn mod-cta",text:i("nav_next")}).addEventListener("click",()=>{if(this._step===3){let s=this._validateStep3();if(s.blocked){if(s.reason==="zotero")return;if(s.reason==="ocr"){this._showSkipConfirm=!0,this._render();return}}}this._step++,this._showSkipConfirm=!1,this._render()}):e.createEl("button",{cls:"paperforge-step-btn",text:i("nav_close")}).addEventListener("click",()=>this.close())}_validateStep3(){let e=this.plugin.settings,t=lt(this._apiKeyValidated,e.zotero_data_dir);if(t.reason==="ocr")return t;let s=(e.zotero_data_dir||"").trim();if(!s)return new Z.Notice("Zotero \u6570\u636E\u76EE\u5F55\u4E3A\u5FC5\u586B\u9879\uFF0C\u8BF7\u586B\u5199\u8DEF\u5F84"),{blocked:!0,reason:"zotero"};if(!U.existsSync(s))return new Z.Notice("Zotero \u6570\u636E\u76EE\u5F55\u8DEF\u5F84\u4E0D\u5B58\u5728"),{blocked:!0,reason:"zotero"};if(!U.statSync(s).isDirectory())return new Z.Notice("Zotero \u6570\u636E\u76EE\u5F55\u8DEF\u5F84\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55"),{blocked:!0,reason:"zotero"};let n=ct.join(s,"storage");return!U.existsSync(n)||!U.statSync(n).isDirectory()?(new Z.Notice("Zotero \u6570\u636E\u76EE\u5F55\u4E2D\u672A\u627E\u5230 storage/ \u5B50\u76EE\u5F55"),{blocked:!0,reason:"zotero"}):{blocked:!1}}_stepOverview(e){e.createEl("h2",{text:i("wizard_title")}),e.createEl("p",{text:i("wizard_intro")});let t=this.plugin.settings,s=this.app.vault.adapter.basePath,n=e.createEl("div",{cls:"paperforge-dir-tree"}),a=n.createEl("div",{cls:"paperforge-dir-node root"});a.textContent=`\u{1F4C1} Vault (${s})`;let r=n.createEl("div",{cls:"paperforge-dir-children"}),o=r.createEl("div",{cls:"paperforge-dir-node folder"});o.textContent=`\u{1F4C1} ${t.resources_dir||"Resources"}/ \u2014 \u6587\u732E\u5361\u7247\u76EE\u5F55\uFF08Base \u6570\u636E\u6765\u6E90\uFF09`,o.createEl("div",{cls:"paperforge-dir-children"}).createEl("div",{cls:"paperforge-dir-node file",text:`\u{1F4C1} ${t.literature_dir||"Literature"}/ \u2014 \u6587\u732E\u5361\u7247`}),r.createEl("div",{cls:"paperforge-dir-node folder",text:`\u{1F4C1} ${t.base_dir||"Bases"}/ \u2014 \u6570\u636E\u7BA1\u7406\u9762\u677F`}),r.createEl("div",{cls:"paperforge-dir-node folder",text:`\u{1F4C1} ${t.system_dir||"System"}/ \u2014 Zotero \u8F6F\u94FE\u63A5 + PaperForge \u7CFB\u7EDF\u6587\u4EF6\u5939`}),e.createEl("p",{text:i("wizard_preview"),cls:"paperforge-modal-hint"}),e.createEl("p",{text:i("wizard_safety"),cls:"paperforge-modal-hint"});let c=e.createEl("div",{cls:"paperforge-summary"}),p=[{label:i("dir_resources"),val:`${s}/${t.resources_dir||"Resources"}`},{label:i("dir_notes"),val:`${s}/${t.resources_dir||"Resources"}/${t.literature_dir||"Literature"}`},{label:i("dir_base"),val:`${s}/${t.base_dir||"Bases"}`},{label:i("dir_system"),val:`${s}/${t.system_dir||"System"}`}];for(let u of p){let d=c.createEl("div",{cls:"paperforge-summary-row"});d.createEl("span",{cls:"paperforge-summary-label",text:u.label}),d.createEl("span",{cls:"paperforge-summary-value",text:u.val})}}_stepDirectories(e){e.createEl("h2",{text:i("wizard_step2")}),e.createEl("p",{text:i("wizard_intro")});let t=this.plugin.settings,s=this.app.vault.adapter.basePath;this._modalField(e,i("dir_vault"),s,!0),e.createEl("p",{text:i("wizard_dir_hint"),cls:"paperforge-modal-hint"}),this._modalInput(e,"\u8D44\u6E90\u76EE\u5F55\uFF08\u521B\u5EFA\u6587\u732E\u5361\u7247\u76EE\u5F55\u7684\u5730\u65B9\uFF09","resources_dir",t.resources_dir,"Resources"),e.createEl("p",{text:i("wizard_dir_sub_hint"),cls:"paperforge-modal-hint"}),this._modalInput(e,"\u6587\u732E\u5361\u7247\u76EE\u5F55\uFF08\u5B58\u653E\u6587\u732E\u5361\u7247\u7684\u5730\u65B9\uFF0CBase \u6570\u636E\u6765\u6E90\uFF09","literature_dir",t.literature_dir,"Literature"),e.createEl("p",{text:i("wizard_sys_hint"),cls:"paperforge-modal-hint"}),this._modalInput(e,"\u7CFB\u7EDF\u76EE\u5F55\uFF08\u5B58\u653E Zotero \u8F6F\u94FE\u63A5\u548C PaperForge \u7CFB\u7EDF\u6587\u4EF6\uFF09","system_dir",t.system_dir,"System"),this._modalInput(e,"Base \u76EE\u5F55\uFF08\u5B58\u653E\u6570\u636E\u7BA1\u7406\u9762\u677F\u7684\u5730\u65B9\uFF09","base_dir",t.base_dir,"Bases"),e.createEl("p",{text:i("wizard_safety"),cls:"paperforge-modal-hint"});let n=e.createEl("div",{cls:"paperforge-summary"}),a=[{label:i("dir_resources"),val:`${s}/${t.resources_dir||""}`},{label:i("dir_notes"),val:`${s}/${t.resources_dir||""}/${t.literature_dir||""}`},{label:i("dir_system"),val:`${s}/${t.system_dir||""}`},{label:i("dir_base"),val:`${s}/${t.base_dir||""}`}];for(let r of a){let o=n.createEl("div",{cls:"paperforge-summary-row"});o.createEl("span",{cls:"paperforge-summary-label",text:r.label}),o.createEl("span",{cls:"paperforge-summary-value",text:r.val})}}_stepKeys(e){if(e.createEl("h2",{text:i("wizard_step3")}),this._showSkipConfirm){this._renderSkipConfirm(e);return}let t=this.plugin.settings;e.createEl("p",{text:i("wizard_agent_hint"),cls:"paperforge-modal-hint"});let s=[{key:"opencode",name:"OpenCode"},{key:"claude",name:"Claude Code"},{key:"cursor",name:"Cursor"},{key:"github_copilot",name:"GitHub Copilot"},{key:"windsurf",name:"Windsurf"},{key:"codex",name:"Codex"},{key:"cline",name:"Cline"}],n=e.createEl("div",{cls:"paperforge-modal-field"});n.createEl("label",{cls:"paperforge-modal-label",text:i("label_agent")});let a=n.createEl("select",{cls:"paperforge-modal-select"});for(let u of s){let d=a.createEl("option",{text:u.name,attr:{value:u.key}});u.key===(t.agent_platform||"opencode")&&(d.selected=!0)}a.addEventListener("change",()=>{t.agent_platform=a.value,this._pendingSave&&clearTimeout(this._pendingSave),this._pendingSave=setTimeout(()=>{this.plugin.saveSettings(),this._pendingSave=null},500)}),e.createEl("p",{text:i("wizard_keys_hint"),cls:"paperforge-modal-hint"});let r=e.createEl("div",{cls:"paperforge-modal-field"});r.createEl("label",{cls:"paperforge-modal-label",text:i("field_paddleocr")});let o=r.createEl("input",{cls:"paperforge-modal-input",attr:{type:"password",placeholder:"API Key"}});o.value=t.paddleocr_api_key||"",this._apiKeyValidated=!1,this._apiKeyStatus=r.createEl("span",{cls:"paperforge-apikey-status",text:""});let l=r.createEl("button",{cls:"paperforge-step-btn",text:"\u9A8C\u8BC1"});l.addEventListener("click",()=>this._validateApiKey(o.value,l)),o.addEventListener("input",()=>{t.paddleocr_api_key=o.value,this._apiKeyValidated=!1,this._apiKeyStatus.textContent="",this._apiKeyStatus.className="paperforge-apikey-status"}),this._pendingSave&&clearTimeout(this._pendingSave),this._pendingSave=setTimeout(()=>{this.plugin.saveSettings(),this._pendingSave=null},500),e.createEl("p",{text:i("wizard_api_hint_skip"),cls:"paperforge-modal-hint"});let c=e.createEl("div",{cls:"paperforge-modal-field"});c.createEl("label",{cls:"paperforge-modal-label",text:i("field_zotero_data")});let p=c.createEl("input",{cls:"paperforge-modal-input",attr:{type:"text",placeholder:i("field_zotero_placeholder")}});p.value=t.zotero_data_dir||"",p.addEventListener("input",()=>{t.zotero_data_dir=p.value,this._pendingSave&&clearTimeout(this._pendingSave),this._pendingSave=setTimeout(()=>{this.plugin.saveSettings(),this._pendingSave=null},500)})}_validateApiKey(e,t){if(!e||e.length<10){this._apiKeyStatus.textContent="\u5BC6\u94A5\u683C\u5F0F\u4E0D\u6B63\u786E\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002",this._apiKeyStatus.className="paperforge-apikey-status error";return}t.disabled=!0,t.textContent="\u9A8C\u8BC1\u4E2D\u2026",this._apiKeyStatus.textContent="\u6B63\u5728\u9A8C\u8BC1\u2026",this._apiKeyStatus.className="paperforge-apikey-status";let s=JSON.stringify({model:"PaddleOCR-VL-1.5"}),n={hostname:"paddleocr.aistudio-app.com",path:"/api/v2/ocr/jobs",method:"POST",headers:{Authorization:"bearer "+e,"Content-Type":"application/json","Content-Length":Buffer.byteLength(s)},timeout:1e4},a=pt.request(n,r=>{t.disabled=!1,t.textContent="\u9A8C\u8BC1";let o="";r.on("data",l=>o+=l),r.on("end",()=>{try{let l=JSON.parse(o);r.statusCode===400&&l.code===10001?(this._apiKeyStatus.textContent="\u2713 \u5BC6\u94A5\u6709\u6548",this._apiKeyStatus.className="paperforge-apikey-status ok",this._apiKeyValidated=!0):r.statusCode===401?(this._apiKeyStatus.textContent="\u9A8C\u8BC1\u5931\u8D25\uFF1A\u5BC6\u94A5\u65E0\u6548\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002",this._apiKeyStatus.className="paperforge-apikey-status error",this._apiKeyValidated=!1):(this._apiKeyStatus.textContent="\u9A8C\u8BC1\u5931\u8D25\uFF1AAPI \u8FD4\u56DE "+r.statusCode+"\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002",this._apiKeyStatus.className="paperforge-apikey-status error",this._apiKeyValidated=!1)}catch(l){this._apiKeyStatus.textContent="\u9A8C\u8BC1\u5931\u8D25\uFF1A\u65E0\u6CD5\u89E3\u6790\u54CD\u5E94\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002",this._apiKeyStatus.className="paperforge-apikey-status error",this._apiKeyValidated=!1}})});a.on("error",r=>{t.disabled=!1,t.textContent="\u9A8C\u8BC1",this._apiKeyStatus.textContent="\u9A8C\u8BC1\u5931\u8D25\uFF1A\u65E0\u6CD5\u8FDE\u63A5 ("+r.message+")\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002",this._apiKeyStatus.className="paperforge-apikey-status error",this._apiKeyValidated=!1}),a.write(s),a.end()}_renderSkipConfirm(e){e.createEl("p",{text:i("wizard_skip_ocr_desc"),cls:"paperforge-modal-desc"});let t=e.createEl("div",{cls:"paperforge-modal-actions"});t.createEl("button",{cls:"paperforge-step-btn mod-cta",text:i("wizard_skip_ocr_continue")}).addEventListener("click",()=>{this._showSkipConfirm=!1,this._step++,this._render()}),t.createEl("button",{cls:"paperforge-step-btn",text:i("wizard_skip_ocr_back")}).addEventListener("click",()=>{this._showSkipConfirm=!1,this._render()})}_modalField(e,t,s,n){let a=e.createEl("div",{cls:"paperforge-modal-field"});a.createEl("label",{cls:"paperforge-modal-label",text:t});let r=a.createEl("input",{cls:"paperforge-modal-input",attr:{type:"text"}});r.value=s,r.disabled=!!n}_modalInput(e,t,s,n,a){let r=e.createEl("div",{cls:"paperforge-modal-field"});r.createEl("label",{cls:"paperforge-modal-label",text:t});let o=r.createEl("input",{cls:"paperforge-modal-input",attr:{type:"text",placeholder:a||""}});o.value=n;let l=this.plugin.settings;o.addEventListener("input",()=>{l[s]=o.value,this._pendingSave&&clearTimeout(this._pendingSave),this._pendingSave=setTimeout(()=>{this.plugin.saveSettings(),this._pendingSave=null},500)})}_modalSecret(e,t,s,n,a){let r=e.createEl("div",{cls:"paperforge-modal-field"});r.createEl("label",{cls:"paperforge-modal-label",text:t});let o=r.createEl("input",{cls:"paperforge-modal-input",attr:{type:"password",placeholder:a||""}});o.value=n;let l=this.plugin.settings;o.addEventListener("input",()=>{l[s]=o.value,this._pendingSave&&clearTimeout(this._pendingSave),this._pendingSave=setTimeout(()=>{this.plugin.saveSettings(),this._pendingSave=null},500)})}_stepInstall(e){e.createEl("h2",{text:i("wizard_step4")}),this._installLog=e.createEl("div",{cls:"paperforge-install-log"});let t=e.createEl("button",{cls:"paperforge-step-btn mod-cta",text:i("install_btn")});t.addEventListener("click",()=>this._runInstall(t))}async _runInstall(e){var r,o,l,c,p,u;e.disabled=!0,e.textContent=i("install_btn_running"),this._installLog.setText(i("install_validating")+` -`),this._log(i("install_validating"));let t=this.plugin.settings,s=this._validate();if(s.length>0){this._log(i("validate_fail")+":"),s.forEach(d=>this._log(" \u2717 "+d)),e.disabled=!1,e.textContent=i("install_btn_retry");return}let n=(d,_={})=>new Promise((y,x)=>{let{path:f,extraArgs:E=[]}=A(t.vault_path.trim(),this.plugin.settings,void 0,void 0),S=(0,Pe.spawn)(f,[...E,...d],{cwd:t.vault_path.trim(),env:de(),timeout:12e4,..._}),b="",m="";S.stdout.on("data",v=>{let k=v.toString("utf-8");b+=k,_.logStdout&&this._processSetupOutput(k)}),S.stderr.on("data",v=>{let k=v.toString("utf-8");m+=k,this._log("[stderr] "+k.trim())}),S.on("close",v=>{v===0?y({stdout:b,stderr:m}):x(new Error(m.trim()||b.trim()||`exit code ${v}`))}),S.on("error",v=>x(v))}),a=["-m","paperforge","--vault",t.vault_path.trim(),"setup","--headless","--system-dir",t.system_dir.trim(),"--resources-dir",t.resources_dir.trim(),"--literature-dir",t.literature_dir.trim(),"--base-dir",t.base_dir.trim(),"--agent",t.agent_platform||"opencode"];t.zotero_data_dir&&t.zotero_data_dir.trim()&&a.push("--zotero-data",t.zotero_data_dir.trim()),t.paddleocr_api_key&&t.paddleocr_api_key.trim()&&a.push("--paddleocr-key",t.paddleocr_api_key.trim());try{let d=!0;try{await n(["-c","import paperforge"])}catch(_){d=!1}if(!d){this._log(i("install_bootstrapping"));let _=this.plugin.manifest.version;this._log(`[install] Trying PyPI: pip install paperforge==${_}`);let y=["-m","pip","install","--upgrade"];process.platform!=="win32"&&y.push("--user"),y.push(`paperforge==${_}`);try{await n(y,{logStdout:!0})}catch(x){this._log(`[install] PyPI failed, falling back to git: git+https://...@v${_}`),console.warn("[PaperForge] PyPI install failed, falling back to git:",(r=x.message)==null?void 0:r.slice(0,200));let f=["-m","pip","install","--upgrade"];process.platform!=="win32"&&f.push("--user"),f.push(`git+https://github.com/LLLin000/PaperForge.git@v${_}`),await n(f,{logStdout:!0})}}await n(a,{logStdout:!0,env:de()}),this._log(i("install_complete")),t.setup_complete=!0,await this.plugin.saveSettings(),setTimeout(()=>{this._step=5,this._render()},800)}catch(d){console.error("PaperForge setup failed:",d.message);let _=this._formatSetupError(d.message);this._log(i("install_failed")+_);let y=(o=this._installLog.parentElement)==null?void 0:o.createEl("button",{cls:"paperforge-copy-diag-btn",text:i("error_copy_diagnostic")||"Copy diagnostic"});if(y){let x=d.message,f=((c=(l=this.plugin)==null?void 0:l.settings)==null?void 0:c.python_path)||"auto",E=((u=(p=this.plugin)==null?void 0:p.manifest)==null?void 0:u.version)||"?",S=process.platform+" "+process.arch,b,m;try{b=Ne()||"(not found)"}catch(F){b="(error)"}try{m=A(t.vault_path.trim(),this.plugin.settings,void 0,void 0)}catch(F){m=null}let v=(process.env.PATH||"").length,k=(process.env.PATH||"").toLowerCase().includes("git"),C=["[PaperForge Diagnostic]","Category: "+_,"Plugin version: "+E,"Python: "+f,"Resolved Python: "+((m==null?void 0:m.path)||"?"),"OS: "+S,"Vault path: "+(t.vault_path||"?"),"--- Git ---","Git dir (resolved): "+b,"PATH length: "+v+" chars","PATH contains git: "+k,"--- Raw error ---",x.slice(0,2e3)].join(` -`);y.addEventListener("click",()=>{navigator.clipboard.writeText(C).then(()=>{y.setText(i("error_copied")||"Copied!"),setTimeout(()=>{y.setText(i("error_copy_diagnostic")||"Copy diagnostic")},3e3)}).catch(()=>{new Z.Notice("[!!] Clipboard write failed",6e3)})})}e.disabled=!1,e.textContent=i("install_btn_retry")}}_log(e){this._installLog&&this._installLog.setText(this._installLog.textContent+e+` -`)}_validate(){let e=[],t=this.plugin.settings;return(!t.vault_path||!t.vault_path.trim())&&e.push(i("validate_vault")),(!t.resources_dir||!t.resources_dir.trim())&&e.push(i("validate_resources")),(!t.literature_dir||!t.literature_dir.trim())&&e.push(i("validate_notes")),(!t.base_dir||!t.base_dir.trim())&&e.push(i("validate_base")),(!t.paddleocr_api_key||!t.paddleocr_api_key.trim())&&this._log(" ! "+i("validate_key")+" "+i("optional_later")),(!t.zotero_data_dir||!t.zotero_data_dir.trim())&&this._log(" ! "+i("validate_zotero")+" "+i("optional_later")),e}_processSetupOutput(e){let t=e.split(` +"use strict";var mt=Object.create;var Fe=Object.defineProperty;var yt=Object.getOwnPropertyDescriptor;var bt=Object.getOwnPropertyNames;var vt=Object.getPrototypeOf,xt=Object.prototype.hasOwnProperty;var Et=(g,u)=>()=>(u||g((u={exports:{}}).exports,u),u.exports),kt=(g,u)=>{for(var e in u)Fe(g,e,{get:u[e],enumerable:!0})},Ye=(g,u,e,t)=>{if(u&&typeof u=="object"||typeof u=="function")for(let s of bt(u))!xt.call(g,s)&&s!==e&&Fe(g,s,{get:()=>u[s],enumerable:!(t=yt(u,s))||t.enumerable});return g};var z=(g,u,e)=>(e=g!=null?mt(vt(g)):{},Ye(u||!g||!g.__esModule?Fe(e,"default",{value:g,enumerable:!0}):e,g)),wt=g=>Ye(Fe({},"__esModule",{value:!0}),g);var Ne=Et((At,St)=>{St.exports={versions:[{version:"1.5.15",date:"2026-06-01",title:"OCR \u91CD\u65B0\u8BBE\u8BA1 \u2014 \u5168\u6587\u5355\u4E00\u771F\u76F8\u6E90 + Redo \u95ED\u73AF + \u9605\u8BFB\u987A\u5E8F\u4FEE\u590D",breaking_or_migration:["fulltext \u6B63\u6587\u73B0\u5728\u7EDF\u4E00\u4F4D\u4E8E System/PaperForge/ocr/<key>/fulltext.md\uFF0C\u5DE5\u4F5C\u533A Resources/.../fulltext.md \u4E0D\u518D\u4FDD\u7559\u4E3A\u6B63\u6587\u526F\u672C","OCR Redo \u73B0\u5728\u4F1A\u7ACB\u5373\u91CD\u8DD1 OCR\uFF08\u95ED\u73AF\u6267\u884C\uFF09\uFF0C\u4E0D\u518D\u53EA\u662F\u6807\u8BB0\u72B6\u6001\u540E\u9700\u8981\u624B\u52A8\u518D\u8DD1\u4E00\u6B21"],new_features:["\u65B0\u589E OCR Redo \u95ED\u73AF\u6267\u884C\uFF1A\u52FE\u9009\u91CD\u505A\u540E\u81EA\u52A8\u5220\u9664\u65E7\u4EA7\u7269 \u2192 \u5F3A\u5236 do_ocr \u2192 \u7ACB\u5373\u91CD\u8DD1 OCR \u2192 \u6210\u529F\u540E\u5199\u56DE ocr_redo:false","\u8BBE\u7F6E\u9875\u65B0\u589E\u300C\u66F4\u65B0\u4E0E\u624B\u518C\u300D\u6807\u7B7E\u9875\uFF0C\u53EF\u968F\u65F6\u67E5\u770B\u7248\u672C\u66F4\u65B0\u8BB0\u5F55\u4E0E\u4F7F\u7528\u624B\u518C\u94FE\u63A5","\u9996\u6B21\u66F4\u65B0\u65F6\u81EA\u52A8\u5F39\u51FA\u66F4\u65B0\u8BF4\u660E"],fixes:["\u4FEE\u590D OCR \u53CC\u680F\u9605\u8BFB\u987A\u5E8F\u6DF7\u4E71\uFF1A2.5 / Discussion / Introduction \u7B49\u7AE0\u8282\u6807\u9898\u548C\u6B63\u6587\u9519\u4F4D","\u4FEE\u590D Kwon et al. \u7B49\u53D9\u8FF0\u53E5\u88AB\u8BEF\u5224\u4E3A\u53C2\u8003\u6587\u732E\uFF0C\u5BFC\u81F4\u6B63\u6587\u6BB5\u843D\u88AB\u63D0\u524D\u622A\u65AD\u5E76\u63D2\u5165 Discussion","\u4FEE\u590D Fig.3 / Fig.4 \u7B49\u9875\u9996\u56FE\u6CE8\u548C\u56FE\u5757\u88AB\u91CD\u6392\u903B\u8F91\u6574\u4F53\u540E\u79FB\uFF0C\u5BFC\u81F4\u56FE\u6587\u65AD\u88C2","\u4FEE\u590D\u9996\u9875\uFF08page 1\uFF09Abstract / Introduction \u6392\u5E8F\u9519\u4E71\uFF0CMETHODS \u884C\u8DD1\u5230 Introduction \u540E","\u4FEE\u590D\u5E76\u6392 panel\uFF08\u5982 Fig.2 \u7684\u5DE6\u53F3\u4E24\u5757\uFF09\u672A\u5408\u5E76\u4E3A\u5355\u5F20 composite figure\uFF0C\u88AB\u62C6\u6210\u4E24\u5F20\u72EC\u7ACB\u56FE\u7247","\u4FEE\u590D redo OCR \u540E\u5DE5\u4F5C\u533A fulltext \u672A\u88AB\u540C\u6B65\u8986\u76D6\uFF0C\u5BFC\u81F4\u7528\u6237\u770B\u5230\u7684\u4ECD\u662F\u65E7\u4E71\u5E8F\u7248\u672C","Dashboard \u73B0\u5728\u53EF\u4EE5\u8BC6\u522B System/PaperForge/ocr/<key>/fulltext.md \u8DEF\u5F84\uFF0C\u6253\u5F00\u5168\u6587\u540E\u80FD\u5207\u5230\u5BF9\u5E94 paper \u89C6\u56FE"],recommended_actions:["\u5982\u679C\u8BBA\u6587\u662F\u6B64\u7248\u672C\u524D\u5B8C\u6210\u7684 OCR\uFF0C\u5EFA\u8BAE\u5BF9\u91CD\u8981\u8BBA\u6587\u6267\u884C\u4E00\u6B21\u300C\u91CD\u505AOCR\u300D\uFF08\u5728 Base \u89C6\u56FE\u52FE\u9009 ocr_redo \u540E\u70B9 ribbon Redo \u6309\u94AE\uFF09\uFF0C\u4EE5\u4FEE\u590D\u9605\u8BFB\u987A\u5E8F\u95EE\u9898","\u6253\u5F00\u5168\u6587\u8BF7\u76F4\u63A5\u4F7F\u7528 Dashboard \u7684\u300C\u6253\u5F00\u5168\u6587\u300D\u6309\u94AE\uFF0C\u6B63\u6587\u5DF2\u7ECF\u7EDF\u4E00\u8FC1\u81F3 System/PaperForge/ocr/ \u4E0B"]}]}});var Tt={};kt(Tt,{default:()=>Ae});module.exports=wt(Tt);var M=require("obsidian"),L=z(require("fs")),_e=z(require("path")),se=require("child_process");var ce="paperforge-status",xe="paperforge",Qe='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path><line x1="8" y1="7" x2="16" y2="7"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>',H=[{id:"paperforge-sync",title:"Sync Library",desc:"Pull new references from Zotero and generate literature notes",icon:"\u21BB",cmd:"sync",okMsg:"Sync complete"},{id:"paperforge-ocr",title:"Run OCR",desc:"Extract full text and figures from PDFs via PaddleOCR",icon:"\u229E",cmd:"ocr",okMsg:"OCR started"},{id:"paperforge-doctor",title:"Run Doctor",desc:"Verify PaperForge setup \u2014 check configs, Zotero, paths, and index health",icon:"\u2695",cmd:"doctor",okMsg:"Doctor complete"},{id:"paperforge-repair",title:"Repair Issues",desc:"Fix three-way state divergence, path errors, and rebuild index",icon:"\u21BA",cmd:"repair",args:["--fix","--fix-paths"],okMsg:"Repair complete"},{id:"paperforge-ocr-redo",title:"Redo OCR",desc:"Re-run OCR for papers marked ocr_redo: true",icon:"\u21BA",cmd:"ocr",args:["redo"],okMsg:"OCR redo started"}],Ee={vault_path:"",setup_complete:!1,auto_update:!0,auto_update_on_startup:!0,agent_platform:"opencode",language:"",paddleocr_api_key:"",zotero_data_dir:"",python_path:"",features:{memory_layer:!0,vector_db:!1},selected_skill_platform:"opencode",vector_db_api_key:"",vector_db_api_base:"",vector_db_api_model:"text-embedding-3-small",frozen_skills:{},system_dir:"",resources_dir:"",literature_dir:"",base_dir:"",last_seen_version:""};function et(g,u){if(!u||!u.note_path)return u;let e=g.vault.getAbstractFileByPath(u.note_path);if(!e)return u;let t=g.metadataCache.getFileCache(e),s=t&&t.frontmatter;if(!s)return u;let n={...u};for(let a of["do_ocr","analyze","ocr_status","ocr_redo","deep_reading_status"])Object.prototype.hasOwnProperty.call(s,a)&&(n[a]=s[a]);return n}function Ie(g,u){return g&&{...g,...u}}var Le={en:{action_running:"Running ",api_key_missing:"Missing",api_key_set:"Entered",btn_install:"Open Setup Wizard",btn_install_desc:"Check whether the environment is ready, then open the step-by-step setup wizard",btn_reconfig:"Reconfigure",btn_reconfig_desc:"Open the setup wizard again to change directories, platform, or API keys",btn_validate:"Validate",check_bbt_fail:"Not detected",check_bbt_ok:"Installed",check_python_fail:"Not found",check_python_ok:"Ready",check_zotero_fail:"Not detected",check_zotero_ok:"Found",complete_export_path:"Save Better BibTeX JSON exports into:",complete_next:"Recommended next steps",complete_step1:"Open Dashboard",complete_step1_desc:'Press Ctrl+P and run "PaperForge: Open Main Panel", or click the PaperForge icon in the left sidebar.',complete_step2:"Sync Literature",complete_step2_desc:"In the main panel, click Sync Library to bring papers from Zotero into Obsidian and generate notes.",complete_step3:"Run OCR",complete_step3_desc:"In the Obsidian Base view, mark do_ocr:true on papers, then run OCR in the main panel.",complete_step4:"Configure Better BibTeX Auto-export",complete_step4_desc:'In Zotero, right-click the library or collection you want to sync -> Export -> Better BibTeX JSON -> enable "Keep updated".',complete_summary:"Saved Configuration",complete_title:"Setup Complete",copied:"Copied!",copy_pf_deep_cmd:"Copy /pf-deep Command",dashboard_drift_warning:"PaperForge CLI (v{0}) differs from plugin (v{1}). Open Settings \u2192 Runtime Health to sync.",deep_reading_not_found:"Deep reading file not found",desc:"Obsidian + Zotero literature pipeline. Sync papers, generate notes, run OCR, and read deeply in one place.",dir_base:"Base Dir",dir_index:"Index Dir",dir_notes:"Notes Dir",dir_resources:"Resource Dir",dir_system:"System Dir",dir_vault:"Vault Path",error_copied:"Copied!",error_copy_diagnostic:"Copy diagnostic",feat_agent_platform:"Agent Platform",feat_agent_platform_desc:"Select which agent platform to manage skills for.",feat_api_base_url:"API Base URL",feat_api_base_url_desc:"Custom OpenAI-compatible API endpoint. Leave empty for default.",feat_api_model:"API Model",feat_api_model_desc:"Embedding model name for this endpoint.",feat_build_btn:"Build",feat_build_complete:"Vector build complete.",feat_build_failed:"Build failed. See terminal output.",feat_building:"Building...",feat_cache_remove_failed:"Failed: {0}",feat_cache_removed:"Model cache removed.",feat_checking:"Checking...",feat_checking_btn:"Checking...",feat_deps_checking:"Checking dependencies...",feat_deps_missing:"Dependencies not installed. Required: chromadb, openai.",feat_enter_key:"Enter a valid OpenAI API key.",feat_install_btn:"Install",feat_install_deps:"Install Dependencies",feat_install_deps_desc:"pip install chromadb openai (~35MB).",feat_install_done:"Dependencies installed. Building vectors...",feat_install_failed:"Install failed: ",feat_installing:"Installing...",feat_installing_pkgs:"Installing {pkgs}...",feat_key_rejected:"API key rejected.",feat_memory_desc:"The Memory Layer is the core data engine of PaperForge, powered by SQLite. It integrates literature metadata (papers, assets, aliases, reading events), provides FTS5 metadata search across titles, abstracts, authors, domains, and collections, and powers agent-context and paper-status. Always active \u2014 no toggle needed.",feat_memory_rebuild_btn:"Rebuild",feat_memory_rebuild_done:"Memory DB rebuilt.",feat_memory_rebuild_failed:"Rebuild failed.",feat_memory_rebuilding:"Rebuilding...",feat_model_changed_warn:"Model changed ({0} -> {1}). Existing vectors are incompatible \u2014 rebuild required.",feat_network_error:"Network error: ",feat_no_python:"No Python found. Check Installation tab.",feat_not_cached:"Not cached",feat_openai_key:"OpenAI API Key",feat_openai_key_desc:"Used for API embedding calls. Model is defined below.",feat_output_copied:"Output copied to clipboard.",feat_rebuild_btn:"Rebuild",feat_rebuild_vectors:"Rebuild Vectors",feat_rebuild_vectors_changed:"Model changed \u2014 rebuild to update all vectors.",feat_rebuild_vectors_desc:"Rebuild all OCR fulltext vectors. Required after model or mode change.",feat_removing:"Removing...",feat_retry_btn:"Retry",feat_skills_desc:"Manage and enable/disable agent skills installed in your vault. Each row corresponds to a SKILL.md file \u2014 toggle off to prevent the agent from auto-invoking that skill.",feat_skills_system:"System Skills ship with PaperForge and are updated alongside PaperForge.",feat_skills_user:"User Skills are custom skills you install from community or create yourself.",feat_uninstall_btn:"Uninstall",feat_valid_key:"API key valid.",feat_vector_config_label:"Vector Settings",feat_vector_corrupted:"Vector index corrupted \u2014 needs force rebuild.",feat_vector_desc:"Vector Database enables semantic search across OCR-extracted fulltext via API embedding. Documents are split into chunks, embedded via OpenAI-compatible API, and stored in ChromaDB.",feat_vector_enable:"Enable Vector Retrieval",feat_vector_enable_desc:"Semantic search across OCR fulltext. Requires: pip install openai chromadb (~35MB).",feat_vector_rebuild_force_btn:"Force Rebuild",feat_verify:"Verify",feat_verify_btn:"Verify",field_paddleocr:"PaddleOCR API Key",field_python_custom:"Custom Path",field_python_interp:"Python Interpreter",field_zotero_data:"Zotero Data Dir",field_zotero_placeholder:"Required. Path to Zotero data directory for PDF attachment resolution.",guide_ocr:"Run OCR",guide_ocr_desc:"In the main panel, click Run OCR to extract full text and figures from PDFs for later reading and analysis.",guide_open:"Open Main Panel",guide_open_desc:'Press Ctrl+P and run "PaperForge: Open Main Panel", or click the PaperForge icon in the left sidebar.',guide_sync:"Sync Literature",guide_sync_desc:"After Better BibTeX JSON export is configured, click Sync Library to import papers from Zotero into Obsidian and generate notes automatically.",header_title:"PaperForge",install_bootstrapping:"PaperForge Python package not found. Installing automatically...",install_btn:"Start Install",install_btn_retry:"Retry",install_btn_running:"Installing...",install_complete:"Installation complete!",install_failed:"Installation failed: ",install_validating:"Validating setup...",jump_to_deep_reading:"Open Deep Reading",label_agent:"Agent Platform",nav_close:"Close",nav_next:"Next",nav_prev:"Back",not_set:"Not entered",notice_check_fail:"Missing: ",notice_python_missing:"Python was not detected. Install Python 3.10+ and add it to PATH.",ocr_privacy_title:"OCR Privacy Notice",ocr_privacy_warning:"OCR will upload PDFs to the PaddleOCR API. Do not upload sensitive or confidential documents.",ocr_queue_add:"Add to OCR Queue",ocr_queue_added:"Added to OCR queue",ocr_queue_remove:"Remove from OCR Queue",ocr_queue_removed:"Removed from OCR queue",ocr_understand:"I understand, continue",optional_later:"(can be set later in Settings)",orphan_delete_failed:"Prune failed",orphan_delete_selected:"Delete {count} selected",orphan_deleted:"Deleted {count} orphan workspace(s)",orphan_desc:"These papers are no longer in your Zotero library.",orphan_deselect_all:"Deselect all",orphan_explain:"Removed from Zotero. Workspace files remain on disk.",orphan_keep_all:"Keep all",orphan_none_selected:"No papers selected for deletion",orphan_select_all:"Select all",orphan_title:"Found {count} orphan paper(s)",panel_actions:"Quick Actions",prep_bbt:"Better BibTeX",prep_bbt_desc:"In Zotero: Tools -> Add-ons -> install Better BibTeX.",prep_export:"Better BibTeX Auto-export",prep_export_desc:'In Zotero, right-click the collection you want to sync -> Export Collection -> BetterBibTeX JSON -> enable "Keep updated" -> save the JSON file into the exports folder shown below. Obsidian Base views will use the JSON filename as the Base name:',prep_export_path_label:"Save the exported JSON file into this folder:",prep_key:"PaddleOCR Key",prep_key_desc:"Get your API key from https://aistudio.baidu.com/paddleocr",prep_python:"Python 3.10+",prep_python_desc:"Python must be available from the command line. If you are not sure, click below to auto-detect.",prep_zotero:"Zotero Desktop",prep_zotero_desc:"Install Zotero from https://www.zotero.org",run_in_agent:"Run in {0}",runtime_health:"Runtime Health",runtime_health_checking:"Checking...",runtime_health_desc:"Check whether the installed paperforge Python package matches the plugin version and whether the deployed skill contract is current.",runtime_health_match:"Match",runtime_health_mismatch:"Mismatch",runtime_health_package_ver:"Python package v{0}",runtime_health_plugin_ver:"Plugin v{0}",runtime_health_sync:"Sync Runtime",runtime_health_sync_done:"Runtime synced to v{0}",runtime_health_sync_fail:"Sync failed: {0}",runtime_health_syncing:"Syncing...",section_config:"Current Configuration",section_guide:"How To Use",section_prep:"Preparation",section_prep_desc:"Before first use, finish these 4 preparation items. Better BibTeX auto-export is configured after setup:",setup_done:"PaperForge environment is ready",setup_pending:"Not installed yet. Finish the preparation items below, then open the wizard.",tab_features:"Features",tab_setup:"Installation",validate_base:"Base directory is required",validate_fail:"Please complete the required fields below",validate_index:"Index directory is required",validate_key:"PaddleOCR API key (optional, needed for OCR)",validate_notes:"Notes directory is required",validate_resources:"Resources directory is required",validate_system:"System directory is required",validate_vault:"Vault path is required",validate_zotero:"Zotero data directory (optional, needed for PDF linking)",wizard_agent_hint:"Choose the AI agent platform you use most often. PaperForge will place the matching command and skill files in the correct location.",wizard_dir_hint:"PaperForge stores user-facing literature data under the resources directory. These folders will live there:",wizard_dir_sub_hint:"Resolved folder preview based on the names below:",wizard_intro:"This wizard walks you through the full setup. In most cases, the default values are fine to keep.",wizard_keys_hint:"Enter your PaddleOCR API key below. If you want PaperForge to auto-locate Zotero PDFs, you can also fill in the Zotero data directory.",wizard_preview:"After installation, system files stay at the vault root while literature data stays under the resources directory.",wizard_safety:"Safety: if the selected folders already contain files, setup preserves existing files and only creates missing PaperForge folders and files.",wizard_step1:"Overview",wizard_step2:"Directory Setup",wizard_step3:"Platform & Keys",wizard_step4:"Install",wizard_step5:"Done",wizard_skip_ocr_desc:"OCR will not be available until you configure a valid PaddleOCR API key. You can continue setup now and configure it later in Settings.",wizard_skip_ocr_continue:"Continue without OCR key",wizard_skip_ocr_back:"Back to configure",wizard_api_hint_skip:"OCR key is optional \u2014 you may skip it and configure later.",wizard_sys_hint:"These folders live at the vault root, outside the resources directory:",wizard_title:"PaperForge Setup Wizard"},zh:{action_running:"\u6B63\u5728\u6267\u884C ",api_key_missing:"\u672A\u914D\u7F6E \u2717",api_key_set:"\u5DF2\u914D\u7F6E \u2713",btn_install:"\u6253\u5F00\u5B89\u88C5\u5411\u5BFC",btn_install_desc:"\u81EA\u52A8\u68C0\u6D4B Python + \u524D\u7F6E\u73AF\u5883\uFF0C\u901A\u8FC7\u540E\u6253\u5F00\u5206\u6B65\u5B89\u88C5\u5411\u5BFC",btn_reconfig:"\u91CD\u65B0\u914D\u7F6E",btn_reconfig_desc:"\u91CD\u65B0\u8FD0\u884C\u5B89\u88C5\u5411\u5BFC\uFF0C\u4FEE\u6539\u76EE\u5F55\u6216\u5BC6\u94A5\u914D\u7F6E",btn_validate:"\u9A8C\u8BC1",check_bbt_fail:"\u672A\u68C0\u6D4B\u5230",check_bbt_ok:"\u5DF2\u5B89\u88C5",check_python_fail:"\u672A\u5B89\u88C5",check_python_ok:"\u5DF2\u5C31\u7EEA",check_zotero_fail:"\u672A\u68C0\u6D4B\u5230",check_zotero_ok:"\u5DF2\u5B89\u88C5",complete_next:"\u4E0B\u4E00\u6B65\u64CD\u4F5C",complete_step1:"\u6253\u5F00 PaperForge Dashboard",complete_step1_desc:"Ctrl+P \u2192 \u8F93\u5165 PaperForge: Open Dashboard\uFF0C\u6216\u70B9\u5DE6\u4FA7\u4E66\u672C\u56FE\u6807",complete_step2:"\u540C\u6B65\u6587\u732E",complete_step2_desc:"Dashboard \u4E2D\u70B9 Sync Library\uFF0C\u4ECE Zotero \u62C9\u53D6\u6587\u732E\u751F\u6210\u7B14\u8BB0",complete_step3:"\u8FD0\u884C OCR",complete_step3_desc:"Dashboard \u4E2D\u70B9 Run OCR\uFF0C\u63D0\u53D6 PDF \u5168\u6587\u4E0E\u56FE\u8868",complete_step4:"\u914D\u7F6E BBT \u81EA\u52A8\u5BFC\u51FA",complete_summary:"\u5F53\u524D\u5B8C\u6574\u914D\u7F6E",complete_title:"\u2713 PaperForge \u5B89\u88C5\u5B8C\u6210",copied:"\u5DF2\u590D\u5236\uFF01",copy_pf_deep_cmd:"\u590D\u5236 /pf-deep \u547D\u4EE4",dashboard_drift_warning:'\u63D2\u4EF6\u7248\u672C\u4E0E Python \u8FD0\u884C\u65F6\u7248\u672C\u4E0D\u5339\u914D\u3002\u8BF7\u5728\u8BBE\u7F6E\u4E2D\u70B9\u51FB"\u540C\u6B65\u8FD0\u884C\u65F6"\u3002',deep_reading_not_found:"\u7CBE\u8BFB\u6587\u4EF6\u672A\u627E\u5230",desc:"Obsidian + Zotero \u6587\u732E\u7BA1\u7406\u6D41\u6C34\u7EBF\u3002\u81EA\u52A8\u540C\u6B65\u6587\u732E\u3001\u751F\u6210\u7B14\u8BB0\u3001OCR \u63D0\u53D6\u5168\u6587\uFF0C\u4E00\u7AD9\u5F0F\u6587\u732E\u7CBE\u8BFB\u5DE5\u4F5C\u6D41\u3002",dir_base:"Base \u76EE\u5F55",dir_index:"\u7D22\u5F15\u76EE\u5F55",dir_notes:"\u6B63\u6587\u76EE\u5F55",dir_resources:"\u8D44\u6E90\u76EE\u5F55",dir_system:"\u7CFB\u7EDF\u76EE\u5F55",dir_vault:"Vault \u8DEF\u5F84",error_copied:"\u5DF2\u590D\u5236\uFF01",error_copy_diagnostic:"\u590D\u5236\u8BCA\u65AD\u4FE1\u606F",feat_agent_platform:"Agent \u5E73\u53F0",feat_agent_platform_desc:"\u9009\u62E9\u8981\u7BA1\u7406\u7684 Agent \u5E73\u53F0\u3002",feat_api_base_url:"API \u5730\u5740",feat_api_base_url_desc:"\u81EA\u5B9A\u4E49 OpenAI \u517C\u5BB9 API \u7AEF\u70B9\u3002\u7559\u7A7A\u4F7F\u7528\u9ED8\u8BA4\u5730\u5740\u3002",feat_api_model:"API \u6A21\u578B",feat_api_model_desc:"\u8BE5\u7AEF\u70B9\u4F7F\u7528\u7684\u5D4C\u5165\u6A21\u578B\u540D\u79F0\u3002",feat_build_btn:"\u6784\u5EFA",feat_build_complete:"\u5411\u91CF\u6784\u5EFA\u5B8C\u6210\u3002",feat_build_failed:"\u6784\u5EFA\u5931\u8D25\u3002\u8BF7\u67E5\u770B\u7EC8\u7AEF\u8F93\u51FA\u3002",feat_building:"\u6784\u5EFA\u4E2D\u2026",feat_cache_remove_failed:"\u5931\u8D25\uFF1A{0}",feat_cache_removed:"\u6A21\u578B\u7F13\u5B58\u5DF2\u6E05\u9664\u3002",feat_checking:"\u68C0\u6D4B\u4E2D\u2026",feat_checking_btn:"\u68C0\u6D4B\u4E2D\u2026",feat_deps_checking:"\u6B63\u5728\u68C0\u6D4B\u4F9D\u8D56\u2026",feat_deps_missing:"\u4F9D\u8D56\u672A\u5B89\u88C5\u3002\u9700\u8981\uFF1Achromadb, openai\u3002",feat_enter_key:"\u8BF7\u8F93\u5165\u6709\u6548\u7684 OpenAI API Key\u3002",feat_install_btn:"\u5B89\u88C5",feat_install_deps:"\u5B89\u88C5\u4F9D\u8D56",feat_install_done:"\u4F9D\u8D56\u5DF2\u5B89\u88C5\u3002\u6B63\u5728\u6784\u5EFA\u5411\u91CF\u2026",feat_install_failed:"\u5B89\u88C5\u5931\u8D25\uFF1A",feat_installing:"\u5B89\u88C5\u4E2D\u2026",feat_installing_pkgs:"\u6B63\u5728\u5B89\u88C5 {pkgs}...",feat_key_rejected:"API Key \u88AB\u62D2\u7EDD\u3002",feat_memory_desc:"\u8BB0\u5FC6\u5C42\u662F PaperForge \u7684\u6838\u5FC3\u6570\u636E\u5F15\u64CE\uFF0C\u57FA\u4E8E SQLite \u6784\u5EFA\u3002\u5B83\u6574\u5408\u4E86\u6587\u732E\u5143\u6570\u636E\uFF08\u8BBA\u6587\u3001\u8D44\u6E90\u6587\u4EF6\u3001\u522B\u540D\u3001\u9605\u8BFB\u4E8B\u4EF6\uFF09\uFF0C\u652F\u6301 FTS5 \u5143\u6570\u636E\u68C0\u7D22\uFF08\u6807\u9898\u3001\u6458\u8981\u3001\u4F5C\u8005\u3001domain\u3001collection\uFF09\uFF0C\u5E76\u4E3A agent-context \u548C paper-status \u547D\u4EE4\u63D0\u4F9B\u6570\u636E\u652F\u6491\u3002\u59CB\u7EC8\u8FD0\u884C\uFF0C\u65E0\u9700\u624B\u52A8\u5F00\u542F\u3002",feat_memory_rebuild_btn:"\u91CD\u5EFA\u6570\u636E\u5E93",feat_memory_rebuild_done:"\u8BB0\u5FC6\u6570\u636E\u5E93\u91CD\u5EFA\u5B8C\u6210\u3002",feat_memory_rebuild_failed:"\u91CD\u5EFA\u5931\u8D25\u3002",feat_memory_rebuilding:"\u91CD\u5EFA\u4E2D\u2026",feat_model:"\u6A21\u578B",feat_model_changed_warn:"\u6A21\u578B\u5DF2\u66F4\u6362\uFF08{0} -> {1}\uFF09\u3002\u5DF2\u6709\u5411\u91CF\u4E0D\u517C\u5BB9\u2014\u2014\u9700\u8981\u91CD\u5EFA\u3002",feat_network_error:"\u7F51\u7EDC\u9519\u8BEF\uFF1A",feat_no_python:"\u672A\u627E\u5230 Python\u3002\u8BF7\u67E5\u770B\u5B89\u88C5\u6807\u7B7E\u9875\u3002",feat_not_cached:"\u672A\u7F13\u5B58",feat_openai_key:"OpenAI API Key",feat_openai_key_desc:"\u7528\u4E8E API \u5D4C\u5165\u8C03\u7528\uFF0C\u6A21\u578B\u5728\u4E0B\u65B9\u5B9A\u4E49\u3002",feat_output_copied:"\u8F93\u51FA\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F\u3002",feat_rebuild_btn:"\u91CD\u5EFA",feat_rebuild_vectors:"\u91CD\u5EFA\u5411\u91CF",feat_rebuild_vectors_changed:"\u6A21\u578B\u5DF2\u66F4\u6362 \u2014 \u9700\u8981\u91CD\u5EFA\u5411\u91CF\u3002",feat_rebuild_vectors_desc:"\u91CD\u5EFA\u6240\u6709 OCR \u5168\u6587\u5411\u91CF\u3002\u66F4\u6362\u6A21\u578B\u6216\u6A21\u5F0F\u540E\u9700\u8981\u91CD\u5EFA\u3002",feat_removing:"\u5220\u9664\u4E2D\u2026",feat_retry_btn:"\u91CD\u8BD5",feat_skills_desc:"\u7BA1\u7406 Vault \u4E2D\u5DF2\u5B89\u88C5\u7684 Agent \u6280\u80FD\u3002\u6BCF\u884C\u5BF9\u5E94\u4E00\u4E2A SKILL.md \u6587\u4EF6\uFF0C\u5173\u95ED\u5F00\u5173\u53EF\u963B\u6B62 Agent \u81EA\u52A8\u8C03\u7528\u8BE5\u6280\u80FD\u3002",feat_skills_system:"\u7CFB\u7EDF\u6280\u80FD\u968F PaperForge \u4E00\u540C\u53D1\u5E03\uFF0C\u4F1A\u8DDF\u968F PaperForge \u7248\u672C\u66F4\u65B0\u3002",feat_skills_user:"\u7528\u6237\u6280\u80FD\u662F\u4F60\u81EA\u884C\u5B89\u88C5\u6216\u521B\u5EFA\u7684\u81EA\u5B9A\u4E49\u6280\u80FD\u3002",feat_uninstall_btn:"\u5378\u8F7D",feat_valid_key:"API Key \u6709\u6548\u3002",feat_vector_config_label:"\u5411\u91CF\u5E93\u914D\u7F6E",feat_vector_corrupted:"\u5411\u91CF\u7D22\u5F15\u5DF2\u635F\u574F \u2014 \u9700\u8981\u5F3A\u5236\u91CD\u5EFA\u3002",feat_vector_desc:"\u5411\u91CF\u6570\u636E\u5E93\u901A\u8FC7\u5D4C\u5165\u6A21\u578B\u5B9E\u73B0 OCR \u5168\u6587\u7684\u8BED\u4E49\u641C\u7D22\u3002\u6587\u6863\u88AB\u5207\u5206\u4E3A\u6587\u672C\u5757\uFF08chunk\uFF09\uFF0C\u7F16\u7801\u4E3A\u5411\u91CF\u5B58\u5165 ChromaDB\u3002\u652F\u6301\u672C\u5730\u6A21\u578B\uFF08\u514D\u8D39\uFF0CCPU \u8FD0\u884C\uFF09\u6216 OpenAI API\uFF08\u4ED8\u8D39\uFF0C\u66F4\u5FEB\u901F\uFF09\u3002",feat_vector_enable:"\u542F\u7528\u5411\u91CF\u68C0\u7D22",feat_vector_enable_desc:"\u5BF9 OCR \u5168\u6587\u8FDB\u884C\u8BED\u4E49\u641C\u7D22\u3002\u9700\u5B89\u88C5: pip install chromadb sentence-transformers openai (~500MB)\u3002",feat_vector_rebuild_force_btn:"\u5F3A\u5236\u91CD\u5EFA",feat_verify:"\u9A8C\u8BC1",feat_verify_btn:"\u9A8C\u8BC1",field_paddleocr:"PaddleOCR API \u5BC6\u94A5",field_python_custom:"\u81EA\u5B9A\u4E49 Python \u8DEF\u5F84",field_python_interp:"\u5F53\u524D Python \u89E3\u91CA\u5668",field_zotero_data:"Zotero \u6570\u636E\u76EE\u5F55",field_zotero_placeholder:"\u53EF\u9009\uFF0C\u7528\u4E8E\u81EA\u52A8\u68C0\u6D4B PDF",guide_ocr:"\u8FD0\u884C OCR",guide_ocr_desc:"Dashboard \u4E2D\u70B9 Run OCR\uFF0C\u63D0\u53D6 PDF \u5168\u6587\u4E0E\u56FE\u8868",guide_open:"\u6253\u5F00 Dashboard",guide_open_desc:"Ctrl+P \u2192 \u8F93\u5165 PaperForge: Open Dashboard\uFF0C\u6216\u70B9\u5DE6\u4FA7\u4E66\u672C\u56FE\u6807",guide_sync:"\u540C\u6B65\u6587\u732E",guide_sync_desc:"Dashboard \u4E2D\u70B9 Sync Library\uFF0C\u4ECE Zotero \u62C9\u53D6\u6587\u732E\u751F\u6210\u7B14\u8BB0",header_title:"PaperForge",install_bootstrapping:"\u672A\u68C0\u6D4B\u5230 PaperForge Python \u5305\uFF0C\u6B63\u5728\u81EA\u52A8\u5B89\u88C5\u2026",install_btn:"\u5F00\u59CB\u5B89\u88C5",install_btn_retry:"\u91CD\u8BD5",install_btn_running:"\u6B63\u5728\u5B89\u88C5...",install_complete:"\u2713 \u5B89\u88C5\u5B8C\u6210\uFF01",install_failed:"\u2717 \u5B89\u88C5\u5931\u8D25\uFF1A",install_validating:"\u6B63\u5728\u6821\u9A8C\u5B89\u88C5\u73AF\u5883\u2026",jump_to_deep_reading:"\u8DF3\u8F6C\u5230\u7CBE\u8BFB",label_agent:"Agent \u5E73\u53F0",nav_close:"\u5173\u95ED",nav_next:"\u4E0B\u4E00\u6B65 \u2192",nav_prev:"\u2190 \u4E0A\u4E00\u6B65",no_pending_ocr:"\u6240\u6709 OCR \u4EFB\u52A1\u5DF2\u5B8C\u6210",not_set:"\u672A\u8BBE\u7F6E",notice_check_fail:"\u672A\u901A\u8FC7: ",notice_python_missing:"Python \u672A\u68C0\u6D4B\u5230\uFF0C\u8BF7\u5148\u5B89\u88C5 Python 3.10+ \u5E76\u52A0\u5165 PATH",ocr_privacy_title:"OCR \u9690\u79C1\u63D0\u793A",ocr_privacy_warning:"OCR \u4F1A\u5C06 PDF \u4E0A\u4F20\u5230 PaddleOCR API \u8FDB\u884C\u5904\u7406\u3002\u8BF7\u4E0D\u8981\u4E0A\u4F20\u5305\u542B\u654F\u611F\u4FE1\u606F\u6216\u65E0\u6CD5\u5916\u4F20\u7684\u6587\u732E\u3002",ocr_queue_add:"\u52A0\u5165 OCR \u961F\u5217",ocr_queue_added:"\u5DF2\u52A0\u5165 OCR \u961F\u5217",ocr_queue_remove:"\u79FB\u51FA OCR \u961F\u5217",ocr_queue_removed:"\u5DF2\u79FB\u51FA OCR \u961F\u5217",ocr_understand:"\u6211\u4E86\u89E3\uFF0C\u7EE7\u7EED",optional_later:"\uFF08\u7A0D\u540E\u53EF\u5728\u8BBE\u7F6E\u4E2D\u8865\u5145\uFF09",orphan_delete_failed:"\u6E05\u7406\u5931\u8D25",orphan_delete_selected:"\u5220\u9664 {count} \u7BC7",orphan_deleted:"\u5DF2\u5220\u9664 {count} \u7BC7\u6B8B\u7559\u6587\u732E",orphan_desc:"\u8FD9\u4E9B\u6587\u732E\u5DF2\u4ECE Zotero \u4E2D\u79FB\u9664\u3002",orphan_deselect_all:"\u53D6\u6D88\u5168\u9009",orphan_explain:"\u5DF2\u4ECE Zotero \u4E2D\u79FB\u9664\u3002\u5DE5\u4F5C\u533A\u6587\u4EF6\u4ECD\u4FDD\u7559\u5728\u78C1\u76D8\u4E0A\u3002",orphan_keep_all:"\u4FDD\u7559\u5168\u90E8",orphan_none_selected:"\u672A\u9009\u62E9\u4EFB\u4F55\u6587\u732E",orphan_select_all:"\u5168\u9009",orphan_title:"\u53D1\u73B0 {count} \u7BC7\u6B8B\u7559\u6587\u732E",panel_actions:"\u5FEB\u6377\u64CD\u4F5C",prep_bbt:"Better BibTeX",prep_bbt_desc:"Zotero \u2192 \u5DE5\u5177 \u2192 \u63D2\u4EF6 \u2192 \u5B89\u88C5 Better BibTeX",prep_export:"BBT \u81EA\u52A8\u5BFC\u51FA",prep_export_desc:"\u53F3\u952E\u6587\u732E\u5B50\u5206\u7C7B \u2192 \u5BFC\u51FA\u5206\u7C7B \u2192 BetterBibTeX JSON \u2192 \u52FE\u9009\u4FDD\u6301\u66F4\u65B0 \u2192 \u5BFC\u51FA\u5230\uFF08JSON \u6587\u4EF6\u540D\u5373\u4E3A Base \u540D\uFF09\uFF1A",prep_key:"PaddleOCR Key",prep_key_desc:"\u5728 https://aistudio.baidu.com/paddleocr \u83B7\u53D6 API Key",prep_python:"Python 3.10+",prep_python_desc:"\u786E\u4FDD Python \u53EF\u547D\u4EE4\u884C\u8C03\u7528\u3002\u70B9\u51FB\u4E0B\u65B9\u6309\u94AE\u81EA\u52A8\u68C0\u6D4B\u3002",prep_zotero:"Zotero \u684C\u9762\u7248",prep_zotero_desc:"\u5B89\u88C5 Zotero (https://www.zotero.org)",run_in_agent:"\u5728 {0} \u4E2D\u8FD0\u884C",runtime_health:"\u8FD0\u884C\u65F6\u72B6\u6001",runtime_health_checking:"\u6B63\u5728\u68C0\u6D4B\u2026",runtime_health_desc:"\u68C0\u67E5\u63D2\u4EF6\u4E0E Python \u8FD0\u884C\u65F6\u7248\u672C\u7684\u5339\u914D\u60C5\u51B5\uFF0C\u5E76\u786E\u8BA4\u5DF2\u90E8\u7F72\u7684 skill contract \u662F\u5426\u4E3A\u5F53\u524D\u7248\u672C\u3002",runtime_health_match:"\u5339\u914D",runtime_health_mismatch:"\u4E0D\u5339\u914D",runtime_health_package_ver:"Python \u5305 v{0}",runtime_health_plugin_ver:"\u63D2\u4EF6 v{0}",runtime_health_sync:"\u540C\u6B65\u8FD0\u884C\u65F6",runtime_health_sync_done:"\u8FD0\u884C\u65F6\u5DF2\u540C\u6B65\u81F3 v{0}",runtime_health_sync_fail:"\u8FD0\u884C\u65F6\u540C\u6B65\u5931\u8D25\uFF1A{0}",runtime_health_syncing:"\u6B63\u5728\u540C\u6B65\u2026",section_config:"\u5F53\u524D\u914D\u7F6E",section_guide:"\u64CD\u4F5C\u65B9\u5F0F",section_prep:"\u5B89\u88C5\u51C6\u5907",section_prep_desc:"\u9996\u6B21\u4F7F\u7528\u524D\uFF0C\u8BF7\u4F9D\u6B21\u5B8C\u6210\u4EE5\u4E0B\u51C6\u5907\uFF1A",setup_done:"\u2713 PaperForge \u73AF\u5883\u5DF2\u914D\u7F6E\u5B8C\u6210",setup_pending:"\u5C1A\u672A\u5B89\u88C5\uFF0C\u5B8C\u6210\u5B89\u88C5\u51C6\u5907\u540E\u70B9\u51FB\u5B89\u88C5\u5411\u5BFC",tab_features:"\u529F\u80FD",tab_setup:"\u5B89\u88C5",validate_base:"Base \u76EE\u5F55\u672A\u586B\u5199",validate_fail:"\u914D\u7F6E\u9A8C\u8BC1\u5931\u8D25",validate_index:"\u7D22\u5F15\u76EE\u5F55\u672A\u586B\u5199",validate_key:"PaddleOCR API \u5BC6\u94A5\u672A\u586B\u5199",validate_notes:"\u6B63\u6587\u76EE\u5F55\u672A\u586B\u5199",validate_resources:"\u8D44\u6E90\u76EE\u5F55\u672A\u586B\u5199",validate_system:"\u7CFB\u7EDF\u76EE\u5F55\u672A\u586B\u5199",validate_vault:"Vault \u8DEF\u5F84\u672A\u586B\u5199",validate_zotero:"Zotero \u6570\u636E\u76EE\u5F55\u4E3A\u5FC5\u586B\u9879",wizard_agent_hint:"\u9009\u62E9\u4F60\u4F7F\u7528\u7684 AI Agent \u5E73\u53F0\uFF0C\u5B89\u88C5\u65F6\u5C06\u6309\u5BF9\u5E94\u683C\u5F0F\u90E8\u7F72\u6280\u80FD\u6587\u4EF6\uFF1A",wizard_dir_hint:"\u8D44\u6E90\u76EE\u5F55\u662F\u6587\u732E\u6570\u636E\u7684\u7EDF\u4E00\u6839\u76EE\u5F55\uFF0C\u4EE5\u4E0B\u5B50\u76EE\u5F55\u5C06\u521B\u5EFA\u5728\u5176\u5185\u90E8\uFF1A",wizard_dir_sub_hint:"\u8D44\u6E90\u76EE\u5F55\u5185\u7684\u4E24\u4E2A\u5B50\u76EE\u5F55\uFF1A",wizard_intro:"\u672C\u5411\u5BFC\u5C06\u5F15\u5BFC\u60A8\u5B8C\u6210 PaperForge \u73AF\u5883\u7684\u5B8C\u6574\u914D\u7F6E\u3002\u5B89\u88C5\u8FC7\u7A0B\u4F1A\u81EA\u52A8\u521B\u5EFA\u6240\u6709\u76EE\u5F55\u7ED3\u6784\uFF0C\u65E0\u9700\u624B\u52A8\u64CD\u4F5C\u3002",wizard_keys_hint:"\u4EE5\u4E0B\u4E3A API \u5BC6\u94A5\u4E0E Zotero \u914D\u7F6E\uFF1A",wizard_preview:"\u7CFB\u7EDF\u6587\u4EF6\u548C Agent \u914D\u7F6E\u4F4D\u4E8E Vault \u6839\u76EE\u5F55\u4E0B\u3002\u6587\u732E\u6570\u636E\uFF08\u6B63\u6587\u3001\u7D22\u5F15\uFF09\u7EDF\u4E00\u5B58\u653E\u5728\u8D44\u6E90\u76EE\u5F55\u5185\u3002\u5B89\u88C5\u540E\u4ECD\u53EF\u5728\u8BBE\u7F6E\u4E2D\u4FEE\u6539\u3002",wizard_safety:"\u5B89\u5168\u8BF4\u660E\uFF1A\u5982\u679C\u4F60\u9009\u62E9\u7684\u76EE\u5F55\u91CC\u5DF2\u7ECF\u6709\u6587\u4EF6\uFF0C\u5B89\u88C5\u5411\u5BFC\u4F1A\u4FDD\u7559\u5DF2\u6709\u5185\u5BB9\uFF0C\u53EA\u8865\u5145\u7F3A\u5931\u7684 PaperForge \u6587\u4EF6\u548C\u76EE\u5F55\u3002",wizard_step1:"\u6982\u89C8",wizard_step2:"\u76EE\u5F55",wizard_step3:"Agent",wizard_step4:"\u5B89\u88C5",wizard_step5:"\u5B8C\u6210",wizard_skip_ocr_desc:"OCR \u529F\u80FD\u5728\u914D\u7F6E\u6709\u6548\u7684 PaddleOCR API \u5BC6\u94A5\u4E4B\u524D\u4E0D\u53EF\u7528\u3002\u60A8\u53EF\u4EE5\u7EE7\u7EED\u5B8C\u6210\u8BBE\u7F6E\uFF0C\u7A0D\u540E\u5728\u8BBE\u7F6E\u4E2D\u914D\u7F6E\u3002",wizard_skip_ocr_continue:"\u7EE7\u7EED\uFF0C\u7A0D\u540E\u914D\u7F6E\u5BC6\u94A5",wizard_skip_ocr_back:"\u8FD4\u56DE\u914D\u7F6E",wizard_api_hint_skip:"OCR \u5BC6\u94A5\u4E3A\u9009\u586B\u9879 \u2014 \u53EF\u8DF3\u8FC7\uFF0C\u7A0D\u540E\u5728\u8BBE\u7F6E\u4E2D\u914D\u7F6E\u3002",wizard_sys_hint:"\u72EC\u7ACB\u4E8E\u8D44\u6E90\u76EE\u5F55\u7684\u7CFB\u7EDF\u6587\u4EF6\uFF1A",wizard_title:"PaperForge \u5B89\u88C5\u5411\u5BFC"}},Me=null;function Pt(g){try{let u=g.vault;if(typeof u.getConfig=="function"){let e=u.getConfig("language");if(e&&String(e).startsWith("zh"))return"zh"}}catch(u){}try{if(typeof localStorage!="undefined"){let u=localStorage.getItem("language");if(u&&String(u).startsWith("zh"))return"zh"}}catch(u){}return"en"}function tt(g){Me=Pt(g)==="zh"?Le.zh:Le.en}function o(g){return Me&&Me[g]||Le.en[g]||g}var P=require("obsidian"),I=z(require("fs")),$=z(require("path")),gt=z(require("os")),V=require("child_process");var ft=z(Ne());var Q=z(require("fs")),Y=z(require("path")),rt=z(require("os")),re=require("child_process"),ze=null,st=!1;function O(g,u,e,t){let s=e||Q,n=t||re.execFileSync;if(u&&u.python_path&&u.python_path.trim()){let l=u.python_path.trim();if(s.existsSync(l))return{path:l,source:"manual",extraArgs:[]}}let a=[Y.join(g,".paperforge-test-venv","Scripts","python.exe"),Y.join(g,".venv","Scripts","python.exe"),Y.join(g,"venv","Scripts","python.exe")];for(let l of a)try{if(s.existsSync(l))return{path:l,source:"auto-detected",extraArgs:[]}}catch(i){}let r=[{path:"py",extraArgs:["-3"]},{path:"python",extraArgs:[]},{path:"python3",extraArgs:[]}];for(let l of r)try{let i=n(l.path,[...l.extraArgs,"--version"],{encoding:"utf-8",timeout:5e3,windowsHide:!0});if(i&&i.toLowerCase().includes("python"))return{path:l.path,source:"auto-detected",extraArgs:l.extraArgs}}catch(i){}return{path:"python",source:"auto-detected",extraArgs:[]}}function nt(g,u,e,t,s){t===void 0&&(t=1e4);let n=s||re.execFile;return new Promise(a=>{n(g,["-c","import paperforge; print(paperforge.__version__)"],{cwd:e,timeout:t},(r,l)=>{if(r){a({status:"not-installed",pyVersion:null,pluginVersion:u,error:r.message});return}let i=l&&l.trim()||null;a(i===u?{status:"match",pyVersion:i,pluginVersion:u,error:null}:{status:"mismatch",pyVersion:i,pluginVersion:u,error:null})})})}function at(g,u,e){e===void 0&&(e=[]);let t=`paperforge==${u}`,s=`git+https://github.com/LLLin000/PaperForge.git@${u}`,n=[...e,"-m","pip","install","--upgrade",t],a=[...e,"-m","pip","install","--upgrade",s];return{cmd:g,url:s,args:a,pypiArgs:n,gitArgs:a,timeout:12e4}}function it(g,u,e,t,s,n){let a=s||re.spawn;return new Promise(r=>{let l=Date.now(),i={cwd:e,timeout:t,windowsHide:!0};n&&(i.env=n);let c=a(g,u,i),p=[],h=[];c.stdout.on("data",d=>{p.push(d.toString("utf-8"))}),c.stderr.on("data",d=>{h.push(d.toString("utf-8"))}),c.on("close",d=>{r({stdout:p.join(""),stderr:h.join(""),exitCode:d,elapsed:Date.now()-l})}),c.on("error",d=>{r({stdout:p.join(""),stderr:h.join("")+` +`+d.message,exitCode:-1,elapsed:Date.now()-l})})})}function $e(){if(st)return ze;st=!0;try{let g;if(process.platform==="win32"){let u=process.env.ComSpec||"C:\\Windows\\System32\\cmd.exe";g=(0,re.execFileSync)(u,["/c","where","git"],{timeout:5e3,windowsHide:!0,encoding:"utf-8"})}else g=(0,re.execFileSync)("which",["git"],{timeout:5e3,encoding:"utf-8"});if(g){let u=g.split(` +`)[0].trim();u&&(ze=Y.dirname(u))}}catch(g){}return ze}function pe(){let g={...process.env},u=process.platform,e=rt.homedir(),t=[],s=$e();s&&t.push(s),u==="darwin"?t.push("/opt/homebrew/bin","/usr/local/bin","/usr/bin",`${e}/.local/bin`):u==="linux"&&t.push("/usr/local/bin","/usr/bin",`${e}/.local/bin`);let n=g.PATH||"";return g.PATH=[...t,n].filter(Boolean).join(Y.delimiter),g}function ot(g){return String(g).toLowerCase().replace(/[^a-z0-9]/g,"").includes("betterbibtex")}function je(g){if(!g)return!1;try{if(!Q.existsSync(g))return!1;for(let u of Q.readdirSync(g))if(ot(u))return!0}catch(u){}return!1}function Te(g){if(!g)return!1;try{if(!Q.existsSync(g))return!1;for(let u of Q.readdirSync(g)){let e=Y.join(g,u,"extensions");try{if(!Q.existsSync(e))continue;for(let t of Q.readdirSync(e))if(ot(t))return!0}catch(t){}}}catch(u){}return!1}var de=z(require("fs")),N=z(require("path")),lt=require("child_process"),ee=null;function Ct(g,u){let e=u||de,t=N.join(g,"paperforge.json"),s={system_dir:"System",resources_dir:"Resources",literature_dir:"Literature",base_dir:"Bases"};try{if(!e.existsSync(t))return{...s,_warning:"paperforge.json not found; using defaults"};let n=e.readFileSync(t,"utf-8"),a=JSON.parse(n),r=a.vault_config||{};return{system_dir:r.system_dir||a.system_dir||s.system_dir,resources_dir:r.resources_dir||a.resources_dir||s.resources_dir,literature_dir:r.literature_dir||a.literature_dir||s.literature_dir,base_dir:r.base_dir||a.base_dir||s.base_dir,_warning:null}}catch(n){return console.warn("PaperForge: Failed to read paperforge.json, using defaults",n),{...s,_warning:"paperforge.json invalid; using defaults"}}}function te(g,u){let e=Ct(g,u),t=N.join(g,e.system_dir,"PaperForge");return{vault:g,systemDir:t,indexesDir:N.join(t,"indexes"),logsDir:N.join(t,"logs"),dbPath:N.join(t,"indexes","paperforge.db"),memoryStatePath:N.join(t,"indexes","memory-runtime-state.json"),vectorStatePath:N.join(t,"indexes","vector-runtime-state.json"),healthStatePath:N.join(t,"indexes","runtime-health.json"),buildStatePath:N.join(t,"indexes","vector-build-state.json"),orphanStatePath:N.join(t,"indexes","sync-orphan-state.json"),exportsDir:N.join(t,"exports"),ocrDir:N.join(t,"ocr"),pluginDataPath:N.join(g,".obsidian","plugins","paperforge","data.json"),pfJsonPath:N.join(g,"paperforge.json"),configWarning:e._warning}}function He(g){try{return de.existsSync(g)?JSON.parse(de.readFileSync(g,"utf-8")):null}catch(u){return null}}function Ft(g){let u=te(g);return He(u.memoryStatePath)}function ke(g){let u=te(g);return He(u.vectorStatePath)}function Ve(g){let u=te(g);return He(u.healthStatePath)}function ct(g){var e;let u=Ve(g);return!!(u&&((e=u.summary)==null?void 0:e.status)==="ok")}function De(g){let u=Ft(g);return!u||u.paper_count_db===0?"DB not found. Run paperforge memory build.":"Papers: "+u.paper_count_db+" | "+(u.fresh?"fresh":"stale")}function ge(g){let u=ke(g);return u?u.healthy===!1?"Vector index unreadable - rebuild required":"Chunks: "+u.chunk_count+" | "+u.model+" | "+u.mode:"Status unavailable"}function ne(g,u){if(ee)return ee;if(u&&u.python_path&&u.python_path.trim()){let s=u.python_path.trim();if(de.existsSync(s))return ee={path:s,source:"manual",extraArgs:[]},ee}let e=[N.join(g,".paperforge-test-venv","Scripts","python.exe"),N.join(g,".venv","Scripts","python.exe"),N.join(g,"venv","Scripts","python.exe")];for(let s=0;s<e.length;s++)if(de.existsSync(e[s]))return ee={path:e[s],source:"auto-detected",extraArgs:[]},ee;let t=[{path:"py",extraArgs:["-3"]},{path:"python",extraArgs:[]},{path:"python3",extraArgs:[]}];for(let s=0;s<t.length;s++)try{let n=t[s],a=(0,lt.execFileSync)(n.path,n.extraArgs.concat(["--version"]),{encoding:"utf-8",timeout:5e3,windowsHide:!0});if(a&&a.toLowerCase().indexOf("python")!==-1)return ee={path:n.path,source:"auto-detected",extraArgs:n.extraArgs},ee}catch(n){}return ee={path:"python",source:"auto-detected",extraArgs:[]},ee}function Ke(g,u,e){return!g||typeof g!="object"||!Object.prototype.hasOwnProperty.call(g,u)?!!e:!!g[u]}function pt(g,u,e){let t=!Ke(g,u,e);return g&&typeof g=="object"&&(g[u]=t),t}var Z=require("obsidian"),G=z(require("fs")),ut=z(require("path")),ht=z(require("https")),Pe=require("child_process");function dt(g,u){return!u||!u.trim()?{blocked:!0,reason:"zotero"}:g?{blocked:!1}:{blocked:!0,reason:"ocr"}}var Ze=class extends Z.Modal{constructor(e,t,s,n){super(e);this._rowEls=[];this.orphans=t.map((a,r)=>({...a,_selected:!0,_idx:r})),this.vaultPath=s,this.py=n}_updateUI(){let e=this.orphans.filter(t=>t._selected);this._countEl.setText(o("orphan_delete_selected").replace("{count}",String(e.length))),this._selectAllBtn.setText(e.length===this.orphans.length?o("orphan_deselect_all"):o("orphan_select_all"));for(let t of this.orphans){let s=this._rowEls[t._idx];s&&s.toggleClass("paperforge-orphan-dimmed",!t._selected)}}onOpen(){let{contentEl:e}=this;e.addClass("paperforge-modal"),e.createEl("h2",{text:o("orphan_title").replace("{count}",String(this.orphans.length))}),e.createEl("p",{cls:"paperforge-modal-desc",text:o("orphan_desc")}),this._rowEls=[];let t=e.createEl("div",{cls:"paperforge-orphan-list"});for(let n of this.orphans){let a=t.createEl("div",{cls:"paperforge-orphan-row"+(n._selected?"":" paperforge-orphan-dimmed")});this._rowEls.push(a);let r=a.createEl("div",{cls:"paperforge-orphan-info"}),l=r.createEl("div",{cls:"paperforge-orphan-header"});l.createEl("span",{cls:"paperforge-orphan-key",text:n.citation_key||n.key});let i=l.createEl("span",{cls:"paperforge-orphan-tags"});i.createEl("span",{cls:"paperforge-tag "+(n.has_pdf?"tag-pdf":"tag-nopdf"),text:n.has_pdf?"PDF":"no PDF"}),n.collection_path&&i.createEl("span",{cls:"paperforge-tag tag-collection",text:n.collection_path}),n.title&&r.createEl("div",{cls:"paperforge-orphan-title",text:n.title});let c=[];n.authors&&c.push(n.authors),n.year&&c.push(n.year),c.length>0&&r.createEl("div",{cls:"paperforge-orphan-meta",text:c.join(" \xB7 ")}),r.createEl("div",{cls:"paperforge-orphan-explain",text:o("orphan_explain")}),a.addEventListener("click",()=>{n._selected=!n._selected,this._updateUI()})}let s=e.createEl("div",{cls:"paperforge-modal-actions"});this._selectAllBtn=s.createEl("button",{cls:"paperforge-step-btn",text:"Deselect all"}),this._selectAllBtn.addEventListener("click",()=>{let n=this.orphans.every(a=>a._selected);for(let a of this.orphans)a._selected=!n;this._updateUI()}),this._countEl=s.createEl("button",{cls:"paperforge-step-btn mod-cta",text:"Delete "+this.orphans.length+" selected"}),s.createEl("button",{cls:"paperforge-step-btn",text:"Keep all"}).addEventListener("click",()=>this.close()),this._countEl.addEventListener("click",()=>{let n=this.orphans.filter(r=>r._selected);if(n.length===0){new Z.Notice(o("orphan_none_selected"));return}if(this._countEl.setText("Deleting..."),this._countEl.setAttr("disabled",""),this._selectAllBtn.setAttr("disabled",""),!this.py||!this.py.path){new Z.Notice("PaperForge: Python not found"),this.close();return}let a=n.map(r=>r.key);(0,Pe.execFile)(this.py.path,[...this.py.extraArgs,"-m","paperforge","--vault",this.vaultPath,"prune","--force","--json",...a],{cwd:this.vaultPath,timeout:6e4},(r,l)=>{if(r){new Z.Notice("PaperForge: prune failed"),this.close();return}try{let i=JSON.parse(l),c=i.data&&i.data.deleted||[];new Z.Notice("Deleted "+c.length+" orphan workspace(s)")}catch(i){new Z.Notice("PaperForge: prune done")}this.close()})})}onClose(){this.contentEl.empty()}};function Re(g,u,e){console.log("[PF] checkOrphanState called");try{let s=te(e).orphanStatePath;if(!G.existsSync(s)){console.log("[PF] orphan file NOT FOUND");return}console.log("[PF] orphan file FOUND");let n=G.readFileSync(s,"utf-8"),r=JSON.parse(n).orphans||[];if(console.log("[PF] orphans count:",r.length),r.length===0)return;let l=ne(e,u.settings);console.log("[PF] py.path:",l?l.path:"null"),new Ze(g,r,e,l).open(),G.unlinkSync(s),console.log("[PF] orphan file cleaned")}catch(t){console.log("[PF] checkOrphanState exception:",t.message||t)}}var we=class extends Z.Modal{constructor(e,t){super(e);this._pendingSave=null;this._showSkipConfirm=!1;this.plugin=t,this._step=1}onOpen(){this._render()}onClose(){this.contentEl.empty()}_render(){let{contentEl:e}=this;e.empty(),e.addClass("paperforge-modal"),this._renderStepIndicator(),this._renderStepContent(),this._renderNavigation()}_renderStepIndicator(){let e=[o("wizard_step1"),o("wizard_step2"),o("wizard_step3"),o("wizard_step4"),o("wizard_step5")],t=this.contentEl.createEl("div",{cls:"paperforge-step-bar"});e.forEach((s,n)=>{let a=n+1,r=t.createEl("div",{cls:`paperforge-step-dot ${a===this._step?"active":""} ${a<this._step?"done":""}`});r.createEl("span",{cls:"paperforge-step-num",text:`${a}`}),r.createEl("span",{cls:"paperforge-step-label",text:s})})}_renderStepContent(){let e=this.contentEl.createEl("div",{cls:"paperforge-step-content"});switch(this._step){case 1:this._stepOverview(e);break;case 2:this._stepDirectories(e);break;case 3:this._stepKeys(e);break;case 4:this._stepInstall(e);break;case 5:this._stepComplete(e);break}}_renderNavigation(){let e=this.contentEl.createEl("div",{cls:"paperforge-step-nav"});this._step>1&&e.createEl("button",{cls:"paperforge-step-btn",text:o("nav_prev")}).addEventListener("click",()=>{this._step--,this._showSkipConfirm=!1,this._render()}),this._step<5?e.createEl("button",{cls:"paperforge-step-btn mod-cta",text:o("nav_next")}).addEventListener("click",()=>{if(this._step===3){let s=this._validateStep3();if(s.blocked){if(s.reason==="zotero")return;if(s.reason==="ocr"){this._showSkipConfirm=!0,this._render();return}}}this._step++,this._showSkipConfirm=!1,this._render()}):e.createEl("button",{cls:"paperforge-step-btn",text:o("nav_close")}).addEventListener("click",()=>this.close())}_validateStep3(){let e=this.plugin.settings,t=dt(this._apiKeyValidated,e.zotero_data_dir);if(t.reason==="ocr")return t;let s=(e.zotero_data_dir||"").trim();if(!s)return new Z.Notice("Zotero \u6570\u636E\u76EE\u5F55\u4E3A\u5FC5\u586B\u9879\uFF0C\u8BF7\u586B\u5199\u8DEF\u5F84"),{blocked:!0,reason:"zotero"};if(!G.existsSync(s))return new Z.Notice("Zotero \u6570\u636E\u76EE\u5F55\u8DEF\u5F84\u4E0D\u5B58\u5728"),{blocked:!0,reason:"zotero"};if(!G.statSync(s).isDirectory())return new Z.Notice("Zotero \u6570\u636E\u76EE\u5F55\u8DEF\u5F84\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55"),{blocked:!0,reason:"zotero"};let n=ut.join(s,"storage");return!G.existsSync(n)||!G.statSync(n).isDirectory()?(new Z.Notice("Zotero \u6570\u636E\u76EE\u5F55\u4E2D\u672A\u627E\u5230 storage/ \u5B50\u76EE\u5F55"),{blocked:!0,reason:"zotero"}):{blocked:!1}}_stepOverview(e){e.createEl("h2",{text:o("wizard_title")}),e.createEl("p",{text:o("wizard_intro")});let t=this.plugin.settings,s=this.app.vault.adapter.basePath,n=e.createEl("div",{cls:"paperforge-dir-tree"}),a=n.createEl("div",{cls:"paperforge-dir-node root"});a.textContent=`\u{1F4C1} Vault (${s})`;let r=n.createEl("div",{cls:"paperforge-dir-children"}),l=r.createEl("div",{cls:"paperforge-dir-node folder"});l.textContent=`\u{1F4C1} ${t.resources_dir||"Resources"}/ \u2014 \u6587\u732E\u5361\u7247\u76EE\u5F55\uFF08Base \u6570\u636E\u6765\u6E90\uFF09`,l.createEl("div",{cls:"paperforge-dir-children"}).createEl("div",{cls:"paperforge-dir-node file",text:`\u{1F4C1} ${t.literature_dir||"Literature"}/ \u2014 \u6587\u732E\u5361\u7247`}),r.createEl("div",{cls:"paperforge-dir-node folder",text:`\u{1F4C1} ${t.base_dir||"Bases"}/ \u2014 \u6570\u636E\u7BA1\u7406\u9762\u677F`}),r.createEl("div",{cls:"paperforge-dir-node folder",text:`\u{1F4C1} ${t.system_dir||"System"}/ \u2014 Zotero \u8F6F\u94FE\u63A5 + PaperForge \u7CFB\u7EDF\u6587\u4EF6\u5939`}),e.createEl("p",{text:o("wizard_preview"),cls:"paperforge-modal-hint"}),e.createEl("p",{text:o("wizard_safety"),cls:"paperforge-modal-hint"});let c=e.createEl("div",{cls:"paperforge-summary"}),p=[{label:o("dir_resources"),val:`${s}/${t.resources_dir||"Resources"}`},{label:o("dir_notes"),val:`${s}/${t.resources_dir||"Resources"}/${t.literature_dir||"Literature"}`},{label:o("dir_base"),val:`${s}/${t.base_dir||"Bases"}`},{label:o("dir_system"),val:`${s}/${t.system_dir||"System"}`}];for(let h of p){let d=c.createEl("div",{cls:"paperforge-summary-row"});d.createEl("span",{cls:"paperforge-summary-label",text:h.label}),d.createEl("span",{cls:"paperforge-summary-value",text:h.val})}}_stepDirectories(e){e.createEl("h2",{text:o("wizard_step2")}),e.createEl("p",{text:o("wizard_intro")});let t=this.plugin.settings,s=this.app.vault.adapter.basePath;this._modalField(e,o("dir_vault"),s,!0),e.createEl("p",{text:o("wizard_dir_hint"),cls:"paperforge-modal-hint"}),this._modalInput(e,"\u8D44\u6E90\u76EE\u5F55\uFF08\u521B\u5EFA\u6587\u732E\u5361\u7247\u76EE\u5F55\u7684\u5730\u65B9\uFF09","resources_dir",t.resources_dir,"Resources"),e.createEl("p",{text:o("wizard_dir_sub_hint"),cls:"paperforge-modal-hint"}),this._modalInput(e,"\u6587\u732E\u5361\u7247\u76EE\u5F55\uFF08\u5B58\u653E\u6587\u732E\u5361\u7247\u7684\u5730\u65B9\uFF0CBase \u6570\u636E\u6765\u6E90\uFF09","literature_dir",t.literature_dir,"Literature"),e.createEl("p",{text:o("wizard_sys_hint"),cls:"paperforge-modal-hint"}),this._modalInput(e,"\u7CFB\u7EDF\u76EE\u5F55\uFF08\u5B58\u653E Zotero \u8F6F\u94FE\u63A5\u548C PaperForge \u7CFB\u7EDF\u6587\u4EF6\uFF09","system_dir",t.system_dir,"System"),this._modalInput(e,"Base \u76EE\u5F55\uFF08\u5B58\u653E\u6570\u636E\u7BA1\u7406\u9762\u677F\u7684\u5730\u65B9\uFF09","base_dir",t.base_dir,"Bases"),e.createEl("p",{text:o("wizard_safety"),cls:"paperforge-modal-hint"});let n=e.createEl("div",{cls:"paperforge-summary"}),a=[{label:o("dir_resources"),val:`${s}/${t.resources_dir||""}`},{label:o("dir_notes"),val:`${s}/${t.resources_dir||""}/${t.literature_dir||""}`},{label:o("dir_system"),val:`${s}/${t.system_dir||""}`},{label:o("dir_base"),val:`${s}/${t.base_dir||""}`}];for(let r of a){let l=n.createEl("div",{cls:"paperforge-summary-row"});l.createEl("span",{cls:"paperforge-summary-label",text:r.label}),l.createEl("span",{cls:"paperforge-summary-value",text:r.val})}}_stepKeys(e){if(e.createEl("h2",{text:o("wizard_step3")}),this._showSkipConfirm){this._renderSkipConfirm(e);return}let t=this.plugin.settings;e.createEl("p",{text:o("wizard_agent_hint"),cls:"paperforge-modal-hint"});let s=[{key:"opencode",name:"OpenCode"},{key:"claude",name:"Claude Code"},{key:"cursor",name:"Cursor"},{key:"github_copilot",name:"GitHub Copilot"},{key:"windsurf",name:"Windsurf"},{key:"codex",name:"Codex"},{key:"cline",name:"Cline"}],n=e.createEl("div",{cls:"paperforge-modal-field"});n.createEl("label",{cls:"paperforge-modal-label",text:o("label_agent")});let a=n.createEl("select",{cls:"paperforge-modal-select"});for(let h of s){let d=a.createEl("option",{text:h.name,attr:{value:h.key}});h.key===(t.agent_platform||"opencode")&&(d.selected=!0)}a.addEventListener("change",()=>{t.agent_platform=a.value,this._pendingSave&&clearTimeout(this._pendingSave),this._pendingSave=setTimeout(()=>{this.plugin.saveSettings(),this._pendingSave=null},500)}),e.createEl("p",{text:o("wizard_keys_hint"),cls:"paperforge-modal-hint"});let r=e.createEl("div",{cls:"paperforge-modal-field"});r.createEl("label",{cls:"paperforge-modal-label",text:o("field_paddleocr")});let l=r.createEl("input",{cls:"paperforge-modal-input",attr:{type:"password",placeholder:"API Key"}});l.value=t.paddleocr_api_key||"",this._apiKeyValidated=!1,this._apiKeyStatus=r.createEl("span",{cls:"paperforge-apikey-status",text:""});let i=r.createEl("button",{cls:"paperforge-step-btn",text:"\u9A8C\u8BC1"});i.addEventListener("click",()=>this._validateApiKey(l.value,i)),l.addEventListener("input",()=>{t.paddleocr_api_key=l.value,this._apiKeyValidated=!1,this._apiKeyStatus.textContent="",this._apiKeyStatus.className="paperforge-apikey-status"}),this._pendingSave&&clearTimeout(this._pendingSave),this._pendingSave=setTimeout(()=>{this.plugin.saveSettings(),this._pendingSave=null},500),e.createEl("p",{text:o("wizard_api_hint_skip"),cls:"paperforge-modal-hint"});let c=e.createEl("div",{cls:"paperforge-modal-field"});c.createEl("label",{cls:"paperforge-modal-label",text:o("field_zotero_data")});let p=c.createEl("input",{cls:"paperforge-modal-input",attr:{type:"text",placeholder:o("field_zotero_placeholder")}});p.value=t.zotero_data_dir||"",p.addEventListener("input",()=>{t.zotero_data_dir=p.value,this._pendingSave&&clearTimeout(this._pendingSave),this._pendingSave=setTimeout(()=>{this.plugin.saveSettings(),this._pendingSave=null},500)})}_validateApiKey(e,t){if(!e||e.length<10){this._apiKeyStatus.textContent="\u5BC6\u94A5\u683C\u5F0F\u4E0D\u6B63\u786E\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002",this._apiKeyStatus.className="paperforge-apikey-status error";return}t.disabled=!0,t.textContent="\u9A8C\u8BC1\u4E2D\u2026",this._apiKeyStatus.textContent="\u6B63\u5728\u9A8C\u8BC1\u2026",this._apiKeyStatus.className="paperforge-apikey-status";let s=JSON.stringify({model:"PaddleOCR-VL-1.5"}),n={hostname:"paddleocr.aistudio-app.com",path:"/api/v2/ocr/jobs",method:"POST",headers:{Authorization:"bearer "+e,"Content-Type":"application/json","Content-Length":Buffer.byteLength(s)},timeout:1e4},a=ht.request(n,r=>{t.disabled=!1,t.textContent="\u9A8C\u8BC1";let l="";r.on("data",i=>l+=i),r.on("end",()=>{try{let i=JSON.parse(l);r.statusCode===400&&i.code===10001?(this._apiKeyStatus.textContent="\u2713 \u5BC6\u94A5\u6709\u6548",this._apiKeyStatus.className="paperforge-apikey-status ok",this._apiKeyValidated=!0):r.statusCode===401?(this._apiKeyStatus.textContent="\u9A8C\u8BC1\u5931\u8D25\uFF1A\u5BC6\u94A5\u65E0\u6548\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002",this._apiKeyStatus.className="paperforge-apikey-status error",this._apiKeyValidated=!1):(this._apiKeyStatus.textContent="\u9A8C\u8BC1\u5931\u8D25\uFF1AAPI \u8FD4\u56DE "+r.statusCode+"\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002",this._apiKeyStatus.className="paperforge-apikey-status error",this._apiKeyValidated=!1)}catch(i){this._apiKeyStatus.textContent="\u9A8C\u8BC1\u5931\u8D25\uFF1A\u65E0\u6CD5\u89E3\u6790\u54CD\u5E94\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002",this._apiKeyStatus.className="paperforge-apikey-status error",this._apiKeyValidated=!1}})});a.on("error",r=>{t.disabled=!1,t.textContent="\u9A8C\u8BC1",this._apiKeyStatus.textContent="\u9A8C\u8BC1\u5931\u8D25\uFF1A\u65E0\u6CD5\u8FDE\u63A5 ("+r.message+")\u3002\u53EF\u70B9\u4E0B\u4E00\u6B65\u8DF3\u8FC7\uFF0C\u7B49\u4F1A\u513F\u518D\u914D\u7F6E\u3002",this._apiKeyStatus.className="paperforge-apikey-status error",this._apiKeyValidated=!1}),a.write(s),a.end()}_renderSkipConfirm(e){e.createEl("p",{text:o("wizard_skip_ocr_desc"),cls:"paperforge-modal-desc"});let t=e.createEl("div",{cls:"paperforge-modal-actions"});t.createEl("button",{cls:"paperforge-step-btn mod-cta",text:o("wizard_skip_ocr_continue")}).addEventListener("click",()=>{this._showSkipConfirm=!1,this._step++,this._render()}),t.createEl("button",{cls:"paperforge-step-btn",text:o("wizard_skip_ocr_back")}).addEventListener("click",()=>{this._showSkipConfirm=!1,this._render()})}_modalField(e,t,s,n){let a=e.createEl("div",{cls:"paperforge-modal-field"});a.createEl("label",{cls:"paperforge-modal-label",text:t});let r=a.createEl("input",{cls:"paperforge-modal-input",attr:{type:"text"}});r.value=s,r.disabled=!!n}_modalInput(e,t,s,n,a){let r=e.createEl("div",{cls:"paperforge-modal-field"});r.createEl("label",{cls:"paperforge-modal-label",text:t});let l=r.createEl("input",{cls:"paperforge-modal-input",attr:{type:"text",placeholder:a||""}});l.value=n;let i=this.plugin.settings;l.addEventListener("input",()=>{i[s]=l.value,this._pendingSave&&clearTimeout(this._pendingSave),this._pendingSave=setTimeout(()=>{this.plugin.saveSettings(),this._pendingSave=null},500)})}_modalSecret(e,t,s,n,a){let r=e.createEl("div",{cls:"paperforge-modal-field"});r.createEl("label",{cls:"paperforge-modal-label",text:t});let l=r.createEl("input",{cls:"paperforge-modal-input",attr:{type:"password",placeholder:a||""}});l.value=n;let i=this.plugin.settings;l.addEventListener("input",()=>{i[s]=l.value,this._pendingSave&&clearTimeout(this._pendingSave),this._pendingSave=setTimeout(()=>{this.plugin.saveSettings(),this._pendingSave=null},500)})}_stepInstall(e){e.createEl("h2",{text:o("wizard_step4")}),this._installLog=e.createEl("div",{cls:"paperforge-install-log"});let t=e.createEl("button",{cls:"paperforge-step-btn mod-cta",text:o("install_btn")});t.addEventListener("click",()=>this._runInstall(t))}async _runInstall(e){var r,l,i,c,p,h;e.disabled=!0,e.textContent=o("install_btn_running"),this._installLog.setText(o("install_validating")+` +`),this._log(o("install_validating"));let t=this.plugin.settings,s=this._validate();if(s.length>0){this._log(o("validate_fail")+":"),s.forEach(d=>this._log(" \u2717 "+d)),e.disabled=!1,e.textContent=o("install_btn_retry");return}let n=(d,y={})=>new Promise((b,v)=>{let{path:f,extraArgs:x=[]}=O(t.vault_path.trim(),this.plugin.settings,void 0,void 0),C=(0,Pe.spawn)(f,[...x,...d],{cwd:t.vault_path.trim(),env:pe(),timeout:12e4,...y}),E="",_="";C.stdout.on("data",m=>{let k=m.toString("utf-8");E+=k,y.logStdout&&this._processSetupOutput(k)}),C.stderr.on("data",m=>{let k=m.toString("utf-8");_+=k,this._log("[stderr] "+k.trim())}),C.on("close",m=>{m===0?b({stdout:E,stderr:_}):v(new Error(_.trim()||E.trim()||`exit code ${m}`))}),C.on("error",m=>v(m))}),a=["-m","paperforge","--vault",t.vault_path.trim(),"setup","--headless","--system-dir",t.system_dir.trim(),"--resources-dir",t.resources_dir.trim(),"--literature-dir",t.literature_dir.trim(),"--base-dir",t.base_dir.trim(),"--agent",t.agent_platform||"opencode"];t.zotero_data_dir&&t.zotero_data_dir.trim()&&a.push("--zotero-data",t.zotero_data_dir.trim()),t.paddleocr_api_key&&t.paddleocr_api_key.trim()&&a.push("--paddleocr-key",t.paddleocr_api_key.trim());try{let d=!0;try{await n(["-c","import paperforge"])}catch(y){d=!1}if(!d){this._log(o("install_bootstrapping"));let y=this.plugin.manifest.version;this._log(`[install] Trying PyPI: pip install paperforge==${y}`);let b=["-m","pip","install","--upgrade"];process.platform!=="win32"&&b.push("--user"),b.push(`paperforge==${y}`);try{await n(b,{logStdout:!0})}catch(v){this._log(`[install] PyPI failed, falling back to git: git+https://...@v${y}`),console.warn("[PaperForge] PyPI install failed, falling back to git:",(r=v.message)==null?void 0:r.slice(0,200));let f=["-m","pip","install","--upgrade"];process.platform!=="win32"&&f.push("--user"),f.push(`git+https://github.com/LLLin000/PaperForge.git@v${y}`),await n(f,{logStdout:!0})}}await n(a,{logStdout:!0,env:pe()}),this._log(o("install_complete")),t.setup_complete=!0,await this.plugin.saveSettings(),setTimeout(()=>{this._step=5,this._render()},800)}catch(d){console.error("PaperForge setup failed:",d.message);let y=this._formatSetupError(d.message);this._log(o("install_failed")+y);let b=(l=this._installLog.parentElement)==null?void 0:l.createEl("button",{cls:"paperforge-copy-diag-btn",text:o("error_copy_diagnostic")||"Copy diagnostic"});if(b){let v=d.message,f=((c=(i=this.plugin)==null?void 0:i.settings)==null?void 0:c.python_path)||"auto",x=((h=(p=this.plugin)==null?void 0:p.manifest)==null?void 0:h.version)||"?",C=process.platform+" "+process.arch,E,_;try{E=$e()||"(not found)"}catch(F){E="(error)"}try{_=O(t.vault_path.trim(),this.plugin.settings,void 0,void 0)}catch(F){_=null}let m=(process.env.PATH||"").length,k=(process.env.PATH||"").toLowerCase().includes("git"),S=["[PaperForge Diagnostic]","Category: "+y,"Plugin version: "+x,"Python: "+f,"Resolved Python: "+((_==null?void 0:_.path)||"?"),"OS: "+C,"Vault path: "+(t.vault_path||"?"),"--- Git ---","Git dir (resolved): "+E,"PATH length: "+m+" chars","PATH contains git: "+k,"--- Raw error ---",v.slice(0,2e3)].join(` +`);b.addEventListener("click",()=>{navigator.clipboard.writeText(S).then(()=>{b.setText(o("error_copied")||"Copied!"),setTimeout(()=>{b.setText(o("error_copy_diagnostic")||"Copy diagnostic")},3e3)}).catch(()=>{new Z.Notice("[!!] Clipboard write failed",6e3)})})}e.disabled=!1,e.textContent=o("install_btn_retry")}}_log(e){this._installLog&&this._installLog.setText(this._installLog.textContent+e+` +`)}_validate(){let e=[],t=this.plugin.settings;return(!t.vault_path||!t.vault_path.trim())&&e.push(o("validate_vault")),(!t.resources_dir||!t.resources_dir.trim())&&e.push(o("validate_resources")),(!t.literature_dir||!t.literature_dir.trim())&&e.push(o("validate_notes")),(!t.base_dir||!t.base_dir.trim())&&e.push(o("validate_base")),(!t.paddleocr_api_key||!t.paddleocr_api_key.trim())&&this._log(" ! "+o("validate_key")+" "+o("optional_later")),(!t.zotero_data_dir||!t.zotero_data_dir.trim())&&this._log(" ! "+o("validate_zotero")+" "+o("optional_later")),e}_processSetupOutput(e){let t=e.split(` `).filter(Boolean);for(let s of t)if(s.includes("[*]")||s.includes("[OK]")||s.includes("[FAIL]")){let n=s.replace(/^\[\*\].*\d+:?\s*/,"").replace(/^\[OK\]\s*/,"").replace(/^\[FAIL\]\s*/,"");this._log(" "+n)}}_formatSetupError(e){if(process.platform==="darwin"&&/No module named ['"]?paperforge/i.test(e))return"PaperForge not installed \u2014 install Python from Homebrew or python.org (Apple CLT /Library/Developer/CommandLineTools python often fails); then: python3 -m pip install --user git+https://github.com/LLLin000/PaperForge.git";let t=[{match:/pip.*not found|No module named.*pip|command not found.*pip/i,msg:"pip not found"},{match:/command not found|No such file|not recognized/i,msg:"Python not found"},{match:/resolve host|getaddrinfo.*nodename|connect ETIMEDOUT|connect ECONNREFUSED|fetch failed|Network error|ENOTFOUND|ECONNREFUSED|ECONNRESET/i,msg:"Network error"},{match:/certificate verify failed|SSL.*certificate|self.signed.cert|CERTIFICATE_VERIFY_FAILED/i,msg:"SSL certificate error"},{match:/No space left on device|disk full|ENOSPC/i,msg:"Disk full"},{match:/paperforge.*not found|cannot import|ModuleNotFoundError|No module named/i,msg:"PaperForge not installed"},{match:/permission denied|EACCES|EPERM/i,msg:"Permission denied"},{match:/ENOENT/i,msg:"Path not found"},{match:/timeout|timed out/i,msg:"Timeout"}];for(let n of t)if(n.match.test(e))return n.msg;return e.split(` -`).filter(Boolean).slice(0,3).join(" | ").slice(0,200)||"Unknown error"}_stepComplete(e){e.createEl("h2",{text:i("complete_title")});let t=e.createEl("div",{cls:"paperforge-summary"});t.createEl("div",{cls:"paperforge-summary-title",text:i("complete_summary")});let s=this.plugin.settings,n=this.app.vault.adapter.basePath,a=[{label:i("dir_vault"),val:n},{label:i("dir_resources"),val:`${n}/${s.resources_dir}`},{label:i("dir_notes"),val:`${n}/${s.resources_dir}/${s.literature_dir}`},{label:i("dir_base"),val:`${n}/${s.base_dir}`},{label:i("dir_system"),val:`${n}/${s.system_dir}`},{label:"API Key",val:s.paddleocr_api_key?i("api_key_set"):i("api_key_missing")},{label:i("field_zotero_data"),val:s.zotero_data_dir||i("not_set")}];for(let p of a){let u=t.createEl("div",{cls:"paperforge-summary-row"});u.createEl("span",{cls:"paperforge-summary-label",text:p.label}),u.createEl("span",{cls:"paperforge-summary-value",text:p.val})}let r=t.createEl("div",{cls:"paperforge-summary-row"});r.createEl("span",{cls:"paperforge-summary-label",text:"PaperForge"});let o=r.createEl("span",{cls:"paperforge-summary-value",text:"\u2014"});{let p=n,{path:u,extraArgs:d=[]}=A(p,this.plugin.settings,void 0,void 0);(0,Pe.execFile)(u,[...d,"-c","import paperforge; print(paperforge.__version__)"],{cwd:p,timeout:1e4},(_,y)=>{!_&&y&&(o.textContent="v"+y.trim())})}for(let p of a){let u=t.createEl("div",{cls:"paperforge-summary-row"});u.createEl("span",{cls:"paperforge-summary-label",text:p.label}),u.createEl("span",{cls:"paperforge-summary-value",text:p.val})}e.createEl("h3",{text:i("complete_next")});let l=e.createEl("div",{cls:"paperforge-nextsteps"}),c=[[i("complete_step4"),i("complete_step4_desc")],["",`${i("complete_export_path")} ${n}/${s.system_dir}/PaperForge/exports/`],[i("complete_step1"),i("complete_step1_desc")],[i("complete_step2"),i("complete_step2_desc")],[i("complete_step3"),i("complete_step3_desc")]];for(let[p,u]of c){let d=l.createEl("div",{cls:"paperforge-nextstep-item"});p&&d.createEl("strong",{text:p}),d.createEl("span",{text:u})}}};var De=class extends w.PluginSettingTab{constructor(e,t){super(e,t);this._saveTimeout=null;this._pfConfig=null;this._lastSyncTime=null;this._memoryStatusText=null;this._vectorDepsOk=null;this._embedStatusText=null;this._skillsCollapsed={user:!0};this._featurePanelsCollapsed={};this._advCollapsed=!0;this._refreshPending=!1;this._pythonInterpDescEl=null;this._customPathDescEl=null;this._checkEl=null;this.activeTab="setup";this.plugin=t}_refreshPfConfig(){this._pfConfig=this.plugin.readPaperforgeJson()}display(){let{containerEl:e}=this;if(e.empty(),this._refreshPfConfig(),!document.getElementById("paperforge-tab-styles")){let a=document.createElement("style");a.id="paperforge-tab-styles",a.textContent=` +`).filter(Boolean).slice(0,3).join(" | ").slice(0,200)||"Unknown error"}_stepComplete(e){e.createEl("h2",{text:o("complete_title")});let t=e.createEl("div",{cls:"paperforge-summary"});t.createEl("div",{cls:"paperforge-summary-title",text:o("complete_summary")});let s=this.plugin.settings,n=this.app.vault.adapter.basePath,a=[{label:o("dir_vault"),val:n},{label:o("dir_resources"),val:`${n}/${s.resources_dir}`},{label:o("dir_notes"),val:`${n}/${s.resources_dir}/${s.literature_dir}`},{label:o("dir_base"),val:`${n}/${s.base_dir}`},{label:o("dir_system"),val:`${n}/${s.system_dir}`},{label:"API Key",val:s.paddleocr_api_key?o("api_key_set"):o("api_key_missing")},{label:o("field_zotero_data"),val:s.zotero_data_dir||o("not_set")}];for(let p of a){let h=t.createEl("div",{cls:"paperforge-summary-row"});h.createEl("span",{cls:"paperforge-summary-label",text:p.label}),h.createEl("span",{cls:"paperforge-summary-value",text:p.val})}let r=t.createEl("div",{cls:"paperforge-summary-row"});r.createEl("span",{cls:"paperforge-summary-label",text:"PaperForge"});let l=r.createEl("span",{cls:"paperforge-summary-value",text:"\u2014"});{let p=n,{path:h,extraArgs:d=[]}=O(p,this.plugin.settings,void 0,void 0);(0,Pe.execFile)(h,[...d,"-c","import paperforge; print(paperforge.__version__)"],{cwd:p,timeout:1e4},(y,b)=>{!y&&b&&(l.textContent="v"+b.trim())})}for(let p of a){let h=t.createEl("div",{cls:"paperforge-summary-row"});h.createEl("span",{cls:"paperforge-summary-label",text:p.label}),h.createEl("span",{cls:"paperforge-summary-value",text:p.val})}e.createEl("h3",{text:o("complete_next")});let i=e.createEl("div",{cls:"paperforge-nextsteps"}),c=[[o("complete_step4"),o("complete_step4_desc")],["",`${o("complete_export_path")} ${n}/${s.system_dir}/PaperForge/exports/`],[o("complete_step1"),o("complete_step1_desc")],[o("complete_step2"),o("complete_step2_desc")],[o("complete_step3"),o("complete_step3_desc")]];for(let[p,h]of c){let d=i.createEl("div",{cls:"paperforge-nextstep-item"});p&&d.createEl("strong",{text:p}),d.createEl("span",{text:h})}}};var Be=class extends P.PluginSettingTab{constructor(e,t){super(e,t);this._saveTimeout=null;this._pfConfig=null;this._lastSyncTime=null;this._memoryStatusText=null;this._vectorDepsOk=null;this._embedStatusText=null;this._skillsCollapsed={user:!0};this._featurePanelsCollapsed={};this._advCollapsed=!0;this._refreshPending=!1;this._pythonInterpDescEl=null;this._customPathDescEl=null;this._checkEl=null;this.activeTab="setup";this.plugin=t}_refreshPfConfig(){this._pfConfig=this.plugin.readPaperforgeJson()}display(){let{containerEl:e}=this;if(e.empty(),this._refreshPfConfig(),!document.getElementById("paperforge-tab-styles")){let a=document.createElement("style");a.id="paperforge-tab-styles",a.textContent=` .paperforge-settings-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid var(--background-modifier-border); } .paperforge-settings-tab { padding: 6px 16px; border: none; background: none; cursor: pointer; border-bottom: 2px solid transparent; font-size: 14px; color: var(--text-muted); } .paperforge-settings-tab--active { color: var(--text-accent); border-bottom-color: var(--text-accent); } @@ -17,21 +17,31 @@ .paperforge-skills-group { margin-bottom: 10px; } .paperforge-skills-group:last-child { margin-bottom: 0; } .vertical-tab-content-container { overflow-y: scroll !important; } - `,document.head.appendChild(a)}let t=e.createDiv({cls:"paperforge-settings-tabs"}),s=[{id:"setup",label:i("tab_setup")||"Installation"},{id:"features",label:i("tab_features")||"Features"}],n={};s.forEach(a=>{t.createEl("button",{cls:"paperforge-settings-tab"+(a.id===this.activeTab?" paperforge-settings-tab--active":""),text:a.label}).addEventListener("click",()=>{this.activeTab=a.id,this.display()})}),s.forEach(a=>{n[a.id]=e.createDiv({cls:"paperforge-tab-content"+(a.id===this.activeTab?" paperforge-tab-content--active":"")})}),this.activeTab==="setup"?this._renderSetupTab(n.setup):this._renderFeaturesTab(n.features)}_renderSetupTab(e){let t=this.app.vault.adapter.basePath;this.plugin.settings.vault_path||(this.plugin.settings.vault_path=t,this._debouncedSave()),this.plugin.settings.setup_complete&&(O.existsSync(z.join(t,"paperforge.json"))||(this.plugin.settings.setup_complete=!1,this._debouncedSave())),e.createEl("h2",{text:i("header_title")||"PaperForge"}),e.createEl("p",{text:i("desc"),cls:"paperforge-settings-desc"});let n=e.createEl("div",{cls:"paperforge-setup-bar"}).createEl("span",{cls:"paperforge-setup-label"});this.plugin.settings.setup_complete?(n.setText(i("setup_done")),n.addClass("paperforge-setup-done")):(n.setText(i("setup_pending")),n.addClass("paperforge-setup-pending"));let a=this.app.vault.adapter.basePath,r=A(a,this.plugin.settings,void 0,void 0),o=r.path,l=this.plugin.settings._python_path_stale?"stale":r.source,c=new w.Setting(e).setName(i("field_python_interp")).setDesc(this._getPythonDesc(o,l));this._pythonInterpDescEl=c.descEl;let p=new w.Setting(e).setName(i("field_python_custom")).setDesc("");this._customPathDescEl=p.descEl,p.addText(b=>{b.setPlaceholder("e.g. C:\\Python310\\python.exe").setValue(this.plugin.settings.python_path||"").onChange(m=>{if(this.plugin.settings.python_path=m,this.plugin.saveSettings(),m&&m.trim()){let C=O.existsSync(m.trim());this.plugin.settings._python_path_stale=!C}else this.plugin.settings._python_path_stale=!1;let v=A(this.app.vault.adapter.basePath,this.plugin.settings,void 0,void 0),k=this.plugin.settings._python_path_stale?"stale":v.source;this._pythonInterpDescEl&&(this._pythonInterpDescEl.textContent=this._getPythonDesc(v.path,k))})}),p.addButton(b=>{b.setButtonText(i("btn_validate")).onClick(()=>this._validatePythonOverride())}),e.createEl("h3",{text:i("runtime_health")}),e.createEl("p",{text:i("runtime_health_desc"),cls:"paperforge-settings-desc"});let u=new w.Setting(e).setName("PaperForge").setDesc(i("runtime_health_checking")),d=u.descEl.createEl("span",{cls:"paperforge-runtime-badge"}),_=null;u.addButton(b=>{_=b,b.setButtonText(i("runtime_health_sync")).setDisabled(!0).onClick(()=>this._syncRuntime(b))});{let b=this.app.vault.adapter.basePath,{path:m,extraArgs:v=[]}=A(b,this.plugin.settings,void 0,void 0),k=this.plugin.manifest.version||"?";(0,H.execFile)(m,[...v,"-c","import paperforge; print(paperforge.__version__)"],{cwd:b,timeout:1e4},(C,F)=>{let R=this.plugin.settings.setup_complete,B=!C&&F?F.trim():null,N=B?`${i("runtime_health_plugin_ver").replace("{0}",k)} \u2192 ${i("runtime_health_package_ver").replace("{0}",B)}`:R?`Plugin v${k} \u2192 Python package not installed. Click "Sync Runtime" to install.`:`Plugin v${k} \u2192 Not configured. Please open the setup wizard first.`;u.setDesc(N),B===k?(d.setText(i("runtime_health_match")),d.className="paperforge-runtime-badge match",_&&_.setDisabled(!0)):B?(d.setText(i("runtime_health_mismatch")),d.className="paperforge-runtime-badge mismatch",_&&_.setDisabled(!1)):(d.setText(R?"Not installed":"Setup needed"),d.className="paperforge-runtime-badge missing",_&&_.setDisabled(!1))})}e.createEl("h3",{text:i("section_prep")}),e.createEl("p",{text:i("section_prep_desc"),cls:"paperforge-settings-desc"});let y=e.createEl("div",{cls:"paperforge-guide"}),x=[["prep_python","prep_python_desc"],["prep_zotero","prep_zotero_desc"],["prep_bbt","prep_bbt_desc"],["prep_key","prep_key_desc"]];for(let[b,m]of x){let v=y.createEl("div",{cls:"paperforge-guide-item"});v.createEl("strong",{text:i(b)}),v.createEl("span",{text:" \u2014 "+i(m)})}this._checkEl=e.createEl("div",{cls:"paperforge-message"});let f=!this.plugin.settings.setup_complete;new w.Setting(e).setName(i(f?"btn_install":"btn_reconfig")).setDesc(i(f?"btn_install_desc":"btn_reconfig_desc")).addButton(b=>{b.setButtonText(i(f?"btn_install":"btn_reconfig")).setCta().onClick(()=>{f?this._preCheck(()=>{new we(this.app,this.plugin).open()}):new we(this.app,this.plugin).open()})}),e.createEl("h3",{text:i("section_guide")});let E=e.createEl("div",{cls:"paperforge-guide"}),S=[["guide_open","guide_open_desc"],["guide_sync","guide_sync_desc"],["guide_ocr","guide_ocr_desc"]];for(let[b,m]of S){let v=E.createEl("div",{cls:"paperforge-guide-item"});v.createEl("strong",{text:i(b)}),v.createEl("span",{text:" \u2014 "+i(m)})}if(this.plugin.settings.setup_complete){e.createEl("h3",{text:i("section_config")});let b=e.createEl("div",{cls:"paperforge-summary"}),m=this.plugin.settings,v=this._pfConfig,k=[{label:i("dir_vault"),val:t},{label:i("dir_resources"),val:`${t}/${v==null?void 0:v.resources_dir}`},{label:" "+i("dir_notes"),val:`${t}/${v==null?void 0:v.resources_dir}/${v==null?void 0:v.literature_dir}`},{label:i("dir_base"),val:`${t}/${v==null?void 0:v.base_dir}`},{label:i("dir_system"),val:`${t}/${v==null?void 0:v.system_dir}`},{label:"API Key",val:m.paddleocr_api_key?i("api_key_set"):i("api_key_missing")},{label:i("field_zotero_data"),val:m.zotero_data_dir||i("not_set")}];for(let C of k){let F=b.createEl("div",{cls:"paperforge-summary-row"});F.createEl("span",{cls:"paperforge-summary-label",text:C.label}),F.createEl("span",{cls:"paperforge-summary-value",text:C.val})}}}_execMemoryStatus(e,t,s){(0,H.exec)(`"${e}" -m paperforge --vault "${t}" memory status --json`,{encoding:"utf-8",timeout:15e3},(n,a)=>{if(n){s("Status unavailable");return}try{let r=JSON.parse(a);if(r.ok){let o=r.data,l=o.fresh?"fresh":"stale";s(`Papers: ${o.paper_count_db} | ${l}${o.needs_rebuild?" - needs rebuild":""}`)}else s("DB not found. Run paperforge memory build.")}catch(r){s("Could not parse status.")}})}_execEmbedStatus(e,t,s){(0,H.exec)(`"${e}" -m paperforge --vault "${t}" embed status --json`,{encoding:"utf-8",timeout:15e3},(n,a)=>{if(n){s("Status unavailable");return}try{let r=JSON.parse(a);r.ok?s(`Chunks: ${r.data.chunk_count} | ${r.data.model} | ${r.data.mode}`):s("Could not parse status.")}catch(r){s("Could not parse status.")}})}_callPython(e,t){let s=this.app.vault.adapter.basePath,n=ae(s,this.plugin.settings),a=[...n.extraArgs,"-m","paperforge","--vault",s,...e];if(t&&t.stream){let r=(0,H.spawn)(n.path,a,{cwd:s,env:t.env||process.env,windowsHide:!0});return t.onData&&r.stdout.on("data",t.onData),t.onStderr&&r.stderr.on("data",t.onStderr),t.onError&&r.on("error",t.onError),r.on("close",t.onClose),r}return(0,H.execFile)(n.path,a,{cwd:s,timeout:t&&t.timeout||6e4},(r,o,l)=>{t&&t.onClose&&t.onClose(r?1:0,o,l)}),null}_renderMemoryStatusText(e,t,s){if(e.innerHTML="",e.createEl("span",{text:t,cls:"paperforge-memory-text"}).style.cssText="flex:1;",s==="syncing"){let r=e.createEl("span",{text:"Syncing...",cls:"paperforge-sync-status"});r.style.cssText="opacity:0.7; margin-right:8px;"}else if(s){let r=e.createEl("span",{text:s,cls:"paperforge-sync-status"});r.style.cssText="opacity:0.7; margin-right:8px;"}let n=e.createEl("button",{cls:"paperforge-rebuild-btn",text:i("feat_memory_rebuild_btn")});n.style.cssText="margin-left:auto; border:1px solid var(--background-modifier-border); background:var(--background-secondary); cursor:pointer; font-size:11px; padding:2px 6px; border-radius:3px; margin-right:4px;",n.title="Rebuild memory database",n.onclick=()=>{let r=this.app.vault.adapter.basePath,o=ae(r,this.plugin.settings);if(!o.path){new w.Notice(i("feat_no_python"));return}console.log("[PaperForge] Rebuilding memory:",o.path),n.setText(i("feat_memory_rebuilding")),n.setAttr("disabled",""),this._callPython(["memory","build"],{timeout:6e4,onClose:(l,c,p)=>{console.log("[PaperForge] memory build exit:",l?"FAIL:"+l:"OK",(c||"").slice(0,200),(p||"").slice(0,200)),n.setText(i("feat_memory_rebuild_btn")),n.removeAttribute("disabled"),l===0?new w.Notice(i("feat_memory_rebuild_done")):new w.Notice(i("feat_memory_rebuild_failed")+(p?" "+p.slice(0,80):"")),this._memoryStatusText=Fe(r),this._refreshSnapshots(r)}})};let a=e.createEl("button",{cls:"paperforge-refresh-btn",text:"\u21BB"});a.style.cssText="border:none; background:none; cursor:pointer; font-size:16px; padding:0 4px;",a.title="Sync now",a.onclick=()=>{this._memoryStatusText=null,this._runManualSync()}}_getBuildCommand(e){let t=this.app.vault.adapter.basePath,s=A(t,e,void 0,void 0);return s.path?`"${s.path}" -m paperforge --vault "${t}" sync`:null}_runManualSync(){let e=this.app.vault.adapter.basePath;if(!ae(e,this.plugin.settings).path)return;let s=document.querySelector(".paperforge-memory-status");s&&this._renderMemoryStatusText(s,"Checking...","syncing"),this.plugin._autoSyncRunning=!0,this._callPython(["sync"],{timeout:12e4,onClose:n=>{this.plugin._autoSyncRunning=!1,this._memoryStatusText=null,n===0&&(this._lastSyncTime=new Date().toLocaleTimeString(),this.plugin._lastSyncTime=this._lastSyncTime),this.display(),this._refreshSnapshots(e),Te(this.app,this.plugin,e)}})}_refreshSnapshots(e){let t=ae(e,this.plugin.settings),s=[...t.extraArgs,"-m","paperforge","--vault",e,"runtime-health","--json"];this._refreshPending=!0,(0,H.execFile)(t.path,s,{cwd:e,timeout:3e4,windowsHide:!0},(n,a,r)=>{this._refreshPending=!1,this._memoryStatusText=Fe(e),this._embedStatusText=fe(e),this.display()})}_renderFeaturesTab(e){e.createEl("h3",{text:"Skills"});let t=e.createEl("div",{cls:"paperforge-desc-box"});t.style.cssText="padding:8px 12px; margin:0 0 12px; background:var(--background-secondary); border-radius:4px; font-size:12px; color:var(--text-muted); line-height:1.5;",t.setText(i("feat_skills_desc")),t.createEl("br"),t.createEl("span",{text:i("feat_skills_system"),cls:""}).style.opacity="0.7";let s={opencode:"OpenCode",claude:"Claude Code",codex:"Codex",cursor:"Cursor",windsurf:"Windsurf",github_copilot:"GitHub Copilot"},n={opencode:".opencode/skills",claude:".claude/skills",codex:".codex/skills",cursor:".cursor/skills",windsurf:".windsurf/skills",github_copilot:".github/skills"},a=this.app.vault.adapter.basePath,r=this.plugin.settings.selected_skill_platform||"opencode";new w.Setting(e).setName(i("feat_agent_platform")).setDesc(i("feat_agent_platform_desc")).addDropdown(m=>{Object.entries(s).forEach(([v,k])=>m.addOption(v,k)),m.setValue(r).onChange(v=>{this.plugin.settings.selected_skill_platform=v,this.plugin.saveSettings(),this.display()})}).addExtraButton(m=>{m.setIcon("folder").setTooltip("Open skills folder").onClick(()=>{let v=n[r]||".opencode/skills",k=z.join(a,v);O.existsSync(k)?(0,H.exec)(`start "" "${k}"`):new w.Notice(`Skills folder not found: ${v}`)})});let o=z.join(a,n[r]),l=[],c=[];O.existsSync(o)&&O.readdirSync(o,{withFileTypes:!0}).forEach(m=>{if(!m.isDirectory())return;let v=z.join(o,m.name,"SKILL.md");if(!O.existsSync(v))return;let k=O.readFileSync(v,"utf-8"),C=k.match(/^name:\s*(.+)$/m),F=k.split(` -`),R=F.findIndex(K=>/^description:/.test(K)),B="";if(R>=0){let K=F[R].match(/^description:\s*(.+)$/);if(K&&K[1]&&K[1]!==">"&&K[1]!=="|-"&&K[1]!=="|")B=K[1].trim();else{for(let J=R+1;J<F.length&&(/^\s{2,}/.test(F[J])||F[J].trim()==="");J++)B+=F[J].trim()+" ";B=B.trim()}}let N=k.match(/^source:\s*(.+)$/m),re=k.match(/^disable-model-invocation:\s*(.+)$/m),L=k.match(/^version:\s*(.+)$/m),q={name:C?C[1].trim():m.name,desc:B,source:N?N[1].trim():"user",disabled:re&&re[1].trim()==="true",version:L?L[1].trim():"",path:v,content:k,dirName:m.name};q.source==="paperforge"?l.push(q):c.push(q)});let p=e.createEl("div");p.style.cssText="background:var(--background-secondary); border-radius:8px; padding:12px 12px 10px; margin:8px 0 16px;";let u=(m,v,k)=>{if(v.length===0)return;let C=p.createEl("div",{cls:"paperforge-skills-group"}),F=C.createEl("div",{cls:"paperforge-skills-collapse-header"}),R=C.createEl("div",{cls:"paperforge-skills-collapse-content"}),B=F.createEl("span",{text:"\u25BC",cls:"paperforge-skills-arrow"});B.style.cssText="display:inline-block; font-size:10px; margin-right:6px; transition:transform 0.2s; transform:rotate(0deg);",F.createEl("h4",{text:`${m} (${v.length})`,cls:"paperforge-skills-subheader"}),v.forEach(L=>{let q=L.name+(L.version?" v"+L.version:""),K=k?" [system]":" [user]",J=L.desc||"",oe=new w.Setting(R).setName(q+K).setDesc(J);oe.settingEl.style.opacity=L.disabled?"0.4":"1",oe.addToggle(ge=>{ge.setValue(!L.disabled).onChange(ye=>{let le=!ye,be=L.content.match(/^disable-model-invocation:\s*(.+)$/m)?L.content.replace(/^disable-model-invocation:\s*.+$/m,`disable-model-invocation: ${le}`):L.content.replace(/^(---\r?\n)/,`$1disable-model-invocation: ${le} -`);O.writeFileSync(L.path,be,"utf-8"),L.disabled=le,L.content=be,oe.settingEl.style.opacity=L.disabled?"0.4":"1"})})});let N=k?"system":"user";(this._skillsCollapsed[N]||!1)&&(R.style.display="none",B.style.transform="rotate(-90deg)"),F.addEventListener("click",()=>{R.style.display!=="none"?(R.style.display="none",B.style.transform="rotate(-90deg)"):(R.style.display="",B.style.transform="rotate(0deg)"),this._skillsCollapsed[N]=R.style.display==="none"})};u("System Skills",l,!0),u("User Skills",c,!1),l.length===0&&c.length===0&&p.createEl("p",{text:`No skills found in ${n[r]}. Run setup to deploy skills.`,cls:"setting-item-description"}),this._advCollapsed===void 0&&(this._advCollapsed=!0);let d=e.createEl("div",{cls:"paperforge-collapsible-header"});d.style.cssText="cursor:pointer; display:flex; align-items:center; gap:8px; padding:8px 0; user-select:none;";let _=d.createEl("span",{text:"\u25B6",cls:"paperforge-collapsible-arrow"});_.style.cssText="display:inline-block; transition:transform 0.2s; font-size:10px; transform:"+(this._advCollapsed?"rotate(0deg)":"rotate(90deg)")+";";let y=d.createEl("span",{text:"Advanced"});y.style.cssText="font-size:16px; font-weight:700; line-height:1.4;";let x=d.createEl("span",{text:"Memory + Vector DB + Embedding"});x.style.cssText="font-size:12px; color:var(--text-muted); margin-left:10px;";let f=e.createEl("div",{cls:"paperforge-collapsible-content"});f.style.display=this._advCollapsed?"none":"",d.addEventListener("click",()=>{this._advCollapsed=!this._advCollapsed,f.style.display=this._advCollapsed?"none":"",_.style.transform=this._advCollapsed?"rotate(0deg)":"rotate(90deg)"}),f.createEl("h4",{text:"Memory Layer"});let E=f.createEl("div",{cls:"paperforge-desc-box"});E.style.cssText="padding:8px 12px; margin:0 0 12px; background:var(--background-secondary); border-radius:4px; font-size:12px; color:var(--text-muted); line-height:1.5;",E.setText(i("feat_memory_desc"));let S=f.createEl("div",{cls:"paperforge-memory-status"});S.style.cssText="display:flex; align-items:center; padding:8px 12px; margin:8px 0; background:var(--background-secondary); border-radius:4px;";let b=this.app.vault.adapter.basePath;this.plugin._lastSyncTime&&!this._lastSyncTime&&(this._lastSyncTime=this.plugin._lastSyncTime),this._memoryStatusText===null&&(this._memoryStatusText=Fe(b)),this._renderMemoryStatusText(S,this._memoryStatusText,this._lastSyncTime),this._renderVectorSection(f)}_renderVectorSection(e){var l;e.createEl("h4",{text:"Vector Database"}),this.plugin.settings.features||(this.plugin.settings.features={memory_layer:!0,vector_db:!1});let t=e.createEl("div",{cls:"paperforge-desc-box"});if(t.style.cssText="padding:8px 12px; margin:0 0 8px; background:var(--background-secondary); border-radius:4px; font-size:12px; color:var(--text-muted); line-height:1.5;",t.setText(i("feat_vector_desc")),new w.Setting(e).setName(i("feat_vector_enable")).setDesc(i("feat_vector_enable_desc")).addToggle(c=>{c.setValue(!!this.plugin.settings.features.vector_db).onChange(p=>{this.plugin.settings.features.vector_db=p,this.plugin.saveSettings(),this._vectorDepsOk=null,this._embedStatusText=null,this.display()})}),!this.plugin.settings.features.vector_db)return;let s=this.app.vault.adapter.basePath,n=e.createEl("div",{cls:"paperforge-skills-collapse-header"});n.style.cssText="display:flex; align-items:center; cursor:pointer; padding:6px 0 2px; margin:0;";let a=n.createEl("span",{text:"\u25BC"});a.style.cssText="display:inline-block; font-size:10px; margin-right:6px; transition:transform 0.2s;",n.createEl("span",{text:i("feat_vector_config_label"),cls:""}).style.cssText="font-size:12px; color:var(--text-muted);";let r=e.createEl("div",{cls:"paperforge-vector-config"}),o=c=>{r.style.display=c?"none":"",a.style.transform=c?"rotate(-90deg)":"rotate(0deg)"};if(o(He(this._featurePanelsCollapsed,"vectorConfig",!1)),n.addEventListener("click",()=>{let c=ot(this._featurePanelsCollapsed,"vectorConfig",!1);o(c)}),this._vectorDepsOk===!0){this._renderVectorReady(r,s);return}if(this._vectorDepsOk===!1){this._renderVectorNoDeps(r);return}if(this._vectorDepsOk===null){let c=ke(s);this._vectorDepsOk=c&&(l=c.deps_installed)!=null?l:!1,this._vectorDepsOk&&(this._embedStatusText=fe(s)),this.display()}}_renderApiConfig(e){new w.Setting(e).setName(i("feat_openai_key")).setDesc(i("feat_openai_key_desc")).addText(t=>{t.setPlaceholder("sk-...").setValue(this.plugin.settings.vector_db_api_key||"").onChange(s=>{this.plugin.settings.vector_db_api_key=s,this.plugin.saveSettings()})}),new w.Setting(e).setName(i("feat_api_base_url")).setDesc(i("feat_api_base_url_desc")).addText(t=>{t.setPlaceholder("https://api.openai.com/v1").setValue(this.plugin.settings.vector_db_api_base||"").onChange(s=>{this.plugin.settings.vector_db_api_base=s,this.plugin.saveSettings()})}),new w.Setting(e).setName(i("feat_api_model")).setDesc(i("feat_api_model_desc")).addText(t=>{t.setPlaceholder("text-embedding-3-small").setValue(this.plugin.settings.vector_db_api_model||"text-embedding-3-small").onChange(s=>{this.plugin.settings.vector_db_api_model=s,this.plugin.saveSettings()})})}_renderVectorNoDeps(e){let t=e.createEl("div");t.style.cssText="padding:8px 12px; margin:8px 0; background:var(--background-secondary); border-radius:4px;",t.setText(i("feat_deps_missing")),new w.Setting(e).setName(i("feat_install_deps")).setDesc(i("feat_install_deps_desc")).addButton(s=>{s.setButtonText(i("feat_install_btn")).setCta().onClick(async()=>{let n=this.app.vault.adapter.basePath,a=ae(n,this.plugin.settings);if(!a.path){new w.Notice(i("feat_no_python"));return}s.setButtonText(i("feat_installing")),s.setDisabled(!0);let r="chromadb openai",o=new w.Notice(i("feat_installing_pkgs").replace("{pkgs}",r),0);try{let l=Object.assign({},process.env,{PYTHONIOENCODING:"utf-8",PYTHONUTF8:"1"}),c=r.split(" ");await new Promise((p,u)=>{(0,H.execFile)(a.path,[...a.extraArgs,"-m","pip","install",...c],{cwd:n,timeout:3e5,env:l,windowsHide:!0},d=>{d?u(d):p()})}),o.hide(),new w.Notice(i("feat_install_done")),this._vectorDepsOk=!0,this._embedStatusText=fe(n),this.display()}catch(l){o.hide(),new w.Notice(i("feat_install_failed")+(l.stderr||l.message||l)),s.setButtonText(i("feat_retry_btn")),s.setDisabled(!1)}})})}_renderVectorReady(e,t){let s=e.createEl("div");s.style.cssText="padding:8px 12px; margin:8px 0; background:var(--background-secondary); border-radius:4px;",s.setText(fe(t)),this._renderApiConfig(e);let n=e.createEl("div");n.style.cssText="padding:4px 0;";let a=n.createEl("div");a.style.cssText="display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;",a.createEl("span",{text:i("feat_rebuild_vectors"),cls:"setting-item-name"});let r=n.createEl("div");r.style.cssText="display:flex; align-items:center; gap:8px;";let o=n.createEl("div",{cls:"paperforge-embed-status-text"});(()=>{var y;r.empty(),o.empty();let c=(ke(t)||{}).build_state||{};this.plugin._embedProgress=this.plugin._embedProgress||{current:0,total:0,key:""},!this.plugin._embedProcess&&c.status==="running"&&(this.plugin._embedProgress={current:c.current||0,total:c.total||1,key:c.paper_id||""});let{current:p,total:u,key:d}=this.plugin._embedProgress;if(!!this.plugin._embedProcess||c.status==="running"){let x=r.createEl("div",{cls:"paperforge-progress-track"});x.style.cssText="flex:1;";let f=u>0?(p/u*100).toFixed(1):"0",E=x.createEl("div",{cls:"paperforge-progress-seg done"});if(E.style.cssText=`width:${f}%; min-width:${p>0?"2px":"0"};`,p<u){let b=x.createEl("div",{cls:"paperforge-progress-seg pending"});b.style.cssText=`width:${(100-parseFloat(f)).toFixed(1)}%;`}o.createEl("span",{cls:"paperforge-embed-progress-text",text:`${p}/${u} papers`}),d&&o.createEl("span",{cls:"paperforge-embed-progress-key",text:` (${d})`});let S=r.createEl("button");S.setText("Stop"),S.className="mod-warning",S.addEventListener("click",()=>{this._callPython(["embed","stop","--json"],{timeout:8e3}),this.plugin._embedProcess&&(this.plugin._embedProcess.kill(),this.plugin._embedProcess=null),this.display()})}else{let x=ke(t),f=!!(x&&((y=x.chunk_count)!=null?y:0)>0),E=x&&x.corrupted,S=m=>{if(!ae(t,this.plugin.settings).path){new w.Notice(i("feat_no_python"));return}let k=Object.assign({},process.env,{PYTHONIOENCODING:"utf-8",PYTHONUTF8:"1",VECTOR_DB_API_KEY:this.plugin.settings.vector_db_api_key||"",VECTOR_DB_API_BASE:this.plugin.settings.vector_db_api_base||"",VECTOR_DB_API_MODEL:this.plugin.settings.vector_db_api_model||""});this.plugin._embedStderr="",this.plugin._embedProgress={current:0,total:0,key:""},this.plugin._embedProcess=this._callPython(["embed","build",m],{stream:!0,env:k,onData:C=>{let F=C.toString("utf-8").split(` -`);for(let R of F)if(R.startsWith("EMBED_START:"))this.plugin._embedProgress.total=parseInt(R.split(":")[1])||0;else if(R.startsWith("EMBED_PROGRESS:")){let B=R.split(":");this.plugin._embedProgress.current=parseInt(B[1])||0,this.plugin._embedProgress.key=B[3]||""}else R.startsWith("EMBED_DONE")&&(this.plugin._embedProcess=null,this.plugin._embedProgress.current=this.plugin._embedProgress.total);this.display()},onStderr:C=>{this.plugin._embedStderr||(this.plugin._embedStderr=""),this.plugin._embedStderr+=C.toString("utf-8")},onError:C=>{this.plugin._embedProcess=null,new w.Notice(i("feat_build_failed")+": "+(C.message||C)),this.display()},onClose:C=>{if(this.plugin._embedProcess=null,C===0)this.plugin._embedProgress.current=this.plugin._embedProgress.total,this.plugin.saveSettings(),this._embedStatusText=fe(t),new w.Notice(i("feat_build_complete"));else{this._embedStatusText=null;let F=(this.plugin._embedStderr||"").slice(0,200);new w.Notice(i("feat_build_failed")+(F?": "+F:""),8e3)}this.plugin._embedStderr="",this.display(),this._refreshSnapshots(t)}}),this.display()};if(E){let m=n.createEl("div");m.style.cssText="padding:8px 12px; margin:8px 0; background:var(--background-modifier-warning); border-radius:4px; font-size:12px; display:flex; align-items:center; justify-content:space-between;",m.createEl("span",{text:i("feat_vector_corrupted")});let v=m.createEl("button",{text:i("feat_vector_rebuild_force_btn")});v.className="mod-cta",v.addEventListener("click",()=>S("--force"))}f&&!E&&r.createEl("span",{text:x.chunk_count+" chunks embedded",cls:"setting-item-description"});let b=r.createEl("button");if(b.setText(f?i("feat_rebuild_btn"):i("feat_build_btn")),b.addClass("mod-cta"),b.addEventListener("click",()=>S("--resume")),!E&&f){let m=r.createEl("button");m.setText(i("feat_vector_rebuild_force_btn")),m.style.marginLeft="6px",m.addEventListener("click",()=>S("--force"))}}})()}_getCurrentModelKey(){return this.plugin.settings.vector_db_api_model||"text-embedding-3-small"}_parseEmbedStatus(e){let t={};return e&&(e.split(` -`).forEach(s=>{let n=s.match(/^\s*([^:]+):\s*(.*)/);n&&(t[n[1].trim()]=n[2].trim())}),t.db_exists!==void 0&&(t.db_exists=t.db_exists==="True"),t.chunk_count!==void 0&&(t.chunk_count=parseInt(t.chunk_count,10)||0)),t}_getPythonDesc(e,t){return t==="stale"?`[!!] ${e} (stale \u2014 path no longer exists, update or clear the override below)`:t==="manual"?`${e} (manual)`:`${e} (auto-detected)`}_refreshPythonInterpDesc(e,t){let s=this._pythonInterpDescEl;s&&(t==="stale"?s.textContent=`[!!] ${e} (stale \u2014 path no longer exists, update or clear the override below)`:t==="manual"?s.textContent=`${e} (manual)`:s.textContent=`${e} (auto-detected)`)}_validatePythonOverride(){let e=this.plugin.settings.python_path?this.plugin.settings.python_path.trim():"",t=this._customPathDescEl;if(!e){let s="\u8BF7\u8F93\u5165\u8DEF\u5F84 / Enter a path first";t&&(t.innerHTML=`<span style="color:var(--text-error)">\u2717 ${s}</span>`),new w.Notice(s);return}if(!O.existsSync(e)){let s="\u8DEF\u5F84\u4E0D\u5B58\u5728 / Path does not exist";t&&(t.innerHTML=`<span style="color:var(--text-error)">\u2717 ${s}</span>`),new w.Notice(s,4e3);return}try{O.accessSync(e,O.constants.X_OK)}catch(s){let n="\u4E0D\u53EF\u6267\u884C / Not executable";t&&(t.innerHTML=`<span style="color:var(--text-error)">\u2717 ${n}</span>`),new w.Notice(n,4e3);return}(0,H.execFile)(e,["--version"],{timeout:8e3},(s,n)=>{if(s||!n){let l="\u65E0\u6CD5\u8FD0\u884C / Cannot run";t&&(t.innerHTML=`<span style="color:var(--text-error)">\u2717 ${l}</span>`),new w.Notice(l,4e3);return}let a=n.match(/Python (\d+)\.(\d+)/);if(!a){let l="\u65E0\u6CD5\u89E3\u6790\u7248\u672C / Cannot parse version";t&&(t.innerHTML=`<span style="color:var(--text-error)">\u2717 ${l}</span>`),new w.Notice(l,4e3);return}let r=parseInt(a[1],10),o=parseInt(a[2],10);if(r<3||r===3&&o<10){let l="Python \u7248\u672C\u8FC7\u4F4E\uFF0C\u9700\u8981 3.10+ / Python version too low, need 3.10+";t&&(t.innerHTML=`<span style="color:var(--text-error)">\u2717 ${l}</span>`),new w.Notice(l,4e3);return}(0,H.execFile)(e,["-m","pip","--version"],{timeout:8e3},l=>{if(l){let c=`\u2713 Python ${r}.${o} \u6709\u6548\uFF0C\u4F46\u672A\u68C0\u6D4B\u5230 pip / Valid, but pip not found`;t&&(t.innerHTML=`<span style="color:var(--text-warning)">\u26A0 ${c}</span>`),new w.Notice(c,4e3)}else{let c=`\u2713 Python ${r}.${o} \u6709\u6548 / Valid`;t&&(t.innerHTML=`<span style="color:var(--text-accent)">${c}</span>`),new w.Notice(c,4e3)}})})}_syncRuntime(e){let t=this.app.vault.adapter.basePath,{path:s,extraArgs:n=[]}=A(t,this.plugin.settings,void 0,void 0),a=this.plugin.manifest.version,r=st(s,a,n);e.setDisabled(!0),e.setButtonText(i("runtime_health_syncing"));let o=(c,p)=>(console.log(`[PaperForge] Sync Runtime: trying ${p}`),rt(r.cmd,c,t,r.timeout,void 0,de())),l=()=>{let c="opencode";try{let _=O.readFileSync(z.join(t,"paperforge.json"),"utf-8"),y=JSON.parse(_);y.agent_key&&(c=y.agent_key)}catch(_){}let p=[...n,"-c",'from paperforge.services.skill_deploy import deploy_skills; from pathlib import Path; r=deploy_skills(vault=Path(r"'+t.replace(/\\/g,"\\\\")+'"), agent_key="'+c+'", overwrite=True); print("skills deployed" if r["skill_deployed"] else "skills skipped", flush=True)'],u=(0,H.spawn)(s,p,{cwd:t,timeout:3e4,windowsHide:!0}),d="";u.stdout.on("data",_=>{d+=_.toString("utf-8")}),u.on("close",_=>{console.log(`[PaperForge] Skill deploy: ${d.trim()} (exit ${_})`)})};o(r.pypiArgs,"PyPI").then(c=>{if(c.exitCode===0){console.log("[PaperForge] Sync Runtime: installed via PyPI"),l(),new w.Notice(i("runtime_health_sync_done").replace("{0}",a),5e3),this.display();return}console.warn("[PaperForge] Sync Runtime: PyPI failed, falling back to git..."),o(r.gitArgs,"git").then(p=>{p.exitCode===0?(console.log("[PaperForge] Sync Runtime: installed via git"),l(),new w.Notice(i("runtime_health_sync_done").replace("{0}",a),5e3),this.display()):(e.setDisabled(!1),e.setButtonText(i("runtime_health_sync")),console.error("[PaperForge] git fallback stderr:",p.stderr),new w.Notice(i("runtime_health_sync_fail").replace("{0}","pip exit code "+p.exitCode),8e3))})})}_debouncedSave(){clearTimeout(this._saveTimeout),this._saveTimeout=setTimeout(()=>this.plugin.saveSettings(),500)}_preCheck(e){var a;let t=this.app.vault.adapter.basePath,{path:s,extraArgs:n=[]}=A(t,(a=this.plugin)==null?void 0:a.settings,void 0,void 0);(0,H.execFile)(s,[...n,"--version"],{timeout:8e3},(r,o)=>{let l=[];l.push({label:"Python",ok:!r,detail:r?i("check_python_fail"):o.trim()});let c=!1,p=process.env.HOME||process.env.USERPROFILE||dt.homedir()||"";if(process.platform==="darwin")c=["/Applications/Zotero.app",z.join(p,"Applications","Zotero.app")].some(E=>{try{return O.existsSync(E)}catch(S){return!1}});else if(process.platform==="win32"){let f=process.env.ProgramFiles||"",E=process.env.LOCALAPPDATA||"";c=[z.join(f,"Zotero"),z.join(f,"(x86)","Zotero"),z.join(E,"Programs","Zotero"),z.join(E,"Zotero"),z.join(p,"AppData","Local","Programs","Zotero")].filter(Boolean).some(b=>{try{return O.existsSync(b)}catch(m){return!1}})}else c=[z.join(p,".local","share","zotero","zotero"),"/usr/bin/zotero","/usr/local/bin/zotero"].some(E=>{try{return O.existsSync(E)}catch(S){return!1}});let u=this.plugin.settings.zotero_data_dir;if(!c&&u)try{c=O.existsSync(u)}catch(f){}l.push({label:"Zotero",ok:c,detail:c?i("check_zotero_ok"):i("check_zotero_fail")});let d=!1,_=process.env.APPDATA||"";process.platform==="win32"&&_&&(d=Ce(z.join(_,"Zotero","Zotero","Profiles"))),!d&&process.platform==="darwin"&&p&&(d=Ce(z.join(p,"Library","Application Support","Zotero","Profiles"))),!d&&process.platform!=="win32"&&process.platform!=="darwin"&&p&&(d=Ce(z.join(p,".zotero","zotero","Profiles"))),!d&&u&&String(u).trim()&&(d=ze(u.trim())),!d&&p&&(d=ze(z.join(p,"Zotero"))),l.push({label:"Better BibTeX",ok:d,detail:d?i("check_bbt_ok"):i("check_bbt_fail")});let y={true:"\u2713",false:"\u2717"};if(this._checkEl){this._checkEl.setText(l.map(E=>`${y[String(E.ok)]} ${E.label}: ${E.detail}`).join(` -`));let f=l.some(E=>!E.ok);this._checkEl.className=`paperforge-message msg-${f?"error":"ok"}`}let x=l.filter(f=>!f.ok);x.length>0&&new w.Notice(`[!!] \u672A\u901A\u8FC7: ${x.map(f=>f.label).join(", ")}`,6e3),e()})}};var D=require("obsidian"),ie=j(require("fs")),te=j(require("path")),he=require("child_process");var _e=class extends D.ItemView{constructor(e){super(e);this._currentMode=null;this._currentDomain=null;this._currentPaperKey=null;this._currentPaperEntry=null;this._currentFilePath=null;this._cachedItems=null;this._modeSubscribers=[];this._leafChangeTimer=null;this._ocrPrivacyShown=!1;this._cachedStats=null;this._techDetailsExpanded=!1;this._paperforgeVersion="";this._dashboardPermissions={};this._headerTitle=null;this._versionBadge=null;this._messageEl=null;this._metricsEl=null;this._ocrSection=null;this._ocrEmpty=null;this._ocrBadge=null;this._ocrTrack=null;this._ocrCounts=null;this._driftBannerEl=null;this._currentMode=null,this._currentDomain=null,this._currentPaperKey=null,this._currentPaperEntry=null,this._currentFilePath=null,this._cachedItems=null,this._modeSubscribers=[],this._leafChangeTimer=null,this._ocrPrivacyShown=!1}getViewType(){return pe}getDisplayText(){return"PaperForge"}getIcon(){return xe}async onOpen(){this._buildPanel(),this._modeSubscribers=[],this._leafChangeTimer=null,this._setupEventSubscriptions(),this._fetchVersion(),this._detectAndSwitch()}async onClose(){if(this._modeSubscribers&&this._modeSubscribers.length>0){for(let e of this._modeSubscribers)e.event==="active-leaf-change"?this.app.workspace.off("active-leaf-change",e.ref):e.event==="modify"&&this.app.vault.off("modify",e.ref);this._modeSubscribers=[]}this._leafChangeTimer&&(clearTimeout(this._leafChangeTimer),this._leafChangeTimer=null),this._cachedItems=null,this._cachedStats=null}_buildPanel(){let e=this.containerEl;e.empty(),e.addClass("paperforge-status-panel");let t=e.createEl("div",{cls:"paperforge-header"}),s=t.createEl("div",{cls:"paperforge-header-left"});s.createEl("div",{cls:"paperforge-header-logo",text:"P"}),this._modeContextEl=s.createEl("div",{cls:"paperforge-mode-context"}),this._headerTitle=s.createEl("h3",{cls:"paperforge-header-title",text:"PaperForge"}),this._versionBadge=s.createEl("span",{cls:"paperforge-header-badge",text:"v\u2014"});let n=t.createEl("button",{cls:"paperforge-header-refresh",attr:{"aria-label":"Refresh"}});n.innerHTML="\u21BB",n.addEventListener("click",()=>{this._invalidateIndex(),this._detectAndSwitch()}),this._messageEl=e.createEl("div",{cls:"paperforge-message"}),this._contentEl=e.createEl("div",{cls:"paperforge-content-area"})}_fetchVersion(){var r,o;let e=this.app.vault.adapter.basePath,t=this.app.plugins.plugins.paperforge,s=((r=t==null?void 0:t.manifest)==null?void 0:r.version)||"?",{path:n,extraArgs:a=[]}=A(e,(o=t==null?void 0:t.settings)!=null?o:null,void 0,void 0);tt(n,s,e,1e4,void 0).then(l=>{if(l.status==="not-installed")return;let c=l.pyVersion||"";this._paperforgeVersion=c.startsWith("v")?c:"v"+c,this._versionBadge&&this._versionBadge.setText(this._paperforgeVersion),this._driftBannerEl&&s&&this._paperforgeVersion!=="v"+s.replace(/^v/,"")?(this._driftBannerEl.style.display="block",this._driftBannerEl.setText(i("dashboard_drift_warning").replace("{0}",this._paperforgeVersion).replace("{1}","v"+s.replace(/^v/,"")))):this._driftBannerEl&&(this._driftBannerEl.style.display="none")})}_fetchStats(e){var r;if(!this._metricsEl)return;if(!e&&!this._cachedStats)this._metricsEl.empty(),this._metricsEl.createEl("div",{cls:"paperforge-status-loading",text:"Loading..."});else if(e&&!this._cachedStats)return;let t=this.app.vault.adapter.basePath,s=this.app.plugins.plugins.paperforge,{path:n,extraArgs:a=[]}=A(t,(r=s==null?void 0:s.settings)!=null?r:null,void 0,void 0);(0,he.execFile)(n,[...a,"-m","paperforge","dashboard","--json"],{cwd:t,timeout:3e4},(o,l)=>{if(!o)try{let c=JSON.parse(l);if(c.ok&&c.data){let p=this._normalizeDashboardData(c.data);this._cachedStats=p,this._metricsEl.empty(),this._renderStats(p),this._renderOcr(p),this._dashboardPermissions=c.data.permissions||{};return}}catch(c){}this._fallbackFetchStats(e,t,s)})}_normalizeDashboardData(e){let t=e.stats||{},s=t.ocr_health||{},n=t.pdf_health||{},a=(s.done||0)+(s.pending||0)+(s.failed||0);return{total_papers:t.papers||0,formal_notes:t.papers||0,exports:0,bases:0,ocr:{total:a,pending:s.pending||0,processing:0,done:s.done||0,failed:s.failed||0},path_errors:(n.broken||0)+(n.missing||0)}}_fallbackFetchStats(e,t,s){var r,o,l;let n=((r=s==null?void 0:s.settings)==null?void 0:r.system_dir)||"System",a=te.join(t,n,"PaperForge","indexes","formal-library.json");try{let c=ie.readFileSync(a,"utf-8"),p=JSON.parse(c),u=p.items||[],d={},_={pdf_health:{healthy:0,unhealthy:0},ocr_health:{healthy:0,unhealthy:0},note_health:{healthy:0,unhealthy:0},asset_health:{healthy:0,unhealthy:0}},y=0,x=0,f=0,E=0,S=0,b=0;for(let m of u){m.note_path&&b++;let v=m.lifecycle||"pdf_ready";d[v]=(d[v]||0)+1;let k=m.health||{};for(let F of["pdf_health","ocr_health","note_health","asset_health"])(k[F]||"healthy")==="healthy"?_[F].healthy++:_[F].unhealthy++;let C=m.ocr_status||"";y++,C==="done"?x++:C==="pending"?f++:C==="processing"||C==="queued"||C==="running"?E++:S++}this._cachedStats={version:p.paperforge_version||((o=this._cachedStats)==null?void 0:o.version)||"\u2014",total_papers:u.length,formal_notes:b,exports:0,bases:0,ocr:{total:y,pending:f,processing:E,done:x,failed:S},path_errors:0,lifecycle_level_counts:d,health_aggregate:_},this._metricsEl.empty(),this._renderStats(this._cachedStats),this._renderOcr(this._cachedStats)}catch(c){!e&&!this._cachedStats&&this._metricsEl.createEl("div",{cls:"paperforge-status-loading",text:"No index \u2014 trying CLI..."});let{path:p,extraArgs:u=[]}=A(t,(l=s==null?void 0:s.settings)!=null?l:null,void 0,void 0);(0,he.execFile)(p,[...u,"-m","paperforge","status","--json"],{cwd:t,timeout:3e4},(d,_)=>{if(d){if(this._cachedStats)return;this._metricsEl.createEl("div",{cls:"paperforge-status-error",text:`Cannot reach PaperForge CLI. -Make sure paperforge is installed and in your PATH.`});return}try{let y=JSON.parse(_);this._cachedStats=y,this._metricsEl.empty(),this._renderStats(y),this._renderOcr(y)}catch(y){this._cachedStats||this._metricsEl.createEl("div",{cls:"paperforge-status-error",text:"Invalid response from paperforge status."})}})}}_renderSkeleton(e){e.addClass("paperforge-loading")}_renderEmptyState(e,t){e.createEl("div",{cls:"paperforge-empty-state",text:t||"No data"})}_buildMetricBar(e,t,s){if(s<=0)return;let n=Math.min(100,t/s*100);e.createEl("div",{cls:"paperforge-metric-progress"}).createEl("div",{cls:"paperforge-metric-progress-fill",attr:{style:`width:${n.toFixed(1)}%`}})}_loadIndex(){var a;let e=this.app.vault.adapter.basePath,t=this.app.plugins.plugins.paperforge,s=((a=t==null?void 0:t.settings)==null?void 0:a.system_dir)||"System",n=te.join(e,s,"PaperForge","indexes","formal-library.json");try{let r=ie.readFileSync(n,"utf-8");return JSON.parse(r)}catch(r){return null}}_getCachedIndex(){if(!this._cachedItems){let e=this._loadIndex();this._cachedItems=e?e.items||[]:[]}return this._cachedItems}_findEntry(e){if(!e)return null;let t=this._getCachedIndex().find(s=>s.zotero_key===e)||null;return Xe(this.app,t)}_patchCachedEntry(e,t){if(!e||!this._cachedItems)return;let s=this._cachedItems.findIndex(n=>n.zotero_key===e);s!==-1&&(this._cachedItems[s]=Oe(this._cachedItems[s],t))}_filterByDomain(e){return e?this._getCachedIndex().filter(t=>t.domain===e):[]}_renderStats(e){var a;if(this._versionBadge&&this._versionBadge.setText(this._paperforgeVersion||(e.version?"v"+e.version:"v\u2014")),!e||typeof e.total_papers=="undefined"){this._metricsEl&&this._renderSkeleton(this._metricsEl);return}if(!this._metricsEl)return;this._metricsEl.removeClass("paperforge-loading");let t=e.total_papers||0,s=e.formal_notes||0,n=[{value:t,label:"Papers",color:"var(--color-cyan)",barMax:0},{value:s,label:"Formal Notes",color:"var(--color-blue)",barMax:t},{value:e.exports||0,label:"Exports",color:"var(--color-purple)",barMax:0}];for(let r of n){let o=this._metricsEl.createEl("div",{cls:"paperforge-metric-card"});o.style.setProperty("--metric-color",r.color),o.createEl("div",{cls:"paperforge-metric-value",text:((a=r.value)==null?void 0:a.toString())||"\u2014"}),o.createEl("div",{cls:"paperforge-metric-label",text:r.label}),r.barMax>0&&this._buildMetricBar(o,r.value,r.barMax)}}_renderOcr(e){if(!this._ocrSection)return;let t=e.ocr||{},s=t.total||0;if(s===0){this._ocrSection.style.display="none";return}this._ocrSection.style.display="block",this._ocrEmpty&&(this._ocrEmpty.style.display="none");let n=t.done||0,a=t.pending||0,r=t.processing||0,o=t.failed||0;if(this._ocrBadge&&(this._ocrBadge.removeClass("active","idle"),r>0?(this._ocrBadge.addClass("active"),this._ocrBadge.setText("Processing")):a>0?(this._ocrBadge.addClass("idle"),this._ocrBadge.setText("Pending")):(this._ocrBadge.addClass("idle"),this._ocrBadge.setText("Idle"))),this._ocrTrack){this._ocrTrack.empty(),r>0?this._ocrTrack.addClass("paperforge-processing"):this._ocrTrack.removeClass("paperforge-processing");let l=[{cls:"pending",count:a},{cls:"active",count:r},{cls:"done",count:n},{cls:"failed",count:o}];for(let c of l)if(c.count>0){let p=(c.count/s*100).toFixed(1);this._ocrTrack.createEl("div",{cls:`paperforge-progress-seg ${c.cls}`,attr:{style:`width:${p}%`}})}}if(this._ocrCounts){this._ocrCounts.empty();let l=[{cls:"pending",value:a,label:"Pending"},{cls:"active",value:r,label:"Processing"},{cls:"done",value:n,label:"Done"},{cls:"failed",value:o,label:"Failed"}];for(let c of l){let p=this._ocrCounts.createEl("div",{cls:"paperforge-ocr-count"});p.createEl("div",{cls:"paperforge-ocr-count-value",text:c.value.toString()}),p.createEl("div",{cls:"paperforge-ocr-count-label",text:c.label})}}}_renderLifecycleStepper(e,t,s){if(!t||!s){this._renderSkeleton(e);return}let n=[{key:"indexed",label:"Indexed"},{key:"pdf_ready",label:"PDF Ready"},{key:"fulltext_ready",label:"Fulltext Ready"},{key:"deep_read_done",label:"Deep Read"}],a=e.createEl("div",{cls:"paperforge-lifecycle-stepper"}),r=!1;for(let o of n){let l=a.createEl("div",{cls:"step"});l.createEl("div",{cls:"step-indicator"}),l.createEl("div",{cls:"step-label",text:o.label}),o.key===s?(l.addClass("current"),r=!0):r?l.addClass("pending"):l.addClass("completed")}}_renderHealthMatrix(e,t){if(!t){this._renderSkeleton(e);return}let s=[{key:"pdf_health",label:"PDF Health",iconOk:"\u2713",iconWarn:"\u26A0",iconFail:"\u2717"},{key:"ocr_health",label:"OCR Health",iconOk:"\u2713",iconWarn:"\u26A0",iconFail:"\u2717"},{key:"note_health",label:"Note Health",iconOk:"\u2713",iconWarn:"\u26A0",iconFail:"\u2717"},{key:"asset_health",label:"Asset Health",iconOk:"\u2713",iconWarn:"\u26A0",iconFail:"\u2717"}],n=e.createEl("div",{cls:"paperforge-health-matrix"});for(let a of s){let r=t[a.key]||"healthy",o=n.createEl("div",{cls:"paperforge-health-cell"}),l,c,p;r==="healthy"||r==="ok"?(l=a.iconOk,c="ok",p=`${a.label}: OK`):r==="warn"||r==="warning"||r==="degraded"?(l=a.iconWarn,c="warn",p=`${a.label}: Needs Attention`):(l=a.iconFail,c="fail",p=`${a.label}: Failed`),o.addClass(c),o.setAttribute("title",p),o.createEl("div",{cls:"paperforge-health-cell-icon",text:l}),o.createEl("div",{cls:"paperforge-health-cell-label",text:a.label})}}_renderMaturityGauge(e,t,s){if(t==null||t===void 0){this._renderSkeleton(e);return}let n=e.createEl("div",{cls:"paperforge-maturity-gauge"}),a=n.createEl("div",{cls:"gauge-track"}),r=4,o=Math.max(1,Math.min(r,Math.round(t)));for(let l=1;l<=r;l++){let c=a.createEl("div",{cls:"gauge-segment"});l<=o&&(c.addClass("filled"),c.addClass(`level-${l}`))}if(n.createEl("div",{cls:"gauge-level",text:`Level ${o} / ${r}`}),o<r&&s){let l=typeof s=="string"?[s]:s;if(l.length>0){let c=n.createEl("ul",{cls:"gauge-blockers"});for(let p of l)c.createEl("li",{text:p})}}}_renderBarChart(e,t){if(!t||Object.keys(t).length===0){this._renderEmptyState(e,"No lifecycle data");return}let s=[{key:"indexed",label:"Indexed",cls:"stage-indexed"},{key:"pdf_ready",label:"PDF Ready",cls:"stage-pdf-ready"},{key:"fulltext_ready",label:"Fulltext Ready",cls:"stage-fulltext-ready"},{key:"deep_read_done",label:"Deep Read",cls:"stage-deep-read"}],n=e.createEl("div",{cls:"paperforge-bar-chart"}),a=Math.max(1,...s.map(r=>t[r.key]||0));for(let r of s){let o=t[r.key]||0,l=o/a*100,c=n.createEl("div",{cls:"bar-row"});c.createEl("div",{cls:"bar-label",text:r.label}),c.createEl("div",{cls:"bar-track"}).createEl("div",{cls:`bar-fill ${r.cls}`,attr:{style:`width:${l.toFixed(1)}%`}}),c.createEl("div",{cls:"bar-count",text:o.toString()})}}_invalidateIndex(){this._cachedItems=null}_extractZoteroKeyFromPath(e){if(!e)return null;let t=te.dirname(e);for(;;){let s=te.basename(t);if(!s||s===".")break;let n=s.match(/^([A-Z0-9]{8})(?:\s*-\s*)/i);if(n)return n[1];let a=te.dirname(t);if(a===t)break;t=a}return null}_resolveModeForFile(e){if(!e)return{mode:"global",filePath:null,key:null,domain:null};let t=e.extension,s=e.path;if(t==="base")return{mode:"collection",filePath:s,key:null,domain:e.basename.trim()};if(t==="md"){let a=this.app.metadataCache.getFileCache(e),r=a&&a.frontmatter&&a.frontmatter.zotero_key;if(r)return{mode:"paper",filePath:s,key:r,domain:null}}if(t==="pdf"){let a=this._getCachedIndex();for(let r of a){let o=(r.pdf_path||"").match(/\[\[([^\]]+)\]\]/);if((o?o[1]:r.pdf_path)===s)return{mode:"paper",filePath:s,key:r.zotero_key,domain:null}}}let n=this._extractZoteroKeyFromPath(s);return n?{mode:"paper",filePath:s,key:n,domain:null}:{mode:"global",filePath:s,key:null,domain:null}}_detectAndSwitch(){let e=this._resolveModeForFile(this.app.workspace.getActiveFile());this._currentDomain=e.domain||null,this._currentPaperKey=e.key||null,this._currentPaperEntry=e.key?this._findEntry(e.key):null,this._switchMode(e.mode,e.filePath)}_switchMode(e,t){if(this._currentMode===e&&this._currentFilePath===t){this._refreshCurrentMode();return}if(this._currentMode=e,this._currentFilePath=t,this._techDetailsExpanded=!1,!!this._contentEl)switch(this._contentEl.empty(),this._contentEl.removeClass("switching"),this._renderModeHeader(e),e){case"global":this._renderGlobalMode();break;case"paper":this._renderPaperMode();break;case"collection":this._renderCollectionMode();break}}_renderGlobalMode(){var Be,be,Ke,Ze,We,qe,Je;if(!this._contentEl)return;let e=this._contentEl.createEl("div",{cls:"paperforge-global-view"});this._driftBannerEl=e.createEl("div",{cls:"paperforge-drift-banner"}),this._driftBannerEl.style.display="none";let t=this._getCachedIndex(),s=t.length,n=0,a=0,r=0;for(let P of t)P.has_pdf&&n++,P.ocr_status==="done"&&a++,P.deep_reading_status==="done"&&r++;let o=e.createEl("div",{cls:"paperforge-library-snapshot"});o.createEl("div",{cls:"paperforge-section-label",text:"Library Snapshot"});let l=o.createEl("div",{cls:"paperforge-snapshot-pills"}),c=[{value:s,label:"papers"},{value:n,label:"PDFs ready"},{value:a,label:"OCR done"},{value:r,label:"deep-read done"}];for(let P of c){let T=l.createEl("div",{cls:"paperforge-snapshot-pill"});T.createEl("span",{cls:"paperforge-snapshot-value",text:String(P.value)}),T.createEl("span",{cls:"paperforge-snapshot-label",text:" "+P.label})}let p=e.createEl("div",{cls:"paperforge-system-status"});p.createEl("div",{cls:"paperforge-section-label",text:"System Status"});let u=p.createEl("div",{cls:"paperforge-status-grid"}),d=this.app.plugins.plugins.paperforge,_=((Be=d==null?void 0:d.manifest)==null?void 0:Be.version)||"?",y=this._paperforgeVersion;if(!y)try{let P=this.app.vault.adapter.basePath,{path:T,extraArgs:G=[]}=A(P,(be=d==null?void 0:d.settings)!=null?be:null,void 0,void 0),$=(0,he.execFileSync)(T,[...G,"-c","import paperforge; print(paperforge.__version__)"],{cwd:P,timeout:5e3,encoding:"utf-8",windowsHide:!0}).trim();$&&(y=$.startsWith("v")?$:"v"+$,this._paperforgeVersion=y)}catch(P){}y=y||"\u2014";let x=y==="v"+_;this._renderSystemStatusRow(u,"Runtime",x?"healthy":"mismatch",x?"v"+_:"plugin v"+_+" \u2260 CLI "+y);let f=this._loadIndex(),E=f&&f.items&&f.items.length>0;this._renderSystemStatusRow(u,"Index",E?"healthy":"missing",E?f.items.length+" entries":"formal-library.json not found");let S=((Ke=d==null?void 0:d.settings)==null?void 0:Ke.system_dir)||"System",b=this.app.vault.adapter.basePath,m=!1,v="No exports found";try{let P=te.join(b,S,"PaperForge","exports");if(ie.existsSync(P)){let T=ie.readdirSync(P).filter(G=>G.endsWith(".json"));m=T.length>0,v=m?T.length+" export(s)":"No JSON exports"}}catch(P){}this._renderSystemStatusRow(u,"Zotero Export",m?"healthy":"missing",v);let k=!!((Ze=d==null?void 0:d.settings)!=null&&Ze.paddleocr_api_key);if(!k)try{let P=((We=d==null?void 0:d.settings)==null?void 0:We.system_dir)||"System",T=te.join(b,P,"PaperForge",".env");if(ie.existsSync(T)){let $=ie.readFileSync(T,"utf-8").match(/^PADDLEOCR_API_TOKEN\s*=\s*(.+)$/m);k=!!($&&$[1]&&$[1].trim())}}catch(P){}k||(k=!!(process.env.PADDLEOCR_API_TOKEN||process.env.PADDLEOCR_API_KEY||process.env.OCR_TOKEN)),this._renderSystemStatusRow(u,"OCR Token",k?"configured":"missing",k?"Configured":"Not set");let C=!1,F="",R=this.app.vault.adapter.basePath,B=je(R);C=it(R),F=B&&((qe=B.summary)==null?void 0:qe.reason)||B&&((Je=B.summary)==null?void 0:Je.status)||"Unknown",this._renderSystemStatusRow(u,"Memory Layer",C?"healthy":"fail",F);let N=!x&&y!=="\u2014";if(N||!E||!m||!k){let P=e.createEl("div",{cls:"paperforge-issue-summary"});P.createEl("div",{cls:"paperforge-section-label",text:"\u9700\u8981\u5904\u7406"});let T=P.createEl("div",{cls:"paperforge-issue-list"});N&&T.createEl("div",{cls:"paperforge-issue-item",text:"Runtime version mismatch"}),E||T.createEl("div",{cls:"paperforge-issue-item",text:"Index missing or corrupted"}),m||T.createEl("div",{cls:"paperforge-issue-item",text:"No Zotero export found"}),k||T.createEl("div",{cls:"paperforge-issue-item",text:"PaddleOCR API key not configured"});let G=P.createEl("div",{cls:"paperforge-issue-actions"}),$=G.createEl("button",{cls:"paperforge-contextual-btn"});$.createEl("span",{text:"Run Doctor"}),$.addEventListener("click",()=>{let ve=W.find(Ae=>Ae.id==="paperforge-doctor");ve&&this._runAction(ve,$)});let ce=G.createEl("button",{cls:"paperforge-contextual-btn"});ce.createEl("span",{text:"Repair Issues"}),ce.addEventListener("click",()=>{let ve=W.find(Ae=>Ae.id==="paperforge-repair");ve&&this._runAction(ve,ce)})}let L=e.createEl("div",{cls:"paperforge-global-actions"});L.createEl("div",{cls:"paperforge-section-label",text:"Start Working"});let q=L.createEl("div",{cls:"paperforge-global-actions-row"}),K=q.createEl("button",{cls:"paperforge-contextual-btn primary"});K.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u{1F4C1}"}),K.createEl("span",{text:"Open Literature Hub"}),K.addEventListener("click",()=>{var G;let P=((G=d==null?void 0:d.settings)==null?void 0:G.base_dir)||"Bases",T=this.app.vault.getAbstractFileByPath(P);if(T){let $=null;if(T.children&&($=T.children.find(ce=>ce.extension==="base")),$){let ce=this.app.workspace.getLeaf(!1);ce&&ce.openFile($)}else new D.Notice("[!!] No .base file found in "+P,6e3)}else new D.Notice("[!!] Base directory not found: "+P,6e3)});let J=q.createEl("button",{cls:"paperforge-contextual-btn"});J.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u21BB"}),J.createEl("span",{text:"Sync Library"}),J.addEventListener("click",()=>{let P=W.find(T=>T.id==="paperforge-sync");P&&this._runAction(P,J)});let oe=q.createEl("button",{cls:"paperforge-contextual-btn"});oe.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u229E"}),oe.createEl("span",{text:"Run OCR"}),oe.addEventListener("click",()=>{let P=W.find(T=>T.id==="paperforge-ocr");P&&this._runAction(P,oe)});let ge=q.createEl("button",{cls:"paperforge-contextual-btn"});ge.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u2695"}),ge.createEl("span",{text:"Run Doctor"}),ge.addEventListener("click",()=>{let P=W.find(T=>T.id==="paperforge-doctor");P&&this._runAction(P,ge)});let ye=q.createEl("button",{cls:"paperforge-contextual-btn"});ye.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u2139"}),ye.createEl("span",{text:"Refresh Status"}),ye.addEventListener("click",()=>{this._fetchStats(!1)});let le=q.createEl("button",{cls:"paperforge-contextual-btn"});le.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u21BA"}),le.createEl("span",{text:"Repair Issues"}),le.addEventListener("click",()=>{let P=W.find(T=>T.id==="paperforge-repair");P&&this._runAction(P,le)})}_renderSystemStatusRow(e,t,s,n){let a=e.createEl("div",{cls:"paperforge-status-row"});a.createEl("span",{cls:"paperforge-status-dot"}).addClass(s==="healthy"||s==="configured"?"ok":"fail"),a.createEl("span",{cls:"paperforge-status-label",text:t}),a.createEl("span",{cls:"paperforge-status-detail",text:n||""})}_renderPaperMode(){let e=this._currentPaperEntry,t=this._currentPaperKey;if(!this._contentEl)return;if(!t){this._renderEmptyState(this._contentEl,"No paper data available.");return}if(!e){this._contentEl.createEl("div",{cls:"paperforge-content-placeholder",text:'Paper "'+t+'" not found in canonical index. Sync first.'});return}let s=this._contentEl.createEl("div",{cls:"paperforge-paper-view"}),n=s.createEl("div",{cls:"paperforge-paper-header"});n.createEl("div",{cls:"paperforge-paper-title",text:e.title||"Untitled"});let a=n.createEl("div",{cls:"paperforge-paper-meta"});e.authors&&e.authors.length>0&&a.createEl("span",{cls:"paperforge-paper-authors",text:e.authors.join(", ")}),e.year&&a.createEl("span",{cls:"paperforge-paper-year",text:String(e.year)});let r=s.createEl("div",{cls:"paperforge-status-strip"}),o=r.createEl("div",{cls:"paperforge-status-strip-left"}),l=r.createEl("div",{cls:"paperforge-status-strip-right"}),c=[{key:"pdf",label:"PDF",ok:e.has_pdf===!0},{key:"ocr",label:"OCR",ok:e.ocr_status==="done",pending:["pending","queued","processing"].includes(e.ocr_status||""),fail:["failed","blocked","done_incomplete","nopdf"].includes(e.ocr_status||"")},{key:"deep",label:"\u7CBE\u8BFB",ok:e.deep_reading_status==="done"}];for(let p of c){let u=o.createEl("span",{cls:"paperforge-status-pill"}),d="pending";p.ok?d="ok":p.fail?d="fail":p.pending&&(d="pending"),u.addClass(d);let _=p.ok?"\u2713":p.fail?"\u2717":"\u25CB";u.createEl("span",{cls:"paperforge-status-pill-icon",text:_}),u.createEl("span",{text:" "+p.label})}if(e.pdf_path){let p=l.createEl("button",{cls:"paperforge-contextual-btn"});p.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u{1F4C4}"}),p.createEl("span",{text:"\u6253\u5F00 PDF"}),p.addEventListener("click",()=>{let u=e.pdf_path.match(/\[\[([^\]]+)\]\]/),d=u?u[1]:e.pdf_path;this.app.vault.getAbstractFileByPath(d)?this.app.workspace.openLinkText(d,""):new D.Notice("[!!] PDF not found: "+d,6e3)})}if(e.fulltext_path){let p=l.createEl("button",{cls:"paperforge-contextual-btn"});p.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u{1F4DD}"}),p.createEl("span",{text:"\u6253\u5F00\u5168\u6587"}),p.addEventListener("click",()=>this._openFulltext(e.fulltext_path))}if(this._renderPaperOverviewCard(s,e),e.next_step==="ready"&&e.deep_reading_status==="done"){let p=s.createEl("div",{cls:"paperforge-complete-row"});p.createEl("span",{text:"\u2713"}),p.createEl("span",{text:"\u5DF2\u5B8C\u6210\uFF0C\u53EF\u76F4\u63A5\u4F7F\u7528"})}else this._renderNextStepCard(s,e,t);this._renderRecentDiscussionCard(s,e),this._renderPaperTechnicalDetails(s,e)}_renderPaperOverviewCard(e,t){let s=e.createEl("div",{cls:"paperforge-paper-overview"});s.createEl("div",{cls:"paperforge-paper-overview-header"}).createEl("span",{cls:"paperforge-paper-overview-title",text:"\u6587\u7AE0\u6982\u89C8"});let a=s.createEl("div",{cls:"paperforge-paper-overview-body"}),r=a.createEl("div",{cls:"paperforge-paper-overview-excerpt",text:"\u52A0\u8F7D\u4E2D..."});if(t.note_path){let o=this.app.vault.getAbstractFileByPath(t.note_path);o?this.app.vault.read(o).then(l=>{let c=this._extractOverviewFromNote(l);if(c){let p=c.length>200?c.slice(0,200)+"...":c;if(r.setText(p),c.length>200){let u=a.createEl("div",{cls:"paperforge-expand-container"}),d=u.createEl("button",{cls:"paperforge-expand-icon",title:"\u5C55\u5F00/\u6536\u8D77"});d.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>';let _=!1;u.addEventListener("click",()=>{r.setText(_?p:c),d.innerHTML=_?'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>':'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>',_=!_})}}else r.setText("\u5C1A\u672A\u751F\u6210\u6587\u7AE0\u6982\u89C8\u3002\u8FD0\u884C /pf-deep \u5F00\u59CB\u7CBE\u8BFB\u3002")}).catch(()=>{r.setText("\u65E0\u6CD5\u8BFB\u53D6\u7B14\u8BB0\u5185\u5BB9")}):r.setText("\u7B14\u8BB0\u6587\u4EF6\u4E0D\u5B58\u5728")}else r.setText("\u5C1A\u672A\u751F\u6210\u6587\u7AE0\u6982\u89C8")}_extractOverviewFromNote(e){if(!e)return null;let t=e.indexOf("## \u{1F50D} \u7CBE\u8BFB");if(t===-1)return null;let s=e.slice(t),n=["**\u4E00\u53E5\u8BDD\u603B\u89C8:**","**\u4E00\u53E5\u8BDD\u603B\u89C8**","**\u6587\u7AE0\u6458\u8981:**","**\u6587\u7AE0\u6458\u8981**"];for(let o of n){let l=s.indexOf(o);if(l!==-1){let c=s.slice(l+o.length),p=["**5 Cs","**Figure","**\u8BC1\u636E","### Pass 2","## "],u=c.length;for(let y of p){let x=c.indexOf(y);x!==-1&&x<u&&(u=x)}let d=c.indexOf(` + .paperforge-release-card { border: 1px solid var(--background-modifier-border); border-radius: 6px; padding: 12px; margin-bottom: 12px; } + .paperforge-release-header { margin-bottom: 8px; } + .paperforge-release-date { color: var(--text-muted); font-size: 12px; } + .paperforge-release-section { margin-bottom: 6px; } + .paperforge-release-label { font-weight: 600; color: var(--text-accent); margin-bottom: 2px; font-size: 13px; } + .paperforge-release-item { font-size: 13px; margin-left: 8px; color: var(--text-muted); } + .paperforge-manual-links { margin-top: 8px; } + .paperforge-manual-links a { color: var(--text-accent); } + .paperforge-modal-subtitle { color: var(--text-muted); font-size: 13px; margin-bottom: 12px; } + .paperforge-modal-item { font-size: 13px; margin-left: 8px; color: var(--text-muted); } + `,document.head.appendChild(a)}let t=e.createDiv({cls:"paperforge-settings-tabs"}),s=[{id:"setup",label:o("tab_setup")||"\u5B89\u88C5"},{id:"features",label:o("tab_features")||"\u529F\u80FD"},{id:"release-notes",label:"\u66F4\u65B0\u4E0E\u624B\u518C"}],n={};s.forEach(a=>{t.createEl("button",{cls:"paperforge-settings-tab"+(a.id===this.activeTab?" paperforge-settings-tab--active":""),text:a.label}).addEventListener("click",()=>{this.activeTab=a.id,this.display()})}),s.forEach(a=>{n[a.id]=e.createDiv({cls:"paperforge-tab-content"+(a.id===this.activeTab?" paperforge-tab-content--active":"")})}),this.activeTab==="setup"?this._renderSetupTab(n.setup):this.activeTab==="features"?this._renderFeaturesTab(n.features):this._renderReleaseNotesTab(n["release-notes"])}_renderSetupTab(e){let t=this.app.vault.adapter.basePath;this.plugin.settings.vault_path||(this.plugin.settings.vault_path=t,this._debouncedSave()),this.plugin.settings.setup_complete&&(I.existsSync($.join(t,"paperforge.json"))||(this.plugin.settings.setup_complete=!1,this._debouncedSave())),e.createEl("h2",{text:o("header_title")||"PaperForge"}),e.createEl("p",{text:o("desc"),cls:"paperforge-settings-desc"});let n=e.createEl("div",{cls:"paperforge-setup-bar"}).createEl("span",{cls:"paperforge-setup-label"});this.plugin.settings.setup_complete?(n.setText(o("setup_done")),n.addClass("paperforge-setup-done")):(n.setText(o("setup_pending")),n.addClass("paperforge-setup-pending"));let a=this.app.vault.adapter.basePath,r=O(a,this.plugin.settings,void 0,void 0),l=r.path,i=this.plugin.settings._python_path_stale?"stale":r.source,c=new P.Setting(e).setName(o("field_python_interp")).setDesc(this._getPythonDesc(l,i));this._pythonInterpDescEl=c.descEl;let p=new P.Setting(e).setName(o("field_python_custom")).setDesc("");this._customPathDescEl=p.descEl,p.addText(E=>{E.setPlaceholder("e.g. C:\\Python310\\python.exe").setValue(this.plugin.settings.python_path||"").onChange(_=>{if(this.plugin.settings.python_path=_,this.plugin.saveSettings(),_&&_.trim()){let S=I.existsSync(_.trim());this.plugin.settings._python_path_stale=!S}else this.plugin.settings._python_path_stale=!1;let m=O(this.app.vault.adapter.basePath,this.plugin.settings,void 0,void 0),k=this.plugin.settings._python_path_stale?"stale":m.source;this._pythonInterpDescEl&&(this._pythonInterpDescEl.textContent=this._getPythonDesc(m.path,k))})}),p.addButton(E=>{E.setButtonText(o("btn_validate")).onClick(()=>this._validatePythonOverride())}),e.createEl("h3",{text:o("runtime_health")}),e.createEl("p",{text:o("runtime_health_desc"),cls:"paperforge-settings-desc"});let h=new P.Setting(e).setName("PaperForge").setDesc(o("runtime_health_checking")),d=h.descEl.createEl("span",{cls:"paperforge-runtime-badge"}),y=null;h.addButton(E=>{y=E,E.setButtonText(o("runtime_health_sync")).setDisabled(!0).onClick(()=>this._syncRuntime(E))});{let E=this.app.vault.adapter.basePath,{path:_,extraArgs:m=[]}=O(E,this.plugin.settings,void 0,void 0),k=this.plugin.manifest.version||"?";(0,V.execFile)(_,[...m,"-c","import paperforge; print(paperforge.__version__)"],{cwd:E,timeout:1e4},(S,F)=>{let R=this.plugin.settings.setup_complete,B=!S&&F?F.trim():null,q=B?`${o("runtime_health_plugin_ver").replace("{0}",k)} \u2192 ${o("runtime_health_package_ver").replace("{0}",B)}`:R?`Plugin v${k} \u2192 Python package not installed. Click "Sync Runtime" to install.`:`Plugin v${k} \u2192 Not configured. Please open the setup wizard first.`;h.setDesc(q),B===k?(d.setText(o("runtime_health_match")),d.className="paperforge-runtime-badge match",y&&y.setDisabled(!0)):B?(d.setText(o("runtime_health_mismatch")),d.className="paperforge-runtime-badge mismatch",y&&y.setDisabled(!1)):(d.setText(R?"Not installed":"Setup needed"),d.className="paperforge-runtime-badge missing",y&&y.setDisabled(!1))})}e.createEl("h3",{text:o("section_prep")}),e.createEl("p",{text:o("section_prep_desc"),cls:"paperforge-settings-desc"});let b=e.createEl("div",{cls:"paperforge-guide"}),v=[["prep_python","prep_python_desc"],["prep_zotero","prep_zotero_desc"],["prep_bbt","prep_bbt_desc"],["prep_key","prep_key_desc"]];for(let[E,_]of v){let m=b.createEl("div",{cls:"paperforge-guide-item"});m.createEl("strong",{text:o(E)}),m.createEl("span",{text:" \u2014 "+o(_)})}this._checkEl=e.createEl("div",{cls:"paperforge-message"});let f=!this.plugin.settings.setup_complete;new P.Setting(e).setName(o(f?"btn_install":"btn_reconfig")).setDesc(o(f?"btn_install_desc":"btn_reconfig_desc")).addButton(E=>{E.setButtonText(o(f?"btn_install":"btn_reconfig")).setCta().onClick(()=>{f?this._preCheck(()=>{new we(this.app,this.plugin).open()}):new we(this.app,this.plugin).open()})}),e.createEl("h3",{text:o("section_guide")});let x=e.createEl("div",{cls:"paperforge-guide"}),C=[["guide_open","guide_open_desc"],["guide_sync","guide_sync_desc"],["guide_ocr","guide_ocr_desc"]];for(let[E,_]of C){let m=x.createEl("div",{cls:"paperforge-guide-item"});m.createEl("strong",{text:o(E)}),m.createEl("span",{text:" \u2014 "+o(_)})}if(this.plugin.settings.setup_complete){e.createEl("h3",{text:o("section_config")});let E=e.createEl("div",{cls:"paperforge-summary"}),_=this.plugin.settings,m=this._pfConfig,k=[{label:o("dir_vault"),val:t},{label:o("dir_resources"),val:`${t}/${m==null?void 0:m.resources_dir}`},{label:" "+o("dir_notes"),val:`${t}/${m==null?void 0:m.resources_dir}/${m==null?void 0:m.literature_dir}`},{label:o("dir_base"),val:`${t}/${m==null?void 0:m.base_dir}`},{label:o("dir_system"),val:`${t}/${m==null?void 0:m.system_dir}`},{label:"API Key",val:_.paddleocr_api_key?o("api_key_set"):o("api_key_missing")},{label:o("field_zotero_data"),val:_.zotero_data_dir||o("not_set")}];for(let S of k){let F=E.createEl("div",{cls:"paperforge-summary-row"});F.createEl("span",{cls:"paperforge-summary-label",text:S.label}),F.createEl("span",{cls:"paperforge-summary-value",text:S.val})}}}_execMemoryStatus(e,t,s){(0,V.exec)(`"${e}" -m paperforge --vault "${t}" memory status --json`,{encoding:"utf-8",timeout:15e3},(n,a)=>{if(n){s("Status unavailable");return}try{let r=JSON.parse(a);if(r.ok){let l=r.data,i=l.fresh?"fresh":"stale";s(`Papers: ${l.paper_count_db} | ${i}${l.needs_rebuild?" - needs rebuild":""}`)}else s("DB not found. Run paperforge memory build.")}catch(r){s("Could not parse status.")}})}_execEmbedStatus(e,t,s){(0,V.exec)(`"${e}" -m paperforge --vault "${t}" embed status --json`,{encoding:"utf-8",timeout:15e3},(n,a)=>{if(n){s("Status unavailable");return}try{let r=JSON.parse(a);r.ok?s(`Chunks: ${r.data.chunk_count} | ${r.data.model} | ${r.data.mode}`):s("Could not parse status.")}catch(r){s("Could not parse status.")}})}_callPython(e,t){let s=this.app.vault.adapter.basePath,n=ne(s,this.plugin.settings),a=[...n.extraArgs,"-m","paperforge","--vault",s,...e];if(t&&t.stream){let r=(0,V.spawn)(n.path,a,{cwd:s,env:t.env||process.env,windowsHide:!0});return t.onData&&r.stdout.on("data",t.onData),t.onStderr&&r.stderr.on("data",t.onStderr),t.onError&&r.on("error",t.onError),r.on("close",t.onClose),r}return(0,V.execFile)(n.path,a,{cwd:s,timeout:t&&t.timeout||6e4},(r,l,i)=>{t&&t.onClose&&t.onClose(r?1:0,l,i)}),null}_renderMemoryStatusText(e,t,s){if(e.innerHTML="",e.createEl("span",{text:t,cls:"paperforge-memory-text"}).style.cssText="flex:1;",s==="syncing"){let r=e.createEl("span",{text:"Syncing...",cls:"paperforge-sync-status"});r.style.cssText="opacity:0.7; margin-right:8px;"}else if(s){let r=e.createEl("span",{text:s,cls:"paperforge-sync-status"});r.style.cssText="opacity:0.7; margin-right:8px;"}let n=e.createEl("button",{cls:"paperforge-rebuild-btn",text:o("feat_memory_rebuild_btn")});n.style.cssText="margin-left:auto; border:1px solid var(--background-modifier-border); background:var(--background-secondary); cursor:pointer; font-size:11px; padding:2px 6px; border-radius:3px; margin-right:4px;",n.title="Rebuild memory database",n.onclick=()=>{let r=this.app.vault.adapter.basePath,l=ne(r,this.plugin.settings);if(!l.path){new P.Notice(o("feat_no_python"));return}console.log("[PaperForge] Rebuilding memory:",l.path),n.setText(o("feat_memory_rebuilding")),n.setAttr("disabled",""),this._callPython(["memory","build"],{timeout:6e4,onClose:(i,c,p)=>{console.log("[PaperForge] memory build exit:",i?"FAIL:"+i:"OK",(c||"").slice(0,200),(p||"").slice(0,200)),n.setText(o("feat_memory_rebuild_btn")),n.removeAttribute("disabled"),i===0?new P.Notice(o("feat_memory_rebuild_done")):new P.Notice(o("feat_memory_rebuild_failed")+(p?" "+p.slice(0,80):"")),this._memoryStatusText=De(r),this._refreshSnapshots(r)}})};let a=e.createEl("button",{cls:"paperforge-refresh-btn",text:"\u21BB"});a.style.cssText="border:none; background:none; cursor:pointer; font-size:16px; padding:0 4px;",a.title="Sync now",a.onclick=()=>{this._memoryStatusText=null,this._runManualSync()}}_getBuildCommand(e){let t=this.app.vault.adapter.basePath,s=O(t,e,void 0,void 0);return s.path?`"${s.path}" -m paperforge --vault "${t}" sync`:null}_runManualSync(){let e=this.app.vault.adapter.basePath;if(!ne(e,this.plugin.settings).path)return;let s=document.querySelector(".paperforge-memory-status");s&&this._renderMemoryStatusText(s,"Checking...","syncing"),this.plugin._autoSyncRunning=!0,this._callPython(["sync"],{timeout:12e4,onClose:n=>{this.plugin._autoSyncRunning=!1,this._memoryStatusText=null,n===0&&(this._lastSyncTime=new Date().toLocaleTimeString(),this.plugin._lastSyncTime=this._lastSyncTime),this.display(),this._refreshSnapshots(e),Re(this.app,this.plugin,e)}})}_refreshSnapshots(e){let t=ne(e,this.plugin.settings),s=[...t.extraArgs,"-m","paperforge","--vault",e,"runtime-health","--json"];this._refreshPending=!0,(0,V.execFile)(t.path,s,{cwd:e,timeout:3e4,windowsHide:!0},(n,a,r)=>{this._refreshPending=!1,this._memoryStatusText=De(e),this._embedStatusText=ge(e),this.display()})}_renderFeaturesTab(e){e.createEl("h3",{text:"Skills"});let t=e.createEl("div",{cls:"paperforge-desc-box"});t.style.cssText="padding:8px 12px; margin:0 0 12px; background:var(--background-secondary); border-radius:4px; font-size:12px; color:var(--text-muted); line-height:1.5;",t.setText(o("feat_skills_desc")),t.createEl("br"),t.createEl("span",{text:o("feat_skills_system"),cls:""}).style.opacity="0.7";let s={opencode:"OpenCode",claude:"Claude Code",codex:"Codex",cursor:"Cursor",windsurf:"Windsurf",github_copilot:"GitHub Copilot"},n={opencode:".opencode/skills",claude:".claude/skills",codex:".codex/skills",cursor:".cursor/skills",windsurf:".windsurf/skills",github_copilot:".github/skills"},a=this.app.vault.adapter.basePath,r=this.plugin.settings.selected_skill_platform||"opencode";new P.Setting(e).setName(o("feat_agent_platform")).setDesc(o("feat_agent_platform_desc")).addDropdown(_=>{Object.entries(s).forEach(([m,k])=>_.addOption(m,k)),_.setValue(r).onChange(m=>{this.plugin.settings.selected_skill_platform=m,this.plugin.saveSettings(),this.display()})}).addExtraButton(_=>{_.setIcon("folder").setTooltip("Open skills folder").onClick(()=>{let m=n[r]||".opencode/skills",k=$.join(a,m);I.existsSync(k)?(0,V.exec)(`start "" "${k}"`):new P.Notice(`Skills folder not found: ${m}`)})});let l=$.join(a,n[r]),i=[],c=[];I.existsSync(l)&&I.readdirSync(l,{withFileTypes:!0}).forEach(_=>{if(!_.isDirectory())return;let m=$.join(l,_.name,"SKILL.md");if(!I.existsSync(m))return;let k=I.readFileSync(m,"utf-8"),S=k.match(/^name:\s*(.+)$/m),F=k.split(` +`),R=F.findIndex(K=>/^description:/.test(K)),B="";if(R>=0){let K=F[R].match(/^description:\s*(.+)$/);if(K&&K[1]&&K[1]!==">"&&K[1]!=="|-"&&K[1]!=="|")B=K[1].trim();else{for(let J=R+1;J<F.length&&(/^\s{2,}/.test(F[J])||F[J].trim()==="");J++)B+=F[J].trim()+" ";B=B.trim()}}let q=k.match(/^source:\s*(.+)$/m),U=k.match(/^disable-model-invocation:\s*(.+)$/m),A=k.match(/^version:\s*(.+)$/m),W={name:S?S[1].trim():_.name,desc:B,source:q?q[1].trim():"user",disabled:U&&U[1].trim()==="true",version:A?A[1].trim():"",path:m,content:k,dirName:_.name};W.source==="paperforge"?i.push(W):c.push(W)});let p=e.createEl("div");p.style.cssText="background:var(--background-secondary); border-radius:8px; padding:12px 12px 10px; margin:8px 0 16px;";let h=(_,m,k)=>{if(m.length===0)return;let S=p.createEl("div",{cls:"paperforge-skills-group"}),F=S.createEl("div",{cls:"paperforge-skills-collapse-header"}),R=S.createEl("div",{cls:"paperforge-skills-collapse-content"}),B=F.createEl("span",{text:"\u25BC",cls:"paperforge-skills-arrow"});B.style.cssText="display:inline-block; font-size:10px; margin-right:6px; transition:transform 0.2s; transform:rotate(0deg);",F.createEl("h4",{text:`${_} (${m.length})`,cls:"paperforge-skills-subheader"}),m.forEach(A=>{let W=A.name+(A.version?" v"+A.version:""),K=k?" [system]":" [user]",J=A.desc||"",ie=new P.Setting(R).setName(W+K).setDesc(J);ie.settingEl.style.opacity=A.disabled?"0.4":"1",ie.addToggle(he=>{he.setValue(!A.disabled).onChange(me=>{let oe=!me,be=A.content.match(/^disable-model-invocation:\s*(.+)$/m)?A.content.replace(/^disable-model-invocation:\s*.+$/m,`disable-model-invocation: ${oe}`):A.content.replace(/^(---\r?\n)/,`$1disable-model-invocation: ${oe} +`);I.writeFileSync(A.path,be,"utf-8"),A.disabled=oe,A.content=be,ie.settingEl.style.opacity=A.disabled?"0.4":"1"})})});let q=k?"system":"user";(this._skillsCollapsed[q]||!1)&&(R.style.display="none",B.style.transform="rotate(-90deg)"),F.addEventListener("click",()=>{R.style.display!=="none"?(R.style.display="none",B.style.transform="rotate(-90deg)"):(R.style.display="",B.style.transform="rotate(0deg)"),this._skillsCollapsed[q]=R.style.display==="none"})};h("System Skills",i,!0),h("User Skills",c,!1),i.length===0&&c.length===0&&p.createEl("p",{text:`No skills found in ${n[r]}. Run setup to deploy skills.`,cls:"setting-item-description"}),this._advCollapsed===void 0&&(this._advCollapsed=!0);let d=e.createEl("div",{cls:"paperforge-collapsible-header"});d.style.cssText="cursor:pointer; display:flex; align-items:center; gap:8px; padding:8px 0; user-select:none;";let y=d.createEl("span",{text:"\u25B6",cls:"paperforge-collapsible-arrow"});y.style.cssText="display:inline-block; transition:transform 0.2s; font-size:10px; transform:"+(this._advCollapsed?"rotate(0deg)":"rotate(90deg)")+";";let b=d.createEl("span",{text:"Advanced"});b.style.cssText="font-size:16px; font-weight:700; line-height:1.4;";let v=d.createEl("span",{text:"Memory + Vector DB + Embedding"});v.style.cssText="font-size:12px; color:var(--text-muted); margin-left:10px;";let f=e.createEl("div",{cls:"paperforge-collapsible-content"});f.style.display=this._advCollapsed?"none":"",d.addEventListener("click",()=>{this._advCollapsed=!this._advCollapsed,f.style.display=this._advCollapsed?"none":"",y.style.transform=this._advCollapsed?"rotate(0deg)":"rotate(90deg)"}),f.createEl("h4",{text:"Memory Layer"});let x=f.createEl("div",{cls:"paperforge-desc-box"});x.style.cssText="padding:8px 12px; margin:0 0 12px; background:var(--background-secondary); border-radius:4px; font-size:12px; color:var(--text-muted); line-height:1.5;",x.setText(o("feat_memory_desc"));let C=f.createEl("div",{cls:"paperforge-memory-status"});C.style.cssText="display:flex; align-items:center; padding:8px 12px; margin:8px 0; background:var(--background-secondary); border-radius:4px;";let E=this.app.vault.adapter.basePath;this.plugin._lastSyncTime&&!this._lastSyncTime&&(this._lastSyncTime=this.plugin._lastSyncTime),this._memoryStatusText===null&&(this._memoryStatusText=De(E)),this._renderMemoryStatusText(C,this._memoryStatusText,this._lastSyncTime),this._renderVectorSection(f)}_renderVectorSection(e){var i;e.createEl("h4",{text:"Vector Database"}),this.plugin.settings.features||(this.plugin.settings.features={memory_layer:!0,vector_db:!1});let t=e.createEl("div",{cls:"paperforge-desc-box"});if(t.style.cssText="padding:8px 12px; margin:0 0 8px; background:var(--background-secondary); border-radius:4px; font-size:12px; color:var(--text-muted); line-height:1.5;",t.setText(o("feat_vector_desc")),new P.Setting(e).setName(o("feat_vector_enable")).setDesc(o("feat_vector_enable_desc")).addToggle(c=>{c.setValue(!!this.plugin.settings.features.vector_db).onChange(p=>{this.plugin.settings.features.vector_db=p,this.plugin.saveSettings(),this._vectorDepsOk=null,this._embedStatusText=null,this.display()})}),!this.plugin.settings.features.vector_db)return;let s=this.app.vault.adapter.basePath,n=e.createEl("div",{cls:"paperforge-skills-collapse-header"});n.style.cssText="display:flex; align-items:center; cursor:pointer; padding:6px 0 2px; margin:0;";let a=n.createEl("span",{text:"\u25BC"});a.style.cssText="display:inline-block; font-size:10px; margin-right:6px; transition:transform 0.2s;",n.createEl("span",{text:o("feat_vector_config_label"),cls:""}).style.cssText="font-size:12px; color:var(--text-muted);";let r=e.createEl("div",{cls:"paperforge-vector-config"}),l=c=>{r.style.display=c?"none":"",a.style.transform=c?"rotate(-90deg)":"rotate(0deg)"};if(l(Ke(this._featurePanelsCollapsed,"vectorConfig",!1)),n.addEventListener("click",()=>{let c=pt(this._featurePanelsCollapsed,"vectorConfig",!1);l(c)}),this._vectorDepsOk===!0){this._renderVectorReady(r,s);return}if(this._vectorDepsOk===!1){this._renderVectorNoDeps(r);return}if(this._vectorDepsOk===null){let c=ke(s);this._vectorDepsOk=c&&(i=c.deps_installed)!=null?i:!1,this._vectorDepsOk&&(this._embedStatusText=ge(s)),this.display()}}_renderApiConfig(e){new P.Setting(e).setName(o("feat_openai_key")).setDesc(o("feat_openai_key_desc")).addText(t=>{t.setPlaceholder("sk-...").setValue(this.plugin.settings.vector_db_api_key||"").onChange(s=>{this.plugin.settings.vector_db_api_key=s,this.plugin.saveSettings()})}),new P.Setting(e).setName(o("feat_api_base_url")).setDesc(o("feat_api_base_url_desc")).addText(t=>{t.setPlaceholder("https://api.openai.com/v1").setValue(this.plugin.settings.vector_db_api_base||"").onChange(s=>{this.plugin.settings.vector_db_api_base=s,this.plugin.saveSettings()})}),new P.Setting(e).setName(o("feat_api_model")).setDesc(o("feat_api_model_desc")).addText(t=>{t.setPlaceholder("text-embedding-3-small").setValue(this.plugin.settings.vector_db_api_model||"text-embedding-3-small").onChange(s=>{this.plugin.settings.vector_db_api_model=s,this.plugin.saveSettings()})})}_renderVectorNoDeps(e){let t=e.createEl("div");t.style.cssText="padding:8px 12px; margin:8px 0; background:var(--background-secondary); border-radius:4px;",t.setText(o("feat_deps_missing")),new P.Setting(e).setName(o("feat_install_deps")).setDesc(o("feat_install_deps_desc")).addButton(s=>{s.setButtonText(o("feat_install_btn")).setCta().onClick(async()=>{let n=this.app.vault.adapter.basePath,a=ne(n,this.plugin.settings);if(!a.path){new P.Notice(o("feat_no_python"));return}s.setButtonText(o("feat_installing")),s.setDisabled(!0);let r="chromadb openai",l=new P.Notice(o("feat_installing_pkgs").replace("{pkgs}",r),0);try{let i=Object.assign({},process.env,{PYTHONIOENCODING:"utf-8",PYTHONUTF8:"1"}),c=r.split(" ");await new Promise((p,h)=>{(0,V.execFile)(a.path,[...a.extraArgs,"-m","pip","install",...c],{cwd:n,timeout:3e5,env:i,windowsHide:!0},d=>{d?h(d):p()})}),l.hide(),new P.Notice(o("feat_install_done")),this._vectorDepsOk=!0,this._embedStatusText=ge(n),this.display()}catch(i){l.hide(),new P.Notice(o("feat_install_failed")+(i.stderr||i.message||i)),s.setButtonText(o("feat_retry_btn")),s.setDisabled(!1)}})})}_renderVectorReady(e,t){let s=e.createEl("div");s.style.cssText="padding:8px 12px; margin:8px 0; background:var(--background-secondary); border-radius:4px;",s.setText(ge(t)),this._renderApiConfig(e);let n=e.createEl("div");n.style.cssText="padding:4px 0;";let a=n.createEl("div");a.style.cssText="display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;",a.createEl("span",{text:o("feat_rebuild_vectors"),cls:"setting-item-name"});let r=n.createEl("div");r.style.cssText="display:flex; align-items:center; gap:8px;";let l=n.createEl("div",{cls:"paperforge-embed-status-text"});(()=>{var b;r.empty(),l.empty();let c=(ke(t)||{}).build_state||{};this.plugin._embedProgress=this.plugin._embedProgress||{current:0,total:0,key:""},!this.plugin._embedProcess&&c.status==="running"&&(this.plugin._embedProgress={current:c.current||0,total:c.total||1,key:c.paper_id||""});let{current:p,total:h,key:d}=this.plugin._embedProgress;if(!!this.plugin._embedProcess||c.status==="running"){let v=r.createEl("div",{cls:"paperforge-progress-track"});v.style.cssText="flex:1;";let f=h>0?(p/h*100).toFixed(1):"0",x=v.createEl("div",{cls:"paperforge-progress-seg done"});if(x.style.cssText=`width:${f}%; min-width:${p>0?"2px":"0"};`,p<h){let E=v.createEl("div",{cls:"paperforge-progress-seg pending"});E.style.cssText=`width:${(100-parseFloat(f)).toFixed(1)}%;`}l.createEl("span",{cls:"paperforge-embed-progress-text",text:`${p}/${h} papers`}),d&&l.createEl("span",{cls:"paperforge-embed-progress-key",text:` (${d})`});let C=r.createEl("button");C.setText("Stop"),C.className="mod-warning",C.addEventListener("click",()=>{this._callPython(["embed","stop","--json"],{timeout:8e3}),this.plugin._embedProcess&&(this.plugin._embedProcess.kill(),this.plugin._embedProcess=null),this.display()})}else{let v=ke(t),f=!!(v&&((b=v.chunk_count)!=null?b:0)>0),x=v&&v.corrupted,C=_=>{if(!ne(t,this.plugin.settings).path){new P.Notice(o("feat_no_python"));return}let k=Object.assign({},process.env,{PYTHONIOENCODING:"utf-8",PYTHONUTF8:"1",VECTOR_DB_API_KEY:this.plugin.settings.vector_db_api_key||"",VECTOR_DB_API_BASE:this.plugin.settings.vector_db_api_base||"",VECTOR_DB_API_MODEL:this.plugin.settings.vector_db_api_model||""});this.plugin._embedStderr="",this.plugin._embedProgress={current:0,total:0,key:""},this.plugin._embedProcess=this._callPython(["embed","build",_],{stream:!0,env:k,onData:S=>{let F=S.toString("utf-8").split(` +`);for(let R of F)if(R.startsWith("EMBED_START:"))this.plugin._embedProgress.total=parseInt(R.split(":")[1])||0;else if(R.startsWith("EMBED_PROGRESS:")){let B=R.split(":");this.plugin._embedProgress.current=parseInt(B[1])||0,this.plugin._embedProgress.key=B[3]||""}else R.startsWith("EMBED_DONE")&&(this.plugin._embedProcess=null,this.plugin._embedProgress.current=this.plugin._embedProgress.total);this.display()},onStderr:S=>{this.plugin._embedStderr||(this.plugin._embedStderr=""),this.plugin._embedStderr+=S.toString("utf-8")},onError:S=>{this.plugin._embedProcess=null,new P.Notice(o("feat_build_failed")+": "+(S.message||S)),this.display()},onClose:S=>{if(this.plugin._embedProcess=null,S===0)this.plugin._embedProgress.current=this.plugin._embedProgress.total,this.plugin.saveSettings(),this._embedStatusText=ge(t),new P.Notice(o("feat_build_complete"));else{this._embedStatusText=null;let F=(this.plugin._embedStderr||"").slice(0,200);new P.Notice(o("feat_build_failed")+(F?": "+F:""),8e3)}this.plugin._embedStderr="",this.display(),this._refreshSnapshots(t)}}),this.display()};if(x){let _=n.createEl("div");_.style.cssText="padding:8px 12px; margin:8px 0; background:var(--background-modifier-warning); border-radius:4px; font-size:12px; display:flex; align-items:center; justify-content:space-between;",_.createEl("span",{text:o("feat_vector_corrupted")});let m=_.createEl("button",{text:o("feat_vector_rebuild_force_btn")});m.className="mod-cta",m.addEventListener("click",()=>C("--force"))}f&&!x&&r.createEl("span",{text:v.chunk_count+" chunks embedded",cls:"setting-item-description"});let E=r.createEl("button");if(E.setText(f?o("feat_rebuild_btn"):o("feat_build_btn")),E.addClass("mod-cta"),E.addEventListener("click",()=>C("--resume")),!x&&f){let _=r.createEl("button");_.setText(o("feat_vector_rebuild_force_btn")),_.style.marginLeft="6px",_.addEventListener("click",()=>C("--force"))}}})()}_getCurrentModelKey(){return this.plugin.settings.vector_db_api_model||"text-embedding-3-small"}_parseEmbedStatus(e){let t={};return e&&(e.split(` +`).forEach(s=>{let n=s.match(/^\s*([^:]+):\s*(.*)/);n&&(t[n[1].trim()]=n[2].trim())}),t.db_exists!==void 0&&(t.db_exists=t.db_exists==="True"),t.chunk_count!==void 0&&(t.chunk_count=parseInt(t.chunk_count,10)||0)),t}_getPythonDesc(e,t){return t==="stale"?`[!!] ${e} (stale \u2014 path no longer exists, update or clear the override below)`:t==="manual"?`${e} (manual)`:`${e} (auto-detected)`}_refreshPythonInterpDesc(e,t){let s=this._pythonInterpDescEl;s&&(t==="stale"?s.textContent=`[!!] ${e} (stale \u2014 path no longer exists, update or clear the override below)`:t==="manual"?s.textContent=`${e} (manual)`:s.textContent=`${e} (auto-detected)`)}_validatePythonOverride(){let e=this.plugin.settings.python_path?this.plugin.settings.python_path.trim():"",t=this._customPathDescEl;if(!e){let s="\u8BF7\u8F93\u5165\u8DEF\u5F84 / Enter a path first";t&&(t.innerHTML=`<span style="color:var(--text-error)">\u2717 ${s}</span>`),new P.Notice(s);return}if(!I.existsSync(e)){let s="\u8DEF\u5F84\u4E0D\u5B58\u5728 / Path does not exist";t&&(t.innerHTML=`<span style="color:var(--text-error)">\u2717 ${s}</span>`),new P.Notice(s,4e3);return}try{I.accessSync(e,I.constants.X_OK)}catch(s){let n="\u4E0D\u53EF\u6267\u884C / Not executable";t&&(t.innerHTML=`<span style="color:var(--text-error)">\u2717 ${n}</span>`),new P.Notice(n,4e3);return}(0,V.execFile)(e,["--version"],{timeout:8e3},(s,n)=>{if(s||!n){let i="\u65E0\u6CD5\u8FD0\u884C / Cannot run";t&&(t.innerHTML=`<span style="color:var(--text-error)">\u2717 ${i}</span>`),new P.Notice(i,4e3);return}let a=n.match(/Python (\d+)\.(\d+)/);if(!a){let i="\u65E0\u6CD5\u89E3\u6790\u7248\u672C / Cannot parse version";t&&(t.innerHTML=`<span style="color:var(--text-error)">\u2717 ${i}</span>`),new P.Notice(i,4e3);return}let r=parseInt(a[1],10),l=parseInt(a[2],10);if(r<3||r===3&&l<10){let i="Python \u7248\u672C\u8FC7\u4F4E\uFF0C\u9700\u8981 3.10+ / Python version too low, need 3.10+";t&&(t.innerHTML=`<span style="color:var(--text-error)">\u2717 ${i}</span>`),new P.Notice(i,4e3);return}(0,V.execFile)(e,["-m","pip","--version"],{timeout:8e3},i=>{if(i){let c=`\u2713 Python ${r}.${l} \u6709\u6548\uFF0C\u4F46\u672A\u68C0\u6D4B\u5230 pip / Valid, but pip not found`;t&&(t.innerHTML=`<span style="color:var(--text-warning)">\u26A0 ${c}</span>`),new P.Notice(c,4e3)}else{let c=`\u2713 Python ${r}.${l} \u6709\u6548 / Valid`;t&&(t.innerHTML=`<span style="color:var(--text-accent)">${c}</span>`),new P.Notice(c,4e3)}})})}_syncRuntime(e){let t=this.app.vault.adapter.basePath,{path:s,extraArgs:n=[]}=O(t,this.plugin.settings,void 0,void 0),a=this.plugin.manifest.version,r=at(s,a,n);e.setDisabled(!0),e.setButtonText(o("runtime_health_syncing"));let l=(c,p)=>(console.log(`[PaperForge] Sync Runtime: trying ${p}`),it(r.cmd,c,t,r.timeout,void 0,pe())),i=()=>{let c="opencode";try{let y=I.readFileSync($.join(t,"paperforge.json"),"utf-8"),b=JSON.parse(y);b.agent_key&&(c=b.agent_key)}catch(y){}let p=[...n,"-c",'from paperforge.services.skill_deploy import deploy_skills; from pathlib import Path; r=deploy_skills(vault=Path(r"'+t.replace(/\\/g,"\\\\")+'"), agent_key="'+c+'", overwrite=True); print("skills deployed" if r["skill_deployed"] else "skills skipped", flush=True)'],h=(0,V.spawn)(s,p,{cwd:t,timeout:3e4,windowsHide:!0}),d="";h.stdout.on("data",y=>{d+=y.toString("utf-8")}),h.on("close",y=>{console.log(`[PaperForge] Skill deploy: ${d.trim()} (exit ${y})`)})};l(r.pypiArgs,"PyPI").then(c=>{if(c.exitCode===0){console.log("[PaperForge] Sync Runtime: installed via PyPI"),i(),new P.Notice(o("runtime_health_sync_done").replace("{0}",a),5e3),this.display();return}console.warn("[PaperForge] Sync Runtime: PyPI failed, falling back to git..."),l(r.gitArgs,"git").then(p=>{p.exitCode===0?(console.log("[PaperForge] Sync Runtime: installed via git"),i(),new P.Notice(o("runtime_health_sync_done").replace("{0}",a),5e3),this.display()):(e.setDisabled(!1),e.setButtonText(o("runtime_health_sync")),console.error("[PaperForge] git fallback stderr:",p.stderr),new P.Notice(o("runtime_health_sync_fail").replace("{0}","pip exit code "+p.exitCode),8e3))})})}_debouncedSave(){clearTimeout(this._saveTimeout),this._saveTimeout=setTimeout(()=>this.plugin.saveSettings(),500)}_preCheck(e){var a;let t=this.app.vault.adapter.basePath,{path:s,extraArgs:n=[]}=O(t,(a=this.plugin)==null?void 0:a.settings,void 0,void 0);(0,V.execFile)(s,[...n,"--version"],{timeout:8e3},(r,l)=>{let i=[];i.push({label:"Python",ok:!r,detail:r?o("check_python_fail"):l.trim()});let c=!1,p=process.env.HOME||process.env.USERPROFILE||gt.homedir()||"";if(process.platform==="darwin")c=["/Applications/Zotero.app",$.join(p,"Applications","Zotero.app")].some(x=>{try{return I.existsSync(x)}catch(C){return!1}});else if(process.platform==="win32"){let f=process.env.ProgramFiles||"",x=process.env.LOCALAPPDATA||"";c=[$.join(f,"Zotero"),$.join(f,"(x86)","Zotero"),$.join(x,"Programs","Zotero"),$.join(x,"Zotero"),$.join(p,"AppData","Local","Programs","Zotero")].filter(Boolean).some(E=>{try{return I.existsSync(E)}catch(_){return!1}})}else c=[$.join(p,".local","share","zotero","zotero"),"/usr/bin/zotero","/usr/local/bin/zotero"].some(x=>{try{return I.existsSync(x)}catch(C){return!1}});let h=this.plugin.settings.zotero_data_dir;if(!c&&h)try{c=I.existsSync(h)}catch(f){}i.push({label:"Zotero",ok:c,detail:c?o("check_zotero_ok"):o("check_zotero_fail")});let d=!1,y=process.env.APPDATA||"";process.platform==="win32"&&y&&(d=Te($.join(y,"Zotero","Zotero","Profiles"))),!d&&process.platform==="darwin"&&p&&(d=Te($.join(p,"Library","Application Support","Zotero","Profiles"))),!d&&process.platform!=="win32"&&process.platform!=="darwin"&&p&&(d=Te($.join(p,".zotero","zotero","Profiles"))),!d&&h&&String(h).trim()&&(d=je(h.trim())),!d&&p&&(d=je($.join(p,"Zotero"))),i.push({label:"Better BibTeX",ok:d,detail:d?o("check_bbt_ok"):o("check_bbt_fail")});let b={true:"\u2713",false:"\u2717"};if(this._checkEl){this._checkEl.setText(i.map(x=>`${b[String(x.ok)]} ${x.label}: ${x.detail}`).join(` +`));let f=i.some(x=>!x.ok);this._checkEl.className=`paperforge-message msg-${f?"error":"ok"}`}let v=i.filter(f=>!f.ok);v.length>0&&new P.Notice(`[!!] \u672A\u901A\u8FC7: ${v.map(f=>f.label).join(", ")}`,6e3),e()})}_renderReleaseNotesTab(e){e.createEl("h2",{text:"\u66F4\u65B0\u4E0E\u624B\u518C"}),e.createEl("h3",{text:"\u7248\u672C\u66F4\u65B0\u8BB0\u5F55"});let t=ft.default.versions||[];for(let a of t){let r=e.createEl("div",{cls:"paperforge-release-card"}),l=r.createEl("div",{cls:"paperforge-release-header"});if(l.createEl("strong",{text:`v${a.version} \u2014 ${a.title}`}),l.createEl("span",{cls:"paperforge-release-date",text:` (${a.date})`}),a.breaking_or_migration&&a.breaking_or_migration.length>0){let i=r.createEl("div",{cls:"paperforge-release-section"});i.createEl("div",{cls:"paperforge-release-label",text:"\u884C\u4E3A\u53D8\u66F4 / \u8FC1\u79FB\u6CE8\u610F"});for(let c of a.breaking_or_migration)i.createEl("div",{cls:"paperforge-release-item",text:`\u2022 ${c}`})}if(a.new_features&&a.new_features.length>0){let i=r.createEl("div",{cls:"paperforge-release-section"});i.createEl("div",{cls:"paperforge-release-label",text:"\u65B0\u529F\u80FD"});for(let c of a.new_features)i.createEl("div",{cls:"paperforge-release-item",text:`\u2022 ${c}`})}if(a.fixes&&a.fixes.length>0){let i=r.createEl("div",{cls:"paperforge-release-section"});i.createEl("div",{cls:"paperforge-release-label",text:"\u4FEE\u590D"});for(let c of a.fixes)i.createEl("div",{cls:"paperforge-release-item",text:`\u2022 ${c}`})}if(a.recommended_actions&&a.recommended_actions.length>0){let i=r.createEl("div",{cls:"paperforge-release-section"});i.createEl("div",{cls:"paperforge-release-label",text:"\u5EFA\u8BAE\u64CD\u4F5C"});for(let c of a.recommended_actions)i.createEl("div",{cls:"paperforge-release-item",text:`\u2022 ${c}`})}}e.createEl("h3",{text:"\u4F7F\u7528\u624B\u518C"}),e.createEl("div",{cls:"paperforge-manual-links"}).createEl("a",{text:"\u2192 \u67E5\u770B\u5B8C\u6574\u4F7F\u7528\u624B\u518C\uFF08GitHub\uFF09",href:"https://github.com/LLLin000/PaperForge/blob/main/docs/user-manual.md"}).setAttr("target","_blank")}};var D=require("obsidian"),ae=z(require("fs")),Ce=z(require("path")),ue=require("child_process");var Se=z(require("path"));function _t(g){if(!g)return null;let u=Se.dirname(g);for(;;){let e=Se.basename(u);if(!e||e===".")break;let t=e.match(/^([A-Z0-9]{8})(?:\s*-\s*.*)?$/i);if(t)return t[1];let s=Se.dirname(u);if(s===u)break;u=s}return null}var fe=class extends D.ItemView{constructor(e){super(e);this._currentMode=null;this._currentDomain=null;this._currentPaperKey=null;this._currentPaperEntry=null;this._currentFilePath=null;this._cachedItems=null;this._modeSubscribers=[];this._leafChangeTimer=null;this._ocrPrivacyShown=!1;this._cachedStats=null;this._techDetailsExpanded=!1;this._paperforgeVersion="";this._dashboardPermissions={};this._headerTitle=null;this._versionBadge=null;this._messageEl=null;this._metricsEl=null;this._ocrSection=null;this._ocrEmpty=null;this._ocrBadge=null;this._ocrTrack=null;this._ocrCounts=null;this._driftBannerEl=null;this._currentMode=null,this._currentDomain=null,this._currentPaperKey=null,this._currentPaperEntry=null,this._currentFilePath=null,this._cachedItems=null,this._modeSubscribers=[],this._leafChangeTimer=null,this._ocrPrivacyShown=!1}getViewType(){return ce}getDisplayText(){return"PaperForge"}getIcon(){return xe}async onOpen(){this._buildPanel(),this._modeSubscribers=[],this._leafChangeTimer=null,this._setupEventSubscriptions(),this._fetchVersion(),this._detectAndSwitch()}async onClose(){if(this._modeSubscribers&&this._modeSubscribers.length>0){for(let e of this._modeSubscribers)e.event==="active-leaf-change"?this.app.workspace.off("active-leaf-change",e.ref):e.event==="modify"&&this.app.vault.off("modify",e.ref);this._modeSubscribers=[]}this._leafChangeTimer&&(clearTimeout(this._leafChangeTimer),this._leafChangeTimer=null),this._cachedItems=null,this._cachedStats=null}_buildPanel(){let e=this.containerEl;e.empty(),e.addClass("paperforge-status-panel");let t=e.createEl("div",{cls:"paperforge-header"}),s=t.createEl("div",{cls:"paperforge-header-left"});s.createEl("div",{cls:"paperforge-header-logo",text:"P"}),this._modeContextEl=s.createEl("div",{cls:"paperforge-mode-context"}),this._headerTitle=s.createEl("h3",{cls:"paperforge-header-title",text:"PaperForge"}),this._versionBadge=s.createEl("span",{cls:"paperforge-header-badge",text:"v\u2014"});let n=t.createEl("button",{cls:"paperforge-header-refresh",attr:{"aria-label":"Refresh"}});n.innerHTML="\u21BB",n.addEventListener("click",()=>{this._invalidateIndex(),this._detectAndSwitch()}),this._messageEl=e.createEl("div",{cls:"paperforge-message"}),this._contentEl=e.createEl("div",{cls:"paperforge-content-area"})}_fetchVersion(){var r,l;let e=this.app.vault.adapter.basePath,t=this.app.plugins.plugins.paperforge,s=((r=t==null?void 0:t.manifest)==null?void 0:r.version)||"?",{path:n,extraArgs:a=[]}=O(e,(l=t==null?void 0:t.settings)!=null?l:null,void 0,void 0);nt(n,s,e,1e4,void 0).then(i=>{if(i.status==="not-installed")return;let c=i.pyVersion||"";this._paperforgeVersion=c.startsWith("v")?c:"v"+c,this._versionBadge&&this._versionBadge.setText(this._paperforgeVersion),this._driftBannerEl&&s&&this._paperforgeVersion!=="v"+s.replace(/^v/,"")?(this._driftBannerEl.style.display="block",this._driftBannerEl.setText(o("dashboard_drift_warning").replace("{0}",this._paperforgeVersion).replace("{1}","v"+s.replace(/^v/,"")))):this._driftBannerEl&&(this._driftBannerEl.style.display="none")})}_fetchStats(e){var r;if(!this._metricsEl)return;if(!e&&!this._cachedStats)this._metricsEl.empty(),this._metricsEl.createEl("div",{cls:"paperforge-status-loading",text:"Loading..."});else if(e&&!this._cachedStats)return;let t=this.app.vault.adapter.basePath,s=this.app.plugins.plugins.paperforge,{path:n,extraArgs:a=[]}=O(t,(r=s==null?void 0:s.settings)!=null?r:null,void 0,void 0);(0,ue.execFile)(n,[...a,"-m","paperforge","dashboard","--json"],{cwd:t,timeout:3e4},(l,i)=>{if(!l)try{let c=JSON.parse(i);if(c.ok&&c.data){let p=this._normalizeDashboardData(c.data);this._cachedStats=p,this._metricsEl.empty(),this._renderStats(p),this._renderOcr(p),this._dashboardPermissions=c.data.permissions||{};return}}catch(c){}this._fallbackFetchStats(e,t,s)})}_normalizeDashboardData(e){let t=e.stats||{},s=t.ocr_health||{},n=t.pdf_health||{},a=(s.done||0)+(s.pending||0)+(s.failed||0);return{total_papers:t.papers||0,formal_notes:t.papers||0,exports:0,bases:0,ocr:{total:a,pending:s.pending||0,processing:0,done:s.done||0,failed:s.failed||0},path_errors:(n.broken||0)+(n.missing||0)}}_fallbackFetchStats(e,t,s){var r,l,i;let n=((r=s==null?void 0:s.settings)==null?void 0:r.system_dir)||"System",a=Ce.join(t,n,"PaperForge","indexes","formal-library.json");try{let c=ae.readFileSync(a,"utf-8"),p=JSON.parse(c),h=p.items||[],d={},y={pdf_health:{healthy:0,unhealthy:0},ocr_health:{healthy:0,unhealthy:0},note_health:{healthy:0,unhealthy:0},asset_health:{healthy:0,unhealthy:0}},b=0,v=0,f=0,x=0,C=0,E=0;for(let _ of h){_.note_path&&E++;let m=_.lifecycle||"pdf_ready";d[m]=(d[m]||0)+1;let k=_.health||{};for(let F of["pdf_health","ocr_health","note_health","asset_health"])(k[F]||"healthy")==="healthy"?y[F].healthy++:y[F].unhealthy++;let S=_.ocr_status||"";b++,S==="done"?v++:S==="pending"?f++:S==="processing"||S==="queued"||S==="running"?x++:C++}this._cachedStats={version:p.paperforge_version||((l=this._cachedStats)==null?void 0:l.version)||"\u2014",total_papers:h.length,formal_notes:E,exports:0,bases:0,ocr:{total:b,pending:f,processing:x,done:v,failed:C},path_errors:0,lifecycle_level_counts:d,health_aggregate:y},this._metricsEl.empty(),this._renderStats(this._cachedStats),this._renderOcr(this._cachedStats)}catch(c){!e&&!this._cachedStats&&this._metricsEl.createEl("div",{cls:"paperforge-status-loading",text:"No index \u2014 trying CLI..."});let{path:p,extraArgs:h=[]}=O(t,(i=s==null?void 0:s.settings)!=null?i:null,void 0,void 0);(0,ue.execFile)(p,[...h,"-m","paperforge","status","--json"],{cwd:t,timeout:3e4},(d,y)=>{if(d){if(this._cachedStats)return;this._metricsEl.createEl("div",{cls:"paperforge-status-error",text:`Cannot reach PaperForge CLI. +Make sure paperforge is installed and in your PATH.`});return}try{let b=JSON.parse(y);this._cachedStats=b,this._metricsEl.empty(),this._renderStats(b),this._renderOcr(b)}catch(b){this._cachedStats||this._metricsEl.createEl("div",{cls:"paperforge-status-error",text:"Invalid response from paperforge status."})}})}}_renderSkeleton(e){e.addClass("paperforge-loading")}_renderEmptyState(e,t){e.createEl("div",{cls:"paperforge-empty-state",text:t||"No data"})}_buildMetricBar(e,t,s){if(s<=0)return;let n=Math.min(100,t/s*100);e.createEl("div",{cls:"paperforge-metric-progress"}).createEl("div",{cls:"paperforge-metric-progress-fill",attr:{style:`width:${n.toFixed(1)}%`}})}_loadIndex(){var a;let e=this.app.vault.adapter.basePath,t=this.app.plugins.plugins.paperforge,s=((a=t==null?void 0:t.settings)==null?void 0:a.system_dir)||"System",n=Ce.join(e,s,"PaperForge","indexes","formal-library.json");try{let r=ae.readFileSync(n,"utf-8");return JSON.parse(r)}catch(r){return null}}_getCachedIndex(){if(!this._cachedItems){let e=this._loadIndex();this._cachedItems=e?e.items||[]:[]}return this._cachedItems}_findEntry(e){if(!e)return null;let t=this._getCachedIndex().find(s=>s.zotero_key===e)||null;return et(this.app,t)}_patchCachedEntry(e,t){if(!e||!this._cachedItems)return;let s=this._cachedItems.findIndex(n=>n.zotero_key===e);s!==-1&&(this._cachedItems[s]=Ie(this._cachedItems[s],t))}_filterByDomain(e){return e?this._getCachedIndex().filter(t=>t.domain===e):[]}_renderStats(e){var a;if(this._versionBadge&&this._versionBadge.setText(this._paperforgeVersion||(e.version?"v"+e.version:"v\u2014")),!e||typeof e.total_papers=="undefined"){this._metricsEl&&this._renderSkeleton(this._metricsEl);return}if(!this._metricsEl)return;this._metricsEl.removeClass("paperforge-loading");let t=e.total_papers||0,s=e.formal_notes||0,n=[{value:t,label:"Papers",color:"var(--color-cyan)",barMax:0},{value:s,label:"Formal Notes",color:"var(--color-blue)",barMax:t},{value:e.exports||0,label:"Exports",color:"var(--color-purple)",barMax:0}];for(let r of n){let l=this._metricsEl.createEl("div",{cls:"paperforge-metric-card"});l.style.setProperty("--metric-color",r.color),l.createEl("div",{cls:"paperforge-metric-value",text:((a=r.value)==null?void 0:a.toString())||"\u2014"}),l.createEl("div",{cls:"paperforge-metric-label",text:r.label}),r.barMax>0&&this._buildMetricBar(l,r.value,r.barMax)}}_renderOcr(e){if(!this._ocrSection)return;let t=e.ocr||{},s=t.total||0;if(s===0){this._ocrSection.style.display="none";return}this._ocrSection.style.display="block",this._ocrEmpty&&(this._ocrEmpty.style.display="none");let n=t.done||0,a=t.pending||0,r=t.processing||0,l=t.failed||0;if(this._ocrBadge&&(this._ocrBadge.removeClass("active","idle"),r>0?(this._ocrBadge.addClass("active"),this._ocrBadge.setText("Processing")):a>0?(this._ocrBadge.addClass("idle"),this._ocrBadge.setText("Pending")):(this._ocrBadge.addClass("idle"),this._ocrBadge.setText("Idle"))),this._ocrTrack){this._ocrTrack.empty(),r>0?this._ocrTrack.addClass("paperforge-processing"):this._ocrTrack.removeClass("paperforge-processing");let i=[{cls:"pending",count:a},{cls:"active",count:r},{cls:"done",count:n},{cls:"failed",count:l}];for(let c of i)if(c.count>0){let p=(c.count/s*100).toFixed(1);this._ocrTrack.createEl("div",{cls:`paperforge-progress-seg ${c.cls}`,attr:{style:`width:${p}%`}})}}if(this._ocrCounts){this._ocrCounts.empty();let i=[{cls:"pending",value:a,label:"Pending"},{cls:"active",value:r,label:"Processing"},{cls:"done",value:n,label:"Done"},{cls:"failed",value:l,label:"Failed"}];for(let c of i){let p=this._ocrCounts.createEl("div",{cls:"paperforge-ocr-count"});p.createEl("div",{cls:"paperforge-ocr-count-value",text:c.value.toString()}),p.createEl("div",{cls:"paperforge-ocr-count-label",text:c.label})}}}_renderLifecycleStepper(e,t,s){if(!t||!s){this._renderSkeleton(e);return}let n=[{key:"indexed",label:"Indexed"},{key:"pdf_ready",label:"PDF Ready"},{key:"fulltext_ready",label:"Fulltext Ready"},{key:"deep_read_done",label:"Deep Read"}],a=e.createEl("div",{cls:"paperforge-lifecycle-stepper"}),r=!1;for(let l of n){let i=a.createEl("div",{cls:"step"});i.createEl("div",{cls:"step-indicator"}),i.createEl("div",{cls:"step-label",text:l.label}),l.key===s?(i.addClass("current"),r=!0):r?i.addClass("pending"):i.addClass("completed")}}_renderHealthMatrix(e,t){if(!t){this._renderSkeleton(e);return}let s=[{key:"pdf_health",label:"PDF Health",iconOk:"\u2713",iconWarn:"\u26A0",iconFail:"\u2717"},{key:"ocr_health",label:"OCR Health",iconOk:"\u2713",iconWarn:"\u26A0",iconFail:"\u2717"},{key:"note_health",label:"Note Health",iconOk:"\u2713",iconWarn:"\u26A0",iconFail:"\u2717"},{key:"asset_health",label:"Asset Health",iconOk:"\u2713",iconWarn:"\u26A0",iconFail:"\u2717"}],n=e.createEl("div",{cls:"paperforge-health-matrix"});for(let a of s){let r=t[a.key]||"healthy",l=n.createEl("div",{cls:"paperforge-health-cell"}),i,c,p;r==="healthy"||r==="ok"?(i=a.iconOk,c="ok",p=`${a.label}: OK`):r==="warn"||r==="warning"||r==="degraded"?(i=a.iconWarn,c="warn",p=`${a.label}: Needs Attention`):(i=a.iconFail,c="fail",p=`${a.label}: Failed`),l.addClass(c),l.setAttribute("title",p),l.createEl("div",{cls:"paperforge-health-cell-icon",text:i}),l.createEl("div",{cls:"paperforge-health-cell-label",text:a.label})}}_renderMaturityGauge(e,t,s){if(t==null||t===void 0){this._renderSkeleton(e);return}let n=e.createEl("div",{cls:"paperforge-maturity-gauge"}),a=n.createEl("div",{cls:"gauge-track"}),r=4,l=Math.max(1,Math.min(r,Math.round(t)));for(let i=1;i<=r;i++){let c=a.createEl("div",{cls:"gauge-segment"});i<=l&&(c.addClass("filled"),c.addClass(`level-${i}`))}if(n.createEl("div",{cls:"gauge-level",text:`Level ${l} / ${r}`}),l<r&&s){let i=typeof s=="string"?[s]:s;if(i.length>0){let c=n.createEl("ul",{cls:"gauge-blockers"});for(let p of i)c.createEl("li",{text:p})}}}_renderBarChart(e,t){if(!t||Object.keys(t).length===0){this._renderEmptyState(e,"No lifecycle data");return}let s=[{key:"indexed",label:"Indexed",cls:"stage-indexed"},{key:"pdf_ready",label:"PDF Ready",cls:"stage-pdf-ready"},{key:"fulltext_ready",label:"Fulltext Ready",cls:"stage-fulltext-ready"},{key:"deep_read_done",label:"Deep Read",cls:"stage-deep-read"}],n=e.createEl("div",{cls:"paperforge-bar-chart"}),a=Math.max(1,...s.map(r=>t[r.key]||0));for(let r of s){let l=t[r.key]||0,i=l/a*100,c=n.createEl("div",{cls:"bar-row"});c.createEl("div",{cls:"bar-label",text:r.label}),c.createEl("div",{cls:"bar-track"}).createEl("div",{cls:`bar-fill ${r.cls}`,attr:{style:`width:${i.toFixed(1)}%`}}),c.createEl("div",{cls:"bar-count",text:l.toString()})}}_invalidateIndex(){this._cachedItems=null}_extractZoteroKeyFromPath(e){return _t(e)}_resolveModeForFile(e){if(!e)return{mode:"global",filePath:null,key:null,domain:null};let t=e.extension,s=e.path;if(t==="base")return{mode:"collection",filePath:s,key:null,domain:e.basename.trim()};if(t==="md"){let a=this.app.metadataCache.getFileCache(e),r=a&&a.frontmatter&&a.frontmatter.zotero_key;if(r)return{mode:"paper",filePath:s,key:r,domain:null}}if(t==="pdf"){let a=this._getCachedIndex();for(let r of a){let l=(r.pdf_path||"").match(/\[\[([^\]]+)\]\]/);if((l?l[1]:r.pdf_path)===s)return{mode:"paper",filePath:s,key:r.zotero_key,domain:null}}}let n=this._extractZoteroKeyFromPath(s);return n?{mode:"paper",filePath:s,key:n,domain:null}:{mode:"global",filePath:s,key:null,domain:null}}_detectAndSwitch(){let e=this._resolveModeForFile(this.app.workspace.getActiveFile());this._currentDomain=e.domain||null,this._currentPaperKey=e.key||null,this._currentPaperEntry=e.key?this._findEntry(e.key):null,this._switchMode(e.mode,e.filePath)}_switchMode(e,t){if(this._currentMode===e&&this._currentFilePath===t){this._refreshCurrentMode();return}if(this._currentMode=e,this._currentFilePath=t,this._techDetailsExpanded=!1,!!this._contentEl)switch(this._contentEl.empty(),this._contentEl.removeClass("switching"),this._renderModeHeader(e),e){case"global":this._renderGlobalMode();break;case"paper":this._renderPaperMode();break;case"collection":this._renderCollectionMode();break}}_renderGlobalMode(){var be,We,qe,Je,Ue,Ge,Xe;if(!this._contentEl)return;let e=this._contentEl.createEl("div",{cls:"paperforge-global-view"});this._driftBannerEl=e.createEl("div",{cls:"paperforge-drift-banner"}),this._driftBannerEl.style.display="none";let t=this._getCachedIndex(),s=t.length,n=0,a=0,r=0;for(let w of t)w.has_pdf&&n++,w.ocr_status==="done"&&a++,w.deep_reading_status==="done"&&r++;let l=e.createEl("div",{cls:"paperforge-library-snapshot"});l.createEl("div",{cls:"paperforge-section-label",text:"Library Snapshot"});let i=l.createEl("div",{cls:"paperforge-snapshot-pills"}),c=[{value:s,label:"papers"},{value:n,label:"PDFs ready"},{value:a,label:"OCR done"},{value:r,label:"deep-read done"}];for(let w of c){let T=i.createEl("div",{cls:"paperforge-snapshot-pill"});T.createEl("span",{cls:"paperforge-snapshot-value",text:String(w.value)}),T.createEl("span",{cls:"paperforge-snapshot-label",text:" "+w.label})}let p=e.createEl("div",{cls:"paperforge-system-status"});p.createEl("div",{cls:"paperforge-section-label",text:"System Status"});let h=p.createEl("div",{cls:"paperforge-status-grid"}),d=this.app.plugins.plugins.paperforge,y=((be=d==null?void 0:d.manifest)==null?void 0:be.version)||"?",b=this._paperforgeVersion;if(!b)try{let w=this.app.vault.adapter.basePath,{path:T,extraArgs:X=[]}=O(w,(We=d==null?void 0:d.settings)!=null?We:null,void 0,void 0),j=(0,ue.execFileSync)(T,[...X,"-c","import paperforge; print(paperforge.__version__)"],{cwd:w,timeout:5e3,encoding:"utf-8",windowsHide:!0}).trim();j&&(b=j.startsWith("v")?j:"v"+j,this._paperforgeVersion=b)}catch(w){}b=b||"\u2014";let v=b==="v"+y;this._renderSystemStatusRow(h,"Runtime",v?"healthy":"mismatch",v?"v"+y:"plugin v"+y+" \u2260 CLI "+b);let f=this._loadIndex(),x=f&&f.items&&f.items.length>0;this._renderSystemStatusRow(h,"Index",x?"healthy":"missing",x?f.items.length+" entries":"formal-library.json not found");let C=((qe=d==null?void 0:d.settings)==null?void 0:qe.system_dir)||"System",E=this.app.vault.adapter.basePath,_=!1,m="No exports found";try{let w=Ce.join(E,C,"PaperForge","exports");if(ae.existsSync(w)){let T=ae.readdirSync(w).filter(X=>X.endsWith(".json"));_=T.length>0,m=_?T.length+" export(s)":"No JSON exports"}}catch(w){}this._renderSystemStatusRow(h,"Zotero Export",_?"healthy":"missing",m);let k=!!((Je=d==null?void 0:d.settings)!=null&&Je.paddleocr_api_key);if(!k)try{let w=((Ue=d==null?void 0:d.settings)==null?void 0:Ue.system_dir)||"System",T=Ce.join(E,w,"PaperForge",".env");if(ae.existsSync(T)){let j=ae.readFileSync(T,"utf-8").match(/^PADDLEOCR_API_TOKEN\s*=\s*(.+)$/m);k=!!(j&&j[1]&&j[1].trim())}}catch(w){}k||(k=!!(process.env.PADDLEOCR_API_TOKEN||process.env.PADDLEOCR_API_KEY||process.env.OCR_TOKEN)),this._renderSystemStatusRow(h,"OCR Token",k?"configured":"missing",k?"Configured":"Not set");let S=!1,F="",R=this.app.vault.adapter.basePath,B=Ve(R);S=ct(R),F=B&&((Ge=B.summary)==null?void 0:Ge.reason)||B&&((Xe=B.summary)==null?void 0:Xe.status)||"Unknown",this._renderSystemStatusRow(h,"Memory Layer",S?"healthy":"fail",F);let q=!v&&b!=="\u2014";if(q||!x||!_||!k){let w=e.createEl("div",{cls:"paperforge-issue-summary"});w.createEl("div",{cls:"paperforge-section-label",text:"\u9700\u8981\u5904\u7406"});let T=w.createEl("div",{cls:"paperforge-issue-list"});q&&T.createEl("div",{cls:"paperforge-issue-item",text:"Runtime version mismatch"}),x||T.createEl("div",{cls:"paperforge-issue-item",text:"Index missing or corrupted"}),_||T.createEl("div",{cls:"paperforge-issue-item",text:"No Zotero export found"}),k||T.createEl("div",{cls:"paperforge-issue-item",text:"PaddleOCR API key not configured"});let X=w.createEl("div",{cls:"paperforge-issue-actions"}),j=X.createEl("button",{cls:"paperforge-contextual-btn"});j.createEl("span",{text:"Run Doctor"}),j.addEventListener("click",()=>{let ve=H.find(Oe=>Oe.id==="paperforge-doctor");ve&&this._runAction(ve,j)});let le=X.createEl("button",{cls:"paperforge-contextual-btn"});le.createEl("span",{text:"Repair Issues"}),le.addEventListener("click",()=>{let ve=H.find(Oe=>Oe.id==="paperforge-repair");ve&&this._runAction(ve,le)})}let A=e.createEl("div",{cls:"paperforge-global-actions"});A.createEl("div",{cls:"paperforge-section-label",text:"Start Working"});let W=A.createEl("div",{cls:"paperforge-global-actions-row"}),K=W.createEl("button",{cls:"paperforge-contextual-btn primary"});K.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u{1F4C1}"}),K.createEl("span",{text:"Open Literature Hub"}),K.addEventListener("click",()=>{var X;let w=((X=d==null?void 0:d.settings)==null?void 0:X.base_dir)||"Bases",T=this.app.vault.getAbstractFileByPath(w);if(T){let j=null;if(T.children&&(j=T.children.find(le=>le.extension==="base")),j){let le=this.app.workspace.getLeaf(!1);le&&le.openFile(j)}else new D.Notice("[!!] No .base file found in "+w,6e3)}else new D.Notice("[!!] Base directory not found: "+w,6e3)});let J=W.createEl("button",{cls:"paperforge-contextual-btn"});J.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u21BB"}),J.createEl("span",{text:"Sync Library"}),J.addEventListener("click",()=>{let w=H.find(T=>T.id==="paperforge-sync");w&&this._runAction(w,J)});let ie=W.createEl("button",{cls:"paperforge-contextual-btn"});ie.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u229E"}),ie.createEl("span",{text:"Run OCR"}),ie.addEventListener("click",()=>{let w=H.find(T=>T.id==="paperforge-ocr");w&&this._runAction(w,ie)});let he=W.createEl("button",{cls:"paperforge-contextual-btn"});he.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u2695"}),he.createEl("span",{text:"Run Doctor"}),he.addEventListener("click",()=>{let w=H.find(T=>T.id==="paperforge-doctor");w&&this._runAction(w,he)});let me=W.createEl("button",{cls:"paperforge-contextual-btn"});me.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u2139"}),me.createEl("span",{text:"Refresh Status"}),me.addEventListener("click",()=>{this._fetchStats(!1)});let oe=W.createEl("button",{cls:"paperforge-contextual-btn warn"});oe.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u21BA"}),oe.createEl("span",{text:"Redo OCR"}),oe.addEventListener("click",()=>{let w=H.find(T=>T.id==="paperforge-ocr-redo");w&&this._runAction(w,oe)});let ye=W.createEl("button",{cls:"paperforge-contextual-btn"});ye.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u21BA"}),ye.createEl("span",{text:"Repair Issues"}),ye.addEventListener("click",()=>{let w=H.find(T=>T.id==="paperforge-repair");w&&this._runAction(w,ye)})}_renderSystemStatusRow(e,t,s,n){let a=e.createEl("div",{cls:"paperforge-status-row"});a.createEl("span",{cls:"paperforge-status-dot"}).addClass(s==="healthy"||s==="configured"?"ok":"fail"),a.createEl("span",{cls:"paperforge-status-label",text:t}),a.createEl("span",{cls:"paperforge-status-detail",text:n||""})}_renderPaperMode(){let e=this._currentPaperEntry,t=this._currentPaperKey;if(!this._contentEl)return;if(!t){this._renderEmptyState(this._contentEl,"No paper data available.");return}if(!e){this._contentEl.createEl("div",{cls:"paperforge-content-placeholder",text:'Paper "'+t+'" not found in canonical index. Sync first.'});return}let s=this._contentEl.createEl("div",{cls:"paperforge-paper-view"}),n=s.createEl("div",{cls:"paperforge-paper-header"});n.createEl("div",{cls:"paperforge-paper-title",text:e.title||"Untitled"});let a=n.createEl("div",{cls:"paperforge-paper-meta"});e.authors&&e.authors.length>0&&a.createEl("span",{cls:"paperforge-paper-authors",text:e.authors.join(", ")}),e.year&&a.createEl("span",{cls:"paperforge-paper-year",text:String(e.year)});let r=s.createEl("div",{cls:"paperforge-status-strip"}),l=r.createEl("div",{cls:"paperforge-status-strip-left"}),i=r.createEl("div",{cls:"paperforge-status-strip-right"}),c=[{key:"pdf",label:"PDF",ok:e.has_pdf===!0},{key:"ocr",label:"OCR",ok:e.ocr_status==="done",pending:["pending","queued","processing"].includes(e.ocr_status||""),fail:["failed","blocked","done_incomplete","nopdf"].includes(e.ocr_status||"")},{key:"deep",label:"\u7CBE\u8BFB",ok:e.deep_reading_status==="done"}];for(let p of c){let h=l.createEl("span",{cls:"paperforge-status-pill"}),d="pending";p.ok?d="ok":p.fail?d="fail":p.pending&&(d="pending"),h.addClass(d);let y=p.ok?"\u2713":p.fail?"\u2717":"\u25CB";h.createEl("span",{cls:"paperforge-status-pill-icon",text:y}),h.createEl("span",{text:" "+p.label})}if(e.pdf_path){let p=i.createEl("button",{cls:"paperforge-contextual-btn"});p.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u{1F4C4}"}),p.createEl("span",{text:"\u6253\u5F00 PDF"}),p.addEventListener("click",()=>{let h=e.pdf_path.match(/\[\[([^\]]+)\]\]/),d=h?h[1]:e.pdf_path;this.app.vault.getAbstractFileByPath(d)?this.app.workspace.openLinkText(d,""):new D.Notice("[!!] PDF not found: "+d,6e3)})}if(e.fulltext_path){let p=i.createEl("button",{cls:"paperforge-contextual-btn"});p.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u{1F4DD}"}),p.createEl("span",{text:"\u6253\u5F00\u5168\u6587"}),p.addEventListener("click",()=>this._openFulltext(e.fulltext_path))}if(this._renderPaperOverviewCard(s,e),e.next_step==="ready"&&e.deep_reading_status==="done"){let p=s.createEl("div",{cls:"paperforge-complete-row"});p.createEl("span",{text:"\u2713"}),p.createEl("span",{text:"\u5DF2\u5B8C\u6210\uFF0C\u53EF\u76F4\u63A5\u4F7F\u7528"})}else this._renderNextStepCard(s,e,t);this._renderRecentDiscussionCard(s,e),this._renderPaperTechnicalDetails(s,e)}_renderPaperOverviewCard(e,t){let s=e.createEl("div",{cls:"paperforge-paper-overview"});s.createEl("div",{cls:"paperforge-paper-overview-header"}).createEl("span",{cls:"paperforge-paper-overview-title",text:"\u6587\u7AE0\u6982\u89C8"});let a=s.createEl("div",{cls:"paperforge-paper-overview-body"}),r=a.createEl("div",{cls:"paperforge-paper-overview-excerpt",text:"\u52A0\u8F7D\u4E2D..."});if(t.note_path){let l=this.app.vault.getAbstractFileByPath(t.note_path);l?this.app.vault.read(l).then(i=>{let c=this._extractOverviewFromNote(i);if(c){let p=c.length>200?c.slice(0,200)+"...":c;if(r.setText(p),c.length>200){let h=a.createEl("div",{cls:"paperforge-expand-container"}),d=h.createEl("button",{cls:"paperforge-expand-icon",title:"\u5C55\u5F00/\u6536\u8D77"});d.innerHTML='<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>';let y=!1;h.addEventListener("click",()=>{r.setText(y?p:c),d.innerHTML=y?'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>':'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>',y=!y})}}else r.setText("\u5C1A\u672A\u751F\u6210\u6587\u7AE0\u6982\u89C8\u3002\u8FD0\u884C /pf-deep \u5F00\u59CB\u7CBE\u8BFB\u3002")}).catch(()=>{r.setText("\u65E0\u6CD5\u8BFB\u53D6\u7B14\u8BB0\u5185\u5BB9")}):r.setText("\u7B14\u8BB0\u6587\u4EF6\u4E0D\u5B58\u5728")}else r.setText("\u5C1A\u672A\u751F\u6210\u6587\u7AE0\u6982\u89C8")}_extractOverviewFromNote(e){if(!e)return null;let t=e.indexOf("## \u{1F50D} \u7CBE\u8BFB");if(t===-1)return null;let s=e.slice(t),n=["**\u4E00\u53E5\u8BDD\u603B\u89C8:**","**\u4E00\u53E5\u8BDD\u603B\u89C8**","**\u6587\u7AE0\u6458\u8981:**","**\u6587\u7AE0\u6458\u8981**"];for(let l of n){let i=s.indexOf(l);if(i!==-1){let c=s.slice(i+l.length),p=["**5 Cs","**Figure","**\u8BC1\u636E","### Pass 2","## "],h=c.length;for(let b of p){let v=c.indexOf(b);v!==-1&&v<h&&(h=v)}let d=c.indexOf(` -`);d!==-1&&d<u&&(u=d);let _=c.slice(0,u).trim();return _.startsWith("**")&&(_=_.slice(2)),_.endsWith("**")&&(_=_.slice(0,-2)),_||null}}let a=s.indexOf(` +`);d!==-1&&d<h&&(h=d);let y=c.slice(0,h).trim();return y.startsWith("**")&&(y=y.slice(2)),y.endsWith("**")&&(y=y.slice(0,-2)),y||null}}let a=s.indexOf(` `);if(a===-1)return null;let r=s.slice(a+1).split(` -`)[0].trim();return!r||r.startsWith("###")||r.startsWith("##")?null:r.length>300?r.slice(0,300)+"...":r}_renderRecentDiscussionCard(e,t){let s=e.createEl("div",{cls:"paperforge-discussion-card"});if(s.style.display="none",!t.note_path)return;let n=t.note_path.lastIndexOf("/"),r=(n!==-1?t.note_path.substring(0,n):".")+"/ai/discussion.md";this.app.vault.adapter.exists(r).then(o=>{if(o)return this.app.vault.adapter.read(r)}).then(async o=>{if(!o)return;let l=this._parseDiscussionMD(o);if(!l||l.length===0)return;s.style.display="block",s.createEl("div",{cls:"paperforge-discussion-header"}).createEl("span",{cls:"paperforge-discussion-title",text:"\u6700\u8FD1\u8BA8\u8BBA"});for(let u of l){let d=s.createEl("div",{cls:"paperforge-discussion-item"}),_=d.createEl("div",{cls:"paperforge-discussion-q"});_.createEl("span",{cls:"paperforge-discussion-q-label",text:"\u63D0\u95EE\uFF1A"}),_.createEl("span",{cls:"paperforge-discussion-q-text",text:u.question});let y=d.createEl("div",{cls:"paperforge-discussion-a"}),x=!1;if(u.answer&&u.answer.length>500&&(x=!0,y.classList.add("paperforge-discussion-a-collapsed")),await D.MarkdownRenderer.render(this.app,u.answer||"",y,r,this),x){let f=!1;d.style.cursor="pointer",d.addEventListener("click",()=>{f=!f,y.classList.toggle("paperforge-discussion-a-collapsed",!f),y.classList.toggle("paperforge-discussion-a-expanded",f)})}}s.createEl("a",{cls:"paperforge-discussion-viewall",text:"\u67E5\u770B\u5168\u90E8\u8BA8\u8BBA \u2192"}).addEventListener("click",u=>{u.preventDefault(),this.app.vault.getAbstractFileByPath(r)?this.app.workspace.openLinkText(r,""):new D.Notice("\u8BA8\u8BBA\u6587\u4EF6\u5C1A\u672A\u751F\u6210")})}).catch(o=>{console.error("PaperForge: discussion.md read error",r,o.message)})}_parseDiscussionMD(e){let t=e.split(/\n## /).slice(1);if(t.length===0)return null;let s=t[t.length-1],n=[],a=s.split(/\*\*\u95EE\u9898:\*\*/).slice(1);for(let r of a){let o=r.match(/\*\*\u89E3\u7B54:\*\*/);if(!o)continue;let l=r.substring(0,o.index).trim(),c=r.substring(o.index+3+4).trim();n.push({question:l,answer:c})}return n.slice(-3)}_renderPaperTechnicalDetails(e,t){let s=this._currentPaperKey,n=e.createEl("div",{cls:"paperforge-technical-details"}),a=n.createEl("button",{cls:"paperforge-technical-details-toggle"}),r=n.createEl("div",{cls:"paperforge-technical-details-body"});r.style.display="none",this._techDetailsExpanded?(r.style.display="block",a.setText("\u6280\u672F\u8BE6\u60C5 \u25BE")):a.setText("\u6280\u672F\u8BE6\u60C5 \u25B8"),a.addEventListener("click",()=>{let u=r.style.display!=="none";r.style.display=u?"none":"block",a.setText(u?"\u6280\u672F\u8BE6\u60C5 \u25B8":"\u6280\u672F\u8BE6\u60C5 \u25BE"),this._techDetailsExpanded=!u});let o=r.createEl("div",{cls:"paperforge-workflow-toggles"}),l=[{key:"do_ocr",label:"OCR",hint:"\u52A0\u5165 OCR"},{key:"analyze",label:"\u7CBE\u8BFB",hint:"\u6807\u8BB0\u7CBE\u8BFB"}];for(let u of l){let d=o.createEl("label",{cls:"paperforge-workflow-toggle"}),_=d.createEl("input",{type:"checkbox",cls:"paperforge-workflow-checkbox"});_.checked=t[u.key]===!0,d.createEl("span",{cls:"paperforge-workflow-toggle-label",text:u.label}),d.createEl("span",{cls:"paperforge-workflow-toggle-hint",text:u.hint}),_.addEventListener("change",async()=>{let y=t.note_path?this.app.vault.getAbstractFileByPath(t.note_path):null;if(!y){new D.Notice("[!!] Note file not found",6e3);return}let x=_.checked;await this.app.fileManager.processFrontMatter(y,f=>{f[u.key]=x}),this._patchCachedEntry(s,{[u.key]:x}),this._currentPaperEntry=Oe(this._currentPaperEntry,{[u.key]:x})})}let c=t.health||{},p=[["PDF Health",c.pdf_health||"\u2014"],["OCR Status",t.ocr_status||"\u2014"],["Asset Health",c.asset_health||"\u2014"],["Note Path",t.note_path||"\u2014"],["Fulltext Path",t.fulltext_path||"\u2014"]];for(let[u,d]of p){let _=r.createEl("div",{cls:"paperforge-technical-row"});_.createEl("span",{cls:"paperforge-technical-label",text:u}),_.createEl("span",{cls:"paperforge-technical-value",text:String(d)})}}_renderNextStepCard(e,t,s){var l,c;let n=t.next_step||"ready",a={sync:{label:"Sync Needed",text:"This paper needs to be synced from Zotero. Click to run sync.",cmd:"sync",icon:"\u21BB"},ocr:{label:"OCR Needed",text:"Fulltext is missing but PDF is present. Click to run OCR.",cmd:"ocr",icon:"\u229E"},repair:{label:"Repair Needed",text:"State divergence or path errors detected. Click to repair.",cmd:"repair",icon:"\u21BA"},"rebuild index":{label:"Rebuild Needed",text:"Index may be stale. Click to run sync to rebuild.",cmd:"sync",icon:"\u21BB"},"/pf-deep":{label:"Ready for Deep Reading",text:"Fulltext is ready. Copy /pf-deep command and run in your agent.",cmd:null,icon:"\u{1F50D}"},ready:{label:"All Set",text:"This paper is fully processed and ready for use.",cmd:"ready",icon:"\u2713"}},r=a[n]||a.ready,o=e.createEl("div",{cls:"paperforge-next-step-card"});if(n==="ready"&&o.addClass("ready"),o.createEl("div",{cls:"paperforge-next-step-label",text:"Recommended Next Step"}),o.createEl("div",{cls:"paperforge-next-step-text",text:r.text}),r.cmd&&r.cmd!=="ready"){let p=o.createEl("button",{cls:"paperforge-next-step-trigger"});p.createEl("span",{text:r.icon+" "+r.label}),p.addEventListener("click",()=>{let u=W.find(d=>d.cmd===r.cmd);u&&this._runAction(u,p)})}else if(n==="/pf-deep"){let p=o.createEl("button",{cls:"paperforge-next-step-trigger"});p.createEl("span",{text:"\u{1F4CB} "+i("copy_pf_deep_cmd")}),p.addEventListener("click",()=>{let x="/pf-deep "+s;navigator.clipboard.writeText(x).then(()=>{p.setText("\u2713 "+i("copied")),new D.Notice(x+" copied")}).catch(()=>{new D.Notice("[!!] Clipboard write failed",6e3)})});let u=((c=(l=this.app.plugins.plugins.paperforge)==null?void 0:l.settings)==null?void 0:c.agent_platform)||"opencode",_={opencode:"OpenCode",claude:"Claude Code",cursor:"Cursor",github_copilot:"GitHub Copilot",windsurf:"Windsurf",codex:"Codex",cline:"Cline"}[u]||u;o.createEl("div",{cls:"paperforge-agent-platform-label"}).setText(i("run_in_agent").replace("{0}",_))}else n==="ready"&&o.createEl("button",{cls:"paperforge-next-step-trigger"}).createEl("span",{text:"\u2713 "+r.label})}_openFulltext(e){if(!e){new D.Notice("[!!] No fulltext path available for this paper",6e3);return}let t=this.app.vault.getAbstractFileByPath(e);t?this.app.workspace.openLinkText(t.path,""):new D.Notice("[!!] Fulltext file not found: "+e,6e3)}_renderCollectionMode(){let e=this._currentDomain||"Unknown",t=this._filterByDomain(e);if(t.length===0){this._renderGlobalMode();return}if(!this._contentEl)return;let s=this._contentEl.createEl("div",{cls:"paperforge-collection-view"}),n=t.length,a=0,r=0,o=0,l=0,c=0,p=0,u=0;for(let b of t){b.has_pdf&&a++,b.ocr_status==="done"&&r++,b.ocr_status==="done"&&b.analyze===!0&&o++,b.deep_reading_status==="done"&&l++;let m=b.ocr_status||"";m==="pending"||m==="queued"?c++:m==="processing"?p++:(m==="failed"||m==="blocked"||m==="done_incomplete"||m==="nopdf")&&u++}s.createEl("div",{cls:"paperforge-collection-header"}).createEl("div",{cls:"paperforge-collection-title",text:e});let _=s.createEl("div",{cls:"paperforge-workflow-overview"});_.createEl("div",{cls:"paperforge-section-label",text:"Workflow Overview"});let y=_.createEl("div",{cls:"paperforge-workflow-funnel"}),x=[{value:n,label:"Total"},{value:a,label:"PDF Ready"},{value:r,label:"OCR Done"},{value:l,label:"Deep Read"}];for(let b=0;b<x.length;b++){let m=y.createEl("div",{cls:"paperforge-workflow-stage"});m.createEl("div",{cls:"paperforge-workflow-stage-value",text:String(x[b].value)}),m.createEl("div",{cls:"paperforge-workflow-stage-label",text:x[b].label}),b<x.length-1&&y.createEl("div",{cls:"paperforge-workflow-arrow",text:"\u2192"})}if(c+p+r+u>0){let b=s.createEl("div",{cls:"paperforge-ocr-section"}),m=b.createEl("div",{cls:"paperforge-collection-ocr-header"});m.createEl("h4",{cls:"paperforge-ocr-title",text:"OCR Pipeline"});let v=m.createEl("span",{cls:"paperforge-ocr-badge idle"});p>0?(v.addClass("active"),v.setText("Processing")):c>0?v.setText("Pending"):(v.addClass("idle"),v.setText("Idle"));let k=b.createEl("div",{cls:"paperforge-progress-track"});p>0&&k.addClass("paperforge-processing");let C=c+p+r+u,F=[{cls:"pending",count:c},{cls:"active",count:p},{cls:"done",count:r},{cls:"failed",count:u}];for(let N of F)if(N.count>0){let re=(N.count/C*100).toFixed(1);k.createEl("div",{cls:`paperforge-progress-seg ${N.cls}`,attr:{style:`width:${re}%`}})}let R=b.createEl("div",{cls:"paperforge-ocr-counts"}),B=[{cls:"pending",value:c,label:"Pending"},{cls:"active",value:p,label:"Processing"},{cls:"done",value:r,label:"Done"},{cls:"failed",value:u,label:"Attention"}];for(let N of B){let re=R.createEl("div",{cls:"paperforge-ocr-count"});re.createEl("div",{cls:"paperforge-ocr-count-value",text:N.value.toString()}),re.createEl("div",{cls:"paperforge-ocr-count-label",text:N.label})}}let f=s.createEl("div",{cls:"paperforge-collection-actions"}),E=f.createEl("button",{cls:"paperforge-contextual-btn primary"});E.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u229E"}),E.createEl("span",{text:"Run OCR"}),E.addEventListener("click",()=>{let b=W.find(m=>m.id==="paperforge-ocr");b&&this._runAction(b,E)});let S=f.createEl("button",{cls:"paperforge-contextual-btn"});S.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u21BB"}),S.createEl("span",{text:"Sync Library"}),S.addEventListener("click",()=>{let b=W.find(m=>m.id==="paperforge-sync");b&&this._runAction(b,S)})}_refreshCurrentMode(){if(!(!this._currentMode||!this._contentEl)){this._contentEl.empty(),this._contentEl.addClass("switching"),this._invalidateIndex(),this._currentPaperEntry=this._currentPaperKey?this._findEntry(this._currentPaperKey):null,this._renderModeHeader(this._currentMode);try{switch(this._currentMode){case"global":this._renderGlobalMode();break;case"paper":this._renderPaperMode();break;case"collection":this._renderCollectionMode();break}}finally{setTimeout(()=>{this._contentEl&&this._contentEl.removeClass("switching")},50)}}}_runAction(e,t){var d,_;if(e.disabled){new D.Notice(`[i] ${e.disabledMsg||"This action is not yet available."}`,6e3);return}if(t.classList.contains("running"))return;t.addClass("running");let s=this.app.vault.adapter.basePath;this._showMessage("Processing...","running");let n=Array.isArray(e.args)?[...e.args]:[];if(e.needsKey){let y=this.app.workspace.getActiveFile(),x=null;if(y){let f=this.app.metadataCache.getFileCache(y);if(f&&f.frontmatter&&f.frontmatter.zotero_key)x=f.frontmatter.zotero_key;else if(f&&f.frontmatter){this._showMessage("[!!] No zotero_key in active note frontmatter","error"),new D.Notice("[!!] Open a paper note with a zotero_key in its frontmatter first",6e3),t.removeClass("running");return}else{this._showMessage("[!!] No frontmatter in active note","error"),new D.Notice("[!!] The active note has no frontmatter with a zotero_key",6e3),t.removeClass("running");return}}else{this._showMessage("[!!] No active note open","error"),new D.Notice("[!!] Open a paper note with a zotero_key in its frontmatter first",6e3),t.removeClass("running");return}n=[...n,x]}e.needsFilter&&(n=[...n,"--all"]);let a=e.needsFilter?6e4:e.needsKey?3e4:6e5,{path:r,extraArgs:o=[]}=A(s,(_=(d=this.app.plugins.plugins.paperforge)==null?void 0:d.settings)!=null?_:null,void 0,void 0),l=(0,he.spawn)(r,[...o,"-m","paperforge",e.cmd,...n],{cwd:s,timeout:a}),c=[],p=Date.now(),u=setInterval(()=>this._fetchStats(!0),4e3);l.stdout.on("data",y=>{let x=y.toString("utf-8").split(` -`).filter(Boolean);for(let f of x){let E=f.trim();E&&(c.push(E),this._showMessage(c.slice(-8).join(` -`),"running"))}}),l.stderr.on("data",y=>{let x=y.toString("utf-8").split(` -`).filter(Boolean);for(let f of x){if(f.includes("\r")||f.includes("%")||f.includes("\u2588"))continue;let E=f.trim();E&&!E.match(/^\d+%|^\|/)&&(c.push(E),this._showMessage(c.slice(-8).join(` -`),"running"))}}),l.on("close",y=>{clearInterval(u),t.removeClass("running");let x=((Date.now()-p)/1e3).toFixed(1);if(y!==0){let f=c.slice(-3).join(" | ")||"exit code "+y;(e.cmd==="repair"||e.cmd==="ocr")&&y===1?(this._showMessage("[WARN] "+f,"running"),new D.Notice("[WARN] "+e.cmd+" partial: "+f,8e3),this._fetchStats(!0)):(this._showMessage("[!!] "+f,"error"),new D.Notice("[!!] "+e.cmd+" failed: "+f,8e3))}else if(e.needsKey||e.needsFilter){let f=c.join(` -`);if(f.trim())try{JSON.parse(f),navigator.clipboard.writeText(f).then(()=>{let E=`${x}s \u2014 ${f.length} chars copied`;this._showMessage("[OK] "+e.title+": "+E,"ok"),new D.Notice("[OK] "+e.okMsg+" \u2014 "+f.length+" chars")}).catch(E=>{this._showMessage("[!!] Clipboard write failed: "+E.message,"error"),new D.Notice("[!!] Clipboard error",6e3)})}catch(E){this._showMessage("[!!] Invalid JSON from "+e.title,"error"),new D.Notice("[!!] "+e.title+" returned invalid JSON: "+E.message.slice(0,100),8e3)}else this._showMessage("[!!] No output from context command","error"),new D.Notice("[!!] Context command returned empty output",8e3);this._fetchStats(!0)}else{let E=c.filter(b=>b.match(/updated \d+/)).pop()||c[c.length-1]||"",S=`${x}s \u2014 ${E}`;this._showMessage("[OK] "+e.title+": "+S,"ok"),new D.Notice("[OK] "+e.okMsg),this._contentEl&&this._contentEl.removeClass("switching"),this._cachedStats=null;try{this._fetchStats(!1)}catch(b){console.log("[PF] fetchStats error:",b)}console.log("[PF] close cmd="+e.cmd+" id="+e.id),e.cmd==="sync"&&Te(this.app,this.app.plugins.plugins.paperforge,s)}}),l.on("error",y=>{t.removeClass("running"),this._contentEl&&this._contentEl.removeClass("switching"),this._showMessage("[!!] "+y.message,"error"),new D.Notice("[!!] Cannot start: "+y.message,8e3)})}_showMessage(e,t){this._messageEl&&(this._messageEl.setText(e),this._messageEl.className=`paperforge-message msg-${t}`)}_renderModeHeader(e){if(!this._modeContextEl)return;this._modeContextEl.empty();let t=this._modeContextEl.createEl("span",{cls:"paperforge-mode-badge"}),s="";switch(e){case"global":t.addClass("global"),t.setText("Global"),this._headerTitle&&this._headerTitle.setText("PaperForge");break;case"paper":t.addClass("paper"),t.setText("Paper"),this._headerTitle&&this._headerTitle.setText("Paper"),this._currentPaperEntry&&this._currentPaperEntry.title?s=this._currentPaperEntry.title:this._currentPaperKey?(s=this._currentPaperKey,this._modeContextEl.createEl("span",{cls:"paperforge-mode-warning",text:"Not found in index"})):s="Unknown paper";break;case"collection":t.addClass("collection"),t.setText("Collection"),this._headerTitle&&this._headerTitle.setText("Collection"),s=this._currentDomain||"Unknown Domain";break}s&&this._modeContextEl.createEl("span",{cls:"paperforge-mode-name",text:s})}_setupEventSubscriptions(){let e=this.app.workspace.on("active-leaf-change",()=>{this._leafChangeTimer&&clearTimeout(this._leafChangeTimer),this._leafChangeTimer=setTimeout(()=>{let s=this._resolveModeForFile(this.app.workspace.getActiveFile()),n=s.mode,a=s.filePath;this._currentMode===n&&this._currentFilePath===a||this._detectAndSwitch()},300)});this._modeSubscribers.push({event:"active-leaf-change",ref:e});let t=this.app.vault.on("modify",s=>{s&&s.path&&s.path.endsWith("formal-library.json")&&(this._invalidateIndex(),this._refreshCurrentMode())});this._modeSubscribers.push({event:"modify",ref:t})}static async open(e){let t=e.app.workspace.getLeavesOfType(pe);if(t.length>0){e.app.workspace.revealLeaf(t[0]);return}let s=e.app.workspace.getRightLeaf(!1);s&&(await s.setViewState({type:pe,active:!0}),e.app.workspace.revealLeaf(s))}};var Re=class extends V.Plugin{constructor(){super(...arguments);this._lastExportMtime=0;this._lastOcrMtimes={};this._autoSyncRunning=!1;this._lastSyncTime=null;this._pollTimer=null;this._embedProcess=null;this._embedProgress={current:0,total:0,key:""};this._embedStderr="";this._memoryStatusText=null}async onload(){await this.loadSettings(),this.saveSettings(),Ye(this.app),this.registerView(pe,t=>new _e(t));try{(0,V.addIcon)(xe,Ge)}catch(t){}this.addRibbonIcon(xe,"PaperForge Dashboard",()=>_e.open(this)),W.find(t=>t.id==="paperforge-ocr-redo")&&this.addRibbonIcon("reset","PaperForge: \u91CD\u505AOCR",()=>{let t=this.app.vault.adapter.basePath;new V.Notice("PaperForge: \u91CD\u505AOCR starting...");let{path:s,extraArgs:n}=A(t,this.settings,void 0,void 0);(0,se.execFile)(s,[...n,"-m","paperforge","ocr","redo"],{cwd:t,timeout:6e5},(a,r,o)=>{if(a){new V.Notice("PaperForge: \u91CD\u505AOCR failed");return}new V.Notice("PaperForge: \u91CD\u505AOCR done")})}),this.addSettingTab(new De(this.app,this)),this.addCommand({id:"paperforge-status-panel",name:`PaperForge: ${i("guide_open")}`,callback:()=>_e.open(this)});for(let t of W)this.addCommand({id:t.id,name:`PaperForge: ${t.title}`,callback:()=>{if(t.disabled){new V.Notice(`[i] ${t.disabledMsg||"This action is not yet available."}`,6e3);return}let s=this.app.vault.adapter.basePath;new V.Notice(`PaperForge: running ${t.cmd}...`);let{path:n,extraArgs:a=[]}=A(s,this.settings,void 0,void 0),r=Array.isArray(t.args)?[...t.args]:[];(0,se.execFile)(n,[...a,"-m","paperforge",t.cmd,...r],{cwd:s,timeout:3e5},(o,l,c)=>{if(o){new V.Notice(`[!!] ${t.cmd} failed: ${(c||o.message).slice(0,120)}`,8e3);return}new V.Notice(`[OK] ${t.okMsg||l.trim().split(` -`)[0].slice(0,80)}`)})}});this.settings.auto_update_on_startup===!0&&this.settings.setup_complete&&setTimeout(()=>this._autoUpdate(),3e3),this._startFilePolling(),this._firstLaunchSnapshotMigration()}_firstLaunchSnapshotMigration(){let e=this.app.vault.adapter.basePath;if(!e)return;let s=ee(e).memoryStatePath;if(!I.existsSync(s)){let n=A(e,this.settings,void 0,void 0);[["runtime-health","--json"],["memory","status","--json"],["embed","status","--json"]].forEach(r=>{let o=[...n.extraArgs,"-m","paperforge","--vault",e,...r];(0,se.execFile)(n.path,o,{cwd:e,timeout:6e4,windowsHide:!0},()=>{})})}}_autoUpdate(){let e=this.app.vault.adapter.basePath,{path:t,extraArgs:s=[]}=A(e,this.settings,void 0,void 0),n=this.manifest.version,a=`paperforge==${n}`,r=`git+https://github.com/LLLin000/PaperForge.git@${n}`,o=(l,c)=>{(0,se.spawn)(t,[...s,"-m","pip","install","--upgrade",l],{cwd:e,timeout:12e4,env:de()}).on("close",u=>c(u===0))};(0,se.execFile)(t,[...s,"-c","import paperforge; print(paperforge.__version__)"],{cwd:e,timeout:1e4},(l,c)=>{let p=d=>{console.log(`[PaperForge] Auto-update: trying PyPI (paperforge==${n})`),o(a,_=>{if(_){console.log("[PaperForge] Auto-update: installed via PyPI"),new V.Notice(`[OK] PaperForge CLI ${d}`,5e3);return}console.warn("[PaperForge] Auto-update: PyPI failed, falling back to git..."),o(r,y=>{y&&(console.log("[PaperForge] Auto-update: installed via git"),new V.Notice(`[OK] PaperForge CLI ${d} (via git)`,5e3))})})};if(l){p("installed");return}let u=c.trim();u!==n&&p(`${u} -> ${n}`)})}_startFilePolling(){let e=this.app.vault.adapter.basePath;this._pollTimer=setInterval(()=>{this._checkExports(e),this._checkOcr(e)},12e4)}_checkExports(e){if(this._autoSyncRunning)return;let t=ee(e).exportsDir;if(!I.existsSync(t))return;let s=0;try{I.readdirSync(t).forEach(n=>{if(!n.endsWith(".json"))return;let a=I.statSync(me.join(t,n));a.mtimeMs>s&&(s=a.mtimeMs)})}catch(n){return}s>this._lastExportMtime&&(this._lastExportMtime=s,this._autoSync(e))}_autoSync(e){if(this._autoSyncRunning)return;this._autoSyncRunning=!0;let t=A(e,this.settings,void 0,void 0);if(!t.path){this._autoSyncRunning=!1;return}let s=`"${t.path}" -m paperforge --vault "${e}" sync`;(0,se.exec)(s,{timeout:12e4,encoding:"utf-8"},(n,a,r)=>{this._autoSyncRunning=!1,this._memoryStatusText=null,n||(this._lastSyncTime=new Date().toLocaleTimeString());try{let o=ee(e).exportsDir,l=0;I.readdirSync(o).forEach(c=>{c.endsWith(".json")&&(l=Math.max(l,I.statSync(me.join(o,c)).mtimeMs))}),this._lastExportMtime=l}catch(o){}})}_checkOcr(e){if(this._autoSyncRunning)return;let t=ee(e).ocrDir;if(I.existsSync(t))try{I.readdirSync(t,{withFileTypes:!0}).forEach(s=>{if(!s.isDirectory())return;let n=me.join(t,s.name,"meta.json");if(!I.existsSync(n))return;let a=I.statSync(n),r=this._lastOcrMtimes[s.name]||0;if(a.mtimeMs<=r||(this._lastOcrMtimes[s.name]=a.mtimeMs,this._autoSyncRunning))return;this._autoSyncRunning=!0;let o=A(e,this.settings,void 0,void 0);if(!o.path){this._autoSyncRunning=!1;return}let l=`"${o.path}" -m paperforge --vault "${e}" sync`;(0,se.exec)(l,{timeout:3e4,encoding:"utf-8"},()=>{this._autoSyncRunning=!1,this._memoryStatusText=null})})}catch(s){}}readPaperforgeJson(){let e=this.app.vault.adapter.basePath,t=me.join(e,"paperforge.json"),s={system_dir:"System",resources_dir:"Resources",literature_dir:"Literature",base_dir:"Bases"};try{if(!I.existsSync(t))return s;let n=I.readFileSync(t,"utf-8"),a=JSON.parse(n),r=a.vault_config||{};return{system_dir:r.system_dir||a.system_dir||s.system_dir,resources_dir:r.resources_dir||a.resources_dir||s.resources_dir,literature_dir:r.literature_dir||a.literature_dir||s.literature_dir,base_dir:r.base_dir||a.base_dir||s.base_dir}}catch(n){return console.warn("PaperForge: Failed to read paperforge.json, using defaults",n),s}}savePaperforgeJson(e){let t=this.app.vault.adapter.basePath,s=me.join(t,"paperforge.json"),n={};try{I.existsSync(s)&&(n=JSON.parse(I.readFileSync(s,"utf-8")))}catch(r){console.warn("PaperForge: Failed to read paperforge.json for update",r)}(!n.vault_config||typeof n.vault_config!="object")&&(n.vault_config={});let a=["system_dir","resources_dir","literature_dir","base_dir"];for(let r of a)e[r]!==void 0&&(n.vault_config[r]=e[r]);n.schema_version||(n.schema_version="2");for(let r of a)delete n[r];try{if(I.writeFileSync(s,JSON.stringify(n,null,2),"utf-8"),this.settings){let r=this.readPaperforgeJson();this.settings.system_dir=r.system_dir,this.settings.resources_dir=r.resources_dir,this.settings.literature_dir=r.literature_dir,this.settings.base_dir=r.base_dir}}catch(r){console.error("PaperForge: Failed to write paperforge.json",r),new V.Notice("PaperForge: Failed to save configuration to paperforge.json")}}onunload(){this._pollTimer&&clearInterval(this._pollTimer),this.app.workspace.detachLeavesOfType(pe)}async loadSettings(){this.settings=Object.assign({},Ee,await this.loadData()),this.settings.features&&Ee.features&&(this.settings.features=Object.assign({},Ee.features,this.settings.features||{})),this.settings.frozen_skills||(this.settings.frozen_skills={});let e=this.readPaperforgeJson();if(this.settings.system_dir=e.system_dir,this.settings.resources_dir=e.resources_dir,this.settings.literature_dir=e.literature_dir,this.settings.base_dir=e.base_dir,this.settings.python_path&&this.settings.python_path.trim()){let t=this.settings.python_path.trim();I.existsSync(t)?this.settings._python_path_stale=!1:(console.warn(`PaperForge: Saved python_path "${t}" no longer exists - showing stale warning`),this.settings._python_path_stale=!0)}}async saveSettings(){let e={};for(let t of Object.keys(Ee))t in this.settings&&(e[t]=this.settings[t]);await this.saveData(e)}}; +`)[0].trim();return!r||r.startsWith("###")||r.startsWith("##")?null:r.length>300?r.slice(0,300)+"...":r}_renderRecentDiscussionCard(e,t){let s=e.createEl("div",{cls:"paperforge-discussion-card"});if(s.style.display="none",!t.note_path)return;let n=t.note_path.lastIndexOf("/"),r=(n!==-1?t.note_path.substring(0,n):".")+"/ai/discussion.md";this.app.vault.adapter.exists(r).then(l=>{if(l)return this.app.vault.adapter.read(r)}).then(async l=>{if(!l)return;let i=this._parseDiscussionMD(l);if(!i||i.length===0)return;s.style.display="block",s.createEl("div",{cls:"paperforge-discussion-header"}).createEl("span",{cls:"paperforge-discussion-title",text:"\u6700\u8FD1\u8BA8\u8BBA"});for(let h of i){let d=s.createEl("div",{cls:"paperforge-discussion-item"}),y=d.createEl("div",{cls:"paperforge-discussion-q"});y.createEl("span",{cls:"paperforge-discussion-q-label",text:"\u63D0\u95EE\uFF1A"}),y.createEl("span",{cls:"paperforge-discussion-q-text",text:h.question});let b=d.createEl("div",{cls:"paperforge-discussion-a"}),v=!1;if(h.answer&&h.answer.length>500&&(v=!0,b.classList.add("paperforge-discussion-a-collapsed")),await D.MarkdownRenderer.render(this.app,h.answer||"",b,r,this),v){let f=!1;d.style.cursor="pointer",d.addEventListener("click",()=>{f=!f,b.classList.toggle("paperforge-discussion-a-collapsed",!f),b.classList.toggle("paperforge-discussion-a-expanded",f)})}}s.createEl("a",{cls:"paperforge-discussion-viewall",text:"\u67E5\u770B\u5168\u90E8\u8BA8\u8BBA \u2192"}).addEventListener("click",h=>{h.preventDefault(),this.app.vault.getAbstractFileByPath(r)?this.app.workspace.openLinkText(r,""):new D.Notice("\u8BA8\u8BBA\u6587\u4EF6\u5C1A\u672A\u751F\u6210")})}).catch(l=>{console.error("PaperForge: discussion.md read error",r,l.message)})}_parseDiscussionMD(e){let t=e.split(/\n## /).slice(1);if(t.length===0)return null;let s=t[t.length-1],n=[],a=s.split(/\*\*\u95EE\u9898:\*\*/).slice(1);for(let r of a){let l=r.match(/\*\*\u89E3\u7B54:\*\*/);if(!l)continue;let i=r.substring(0,l.index).trim(),c=r.substring(l.index+3+4).trim();n.push({question:i,answer:c})}return n.slice(-3)}_renderPaperTechnicalDetails(e,t){let s=this._currentPaperKey,n=e.createEl("div",{cls:"paperforge-technical-details"}),a=n.createEl("button",{cls:"paperforge-technical-details-toggle"}),r=n.createEl("div",{cls:"paperforge-technical-details-body"});r.style.display="none",this._techDetailsExpanded?(r.style.display="block",a.setText("\u6280\u672F\u8BE6\u60C5 \u25BE")):a.setText("\u6280\u672F\u8BE6\u60C5 \u25B8"),a.addEventListener("click",()=>{let h=r.style.display!=="none";r.style.display=h?"none":"block",a.setText(h?"\u6280\u672F\u8BE6\u60C5 \u25B8":"\u6280\u672F\u8BE6\u60C5 \u25BE"),this._techDetailsExpanded=!h});let l=r.createEl("div",{cls:"paperforge-workflow-toggles"}),i=[{key:"do_ocr",label:"OCR",hint:"\u52A0\u5165 OCR"},{key:"analyze",label:"\u7CBE\u8BFB",hint:"\u6807\u8BB0\u7CBE\u8BFB"}];for(let h of i){let d=l.createEl("label",{cls:"paperforge-workflow-toggle"}),y=d.createEl("input",{type:"checkbox",cls:"paperforge-workflow-checkbox"});y.checked=t[h.key]===!0,d.createEl("span",{cls:"paperforge-workflow-toggle-label",text:h.label}),d.createEl("span",{cls:"paperforge-workflow-toggle-hint",text:h.hint}),y.addEventListener("change",async()=>{let b=t.note_path?this.app.vault.getAbstractFileByPath(t.note_path):null;if(!b){new D.Notice("[!!] Note file not found",6e3);return}let v=y.checked;await this.app.fileManager.processFrontMatter(b,f=>{f[h.key]=v}),this._patchCachedEntry(s,{[h.key]:v}),this._currentPaperEntry=Ie(this._currentPaperEntry,{[h.key]:v})})}let c=t.health||{},p=[["PDF Health",c.pdf_health||"\u2014"],["OCR Status",t.ocr_status||"\u2014"],["Asset Health",c.asset_health||"\u2014"],["Note Path",t.note_path||"\u2014"],["Fulltext Path",t.fulltext_path||"\u2014"]];for(let[h,d]of p){let y=r.createEl("div",{cls:"paperforge-technical-row"});y.createEl("span",{cls:"paperforge-technical-label",text:h}),y.createEl("span",{cls:"paperforge-technical-value",text:String(d)})}}_renderNextStepCard(e,t,s){var i,c;let n=t.next_step||"ready",a={sync:{label:"Sync Needed",text:"This paper needs to be synced from Zotero. Click to run sync.",cmd:"sync",icon:"\u21BB"},ocr:{label:"OCR Needed",text:"Fulltext is missing but PDF is present. Click to run OCR.",cmd:"ocr",icon:"\u229E"},repair:{label:"Repair Needed",text:"State divergence or path errors detected. Click to repair.",cmd:"repair",icon:"\u21BA"},"rebuild index":{label:"Rebuild Needed",text:"Index may be stale. Click to run sync to rebuild.",cmd:"sync",icon:"\u21BB"},"/pf-deep":{label:"Ready for Deep Reading",text:"Fulltext is ready. Copy /pf-deep command and run in your agent.",cmd:null,icon:"\u{1F50D}"},ready:{label:"All Set",text:"This paper is fully processed and ready for use.",cmd:"ready",icon:"\u2713"}},r=a[n]||a.ready,l=e.createEl("div",{cls:"paperforge-next-step-card"});if(n==="ready"&&l.addClass("ready"),l.createEl("div",{cls:"paperforge-next-step-label",text:"Recommended Next Step"}),l.createEl("div",{cls:"paperforge-next-step-text",text:r.text}),r.cmd&&r.cmd!=="ready"){let p=l.createEl("button",{cls:"paperforge-next-step-trigger"});p.createEl("span",{text:r.icon+" "+r.label}),p.addEventListener("click",()=>{let h=H.find(d=>d.cmd===r.cmd);h&&this._runAction(h,p)})}else if(n==="/pf-deep"){let p=l.createEl("button",{cls:"paperforge-next-step-trigger"});p.createEl("span",{text:"\u{1F4CB} "+o("copy_pf_deep_cmd")}),p.addEventListener("click",()=>{let v="/pf-deep "+s;navigator.clipboard.writeText(v).then(()=>{p.setText("\u2713 "+o("copied")),new D.Notice(v+" copied")}).catch(()=>{new D.Notice("[!!] Clipboard write failed",6e3)})});let h=((c=(i=this.app.plugins.plugins.paperforge)==null?void 0:i.settings)==null?void 0:c.agent_platform)||"opencode",y={opencode:"OpenCode",claude:"Claude Code",cursor:"Cursor",github_copilot:"GitHub Copilot",windsurf:"Windsurf",codex:"Codex",cline:"Cline"}[h]||h;l.createEl("div",{cls:"paperforge-agent-platform-label"}).setText(o("run_in_agent").replace("{0}",y))}else n==="ready"&&l.createEl("button",{cls:"paperforge-next-step-trigger"}).createEl("span",{text:"\u2713 "+r.label})}_openFulltext(e){if(!e){new D.Notice("[!!] No fulltext path available for this paper",6e3);return}let t=this.app.vault.getAbstractFileByPath(e);t?this.app.workspace.openLinkText(t.path,""):new D.Notice("[!!] Fulltext file not found: "+e,6e3)}_renderCollectionMode(){let e=this._currentDomain||"Unknown",t=this._filterByDomain(e);if(t.length===0){this._renderGlobalMode();return}if(!this._contentEl)return;let s=this._contentEl.createEl("div",{cls:"paperforge-collection-view"}),n=t.length,a=0,r=0,l=0,i=0,c=0,p=0,h=0;for(let _ of t){_.has_pdf&&a++,_.ocr_status==="done"&&r++,_.ocr_status==="done"&&_.analyze===!0&&l++,_.deep_reading_status==="done"&&i++;let m=_.ocr_status||"";m==="pending"||m==="queued"?c++:m==="processing"?p++:(m==="failed"||m==="blocked"||m==="done_incomplete"||m==="nopdf")&&h++}s.createEl("div",{cls:"paperforge-collection-header"}).createEl("div",{cls:"paperforge-collection-title",text:e});let y=s.createEl("div",{cls:"paperforge-workflow-overview"});y.createEl("div",{cls:"paperforge-section-label",text:"Workflow Overview"});let b=y.createEl("div",{cls:"paperforge-workflow-funnel"}),v=[{value:n,label:"Total"},{value:a,label:"PDF Ready"},{value:r,label:"OCR Done"},{value:i,label:"Deep Read"}];for(let _=0;_<v.length;_++){let m=b.createEl("div",{cls:"paperforge-workflow-stage"});m.createEl("div",{cls:"paperforge-workflow-stage-value",text:String(v[_].value)}),m.createEl("div",{cls:"paperforge-workflow-stage-label",text:v[_].label}),_<v.length-1&&b.createEl("div",{cls:"paperforge-workflow-arrow",text:"\u2192"})}if(c+p+r+h>0){let _=s.createEl("div",{cls:"paperforge-ocr-section"}),m=_.createEl("div",{cls:"paperforge-collection-ocr-header"});m.createEl("h4",{cls:"paperforge-ocr-title",text:"OCR Pipeline"});let k=m.createEl("span",{cls:"paperforge-ocr-badge idle"});p>0?(k.addClass("active"),k.setText("Processing")):c>0?k.setText("Pending"):(k.addClass("idle"),k.setText("Idle"));let S=_.createEl("div",{cls:"paperforge-progress-track"});p>0&&S.addClass("paperforge-processing");let F=c+p+r+h,R=[{cls:"pending",count:c},{cls:"active",count:p},{cls:"done",count:r},{cls:"failed",count:h}];for(let U of R)if(U.count>0){let A=(U.count/F*100).toFixed(1);S.createEl("div",{cls:`paperforge-progress-seg ${U.cls}`,attr:{style:`width:${A}%`}})}let B=_.createEl("div",{cls:"paperforge-ocr-counts"}),q=[{cls:"pending",value:c,label:"Pending"},{cls:"active",value:p,label:"Processing"},{cls:"done",value:r,label:"Done"},{cls:"failed",value:h,label:"Attention"}];for(let U of q){let A=B.createEl("div",{cls:"paperforge-ocr-count"});A.createEl("div",{cls:"paperforge-ocr-count-value",text:U.value.toString()}),A.createEl("div",{cls:"paperforge-ocr-count-label",text:U.label})}}let f=s.createEl("div",{cls:"paperforge-collection-actions"}),x=f.createEl("button",{cls:"paperforge-contextual-btn primary"});x.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u229E"}),x.createEl("span",{text:"Run OCR"}),x.addEventListener("click",()=>{let _=H.find(m=>m.id==="paperforge-ocr");_&&this._runAction(_,x)});let C=f.createEl("button",{cls:"paperforge-contextual-btn"});C.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u21BB"}),C.createEl("span",{text:"Sync Library"}),C.addEventListener("click",()=>{let _=H.find(m=>m.id==="paperforge-sync");_&&this._runAction(_,C)});let E=f.createEl("button",{cls:"paperforge-contextual-btn warn"});E.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u21BA"}),E.createEl("span",{text:"Redo OCR"}),E.addEventListener("click",()=>{let _=H.find(m=>m.id==="paperforge-ocr-redo");_&&this._runAction(_,E)})}_refreshCurrentMode(){if(!(!this._currentMode||!this._contentEl)){this._contentEl.empty(),this._contentEl.addClass("switching"),this._invalidateIndex(),this._currentPaperEntry=this._currentPaperKey?this._findEntry(this._currentPaperKey):null,this._renderModeHeader(this._currentMode);try{switch(this._currentMode){case"global":this._renderGlobalMode();break;case"paper":this._renderPaperMode();break;case"collection":this._renderCollectionMode();break}}finally{setTimeout(()=>{this._contentEl&&this._contentEl.removeClass("switching")},50)}}}_runAction(e,t){var d,y;if(e.disabled){new D.Notice(`[i] ${e.disabledMsg||"This action is not yet available."}`,6e3);return}if(t.classList.contains("running"))return;t.addClass("running");let s=this.app.vault.adapter.basePath;this._showMessage("Processing...","running");let n=Array.isArray(e.args)?[...e.args]:[];if(e.needsKey){let b=this.app.workspace.getActiveFile(),v=null;if(b){let f=this.app.metadataCache.getFileCache(b);if(f&&f.frontmatter&&f.frontmatter.zotero_key?v=f.frontmatter.zotero_key:v=this._extractZoteroKeyFromPath(b.path),v)n=[...n,v];else if(f&&f.frontmatter){this._showMessage("[!!] No zotero_key in active note frontmatter","error"),new D.Notice("[!!] Open a paper note with a zotero_key in its frontmatter first",6e3),t.removeClass("running");return}else{this._showMessage("[!!] No frontmatter in active note","error"),new D.Notice("[!!] The active note has no frontmatter with a zotero_key",6e3),t.removeClass("running");return}}else{this._showMessage("[!!] No active note open","error"),new D.Notice("[!!] Open a paper note with a zotero_key in its frontmatter first",6e3),t.removeClass("running");return}}e.needsFilter&&(n=[...n,"--all"]);let a=e.needsFilter?6e4:e.needsKey?3e4:6e5,{path:r,extraArgs:l=[]}=O(s,(y=(d=this.app.plugins.plugins.paperforge)==null?void 0:d.settings)!=null?y:null,void 0,void 0),i=(0,ue.spawn)(r,[...l,"-m","paperforge",e.cmd,...n],{cwd:s,timeout:a}),c=[],p=Date.now(),h=setInterval(()=>this._fetchStats(!0),4e3);i.stdout.on("data",b=>{let v=b.toString("utf-8").split(` +`).filter(Boolean);for(let f of v){let x=f.trim();x&&(c.push(x),this._showMessage(c.slice(-8).join(` +`),"running"))}}),i.stderr.on("data",b=>{let v=b.toString("utf-8").split(` +`).filter(Boolean);for(let f of v){if(f.includes("\r")||f.includes("%")||f.includes("\u2588"))continue;let x=f.trim();x&&!x.match(/^\d+%|^\|/)&&(c.push(x),this._showMessage(c.slice(-8).join(` +`),"running"))}}),i.on("close",b=>{clearInterval(h),t.removeClass("running");let v=((Date.now()-p)/1e3).toFixed(1);if(b!==0){let f=c.slice(-3).join(" | ")||"exit code "+b;(e.cmd==="repair"||e.cmd==="ocr")&&b===1?(this._showMessage("[WARN] "+f,"running"),new D.Notice("[WARN] "+e.cmd+" partial: "+f,8e3),this._fetchStats(!0)):(this._showMessage("[!!] "+f,"error"),new D.Notice("[!!] "+e.cmd+" failed: "+f,8e3))}else if(e.needsKey||e.needsFilter){let f=c.join(` +`);if(f.trim())try{JSON.parse(f),navigator.clipboard.writeText(f).then(()=>{let x=`${v}s \u2014 ${f.length} chars copied`;this._showMessage("[OK] "+e.title+": "+x,"ok"),new D.Notice("[OK] "+e.okMsg+" \u2014 "+f.length+" chars")}).catch(x=>{this._showMessage("[!!] Clipboard write failed: "+x.message,"error"),new D.Notice("[!!] Clipboard error",6e3)})}catch(x){this._showMessage("[!!] Invalid JSON from "+e.title,"error"),new D.Notice("[!!] "+e.title+" returned invalid JSON: "+x.message.slice(0,100),8e3)}else this._showMessage("[!!] No output from context command","error"),new D.Notice("[!!] Context command returned empty output",8e3);this._fetchStats(!0)}else{let x=c.filter(E=>E.match(/updated \d+/)).pop()||c[c.length-1]||"",C=`${v}s \u2014 ${x}`;this._showMessage("[OK] "+e.title+": "+C,"ok"),new D.Notice("[OK] "+e.okMsg),this._contentEl&&this._contentEl.removeClass("switching"),this._cachedStats=null;try{this._fetchStats(!1)}catch(E){console.log("[PF] fetchStats error:",E)}console.log("[PF] close cmd="+e.cmd+" id="+e.id),e.cmd==="sync"&&Re(this.app,this.app.plugins.plugins.paperforge,s)}}),i.on("error",b=>{t.removeClass("running"),this._contentEl&&this._contentEl.removeClass("switching"),this._showMessage("[!!] "+b.message,"error"),new D.Notice("[!!] Cannot start: "+b.message,8e3)})}_showMessage(e,t){this._messageEl&&(this._messageEl.setText(e),this._messageEl.className=`paperforge-message msg-${t}`)}_renderModeHeader(e){if(!this._modeContextEl)return;this._modeContextEl.empty();let t=this._modeContextEl.createEl("span",{cls:"paperforge-mode-badge"}),s="";switch(e){case"global":t.addClass("global"),t.setText("Global"),this._headerTitle&&this._headerTitle.setText("PaperForge");break;case"paper":t.addClass("paper"),t.setText("Paper"),this._headerTitle&&this._headerTitle.setText("Paper"),this._currentPaperEntry&&this._currentPaperEntry.title?s=this._currentPaperEntry.title:this._currentPaperKey?(s=this._currentPaperKey,this._modeContextEl.createEl("span",{cls:"paperforge-mode-warning",text:"Not found in index"})):s="Unknown paper";break;case"collection":t.addClass("collection"),t.setText("Collection"),this._headerTitle&&this._headerTitle.setText("Collection"),s=this._currentDomain||"Unknown Domain";break}s&&this._modeContextEl.createEl("span",{cls:"paperforge-mode-name",text:s})}_setupEventSubscriptions(){let e=this.app.workspace.on("active-leaf-change",()=>{this._leafChangeTimer&&clearTimeout(this._leafChangeTimer),this._leafChangeTimer=setTimeout(()=>{let s=this._resolveModeForFile(this.app.workspace.getActiveFile()),n=s.mode,a=s.filePath;this._currentMode===n&&this._currentFilePath===a||this._detectAndSwitch()},300)});this._modeSubscribers.push({event:"active-leaf-change",ref:e});let t=this.app.vault.on("modify",s=>{s&&s.path&&s.path.endsWith("formal-library.json")&&(this._invalidateIndex(),this._refreshCurrentMode())});this._modeSubscribers.push({event:"modify",ref:t})}static async open(e){let t=e.app.workspace.getLeavesOfType(ce);if(t.length>0){e.app.workspace.revealLeaf(t[0]);return}let s=e.app.workspace.getRightLeaf(!1);s&&(await s.setViewState({type:ce,active:!0}),e.app.workspace.revealLeaf(s))}};var Ae=class extends M.Plugin{constructor(){super(...arguments);this._lastExportMtime=0;this._lastOcrMtimes={};this._autoSyncRunning=!1;this._lastSyncTime=null;this._pollTimer=null;this._embedProcess=null;this._embedProgress={current:0,total:0,key:""};this._embedStderr="";this._memoryStatusText=null}async onload(){await this.loadSettings(),this.saveSettings(),tt(this.app),this.registerView(ce,t=>new fe(t));try{(0,M.addIcon)(xe,Qe)}catch(t){}this.addRibbonIcon(xe,"PaperForge Dashboard",()=>fe.open(this)),H.find(t=>t.id==="paperforge-ocr-redo")&&this.addRibbonIcon("reset","PaperForge: Redo OCR",()=>{let t=this.app.vault.adapter.basePath;new M.Notice("PaperForge: Redo OCR starting...");let{path:s,extraArgs:n}=O(t,this.settings,void 0,void 0);(0,se.execFile)(s,[...n,"-m","paperforge","ocr","redo"],{cwd:t,timeout:6e5},(a,r,l)=>{if(a){new M.Notice("PaperForge: Redo OCR failed");return}new M.Notice("PaperForge: Redo OCR done")})}),this.addSettingTab(new Be(this.app,this)),this.addCommand({id:"paperforge-status-panel",name:`PaperForge: ${o("guide_open")}`,callback:()=>fe.open(this)});for(let t of H)this.addCommand({id:t.id,name:`PaperForge: ${t.title}`,callback:()=>{if(t.disabled){new M.Notice(`[i] ${t.disabledMsg||"This action is not yet available."}`,6e3);return}let s=this.app.vault.adapter.basePath;new M.Notice(`PaperForge: running ${t.cmd}...`);let{path:n,extraArgs:a=[]}=O(s,this.settings,void 0,void 0),r=Array.isArray(t.args)?[...t.args]:[];(0,se.execFile)(n,[...a,"-m","paperforge",t.cmd,...r],{cwd:s,timeout:3e5},(l,i,c)=>{if(l){new M.Notice(`[!!] ${t.cmd} failed: ${(c||l.message).slice(0,120)}`,8e3);return}new M.Notice(`[OK] ${t.okMsg||i.trim().split(` +`)[0].slice(0,80)}`)})}});this.settings.auto_update_on_startup===!0&&this.settings.setup_complete&&setTimeout(()=>this._autoUpdate(),3e3),this._startFilePolling(),this._firstLaunchSnapshotMigration(),this._checkReleaseNotes()}_firstLaunchSnapshotMigration(){let e=this.app.vault.adapter.basePath;if(!e)return;let s=te(e).memoryStatePath;if(!L.existsSync(s)){let n=O(e,this.settings,void 0,void 0);[["runtime-health","--json"],["memory","status","--json"],["embed","status","--json"]].forEach(r=>{let l=[...n.extraArgs,"-m","paperforge","--vault",e,...r];(0,se.execFile)(n.path,l,{cwd:e,timeout:6e4,windowsHide:!0},()=>{})})}}_autoUpdate(){let e=this.app.vault.adapter.basePath,{path:t,extraArgs:s=[]}=O(e,this.settings,void 0,void 0),n=this.manifest.version,a=`paperforge==${n}`,r=`git+https://github.com/LLLin000/PaperForge.git@${n}`,l=(i,c)=>{(0,se.spawn)(t,[...s,"-m","pip","install","--upgrade",i],{cwd:e,timeout:12e4,env:pe()}).on("close",h=>c(h===0))};(0,se.execFile)(t,[...s,"-c","import paperforge; print(paperforge.__version__)"],{cwd:e,timeout:1e4},(i,c)=>{let p=d=>{console.log(`[PaperForge] Auto-update: trying PyPI (paperforge==${n})`),l(a,y=>{if(y){console.log("[PaperForge] Auto-update: installed via PyPI"),new M.Notice(`[OK] PaperForge CLI ${d}`,5e3);return}console.warn("[PaperForge] Auto-update: PyPI failed, falling back to git..."),l(r,b=>{b&&(console.log("[PaperForge] Auto-update: installed via git"),new M.Notice(`[OK] PaperForge CLI ${d} (via git)`,5e3))})})};if(i){p("installed");return}let h=c.trim();h!==n&&p(`${h} -> ${n}`)})}_startFilePolling(){let e=this.app.vault.adapter.basePath;this._pollTimer=setInterval(()=>{this._checkExports(e),this._checkOcr(e)},12e4)}_checkExports(e){if(this._autoSyncRunning)return;let t=te(e).exportsDir;if(!L.existsSync(t))return;let s=0;try{L.readdirSync(t).forEach(n=>{if(!n.endsWith(".json"))return;let a=L.statSync(_e.join(t,n));a.mtimeMs>s&&(s=a.mtimeMs)})}catch(n){return}s>this._lastExportMtime&&(this._lastExportMtime=s,this._autoSync(e))}_autoSync(e){if(this._autoSyncRunning)return;this._autoSyncRunning=!0;let t=O(e,this.settings,void 0,void 0);if(!t.path){this._autoSyncRunning=!1;return}let s=`"${t.path}" -m paperforge --vault "${e}" sync`;(0,se.exec)(s,{timeout:12e4,encoding:"utf-8"},(n,a,r)=>{this._autoSyncRunning=!1,this._memoryStatusText=null,n||(this._lastSyncTime=new Date().toLocaleTimeString());try{let l=te(e).exportsDir,i=0;L.readdirSync(l).forEach(c=>{c.endsWith(".json")&&(i=Math.max(i,L.statSync(_e.join(l,c)).mtimeMs))}),this._lastExportMtime=i}catch(l){}})}_checkOcr(e){if(this._autoSyncRunning)return;let t=te(e).ocrDir;if(L.existsSync(t))try{L.readdirSync(t,{withFileTypes:!0}).forEach(s=>{if(!s.isDirectory())return;let n=_e.join(t,s.name,"meta.json");if(!L.existsSync(n))return;let a=L.statSync(n),r=this._lastOcrMtimes[s.name]||0;if(a.mtimeMs<=r||(this._lastOcrMtimes[s.name]=a.mtimeMs,this._autoSyncRunning))return;this._autoSyncRunning=!0;let l=O(e,this.settings,void 0,void 0);if(!l.path){this._autoSyncRunning=!1;return}let i=`"${l.path}" -m paperforge --vault "${e}" sync`;(0,se.exec)(i,{timeout:3e4,encoding:"utf-8"},()=>{this._autoSyncRunning=!1,this._memoryStatusText=null})})}catch(s){}}readPaperforgeJson(){let e=this.app.vault.adapter.basePath,t=_e.join(e,"paperforge.json"),s={system_dir:"System",resources_dir:"Resources",literature_dir:"Literature",base_dir:"Bases"};try{if(!L.existsSync(t))return s;let n=L.readFileSync(t,"utf-8"),a=JSON.parse(n),r=a.vault_config||{};return{system_dir:r.system_dir||a.system_dir||s.system_dir,resources_dir:r.resources_dir||a.resources_dir||s.resources_dir,literature_dir:r.literature_dir||a.literature_dir||s.literature_dir,base_dir:r.base_dir||a.base_dir||s.base_dir}}catch(n){return console.warn("PaperForge: Failed to read paperforge.json, using defaults",n),s}}savePaperforgeJson(e){let t=this.app.vault.adapter.basePath,s=_e.join(t,"paperforge.json"),n={};try{L.existsSync(s)&&(n=JSON.parse(L.readFileSync(s,"utf-8")))}catch(r){console.warn("PaperForge: Failed to read paperforge.json for update",r)}(!n.vault_config||typeof n.vault_config!="object")&&(n.vault_config={});let a=["system_dir","resources_dir","literature_dir","base_dir"];for(let r of a)e[r]!==void 0&&(n.vault_config[r]=e[r]);n.schema_version||(n.schema_version="2");for(let r of a)delete n[r];try{if(L.writeFileSync(s,JSON.stringify(n,null,2),"utf-8"),this.settings){let r=this.readPaperforgeJson();this.settings.system_dir=r.system_dir,this.settings.resources_dir=r.resources_dir,this.settings.literature_dir=r.literature_dir,this.settings.base_dir=r.base_dir}}catch(r){console.error("PaperForge: Failed to write paperforge.json",r),new M.Notice("PaperForge: Failed to save configuration to paperforge.json")}}onunload(){this._pollTimer&&clearInterval(this._pollTimer),this.app.workspace.detachLeavesOfType(ce)}async loadSettings(){this.settings=Object.assign({},Ee,await this.loadData()),this.settings.features&&Ee.features&&(this.settings.features=Object.assign({},Ee.features,this.settings.features||{})),this.settings.frozen_skills||(this.settings.frozen_skills={});let e=this.readPaperforgeJson();if(this.settings.system_dir=e.system_dir,this.settings.resources_dir=e.resources_dir,this.settings.literature_dir=e.literature_dir,this.settings.base_dir=e.base_dir,this.settings.python_path&&this.settings.python_path.trim()){let t=this.settings.python_path.trim();L.existsSync(t)?this.settings._python_path_stale=!1:(console.warn(`PaperForge: Saved python_path "${t}" no longer exists - showing stale warning`),this.settings._python_path_stale=!0)}}async saveSettings(){let e={};for(let t of Object.keys(Ee))t in this.settings&&(e[t]=this.settings[t]);await this.saveData(e)}_checkReleaseNotes(){let e=this.manifest.version;if(this.settings.last_seen_version===e)return;let a=(Ne().versions||[]).find(l=>l.version===e);class r extends M.Modal{constructor(i,c){super(i),this._entry=c}onOpen(){let{contentEl:i}=this;if(i.createEl("h2",{text:`PaperForge v${e} \u66F4\u65B0\u8BF4\u660E`}),this._entry){if(i.createEl("p",{text:this._entry.title,cls:"paperforge-modal-subtitle"}),this._entry.breaking_or_migration&&this._entry.breaking_or_migration.length>0){i.createEl("h4",{text:"\u884C\u4E3A\u53D8\u66F4 / \u8FC1\u79FB\u6CE8\u610F"});for(let c of this._entry.breaking_or_migration)i.createEl("p",{text:`\u2022 ${c}`,cls:"paperforge-modal-item"})}if(this._entry.new_features&&this._entry.new_features.length>0){i.createEl("h4",{text:"\u65B0\u529F\u80FD"});for(let c of this._entry.new_features)i.createEl("p",{text:`\u2022 ${c}`,cls:"paperforge-modal-item"})}if(this._entry.fixes&&this._entry.fixes.length>0){i.createEl("h4",{text:"\u4FEE\u590D"});for(let c of this._entry.fixes)i.createEl("p",{text:`\u2022 ${c}`,cls:"paperforge-modal-item"})}if(this._entry.recommended_actions&&this._entry.recommended_actions.length>0){i.createEl("h4",{text:"\u5EFA\u8BAE\u64CD\u4F5C"});for(let c of this._entry.recommended_actions)i.createEl("p",{text:`\u2022 ${c}`,cls:"paperforge-modal-item"})}}else i.createEl("p",{text:"\u7248\u672C\u5DF2\u66F4\u65B0\u81F3 v"+e+"\uFF0C\u8BF7\u524D\u5F80\u8BBE\u7F6E \u2192 \u66F4\u65B0\u4E0E\u624B\u518C \u67E5\u770B\u5B8C\u6574\u66F4\u65B0\u8BB0\u5F55\u3002"});new M.Setting(i).addButton(c=>c.setButtonText("\u77E5\u9053\u4E86").setCta().onClick(()=>{this.close()}))}onClose(){let{contentEl:i}=this;i.empty()}}new r(this.app,a).open(),this.settings.last_seen_version=e,this.saveSettings()}}; diff --git a/paperforge/plugin/src/constants.ts b/paperforge/plugin/src/constants.ts index 0ab0b01a..7843f98a 100644 --- a/paperforge/plugin/src/constants.ts +++ b/paperforge/plugin/src/constants.ts @@ -56,7 +56,7 @@ export const ACTIONS: ActionDef[] = [ }, { id: "paperforge-ocr-redo", - title: "重做OCR", + title: "Redo OCR", desc: "Re-run OCR for papers marked ocr_redo: true", icon: "\u21BA", cmd: "ocr", @@ -87,6 +87,7 @@ export interface PaperForgeSettings { vector_db_api_key: string; vector_db_api_base: string; vector_db_api_model: string; + last_seen_version: string; _python_path_stale?: boolean; [key: string]: unknown; } @@ -114,6 +115,7 @@ export const DEFAULT_SETTINGS: PaperForgeSettings = { resources_dir: "", literature_dir: "", base_dir: "", + last_seen_version: "", }; // ── Workflow state helpers ── diff --git a/paperforge/plugin/src/main.ts b/paperforge/plugin/src/main.ts index 6e683c78..f736372b 100644 --- a/paperforge/plugin/src/main.ts +++ b/paperforge/plugin/src/main.ts @@ -1,4 +1,4 @@ -import { Plugin, addIcon, Notice } from "obsidian"; +import { Plugin, addIcon, Notice, Modal, Setting } from "obsidian"; import * as fs from "fs"; import * as path from "path"; import { execFile, exec, spawn } from "child_process"; @@ -32,13 +32,13 @@ export default class PaperForgePlugin extends Plugin { const redoAction = ACTIONS.find(a => a.id === "paperforge-ocr-redo"); if (redoAction) { - this.addRibbonIcon("reset", "PaperForge: 重做OCR", () => { + this.addRibbonIcon("reset", "PaperForge: Redo OCR", () => { const vp = (this.app.vault.adapter as any).basePath as string; - new Notice(`PaperForge: 重做OCR starting...`); + new Notice(`PaperForge: Redo OCR starting...`); const { path: py, extraArgs: ex } = resolvePythonExecutable(vp, this.settings, undefined, undefined); execFile(py, [...ex, "-m", "paperforge", "ocr", "redo"], { cwd: vp, timeout: 600000 }, (err, stdout, stderr) => { - if (err) { new Notice(`PaperForge: 重做OCR failed`); return; } - new Notice(`PaperForge: 重做OCR done`); + if (err) { new Notice(`PaperForge: Redo OCR failed`); return; } + new Notice(`PaperForge: Redo OCR done`); }); }); } @@ -80,6 +80,7 @@ export default class PaperForgePlugin extends Plugin { } this._startFilePolling(); this._firstLaunchSnapshotMigration(); + this._checkReleaseNotes(); } private _firstLaunchSnapshotMigration() { @@ -335,4 +336,67 @@ export default class PaperForgePlugin extends Plugin { } await this.saveData(dataToSave); } + + private _checkReleaseNotes() { + const currentVersion = this.manifest.version; + const seen = this.settings.last_seen_version; + if (seen === currentVersion) return; + + const releaseNotesData = require("./release-notes.json"); + const versions = releaseNotesData.versions || []; + const currentEntry = versions.find((v: any) => v.version === currentVersion); + + class ReleaseNotesModal extends Modal { + private _entry: any; + constructor(app: any, entry: any) { + super(app); + this._entry = entry; + } + onOpen() { + const { contentEl } = this; + contentEl.createEl("h2", { text: `PaperForge v${currentVersion} \u66F4\u65B0\u8BF4\u660E` }); + if (this._entry) { + contentEl.createEl("p", { text: this._entry.title, cls: "paperforge-modal-subtitle" }); + if (this._entry.breaking_or_migration && this._entry.breaking_or_migration.length > 0) { + contentEl.createEl("h4", { text: "\u884C\u4E3A\u53D8\u66F4 / \u8FC1\u79FB\u6CE8\u610F" }); + for (const item of this._entry.breaking_or_migration) { + contentEl.createEl("p", { text: `\u2022 ${item}`, cls: "paperforge-modal-item" }); + } + } + if (this._entry.new_features && this._entry.new_features.length > 0) { + contentEl.createEl("h4", { text: "\u65B0\u529F\u80FD" }); + for (const item of this._entry.new_features) { + contentEl.createEl("p", { text: `\u2022 ${item}`, cls: "paperforge-modal-item" }); + } + } + if (this._entry.fixes && this._entry.fixes.length > 0) { + contentEl.createEl("h4", { text: "\u4FEE\u590D" }); + for (const item of this._entry.fixes) { + contentEl.createEl("p", { text: `\u2022 ${item}`, cls: "paperforge-modal-item" }); + } + } + if (this._entry.recommended_actions && this._entry.recommended_actions.length > 0) { + contentEl.createEl("h4", { text: "\u5EFA\u8BAE\u64CD\u4F5C" }); + for (const item of this._entry.recommended_actions) { + contentEl.createEl("p", { text: `\u2022 ${item}`, cls: "paperforge-modal-item" }); + } + } + } else { + contentEl.createEl("p", { text: "\u7248\u672C\u5DF2\u66F4\u65B0\u81F3 v" + currentVersion + "\uFF0C\u8BF7\u524D\u5F80\u8BBE\u7F6E \u2192 \u66F4\u65B0\u4E0E\u624B\u518C \u67E5\u770B\u5B8C\u6574\u66F4\u65B0\u8BB0\u5F55\u3002" }); + } + new Setting(contentEl) + .addButton(btn => btn.setButtonText("\u77E5\u9053\u4E86").setCta().onClick(() => { + this.close(); + })); + } + onClose() { + const { contentEl } = this; + contentEl.empty(); + } + } + + new ReleaseNotesModal(this.app, currentEntry).open(); + this.settings.last_seen_version = currentVersion; + this.saveSettings(); + } } diff --git a/paperforge/plugin/src/release-notes.json b/paperforge/plugin/src/release-notes.json new file mode 100644 index 00000000..a6da7980 --- /dev/null +++ b/paperforge/plugin/src/release-notes.json @@ -0,0 +1,31 @@ +{ + "versions": [ + { + "version": "1.5.15", + "date": "2026-06-01", + "title": "OCR 重新设计 — 全文单一真相源 + Redo 闭环 + 阅读顺序修复", + "breaking_or_migration": [ + "fulltext 正文现在统一位于 System/PaperForge/ocr/<key>/fulltext.md,工作区 Resources/.../fulltext.md 不再保留为正文副本", + "OCR Redo 现在会立即重跑 OCR(闭环执行),不再只是标记状态后需要手动再跑一次" + ], + "new_features": [ + "新增 OCR Redo 闭环执行:勾选重做后自动删除旧产物 → 强制 do_ocr → 立即重跑 OCR → 成功后写回 ocr_redo:false", + "设置页新增「更新与手册」标签页,可随时查看版本更新记录与使用手册链接", + "首次更新时自动弹出更新说明" + ], + "fixes": [ + "修复 OCR 双栏阅读顺序混乱:2.5 / Discussion / Introduction 等章节标题和正文错位", + "修复 Kwon et al. 等叙述句被误判为参考文献,导致正文段落被提前截断并插入 Discussion", + "修复 Fig.3 / Fig.4 等页首图注和图块被重排逻辑整体后移,导致图文断裂", + "修复首页(page 1)Abstract / Introduction 排序错乱,METHODS 行跑到 Introduction 后", + "修复并排 panel(如 Fig.2 的左右两块)未合并为单张 composite figure,被拆成两张独立图片", + "修复 redo OCR 后工作区 fulltext 未被同步覆盖,导致用户看到的仍是旧乱序版本", + "Dashboard 现在可以识别 System/PaperForge/ocr/<key>/fulltext.md 路径,打开全文后能切到对应 paper 视图" + ], + "recommended_actions": [ + "如果论文是此版本前完成的 OCR,建议对重要论文执行一次「重做OCR」(在 Base 视图勾选 ocr_redo 后点 ribbon Redo 按钮),以修复阅读顺序问题", + "打开全文请直接使用 Dashboard 的「打开全文」按钮,正文已经统一迁至 System/PaperForge/ocr/ 下" + ] + } + ] +} diff --git a/paperforge/plugin/src/settings.ts b/paperforge/plugin/src/settings.ts index da615522..ca7b4a18 100644 --- a/paperforge/plugin/src/settings.ts +++ b/paperforge/plugin/src/settings.ts @@ -5,6 +5,7 @@ import * as os from "os"; import { execFile, execFileSync, spawn, exec } from "child_process"; import { t, setLanguage } from "./i18n"; import { PaperForgeSettings } from "./constants"; +import releaseNotesData from "./release-notes.json"; import { resolvePythonExecutable, buildRuntimeInstallCommand, paperforgeEnrichedEnv, scanBbtUnderProfiles, scanBbtDirectChildren, @@ -83,6 +84,16 @@ export class PaperForgeSettingTab extends PluginSettingTab { .paperforge-skills-group { margin-bottom: 10px; } .paperforge-skills-group:last-child { margin-bottom: 0; } .vertical-tab-content-container { overflow-y: scroll !important; } + .paperforge-release-card { border: 1px solid var(--background-modifier-border); border-radius: 6px; padding: 12px; margin-bottom: 12px; } + .paperforge-release-header { margin-bottom: 8px; } + .paperforge-release-date { color: var(--text-muted); font-size: 12px; } + .paperforge-release-section { margin-bottom: 6px; } + .paperforge-release-label { font-weight: 600; color: var(--text-accent); margin-bottom: 2px; font-size: 13px; } + .paperforge-release-item { font-size: 13px; margin-left: 8px; color: var(--text-muted); } + .paperforge-manual-links { margin-top: 8px; } + .paperforge-manual-links a { color: var(--text-accent); } + .paperforge-modal-subtitle { color: var(--text-muted); font-size: 13px; margin-bottom: 12px; } + .paperforge-modal-item { font-size: 13px; margin-left: 8px; color: var(--text-muted); } `; document.head.appendChild(style); } @@ -90,8 +101,9 @@ export class PaperForgeSettingTab extends PluginSettingTab { // --- Tab bar --- const tabBar = containerEl.createDiv({ cls: "paperforge-settings-tabs" }); const tabs = [ - { id: "setup", label: t("tab_setup") || "Installation" }, - { id: "features", label: t("tab_features") || "Features" }, + { id: "setup", label: t("tab_setup") || "\u5B89\u88C5" }, + { id: "features", label: t("tab_features") || "\u529F\u80FD" }, + { id: "release-notes", label: "\u66F4\u65B0\u4E0E\u624B\u518C" }, ]; const tabContents: Record<string, HTMLDivElement> = {}; @@ -116,8 +128,10 @@ export class PaperForgeSettingTab extends PluginSettingTab { // --- Render active tab --- if (this.activeTab === "setup") { this._renderSetupTab(tabContents.setup); - } else { + } else if (this.activeTab === "features") { this._renderFeaturesTab(tabContents.features); + } else { + this._renderReleaseNotesTab(tabContents["release-notes"]); } } @@ -1242,4 +1256,59 @@ export class PaperForgeSettingTab extends PluginSettingTab { onPass(); }); } + + _renderReleaseNotesTab(containerEl: HTMLElement) { + containerEl.createEl("h2", { text: "\u66F4\u65B0\u4E0E\u624B\u518C" }); + + containerEl.createEl("h3", { text: "\u7248\u672C\u66F4\u65B0\u8BB0\u5F55" }); + + const versions = (releaseNotesData as any).versions || []; + for (const ver of versions) { + const card = containerEl.createEl("div", { cls: "paperforge-release-card" }); + + const header = card.createEl("div", { cls: "paperforge-release-header" }); + header.createEl("strong", { text: `v${ver.version} \u2014 ${ver.title}` }); + header.createEl("span", { cls: "paperforge-release-date", text: ` (${ver.date})` }); + + if (ver.breaking_or_migration && ver.breaking_or_migration.length > 0) { + const section = card.createEl("div", { cls: "paperforge-release-section" }); + section.createEl("div", { cls: "paperforge-release-label", text: "\u884C\u4E3A\u53D8\u66F4 / \u8FC1\u79FB\u6CE8\u610F" }); + for (const item of ver.breaking_or_migration) { + section.createEl("div", { cls: "paperforge-release-item", text: `\u2022 ${item}` }); + } + } + + if (ver.new_features && ver.new_features.length > 0) { + const section = card.createEl("div", { cls: "paperforge-release-section" }); + section.createEl("div", { cls: "paperforge-release-label", text: "\u65B0\u529F\u80FD" }); + for (const item of ver.new_features) { + section.createEl("div", { cls: "paperforge-release-item", text: `\u2022 ${item}` }); + } + } + + if (ver.fixes && ver.fixes.length > 0) { + const section = card.createEl("div", { cls: "paperforge-release-section" }); + section.createEl("div", { cls: "paperforge-release-label", text: "\u4FEE\u590D" }); + for (const item of ver.fixes) { + section.createEl("div", { cls: "paperforge-release-item", text: `\u2022 ${item}` }); + } + } + + if (ver.recommended_actions && ver.recommended_actions.length > 0) { + const section = card.createEl("div", { cls: "paperforge-release-section" }); + section.createEl("div", { cls: "paperforge-release-label", text: "\u5EFA\u8BAE\u64CD\u4F5C" }); + for (const item of ver.recommended_actions) { + section.createEl("div", { cls: "paperforge-release-item", text: `\u2022 ${item}` }); + } + } + } + + containerEl.createEl("h3", { text: "\u4F7F\u7528\u624B\u518C" }); + const manualSection = containerEl.createEl("div", { cls: "paperforge-manual-links" }); + const manualLink = manualSection.createEl("a", { + text: "\u2192 \u67E5\u770B\u5B8C\u6574\u4F7F\u7528\u624B\u518C\uFF08GitHub\uFF09", + href: "https://github.com/LLLin000/PaperForge/blob/main/docs/user-manual.md", + }); + manualLink.setAttr("target", "_blank"); + } } diff --git a/paperforge/plugin/src/utils/zotero-path.ts b/paperforge/plugin/src/utils/zotero-path.ts new file mode 100644 index 00000000..1954fdf8 --- /dev/null +++ b/paperforge/plugin/src/utils/zotero-path.ts @@ -0,0 +1,16 @@ +import * as path from 'path'; + +export function extractZoteroKeyFromPath(filePath: string): string | null { + if (!filePath) return null; + let dir = path.dirname(filePath); + while (true) { + const basename = path.basename(dir); + if (!basename || basename === '.') break; + const match = basename.match(/^([A-Z0-9]{8})(?:\s*-\s*.*)?$/i); + if (match) return match[1]; + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return null; +} diff --git a/paperforge/plugin/src/views/dashboard.ts b/paperforge/plugin/src/views/dashboard.ts index 1fd2aa55..8ed6c8fc 100644 --- a/paperforge/plugin/src/views/dashboard.ts +++ b/paperforge/plugin/src/views/dashboard.ts @@ -17,6 +17,7 @@ import { checkRuntimeVersion, paperforgeEnrichedEnv } from "../services/python-bridge"; import { getDisclosureState, toggleDisclosureState } from "../utils/disclosure"; +import { extractZoteroKeyFromPath } from "../utils/zotero-path"; import { checkOrphanState } from "./modals"; // ── Interface for plugin ref used by static open ── @@ -557,18 +558,7 @@ export class PaperForgeStatusView extends ItemView { /* ── Extract zotero_key from workspace directory name ── */ _extractZoteroKeyFromPath(filePath: string): string | null { - if (!filePath) return null; - let dir = path.dirname(filePath); - while (true) { - const basename = path.basename(dir); - if (!basename || basename === '.') break; - const match = basename.match(/^([A-Z0-9]{8})(?:\s*-\s*)/i); - if (match) return match[1]; - const parent = path.dirname(dir); - if (parent === dir) break; - dir = parent; - } - return null; + return extractZoteroKeyFromPath(filePath); } /* ── Pure Mode Resolution (D-07, Phase 32) ── */ @@ -800,7 +790,7 @@ export class PaperForgeStatusView extends ItemView { }); const globalRedoBtn = btnsRow.createEl('button', { cls: 'paperforge-contextual-btn warn' }); globalRedoBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u21BA' }); - globalRedoBtn.createEl('span', { text: '\u91CD\u505AOCR' }); + globalRedoBtn.createEl('span', { text: 'Redo OCR' }); globalRedoBtn.addEventListener('click', () => { const action = ACTIONS.find(a => a.id === 'paperforge-ocr-redo'); if (action) this._runAction(action, globalRedoBtn); @@ -1263,7 +1253,7 @@ export class PaperForgeStatusView extends ItemView { }); const redoBtn = actionsRow.createEl('button', { cls: 'paperforge-contextual-btn warn' }); redoBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u21BA' }); - redoBtn.createEl('span', { text: '\u91CD\u505AOCR' }); + redoBtn.createEl('span', { text: 'Redo OCR' }); redoBtn.addEventListener('click', () => { const action = ACTIONS.find(a => a.id === 'paperforge-ocr-redo'); if (action) this._runAction(action, redoBtn); @@ -1317,6 +1307,11 @@ export class PaperForgeStatusView extends ItemView { const cache = this.app.metadataCache.getFileCache(activeFile); if (cache && cache.frontmatter && cache.frontmatter.zotero_key) { key = cache.frontmatter.zotero_key; + } else { + key = this._extractZoteroKeyFromPath(activeFile.path); + } + if (key) { + extraArgs = [...extraArgs, key]; } else if (cache && cache.frontmatter) { this._showMessage('[!!] No zotero_key in active note frontmatter', 'error'); new Notice('[!!] Open a paper note with a zotero_key in its frontmatter first', 6000); @@ -1334,7 +1329,6 @@ export class PaperForgeStatusView extends ItemView { card.removeClass('running'); return; } - extraArgs = [...extraArgs, key]; } if (a.needsFilter) { extraArgs = [...extraArgs, '--all']; diff --git a/paperforge/plugin/tests/zotero-path.test.ts b/paperforge/plugin/tests/zotero-path.test.ts new file mode 100644 index 00000000..90ea513b --- /dev/null +++ b/paperforge/plugin/tests/zotero-path.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest'; + +import { extractZoteroKeyFromPath } from '../src/utils/zotero-path'; + +describe('extractZoteroKeyFromPath', () => { + it('extracts zotero key from workspace directory path', () => { + expect(extractZoteroKeyFromPath('Resources/Literature/test/2AZ2EGQB - Paper/2AZ2EGQB.md')).toBe('2AZ2EGQB'); + }); + + it('extracts zotero key from canonical OCR fulltext path', () => { + expect(extractZoteroKeyFromPath('System/PaperForge/ocr/2AZ2EGQB/fulltext.md')).toBe('2AZ2EGQB'); + }); +}); diff --git a/paperforge/worker/_utils.py b/paperforge/worker/_utils.py index 5af6159d..028068aa 100644 --- a/paperforge/worker/_utils.py +++ b/paperforge/worker/_utils.py @@ -92,6 +92,20 @@ def write_jsonl(path: Path, rows) -> None: path.write_text(text, encoding="utf-8") +def sync_workspace_fulltext(source_fulltext: Path, target_fulltext: Path) -> bool: + if not source_fulltext.exists(): + return False + if target_fulltext.exists(): + try: + if source_fulltext.read_text(encoding="utf-8") == target_fulltext.read_text(encoding="utf-8"): + return False + except Exception: + pass + target_fulltext.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(str(source_fulltext), str(target_fulltext)) + return True + + # --- YAML Helpers --- diff --git a/paperforge/worker/asset_index.py b/paperforge/worker/asset_index.py index c0ed172c..326efa3e 100644 --- a/paperforge/worker/asset_index.py +++ b/paperforge/worker/asset_index.py @@ -341,16 +341,15 @@ def _build_entry(item: dict, vault: Path, paths: dict, domain: str, zotero_dir: except Exception: pass # alias will be injected on next full frontmatter_note pass break # only one old file per key - target_fulltext = workspace_dir / "fulltext.md" source_fulltext = paths["ocr"] / key / "fulltext.md" + stale_workspace_fulltext = workspace_dir / "fulltext.md" workspace_dir.mkdir(parents=True, exist_ok=True) (workspace_dir / "ai").mkdir(parents=True, exist_ok=True) - if meta.get("ocr_status") == "done" and source_fulltext.exists() and not target_fulltext.exists(): - shutil.copy2(str(source_fulltext), str(target_fulltext)) - logger.info("Bridged fulltext.md to workspace for %s", key) + if stale_workspace_fulltext.exists(): + stale_workspace_fulltext.unlink() - fulltext_exists = target_fulltext.exists() + fulltext_exists = source_fulltext.exists() # ---- entry dict ------------------------------------------------------- authors = item.get("authors", []) @@ -447,7 +446,7 @@ def _build_entry(item: dict, vault: Path, paths: dict, domain: str, zotero_dir: # Workspace path fields are only advertised when the backing files/dirs exist. "paper_root": str(workspace_dir.relative_to(vault)).replace("\\", "/") + "/", "main_note_path": str(main_note_path.relative_to(vault)).replace("\\", "/"), - "fulltext_path": str(target_fulltext.relative_to(vault)).replace("\\", "/") if fulltext_exists else "", + "fulltext_path": str(source_fulltext.relative_to(vault)).replace("\\", "/") if fulltext_exists else "", "deep_reading_path": "", # deprecated: deep reading content now lives in main note as ## 🔍 精读 "ai_path": str((workspace_dir / "ai").relative_to(vault)).replace("\\", "/") + "/", } diff --git a/paperforge/worker/ocr.py b/paperforge/worker/ocr.py index 33d60bb1..0cfae078 100644 --- a/paperforge/worker/ocr.py +++ b/paperforge/worker/ocr.py @@ -385,15 +385,31 @@ def _col_xc(block: dict) -> float | None: return (bbox[0] + bbox[2]) / 2 -def _col_switches(blocks: list[dict], page_width: int) -> int: +def _column_kind(block: dict, page_width: int) -> str | None: + bbox = block.get("block_bbox", [0, 0, 0, 0]) + if len(bbox) < 4 or bbox[2] <= bbox[0]: + return None mid = page_width / 2 if page_width else 600 + x1, _, x2, _ = bbox + width = x2 - x1 + # Full-width or center-spanning blocks should keep their original slot. + if (x1 < mid and x2 > mid) or (page_width and width >= page_width * 0.55): + return "S" + xc = (x1 + x2) / 2 + if xc < mid * 0.95: + return "L" + if xc > mid * 1.05: + return "R" + return "S" + + +def _col_switches(blocks: list[dict], page_width: int) -> int: switches = 0 prev_col = None for b in blocks: - xc = _col_xc(b) - if xc is None: + col = _column_kind(b, page_width) + if col is None: continue - col = "L" if xc < mid * 0.95 else "R" if xc > mid * 1.05 else "S" if prev_col and col != "S" and prev_col != "S" and col != prev_col: switches += 1 if col != "S": @@ -404,25 +420,34 @@ def _col_switches(blocks: list[dict], page_width: int) -> int: def validate_block_order(blocks: list[dict], page_width: int) -> list[dict]: if len(blocks) < 4 or not page_width: return blocks - mid = page_width / 2 if _col_switches(blocks, page_width) > 3: - left = [b for b in blocks if _col_xc(b) is not None and (_col_xc(b) or 0) < mid] - right = [b for b in blocks if _col_xc(b) is not None and (_col_xc(b) or 0) >= mid] - other = [b for b in blocks if _col_xc(b) is None] + left = [b for b in blocks if _column_kind(b, page_width) == "L"] + right = [b for b in blocks if _column_kind(b, page_width) == "R"] + centered = [b for b in blocks if _column_kind(b, page_width) not in {"L", "R"}] left.sort(key=lambda b: b.get("block_bbox", [0, 0, 0, 0])[1]) right.sort(key=lambda b: b.get("block_bbox", [0, 0, 0, 0])[1]) - return left + right + other + if not centered: + return left + right + result: list[dict] = [] + iters = {"L": iter(left), "R": iter(right)} + for b in blocks: + col_key = _column_kind(b, page_width) + if col_key not in {"L", "R"}: + result.append(b) + continue + try: + result.append(next(iters[col_key])) + except StopIteration: + result.append(b) + return result col_buckets: dict[str, list[dict]] = {"L": [], "R": []} for b in blocks: - xc = _col_xc(b) - if xc is None: + col_key = _column_kind(b, page_width) + if col_key not in {"L", "R"}: continue - if xc < mid: - col_buckets["L"].append(b) - else: - col_buckets["R"].append(b) + col_buckets[col_key].append(b) for key in ("L", "R"): sorted_col = sorted(col_buckets[key], key=lambda b: b.get("block_bbox", [0, 0, 0, 0])[1]) @@ -432,11 +457,10 @@ def validate_block_order(blocks: list[dict], page_width: int) -> list[dict]: result: list[dict] = [] iters = {"L": iter(col_buckets["L"]), "R": iter(col_buckets["R"])} for b in blocks: - xc = _col_xc(b) - if xc is None: + col_key = _column_kind(b, page_width) + if col_key not in {"L", "R"}: result.append(b) continue - col_key = "L" if xc < mid else "R" try: result.append(next(iters[col_key])) except StopIteration: @@ -794,7 +818,17 @@ def media_clusters(blocks: list[dict]) -> tuple[dict[int, int], list[list[dict]] cy1 = min(item["block_bbox"][1] for item in cluster) - 40 cx2 = max(item["block_bbox"][2] for item in cluster) + 40 cy2 = max(item["block_bbox"][3] for item in cluster) + 40 - if not (x2 < cx1 or x1 > cx2 or y2 < cy1 or (y1 > cy2)): + vertical_overlap = max(0, min(y2, cy2) - max(y1, cy1)) + min_height = max(1, min(y2 - y1, cy2 - cy1)) + if x2 < cx1: + horizontal_gap = cx1 - x2 + elif x1 > cx2: + horizontal_gap = x1 - cx2 + else: + horizontal_gap = 0 + overlaps_cluster = not (x2 < cx1 or x1 > cx2 or y2 < cy1 or y1 > cy2) + side_by_side_panel = vertical_overlap >= int(min_height * 0.35) and horizontal_gap <= 60 + if overlaps_cluster or side_by_side_panel: assigned = idx break if assigned is None: @@ -1373,9 +1407,10 @@ def render_page_blocks( pruned = result.get("prunedResult", {}) ocr_width = int(pruned.get("width", 0) or 0) blocks = sorted(pruned.get("parsing_res_list", []), key=block_sort_key) - blocks = validate_block_order(blocks, ocr_width) ocr_height = int(pruned.get("height", 0) or 0) - blocks = _apply_layered_body_reorder(blocks, ocr_width, ocr_height) + if page_index != 1: + blocks = validate_block_order(blocks, ocr_width) + blocks = _apply_layered_body_reorder(blocks, ocr_width, ocr_height) raw_reference_blocks = [block for block in blocks if block.get("block_label") == "reference_content"] first_reference_y = min( (block.get("block_bbox", [0, 10**9, 0, 0])[1] for block in raw_reference_blocks), default=10**9 @@ -1467,11 +1502,11 @@ def render_page_blocks( if page_index == 1 and affiliation_buffer: rendered.extend(affiliation_buffer) affiliation_buffer.clear() - rendered.append(clean_block_text(content)) if page_index == 1 and deferred_meta: rendered.extend(deferred_meta) deferred_meta.clear() first_page_meta_done = True + rendered.append(clean_block_text(content)) continue if label == "reference_content": text = clean_block_text(content) @@ -1667,14 +1702,32 @@ def postprocess_ocr_result(vault: Path, key: str, all_results: list[dict]) -> tu return (page_num, markdown_path, json_path, fulltext_md_path) -def ocr_redo_papers(vault: Path, dry_run: bool = False, verbose: bool = False) -> int: - """Scan for papers with ocr_redo: true, reset their OCR state. +def _workspace_fulltext_candidates(lit_root: Path, zotero_key: str) -> list[Path]: + if not lit_root.exists(): + return [] + matches: list[Path] = [] + for candidate in lit_root.rglob(f"{zotero_key} - *"): + if candidate.is_dir(): + fulltext_path = candidate / "fulltext.md" + if fulltext_path.exists(): + matches.append(fulltext_path) + return matches + + +def _rewrite_note_fields(text: str, **fields: object) -> str: + updated = text + for field, value in fields.items(): + updated = _sync.update_frontmatter_field(updated, field, value) + return updated + + +def ocr_redo_papers(vault: Path, dry_run: bool = False, verbose: bool = False, no_progress: bool = False) -> int: + """Scan for papers with ocr_redo: true, reset and immediately rerun OCR. Spec 2.6: paperforge ocr redo [--dry-run] """ from paperforge.adapters.obsidian_frontmatter import ( extract_preserved_ocr_redo, - update_frontmatter_field, ) paths = pipeline_paths(vault) @@ -1686,7 +1739,7 @@ def ocr_redo_papers(vault: Path, dry_run: bool = False, verbose: bool = False) - logger.info("No literature directory found, nothing to redo") return 0 - redo_entries = [] + redo_entry_map: dict[str, tuple[Path, str]] = {} for note_file in sorted(lit_root.rglob("*.md")): if note_file.name in ("fulltext.md", "deep-reading.md", "discussion.md"): continue @@ -1704,7 +1757,11 @@ def ocr_redo_papers(vault: Path, dry_run: bool = False, verbose: bool = False) - if verbose: logger.warning("Skipping invalid zotero_key: %s", zotero_key) continue - redo_entries.append((zotero_key, note_file, text)) + existing = redo_entry_map.get(zotero_key) + if existing is None or note_file.name == f"{zotero_key}.md": + redo_entry_map[zotero_key] = (note_file, text) + + redo_entries = [(key, note_file, text) for key, (note_file, text) in sorted(redo_entry_map.items())] if not redo_entries: print("No papers with ocr_redo: true found", flush=True) @@ -1715,29 +1772,61 @@ def ocr_redo_papers(vault: Path, dry_run: bool = False, verbose: bool = False) - print(f" {zotero_key} ({note_file.parent.name})", flush=True) if dry_run: - print("\nDry-run mode: no changes made. Run without --dry-run to reset.", flush=True) + print("\nDry-run mode: no changes made. Run without --dry-run to rerun OCR.", flush=True) return 0 reset_keys = [] for zotero_key, note_file, text in redo_entries: + for workspace_fulltext in _workspace_fulltext_candidates(lit_root, zotero_key): + workspace_fulltext.unlink(missing_ok=True) + if verbose: + logger.info("Deleted workspace fulltext for %s", zotero_key) ocr_dir = ocr_root / zotero_key if ocr_root else None if ocr_dir and ocr_dir.exists(): shutil.rmtree(ocr_dir) if verbose: logger.info("Deleted OCR directory for %s", zotero_key) - text = update_frontmatter_field(text, "ocr_status", "pending") - text = update_frontmatter_field(text, "ocr_redo", False) - text = update_frontmatter_field(text, "fulltext_md_path", "") + text = _rewrite_note_fields( + text, + do_ocr=True, + ocr_status="pending", + fulltext_md_path="", + ocr_redo=True, + ) note_file.write_text(text, encoding="utf-8") reset_keys.append(zotero_key) print(f"Reset {len(reset_keys)} paper(s): {', '.join(reset_keys)}", flush=True) - print("Run 'paperforge ocr run' to process the reset papers.", flush=True) - return 0 + print("Launching OCR immediately for reset papers...", flush=True) + + exit_code = run_ocr(vault, verbose=verbose, no_progress=no_progress, selected_keys=set(reset_keys)) + + success_keys: list[str] = [] + failed_keys: list[str] = [] + for zotero_key, note_file, _original_text in redo_entries: + current_text = note_file.read_text(encoding="utf-8") + meta_path = ocr_root / zotero_key / "meta.json" if ocr_root else None + meta = read_json(meta_path) if meta_path and meta_path.exists() else {} + status, _error = validate_ocr_meta(paths, meta) if meta else ("pending", "") + current_text = _rewrite_note_fields(current_text, ocr_status=status) + if status == "done": + current_text = _rewrite_note_fields(current_text, ocr_redo=False) + success_keys.append(zotero_key) + else: + current_text = _rewrite_note_fields(current_text, ocr_redo=True) + failed_keys.append(zotero_key) + note_file.write_text(current_text, encoding="utf-8") + refresh_index_entry(vault, zotero_key) + + if success_keys: + print(f"Redo OCR done={len(success_keys)}: {', '.join(success_keys)}", flush=True) + if failed_keys: + print(f"Redo OCR pending/failed={len(failed_keys)}: {', '.join(failed_keys)}", flush=True) + return exit_code -def run_ocr(vault: Path, verbose: bool = False, no_progress: bool = False) -> int: +def run_ocr(vault: Path, verbose: bool = False, no_progress: bool = False, selected_keys: set[str] | None = None) -> int: from paperforge.pdf_resolver import resolve_pdf_path paths = pipeline_paths(vault) @@ -1774,6 +1863,8 @@ def run_ocr(vault: Path, verbose: bool = False, no_progress: bool = False) -> in control_actions = load_control_actions(paths) target_keys = {key for key, action in control_actions.items() if action.get("do_ocr", False)} + if selected_keys is not None: + target_keys &= set(selected_keys) target_rows = [] for export_path in sorted(paths["exports"].glob("*.json")): diff --git a/paperforge/worker/ocr_orchestrator.py b/paperforge/worker/ocr_orchestrator.py index 2c882d53..b5e867ef 100644 --- a/paperforge/worker/ocr_orchestrator.py +++ b/paperforge/worker/ocr_orchestrator.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass @dataclass @@ -16,6 +16,14 @@ def _bbox_order_key(block: dict) -> tuple: return (int(bbox[1]), int(bbox[0])) +def _bbox_top(block: dict) -> int: + return int(block.get("block_bbox", [0, 0, 0, 0])[1]) + + +def _bbox_bottom(block: dict) -> int: + return int(block.get("block_bbox", [0, 0, 0, 0])[3]) + + def reorder_blocks_layered( blocks: list[dict], page_width: int = 0, @@ -25,9 +33,9 @@ def reorder_blocks_layered( return blocks try: - from paperforge.worker.ocr_roles import assign_block_role from paperforge.worker.ocr_body_spine import BodySpineNode from paperforge.worker.ocr_layout import detect_layout_zones, order_body_spine + from paperforge.worker.ocr_roles import assign_block_role except ImportError: return blocks @@ -76,5 +84,27 @@ def reorder_blocks_layered( if idx is not None and idx < len(body_annotated): body_result.append(body_annotated[idx].block) - non_body_result: list[dict] = [a.block for a in non_body] - return body_result + non_body_result + body_iter = iter(body_result) + result: list[dict] = [] + for annotated_block in annotated: + if annotated_block in body_annotated: + try: + result.append(next(body_iter)) + except StopIteration: + result.append(annotated_block.block) + else: + result.append(annotated_block.block) + result.extend(body_iter) + + first_body_y = min((_bbox_top(block) for block in body_result), default=None) + if first_body_y is not None: + leading_roles = {"figure_caption", "table_caption", "media_asset"} + leading_blocks = [ + a.block + for a in annotated + if a.role in leading_roles and _bbox_bottom(a.block) <= first_body_y + ] + if leading_blocks: + leading_ids = {id(block) for block in leading_blocks} + result = leading_blocks + [block for block in result if id(block) not in leading_ids] + return result diff --git a/paperforge/worker/ocr_roles.py b/paperforge/worker/ocr_roles.py index fa2ce589..6e687cbf 100644 --- a/paperforge/worker/ocr_roles.py +++ b/paperforge/worker/ocr_roles.py @@ -27,7 +27,7 @@ _TABLE_PREFIX_PATTERN = re.compile( ) _REFERENCE_PATTERN = re.compile( - r"^\s*(?:\d+\.\s|[A-Z][a-z]+ et al\.|\([A-Z][a-z]+ et al\., \d{4}\))", + r"^\s*(?:\d+\.\s|[A-Z][A-Za-z'’\-]+\s+et al\.\s*\(\d{4}[a-z]?\)|\([A-Z][A-Za-z'’\-]+\s+et al\.,\s*\d{4}[a-z]?\))", ) @@ -73,6 +73,12 @@ def assign_block_role( # Paddle priors if raw_label == "paragraph_title": + if text.strip().lower() == "abstract": + return RoleAssignment( + role="frontmatter_heading", + confidence=0.95, + evidence=["abstract heading stays out of body reorder"], + ) if _has_heading_numbering(text): return RoleAssignment( role="section_heading" if re.match(r"^\d+\s", text) else "subsection_heading", diff --git a/paperforge/worker/sync.py b/paperforge/worker/sync.py index 4ef4e0d9..b1d1fa00 100644 --- a/paperforge/worker/sync.py +++ b/paperforge/worker/sync.py @@ -1158,6 +1158,9 @@ def migrate_to_workspace(vault: Path, paths: dict) -> int: legacy_flags = _legacy_control_flags(paths, key) if workspace_dir.exists(): + stale_workspace_fulltext = workspace_dir / "fulltext.md" + if stale_workspace_fulltext.exists(): + stale_workspace_fulltext.unlink() if main_note_path.exists() and any(value is not None for value in legacy_flags.values()): try: workspace_content = main_note_path.read_text(encoding="utf-8") @@ -1198,21 +1201,6 @@ def migrate_to_workspace(vault: Path, paths: dict) -> int: ai_dir = workspace_dir / "ai" ai_dir.mkdir(exist_ok=True) - # WS-04: Bridge OCR fulltext to workspace if available - meta_path = paths.get("ocr", Path()) / key / "meta.json" - if meta_path.exists(): - try: - meta = read_json(meta_path) - if meta.get("ocr_status") == "done": - source_fulltext = meta_path.parent / "fulltext.md" - target_fulltext = workspace_dir / "fulltext.md" - if source_fulltext.exists() and not target_fulltext.exists(): - import shutil - - shutil.copy2(str(source_fulltext), str(target_fulltext)) - except Exception: - pass - migrated += 1 if migrated > 0: diff --git a/tests/test_ocr.py b/tests/test_ocr.py index 69afac89..dc04ff36 100644 --- a/tests/test_ocr.py +++ b/tests/test_ocr.py @@ -53,27 +53,99 @@ ocr_redo: true def test_ocr_redo_papers_updates_frontmatter(tmp_path): - from paperforge.worker.ocr import ocr_redo_papers + from paperforge.worker import ocr as ocr_worker vault = tmp_path / "vault" vault.mkdir() literature = vault / "Resources" / "Literature" / "test_domain" literature.mkdir(parents=True) - note = literature / "good note.md" + workspace = literature / "AB12CD34 - Test" + workspace.mkdir() + note = workspace / "AB12CD34.md" note.write_text("""--- zotero_key: "AB12CD34" title: "Test" ocr_redo: true +do_ocr: false ocr_status: done fulltext_md_path: "[[old_path]]" --- """, encoding="utf-8") - rc = ocr_redo_papers(vault, dry_run=False, verbose=True) - assert rc == 0 + (workspace / "fulltext.md").write_text("old workspace copy\n", encoding="utf-8") + + ocr_root = vault / "System" / "PaperForge" / "ocr" + ocr_root.mkdir(parents=True) + ocr_dir = ocr_root / "AB12CD34" + ocr_dir.mkdir() + (ocr_dir / "fulltext.md").write_text("old canonical content\n", encoding="utf-8") + + calls = {} + + def fake_run_ocr(vault_arg, verbose=False, no_progress=False, selected_keys=None): + calls["selected_keys"] = selected_keys + new_dir = ocr_root / "AB12CD34" + (new_dir / "json").mkdir(parents=True, exist_ok=True) + fulltext = "<!-- page 1 -->\n" + ("A" * 700) + (new_dir / "fulltext.md").write_text(fulltext, encoding="utf-8") + (new_dir / "json" / "result.json").write_text("{" + ('"x":1,' * 600) + '"done":true}', encoding="utf-8") + (new_dir / "meta.json").write_text( + '{"zotero_key":"AB12CD34","ocr_status":"done","page_count":1,"fulltext_md_path":"",' + '"markdown_path":"System/PaperForge/ocr/AB12CD34/fulltext.md","json_path":"System/PaperForge/ocr/AB12CD34/json/result.json"}', + encoding="utf-8", + ) + return 0 + + monkeypatch = __import__("pytest").MonkeyPatch() + monkeypatch.setattr(ocr_worker, "run_ocr", fake_run_ocr) + monkeypatch.setattr(ocr_worker, "refresh_index_entry", lambda _vault, _key: None) + + try: + rc = ocr_worker.ocr_redo_papers(vault, dry_run=False, verbose=True) + assert rc == 0 + finally: + monkeypatch.undo() content = note.read_text(encoding="utf-8") assert 'ocr_redo: false' in content - assert 'ocr_status: "pending"' in content + assert 'ocr_status: "done"' in content assert 'fulltext_md_path: ""' in content + assert 'do_ocr: true' in content + assert calls["selected_keys"] == {"AB12CD34"} + assert not (workspace / "fulltext.md").exists() + + +def test_ocr_redo_papers_keeps_flag_true_when_rerun_not_done(tmp_path): + from paperforge.worker import ocr as ocr_worker + + vault = tmp_path / "vault" + vault.mkdir() + literature = vault / "Resources" / "Literature" / "test_domain" + literature.mkdir(parents=True) + + workspace = literature / "ZXCV1234 - Test" + workspace.mkdir() + note = workspace / "ZXCV1234.md" + note.write_text("""--- +zotero_key: "ZXCV1234" +title: "Test" +ocr_redo: true +do_ocr: false +ocr_status: done +--- +""", encoding="utf-8") + + monkeypatch = __import__("pytest").MonkeyPatch() + monkeypatch.setattr(ocr_worker, "run_ocr", lambda *_args, **_kwargs: 1) + monkeypatch.setattr(ocr_worker, "refresh_index_entry", lambda _vault, _key: None) + + try: + rc = ocr_worker.ocr_redo_papers(vault, dry_run=False, verbose=True) + assert rc == 1 + finally: + monkeypatch.undo() + + content = note.read_text(encoding="utf-8") + assert 'ocr_redo: true' in content + assert 'ocr_status: "pending"' in content diff --git a/tests/test_ocr_rendering.py b/tests/test_ocr_rendering.py index f2d132b3..92eaa2bf 100644 --- a/tests/test_ocr_rendering.py +++ b/tests/test_ocr_rendering.py @@ -404,6 +404,225 @@ def test_render_page_blocks_orders_references_within_column(tmp_path: Path) -> N assert ref_items == ["Amin, B. (2019).", "Cai, J. (2018).", "Bagnato, G. L. (2016).", "Bentley, G. (2012)."] +def test_render_page_blocks_keeps_top_figure_before_body_and_keeps_author_et_al_sentence_in_body(tmp_path: Path) -> None: + from paperforge.worker.ocr import render_page_blocks + + vault = tmp_path / "vault" + images_dir = vault / "System" / "PaperForge" / "ocr" / "KEY" / "images" + page_cache_dir = vault / "System" / "PaperForge" / "ocr" / "KEY" / "pages" + images_dir.mkdir(parents=True) + page_cache_dir.mkdir(parents=True) + + page_image = page_cache_dir / "page_008.png" + Image.new("RGB", (1200, 1600), color="white").save(page_image) + + result = { + "prunedResult": { + "width": 1200, + "height": 1600, + "parsing_res_list": [ + { + "block_id": 1, + "block_label": "figure_title", + "block_order": 0, + "block_bbox": [97, 114, 306, 197], + "block_content": "Fig. 3 Schematic representation of mechanical stimulation for tissue engineering constructs", + }, + { + "block_id": 2, + "block_label": "image", + "block_order": 1, + "block_bbox": [374, 117, 1089, 436], + "block_content": "", + }, + { + "block_id": 3, + "block_label": "paragraph_title", + "block_order": 2, + "block_bbox": [96, 470, 513, 520], + "block_content": "2.5 Electrical stimulation for tissue-engineered articular cartilage", + }, + { + "block_id": 4, + "block_label": "text", + "block_order": 3, + "block_bbox": [95, 543, 583, 918], + "block_content": "As hyaline cartilage is an avascular tissue, the synovial fluid is responsible for chondrocyte nutrition and maintenance.", + }, + { + "block_id": 5, + "block_label": "text", + "block_order": 4, + "block_bbox": [95, 920, 583, 1043], + "block_content": "Regenerative pathways for cartilage regeneration on iPSCs engineered constructs can be stimulated by endogenous electrical stimulation.", + }, + { + "block_id": 6, + "block_label": "text", + "block_order": 5, + "block_bbox": [95, 1045, 583, 1265], + "block_content": "After incorporating ES on stem cells, this stimulus generates ATP oscillations driven by calcium oscillations.", + }, + { + "block_id": 7, + "block_label": "paragraph_title", + "block_order": 6, + "block_bbox": [607, 893, 736, 917], + "block_content": "3 Discussion", + }, + { + "block_id": 8, + "block_label": "text", + "block_order": 7, + "block_bbox": [604, 943, 1094, 1341], + "block_content": "Bioreactors can be one of the most efficient and reliable methods for testing in vitro articular cartilage-engineered constructs.", + }, + { + "block_id": 9, + "block_label": "text", + "block_order": 8, + "block_bbox": [95, 1268, 583, 1418], + "block_content": "Kwon et al. demonstrated that ES also drives ATP oscillations by cAMP modulation, leading to chondrogenic differentiation in the absence of exogenous growth factors.", + }, + ], + }, + "inputImage": "", + } + + with patch("paperforge.worker.ocr.render_pdf_page_cached", return_value=page_image): + rendered = render_page_blocks(vault, 8, result, images_dir, page_cache_dir, pdf_doc=None) + + body_lines = [line for line in rendered if line and not line.startswith("<!-- page")] + + assert body_lines[0].startswith("Fig. 3 Schematic representation") + assert body_lines[1].startswith("![[") + assert body_lines[2] == "### 2.5 Electrical stimulation for tissue-engineered articular cartilage" + assert body_lines[6].startswith("Kwon et al. demonstrated") + assert body_lines[7] == "### 3 Discussion" + + +def test_validate_block_order_preserves_full_width_blocks_between_columns() -> None: + from paperforge.worker.ocr import validate_block_order + + blocks = [ + { + "block_id": 1, + "block_label": "abstract", + "block_order": 6, + "block_bbox": [96, 603, 1094, 800], + "block_content": "BACKGROUND", + }, + { + "block_id": 2, + "block_label": "abstract", + "block_order": 7, + "block_bbox": [97, 802, 1094, 851], + "block_content": "METHODS", + }, + { + "block_id": 3, + "block_label": "abstract", + "block_order": 8, + "block_bbox": [96, 899, 1093, 977], + "block_content": "CONCLUSION", + }, + { + "block_id": 4, + "block_label": "text", + "block_order": 9, + "block_bbox": [97, 1000, 872, 1025], + "block_content": "Keywords", + }, + { + "block_id": 5, + "block_label": "paragraph_title", + "block_order": 10, + "block_bbox": [98, 1062, 246, 1086], + "block_content": "1 Introduction", + }, + { + "block_id": 6, + "block_label": "text", + "block_order": 11, + "block_bbox": [96, 1113, 582, 1190], + "block_content": "Introduction body", + }, + { + "block_id": 7, + "block_label": "text", + "block_order": 12, + "block_bbox": [604, 1063, 1094, 1414], + "block_content": "Right column body", + }, + ] + + ordered = validate_block_order(blocks, page_width=1191) + + assert [block["block_content"] for block in ordered[:5]] == [ + "BACKGROUND", + "METHODS", + "CONCLUSION", + "Keywords", + "1 Introduction", + ] + + +def test_render_page_blocks_keeps_abstract_heading_before_abstract_body_on_first_page(tmp_path: Path) -> None: + from paperforge.worker.ocr import render_page_blocks + + vault = tmp_path / "vault" + images_dir = vault / "System" / "PaperForge" / "ocr" / "KEY" / "images" + page_cache_dir = vault / "System" / "PaperForge" / "ocr" / "KEY" / "pages" + images_dir.mkdir(parents=True) + page_cache_dir.mkdir(parents=True) + + result = { + "prunedResult": { + "width": 1200, + "height": 1600, + "parsing_res_list": [ + {"block_id": 1, "block_label": "doc_title", "block_order": 0, "block_bbox": [96, 210, 949, 284], "block_content": "Paper Title"}, + {"block_id": 2, "block_label": "text", "block_order": 1, "block_bbox": [95, 312, 761, 365], "block_content": "Author One · Author Two"}, + {"block_id": 3, "block_label": "text", "block_order": 2, "block_bbox": [96, 485, 880, 528], "block_content": "Received: 10 February 2023"}, + {"block_id": 4, "block_label": "paragraph_title", "block_order": 3, "block_bbox": [97, 577, 181, 599], "block_content": "Abstract"}, + {"block_id": 5, "block_label": "abstract", "block_order": 4, "block_bbox": [96, 603, 1094, 800], "block_content": "BACKGROUND: Background text."}, + {"block_id": 6, "block_label": "abstract", "block_order": 5, "block_bbox": [97, 802, 1094, 851], "block_content": "METHODS: Methods text."}, + {"block_id": 7, "block_label": "abstract", "block_order": 6, "block_bbox": [96, 899, 1093, 977], "block_content": "CONCLUSION: Conclusion text."}, + {"block_id": 8, "block_label": "text", "block_order": 7, "block_bbox": [97, 1000, 872, 1025], "block_content": "Keywords one two three"}, + {"block_id": 9, "block_label": "paragraph_title", "block_order": 8, "block_bbox": [98, 1062, 246, 1086], "block_content": "1 Introduction"}, + {"block_id": 10, "block_label": "text", "block_order": 9, "block_bbox": [96, 1113, 582, 1190], "block_content": "Introduction body."}, + ], + }, + "inputImage": "", + } + + rendered = render_page_blocks(vault, 1, result, images_dir, page_cache_dir, pdf_doc=None) + body_lines = [line for line in rendered if line and not line.startswith("<!-- page")] + + abstract_idx = body_lines.index("### Abstract") + background_idx = body_lines.index("BACKGROUND: Background text.") + methods_idx = body_lines.index("METHODS: Methods text.") + conclusion_idx = body_lines.index("CONCLUSION: Conclusion text.") + keywords_idx = body_lines.index("Keywords one two three") + intro_idx = body_lines.index("### 1 Introduction") + + assert abstract_idx < background_idx < methods_idx < conclusion_idx < keywords_idx < intro_idx + + +def test_media_clusters_merge_side_by_side_panels_with_small_gap() -> None: + from paperforge.worker.ocr import media_clusters + + blocks = [ + {"block_id": 1, "block_label": "image", "block_bbox": [366, 165, 631, 766], "block_content": ""}, + {"block_id": 2, "block_label": "image", "block_bbox": [675, 118, 1091, 738], "block_content": ""}, + ] + + block_to_cluster, clusters = media_clusters(blocks) + + assert len(clusters) == 1 + assert block_to_cluster[1] == block_to_cluster[2] + + def test_embedded_figure_text_excludes_body_paragraph_like_text() -> None: from paperforge.worker.ocr import is_embedded_figure_text_block diff --git a/tests/test_sync.py b/tests/test_sync.py index f9d47187..01a2ee41 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -57,3 +57,41 @@ def test_frontmatter_note_preserves_ocr_redo_true(): result = frontmatter_note(entry, preserved_ocr_redo=True) assert "ocr_redo: true" in result assert "ocr_redo: false" not in result + + +def test_migrate_to_workspace_deletes_existing_workspace_fulltext(tmp_path, monkeypatch): + from paperforge.worker import sync + + vault = tmp_path / "vault" + literature = vault / "Resources" / "Literature" + ocr = vault / "PaperForge" / "ocr" + domain_dir = literature / "test_domain" + domain_dir.mkdir(parents=True) + + key = "ABCD1234" + title = "Test Paper" + flat_note = domain_dir / f"{key} - {title}.md" + flat_note.write_text( + "---\n" + f'title: "{title}"\n' + f"zotero_key: {key}\n" + "---\n\n" + "# Test Paper\n", + encoding="utf-8", + ) + + workspace_dir = domain_dir / f"{key} - {title}" + workspace_dir.mkdir() + (workspace_dir / f"{key}.md").write_text("# Existing note\n", encoding="utf-8") + (workspace_dir / "fulltext.md").write_text("old workspace fulltext\n", encoding="utf-8") + + monkeypatch.setattr(sync.asset_index, "read_index", lambda _vault: {"items": []}) + + paths = { + "literature": literature, + "ocr": ocr, + } + + sync.migrate_to_workspace(vault, paths) + + assert not (workspace_dir / "fulltext.md").exists()