diff --git a/README.md b/README.md index 025b300..df29bb0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ **English**: [README_EN.md](README_EN.md) -[DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) 插件:**论文伴读工作台** —— 文献库 + PDF 阅读器 + AI 伴读,回答自带页码、点页码跳回原文。当前版本 **v1.4.1**。 +[DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) 插件:**论文伴读工作台** —— 文献库 + PDF 阅读器 + AI 伴读,回答自带页码、点页码跳回原文。当前版本 **v1.5.0**。 > 💬 **QQ 交流群:1127244508** —— 安装/使用问题、功能建议、bug 反馈都欢迎来聊。 @@ -24,6 +24,8 @@ - 🤖 **教练式带读**:paper 会话自动用专用 preset——定分步阅读计划、逐节指导、出题批改,学习进度跨会话累积 - 🀄 **中文版**:原文 ↔ 纯中文原地切换(babeldoc 零预装,自动安装),译文 PDF 一键导出(纯中文 / 中英对照) - 🗂️ **文献管理**:专题/论文改名、删除(删除前列文件清单、进回收站**可恢复**、非空专题批量清空一次确认) +- 🈯 **划词翻译(公式保留)**(v1.5.0):阅读器里选中正文 → 提问气泡上多一个 **「译」** 按钮 → 弹出**只显示译文**的气泡(**没有**「插入会话」「复制」按钮,也从不自动写剪贴板)。**选区含公式时**,用 MinerU 识别出的 LaTeX 替换 PDF 文本层里的乱码公式,并要求模型**原样保留**公式(`$…$` 里的内容不翻译、不改写)。**前提与代价**:① 需先配置翻译端点(与生成中文版用的是同一份配置;未配置时点「译」会直接弹设置表单,保存后自动重试);② **公式保留要求该论文用 MinerU 转录过**——没有产物时**降级为普通翻译**,公式在文本层里本就不读,气泡会**如实提示「译文可能不准」**(这是设计,不是缺陷) +- 🖥️ **中英分屏对照(两种模式,可切换)**(v1.5.0):顶栏新增 **「对照」** 与 **「分屏」** 两个按钮,随时切换、再点一次回原文,选择按文献记忆。**模式 A「对照」** 直接看 babeldoc 的 `-dual.pdf`(原文与译文并排画在同一张加宽页面上,自动适宽);**模式 B「分屏」** 左原文、右中文版,两列**同步滚动**(同页号 + 页内比例)。**前提与代价**:模式 A 需要 `-dual.pdf`、模式 B 需要 `-zh.pdf`,两者都要先用 babeldoc 生成(顶栏「中」一键触发,**首次约几分钟**,且需要翻译端点);**没有对应产物时按钮置灰并说明**;分屏需要面板宽度 ≥640px,太窄会提示而不切换 - ➕ **两个可选增强**(默认关,见「进阶」):MinerU 解析——扫描件可读、公式可点引用可检索;语义检索端点——大白话命中术语段落 安装后**不改动官方界面**:侧栏底部多一行「📚 论文伴读」开关,点击才进入伴读模式,再点即还原。 @@ -68,6 +70,7 @@ dsh plugin --profile web add @ggboy123/dsh-paper-reader@latest ```bash dsh plugin --profile web add @ggboy123/dsh-paper-reader@latest # 永远装最新 +# dsh plugin --profile web add @ggboy123/dsh-paper-reader@1.5.0 # 或钉住具体版本 grep '"version"' ~/.dsh/profiles/web/node_modules/@ggboy123/dsh-paper-reader/package.json # 查当前版本 # 重启 dsh web 生效;浏览器 Cmd+Shift+R 强刷,避免旧阅读器页面缓存 ``` @@ -88,7 +91,7 @@ grep '"version"' ~/.dsh/profiles/web/node_modules/@ggboy123/dsh-paper-reader/pac 默认用本地 pdf.js 提取文本——**普通文本型 PDF 完全够用,什么都不用配**。两类场景需要 MinerU: - **扫描版 PDF**(老论文、书章、扫描讲义):没有文本层,pdf.js 读不了,MinerU(OCR)能读 -- **公式**:PDF 里公式是图形乱码,选不中也搜不到。**公式引用**——MinerU 把公式识别成 LaTeX 后,点 PDF 上的公式即引用进对话提问;`QK^T` 这类公式检索也能命中;顶栏「∑ 公式」面板按页浏览全文公式 +- **公式引用**:PDF 里公式是图形乱码,选不中也搜不到。MinerU 把公式识别成 LaTeX 后,**点 PDF 上的公式会把该公式的 LaTeX 带进提问气泡**(发送即随问题引用进对话);`QK^T` 这类公式检索也能命中;顶栏「∑ 公式」面板按页浏览全文公式 **需要自己准备(二选一)**,在 **设置 → 论文伴读 → 「MinerU 解析后端」** 里配置: @@ -120,6 +123,50 @@ grep '"version"' ~/.dsh/profiles/web/node_modules/@ggboy123/dsh-paper-reader/pac - 改名后:转录缓存/译文/学习档案全部跟随搬家;历史伴读会话**本体保留**但不再挂在新名字下(会话 id 编码旧名);纯大小写改名在 macOS/Windows 可用 - 手工放进 `data/` 的怪名字文件(如 `.hidden.pdf`)也能正常删除/改名,不会成为孤儿 +## 划词翻译与中英分屏对照(v1.5.0) + +### 划词翻译(公式保留) + +**怎么用**:在阅读器里**选中一段正文** → 弹出「选中即问」气泡(行为与以前完全一致)→ 点气泡里的 **「译」** → 弹出**译文气泡**,里面**只有译文**(外加一行诚实提示)。想重翻就重新选一次;点气泡外或按 Esc 关掉。 + +译文气泡里**没有**「插入会话」「复制」按钮(按你的要求),也**从不**自动写剪贴板。提问链路一点没动:重新选一次字就回到原来的提问气泡。 + +**公式是怎么处理的**(这是本功能的核心): + +- PDF 文本层里的公式**不可读**:`$F_{\theta}$` 常常只剩一个 `F`(下标整字丢失)、`\sum` 变成 `X`、分式被拉平成一行、括号对甚至变成控制字符。把这串东西直接发给模型,译文里的公式必然错。 +- 所以:**选区里的公式先用 MinerU 的 LaTeX 把那一段乱码替换掉**,再明确要求模型**原样保留** `$…$` 里的内容(不翻译、不改写、不省略、不增删符号)。译文回来后还会**逐个公式核对**是否原样保留,缺了就在气泡里如实告诉你。 +- 两类公式都覆盖:**独立成行的公式**(用 MinerU `equation` 块的 bbox 精确定位)与**正文里穿插的行内公式**(如 `π(a|s)`、`Q(s,a)`、`x_t`)。行内公式在 MinerU 产物里**没有自己的 bbox**(它嵌在正文块里),插件靠「选区文本 ↔ MinerU 块文本」的模糊对齐把它还原出来。 + +**前提与代价(务必先读)**: + +| # | 前提 | 没有它会怎样 | +| --- | --- | --- | +| 1 | **需要先配置翻译端点**(设置 → 论文伴读,或阅读器右上角 ⚙;与生成中文版共用同一份配置) | 点「译」会**自动弹出端点设置表单**;填好保存后**自动重试这次翻译**(不会白点一次)。端点三项不齐时服务端直接拒绝,前端据此弹表单 | +| 2 | **公式保留需要该论文用 MinerU 转录过**(存在 `<文献>.mineru.json`) | **降级为普通翻译**:正文照常翻,但公式在文本层里本就不读,气泡会如实提示「该论文未用 MinerU 转录:原文里的公式在文本层不可读,译文可能不准」。**这是设计,不是缺陷** | +| 3 | 有 MinerU 产物、但某一段公式没对上 | 只还原能对上的部分,气泡提示「部分公式未能还原,译文可能不准」 | +| 4 | 选区太短(不足 4 个有效字符,例如只选中孤零零的 `F`) | 不做还原(不足以判别),气泡提示「选区太短,公式未还原」 | + +**已知限制(如实声明,不粉饰)**: + +- **未对真实 LLM 端点打过任何请求**:开发期验证全程使用假上游(避免花钱、避免用你的 key)。「模型是否真的原样保留公式」目前靠**提示词字节契约**+**回包逐公式核对**两道可判定断言保证,没有真实服务商 key 的端到端实测。 +- MinerU 偶尔会把**一个公式拆成两个 `$…$`**(首个片段以 `=` 结尾)。这种形态下可能只还原出半截公式(如 `action $\hat{a}_t\sim\pi_\theta(\cdot\ |$`),`$` 配对计数拦不住;本机语料里未出现,留待后续。 +- 选区里含**字面** `$`(正文里真印了一个美元符号)时,极端形态下可能产生一句「有 N 个公式未原样保留」的**误报提示**(已记录,不影响译文本身)。 +- 中文版 / 对照视图下**不做**公式替换(bbox 只对原文页成立),所以「译」按钮只在原文视图出现。 + +### 中英分屏对照(两种模式) + +顶栏新增两个按钮,随时切换、**再点一次回原文**;每篇文献上次用的视图会被记住(跟着文献走,改名也不丢): + +| 按钮 | 模式 | 看的是哪个文件 | 前提 | +| --- | --- | --- | --- | +| **对照** | A:单页内对照 | babeldoc 的 `-dual.pdf`(原文与译文并排画在同一张加宽页上,页宽约 2 倍) | 需要 `-dual.pdf` | +| **分屏** | B:左右并排 | 左:原文 `<文献>.pdf`;右:`<文献>-zh.pdf` | 需要 `-zh.pdf` | + +- **两个产物都要先用 babeldoc 生成**:顶栏点「中」一键触发(**首次要装引擎 + 翻译,约几分钟**,需要翻译端点)。没有对应产物时按钮**置灰并写明原因**,不会点了没反应。 +- **同步滚动**:滚任意一列,另一列自动跟到「同一页号 + 同一页内比例」;谁在滚谁主导,不会两列互相拉锯。翻页模式(`▣`)下两列同步同页。**中文版与原文页数不一致时按比例映射**,并在状态栏给出一次性提示。 +- **分屏宽度**:需要面板宽度 ≥640px;太窄时点「分屏」只提示「请把阅读器拉宽」,**不会**切进去;分屏期间把面板缩到 640px 以下会自动退回原文。 +- **与既有能力的关系**:分屏时左列就是普通原文视图——**公式热区照常可点**(右列不参与命中,也不会被画上热区);公式面板照常可用;「中」按钮在分屏期间置灰(避免与分屏语义打架)。切到中文 / 对照视图时热区仍按既有规则关闭(bbox 只对原文页成立)。 + ## 参与开发 ```bash @@ -154,6 +201,7 @@ src/ study.ts 纯函数:学习档案(计划 + 检验成绩)读写 translate.ts 纯函数:babeldoc 调用(中文/中英对照 PDF 生成) babeldoc-install.ts 纯函数:无 Python 环境时自动安装 babeldoc(uv → 托管 Python → venv) + translate-selection.ts 纯函数:划词翻译(公式保留 —— 行内公式与 MinerU 块文本对齐还原、请求体与提示词契约、逐公式保全校验、内存 LRU 缓存) rerank.ts 纯函数:search_paper 语义重排(TypeSafe/Jev,无凭据时退回关键词排序) preset.ts 纯函数:自带 agent preset 安装到 $DSH_HOME/.agent-presets/ presets/paper-reader/ 「论文伴读」agent preset(persona 完整提示词 + compaction) diff --git a/README_EN.md b/README_EN.md index 43cefe4..3dded2f 100644 --- a/README_EN.md +++ b/README_EN.md @@ -7,7 +7,7 @@ **中文**: [README.md](README.md) -A [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) plugin: a **paper reading workbench** — paper library + PDF reader + AI companion. Every answer cites page numbers, and clicking a page jumps back to the exact highlighted passage. Current version: **v1.4.1**. +A [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) plugin: a **paper reading workbench** — paper library + PDF reader + AI companion. Every answer cites page numbers, and clicking a page jumps back to the exact highlighted passage. Current version: **v1.5.0**. > 💬 **QQ group: 1127244508** — questions, feature requests, and bug reports are all welcome. @@ -24,6 +24,8 @@ A [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) plug - 🤖 **Tutor mode**: paper sessions automatically use a dedicated preset — step-by-step reading plans, section-by-section coaching, graded quizzes; progress accumulates across sessions - 🀄 **Chinese version**: original ↔ full-Chinese in-place toggle (babeldoc auto-installed, zero preinstall); one-click export (Chinese / bilingual PDF) - 🗂️ **Library management**: rename/delete topics and papers (deletion shows a file manifest first, goes to a **recoverable** trash; non-empty topics support batch-clear with one confirmation) +- 🈯 **Selection translation (formulas preserved)** (v1.5.0): select text in the reader → the ask bubble gains a **"译" (Translate)** button → a **translation-only** bubble opens (there is **no** "insert into chat" and **no** "copy" button, and the plugin never writes to your clipboard on its own). **When the selection overlaps a formula**, the plugin replaces the garbage formula glyphs in the PDF text layer with the LaTeX MinerU recognized and asks the model to **keep formulas verbatim** (nothing inside `$…$` is translated or rewritten). **Costs and prerequisites**: ① you must configure the translation endpoint first (the same config the Chinese-PDF generation uses; with nothing configured, clicking "译" opens the endpoint form and retries automatically after saving); ② **formula preservation requires that paper to have been parsed by MinerU** — without an artifact it **falls back to plain translation**, and since formulas are unreadable in the text layer the bubble **says so honestly** ("translation may be inaccurate"). That is by design, not a defect. +- 🖥️ **Bilingual split view (two modes, switchable)** (v1.5.0): two new toolbar buttons, **"对照" (Side-by-side page)** and **"分屏" (Split)** — switch any time, click again to go back to the original; the choice is remembered per paper. **Mode A 「对照」** shows babeldoc's `-dual.pdf` (original and translation laid out on the same doubled-width page, auto fit-width); **Mode B 「分屏」** puts the original on the left and `-zh.pdf` on the right with **synchronised scrolling** (same page + same in-page ratio). **Costs and prerequisites**: mode A needs `-dual.pdf` and mode B needs `-zh.pdf`; both are produced by babeldoc (click "中" in the toolbar — **the first run takes a few minutes** and needs the translation endpoint); **without the artifact the button is greyed out with an explanation**; the split view needs a panel at least 640px wide — narrower than that it explains instead of switching. - ➕ **Two optional add-ons** (off by default, see "Advanced"): MinerU parsing — scanned PDFs become readable, formulas become click-citable and searchable; an embeddings endpoint — plain-language semantic search Installing **does not modify the official UI**: a single "📚 Paper Reading" toggle appears at the bottom of the sidebar; click to enter reading mode, click again to restore. @@ -68,6 +70,7 @@ dsh plugin --profile web add @ggboy123/dsh-paper-reader@latest ```bash dsh plugin --profile web add @ggboy123/dsh-paper-reader@latest # always newest +# dsh plugin --profile web add @ggboy123/dsh-paper-reader@1.5.0 # or pin a version grep '"version"' ~/.dsh/profiles/web/node_modules/@ggboy123/dsh-paper-reader/package.json # check current version # Restart dsh web; hard-refresh the browser (Cmd+Shift+R) to avoid cached reader pages ``` @@ -77,7 +80,7 @@ grep '"version"' ~/.dsh/profiles/web/node_modules/@ggboy123/dsh-paper-reader/pac 1. **Enter**: click "📚 Paper Reading" at the bottom of the sidebar — it becomes the library tree (click again to restore the official workspace list) 2. **Add papers**: + create a topic → ⬆ upload PDFs (with an empty library, "Upload PDF" auto-creates a default topic) 3. **Read + ask**: click a paper — reader in the middle, chat on the right. Ask in the input box (no need to name the paper; answers cite pages), or **select text** in the reader for a pop-up question box; click "page N" in an answer to jump back -4. **Chinese version**: the "中" button — one click generates a translation if none exists (first use auto-installs the engine; fill in an OpenAI-compatible endpoint once — DeepSeek/GLM/Kimi keys all work); ⤓ downloads the version you're viewing; ▣ toggles single-page mode +4. **Chinese version**: the "中" button — one click generates a translation if none exists (first use auto-installs the engine; fill in an OpenAI-compatible endpoint once — DeepSeek/GLM/Kimi keys all work); ⤓ downloads the version you're viewing; ▣ toggles single-page mode ↔ scroll 5. **Manage**: hover a row for ✎ rename / 🗑 delete. Deletion lists the files first, then moves them to a **recoverable trash** (`data/.trash/`); deleting a non-empty topic = batch-clear with one confirmation 6. **Chats**: 🕐 in the chat header shows that paper's history; + starts a new conversation @@ -88,7 +91,7 @@ grep '"version"' ~/.dsh/profiles/web/node_modules/@ggboy123/dsh-paper-reader/pac By default text is extracted locally with pdf.js — **plain text-native PDFs need no configuration at all**. Two scenarios need MinerU: - **Scanned PDFs** (old papers, book chapters, scanned handouts): no text layer, pdf.js can't read them; MinerU (OCR) can -- **Formulas**: formulas in PDFs are graphical garbage — unselectable and unsearchable. Once MinerU recovers them as LaTeX, **click a formula and its LaTeX is cited straight into the chat**; formula queries like `QK^T` hit search; the "∑ 公式" panel lists every formula by page +- **Click to cite a formula**: formulas in PDFs are graphical garbage — unselectable and unsearchable. Once MinerU recovers them as LaTeX, **clicking a formula in the PDF puts its LaTeX into the ask bubble** (send it and the formula is cited with your question); formula queries like `QK^T` hit search; the "∑ 公式" (Formulas) panel lists every formula by page **You need to set this up yourself (pick one)**, configured under **Settings → Paper Reader → "MinerU parser"**: @@ -120,6 +123,50 @@ Configure one of three ways (same priority as the translate endpoint): the setti - After a rename: transcripts/translations/study profiles move along; past chat sessions are **kept** but no longer listed under the new name (session ids encode the old name); case-only renames work on macOS/Windows - Oddly named files placed into `data/` by hand (e.g. `.hidden.pdf`) can still be deleted/renamed — nothing becomes a visible-but-undeletable orphan +## Selection translation and bilingual split view (v1.5.0) + +### Selection translation (formulas preserved) + +**How to use**: select a passage in the reader → the familiar "select-to-ask" bubble appears (behaviour unchanged) → click **"译" (Translate)** → a **translation bubble** opens containing **only the translation** (plus one honest note line). Re-select to retranslate; click outside or press Esc to dismiss. + +That bubble has **no** "insert into chat" and **no** "copy" button (as requested), and the plugin **never** writes to your clipboard automatically. The asking path is untouched: just select again and you are back in the original ask bubble. + +**What happens to formulas** (the core of this feature): + +- Formulas in the PDF text layer are **unreadable**: `$F_{\theta}$` often degrades to a bare `F` (the subscript is dropped entirely), `\sum` becomes `X`, fractions get flattened into one line, and delimiter pairs can even turn into control characters. Feeding that to a model guarantees wrong formulas in the translation. +- So: **each formula inside the selection is replaced by the LaTeX MinerU recognized**, and the model is explicitly told to **keep `$…$` verbatim** (no translation, no rewriting, no omissions, no added symbols). When the reply comes back, every formula token is **checked** to still be present, and the bubble tells you honestly if any went missing. +- Both kinds are covered: **display formulas** (located exactly with the MinerU `equation` block bbox) and **inline formulas embedded in prose** (e.g. `π(a|s)`, `Q(s,a)`, `x_t`). Inline formulas have **no bbox of their own** in MinerU's output (they live inside a text block), so the plugin recovers them by fuzzy-aligning the selection text against the MinerU block text. + +**Prerequisites and costs (please read first)**: + +| # | Prerequisite | What happens without it | +| --- | --- | --- | +| 1 | **A configured translation endpoint** (Settings → Paper Reader, or ⚙ in the reader toolbar; the same config used for Chinese-PDF generation) | Clicking "译" **opens the endpoint form automatically**, and **retries this translation** after you save (no wasted click). If the endpoint is incomplete the server refuses and the UI opens that form | +| 2 | **Formula preservation requires the paper to have been parsed by MinerU** (a `.mineru.json` artifact) | **Falls back to plain translation**: prose is translated normally, but formulas are unreadable in the text layer, so the bubble honestly says "this paper was not parsed with MinerU: the translation may be inaccurate". **By design, not a defect** | +| 3 | An artifact exists but one passage could not be aligned | Only the alignable parts are restored; the bubble says "some formulas could not be restored" | +| 4 | A very short selection (fewer than 4 effective characters, e.g. the lone `F`) | No restoration (not enough signal); the bubble says "selection too short, formula not restored" | + +**Known limitations (stated plainly, not glossed over)**: + +- **No request has ever been sent to a real LLM endpoint**: all development verification used a fake upstream (to avoid spending money and to avoid using your key). "Does the model really keep formulas verbatim" is backed by two decidable assertions — a **byte-level prompt contract** and a **per-formula check on the reply** — but there is no end-to-end run against a real provider key. +- MinerU occasionally splits **one formula into two `$…$` fragments** (the first ending in `=`). In that shape only half a formula may be restored (e.g. `action $\hat{a}_t\sim\pi_\theta(\cdot\ |$`); the `$`-pairing check cannot catch it. Not observed in the local corpus; left for follow-up. +- A **literal `$`** inside the selection (a real dollar sign printed in the text) can, in an extreme shape, produce a false "N formulas were not preserved" note. Recorded; it does not affect the translation itself. +- **No formula replacement in the Chinese / dual views** (the bbox only holds for original pages), which is why the "译" button only appears in the original view. + +### Bilingual split view (two modes) + +Two new toolbar buttons; switch any time and **click again to return to the original**. Each paper remembers its last view (and the memory follows the paper across renames): + +| Button | Mode | What you are looking at | Prerequisite | +| --- | --- | --- | --- | +| **对照** | A: side-by-side page | babeldoc's `-dual.pdf` (original + translation on one doubled-width page) | needs `-dual.pdf` | +| **分屏** | B: two panes | left: original `.pdf`; right: `-zh.pdf` | needs `-zh.pdf` | + +- **Both artifacts come from babeldoc**: click "中" in the toolbar (**the first run installs the engine and translates — a few minutes**, and needs the translation endpoint). If the artifact is missing the button is **greyed out with the reason stated** — never a dead click. +- **Synchronised scrolling**: scrolling either pane moves the other to the **same page + same in-page ratio**; whichever pane you are scrolling leads, so the two never fight each other. In paged mode (`▣`) both panes show the same page. **If the Chinese PDF has a different page count, the panes are mapped proportionally** and a one-off notice appears in the status bar. +- **Width**: the split view needs a panel at least **640px** wide; narrower, clicking "分屏" only explains and does **not** switch. Shrinking below 640px while split falls back to the original automatically. +- **Interaction with existing features**: while split, the left pane is the ordinary original view — **formula hotspots stay clickable** (the right pane neither participates in hit-testing nor gets a hotspot layer), the formula panel keeps working, and the "中" toggle is disabled during split (to avoid conflicting semantics). Switching to the Chinese / dual view still disables hotspots, exactly as before (the bbox only holds for original pages). + ## Contributing ```bash @@ -154,6 +201,7 @@ src/ study.ts pure functions: study profile (plan + quiz scores) I/O translate.ts pure functions: babeldoc invocation (Chinese / bilingual PDF generation) babeldoc-install.ts pure functions: auto-install babeldoc without a Python env (uv → managed Python → venv) + translate-selection.ts pure functions: selection translation with formula preservation (aligns inline formulas against MinerU block text, prompt/request-body contract, per-formula preservation check, in-memory LRU cache) rerank.ts pure functions: search_paper semantic rerank (TypeSafe/Jev; falls back to keyword ranking) preset.ts pure functions: installs the bundled agent preset into $DSH_HOME/.agent-presets/ presets/paper-reader/ the "Paper Tutor" agent preset (full persona prompt + compaction) diff --git a/lib/client.js b/lib/client.js index 95c9d7b..b6f01a3 100644 --- a/lib/client.js +++ b/lib/client.js @@ -39,6 +39,22 @@ function clearFailureText(err, t) { } } +/** + * 分屏「同步滚动」偏好 —— **纯函数**(文件顶层:Node 里执行本文件即可取用,测试 harness 追加导出)。 + * 与 `reader/index.html` 里的同键读写必须保持同一套编码,所以两边都只认 `'1'` / `'0'`。 + * + * 键为什么长这样(`dpr.syncScroll`:无 topic/name、也不带 `dpr.variant.` 前缀): + * ① 用户明确要求**全局**(所有论文共用一个值,不按篇记); + * ② `migrateLsKeys`(改名搬家)只搬 `dpr.variant.` / `dpr.sessionN.` 两个前缀 ⇒ 这个键既不会被搬 + * 也不会被删,论文/专题改名**不可能**让偏好丢失(全局键本来就没有「跟着某篇走」的身份)。 + * 反例:若塞进 `dpr.variant./`(按篇),改名搬家会搬它,而「全局」语义就破了。 + * ③ 缺省 = 开:与改造前「分屏两列被 rAF 强行同步」的既有行为一致(老用户升级后观感不变)。 + */ +const DPR_SYNC_SCROLL_KEY = 'dpr.syncScroll' +/** 只认显式的 '0' 为关;缺失 / 脏值 / 隐私模式都算开(与 reader 的 readSyncPref 逐字同义)。 */ +function dprSyncScrollFromRaw(raw) { return raw !== '0' } +function dprSyncScrollRaw(on) { return on ? '1' : '0' } + window.__ModuleLoader__.load({ // 必须等于 npm 包名:浏览器端 loader 按包名期待注册(改名时漏改这里 → // "loaded without registering" 整页插件加载失败,实测) @@ -86,6 +102,13 @@ window.__ModuleLoader__.load({ setMineruWhyLocal: '本地模式需要你自己跑一个 MinerU 服务(127.0.0.1:8000),适合有显卡、论文不外传的场景。', setMineruWhyCloud: '云端模式只要一个 mineru.net token,无需本地显卡;论文会上传到 mineru.net 解析。', setMineruOffHint: '当前为「关闭」:转录走本地 pdf.js——有文本层的普通 PDF 完全够用;扫描版(没有文本层)与公式引用才需要 MinerU(选本地或云端)。', + // 分屏「同步滚动」(模式 B 的全局偏好):与阅读器分屏控件旁的那个开关是**同一个值** + // (键 dpr.syncScroll,见文件顶层的纯函数),任一处改动另一处立即跟着变。 + setSyncScrollTitle: '分屏同步滚动', + setSyncScrollDesc: '阅读器里「分屏」对照(模式 B:原文 PDF ↔ 中文版 PDF 两列)时,两列是否一起滚动。关闭后两列各自独立滚动(适合对照着看不同页);重新打开会自动把两列对齐回当前页,不会停在两页上。全局偏好:所有论文共用一个值,不按篇记;默认开。模式 A(babeldoc 的中英对照 PDF)只有一个滚动条,不受此项影响。', + setSyncScrollLabel: '分屏两列同步滚动', + setSyncScrollOnHint: '当前:开 —— 两列一起滚动(默认)', + setSyncScrollOffHint: '当前:关 —— 两列各自独立滚动;再打开时会自动对齐回当前页', // 文献管理(删除/重命名):契约见 docs/reader-ux-requirements.md §2.9。 // 去重说明:rename/renameTopic/renamePaper/renameFail/delFail 只保留上面上游那组 // (JS 对象字面量重复 key 不报错、后者静默胜出 → 必须手工去重,否则文案悄悄取自另一份)。 @@ -158,6 +181,11 @@ window.__ModuleLoader__.load({ setMineruWhyLocal: 'Local mode expects your own MinerU service (127.0.0.1:8000) — for GPUs and papers that should stay on your machine.', setMineruWhyCloud: 'Cloud mode needs only a mineru.net token and no local GPU; the paper is uploaded to mineru.net for parsing.', setMineruOffHint: 'Currently Off: transcription uses local pdf.js — fine for PDFs that already have a text layer; scanned PDFs (no text layer) and formula citing need MinerU (local or cloud).', + setSyncScrollTitle: 'Split-view sync scrolling', + setSyncScrollDesc: 'In the reader split view (mode B: the original PDF next to the translated -zh.pdf) — should the two columns scroll together? Off: each column scrolls independently (handy to compare different pages); turning it back on realigns both columns to the current page instead of leaving them on different pages. Global preference: one value for every paper, never per paper; on by default. Mode A (babeldoc dual PDF) has a single scrollbar and is unaffected.', + setSyncScrollLabel: 'Scroll the two split columns together', + setSyncScrollOnHint: 'Currently: on — the two columns scroll together (default)', + setSyncScrollOffHint: 'Currently: off — the two columns scroll independently; turning it on realigns them to the current page', newName: 'New name', renameDone: 'Renamed', renameDetached: 'Note: {n} past chats are kept but no longer listed', renameTargetExists: 'A paper or topic with that name already exists', @@ -1418,6 +1446,47 @@ window.__ModuleLoader__.load({ ) } + /* ---------- 分屏同步滚动卡片(模式 B 的**全局**偏好)---------- + 本卡片与阅读器是**两个文档**(阅读器是同源 iframe ~ /paper-reader/),共享同一个 localStorage: + · 这里勾/取消 → 写 `dpr.syncScroll` → 阅读器收到 storage 事件 → 开关与滚动行为立即跟着变 + (打开时还会自动把两列对齐回当前页); + · 反之在阅读器「分屏」控件旁点开关 → 这里收到 storage 事件 → 勾选态立即跟着变。 + ⇒ 两处入口互相同步,且都是**同一个键**(键名与 '1'/'0' 编码由文件顶层的纯函数定义, + 与 reader/index.html 逐字一致)。默认开 = 缺省值(与改造前行为一致)。 */ + function SyncScrollCard() { + const readPref = () => { try { return dprSyncScrollFromRaw(localStorage.getItem(DPR_SYNC_SCROLL_KEY)) } catch { return true } } + const [on, setOn] = useState(readPref) + useEffect(() => { + const onStorage = (e) => { + if (!e || e.key !== DPR_SYNC_SCROLL_KEY) return + setOn(dprSyncScrollFromRaw(e.newValue)) // 另一处(阅读器/另一个窗口)改了 → 立即反映 + } + window.addEventListener('storage', onStorage) + return () => window.removeEventListener('storage', onStorage) + }, []) + const apply = (v) => { + setOn(v) + try { localStorage.setItem(DPR_SYNC_SCROLL_KEY, dprSyncScrollRaw(v)) } catch { /* 隐私模式写不了:阅读器里仍可单独改 */ } + } + return h('div', { style: SET.card, 'data-dsh-plugin': 'dsh-paper-reader', 'data-dsh-part': 'settings-card' }, + h('h3', { style: SET.title }, t('setSyncScrollTitle')), + h('p', { style: SET.desc }, t('setSyncScrollDesc')), + h('div', { style: SET.row }, + h('label', { style: { ...SET.label, display: 'flex', alignItems: 'center', gap: '8px', margin: '0' }, 'data-dpr-sync-label': '' }, + h('input', { + type: 'checkbox', checked: on, 'data-dpr-sync-input': '', + // 与面板主题色对齐(原生 checkbox 的默认蓝与每套主题都对不上):accent-color 是标准 + // 属性,不支持的旧壳只是回落成默认色,不影响可用性 + style: { width: '16px', height: '16px', cursor: 'pointer', accentColor: 'var(--dsw-alias-button-primary-fill, #1f6feb)' }, + onChange: (e) => apply(!!e.target.checked), + }), + t('setSyncScrollLabel'), + ), + h('span', { style: SET.meta, 'data-dpr-sync-state': '' }, on ? t('setSyncScrollOnHint') : t('setSyncScrollOffHint')), + ), + ) + } + function SettingsPanel() { return h('div', { style: { maxWidth: '560px' }, 'data-dsh-plugin': 'dsh-paper-reader', 'data-dsh-surface': 'settings-modal' }, h(EndpointCard, { @@ -1433,6 +1502,7 @@ window.__ModuleLoader__.load({ baseUrlPlaceholder: 'https://api.deepseek.com/v1', modelPlaceholder: 'deepseek-chat', keyPlaceholder: 'sk-...', }), h(MineruCard, null), + h(SyncScrollCard, null), ) } diff --git a/package.json b/package.json index 3db6823..6c59331 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ggboy123/dsh-paper-reader", - "version": "1.4.1", + "version": "1.5.0", "description": "DeepSeek Harness 插件:论文伴读工作台 — PDF 转录 / 检索 / 原生对话伴读 + 内置阅读器页面。", "license": "MIT", "type": "module", diff --git a/reader/index.html b/reader/index.html index f97fbba..5be9db9 100644 --- a/reader/index.html +++ b/reader/index.html @@ -8,6 +8,11 @@ 不用