@ggboy123/dsh-paper-reader 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -297
- package/dist/babeldoc-install.js +6 -2
- package/dist/formulas.js +87 -14
- package/dist/host.js +107 -3
- package/dist/translate-config.js +4 -1
- package/dist/translate-selection.js +772 -0
- package/lib/client.js +70 -0
- package/package.json +1 -1
- package/reader/index.html +1064 -64
- package/reader/vendor/KaTeX_AMS-Regular.woff2 +0 -0
- package/reader/vendor/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/reader/vendor/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/reader/vendor/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/reader/vendor/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/reader/vendor/KaTeX_Main-Bold.woff2 +0 -0
- package/reader/vendor/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/reader/vendor/KaTeX_Main-Italic.woff2 +0 -0
- package/reader/vendor/KaTeX_Main-Regular.woff2 +0 -0
- package/reader/vendor/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/reader/vendor/KaTeX_Math-Italic.woff2 +0 -0
- package/reader/vendor/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/reader/vendor/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/reader/vendor/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/reader/vendor/KaTeX_Script-Regular.woff2 +0 -0
- package/reader/vendor/KaTeX_Size1-Regular.woff2 +0 -0
- package/reader/vendor/KaTeX_Size2-Regular.woff2 +0 -0
- package/reader/vendor/KaTeX_Size3-Regular.woff2 +0 -0
- package/reader/vendor/KaTeX_Size4-Regular.woff2 +0 -0
- package/reader/vendor/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/reader/vendor/katex.LICENSE +21 -0
- package/reader/vendor/katex.NOTICE.md +26 -0
- package/reader/vendor/katex.min.css +1 -0
- package/reader/vendor/katex.min.js +1 -0
package/README.md
CHANGED
|
@@ -7,228 +7,28 @@
|
|
|
7
7
|
|
|
8
8
|
**English**: [README_EN.md](README_EN.md)
|
|
9
9
|
|
|
10
|
-
[DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness)
|
|
11
|
-
|
|
12
|
-
> **👉 普通用户直接看「安装 → 普通用户:桌面端装好即用」**:装 DeepSeek 官方桌面端,添加插件即可,不需要任何开发环境。本仓库面向的是想自己装插件、或想改代码的用户。
|
|
10
|
+
[DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) 插件:**论文伴读工作台** —— 文献库 + PDF 阅读器 + AI 伴读,回答自带页码、点页码跳回原文。当前版本 **v1.5.0**。
|
|
13
11
|
|
|
14
12
|
> 💬 **QQ 交流群:1127244508** —— 安装/使用问题、功能建议、bug 反馈都欢迎来聊。
|
|
15
13
|
|
|
16
|
-
功能迁移自本地独立产品 pdfqa(Go 实现的论文伴读工具)。原则:agent 循环 / 模型层 / 会话持久化全部交给 dsh 框架,插件只做「转录 + 检索 + 阅读器 UI」的薄壳组合。
|
|
17
|
-
|
|
18
|
-
**安装后不改动官方界面** —— 侧栏底部多一行「📚 论文伴读」开关,点击才进入伴读模式(左侧变为文献库树),再点即还原官方工作区列表:
|
|
19
|
-
|
|
20
14
|

|
|
21
15
|
|
|
22
16
|

|
|
23
17
|
|
|
24
18
|

|
|
25
19
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
- 📚 **侧栏文献库树**:「论文伴读」模式开启时接管工作区侧栏,专题 → 论文两级;支持新建专题、上传 PDF、**专题/论文改名与删除**(行悬停出 ✎ 🗑;删除先出「服务端删除清单 + 确认框」、专题还要手输专题名;产物移入回收站可恢复;改名连带转录缓存 / 译文 / 学习档案一起搬家)(默认关闭,不覆盖官方界面)
|
|
29
|
-
- 💬 **原生对话伴读**:点击论文即在右侧打开它的 dsh 原生会话(流式 / 工具卡 / 用量条全是官方 UI);默认只呈现当前对话,对话列头部的 🕐 下拉回看**历史对话**(真实标题+日期,点哪条进哪条),+ 新建对话
|
|
30
|
-
- 🔗 **会话绑定论文**:会话 id 编码文献身份,工具调用自动定位当前论文——直接在输入框提问无需点名论文,回答必带页码
|
|
31
|
-
- 🤖 **专用 agent preset「论文伴读」**:paper 会话自动使用专用 preset —— 像老师一样带读:制定分步阅读计划(每步带页码和思考题)→ 逐节指导、点评 → 出题检验、逐题批改 → 成绩与薄弱点记入 `<文献>.study.json` 学习档案,跨会话累积;快速问答(选中即问)则直接回答,不上课
|
|
32
|
-
- 📖 **PDF 阅读器居中**:中间是论文(PDF.js 缩放 / Retina 高清 / 文本层选择 / 适宽模式 / **滚动 ↔ 单页翻页双模式**——‹ › 翻页、页码输入跳转、← → / PageUp / PageDown / Space / Home / End 键盘翻页),最右是该论文的原生对话 —— 视觉换位实现,列宽拖拽/折叠仍是 dsh 原生行为,关掉 PDF 页签即还原官方布局;状态栏显示 PDF 完整路径,点击复制
|
|
33
|
-
- 💬 **选中即问**:阅读器里选中文字 → 弹出提问框 → 注入当前会话,原生对话区实时回答
|
|
34
|
-
- 📍 **引用定位**:回答里的「第 N 页」可点击,平滑跳回 PDF 对应页并闪烁
|
|
35
|
-
- 🀄 **中英切换 + 导出**:顶栏「中」按钮原文 ↔ 纯中文切换;无译文时一键后台生成(babeldoc),生成需配置 `translate` 端点;**无需预装 Python**——首次生成时自动下载 uv + 托管 Python + babeldoc(macOS / Linux / Windows,约几分钟),全程落在用户目录;顶栏 ⤓ 一键**下载当前版本**(原文 / 纯中文 / 中英对照 PDF)
|
|
36
|
-
- 🔍 **PDF 转录 + 检索**:本地提取(pdf.js,纯 Node 无需 Python),页眉页脚剔除 / 连字 / 断词愈合 / 段落重排,产出页码偏移表;可选接入 TypeSafe/Jev 语义重排(设置面板填端点即启用,不配置时退回纯关键词排序);兼容 pdfqa 的 `data/` 缓存布局(旧缓存读取时自动补建页码索引)
|
|
37
|
-
- 🧲 **可选 MinerU 解析后端**:扫描件 / 复杂版式 / 表格公式场景可切到 MinerU(本地 API 或 mineru.net v4 云端),产出仍是同一套带页码索引的缓存;默认关闭,不配置时行为与旧版本完全一致(详见下文「MinerU 解析后端」)
|
|
38
|
-
- 🧮 **公式鼠标引用**(v1.4.0):PDF 文本层里的公式是乱码(`𝑧𝑧1 𝑥𝑥𝑡𝑡` 这类重复字形),选中它毫无意义;现在直接用鼠标**点 PDF 上的公式**,引用的是 MinerU 识别出的 **LaTeX**(如 `$$\pi(\mathbf{s}_t)$$`),经「选中即问」链路进入会话。**前提:该论文必须先用 MinerU 转录过**(见下文「公式引用与文献管理」)
|
|
39
|
-
- 🗂️ **文献管理:删除与重命名**(v1.4.0):侧栏文献库树的专题行/文献行悬停出现 `✎`(重命名)/ `🗑`(删除)两个按钮——重命名专题/文献、删除文献/专题,均走页内 portal 弹窗(Electron 无 window.prompt/confirm)。删除是**破坏性操作**:**点一次 `🗑`** 即弹出确认框,框里逐条列出的文件清单**来自服务端**(`/api/library/paper/delete-plan`,前端不自己拼);产物会移入文献库内的回收站 `data/.trash/`(可恢复),另有服务端前置校验(详见下文,务必先读「删除的后果」)
|
|
40
|
-
|
|
41
|
-
## 公式引用与文献管理(v1.4.0)
|
|
42
|
-
|
|
43
|
-
### 公式鼠标引用
|
|
44
|
-
|
|
45
|
-
阅读器顶栏新增 **「∑ 公式」** 按钮,两条入口:
|
|
46
|
-
|
|
47
|
-
1. **点击 PDF 上的公式** → 弹出提问框,正文自动填入该公式的 LaTeX(气泡里显示「第 N 页 · 公式 k」+ LaTeX 预览),提问框里写你的问题、回车即发送;气泡里另有「复制 LaTeX」按钮(**不会**自动写剪贴板,避免静默覆盖你的剪贴板)。
|
|
48
|
-
2. **「∑ 公式」面板**:按页列出该论文的全部公式与 LaTeX,点条目也能引用(热区点不中时的兜底,也方便抄 LaTeX)。
|
|
49
|
-
|
|
50
|
-
- **前提:这篇论文必须先用 MinerU 转录过**(设置 → 论文伴读 → MinerU 解析后端,或上传后在阅读器里点面板中的「用 MinerU 重新转录」)。公式块(`equation` 的 `bbox` + LaTeX)只存在于 MinerU 产物 `<文献>.mineru.json` 里,pdf.js 文本层给不出可用公式。
|
|
51
|
-
- **没有产物时不会报错**:入口置灰并在面板里说明原因,提供「用 MinerU 重新转录」按钮;「选中即问」等原有能力完全不受影响。
|
|
52
|
-
- 产物损坏 / 没有公式块时同样只是「没有公式可用」,不会影响阅读与提问。
|
|
53
|
-
- **译文视图(中/对照)下不显示公式热区**:bbox 只对原文页面成立,切回原文即可点击。
|
|
54
|
-
- 公式引用与划词提问**互不抢**:有选中文字时优先按文字提问;公式热区不拦截鼠标事件(不破坏拖拽划选)。
|
|
55
|
-
|
|
56
|
-
### 文献管理:删除与重命名
|
|
57
|
-
|
|
58
|
-
侧栏文献库树里,**专题行**和**文献行**右侧都有 `✎`(重命名)与 `🗑`(删除)两个按钮(悬停出现):
|
|
59
|
-
|
|
60
|
-
| 操作 | 行为 |
|
|
61
|
-
| --- | --- |
|
|
62
|
-
| 重命名专题 / 重命名文献 | 改名(文献的全部同名产物与译文变体一起改)。**注意**:伴读会话 id 内嵌「专题/文献」名,改名后旧会话不会删除,但不再显示在该文献下面(确认框会写明) |
|
|
63
|
-
| 删除文献 | **点一次 `🗑`** → 弹出确认框,逐条列出**服务端返回**的将删除文件与大小 → 确认后**移入回收站** |
|
|
64
|
-
| 删除专题 | **空专题**:点 `🗑` → 按提示输入专题名确认即删。**非空专题**:点 `🗑` 进入**批量清空**中间态(逐篇列出服务端清空清单 → 手输专题名 → 一次确认) |
|
|
65
|
-
|
|
66
|
-
**批量清空非空专题(一次确认)**:非空专题不再要求「30 篇点 30 次」。点 `🗑` 后的确认框里**逐篇**列出将移入回收站的文献(篇名 + 文件数 + 大小,清单来自服务端 dry-run `/api/library/topic/clear-plan`,前端不自己拼),手输专题名后**一次确认**即可:
|
|
67
|
-
|
|
68
|
-
- 每篇仍走**同一个回收站**(`data/.trash/<bundle>/<文献名>/`,每篇一份 `manifest.json` + bundle 根一份总清单,都可恢复),仍按「同名产物 + 译文变体」的**精确 stem** 逐个移动,`.pdf` 最后移;
|
|
69
|
-
- 仍受**运行中会话 / 正在翻译**的**前置拒绝**(确认框会先把会被拦下的篇名标出来;服务端在任何字节被移动之前完成全部校验);
|
|
70
|
-
- 任何一步失败都会**回滚本次已移动的全部文件**(rename 回原位,绝不 unlink),未能回滚的文件会如实列在响应里(`rollbackFailed` + `trashDir`,可手动恢复)——不会留下「删了一半」的散落状态;
|
|
71
|
-
- 全部移完后才**非递归**删除专题目录(绝无 `rm -rf`);专题内的隐藏条目(`.DS_Store` 等)也进回收站,不销毁。
|
|
72
|
-
|
|
73
|
-
**删除的后果(重要)**:
|
|
74
|
-
|
|
75
|
-
- 会被处理的文件 = 该文献在 `data/` 下的**同名产物**:`<文献>.pdf`、`.txt`、`.pages.json`、`.transcript.json`、`.mineru.md`、`.mineru.json`、`.embeddings.json`、`.study.json`(学习档案:阅读计划与检验成绩),以及译文变体 `<文献>-zh.pdf` / `-dual.pdf` / `-en.pdf` 与中断残留的 `.tmp-*`。
|
|
76
|
-
- **可以恢复**:文件被 `rename` 到 `data/.trash/<时间戳>-<随机>/`(同文件系统,不是 unlink),里面有 `manifest.json` 记录清单。把需要的文件从回收站移回专题目录即恢复(本版**不**做自动清理,也不提供一键恢复 UI)。删除成功的响应里会带该回收站的绝对路径 `trashDir`(契约字段,方便脚本/用户直接去恢复;**错误**响应一律不含服务端绝对路径)。
|
|
77
|
-
- **不会被删除**:伴读会话历史(保存在 dsh 会话库里;注意**学习档案文件** `<文献>.study.json` 属于上面的派生产物,会随文献移入回收站,可在回收站恢复)、共享翻译环境 `data/.venv-pdf2zh/`、翻译草稿 `data/.pdf2zh-tmp/`、其他文献与其他专题、专题目录本身。
|
|
78
|
-
- **非规范文献名(手工放进库的)**:插件的写入路径(新建专题 / 上传 / 改名)都按命名规范校验,所以插件自己不会产生不合规的名字。但如果你手工往 `data/` 里放了以 `.` 开头的 PDF(如 `.hidden.pdf`),它会被列进文献树,并且**可以正常删除 / 改名**——删除、以及改名的**原名**只做**边界校验**(拒绝路径分隔符、`..`、NUL、保留名),命名规范只在**新建**或**改成新名字**时强制。这样任何已经存在于磁盘上的条目都不会变成「看得见、删不掉」的孤儿。
|
|
79
|
-
- **安全边界**:所有删除/重命名只作用于**文献库目录内**(`DSH_PAPER_READER_DATA` 指定的 `data/`,默认 `~/.dsh-paper-reader/data`)。路径穿越(`..`、绝对路径、符号链接逃逸)一律拒绝;写路径**只接受**「专题名 + 文献名」,不接受任意路径参数;符号链接专题直接拒绝,符号链接文件只移动链接本身、不动其指向的目标。
|
|
80
|
-
- **前置校验**:文献有**运行中的伴读会话**或**正在生成译文**时,删除/重命名会被拒绝(避免删掉运行中会话的工作目录、或翻译完成后把文件写回来)。
|
|
81
|
-
- 另两种会被拒绝的情况(都是刻意的保守设计):**会话状态一时查不到**时一律拒绝(提示稍后重试,而不是「当作没有会话」冒险动手);**同一篇文献已有一个删除/重命名在进行中**时,第二个请求会被拒绝(避免两个操作交错改同一批文件)。
|
|
82
|
-
|
|
83
|
-
## MinerU 解析后端(可选)
|
|
84
|
-
|
|
85
|
-
默认用本地 pdf.js 抽文本层(纯 Node,无需 Python,文本型 PDF 毫秒级)。可选接入 **MinerU** 作为解析后端,
|
|
86
|
-
用于扫描件 / 复杂版式 / 表格公式场景 —— 产出仍然是同一套带页码索引的缓存(`.txt` + `.pages.json`),
|
|
87
|
-
检索、跳页、旧缓存行为都不变。
|
|
88
|
-
|
|
89
|
-
两种模式,在 **设置 → 论文伴读 → 「MinerU 解析后端」卡片** 里选(也可写配置文件):
|
|
90
|
-
|
|
91
|
-
| 模式 | 说明 |
|
|
92
|
-
| --- | --- |
|
|
93
|
-
| `off`(默认) | 不启用,行为与旧版本完全一致 |
|
|
94
|
-
| `local` | 本机 MinerU API(默认 `http://127.0.0.1:8000`),启动方式见下 |
|
|
95
|
-
| `cloud` | mineru.net v4 云端,需要一个 token |
|
|
96
|
-
|
|
97
|
-
**本地 MinerU**(官方 API 服务,本仓库用 3.4.5 实测):
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
pip install -U "mineru[core]"
|
|
101
|
-
mineru-api --host 127.0.0.1 --port 8000 # 或 uvicorn mineru.cli.fast_api:app
|
|
102
|
-
curl -s http://127.0.0.1:8000/health # {"status":"healthy","version":"3.4.5",...}
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
默认 backend 取 `pipeline`(通用、不依赖 VLM 模型);有 GPU 且想更准可切 `hybrid-engine`
|
|
106
|
-
(需要 VLM 模型已就绪,实测 2 页合成 PDF 约 5 秒)。扫描件建议把「本地解析方式」设为 `auto`,由 MinerU 自行决定 OCR。
|
|
107
|
-
|
|
108
|
-
**云端 MinerU(三步,约 1 分钟)**:
|
|
20
|
+
## 核心能力
|
|
109
21
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
22
|
+
- 📚 **三栏工作台**:专题文献库 + PDF 阅读器 + 右侧原生对话;安装后不改动官方界面,侧栏只多一行开关
|
|
23
|
+
- 💬 **带页码的伴读**:回答必带页码,点页码跳回原文并高亮;选中文字即问;支持历史对话
|
|
24
|
+
- 🤖 **教练式带读**:专用 preset 定分步阅读计划、逐节指导、出题批改,学习进度跨会话累积
|
|
25
|
+
- 🀄 **一键中文版**:原文 ↔ 纯中文原地切换,译文 PDF 一键导出(纯中文 / 中英对照),翻译引擎零预装自动安装
|
|
26
|
+
- 🈯 **划词翻译(公式保留)**(v1.5.0):选中点「译」出纯译文气泡;选区含公式时用 MinerU 识别的 LaTeX 替换文本层乱码、要求模型原样保留
|
|
27
|
+
- 🖥️ **中英对照 / 分屏**(v1.5.0):并排对照页或左右分屏同步滚动,随时切换、按文献记忆
|
|
28
|
+
- 🗂️ **文献管理**:专题/论文改名、删除进回收站可恢复
|
|
29
|
+
- ➕ **可选增强**(默认关):MinerU 解析——扫描件可读、公式可点引用可检索;嵌入端点——大白话命中术语段落
|
|
113
30
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
- **公式引用**:PDF 文本层里的公式是乱码(`𝑧𝑧1 𝑥𝑥𝑡𝑡` 这类重复字形),只有 MinerU 识别出的 LaTeX 能点着引用;
|
|
117
|
-
- **扫描版 / 复杂版式**:这类 PDF 没有文本层,pdf.js 抽不出东西,MinerU(OCR)能读。
|
|
118
|
-
|
|
119
|
-
**不配置 / 关闭时能做什么**(与实现一致,不夸大):转录走本地 pdf.js——有文本层的普通 PDF 完全够用(转录、检索、划词提问、导出译文都不受影响);**扫描件与公式引用**才需要 MinerU。配置并保存后,阅读器公式面板里才会出现「用 MinerU 重新转录」(模式为「关闭」时该按钮不渲染,避免误用 pdf.js 覆盖已有的 MinerU 产物)。
|
|
120
|
-
|
|
121
|
-
一次解析的序列是:申请批量上传地址 → PUT 上传 → 轮询结果 → 下载并解析 zip。
|
|
122
|
-
✅ **云端 `pipeline` 通道已端到端实测**(2026-10-11,真实 token + 一篇 **19 页真实论文**:提交 → 轮询 → 下载 zip → 投影 → 检索,**25 条断言 0 失败,约 19s**;**18/18** 个 equation 的 bbox 与 LaTeX 与本地产物**逐字相同**)。
|
|
123
|
-
⚠️ **仍未验证**(按实验特性对待):`vlm` 系列模型(`vlm-engine`/`vlm-http-client`)、配额耗尽 / HTTP 429 / zip 重试等异常路径——这些只有 mock 覆盖。
|
|
124
|
-
token 只落盘到 `~/.dsh/.dsh-paper-reader/mineru.json`(0600);读取接口只回掩码,日志 / 错误 / 响应都不出现明文。
|
|
125
|
-
|
|
126
|
-
**配置文件 / 环境变量**(优先级逐字段:文件 > profile YAML `config.mineru` > 环境变量 > 默认值):
|
|
127
|
-
|
|
128
|
-
```yaml
|
|
129
|
-
- id: dsh-paper-reader
|
|
130
|
-
config:
|
|
131
|
-
mineru:
|
|
132
|
-
mode: local
|
|
133
|
-
local:
|
|
134
|
-
baseUrl: http://127.0.0.1:8000
|
|
135
|
-
backend: pipeline
|
|
136
|
-
parseMethod: auto
|
|
137
|
-
# cloud:
|
|
138
|
-
# apiKey: '' # 一般不用写在这里,UI 里填即可
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
| 环境变量 | 覆盖字段 |
|
|
142
|
-
| --- | --- |
|
|
143
|
-
| `DSH_MINERU_LOCAL_URL` | `local.baseUrl` |
|
|
144
|
-
| `MINERU_API_KEY` | `cloud.apiKey` |
|
|
145
|
-
|
|
146
|
-
**缓存与来源**:MinerU 解析仍然写 `.txt` + `.pages.json`(`page` 从 1 起,检索映射页码用),
|
|
147
|
-
另加 `.transcript.json`(来源标记:`pdfjs` / `mineru-local` / `mineru-cloud`)与 MinerU 富产物
|
|
148
|
-
`.mineru.md` / `.mineru.json`(Markdown + content_list,供人工核对)。旧缓存没有来源标记时归类为 pdfjs,
|
|
149
|
-
**升级后不会触发重新解析**;要换来源就对目标论文显式指定(agent 侧 `transcribe_pdf` 的 `source` 参数,
|
|
150
|
-
HTTP 侧 `POST /api/transcribe` 的 `source`,取值 `auto|pdfjs|mineru-local|mineru-cloud`)。
|
|
151
|
-
MinerU 解析失败时**不动**已有缓存,错误信息带 HTTP 状态与脱敏后的服务端 message。
|
|
152
|
-
|
|
153
|
-
**公式可检索(v1.3.4 起)**:MinerU 产出的 LaTeX 在**每个 token 之间**都带空格(`x _ { t - 1 }`),
|
|
154
|
-
此前 `search_paper` 只有逐字照抄带空格写法才命中。投影成 `.txt` 时会先把数学区间(`$...$` / `$$...$$`)
|
|
155
|
-
内的空白压掉、再把**单 token** 下标/上标的花括号归一化(`Q_{t}` → `Q_t`、`^{2}` → `^2`),
|
|
156
|
-
因此 `x_{t-1}`、`\mathbb{R}`、`Q_t`、`q(x_t|x_{t-1})` 这类**正常写法**的公式查询可直接命中
|
|
157
|
-
(真实论文实测:改造前 0 命中 → 改造后命中原页)。`\text{...}` 等空格有语义的命令整组原样保留;
|
|
158
|
-
多 token 参数(`x_{t-1}`、`x_{ij}`、`^{K \times K}`)保留花括号;`.mineru.md` / `.mineru.json`
|
|
159
|
-
仍是 MinerU 原样产出,不受影响。
|
|
160
|
-
|
|
161
|
-
> **升级须知**:盘上已有的 `.txt` 缓存**不会被自动改写**(仍是旧格式,公式查询依旧搜不到)。
|
|
162
|
-
> 要让某篇论文的公式可搜,需对该论文**显式重新转录**:agent 侧 `transcribe_pdf` 传
|
|
163
|
-
> `source: mineru-local`(或 `force: true`),HTTP 侧 `POST /api/transcribe` 传同样的 `source`。
|
|
164
|
-
>
|
|
165
|
-
> **写法约定**:`.txt` 里存的是**紧凑写法**(`Q_t`)。查询侧会用**与投影侧同一套规则**先归一化再用原查询
|
|
166
|
-
> 一并匹配,所以 `Q_t` 与 `Q_{t}` **两种写法都能命中同一页**(原查询永远保留,不会丢任何既有命中);
|
|
167
|
-
> 多 token 的 `x_{t-1}`、`x_{ij}` 不被归一化,按原样书写即可。
|
|
168
|
-
>
|
|
169
|
-
> **两处 low 级待修项(已排入下一轮)**:
|
|
170
|
-
> F-R1 `keepsInnerSpaces` 用前缀匹配把双参数 `\textcolor` 误纳入「空格有语义」保护
|
|
171
|
-
> (`$\textcolor{red}{hello world}$` 的第二组空格被压缩);F-R2 区间界定扫 `$` 时不感知 `\text{}` 组
|
|
172
|
-
> (`$\text{costs $5}$` 会被内层 `$` 切断,属已声明边界的子形态)。
|
|
173
|
-
|
|
174
|
-
**API**(设置面板用的读写口,走 `/paper-reader` 前缀与既有鉴权):`GET|POST|DELETE /api/mineru/config`
|
|
175
|
-
(GET 只回掩码;POST 预检通过才落盘;DELETE 回落 profile/环境变量)、`POST /api/mineru/test`
|
|
176
|
-
与 `GET /api/mineru/health`(连通性预检,不落盘)。
|
|
177
|
-
|
|
178
|
-
**常见错误**:
|
|
179
|
-
|
|
180
|
-
| 现象 | 原因 / 处理 |
|
|
181
|
-
| --- | --- |
|
|
182
|
-
| 保存时报「本地 MinerU 连接测试失败:HTTP 404 …」 | 地址指向了别的服务,或 MinerU 版本过老(2.x 没有 `/health`);`curl {baseUrl}/health` 自查 |
|
|
183
|
-
| 保存时报「不是已识别的 MinerU API 服务」 | `/health` 返回里缺 `status`/`version`,不是 MinerU |
|
|
184
|
-
| 解析报「本地 MinerU 解析失败(HTTP 409)」 | 服务端任务执行失败,看响应里的 error 字段。MinerU 3.4.5 的 409 是**解析失败**,不是「服务忙」 |
|
|
185
|
-
| 解析报「MinerU 未返回可用内容(md_content 与投影文本均为空)」或「MinerU 转录结果过短(N 字符),疑似失败,请重试」 | 服务端没产出 md/content_list(模型未就绪等);先用 `curl -F files=@x.pdf http://127.0.0.1:8000/file_parse` 验证服务本身 |
|
|
186
|
-
| 解析超时 | 长论文默认总上限 30 分钟(`jobTimeoutMs`),仍不够时在配置文件里调大 |
|
|
187
|
-
| 云端报「HTTP 401:云端 token 无效或已过期」 | token 过期,或把 `Bearer` 前缀也填进了输入框(只填 token 本体) |
|
|
188
|
-
|
|
189
|
-
> **云端实测范围(如实交代,别读成「全链路已验证」)**:mineru.net v4 的 **`pipeline` 通道已用真实 token 跑通**
|
|
190
|
-
> (19 页真实论文一篇,25 条断言 0 失败、约 19s;18/18 equation 的 bbox/LaTeX 与本地产物逐字一致);
|
|
191
|
-
> **`vlm` 系列模型、配额耗尽 / 429 / zip 重试**等异常路径**仍未验证**,只有 mock 覆盖。
|
|
192
|
-
> 本地链路在 MinerU 3.4.5 上端到端实测通过。其余为若干 low 级记录项(云端多文件写盘非事务、云端轮询间隔不随时间递增、
|
|
193
|
-
> zip 解析里的残留死代码等),均不影响默认路径:`mode` 默认 `off`,不配置 MinerU 时行为与接入前完全一致。
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
## 嵌入模型检索(可选,默认关闭)
|
|
197
|
-
|
|
198
|
-
除了关键词子串匹配,还可以给 `search_paper` 接一个 **OpenAI 兼容 `/embeddings` 端点**做语义召回:
|
|
199
|
-
关键词一个词都没命中的**语义相近**片段也能进候选(融合,不替代关键词)。跨语言提问、换同义词、
|
|
200
|
-
问「那段讲扩散过程的公式」这类场景命中率明显更高。
|
|
201
|
-
|
|
202
|
-
**怎么配**(三选一,与翻译/重排端点同一套优先级:设置面板 > profile YAML > 环境变量):
|
|
203
|
-
|
|
204
|
-
| 方式 | 写法 |
|
|
205
|
-
| --- | --- |
|
|
206
|
-
| 设置面板(推荐) | 设置 → 论文伴读 → **「嵌入模型检索(可选)」** 卡片,填 端点 URL / 模型 / API Key,保存前会**真打一发** `/embeddings` 预检 |
|
|
207
|
-
| profile YAML | `config.embed: { baseUrl, apiKey, model }` |
|
|
208
|
-
| 环境变量 | `DSH_EMBED_API_KEY`(只补 key;baseUrl/model 仍要另配) |
|
|
209
|
-
|
|
210
|
-
落盘位置 `$DSH_HOME/.dsh-paper-reader/embed.json`(**0600**,与 `translate.json`/`typesafe.json` 同一约定);
|
|
211
|
-
读取接口只回 `hasApiKey` + 掩码 `apiKeyHint`,明文 key 不回传。
|
|
212
|
-
|
|
213
|
-
> ⚠️ **隐私提示**:启用后,**两类文本都会发送到你配置的那个端点**——① 论文的**分块文本(正文)**(算分块向量;
|
|
214
|
-
> mock 抓包确认发出去的就是分块文本本身);② **检索时的查询文本**(每次检索先把查询发去算查询向量)。
|
|
215
|
-
> 端点必须是你要信任的服务;**不配则一条请求都不发**。缓存文件与错误信息里都不会出现 API Key
|
|
216
|
-
> (查询向量只留在进程内备忘,不落盘)。
|
|
217
|
-
|
|
218
|
-
**行为与降级**:不配(或只配一半)= **默认关闭**,`search_paper` 与改造前**逐字节一致**(纯关键词排序、零新请求)。
|
|
219
|
-
配了之后:关键词候选 ∪ 嵌入候选 → RRF 融合 → 若配了 Jev/TypeSafe 重排则继续走既有重排 → top-k。
|
|
220
|
-
**融合行为说明**:关键词候选与语义候选按**名次**融合(RRF,k=60)——只看名次、**不看分数大小**,所以两列候选会**交替占用**最终 top-k 名额(语义第 1 名可能排到关键词第 2 名之前);「关键词优先」只在**精确同分**时生效。嵌入是**召回**步骤、不替代关键词(关键词检索始终在跑并贡献候选),但**语义候选确实可能占用部分 top-k 名额**——这是有意为之的形态(无真实嵌入可调参,故不引入按分数插值/名额保底这类无法验证的策略)。**未配置嵌入端点时完全不受影响**:一条请求都不发,排序与改造前逐字节一致。
|
|
221
|
-
端点在这次检索里不可达 / 超时 / 返回错误 / 维度或数值非法时:**照常返回关键词结果**,并在结果末尾如实标注降级原因
|
|
222
|
-
(`(嵌入检索不可用,已降级为纯关键词:…)`),不会静默失败、也不会中断检索。
|
|
223
|
-
|
|
224
|
-
**缓存与失效**:`<专题>/<论文名>.embeddings.json`,键 = `{baseUrl, model, dimensions, 内容哈希}`。
|
|
225
|
-
**换端点(baseUrl)/ 换模型 / 改 `.txt`(重新转录)/ 改分块大小 / 返回维度与缓存不一致** → 自动判定失效并重算;
|
|
226
|
-
**命中缓存时不再发分块嵌入请求**(查询向量另有进程内备忘,同一查询重复检索零请求)。
|
|
227
|
-
按 `batchSize`(默认 32)分批提交;**任一批失败就整体降级且不落盘**——不产生半截缓存。
|
|
228
|
-
|
|
229
|
-
惰性:**首次检索该论文时才计算**,转录(`transcribe_pdf`)阶段完全不碰嵌入,转录速度与失败面不受影响。
|
|
230
|
-
|
|
231
|
-
> **非目标(留后续)**:本地 ONNX / transformers.js 嵌入模型、全库预计算、向量数据库 —— 本轮只做 API 端点。
|
|
31
|
+
各功能的前提与配置细节见 [docs/features.md](docs/features.md)。
|
|
232
32
|
|
|
233
33
|
## 安装
|
|
234
34
|
|
|
@@ -241,51 +41,38 @@ MinerU 解析失败时**不动**已有缓存,错误信息带 HTTP 状态与脱
|
|
|
241
41
|
|
|
242
42
|
完成。侧栏底部点「📚 论文伴读」进入文献库,上传 PDF 开始用。
|
|
243
43
|
|
|
244
|
-
完整流程演示(添加插件 → 启用 → 重启 → 打开论文直接提问):
|
|
245
|
-
|
|
246
44
|

|
|
247
45
|
|
|
248
|
-
> 已在官方桌面端 V0.2.0-rc.2(内置 dsh 0.2.0-rc.2,macOS arm64
|
|
249
|
-
>
|
|
250
|
-
|
|
251
|
-
> 想直接在客户端里逛插件市场(4000+ 插件、分类、评论、一键安装),可另装社区插件 [`dshmarket`](https://github.com/dsh-market/dsh-market),本插件已在其目录中。
|
|
46
|
+
> 已在官方桌面端 V0.2.0-rc.2(内置 dsh 0.2.0-rc.2,macOS arm64)实测通过;插件需 ≥1.2.0。
|
|
47
|
+
> 想逛插件市场可另装社区插件 [`dshmarket`](https://github.com/dsh-market/dsh-market),本插件已在其目录中。
|
|
252
48
|
|
|
253
|
-
|
|
49
|
+
**排坑**(都是实测踩过的):
|
|
254
50
|
|
|
255
|
-
-
|
|
256
|
-
-
|
|
257
|
-
-
|
|
51
|
+
- **升级后插件开关会回到停用**:重新打开「启用」并重启。症状有迷惑性——文献树还在但点论文没反应
|
|
52
|
+
- **添加务必带 `@latest`**:不带版本号对已装过的插件是 no-op,不会追新
|
|
53
|
+
- pnpm 供应链策略 `minimumReleaseAge` 拦新包时:等过窗口期,或把本包加进豁免清单(`~/.dsh/profiles/web/.npmrc` 的 `minimum-release-age-exclude[]`;桌面端在 `~/.dsh/profiles/desktop/pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude`)
|
|
258
54
|
|
|
259
55
|
### 开发者:命令行 dsh
|
|
260
56
|
|
|
261
57
|
```bash
|
|
262
|
-
dsh plugin --profile web add @ggboy123/dsh-paper-reader@latest
|
|
58
|
+
dsh plugin --profile web add @ggboy123/dsh-paper-reader@latest # 升级也是这条;不带版本号是 no-op
|
|
263
59
|
```
|
|
264
60
|
|
|
265
|
-
>
|
|
266
|
-
> 不要用 `github:GGboya/dsh-paper-reader` 安装 —— dist 不入库,git 安装会导致 profile 起不来(构建产物只随 npm 包分发)。
|
|
61
|
+
> 不要用 `github:GGboya/dsh-paper-reader` 安装 —— dist 不入库,git 安装会导致 profile 起不来。
|
|
267
62
|
|
|
268
|
-
|
|
63
|
+
## 怎么用
|
|
269
64
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
65
|
+
1. **进入**:侧栏底部点「📚 论文伴读」(再点还原)
|
|
66
|
+
2. **入库**:+ 新建专题 → ⬆ 上传 PDF
|
|
67
|
+
3. **读 + 问**:直接提问(回答自带页码),或选中文字弹框提问;点「第 N 页」跳回原文
|
|
68
|
+
4. **中文版**:顶栏「中」一键生成(首次需在弹出表单里配一次 OpenAI 兼容端点,DeepSeek/GLM/Kimi 的 key 都行)
|
|
69
|
+
5. **管理**:行悬停出 ✎ 改名 / 🗑 删除;对话头部 🕐 历史、+ 新开
|
|
275
70
|
|
|
276
|
-
|
|
71
|
+
## 文献管理(安全说明)
|
|
277
72
|
|
|
278
|
-
|
|
279
|
-
grep '"version"' ~/.dsh/profiles/web/node_modules/@ggboy123/dsh-paper-reader/package.json
|
|
280
|
-
```
|
|
73
|
+
- 删除 = **移入回收站** `data/.trash/`(带 manifest,移回即恢复);会处理该文献的全部产物:`<文献>.pdf`、`.txt`、`.pages.json`、`.transcript.json`、`.mineru.md`、`.mineru.json`、`.embeddings.json`、`.study.json` 与译文变体 `<文献>-en.pdf` / `-zh.pdf` / `-dual.pdf`(含 `.tmp-*` 残留);伴读会话历史不删。运行中会话/翻译中的文献会被前置拒绝;路径穿越一律拒绝、失败自动回滚——细则见 [docs/features.md](docs/features.md)
|
|
281
74
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
### 参与开发(本地调试)
|
|
285
|
-
|
|
286
|
-
欢迎改代码。流程:clone → 构建 → 以本地目录装进 profile,之后改完 `pnpm run build` 重启对应 profile 即可验证:
|
|
287
|
-
|
|
288
|
-

|
|
75
|
+
## 参与开发
|
|
289
76
|
|
|
290
77
|
```bash
|
|
291
78
|
git clone https://github.com/GGboya/dsh-paper-reader
|
|
@@ -294,67 +81,19 @@ dsh plugin --profile web add ./dsh-paper-reader # 从父目录执行,或用
|
|
|
294
81
|
dsh --profile web # 侧栏变为文献库树
|
|
295
82
|
```
|
|
296
83
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
```yaml
|
|
300
|
-
- id: dsh-paper-reader
|
|
301
|
-
config:
|
|
302
|
-
dataDir: /path/to/pdfqa/data # 直接指向 pdfqa 文献库即可复用全部缓存
|
|
303
|
-
# translate: # 可选:生成中文版(babeldoc)用的 OpenAI 兼容端点。
|
|
304
|
-
# baseUrl: https://api.deepseek.com/v1 # 一般不用写在这里——阅读器里点「中」
|
|
305
|
-
# apiKey: sk-... # 或右上角 ⚙ 可直接填,存到
|
|
306
|
-
# model: deepseek-chat # ~/.dsh/.dsh-paper-reader/translate.json(0600)
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
> 中文版端点:**优先在阅读器 UI 里填**(点「中」→ 没配会自动弹表单,或点 ⚙)——填完会先测连接再落盘,不用重启。
|
|
310
|
-
> 上面的 `translate` 配置退为**部署方默认值**,仅当 UI 未配置时生效。babeldoc 只支持 OpenAI 协议端点,
|
|
311
|
-
> Anthropic 协议的端点(如 `api.kimi.com/coding/`)不能直连。
|
|
312
|
-
>
|
|
313
|
-
> 翻译引擎**零预装**:找不到 babeldoc 时自动走 uv 链路安装——uv 独立二进制(GitHub Releases API 下载 + sha256 校验)→
|
|
314
|
-
> uv 托管 Python 3.12 → `uv pip install babeldoc`,落在 `~/.dsh/.dsh-paper-reader/bin/` 与文献库同级 `.venv-pdf2zh/`,
|
|
315
|
-
> 不碰系统 Python。已装有 uv / babeldoc(含 pdfqa 的 `.venv-pdf2zh`)则直接复用。macOS / Linux / Windows 均支持自动安装(Windows 走 uv 的 zip 包 + 系统自带 tar 解包)。
|
|
84
|
+
改完 `pnpm run build` 重启 profile 即生效;`npm test` 跑全量测试(354 用例,CI 三平台)。架构、测试守卫与开发约定见 [AGENTS.md](AGENTS.md)。
|
|
316
85
|
|
|
317
|
-
|
|
86
|
+
## 贡献者
|
|
318
87
|
|
|
319
|
-
|
|
88
|
+
感谢每一位贡献者:
|
|
320
89
|
|
|
321
|
-
|
|
90
|
+
<a href="https://github.com/GGboya/dsh-paper-reader/graphs/contributors">
|
|
91
|
+
<img src="https://contrib.rocks/image?repo=GGboya/dsh-paper-reader" alt="贡献者头像墙" />
|
|
92
|
+
</a>
|
|
322
93
|
|
|
323
|
-
|
|
324
|
-
pnpm build
|
|
325
|
-
pnpm smoke:engine clean # 全新安装全链路
|
|
326
|
-
pnpm smoke:engine managed-uv # 强制走插件托管 uv 下载(sha256 校验路径)
|
|
327
|
-
pnpm smoke:engine pip-fallback # 强制走 pip + 国内镜像备用通道
|
|
328
|
-
pnpm smoke:engine repair # 破坏 venv(删 pymupdf)后验证自检→自动修复
|
|
329
|
-
pnpm smoke:engine translate # 再起本地 mock OpenAI 端点,真跑一遍 babeldoc 翻译管线
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
CI(`.github/workflows/engine-smoke.yml`):`windows-latest` 跑全部四个场景 + translate-e2e,`macos-latest`/`ubuntu-latest` 跑 clean 防回归;改安装/翻译代码的 push 触发,外加每周一定时(uv/PyPI 在变,代码不动也可能突然坏)。两个测试钩子环境变量:`DSH_PR_MANAGED_UV_ONLY=1`(跳过系统 uv 查找)、`DSH_PR_FORCE_PIP_CHANNEL=1`(跳过 uv 装包通道)。
|
|
94
|
+
## Star 趋势
|
|
333
95
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
```
|
|
337
|
-
src/
|
|
338
|
-
index.ts Cordis 壳:inject tools;webServer 等服务就绪后挂路由
|
|
339
|
-
tools.ts 5 个 agent 工具:list_papers / transcribe_pdf / search_paper / study_progress / study_update
|
|
340
|
-
host.ts webServer 路由(/paper-reader/*),connection.requestRejection 鉴权;
|
|
341
|
-
sessionController.create/prompt(agentPreset=paper-reader)+ follow SSE 桥
|
|
342
|
-
library.ts 纯函数:文献库目录约定与解析(.txt/.pages.json/.transcript.json/.mineru.* 产物路径)
|
|
343
|
-
transcribe.ts 纯函数:pdf.js 提取 + 页码偏移表 + MinerU 后端选择与缓存来源语义
|
|
344
|
-
mineru.ts 纯函数:MinerU 客户端(本地 legacy /tasks 轮询 + /file_parse;mineru.net v4 云端 + 最小 ZIP 读取器)
|
|
345
|
-
mineru-config.ts 纯函数:MinerU 配置读写($DSH_HOME 下 0600)+ 掩码 + 连通性预检
|
|
346
|
-
search.ts 纯函数:分段 + 关键词打分 + 页码映射
|
|
347
|
-
study.ts 纯函数:学习档案(计划 + 检验成绩)读写
|
|
348
|
-
translate.ts 纯函数:babeldoc 调用(中文/中英对照 PDF 生成)
|
|
349
|
-
babeldoc-install.ts 纯函数:无 Python 环境时自动安装 babeldoc(uv → 托管 Python → venv)
|
|
350
|
-
translate-config.ts 纯函数:翻译端点配置读写($DSH_HOME 下 0600)+ 保存前连接预检
|
|
351
|
-
rerank.ts 纯函数:search_paper 语义重排(TypeSafe/Jev,无凭据时退回关键词排序)
|
|
352
|
-
typesafe-config.ts 纯函数:TypeSafe 端点配置读写(与翻译配置同一套约定)
|
|
353
|
-
preset.ts 纯函数:自带 agent preset 安装到 $DSH_HOME/.agent-presets/
|
|
354
|
-
presets/paper-reader/ 「论文伴读」agent preset(persona 完整提示词 + compaction)
|
|
355
|
-
reader/index.html 阅读器页面(pdf.js,独立于 React 宿主,iframe 承载)
|
|
356
|
-
lib/client.js 浏览器半边:文献库树(sidebar.workspaces 接管) + PDF 页签 + 主面板
|
|
357
|
-
```
|
|
96
|
+
[](https://star-history.com/#GGboya/dsh-paper-reader&Date)
|
|
358
97
|
|
|
359
98
|
## License
|
|
360
99
|
|
package/dist/babeldoc-install.js
CHANGED
|
@@ -217,13 +217,17 @@ const CN_MIRRORS = [
|
|
|
217
217
|
'https://mirrors.aliyun.com/pypi/simple/',
|
|
218
218
|
'https://mirrors.cloud.tencent.com/pypi/simple',
|
|
219
219
|
];
|
|
220
|
+
// babeldoc 依赖 bitstring>=4.3.0 无上界,而 bitstring 5.0(2026-10 发布)删除了 BitStream,
|
|
221
|
+
// babeldoc 的 pdf_creater.py 仍在 `from bitstring import BitStream` —— 裸装必装出残缺 venv。
|
|
222
|
+
// 上游修掉该 import 或锁上界后可移除此 pin。
|
|
223
|
+
const BITSTRING_PIN = 'bitstring<5';
|
|
220
224
|
async function pipInstallBabeldoc(py) {
|
|
221
225
|
await run(py, ['-m', 'ensurepip', '--upgrade'], { timeout: 5 * 60_000 });
|
|
222
226
|
let lastErr = null;
|
|
223
227
|
for (const m of CN_MIRRORS) {
|
|
224
228
|
try {
|
|
225
229
|
await run(py, [
|
|
226
|
-
'-m', 'pip', 'install', '--no-cache-dir', '--timeout', '120', '--retries', '3', '-i', m, 'babeldoc',
|
|
230
|
+
'-m', 'pip', 'install', '--no-cache-dir', '--timeout', '120', '--retries', '3', '-i', m, 'babeldoc', BITSTRING_PIN,
|
|
227
231
|
], { timeout: 30 * 60_000 });
|
|
228
232
|
return;
|
|
229
233
|
}
|
|
@@ -260,7 +264,7 @@ async function installBabeldoc(uv, venvDir, onPhase) {
|
|
|
260
264
|
await viaPipFallback(null);
|
|
261
265
|
else {
|
|
262
266
|
try {
|
|
263
|
-
await run(uv, ['pip', 'install', '--python', py, 'babeldoc'], { timeout: 20 * 60_000 });
|
|
267
|
+
await run(uv, ['pip', 'install', '--python', py, 'babeldoc', BITSTRING_PIN], { timeout: 20 * 60_000 });
|
|
264
268
|
}
|
|
265
269
|
catch (uvErr) {
|
|
266
270
|
await viaPipFallback(uvErr);
|
package/dist/formulas.js
CHANGED
|
@@ -19,6 +19,51 @@
|
|
|
19
19
|
// `\text{}`/`\textrm`/`\mbox` 一族按既有规则整组原样保留(组内空格是排版语义)。
|
|
20
20
|
import { readFile, stat } from 'node:fs/promises';
|
|
21
21
|
import { compressMathSpaces, normalizeMathBraces } from "./mineru.js";
|
|
22
|
+
/** 从 `.mineru.json` 的原始文本建 text 块索引(只收含 `$` 的块)。 */
|
|
23
|
+
export function buildTextBlockIndex(raw) {
|
|
24
|
+
if (raw === null)
|
|
25
|
+
return { source: 'none', blocks: [], reason: 'no-mineru-artifact' };
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { source: 'none', blocks: [], reason: 'bad-artifact' };
|
|
32
|
+
}
|
|
33
|
+
if (!isRecord(parsed))
|
|
34
|
+
return { source: 'none', blocks: [], reason: 'bad-artifact' };
|
|
35
|
+
const list = parsed['contentList'];
|
|
36
|
+
if (!Array.isArray(list))
|
|
37
|
+
return { source: 'none', blocks: [], reason: 'bad-artifact' };
|
|
38
|
+
// page_idx 缺失的块沿用上一个有页码的块(与 buildFormulaIndex / transcribe 的投影口径一致)
|
|
39
|
+
const blocks = [];
|
|
40
|
+
const perPage = new Map();
|
|
41
|
+
let lastPageIdx = 0;
|
|
42
|
+
for (const b of list) {
|
|
43
|
+
if (!isRecord(b))
|
|
44
|
+
continue;
|
|
45
|
+
if (b['type'] !== 'text')
|
|
46
|
+
continue;
|
|
47
|
+
const pi = b['page_idx'];
|
|
48
|
+
if (typeof pi === 'number' && Number.isFinite(pi) && pi >= 0)
|
|
49
|
+
lastPageIdx = Math.trunc(pi);
|
|
50
|
+
const text = b['text'];
|
|
51
|
+
if (typeof text !== 'string' || !text.includes('$'))
|
|
52
|
+
continue;
|
|
53
|
+
const index = (perPage.get(lastPageIdx) ?? 0) + 1;
|
|
54
|
+
perPage.set(lastPageIdx, index);
|
|
55
|
+
blocks.push({
|
|
56
|
+
page: lastPageIdx + 1,
|
|
57
|
+
pageIdx: lastPageIdx,
|
|
58
|
+
index,
|
|
59
|
+
bbox: toBbox(b['bbox']),
|
|
60
|
+
text: compactLatex(text),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// 注意:blocks 为空**不是**错误——产物可能根本没有含 `$` 的 text 块(行内还原直接
|
|
64
|
+
// 走 unavailable)。reason 只在「读不到 contentList」时给,语义与 FormulaReason 一致。
|
|
65
|
+
return { source: 'mineru', blocks };
|
|
66
|
+
}
|
|
22
67
|
/** 供测试与调用方复用的紧凑化:既有投影两步 + 算子名统一(见下)。 */
|
|
23
68
|
export function compactLatex(rawLatex) {
|
|
24
69
|
return unifyOperatorNames(normalizeMathBraces(compressMathSpaces(rawLatex)));
|
|
@@ -240,33 +285,61 @@ export function buildFormulaIndex(raw, opts = {}) {
|
|
|
240
285
|
};
|
|
241
286
|
}
|
|
242
287
|
let cache = null;
|
|
243
|
-
|
|
244
|
-
|
|
288
|
+
let rawCache = null;
|
|
289
|
+
let textCache = null;
|
|
290
|
+
/**
|
|
291
|
+
* 读 `.mineru.json` 原始文本(缺失 → null)。**不抛异常**:权限等异常也降级成 null,
|
|
292
|
+
* 由调用方决定 reason(读取失败按 bad-artifact 记)。
|
|
293
|
+
*/
|
|
294
|
+
async function readMineruRaw(ref) {
|
|
245
295
|
let key = `${ref.mineruJsonPath}:absent`;
|
|
246
296
|
try {
|
|
247
297
|
const st = await stat(ref.mineruJsonPath);
|
|
248
298
|
key = `${ref.mineruJsonPath}:${st.mtimeMs}:${st.size}`;
|
|
249
|
-
if (
|
|
250
|
-
return
|
|
299
|
+
if (rawCache && rawCache.key === key)
|
|
300
|
+
return { key, raw: rawCache.raw };
|
|
251
301
|
const raw = await readFile(ref.mineruJsonPath, 'utf8');
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return value;
|
|
302
|
+
rawCache = { key, raw };
|
|
303
|
+
return { key, raw };
|
|
255
304
|
}
|
|
256
305
|
catch (err) {
|
|
257
306
|
const code = err.code;
|
|
258
307
|
if (code === 'ENOENT' || code === 'EISDIR') {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
cache = { key, value };
|
|
263
|
-
return value;
|
|
308
|
+
if (!(rawCache && rawCache.key === key))
|
|
309
|
+
rawCache = { key, raw: null };
|
|
310
|
+
return { key, raw: null };
|
|
264
311
|
}
|
|
265
|
-
|
|
266
|
-
return EMPTY('bad-artifact', `读取 mineru.json 失败:${code ?? 'unknown'}`);
|
|
312
|
+
return { key, raw: null, failed: code ?? 'unknown' };
|
|
267
313
|
}
|
|
268
314
|
}
|
|
315
|
+
/** 读 `<ref.mineruJsonPath>` 并建索引。文件缺失/损坏一律返回可用的空索引,**不抛异常**。 */
|
|
316
|
+
export async function readFormulaIndex(ref) {
|
|
317
|
+
const { key, raw, failed } = await readMineruRaw(ref);
|
|
318
|
+
if (failed !== undefined)
|
|
319
|
+
return EMPTY('bad-artifact', `读取 mineru.json 失败:${failed}`);
|
|
320
|
+
if (cache && cache.key === key)
|
|
321
|
+
return cache.value;
|
|
322
|
+
const value = buildFormulaIndex(raw);
|
|
323
|
+
cache = { key, value };
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* 读同一份产物并建 **text 块索引**(行内公式还原用,服务端内部;不经 `/api/formulas`
|
|
328
|
+
* 下发——§7 R14/R15 明确要求响应面不变)。与 `readFormulaIndex` 共用原始文本缓存。
|
|
329
|
+
*/
|
|
330
|
+
export async function readTextBlockIndex(ref) {
|
|
331
|
+
const { key, raw, failed } = await readMineruRaw(ref);
|
|
332
|
+
if (failed !== undefined)
|
|
333
|
+
return { source: 'none', blocks: [], reason: 'bad-artifact' };
|
|
334
|
+
if (textCache && textCache.key === key)
|
|
335
|
+
return textCache.value;
|
|
336
|
+
const value = buildTextBlockIndex(raw);
|
|
337
|
+
textCache = { key, value };
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
269
340
|
/** 仅供测试:清空记忆化。 */
|
|
270
341
|
export function resetFormulaCache() {
|
|
271
342
|
cache = null;
|
|
343
|
+
rawCache = null;
|
|
344
|
+
textCache = null;
|
|
272
345
|
}
|