diff --git a/README.md b/README.md
index fc327b0..ca8754b 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ Inspired by the reading workflow demo in [this Bilibili video](https://www.bilib
- **Vault lifecycle aware** — cached summaries follow Markdown file renames and are removed when the source note is deleted.
- **Configurable input limit** — default 20,000 characters, adjustable for long-context models and included in the cache fingerprint.
- **Prompt controls** — choose summary language, card count range, and an optional custom system prompt. Prompt settings are included in the cache fingerprint.
+- **Localized UI** — plugin commands, settings, pane text, and notices support Chinese and English.
- **Multiple model access paths** — Claude Code CLI, Codex CLI, direct provider APIs, and OpenAI/Anthropic-compatible proxies. The API backend follows the same lightweight `provider/model + base URL + protocol` idea as OpenClaw.
## Installation
@@ -130,6 +131,9 @@ The plugin keeps `main.js` as the generated Obsidian runtime bundle. Edit `main.
| File | Responsibility |
|------|----------------|
| `main.ts` | Obsidian lifecycle, commands, right-pane view, settings tab orchestration |
+| `src/anchor.ts` | Anchor-to-line matching and whitespace-normalized fallback |
+| `src/i18n.ts` | Chinese/English UI strings and translation helper |
+| `src/prompt.ts` | Prompt construction, language controls, and custom prompt templating |
| `src/settings.ts` | Defaults, provider presets, settings normalization, cache fingerprinting and pruning |
| `src/schema.ts` | JSON extraction, card payload normalization, structured-output schemas |
| `src/providers.ts` | API provider request/response adapters |
diff --git a/main.js b/main.js
index 99df6d4..3b53b1d 100644
--- a/main.js
+++ b/main.js
@@ -1,26 +1,26 @@
'use strict';
-"use strict";var De=Object.create;var D=Object.defineProperty;var Ne=Object.getOwnPropertyDescriptor;var Re=Object.getOwnPropertyNames;var $e=Object.getPrototypeOf,je=Object.prototype.hasOwnProperty;var Ue=(a,e)=>{for(var t in e)D(a,t,{get:e[t],enumerable:!0})},ve=(a,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of Re(e))!je.call(a,n)&&n!==t&&D(a,n,{get:()=>e[n],enumerable:!(r=Ne(e,n))||r.enumerable});return a};var N=(a,e,t)=>(t=a!=null?De($e(a)):{},ve(e||!a||!a.__esModule?D(t,"default",{value:a,enumerable:!0}):t,a)),Be=a=>ve(D({},"__esModule",{value:!0}),a);var pt={};Ue(pt,{__test:()=>lt,default:()=>ct});module.exports=Be(pt);var c=require("obsidian"),Me=require("child_process"),Q=N(require("os")),A=N(require("path")),Ve=N(require("fs"));function te(a){let e=[`## ${a.title}`];if(a.anchor){let t=a.anchor.replace(/\s+/g," ").trim();e.push(`> ${t}`)}return a.gist&&e.push(a.gist),a.bullets&&a.bullets.length>0&&e.push(a.bullets.map(t=>`- ${t}`).join(`
+"use strict";var Be=Object.create;var V=Object.defineProperty;var Ue=Object.getOwnPropertyDescriptor;var je=Object.getOwnPropertyNames;var Ge=Object.getPrototypeOf,$e=Object.prototype.hasOwnProperty;var He=(a,e)=>{for(var t in e)V(a,t,{get:e[t],enumerable:!0})},Te=(a,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of je(e))!$e.call(a,i)&&i!==t&&V(a,i,{get:()=>e[i],enumerable:!(n=Ue(e,i))||n.enumerable});return a};var R=(a,e,t)=>(t=a!=null?Be(Ge(a)):{},Te(e||!a||!a.__esModule?V(t,"default",{value:a,enumerable:!0}):t,a)),Ke=a=>Te(V({},"__esModule",{value:!0}),a);var mt={};He(mt,{__test:()=>ht,default:()=>dt});module.exports=Ke(mt);var c=require("obsidian"),Ve=require("child_process"),ee=R(require("os")),v=R(require("path")),Re=R(require("fs"));function B(a,e){if(!e)return-1;let t=p=>p.replace(/\s+/g," ").trim(),n=p=>{let h=[],g=[],u=!1;for(let E=0;E
0;continue}u&&(h.push(" "),g.push(E),u=!1),h.push(Ae),g.push(E)}return{text:h.join(""),map:g}},i=p=>{if(!p)return-1;let h=a.indexOf(p);if(h===-1)return-1;let g=0;for(let u=0;u=0||(r=i(e.trim()),r>=0))return r;for(let p of[60,40,25,15]){let h=e.trim().slice(0,p);if(r=i(h),r>=0)return r}let o=n(a),s=t(e).slice(0,30);if(!s)return-1;let l=o.text.indexOf(s);if(l===-1)return-1;let d=o.map[l];if(d==null)return-1;let f=0;for(let p=0;p --version` \u9A8C\u8BC1 spawn \u80FD\u627E\u5230\u4E8C\u8FDB\u5236",backendTestFailed:"\u2717 \u540E\u7AEF\u6D4B\u8BD5\u5931\u8D25\uFF1A{error}",settingExportFolderName:"\u5BFC\u51FA\u6587\u4EF6\u5939",settingExportFolderDesc:"\u5BF9\u7167\u7B14\u8BB0\u751F\u6210\u4F4D\u7F6E\uFF08\u76F8\u5BF9 Vault \u6839\uFF09",cacheHeader:"\u7F13\u5B58",settingMaxCacheName:"\u6700\u5927\u7F13\u5B58\u7BC7\u6570",settingMaxCacheDesc:"\u8D85\u8FC7\u4E0A\u9650\u540E\u6309\u6700\u8FD1\u8BBF\u95EE\u65F6\u95F4\u6DD8\u6C70\u6700\u65E7\u7684\u7B14\u8BB0\u7F13\u5B58\uFF1B\u7F13\u5B58\u4FDD\u5B58\u5728\u63D2\u4EF6\u76EE\u5F55\u7684 cache.json",cachePruned:"\u5DF2\u6DD8\u6C70 {count} \u6761\u65E7\u7F13\u5B58",cachedNotesName:"\u5DF2\u7F13\u5B58\u7B14\u8BB0\uFF1A{count} \u7BC7",cachedNotesDesc:"\u7F13\u5B58\u4EE5\u6E90\u7B14\u8BB0 SHA1 + \u751F\u6210\u914D\u7F6E\u6307\u7EB9\u4F5C\u4E3A\u5931\u6548\u952E\uFF0C\u6E90\u7B14\u8BB0\u6216\u6A21\u578B\u914D\u7F6E\u4FEE\u6539\u540E\u4F1A\u663E\u793A stale \u63D0\u793A",clearAllCacheButton:"\u6E05\u9664\u6240\u6709\u7F13\u5B58"},en:{appTitle:"Parallel Reader",settingsTitle:"Parallel Reader Settings",emptyOpenNote:"Open a note, then run:",emptyNoCache:"This note has no cached parallel notes. Run:",commandGenerate:"Parallel Reader: Generate notes for current note",displayName:"Parallel Reader",ribbonOpen:"Open Parallel Reader pane",cmdRun:"Generate parallel notes for current note (cache first)",cmdRegen:"Regenerate parallel notes (bypass cache)",cmdOpenView:"Open Parallel Reader pane",cmdExport:"Export current parallel notes to Vault",cmdCopyMarkdown:"Copy current parallel notes as Markdown",cmdCancel:"Cancel current parallel-note generation",cmdClearCurrent:"Clear cache for current note",cmdClearAll:"Clear all caches",actionCancel:"Cancel generation",actionRegenerate:"Regenerate",actionCopyAll:"Copy all Markdown",actionExport:"Export to Vault",staleBanner:"The source note or generation settings changed. This is stale cache.",loadingDefault:"Generating parallel notes...",loadingGenerating:"Parallel Reader: asking the LLM to read and segment the full note...",loadingSubtitle:"You can keep reading. Cards will refresh when generation finishes.",errorTitle:"Generation failed",emptyCard:"(not generated)",anchorMismatch:"Anchor did not match; scroll sync is unavailable",menuCopyMarkdown:"Copy Markdown",menuCopyPlain:"Copy plain text",menuCopyAnchor:"Copy anchor quote",menuJumpSource:"Jump to source",copiedMarkdown:"Copied Markdown",copiedPlain:"Copied plain text",copiedAnchor:"Copied anchor quote",copiedAllMarkdown:"Copied all Markdown",copyFailed:"Copy failed: {error}",actionFailed:"{label} failed: {error}",exported:"Exported \u2192 {path}",noCurrentNote:"No current note",cacheClearedFile:"Cleared cache: {name}",cacheClearedAll:"Cleared {count} cache entries",noCancelableJob:"No cancellable generation job",cancelRequested:"Cancel requested",fileMenuGenerate:"Generate parallel notes",fileMenuRegen:"Regenerate parallel notes",fileMenuClear:"Clear parallel-note cache",noExportContent:"No parallel notes to export",noCopyContent:"No parallel notes to copy",openNoteFirst:"Open a note first",alreadyGenerating:"This note is already being generated",emptyNote:"The note is empty",longNoteTruncated:"Long note: only the first {count} characters will be sent to the model",generatingNotice:"Parallel Reader: asking the LLM to read and segment the full note...",noCardsReturned:"LLM returned no cards",generationDone:"Generated {count} sections{suffix}",unanchoredSuffix:" (\u26A0 {count} anchors unmatched)",cancelled:"Generation cancelled",cancelledError:"Generation cancelled",generationFailed:"Generation failed{kind}: {error}",noEditor:"Could not find the source note editor",settingUiLanguageName:"UI language",settingUiLanguageDesc:"Controls plugin UI, commands, and notices. Auto follows Obsidian/system language.",settingBackendName:"Backend",settingBackendDesc:"Backend for generating bullets: CLI reuses local login; API supports OpenAI, Anthropic, Gemini, and compatible proxies.",settingCliPathName:"CLI path (optional)",settingCliPathDesc:"Leave blank to auto-detect common paths. Obsidian launched from the GUI may not inherit shell PATH.",settingCliPathPlaceholder:"Example: /Users/you/bin/codex",settingCliTimeoutName:"CLI timeout (ms)",apiProviderHeader:"API Provider",settingProviderPresetName:"Provider preset",settingProviderPresetDesc:"OpenClaw-style provider/model setup: presets define protocol, base URL, and auth defaults.",settingApiFormatName:"API format",settingApiFormatDesc:"Wire protocol for the provider. OpenAI-compatible proxies usually use Chat Completions.",settingBaseUrlName:"Base URL",settingBaseUrlDesc:"Provider root URL, without /chat/completions. Blank uses the preset default.",settingApiKeyName:"API Key",settingApiKeyDesc:"Can be blank for local Ollama/LM Studio; most hosted providers require a key.",settingApiKeyEnvName:"API key env var",settingApiKeyEnvDesc:"Optional. Obsidian GUI may not inherit shell env; direct API Key is usually more reliable.",settingAuthTypeName:"Auth type",settingAuthTypeDesc:"Auto uses provider preset. Custom proxies can use Bearer, x-api-key, or none.",settingHeadersName:"Extra headers",settingHeadersDesc:"Optional. JSON object or one `Header: value` per line, useful for Cloudflare AI Gateway.",settingMaxTokensName:"Max output tokens",settingModelName:"Model",settingModelDescApi:"Model ID for API calls. Supports OpenClaw-style provider/model; matching provider prefixes are stripped.",settingModelDescCli:"Passed as --model for Claude Code. Usually ignored by Codex, which uses its default config.",settingMaxInputName:"Max input characters",settingMaxInputDesc:"Longer notes are truncated before sending to the model. Raise this for long-context models.",promptHeader:"Prompt",settingPromptLanguageName:"Output language",settingPromptLanguageDesc:"Controls title/gist/bullets language. Anchor is always copied verbatim from source.",settingCardRangeName:"Card count range",settingCardRangeDesc:"The model adapts segmentation within this range.",settingCustomPromptName:"Custom system prompt",settingCustomPromptDesc:"Optional. Variables: {minCards}, {maxCards}, {languageInstruction}, {schema}, {example}",settingCustomPromptPlaceholder:"Leave blank to use built-in prompt",settingTestBackendName:"Test current backend",settingTestBackendDescApi:"Sends a minimal LLM request to validate API settings.",settingTestBackendDescCli:"Runs ` --version` to verify the binary can be spawned.",backendTestFailed:"\u2717 Backend test failed: {error}",settingExportFolderName:"Export folder",settingExportFolderDesc:"Parallel-note output location, relative to the Vault root.",cacheHeader:"Cache",settingMaxCacheName:"Max cached notes",settingMaxCacheDesc:"Prunes least-recently accessed note caches above this limit. Cache is stored in plugin cache.json.",cachePruned:"Pruned {count} old cache entries",cachedNotesName:"Cached notes: {count}",cachedNotesDesc:"Cache is invalidated by source SHA1 and generation settings fingerprint.",clearAllCacheButton:"Clear all caches"}};function Je(a){let e=a&&a.uiLanguage;return e==="zh"||e==="en"?e:String((typeof navigator<"u"?navigator:null)?.language||"").toLowerCase().startsWith("zh")?"zh":"en"}function ne(a,e,t){let n=Je(a),i=U[n]||U.en,r=U.en[e]||U.zh[e]||e,o=i[e]||r;return String(o).replace(/\{([a-zA-Z0-9_]+)\}/g,(s,l)=>t&&Object.prototype.hasOwnProperty.call(t,l)?String(t[l]):s)}function re(a){let e=[`## ${a.title}`];if(a.anchor){let t=a.anchor.replace(/\s+/g," ").trim();e.push(`> ${t}`)}return a.gist&&e.push(a.gist),a.bullets&&a.bullets.length>0&&e.push(a.bullets.map(t=>`- ${t}`).join(`
`)),e.join(`
-`)}function xe(a){return[a.title,a.gist||"",...(a.bullets||[]).map(e=>"\u2022 "+e)].filter(Boolean).join(`
-`)}function R(a,e){let t=[`# ${a||"\u5BF9\u7167\u7B14\u8BB0"}`];for(let r of e||[])t.push(te(r));return t.join(`
+`)}function Pe(a){return[a.title,a.gist||"",...(a.bullets||[]).map(e=>"\u2022 "+e)].filter(Boolean).join(`
+`)}function j(a,e){let t=[`# ${a||"\u5BF9\u7167\u7B14\u8BB0"}`];for(let n of e||[])t.push(re(n));return t.join(`
-`)}var $="record_parallel_reader_cards";function Ge(a){let e=[],t=-1,r=0,n=!1,i=!1;for(let s=0;s0&&r--,r===0&&t>=0&&(e.push(a.slice(t,s+1)),t=-1))}return e}function ae(a){let e=(a||"").trim();if(!e)return e;try{return JSON.parse(e),e}catch{}let t=e.match(/```(?:json)?\s*([\s\S]*?)```/);if(t){let n=t[1].trim();try{return JSON.parse(n),n}catch{}}let r=Ge(e);r.sort((n,i)=>i.length-n.length);for(let n of r)try{return JSON.parse(n),n}catch{}return e}function C(a){let e=ae(a),t;try{t=JSON.parse(e)}catch{throw new Error(`LLM \u8FD4\u56DE\u975E JSON\uFF1A
-`+(a||"").slice(0,500))}return F(t)}function F(a){return(a&&Array.isArray(a.cards)?a.cards:[]).filter(t=>t&&typeof t=="object").map(t=>({title:typeof t.title=="string"?t.title:"(\u65E0\u6807\u9898)",anchor:typeof t.anchor=="string"?t.anchor:"",gist:typeof t.gist=="string"?t.gist:"",bullets:Array.isArray(t.bullets)?t.bullets.filter(r=>typeof r=="string"):[]}))}function I(a){let e={type:"object",properties:{title:{type:"string"},anchor:{type:"string"},gist:{type:"string"},bullets:{type:"array",items:{type:"string"}}},required:["title","anchor","gist","bullets"]},t={type:"object",properties:{cards:{type:"array",items:e}},required:["cards"]};return a&&(e.additionalProperties=!1,t.additionalProperties=!1),t}function Ae(){return{type:"json_schema",json_schema:{name:"parallel_reader_cards",strict:!0,schema:I(!0)}}}function Ce(){return{format:{type:"json_schema",name:"parallel_reader_cards",strict:!0,schema:I(!0)}}}function Te(){return{name:$,description:"Return the generated Parallel Reader summary cards.",input_schema:I(!0)}}var ke=N(require("crypto")),ie=2e4,He=2,P=2,_=100,j={zh:"\u4E2D\u6587",en:"English",auto:"Auto-detect"},f={backend:"claude-code",cliPath:"",apiProvider:"anthropic",apiFormat:"anthropic-messages",apiBaseUrl:"",apiKey:"",apiKeyEnvVar:"",apiAuthType:"auto",apiHeaders:"",apiMaxTokens:4096,maxDocChars:ie,maxCacheEntries:_,promptLanguage:"zh",minCards:5,maxCards:15,customSystemPrompt:"",model:"claude-sonnet-4-6",exportFolder:"Reading/Articles",cliTimeoutMs:12e4},w={"anthropic-messages":{label:"Anthropic Messages",defaultBaseUrl:"https://api.anthropic.com/v1",defaultAuthType:"x-api-key"},"openai-chat":{label:"OpenAI Chat Completions",defaultBaseUrl:"https://api.openai.com/v1",defaultAuthType:"bearer",tokenLimitField:"max_tokens"},"openai-responses":{label:"OpenAI Responses",defaultBaseUrl:"https://api.openai.com/v1",defaultAuthType:"bearer"},"google-generative-ai":{label:"Google Gemini generateContent",defaultBaseUrl:"https://generativelanguage.googleapis.com/v1beta",defaultAuthType:"x-goog-api-key"}},ne={auto:"Auto",bearer:"Authorization: Bearer","x-api-key":"x-api-key","x-goog-api-key":"x-goog-api-key","api-key":"api-key",none:"None"},T={anthropic:{label:"Anthropic",format:"anthropic-messages",baseUrl:"https://api.anthropic.com/v1",authType:"x-api-key",envVar:"ANTHROPIC_API_KEY",model:"claude-sonnet-4-6"},openai:{label:"OpenAI",format:"openai-chat",baseUrl:"https://api.openai.com/v1",authType:"bearer",envVar:"OPENAI_API_KEY",tokenLimitField:"max_completion_tokens",model:"gpt-5.1"},"openai-responses":{label:"OpenAI Responses",format:"openai-responses",baseUrl:"https://api.openai.com/v1",authType:"bearer",envVar:"OPENAI_API_KEY",modelPrefix:"openai",model:"gpt-5.1"},google:{label:"Google Gemini",format:"google-generative-ai",baseUrl:"https://generativelanguage.googleapis.com/v1beta",authType:"x-goog-api-key",envVar:"GEMINI_API_KEY",model:"gemini-3-pro-preview"},openrouter:{label:"OpenRouter",format:"openai-chat",baseUrl:"https://openrouter.ai/api/v1",authType:"bearer",envVar:"OPENROUTER_API_KEY",model:""},groq:{label:"Groq",format:"openai-chat",baseUrl:"https://api.groq.com/openai/v1",authType:"bearer",envVar:"GROQ_API_KEY",model:""},deepseek:{label:"DeepSeek",format:"openai-chat",baseUrl:"https://api.deepseek.com",authType:"bearer",envVar:"DEEPSEEK_API_KEY",model:"deepseek-chat"},moonshot:{label:"Moonshot / Kimi",format:"openai-chat",baseUrl:"https://api.moonshot.ai/v1",authType:"bearer",envVar:"MOONSHOT_API_KEY",model:"kimi-k2.5"},qianfan:{label:"Baidu Qianfan",format:"openai-chat",baseUrl:"https://qianfan.baidubce.com/v2",authType:"bearer",envVar:"QIANFAN_API_KEY",model:"deepseek-v3.2"},minimax:{label:"MiniMax (Anthropic-compatible)",format:"anthropic-messages",baseUrl:"https://api.minimax.io/anthropic",authType:"bearer",envVar:"MINIMAX_API_KEY",model:"MiniMax-M2.1"},xai:{label:"xAI",format:"openai-chat",baseUrl:"https://api.x.ai/v1",authType:"bearer",envVar:"XAI_API_KEY",model:""},mistral:{label:"Mistral",format:"openai-chat",baseUrl:"https://api.mistral.ai/v1",authType:"bearer",envVar:"MISTRAL_API_KEY",model:""},cerebras:{label:"Cerebras",format:"openai-chat",baseUrl:"https://api.cerebras.ai/v1",authType:"bearer",envVar:"CEREBRAS_API_KEY",model:""},zai:{label:"Z.AI / GLM",format:"openai-chat",baseUrl:"https://api.z.ai/api/paas/v4",authType:"bearer",envVar:"ZAI_API_KEY",model:""},ollama:{label:"Ollama (local)",format:"openai-chat",baseUrl:"http://127.0.0.1:11434/v1",authType:"none",envVar:"",model:""},lmstudio:{label:"LM Studio (local)",format:"openai-chat",baseUrl:"http://127.0.0.1:1234/v1",authType:"none",envVar:"",model:""},"custom-openai":{label:"Custom OpenAI-compatible",format:"openai-chat",baseUrl:"",authType:"bearer",envVar:"",model:""},"custom-anthropic":{label:"Custom Anthropic-compatible",format:"anthropic-messages",baseUrl:"",authType:"x-api-key",envVar:"",model:""}};function S(a){return ke.default.createHash("sha1").update(a,"utf8").digest("hex")}function re(a){return Array.isArray(a)?"["+a.map(re).join(",")+"]":a&&typeof a=="object"?"{"+Object.keys(a).sort().map(e=>JSON.stringify(e)+":"+re(a[e])).join(",")+"}":JSON.stringify(a)}function U(a){return a==="api"||a==="anthropic-api"}function y(a){return T[a.apiProvider]||T.anthropic}function v(a){let e=y(a),t=(a.apiFormat||e.format||"").trim();return w[t]?t:e.format}function O(a){let e=v(a),t=y(a),r=(a.apiBaseUrl||"").trim();if(r)return r.replace(/\/+$/,"");if((a.apiProvider||"").startsWith("custom-"))throw new Error("\u81EA\u5B9A\u4E49 provider \u9700\u8981\u586B\u5199 API Base URL\u3002");let n=(t.baseUrl||w[e].defaultBaseUrl||"").trim();if(!n)throw new Error("API Base URL \u672A\u8BBE\u7F6E\u3002\u8BF7\u5728\u8BBE\u7F6E\u91CC\u9009\u62E9 provider \u6216\u586B\u5199\u81EA\u5B9A\u4E49 base URL\u3002");return n.replace(/\/+$/,"")}function se(a){let e=(a.apiAuthType||"auto").trim();if(e&&e!=="auto")return e;let t=y(a),r=v(a);return t.authType||w[r].defaultAuthType||"bearer"}function Fe(a){let e=(a.apiKey||"").trim();if(e)return e;let t=(a.apiKeyEnvVar||y(a).envVar||"").trim();return t&&process.env&&process.env[t]?String(process.env[t]).trim():""}function M(a){let e=(a.model||"").trim();if(!e)throw new Error("Model \u672A\u8BBE\u7F6E\u3002\u8BF7\u5728\u8BBE\u7F6E\u91CC\u586B\u5199\u6A21\u578B ID\u3002");let t=y(a),r=[a.apiProvider,t.modelPrefix].map(i=>(i||"").trim()).filter(Boolean),n=e.toLowerCase();for(let i of r){let s=i.toLowerCase();if(n.startsWith(s+"/"))return e.slice(i.length+1).trim()}return e}function oe(a,e){let t=y(a),r=T[e]||T.anthropic,n=(a.model||"").trim(),i=!n||n===f.model||!!t.model&&n===t.model;a.apiProvider=e,a.apiFormat=r.format,a.apiBaseUrl=r.baseUrl,a.apiAuthType=r.authType||"auto",a.apiKeyEnvVar=r.envVar||"",i&&(a.model=r.model||"")}function ce(a){(!a.apiProvider||!T[a.apiProvider])&&(a.apiProvider="anthropic");let e=y(a);(!a.apiFormat||!w[a.apiFormat])&&(a.apiFormat=e.format),(!a.apiAuthType||!ne[a.apiAuthType])&&(a.apiAuthType="auto"),a.backend==="anthropic-api"&&(a.apiProvider=a.apiProvider||"anthropic",a.apiFormat=a.apiFormat||"anthropic-messages",a.apiBaseUrl=a.apiBaseUrl||T.anthropic.baseUrl,a.apiAuthType=a.apiAuthType||"x-api-key",a.apiKeyEnvVar=a.apiKeyEnvVar||"ANTHROPIC_API_KEY");let t=Number(a.apiMaxTokens);(!Number.isFinite(t)||t<=0)&&(a.apiMaxTokens=f.apiMaxTokens);let r=Number(a.maxDocChars);return(!Number.isFinite(r)||r<1e3)&&(a.maxDocChars=f.maxDocChars),a.maxCacheEntries=Ie(a.maxCacheEntries),j[a.promptLanguage]||(a.promptLanguage=f.promptLanguage),a.minCards=Se(a.minCards,f.minCards),a.maxCards=Se(a.maxCards,f.maxCards),a.maxCardsEe(s)-Ee(u)||i.localeCompare(o)).slice(0,r.length-t).map(([i])=>i);for(let i of n)delete a[i];return n}function B(a){let e=ce(Object.assign({},f,a||{})),t=U(e.backend),r=y(e),n=v(e),i=t?(e.apiBaseUrl||r.baseUrl||w[n]?.defaultBaseUrl||"").trim().replace(/\/+$/,""):"";return S(re({cacheSchemaVersion:P,promptVersion:He,maxDocChars:Number(e.maxDocChars)||f.maxDocChars,promptLanguage:e.promptLanguage,minCards:e.minCards,maxCards:e.maxCards,customSystemPromptHash:S(e.customSystemPrompt||""),backend:e.backend,model:e.model,apiProvider:t?e.apiProvider:"",apiFormat:t?n:"",apiBaseUrl:i,apiAuthType:t?se(e):"",apiHeadersHash:t?S(e.apiHeaders||""):"",apiMaxTokens:t?Number(e.apiMaxTokens)||f.apiMaxTokens:0,structuredOutputVersion:1}))}function G(a,e,t){return!!a&&a.schemaVersion===P&&a.contentHash===S(e)&&a.settingsHash===B(t)}function pe(a,e){let t=a.replace(/\/+$/,"");for(let r of e)if(t.endsWith(r))return t;return t+e[0]}function Je(a){let e=(a||"").trim();if(!e)return{};if(e.startsWith("{")){let r;try{r=JSON.parse(e)}catch(i){throw new Error("\u81EA\u5B9A\u4E49 headers JSON \u89E3\u6790\u5931\u8D25\uFF1A"+i.message)}if(!r||typeof r!="object"||Array.isArray(r))throw new Error("\u81EA\u5B9A\u4E49 headers JSON \u5FC5\u987B\u662F\u5BF9\u8C61");let n={};for(let[i,s]of Object.entries(r))typeof s=="string"&&i.trim()&&(n[i.trim()]=s);return n}let t={};for(let r of e.split(/\r?\n/)){let n=r.trim();if(!n||n.startsWith("#"))continue;let i=n.indexOf(":");if(i<=0)throw new Error("\u81EA\u5B9A\u4E49 headers \u6BCF\u884C\u683C\u5F0F\u5E94\u4E3A `Header-Name: value`");let s=n.slice(0,i).trim(),o=n.slice(i+1).trim();s&&(t[s]=o)}return t}function Ke(a){let e=se(a);if(e==="none")return{};let t=Fe(a);if(!t){let r=(a.apiKeyEnvVar||y(a).envVar||"").trim(),n=r?` \u6216\u73AF\u5883\u53D8\u91CF ${r}`:"";throw new Error(`API key \u672A\u8BBE\u7F6E\u3002\u8BF7\u5728\u8BBE\u7F6E\u91CC\u586B\u5199 API Key${n}\u3002`)}return e==="bearer"?{authorization:`Bearer ${t}`}:e==="x-api-key"?{"x-api-key":t}:e==="x-goog-api-key"?{"x-goog-api-key":t}:e==="api-key"?{"api-key":t}:{authorization:`Bearer ${t}`}}function W(a,e){return{"content-type":"application/json",...Ke(a),...e||{},...Je(a.apiHeaders)}}function ze(a,e){if(a.json&&typeof a.json=="object")return a.json;try{return JSON.parse(a.text||"{}")}catch{throw new Error(`${e} \u8FD4\u56DE\u975E JSON\uFF1A
-${(a.text||"").slice(0,500)}`)}}async function Pe(a,e,t,r,n){let i;try{i=await a({url:t,method:"POST",headers:r,body:JSON.stringify(n),throw:!1})}catch(s){throw new Error(`${e} \u8BF7\u6C42\u5931\u8D25\uFF1A`+(s.message||s))}if(i.status>=400)throw new Error(`${e} API ${i.status}: ${(i.text||"").slice(0,500)}`);return ze(i,e)}function We(a){let e=String(a&&a.message?a.message:a);return/API (400|404|422):/.test(e)?/response_format|json_schema|responseJsonSchema|responseMimeType|tools?|tool_choice|unsupported|unrecognized|unknown|schema/i.test(e):!1}async function Y(a,e,t,r,n,i){try{return await Pe(a,e,t,r,n)}catch(s){if(!i||!We(s))throw s;return console.warn(`[parallel-reader] ${e} structured output rejected; retrying without structured output`,s),Pe(a,e+" fallback",t,r,i)}}function ue(a){return typeof a=="string"?a:Array.isArray(a)?a.map(e=>typeof e=="string"?e:e&&typeof e=="object"&&(e.text||e.output_text)||"").join(""):a&&typeof a=="object"&&(a.text||a.output_text)||""}function Ye(a){if(typeof a.output_text=="string")return a.output_text;let e=[],t=r=>{if(r&&typeof r!="string"){if(Array.isArray(r)){r.forEach(t);return}typeof r=="object"&&(typeof r.text=="string"&&e.push(r.text),typeof r.output_text=="string"&&e.push(r.output_text),r.type==="output_text"&&typeof r.content=="string"&&e.push(r.content),r.content&&t(r.content),r.output&&t(r.output))}};return t(a.output),e.join("")}function he(a){let e=y(a),t=w[v(a)]||{};return e.tokenLimitField||t.tokenLimitField||"max_tokens"}function H(a,e,t,r){let n=!r||r.structured!==!1,i={model:M(t),max_tokens:Number(t.apiMaxTokens)||4096,system:a,messages:[{role:"user",content:e}]};return n&&(i.tools=[Te()],i.tool_choice={type:"tool",name:$}),i}function J(a,e,t,r){let n=!r||r.structured!==!1,i={model:M(t),messages:[{role:"system",content:a},{role:"user",content:e}]};return i[he(t)]=Number(t.apiMaxTokens)||4096,n&&(i.response_format=Ae()),i}function K(a,e,t,r){let n=!r||r.structured!==!1,i={model:M(t),instructions:a,input:e,max_output_tokens:Number(t.apiMaxTokens)||4096};return n&&(i.text=Ce()),i}function z(a,e,t,r){let n=!r||r.structured!==!1,i={temperature:0,maxOutputTokens:Number(t.apiMaxTokens)||4096};return n&&(i.responseMimeType="application/json",i.responseJsonSchema=I(!1)),{systemInstruction:{parts:[{text:a}]},contents:[{role:"user",parts:[{text:e}]}],generationConfig:i}}function qe(a){let t=(Array.isArray(a&&a.content)?a.content:[]).find(r=>r&&r.type==="tool_use"&&r.name===$);return t?typeof t.input=="string"?C(t.input):t.input&&typeof t.input=="object"?F(t.input):[]:null}async function Xe(a,e,t,r){let n=pe(O(r),["/messages"]),i=await Y(a,"Anthropic-compatible",n,W(r,{"anthropic-version":"2023-06-01"}),H(e,t,r),H(e,t,r,{structured:!1})),s=qe(i);if(s)return s;let o=(i.content||[]).map(u=>ue(u)).join("").trim();return C(o)}async function Qe(a,e,t,r){let n=pe(O(r),["/chat/completions"]),s=((await Y(a,"OpenAI-compatible Chat",n,W(r),J(e,t,r),J(e,t,r,{structured:!1}))).choices||[])[0]||{},o=ue(s.message?.content||s.text||"").trim();return C(o)}async function Ze(a,e,t,r){let n=pe(O(r),["/responses"]),i=await Y(a,"OpenAI Responses",n,W(r),K(e,t,r),K(e,t,r,{structured:!1}));return C(Ye(i).trim())}async function et(a,e,t,r){let n=encodeURIComponent(M(r)),i=O(r);/:generateContent(?:\?|$)/.test(i)||(i=`${i.replace(/\/+$/,"")}/models/${n}:generateContent`);let s=W(r),g=((((await Y(a,"Google Gemini",i,s,z(e,t,r),z(e,t,r,{structured:!1}))).candidates||[])[0]||{}).content?.parts||[]).map(l=>ue(l)).join("").trim();return C(g)}async function V(a,e,t,r){switch(v(r)){case"openai-chat":return Qe(a,e,t,r);case"openai-responses":return Ze(a,e,t,r);case"google-generative-ai":return et(a,e,t,r);case"anthropic-messages":default:return Xe(a,e,t,r)}}async function _e(a,e){await V(a,'\u53EA\u8F93\u51FA JSON\uFF1A{"cards":[]}','\u8FDE\u901A\u6027\u6D4B\u8BD5\uFF1A\u8BF7\u539F\u6837\u8F93\u51FA {"cards":[]}',e);let t=v(e);return`${y(e).label} / ${w[t].label}`}var E=class extends Error{constructor(e){super("\u8BE5\u7B14\u8BB0\u6B63\u5728\u751F\u6210\u5BF9\u7167\u7B14\u8BB0"),this.name="GenerationJobAlreadyRunningError",this.code="already-running",this.key=e}},x=class extends Error{constructor(e){super("\u751F\u6210\u5DF2\u53D6\u6D88"),this.name="GenerationJobCancelledError",this.code="cancelled",this.key=e}},de=class{constructor(e){this.key=e,this.phase="queued",this.cancelled=!1,this.startedAt=new Date().toISOString(),this.updatedAt=this.startedAt,this._cancelHandlers=[]}setPhase(e){this.phase=e,this.updatedAt=new Date().toISOString()}onCancel(e){if(typeof e=="function"){if(this.cancelled){e();return}this._cancelHandlers.push(e)}}cancel(){if(this.cancelled)return!1;this.cancelled=!0,this.setPhase("cancelled");for(let e of this._cancelHandlers.splice(0))try{e()}catch{}return!0}throwIfCancelled(){if(this.cancelled)throw new x(this.key)}},L=class{constructor(){this.jobs=new Map}get(e){return this.jobs.get(e)||null}isRunning(e){return this.jobs.has(e)}async start(e,t){if(this.jobs.has(e))throw new E(e);let r=new de(e);this.jobs.set(e,r);try{r.setPhase("running");let n=await t(r);return r.throwIfCancelled(),n}catch(n){throw r.cancelled&&!(n instanceof x)?new x(e):n}finally{this.jobs.delete(e)}}cancel(e){let t=this.jobs.get(e);return t?t.cancel():!1}};function me(a){if(a instanceof x||a?.code==="cancelled")return"cancelled";let e=String(a&&a.message?a.message:a);return/api key|unauthorized|401|403|认证|权限/i.test(e)?"auth":/timeout|超时|timed out/i.test(e)?"timeout":/429|rate limit|too many requests/i.test(e)?"rate-limit":/非 JSON|json_schema|schema|structured/i.test(e)?"schema":/model 未设置|base url|配置|config/i.test(e)?"config":"unknown"}var b="parallel-reader-view";function tt(a){return a==="en"?"Write title, gist, and bullets in English.":a==="auto"?"Write title, gist, and bullets in the main language of the source document.":"\u7528\u4E2D\u6587\u8F93\u51FA title\u3001gist \u548C bullets\u3002"}function at(a){return a==="en"?`{"cards":[
+`)}var Se=R(require("crypto")),se=2e4,ze=2,S=2,N=100,F={zh:"\u4E2D\u6587",en:"English",auto:"Auto-detect"},oe={auto:"Auto",zh:"\u4E2D\u6587",en:"English"},m={uiLanguage:"auto",backend:"claude-code",cliPath:"",apiProvider:"anthropic",apiFormat:"anthropic-messages",apiBaseUrl:"",apiKey:"",apiKeyEnvVar:"",apiAuthType:"auto",apiHeaders:"",apiMaxTokens:4096,maxDocChars:se,maxCacheEntries:N,promptLanguage:"zh",minCards:5,maxCards:15,customSystemPrompt:"",model:"claude-sonnet-4-6",exportFolder:"Reading/Articles",cliTimeoutMs:12e4},w={"anthropic-messages":{label:"Anthropic Messages",defaultBaseUrl:"https://api.anthropic.com/v1",defaultAuthType:"x-api-key"},"openai-chat":{label:"OpenAI Chat Completions",defaultBaseUrl:"https://api.openai.com/v1",defaultAuthType:"bearer",tokenLimitField:"max_tokens"},"openai-responses":{label:"OpenAI Responses",defaultBaseUrl:"https://api.openai.com/v1",defaultAuthType:"bearer"},"google-generative-ai":{label:"Google Gemini generateContent",defaultBaseUrl:"https://generativelanguage.googleapis.com/v1beta",defaultAuthType:"x-goog-api-key"}},ce={auto:"Auto",bearer:"Authorization: Bearer","x-api-key":"x-api-key","x-goog-api-key":"x-goog-api-key","api-key":"api-key",none:"None"},T={anthropic:{label:"Anthropic",format:"anthropic-messages",baseUrl:"https://api.anthropic.com/v1",authType:"x-api-key",envVar:"ANTHROPIC_API_KEY",model:"claude-sonnet-4-6"},openai:{label:"OpenAI",format:"openai-chat",baseUrl:"https://api.openai.com/v1",authType:"bearer",envVar:"OPENAI_API_KEY",tokenLimitField:"max_completion_tokens",model:"gpt-5.1"},"openai-responses":{label:"OpenAI Responses",format:"openai-responses",baseUrl:"https://api.openai.com/v1",authType:"bearer",envVar:"OPENAI_API_KEY",modelPrefix:"openai",model:"gpt-5.1"},google:{label:"Google Gemini",format:"google-generative-ai",baseUrl:"https://generativelanguage.googleapis.com/v1beta",authType:"x-goog-api-key",envVar:"GEMINI_API_KEY",model:"gemini-3-pro-preview"},openrouter:{label:"OpenRouter",format:"openai-chat",baseUrl:"https://openrouter.ai/api/v1",authType:"bearer",envVar:"OPENROUTER_API_KEY",model:""},groq:{label:"Groq",format:"openai-chat",baseUrl:"https://api.groq.com/openai/v1",authType:"bearer",envVar:"GROQ_API_KEY",model:""},deepseek:{label:"DeepSeek",format:"openai-chat",baseUrl:"https://api.deepseek.com",authType:"bearer",envVar:"DEEPSEEK_API_KEY",model:"deepseek-chat"},moonshot:{label:"Moonshot / Kimi",format:"openai-chat",baseUrl:"https://api.moonshot.ai/v1",authType:"bearer",envVar:"MOONSHOT_API_KEY",model:"kimi-k2.5"},qianfan:{label:"Baidu Qianfan",format:"openai-chat",baseUrl:"https://qianfan.baidubce.com/v2",authType:"bearer",envVar:"QIANFAN_API_KEY",model:"deepseek-v3.2"},minimax:{label:"MiniMax (Anthropic-compatible)",format:"anthropic-messages",baseUrl:"https://api.minimax.io/anthropic",authType:"bearer",envVar:"MINIMAX_API_KEY",model:"MiniMax-M2.1"},xai:{label:"xAI",format:"openai-chat",baseUrl:"https://api.x.ai/v1",authType:"bearer",envVar:"XAI_API_KEY",model:""},mistral:{label:"Mistral",format:"openai-chat",baseUrl:"https://api.mistral.ai/v1",authType:"bearer",envVar:"MISTRAL_API_KEY",model:""},cerebras:{label:"Cerebras",format:"openai-chat",baseUrl:"https://api.cerebras.ai/v1",authType:"bearer",envVar:"CEREBRAS_API_KEY",model:""},zai:{label:"Z.AI / GLM",format:"openai-chat",baseUrl:"https://api.z.ai/api/paas/v4",authType:"bearer",envVar:"ZAI_API_KEY",model:""},ollama:{label:"Ollama (local)",format:"openai-chat",baseUrl:"http://127.0.0.1:11434/v1",authType:"none",envVar:"",model:""},lmstudio:{label:"LM Studio (local)",format:"openai-chat",baseUrl:"http://127.0.0.1:1234/v1",authType:"none",envVar:"",model:""},"custom-openai":{label:"Custom OpenAI-compatible",format:"openai-chat",baseUrl:"",authType:"bearer",envVar:"",model:""},"custom-anthropic":{label:"Custom Anthropic-compatible",format:"anthropic-messages",baseUrl:"",authType:"x-api-key",envVar:"",model:""}};function P(a){return Se.default.createHash("sha1").update(a,"utf8").digest("hex")}function ie(a){return Array.isArray(a)?"["+a.map(ie).join(",")+"]":a&&typeof a=="object"?"{"+Object.keys(a).sort().map(e=>JSON.stringify(e)+":"+ie(a[e])).join(",")+"}":JSON.stringify(a)}function G(a){return a==="api"||a==="anthropic-api"}function y(a){return T[a.apiProvider]||T.anthropic}function b(a){let e=y(a),t=(a.apiFormat||e.format||"").trim();return w[t]?t:e.format}function D(a){let e=b(a),t=y(a),n=(a.apiBaseUrl||"").trim();if(n)return n.replace(/\/+$/,"");if((a.apiProvider||"").startsWith("custom-"))throw new Error("\u81EA\u5B9A\u4E49 provider \u9700\u8981\u586B\u5199 API Base URL\u3002");let i=(t.baseUrl||w[e].defaultBaseUrl||"").trim();if(!i)throw new Error("API Base URL \u672A\u8BBE\u7F6E\u3002\u8BF7\u5728\u8BBE\u7F6E\u91CC\u9009\u62E9 provider \u6216\u586B\u5199\u81EA\u5B9A\u4E49 base URL\u3002");return i.replace(/\/+$/,"")}function le(a){let e=(a.apiAuthType||"auto").trim();if(e&&e!=="auto")return e;let t=y(a),n=b(a);return t.authType||w[n].defaultAuthType||"bearer"}function Ne(a){let e=(a.apiKey||"").trim();if(e)return e;let t=(a.apiKeyEnvVar||y(a).envVar||"").trim();return t&&process.env&&process.env[t]?String(process.env[t]).trim():""}function I(a){let e=(a.model||"").trim();if(!e)throw new Error("Model \u672A\u8BBE\u7F6E\u3002\u8BF7\u5728\u8BBE\u7F6E\u91CC\u586B\u5199\u6A21\u578B ID\u3002");let t=y(a),n=[a.apiProvider,t.modelPrefix].map(r=>(r||"").trim()).filter(Boolean),i=e.toLowerCase();for(let r of n){let o=r.toLowerCase();if(i.startsWith(o+"/"))return e.slice(r.length+1).trim()}return e}function pe(a,e){let t=y(a),n=T[e]||T.anthropic,i=(a.model||"").trim(),r=!i||i===m.model||!!t.model&&i===t.model;a.apiProvider=e,a.apiFormat=n.format,a.apiBaseUrl=n.baseUrl,a.apiAuthType=n.authType||"auto",a.apiKeyEnvVar=n.envVar||"",r&&(a.model=n.model||"")}function ue(a){oe[a.uiLanguage]||(a.uiLanguage=m.uiLanguage),(!a.apiProvider||!T[a.apiProvider])&&(a.apiProvider="anthropic");let e=y(a);(!a.apiFormat||!w[a.apiFormat])&&(a.apiFormat=e.format),(!a.apiAuthType||!ce[a.apiAuthType])&&(a.apiAuthType="auto"),a.backend==="anthropic-api"&&(a.apiProvider=a.apiProvider||"anthropic",a.apiFormat=a.apiFormat||"anthropic-messages",a.apiBaseUrl=a.apiBaseUrl||T.anthropic.baseUrl,a.apiAuthType=a.apiAuthType||"x-api-key",a.apiKeyEnvVar=a.apiKeyEnvVar||"ANTHROPIC_API_KEY");let t=Number(a.apiMaxTokens);(!Number.isFinite(t)||t<=0)&&(a.apiMaxTokens=m.apiMaxTokens);let n=Number(a.maxDocChars);return(!Number.isFinite(n)||n<1e3)&&(a.maxDocChars=m.maxDocChars),a.maxCacheEntries=Fe(a.maxCacheEntries),F[a.promptLanguage]||(a.promptLanguage=m.promptLanguage),a.minCards=ke(a.minCards,m.minCards),a.maxCards=ke(a.maxCards,m.maxCards),a.maxCardsEe(o)-Ee(l)||r.localeCompare(s)).slice(0,n.length-t).map(([r])=>r);for(let r of i)delete a[r];return i}function $(a){let e=ue(Object.assign({},m,a||{})),t=G(e.backend),n=y(e),i=b(e),r=t?(e.apiBaseUrl||n.baseUrl||w[i]?.defaultBaseUrl||"").trim().replace(/\/+$/,""):"";return P(ie({cacheSchemaVersion:S,promptVersion:ze,maxDocChars:Number(e.maxDocChars)||m.maxDocChars,promptLanguage:e.promptLanguage,minCards:e.minCards,maxCards:e.maxCards,customSystemPromptHash:P(e.customSystemPrompt||""),backend:e.backend,model:e.model,apiProvider:t?e.apiProvider:"",apiFormat:t?i:"",apiBaseUrl:r,apiAuthType:t?le(e):"",apiHeadersHash:t?P(e.apiHeaders||""):"",apiMaxTokens:t?Number(e.apiMaxTokens)||m.apiMaxTokens:0,structuredOutputVersion:1}))}function H(a,e,t){return!!a&&a.schemaVersion===S&&a.contentHash===P(e)&&a.settingsHash===$(t)}function We(a){return a==="en"?"Write title, gist, and bullets in English.":a==="auto"?"Write title, gist, and bullets in the main language of the source document.":"\u7528\u4E2D\u6587\u8F93\u51FA title\u3001gist \u548C bullets\u3002"}function Ye(a){return a==="en"?`{"cards":[
{"title":"U-shaped gains","anchor":"Who benefits from AI? Overall, it shifts the score from one to seven","gist":"AI productivity gains form a U shape, with both ends benefiting most","bullets":["Top-paid software managers benefit by accelerating existing work","Low-paid workers use AI to create new side income","Middle-layer specialists gain less because prompt precision is hard to trust","Average reported benefit is 5.1/7, with 42% describing gains as unclear"]}
]}`:`{"cards":[
{"title":"U \u578B\u6536\u76CA\u66F2\u7EBF","anchor":"\u90A3\u8C01\u53C8\u4F1A\u88AB AI \u6240\u53D7\u76CA\uFF1F\u6574\u4F53\u6765\u770B\uFF0C\u5B83\u628A\u6574\u4E2A\u5206\u6570\u53D8\u6210\u4E86\u4E00\u5206\u5230\u4E03\u5206","gist":"AI \u751F\u4EA7\u529B\u6536\u76CA\u5448 U \u578B\uFF0C\u4E24\u7AEF\u53D7\u76CA\u6700\u5927\u3001\u4E2D\u95F4\u5C42\u584C\u9677","bullets":["\u6700\u9AD8\u85AA\u5C97\u4F4D\uFF08\u8F6F\u4EF6\u7BA1\u7406\uFF09\u901A\u8FC7\u52A0\u901F\u65E2\u6709\u5DE5\u4F5C\u53D7\u76CA\u6700\u5927","\u6700\u4F4E\u85AA\u5C97\u4F4D\uFF08\u5916\u5356\u5458\u3001\u56ED\u827A\u5DE5\uFF09\u7528 AI \u5F00\u526F\u4E1A\u521B\u9020\u65B0\u6536\u5165","\u4E2D\u95F4\u5C42\u79D1\u5B66\u5BB6\u3001\u5F8B\u5E08\u6536\u76CA\u6700\u5C11\uFF0C\u90E8\u5206\u56E0\u5BF9 prompt \u7CBE\u5EA6\u4FE1\u4EFB\u4E0D\u8DB3","\u5168\u4F53\u5747\u5206 5.1/7\uFF0C42% \u62A5\u544A\u6536\u76CA\u6A21\u7CCA"]}
-]}`}function rt(a,e){return String(a||"").replace(/\{([a-zA-Z0-9_]+)\}/g,(t,r)=>Object.prototype.hasOwnProperty.call(e,r)?String(e[r]):t)}function Le(a,e){let t=Number(e.maxDocChars)||ie,r=j[e.promptLanguage]?e.promptLanguage:f.promptLanguage,n=Math.max(1,Number(e.minCards)||f.minCards),i=Math.max(n,Number(e.maxCards)||f.maxCards),s=tt(r),o=a.length>t?a.slice(0,t)+(r==="en"?`
+]}`}function qe(a,e){return String(a||"").replace(/\{([a-zA-Z0-9_]+)\}/g,(t,n)=>Object.prototype.hasOwnProperty.call(e,n)?String(e[n]):t)}function he(a,e){let t=Number(e.maxDocChars)||se,n=F[e.promptLanguage]?e.promptLanguage:m.promptLanguage,i=Math.max(1,Number(e.minCards)||m.minCards),r=Math.max(i,Number(e.maxCards)||m.maxCards),o=We(n),s=a.length>t?a.slice(0,t)+(n==="en"?`
[Document truncated]`:`
-[\u6587\u6863\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD]`):a,u='{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}',h=at(r),g={minCards:n,maxCards:i,languageInstruction:s,schema:u,example:h},l=rt(e.customSystemPrompt,g).trim(),d=`\u4F60\u662F\u4E00\u4E2A\u957F\u6587\u9605\u8BFB\u6458\u8981\u52A9\u624B\u3002\u9605\u8BFB\u5168\u6587\u540E\uFF0C\u628A\u6587\u7AE0\u5207\u6210 ${n}-${i} \u4E2A"\u81EA\u7136\u4E3B\u9898\u5355\u5143"\u2014\u2014\u4E0D\u5FC5\u5BF9\u5E94 markdown heading\uFF0C\u4EE5"\u4E00\u4E2A\u5B8C\u6574\u8BBA\u70B9\u6216\u8BDD\u9898"\u4E3A\u5355\u4F4D\u81EA\u884C\u5224\u65AD\u7C92\u5EA6\uFF1A\u77ED\u7AE0\u8282\u5408\u5E76\u3001\u957F\u7AE0\u8282\u62C6\u5206\u3002
+[\u6587\u6863\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD]`):a,l='{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}',d=Ye(n),f={minCards:i,maxCards:r,languageInstruction:o,schema:l,example:d},p=qe(e.customSystemPrompt,f).trim(),h=`\u4F60\u662F\u4E00\u4E2A\u957F\u6587\u9605\u8BFB\u6458\u8981\u52A9\u624B\u3002\u9605\u8BFB\u5168\u6587\u540E\uFF0C\u628A\u6587\u7AE0\u5207\u6210 ${i}-${r} \u4E2A"\u81EA\u7136\u4E3B\u9898\u5355\u5143"\u2014\u2014\u4E0D\u5FC5\u5BF9\u5E94 markdown heading\uFF0C\u4EE5"\u4E00\u4E2A\u5B8C\u6574\u8BBA\u70B9\u6216\u8BDD\u9898"\u4E3A\u5355\u4F4D\u81EA\u884C\u5224\u65AD\u7C92\u5EA6\uFF1A\u77ED\u7AE0\u8282\u5408\u5E76\u3001\u957F\u7AE0\u8282\u62C6\u5206\u3002
**\u6BCF\u5F20\u5361\u7247\u7684\u7ED3\u6784\uFF1A\u4E00\u53E5\u8BDD\u9886\u8BFB + \u82E5\u5E72\u6761 bullet\u3002bullet \u627F\u8F7D\u7EC6\u8282\uFF0Cgist \u662F\u4E00\u53E5\u8BDD\u5BFC\u8BFB\u3002**
\u8BED\u8A00\uFF1A
-- ${s}
+- ${o}
\u5BF9\u6BCF\u4E2A\u5355\u5143\u8F93\u51FA\uFF1A
@@ -37,32 +37,32 @@ ${(a.text||"").slice(0,500)}`)}}async function Pe(a,e,t,r,n){let i;try{i=await a
- \u4E25\u683C\u53EA\u8F93\u51FA JSON\uFF0C\u65E0 markdown fence\u3001\u65E0\u89E3\u91CA\u3001\u65E0 tool call
\u8F93\u51FA\u683C\u5F0F\uFF1A
-${u}
+${l}
\u793A\u4F8B\uFF1A
-${h}`,m=l?`${l}
+${d}`,g=p?`${p}
\u4E0D\u53EF\u8986\u76D6\u7684\u8F93\u51FA\u5951\u7EA6\uFF1A
-- \u5FC5\u987B\u8F93\u51FA ${n}-${i} \u5F20 cards\u3002
-- ${s}
+- \u5FC5\u987B\u8F93\u51FA ${i}-${r} \u5F20 cards\u3002
+- ${o}
- anchor \u5FC5\u987B\u4ECE\u539F\u6587\u9010\u5B57\u590D\u5236\uFF0C\u5FC5\u987B\u80FD\u5728\u539F\u6587 exact substring match \u627E\u5230\u3002
- \u4E25\u683C\u53EA\u8F93\u51FA JSON\uFF0C\u65E0 markdown fence\u3001\u65E0\u89E3\u91CA\u3001\u65E0 tool call\u3002
-- JSON shape: ${u}`:d,p=r==="en"?`Source document:
+- JSON shape: ${l}`:h,u=n==="en"?`Source document:
-${o}`:`\u4EE5\u4E0B\u662F\u9700\u8981\u5904\u7406\u7684\u6587\u6863\u5168\u6587\uFF1A
+${s}`:`\u4EE5\u4E0B\u662F\u9700\u8981\u5904\u7406\u7684\u6587\u6863\u5168\u6587\uFF1A
-${o}`;return{system:m,user:p}}function we(a,e){if(!e)return-1;let t=l=>l.replace(/\s+/g," ").trim(),r=l=>{let d=[],m=[],p=!1;for(let k=0;k0;continue}p&&(d.push(" "),m.push(k),p=!1),d.push(be),m.push(k)}return{text:d.join(""),map:m}},n=l=>{if(!l)return-1;let d=a.indexOf(l);if(d===-1)return-1;let m=0;for(let p=0;p=0||(i=n(e.trim()),i>=0))return i;for(let l of[60,40,25,15]){let d=e.trim().slice(0,l);if(i=n(d),i>=0)return i}let s=r(a),o=t(e).slice(0,30);if(!o)return-1;let u=s.text.indexOf(o);if(u===-1)return-1;let h=s.map[u];if(h==null)return-1;let g=0;for(let l=0;l{let o,u=!1,h,g=p=>{u||(u=!0,h&&clearTimeout(h),s(p))},l=p=>{u||(u=!0,h&&clearTimeout(h),i(p))};try{o=(0,Me.spawn)(a,e,{stdio:["pipe","pipe","pipe"],env:{...process.env,PATH:[process.env.PATH||"","/usr/local/bin","/opt/homebrew/bin",A.default.join(Q.default.homedir(),".local/bin"),A.default.join(Q.default.homedir(),".claude/local")].filter(Boolean).join(":")}})}catch(p){return s(new Error(`\u65E0\u6CD5\u542F\u52A8 ${a}: ${p.message}`))}let d="",m="";if(h=setTimeout(()=>{try{o.kill("SIGKILL")}catch{}g(new Error(`CLI \u8D85\u65F6 (${r}ms)`))},r),n&&n.onCancel(()=>{try{o.kill("SIGKILL")}catch{}g(new x(n.key))}),o.stdout.on("data",p=>{d+=p.toString("utf8")}),o.stderr.on("data",p=>{m+=p.toString("utf8")}),o.on("error",p=>{g(new Error(`CLI \u542F\u52A8\u9519\u8BEF: ${p.message}\uFF08\u5C1D\u8BD5\u5728\u8BBE\u7F6E\u91CC\u586B\u7EDD\u5BF9\u8DEF\u5F84\uFF09`))}),o.on("close",p=>{if(!u){if(p!==0)return g(new Error(`CLI \u9000\u51FA\u7801 ${p}
+${s}`;return{system:g,user:u}}var K="record_parallel_reader_cards";function Xe(a){let e=[],t=-1,n=0,i=!1,r=!1;for(let o=0;o0&&n--,n===0&&t>=0&&(e.push(a.slice(t,o+1)),t=-1))}return e}function me(a){let e=(a||"").trim();if(!e)return e;try{return JSON.parse(e),e}catch{}let t=e.match(/```(?:json)?\s*([\s\S]*?)```/);if(t){let i=t[1].trim();try{return JSON.parse(i),i}catch{}}let n=Xe(e);n.sort((i,r)=>r.length-i.length);for(let i of n)try{return JSON.parse(i),i}catch{}return e}function A(a){let e=me(a),t;try{t=JSON.parse(e)}catch{throw new Error(`LLM \u8FD4\u56DE\u975E JSON\uFF1A
+`+(a||"").slice(0,500))}return M(t)}function M(a){return(a&&Array.isArray(a.cards)?a.cards:[]).filter(t=>t&&typeof t=="object").map(t=>({title:typeof t.title=="string"?t.title:"(\u65E0\u6807\u9898)",anchor:typeof t.anchor=="string"?t.anchor:"",gist:typeof t.gist=="string"?t.gist:"",bullets:Array.isArray(t.bullets)?t.bullets.filter(n=>typeof n=="string"):[]}))}function L(a){let e={type:"object",properties:{title:{type:"string"},anchor:{type:"string"},gist:{type:"string"},bullets:{type:"array",items:{type:"string"}}},required:["title","anchor","gist","bullets"]},t={type:"object",properties:{cards:{type:"array",items:e}},required:["cards"]};return a&&(e.additionalProperties=!1,t.additionalProperties=!1),t}function De(){return{type:"json_schema",json_schema:{name:"parallel_reader_cards",strict:!0,schema:L(!0)}}}function Ie(){return{format:{type:"json_schema",name:"parallel_reader_cards",strict:!0,schema:L(!0)}}}function Me(){return{name:K,description:"Return the generated Parallel Reader summary cards.",input_schema:L(!0)}}function ge(a,e){let t=a.replace(/\/+$/,"");for(let n of e)if(t.endsWith(n))return t;return t+e[0]}function Ze(a){let e=(a||"").trim();if(!e)return{};if(e.startsWith("{")){let n;try{n=JSON.parse(e)}catch(r){throw new Error("\u81EA\u5B9A\u4E49 headers JSON \u89E3\u6790\u5931\u8D25\uFF1A"+r.message)}if(!n||typeof n!="object"||Array.isArray(n))throw new Error("\u81EA\u5B9A\u4E49 headers JSON \u5FC5\u987B\u662F\u5BF9\u8C61");let i={};for(let[r,o]of Object.entries(n))typeof o=="string"&&r.trim()&&(i[r.trim()]=o);return i}let t={};for(let n of e.split(/\r?\n/)){let i=n.trim();if(!i||i.startsWith("#"))continue;let r=i.indexOf(":");if(r<=0)throw new Error("\u81EA\u5B9A\u4E49 headers \u6BCF\u884C\u683C\u5F0F\u5E94\u4E3A `Header-Name: value`");let o=i.slice(0,r).trim(),s=i.slice(r+1).trim();o&&(t[o]=s)}return t}function Qe(a){let e=le(a);if(e==="none")return{};let t=Ne(a);if(!t){let n=(a.apiKeyEnvVar||y(a).envVar||"").trim(),i=n?` \u6216\u73AF\u5883\u53D8\u91CF ${n}`:"";throw new Error(`API key \u672A\u8BBE\u7F6E\u3002\u8BF7\u5728\u8BBE\u7F6E\u91CC\u586B\u5199 API Key${i}\u3002`)}return e==="bearer"?{authorization:`Bearer ${t}`}:e==="x-api-key"?{"x-api-key":t}:e==="x-goog-api-key"?{"x-goog-api-key":t}:e==="api-key"?{"api-key":t}:{authorization:`Bearer ${t}`}}function q(a,e){return{"content-type":"application/json",...Qe(a),...e||{},...Ze(a.apiHeaders)}}function et(a,e){if(a.json&&typeof a.json=="object")return a.json;try{return JSON.parse(a.text||"{}")}catch{throw new Error(`${e} \u8FD4\u56DE\u975E JSON\uFF1A
+${(a.text||"").slice(0,500)}`)}}async function Le(a,e,t,n,i){let r;try{r=await a({url:t,method:"POST",headers:n,body:JSON.stringify(i),throw:!1})}catch(o){throw new Error(`${e} \u8BF7\u6C42\u5931\u8D25\uFF1A`+(o.message||o))}if(r.status>=400)throw new Error(`${e} API ${r.status}: ${(r.text||"").slice(0,500)}`);return et(r,e)}function tt(a){let e=String(a&&a.message?a.message:a);return/API (400|404|422):/.test(e)?/response_format|json_schema|responseJsonSchema|responseMimeType|tools?|tool_choice|unsupported|unrecognized|unknown|schema/i.test(e):!1}async function X(a,e,t,n,i,r){try{return await Le(a,e,t,n,i)}catch(o){if(!r||!tt(o))throw o;return console.warn(`[parallel-reader] ${e} structured output rejected; retrying without structured output`,o),Le(a,e+" fallback",t,n,r)}}function fe(a){return typeof a=="string"?a:Array.isArray(a)?a.map(e=>typeof e=="string"?e:e&&typeof e=="object"&&(e.text||e.output_text)||"").join(""):a&&typeof a=="object"&&(a.text||a.output_text)||""}function at(a){if(typeof a.output_text=="string")return a.output_text;let e=[],t=n=>{if(n&&typeof n!="string"){if(Array.isArray(n)){n.forEach(t);return}typeof n=="object"&&(typeof n.text=="string"&&e.push(n.text),typeof n.output_text=="string"&&e.push(n.output_text),n.type==="output_text"&&typeof n.content=="string"&&e.push(n.content),n.content&&t(n.content),n.output&&t(n.output))}};return t(a.output),e.join("")}function ye(a){let e=y(a),t=w[b(a)]||{};return e.tokenLimitField||t.tokenLimitField||"max_tokens"}function J(a,e,t,n){let i=!n||n.structured!==!1,r={model:I(t),max_tokens:Number(t.apiMaxTokens)||4096,system:a,messages:[{role:"user",content:e}]};return i&&(r.tools=[Me()],r.tool_choice={type:"tool",name:K}),r}function z(a,e,t,n){let i=!n||n.structured!==!1,r={model:I(t),messages:[{role:"system",content:a},{role:"user",content:e}]};return r[ye(t)]=Number(t.apiMaxTokens)||4096,i&&(r.response_format=De()),r}function W(a,e,t,n){let i=!n||n.structured!==!1,r={model:I(t),instructions:a,input:e,max_output_tokens:Number(t.apiMaxTokens)||4096};return i&&(r.text=Ie()),r}function Y(a,e,t,n){let i=!n||n.structured!==!1,r={temperature:0,maxOutputTokens:Number(t.apiMaxTokens)||4096};return i&&(r.responseMimeType="application/json",r.responseJsonSchema=L(!1)),{systemInstruction:{parts:[{text:a}]},contents:[{role:"user",parts:[{text:e}]}],generationConfig:r}}function nt(a){let t=(Array.isArray(a&&a.content)?a.content:[]).find(n=>n&&n.type==="tool_use"&&n.name===K);return t?typeof t.input=="string"?A(t.input):t.input&&typeof t.input=="object"?M(t.input):[]:null}async function rt(a,e,t,n){let i=ge(D(n),["/messages"]),r=await X(a,"Anthropic-compatible",i,q(n,{"anthropic-version":"2023-06-01"}),J(e,t,n),J(e,t,n,{structured:!1})),o=nt(r);if(o)return o;let s=(r.content||[]).map(l=>fe(l)).join("").trim();return A(s)}async function it(a,e,t,n){let i=ge(D(n),["/chat/completions"]),o=((await X(a,"OpenAI-compatible Chat",i,q(n),z(e,t,n),z(e,t,n,{structured:!1}))).choices||[])[0]||{},s=fe(o.message?.content||o.text||"").trim();return A(s)}async function st(a,e,t,n){let i=ge(D(n),["/responses"]),r=await X(a,"OpenAI Responses",i,q(n),W(e,t,n),W(e,t,n,{structured:!1}));return A(at(r).trim())}async function ot(a,e,t,n){let i=encodeURIComponent(I(n)),r=D(n);/:generateContent(?:\?|$)/.test(r)||(r=`${r.replace(/\/+$/,"")}/models/${i}:generateContent`);let o=q(n),f=((((await X(a,"Google Gemini",r,o,Y(e,t,n),Y(e,t,n,{structured:!1}))).candidates||[])[0]||{}).content?.parts||[]).map(p=>fe(p)).join("").trim();return A(f)}async function O(a,e,t,n){switch(b(n)){case"openai-chat":return it(a,e,t,n);case"openai-responses":return st(a,e,t,n);case"google-generative-ai":return ot(a,e,t,n);case"anthropic-messages":default:return rt(a,e,t,n)}}async function Oe(a,e){await O(a,'\u53EA\u8F93\u51FA JSON\uFF1A{"cards":[]}','\u8FDE\u901A\u6027\u6D4B\u8BD5\uFF1A\u8BF7\u539F\u6837\u8F93\u51FA {"cards":[]}',e);let t=b(e);return`${y(e).label} / ${w[t].label}`}var k=class extends Error{constructor(e){super("\u8BE5\u7B14\u8BB0\u6B63\u5728\u751F\u6210\u5BF9\u7167\u7B14\u8BB0"),this.name="GenerationJobAlreadyRunningError",this.code="already-running",this.key=e}},x=class extends Error{constructor(e){super("\u751F\u6210\u5DF2\u53D6\u6D88"),this.name="GenerationJobCancelledError",this.code="cancelled",this.key=e}},we=class{constructor(e){this.key=e,this.phase="queued",this.cancelled=!1,this.startedAt=new Date().toISOString(),this.updatedAt=this.startedAt,this._cancelHandlers=[]}setPhase(e){this.phase=e,this.updatedAt=new Date().toISOString()}onCancel(e){if(typeof e=="function"){if(this.cancelled){e();return}this._cancelHandlers.push(e)}}cancel(){if(this.cancelled)return!1;this.cancelled=!0,this.setPhase("cancelled");for(let e of this._cancelHandlers.splice(0))try{e()}catch{}return!0}throwIfCancelled(){if(this.cancelled)throw new x(this.key)}},_=class{constructor(){this.jobs=new Map}get(e){return this.jobs.get(e)||null}isRunning(e){return this.jobs.has(e)}async start(e,t){if(this.jobs.has(e))throw new k(e);let n=new we(e);this.jobs.set(e,n);try{n.setPhase("running");let i=await t(n);return n.throwIfCancelled(),i}catch(i){throw n.cancelled&&!(i instanceof x)?new x(e):i}finally{this.jobs.delete(e)}}cancel(e){let t=this.jobs.get(e);return t?t.cancel():!1}};function Ce(a){if(a instanceof x||a?.code==="cancelled")return"cancelled";let e=String(a&&a.message?a.message:a);return/api key|unauthorized|401|403|认证|权限/i.test(e)?"auth":/timeout|超时|timed out/i.test(e)?"timeout":/429|rate limit|too many requests/i.test(e)?"rate-limit":/非 JSON|json_schema|schema|structured/i.test(e)?"schema":/model 未设置|base url|配置|config/i.test(e)?"config":"unknown"}var C="parallel-reader-view";function te(a,e){if(e&&e.trim())return e.trim();let t=ee.default.homedir(),n=[v.default.join(t,"bin",a),v.default.join(t,".local/bin",a),v.default.join(t,".claude/local",a),v.default.join(t,".codex/bin",a),v.default.join(t,".bun/bin",a),v.default.join(t,".npm-global/bin",a),v.default.join(t,".cargo/bin",a),"/opt/homebrew/bin/"+a,"/usr/local/bin/"+a];for(let i of n)try{if(Re.default.existsSync(i))return i}catch{}return a}function ae(a,e,t,n,i){return new Promise((r,o)=>{let s,l=!1,d,f=u=>{l||(l=!0,d&&clearTimeout(d),o(u))},p=u=>{l||(l=!0,d&&clearTimeout(d),r(u))};try{s=(0,Ve.spawn)(a,e,{stdio:["pipe","pipe","pipe"],env:{...process.env,PATH:[process.env.PATH||"","/usr/local/bin","/opt/homebrew/bin",v.default.join(ee.default.homedir(),".local/bin"),v.default.join(ee.default.homedir(),".claude/local")].filter(Boolean).join(":")}})}catch(u){return o(new Error(`Failed to start ${a}: ${u.message}`))}let h="",g="";if(d=setTimeout(()=>{try{s.kill("SIGKILL")}catch{}f(new Error(`CLI timed out (${n}ms)`))},n),i&&i.onCancel(()=>{try{s.kill("SIGKILL")}catch{}f(new x(i.key))}),s.stdout.on("data",u=>{h+=u.toString("utf8")}),s.stderr.on("data",u=>{g+=u.toString("utf8")}),s.on("error",u=>{f(new Error(`CLI startup error: ${u.message}. Try setting an absolute CLI path.`))}),s.on("close",u=>{if(!l){if(u!==0)return f(new Error(`CLI exited with code ${u}
stderr:
-${m.slice(0,1e3)}`));l({stdout:d,stderr:m})}}),t)try{o.stdin.write(t),o.stdin.end()}catch{}else try{o.stdin.end()}catch{}})}async function it(a,e,t,r){let n=Z("claude",t.cliPath),i=["-p","--output-format","json","--append-system-prompt",a,"--disallowed-tools","Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch,TodoWrite,Task"];t.model&&i.push("--model",t.model);let{stdout:s}=await ee(n,i,e,t.cliTimeoutMs,r),o;try{o=JSON.parse(s)}catch{throw new Error(`claude CLI \u8F93\u51FA\u975E JSON envelope\uFF1A
-`+s.slice(0,500))}let u=o.result||o.content||"";return C(u)}async function nt(a,e,t,r){let n=Z("codex",t.cliPath),i=`<>
+${g.slice(0,1e3)}`));p({stdout:h,stderr:g})}}),t)try{s.stdin.write(t),s.stdin.end()}catch{}else try{s.stdin.end()}catch{}})}async function ct(a,e,t,n){let i=te("claude",t.cliPath),r=["-p","--output-format","json","--append-system-prompt",a,"--disallowed-tools","Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch,TodoWrite,Task"];t.model&&r.push("--model",t.model);let{stdout:o}=await ae(i,r,e,t.cliTimeoutMs,n),s;try{s=JSON.parse(o)}catch{throw new Error(`claude CLI returned a non-JSON envelope:
+`+o.slice(0,500))}let l=s.result||s.content||"";return A(l)}async function lt(a,e,t,n){let i=te("codex",t.cliPath),r=`<>
${a}
<>
${e}
-\u76F4\u63A5\u8F93\u51FA JSON\uFF0C\u4E0D\u8981\u4EFB\u4F55\u89E3\u91CA\u3002`,s=["exec","--skip-git-repo-check","-"],{stdout:o}=await ee(n,s,i,t.cliTimeoutMs,r);return C(o)}async function st(a){if(a.backend==="codex"){let e=Z("codex",a.cliPath),{stdout:t}=await ee(e,["--version"],"",1e4);return`codex @ ${e}
-${t.trim()}`}if(a.backend==="claude-code"){let e=Z("claude",a.cliPath),{stdout:t}=await ee(e,["--version"],"",1e4);return`claude @ ${e}
-${t.trim()}`}if(U(a.backend))return _e(c.requestUrl,a);throw new Error("\u672A\u77E5 backend\uFF1A"+a.backend)}async function ot(a,e,t){let{system:r,user:n}=Le(a,e),i;switch(e.backend){case"codex":i=await nt(r,n,e,t);break;case"api":i=await V(c.requestUrl,r,n,e);break;case"anthropic-api":i=await V(c.requestUrl,r,n,e);break;case"claude-code":default:i=await it(r,n,e,t);break}let s=i.map(o=>({title:o.title,level:2,anchor:o.anchor,gist:o.gist,startLine:we(a,o.anchor),bullets:o.bullets}));return s.sort((o,u)=>o.startLine<0&&u.startLine<0?0:o.startLine<0?1:u.startLine<0?-1:o.startLine-u.startLine),s}function q(a,e,t,r){let n=a.createEl("button",{cls:"parallel-reader-icon-button",attr:{type:"button","aria-label":t}});return n.title=t,typeof c.setIcon=="function"?(0,c.setIcon)(n,e):n.textContent=t,n.addEventListener("click",async i=>{i.preventDefault(),i.stopPropagation();try{await r()}catch(s){console.error(s),new c.Notice(`${t}\u5931\u8D25\uFF1A`+(s.message||s))}}),n}function Oe(a,e,t,r,n){let i=a.createEl("button",{cls:n||"parallel-reader-text-button",attr:{type:"button"}});return e&&typeof c.setIcon=="function"&&(0,c.setIcon)(i,e),i.createSpan({text:t}),i.addEventListener("click",async s=>{s.preventDefault(),s.stopPropagation();try{await r()}catch(o){console.error(o),new c.Notice(`${t}\u5931\u8D25\uFF1A`+(o.message||o))}}),i}async function X(a,e){try{await navigator.clipboard.writeText(a),new c.Notice(e)}catch(t){new c.Notice("\u590D\u5236\u5931\u8D25\uFF1A"+(t.message||t))}}var fe=class extends c.ItemView{constructor(e,t){super(e),this.plugin=t,this.sections=[],this.sourceFile=null,this.cards=[],this.activeIdx=-1,this.loadingMessage="",this.errorMessage=""}getViewType(){return b}getDisplayText(){return"\u5BF9\u7167\u9605\u8BFB\u7B14\u8BB0"}getIcon(){return"book-open"}async onOpen(){let e=this.containerEl.children[1];e.empty(),e.addClass("parallel-reader-container"),this.renderEmpty()}async onClose(){}renderEmpty(){this.sourceFile=null,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage="";let e=this.containerEl.children[1];e.empty();let t=e.createDiv({cls:"parallel-reader-empty"});t.createEl("h3",{text:"\u5BF9\u7167\u9605\u8BFB\u7B14\u8BB0"}),t.createEl("p",{text:"\u6253\u5F00\u4E00\u7BC7\u7B14\u8BB0\uFF0C\u7136\u540E\u8FD0\u884C\u547D\u4EE4\uFF1A"}),t.createEl("code",{text:"Parallel Reader: \u4E3A\u5F53\u524D\u7B14\u8BB0\u751F\u6210\u5BF9\u7167\u7B14\u8BB0"})}async loadFor(e,t,r){this.sourceFile=e,this.sections=t,this.stale=!!r,this.loadingMessage="",this.errorMessage="",this.render()}async renderLoading(e,t){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage=t||"\u6B63\u5728\u751F\u6210\u5BF9\u7167\u7B14\u8BB0...",this.errorMessage="",this.render()}async renderError(e,t){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage=t||"\u751F\u6210\u5931\u8D25",this.render()}renderEmptyWithHint(e){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage="";let t=this.containerEl.children[1];t.empty();let r=t.createDiv({cls:"parallel-reader-empty"});r.createEl("h3",{text:e.basename}),r.createEl("p",{text:"\u8BE5\u7B14\u8BB0\u5C1A\u65E0\u5BF9\u7167\u7B14\u8BB0\u7F13\u5B58\u3002\u8FD0\u884C\u547D\u4EE4\uFF1A"}),r.createEl("code",{text:"Parallel Reader: \u4E3A\u5F53\u524D\u7B14\u8BB0\u751F\u6210\u5BF9\u7167\u7B14\u8BB0"})}render(){let e=this.containerEl.children[1];e.empty();let r=e.createDiv({cls:"parallel-reader-header"}).createDiv({cls:"parallel-reader-header-row"});r.createEl("div",{text:this.sourceFile?.basename||"",cls:"parallel-reader-title"});let n=r.createDiv({cls:"parallel-reader-actions"});if(this.sourceFile&&(this.plugin.isGeneratingFile(this.sourceFile)?q(n,"square","\u53D6\u6D88\u751F\u6210",()=>this.plugin.cancelGenerationForFile(this.sourceFile)):q(n,"refresh-cw","\u91CD\u65B0\u751F\u6210",()=>this.plugin.runForFile(this.sourceFile,!0)),q(n,"copy","\u590D\u5236\u5168\u90E8 Markdown",()=>this.plugin.copyCurrentViewMarkdown()),q(n,"download","\u5BFC\u51FA\u5230 Vault",()=>this.exportToVault())),this.stale){let o=e.createDiv({cls:"parallel-reader-stale-banner"});o.createSpan({text:"\u6E90\u7B14\u8BB0\u6216\u751F\u6210\u914D\u7F6E\u5DF2\u4FEE\u6539\uFF0C\u5F53\u524D\u662F\u65E7\u7F13\u5B58\u3002"}),Oe(o,"refresh-cw","\u91CD\u65B0\u751F\u6210",()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-stale-button")}if(this.loadingMessage){let o=e.createDiv({cls:"parallel-reader-state parallel-reader-loading"});o.createDiv({cls:"parallel-reader-spinner"}),o.createEl("div",{text:this.loadingMessage,cls:"parallel-reader-state-title"}),o.createEl("div",{text:"\u53EF\u4EE5\u7EE7\u7EED\u9605\u8BFB\u539F\u6587\uFF0C\u751F\u6210\u5B8C\u6210\u540E\u4F1A\u81EA\u52A8\u5237\u65B0\u53F3\u4FA7\u5361\u7247\u3002",cls:"parallel-reader-state-subtitle"});return}if(this.errorMessage){let o=e.createDiv({cls:"parallel-reader-state parallel-reader-error"});o.createEl("div",{text:"\u751F\u6210\u5931\u8D25",cls:"parallel-reader-state-title"}),o.createEl("div",{text:this.errorMessage,cls:"parallel-reader-state-subtitle"}),Oe(o,"refresh-cw","\u91CD\u65B0\u751F\u6210",()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-text-button");return}let i=e.createDiv({cls:"parallel-reader-cards"});this.cards=[];let s=this.sourceFile?.path||"";this.sections.forEach((o,u)=>{let h=i.createDiv({cls:"parallel-reader-card"});h.dataset.idx=String(u),o.startLine<0&&h.addClass("parallel-reader-card-unanchored");let g=h.createEl("div",{cls:"parallel-reader-card-title"});if(g.createSpan({text:o.title}),o.startLine<0&&g.createEl("span",{text:" \u26A0",cls:"parallel-reader-warn",title:"anchor \u5339\u914D\u5931\u8D25\uFF0C\u65E0\u6CD5\u6EDA\u52A8\u8054\u52A8"}),o.gist){let d=h.createEl("div",{cls:"parallel-reader-gist"});c.MarkdownRenderer.render(this.app,o.gist,d,s,this).catch(()=>{d.setText(o.gist)})}let l=o.bullets||[];if(l.length>0){let d=h.createEl("div",{cls:"parallel-reader-bullets-md"}),m=l.map(p=>`- ${p}`).join(`
-`);c.MarkdownRenderer.render(this.app,m,d,s,this).catch(()=>{d.setText(m)})}else o.gist||h.createEl("div",{cls:"parallel-reader-empty-li",text:"\uFF08\u672A\u751F\u6210\uFF09"});h.addEventListener("click",d=>{let m=window.getSelection();if(m&&m.toString().length>0)return;let p=d.target;p&&p.tagName==="A"||o.startLine>=0&&this.plugin.scrollEditorToLine(o.startLine,this.sourceFile)}),h.addEventListener("contextmenu",d=>{d.preventDefault();let m=new c.Menu;m.addItem(p=>p.setTitle("\u590D\u5236 Markdown").setIcon("copy").onClick(()=>X(te(o),"\u5DF2\u590D\u5236 Markdown"))),m.addItem(p=>p.setTitle("\u590D\u5236\u7EAF\u6587\u672C").setIcon("clipboard-copy").onClick(()=>X(xe(o),"\u5DF2\u590D\u5236\u7EAF\u6587\u672C"))),o.anchor&&m.addItem(p=>p.setTitle("\u590D\u5236 anchor \u5F15\u7528").setIcon("quote-glyph").onClick(()=>X(o.anchor,"\u5DF2\u590D\u5236\u5F15\u7528\u539F\u6587"))),m.addSeparator(),o.startLine>=0&&m.addItem(p=>p.setTitle("\u8DF3\u8F6C\u5230\u539F\u6587").setIcon("arrow-right").onClick(()=>this.plugin.scrollEditorToLine(o.startLine,this.sourceFile))),m.showAtMouseEvent(d)}),this.cards.push(h)})}setActiveSection(e){e!==this.activeIdx&&(this.activeIdx>=0&&this.cards[this.activeIdx]&&this.cards[this.activeIdx].removeClass("is-active"),this.activeIdx=e,e>=0&&this.cards[e]&&(this.cards[e].addClass("is-active"),this.cards[e].scrollIntoView({block:"nearest",behavior:"smooth"})))}async exportToVault(){if(!this.sourceFile)return;let e=this.plugin.settings.exportFolder.replace(/\/$/,""),t=`${this.sourceFile.basename} - \u5BF9\u7167\u7B14\u8BB0.md`,r=`${e}/${t}`,n=["---",`source: [[${this.sourceFile.basename}]]`,`generated: ${new Date().toISOString().slice(0,10)}`,"tool: parallel-reader","---","",R(`${this.sourceFile.basename} \xB7 \u5BF9\u7167\u7B14\u8BB0`,this.sections),""].join(`
-`),i=this.plugin.app;if(!i.vault.getAbstractFileByPath(e))try{await i.vault.createFolder(e)}catch{}let o=i.vault.getAbstractFileByPath(r);o instanceof c.TFile?await i.vault.modify(o,n):await i.vault.create(r,n),new c.Notice(`\u5DF2\u5BFC\u51FA \u2192 ${r}`)}},ge=class extends c.Plugin{async onload(){await this.loadSettings(),this.jobs=new L,this.addRibbonIcon("book-open","\u6253\u5F00\u5BF9\u7167\u7B14\u8BB0\u9762\u677F",async()=>{let e=this.getActiveView();await this.ensureView(),e?.file&&await this.syncViewToFile(e.file)}),this.registerView(b,e=>new fe(e,this)),this.addCommand({id:"parallel-reader-run",name:"\u4E3A\u5F53\u524D\u7B14\u8BB0\u751F\u6210\u5BF9\u7167\u7B14\u8BB0\uFF08\u7F13\u5B58\u4F18\u5148\uFF09",callback:()=>this.runForActiveFile(!1)}),this.addCommand({id:"parallel-reader-regen",name:"\u5F3A\u5236\u91CD\u65B0\u751F\u6210\uFF08\u7ED5\u8FC7\u7F13\u5B58\uFF09",callback:()=>this.runForActiveFile(!0)}),this.addCommand({id:"parallel-reader-open-view",name:"\u6253\u5F00\u5BF9\u7167\u7B14\u8BB0\u9762\u677F",callback:async()=>{let e=this.getActiveView();await this.ensureView(),e?.file&&this.syncViewToFile(e.file)}}),this.addCommand({id:"parallel-reader-export-current",name:"\u5BFC\u51FA\u5F53\u524D\u5BF9\u7167\u7B14\u8BB0\u5230 Vault",callback:()=>this.exportCurrentView()}),this.addCommand({id:"parallel-reader-copy-current-markdown",name:"\u590D\u5236\u5F53\u524D\u5BF9\u7167\u7B14\u8BB0 Markdown",callback:()=>this.copyCurrentViewMarkdown()}),this.addCommand({id:"parallel-reader-cancel-current",name:"\u53D6\u6D88\u5F53\u524D\u5BF9\u7167\u7B14\u8BB0\u751F\u6210",callback:()=>this.cancelActiveGeneration()}),this.addCommand({id:"parallel-reader-clear-current",name:"\u6E05\u9664\u5F53\u524D\u7B14\u8BB0\u7684\u7F13\u5B58",callback:async()=>{let e=this.getActiveView();if(!e?.file)return new c.Notice("\u6CA1\u6709\u5F53\u524D\u7B14\u8BB0");await this.cacheDelete(e.file.path),new c.Notice("\u5DF2\u6E05\u9664\u7F13\u5B58\uFF1A"+e.file.basename)}}),this.addCommand({id:"parallel-reader-clear-all",name:"\u6E05\u9664\u6240\u6709\u7F13\u5B58",callback:async()=>{let e=Object.keys(this.cache).length;await this.cacheClear(),new c.Notice(`\u5DF2\u6E05\u9664 ${e} \u6761\u7F13\u5B58`)}}),this.addSettingTab(new ye(this.app,this)),this.registerEvent(this.app.workspace.on("active-leaf-change",()=>this.bindScrollSync())),this.registerEvent(this.app.workspace.on("file-open",e=>{e&&this.syncViewToFile(e)})),this.registerEvent(this.app.workspace.on("file-menu",(e,t)=>this.addFileMenuItems(e,t))),this.registerEvent(this.app.vault.on("rename",(e,t)=>this.handleFileRename(e,t))),this.registerEvent(this.app.vault.on("delete",e=>this.handleFileDelete(e))),this.bindScrollSync()}async onunload(){await this.flushSettingsSave(),this.app.workspace.detachLeavesOfType(b)}async loadSettings(){let t=(await this.loadData()||{}).settings||{};this.settings=ce(Object.assign({},f,t)),await this.loadCache()}async saveSettings(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null),await this.saveData({settings:this.settings})}saveSettingsDebounced(e=400){this._settingsSaveTimer&&clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=setTimeout(()=>{this._settingsSaveTimer=null,this.saveSettings().catch(t=>console.error("[parallel-reader] failed to save settings",t))},e)}async flushSettingsSave(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null,await this.saveSettings())}async saveCache(){this.pruneCache(),await this.writeCacheFile()}cacheFilePath(){let e=this.app.vault.configDir||".obsidian",t=this.manifest?.id||"parallel-reader";return`${e}/plugins/${t}/cache.json`}async ensurePluginDataDir(){let e=this.app.vault.adapter,t=this.app.vault.configDir||".obsidian",r=this.manifest?.id||"parallel-reader",n=`${t}/plugins/${r}`;try{if(typeof e.exists=="function"&&await e.exists(n))return;await e.mkdir(n)}catch{}}async readCacheFile(){let e=this.app.vault.adapter;try{let t=await e.read(this.cacheFilePath()),r=JSON.parse(t);if(r&&typeof r=="object"&&r.entries&&typeof r.entries=="object")return r.entries}catch(t){let r=String(t?.message||t||"");/not found|does not exist|ENOENT/i.test(r)||console.warn("[parallel-reader] failed to read cache.json",t)}return{}}async writeCacheFile(){await this.ensurePluginDataDir(),await this.app.vault.adapter.write(this.cacheFilePath(),JSON.stringify({version:1,entries:this.cache},null,2))}async loadCache(){let e=await this.readCacheFile();this.cache=e,this.pruneCache().length>0&&await this.writeCacheFile()}pruneCache(){return le(this.cache,this.settings?.maxCacheEntries||_)}async pruneCacheIfNeeded(){let e=this.pruneCache();return e.length>0&&await this.writeCacheFile(),e}cacheGet(e){let t=this.cache[e]||null;return t&&(t.lastAccessedAt=new Date().toISOString()),t}async cachePut(e,t,r,n){let i=new Date().toISOString();this.cache[e]={schemaVersion:P,contentHash:S(t),settingsHash:B(n||this.settings),cards:r,generatedAt:i,lastAccessedAt:i},await this.saveCache()}async cacheDelete(e){this.cache[e]&&(delete this.cache[e],await this.saveCache())}async cacheClear(){this.cache={},await this.saveCache()}async ensureView(){let{workspace:e}=this.app,t=e.getLeavesOfType(b)[0];return t||(t=e.getRightLeaf(!1),await t.setViewState({type:b,active:!0})),e.revealLeaf(t),t.view}getActiveView(){return this.app.workspace.getActiveViewOfType(c.MarkdownView)}isGeneratingFile(e){return!!e&&!!e.path&&this.jobs.isRunning(e.path)}cancelGenerationForFile(e){if(!e||!e.path)return new c.Notice("\u5F53\u524D\u6CA1\u6709\u53EF\u53D6\u6D88\u7684\u751F\u6210\u4EFB\u52A1"),!1;let t=this.jobs.cancel(e.path);return new c.Notice(t?"\u5DF2\u8BF7\u6C42\u53D6\u6D88\u751F\u6210":"\u5F53\u524D\u6CA1\u6709\u53EF\u53D6\u6D88\u7684\u751F\u6210\u4EFB\u52A1"),t}viewIsShowingFile(e,t){return!!e&&!!t&&e.sourceFile?.path===t.path}activeFileStillMatches(e){let t=this.getActiveView();return!t?.file||t.file.path===e.path}cancelActiveGeneration(){let e=this.getActiveView();if(e?.file&&this.cancelGenerationForFile(e.file))return;let t=this.app.workspace.getLeavesOfType(b)[0]?.view;t?.sourceFile?this.cancelGenerationForFile(t.sourceFile):new c.Notice("\u5F53\u524D\u6CA1\u6709\u53EF\u53D6\u6D88\u7684\u751F\u6210\u4EFB\u52A1")}addFileMenuItems(e,t){!(t instanceof c.TFile)||!t.path.endsWith(".md")||(e.addSeparator(),e.addItem(r=>r.setTitle("\u751F\u6210\u5BF9\u7167\u7B14\u8BB0").setIcon("book-open").onClick(()=>this.runForFile(t,!1))),e.addItem(r=>r.setTitle("\u5F3A\u5236\u91CD\u65B0\u751F\u6210\u5BF9\u7167\u7B14\u8BB0").setIcon("refresh-cw").onClick(()=>this.runForFile(t,!0))),this.cacheGet(t.path)&&e.addItem(r=>r.setTitle("\u6E05\u9664\u5BF9\u7167\u7B14\u8BB0\u7F13\u5B58").setIcon("trash").onClick(async()=>{await this.cacheDelete(t.path),new c.Notice("\u5DF2\u6E05\u9664\u7F13\u5B58\uFF1A"+t.basename)})))}async handleFileRename(e,t){if(!(e instanceof c.TFile)||!t)return;let r=t.endsWith(".md"),n=e.path.endsWith(".md");if(!r&&!n)return;if(r&&!n){this.cache[t]&&(delete this.cache[t],await this.saveCache());let u=this.app.workspace.getLeavesOfType(b)[0]?.view;u?.sourceFile?.path===t&&u.renderEmpty();return}if(!r)return;this.cache[t]&&(this.cache[e.path]=this.cache[t],delete this.cache[t],await this.saveCache());let s=this.app.workspace.getLeavesOfType(b)[0]?.view;s?.sourceFile&&(s.sourceFile.path===t||s.sourceFile.path===e.path)&&(s.sourceFile=e,await this.syncViewToFile(e))}async handleFileDelete(e){if(!(e instanceof c.TFile))return;!!this.cache[e.path]&&(delete this.cache[e.path],await this.saveCache());let n=this.app.workspace.getLeavesOfType(b)[0]?.view;n?.sourceFile?.path===e.path&&n.renderEmpty()}async exportCurrentView(){let e=this.getActiveView(),t=await this.ensureView();if((!t.sourceFile||!t.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!t.sourceFile||!t.sections.length){new c.Notice("\u5F53\u524D\u6CA1\u6709\u53EF\u5BFC\u51FA\u7684\u5BF9\u7167\u7B14\u8BB0");return}await t.exportToVault()}async copyCurrentViewMarkdown(){let e=this.getActiveView(),t=await this.ensureView();if((!t.sourceFile||!t.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!t.sourceFile||!t.sections.length){new c.Notice("\u5F53\u524D\u6CA1\u6709\u53EF\u590D\u5236\u7684\u5BF9\u7167\u7B14\u8BB0");return}await X(R(`${t.sourceFile.basename} \xB7 \u5BF9\u7167\u7B14\u8BB0`,t.sections),"\u5DF2\u590D\u5236\u5168\u90E8 Markdown")}async runForActiveFile(e){let t=this.getActiveView();if(!t||!t.file){new c.Notice("\u5148\u6253\u5F00\u4E00\u7BC7\u7B14\u8BB0");return}return this.runForFile(t.file,e)}async runForFile(e,t){if(!e){new c.Notice("\u5148\u6253\u5F00\u4E00\u7BC7\u7B14\u8BB0");return}let r=e.path;if(this.jobs.isRunning(r)){new c.Notice("\u8BE5\u7B14\u8BB0\u6B63\u5728\u751F\u6210\u5BF9\u7167\u7B14\u8BB0");return}let n=null;return this.jobs.start(r,async i=>{i.setPhase("reading");let s=await this.app.vault.read(e);if(i.throwIfCancelled(),!s.trim()){new c.Notice("\u7B14\u8BB0\u4E3A\u7A7A");return}if(n=await this.ensureView(),i.throwIfCancelled(),i.setPhase("cache-check"),!t){let l=this.cacheGet(e.path);if(G(l,s,this.settings)){this.activeFileStillMatches(e)&&await n.loadFor(e,this.resolveCardAnchors(s,l.cards),!1);return}}await n.renderLoading(e,"\u5BF9\u7167\u9605\u8BFB\uFF1A\u8BA9 LLM \u8BFB\u5168\u6587\u5E76\u81EA\u9002\u5E94\u5207\u6BB5...");let o=Number(this.settings.maxDocChars)||f.maxDocChars;s.length>o&&new c.Notice(`\u7B14\u8BB0\u8F83\u957F\uFF1A\u4EC5\u53D1\u9001\u524D ${o} \u4E2A\u5B57\u7B26\u7ED9\u6A21\u578B`),new c.Notice("\u5BF9\u7167\u9605\u8BFB\uFF1A\u8BA9 LLM \u8BFB\u5168\u6587\u5E76\u81EA\u9002\u5E94\u5207\u6BB5\u2026"),i.setPhase("generating");let u=await ot(s,this.settings,i);if(i.throwIfCancelled(),u.length===0){new c.Notice("LLM \u672A\u8FD4\u56DE\u4EFB\u4F55 card");return}let h=u.map(l=>({title:l.title,anchor:l.anchor,gist:l.gist,bullets:l.bullets}));i.setPhase("saving"),await this.cachePut(e.path,s,h,this.settings),i.throwIfCancelled(),this.viewIsShowingFile(n,e)&&await n.loadFor(e,u,!1);let g=u.filter(l=>l.startLine<0).length;new c.Notice(`\u5BF9\u7167\u7B14\u8BB0\u751F\u6210\u5B8C\u6210\uFF1A${u.length} \u6BB5${g?`\uFF08\u26A0 ${g} \u6BB5 anchor \u672A\u5339\u914D\uFF09`:""}`)}).catch(async i=>{if(i instanceof E){new c.Notice(i.message);return}if(i instanceof x){this.viewIsShowingFile(n,e)&&await n.renderError(e,"\u751F\u6210\u5DF2\u53D6\u6D88"),new c.Notice("\u5DF2\u53D6\u6D88\u751F\u6210");return}let s=me(i);console.error(i),this.viewIsShowingFile(n,e)&&await n.renderError(e,i.message||String(i)),new c.Notice(`\u751F\u6210\u5931\u8D25${s==="unknown"?"":`\uFF08${s}\uFF09`}\uFF1A`+(i.message||i))})}resolveCardAnchors(e,t){let r=(t||[]).map(n=>({title:n.title,level:2,anchor:n.anchor,gist:n.gist,startLine:we(e,n.anchor),bullets:n.bullets||[]}));return r.sort((n,i)=>n.startLine<0&&i.startLine<0?0:n.startLine<0?1:i.startLine<0?-1:n.startLine-i.startLine),r}async syncViewToFile(e){if(!e||!e.path||!e.path.endsWith(".md"))return;let t=this.app.workspace.getLeavesOfType(b);if(t.length===0)return;let r=t[0].view;if(!r||!this.activeFileStillMatches(e))return;let n=this.cacheGet(e.path);if(!n){await r.loadFor(e,[],!1),r.renderEmptyWithHint(e);return}let i=await this.app.vault.read(e);if(!this.activeFileStillMatches(e))return;let s=!G(n,i,this.settings),o=this.resolveCardAnchors(i,n.cards);await r.loadFor(e,o,s)}bindScrollSync(){this._scrollDispose&&(this._scrollDispose(),this._scrollDispose=null);let e=this.getActiveView();if(!e)return;let t=e.editor,r=t&&t.cm,n=r&&r.scrollDOM?r.scrollDOM:e.contentEl.querySelector(".cm-scroller");if(!n)return;let i=()=>this.handleEditorScroll(e);n.addEventListener("scroll",i,{passive:!0}),this._scrollDispose=()=>n.removeEventListener("scroll",i)}handleEditorScroll(e){let t=this.app.workspace.getLeavesOfType(b);if(t.length===0)return;let r=t[0].view;if(!r||!e.file||r.sourceFile?.path!==e.file.path)return;let n=e.editor,i=n&&n.cm;if(!i)return;let s=i.scrollDOM;if(!s)return;let o=s.getBoundingClientRect(),u=o.top+80,h=0;try{let l=i.posAtCoords({x:o.left+20,y:u});l!=null&&(h=i.state.doc.lineAt(l).number-1)}catch{return}let g=-1;for(let l=0;ln.addOption("claude-code","Claude Code CLI").addOption("codex","Codex CLI").addOption("api","API / Provider").addOption("anthropic-api","Anthropic API (legacy)").setValue(this.plugin.settings.backend).onChange(async i=>{this.plugin.settings.backend=i,i==="api"&&!this.plugin.settings.apiBaseUrl&&oe(this.plugin.settings,this.plugin.settings.apiProvider||"anthropic"),await this.plugin.saveSettings(),this.display()}));let t=U(this.plugin.settings.backend);if(!t)new c.Setting(e).setName("CLI \u8DEF\u5F84\uFF08\u53EF\u9009\uFF09").setDesc("\u7559\u7A7A\u5219\u81EA\u52A8\u63A2\u6D4B\u5E38\u89C1\u4F4D\u7F6E\uFF1BObsidian GUI \u542F\u52A8\u65F6\u4E0D\u7EE7\u627F shell PATH\uFF0C\u5FC5\u8981\u65F6\u586B\u7EDD\u5BF9\u8DEF\u5F84").addText(n=>n.setPlaceholder("\u4F8B\uFF1A/Users/you/bin/codex").setValue(this.plugin.settings.cliPath).onChange(async i=>{this.plugin.settings.cliPath=i.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName("CLI \u8D85\u65F6 (ms)").addText(n=>n.setValue(String(this.plugin.settings.cliTimeoutMs)).onChange(async i=>{let s=parseInt(i,10);!isNaN(s)&&s>0&&(this.plugin.settings.cliTimeoutMs=s,this.plugin.saveSettingsDebounced())}));else{e.createEl("h3",{text:"API Provider"});let n=y(this.plugin.settings);new c.Setting(e).setName("Provider preset").setDesc("\u53C2\u8003 OpenClaw \u7684 provider/model \u601D\u8DEF\uFF1Apreset \u53EA\u8D1F\u8D23\u534F\u8BAE\u3001base URL \u548C\u8BA4\u8BC1\u9ED8\u8BA4\u503C").addDropdown(i=>{for(let[s,o]of Object.entries(T))i.addOption(s,o.label);return i.setValue(this.plugin.settings.apiProvider).onChange(async s=>{oe(this.plugin.settings,s),await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName("API format").setDesc("\u4E0D\u540C provider \u7684 wire protocol\uFF1BOpenAI-compatible \u4EE3\u7406\u901A\u5E38\u9009 Chat Completions").addDropdown(i=>{for(let[s,o]of Object.entries(w))i.addOption(s,o.label);return i.setValue(v(this.plugin.settings)).onChange(async s=>{this.plugin.settings.apiFormat=s,await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName("Base URL").setDesc("\u586B provider \u6839\u5730\u5740\uFF0C\u4E0D\u8981\u9644\u52A0 /chat/completions\uFF1B\u7559\u7A7A\u65F6\u4F7F\u7528 preset \u9ED8\u8BA4\u503C").addText(i=>i.setPlaceholder((this.plugin.settings.apiProvider||"").startsWith("custom-")?"https://your-provider.example/v1":n.baseUrl||w[v(this.plugin.settings)].defaultBaseUrl).setValue(this.plugin.settings.apiBaseUrl).onChange(async s=>{this.plugin.settings.apiBaseUrl=s.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName("API Key").setDesc("\u672C\u5730 Ollama/LM Studio \u53EF\u7559\u7A7A\uFF1B\u5176\u4ED6 provider \u901A\u5E38\u9700\u8981 key").addText(i=>(i.inputEl.type="password",i.setPlaceholder("sk-...").setValue(this.plugin.settings.apiKey).onChange(async s=>{this.plugin.settings.apiKey=s.trim(),this.plugin.saveSettingsDebounced()}))),new c.Setting(e).setName("API Key \u73AF\u5883\u53D8\u91CF").setDesc("\u53EF\u9009\uFF1BObsidian GUI \u4E0D\u4E00\u5B9A\u7EE7\u627F shell \u73AF\u5883\uFF0C\u76F4\u63A5\u586B API Key \u66F4\u7A33\u5B9A").addText(i=>i.setPlaceholder(n.envVar||"OPENAI_API_KEY").setValue(this.plugin.settings.apiKeyEnvVar).onChange(async s=>{this.plugin.settings.apiKeyEnvVar=s.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName("\u8BA4\u8BC1\u65B9\u5F0F").setDesc("Auto \u4F7F\u7528 provider preset\uFF1B\u81EA\u5B9A\u4E49\u4EE3\u7406\u53EF\u6309\u9700\u8981\u6539\u6210 Bearer\u3001x-api-key \u6216 none").addDropdown(i=>{for(let[s,o]of Object.entries(ne))i.addOption(s,o);return i.setValue(this.plugin.settings.apiAuthType||"auto").onChange(async s=>{this.plugin.settings.apiAuthType=s,await this.plugin.saveSettings()})}),new c.Setting(e).setName("\u989D\u5916 headers").setDesc("\u53EF\u9009\u3002\u652F\u6301 JSON \u5BF9\u8C61\u6216\u6BCF\u884C `Header: value`\uFF0C\u7528\u4E8E Cloudflare AI Gateway \u7B49\u4EE3\u7406").addTextArea(i=>i.setPlaceholder("cf-aig-authorization: Bearer ...").setValue(this.plugin.settings.apiHeaders).onChange(async s=>{this.plugin.settings.apiHeaders=s,this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName("\u6700\u5927\u8F93\u51FA tokens").addText(i=>i.setValue(String(this.plugin.settings.apiMaxTokens)).onChange(async s=>{let o=parseInt(s,10);!isNaN(o)&&o>0&&(this.plugin.settings.apiMaxTokens=o,this.plugin.saveSettingsDebounced())}))}new c.Setting(e).setName("Model").setDesc(t?"API \u8C03\u7528\u7684\u6A21\u578B ID\uFF1B\u652F\u6301 OpenClaw \u98CE\u683C provider/model\uFF0C\u82E5 provider \u524D\u7F00\u5339\u914D\u5F53\u524D preset \u4F1A\u81EA\u52A8\u5265\u79BB":"Claude Code \u4E0B\u4F1A\u4F20 --model\uFF1BCodex \u4E0B\u901A\u5E38\u5FFD\u7565\uFF08\u7528 Codex \u9ED8\u8BA4\u914D\u7F6E\uFF09").addText(n=>n.setPlaceholder(t?y(this.plugin.settings).model||"model-id":f.model).setValue(this.plugin.settings.model).onChange(async i=>{this.plugin.settings.model=i.trim()||(t?"":f.model),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName("\u6700\u5927\u8F93\u5165\u5B57\u7B26\u6570").setDesc("\u8D85\u8FC7\u8BE5\u957F\u5EA6\u4F1A\u622A\u65AD\u540E\u518D\u53D1\u9001\u7ED9\u6A21\u578B\uFF1B\u957F\u4E0A\u4E0B\u6587\u6A21\u578B\u53EF\u9002\u5F53\u8C03\u5927").addText(n=>n.setValue(String(this.plugin.settings.maxDocChars||f.maxDocChars)).onChange(async i=>{let s=parseInt(i,10);!isNaN(s)&&s>=1e3&&(this.plugin.settings.maxDocChars=s,this.plugin.saveSettingsDebounced())})),e.createEl("h3",{text:"Prompt"}),new c.Setting(e).setName("\u8F93\u51FA\u8BED\u8A00").setDesc("\u63A7\u5236 title/gist/bullets \u7684\u8BED\u8A00\uFF1Banchor \u59CB\u7EC8\u9010\u5B57\u590D\u5236\u539F\u6587").addDropdown(n=>{for(let[i,s]of Object.entries(j))n.addOption(i,s);return n.setValue(this.plugin.settings.promptLanguage||f.promptLanguage).onChange(async i=>{this.plugin.settings.promptLanguage=i,await this.plugin.saveSettings()})}),new c.Setting(e).setName("\u5361\u7247\u6570\u91CF\u8303\u56F4").setDesc("\u6A21\u578B\u4F1A\u5728\u8FD9\u4E2A\u8303\u56F4\u5185\u81EA\u9002\u5E94\u5207\u6BB5").addText(n=>n.setPlaceholder("min").setValue(String(this.plugin.settings.minCards||f.minCards)).onChange(async i=>{let s=parseInt(i,10);!isNaN(s)&&s>0&&(this.plugin.settings.minCards=s,this.plugin.settings.maxCardsn.setPlaceholder("max").setValue(String(this.plugin.settings.maxCards||f.maxCards)).onChange(async i=>{let s=parseInt(i,10);!isNaN(s)&&s>0&&(this.plugin.settings.maxCards=Math.max(s,this.plugin.settings.minCards||f.minCards),this.plugin.saveSettingsDebounced())})),new c.Setting(e).setName("\u81EA\u5B9A\u4E49 system prompt").setDesc("\u53EF\u9009\u3002\u652F\u6301\u53D8\u91CF\uFF1A{minCards}\u3001{maxCards}\u3001{languageInstruction}\u3001{schema}\u3001{example}").addTextArea(n=>(n.inputEl.rows=8,n.setPlaceholder("\u7559\u7A7A\u4F7F\u7528\u5185\u7F6E prompt").setValue(this.plugin.settings.customSystemPrompt||"").onChange(async i=>{this.plugin.settings.customSystemPrompt=i,this.plugin.saveSettingsDebounced()}))),new c.Setting(e).setName("\u6D4B\u8BD5\u5F53\u524D\u540E\u7AEF").setDesc(t?"\u4F1A\u53D1\u8D77\u4E00\u6B21\u6700\u5C0F LLM \u8BF7\u6C42\u9A8C\u8BC1 API \u8BBE\u7F6E":"\u8C03\u7528 ` --version` \u9A8C\u8BC1 spawn \u80FD\u627E\u5230\u4E8C\u8FDB\u5236").addButton(n=>n.setButtonText("Test").onClick(async()=>{try{let i=await st(this.plugin.settings);new c.Notice(`\u2713 ${i.slice(0,180)}`,8e3)}catch(i){new c.Notice(`\u2717 \u540E\u7AEF\u6D4B\u8BD5\u5931\u8D25\uFF1A${i.message}`,1e4)}})),new c.Setting(e).setName("\u5BFC\u51FA\u6587\u4EF6\u5939").setDesc("\u5BF9\u7167\u7B14\u8BB0\u751F\u6210\u4F4D\u7F6E\uFF08\u76F8\u5BF9 Vault \u6839\uFF09").addText(n=>n.setValue(this.plugin.settings.exportFolder).onChange(async i=>{this.plugin.settings.exportFolder=i.trim()||f.exportFolder,this.plugin.saveSettingsDebounced()})),e.createEl("h3",{text:"\u7F13\u5B58"}),new c.Setting(e).setName("\u6700\u5927\u7F13\u5B58\u7BC7\u6570").setDesc("\u8D85\u8FC7\u4E0A\u9650\u540E\u6309\u6700\u8FD1\u8BBF\u95EE\u65F6\u95F4\u6DD8\u6C70\u6700\u65E7\u7684\u7B14\u8BB0\u7F13\u5B58\uFF1B\u7F13\u5B58\u4FDD\u5B58\u5728\u63D2\u4EF6\u76EE\u5F55\u7684 cache.json").addText(n=>{n.setValue(String(this.plugin.settings.maxCacheEntries||_));let i=async()=>{let s=parseInt(n.getValue(),10);if(Number.isFinite(s)&&s>0){this.plugin.settings.maxCacheEntries=s,await this.plugin.saveSettings();let o=await this.plugin.pruneCacheIfNeeded();o.length>0&&new c.Notice(`\u5DF2\u6DD8\u6C70 ${o.length} \u6761\u65E7\u7F13\u5B58`),this.display()}};return n.inputEl.addEventListener("change",i),n.inputEl.addEventListener("keydown",s=>{s.key==="Enter"&&n.inputEl.blur()}),n});let r=Object.keys(this.plugin.cache).length;new c.Setting(e).setName(`\u5DF2\u7F13\u5B58\u7B14\u8BB0\uFF1A${r} \u7BC7`).setDesc("\u7F13\u5B58\u4EE5\u6E90\u7B14\u8BB0 SHA1 + \u751F\u6210\u914D\u7F6E\u6307\u7EB9\u4F5C\u4E3A\u5931\u6548\u952E\uFF0C\u6E90\u7B14\u8BB0\u6216\u6A21\u578B\u914D\u7F6E\u4FEE\u6539\u540E\u4F1A\u663E\u793A stale \u63D0\u793A").addButton(n=>n.setButtonText("\u6E05\u9664\u6240\u6709\u7F13\u5B58").setWarning().onClick(async()=>{let i=Object.keys(this.plugin.cache).length;await this.plugin.cacheClear(),new c.Notice(`\u5DF2\u6E05\u9664 ${i} \u6761\u7F13\u5B58`),this.display()}))}},ct=ge,lt={CACHE_SCHEMA_VERSION:P,GenerationJobAlreadyRunningError:E,GenerationJobCancelledError:x,GenerationJobManager:L,buildAnthropicMessagesBody:H,buildGeminiBody:z,buildOpenAiChatBody:J,buildOpenAiResponsesBody:K,buildPrompts:Le,cardsToMarkdown:R,cacheEntryMatches:G,classifyGenerationError:me,extractJson:ae,findLineForAnchor:we,generationFingerprint:B,normalizeCardsPayload:F,pruneCacheEntries:le,summarizeViaApi:V,tokenLimitFieldForOpenAiChat:he};0&&(module.exports={__test});
+Output JSON directly with no explanation.`,o=["exec","--skip-git-repo-check","-"],{stdout:s}=await ae(i,o,r,t.cliTimeoutMs,n);return A(s)}async function pt(a){if(a.backend==="codex"){let e=te("codex",a.cliPath),{stdout:t}=await ae(e,["--version"],"",1e4);return`codex @ ${e}
+${t.trim()}`}if(a.backend==="claude-code"){let e=te("claude",a.cliPath),{stdout:t}=await ae(e,["--version"],"",1e4);return`claude @ ${e}
+${t.trim()}`}if(G(a.backend))return Oe(c.requestUrl,a);throw new Error("Unknown backend: "+a.backend)}async function ut(a,e,t){let{system:n,user:i}=he(a,e),r;switch(e.backend){case"codex":r=await lt(n,i,e,t);break;case"api":r=await O(c.requestUrl,n,i,e);break;case"anthropic-api":r=await O(c.requestUrl,n,i,e);break;case"claude-code":default:r=await ct(n,i,e,t);break}let o=r.map(s=>({title:s.title,level:2,anchor:s.anchor,gist:s.gist,startLine:B(a,s.anchor),bullets:s.bullets}));return o.sort((s,l)=>s.startLine<0&&l.startLine<0?0:s.startLine<0?1:l.startLine<0?-1:s.startLine-l.startLine),o}function Z(a,e,t,n){let i=a.createEl("button",{cls:"parallel-reader-icon-button",attr:{type:"button","aria-label":t}});return i.title=t,typeof c.setIcon=="function"?(0,c.setIcon)(i,e):i.textContent=t,i.addEventListener("click",async r=>{r.preventDefault(),r.stopPropagation();try{await n()}catch(o){console.error(o),new c.Notice(`${t} failed: `+(o.message||o))}}),i}function _e(a,e,t,n,i){let r=a.createEl("button",{cls:i||"parallel-reader-text-button",attr:{type:"button"}});return e&&typeof c.setIcon=="function"&&(0,c.setIcon)(r,e),r.createSpan({text:t}),r.addEventListener("click",async o=>{o.preventDefault(),o.stopPropagation();try{await n()}catch(s){console.error(s),new c.Notice(`${t} failed: `+(s.message||s))}}),r}async function Q(a,e){try{await navigator.clipboard.writeText(a),new c.Notice(e)}catch(t){new c.Notice("Copy failed: "+(t.message||t))}}var be=class extends c.ItemView{constructor(e,t){super(e),this.plugin=t,this.sections=[],this.sourceFile=null,this.cards=[],this.activeIdx=-1,this.loadingMessage="",this.errorMessage=""}getViewType(){return C}getDisplayText(){return this.plugin.t("displayName")}getIcon(){return"book-open"}async onOpen(){let e=this.containerEl.children[1];e.empty(),e.addClass("parallel-reader-container"),this.renderEmpty()}async onClose(){}renderEmpty(){this.sourceFile=null,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage="";let e=this.containerEl.children[1];e.empty();let t=e.createDiv({cls:"parallel-reader-empty"});t.createEl("h3",{text:this.plugin.t("appTitle")}),t.createEl("p",{text:this.plugin.t("emptyOpenNote")}),t.createEl("code",{text:this.plugin.t("commandGenerate")})}async loadFor(e,t,n){this.sourceFile=e,this.sections=t,this.stale=!!n,this.loadingMessage="",this.errorMessage="",this.render()}async renderLoading(e,t){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage=t||this.plugin.t("loadingDefault"),this.errorMessage="",this.render()}async renderError(e,t){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage=t||this.plugin.t("errorTitle"),this.render()}renderEmptyWithHint(e){this.sourceFile=e,this.sections=[],this.stale=!1,this.loadingMessage="",this.errorMessage="";let t=this.containerEl.children[1];t.empty();let n=t.createDiv({cls:"parallel-reader-empty"});n.createEl("h3",{text:e.basename}),n.createEl("p",{text:this.plugin.t("emptyNoCache")}),n.createEl("code",{text:this.plugin.t("commandGenerate")})}render(){let e=this.containerEl.children[1];e.empty();let n=e.createDiv({cls:"parallel-reader-header"}).createDiv({cls:"parallel-reader-header-row"});n.createEl("div",{text:this.sourceFile?.basename||"",cls:"parallel-reader-title"});let i=n.createDiv({cls:"parallel-reader-actions"});if(this.sourceFile&&(this.plugin.isGeneratingFile(this.sourceFile)?Z(i,"square",this.plugin.t("actionCancel"),()=>this.plugin.cancelGenerationForFile(this.sourceFile)):Z(i,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0)),Z(i,"copy",this.plugin.t("actionCopyAll"),()=>this.plugin.copyCurrentViewMarkdown()),Z(i,"download",this.plugin.t("actionExport"),()=>this.exportToVault())),this.stale){let s=e.createDiv({cls:"parallel-reader-stale-banner"});s.createSpan({text:this.plugin.t("staleBanner")}),_e(s,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-stale-button")}if(this.loadingMessage){let s=e.createDiv({cls:"parallel-reader-state parallel-reader-loading"});s.createDiv({cls:"parallel-reader-spinner"}),s.createEl("div",{text:this.loadingMessage,cls:"parallel-reader-state-title"}),s.createEl("div",{text:this.plugin.t("loadingSubtitle"),cls:"parallel-reader-state-subtitle"});return}if(this.errorMessage){let s=e.createDiv({cls:"parallel-reader-state parallel-reader-error"});s.createEl("div",{text:this.plugin.t("errorTitle"),cls:"parallel-reader-state-title"}),s.createEl("div",{text:this.errorMessage,cls:"parallel-reader-state-subtitle"}),_e(s,"refresh-cw",this.plugin.t("actionRegenerate"),()=>this.plugin.runForFile(this.sourceFile,!0),"parallel-reader-text-button");return}let r=e.createDiv({cls:"parallel-reader-cards"});this.cards=[];let o=this.sourceFile?.path||"";this.sections.forEach((s,l)=>{let d=r.createDiv({cls:"parallel-reader-card"});d.dataset.idx=String(l),s.startLine<0&&d.addClass("parallel-reader-card-unanchored");let f=d.createEl("div",{cls:"parallel-reader-card-title"});if(f.createSpan({text:s.title}),s.startLine<0&&f.createEl("span",{text:" \u26A0",cls:"parallel-reader-warn",title:this.plugin.t("anchorMismatch")}),s.gist){let h=d.createEl("div",{cls:"parallel-reader-gist"});c.MarkdownRenderer.render(this.app,s.gist,h,o,this).catch(()=>{h.setText(s.gist)})}let p=s.bullets||[];if(p.length>0){let h=d.createEl("div",{cls:"parallel-reader-bullets-md"}),g=p.map(u=>`- ${u}`).join(`
+`);c.MarkdownRenderer.render(this.app,g,h,o,this).catch(()=>{h.setText(g)})}else s.gist||d.createEl("div",{cls:"parallel-reader-empty-li",text:this.plugin.t("emptyCard")});d.addEventListener("click",h=>{let g=window.getSelection();if(g&&g.toString().length>0)return;let u=h.target;u&&u.tagName==="A"||s.startLine>=0&&this.plugin.scrollEditorToLine(s.startLine,this.sourceFile)}),d.addEventListener("contextmenu",h=>{h.preventDefault();let g=new c.Menu;g.addItem(u=>u.setTitle(this.plugin.t("menuCopyMarkdown")).setIcon("copy").onClick(()=>Q(re(s),this.plugin.t("copiedMarkdown")))),g.addItem(u=>u.setTitle(this.plugin.t("menuCopyPlain")).setIcon("clipboard-copy").onClick(()=>Q(Pe(s),this.plugin.t("copiedPlain")))),s.anchor&&g.addItem(u=>u.setTitle(this.plugin.t("menuCopyAnchor")).setIcon("quote-glyph").onClick(()=>Q(s.anchor,this.plugin.t("copiedAnchor")))),g.addSeparator(),s.startLine>=0&&g.addItem(u=>u.setTitle(this.plugin.t("menuJumpSource")).setIcon("arrow-right").onClick(()=>this.plugin.scrollEditorToLine(s.startLine,this.sourceFile))),g.showAtMouseEvent(h)}),this.cards.push(d)})}setActiveSection(e){e!==this.activeIdx&&(this.activeIdx>=0&&this.cards[this.activeIdx]&&this.cards[this.activeIdx].removeClass("is-active"),this.activeIdx=e,e>=0&&this.cards[e]&&(this.cards[e].addClass("is-active"),this.cards[e].scrollIntoView({block:"nearest",behavior:"smooth"})))}async exportToVault(){if(!this.sourceFile)return;let e=this.plugin.settings.exportFolder.replace(/\/$/,""),t=`${this.sourceFile.basename} - ${this.plugin.t("displayName")}.md`,n=`${e}/${t}`,i=["---",`source: [[${this.sourceFile.basename}]]`,`generated: ${new Date().toISOString().slice(0,10)}`,"tool: parallel-reader","---","",j(`${this.sourceFile.basename} \xB7 ${this.plugin.t("displayName")}`,this.sections),""].join(`
+`),r=this.plugin.app;if(!r.vault.getAbstractFileByPath(e))try{await r.vault.createFolder(e)}catch{}let s=r.vault.getAbstractFileByPath(n);s instanceof c.TFile?await r.vault.modify(s,i):await r.vault.create(n,i),new c.Notice(this.plugin.t("exported",{path:n}))}},xe=class extends c.Plugin{t(e,t){return ne(this.settings||m,e,t)}async onload(){await this.loadSettings(),this.jobs=new _,this.addRibbonIcon("book-open",this.t("ribbonOpen"),async()=>{let e=this.getActiveView();await this.ensureView(),e?.file&&await this.syncViewToFile(e.file)}),this.registerView(C,e=>new be(e,this)),this.addCommand({id:"parallel-reader-run",name:this.t("cmdRun"),callback:()=>this.runForActiveFile(!1)}),this.addCommand({id:"parallel-reader-regen",name:this.t("cmdRegen"),callback:()=>this.runForActiveFile(!0)}),this.addCommand({id:"parallel-reader-open-view",name:this.t("cmdOpenView"),callback:async()=>{let e=this.getActiveView();await this.ensureView(),e?.file&&this.syncViewToFile(e.file)}}),this.addCommand({id:"parallel-reader-export-current",name:this.t("cmdExport"),callback:()=>this.exportCurrentView()}),this.addCommand({id:"parallel-reader-copy-current-markdown",name:this.t("cmdCopyMarkdown"),callback:()=>this.copyCurrentViewMarkdown()}),this.addCommand({id:"parallel-reader-cancel-current",name:this.t("cmdCancel"),callback:()=>this.cancelActiveGeneration()}),this.addCommand({id:"parallel-reader-clear-current",name:this.t("cmdClearCurrent"),callback:async()=>{let e=this.getActiveView();if(!e?.file)return new c.Notice(this.t("noCurrentNote"));await this.cacheDelete(e.file.path),new c.Notice(this.t("cacheClearedFile",{name:e.file.basename}))}}),this.addCommand({id:"parallel-reader-clear-all",name:this.t("cmdClearAll"),callback:async()=>{let e=Object.keys(this.cache).length;await this.cacheClear(),new c.Notice(this.t("cacheClearedAll",{count:e}))}}),this.addSettingTab(new ve(this.app,this)),this.registerEvent(this.app.workspace.on("active-leaf-change",()=>this.bindScrollSync())),this.registerEvent(this.app.workspace.on("file-open",e=>{e&&this.syncViewToFile(e)})),this.registerEvent(this.app.workspace.on("file-menu",(e,t)=>this.addFileMenuItems(e,t))),this.registerEvent(this.app.vault.on("rename",(e,t)=>this.handleFileRename(e,t))),this.registerEvent(this.app.vault.on("delete",e=>this.handleFileDelete(e))),this.bindScrollSync()}async onunload(){await this.flushSettingsSave(),this.app.workspace.detachLeavesOfType(C)}async loadSettings(){let t=(await this.loadData()||{}).settings||{};this.settings=ue(Object.assign({},m,t)),await this.loadCache()}async saveSettings(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null),await this.saveData({settings:this.settings})}saveSettingsDebounced(e=400){this._settingsSaveTimer&&clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=setTimeout(()=>{this._settingsSaveTimer=null,this.saveSettings().catch(t=>console.error("[parallel-reader] failed to save settings",t))},e)}async flushSettingsSave(){this._settingsSaveTimer&&(clearTimeout(this._settingsSaveTimer),this._settingsSaveTimer=null,await this.saveSettings())}async saveCache(){this.pruneCache(),await this.writeCacheFile()}cacheFilePath(){let e=this.app.vault.configDir||".obsidian",t=this.manifest?.id||"parallel-reader";return`${e}/plugins/${t}/cache.json`}async ensurePluginDataDir(){let e=this.app.vault.adapter,t=this.app.vault.configDir||".obsidian",n=this.manifest?.id||"parallel-reader",i=`${t}/plugins/${n}`;try{if(typeof e.exists=="function"&&await e.exists(i))return;await e.mkdir(i)}catch{}}async readCacheFile(){let e=this.app.vault.adapter;try{let t=await e.read(this.cacheFilePath()),n=JSON.parse(t);if(n&&typeof n=="object"&&n.entries&&typeof n.entries=="object")return n.entries}catch(t){let n=String(t?.message||t||"");/not found|does not exist|ENOENT/i.test(n)||console.warn("[parallel-reader] failed to read cache.json",t)}return{}}async writeCacheFile(){await this.ensurePluginDataDir(),await this.app.vault.adapter.write(this.cacheFilePath(),JSON.stringify({version:1,entries:this.cache},null,2))}async loadCache(){let e=await this.readCacheFile();this.cache=e,this.pruneCache().length>0&&await this.writeCacheFile()}pruneCache(){return de(this.cache,this.settings?.maxCacheEntries||N)}async pruneCacheIfNeeded(){let e=this.pruneCache();return e.length>0&&await this.writeCacheFile(),e}cacheGet(e){let t=this.cache[e]||null;return t&&(t.lastAccessedAt=new Date().toISOString()),t}async cachePut(e,t,n,i){let r=new Date().toISOString();this.cache[e]={schemaVersion:S,contentHash:P(t),settingsHash:$(i||this.settings),cards:n,generatedAt:r,lastAccessedAt:r},await this.saveCache()}async cacheDelete(e){this.cache[e]&&(delete this.cache[e],await this.saveCache())}async cacheClear(){this.cache={},await this.saveCache()}async ensureView(){let{workspace:e}=this.app,t=e.getLeavesOfType(C)[0];return t||(t=e.getRightLeaf(!1),await t.setViewState({type:C,active:!0})),e.revealLeaf(t),t.view}getActiveView(){return this.app.workspace.getActiveViewOfType(c.MarkdownView)}isGeneratingFile(e){return!!e&&!!e.path&&this.jobs.isRunning(e.path)}cancelGenerationForFile(e){if(!e||!e.path)return new c.Notice(this.t("noCancelableJob")),!1;let t=this.jobs.cancel(e.path);return new c.Notice(t?this.t("cancelRequested"):this.t("noCancelableJob")),t}viewIsShowingFile(e,t){return!!e&&!!t&&e.sourceFile?.path===t.path}activeFileStillMatches(e){let t=this.getActiveView();return!t?.file||t.file.path===e.path}cancelActiveGeneration(){let e=this.getActiveView();if(e?.file&&this.cancelGenerationForFile(e.file))return;let t=this.app.workspace.getLeavesOfType(C)[0]?.view;t?.sourceFile?this.cancelGenerationForFile(t.sourceFile):new c.Notice(this.t("noCancelableJob"))}addFileMenuItems(e,t){!(t instanceof c.TFile)||!t.path.endsWith(".md")||(e.addSeparator(),e.addItem(n=>n.setTitle(this.t("fileMenuGenerate")).setIcon("book-open").onClick(()=>this.runForFile(t,!1))),e.addItem(n=>n.setTitle(this.t("fileMenuRegen")).setIcon("refresh-cw").onClick(()=>this.runForFile(t,!0))),this.cacheGet(t.path)&&e.addItem(n=>n.setTitle(this.t("fileMenuClear")).setIcon("trash").onClick(async()=>{await this.cacheDelete(t.path),new c.Notice(this.t("cacheClearedFile",{name:t.basename}))})))}async handleFileRename(e,t){if(!(e instanceof c.TFile)||!t)return;let n=t.endsWith(".md"),i=e.path.endsWith(".md");if(!n&&!i)return;if(n&&!i){this.cache[t]&&(delete this.cache[t],await this.saveCache());let l=this.app.workspace.getLeavesOfType(C)[0]?.view;l?.sourceFile?.path===t&&l.renderEmpty();return}if(!n)return;this.cache[t]&&(this.cache[e.path]=this.cache[t],delete this.cache[t],await this.saveCache());let o=this.app.workspace.getLeavesOfType(C)[0]?.view;o?.sourceFile&&(o.sourceFile.path===t||o.sourceFile.path===e.path)&&(o.sourceFile=e,await this.syncViewToFile(e))}async handleFileDelete(e){if(!(e instanceof c.TFile))return;!!this.cache[e.path]&&(delete this.cache[e.path],await this.saveCache());let i=this.app.workspace.getLeavesOfType(C)[0]?.view;i?.sourceFile?.path===e.path&&i.renderEmpty()}async exportCurrentView(){let e=this.getActiveView(),t=await this.ensureView();if((!t.sourceFile||!t.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!t.sourceFile||!t.sections.length){new c.Notice(this.t("noExportContent"));return}await t.exportToVault()}async copyCurrentViewMarkdown(){let e=this.getActiveView(),t=await this.ensureView();if((!t.sourceFile||!t.sections.length)&&e?.file&&await this.syncViewToFile(e.file),!t.sourceFile||!t.sections.length){new c.Notice(this.t("noCopyContent"));return}await Q(j(`${t.sourceFile.basename} \xB7 ${this.t("displayName")}`,t.sections),this.t("copiedAllMarkdown"))}async runForActiveFile(e){let t=this.getActiveView();if(!t||!t.file){new c.Notice(this.t("openNoteFirst"));return}return this.runForFile(t.file,e)}async runForFile(e,t){if(!e){new c.Notice(this.t("openNoteFirst"));return}let n=e.path;if(this.jobs.isRunning(n)){new c.Notice(this.t("alreadyGenerating"));return}let i=null;return this.jobs.start(n,async r=>{r.setPhase("reading");let o=await this.app.vault.read(e);if(r.throwIfCancelled(),!o.trim()){new c.Notice(this.t("emptyNote"));return}if(i=await this.ensureView(),r.throwIfCancelled(),r.setPhase("cache-check"),!t){let p=this.cacheGet(e.path);if(H(p,o,this.settings)){this.activeFileStillMatches(e)&&await i.loadFor(e,this.resolveCardAnchors(o,p.cards),!1);return}}await i.renderLoading(e,this.t("loadingGenerating"));let s=Number(this.settings.maxDocChars)||m.maxDocChars;o.length>s&&new c.Notice(this.t("longNoteTruncated",{count:s})),new c.Notice(this.t("generatingNotice")),r.setPhase("generating");let l=await ut(o,this.settings,r);if(r.throwIfCancelled(),l.length===0){new c.Notice(this.t("noCardsReturned"));return}let d=l.map(p=>({title:p.title,anchor:p.anchor,gist:p.gist,bullets:p.bullets}));r.setPhase("saving"),await this.cachePut(e.path,o,d,this.settings),r.throwIfCancelled(),this.viewIsShowingFile(i,e)&&await i.loadFor(e,l,!1);let f=l.filter(p=>p.startLine<0).length;new c.Notice(this.t("generationDone",{count:l.length,suffix:f?this.t("unanchoredSuffix",{count:f}):""}))}).catch(async r=>{if(r instanceof k){new c.Notice(r.message);return}if(r instanceof x){this.viewIsShowingFile(i,e)&&await i.renderError(e,this.t("cancelledError")),new c.Notice(this.t("cancelled"));return}let o=Ce(r);console.error(r),this.viewIsShowingFile(i,e)&&await i.renderError(e,r.message||String(r)),new c.Notice(this.t("generationFailed",{kind:o==="unknown"?"":` (${o})`,error:r.message||r}))})}resolveCardAnchors(e,t){let n=(t||[]).map(i=>({title:i.title,level:2,anchor:i.anchor,gist:i.gist,startLine:B(e,i.anchor),bullets:i.bullets||[]}));return n.sort((i,r)=>i.startLine<0&&r.startLine<0?0:i.startLine<0?1:r.startLine<0?-1:i.startLine-r.startLine),n}async syncViewToFile(e){if(!e||!e.path||!e.path.endsWith(".md"))return;let t=this.app.workspace.getLeavesOfType(C);if(t.length===0)return;let n=t[0].view;if(!n||!this.activeFileStillMatches(e))return;let i=this.cacheGet(e.path);if(!i){await n.loadFor(e,[],!1),n.renderEmptyWithHint(e);return}let r=await this.app.vault.read(e);if(!this.activeFileStillMatches(e))return;let o=!H(i,r,this.settings),s=this.resolveCardAnchors(r,i.cards);await n.loadFor(e,s,o)}bindScrollSync(){this._scrollDispose&&(this._scrollDispose(),this._scrollDispose=null);let e=this.getActiveView();if(!e)return;let t=e.editor,n=t&&t.cm,i=n&&n.scrollDOM?n.scrollDOM:e.contentEl.querySelector(".cm-scroller");if(!i)return;let r=()=>this.handleEditorScroll(e);i.addEventListener("scroll",r,{passive:!0}),this._scrollDispose=()=>i.removeEventListener("scroll",r)}handleEditorScroll(e){let t=this.app.workspace.getLeavesOfType(C);if(t.length===0)return;let n=t[0].view;if(!n||!e.file||n.sourceFile?.path!==e.file.path)return;let i=e.editor,r=i&&i.cm;if(!r)return;let o=r.scrollDOM;if(!o)return;let s=o.getBoundingClientRect(),l=s.top+80,d=0;try{let p=r.posAtCoords({x:s.left+20,y:l});p!=null&&(d=r.state.doc.lineAt(p).number-1)}catch{return}let f=-1;for(let p=0;pthis.plugin.t(r,o);e.empty(),e.createEl("h2",{text:t("settingsTitle")}),new c.Setting(e).setName(t("settingUiLanguageName")).setDesc(t("settingUiLanguageDesc")).addDropdown(r=>{for(let[o,s]of Object.entries(oe))r.addOption(o,s);return r.setValue(this.plugin.settings.uiLanguage||m.uiLanguage).onChange(async o=>{this.plugin.settings.uiLanguage=o,await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName(t("settingBackendName")).setDesc(t("settingBackendDesc")).addDropdown(r=>r.addOption("claude-code","Claude Code CLI").addOption("codex","Codex CLI").addOption("api","API / Provider").addOption("anthropic-api","Anthropic API (legacy)").setValue(this.plugin.settings.backend).onChange(async o=>{this.plugin.settings.backend=o,o==="api"&&!this.plugin.settings.apiBaseUrl&&pe(this.plugin.settings,this.plugin.settings.apiProvider||"anthropic"),await this.plugin.saveSettings(),this.display()}));let n=G(this.plugin.settings.backend);if(!n)new c.Setting(e).setName(t("settingCliPathName")).setDesc(t("settingCliPathDesc")).addText(r=>r.setPlaceholder(t("settingCliPathPlaceholder")).setValue(this.plugin.settings.cliPath).onChange(async o=>{this.plugin.settings.cliPath=o.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingCliTimeoutName")).addText(r=>r.setValue(String(this.plugin.settings.cliTimeoutMs)).onChange(async o=>{let s=parseInt(o,10);!isNaN(s)&&s>0&&(this.plugin.settings.cliTimeoutMs=s,this.plugin.saveSettingsDebounced())}));else{e.createEl("h3",{text:t("apiProviderHeader")});let r=y(this.plugin.settings);new c.Setting(e).setName(t("settingProviderPresetName")).setDesc(t("settingProviderPresetDesc")).addDropdown(o=>{for(let[s,l]of Object.entries(T))o.addOption(s,l.label);return o.setValue(this.plugin.settings.apiProvider).onChange(async s=>{pe(this.plugin.settings,s),await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName(t("settingApiFormatName")).setDesc(t("settingApiFormatDesc")).addDropdown(o=>{for(let[s,l]of Object.entries(w))o.addOption(s,l.label);return o.setValue(b(this.plugin.settings)).onChange(async s=>{this.plugin.settings.apiFormat=s,await this.plugin.saveSettings(),this.display()})}),new c.Setting(e).setName(t("settingBaseUrlName")).setDesc(t("settingBaseUrlDesc")).addText(o=>o.setPlaceholder((this.plugin.settings.apiProvider||"").startsWith("custom-")?"https://your-provider.example/v1":r.baseUrl||w[b(this.plugin.settings)].defaultBaseUrl).setValue(this.plugin.settings.apiBaseUrl).onChange(async s=>{this.plugin.settings.apiBaseUrl=s.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingApiKeyName")).setDesc(t("settingApiKeyDesc")).addText(o=>(o.inputEl.type="password",o.setPlaceholder("sk-...").setValue(this.plugin.settings.apiKey).onChange(async s=>{this.plugin.settings.apiKey=s.trim(),this.plugin.saveSettingsDebounced()}))),new c.Setting(e).setName(t("settingApiKeyEnvName")).setDesc(t("settingApiKeyEnvDesc")).addText(o=>o.setPlaceholder(r.envVar||"OPENAI_API_KEY").setValue(this.plugin.settings.apiKeyEnvVar).onChange(async s=>{this.plugin.settings.apiKeyEnvVar=s.trim(),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingAuthTypeName")).setDesc(t("settingAuthTypeDesc")).addDropdown(o=>{for(let[s,l]of Object.entries(ce))o.addOption(s,l);return o.setValue(this.plugin.settings.apiAuthType||"auto").onChange(async s=>{this.plugin.settings.apiAuthType=s,await this.plugin.saveSettings()})}),new c.Setting(e).setName(t("settingHeadersName")).setDesc(t("settingHeadersDesc")).addTextArea(o=>o.setPlaceholder("cf-aig-authorization: Bearer ...").setValue(this.plugin.settings.apiHeaders).onChange(async s=>{this.plugin.settings.apiHeaders=s,this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingMaxTokensName")).addText(o=>o.setValue(String(this.plugin.settings.apiMaxTokens)).onChange(async s=>{let l=parseInt(s,10);!isNaN(l)&&l>0&&(this.plugin.settings.apiMaxTokens=l,this.plugin.saveSettingsDebounced())}))}new c.Setting(e).setName(t("settingModelName")).setDesc(t(n?"settingModelDescApi":"settingModelDescCli")).addText(r=>r.setPlaceholder(n?y(this.plugin.settings).model||"model-id":m.model).setValue(this.plugin.settings.model).onChange(async o=>{this.plugin.settings.model=o.trim()||(n?"":m.model),this.plugin.saveSettingsDebounced()})),new c.Setting(e).setName(t("settingMaxInputName")).setDesc(t("settingMaxInputDesc")).addText(r=>r.setValue(String(this.plugin.settings.maxDocChars||m.maxDocChars)).onChange(async o=>{let s=parseInt(o,10);!isNaN(s)&&s>=1e3&&(this.plugin.settings.maxDocChars=s,this.plugin.saveSettingsDebounced())})),e.createEl("h3",{text:t("promptHeader")}),new c.Setting(e).setName(t("settingPromptLanguageName")).setDesc(t("settingPromptLanguageDesc")).addDropdown(r=>{for(let[o,s]of Object.entries(F))r.addOption(o,s);return r.setValue(this.plugin.settings.promptLanguage||m.promptLanguage).onChange(async o=>{this.plugin.settings.promptLanguage=o,await this.plugin.saveSettings()})}),new c.Setting(e).setName(t("settingCardRangeName")).setDesc(t("settingCardRangeDesc")).addText(r=>r.setPlaceholder("min").setValue(String(this.plugin.settings.minCards||m.minCards)).onChange(async o=>{let s=parseInt(o,10);!isNaN(s)&&s>0&&(this.plugin.settings.minCards=s,this.plugin.settings.maxCardsr.setPlaceholder("max").setValue(String(this.plugin.settings.maxCards||m.maxCards)).onChange(async o=>{let s=parseInt(o,10);!isNaN(s)&&s>0&&(this.plugin.settings.maxCards=Math.max(s,this.plugin.settings.minCards||m.minCards),this.plugin.saveSettingsDebounced())})),new c.Setting(e).setName(t("settingCustomPromptName")).setDesc(t("settingCustomPromptDesc")).addTextArea(r=>(r.inputEl.rows=8,r.setPlaceholder(t("settingCustomPromptPlaceholder")).setValue(this.plugin.settings.customSystemPrompt||"").onChange(async o=>{this.plugin.settings.customSystemPrompt=o,this.plugin.saveSettingsDebounced()}))),new c.Setting(e).setName(t("settingTestBackendName")).setDesc(t(n?"settingTestBackendDescApi":"settingTestBackendDescCli")).addButton(r=>r.setButtonText("Test").onClick(async()=>{try{let o=await pt(this.plugin.settings);new c.Notice(`\u2713 ${o.slice(0,180)}`,8e3)}catch(o){new c.Notice(t("backendTestFailed",{error:o.message}),1e4)}})),new c.Setting(e).setName(t("settingExportFolderName")).setDesc(t("settingExportFolderDesc")).addText(r=>r.setValue(this.plugin.settings.exportFolder).onChange(async o=>{this.plugin.settings.exportFolder=o.trim()||m.exportFolder,this.plugin.saveSettingsDebounced()})),e.createEl("h3",{text:t("cacheHeader")}),new c.Setting(e).setName(t("settingMaxCacheName")).setDesc(t("settingMaxCacheDesc")).addText(r=>{r.setValue(String(this.plugin.settings.maxCacheEntries||N));let o=async()=>{let s=parseInt(r.getValue(),10);if(Number.isFinite(s)&&s>0){this.plugin.settings.maxCacheEntries=s,await this.plugin.saveSettings();let l=await this.plugin.pruneCacheIfNeeded();l.length>0&&new c.Notice(t("cachePruned",{count:l.length})),this.display()}};return r.inputEl.addEventListener("change",o),r.inputEl.addEventListener("keydown",s=>{s.key==="Enter"&&r.inputEl.blur()}),r});let i=Object.keys(this.plugin.cache).length;new c.Setting(e).setName(t("cachedNotesName",{count:i})).setDesc(t("cachedNotesDesc")).addButton(r=>r.setButtonText(t("clearAllCacheButton")).setWarning().onClick(async()=>{let o=Object.keys(this.plugin.cache).length;await this.plugin.cacheClear(),new c.Notice(t("cacheClearedAll",{count:o})),this.display()}))}},dt=xe,ht={CACHE_SCHEMA_VERSION:S,GenerationJobAlreadyRunningError:k,GenerationJobCancelledError:x,GenerationJobManager:_,buildAnthropicMessagesBody:J,buildGeminiBody:Y,buildOpenAiChatBody:z,buildOpenAiResponsesBody:W,buildPrompts:he,cardsToMarkdown:j,cacheEntryMatches:H,classifyGenerationError:Ce,extractJson:me,findLineForAnchor:B,generationFingerprint:$,normalizeCardsPayload:M,pruneCacheEntries:de,summarizeViaApi:O,translate:ne,tokenLimitFieldForOpenAiChat:ye};0&&(module.exports={__test});
diff --git a/main.ts b/main.ts
index 2a07485..97ffcd3 100644
--- a/main.ts
+++ b/main.ts
@@ -4,7 +4,10 @@ import { spawn } from 'child_process';
import os from 'os';
import path from 'path';
import fs from 'fs';
+import { findLineForAnchor } from './src/anchor';
+import { translate } from './src/i18n';
import { cardToMarkdown, cardToPlain, cardsToMarkdown } from './src/markdown';
+import { buildPrompts } from './src/prompt';
import {
extractJson,
normalizeCardsPayload,
@@ -32,8 +35,8 @@ import {
CACHE_SCHEMA_VERSION,
DEFAULT_SETTINGS,
DEFAULT_MAX_CACHE_ENTRIES,
- MAX_DOC_CHARS,
PROMPT_LANGUAGES,
+ UI_LANGUAGES,
applyApiProviderPreset,
cacheEntryMatches,
generationFingerprint,
@@ -47,149 +50,6 @@ import {
const VIEW_TYPE_PARALLEL = 'parallel-reader-view';
-/* ---------- Document preparation & anchor resolution ---------- */
-
-function promptLanguageInstruction(language) {
- if (language === 'en') return 'Write title, gist, and bullets in English.';
- if (language === 'auto') return 'Write title, gist, and bullets in the main language of the source document.';
- return '用中文输出 title、gist 和 bullets。';
-}
-
-function promptSchemaExample(language) {
- if (language === 'en') {
- return `{"cards":[
- {"title":"U-shaped gains","anchor":"Who benefits from AI? Overall, it shifts the score from one to seven","gist":"AI productivity gains form a U shape, with both ends benefiting most","bullets":["Top-paid software managers benefit by accelerating existing work","Low-paid workers use AI to create new side income","Middle-layer specialists gain less because prompt precision is hard to trust","Average reported benefit is 5.1/7, with 42% describing gains as unclear"]}
-]}`;
- }
- return `{"cards":[
- {"title":"U 型收益曲线","anchor":"那谁又会被 AI 所受益?整体来看,它把整个分数变成了一分到七分","gist":"AI 生产力收益呈 U 型,两端受益最大、中间层塌陷","bullets":["最高薪岗位(软件管理)通过加速既有工作受益最大","最低薪岗位(外卖员、园艺工)用 AI 开副业创造新收入","中间层科学家、律师收益最少,部分因对 prompt 精度信任不足","全体均分 5.1/7,42% 报告收益模糊"]}
-]}`;
-}
-
-function renderPromptTemplate(template, vars) {
- return String(template || '').replace(/\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
- return Object.prototype.hasOwnProperty.call(vars, key) ? String(vars[key]) : match;
- });
-}
-
-function buildPrompts(content, settings) {
- const maxDocChars = Number(settings.maxDocChars) || MAX_DOC_CHARS;
- const promptLanguage = PROMPT_LANGUAGES[settings.promptLanguage] ? settings.promptLanguage : DEFAULT_SETTINGS.promptLanguage;
- const minCards = Math.max(1, Number(settings.minCards) || DEFAULT_SETTINGS.minCards);
- const maxCards = Math.max(minCards, Number(settings.maxCards) || DEFAULT_SETTINGS.maxCards);
- const languageInstruction = promptLanguageInstruction(promptLanguage);
- const doc = content.length > maxDocChars
- ? content.slice(0, maxDocChars) + (promptLanguage === 'en' ? '\n\n[Document truncated]' : '\n\n[文档过长,已截断]')
- : content;
-
- const schema = '{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}';
- const example = promptSchemaExample(promptLanguage);
- const templateVars = { minCards, maxCards, languageInstruction, schema, example };
- const customSystem = renderPromptTemplate(settings.customSystemPrompt, templateVars).trim();
-
- const defaultSystem = `你是一个长文阅读摘要助手。阅读全文后,把文章切成 ${minCards}-${maxCards} 个"自然主题单元"——不必对应 markdown heading,以"一个完整论点或话题"为单位自行判断粒度:短章节合并、长章节拆分。
-
-**每张卡片的结构:一句话领读 + 若干条 bullet。bullet 承载细节,gist 是一句话导读。**
-
-语言:
-- ${languageInstruction}
-
-对每个单元输出:
-
-- title: 3-10 字的短标题,要能独立说明这段讲什么,避免"背景""介绍"这类空泛词
-- anchor: 该单元开头的**逐字引用**,从原文 1:1 复制 40-80 字,保留原始标点/空格/换行;仅供插件内部定位,用户不可见
-- gist: **一句话领读**(20-40 字),点出该单元的核心立场或结论,作为 bullets 的导读
-- bullets: **3-6 条**支撑 bullet,每条 20-50 字。承载数据、对比、机制、例子、反直觉观察。gist 是立场,bullets 是具体内容,两者不允许重复。
-
-规则:
-- anchor 必须能在原文里 exact substring match 找到。绝对不要改动、总结、翻译,必须原样复制
-- anchor 选用该单元最靠前且足够独特的一段(避免通用套话如"综上所述")
-- 每张卡都必须同时有 gist 和 bullets——不要只有 gist,也不要只有 bullets
-- bullet 每条是一个完整独立的断言,不要用"首先""其次"这种顺序词
-- 严格只输出 JSON,无 markdown fence、无解释、无 tool call
-
-输出格式:
-${schema}
-
-示例:
-${example}`;
-
- const system = customSystem
- ? `${customSystem}
-
-不可覆盖的输出契约:
-- 必须输出 ${minCards}-${maxCards} 张 cards。
-- ${languageInstruction}
-- anchor 必须从原文逐字复制,必须能在原文 exact substring match 找到。
-- 严格只输出 JSON,无 markdown fence、无解释、无 tool call。
-- JSON shape: ${schema}`
- : defaultSystem;
-
- const user = promptLanguage === 'en'
- ? `Source document:\n\n${doc}`
- : `以下是需要处理的文档全文:\n\n${doc}`;
- return { system, user };
-}
-
-function findLineForAnchor(content, anchor) {
- if (!anchor) return -1;
- const normalize = s => s.replace(/\s+/g, ' ').trim();
- const normalizeWithMap = s => {
- const chars = [];
- const map = [];
- let pendingWhitespace = false;
- for (let i = 0; i < s.length; i++) {
- const c = s[i];
- if (/\s/.test(c)) {
- pendingWhitespace = chars.length > 0;
- continue;
- }
- if (pendingWhitespace) {
- chars.push(' ');
- map.push(i);
- pendingWhitespace = false;
- }
- chars.push(c);
- map.push(i);
- }
- return { text: chars.join(''), map };
- };
- const tryAt = needle => {
- if (!needle) return -1;
- const idx = content.indexOf(needle);
- if (idx === -1) return -1;
- let line = 0;
- for (let i = 0; i < idx; i++) if (content[i] === '\n') line++;
- return line;
- };
-
- let line = tryAt(anchor);
- if (line >= 0) return line;
-
- // Fallback 1: trim trailing whitespace variants
- line = tryAt(anchor.trim());
- if (line >= 0) return line;
-
- // Fallback 2: progressively shorter prefix (LLM may paraphrase the tail)
- for (const len of [60, 40, 25, 15]) {
- const prefix = anchor.trim().slice(0, len);
- line = tryAt(prefix);
- if (line >= 0) return line;
- }
-
- // Fallback 3: whitespace-normalized search (costlier, rarely needed)
- const normDoc = normalizeWithMap(content);
- const normAnchor = normalize(anchor).slice(0, 30);
- if (!normAnchor) return -1;
- const normIdx = normDoc.text.indexOf(normAnchor);
- if (normIdx === -1) return -1;
- const originalIdx = normDoc.map[normIdx];
- if (originalIdx == null) return -1;
- let l = 0;
- for (let j = 0; j < originalIdx; j++) if (content[j] === '\n') l++;
- return l;
-}
-
/* CLI discovery: Obsidian launched from GUI doesn't inherit shell PATH */
function resolveCliPath(name, override) {
if (override && override.trim()) return override.trim();
@@ -244,14 +104,14 @@ function runCli(cmd, args, stdinText, timeoutMs, job?) {
},
});
} catch (e) {
- return reject(new Error(`无法启动 ${cmd}: ${e.message}`));
+ return reject(new Error(`Failed to start ${cmd}: ${e.message}`));
}
let stdout = '';
let stderr = '';
timer = setTimeout(() => {
try { child.kill('SIGKILL'); } catch (_) {}
- fail(new Error(`CLI 超时 (${timeoutMs}ms)`));
+ fail(new Error(`CLI timed out (${timeoutMs}ms)`));
}, timeoutMs);
if (job) {
job.onCancel(() => {
@@ -263,12 +123,12 @@ function runCli(cmd, args, stdinText, timeoutMs, job?) {
child.stdout.on('data', d => { stdout += d.toString('utf8'); });
child.stderr.on('data', d => { stderr += d.toString('utf8'); });
child.on('error', e => {
- fail(new Error(`CLI 启动错误: ${e.message}(尝试在设置里填绝对路径)`));
+ fail(new Error(`CLI startup error: ${e.message}. Try setting an absolute CLI path.`));
});
child.on('close', code => {
if (settled) return;
if (code !== 0) {
- return fail(new Error(`CLI 退出码 ${code}\nstderr:\n${stderr.slice(0, 1000)}`));
+ return fail(new Error(`CLI exited with code ${code}\nstderr:\n${stderr.slice(0, 1000)}`));
}
succeed({ stdout, stderr });
});
@@ -303,7 +163,7 @@ async function summarizeViaClaudeCode(system, user, settings, job) {
// claude -p --output-format json returns {"type":"result", ..., "result":"..."}
let envelope;
try { envelope = JSON.parse(stdout); } catch (e) {
- throw new Error('claude CLI 输出非 JSON envelope:\n' + stdout.slice(0, 500));
+ throw new Error('claude CLI returned a non-JSON envelope:\n' + stdout.slice(0, 500));
}
const resultText = envelope.result || envelope.content || '';
return parseCardsJson(resultText);
@@ -311,7 +171,7 @@ async function summarizeViaClaudeCode(system, user, settings, job) {
async function summarizeViaCodex(system, user, settings, job) {
const cmd = resolveCliPath('codex', settings.cliPath);
- const combined = `<>\n${system}\n<>\n${user}\n\n直接输出 JSON,不要任何解释。`;
+ const combined = `<>\n${system}\n<>\n${user}\n\nOutput JSON directly with no explanation.`;
const args = ['exec', '--skip-git-repo-check', '-'];
const { stdout } = await runCli(cmd, args, combined, settings.cliTimeoutMs, job);
return parseCardsJson(stdout);
@@ -331,7 +191,7 @@ async function testBackend(settings) {
if (isApiBackend(settings.backend)) {
return testApiBackend(requestUrl, settings);
}
- throw new Error('未知 backend:' + settings.backend);
+ throw new Error('Unknown backend: ' + settings.backend);
}
async function summarizeDocument(content, settings, job) {
@@ -388,7 +248,7 @@ function addIconButton(parent, icon, title, onClick) {
await onClick();
} catch (err) {
console.error(err);
- new Notice(`${title}失败:` + (err.message || err));
+ new Notice(`${title} failed: ` + (err.message || err));
}
});
return button;
@@ -408,7 +268,7 @@ function addTextButton(parent, icon, label, onClick, cls) {
await onClick();
} catch (err) {
console.error(err);
- new Notice(`${label}失败:` + (err.message || err));
+ new Notice(`${label} failed: ` + (err.message || err));
}
});
return button;
@@ -419,7 +279,7 @@ async function copyToClipboard(text, successMsg) {
await navigator.clipboard.writeText(text);
new Notice(successMsg);
} catch (e) {
- new Notice('复制失败:' + (e.message || e));
+ new Notice('Copy failed: ' + (e.message || e));
}
}
@@ -447,7 +307,7 @@ class ParallelReaderView extends ItemView {
}
getViewType() { return VIEW_TYPE_PARALLEL; }
- getDisplayText() { return '对照阅读笔记'; }
+ getDisplayText() { return this.plugin.t('displayName'); }
getIcon() { return 'book-open'; }
async onOpen() {
@@ -468,9 +328,9 @@ class ParallelReaderView extends ItemView {
const container = this.containerEl.children[1];
container.empty();
const hint = container.createDiv({ cls: 'parallel-reader-empty' });
- hint.createEl('h3', { text: '对照阅读笔记' });
- hint.createEl('p', { text: '打开一篇笔记,然后运行命令:' });
- hint.createEl('code', { text: 'Parallel Reader: 为当前笔记生成对照笔记' });
+ hint.createEl('h3', { text: this.plugin.t('appTitle') });
+ hint.createEl('p', { text: this.plugin.t('emptyOpenNote') });
+ hint.createEl('code', { text: this.plugin.t('commandGenerate') });
}
async loadFor(file, sections, stale) {
@@ -486,7 +346,7 @@ class ParallelReaderView extends ItemView {
this.sourceFile = file;
this.sections = [];
this.stale = false;
- this.loadingMessage = message || '正在生成对照笔记...';
+ this.loadingMessage = message || this.plugin.t('loadingDefault');
this.errorMessage = '';
this.render();
}
@@ -496,7 +356,7 @@ class ParallelReaderView extends ItemView {
this.sections = [];
this.stale = false;
this.loadingMessage = '';
- this.errorMessage = message || '生成失败';
+ this.errorMessage = message || this.plugin.t('errorTitle');
this.render();
}
@@ -510,8 +370,8 @@ class ParallelReaderView extends ItemView {
container.empty();
const hint = container.createDiv({ cls: 'parallel-reader-empty' });
hint.createEl('h3', { text: file.basename });
- hint.createEl('p', { text: '该笔记尚无对照笔记缓存。运行命令:' });
- hint.createEl('code', { text: 'Parallel Reader: 为当前笔记生成对照笔记' });
+ hint.createEl('p', { text: this.plugin.t('emptyNoCache') });
+ hint.createEl('code', { text: this.plugin.t('commandGenerate') });
}
render() {
@@ -524,21 +384,21 @@ class ParallelReaderView extends ItemView {
const actions = headerRow.createDiv({ cls: 'parallel-reader-actions' });
if (this.sourceFile) {
if (this.plugin.isGeneratingFile(this.sourceFile)) {
- addIconButton(actions, 'square', '取消生成', () => this.plugin.cancelGenerationForFile(this.sourceFile));
+ addIconButton(actions, 'square', this.plugin.t('actionCancel'), () => this.plugin.cancelGenerationForFile(this.sourceFile));
} else {
- addIconButton(actions, 'refresh-cw', '重新生成', () => this.plugin.runForFile(this.sourceFile, true));
+ addIconButton(actions, 'refresh-cw', this.plugin.t('actionRegenerate'), () => this.plugin.runForFile(this.sourceFile, true));
}
- addIconButton(actions, 'copy', '复制全部 Markdown', () => this.plugin.copyCurrentViewMarkdown());
- addIconButton(actions, 'download', '导出到 Vault', () => this.exportToVault());
+ addIconButton(actions, 'copy', this.plugin.t('actionCopyAll'), () => this.plugin.copyCurrentViewMarkdown());
+ addIconButton(actions, 'download', this.plugin.t('actionExport'), () => this.exportToVault());
}
if (this.stale) {
const banner = container.createDiv({ cls: 'parallel-reader-stale-banner' });
- banner.createSpan({ text: '源笔记或生成配置已修改,当前是旧缓存。' });
+ banner.createSpan({ text: this.plugin.t('staleBanner') });
addTextButton(
banner,
'refresh-cw',
- '重新生成',
+ this.plugin.t('actionRegenerate'),
() => this.plugin.runForFile(this.sourceFile, true),
'parallel-reader-stale-button'
);
@@ -548,18 +408,18 @@ class ParallelReaderView extends ItemView {
const state = container.createDiv({ cls: 'parallel-reader-state parallel-reader-loading' });
state.createDiv({ cls: 'parallel-reader-spinner' });
state.createEl('div', { text: this.loadingMessage, cls: 'parallel-reader-state-title' });
- state.createEl('div', { text: '可以继续阅读原文,生成完成后会自动刷新右侧卡片。', cls: 'parallel-reader-state-subtitle' });
+ state.createEl('div', { text: this.plugin.t('loadingSubtitle'), cls: 'parallel-reader-state-subtitle' });
return;
}
if (this.errorMessage) {
const state = container.createDiv({ cls: 'parallel-reader-state parallel-reader-error' });
- state.createEl('div', { text: '生成失败', cls: 'parallel-reader-state-title' });
+ state.createEl('div', { text: this.plugin.t('errorTitle'), cls: 'parallel-reader-state-title' });
state.createEl('div', { text: this.errorMessage, cls: 'parallel-reader-state-subtitle' });
addTextButton(
state,
'refresh-cw',
- '重新生成',
+ this.plugin.t('actionRegenerate'),
() => this.plugin.runForFile(this.sourceFile, true),
'parallel-reader-text-button'
);
@@ -578,7 +438,7 @@ class ParallelReaderView extends ItemView {
const title = card.createEl('div', { cls: 'parallel-reader-card-title' });
title.createSpan({ text: s.title });
if (s.startLine < 0) {
- title.createEl('span', { text: ' ⚠', cls: 'parallel-reader-warn', title: 'anchor 匹配失败,无法滚动联动' });
+ title.createEl('span', { text: ' ⚠', cls: 'parallel-reader-warn', title: this.plugin.t('anchorMismatch') });
}
// --- Gist (rendered as markdown so inline bold/code/links work) ---
@@ -598,7 +458,7 @@ class ParallelReaderView extends ItemView {
bulletsEl.setText(md);
});
} else if (!s.gist) {
- card.createEl('div', { cls: 'parallel-reader-empty-li', text: '(未生成)' });
+ card.createEl('div', { cls: 'parallel-reader-empty-li', text: this.plugin.t('emptyCard') });
}
// Left click → jump to source line
@@ -616,17 +476,17 @@ class ParallelReaderView extends ItemView {
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
const menu = new Menu();
- menu.addItem(it => it.setTitle('复制 Markdown').setIcon('copy')
- .onClick(() => copyToClipboard(cardToMarkdown(s), '已复制 Markdown')));
- menu.addItem(it => it.setTitle('复制纯文本').setIcon('clipboard-copy')
- .onClick(() => copyToClipboard(cardToPlain(s), '已复制纯文本')));
+ menu.addItem(it => it.setTitle(this.plugin.t('menuCopyMarkdown')).setIcon('copy')
+ .onClick(() => copyToClipboard(cardToMarkdown(s), this.plugin.t('copiedMarkdown'))));
+ menu.addItem(it => it.setTitle(this.plugin.t('menuCopyPlain')).setIcon('clipboard-copy')
+ .onClick(() => copyToClipboard(cardToPlain(s), this.plugin.t('copiedPlain'))));
if (s.anchor) {
- menu.addItem(it => it.setTitle('复制 anchor 引用').setIcon('quote-glyph')
- .onClick(() => copyToClipboard(s.anchor, '已复制引用原文')));
+ menu.addItem(it => it.setTitle(this.plugin.t('menuCopyAnchor')).setIcon('quote-glyph')
+ .onClick(() => copyToClipboard(s.anchor, this.plugin.t('copiedAnchor'))));
}
menu.addSeparator();
if (s.startLine >= 0) {
- menu.addItem(it => it.setTitle('跳转到原文').setIcon('arrow-right')
+ menu.addItem(it => it.setTitle(this.plugin.t('menuJumpSource')).setIcon('arrow-right')
.onClick(() => this.plugin.scrollEditorToLine(s.startLine, this.sourceFile)));
}
menu.showAtMouseEvent(e);
@@ -651,7 +511,7 @@ class ParallelReaderView extends ItemView {
async exportToVault() {
if (!this.sourceFile) return;
const folder = this.plugin.settings.exportFolder.replace(/\/$/, '');
- const name = `${this.sourceFile.basename} - 对照笔记.md`;
+ const name = `${this.sourceFile.basename} - ${this.plugin.t('displayName')}.md`;
const targetPath = `${folder}/${name}`;
const markdown = [
@@ -661,7 +521,7 @@ class ParallelReaderView extends ItemView {
'tool: parallel-reader',
'---',
'',
- cardsToMarkdown(`${this.sourceFile.basename} · 对照笔记`, this.sections),
+ cardsToMarkdown(`${this.sourceFile.basename} · ${this.plugin.t('displayName')}`, this.sections),
'',
].join('\n');
@@ -678,7 +538,7 @@ class ParallelReaderView extends ItemView {
} else {
await app.vault.create(targetPath, markdown);
}
- new Notice(`已导出 → ${targetPath}`);
+ new Notice(this.plugin.t('exported', { path: targetPath }));
}
}
@@ -691,11 +551,15 @@ class ParallelReaderPlugin extends Plugin {
_scrollDispose: (() => void) | null;
_settingsSaveTimer: ReturnType | null;
+ t(key, vars?) {
+ return translate(this.settings || DEFAULT_SETTINGS, key, vars);
+ }
+
async onload() {
await this.loadSettings();
this.jobs = new GenerationJobManager();
- this.addRibbonIcon('book-open', '打开对照笔记面板', async () => {
+ this.addRibbonIcon('book-open', this.t('ribbonOpen'), async () => {
const active = this.getActiveView();
await this.ensureView();
if (active?.file) await this.syncViewToFile(active.file);
@@ -705,19 +569,19 @@ class ParallelReaderPlugin extends Plugin {
this.addCommand({
id: 'parallel-reader-run',
- name: '为当前笔记生成对照笔记(缓存优先)',
+ name: this.t('cmdRun'),
callback: () => this.runForActiveFile(false),
});
this.addCommand({
id: 'parallel-reader-regen',
- name: '强制重新生成(绕过缓存)',
+ name: this.t('cmdRegen'),
callback: () => this.runForActiveFile(true),
});
this.addCommand({
id: 'parallel-reader-open-view',
- name: '打开对照笔记面板',
+ name: this.t('cmdOpenView'),
callback: async () => {
const active = this.getActiveView();
await this.ensureView();
@@ -727,40 +591,40 @@ class ParallelReaderPlugin extends Plugin {
this.addCommand({
id: 'parallel-reader-export-current',
- name: '导出当前对照笔记到 Vault',
+ name: this.t('cmdExport'),
callback: () => this.exportCurrentView(),
});
this.addCommand({
id: 'parallel-reader-copy-current-markdown',
- name: '复制当前对照笔记 Markdown',
+ name: this.t('cmdCopyMarkdown'),
callback: () => this.copyCurrentViewMarkdown(),
});
this.addCommand({
id: 'parallel-reader-cancel-current',
- name: '取消当前对照笔记生成',
+ name: this.t('cmdCancel'),
callback: () => this.cancelActiveGeneration(),
});
this.addCommand({
id: 'parallel-reader-clear-current',
- name: '清除当前笔记的缓存',
+ name: this.t('cmdClearCurrent'),
callback: async () => {
const active = this.getActiveView();
- if (!active?.file) return new Notice('没有当前笔记');
+ if (!active?.file) return new Notice(this.t('noCurrentNote'));
await this.cacheDelete(active.file.path);
- new Notice('已清除缓存:' + active.file.basename);
+ new Notice(this.t('cacheClearedFile', { name: active.file.basename }));
},
});
this.addCommand({
id: 'parallel-reader-clear-all',
- name: '清除所有缓存',
+ name: this.t('cmdClearAll'),
callback: async () => {
const n = Object.keys(this.cache).length;
await this.cacheClear();
- new Notice(`已清除 ${n} 条缓存`);
+ new Notice(this.t('cacheClearedAll', { count: n }));
},
});
@@ -942,11 +806,11 @@ class ParallelReaderPlugin extends Plugin {
cancelGenerationForFile(file) {
if (!file || !file.path) {
- new Notice('当前没有可取消的生成任务');
+ new Notice(this.t('noCancelableJob'));
return false;
}
const cancelled = this.jobs.cancel(file.path);
- new Notice(cancelled ? '已请求取消生成' : '当前没有可取消的生成任务');
+ new Notice(cancelled ? this.t('cancelRequested') : this.t('noCancelableJob'));
return cancelled;
}
@@ -964,27 +828,27 @@ class ParallelReaderPlugin extends Plugin {
if (active?.file && this.cancelGenerationForFile(active.file)) return;
const view = this.app.workspace.getLeavesOfType(VIEW_TYPE_PARALLEL)[0]?.view as ParallelReaderView | undefined;
if (view?.sourceFile) this.cancelGenerationForFile(view.sourceFile);
- else new Notice('当前没有可取消的生成任务');
+ else new Notice(this.t('noCancelableJob'));
}
addFileMenuItems(menu, file) {
if (!(file instanceof TFile) || !file.path.endsWith('.md')) return;
menu.addSeparator();
menu.addItem(it => it
- .setTitle('生成对照笔记')
+ .setTitle(this.t('fileMenuGenerate'))
.setIcon('book-open')
.onClick(() => this.runForFile(file, false)));
menu.addItem(it => it
- .setTitle('强制重新生成对照笔记')
+ .setTitle(this.t('fileMenuRegen'))
.setIcon('refresh-cw')
.onClick(() => this.runForFile(file, true)));
if (this.cacheGet(file.path)) {
menu.addItem(it => it
- .setTitle('清除对照笔记缓存')
+ .setTitle(this.t('fileMenuClear'))
.setIcon('trash')
.onClick(async () => {
await this.cacheDelete(file.path);
- new Notice('已清除缓存:' + file.basename);
+ new Notice(this.t('cacheClearedFile', { name: file.basename }));
}));
}
}
@@ -1037,7 +901,7 @@ class ParallelReaderPlugin extends Plugin {
if (active?.file) await this.syncViewToFile(active.file);
}
if (!view.sourceFile || !view.sections.length) {
- new Notice('当前没有可导出的对照笔记');
+ new Notice(this.t('noExportContent'));
return;
}
await view.exportToVault();
@@ -1050,19 +914,19 @@ class ParallelReaderPlugin extends Plugin {
if (active?.file) await this.syncViewToFile(active.file);
}
if (!view.sourceFile || !view.sections.length) {
- new Notice('当前没有可复制的对照笔记');
+ new Notice(this.t('noCopyContent'));
return;
}
await copyToClipboard(
- cardsToMarkdown(`${view.sourceFile.basename} · 对照笔记`, view.sections),
- '已复制全部 Markdown'
+ cardsToMarkdown(`${view.sourceFile.basename} · ${this.t('displayName')}`, view.sections),
+ this.t('copiedAllMarkdown')
);
}
async runForActiveFile(force) {
const mdView = this.getActiveView();
if (!mdView || !mdView.file) {
- new Notice('先打开一篇笔记');
+ new Notice(this.t('openNoteFirst'));
return;
}
return this.runForFile(mdView.file, force);
@@ -1070,12 +934,12 @@ class ParallelReaderPlugin extends Plugin {
async runForFile(file, force) {
if (!file) {
- new Notice('先打开一篇笔记');
+ new Notice(this.t('openNoteFirst'));
return;
}
const runningKey = file.path;
if (this.jobs.isRunning(runningKey)) {
- new Notice('该笔记正在生成对照笔记');
+ new Notice(this.t('alreadyGenerating'));
return;
}
@@ -1085,7 +949,7 @@ class ParallelReaderPlugin extends Plugin {
const content = await this.app.vault.read(file);
job.throwIfCancelled();
if (!content.trim()) {
- new Notice('笔记为空');
+ new Notice(this.t('emptyNote'));
return;
}
@@ -1104,18 +968,18 @@ class ParallelReaderPlugin extends Plugin {
}
}
- await view.renderLoading(file, '对照阅读:让 LLM 读全文并自适应切段...');
+ await view.renderLoading(file, this.t('loadingGenerating'));
const maxDocChars = Number(this.settings.maxDocChars) || DEFAULT_SETTINGS.maxDocChars;
if (content.length > maxDocChars) {
- new Notice(`笔记较长:仅发送前 ${maxDocChars} 个字符给模型`);
+ new Notice(this.t('longNoteTruncated', { count: maxDocChars }));
}
- new Notice(`对照阅读:让 LLM 读全文并自适应切段…`);
+ new Notice(this.t('generatingNotice'));
job.setPhase('generating');
const sections = await summarizeDocument(content, this.settings, job);
job.throwIfCancelled();
if (sections.length === 0) {
- new Notice('LLM 未返回任何 card');
+ new Notice(this.t('noCardsReturned'));
return;
}
// Persist raw cards (without computed startLine — re-resolve on load, in case source was renamed/edited)
@@ -1133,21 +997,27 @@ class ParallelReaderPlugin extends Plugin {
await view.loadFor(file, sections, false);
}
const unanchored = sections.filter(s => s.startLine < 0).length;
- new Notice(`对照笔记生成完成:${sections.length} 段${unanchored ? `(⚠ ${unanchored} 段 anchor 未匹配)` : ''}`);
+ new Notice(this.t('generationDone', {
+ count: sections.length,
+ suffix: unanchored ? this.t('unanchoredSuffix', { count: unanchored }) : '',
+ }));
}).catch(async e => {
if (e instanceof GenerationJobAlreadyRunningError) {
new Notice(e.message);
return;
}
if (e instanceof GenerationJobCancelledError) {
- if (this.viewIsShowingFile(view, file)) await view.renderError(file, '生成已取消');
- new Notice('已取消生成');
+ if (this.viewIsShowingFile(view, file)) await view.renderError(file, this.t('cancelledError'));
+ new Notice(this.t('cancelled'));
return;
}
const kind = classifyGenerationError(e);
console.error(e);
if (this.viewIsShowingFile(view, file)) await view.renderError(file, e.message || String(e));
- new Notice(`生成失败${kind === 'unknown' ? '' : `(${kind})`}:` + (e.message || e));
+ new Notice(this.t('generationFailed', {
+ kind: kind === 'unknown' ? '' : ` (${kind})`,
+ error: e.message || e,
+ }));
});
}
@@ -1275,7 +1145,7 @@ class ParallelReaderPlugin extends Plugin {
if (active) leaf = active.leaf;
}
if (!leaf) {
- new Notice('找不到源笔记对应的编辑器窗口');
+ new Notice(this.t('noEditor'));
return;
}
@@ -1308,12 +1178,29 @@ class ParallelReaderSettingTab extends PluginSettingTab {
display() {
const { containerEl } = this;
+ const tr = (key, vars?) => this.plugin.t(key, vars);
containerEl.empty();
- containerEl.createEl('h2', { text: 'Parallel Reader 设置' });
+ containerEl.createEl('h2', { text: tr('settingsTitle') });
new Setting(containerEl)
- .setName('Backend')
- .setDesc('生成 bullet 的后端:CLI 复用本机登录;API 支持 OpenAI/Anthropic/Gemini 及兼容代理')
+ .setName(tr('settingUiLanguageName'))
+ .setDesc(tr('settingUiLanguageDesc'))
+ .addDropdown(d => {
+ for (const [id, label] of Object.entries(UI_LANGUAGES)) {
+ d.addOption(id, label);
+ }
+ return d
+ .setValue(this.plugin.settings.uiLanguage || DEFAULT_SETTINGS.uiLanguage)
+ .onChange(async v => {
+ this.plugin.settings.uiLanguage = v;
+ await this.plugin.saveSettings();
+ this.display();
+ });
+ });
+
+ new Setting(containerEl)
+ .setName(tr('settingBackendName'))
+ .setDesc(tr('settingBackendDesc'))
.addDropdown(d => d
.addOption('claude-code', 'Claude Code CLI')
.addOption('codex', 'Codex CLI')
@@ -1333,10 +1220,10 @@ class ParallelReaderSettingTab extends PluginSettingTab {
if (!apiBackend) {
new Setting(containerEl)
- .setName('CLI 路径(可选)')
- .setDesc('留空则自动探测常见位置;Obsidian GUI 启动时不继承 shell PATH,必要时填绝对路径')
+ .setName(tr('settingCliPathName'))
+ .setDesc(tr('settingCliPathDesc'))
.addText(t => t
- .setPlaceholder('例:/Users/you/bin/codex')
+ .setPlaceholder(tr('settingCliPathPlaceholder'))
.setValue(this.plugin.settings.cliPath)
.onChange(async v => {
this.plugin.settings.cliPath = v.trim();
@@ -1344,7 +1231,7 @@ class ParallelReaderSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
- .setName('CLI 超时 (ms)')
+ .setName(tr('settingCliTimeoutName'))
.addText(t => t
.setValue(String(this.plugin.settings.cliTimeoutMs))
.onChange(async v => {
@@ -1355,12 +1242,12 @@ class ParallelReaderSettingTab extends PluginSettingTab {
}
}));
} else {
- containerEl.createEl('h3', { text: 'API Provider' });
+ containerEl.createEl('h3', { text: tr('apiProviderHeader') });
const preset = getApiPreset(this.plugin.settings);
new Setting(containerEl)
- .setName('Provider preset')
- .setDesc('参考 OpenClaw 的 provider/model 思路:preset 只负责协议、base URL 和认证默认值')
+ .setName(tr('settingProviderPresetName'))
+ .setDesc(tr('settingProviderPresetDesc'))
.addDropdown(d => {
for (const [id, entry] of Object.entries(API_PROVIDER_PRESETS)) {
d.addOption(id, (entry as any).label);
@@ -1375,8 +1262,8 @@ class ParallelReaderSettingTab extends PluginSettingTab {
});
new Setting(containerEl)
- .setName('API format')
- .setDesc('不同 provider 的 wire protocol;OpenAI-compatible 代理通常选 Chat Completions')
+ .setName(tr('settingApiFormatName'))
+ .setDesc(tr('settingApiFormatDesc'))
.addDropdown(d => {
for (const [id, entry] of Object.entries(API_FORMATS)) {
d.addOption(id, (entry as any).label);
@@ -1391,8 +1278,8 @@ class ParallelReaderSettingTab extends PluginSettingTab {
});
new Setting(containerEl)
- .setName('Base URL')
- .setDesc('填 provider 根地址,不要附加 /chat/completions;留空时使用 preset 默认值')
+ .setName(tr('settingBaseUrlName'))
+ .setDesc(tr('settingBaseUrlDesc'))
.addText(t => t
.setPlaceholder(
(this.plugin.settings.apiProvider || '').startsWith('custom-')
@@ -1406,8 +1293,8 @@ class ParallelReaderSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
- .setName('API Key')
- .setDesc('本地 Ollama/LM Studio 可留空;其他 provider 通常需要 key')
+ .setName(tr('settingApiKeyName'))
+ .setDesc(tr('settingApiKeyDesc'))
.addText(t => {
t.inputEl.type = 'password';
return t
@@ -1420,8 +1307,8 @@ class ParallelReaderSettingTab extends PluginSettingTab {
});
new Setting(containerEl)
- .setName('API Key 环境变量')
- .setDesc('可选;Obsidian GUI 不一定继承 shell 环境,直接填 API Key 更稳定')
+ .setName(tr('settingApiKeyEnvName'))
+ .setDesc(tr('settingApiKeyEnvDesc'))
.addText(t => t
.setPlaceholder(preset.envVar || 'OPENAI_API_KEY')
.setValue(this.plugin.settings.apiKeyEnvVar)
@@ -1431,8 +1318,8 @@ class ParallelReaderSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
- .setName('认证方式')
- .setDesc('Auto 使用 provider preset;自定义代理可按需要改成 Bearer、x-api-key 或 none')
+ .setName(tr('settingAuthTypeName'))
+ .setDesc(tr('settingAuthTypeDesc'))
.addDropdown(d => {
for (const [id, label] of Object.entries(API_AUTH_TYPES)) {
d.addOption(id, label);
@@ -1446,8 +1333,8 @@ class ParallelReaderSettingTab extends PluginSettingTab {
});
new Setting(containerEl)
- .setName('额外 headers')
- .setDesc('可选。支持 JSON 对象或每行 `Header: value`,用于 Cloudflare AI Gateway 等代理')
+ .setName(tr('settingHeadersName'))
+ .setDesc(tr('settingHeadersDesc'))
.addTextArea(t => t
.setPlaceholder('cf-aig-authorization: Bearer ...')
.setValue(this.plugin.settings.apiHeaders)
@@ -1457,7 +1344,7 @@ class ParallelReaderSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
- .setName('最大输出 tokens')
+ .setName(tr('settingMaxTokensName'))
.addText(t => t
.setValue(String(this.plugin.settings.apiMaxTokens))
.onChange(async v => {
@@ -1470,10 +1357,10 @@ class ParallelReaderSettingTab extends PluginSettingTab {
}
new Setting(containerEl)
- .setName('Model')
+ .setName(tr('settingModelName'))
.setDesc(apiBackend
- ? 'API 调用的模型 ID;支持 OpenClaw 风格 provider/model,若 provider 前缀匹配当前 preset 会自动剥离'
- : 'Claude Code 下会传 --model;Codex 下通常忽略(用 Codex 默认配置)')
+ ? tr('settingModelDescApi')
+ : tr('settingModelDescCli'))
.addText(t => t
.setPlaceholder(apiBackend ? (getApiPreset(this.plugin.settings).model || 'model-id') : DEFAULT_SETTINGS.model)
.setValue(this.plugin.settings.model)
@@ -1483,8 +1370,8 @@ class ParallelReaderSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
- .setName('最大输入字符数')
- .setDesc('超过该长度会截断后再发送给模型;长上下文模型可适当调大')
+ .setName(tr('settingMaxInputName'))
+ .setDesc(tr('settingMaxInputDesc'))
.addText(t => t
.setValue(String(this.plugin.settings.maxDocChars || DEFAULT_SETTINGS.maxDocChars))
.onChange(async v => {
@@ -1495,11 +1382,11 @@ class ParallelReaderSettingTab extends PluginSettingTab {
}
}));
- containerEl.createEl('h3', { text: 'Prompt' });
+ containerEl.createEl('h3', { text: tr('promptHeader') });
new Setting(containerEl)
- .setName('输出语言')
- .setDesc('控制 title/gist/bullets 的语言;anchor 始终逐字复制原文')
+ .setName(tr('settingPromptLanguageName'))
+ .setDesc(tr('settingPromptLanguageDesc'))
.addDropdown(d => {
for (const [id, label] of Object.entries(PROMPT_LANGUAGES)) {
d.addOption(id, label);
@@ -1513,8 +1400,8 @@ class ParallelReaderSettingTab extends PluginSettingTab {
});
new Setting(containerEl)
- .setName('卡片数量范围')
- .setDesc('模型会在这个范围内自适应切段')
+ .setName(tr('settingCardRangeName'))
+ .setDesc(tr('settingCardRangeDesc'))
.addText(t => t
.setPlaceholder('min')
.setValue(String(this.plugin.settings.minCards || DEFAULT_SETTINGS.minCards))
@@ -1538,12 +1425,12 @@ class ParallelReaderSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
- .setName('自定义 system prompt')
- .setDesc('可选。支持变量:{minCards}、{maxCards}、{languageInstruction}、{schema}、{example}')
+ .setName(tr('settingCustomPromptName'))
+ .setDesc(tr('settingCustomPromptDesc'))
.addTextArea(t => {
t.inputEl.rows = 8;
return t
- .setPlaceholder('留空使用内置 prompt')
+ .setPlaceholder(tr('settingCustomPromptPlaceholder'))
.setValue(this.plugin.settings.customSystemPrompt || '')
.onChange(async v => {
this.plugin.settings.customSystemPrompt = v;
@@ -1552,8 +1439,8 @@ class ParallelReaderSettingTab extends PluginSettingTab {
});
new Setting(containerEl)
- .setName('测试当前后端')
- .setDesc(apiBackend ? '会发起一次最小 LLM 请求验证 API 设置' : '调用 ` --version` 验证 spawn 能找到二进制')
+ .setName(tr('settingTestBackendName'))
+ .setDesc(apiBackend ? tr('settingTestBackendDescApi') : tr('settingTestBackendDescCli'))
.addButton(b => b
.setButtonText('Test')
.onClick(async () => {
@@ -1561,13 +1448,13 @@ class ParallelReaderSettingTab extends PluginSettingTab {
const result = await testBackend(this.plugin.settings);
new Notice(`✓ ${result.slice(0, 180)}`, 8000);
} catch (e) {
- new Notice(`✗ 后端测试失败:${e.message}`, 10000);
+ new Notice(tr('backendTestFailed', { error: e.message }), 10000);
}
}));
new Setting(containerEl)
- .setName('导出文件夹')
- .setDesc('对照笔记生成位置(相对 Vault 根)')
+ .setName(tr('settingExportFolderName'))
+ .setDesc(tr('settingExportFolderDesc'))
.addText(t => t
.setValue(this.plugin.settings.exportFolder)
.onChange(async v => {
@@ -1575,11 +1462,11 @@ class ParallelReaderSettingTab extends PluginSettingTab {
this.plugin.saveSettingsDebounced();
}));
- containerEl.createEl('h3', { text: '缓存' });
+ containerEl.createEl('h3', { text: tr('cacheHeader') });
new Setting(containerEl)
- .setName('最大缓存篇数')
- .setDesc('超过上限后按最近访问时间淘汰最旧的笔记缓存;缓存保存在插件目录的 cache.json')
+ .setName(tr('settingMaxCacheName'))
+ .setDesc(tr('settingMaxCacheDesc'))
.addText(t => {
t.setValue(String(this.plugin.settings.maxCacheEntries || DEFAULT_MAX_CACHE_ENTRIES));
const commit = async () => {
@@ -1588,7 +1475,7 @@ class ParallelReaderSettingTab extends PluginSettingTab {
this.plugin.settings.maxCacheEntries = n;
await this.plugin.saveSettings();
const removed = await this.plugin.pruneCacheIfNeeded();
- if (removed.length > 0) new Notice(`已淘汰 ${removed.length} 条旧缓存`);
+ if (removed.length > 0) new Notice(tr('cachePruned', { count: removed.length }));
this.display();
}
};
@@ -1601,15 +1488,15 @@ class ParallelReaderSettingTab extends PluginSettingTab {
const cacheCount = Object.keys(this.plugin.cache).length;
new Setting(containerEl)
- .setName(`已缓存笔记:${cacheCount} 篇`)
- .setDesc('缓存以源笔记 SHA1 + 生成配置指纹作为失效键,源笔记或模型配置修改后会显示 stale 提示')
+ .setName(tr('cachedNotesName', { count: cacheCount }))
+ .setDesc(tr('cachedNotesDesc'))
.addButton(b => b
- .setButtonText('清除所有缓存')
+ .setButtonText(tr('clearAllCacheButton'))
.setWarning()
.onClick(async () => {
const n = Object.keys(this.plugin.cache).length;
await this.plugin.cacheClear();
- new Notice(`已清除 ${n} 条缓存`);
+ new Notice(tr('cacheClearedAll', { count: n }));
this.display();
}));
}
@@ -1635,5 +1522,6 @@ export const __test = {
normalizeCardsPayload,
pruneCacheEntries,
summarizeViaApi,
+ translate,
tokenLimitFieldForOpenAiChat,
};
diff --git a/src/anchor.ts b/src/anchor.ts
new file mode 100644
index 0000000..2692be5
--- /dev/null
+++ b/src/anchor.ts
@@ -0,0 +1,57 @@
+'use strict';
+
+export function findLineForAnchor(content, anchor) {
+ if (!anchor) return -1;
+ const normalize = s => s.replace(/\s+/g, ' ').trim();
+ const normalizeWithMap = s => {
+ const chars = [];
+ const map = [];
+ let pendingWhitespace = false;
+ for (let i = 0; i < s.length; i++) {
+ const c = s[i];
+ if (/\s/.test(c)) {
+ pendingWhitespace = chars.length > 0;
+ continue;
+ }
+ if (pendingWhitespace) {
+ chars.push(' ');
+ map.push(i);
+ pendingWhitespace = false;
+ }
+ chars.push(c);
+ map.push(i);
+ }
+ return { text: chars.join(''), map };
+ };
+ const tryAt = needle => {
+ if (!needle) return -1;
+ const idx = content.indexOf(needle);
+ if (idx === -1) return -1;
+ let line = 0;
+ for (let i = 0; i < idx; i++) if (content[i] === '\n') line++;
+ return line;
+ };
+
+ let line = tryAt(anchor);
+ if (line >= 0) return line;
+
+ line = tryAt(anchor.trim());
+ if (line >= 0) return line;
+
+ for (const len of [60, 40, 25, 15]) {
+ const prefix = anchor.trim().slice(0, len);
+ line = tryAt(prefix);
+ if (line >= 0) return line;
+ }
+
+ const normDoc = normalizeWithMap(content);
+ const normAnchor = normalize(anchor).slice(0, 30);
+ if (!normAnchor) return -1;
+ const normIdx = normDoc.text.indexOf(normAnchor);
+ if (normIdx === -1) return -1;
+ const originalIdx = normDoc.map[normIdx];
+ if (originalIdx == null) return -1;
+ let l = 0;
+ for (let j = 0; j < originalIdx; j++) if (content[j] === '\n') l++;
+ return l;
+}
diff --git a/src/i18n.ts b/src/i18n.ts
new file mode 100644
index 0000000..7ec43b0
--- /dev/null
+++ b/src/i18n.ts
@@ -0,0 +1,244 @@
+'use strict';
+
+export const STRINGS = {
+ zh: {
+ appTitle: '对照阅读笔记',
+ settingsTitle: 'Parallel Reader 设置',
+ emptyOpenNote: '打开一篇笔记,然后运行命令:',
+ emptyNoCache: '该笔记尚无对照笔记缓存。运行命令:',
+ commandGenerate: 'Parallel Reader: 为当前笔记生成对照笔记',
+ displayName: '对照阅读笔记',
+ ribbonOpen: '打开对照笔记面板',
+ cmdRun: '为当前笔记生成对照笔记(缓存优先)',
+ cmdRegen: '强制重新生成(绕过缓存)',
+ cmdOpenView: '打开对照笔记面板',
+ cmdExport: '导出当前对照笔记到 Vault',
+ cmdCopyMarkdown: '复制当前对照笔记 Markdown',
+ cmdCancel: '取消当前对照笔记生成',
+ cmdClearCurrent: '清除当前笔记的缓存',
+ cmdClearAll: '清除所有缓存',
+ actionCancel: '取消生成',
+ actionRegenerate: '重新生成',
+ actionCopyAll: '复制全部 Markdown',
+ actionExport: '导出到 Vault',
+ staleBanner: '源笔记或生成配置已修改,当前是旧缓存。',
+ loadingDefault: '正在生成对照笔记...',
+ loadingGenerating: '对照阅读:让 LLM 读全文并自适应切段...',
+ loadingSubtitle: '可以继续阅读原文,生成完成后会自动刷新右侧卡片。',
+ errorTitle: '生成失败',
+ emptyCard: '(未生成)',
+ anchorMismatch: 'anchor 匹配失败,无法滚动联动',
+ menuCopyMarkdown: '复制 Markdown',
+ menuCopyPlain: '复制纯文本',
+ menuCopyAnchor: '复制 anchor 引用',
+ menuJumpSource: '跳转到原文',
+ copiedMarkdown: '已复制 Markdown',
+ copiedPlain: '已复制纯文本',
+ copiedAnchor: '已复制引用原文',
+ copiedAllMarkdown: '已复制全部 Markdown',
+ copyFailed: '复制失败:{error}',
+ actionFailed: '{label}失败:{error}',
+ exported: '已导出 → {path}',
+ noCurrentNote: '没有当前笔记',
+ cacheClearedFile: '已清除缓存:{name}',
+ cacheClearedAll: '已清除 {count} 条缓存',
+ noCancelableJob: '当前没有可取消的生成任务',
+ cancelRequested: '已请求取消生成',
+ fileMenuGenerate: '生成对照笔记',
+ fileMenuRegen: '强制重新生成对照笔记',
+ fileMenuClear: '清除对照笔记缓存',
+ noExportContent: '当前没有可导出的对照笔记',
+ noCopyContent: '当前没有可复制的对照笔记',
+ openNoteFirst: '先打开一篇笔记',
+ alreadyGenerating: '该笔记正在生成对照笔记',
+ emptyNote: '笔记为空',
+ longNoteTruncated: '笔记较长:仅发送前 {count} 个字符给模型',
+ generatingNotice: '对照阅读:让 LLM 读全文并自适应切段…',
+ noCardsReturned: 'LLM 未返回任何 card',
+ generationDone: '对照笔记生成完成:{count} 段{suffix}',
+ unanchoredSuffix: '(⚠ {count} 段 anchor 未匹配)',
+ cancelled: '已取消生成',
+ cancelledError: '生成已取消',
+ generationFailed: '生成失败{kind}:{error}',
+ noEditor: '找不到源笔记对应的编辑器窗口',
+ settingUiLanguageName: '界面语言',
+ settingUiLanguageDesc: '控制插件界面、命令和提示文案;Auto 跟随 Obsidian/系统语言',
+ settingBackendName: 'Backend',
+ settingBackendDesc: '生成 bullet 的后端:CLI 复用本机登录;API 支持 OpenAI/Anthropic/Gemini 及兼容代理',
+ settingCliPathName: 'CLI 路径(可选)',
+ settingCliPathDesc: '留空则自动探测常见位置;Obsidian GUI 启动时不继承 shell PATH,必要时填绝对路径',
+ settingCliPathPlaceholder: '例:/Users/you/bin/codex',
+ settingCliTimeoutName: 'CLI 超时 (ms)',
+ apiProviderHeader: 'API Provider',
+ settingProviderPresetName: 'Provider preset',
+ settingProviderPresetDesc: '参考 OpenClaw 的 provider/model 思路:preset 只负责协议、base URL 和认证默认值',
+ settingApiFormatName: 'API format',
+ settingApiFormatDesc: '不同 provider 的 wire protocol;OpenAI-compatible 代理通常选 Chat Completions',
+ settingBaseUrlName: 'Base URL',
+ settingBaseUrlDesc: '填 provider 根地址,不要附加 /chat/completions;留空时使用 preset 默认值',
+ settingApiKeyName: 'API Key',
+ settingApiKeyDesc: '本地 Ollama/LM Studio 可留空;其他 provider 通常需要 key',
+ settingApiKeyEnvName: 'API Key 环境变量',
+ settingApiKeyEnvDesc: '可选;Obsidian GUI 不一定继承 shell 环境,直接填 API Key 更稳定',
+ settingAuthTypeName: '认证方式',
+ settingAuthTypeDesc: 'Auto 使用 provider preset;自定义代理可按需要改成 Bearer、x-api-key 或 none',
+ settingHeadersName: '额外 headers',
+ settingHeadersDesc: '可选。支持 JSON 对象或每行 `Header: value`,用于 Cloudflare AI Gateway 等代理',
+ settingMaxTokensName: '最大输出 tokens',
+ settingModelName: 'Model',
+ settingModelDescApi: 'API 调用的模型 ID;支持 OpenClaw 风格 provider/model,若 provider 前缀匹配当前 preset 会自动剥离',
+ settingModelDescCli: 'Claude Code 下会传 --model;Codex 下通常忽略(用 Codex 默认配置)',
+ settingMaxInputName: '最大输入字符数',
+ settingMaxInputDesc: '超过该长度会截断后再发送给模型;长上下文模型可适当调大',
+ promptHeader: 'Prompt',
+ settingPromptLanguageName: '输出语言',
+ settingPromptLanguageDesc: '控制 title/gist/bullets 的语言;anchor 始终逐字复制原文',
+ settingCardRangeName: '卡片数量范围',
+ settingCardRangeDesc: '模型会在这个范围内自适应切段',
+ settingCustomPromptName: '自定义 system prompt',
+ settingCustomPromptDesc: '可选。支持变量:{minCards}、{maxCards}、{languageInstruction}、{schema}、{example}',
+ settingCustomPromptPlaceholder: '留空使用内置 prompt',
+ settingTestBackendName: '测试当前后端',
+ settingTestBackendDescApi: '会发起一次最小 LLM 请求验证 API 设置',
+ settingTestBackendDescCli: '调用 ` --version` 验证 spawn 能找到二进制',
+ backendTestFailed: '✗ 后端测试失败:{error}',
+ settingExportFolderName: '导出文件夹',
+ settingExportFolderDesc: '对照笔记生成位置(相对 Vault 根)',
+ cacheHeader: '缓存',
+ settingMaxCacheName: '最大缓存篇数',
+ settingMaxCacheDesc: '超过上限后按最近访问时间淘汰最旧的笔记缓存;缓存保存在插件目录的 cache.json',
+ cachePruned: '已淘汰 {count} 条旧缓存',
+ cachedNotesName: '已缓存笔记:{count} 篇',
+ cachedNotesDesc: '缓存以源笔记 SHA1 + 生成配置指纹作为失效键,源笔记或模型配置修改后会显示 stale 提示',
+ clearAllCacheButton: '清除所有缓存',
+ },
+ en: {
+ appTitle: 'Parallel Reader',
+ settingsTitle: 'Parallel Reader Settings',
+ emptyOpenNote: 'Open a note, then run:',
+ emptyNoCache: 'This note has no cached parallel notes. Run:',
+ commandGenerate: 'Parallel Reader: Generate notes for current note',
+ displayName: 'Parallel Reader',
+ ribbonOpen: 'Open Parallel Reader pane',
+ cmdRun: 'Generate parallel notes for current note (cache first)',
+ cmdRegen: 'Regenerate parallel notes (bypass cache)',
+ cmdOpenView: 'Open Parallel Reader pane',
+ cmdExport: 'Export current parallel notes to Vault',
+ cmdCopyMarkdown: 'Copy current parallel notes as Markdown',
+ cmdCancel: 'Cancel current parallel-note generation',
+ cmdClearCurrent: 'Clear cache for current note',
+ cmdClearAll: 'Clear all caches',
+ actionCancel: 'Cancel generation',
+ actionRegenerate: 'Regenerate',
+ actionCopyAll: 'Copy all Markdown',
+ actionExport: 'Export to Vault',
+ staleBanner: 'The source note or generation settings changed. This is stale cache.',
+ loadingDefault: 'Generating parallel notes...',
+ loadingGenerating: 'Parallel Reader: asking the LLM to read and segment the full note...',
+ loadingSubtitle: 'You can keep reading. Cards will refresh when generation finishes.',
+ errorTitle: 'Generation failed',
+ emptyCard: '(not generated)',
+ anchorMismatch: 'Anchor did not match; scroll sync is unavailable',
+ menuCopyMarkdown: 'Copy Markdown',
+ menuCopyPlain: 'Copy plain text',
+ menuCopyAnchor: 'Copy anchor quote',
+ menuJumpSource: 'Jump to source',
+ copiedMarkdown: 'Copied Markdown',
+ copiedPlain: 'Copied plain text',
+ copiedAnchor: 'Copied anchor quote',
+ copiedAllMarkdown: 'Copied all Markdown',
+ copyFailed: 'Copy failed: {error}',
+ actionFailed: '{label} failed: {error}',
+ exported: 'Exported → {path}',
+ noCurrentNote: 'No current note',
+ cacheClearedFile: 'Cleared cache: {name}',
+ cacheClearedAll: 'Cleared {count} cache entries',
+ noCancelableJob: 'No cancellable generation job',
+ cancelRequested: 'Cancel requested',
+ fileMenuGenerate: 'Generate parallel notes',
+ fileMenuRegen: 'Regenerate parallel notes',
+ fileMenuClear: 'Clear parallel-note cache',
+ noExportContent: 'No parallel notes to export',
+ noCopyContent: 'No parallel notes to copy',
+ openNoteFirst: 'Open a note first',
+ alreadyGenerating: 'This note is already being generated',
+ emptyNote: 'The note is empty',
+ longNoteTruncated: 'Long note: only the first {count} characters will be sent to the model',
+ generatingNotice: 'Parallel Reader: asking the LLM to read and segment the full note...',
+ noCardsReturned: 'LLM returned no cards',
+ generationDone: 'Generated {count} sections{suffix}',
+ unanchoredSuffix: ' (⚠ {count} anchors unmatched)',
+ cancelled: 'Generation cancelled',
+ cancelledError: 'Generation cancelled',
+ generationFailed: 'Generation failed{kind}: {error}',
+ noEditor: 'Could not find the source note editor',
+ settingUiLanguageName: 'UI language',
+ settingUiLanguageDesc: 'Controls plugin UI, commands, and notices. Auto follows Obsidian/system language.',
+ settingBackendName: 'Backend',
+ settingBackendDesc: 'Backend for generating bullets: CLI reuses local login; API supports OpenAI, Anthropic, Gemini, and compatible proxies.',
+ settingCliPathName: 'CLI path (optional)',
+ settingCliPathDesc: 'Leave blank to auto-detect common paths. Obsidian launched from the GUI may not inherit shell PATH.',
+ settingCliPathPlaceholder: 'Example: /Users/you/bin/codex',
+ settingCliTimeoutName: 'CLI timeout (ms)',
+ apiProviderHeader: 'API Provider',
+ settingProviderPresetName: 'Provider preset',
+ settingProviderPresetDesc: 'OpenClaw-style provider/model setup: presets define protocol, base URL, and auth defaults.',
+ settingApiFormatName: 'API format',
+ settingApiFormatDesc: 'Wire protocol for the provider. OpenAI-compatible proxies usually use Chat Completions.',
+ settingBaseUrlName: 'Base URL',
+ settingBaseUrlDesc: 'Provider root URL, without /chat/completions. Blank uses the preset default.',
+ settingApiKeyName: 'API Key',
+ settingApiKeyDesc: 'Can be blank for local Ollama/LM Studio; most hosted providers require a key.',
+ settingApiKeyEnvName: 'API key env var',
+ settingApiKeyEnvDesc: 'Optional. Obsidian GUI may not inherit shell env; direct API Key is usually more reliable.',
+ settingAuthTypeName: 'Auth type',
+ settingAuthTypeDesc: 'Auto uses provider preset. Custom proxies can use Bearer, x-api-key, or none.',
+ settingHeadersName: 'Extra headers',
+ settingHeadersDesc: 'Optional. JSON object or one `Header: value` per line, useful for Cloudflare AI Gateway.',
+ settingMaxTokensName: 'Max output tokens',
+ settingModelName: 'Model',
+ settingModelDescApi: 'Model ID for API calls. Supports OpenClaw-style provider/model; matching provider prefixes are stripped.',
+ settingModelDescCli: 'Passed as --model for Claude Code. Usually ignored by Codex, which uses its default config.',
+ settingMaxInputName: 'Max input characters',
+ settingMaxInputDesc: 'Longer notes are truncated before sending to the model. Raise this for long-context models.',
+ promptHeader: 'Prompt',
+ settingPromptLanguageName: 'Output language',
+ settingPromptLanguageDesc: 'Controls title/gist/bullets language. Anchor is always copied verbatim from source.',
+ settingCardRangeName: 'Card count range',
+ settingCardRangeDesc: 'The model adapts segmentation within this range.',
+ settingCustomPromptName: 'Custom system prompt',
+ settingCustomPromptDesc: 'Optional. Variables: {minCards}, {maxCards}, {languageInstruction}, {schema}, {example}',
+ settingCustomPromptPlaceholder: 'Leave blank to use built-in prompt',
+ settingTestBackendName: 'Test current backend',
+ settingTestBackendDescApi: 'Sends a minimal LLM request to validate API settings.',
+ settingTestBackendDescCli: 'Runs ` --version` to verify the binary can be spawned.',
+ backendTestFailed: '✗ Backend test failed: {error}',
+ settingExportFolderName: 'Export folder',
+ settingExportFolderDesc: 'Parallel-note output location, relative to the Vault root.',
+ cacheHeader: 'Cache',
+ settingMaxCacheName: 'Max cached notes',
+ settingMaxCacheDesc: 'Prunes least-recently accessed note caches above this limit. Cache is stored in plugin cache.json.',
+ cachePruned: 'Pruned {count} old cache entries',
+ cachedNotesName: 'Cached notes: {count}',
+ cachedNotesDesc: 'Cache is invalidated by source SHA1 and generation settings fingerprint.',
+ clearAllCacheButton: 'Clear all caches',
+ },
+};
+
+export function resolveUiLanguage(settings) {
+ const configured = settings && settings.uiLanguage;
+ if (configured === 'zh' || configured === 'en') return configured;
+ const nav = typeof navigator !== 'undefined' ? navigator : null;
+ const language = String(nav?.language || '').toLowerCase();
+ return language.startsWith('zh') ? 'zh' : 'en';
+}
+
+export function translate(settings, key, vars?) {
+ const lang = resolveUiLanguage(settings);
+ const table = STRINGS[lang] || STRINGS.en;
+ const fallback = STRINGS.en[key] || STRINGS.zh[key] || key;
+ const template = table[key] || fallback;
+ return String(template).replace(/\{([a-zA-Z0-9_]+)\}/g, (match, name) => {
+ return vars && Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match;
+ });
+}
diff --git a/src/prompt.ts b/src/prompt.ts
new file mode 100644
index 0000000..283e37d
--- /dev/null
+++ b/src/prompt.ts
@@ -0,0 +1,89 @@
+'use strict';
+
+import {
+ DEFAULT_SETTINGS,
+ MAX_DOC_CHARS,
+ PROMPT_LANGUAGES,
+} from './settings';
+
+export function promptLanguageInstruction(language) {
+ if (language === 'en') return 'Write title, gist, and bullets in English.';
+ if (language === 'auto') return 'Write title, gist, and bullets in the main language of the source document.';
+ return '用中文输出 title、gist 和 bullets。';
+}
+
+export function promptSchemaExample(language) {
+ if (language === 'en') {
+ return `{"cards":[
+ {"title":"U-shaped gains","anchor":"Who benefits from AI? Overall, it shifts the score from one to seven","gist":"AI productivity gains form a U shape, with both ends benefiting most","bullets":["Top-paid software managers benefit by accelerating existing work","Low-paid workers use AI to create new side income","Middle-layer specialists gain less because prompt precision is hard to trust","Average reported benefit is 5.1/7, with 42% describing gains as unclear"]}
+]}`;
+ }
+ return `{"cards":[
+ {"title":"U 型收益曲线","anchor":"那谁又会被 AI 所受益?整体来看,它把整个分数变成了一分到七分","gist":"AI 生产力收益呈 U 型,两端受益最大、中间层塌陷","bullets":["最高薪岗位(软件管理)通过加速既有工作受益最大","最低薪岗位(外卖员、园艺工)用 AI 开副业创造新收入","中间层科学家、律师收益最少,部分因对 prompt 精度信任不足","全体均分 5.1/7,42% 报告收益模糊"]}
+]}`;
+}
+
+export function renderPromptTemplate(template, vars) {
+ return String(template || '').replace(/\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
+ return Object.prototype.hasOwnProperty.call(vars, key) ? String(vars[key]) : match;
+ });
+}
+
+export function buildPrompts(content, settings) {
+ const maxDocChars = Number(settings.maxDocChars) || MAX_DOC_CHARS;
+ const promptLanguage = PROMPT_LANGUAGES[settings.promptLanguage] ? settings.promptLanguage : DEFAULT_SETTINGS.promptLanguage;
+ const minCards = Math.max(1, Number(settings.minCards) || DEFAULT_SETTINGS.minCards);
+ const maxCards = Math.max(minCards, Number(settings.maxCards) || DEFAULT_SETTINGS.maxCards);
+ const languageInstruction = promptLanguageInstruction(promptLanguage);
+ const doc = content.length > maxDocChars
+ ? content.slice(0, maxDocChars) + (promptLanguage === 'en' ? '\n\n[Document truncated]' : '\n\n[文档过长,已截断]')
+ : content;
+
+ const schema = '{"cards":[{"title":"...","anchor":"...","gist":"...","bullets":["...","..."]}]}';
+ const example = promptSchemaExample(promptLanguage);
+ const templateVars = { minCards, maxCards, languageInstruction, schema, example };
+ const customSystem = renderPromptTemplate(settings.customSystemPrompt, templateVars).trim();
+
+ const defaultSystem = `你是一个长文阅读摘要助手。阅读全文后,把文章切成 ${minCards}-${maxCards} 个"自然主题单元"——不必对应 markdown heading,以"一个完整论点或话题"为单位自行判断粒度:短章节合并、长章节拆分。
+
+**每张卡片的结构:一句话领读 + 若干条 bullet。bullet 承载细节,gist 是一句话导读。**
+
+语言:
+- ${languageInstruction}
+
+对每个单元输出:
+
+- title: 3-10 字的短标题,要能独立说明这段讲什么,避免"背景""介绍"这类空泛词
+- anchor: 该单元开头的**逐字引用**,从原文 1:1 复制 40-80 字,保留原始标点/空格/换行;仅供插件内部定位,用户不可见
+- gist: **一句话领读**(20-40 字),点出该单元的核心立场或结论,作为 bullets 的导读
+- bullets: **3-6 条**支撑 bullet,每条 20-50 字。承载数据、对比、机制、例子、反直觉观察。gist 是立场,bullets 是具体内容,两者不允许重复。
+
+规则:
+- anchor 必须能在原文里 exact substring match 找到。绝对不要改动、总结、翻译,必须原样复制
+- anchor 选用该单元最靠前且足够独特的一段(避免通用套话如"综上所述")
+- 每张卡都必须同时有 gist 和 bullets——不要只有 gist,也不要只有 bullets
+- bullet 每条是一个完整独立的断言,不要用"首先""其次"这种顺序词
+- 严格只输出 JSON,无 markdown fence、无解释、无 tool call
+
+输出格式:
+${schema}
+
+示例:
+${example}`;
+
+ const system = customSystem
+ ? `${customSystem}
+
+不可覆盖的输出契约:
+- 必须输出 ${minCards}-${maxCards} 张 cards。
+- ${languageInstruction}
+- anchor 必须从原文逐字复制,必须能在原文 exact substring match 找到。
+- 严格只输出 JSON,无 markdown fence、无解释、无 tool call。
+- JSON shape: ${schema}`
+ : defaultSystem;
+
+ const user = promptLanguage === 'en'
+ ? `Source document:\n\n${doc}`
+ : `以下是需要处理的文档全文:\n\n${doc}`;
+ return { system, user };
+}
diff --git a/src/settings.ts b/src/settings.ts
index ed8ef92..9d9984a 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -11,8 +11,14 @@ export const PROMPT_LANGUAGES = {
en: 'English',
auto: 'Auto-detect',
};
+export const UI_LANGUAGES = {
+ auto: 'Auto',
+ zh: '中文',
+ en: 'English',
+};
export const DEFAULT_SETTINGS = {
+ uiLanguage: 'auto',
backend: 'claude-code',
cliPath: '',
apiProvider: 'anthropic',
@@ -312,6 +318,7 @@ export function applyApiProviderPreset(settings, providerId) {
}
export function normalizeSettings(settings) {
+ if (!UI_LANGUAGES[settings.uiLanguage]) settings.uiLanguage = DEFAULT_SETTINGS.uiLanguage;
if (!settings.apiProvider || !API_PROVIDER_PRESETS[settings.apiProvider]) {
settings.apiProvider = 'anthropic';
}
diff --git a/tests/main.test.js b/tests/main.test.js
index 058601e..b2c85c8 100644
--- a/tests/main.test.js
+++ b/tests/main.test.js
@@ -47,6 +47,7 @@ assert.strictEqual(typeof t.findLineForAnchor, 'function');
assert.strictEqual(typeof t.generationFingerprint, 'function');
assert.strictEqual(typeof t.GenerationJobManager, 'function');
assert.strictEqual(typeof t.pruneCacheEntries, 'function');
+assert.strictEqual(typeof t.translate, 'function');
const baseSettings = {
backend: 'api',
@@ -84,6 +85,11 @@ assert.notStrictEqual(
t.generationFingerprint({ ...baseSettings, customSystemPrompt: 'custom prompt' }),
'cache fingerprint should change when custom prompt changes'
);
+assert.strictEqual(
+ t.generationFingerprint(baseSettings),
+ t.generationFingerprint({ ...baseSettings, uiLanguage: 'en' }),
+ 'cache fingerprint should not change when UI language changes'
+);
const contentHash = crypto.createHash('sha1').update('hello', 'utf8').digest('hex');
assert.strictEqual(
@@ -152,6 +158,10 @@ assert.ok(customPrompt.system.includes('Make 1-2 cards.'));
assert.ok(customPrompt.system.includes('不可覆盖的输出契约'));
assert.ok(customPrompt.system.includes('JSON shape'));
+assert.strictEqual(t.translate({ uiLanguage: 'en' }, 'cmdOpenView'), 'Open Parallel Reader pane');
+assert.strictEqual(t.translate({ uiLanguage: 'zh' }, 'cmdOpenView'), '打开对照笔记面板');
+assert.strictEqual(t.translate({ uiLanguage: 'en' }, 'cacheClearedAll', { count: 3 }), 'Cleared 3 cache entries');
+
const noisyJson = '说明文字 {"cards":[{"title":"A","anchor":"保留 { 花括号 } 字符","gist":"G","bullets":["B"]}]} trailing';
const extracted = t.extractJson(noisyJson);
assert.deepStrictEqual(JSON.parse(extracted).cards[0].bullets, ['B']);