@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/transcribe.js
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
// transcribe.ts — PDF 转录(纯函数,不依赖 Cordis)。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// 1. 缓存:.txt
|
|
3
|
+
// 三级策略:
|
|
4
|
+
// 1. 缓存:.txt 已存在直接复用(来源由 .transcript.json 标记;缺失 = legacy,等价 pdfjs)
|
|
5
5
|
// 2. 快路径:pdfjs-dist(pdf.js)本地提取文本层(文本型 PDF 毫秒级,纯 Node,无需 Python)
|
|
6
|
-
// 3.
|
|
6
|
+
// 3. MinerU 后端:本地 legacy API / mineru.net v4 云端(扫描件/OCR 兜底,见 mineru.ts)
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
// 产出仍然是同一套 .txt + .pages.json(page 从 1 起),另加 .transcript.json(来源标记)
|
|
9
|
+
// 与 MinerU 富产物 .mineru.md/.mineru.json(Markdown + content_list,供人工核对)。
|
|
10
|
+
import { readFile, writeFile, rename, rm } from 'node:fs/promises';
|
|
11
|
+
import { statSync } from 'node:fs';
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { parseCloudMineru, parseLocalMineru, MineruError } from "./mineru.js";
|
|
14
|
+
import { MINERU_DEFAULTS } from "./mineru-config.js";
|
|
15
|
+
/** 扫描件/文本过短信号:文本层过少,上层据此决定 MinerU 兜底。 */
|
|
16
|
+
export class ShortTextError extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
// 显式设 name:上层可据此区分(否则 err.name 只是 'Error',见验证报告 O3)
|
|
20
|
+
this.name = 'ShortTextError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** 扫描件信号(pdfjs 提取内部使用)。 */
|
|
24
|
+
export class ScannedPdfError extends Error {
|
|
25
|
+
}
|
|
26
|
+
function isRecord(v) {
|
|
27
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
19
28
|
}
|
|
20
29
|
// ---- 以下为 PyMuPDF 版清洗规则的逐条 TS 移植 ----
|
|
21
30
|
const LIGATURES = [
|
|
@@ -120,29 +129,137 @@ async function extractWithPdfjs(pdfPath) {
|
|
|
120
129
|
await loadingTask.destroy();
|
|
121
130
|
}
|
|
122
131
|
}
|
|
132
|
+
/** 来源入参 → 目标 producer;auto 按配置 mode 决定。 */
|
|
133
|
+
function producerForSource(source) {
|
|
134
|
+
if (source === 'pdfjs')
|
|
135
|
+
return 'pdfjs';
|
|
136
|
+
if (source === 'mineru-local')
|
|
137
|
+
return 'mineru-local';
|
|
138
|
+
if (source === 'mineru-cloud')
|
|
139
|
+
return 'mineru-cloud';
|
|
140
|
+
throw new Error(`内部错误:未知 source ${source}`);
|
|
141
|
+
}
|
|
142
|
+
function producerForMode(mode) {
|
|
143
|
+
if (mode === 'local')
|
|
144
|
+
return 'mineru-local';
|
|
145
|
+
if (mode === 'cloud')
|
|
146
|
+
return 'mineru-cloud';
|
|
147
|
+
return 'pdfjs';
|
|
148
|
+
}
|
|
149
|
+
/** 缓存命中判定:source='pdfjs' 命中 legacy/pdfjs;显式 mineru-* 只命中同源。 */
|
|
150
|
+
function producerMatches(requested, actual) {
|
|
151
|
+
if (requested === 'pdfjs')
|
|
152
|
+
return actual === 'pdfjs' || actual === 'legacy';
|
|
153
|
+
return requested === actual;
|
|
154
|
+
}
|
|
155
|
+
/** 读 .transcript.json 的来源标记;缺失/损坏/形状不对返回 null(归类 legacy)。 */
|
|
156
|
+
export async function readTranscriptMeta(ref) {
|
|
157
|
+
try {
|
|
158
|
+
const raw = JSON.parse(await readFile(ref.transcriptPath, 'utf8'));
|
|
159
|
+
if (!isRecord(raw))
|
|
160
|
+
return null;
|
|
161
|
+
const producer = raw['producer'];
|
|
162
|
+
if (producer !== 'pdfjs' && producer !== 'mineru-local' && producer !== 'mineru-cloud')
|
|
163
|
+
return null;
|
|
164
|
+
const engine = isRecord(raw['engine']) ? raw['engine'] : null;
|
|
165
|
+
let backend = null;
|
|
166
|
+
if (engine) {
|
|
167
|
+
if (typeof engine['backend'] === 'string')
|
|
168
|
+
backend = engine['backend'];
|
|
169
|
+
else if (typeof engine['modelVersion'] === 'string')
|
|
170
|
+
backend = engine['modelVersion'];
|
|
171
|
+
}
|
|
172
|
+
return { producer, backend };
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** 读现有缓存 + 来源标记;txt 缺失返回 null。 */
|
|
179
|
+
async function readCached(ref) {
|
|
180
|
+
let text;
|
|
181
|
+
try {
|
|
182
|
+
text = await readFile(ref.txtPath, 'utf8');
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const pages = await readPages(ref);
|
|
188
|
+
let producer = 'legacy';
|
|
189
|
+
let backend = null;
|
|
190
|
+
let corrupted = false;
|
|
191
|
+
try {
|
|
192
|
+
const raw = JSON.parse(await readFile(ref.transcriptPath, 'utf8'));
|
|
193
|
+
if (isRecord(raw)) {
|
|
194
|
+
const p = raw['producer'];
|
|
195
|
+
if (p === 'pdfjs' || p === 'mineru-local' || p === 'mineru-cloud')
|
|
196
|
+
producer = p;
|
|
197
|
+
const engine = isRecord(raw['engine']) ? raw['engine'] : null;
|
|
198
|
+
if (engine) {
|
|
199
|
+
if (typeof engine['backend'] === 'string')
|
|
200
|
+
backend = engine['backend'];
|
|
201
|
+
else if (typeof engine['modelVersion'] === 'string')
|
|
202
|
+
backend = engine['modelVersion'];
|
|
203
|
+
}
|
|
204
|
+
const tb = isRecord(raw['text']) ? raw['text']['bytes'] : undefined;
|
|
205
|
+
if (typeof tb === 'number' && tb !== Buffer.byteLength(text, 'utf8'))
|
|
206
|
+
corrupted = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch { /* transcript.json 缺失/损坏 → legacy,不判损坏 */ }
|
|
210
|
+
return { text, pages, producer, backend, corrupted };
|
|
211
|
+
}
|
|
123
212
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
213
|
+
* 转录一篇文献:缓存优先(来源语义见 §8),否则按 source/mode 选引擎解析并落盘。
|
|
214
|
+
* @param opts.source 入参来源(默认 auto);auto 命中缓存即复用,无缓存按配置 mode。
|
|
215
|
+
* @param opts.mineru 已解析的 MinerU 配置(缺省 mode=off,行为与改造前一致)。
|
|
126
216
|
*/
|
|
127
217
|
export async function transcribePaper(ref, opts = {}) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
218
|
+
const source = opts.source ?? 'auto';
|
|
219
|
+
const mineru = opts.mineru ?? { config: MINERU_DEFAULTS, source: 'none' };
|
|
220
|
+
const force = opts.force === true;
|
|
221
|
+
// auto 无缓存时按配置 mode 决定目标引擎;显式 source 优先于 mode(mode=off 不阻止显式来源)
|
|
222
|
+
const targetProducer = source === 'auto' ? producerForMode(mineru.config.mode) : producerForSource(source);
|
|
223
|
+
if (!force) {
|
|
224
|
+
const cached = await readCached(ref);
|
|
225
|
+
// 沿用 v1.2.0 的「缓存有效」门槛(.txt 字节数 > 1000):过短的缓存视为无效,重新解析。
|
|
226
|
+
const valid = cached !== null && !cached.corrupted && Buffer.byteLength(cached.text, 'utf8') > 1000;
|
|
227
|
+
if (valid) {
|
|
228
|
+
const hit = source === 'auto' ? true : producerMatches(targetProducer, cached.producer);
|
|
229
|
+
if (hit) {
|
|
230
|
+
let pages = cached.pages;
|
|
231
|
+
// 兼容升级:pdfqa 时代的缓存只有 .txt 没有页码索引。重新提取生成页码索引;
|
|
232
|
+
// 新旧文本长度接近才覆盖(视觉转录的扫描件缓存不容侵犯)。
|
|
233
|
+
if (!pages && cached.producer === 'legacy') {
|
|
234
|
+
const upgraded = await tryUpgradePageIndex(ref, cached.text.length);
|
|
235
|
+
if (upgraded) {
|
|
236
|
+
pages = upgraded.pages;
|
|
237
|
+
return { text: upgraded.text, pages, pageCount: pages.length, chars: upgraded.text.length, source: 'cache', producer: 'legacy', backend: null };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
text: cached.text,
|
|
242
|
+
pages: pages ?? [],
|
|
243
|
+
pageCount: pages?.length ?? 0,
|
|
244
|
+
chars: cached.text.length,
|
|
245
|
+
source: 'cache',
|
|
246
|
+
producer: cached.producer,
|
|
247
|
+
backend: cached.backend,
|
|
248
|
+
};
|
|
138
249
|
}
|
|
250
|
+
// 显式 source 与缓存 producer 不命中 → 判定 stale,重新解析并覆盖(§8)
|
|
139
251
|
}
|
|
140
|
-
return { text, pages: pages ?? [], pageCount: pages?.length ?? 0, chars: text.length, source: 'cache' };
|
|
141
252
|
}
|
|
142
|
-
|
|
143
|
-
|
|
253
|
+
// 重新解析
|
|
254
|
+
if (targetProducer === 'pdfjs') {
|
|
255
|
+
return await parseAndStorePdfjs(ref, mineru);
|
|
256
|
+
}
|
|
257
|
+
if (targetProducer === 'mineru-local') {
|
|
258
|
+
return await parseAndStoreMineru(ref, 'mineru-local', mineru.config);
|
|
259
|
+
}
|
|
260
|
+
return await parseAndStoreMineru(ref, 'mineru-cloud', mineru.config);
|
|
144
261
|
}
|
|
145
|
-
/**
|
|
262
|
+
/** 本地提取(不落盘)。文本过少抛 ShortTextError,供上层做 MinerU 兜底。 */
|
|
146
263
|
async function extractLocal(pdfPath) {
|
|
147
264
|
let data;
|
|
148
265
|
try {
|
|
@@ -150,21 +267,172 @@ async function extractLocal(pdfPath) {
|
|
|
150
267
|
}
|
|
151
268
|
catch (err) {
|
|
152
269
|
if (err instanceof ScannedPdfError) {
|
|
153
|
-
|
|
270
|
+
// 文案只描述「发生了什么」,可执行指引由上层按 MinerU 是否启用补充(见 parseAndStorePdfjs)。
|
|
271
|
+
// 不再声称「视觉转录兜底尚未实现」——1.3.0 起 MinerU 就是那个兜底(验证报告 O2)。
|
|
272
|
+
throw new ShortTextError('本地提取的文本过少,该 PDF 可能是扫描件(没有可用文本层)。');
|
|
154
273
|
}
|
|
155
274
|
throw new Error(`PDF 文本提取失败:${err instanceof Error ? err.message : String(err)}`);
|
|
156
275
|
}
|
|
157
276
|
if (data.text.length < 1000) {
|
|
158
|
-
throw new
|
|
277
|
+
throw new ShortTextError(`转录结果过短(${data.text.length} 字符),疑似失败,请重试`);
|
|
159
278
|
}
|
|
160
|
-
return { text: data.text, pages: data.pages, pageCount: data.pageCount, chars: data.chars, source: 'local' };
|
|
279
|
+
return { text: data.text, pages: data.pages, pageCount: data.pageCount, chars: data.chars, source: 'local', producer: 'pdfjs', backend: null };
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* 原子写的临时文件路径:pid + 随机后缀。
|
|
283
|
+
* 只用 pid 时,同一进程内并发写同一目标会撞名并交叉覆盖多文件提交序列(验证报告 F6)。
|
|
284
|
+
*/
|
|
285
|
+
export function atomicTempPath(path) {
|
|
286
|
+
return `${path}.tmp-${process.pid}-${randomUUID().slice(0, 8)}`;
|
|
161
287
|
}
|
|
162
|
-
/**
|
|
163
|
-
async function
|
|
164
|
-
const
|
|
165
|
-
await writeFile(
|
|
166
|
-
await
|
|
167
|
-
|
|
288
|
+
/** 原子写:写临时文件再 rename,POSIX rename 原子覆盖,避免半成品缓存。 */
|
|
289
|
+
async function atomicWrite(path, content) {
|
|
290
|
+
const tmp = atomicTempPath(path);
|
|
291
|
+
await writeFile(tmp, content, 'utf8');
|
|
292
|
+
await rename(tmp, path);
|
|
293
|
+
}
|
|
294
|
+
/** pdfjs 提取并落盘(.txt + .pages.json + .transcript.json),并清理 MinerU 富产物。 */
|
|
295
|
+
async function parseAndStorePdfjs(ref, mineru) {
|
|
296
|
+
try {
|
|
297
|
+
const t = await extractLocal(ref.pdfPath);
|
|
298
|
+
await writeFile(ref.txtPath, t.text, 'utf8');
|
|
299
|
+
await writeFile(ref.pagesPath, JSON.stringify({ pageCount: t.pageCount, pages: t.pages }, null, 2), 'utf8');
|
|
300
|
+
await writeFile(ref.transcriptPath, JSON.stringify(pdfjsTranscriptMeta(t), null, 2), 'utf8');
|
|
301
|
+
// 非 MinerU 来源重写成功后删除 MinerU 富产物(避免富产物与文本不同源,AC-C9)
|
|
302
|
+
await rm(ref.mineruMdPath, { force: true });
|
|
303
|
+
await rm(ref.mineruJsonPath, { force: true });
|
|
304
|
+
return t;
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
// 扫描件/文本过短:若 MinerU 已配置可用 → 自动兜底改走 MinerU(S10)
|
|
308
|
+
if (err instanceof ShortTextError && mineru.config.mode !== 'off') {
|
|
309
|
+
const fallback = mineru.config.mode === 'cloud' ? 'mineru-cloud' : 'mineru-local';
|
|
310
|
+
try {
|
|
311
|
+
return await parseAndStoreMineru(ref, fallback, mineru.config);
|
|
312
|
+
}
|
|
313
|
+
catch (mineruErr) {
|
|
314
|
+
// MinerU 兜底也失败:报扫描件错误 + MinerU 指引,不静默吞掉
|
|
315
|
+
throw new ShortTextError(`${err.message}(已尝试 MinerU 兜底也失败:${mineruErr instanceof Error ? mineruErr.message : String(mineruErr)})`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (err instanceof ShortTextError) {
|
|
319
|
+
throw new ShortTextError(`${err.message}(可在「设置 → 论文伴读」启用 MinerU 后端自动解析扫描件,或换文本型 PDF、手动放置同名 .txt 缓存)`);
|
|
320
|
+
}
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** MinerU 解析并落盘(§7.3/§7.5:先写富产物,最后写 .transcript.json 提交标记)。 */
|
|
325
|
+
async function parseAndStoreMineru(ref, producer, cfg) {
|
|
326
|
+
const pdfBytes = new Uint8Array(await readFile(ref.pdfPath));
|
|
327
|
+
const fileName = ref.name + '.pdf';
|
|
328
|
+
const result = producer === 'mineru-local'
|
|
329
|
+
? await parseLocalMineru({
|
|
330
|
+
baseUrl: cfg.local.baseUrl,
|
|
331
|
+
apiKey: cfg.local.apiKey,
|
|
332
|
+
backend: cfg.local.backend,
|
|
333
|
+
effort: cfg.local.effort,
|
|
334
|
+
parseMethod: cfg.local.parseMethod,
|
|
335
|
+
serverUrl: cfg.local.serverUrl,
|
|
336
|
+
langList: cfg.local.langList,
|
|
337
|
+
imageAnalysis: cfg.local.imageAnalysis,
|
|
338
|
+
requestTimeoutMs: cfg.local.requestTimeoutMs,
|
|
339
|
+
pollIntervalMs: cfg.local.pollIntervalMs,
|
|
340
|
+
noResponseTimeoutMs: cfg.local.noResponseTimeoutMs,
|
|
341
|
+
jobTimeoutMs: cfg.local.jobTimeoutMs,
|
|
342
|
+
pdfBytes,
|
|
343
|
+
fileName,
|
|
344
|
+
})
|
|
345
|
+
: await parseCloudMineru({
|
|
346
|
+
baseUrl: cfg.cloud.baseUrl,
|
|
347
|
+
apiKey: cfg.cloud.apiKey,
|
|
348
|
+
modelVersion: cfg.cloud.modelVersion,
|
|
349
|
+
pollIntervalMs: cfg.cloud.pollIntervalMs,
|
|
350
|
+
zipTimeoutMs: cfg.cloud.zipTimeoutMs,
|
|
351
|
+
pdfBytes,
|
|
352
|
+
fileName,
|
|
353
|
+
});
|
|
354
|
+
const { projection, markdown, blocks, meta } = result;
|
|
355
|
+
const text = projection.text;
|
|
356
|
+
const hasMd = markdown.trim().length > 0;
|
|
357
|
+
// 写盘前校验(§7.5.1):至少 md_content 或投影文本有一个非空;有 content_list 能建页码索引;文本 >=1000。
|
|
358
|
+
// 任一不过直接抛错,不写任何文件(失败不得污染缓存)。
|
|
359
|
+
// 顺序刻意把「content_list 缺失」放在「文本过短」之前:md 好但 content_list 为 null 时,
|
|
360
|
+
// 真正原因是拿不到页码索引,而不是解析失败(验证报告 F4)。
|
|
361
|
+
if (!hasMd && !text.trim())
|
|
362
|
+
throw new MineruError('MinerU 未返回可用内容(md_content 与投影文本均为空)');
|
|
363
|
+
if (projection.pages.length < 1) {
|
|
364
|
+
throw new MineruError('MinerU 未返回 content_list(或该字段为空),无法建立页码索引;请确认服务端请求带了 return_content_list=true,或改用 pdfjs 后端。');
|
|
365
|
+
}
|
|
366
|
+
if (text.length < 1000)
|
|
367
|
+
throw new MineruError(`MinerU 转录结果过短(${text.length} 字符),疑似失败,请重试`);
|
|
368
|
+
const createdAt = new Date().toISOString();
|
|
369
|
+
const warnings = projection.warnings;
|
|
370
|
+
// 写盘顺序:mineru.md → mineru.json → pages.json → txt → transcript.json(最后=提交标记)
|
|
371
|
+
await atomicWrite(ref.mineruMdPath, markdown);
|
|
372
|
+
await atomicWrite(ref.mineruJsonPath, JSON.stringify(mineruJsonPayload(producer, meta, projection, blocks, createdAt), null, 2));
|
|
373
|
+
await atomicWrite(ref.pagesPath, JSON.stringify({ pageCount: projection.pageCount, pages: projection.pages }, null, 2));
|
|
374
|
+
await atomicWrite(ref.txtPath, text);
|
|
375
|
+
await atomicWrite(ref.transcriptPath, JSON.stringify(mineruTranscriptMeta(producer, meta, text, projection, warnings, createdAt), null, 2));
|
|
376
|
+
const backend = producer === 'mineru-local' ? (meta.backend ?? null) : (meta.modelVersion ?? null);
|
|
377
|
+
return {
|
|
378
|
+
text,
|
|
379
|
+
pages: projection.pages,
|
|
380
|
+
pageCount: projection.pageCount,
|
|
381
|
+
chars: text.length,
|
|
382
|
+
source: producer,
|
|
383
|
+
producer,
|
|
384
|
+
backend,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function pdfjsTranscriptMeta(t) {
|
|
388
|
+
return {
|
|
389
|
+
v: 1,
|
|
390
|
+
producer: 'pdfjs',
|
|
391
|
+
createdAt: new Date().toISOString(),
|
|
392
|
+
text: { bytes: Buffer.byteLength(t.text, 'utf8'), chars: t.chars, pageCount: t.pageCount },
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function mineruTranscriptMeta(producer, meta, text, projection, warnings, createdAt) {
|
|
396
|
+
const engine = producer === 'mineru-local'
|
|
397
|
+
? {
|
|
398
|
+
kind: 'mineru-local',
|
|
399
|
+
api: 'legacy',
|
|
400
|
+
baseUrl: meta.baseUrl,
|
|
401
|
+
...(meta.serverVersion ? { serverVersion: meta.serverVersion } : {}),
|
|
402
|
+
...(meta.backend ? { backend: meta.backend } : {}),
|
|
403
|
+
...(meta.effort ? { effort: meta.effort } : {}),
|
|
404
|
+
...(meta.parseMethod ? { parseMethod: meta.parseMethod } : {}),
|
|
405
|
+
...(meta.langList ? { langList: meta.langList } : {}),
|
|
406
|
+
...(meta.taskId ? { taskId: meta.taskId } : {}),
|
|
407
|
+
elapsedMs: meta.elapsedMs,
|
|
408
|
+
}
|
|
409
|
+
: {
|
|
410
|
+
kind: 'mineru-cloud',
|
|
411
|
+
api: 'v4',
|
|
412
|
+
baseUrl: meta.baseUrl,
|
|
413
|
+
...(meta.batchId ? { batchId: meta.batchId } : {}),
|
|
414
|
+
modelVersion: meta.modelVersion ?? 'pipeline',
|
|
415
|
+
};
|
|
416
|
+
return {
|
|
417
|
+
v: 1,
|
|
418
|
+
producer,
|
|
419
|
+
createdAt,
|
|
420
|
+
text: { bytes: Buffer.byteLength(text, 'utf8'), chars: text.length, pageCount: projection.pageCount },
|
|
421
|
+
engine,
|
|
422
|
+
warnings,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function mineruJsonPayload(producer, meta, projection, blocks, createdAt) {
|
|
426
|
+
return {
|
|
427
|
+
v: 1,
|
|
428
|
+
producer,
|
|
429
|
+
createdAt,
|
|
430
|
+
...(meta.serverVersion ? { serverVersion: meta.serverVersion } : {}),
|
|
431
|
+
...(meta.backend ? { backend: meta.backend } : {}),
|
|
432
|
+
pageCount: projection.pageCount,
|
|
433
|
+
imagesAvailable: false,
|
|
434
|
+
contentList: blocks,
|
|
435
|
+
};
|
|
168
436
|
}
|
|
169
437
|
/** 为旧缓存补建页码索引:成功且长度相近才覆盖 .txt,返回新内容;否则返回 null。 */
|
|
170
438
|
async function tryUpgradePageIndex(ref, cachedLen) {
|
|
@@ -201,3 +469,12 @@ export async function readTranscript(ref) {
|
|
|
201
469
|
return null;
|
|
202
470
|
}
|
|
203
471
|
}
|
|
472
|
+
/** 转录缓存大小(供外部快速判断,保留 v1.2.0 语义)。 */
|
|
473
|
+
export function transcriptBytes(ref) {
|
|
474
|
+
try {
|
|
475
|
+
return statSync(ref.txtPath).size;
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
return 0;
|
|
479
|
+
}
|
|
480
|
+
}
|
package/dist/translate-config.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
9
|
import { dshHome } from "./library.js";
|
|
10
|
+
// 预检会把端点响应体片段拼进错误文案,必须先脱敏(否则端点回显 Authorization 时 key 会泄露)。
|
|
11
|
+
// 这里复用 MinerU 客户端里已经过三轮评审的 sanitizeDetail(含 URL 编码 / JSON 转义 / 短 key
|
|
12
|
+
// 词边界处理)。mineru.ts 运行时只 import node:zlib(对 mineru-config.ts 是 type-only import),
|
|
13
|
+
// 所以 translate-config → mineru 是单向依赖,**不会形成运行时循环**。
|
|
14
|
+
import { sanitizeDetail } from "./mineru.js";
|
|
10
15
|
export function translateConfigPath(home = dshHome()) {
|
|
11
16
|
return join(home, '.dsh-paper-reader', 'translate.json');
|
|
12
17
|
}
|
|
@@ -102,10 +107,11 @@ export async function testTranslateEndpoint(ep) {
|
|
|
102
107
|
return { ok: true };
|
|
103
108
|
}
|
|
104
109
|
catch { /* 非 JSON 也算失败,走下面报 body */ }
|
|
105
|
-
|
|
110
|
+
// 响应体可能回显 Authorization / 错误页:先按本次 key 脱敏再截断展示(保留排查价值)
|
|
111
|
+
return { ok: false, detail: `HTTP 200 但响应不是补全结果:${sanitizeDetail(text.slice(0, 200), apiKey)}(多半是端点路径不对,如智谱应为 /api/paas/v4)` };
|
|
106
112
|
}
|
|
107
113
|
const text = (await r.text().catch(() => '')).slice(0, 200);
|
|
108
|
-
return { ok: false, detail: `HTTP ${r.status}${text ? ' ' + text : ''}` };
|
|
114
|
+
return { ok: false, detail: `HTTP ${r.status}${text ? ' ' + sanitizeDetail(text, apiKey) : ''}` };
|
|
109
115
|
}
|
|
110
116
|
catch (err) {
|
|
111
117
|
// Node 的 fetch 把真实原因(ECONNREFUSED / 证书错误 / 超时)塞在 cause 里,
|