diff --git a/docs/superpowers/specs/2026-05-27-paperforge-query-planning-design.md b/docs/superpowers/specs/2026-05-27-paperforge-query-planning-design.md new file mode 100644 index 00000000..eb969f3e --- /dev/null +++ b/docs/superpowers/specs/2026-05-27-paperforge-query-planning-design.md @@ -0,0 +1,898 @@ +# PaperForge Query Planning and Skill Trigger Design + +**Date:** 2026-05-27 +**Status:** Proposed +**Audience:** Maintainers, CLI contributors, skill authors, agent implementers + +--- + +## 1. Summary + +PaperForge's current literature retrieval flow is too dependent on agent-side judgment about how to write queries for `search`, when to prefer `retrieve`, and when a zero-result search means "not found" versus "query was malformed for this command". + +This design introduces a new CLI planning layer: + +- `paperforge query-plan` + +It becomes the truth source for: + +1. query classification +2. retrieval mode selection +3. query writing guidance +4. zero-result fallback policy +5. user-interactive fallback suggestions when content lookup cannot safely degrade automatically + +This design also proposes a minimal skill-layer improvement: + +- keep the current top-level compound routing order intact +- improve the skill trigger contract so agents are more reliably forced into the `paperforge` skill for literature retrieval, paper reading, and evidence lookup tasks +- make molecule-level retrieval execution depend on `query-plan` rather than freeform agent improvisation + +The goal is to make paper lookup and content lookup reliable without destabilizing the existing PaperForge skill graph. + +--- + +## 2. Problems Observed + +### 2.1 `search` is easy for agents to misuse + +Agents currently write natural-language or mixed-structure queries such as: + +- `Lin 2024 Electrical Stimulation` + +This is unreliable for `paperforge search` because `search` behaves like metadata retrieval, not semantic fulltext discovery. + +In practice, the query should be decomposed before execution: + +- first-pass: `author + year` +- second-pass narrowing: human-visible candidate inspection, optionally using title words + +### 2.2 Zero results are semantically ambiguous + +A zero-result `search` currently does not distinguish between: + +1. the paper is probably not in the library +2. the query was poorly formed for metadata search +3. the user is actually asking for content lookup rather than paper lookup + +Agents then incorrectly conclude: + +- `库里没有` + +This is a contract failure. + +### 2.3 `retrieve` is not just a low-confidence fallback + +For content lookup tasks such as: + +- finding where a method, parameter, term, or claim appears inside papers + +`retrieve` should be the primary path, not the last step in a generic paper-discovery ladder. + +### 2.4 Metadata search and fulltext search have different query-writing rules + +The current system does not expose this clearly enough to agents. + +As a result: + +1. agents write retrieval-style queries for metadata search +2. agents write metadata-style queries for content retrieval +3. agents use one tool's output count to reason about another tool's scope + +This causes both false negatives and unstable fallback behavior. + +### 2.5 Skill triggering is still too phrase-fragile + +The current `paperforge` skill frontmatter `description` contains many examples, but it still behaves like a keyword bucket rather than an explicit intent contract. + +For high-value tasks such as: + +1. paper discovery +2. known-paper reading +3. evidence lookup +4. deep reading +5. literature collection inspection + +the skill should be treated as mandatory, not optional. + +--- + +## 3. Design Goals + +### 3.1 Primary Goals + +1. Move query decomposition and retrieval planning into the CLI. +2. Prevent agents from interpreting zero-result search as library absence without additional reasoning. +3. Make `retrieve` the default for content lookup intent. +4. Make query-writing rules explicit per retrieval strategy. +5. Improve skill-trigger reliability without replacing the current skill graph. + +### 3.2 Secondary Goals + +1. Reduce prompt-only retrieval rules that can drift from implementation. +2. Preserve the existing compound routing order in `SKILL.md`. +3. Make testing easier with deterministic planning output. + +### 3.3 Non-Goals + +1. No redesign of the five top-level research intents. +2. No replacement of `/pf-paper` or `/pf-deep`. +3. No silent auto-reading of source files outside current safety rules. +4. No attempt to make `search` itself become semantic fulltext retrieval. + +--- + +## 4. Design Principle + +PaperForge retrieval should separate two user jobs: + +### 4.1 Paper Lookup + +The user wants to identify which paper(s) to inspect. + +Examples: + +- `找 Lin 2024 那篇` +- `找几篇关于 electrical stimulation 的文章` +- `这个 collection 里有什么` + +### 4.2 Content Lookup + +The user wants to find where some concept, parameter, method, or claim appears in paper content. + +Examples: + +- `找 galvanotaxis` +- `找 75 Hz` +- `找这句话的支持` +- `看看哪些文章正文里提到了某个方法` + +These two jobs must not share the same first-pass query contract. + +--- + +## 5. New CLI Command: `paperforge query-plan` + +### 5.1 Purpose + +`query-plan` is a planning command, not a retrieval command. + +It does not search the library directly. It tells the agent: + +1. what kind of query this is +2. which command should be used first +3. how the query should be rewritten +4. what fallback sequence should apply + +### 5.2 Example CLI + +```bash +paperforge query-plan "" --intent discover --json +paperforge query-plan "" --intent content --json +paperforge query-plan "" --intent known-paper --json +``` + +### 5.3 Intent Values + +Allowed planning intents: + +- `discover` +- `content` +- `known-paper` + +These map onto existing molecules without replacing them: + +- `discover` -> `discover-papers` +- `content` -> `find-supporting-evidence` +- `known-paper` -> `read-known-paper` + +### 5.4 Output Contract + +Recommended shape: + +```json +{ + "ok": true, + "command": "query-plan", + "version": "1.5.x", + "data": { + "intent": "discover", + "query_class": "mixed_query", + "signals": { + "doi": null, + "zotero_key": null, + "citation_key": null, + "author_tokens": ["Lin"], + "year_tokens": [2024], + "title_like_tokens": ["Electrical", "Stimulation"], + "content_terms": [] + }, + "signal_priority": [ + "doi", + "zotero_key", + "citation_key", + "author", + "year", + "title", + "topic" + ], + "recommended_primary": { + "command": "search", + "args": { + "query": "Lin", + "year_from": 2024, + "year_to": 2024, + "limit": 10 + } + }, + "query_writing_rules": [ + "For metadata search, prefer author and year over mixed natural-language query strings.", + "Do not include title words in first-pass author+year lookup." + ], + "fallback_plan": [ + { + "when": "zero_results", + "action": "report_noncanonical_query_risk" + }, + { + "when": "multiple_results", + "action": "let_user_or_agent_visually_narrow" + } + ] + } +} +``` + +--- + +## 6. Query Classification + +`query-plan` should classify input into stable, agent-facing categories. + +### 6.1 `identifier_exact` + +Used when the input contains: + +- DOI +- zotero key +- citation key + +Primary route: + +- `paper-context` + +No freeform search should happen first. + +### 6.2 `author_year` + +Used when the input contains a plausible author token and a year. + +Primary route: + +- `search "" --year-from YYYY --year-to YYYY` + +Title words must not be included in first-pass execution. + +### 6.3 `metadata_topic` + +Used when the user is discovering papers by topic, title-like words, domain, or collection. + +Primary route: + +- `search` +- or `context --collection` / `context --domain` when scope terms are explicit + +### 6.4 `content_term` + +Used when the user wants to find content inside papers: + +- parameter +- method +- exact term +- evidence support + +Primary route: + +- `retrieve` + +### 6.5 `mixed_query` + +Used when the query mixes multiple signal types into one string. + +Example: + +- `Lin 2024 Electrical Stimulation` + +For `discover`, mixed queries should be normalized into structured metadata lookup. + +For `content`, mixed queries should be split into content-bearing terms only if the user is searching inside papers. + +--- + +## 7. Signal Reliability Hierarchy + +The CLI should expose a stable signal hierarchy so the agent stops guessing. + +### 7.1 High-Reliability Signals + +These should dominate routing: + +1. `doi` +2. `zotero_key` +3. `citation_key` +4. `author` +5. `year` + +### 7.2 Medium-Reliability Signals + +1. short title phrase +2. explicit domain +3. explicit collection + +### 7.3 Lower-Reliability Signals + +1. broad topic phrase +2. natural-language description +3. long mixed query strings + +### 7.4 Rule + +When a high-reliability signal exists, lower-reliability tokens must not corrupt the first-pass query. + +Example: + +- if author + year are present, title words do not belong in first-pass `search` + +--- + +## 8. Query Writing Rules by Command + +These rules should appear both in `query-plan` output and in skill prose. + +### 8.1 `paper-context` + +Use when: + +- DOI +- zotero key +- citation key +- already-locked single paper + +Writing rule: + +- use exact identifier, no query rewriting + +### 8.2 `search` + +Use when: + +- paper lookup +- paper discovery +- collection/domain inspection + +Writing rules: + +1. prefer author and year for first-pass known-paper lookup +2. prefer short metadata-facing terms +3. do not mix long title fragments into first-pass author+year lookup +4. treat titles as a manual narrowing aid after candidate generation, not as mandatory first-pass input + +### 8.3 `retrieve` + +Use when: + +- content lookup +- evidence lookup +- in-paper term finding +- method/parameter/claim search + +Writing rules: + +1. prefer content-bearing terminology +2. use terms as they are likely to appear in正文 +3. completeness matters more than metadata neatness +4. do not treat `retrieve` as final evidence without later verification when exact quoting matters + +### 8.4 `rg` / `grep` + +Use when: + +- exact fulltext verification is needed +- `retrieve` is unavailable or insufficient + +Writing rules: + +1. prefer literal strings, units, abbreviations, parameter patterns, and stable terms +2. do not use long conversational questions as grep queries + +--- + +## 9. Zero-Result Semantics + +### 9.1 Problem + +A zero-result `search` should not automatically mean: + +- `库里没有` + +### 9.2 New Contract + +When `query-plan` classifies the query as noncanonical for the chosen command, the CLI should distinguish: + +1. `no_match_after_normalization` +2. `noncanonical_query_for_command` +3. `likely_wrong_intent_for_command` + +### 9.3 Recommended Behavior + +For known-paper and discover flows: + +1. `query-plan` rewrites the first pass +2. the molecule executes the recommended primary command +3. if zero results remain after normalized first pass, the response may say the library likely lacks a match +4. before that point, the response must not treat raw zero results as absence + +### 9.4 Why Planning Is Better Than Silent Auto-Retry in `search` + +This design intentionally keeps the retry logic visible at the planning layer. + +That preserves: + +1. transparency +2. deterministic testing +3. compatibility with existing molecules + +--- + +## 10. Content Lookup Contract + +### 10.1 Primary Rule + +If the user's intent is to find something inside paper content, the first retrieval command should be: + +- `retrieve` + +This is true even if paper discovery would also be possible. + +### 10.2 `retrieve` Is Not a Mere Late Fallback + +For content lookup, `retrieve` is the primary route, not a low-priority optional add-on. + +### 10.3 Fallback When `retrieve` Is Unavailable or Returns 0 + +Do not automatically conclude failure. + +Instead, return a structured fallback mode suggestion. The preferred fallback family is: + +- fulltext exact search (`rg` / `grep`) + +### 10.4 User-Interactive Fallback + +When `retrieve` is unavailable or insufficient, the CLI should support returning: + +- `interactive_fallback_required: true` +- `suggested_modes` + +Example modes: + +1. limit to a collection/domain and run `rg` +2. run broader fulltext grep now +3. use metadata search first to narrow paper candidates + +This preserves user control when exact fulltext search could be expensive or noisy. + +--- + +## 11. Scope Assessment Rules + +### 11.1 Important Constraint + +Metadata search result count must not be treated as the truth source for fulltext grep scope. + +This is because: + +1. metadata and正文 have different recall behavior +2. a term absent from abstract/title may still be common in methods or results +3. a term common in metadata may still be rare in正文 + +### 11.2 Recommended Scope Sources + +Scope estimation should prefer: + +1. explicit collection/domain restriction +2. inventory-level counts from `context` +3. fulltext-ready counts from runtime/index data +4. only then metadata search counts, and only as weak hints + +### 11.3 CLI Output Suggestion + +When relevant, `query-plan` may return: + +```json +"scope_assessment": { + "source": "collection_inventory", + "estimated_paper_count": 42, + "fulltext_ready_count": 39, + "confidence": "medium", + "recommended_mode": "ask_user_before_broad_grep" +} +``` + +This lets the skill ask the user with evidence rather than guesswork. + +--- + +## 12. Skill Routing Changes: Minimal and Safe + +### 12.1 Keep the Current Compound Order + +The current top-level routing order in `SKILL.md` should remain intact: + +1. mechanical commands +2. aliases +3. capture +4. known paper +5. discover papers +6. supporting evidence +7. clarify +8. post-action capture + +This design does not change the compound graph. + +### 12.2 Insert `query-plan` Only Inside Molecules + +Recommended insertion points: + +- `read-known-paper.md` +- `discover-papers.md` +- `find-supporting-evidence.md` + +The compound should not call `query-plan` globally. + +Instead: + +1. compound decides the molecule +2. molecule calls `query-plan` +3. molecule executes the recommended retrieval command + +This keeps routing simple and avoids top-level drift. + +### 12.3 Molecule-Specific Use + +#### `read-known-paper` + +Use `query-plan --intent known-paper` + +Expected outcomes: + +- identifier -> `paper-context` +- author+year -> normalized `search` +- vague title/topic -> `search` or `paper-status` + +#### `discover-papers` + +Use `query-plan --intent discover` + +Expected outcomes: + +- collection/domain inventory route +- metadata search route +- normalized mixed-query route + +#### `find-supporting-evidence` + +Use `query-plan --intent content` + +Expected outcomes: + +- primary `retrieve` +- interactive fallback suggestion when vector retrieval is unavailable or returns poor signal + +--- + +## 13. Skill Trigger Improvements + +### 13.1 Problem + +The current `description` lists many example phrases, but it does not strongly encode the idea that certain intents must always load the PaperForge skill. + +### 13.2 Proposed Direction + +Revise the skill frontmatter description and top prose so the trigger is intent-driven, not just phrase-driven. + +### 13.3 Target Trigger Semantics + +The `paperforge` skill must be invoked whenever the user is asking for any of the following: + +1. literature retrieval from the vault/library +2. identifying or reading a specific paper in the library +3. finding evidence, parameters, methods, or terms inside stored papers +4. inspecting collections or domains in the literature vault +5. deep reading or structured discussion of a stored paper +6. saving paper-derived research knowledge into PaperForge logs + +### 13.4 Description Direction + +The description should speak in terms of user intents, for example: + +- mandatory for paper lookup, paper discovery, evidence lookup, deep reading, and PaperForge research-memory capture + +This is preferable to relying only on examples like: + +- `找文献` +- `找支持` +- `collection` + +### 13.5 Why This Helps + +Intent-driven trigger wording: + +1. generalizes better to unseen phrasing +2. reduces accidental non-use of the skill +3. aligns better with the AGENTS.md rule that literature retrieval and reading must go through PaperForge + +--- + +## 14. CLI and Documentation Consistency Fixes + +The current system must also fix misleading terminology. + +### 14.1 `search` Description Fix + +`search` should no longer be described as: + +- full-text search across the library + +It should be described as: + +- metadata/FTS search across indexed paper fields such as title, abstract, author, journal, domain, and collection + +### 14.2 `retrieve` Description Fix + +`retrieve` should be described as: + +- semantic content retrieval across OCR fulltext + +### 14.3 Agent Context Rules + +`agent-context` should explicitly state: + +1. `search` and `retrieve` have different query-writing contracts +2. content lookup should begin with `retrieve` +3. author+year first-pass lookup should exclude title words +4. raw zero-result search is not enough to conclude absence + +--- + +## 15. Versioning and Compatibility Model + +### 15.1 Problem + +The current skill preflight expects the skill frontmatter version to match the plugin/package version. + +This is too coarse and creates ambiguity: + +1. plugin code may change while the skill contract does not +2. the skill contract may change while plugin packaging changes are unrelated +3. maintainers cannot tell whether a mismatch means a real incompatibility or only a release-version drift + +### 15.2 Design Rule + +PaperForge should distinguish: + +1. release/package version +2. skill contract version +3. compatibility level + +These are not the same thing and should not be forced into one shared version string. + +### 15.3 Proposed Fields + +Recommended model: + +- `plugin_version` +- `skill_version` +- `skill_api_version` + +#### `plugin_version` + +Meaning: + +- the released PaperForge package/plugin version + +Used for: + +- manifest +- installer/update flow +- git tag / release tracking +- package-level changelog + +Example: + +- `1.5.14` + +#### `skill_version` + +Meaning: + +- the concrete shipped version of the `paperforge` skill bundle + +Used for: + +- skill deployment identity +- debugging whether the vault has the latest skill copy +- distinguishing skill-only updates from package-only updates + +Example: + +- `2026-05-27.1` + +#### `skill_api_version` + +Meaning: + +- the compatibility contract version between CLI planning/runtime outputs and the skill's expectations + +Used for: + +- preflight compatibility checks +- determining whether the currently deployed skill can safely interpret the current CLI outputs + +Example: + +- `2` + +### 15.4 Compatibility Principle + +Agents should care primarily about: + +- `skill_api_version` + +Maintainers may also inspect: + +- `skill_version` + +Package release tooling should care primarily about: + +- `plugin_version` + +### 15.5 Preflight Rule Change + +The current rule: + +- skill frontmatter version must equal package version + +should be removed. + +Replace it with: + +1. confirm the skill exists and is readable +2. confirm the deployed skill reports `skill_api_version` compatible with the CLI +3. report `plugin_version` and `skill_version` for debugging +4. if `skill_api_version` mismatches, tell the user to run `paperforge update` + +### 15.6 Bootstrap Contract Change + +`pf_bootstrap.py` should return all three fields when available: + +```json +{ + "plugin_version": "1.5.14", + "skill_version": "2026-05-27.1", + "skill_api_version": 2 +} +``` + +This should replace the current weaker pattern where the skill version may be inferred only from the skill file's frontmatter. + +### 15.7 Example Scenarios + +#### Scenario A: package update, skill unchanged + +- `plugin_version` changes +- `skill_version` unchanged +- `skill_api_version` unchanged + +Interpretation: + +- safe; the skill contract did not change + +#### Scenario B: skill routing update, package release also happens + +- `plugin_version` changes +- `skill_version` changes +- `skill_api_version` may or may not change + +Interpretation: + +- if `skill_api_version` unchanged, this is a compatible skill refinement +- if `skill_api_version` changes, the deployed skill and CLI must be updated together + +#### Scenario C: local vault still has old deployed skill + +- installed CLI/package is newer +- deployed skill in vault is older +- `skill_api_version` mismatch + +Interpretation: + +- this is a real compatibility problem and the user should update/redeploy the skill + +### 15.8 Frontmatter Direction + +The `paperforge` skill frontmatter should no longer overload a single `version` field to mean package release compatibility. + +Recommended direction: + +- keep `source: paperforge` +- add `skill_version` +- add `skill_api_version` + +If a plain `version` field is still required by tooling, it should be documented as the skill bundle version, not the package version. + +--- + +## 16. Testing Focus + +### 15.1 CLI Query Planning Tests + +Add tests for: + +1. DOI -> `paper-context` +2. zotero key -> `paper-context` +3. `Lin 2024 Electrical Stimulation` with `discover` intent -> normalized `search(author + year)` +4. `galvanotaxis` with `content` intent -> `retrieve` +5. content lookup without vector availability -> interactive fallback plan + +### 15.2 Skill Contract Tests + +Add tests to assert: + +1. molecules reference `query-plan` +2. content molecule makes `retrieve` the primary route +3. known-paper molecule prioritizes identifiers and author+year +4. skill description contains intent-level trigger language + +### 15.3 Regression Target + +The key regression to prevent is: + +- raw mixed query -> zero results -> agent claims the paper is absent + +--- + +## 17. Implementation Sequence + +Recommended order: + +1. add `paperforge query-plan` CLI command +2. add query classification and planning logic +3. update `agent-context` and command help text +4. introduce `plugin_version` / `skill_version` / `skill_api_version` contract in bootstrap and skill metadata +5. revise skill `description` and top trigger prose +6. update `read-known-paper.md` +7. update `discover-papers.md` +8. update `find-supporting-evidence.md` +9. add tests for CLI planning, version compatibility, and skill contracts + +--- + +## 18. Final Principle + +The agent should not be responsible for inventing PaperForge retrieval syntax. + +The agent's job should be: + +1. route to the correct molecule +2. ask the CLI how to search +3. execute the recommended command +4. interpret the result honestly + +The CLI's job should be: + +1. classify the query +2. expose signal strength +3. recommend the correct retrieval strategy +4. prevent false "not found" conclusions from malformed first-pass queries + +This preserves the current skill graph while making retrieval behavior far more stable. diff --git a/paperforge/cli.py b/paperforge/cli.py index ecfe0206..918ff9c3 100644 --- a/paperforge/cli.py +++ b/paperforge/cli.py @@ -272,12 +272,17 @@ def build_parser() -> argparse.ArgumentParser: p_embed_stop = p_embed_sp.add_parser("stop", help="Stop running embed build") p_embed_stop.add_argument("--json", action="store_true") - p_retrieve = sub.add_parser("retrieve", help="Semantic search across OCR fulltext") + p_retrieve = sub.add_parser("retrieve", help="Semantic content retrieval across OCR fulltext") p_retrieve.add_argument("query", help="Search query") p_retrieve.add_argument("--json", action="store_true") p_retrieve.add_argument("--limit", type=int, default=5) p_retrieve.add_argument("--expand", action="store_true", default=True) + p_qp = sub.add_parser("query-plan", help="Classify a literature query and recommend the first retrieval command") + p_qp.add_argument("query", help="User query to classify") + p_qp.add_argument("--intent", choices=["discover", "content", "known-paper"], required=True, help="Retrieval intent") + p_qp.add_argument("--json", action="store_true", help="Output as JSON") + # prune p_prune = sub.add_parser("prune", help="Delete orphan paper artifacts (dry-run by default)") p_prune.add_argument("--force", action="store_true", help="Actually delete (default: dry-run)") @@ -296,8 +301,8 @@ def build_parser() -> argparse.ArgumentParser: p_paper_status.add_argument("query", help="Paper identifier (zotero_key, DOI, title, alias)") p_paper_status.add_argument("--json", action="store_true", help="Output as JSON") - p_pc = sub.add_parser("paper-context", help="Get full context for a paper (metadata + reading notes + corrections)") - p_pc.add_argument("key", help="Zotero key") + p_pc = sub.add_parser("paper-context", help="Get full context for a paper by zotero key, DOI, or citation key") + p_pc.add_argument("key", help="Paper identifier (zotero key, DOI, or citation key)") p_pc.add_argument("--json", action="store_true", help="Output as JSON") p_rl = sub.add_parser("reading-log", help="Record or export reading notes") @@ -330,8 +335,8 @@ def build_parser() -> argparse.ArgumentParser: p_pl.add_argument("--limit", type=int, default=50, help="Max entries to list") p_pl.add_argument("--json", action="store_true", help="Output as PFResult JSON") - p_search = sub.add_parser("search", help="Full-text search across the library") - p_search.add_argument("query", help="Search query (supports FTS5 syntax)") + p_search = sub.add_parser("search", help="Metadata FTS search across indexed paper fields") + p_search.add_argument("query", help="Search query for title/abstract/author/journal/domain/collection metadata") p_search.add_argument("--json", action="store_true", help="Output as JSON") p_search.add_argument("--limit", type=int, default=20, help="Max results") p_search.add_argument("--domain", help="Filter by domain") @@ -462,9 +467,6 @@ def main(argv: list[str] | None = None) -> int: if argv is None: argv = sys.argv[1:] - _resolve_pipeline() - _import_worker_functions() - parser = build_parser() args = parser.parse_args(argv) @@ -472,6 +474,20 @@ def main(argv: list[str] | None = None) -> int: parser.print_help() return 1 + # Resolve/import worker modules only for commands that actually need them. + lightweight_commands = { + "paths", "sync", "ocr", "status", "deep-reading", "deep-finalize", + "context", "repair", "dashboard", "memory", "embed", "retrieve", + "query-plan", "prune", "paper-status", "paper-context", "reading-log", + "project-log", "search", "agent-context", "runtime-health", "doctor", + "update", "setup", "selection-sync", "index-refresh", "base-refresh", + } + if args.command in lightweight_commands: + _resolve_pipeline() + worker_import_commands = {"status", "deep-reading", "ocr", "selection-sync", "index-refresh", "base-refresh"} + if args.command in worker_import_commands: + _import_worker_functions() + # Resolve vault try: vault = resolve_vault(cli_vault=args.vault) @@ -577,6 +593,11 @@ def main(argv: list[str] | None = None) -> int: return run(args) + if args.command == "query-plan": + from paperforge.commands.query_plan import run + + return run(args) + if args.command == "prune": from paperforge.commands.prune import run @@ -696,6 +717,7 @@ def _cmd_paths(vault: Path, args: argparse.Namespace) -> int: filtered["vault"] = str(vault.resolve()) filtered["worker_script"] = str(paths["worker_script"].resolve()) filtered["pf_deep_script"] = str(paths["pf_deep_script"].resolve()) + filtered["ld_deep_script"] = filtered["pf_deep_script"] print(json.dumps(filtered, ensure_ascii=False, indent=2)) else: for key, path_str in sorted(all_paths.items()): diff --git a/paperforge/commands/__init__.py b/paperforge/commands/__init__.py index 6c98f2f8..e6b2d4be 100644 --- a/paperforge/commands/__init__.py +++ b/paperforge/commands/__init__.py @@ -13,6 +13,7 @@ _COMMAND_REGISTRY: dict[str, str] = { "memory": "paperforge.commands.memory", "embed": "paperforge.commands.embed", "retrieve": "paperforge.commands.retrieve", + "query-plan": "paperforge.commands.query_plan", "paper-status": "paperforge.commands.paper_status", "agent-context": "paperforge.commands.agent_context", "prune": "paperforge.commands.prune", diff --git a/paperforge/commands/agent_context.py b/paperforge/commands/agent_context.py index fb444c71..08513adb 100644 --- a/paperforge/commands/agent_context.py +++ b/paperforge/commands/agent_context.py @@ -9,17 +9,21 @@ from paperforge.core.result import PFError, PFResult from paperforge.memory.context import get_agent_context COMMANDS = { + "query-plan": { + "usage": "paperforge query-plan --intent discover|content|known-paper --json", + "purpose": "Classify the query, choose the best first retrieval command, and explain fallback rules", + }, "paper-status": { "usage": "paperforge paper-status --json", "purpose": "Look up one paper's full status and recommended next action", }, "search": { "usage": "paperforge search --json [--domain NAME] [--ocr done|pending|failed|processing] [--year-from N] [--year-to N] [--limit N] [--lifecycle indexed|pdf_ready|fulltext_ready|deep_read_done]", - "purpose": "Full-text search with optional domain and workflow-state filters", + "purpose": "Metadata FTS search over title, abstract, authors, journal, domain, and collection fields", }, "retrieve": { "usage": "paperforge retrieve --json [--limit N]", - "purpose": "Semantic search across OCR fulltext — discover papers by body content, not just title/abstract", + "purpose": "Semantic content retrieval across OCR fulltext — use this first when the user wants something inside papers", }, "context": { "usage": "paperforge context | --domain D | --collection P | --all", @@ -45,7 +49,11 @@ RULES = [ "Read source files only after resolving candidates via paper-status, search, retrieve, or context.", "To locate a paper: start with collection scope if known, then expand to full library search.", "Paper discovery must use multi-arm strategy: retrieve (body text) + search (metadata) + context --collection (inventory). Never rely on a single search tool.", + "Use query-plan before freeform retrieval when the query mixes author/year/title/content signals.", + "For known-paper lookup, DOI/zotero_key/citation_key are higher-signal than title words; author+year first-pass search should exclude title terms.", + "For content lookup, start with retrieve. Search is not semantic fulltext discovery.", "When a search returns > 20 results, present the count to the user and offer to narrow — never silently skip large result sets.", + "A raw zero-result search does not prove library absence when the query is noncanonical for metadata search.", "Check embed status --json (db_exists + chunk_count) before calling retrieve; skip retrieve if vector index is unavailable.", ] diff --git a/paperforge/commands/paper_context.py b/paperforge/commands/paper_context.py index 193d82cf..1faef3ca 100644 --- a/paperforge/commands/paper_context.py +++ b/paperforge/commands/paper_context.py @@ -9,6 +9,7 @@ from paperforge.core.errors import ErrorCode from paperforge.core.result import PFError, PFResult from paperforge.memory.db import get_connection, get_memory_db_path from paperforge.memory.permanent import get_corrections_for_paper, get_reading_notes_for_paper +from paperforge.memory.query import lookup_paper def _build_paper_context(vault, key: str) -> dict | None: @@ -20,13 +21,17 @@ def _build_paper_context(vault, key: str) -> dict | None: conn = get_connection(db_path, read_only=True) try: + matches = lookup_paper(conn, key) + if not matches or len(matches) != 1: + return None + resolved_key = matches[0]["zotero_key"] row = conn.execute( """SELECT zotero_key, citation_key, title, year, doi, journal, first_author, domain, collection_path, has_pdf, ocr_status, analyze, deep_reading_status, lifecycle, next_step, pdf_path, note_path, fulltext_path, paper_root FROM papers WHERE zotero_key = ?""", - (key,), + (resolved_key,), ).fetchone() if not row: @@ -34,7 +39,7 @@ def _build_paper_context(vault, key: str) -> dict | None: paper = dict(row) - prior_notes = get_reading_notes_for_paper(vault, key) + prior_notes = get_reading_notes_for_paper(vault, resolved_key) corrections = [] corr_rows = conn.execute( @@ -42,7 +47,7 @@ def _build_paper_context(vault, key: str) -> dict | None: FROM paper_events WHERE paper_id = ? AND event_type = 'correction_note' ORDER BY created_at DESC""", - (key,), + (resolved_key,), ).fetchall() seen_ids: set[str] = set() for cr in corr_rows: @@ -57,7 +62,7 @@ def _build_paper_context(vault, key: str) -> dict | None: if orig_id: seen_ids.add(orig_id) - jsonl_corrections = get_corrections_for_paper(vault, key) + jsonl_corrections = get_corrections_for_paper(vault, resolved_key) for c in jsonl_corrections: cid = c.get("original_id", "") if cid and cid in seen_ids: diff --git a/paperforge/commands/query_plan.py b/paperforge/commands/query_plan.py new file mode 100644 index 00000000..ffdbac82 --- /dev/null +++ b/paperforge/commands/query_plan.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import argparse +import sys + +from paperforge import __version__ as PF_VERSION +from paperforge.core.errors import ErrorCode +from paperforge.core.result import PFError, PFResult +from paperforge.query_planning import build_query_plan, enrich_query_plan_with_runtime + + +def run(args: argparse.Namespace) -> int: + try: + data = build_query_plan(args.query, args.intent) + data = enrich_query_plan_with_runtime(data, args.vault_path) + result = PFResult(ok=True, command="query-plan", version=PF_VERSION, data=data) + except Exception as exc: + result = PFResult( + ok=False, + command="query-plan", + version=PF_VERSION, + error=PFError(code=ErrorCode.INTERNAL_ERROR, message=str(exc)), + ) + + if args.json: + print(result.to_json()) + else: + if result.ok: + primary = result.data["recommended_primary"] + print(f"Intent: {result.data['intent']}") + print(f"Query class: {result.data['query_class']}") + print(f"Primary: {primary['command']}") + print(f"Args: {primary['args']}") + else: + print(f"Error: {result.error.message}", file=sys.stderr) + + return 0 if result.ok else 1 diff --git a/paperforge/commands/retrieve.py b/paperforge/commands/retrieve.py index e03c9beb..92bad714 100644 --- a/paperforge/commands/retrieve.py +++ b/paperforge/commands/retrieve.py @@ -8,6 +8,26 @@ from paperforge.core.errors import ErrorCode from paperforge.core.result import PFError, PFResult from paperforge.embedding import retrieve_chunks from paperforge.memory.db import get_connection, get_memory_db_path +from paperforge.query_planning import build_query_plan, enrich_query_plan_with_runtime + + +def _looks_generic_chunk(text: str) -> bool: + compact = (text or "").strip().lower() + if not compact: + return True + if compact in {"[figure]", "none.", "or", "### keywords", "### abbreviations", "### reference", "### conclusion", "### references"}: + return True + if len(compact) <= 12: + return True + return False + + +def _is_low_confidence_semantic_result(chunks: list[dict]) -> bool: + if not chunks: + return False + generic_top = sum(1 for chunk in chunks[:5] if _looks_generic_chunk(chunk.get("chunk_text", ""))) + max_score = max(float(chunk.get("score", 0) or 0) for chunk in chunks[:5]) + return generic_top >= 3 or max_score < 0.62 def run(args: argparse.Namespace) -> int: @@ -19,6 +39,7 @@ def run(args: argparse.Namespace) -> int: from paperforge.embedding import get_embed_status status = get_embed_status(vault) if not status.get("healthy", True): + plan = enrich_query_plan_with_runtime(build_query_plan(query, "content"), vault) result = PFResult( ok=False, command="retrieve", @@ -30,6 +51,9 @@ def run(args: argparse.Namespace) -> int: data={ "next_action": "paperforge embed build --force", "details": status.get("error", ""), + "interactive_fallback_required": plan.get("interactive_fallback_required", False), + "scope_assessment": plan.get("scope_assessment"), + "suggested_modes": plan.get("suggested_modes", []), }, ) if args.json: @@ -39,6 +63,7 @@ def run(args: argparse.Namespace) -> int: return 1 if status.get("chunk_count", 0) == 0: + plan = enrich_query_plan_with_runtime(build_query_plan(query, "content"), vault) result = PFResult( ok=False, command="retrieve", @@ -47,7 +72,12 @@ def run(args: argparse.Namespace) -> int: code=ErrorCode.PATH_NOT_FOUND, message="Vector index is empty. Run paperforge embed build first.", ), - data={"next_action": "paperforge embed build"}, + data={ + "next_action": "paperforge embed build", + "interactive_fallback_required": plan.get("interactive_fallback_required", False), + "scope_assessment": plan.get("scope_assessment"), + "suggested_modes": plan.get("suggested_modes", []), + }, ) if args.json: print(result.to_json()) @@ -83,7 +113,38 @@ def run(args: argparse.Namespace) -> int: conn.close() data = {"query": query, "chunks": chunks, "count": len(chunks)} - result = PFResult(ok=True, command="retrieve", version=PF_VERSION, data=data) + warnings: list[str] = [] + next_actions: list[dict] = [] + if len(chunks) == 0: + plan = enrich_query_plan_with_runtime(build_query_plan(query, "content"), vault) + data["query_diagnostic"] = { + "scope_assessment": plan.get("scope_assessment"), + "interactive_fallback_required": plan.get("interactive_fallback_required", False), + "suggested_modes": plan.get("suggested_modes", []), + } + warnings.append("Semantic retrieval returned no chunks. This does not prove the content is absent from the library.") + next_actions.append( + { + "command": "paperforge query-plan", + "reason": "Review the fallback modes for content lookup and fulltext verification.", + } + ) + elif _is_low_confidence_semantic_result(chunks): + plan = enrich_query_plan_with_runtime(build_query_plan(query, "content"), vault) + data["query_diagnostic"] = { + "scope_assessment": plan.get("scope_assessment"), + "interactive_fallback_required": True, + "suggested_modes": plan.get("suggested_modes", []), + "reason": "Top semantic hits look generic or weakly related to the query.", + } + warnings.append("Semantic retrieval returned low-confidence hits. Verify with fulltext grep or narrow the scope before treating these as evidence.") + next_actions.append( + { + "command": "paperforge query-plan", + "reason": "Inspect fallback modes for exact fulltext verification.", + } + ) + result = PFResult(ok=True, command="retrieve", version=PF_VERSION, data=data, warnings=warnings, next_actions=next_actions) if args.json: print(result.to_json()) diff --git a/paperforge/commands/search.py b/paperforge/commands/search.py index 7edb9baf..3b26b7b5 100644 --- a/paperforge/commands/search.py +++ b/paperforge/commands/search.py @@ -8,6 +8,7 @@ from paperforge.core.errors import ErrorCode from paperforge.core.result import PFError, PFResult from paperforge.memory.db import get_connection, get_memory_db_path from paperforge.memory.fts import search_papers +from paperforge.query_planning import build_query_plan, enrich_query_plan_with_runtime def run(args: argparse.Namespace) -> int: @@ -58,7 +59,32 @@ def run(args: argparse.Namespace) -> int: "next_step": args.next_step, }, } - result = PFResult(ok=True, command="search", version=PF_VERSION, data=data) + warnings: list[str] = [] + next_actions: list[dict] = [] + if len(results) == 0: + plan = enrich_query_plan_with_runtime(build_query_plan(query, "discover"), vault) + data["query_diagnostic"] = { + "query_class": plan["query_class"], + "recommended_primary": plan["recommended_primary"], + "query_writing_rules": plan["query_writing_rules"], + "scope_assessment": plan.get("scope_assessment"), + } + if plan["query_class"] in {"mixed_query", "author_year"}: + warnings.append("Zero results may reflect a noncanonical metadata query rather than library absence.") + next_actions.append( + { + "command": "paperforge query-plan", + "reason": "Normalize the query and choose the correct first retrieval command.", + } + ) + if plan["recommended_primary"]["command"] != "search": + next_actions.append( + { + "command": f"paperforge {plan['recommended_primary']['command']}", + "reason": "The planning layer recommends a different first command for this query.", + } + ) + result = PFResult(ok=True, command="search", version=PF_VERSION, data=data, warnings=warnings, next_actions=next_actions) except Exception as exc: result = PFResult( ok=False, command="search", version=PF_VERSION, diff --git a/paperforge/commands/sync.py b/paperforge/commands/sync.py index eeb76b27..17383712 100644 --- a/paperforge/commands/sync.py +++ b/paperforge/commands/sync.py @@ -1,6 +1,7 @@ """Sync command — unified sync through SyncService.""" import argparse +import inspect import logging from paperforge import __version__ @@ -54,8 +55,21 @@ def run(args: argparse.Namespace) -> int: from paperforge.services.sync_service import SyncService svc = SyncService(vault) - result = svc.run(verbose=verbose, json_output=json_output, selection_only=selection_only, index_only=index_only, - prune=prune_flag, prune_force=prune_force) + run_kwargs = { + "verbose": verbose, + "json_output": json_output, + "selection_only": selection_only, + "index_only": index_only, + "prune": prune_flag, + "prune_force": prune_force, + } + try: + sig = inspect.signature(svc.run) + accepted = {name for name in sig.parameters.keys() if name != "self"} + filtered_kwargs = {k: v for k, v in run_kwargs.items() if k in accepted} + except Exception: + filtered_kwargs = run_kwargs + result = svc.run(**filtered_kwargs) _write_orphan_state(vault, result) diff --git a/paperforge/memory/query.py b/paperforge/memory/query.py index c2b8c3a5..9421c7a8 100644 --- a/paperforge/memory/query.py +++ b/paperforge/memory/query.py @@ -80,7 +80,8 @@ def get_memory_status(vault: Path) -> dict: def _entry_from_row(row) -> dict: """Reconstruct an entry dict from a papers row (sqlite3.Row).""" - entry = {k: row[k] for k in row} + keys = row.keys() if hasattr(row, "keys") else row + entry = {k: row[k] for k in keys} for key in ("has_pdf", "do_ocr", "analyze"): if key in entry and entry[key] is not None: entry[key] = bool(entry[key]) diff --git a/paperforge/plugin/main.js b/paperforge/plugin/main.js index 7f0ede1b..75ce4cab 100644 --- a/paperforge/plugin/main.js +++ b/paperforge/plugin/main.js @@ -1,11 +1,11 @@ -"use strict";var dt=Object.create;var we=Object.defineProperty;var ut=Object.getOwnPropertyDescriptor;var ht=Object.getOwnPropertyNames;var gt=Object.getPrototypeOf,ft=Object.prototype.hasOwnProperty;var _t=(h,g)=>{for(var e in g)we(h,e,{get:g[e],enumerable:!0})},qe=(h,g,e,t)=>{if(g&&typeof g=="object"||typeof g=="function")for(let s of ht(g))!ft.call(h,s)&&s!==e&&we(h,s,{get:()=>g[s],enumerable:!(t=ut(g,s))||t.enumerable});return h};var j=(h,g,e)=>(e=h!=null?dt(gt(h)):{},qe(g||!h||!h.__esModule?we(e,"default",{value:h,enumerable:!0}):e,h)),mt=h=>qe(we({},"__esModule",{value:!0}),h);var xt={};_t(xt,{default:()=>Re});module.exports=mt(xt);var J=require("obsidian"),I=j(require("fs")),me=j(require("path")),ie=require("child_process");var pe="paperforge-status",xe="paperforge",Ge='',U=[{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"}],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 all literature metadata (papers, assets, aliases, reading events), provides FTS5 full-text search across titles/abstracts/authors/collections, and enables the agent-context and paper-status commands. 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",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_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\u6240\u6709\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 \u5168\u6587\u68C0\u7D22\uFF08\u53EF\u641C\u7D22\u6807\u9898\u3001\u6458\u8981\u3001\u4F5C\u8005\u3001\u5206\u7C7B\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",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_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 yt(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=yt(h)==="zh"?Ie.zh:Ie.en}function i(h){return Le&&Le[h]||Ie.en[h]||h}var P=require("obsidian"),O=j(require("fs")),$=j(require("path")),pt=j(require("os")),V=require("child_process");var Y=j(require("fs")),X=j(require("path")),et=j(require("os")),re=require("child_process"),Me=null,Qe=!1;function B(h,g,e,t){let s=e||Y,n=t||re.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||re.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@v${g}`,n=[...e,"-m","pip","install","--upgrade",t],a=[...e,"-m","pip","install","--upgrade",s];return{cmd:h,url:s.replace("@v","@"),args:a,pypiArgs:n,gitArgs:a,timeout:12e4}}function rt(h,g,e,t,s,n){let a=s||re.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("")+` +"use strict";var dt=Object.create;var Se=Object.defineProperty;var ut=Object.getOwnPropertyDescriptor;var ht=Object.getOwnPropertyNames;var gt=Object.getPrototypeOf,ft=Object.prototype.hasOwnProperty;var _t=(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 ht(g))!ft.call(h,s)&&s!==e&&Se(h,s,{get:()=>g[s],enumerable:!(t=ut(g,s))||t.enumerable});return h};var j=(h,g,e)=>(e=h!=null?dt(gt(h)):{},Ue(g||!h||!h.__esModule?Se(e,"default",{value:h,enumerable:!0}):e,h)),mt=h=>Ue(Se({},"__esModule",{value:!0}),h);var xt={};_t(xt,{default:()=>Re});module.exports=mt(xt);var J=require("obsidian"),I=j(require("fs")),me=j(require("path")),ie=require("child_process");var pe="paperforge-status",xe="paperforge",Ge='',q=[{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"}],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_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_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 yt(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=yt(h)==="zh"?Ie.zh:Ie.en}function i(h){return Le&&Le[h]||Ie.en[h]||h}var P=require("obsidian"),O=j(require("fs")),$=j(require("path")),pt=j(require("os")),V=require("child_process");var Y=j(require("fs")),X=j(require("path")),et=j(require("os")),re=require("child_process"),Me=null,Qe=!1;function B(h,g,e,t){let s=e||Y,n=t||re.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||re.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||re.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,re.execFileSync)(g,["/c","where","git"],{timeout:5e3,windowsHide:!0,encoding:"utf-8"})}else h=(0,re.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 $e(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 bt(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=bt(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 ze(h){try{return ue.existsSync(h)?JSON.parse(ue.readFileSync(h,"utf-8")):null}catch(g){return null}}function vt(h){let g=ee(h);return ze(g.memoryStatePath)}function ke(h){let g=ee(h);return ze(g.vectorStatePath)}function je(h){let g=ee(h);return ze(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=vt(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 ne(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({...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 H.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 H.Notice("PaperForge: Python not found"),this.close();return}let a=n.map(r=>r.key);(0,Se.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 H.Notice("PaperForge: prune failed"),this.close();return}try{let l=JSON.parse(o),c=l.data&&l.data.deleted||[];new H.Notice("Deleted "+c.length+" orphan workspace(s)")}catch(l){new H.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(!q.existsSync(s)){console.log("[PF] orphan file NOT FOUND");return}console.log("[PF] orphan file FOUND");let n=q.readFileSync(s,"utf-8"),r=JSON.parse(n).orphans||[];if(console.log("[PF] orphans count:",r.length),r.length===0)return;let o=ne(e,g.settings);console.log("[PF] py.path:",o?o.path:"null"),new Ve(h,r,e,o).open(),q.unlinkSync(s),console.log("[PF] orphan file cleaned")}catch(t){console.log("[PF] checkOrphanState exception:",t.message||t)}}var Pe=class extends H.Modal{constructor(e,t){super(e);this._pendingSave=null;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":""} ${a1&&e.createEl("button",{cls:"paperforge-step-btn",text:i("nav_prev")}).addEventListener("click",()=>{this._step--,this._render()}),this._step<5?e.createEl("button",{cls:"paperforge-step-btn mod-cta",text:i("nav_next")}).addEventListener("click",()=>{this._step===3&&!this._validateStep3()||(this._step++,this._render())}):e.createEl("button",{cls:"paperforge-step-btn",text:i("nav_close")}).addEventListener("click",()=>this.close())}_validateStep3(){let e=this.plugin.settings;if(!this._apiKeyValidated)return new H.Notice("\u8BF7\u5148\u9A8C\u8BC1 PaddleOCR API \u5BC6\u94A5"),!1;if(!e.zotero_data_dir||!e.zotero_data_dir.trim())return new H.Notice("Zotero \u6570\u636E\u76EE\u5F55\u4E3A\u5FC5\u586B\u9879\uFF0C\u8BF7\u586B\u5199\u8DEF\u5F84"),!1;let t=e.zotero_data_dir.trim();if(!q.existsSync(t))return new H.Notice("Zotero \u6570\u636E\u76EE\u5F55\u8DEF\u5F84\u4E0D\u5B58\u5728"),!1;if(!q.statSync(t).isDirectory())return new H.Notice("Zotero \u6570\u636E\u76EE\u5F55\u8DEF\u5F84\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55"),!1;let s=lt.join(t,"storage");return!q.existsSync(s)||!q.statSync(s).isDirectory()?(new H.Notice("Zotero \u6570\u636E\u76EE\u5F55\u4E2D\u672A\u627E\u5230 storage/ \u5B50\u76EE\u5F55"),!1):!0}_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){e.createEl("h2",{text:i("wizard_step3")});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);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",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=ct.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",this._apiKeyStatus.className="paperforge-apikey-status error",this._apiKeyValidated=!1):(this._apiKeyStatus.textContent="\u9A8C\u8BC1\u5931\u8D25\uFF1AAPI \u8FD4\u56DE "+r.statusCode,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",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+")",this._apiKeyStatus.className="paperforge-apikey-status error",this._apiKeyValidated=!1}),a.write(s),a.end()}_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=[]}=B(t.vault_path.trim(),this.plugin.settings,void 0,void 0),w=(0,Se.spawn)(f,[...E,...d],{cwd:t.vault_path.trim(),env:de(),timeout:12e4,..._}),b="",m="";w.stdout.on("data",v=>{let k=v.toString("utf-8");b+=k,_.logStdout&&this._processSetupOutput(k)}),w.stderr.on("data",v=>{let k=v.toString("utf-8");m+=k,this._log("[stderr] "+k.trim())}),w.on("close",v=>{v===0?y({stdout:b,stderr:m}):x(new Error(m.trim()||b.trim()||`exit code ${v}`))}),w.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)||"?",w=process.platform+" "+process.arch,b,m;try{b=Ne()||"(not found)"}catch(F){b="(error)"}try{m=B(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: "+w,"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(` +`)[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 $e(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 bt(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=bt(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 ze(h){try{return ue.existsSync(h)?JSON.parse(ue.readFileSync(h,"utf-8")):null}catch(g){return null}}function vt(h){let g=ee(h);return ze(g.memoryStatePath)}function ke(h){let g=ee(h);return ze(g.vectorStatePath)}function je(h){let g=ee(h);return ze(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=vt(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 ne(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({...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 H.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 H.Notice("PaperForge: Python not found"),this.close();return}let a=n.map(r=>r.key);(0,we.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 H.Notice("PaperForge: prune failed"),this.close();return}try{let l=JSON.parse(o),c=l.data&&l.data.deleted||[];new H.Notice("Deleted "+c.length+" orphan workspace(s)")}catch(l){new H.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=ne(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 Pe=class extends H.Modal{constructor(e,t){super(e);this._pendingSave=null;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":""} ${a1&&e.createEl("button",{cls:"paperforge-step-btn",text:i("nav_prev")}).addEventListener("click",()=>{this._step--,this._render()}),this._step<5?e.createEl("button",{cls:"paperforge-step-btn mod-cta",text:i("nav_next")}).addEventListener("click",()=>{this._step===3&&!this._validateStep3()||(this._step++,this._render())}):e.createEl("button",{cls:"paperforge-step-btn",text:i("nav_close")}).addEventListener("click",()=>this.close())}_validateStep3(){let e=this.plugin.settings;if(!this._apiKeyValidated)return new H.Notice("\u8BF7\u5148\u9A8C\u8BC1 PaddleOCR API \u5BC6\u94A5"),!1;if(!e.zotero_data_dir||!e.zotero_data_dir.trim())return new H.Notice("Zotero \u6570\u636E\u76EE\u5F55\u4E3A\u5FC5\u586B\u9879\uFF0C\u8BF7\u586B\u5199\u8DEF\u5F84"),!1;let t=e.zotero_data_dir.trim();if(!U.existsSync(t))return new H.Notice("Zotero \u6570\u636E\u76EE\u5F55\u8DEF\u5F84\u4E0D\u5B58\u5728"),!1;if(!U.statSync(t).isDirectory())return new H.Notice("Zotero \u6570\u636E\u76EE\u5F55\u8DEF\u5F84\u4E0D\u662F\u4E00\u4E2A\u76EE\u5F55"),!1;let s=lt.join(t,"storage");return!U.existsSync(s)||!U.statSync(s).isDirectory()?(new H.Notice("Zotero \u6570\u636E\u76EE\u5F55\u4E2D\u672A\u627E\u5230 storage/ \u5B50\u76EE\u5F55"),!1):!0}_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){e.createEl("h2",{text:i("wizard_step3")});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);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",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=ct.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",this._apiKeyStatus.className="paperforge-apikey-status error",this._apiKeyValidated=!1):(this._apiKeyStatus.textContent="\u9A8C\u8BC1\u5931\u8D25\uFF1AAPI \u8FD4\u56DE "+r.statusCode,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",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+")",this._apiKeyStatus.className="paperforge-apikey-status error",this._apiKeyValidated=!1}),a.write(s),a.end()}_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=[]}=B(t.vault_path.trim(),this.plugin.settings,void 0,void 0),S=(0,we.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=B(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 H.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(` `).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=[]}=B(p,this.plugin.settings,void 0,void 0);(0,Se.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 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=` +`).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=[]}=B(p,this.plugin.settings,void 0,void 0);(0,we.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 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,21 @@ .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($.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=B(a,this.plugin.settings,void 0,void 0),o=r.path,l=this.plugin.settings._python_path_stale?"stale":r.source,c=new P.Setting(e).setName(i("field_python_interp")).setDesc(this._getPythonDesc(o,l));this._pythonInterpDescEl=c.descEl;let p=new P.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=B(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 P.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=[]}=B(b,this.plugin.settings,void 0,void 0),k=this.plugin.manifest.version||"?";(0,V.execFile)(m,[...v,"-c","import paperforge; print(paperforge.__version__)"],{cwd:b,timeout:1e4},(C,F)=>{let R=this.plugin.settings.setup_complete,A=!C&&F?F.trim():null,N=A?`${i("runtime_health_plugin_ver").replace("{0}",k)} \u2192 ${i("runtime_health_package_ver").replace("{0}",A)}`: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),A===k?(d.setText(i("runtime_health_match")),d.className="paperforge-runtime-badge match",_&&_.setDisabled(!0)):A?(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 P.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 Pe(this.app,this.plugin).open()}):new Pe(this.app,this.plugin).open()})}),e.createEl("h3",{text:i("section_guide")});let E=e.createEl("div",{cls:"paperforge-guide"}),w=[["guide_open","guide_open_desc"],["guide_sync","guide_sync_desc"],["guide_ocr","guide_ocr_desc"]];for(let[b,m]of w){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,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 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,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,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=ne(r,this.plugin.settings);if(!o.path){new P.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 P.Notice(i("feat_memory_rebuild_done")):new P.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=B(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),Te(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=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 P.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=$.join(a,v);O.existsSync(k)?(0,V.exec)(`start "" "${k}"`):new P.Notice(`Skills folder not found: ${v}`)})});let o=$.join(a,n[r]),l=[],c=[];O.existsSync(o)&&O.readdirSync(o,{withFileTypes:!0}).forEach(m=>{if(!m.isDirectory())return;let v=$.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(` + `,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($.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=B(a,this.plugin.settings,void 0,void 0),o=r.path,l=this.plugin.settings._python_path_stale?"stale":r.source,c=new P.Setting(e).setName(i("field_python_interp")).setDesc(this._getPythonDesc(o,l));this._pythonInterpDescEl=c.descEl;let p=new P.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=B(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 P.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=[]}=B(b,this.plugin.settings,void 0,void 0),k=this.plugin.manifest.version||"?";(0,V.execFile)(m,[...v,"-c","import paperforge; print(paperforge.__version__)"],{cwd:b,timeout:1e4},(C,F)=>{let R=this.plugin.settings.setup_complete,A=!C&&F?F.trim():null,N=A?`${i("runtime_health_plugin_ver").replace("{0}",k)} \u2192 ${i("runtime_health_package_ver").replace("{0}",A)}`: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),A===k?(d.setText(i("runtime_health_match")),d.className="paperforge-runtime-badge match",_&&_.setDisabled(!0)):A?(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 P.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 Pe(this.app,this.plugin).open()}):new Pe(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,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 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,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,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=ne(r,this.plugin.settings);if(!o.path){new P.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 P.Notice(i("feat_memory_rebuild_done")):new P.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=B(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),Te(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=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 P.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=$.join(a,v);O.existsSync(k)?(0,V.exec)(`start "" "${k}"`):new P.Notice(`Skills folder not found: ${v}`)})});let o=$.join(a,n[r]),l=[],c=[];O.existsSync(o)&&O.readdirSync(o,{withFileTypes:!0}).forEach(m=>{if(!m.isDirectory())return;let v=$.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)),A="";if(R>=0){let K=F[R].match(/^description:\s*(.+)$/);if(K&&K[1]&&K[1]!==">"&&K[1]!=="|-"&&K[1]!=="|")A=K[1].trim();else{for(let W=R+1;W{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"}),A=F.createEl("span",{text:"\u25BC",cls:"paperforge-skills-arrow"});A.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 Z=L.name+(L.version?" v"+L.version:""),K=k?" [system]":" [user]",W=L.desc||"",oe=new P.Setting(R).setName(Z+K).setDesc(W);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",A.style.transform="rotate(-90deg)"),F.addEventListener("click",()=>{R.style.display!=="none"?(R.style.display="none",A.style.transform="rotate(-90deg)"):(R.style.display="",A.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 w=f.createEl("div",{cls:"paperforge-memory-status"});w.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(w,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 P.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 P.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 P.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 P.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 P.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=ne(n,this.plugin.settings);if(!a.path){new P.Notice(i("feat_no_python"));return}s.setButtonText(i("feat_installing")),s.setDisabled(!0);let r="chromadb openai",o=new P.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,V.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 P.Notice(i("feat_install_done")),this._vectorDepsOk=!0,this._embedStatusText=fe(n),this.display()}catch(l){o.hide(),new P.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{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,w=m=>{if(!ne(t,this.plugin.settings).path){new P.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 A=R.split(":");this.plugin._embedProgress.current=parseInt(A[1])||0,this.plugin._embedProgress.key=A[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 P.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 P.Notice(i("feat_build_complete"));else{this._embedStatusText=null;let F=(this.plugin._embedStderr||"").slice(0,200);new P.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",()=>w("--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",()=>w("--resume")),!E&&f){let m=r.createEl("button");m.setText(i("feat_vector_rebuild_force_btn")),m.style.marginLeft="6px",m.addEventListener("click",()=>w("--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=`\u2717 ${s}`),new P.Notice(s);return}if(!O.existsSync(e)){let s="\u8DEF\u5F84\u4E0D\u5B58\u5728 / Path does not exist";t&&(t.innerHTML=`\u2717 ${s}`),new P.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=`\u2717 ${n}`),new P.Notice(n,4e3);return}(0,V.execFile)(e,["--version"],{timeout:8e3},(s,n)=>{if(s||!n){let l="\u65E0\u6CD5\u8FD0\u884C / Cannot run";t&&(t.innerHTML=`\u2717 ${l}`),new P.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=`\u2717 ${l}`),new P.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=`\u2717 ${l}`),new P.Notice(l,4e3);return}(0,V.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=`\u26A0 ${c}`),new P.Notice(c,4e3)}else{let c=`\u2713 Python ${r}.${o} \u6709\u6548 / Valid`;t&&(t.innerHTML=`${c}`),new P.Notice(c,4e3)}})})}_syncRuntime(e){let t=this.app.vault.adapter.basePath,{path:s,extraArgs:n=[]}=B(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($.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,V.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 P.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 P.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 P.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=[]}=B(t,(a=this.plugin)==null?void 0:a.settings,void 0,void 0);(0,V.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||pt.homedir()||"";if(process.platform==="darwin")c=["/Applications/Zotero.app",$.join(p,"Applications","Zotero.app")].some(E=>{try{return O.existsSync(E)}catch(w){return!1}});else if(process.platform==="win32"){let f=process.env.ProgramFiles||"",E=process.env.LOCALAPPDATA||"";c=[$.join(f,"Zotero"),$.join(f,"(x86)","Zotero"),$.join(E,"Programs","Zotero"),$.join(E,"Zotero"),$.join(p,"AppData","Local","Programs","Zotero")].filter(Boolean).some(b=>{try{return O.existsSync(b)}catch(m){return!1}})}else c=[$.join(p,".local","share","zotero","zotero"),"/usr/bin/zotero","/usr/local/bin/zotero"].some(E=>{try{return O.existsSync(E)}catch(w){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($.join(_,"Zotero","Zotero","Profiles"))),!d&&process.platform==="darwin"&&p&&(d=Ce($.join(p,"Library","Application Support","Zotero","Profiles"))),!d&&process.platform!=="win32"&&process.platform!=="darwin"&&p&&(d=Ce($.join(p,".zotero","zotero","Profiles"))),!d&&u&&String(u).trim()&&(d=$e(u.trim())),!d&&p&&(d=$e($.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 P.Notice(`[!!] \u672A\u901A\u8FC7: ${x.map(f=>f.label).join(", ")}`,6e3),e()})}};var T=require("obsidian"),ae=j(require("fs")),te=j(require("path")),he=require("child_process");var _e=class extends T.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.paperforge,s=((r=t==null?void 0:t.manifest)==null?void 0:r.version)||"?",{path:n,extraArgs:a=[]}=B(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.paperforge,{path:n,extraArgs:a=[]}=B(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=ae.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,w=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++:w++}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:w},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=[]}=B(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.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=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 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}`}),o0){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 Ae,be,Ke,Ze,We,Ue,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 S of t)S.has_pdf&&n++,S.ocr_status==="done"&&a++,S.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 S of c){let D=l.createEl("div",{cls:"paperforge-snapshot-pill"});D.createEl("span",{cls:"paperforge-snapshot-value",text:String(S.value)}),D.createEl("span",{cls:"paperforge-snapshot-label",text:" "+S.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.paperforge,_=((Ae=d==null?void 0:d.manifest)==null?void 0:Ae.version)||"?",y=this._paperforgeVersion;if(!y)try{let S=this.app.vault.adapter.basePath,{path:D,extraArgs:G=[]}=B(S,(be=d==null?void 0:d.settings)!=null?be:null,void 0,void 0),z=(0,he.execFileSync)(D,[...G,"-c","import paperforge; print(paperforge.__version__)"],{cwd:S,timeout:5e3,encoding:"utf-8",windowsHide:!0}).trim();z&&(y=z.startsWith("v")?z:"v"+z,this._paperforgeVersion=y)}catch(S){}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 w=((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 S=te.join(b,w,"PaperForge","exports");if(ae.existsSync(S)){let D=ae.readdirSync(S).filter(G=>G.endsWith(".json"));m=D.length>0,v=m?D.length+" export(s)":"No JSON exports"}}catch(S){}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 S=((We=d==null?void 0:d.settings)==null?void 0:We.system_dir)||"System",D=te.join(b,S,"PaperForge",".env");if(ae.existsSync(D)){let z=ae.readFileSync(D,"utf-8").match(/^PADDLEOCR_API_TOKEN\s*=\s*(.+)$/m);k=!!(z&&z[1]&&z[1].trim())}}catch(S){}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,A=je(R);C=it(R),F=A&&((Ue=A.summary)==null?void 0:Ue.reason)||A&&((Je=A.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 S=e.createEl("div",{cls:"paperforge-issue-summary"});S.createEl("div",{cls:"paperforge-section-label",text:"\u9700\u8981\u5904\u7406"});let D=S.createEl("div",{cls:"paperforge-issue-list"});N&&D.createEl("div",{cls:"paperforge-issue-item",text:"Runtime version mismatch"}),E||D.createEl("div",{cls:"paperforge-issue-item",text:"Index missing or corrupted"}),m||D.createEl("div",{cls:"paperforge-issue-item",text:"No Zotero export found"}),k||D.createEl("div",{cls:"paperforge-issue-item",text:"PaddleOCR API key not configured"});let G=S.createEl("div",{cls:"paperforge-issue-actions"}),z=G.createEl("button",{cls:"paperforge-contextual-btn"});z.createEl("span",{text:"Run Doctor"}),z.addEventListener("click",()=>{let ve=U.find(Be=>Be.id==="paperforge-doctor");ve&&this._runAction(ve,z)});let ce=G.createEl("button",{cls:"paperforge-contextual-btn"});ce.createEl("span",{text:"Repair Issues"}),ce.addEventListener("click",()=>{let ve=U.find(Be=>Be.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 Z=L.createEl("div",{cls:"paperforge-global-actions-row"}),K=Z.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 S=((G=d==null?void 0:d.settings)==null?void 0:G.base_dir)||"Bases",D=this.app.vault.getAbstractFileByPath(S);if(D){let z=null;if(D.children&&(z=D.children.find(ce=>ce.extension==="base")),z){let ce=this.app.workspace.getLeaf(!1);ce&&ce.openFile(z)}else new T.Notice("[!!] No .base file found in "+S,6e3)}else new T.Notice("[!!] Base directory not found: "+S,6e3)});let W=Z.createEl("button",{cls:"paperforge-contextual-btn"});W.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u21BB"}),W.createEl("span",{text:"Sync Library"}),W.addEventListener("click",()=>{let S=U.find(D=>D.id==="paperforge-sync");S&&this._runAction(S,W)});let oe=Z.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 S=U.find(D=>D.id==="paperforge-ocr");S&&this._runAction(S,oe)});let ge=Z.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 S=U.find(D=>D.id==="paperforge-doctor");S&&this._runAction(S,ge)});let ye=Z.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=Z.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 S=U.find(D=>D.id==="paperforge-repair");S&&this._runAction(S,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 T.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='';let _=!1;u.addEventListener("click",()=>{r.setText(_?p:c),d.innerHTML=_?'':'',_=!_})}}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{R.style.display!=="none"?(R.style.display="none",A.style.transform="rotate(-90deg)"):(R.style.display="",A.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 P.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 P.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 P.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 P.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 P.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=ne(n,this.plugin.settings);if(!a.path){new P.Notice(i("feat_no_python"));return}s.setButtonText(i("feat_installing")),s.setDisabled(!0);let r="chromadb openai",o=new P.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,V.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 P.Notice(i("feat_install_done")),this._vectorDepsOk=!0,this._embedStatusText=fe(n),this.display()}catch(l){o.hide(),new P.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{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(!ne(t,this.plugin.settings).path){new P.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 A=R.split(":");this.plugin._embedProgress.current=parseInt(A[1])||0,this.plugin._embedProgress.key=A[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 P.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 P.Notice(i("feat_build_complete"));else{this._embedStatusText=null;let F=(this.plugin._embedStderr||"").slice(0,200);new P.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=`\u2717 ${s}`),new P.Notice(s);return}if(!O.existsSync(e)){let s="\u8DEF\u5F84\u4E0D\u5B58\u5728 / Path does not exist";t&&(t.innerHTML=`\u2717 ${s}`),new P.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=`\u2717 ${n}`),new P.Notice(n,4e3);return}(0,V.execFile)(e,["--version"],{timeout:8e3},(s,n)=>{if(s||!n){let l="\u65E0\u6CD5\u8FD0\u884C / Cannot run";t&&(t.innerHTML=`\u2717 ${l}`),new P.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=`\u2717 ${l}`),new P.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=`\u2717 ${l}`),new P.Notice(l,4e3);return}(0,V.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=`\u26A0 ${c}`),new P.Notice(c,4e3)}else{let c=`\u2713 Python ${r}.${o} \u6709\u6548 / Valid`;t&&(t.innerHTML=`${c}`),new P.Notice(c,4e3)}})})}_syncRuntime(e){let t=this.app.vault.adapter.basePath,{path:s,extraArgs:n=[]}=B(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($.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,V.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 P.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 P.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 P.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=[]}=B(t,(a=this.plugin)==null?void 0:a.settings,void 0,void 0);(0,V.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||pt.homedir()||"";if(process.platform==="darwin")c=["/Applications/Zotero.app",$.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=[$.join(f,"Zotero"),$.join(f,"(x86)","Zotero"),$.join(E,"Programs","Zotero"),$.join(E,"Zotero"),$.join(p,"AppData","Local","Programs","Zotero")].filter(Boolean).some(b=>{try{return O.existsSync(b)}catch(m){return!1}})}else c=[$.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($.join(_,"Zotero","Zotero","Profiles"))),!d&&process.platform==="darwin"&&p&&(d=Ce($.join(p,"Library","Application Support","Zotero","Profiles"))),!d&&process.platform!=="win32"&&process.platform!=="darwin"&&p&&(d=Ce($.join(p,".zotero","zotero","Profiles"))),!d&&u&&String(u).trim()&&(d=$e(u.trim())),!d&&p&&(d=$e($.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 P.Notice(`[!!] \u672A\u901A\u8FC7: ${x.map(f=>f.label).join(", ")}`,6e3),e()})}};var D=require("obsidian"),ae=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=[]}=B(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=[]}=B(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=ae.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=[]}=B(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=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 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}`}),o0){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 Ae,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 w of t)w.has_pdf&&n++,w.ocr_status==="done"&&a++,w.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 w of c){let T=l.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 u=p.createEl("div",{cls:"paperforge-status-grid"}),d=this.app.plugins.plugins.paperforge,_=((Ae=d==null?void 0:d.manifest)==null?void 0:Ae.version)||"?",y=this._paperforgeVersion;if(!y)try{let w=this.app.vault.adapter.basePath,{path:T,extraArgs:G=[]}=B(w,(be=d==null?void 0:d.settings)!=null?be:null,void 0,void 0),z=(0,he.execFileSync)(T,[...G,"-c","import paperforge; print(paperforge.__version__)"],{cwd:w,timeout:5e3,encoding:"utf-8",windowsHide:!0}).trim();z&&(y=z.startsWith("v")?z:"v"+z,this._paperforgeVersion=y)}catch(w){}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 w=te.join(b,S,"PaperForge","exports");if(ae.existsSync(w)){let T=ae.readdirSync(w).filter(G=>G.endsWith(".json"));m=T.length>0,v=m?T.length+" export(s)":"No JSON exports"}}catch(w){}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 w=((We=d==null?void 0:d.settings)==null?void 0:We.system_dir)||"System",T=te.join(b,w,"PaperForge",".env");if(ae.existsSync(T)){let z=ae.readFileSync(T,"utf-8").match(/^PADDLEOCR_API_TOKEN\s*=\s*(.+)$/m);k=!!(z&&z[1]&&z[1].trim())}}catch(w){}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,A=je(R);C=it(R),F=A&&((qe=A.summary)==null?void 0:qe.reason)||A&&((Je=A.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 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"});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=w.createEl("div",{cls:"paperforge-issue-actions"}),z=G.createEl("button",{cls:"paperforge-contextual-btn"});z.createEl("span",{text:"Run Doctor"}),z.addEventListener("click",()=>{let ve=q.find(Be=>Be.id==="paperforge-doctor");ve&&this._runAction(ve,z)});let ce=G.createEl("button",{cls:"paperforge-contextual-btn"});ce.createEl("span",{text:"Repair Issues"}),ce.addEventListener("click",()=>{let ve=q.find(Be=>Be.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 Z=L.createEl("div",{cls:"paperforge-global-actions-row"}),K=Z.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 w=((G=d==null?void 0:d.settings)==null?void 0:G.base_dir)||"Bases",T=this.app.vault.getAbstractFileByPath(w);if(T){let z=null;if(T.children&&(z=T.children.find(ce=>ce.extension==="base")),z){let ce=this.app.workspace.getLeaf(!1);ce&&ce.openFile(z)}else new D.Notice("[!!] No .base file found in "+w,6e3)}else new D.Notice("[!!] Base directory not found: "+w,6e3)});let W=Z.createEl("button",{cls:"paperforge-contextual-btn"});W.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u21BB"}),W.createEl("span",{text:"Sync Library"}),W.addEventListener("click",()=>{let w=q.find(T=>T.id==="paperforge-sync");w&&this._runAction(w,W)});let oe=Z.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 w=q.find(T=>T.id==="paperforge-ocr");w&&this._runAction(w,oe)});let ge=Z.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 w=q.find(T=>T.id==="paperforge-doctor");w&&this._runAction(w,ge)});let ye=Z.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=Z.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 w=q.find(T=>T.id==="paperforge-repair");w&&this._runAction(w,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='';let _=!1;u.addEventListener("click",()=>{r.setText(_?p:c),d.innerHTML=_?'':'',_=!_})}}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&&x300?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 T.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 T.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 T.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=U.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 T.Notice(x+" copied")}).catch(()=>{new T.Notice("[!!] Clipboard write failed",6e3)})});let u=((c=(l=this.app.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 T.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 T.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;b0){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 se=(N.count/C*100).toFixed(1);k.createEl("div",{cls:`paperforge-progress-seg ${N.cls}`,attr:{style:`width:${se}%`}})}let R=b.createEl("div",{cls:"paperforge-ocr-counts"}),A=[{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 A){let se=R.createEl("div",{cls:"paperforge-ocr-count"});se.createEl("div",{cls:"paperforge-ocr-count-value",text:N.value.toString()}),se.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=U.find(m=>m.id==="paperforge-ocr");b&&this._runAction(b,E)});let w=f.createEl("button",{cls:"paperforge-contextual-btn"});w.createEl("span",{cls:"paperforge-contextual-btn-icon",text:"\u21BB"}),w.createEl("span",{text:"Sync Library"}),w.addEventListener("click",()=>{let b=U.find(m=>m.id==="paperforge-sync");b&&this._runAction(b,w)})}_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 T.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 T.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 T.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 T.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=[]}=B(s,(_=(d=this.app.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(` +`)[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=q.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;b0){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 se=(N.count/C*100).toFixed(1);k.createEl("div",{cls:`paperforge-progress-seg ${N.cls}`,attr:{style:`width:${se}%`}})}let R=b.createEl("div",{cls:"paperforge-ocr-counts"}),A=[{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 A){let se=R.createEl("div",{cls:"paperforge-ocr-count"});se.createEl("div",{cls:"paperforge-ocr-count-value",text:N.value.toString()}),se.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=q.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=q.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=[]}=B(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"),new T.Notice(E,5e3))}}),l.stderr.on("data",y=>{let x=y.toString("utf-8").split(` +`),"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"),new T.Notice(E,5e3))}}),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 T.Notice("[WARN] "+e.cmd+" partial: "+f,8e3),this._fetchStats(!0)):(this._showMessage("[!!] "+f,"error"),new T.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 T.Notice("[OK] "+e.okMsg+" \u2014 "+f.length+" chars")}).catch(E=>{this._showMessage("[!!] Clipboard write failed: "+E.message,"error"),new T.Notice("[!!] Clipboard error",6e3)})}catch(E){this._showMessage("[!!] Invalid JSON from "+e.title,"error"),new T.Notice("[!!] "+e.title+" returned invalid JSON: "+E.message.slice(0,100),8e3)}else this._showMessage("[!!] No output from context command","error"),new T.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]||"",w=`${x}s \u2014 ${E}`;this._showMessage("[OK] "+e.title+": "+w,"ok"),new T.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.paperforge,s)}}),l.on("error",y=>{t.removeClass("running"),this._contentEl&&this._contentEl.removeClass("switching"),this._showMessage("[!!] "+y.message,"error"),new T.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 J.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,e=>new _e(e));try{(0,J.addIcon)(xe,Ge)}catch(e){}this.addRibbonIcon(xe,"PaperForge Dashboard",()=>_e.open(this)),this.addSettingTab(new De(this.app,this)),this.addCommand({id:"paperforge-status-panel",name:`PaperForge: ${i("guide_open")}`,callback:()=>_e.open(this)});for(let e of U)this.addCommand({id:e.id,name:`PaperForge: ${e.title}`,callback:()=>{if(e.disabled){new J.Notice(`[i] ${e.disabledMsg||"This action is not yet available."}`,6e3);return}let t=this.app.vault.adapter.basePath;new J.Notice(`PaperForge: running ${e.cmd}...`);let{path:s,extraArgs:n=[]}=B(t,this.settings,void 0,void 0);(0,ie.execFile)(s,[...n,"-m","paperforge",e.cmd],{cwd:t,timeout:3e5},(a,r,o)=>{if(a){new J.Notice(`[!!] ${e.cmd} failed: ${(o||a.message).slice(0,120)}`,8e3);return}new J.Notice(`[OK] ${e.okMsg||r.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=B(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,ie.execFile)(n.path,o,{cwd:e,timeout:6e4,windowsHide:!0},()=>{})})}}_autoUpdate(){let e=this.app.vault.adapter.basePath,{path:t,extraArgs:s=[]}=B(e,this.settings,void 0,void 0),n=this.manifest.version,a=`paperforge==${n}`,r=`git+https://github.com/LLLin000/PaperForge.git@v${n}`,o=(l,c)=>{(0,ie.spawn)(t,[...s,"-m","pip","install","--upgrade",l],{cwd:e,timeout:12e4,env:de()}).on("close",u=>c(u===0))};(0,ie.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 J.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 J.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=B(e,this.settings,void 0,void 0);if(!t.path){this._autoSyncRunning=!1;return}let s=`"${t.path}" -m paperforge --vault "${e}" sync`;(0,ie.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=B(e,this.settings,void 0,void 0);if(!o.path){this._autoSyncRunning=!1;return}let l=`"${o.path}" -m paperforge --vault "${e}" sync`;(0,ie.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 J.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)}}; +`),"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 J.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,e=>new _e(e));try{(0,J.addIcon)(xe,Ge)}catch(e){}this.addRibbonIcon(xe,"PaperForge Dashboard",()=>_e.open(this)),this.addSettingTab(new De(this.app,this)),this.addCommand({id:"paperforge-status-panel",name:`PaperForge: ${i("guide_open")}`,callback:()=>_e.open(this)});for(let e of q)this.addCommand({id:e.id,name:`PaperForge: ${e.title}`,callback:()=>{if(e.disabled){new J.Notice(`[i] ${e.disabledMsg||"This action is not yet available."}`,6e3);return}let t=this.app.vault.adapter.basePath;new J.Notice(`PaperForge: running ${e.cmd}...`);let{path:s,extraArgs:n=[]}=B(t,this.settings,void 0,void 0);(0,ie.execFile)(s,[...n,"-m","paperforge",e.cmd],{cwd:t,timeout:3e5},(a,r,o)=>{if(a){new J.Notice(`[!!] ${e.cmd} failed: ${(o||a.message).slice(0,120)}`,8e3);return}new J.Notice(`[OK] ${e.okMsg||r.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=B(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,ie.execFile)(n.path,o,{cwd:e,timeout:6e4,windowsHide:!0},()=>{})})}}_autoUpdate(){let e=this.app.vault.adapter.basePath,{path:t,extraArgs:s=[]}=B(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,ie.spawn)(t,[...s,"-m","pip","install","--upgrade",l],{cwd:e,timeout:12e4,env:de()}).on("close",u=>c(u===0))};(0,ie.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 J.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 J.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=B(e,this.settings,void 0,void 0);if(!t.path){this._autoSyncRunning=!1;return}let s=`"${t.path}" -m paperforge --vault "${e}" sync`;(0,ie.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=B(e,this.settings,void 0,void 0);if(!o.path){this._autoSyncRunning=!1;return}let l=`"${o.path}" -m paperforge --vault "${e}" sync`;(0,ie.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 J.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)}}; diff --git a/paperforge/plugin/src/i18n.ts b/paperforge/plugin/src/i18n.ts index dbfac90b..4e619095 100644 --- a/paperforge/plugin/src/i18n.ts +++ b/paperforge/plugin/src/i18n.ts @@ -66,7 +66,7 @@ const LANG: Record> = { 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 all literature metadata (papers, assets, aliases, reading events), provides FTS5 full-text search across titles/abstracts/authors/collections, and enables the agent-context and paper-status commands. Always active — no toggle needed.', + 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 — no toggle needed.', feat_memory_rebuild_btn: 'Rebuild', feat_memory_rebuild_done: 'Memory DB rebuilt.', feat_memory_rebuild_failed: 'Rebuild failed.', @@ -157,7 +157,7 @@ const LANG: Record> = { 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', + 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}', @@ -261,7 +261,7 @@ const LANG: Record> = { feat_installing: '安装中…', feat_installing_pkgs: '正在安装 {pkgs}...', feat_key_rejected: 'API Key 被拒绝。', - feat_memory_desc: '记忆层是 PaperForge 的核心数据引擎,基于 SQLite 构建。它整合了所有文献元数据(论文、资源文件、别名、阅读事件),支持 FTS5 全文检索(可搜索标题、摘要、作者、分类),并为 agent-context 和 paper-status 命令提供数据支撑。始终运行,无需手动开启。', + feat_memory_desc: '记忆层是 PaperForge 的核心数据引擎,基于 SQLite 构建。它整合了文献元数据(论文、资源文件、别名、阅读事件),支持 FTS5 元数据检索(标题、摘要、作者、domain、collection),并为 agent-context 和 paper-status 命令提供数据支撑。始终运行,无需手动开启。', feat_memory_rebuild_btn: '重建数据库', feat_memory_rebuild_done: '记忆数据库重建完成。', feat_memory_rebuild_failed: '重建失败。', @@ -353,7 +353,7 @@ const LANG: Record> = { run_in_agent: '在 {0} 中运行', runtime_health: '运行时状态', runtime_health_checking: '正在检测…', - runtime_health_desc: '检查插件与 Python 运行时版本的匹配情况', + runtime_health_desc: '检查插件与 Python 运行时版本的匹配情况,并确认已部署的 skill contract 是否为当前版本。', runtime_health_match: '匹配', runtime_health_mismatch: '不匹配', runtime_health_package_ver: 'Python 包 v{0}', diff --git a/paperforge/plugin/src/services/python-bridge.ts b/paperforge/plugin/src/services/python-bridge.ts index de7ad81a..da72f152 100644 --- a/paperforge/plugin/src/services/python-bridge.ts +++ b/paperforge/plugin/src/services/python-bridge.ts @@ -44,6 +44,14 @@ export interface RuntimeStatus { action?: string; } +export interface QueryPlanResult { + ok: boolean; + command: string; + version: string; + data: Record | null; + error: Record | null; +} + // ── Cross-platform state ── let _gitDir: string | null = null; @@ -214,6 +222,44 @@ export function runSubprocess(pythonExe: string, args: string[], cwd: string, ti }); } +export function runQueryPlan( + pythonExe: string, + extraArgs: string[], + vaultPath: string, + query: string, + intent: "discover" | "content" | "known-paper", + timeout = 20000, + _execFile?: any, +): Promise { + const exe = _execFile || execFile; + return new Promise((resolve) => { + const args = [...extraArgs, "-m", "paperforge", "--vault", vaultPath, "query-plan", query, "--intent", intent, "--json"]; + exe(pythonExe, args, { cwd: vaultPath, timeout, windowsHide: true }, (err: any, stdout: any, stderr: any) => { + if (err) { + resolve({ + ok: false, + command: "query-plan", + version: "", + data: null, + error: { message: stderr || err.message || "query-plan failed" }, + }); + return; + } + try { + resolve(JSON.parse(stdout)); + } catch (parseErr: any) { + resolve({ + ok: false, + command: "query-plan", + version: "", + data: null, + error: { message: parseErr.message || "Invalid query-plan JSON" }, + }); + } + }); + }); +} + // ── Cross-platform Python and BBT detection (macOS/Linux) ── export function resolveGitDir(): string | null { diff --git a/paperforge/query_planning.py b/paperforge/query_planning.py new file mode 100644 index 00000000..81d2d3d8 --- /dev/null +++ b/paperforge/query_planning.py @@ -0,0 +1,350 @@ +from __future__ import annotations + +import re +from dataclasses import dataclass +from pathlib import Path + + +HIGH_SIGNAL_PRIORITY = [ + "doi", + "zotero_key", + "citation_key", + "author", + "year", + "title", + "topic", +] + + +@dataclass +class QuerySignals: + doi: str | None + zotero_key: str | None + citation_key: str | None + author_tokens: list[str] + year_tokens: list[int] + title_like_tokens: list[str] + content_terms: list[str] + collection_hint: str | None = None + domain_hint: str | None = None + + +def _dedupe_keep_order(items: list[str]) -> list[str]: + seen: set[str] = set() + out: list[str] = [] + for item in items: + key = item.lower() + if key in seen: + continue + seen.add(key) + out.append(item) + return out + + +def detect_doi(text: str) -> str | None: + match = re.search(r"(10\.\d{4,9}/[-._;()/:A-Z0-9]+)", text, re.IGNORECASE) + return match.group(1) if match else None + + +def detect_zotero_key(text: str) -> str | None: + if re.fullmatch(r"[A-Z0-9]{8}", text.strip().upper()): + return text.strip().upper() + return None + + +def detect_citation_key(text: str) -> str | None: + candidate = text.strip() + if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_-]{5,}", candidate): + return None + if not any(ch.isdigit() for ch in candidate) and "-" not in candidate and "_" not in candidate: + return None + return candidate + + +def extract_years(text: str) -> list[int]: + years = [] + for raw in re.findall(r"\b((?:19|20)\d{2})\b", text): + years.append(int(raw)) + return years + + +def tokenize(text: str) -> list[str]: + return re.findall(r"[A-Za-z][A-Za-z0-9.+-]*|[\u4e00-\u9fff]{2,}", text) + + +def classify_signals(query: str) -> QuerySignals: + doi = detect_doi(query) + zotero_key = detect_zotero_key(query) + citation_key = None if (doi or zotero_key) else detect_citation_key(query) + year_tokens = extract_years(query) + + raw_tokens = tokenize(query) + author_tokens: list[str] = [] + title_like_tokens: list[str] = [] + content_terms: list[str] = [] + + lowered = [t.lower() for t in raw_tokens] + content_keywords = { + "hz", "v/cm", "v", "ma", "mv", "galvanotaxis", "electrotaxis", + "dc", "ac", "cc", "pemf", "field", "scaffold", "stimulation", + "migration", "chondrocyte", "cartilage", "method", "parameter", + } + + for token in raw_tokens: + if any(ch.isdigit() for ch in token): + content_terms.append(token) + continue + low = token.lower() + if low in {"doi", "pmid", "title", "paper", "article"}: + continue + if low in content_keywords: + content_terms.append(token) + title_like_tokens.append(token) + continue + if token[0].isupper() and len(token) >= 3 and not year_tokens: + author_tokens.append(token) + continue + if token[0].isupper() and len(token) >= 3 and year_tokens and not author_tokens: + author_tokens.append(token) + continue + title_like_tokens.append(token) + + if not author_tokens and year_tokens: + author_tokens = [t for t in raw_tokens if t.lower() not in content_keywords and len(t) >= 3][:1] + + return QuerySignals( + doi=doi, + zotero_key=zotero_key, + citation_key=citation_key, + author_tokens=_dedupe_keep_order(author_tokens), + year_tokens=year_tokens, + title_like_tokens=_dedupe_keep_order(title_like_tokens), + content_terms=_dedupe_keep_order(content_terms), + ) + + +def build_query_plan(query: str, intent: str) -> dict: + signals = classify_signals(query) + query_class = "metadata_topic" + + if signals.doi or signals.zotero_key or signals.citation_key: + query_class = "identifier_exact" + elif signals.author_tokens and signals.year_tokens: + query_class = "author_year" if len(signals.title_like_tokens) == 0 else "mixed_query" + elif intent == "content": + query_class = "content_term" + elif signals.content_terms and not signals.author_tokens: + query_class = "metadata_topic" + + query_rules: list[str] = [] + fallback_plan: list[dict] = [] + + if query_class == "identifier_exact": + identifier = signals.doi or signals.zotero_key or signals.citation_key or query.strip() + primary = {"command": "paper-context", "args": {"key": identifier}} + query_rules.append("Use exact identifiers directly with paper-context; do not rewrite them.") + fallback_plan.append({"when": "not_found", "action": "fallback_to_paper_status"}) + elif intent == "content": + content_query = " ".join(signals.content_terms or signals.title_like_tokens or tokenize(query)[:6]).strip() or query.strip() + primary = {"command": "retrieve", "args": {"query": content_query, "limit": 30}} + query_rules.extend([ + "For content lookup, start with retrieve rather than metadata search.", + "Use content-bearing terms, parameters, and method phrases as they would appear in fulltext.", + ]) + fallback_plan.extend([ + {"when": "retrieve_unavailable", "action": "interactive_fulltext_fallback"}, + {"when": "zero_results", "action": "interactive_fulltext_fallback"}, + ]) + elif signals.author_tokens and signals.year_tokens: + primary = { + "command": "search", + "args": { + "query": signals.author_tokens[0], + "year_from": min(signals.year_tokens), + "year_to": max(signals.year_tokens), + "limit": 10, + }, + } + query_rules.extend([ + "For metadata search, prefer author and year over mixed natural-language query strings.", + "When author and year are known, do not include title words in the first-pass search query.", + ]) + fallback_plan.extend([ + {"when": "zero_results", "action": "report_noncanonical_query_risk"}, + {"when": "multiple_results", "action": "visually_narrow_with_title_terms"}, + ]) + else: + topic_query = " ".join(signals.title_like_tokens or tokenize(query)[:6]).strip() or query.strip() + primary = {"command": "search", "args": {"query": topic_query, "limit": 30}} + query_rules.extend([ + "Use short metadata-facing terms for search: title keywords, author names, domain, or collection.", + "Do not treat search as semantic fulltext discovery.", + ]) + fallback_plan.extend([ + {"when": "zero_results", "action": "report_metadata_miss_not_library_absence"}, + {"when": "large_result_set", "action": "narrow_by_domain_year_or_author"}, + ]) + + return { + "intent": intent, + "query_class": query_class, + "signals": { + "doi": signals.doi, + "zotero_key": signals.zotero_key, + "citation_key": signals.citation_key, + "author_tokens": signals.author_tokens, + "year_tokens": signals.year_tokens, + "title_like_tokens": signals.title_like_tokens, + "content_terms": signals.content_terms, + }, + "signal_priority": HIGH_SIGNAL_PRIORITY, + "recommended_primary": primary, + "query_writing_rules": query_rules, + "fallback_plan": fallback_plan, + } + + +def enrich_query_plan_with_runtime(plan: dict, vault: Path) -> dict: + from paperforge.embedding import get_embed_status + from paperforge.worker.asset_index import read_index + + embed = get_embed_status(vault) + retrieve_available = bool(embed.get("healthy", True) and embed.get("db_exists") and embed.get("chunk_count", 0) > 0) + plan["runtime"] = { + "retrieve_available": retrieve_available, + "vector_status": { + "db_exists": embed.get("db_exists", False), + "chunk_count": embed.get("chunk_count", 0), + "healthy": embed.get("healthy", True), + "error": embed.get("error", ""), + }, + } + + data = read_index(vault) + items = [] + if isinstance(data, dict): + items = data.get("items", []) + elif isinstance(data, list): + items = data + + scope = _assess_scope(items, plan["signals"]) + plan["scope_assessment"] = scope + + if plan["intent"] == "discover" and scope["source"] in {"domain", "collection"}: + if scope["source"] == "domain": + plan["recommended_primary"] = { + "command": "context", + "args": {"domain": scope["label"]}, + } + plan["query_writing_rules"].append("When the user names a known domain, prefer context --domain over metadata search.") + else: + plan["recommended_primary"] = { + "command": "context", + "args": {"collection": scope["label"]}, + } + plan["query_writing_rules"].append("When the user names a known collection, prefer context --collection over metadata search.") + plan["fallback_plan"].insert(0, {"when": "large_inventory", "action": "summarize_and_offer_narrowing"}) + + if plan["intent"] == "content": + plan["suggested_modes"] = _suggest_content_fallback_modes(scope) + if plan["intent"] == "content" and not retrieve_available: + plan["interactive_fallback_required"] = True + elif plan["intent"] == "content": + plan["interactive_fallback_required"] = False + + return plan + + +def _assess_scope(items: list[dict], signals: dict) -> dict: + matched_items = items + source = "library" + label = "full library" + + title_terms = [str(t) for t in signals.get("title_like_tokens", [])] + content_terms = [str(t) for t in signals.get("content_terms", [])] + query_terms = title_terms + content_terms + + domain_counts: dict[str, int] = {} + collection_counts: dict[str, int] = {} + for entry in items: + domain = entry.get("domain", "") + if domain: + domain_counts[domain] = domain_counts.get(domain, 0) + 1 + for collection in entry.get("collections", []) or []: + if isinstance(collection, str) and collection: + collection_counts[collection] = collection_counts.get(collection, 0) + 1 + + normalized_terms = [str(t).strip().lower() for t in query_terms if str(t).strip()] + matched_domain = next( + ( + d for d in domain_counts + if any(str(d).strip().lower() == token or token in str(d).strip().lower() for token in normalized_terms) + ), + None, + ) + if matched_domain: + matched_items = [entry for entry in items if entry.get("domain") == matched_domain] + source = "domain" + label = matched_domain + else: + matched_collection = next( + ( + c for c in collection_counts + if any( + str(c).strip().lower() == token + or token in str(c).strip().lower() + or any(part.strip().lower() == token for part in str(c).split("/")) + for token in normalized_terms + ) + ), + None, + ) + if matched_collection: + matched_items = [ + entry for entry in items + if any(isinstance(col, str) and col.startswith(matched_collection) for col in entry.get("collections", []) or []) + ] + source = "collection" + label = matched_collection + + estimated = len(matched_items) + fulltext_ready = sum(1 for entry in matched_items if entry.get("lifecycle") in {"fulltext_ready", "deep_read_done", "ai_context_ready"}) + + if estimated <= 20: + recommended_mode = "rg_now" + elif estimated <= 60: + recommended_mode = "rg_with_warning" + else: + recommended_mode = "ask_user_before_broad_grep" + + return { + "source": source, + "label": label, + "estimated_paper_count": estimated, + "fulltext_ready_count": fulltext_ready, + "recommended_mode": recommended_mode, + } + + +def _suggest_content_fallback_modes(scope: dict) -> list[dict]: + source = scope.get("source", "library") + label = scope.get("label", "current scope") + modes = [ + { + "mode": "fulltext_rg_or_grep", + "description": f"Run literal fulltext search within {label} ({source}) for exact verification.", + }, + { + "mode": "metadata_narrow_then_fulltext", + "description": "Use metadata search to narrow candidate papers first, then run fulltext verification.", + }, + ] + if scope.get("recommended_mode") == "ask_user_before_broad_grep": + modes.insert( + 0, + { + "mode": "ask_for_scope_limit", + "description": "Ask the user to limit collection, domain, or year before broad fulltext search.", + }, + ) + return modes diff --git a/paperforge/services/sync_service.py b/paperforge/services/sync_service.py index 71f7e5de..6fe49705 100644 --- a/paperforge/services/sync_service.py +++ b/paperforge/services/sync_service.py @@ -225,6 +225,7 @@ class SyncService: _export_code = "BBT_EXPORT_INVALID" selection_result: dict = {"new": 0, "updated": 0, "skipped": 0, "failed": 0, "errors": []} + _prune_preview: list[dict] = [] # ── Phase 1: Select ── if not index_only: @@ -319,9 +320,6 @@ class SyncService: is_ok = selection_result.get("failed", 0) == 0 and not selection_result.get("errors") - if not selection_only: - _prune_preview = locals().get("_prune_preview", []) - result = PFResult( ok=is_ok, command="sync", diff --git a/paperforge/skills/paperforge/SKILL.md b/paperforge/skills/paperforge/SKILL.md index d26e759c..8ce13475 100644 --- a/paperforge/skills/paperforge/SKILL.md +++ b/paperforge/skills/paperforge/SKILL.md @@ -8,7 +8,8 @@ description: > "讨论" "记录阅读" "记录工作" "总结会话" "提取方法论" "记一下" "保存这次" "找证据" "找75 Hz" "找支持" "collection" "库里". source: paperforge -version: 1.5.9 +skill_version: 2026-05-27.1 +skill_api_version: 2 --- # PaperForge — Research Memory Runtime @@ -16,6 +17,14 @@ version: 1.5.9 PaperForge 将文献、阅读痕迹、工作过程、方法论和产物 组织成可检索、可复核、可由 agent 调用的研究记忆。 +**触发原则:只要用户意图涉及以下任一类,必须调用 paperforge skill:** + +- 文献库 / vault 中的论文检索、collection / domain 盘点 +- 已知论文的定位、阅读、问答、/pf-paper +- 正文内容、术语、参数、方法、支持证据的查找 +- 单篇论文精读、/pf-deep +- PaperForge 阅读笔记、项目日志、方法论卡片保存 + --- ## 1. Bootstrap — 必须先执行 @@ -46,7 +55,7 @@ bootstrap 现在也返回一个 `capabilities` 块(rg, semantic, metadata 等 **进入任何 molecule 之前,必须完成以下检查。标记为 `[x]` 后才可前进。** - [ ] **bootstrap completed**:`$VAULT`、`$PYTHON`、`$LIT_DIR`、`$SKILL_DIR` 已定义 -- [ ] **version match**:`$PYTHON -m paperforge --version` 输出的版本号与 SKILL.md frontmatter 的 `version` 字段一致;不一致时提示用户运行 `paperforge update` +- [ ] **skill contract**:bootstrap 已返回 `plugin_version`、`skill_version`、`skill_api_version`;若 `skill_api_version` 不兼容,提示用户运行 `paperforge update` - [ ] **capabilities**:已读取 `capabilities` 块中的 `rg`、`metadata_search`、`paper_context`、`semantic_enabled`、`semantic_ready` - [ ] **runtime-health passed**:`safe_read`、`safe_write` 已知,vector 状态已知 - [ ] **intent identified**:用户意图已映射到一个顶层 research intent diff --git a/paperforge/skills/paperforge/molecules/discover-papers.md b/paperforge/skills/paperforge/molecules/discover-papers.md index 5381a485..7643308f 100644 --- a/paperforge/skills/paperforge/molecules/discover-papers.md +++ b/paperforge/skills/paperforge/molecules/discover-papers.md @@ -24,6 +24,15 @@ - **范围**:domain(如"骨科")、不指定=全库 - **过滤条件**:OCR 状态、年份范围(`--year-from`/`--year-to`)、lifecycle +**先调用 query-plan:** + +```bash +$PYTHON -m paperforge --vault "$VAULT" query-plan "" --intent discover --json +``` + +执行 `recommended_primary.command` 与其 `args`。不要把作者、年份、标题词、主题词混成一条 search query。 +如果 `query-plan` 判断是 `author + year` 或 `mixed_query`,第一轮只用作者 + 年份。 + ### Step 2: 多臂搜索策略 打开 `atoms/retrieval-routing.md` 参照 Ladder A。 @@ -69,6 +78,7 @@ $PYTHON -m paperforge --vault "$VAULT" search "" --json --limit 30 \ - FTS5 搜索标题、摘要、作者、期刊、domain、collection 路径 - 返回 JSON:`data.matches[]` 包含 `zotero_key`、`title`、`year`、`first_author` 等 - **作者+年份搜索**:query 放作者名,年份放 `--year-from`/`--year-to`(如 `search "Brighton" --year-from 1983 --year-to 1983`) +- 不要把标题词塞进第一轮 author+year 查询;标题词只用于人工二次缩小 #### Arm 3 — Collection/Domain 列举(完整列表,不截断) @@ -103,6 +113,12 @@ $PYTHON -m paperforge --vault "$VAULT" context --domain "" --json 然后只运行 Arm 2(或 Arm 3 如果是 collection/domain 查询)。 +#### 当 `search` 0 结果时 + +- 不要把 raw 0 结果直接解释成“库里没有” +- 先检查 query-plan 是否已给出规范化写法 +- 只有规范化后的第一轮也 0 结果时,才能报告“未找到匹配” + ### Step 3: Top-hit 富化(`paperforge paper-context`) 对候选列表中的 **前 10 篇**(如果超过 10 篇,只富化前 10),调 `paper-context` 获取详细状态: diff --git a/paperforge/skills/paperforge/molecules/find-supporting-evidence.md b/paperforge/skills/paperforge/molecules/find-supporting-evidence.md index 83f8a8c9..60e082d9 100644 --- a/paperforge/skills/paperforge/molecules/find-supporting-evidence.md +++ b/paperforge/skills/paperforge/molecules/find-supporting-evidence.md @@ -25,6 +25,14 @@ - **范围限制**:是否限定特定 domain、作者、年份 - **证据类型**:统计结果、方法引用、临床发现、机制解释等 +**先调用 query-plan:** + +```bash +$PYTHON -m paperforge --vault "$VAULT" query-plan "" --intent content --json +``` + +如果 `query-plan` 推荐 `retrieve`,直接从 `retrieve` 开始,不要先走一遍 paper discovery。 + ### Step 2: 检索梯级选择(`atoms/retrieval-routing.md`) **先检查 `retrieve` 是否可用:** @@ -44,7 +52,9 @@ $PYTHON -m paperforge --vault "$VAULT" embed status --json - 如需精确定位验证,再用 `rg` / `grep` 在论文全文中确认 2. **Ladder B2**(备选,无 `retrieve` 但有 `rg`)-- 元数据→全文 grep - - 先用 `paperforge search` 生成元数据候选集 + - 默认不要静默降级到 metadata search + - 先根据 query-plan 返回向用户解释:retrieve 不可用 / 0 结果,需要选择 fallback 模式 + - 如果用户同意 metadata 缩一轮,再用 `paperforge search` 生成候选集 - 用 `runtime-health` 或 `paper-context` 筛选有 OCR/全文的论文 - 在解析后的全文中运行 `rg` 定位匹配片段 diff --git a/paperforge/skills/paperforge/molecules/read-known-paper.md b/paperforge/skills/paperforge/molecules/read-known-paper.md index e307b113..aa0f7120 100644 --- a/paperforge/skills/paperforge/molecules/read-known-paper.md +++ b/paperforge/skills/paperforge/molecules/read-known-paper.md @@ -27,16 +27,28 @@ 用户可能给 zotero_key、DOI、标题关键词、作者+年份。按以下方式查找: +**先调用 query-plan,不要自己发明第一条 query:** + +```bash +$PYTHON -m paperforge --vault "$VAULT" query-plan "" --intent known-paper --json +``` + +执行 `recommended_primary.command` 与其 `args`。规则: + +- `doi` / `zotero_key` / `citation_key` → 直接 `paper-context` +- `author + year` → 第一轮只跑作者 + 年份过滤,不加标题词 +- 模糊标题 / 主题词 → `search` 或 `paper-status` + **如果知道 zotero_key:** ```bash $PYTHON -m paperforge --vault "$VAULT" paper-context --json ``` -**如果知道 DOI:** +**如果知道 DOI / citation_key:** ```bash -$PYTHON -m paperforge --vault "$VAULT" paper-context --json +$PYTHON -m paperforge --vault "$VAULT" paper-context --json ``` **如果知道 作者+年份:** @@ -65,7 +77,7 @@ $PYTHON -m paperforge --vault "$VAULT" search "" --json --limit 5 无论哪种方式命中,取 `zotero_key` 调 `paper-context` 获取完整信息。 如果多候选,列出让用户选。 -如果无结果,告知用户并停止。 +如果无结果,不要直接说“库里没有”;先说明这是规范化后的无结果,再告知用户未找到。 ### Step 2: 加载文献内容 diff --git a/paperforge/skills/paperforge/scripts/pf_bootstrap.py b/paperforge/skills/paperforge/scripts/pf_bootstrap.py index 85dec80e..211b942d 100644 --- a/paperforge/skills/paperforge/scripts/pf_bootstrap.py +++ b/paperforge/skills/paperforge/scripts/pf_bootstrap.py @@ -31,15 +31,65 @@ If anything fails: ok=false, error explains why. from __future__ import annotations import json +import re import subprocess import sys from pathlib import Path -try: - import paperforge - SKILL_VERSION = paperforge.__version__ -except Exception: - SKILL_VERSION = "unknown" +REPO_ROOT = Path(__file__).resolve().parents[4] +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + + +def _read_plugin_version() -> str: + init_py = REPO_ROOT / "paperforge" / "__init__.py" + if not init_py.exists(): + return "unknown" + try: + text = init_py.read_text(encoding="utf-8") + except Exception: + return "unknown" + match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', text) + return match.group(1) if match else "unknown" + + +PLUGIN_VERSION = _read_plugin_version() + + +def _parse_skill_metadata() -> tuple[str, int | None]: + skill_md = Path(__file__).resolve().parent.parent / "SKILL.md" + if not skill_md.exists(): + return ("unknown", None) + try: + text = skill_md.read_text(encoding="utf-8") + except Exception: + return ("unknown", None) + + if not text.startswith("---"): + return ("unknown", None) + lines = text.splitlines() + skill_version = "unknown" + skill_api_version: int | None = None + in_frontmatter = False + for line in lines: + if line.strip() == "---": + if not in_frontmatter: + in_frontmatter = True + continue + break + if not in_frontmatter or ":" not in line: + continue + key, value = line.split(":", 1) + key = key.strip() + value = value.strip().strip('"').strip("'") + if key == "skill_version": + skill_version = value + elif key == "skill_api_version": + try: + skill_api_version = int(value) + except ValueError: + pass + return (skill_version, skill_api_version) def _find_paperforge_json(start: Path) -> Path | None: @@ -164,6 +214,7 @@ def main() -> None: args = p.parse_args() result: dict = {"ok": False} + skill_version, skill_api_version = _parse_skill_metadata() # --- 1. Find vault --- if args.vault: @@ -294,7 +345,9 @@ def main() -> None: } result["ok"] = True - result["skill_version"] = SKILL_VERSION + result["plugin_version"] = PLUGIN_VERSION + result["skill_version"] = skill_version + result["skill_api_version"] = skill_api_version json.dump(result, sys.stdout, ensure_ascii=False, indent=2) diff --git a/tests/test_command_docs.py b/tests/test_command_docs.py index 6fab0bca..11123b25 100644 --- a/tests/test_command_docs.py +++ b/tests/test_command_docs.py @@ -61,7 +61,7 @@ def code_block_lines(content: str) -> list[str]: @pytest.fixture def command_docs() -> dict[str, Path]: - base = REPO_ROOT / "command" + base = REPO_ROOT / "paperforge" / "command_files" return { "pf-status": base / "pf-status.md", "pf-sync": base / "pf-sync.md", diff --git a/tests/test_literature_hub_regressions.py b/tests/test_literature_hub_regressions.py new file mode 100644 index 00000000..3ea42658 --- /dev/null +++ b/tests/test_literature_hub_regressions.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + + +VAULT = Path(r"D:\L\OB\Literature-hub") + + +pytestmark = pytest.mark.skipif(not VAULT.exists(), reason="Local Literature-hub vault not available") + + +def _run_json(*args: str) -> dict: + result = subprocess.run( + [sys.executable, "-m", "paperforge", "--vault", str(VAULT), *args], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=30, + check=True, + ) + return json.loads(result.stdout) + + +def test_mixed_author_year_query_returns_search_diagnostic() -> None: + payload = _run_json("search", "Lin 2024 Electrical Stimulation", "--json", "--limit", "10") + assert payload["ok"] is True + assert payload["data"]["count"] == 0 + diagnostic = payload["data"]["query_diagnostic"] + assert diagnostic["query_class"] == "mixed_query" + assert diagnostic["recommended_primary"]["args"]["query"] == "Lin" + assert diagnostic["recommended_primary"]["args"]["year_from"] == 2024 + + +def test_content_query_prefers_retrieve() -> None: + payload = _run_json("query-plan", "galvanotaxis", "--intent", "content", "--json") + assert payload["ok"] is True + assert payload["data"]["recommended_primary"]["command"] == "retrieve" + + +def test_domain_inventory_query_prefers_context() -> None: + payload = _run_json("query-plan", "骨科 里有什么", "--intent", "discover", "--json") + assert payload["ok"] is True + assert payload["data"]["recommended_primary"]["command"] == "context" + assert payload["data"]["recommended_primary"]["args"]["domain"] == "骨科" + + +def test_doi_paper_context_resolves_known_paper() -> None: + payload = _run_json("paper-context", "10.1016/j.heliyon.2024.e38112", "--json") + assert payload["ok"] is True + assert payload["data"]["paper"]["zotero_key"] == "L6ALWJFP" diff --git a/tests/test_pf_bootstrap_capabilities.py b/tests/test_pf_bootstrap_capabilities.py index c3a165bc..f9b87234 100644 --- a/tests/test_pf_bootstrap_capabilities.py +++ b/tests/test_pf_bootstrap_capabilities.py @@ -76,7 +76,13 @@ def test_bootstrap_capabilities_contract(tmp_path: Path) -> None: assert "semantic_ready" in caps, "capabilities.semantic_ready missing" assert isinstance(caps["semantic_ready"], bool), "capabilities.semantic_ready must be bool" - # Skill version + pv = output.get("plugin_version") + assert pv is not None, "plugin_version missing from bootstrap output" + assert isinstance(pv, str) and pv, f"plugin_version invalid: {pv}" + sv = output.get("skill_version") assert sv is not None, "skill_version missing from bootstrap output" - assert isinstance(sv, str) and sv != "unknown", f"skill_version invalid: {sv}" + assert isinstance(sv, str) and sv, f"skill_version invalid: {sv}" + + sav = output.get("skill_api_version") + assert sav is None or isinstance(sav, int), f"skill_api_version invalid: {sav}" diff --git a/tests/test_query_command_diagnostics.py b/tests/test_query_command_diagnostics.py new file mode 100644 index 00000000..f7bb4795 --- /dev/null +++ b/tests/test_query_command_diagnostics.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from paperforge.commands import retrieve as retrieve_command +from paperforge.commands import search as search_command + + +class _Args: + def __init__(self, vault_path: Path) -> None: + self.vault_path = vault_path + self.query = "Lin 2024 Electrical Stimulation" + self.json = True + self.limit = 10 + self.domain = None + self.year_from = None + self.year_to = None + self.ocr = None + self.deep = None + self.lifecycle = None + self.next_step = None + self.expand = True + + +class _Conn: + def close(self) -> None: + return None + + +def test_search_zero_results_emits_query_diagnostic(monkeypatch, tmp_path: Path, capsys) -> None: + db_path = tmp_path / "paperforge.db" + db_path.write_text("", encoding="utf-8") + monkeypatch.setattr(search_command, "get_memory_db_path", lambda vault: db_path) + monkeypatch.setattr(search_command, "get_connection", lambda db_path, read_only=True: _Conn()) + monkeypatch.setattr(search_command, "search_papers", lambda *args, **kwargs: []) + + args = _Args(tmp_path) + assert search_command.run(args) == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["ok"] is True + assert payload["data"]["count"] == 0 + assert "query_diagnostic" in payload["data"] + assert payload["warnings"] + + +def test_retrieve_unavailable_emits_suggested_modes(monkeypatch, tmp_path: Path, capsys) -> None: + monkeypatch.setattr("paperforge.embedding.get_embed_status", lambda vault: {"healthy": True, "chunk_count": 0, "db_exists": False, "error": ""}) + + args = _Args(tmp_path) + args.query = "galvanotaxis" + assert retrieve_command.run(args) == 1 + payload = json.loads(capsys.readouterr().out) + assert payload["ok"] is False + assert payload["data"]["interactive_fallback_required"] is True + assert payload["data"]["suggested_modes"] + + +def test_retrieve_low_confidence_hits_emit_warning(monkeypatch, tmp_path: Path, capsys) -> None: + monkeypatch.setattr("paperforge.embedding.get_embed_status", lambda vault: {"healthy": True, "chunk_count": 5, "db_exists": True, "error": ""}) + monkeypatch.setattr(retrieve_command, "retrieve_chunks", lambda vault, query, limit=5, expand=True: [ + {"paper_id": "AAA11111", "chunk_text": "[Figure]", "score": 0.58}, + {"paper_id": "AAA11111", "chunk_text": "### Keywords", "score": 0.57}, + {"paper_id": "AAA11111", "chunk_text": "None.", "score": 0.56}, + ]) + monkeypatch.setattr(retrieve_command, "get_memory_db_path", lambda vault: tmp_path / "missing.db") + + args = _Args(tmp_path) + args.query = "unlikelynonexistenttermxyz" + assert retrieve_command.run(args) == 0 + payload = json.loads(capsys.readouterr().out) + assert payload["ok"] is True + assert payload["warnings"] + assert payload["data"]["query_diagnostic"]["interactive_fallback_required"] is True diff --git a/tests/test_query_plan.py b/tests/test_query_plan.py new file mode 100644 index 00000000..f1a32bc9 --- /dev/null +++ b/tests/test_query_plan.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from paperforge.query_planning import build_query_plan, enrich_query_plan_with_runtime +from paperforge.commands.query_plan import run as query_plan_run + + +class _Args: + def __init__(self, query: str, intent: str, vault_path: Path, json: bool = True) -> None: + self.query = query + self.intent = intent + self.vault_path = vault_path + self.json = json + + +def test_query_plan_identifier_exact_prefers_paper_context() -> None: + plan = build_query_plan("10.1016/j.heliyon.2024.e38112", "known-paper") + assert plan["query_class"] == "identifier_exact" + assert plan["recommended_primary"]["command"] == "paper-context" + + +def test_query_plan_mixed_author_year_rewrites_to_author_year_search() -> None: + plan = build_query_plan("Lin 2024 Electrical Stimulation", "discover") + primary = plan["recommended_primary"] + assert plan["query_class"] == "mixed_query" + assert primary["command"] == "search" + assert primary["args"]["query"] == "Lin" + assert primary["args"]["year_from"] == 2024 + assert primary["args"]["year_to"] == 2024 + + +def test_query_plan_content_prefers_retrieve() -> None: + plan = build_query_plan("galvanotaxis", "content") + assert plan["query_class"] == "content_term" + assert plan["recommended_primary"]["command"] == "retrieve" + assert any(step["action"] == "interactive_fulltext_fallback" for step in plan["fallback_plan"]) + + +def test_query_plan_runtime_enrichment_without_retrieve(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + monkeypatch.setattr( + "paperforge.commands.query_plan.enrich_query_plan_with_runtime", + lambda plan, vault: { + **plan, + "runtime": {"retrieve_available": False, "vector_status": {"db_exists": False, "chunk_count": 0, "healthy": True, "error": ""}}, + "scope_assessment": { + "source": "library", + "label": "full library", + "estimated_paper_count": 120, + "fulltext_ready_count": 80, + "recommended_mode": "ask_user_before_broad_grep", + }, + "interactive_fallback_required": True, + "suggested_modes": [ + {"mode": "ask_for_scope_limit", "description": "Ask first."}, + {"mode": "fulltext_rg_or_grep", "description": "Search fulltext."}, + ], + }, + ) + args = _Args("galvanotaxis", "content", tmp_path) + assert query_plan_run(args) == 0 + out = capsys.readouterr().out + assert '"interactive_fallback_required": true' in out.lower() + assert '"mode": "ask_for_scope_limit"' in out + + +def test_query_plan_discover_prefers_context_for_known_domain(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + plan = build_query_plan("骨科 里有什么", "discover") + monkeypatch.setattr("paperforge.embedding.get_embed_status", lambda vault: {"db_exists": False, "chunk_count": 0, "healthy": True, "error": ""}) + monkeypatch.setattr( + "paperforge.worker.asset_index.read_index", + lambda vault: {"items": [{"domain": "骨科", "lifecycle": "fulltext_ready", "collections": []} for _ in range(5)]}, + ) + enriched = enrich_query_plan_with_runtime(plan, tmp_path) + assert enriched["recommended_primary"]["command"] == "context" + assert enriched["recommended_primary"]["args"]["domain"] == "骨科" diff --git a/tests/test_skill_graph_contracts.py b/tests/test_skill_graph_contracts.py index 70f6f696..ae2ad048 100644 --- a/tests/test_skill_graph_contracts.py +++ b/tests/test_skill_graph_contracts.py @@ -62,6 +62,12 @@ def test_retrieval_routing_has_semantic_optional_rule() -> None: assert "optional" in text.lower() or "supplementary" in text.lower() or "only for candidate expansion" in text +def test_skill_md_has_intent_level_trigger_language() -> None: + text = SKILL_MD.read_text(encoding="utf-8") + assert "必须调用 paperforge skill" in text + assert "collection / domain" in text or "正文内容" in text + + # --- Group 4: Molecule output shape contracts --- def test_discover_papers_output_is_paper_list() -> None: @@ -72,3 +78,10 @@ def test_discover_papers_output_is_paper_list() -> None: def test_find_evidence_output_is_grouped_hits() -> None: text = EVIDENCE_MD.read_text(encoding="utf-8") assert "group" in text.lower() or "evidence hit" in text.lower() or "snippet" in text.lower() + + +def test_molecules_reference_query_plan() -> None: + discover_text = DISCOVER_MD.read_text(encoding="utf-8") + evidence_text = EVIDENCE_MD.read_text(encoding="utf-8") + assert "query-plan" in discover_text + assert "query-plan" in evidence_text