@ggboy123/dsh-paper-reader 1.3.4 → 1.4.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/LICENSE +21 -0
- package/README.md +221 -21
- package/dist/embed-config.js +116 -0
- package/dist/embed.js +349 -0
- package/dist/formulas.js +272 -0
- package/dist/host.js +733 -110
- package/dist/library-manage.js +752 -0
- package/dist/library.js +187 -69
- package/dist/mineru-config.js +343 -0
- package/dist/mineru.js +1168 -0
- package/dist/search.js +52 -0
- package/dist/tools.js +150 -21
- package/dist/transcribe.js +316 -39
- package/dist/translate-config.js +8 -2
- package/lib/client.js +479 -60
- package/package.json +3 -1
- package/reader/index.html +417 -24
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GGboya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
[DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness) 插件:论文伴读工作台 —— PDF 转录 / 检索 + 原生对话伴读 + 内置阅读器。
|
|
11
11
|
|
|
12
|
-
> **👉 普通用户直接看「安装 →
|
|
12
|
+
> **👉 普通用户直接看「安装 → 普通用户:桌面端装好即用」**:装 DeepSeek 官方桌面端,添加插件即可,不需要任何开发环境。本仓库面向的是想自己装插件、或想改代码的用户。
|
|
13
|
+
|
|
14
|
+
> 💬 **QQ 交流群:1127244508** —— 安装/使用问题、功能建议、bug 反馈都欢迎来聊。
|
|
13
15
|
|
|
14
16
|
功能迁移自本地独立产品 pdfqa(Go 实现的论文伴读工具)。原则:agent 循环 / 模型层 / 会话持久化全部交给 dsh 框架,插件只做「转录 + 检索 + 阅读器 UI」的薄壳组合。
|
|
15
17
|
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
|
|
24
26
|
## 功能
|
|
25
27
|
|
|
26
|
-
- 📚 **侧栏文献库树**:「论文伴读」模式开启时接管工作区侧栏,专题 → 论文两级;支持新建专题、上传 PDF、**专题/论文改名与删除**(行悬停出 ✎
|
|
28
|
+
- 📚 **侧栏文献库树**:「论文伴读」模式开启时接管工作区侧栏,专题 → 论文两级;支持新建专题、上传 PDF、**专题/论文改名与删除**(行悬停出 ✎ 🗑;删除先出「服务端删除清单 + 确认框」、专题还要手输专题名;产物移入回收站可恢复;改名连带转录缓存 / 译文 / 学习档案一起搬家)(默认关闭,不覆盖官方界面)
|
|
27
29
|
- 💬 **原生对话伴读**:点击论文即在右侧打开它的 dsh 原生会话(流式 / 工具卡 / 用量条全是官方 UI);默认只呈现当前对话,对话列头部的 🕐 下拉回看**历史对话**(真实标题+日期,点哪条进哪条),+ 新建对话
|
|
28
30
|
- 🔗 **会话绑定论文**:会话 id 编码文献身份,工具调用自动定位当前论文——直接在输入框提问无需点名论文,回答必带页码
|
|
29
31
|
- 🤖 **专用 agent preset「论文伴读」**:paper 会话自动使用专用 preset —— 像老师一样带读:制定分步阅读计划(每步带页码和思考题)→ 逐节指导、点评 → 出题检验、逐题批改 → 成绩与薄弱点记入 `<文献>.study.json` 学习档案,跨会话累积;快速问答(选中即问)则直接回答,不上课
|
|
@@ -32,19 +34,212 @@
|
|
|
32
34
|
- 📍 **引用定位**:回答里的「第 N 页」可点击,平滑跳回 PDF 对应页并闪烁
|
|
33
35
|
- 🀄 **中英切换 + 导出**:顶栏「中」按钮原文 ↔ 纯中文切换;无译文时一键后台生成(babeldoc),生成需配置 `translate` 端点;**无需预装 Python**——首次生成时自动下载 uv + 托管 Python + babeldoc(macOS / Linux / Windows,约几分钟),全程落在用户目录;顶栏 ⤓ 一键**下载当前版本**(原文 / 纯中文 / 中英对照 PDF)
|
|
34
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 分钟)**:
|
|
109
|
+
|
|
110
|
+
1. 到 [mineru.net](https://mineru.net) 注册账号;
|
|
111
|
+
2. 在站内「API 管理」里创建一个 token 并复制;
|
|
112
|
+
3. 打开 dsh **设置 → 论文伴读 → MinerU 解析后端**:模式选「云端」,把 token 粘进「云端 token」,点「测试连接」→ 保存。
|
|
113
|
+
|
|
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);读取接口只回掩码,日志 / 错误 / 响应都不出现明文。
|
|
35
125
|
|
|
36
|
-
>
|
|
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 端点。
|
|
37
232
|
|
|
38
233
|
## 安装
|
|
39
234
|
|
|
40
|
-
###
|
|
235
|
+
### 普通用户:桌面端装好即用(推荐)
|
|
41
236
|
|
|
42
|
-
|
|
237
|
+
**两步**:装 DeepSeek 官方桌面端 → 里面直接装插件。
|
|
43
238
|
|
|
44
|
-
1.
|
|
45
|
-
2. 侧栏 → **插件** → **添加插件**,输入 `@ggboy123/dsh-paper-reader@
|
|
46
|
-
|
|
47
|
-
|
|
239
|
+
1. 下载安装 [DeepSeek 官方桌面端](https://www.deepseek.com/download/)(即 DeepSeek Harness 桌面版),登录 DeepSeek 账号
|
|
240
|
+
2. 侧栏 → **插件** → **添加插件**,输入 `@ggboy123/dsh-paper-reader@latest`,装完点进插件详情打开「启用」开关,**重启桌面端**
|
|
241
|
+
|
|
242
|
+
完成。侧栏底部点「📚 论文伴读」进入文献库,上传 PDF 开始用。
|
|
48
243
|
|
|
49
244
|
完整流程演示(添加插件 → 启用 → 重启 → 打开论文直接提问):
|
|
50
245
|
|
|
@@ -53,35 +248,38 @@
|
|
|
53
248
|
> 已在官方桌面端 V0.2.0-rc.2(内置 dsh 0.2.0-rc.2,macOS arm64)实测通过:侧栏文献库、新建专题、上传 PDF、PDF 阅读器、原生对话伴读、历史/新建对话按钮全部可用。
|
|
54
249
|
> 需要 ≥1.2.0:更早版本会被 0.2.x 运行时的 peer 兼容校验拒载(插件列表里显示异常)。
|
|
55
250
|
|
|
56
|
-
>
|
|
251
|
+
> 想直接在客户端里逛插件市场(4000+ 插件、分类、评论、一键安装),可另装社区插件 [`dshmarket`](https://github.com/dsh-market/dsh-market),本插件已在其目录中。
|
|
252
|
+
|
|
253
|
+
**排坑(都是实测踩过的)**:
|
|
57
254
|
|
|
58
|
-
|
|
255
|
+
- **升级后插件开关会回到停用**:侧栏 → 插件 → 打开「启用」→ 重启。症状有迷惑性——文献树还在但点论文没反应(浏览器侧缓存活着、宿主侧已下线),其实是没启用
|
|
256
|
+
- **添加时务必带 `@latest`**:不带版本号对已装过的插件是 no-op(dsh 的插件管理器按首次记录的版本处理,不会追新)
|
|
257
|
+
- 旧版 PaperReader 桌面版(社区 DSH Desktop 预装打包)**已停止维护**,请迁移到本方式
|
|
258
|
+
|
|
259
|
+
### 开发者:命令行 dsh
|
|
59
260
|
|
|
60
261
|
```bash
|
|
61
|
-
dsh plugin --profile web add @ggboy123/dsh-paper-reader
|
|
262
|
+
dsh plugin --profile web add @ggboy123/dsh-paper-reader@latest
|
|
62
263
|
```
|
|
63
264
|
|
|
64
265
|
> npm 包名走 `@ggboy123` 作用域:裸名 `dsh-paper-reader` 已被另一个插件占用(那是「输入文献、吐精读报告」的一次性分析工具,与本插件的「阅读器 + 会话伴读」定位不同)。
|
|
65
266
|
> 不要用 `github:GGboya/dsh-paper-reader` 安装 —— dist 不入库,git 安装会导致 profile 起不来(构建产物只随 npm 包分发)。
|
|
66
267
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
**已装过的用户升级必须带显式版本号**——不带版本的 `add` 对已存在的依赖是 no-op(pnpm 按首次安装时记录的版本范围解析,不会追新):
|
|
268
|
+
**升级**:用 `@latest`(或显式版本号)——不带任何版本/tag 的 `add` 对已装过的依赖是 no-op(实测):
|
|
70
269
|
|
|
71
270
|
```bash
|
|
72
|
-
dsh plugin --profile web add @ggboy123/dsh-paper-reader@
|
|
271
|
+
dsh plugin --profile web add @ggboy123/dsh-paper-reader@latest # 永远装最新
|
|
272
|
+
# dsh plugin --profile web add @ggboy123/dsh-paper-reader@1.4.0 # 或钉住具体版本
|
|
73
273
|
# 重启 dsh web 生效;浏览器 Cmd+Shift+R 强刷,避免旧阅读器页面缓存
|
|
74
274
|
```
|
|
75
275
|
|
|
76
|
-
> **桌面端升级后插件开关会回到停用**(实测):侧栏 → 插件 → 打开 `@ggboy123/dsh-paper-reader` 的「启用」开关 → 重启桌面端。不打开时的症状很有迷惑性:文献树还在(浏览器侧缓存活着)但点论文没反应(宿主侧已下线),看起来像坏了,其实只是没启用。
|
|
77
|
-
|
|
78
276
|
确认当前装的是哪个版本:
|
|
79
277
|
|
|
80
278
|
```bash
|
|
81
279
|
grep '"version"' ~/.dsh/profiles/web/node_modules/@ggboy123/dsh-paper-reader/package.json
|
|
82
280
|
```
|
|
83
281
|
|
|
84
|
-
> pnpm 若开了供应链策略 `minimumReleaseAge`(发布 N
|
|
282
|
+
> pnpm 若开了供应链策略 `minimumReleaseAge`(发布 N 小时内的新包拒装,桌面端默认开):要么等过窗口期,要么在 `~/.dsh/profiles/web/.npmrc` 加一行 `minimum-release-age-exclude[]=@ggboy123/dsh-paper-reader`(只豁免本插件,不动全局策略)。桌面端的豁免清单在 `~/.dsh/profiles/desktop/pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude`。
|
|
85
283
|
|
|
86
284
|
### 参与开发(本地调试)
|
|
87
285
|
|
|
@@ -141,8 +339,10 @@ src/
|
|
|
141
339
|
tools.ts 5 个 agent 工具:list_papers / transcribe_pdf / search_paper / study_progress / study_update
|
|
142
340
|
host.ts webServer 路由(/paper-reader/*),connection.requestRejection 鉴权;
|
|
143
341
|
sessionController.create/prompt(agentPreset=paper-reader)+ follow SSE 桥
|
|
144
|
-
library.ts
|
|
145
|
-
transcribe.ts 纯函数:pdf.js 提取 +
|
|
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)+ 掩码 + 连通性预检
|
|
146
346
|
search.ts 纯函数:分段 + 关键词打分 + 页码映射
|
|
147
347
|
study.ts 纯函数:学习档案(计划 + 检验成绩)读写
|
|
148
348
|
translate.ts 纯函数:babeldoc 调用(中文/中英对照 PDF 生成)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// embed-config.ts — OpenAI 兼容嵌入端点配置的落盘与解析(纯函数,不依赖 Cordis)。
|
|
2
|
+
//
|
|
3
|
+
// 与 translate-config.ts / typesafe-config.ts **同一套约定**:存
|
|
4
|
+
// $DSH_HOME/.dsh-paper-reader/embed.json(0600,刻意不放 dataDir——密钥不跟文献库/云盘走);
|
|
5
|
+
// 密钥只落盘、只回显掩码(apiKeyHint)。优先级(逐字段):
|
|
6
|
+
// 文件(设置面板填的)> profile 的 cordis.patch.yml config.embed > 环境变量 DSH_EMBED_API_KEY。
|
|
7
|
+
// 未配到 baseUrl+model 时嵌入功能整体关闭(默认关闭,零行为变化)。
|
|
8
|
+
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { dshHome } from "./library.js";
|
|
11
|
+
import { maskApiKey } from "./translate-config.js";
|
|
12
|
+
import { EmbedError, requestEmbeddings } from "./embed.js";
|
|
13
|
+
export { maskApiKey };
|
|
14
|
+
export function embedConfigPath(home = dshHome()) {
|
|
15
|
+
return join(home, '.dsh-paper-reader', 'embed.json');
|
|
16
|
+
}
|
|
17
|
+
/** 读配置;文件缺失/损坏/形状不对一律返回 null(与 translate/typesafe 的读策略一致)。 */
|
|
18
|
+
export async function readEmbedConfig(home = dshHome()) {
|
|
19
|
+
try {
|
|
20
|
+
const data = JSON.parse(await readFile(embedConfigPath(home), 'utf8'));
|
|
21
|
+
if (typeof data !== 'object' || data === null)
|
|
22
|
+
return null;
|
|
23
|
+
const rec = data;
|
|
24
|
+
const out = {};
|
|
25
|
+
if (typeof rec['baseUrl'] === 'string')
|
|
26
|
+
out.baseUrl = rec['baseUrl'];
|
|
27
|
+
if (typeof rec['apiKey'] === 'string')
|
|
28
|
+
out.apiKey = rec['apiKey'];
|
|
29
|
+
if (typeof rec['model'] === 'string')
|
|
30
|
+
out.model = rec['model'];
|
|
31
|
+
if (Number.isInteger(rec['timeoutMs']))
|
|
32
|
+
out.timeoutMs = rec['timeoutMs'];
|
|
33
|
+
if (Number.isInteger(rec['batchSize']))
|
|
34
|
+
out.batchSize = rec['batchSize'];
|
|
35
|
+
if (Number.isInteger(rec['candidateSize']))
|
|
36
|
+
out.candidateSize = rec['candidateSize'];
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** 写配置。0600:文件含明文密钥,不靠 umask。 */
|
|
44
|
+
export async function writeEmbedConfig(cfg, home = dshHome()) {
|
|
45
|
+
const file = embedConfigPath(home);
|
|
46
|
+
await mkdir(dirname(file), { recursive: true });
|
|
47
|
+
await writeFile(file, JSON.stringify(cfg, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
48
|
+
await chmod(file, 0o600); // mode 只在新建时生效;已存在的文件要显式收紧
|
|
49
|
+
}
|
|
50
|
+
/** 删除配置(回落到 profile 的 embed / 环境变量)。文件不存在也算成功。 */
|
|
51
|
+
export async function clearEmbedConfig(home = dshHome()) {
|
|
52
|
+
await rm(embedConfigPath(home), { force: true });
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 解析生效配置:按字段 file > profile > env(只 env 有 key 时 source=env)。
|
|
56
|
+
* enabled 要求 baseUrl + model + apiKey 三者齐全——缺任一项都退回「嵌入关闭」。
|
|
57
|
+
*/
|
|
58
|
+
export async function resolveEmbedConfig(profile, home = dshHome()) {
|
|
59
|
+
const file = await readEmbedConfig(home);
|
|
60
|
+
const envKey = process.env['DSH_EMBED_API_KEY']?.trim() || undefined;
|
|
61
|
+
const cfg = {};
|
|
62
|
+
const apiKey = file?.apiKey || profile?.apiKey || envKey;
|
|
63
|
+
const baseUrl = file?.baseUrl || profile?.baseUrl;
|
|
64
|
+
const model = file?.model || profile?.model;
|
|
65
|
+
const timeoutMs = file?.timeoutMs ?? profile?.timeoutMs;
|
|
66
|
+
const batchSize = file?.batchSize ?? profile?.batchSize;
|
|
67
|
+
const candidateSize = file?.candidateSize ?? profile?.candidateSize;
|
|
68
|
+
if (apiKey)
|
|
69
|
+
cfg.apiKey = apiKey;
|
|
70
|
+
if (baseUrl)
|
|
71
|
+
cfg.baseUrl = baseUrl;
|
|
72
|
+
if (model)
|
|
73
|
+
cfg.model = model;
|
|
74
|
+
if (timeoutMs !== undefined)
|
|
75
|
+
cfg.timeoutMs = timeoutMs;
|
|
76
|
+
if (batchSize !== undefined)
|
|
77
|
+
cfg.batchSize = batchSize;
|
|
78
|
+
if (candidateSize !== undefined)
|
|
79
|
+
cfg.candidateSize = candidateSize;
|
|
80
|
+
const source = file?.apiKey
|
|
81
|
+
? 'file'
|
|
82
|
+
: profile?.apiKey
|
|
83
|
+
? 'profile'
|
|
84
|
+
: envKey
|
|
85
|
+
? 'env'
|
|
86
|
+
: 'none';
|
|
87
|
+
return { cfg, source, enabled: Boolean(cfg.baseUrl && cfg.model && cfg.apiKey) };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 连接预检:拿一个极小的输入打一发 /embeddings,确认 url/key/model 三者匹配且返回维度正常。
|
|
91
|
+
* 必要性同翻译/重排:嵌入失败是**静默降级**,key 或模型名填错用户很难察觉,落盘前先拦。
|
|
92
|
+
*/
|
|
93
|
+
export async function testEmbedEndpoint(cfg) {
|
|
94
|
+
if (!cfg.baseUrl?.trim())
|
|
95
|
+
return { ok: false, detail: '端点 URL 不能为空' };
|
|
96
|
+
if (!cfg.apiKey?.trim())
|
|
97
|
+
return { ok: false, detail: 'API Key 不能为空' };
|
|
98
|
+
if (!cfg.model?.trim())
|
|
99
|
+
return { ok: false, detail: '嵌入模型名不能为空' };
|
|
100
|
+
try {
|
|
101
|
+
const [vec] = await requestEmbeddings({
|
|
102
|
+
baseUrl: cfg.baseUrl.trim(),
|
|
103
|
+
apiKey: cfg.apiKey.trim(),
|
|
104
|
+
model: cfg.model.trim(),
|
|
105
|
+
timeoutMs: cfg.timeoutMs ?? 30_000,
|
|
106
|
+
}, ['connectivity check']);
|
|
107
|
+
if (!vec || vec.length === 0)
|
|
108
|
+
return { ok: false, detail: '端点返回了空向量' };
|
|
109
|
+
return { ok: true, detail: `维度 ${vec.length}` };
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (err instanceof EmbedError)
|
|
113
|
+
return { ok: false, detail: err.message };
|
|
114
|
+
return { ok: false, detail: err instanceof Error ? err.message : String(err) };
|
|
115
|
+
}
|
|
116
|
+
}
|