@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/dist/search.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// search.ts — 长文档分段检索(纯函数,不依赖 Cordis)。
|
|
2
2
|
// 移植 pdfqa 的 chunkText + searchPaper,新增:片段 → 页码映射(经 pages.json 偏移表)。
|
|
3
|
+
// 折叠项 1:查询侧归一化复用**投影侧同一个** token 级归一化函数(normalizeMathBody 的导出别名),
|
|
4
|
+
// 规则逐字符一致,不存在「两套实现各自漂移」的可能。
|
|
5
|
+
import { normalizeMathTokens } from "./mineru.js";
|
|
3
6
|
/** 按空行分段后打包成 ~size 字符的片段(忠实移植 pdfqa chunkText)。 */
|
|
4
7
|
export function chunkText(text, size = 1500) {
|
|
5
8
|
const paras = text.split('\n\n');
|
|
@@ -96,3 +99,52 @@ export function formatHits(hits, totalChunks, query) {
|
|
|
96
99
|
}
|
|
97
100
|
return parts.join('\n');
|
|
98
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* 折叠项 1 —— 查询侧 LaTeX 归一化。
|
|
104
|
+
* 投影侧把 `.txt` 里单 token 的下标/上标去了花括号(`Q_{t}` → `Q_t`),代价是带花括号的查询
|
|
105
|
+
* 命中为 0。这里把同一个规则作用在**查询**上:`Q_{t}` → `Q_t`,于是两种写法命中同一页。
|
|
106
|
+
* 规则一致由代码保证(直接调投影侧同一个 normalizeMathBody);无需归一化时返回 null:
|
|
107
|
+
* 纯散文查询(没有 `_{...}`/`^{...}` 形态)与多 token 参数(`x_{ij}`、`x_{t-1}`)都原样返回。
|
|
108
|
+
*/
|
|
109
|
+
export function compactQuery(query) {
|
|
110
|
+
const compact = normalizeMathTokens(query);
|
|
111
|
+
return compact === query ? null : compact;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 检索实际要用的查询写法:**原样永远保留**,有紧凑写法时追加一条。
|
|
115
|
+
* 保留原样是关键——归一化只增加召回,不会让任何既有命中消失(对 pdfjs 来源的论文同理)。
|
|
116
|
+
*/
|
|
117
|
+
export function queryVariants(query) {
|
|
118
|
+
const compact = compactQuery(query);
|
|
119
|
+
return compact === null ? [query] : [query, compact];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 按论文来源决定查询写法:投影侧的紧凑写法只出现在 **MinerU** 产物里,
|
|
123
|
+
* 所以查询侧的镜像变体也只在 MinerU 来源的论文上追加——pdfjs / 旧缓存论文保持
|
|
124
|
+
* 「原查询原样」(不引入任何新行为,也不丢任何既有命中)。
|
|
125
|
+
*/
|
|
126
|
+
export function queryVariantsFor(producer, query) {
|
|
127
|
+
const mineru = producer === 'mineru-local' || producer === 'mineru-cloud';
|
|
128
|
+
return mineru ? queryVariants(query) : [query];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 多写法关键词检索:逐个写法跑一遍 searchChunks,按 chunk 合并(取最高分),再按原有语义取 top-k
|
|
132
|
+
* 并按原文顺序返回。只有一个写法时**直接走 searchChunks**,与改造前逐字节一致。
|
|
133
|
+
*/
|
|
134
|
+
export function searchChunksMulti(chunks, pages, queries, k = 5) {
|
|
135
|
+
const use = queries.filter((q) => q !== '');
|
|
136
|
+
if (use.length <= 1)
|
|
137
|
+
return searchChunks(chunks, pages, use[0] ?? '', k);
|
|
138
|
+
const merged = new Map();
|
|
139
|
+
for (const q of use) {
|
|
140
|
+
for (const hit of searchChunks(chunks, pages, q, k)) {
|
|
141
|
+
const prev = merged.get(hit.chunk.index);
|
|
142
|
+
if (prev === undefined || hit.score > prev.score)
|
|
143
|
+
merged.set(hit.chunk.index, hit);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return [...merged.values()]
|
|
147
|
+
.sort((a, b) => b.score - a.score)
|
|
148
|
+
.slice(0, Math.max(1, k))
|
|
149
|
+
.sort((a, b) => a.chunk.index - b.chunk.index);
|
|
150
|
+
}
|
package/dist/tools.js
CHANGED
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
3
3
|
import { listPapers, listTopics, resolveDataDir, resolvePaper } from "./library.js";
|
|
4
4
|
import { transcribePaper, readTranscript } from "./transcribe.js";
|
|
5
|
-
import { chunkText,
|
|
5
|
+
import { chunkText, queryVariantsFor, searchChunksMulti, formatHits } from "./search.js";
|
|
6
|
+
import { readTranscriptMeta } from "./transcribe.js";
|
|
7
|
+
import { EMBED_DEFAULTS, embeddingsPathFor, embeddingRecallFor, fuseHybridCandidates } from "./embed.js";
|
|
8
|
+
import { resolveEmbedConfig } from "./embed-config.js";
|
|
6
9
|
import { resolveRerankClient, resolveShortlistSize, rerankHits } from "./rerank.js";
|
|
7
10
|
import { completeStep, formatStudy, readStudy, recordQuiz, setPlan } from "./study.js";
|
|
8
11
|
import { readerOrigin } from "./origin.js";
|
|
12
|
+
import { resolveMineruConfig } from "./mineru-config.js";
|
|
13
|
+
const MINERU_SOURCES = ['auto', 'pdfjs', 'mineru-local', 'mineru-cloud'];
|
|
14
|
+
function sourceArg(v) {
|
|
15
|
+
return MINERU_SOURCES.includes(v) ? v : 'auto';
|
|
16
|
+
}
|
|
9
17
|
const locateParams = {
|
|
10
18
|
path: { type: 'string', description: 'PDF 绝对路径。与 topic+name 二选一。' },
|
|
11
19
|
topic: { type: 'string', description: '专题名(文献库一级目录)。' },
|
|
@@ -59,6 +67,24 @@ const readerUrlSchema = {
|
|
|
59
67
|
};
|
|
60
68
|
/** render 文本尾部统一带出的链接基址行(preset 规定了用法,这里只暴露值)。 */
|
|
61
69
|
const readerUrlLine = (url) => (url ? `\n阅读器链接基址:${url}` : '');
|
|
70
|
+
/** 4 个 source 值的文案(§10.1:不得把 MinerU 结果误报成「本地提取」)。 */
|
|
71
|
+
function sourceLabel(source) {
|
|
72
|
+
switch (source) {
|
|
73
|
+
case 'cache': return '缓存复用';
|
|
74
|
+
case 'local': return '本地提取(pdf.js)';
|
|
75
|
+
case 'mineru-local': return '本地 MinerU 解析';
|
|
76
|
+
case 'mineru-cloud': return '云端 MinerU 解析';
|
|
77
|
+
default: return source;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function producerLabel(producer) {
|
|
81
|
+
switch (producer) {
|
|
82
|
+
case 'pdfjs': return 'pdf.js';
|
|
83
|
+
case 'mineru-local': return '本地 MinerU';
|
|
84
|
+
case 'mineru-cloud': return '云端 MinerU';
|
|
85
|
+
default: return producer;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
62
88
|
export function registerTools(ctx, config) {
|
|
63
89
|
const dataDir = resolveDataDir(config.dataDir);
|
|
64
90
|
const shortlistSize = resolveShortlistSize(config.typesafe);
|
|
@@ -104,11 +130,18 @@ export function registerTools(ctx, config) {
|
|
|
104
130
|
}));
|
|
105
131
|
ctx.tools.register(defineTool({
|
|
106
132
|
name: 'transcribe_pdf',
|
|
107
|
-
description: '把一篇 PDF
|
|
133
|
+
description: '把一篇 PDF 论文转录为纯文本(默认本地 pdf.js 提取,带页码索引),结果落盘缓存。' +
|
|
134
|
+
'可指定 source=mineru-local/mineru-cloud 改走 MinerU 解析后端(本地/云端,适合扫描件)。' +
|
|
108
135
|
'读一篇新论文的第一步:先转录,再用 search_paper 检索。已有缓存时秒回。',
|
|
109
136
|
parameters: {
|
|
110
137
|
...locateParams,
|
|
111
138
|
force: { type: 'boolean', description: '忽略缓存重新转录。' },
|
|
139
|
+
source: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
enum: ['auto', 'pdfjs', 'mineru-local', 'mineru-cloud'],
|
|
142
|
+
description: '解析来源:auto=有缓存复用、无缓存按配置(默认 pdfjs);pdfjs=本地 pdf.js 文本层;' +
|
|
143
|
+
'mineru-local=本地 MinerU API;mineru-cloud=mineru.net v4 云端。',
|
|
144
|
+
},
|
|
112
145
|
},
|
|
113
146
|
output: {
|
|
114
147
|
schema: {
|
|
@@ -119,7 +152,18 @@ export function registerTools(ctx, config) {
|
|
|
119
152
|
chars: { type: 'integer', required: true },
|
|
120
153
|
pageCount: { type: 'integer', required: true },
|
|
121
154
|
hasPageIndex: { type: 'boolean', required: true, description: '是否有页码索引(检索结果能否带页码)。' },
|
|
122
|
-
source: { type: 'string', required: true, enum: ['cache', 'local'] },
|
|
155
|
+
source: { type: 'string', required: true, enum: ['cache', 'local', 'mineru-local', 'mineru-cloud'] },
|
|
156
|
+
producer: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
required: true,
|
|
159
|
+
enum: ['legacy', 'pdfjs', 'mineru-local', 'mineru-cloud'],
|
|
160
|
+
description: '产出这份文本的引擎;legacy=旧缓存(缺来源标记,等价 pdfjs)。',
|
|
161
|
+
},
|
|
162
|
+
backend: {
|
|
163
|
+
oneOf: [{ type: 'string' }, { type: 'null' }],
|
|
164
|
+
required: true,
|
|
165
|
+
description: '实际 MinerU backend(本地)或 modelVersion(云端);非 MinerU 为 null。',
|
|
166
|
+
},
|
|
123
167
|
readerUrl: readerUrlSchema,
|
|
124
168
|
},
|
|
125
169
|
additionalProperties: false,
|
|
@@ -128,7 +172,10 @@ export function registerTools(ctx, config) {
|
|
|
128
172
|
{
|
|
129
173
|
type: 'text',
|
|
130
174
|
text: `已转录《${value.paper}》:${value.chars} 字符,${value.pageCount} 页` +
|
|
131
|
-
`(${value.source
|
|
175
|
+
`(${sourceLabel(value.source)}${value.hasPageIndex ? ',含页码索引' : ',无页码索引'})。` +
|
|
176
|
+
(value.source !== 'cache' && value.producer !== 'pdfjs' && value.producer !== 'legacy'
|
|
177
|
+
? ` 引擎:${producerLabel(value.producer)}${value.backend ? `(${value.backend})` : ''}。`
|
|
178
|
+
: '') +
|
|
132
179
|
`接下来可用 search_paper 检索具体内容。` +
|
|
133
180
|
readerUrlLine(value.readerUrl),
|
|
134
181
|
},
|
|
@@ -136,7 +183,8 @@ export function registerTools(ctx, config) {
|
|
|
136
183
|
},
|
|
137
184
|
execute: async (args, exec) => {
|
|
138
185
|
const ref = locate(dataDir, args, exec);
|
|
139
|
-
const
|
|
186
|
+
const mineru = await resolveMineruConfig(config.mineru);
|
|
187
|
+
const t = await transcribePaper(ref, { force: args.force === true, source: sourceArg(args.source), mineru });
|
|
140
188
|
return {
|
|
141
189
|
paper: `${ref.topic}/${ref.name}`,
|
|
142
190
|
txtPath: ref.txtPath,
|
|
@@ -144,6 +192,8 @@ export function registerTools(ctx, config) {
|
|
|
144
192
|
pageCount: t.pageCount,
|
|
145
193
|
hasPageIndex: t.pages.length > 0,
|
|
146
194
|
source: t.source,
|
|
195
|
+
producer: t.producer,
|
|
196
|
+
backend: t.backend,
|
|
147
197
|
readerUrl: readerUrl(ref),
|
|
148
198
|
};
|
|
149
199
|
},
|
|
@@ -153,7 +203,8 @@ export function registerTools(ctx, config) {
|
|
|
153
203
|
description: '在论文全文中按关键词检索相关片段,返回带页码的原文片段。' +
|
|
154
204
|
'当需要论文的具体内容(定义、算法步骤、章节细节、数据)时使用,基于检索到的原文回答并注明页码。' +
|
|
155
205
|
'关键词建议:用论文中的英文术语、章节号(如 "Section 3.1")或概念名,不要用完整长句;找不到时换同义词或更短的词重试。' +
|
|
156
|
-
'论文未转录时会自动先转录。'
|
|
206
|
+
'论文未转录时会自动先转录。' +
|
|
207
|
+
'(配置了嵌入端点时额外做语义召回;未配置则纯关键词,与改造前一致)',
|
|
157
208
|
parameters: {
|
|
158
209
|
query: { type: 'string', required: true, description: '检索关键词,如 "backup task" 或 "locality"。' },
|
|
159
210
|
k: { type: 'integer', description: '返回片段数量,默认 5,最多 8。' },
|
|
@@ -171,6 +222,23 @@ export function registerTools(ctx, config) {
|
|
|
171
222
|
required: true,
|
|
172
223
|
description: '返回片段是否经过语义重排(配置了 Jev/TypeSafe 凭据且候选充足时为 true)。',
|
|
173
224
|
},
|
|
225
|
+
compactQuery: {
|
|
226
|
+
oneOf: [{ type: 'string' }, { type: 'null' }],
|
|
227
|
+
required: true,
|
|
228
|
+
description: '查询侧 LaTeX 紧凑写法(MinerU 来源论文才会一并匹配,如 Q_{t} → Q_t);无需归一化时为 null。',
|
|
229
|
+
},
|
|
230
|
+
embedding: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
required: true,
|
|
233
|
+
description: '嵌入语义召回的状态:未配置时 configured=false 且不影响结果;降级时 degraded 给出原因。',
|
|
234
|
+
properties: {
|
|
235
|
+
configured: { type: 'boolean', required: true },
|
|
236
|
+
used: { type: 'boolean', required: true },
|
|
237
|
+
fromCache: { type: 'boolean', required: true },
|
|
238
|
+
degraded: { oneOf: [{ type: 'string' }, { type: 'null' }], required: true },
|
|
239
|
+
},
|
|
240
|
+
additionalProperties: false,
|
|
241
|
+
},
|
|
174
242
|
hits: {
|
|
175
243
|
type: 'array',
|
|
176
244
|
required: true,
|
|
@@ -192,32 +260,84 @@ export function registerTools(ctx, config) {
|
|
|
192
260
|
},
|
|
193
261
|
additionalProperties: false,
|
|
194
262
|
},
|
|
195
|
-
render: (_args, value) =>
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
263
|
+
render: (_args, value) => {
|
|
264
|
+
const notes = [];
|
|
265
|
+
if (value.compactQuery) {
|
|
266
|
+
notes.push(`(查询已按紧凑写法 \`${value.compactQuery}\` 一并匹配;原查询保留)`);
|
|
267
|
+
}
|
|
268
|
+
if (value.embedding.degraded) {
|
|
269
|
+
notes.push(`(嵌入检索不可用,已降级为纯关键词:${value.embedding.degraded})`);
|
|
270
|
+
}
|
|
271
|
+
else if (value.embedding.configured && value.embedding.used) {
|
|
272
|
+
notes.push(value.embedding.fromCache ? '(含嵌入语义召回;分块向量命中缓存)' : '(含嵌入语义召回)');
|
|
273
|
+
}
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
type: 'text',
|
|
277
|
+
text: formatHits(value.hits.map((h) => ({
|
|
278
|
+
chunk: { index: h.chunk, start: 0, end: 0, text: h.text },
|
|
279
|
+
score: 0,
|
|
280
|
+
page: h.page,
|
|
281
|
+
})), value.totalChunks, value.query) +
|
|
282
|
+
(notes.length ? '\n' + notes.join('\n') : '') +
|
|
283
|
+
readerUrlLine(value.readerUrl),
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
},
|
|
205
287
|
},
|
|
206
288
|
execute: async (args, exec) => {
|
|
207
289
|
const ref = locate(dataDir, args, exec);
|
|
208
290
|
let cached = await readTranscript(ref);
|
|
209
291
|
if (!cached) {
|
|
210
|
-
await
|
|
292
|
+
const mineru = await resolveMineruConfig(config.mineru);
|
|
293
|
+
await transcribePaper(ref, { source: 'auto', mineru });
|
|
211
294
|
cached = await readTranscript(ref);
|
|
212
295
|
}
|
|
213
296
|
if (!cached)
|
|
214
297
|
throw new Error('转录失败,无法检索');
|
|
215
298
|
const chunks = chunkText(cached.text, 1500);
|
|
216
299
|
const k = Math.max(1, Math.min(args.k ?? 5, 8));
|
|
217
|
-
//
|
|
218
|
-
//
|
|
300
|
+
// 折叠项 1(查询侧 LaTeX 归一化):投影侧只对 MinerU 产物做紧凑写法归一化,
|
|
301
|
+
// 因此查询侧的同名变体也**只对 MinerU 来源的论文**追加;原查询永远保留 →
|
|
302
|
+
// pdfjs/旧缓存论文既不丢命中也不引入新行为(与改造前逐字节一致)。
|
|
303
|
+
const producer = (await readTranscriptMeta(ref))?.producer ?? null;
|
|
304
|
+
const variants = queryVariantsFor(producer, args.query);
|
|
305
|
+
const compact = variants.length > 1 ? variants[1] : null;
|
|
306
|
+
// 关键词快搜捞 shortlist(宽召回)→ 可选语义召回 → 既有重排 → top-k。
|
|
307
|
+
// 凭据每次现查(设置面板保存即生效);未配置时三段都退回纯关键词,行为与改造前一致。
|
|
219
308
|
const rerankClient = await resolveRerankClient(config.typesafe);
|
|
220
|
-
|
|
309
|
+
const embedCfg = await resolveEmbedConfig(config.embed);
|
|
310
|
+
const wideRecall = Boolean(rerankClient) || embedCfg.enabled;
|
|
311
|
+
const recallSize = wideRecall ? Math.max(k, shortlistSize) : k;
|
|
312
|
+
let hits = searchChunksMulti(chunks, cached.pages, variants, recallSize);
|
|
313
|
+
// 嵌入召回:惰性(首次检索该论文才算)+ 缓存(<论文>.embeddings.json);失败只影响本次召回。
|
|
314
|
+
const embedding = await embeddingRecallFor({
|
|
315
|
+
configured: embedCfg.enabled,
|
|
316
|
+
cachePath: embeddingsPathFor(ref.txtPath),
|
|
317
|
+
chunks,
|
|
318
|
+
pages: cached.pages,
|
|
319
|
+
query: args.query,
|
|
320
|
+
chunkSize: 1500,
|
|
321
|
+
limit: embedCfg.cfg.candidateSize ?? EMBED_DEFAULTS.candidateSize,
|
|
322
|
+
...(embedCfg.enabled
|
|
323
|
+
? {
|
|
324
|
+
req: {
|
|
325
|
+
baseUrl: embedCfg.cfg.baseUrl,
|
|
326
|
+
apiKey: embedCfg.cfg.apiKey,
|
|
327
|
+
model: embedCfg.cfg.model,
|
|
328
|
+
...(embedCfg.cfg.timeoutMs !== undefined ? { timeoutMs: embedCfg.cfg.timeoutMs } : {}),
|
|
329
|
+
},
|
|
330
|
+
...(embedCfg.cfg.batchSize !== undefined ? { batchSize: embedCfg.cfg.batchSize } : {}),
|
|
331
|
+
}
|
|
332
|
+
: {}),
|
|
333
|
+
});
|
|
334
|
+
if (embedding.hits.length > 0)
|
|
335
|
+
hits = fuseHybridCandidates(hits, embedding.hits, recallSize);
|
|
336
|
+
if (!wideRecall)
|
|
337
|
+
hits = hits.slice(0, k);
|
|
338
|
+
if (embedding.degraded) {
|
|
339
|
+
console.warn('[dsh-paper-reader] 嵌入检索降级为纯关键词:', embedding.degraded);
|
|
340
|
+
}
|
|
221
341
|
let reranked = false;
|
|
222
342
|
if (rerankClient && hits.length > k) {
|
|
223
343
|
try {
|
|
@@ -225,17 +345,26 @@ export function registerTools(ctx, config) {
|
|
|
225
345
|
reranked = true;
|
|
226
346
|
}
|
|
227
347
|
catch (e) {
|
|
228
|
-
// 超预算/网络/鉴权任何失败都退回关键词 top-k
|
|
348
|
+
// 超预算/网络/鉴权任何失败都退回关键词 top-k(结果已算好,零成本降级)
|
|
229
349
|
console.warn('[dsh-paper-reader] 检索重排失败,退回关键词排序:', e);
|
|
230
350
|
hits = hits.slice(0, k);
|
|
231
351
|
}
|
|
232
352
|
}
|
|
353
|
+
if (!reranked)
|
|
354
|
+
hits = hits.slice(0, k);
|
|
233
355
|
return {
|
|
234
356
|
paper: `${ref.topic}/${ref.name}`,
|
|
235
357
|
query: args.query,
|
|
236
358
|
totalChunks: chunks.length,
|
|
237
359
|
hits: hits.map((h) => ({ chunk: h.chunk.index, page: h.page, text: h.chunk.text })),
|
|
238
360
|
reranked,
|
|
361
|
+
compactQuery: compact,
|
|
362
|
+
embedding: {
|
|
363
|
+
configured: embedding.configured,
|
|
364
|
+
used: embedding.used,
|
|
365
|
+
fromCache: embedding.fromCache,
|
|
366
|
+
degraded: embedding.degraded,
|
|
367
|
+
},
|
|
239
368
|
readerUrl: readerUrl(ref),
|
|
240
369
|
};
|
|
241
370
|
},
|