@huanlin/dsh-plugin-mineru 0.2.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 +664 -0
- package/README.md +87 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +419 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +875 -0
- package/package.json +88 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
import z from "schemastery";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, extname, join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
6
|
+
//#region src/client.ts
|
|
7
|
+
/**
|
|
8
|
+
* client.ts — MinerU HTTP client.
|
|
9
|
+
*
|
|
10
|
+
* Minimal fetch-based client for the MinerU FastAPI server (v3.4.4, protocol v2).
|
|
11
|
+
* Endpoints: GET /health, POST /tasks, GET /tasks/{id}, GET /tasks/{id}/result.
|
|
12
|
+
*
|
|
13
|
+
* Auth is optional: MinerU's open-source server has no built-in auth. When an
|
|
14
|
+
* API key is resolved (via the credential store or env var), it is sent as
|
|
15
|
+
* `Authorization: Bearer <key>`. Credential-bearing requests reject redirects.
|
|
16
|
+
*/
|
|
17
|
+
var MinerUError = class extends Error {
|
|
18
|
+
status;
|
|
19
|
+
body;
|
|
20
|
+
constructor(message, status, body) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.status = status;
|
|
23
|
+
this.body = body;
|
|
24
|
+
this.name = "MinerUError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const MIME_BY_EXT = {
|
|
28
|
+
".pdf": "application/pdf",
|
|
29
|
+
".png": "image/png",
|
|
30
|
+
".jpg": "image/jpeg",
|
|
31
|
+
".jpeg": "image/jpeg",
|
|
32
|
+
".gif": "image/gif",
|
|
33
|
+
".bmp": "image/bmp",
|
|
34
|
+
".tiff": "image/tiff",
|
|
35
|
+
".tif": "image/tiff",
|
|
36
|
+
".webp": "image/webp",
|
|
37
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
38
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
39
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
40
|
+
};
|
|
41
|
+
function mimeTypeForExt(ext) {
|
|
42
|
+
return MIME_BY_EXT[ext.toLowerCase()] ?? "application/octet-stream";
|
|
43
|
+
}
|
|
44
|
+
function sleep(ms, signal) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
if (signal.aborted) {
|
|
47
|
+
const err = /* @__PURE__ */ new Error("Aborted");
|
|
48
|
+
err.name = "AbortError";
|
|
49
|
+
reject(err);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const timer = setTimeout(resolve, ms);
|
|
53
|
+
signal.addEventListener("abort", () => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
const err = /* @__PURE__ */ new Error("Aborted");
|
|
56
|
+
err.name = "AbortError";
|
|
57
|
+
reject(err);
|
|
58
|
+
}, { once: true });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function buildFormData(filePath, params) {
|
|
62
|
+
const buffer = await readFile(filePath);
|
|
63
|
+
const fileName = basename(filePath);
|
|
64
|
+
const mime = mimeTypeForExt(extname(fileName));
|
|
65
|
+
const blob = new Blob([buffer], { type: mime });
|
|
66
|
+
const form = new FormData();
|
|
67
|
+
form.append("files", blob, fileName);
|
|
68
|
+
const appendBool = (key, val) => {
|
|
69
|
+
if (val !== void 0) form.append(key, String(val));
|
|
70
|
+
};
|
|
71
|
+
const appendStr = (key, val) => {
|
|
72
|
+
if (val !== void 0) form.append(key, val);
|
|
73
|
+
};
|
|
74
|
+
const appendInt = (key, val) => {
|
|
75
|
+
if (val !== void 0) form.append(key, String(val));
|
|
76
|
+
};
|
|
77
|
+
appendStr("backend", params.backend);
|
|
78
|
+
appendStr("parse_method", params.parse_method);
|
|
79
|
+
appendStr("effort", params.effort);
|
|
80
|
+
appendStr("server_url", params.server_url);
|
|
81
|
+
appendBool("formula_enable", params.formula_enable);
|
|
82
|
+
appendBool("table_enable", params.table_enable);
|
|
83
|
+
appendBool("image_analysis", params.image_analysis);
|
|
84
|
+
appendBool("return_md", params.return_md);
|
|
85
|
+
appendBool("return_middle_json", params.return_middle_json);
|
|
86
|
+
appendBool("return_model_output", params.return_model_output);
|
|
87
|
+
appendBool("return_content_list", params.return_content_list);
|
|
88
|
+
appendBool("return_images", params.return_images);
|
|
89
|
+
appendBool("response_format_zip", params.response_format_zip);
|
|
90
|
+
appendBool("return_original_file", params.return_original_file);
|
|
91
|
+
appendInt("start_page_id", params.start_page_id);
|
|
92
|
+
appendInt("end_page_id", params.end_page_id);
|
|
93
|
+
if (params.lang_list !== void 0) for (const lang of params.lang_list) form.append("lang_list", lang);
|
|
94
|
+
return form;
|
|
95
|
+
}
|
|
96
|
+
var MinerUClient = class {
|
|
97
|
+
baseURL;
|
|
98
|
+
timeoutMs;
|
|
99
|
+
apiKeyResolver;
|
|
100
|
+
constructor(opts) {
|
|
101
|
+
this.baseURL = opts.baseURL.replace(/\/+$/, "");
|
|
102
|
+
this.timeoutMs = opts.timeoutMs;
|
|
103
|
+
this.apiKeyResolver = opts.apiKeyResolver;
|
|
104
|
+
}
|
|
105
|
+
async health(signal) {
|
|
106
|
+
return this.request("GET", "/health", void 0, signal, [200]);
|
|
107
|
+
}
|
|
108
|
+
async submitTask(filePath, params, signal) {
|
|
109
|
+
const form = await buildFormData(filePath, params);
|
|
110
|
+
return this.request("POST", "/tasks", form, signal, [202]);
|
|
111
|
+
}
|
|
112
|
+
async getTaskStatus(taskId, signal) {
|
|
113
|
+
return this.request("GET", `/tasks/${encodeURIComponent(taskId)}`, void 0, signal, [200]);
|
|
114
|
+
}
|
|
115
|
+
async getTaskResult(taskId, signal) {
|
|
116
|
+
return this.request("GET", `/tasks/${encodeURIComponent(taskId)}/result`, void 0, signal, [200]);
|
|
117
|
+
}
|
|
118
|
+
async request(method, path, body, parentSignal, acceptedStatuses) {
|
|
119
|
+
parentSignal.throwIfAborted();
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
122
|
+
const onParentAbort = () => controller.abort();
|
|
123
|
+
parentSignal.addEventListener("abort", onParentAbort, { once: true });
|
|
124
|
+
try {
|
|
125
|
+
const apiKey = this.apiKeyResolver ? await this.apiKeyResolver() : void 0;
|
|
126
|
+
parentSignal.throwIfAborted();
|
|
127
|
+
const headers = {};
|
|
128
|
+
if (apiKey) headers["authorization"] = `Bearer ${apiKey}`;
|
|
129
|
+
const response = await fetch(`${this.baseURL}${path}`, {
|
|
130
|
+
method,
|
|
131
|
+
headers,
|
|
132
|
+
body,
|
|
133
|
+
signal: controller.signal,
|
|
134
|
+
redirect: apiKey ? "error" : "follow"
|
|
135
|
+
});
|
|
136
|
+
const status = response.status;
|
|
137
|
+
if (!acceptedStatuses.includes(status)) {
|
|
138
|
+
let errorBody;
|
|
139
|
+
try {
|
|
140
|
+
errorBody = await response.json();
|
|
141
|
+
} catch {
|
|
142
|
+
try {
|
|
143
|
+
errorBody = await response.text();
|
|
144
|
+
} catch {
|
|
145
|
+
errorBody = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
throw new MinerUError(`MinerU ${method} ${path} returned ${status}`, status, errorBody);
|
|
149
|
+
}
|
|
150
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
151
|
+
if (!contentType.includes("application/json")) throw new MinerUError(`MinerU ${method} ${path} returned non-JSON content-type: ${contentType}`, status, null);
|
|
152
|
+
return await response.json();
|
|
153
|
+
} finally {
|
|
154
|
+
clearTimeout(timeoutId);
|
|
155
|
+
parentSignal.removeEventListener("abort", onParentAbort);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
async function pollUntilDone(client, taskId, opts) {
|
|
160
|
+
const deadline = Date.now() + opts.timeoutMs;
|
|
161
|
+
for (;;) {
|
|
162
|
+
opts.signal.throwIfAborted();
|
|
163
|
+
const status = await client.getTaskStatus(taskId, opts.signal);
|
|
164
|
+
if (status.status === "completed" || status.status === "failed") return status;
|
|
165
|
+
if (Date.now() >= deadline) throw new MinerUError(`Polling timed out after ${opts.timeoutMs}ms for task ${taskId}`, 408, status);
|
|
166
|
+
await sleep(opts.intervalMs, opts.signal);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/tools.ts
|
|
171
|
+
/**
|
|
172
|
+
* tools.ts — 5 model-facing MinerU tools.
|
|
173
|
+
*
|
|
174
|
+
* Tools:
|
|
175
|
+
* mineru_health — GET /health (capacity preflight)
|
|
176
|
+
* mineru_submit_parse_job — POST /tasks (async submit, returns task_id)
|
|
177
|
+
* mineru_get_parse_status — GET /tasks/{id} (poll status)
|
|
178
|
+
* mineru_get_parse_result — GET /tasks/{id}/result (fetch completed result)
|
|
179
|
+
* mineru_parse_document — high-level folded flow: submit → poll → result
|
|
180
|
+
*
|
|
181
|
+
* Conventions (per plugin-development-guide.md §3):
|
|
182
|
+
* C4 — execute returns a canonical JSON value; render is a separate pure projection.
|
|
183
|
+
* C6 — exec.signal is honored at every await point.
|
|
184
|
+
* C10 — no UI-specific formats in the canonical value.
|
|
185
|
+
*/
|
|
186
|
+
const MINERU_BACKENDS = [
|
|
187
|
+
"pipeline",
|
|
188
|
+
"vlm-engine",
|
|
189
|
+
"hybrid-engine",
|
|
190
|
+
"vlm-http-client",
|
|
191
|
+
"hybrid-http-client"
|
|
192
|
+
];
|
|
193
|
+
const MINERU_PARSE_METHODS = [
|
|
194
|
+
"auto",
|
|
195
|
+
"txt",
|
|
196
|
+
"ocr"
|
|
197
|
+
];
|
|
198
|
+
function textRender(fn) {
|
|
199
|
+
return (_args, value) => [{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: fn(value)
|
|
202
|
+
}];
|
|
203
|
+
}
|
|
204
|
+
function toParseParams(args, config) {
|
|
205
|
+
return {
|
|
206
|
+
backend: args.backend ?? config.defaultBackend,
|
|
207
|
+
parse_method: args.parse_method ?? config.defaultParseMethod,
|
|
208
|
+
lang_list: args.lang_list ?? [config.defaultLang],
|
|
209
|
+
formula_enable: args.formula_enable ?? true,
|
|
210
|
+
table_enable: args.table_enable ?? true,
|
|
211
|
+
return_md: true,
|
|
212
|
+
return_middle_json: args.return_middle_json ?? false,
|
|
213
|
+
return_content_list: args.return_content_list ?? false,
|
|
214
|
+
return_images: args.return_images ?? false,
|
|
215
|
+
start_page_id: args.start_page_id ?? 0,
|
|
216
|
+
end_page_id: args.end_page_id ?? 99999
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
async function maybeTruncateMd(md, maxChars, taskId) {
|
|
220
|
+
if (md.length <= maxChars) return {
|
|
221
|
+
content: md,
|
|
222
|
+
truncated: false
|
|
223
|
+
};
|
|
224
|
+
const fullMdPath = join(tmpdir(), `mineru-${taskId}.md`);
|
|
225
|
+
await writeFile(fullMdPath, md, "utf8");
|
|
226
|
+
return {
|
|
227
|
+
content: md.slice(0, maxChars) + `\n\n... [truncated; full content saved to ${fullMdPath}]`,
|
|
228
|
+
truncated: true,
|
|
229
|
+
fullMdPath
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function renderHealthOutput(value) {
|
|
233
|
+
const lines = [`MinerU server: ${value.status}`];
|
|
234
|
+
if (value.version) lines.push(`Version: ${value.version}`);
|
|
235
|
+
if (value.queued_tasks !== void 0) lines.push(`Queue: ${value.queued_tasks} queued, ${value.processing_tasks ?? 0} processing, ${value.completed_tasks ?? 0} completed, ${value.failed_tasks ?? 0} failed`);
|
|
236
|
+
if (value.max_concurrent_requests !== void 0) lines.push(`Capacity: ${value.max_concurrent_requests} max concurrent`);
|
|
237
|
+
return lines.join("\n");
|
|
238
|
+
}
|
|
239
|
+
function renderSubmitOutput(value) {
|
|
240
|
+
const lines = [`MinerU task submitted: ${value.task_id}`, `Status: ${value.status}`];
|
|
241
|
+
if (value.queued_ahead !== void 0) lines.push(`Queued ahead: ${value.queued_ahead}`);
|
|
242
|
+
if (value.status_url) lines.push(`Status URL: ${value.status_url}`);
|
|
243
|
+
if (value.result_url) lines.push(`Result URL: ${value.result_url}`);
|
|
244
|
+
lines.push("");
|
|
245
|
+
lines.push("Poll with mineru_get_parse_status, then fetch with mineru_get_parse_result.");
|
|
246
|
+
return lines.join("\n");
|
|
247
|
+
}
|
|
248
|
+
function renderStatusOutput(value) {
|
|
249
|
+
const lines = [`Task ${value.task_id}: ${value.status}`];
|
|
250
|
+
if (value.file_names && value.file_names.length > 0) lines.push(`Files: ${value.file_names.join(", ")}`);
|
|
251
|
+
if (value.queued_ahead !== void 0) lines.push(`Queued ahead: ${value.queued_ahead}`);
|
|
252
|
+
if (value.created_at) lines.push(`Created: ${value.created_at}`);
|
|
253
|
+
if (value.completed_at) lines.push(`Completed: ${value.completed_at}`);
|
|
254
|
+
if (value.error) lines.push(`Error: ${value.error}`);
|
|
255
|
+
return lines.join("\n");
|
|
256
|
+
}
|
|
257
|
+
function renderResultOutput(value) {
|
|
258
|
+
const lines = [`MinerU result for task ${value.task_id}`];
|
|
259
|
+
if (value.backend) lines.push(`Backend: ${value.backend} (v${value.version ?? "?"})`);
|
|
260
|
+
if (value.file_stems && value.file_stems.length > 0) lines.push(`Files: ${value.file_stems.join(", ")}`);
|
|
261
|
+
if (value.raw_result_path) lines.push(`Full result JSON: ${value.raw_result_path}`);
|
|
262
|
+
if (value.md_truncated && value.full_md_path) lines.push(`Full markdown: ${value.full_md_path}`);
|
|
263
|
+
if (value.md_content) {
|
|
264
|
+
lines.push("");
|
|
265
|
+
lines.push(value.md_content);
|
|
266
|
+
}
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
function renderParseDocOutput(value) {
|
|
270
|
+
const lines = [`MinerU parse ${value.status} (task: ${value.task_id})`];
|
|
271
|
+
if (value.backend) lines.push(`Backend: ${value.backend} (v${value.version ?? "?"})`);
|
|
272
|
+
if (value.file_stems && value.file_stems.length > 0) lines.push(`Files: ${value.file_stems.join(", ")}`);
|
|
273
|
+
if (value.error) lines.push(`Error: ${value.error}`);
|
|
274
|
+
else if (value.md_content) {
|
|
275
|
+
if (value.md_truncated && value.full_md_path) lines.push(`[Markdown truncated; full content at ${value.full_md_path}]`);
|
|
276
|
+
lines.push("");
|
|
277
|
+
lines.push(value.md_content);
|
|
278
|
+
}
|
|
279
|
+
return lines.join("\n");
|
|
280
|
+
}
|
|
281
|
+
function toHealthOutput(h) {
|
|
282
|
+
return {
|
|
283
|
+
status: h.status,
|
|
284
|
+
version: h.version,
|
|
285
|
+
queued_tasks: h.queued_tasks,
|
|
286
|
+
processing_tasks: h.processing_tasks,
|
|
287
|
+
completed_tasks: h.completed_tasks,
|
|
288
|
+
failed_tasks: h.failed_tasks,
|
|
289
|
+
max_concurrent_requests: h.max_concurrent_requests
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function toSubmitOutput(s) {
|
|
293
|
+
const out = {
|
|
294
|
+
task_id: s.task_id,
|
|
295
|
+
status: s.status
|
|
296
|
+
};
|
|
297
|
+
if (s.status_url) out.status_url = s.status_url;
|
|
298
|
+
if (s.result_url) out.result_url = s.result_url;
|
|
299
|
+
if (s.queued_ahead !== void 0) out.queued_ahead = s.queued_ahead;
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
function toStatusOutput(s) {
|
|
303
|
+
const out = {
|
|
304
|
+
task_id: s.task_id,
|
|
305
|
+
status: s.status
|
|
306
|
+
};
|
|
307
|
+
if (s.file_names && s.file_names.length > 0) out.file_names = s.file_names;
|
|
308
|
+
if (s.created_at) out.created_at = s.created_at;
|
|
309
|
+
if (s.started_at) out.started_at = s.started_at;
|
|
310
|
+
if (s.completed_at) out.completed_at = s.completed_at;
|
|
311
|
+
if (s.error) out.error = s.error;
|
|
312
|
+
if (s.queued_ahead !== void 0) out.queued_ahead = s.queued_ahead;
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
function registerTools(ctx, getClient, getConfig) {
|
|
316
|
+
const client = () => getClient();
|
|
317
|
+
const config = () => getConfig();
|
|
318
|
+
ctx.tools.register(defineTool({
|
|
319
|
+
name: "mineru_health",
|
|
320
|
+
description: "Check MinerU server health and capacity. Returns server status, version, queue depth (queued/processing/completed/failed task counts), and max concurrency. Useful before submitting large batch jobs to check available capacity. No parameters required.",
|
|
321
|
+
parameters: {},
|
|
322
|
+
output: {
|
|
323
|
+
schema: {
|
|
324
|
+
type: "object",
|
|
325
|
+
additionalProperties: false,
|
|
326
|
+
properties: {
|
|
327
|
+
status: {
|
|
328
|
+
type: "string",
|
|
329
|
+
required: true,
|
|
330
|
+
description: "Server health status: \"healthy\" or \"unhealthy\"."
|
|
331
|
+
},
|
|
332
|
+
version: {
|
|
333
|
+
type: "string",
|
|
334
|
+
description: "MinerU server version."
|
|
335
|
+
},
|
|
336
|
+
queued_tasks: {
|
|
337
|
+
type: "integer",
|
|
338
|
+
description: "Number of tasks waiting in queue."
|
|
339
|
+
},
|
|
340
|
+
processing_tasks: {
|
|
341
|
+
type: "integer",
|
|
342
|
+
description: "Number of tasks currently being processed."
|
|
343
|
+
},
|
|
344
|
+
completed_tasks: {
|
|
345
|
+
type: "integer",
|
|
346
|
+
description: "Number of completed tasks (retained 24h)."
|
|
347
|
+
},
|
|
348
|
+
failed_tasks: {
|
|
349
|
+
type: "integer",
|
|
350
|
+
description: "Number of failed tasks."
|
|
351
|
+
},
|
|
352
|
+
max_concurrent_requests: {
|
|
353
|
+
type: "integer",
|
|
354
|
+
description: "Maximum concurrent processing requests."
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
render: textRender(renderHealthOutput)
|
|
359
|
+
},
|
|
360
|
+
execute: async (_args, exec) => {
|
|
361
|
+
exec.signal.throwIfAborted();
|
|
362
|
+
return toHealthOutput(await client().health(exec.signal));
|
|
363
|
+
}
|
|
364
|
+
}));
|
|
365
|
+
ctx.tools.register(defineTool({
|
|
366
|
+
name: "mineru_submit_parse_job",
|
|
367
|
+
description: "Submit a document to MinerU for asynchronous parsing and return immediately with a task_id. Poll the task status with mineru_get_parse_status, then fetch results with mineru_get_parse_result. Use this for large documents that may take minutes to parse, or when submitting multiple jobs in parallel. The file must be a local filesystem path; if you only have a URL, download it first (e.g., via bash curl). Default backend is 'pipeline' (hallucination-free, supports all languages).",
|
|
368
|
+
parameters: {
|
|
369
|
+
file_path: {
|
|
370
|
+
type: "string",
|
|
371
|
+
required: true,
|
|
372
|
+
description: "Local filesystem path to the document (PDF, PNG, JPG, DOCX, PPTX, or XLSX)."
|
|
373
|
+
},
|
|
374
|
+
backend: {
|
|
375
|
+
type: "string",
|
|
376
|
+
enum: MINERU_BACKENDS,
|
|
377
|
+
description: "Parsing backend. 'pipeline': hallucination-free, multi-language. 'hybrid-engine': MinerU default, requires VLM. 'vlm-engine': VLM only."
|
|
378
|
+
},
|
|
379
|
+
parse_method: {
|
|
380
|
+
type: "string",
|
|
381
|
+
enum: MINERU_PARSE_METHODS,
|
|
382
|
+
description: "Parse method (pipeline/hybrid only). 'auto': auto-detect. 'txt': text only (fast). 'ocr': force OCR."
|
|
383
|
+
},
|
|
384
|
+
lang_list: {
|
|
385
|
+
type: "array",
|
|
386
|
+
items: { type: "string" },
|
|
387
|
+
description: "Language codes for pipeline backend (e.g., 'ch' for Chinese/English/Japanese). Defaults to ['ch']."
|
|
388
|
+
},
|
|
389
|
+
formula_enable: {
|
|
390
|
+
type: "boolean",
|
|
391
|
+
description: "Enable formula parsing. Default: true."
|
|
392
|
+
},
|
|
393
|
+
table_enable: {
|
|
394
|
+
type: "boolean",
|
|
395
|
+
description: "Enable table parsing. Default: true."
|
|
396
|
+
},
|
|
397
|
+
start_page_id: {
|
|
398
|
+
type: "integer",
|
|
399
|
+
description: "PDF page range start (0-indexed). Default: 0."
|
|
400
|
+
},
|
|
401
|
+
end_page_id: {
|
|
402
|
+
type: "integer",
|
|
403
|
+
description: "PDF page range end (0-indexed, inclusive). Default: 99999 (all pages)."
|
|
404
|
+
},
|
|
405
|
+
return_middle_json: {
|
|
406
|
+
type: "boolean",
|
|
407
|
+
description: "Include middle JSON (intermediate parsing structure). Default: false."
|
|
408
|
+
},
|
|
409
|
+
return_content_list: {
|
|
410
|
+
type: "boolean",
|
|
411
|
+
description: "Include content list JSON (structured content blocks). Default: false."
|
|
412
|
+
},
|
|
413
|
+
return_images: {
|
|
414
|
+
type: "boolean",
|
|
415
|
+
description: "Include extracted images (base64 data URLs). Can be large. Default: false."
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
output: {
|
|
419
|
+
schema: {
|
|
420
|
+
type: "object",
|
|
421
|
+
additionalProperties: false,
|
|
422
|
+
properties: {
|
|
423
|
+
task_id: {
|
|
424
|
+
type: "string",
|
|
425
|
+
required: true,
|
|
426
|
+
description: "MinerU task ID. Use with mineru_get_parse_status and mineru_get_parse_result."
|
|
427
|
+
},
|
|
428
|
+
status: {
|
|
429
|
+
type: "string",
|
|
430
|
+
required: true,
|
|
431
|
+
description: "Initial task status (typically \"pending\")."
|
|
432
|
+
},
|
|
433
|
+
status_url: {
|
|
434
|
+
type: "string",
|
|
435
|
+
description: "URL to poll task status."
|
|
436
|
+
},
|
|
437
|
+
result_url: {
|
|
438
|
+
type: "string",
|
|
439
|
+
description: "URL to fetch task result."
|
|
440
|
+
},
|
|
441
|
+
queued_ahead: {
|
|
442
|
+
type: "integer",
|
|
443
|
+
description: "Number of tasks ahead in queue."
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
render: textRender(renderSubmitOutput)
|
|
448
|
+
},
|
|
449
|
+
timeoutMs: 12e4,
|
|
450
|
+
execute: async (args, exec) => {
|
|
451
|
+
const a = args;
|
|
452
|
+
exec.signal.throwIfAborted();
|
|
453
|
+
return toSubmitOutput(await client().submitTask(a.file_path, toParseParams(a, config()), exec.signal));
|
|
454
|
+
}
|
|
455
|
+
}));
|
|
456
|
+
ctx.tools.register(defineTool({
|
|
457
|
+
name: "mineru_get_parse_status",
|
|
458
|
+
description: "Check the status of an asynchronous MinerU parsing task. Returns: \"pending\" (in queue), \"processing\" (being parsed), \"completed\" (done — fetch with mineru_get_parse_result), or \"failed\" (error occurred). Poll every few seconds; a 1-page PDF takes ~1-2s, large documents can take minutes.",
|
|
459
|
+
parameters: { task_id: {
|
|
460
|
+
type: "string",
|
|
461
|
+
required: true,
|
|
462
|
+
description: "Task ID returned by mineru_submit_parse_job."
|
|
463
|
+
} },
|
|
464
|
+
output: {
|
|
465
|
+
schema: {
|
|
466
|
+
type: "object",
|
|
467
|
+
additionalProperties: false,
|
|
468
|
+
properties: {
|
|
469
|
+
task_id: {
|
|
470
|
+
type: "string",
|
|
471
|
+
required: true
|
|
472
|
+
},
|
|
473
|
+
status: {
|
|
474
|
+
type: "string",
|
|
475
|
+
description: "Task status: \"pending\", \"processing\", \"completed\", or \"failed\".",
|
|
476
|
+
required: true
|
|
477
|
+
},
|
|
478
|
+
file_names: {
|
|
479
|
+
type: "array",
|
|
480
|
+
items: { type: "string" },
|
|
481
|
+
description: "Normalized file stems being parsed."
|
|
482
|
+
},
|
|
483
|
+
created_at: {
|
|
484
|
+
type: "string",
|
|
485
|
+
description: "ISO-8601 timestamp."
|
|
486
|
+
},
|
|
487
|
+
started_at: {
|
|
488
|
+
type: "string",
|
|
489
|
+
description: "ISO-8601 timestamp."
|
|
490
|
+
},
|
|
491
|
+
completed_at: {
|
|
492
|
+
type: "string",
|
|
493
|
+
description: "ISO-8601 timestamp."
|
|
494
|
+
},
|
|
495
|
+
error: {
|
|
496
|
+
type: "string",
|
|
497
|
+
description: "Error message if status is \"failed\"."
|
|
498
|
+
},
|
|
499
|
+
queued_ahead: {
|
|
500
|
+
type: "integer",
|
|
501
|
+
description: "Tasks ahead in queue (only while pending)."
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
render: textRender(renderStatusOutput)
|
|
506
|
+
},
|
|
507
|
+
execute: async (args, exec) => {
|
|
508
|
+
const a = args;
|
|
509
|
+
exec.signal.throwIfAborted();
|
|
510
|
+
return toStatusOutput(await client().getTaskStatus(a.task_id, exec.signal));
|
|
511
|
+
}
|
|
512
|
+
}));
|
|
513
|
+
ctx.tools.register(defineTool({
|
|
514
|
+
name: "mineru_get_parse_result",
|
|
515
|
+
description: "Fetch the parsing result for a completed MinerU task. The task must have status \"completed\" (check with mineru_get_parse_status first). Returns the parsed markdown content inline (truncated if very large; full content saved to a file). The full structured JSON result (including middle_json, content_list, and images if requested at submit time) is always saved to raw_result_path for inspection with file-reading tools.",
|
|
516
|
+
parameters: { task_id: {
|
|
517
|
+
type: "string",
|
|
518
|
+
required: true,
|
|
519
|
+
description: "Task ID of a completed parsing job."
|
|
520
|
+
} },
|
|
521
|
+
output: {
|
|
522
|
+
schema: {
|
|
523
|
+
type: "object",
|
|
524
|
+
additionalProperties: false,
|
|
525
|
+
properties: {
|
|
526
|
+
task_id: {
|
|
527
|
+
type: "string",
|
|
528
|
+
required: true
|
|
529
|
+
},
|
|
530
|
+
backend: { type: "string" },
|
|
531
|
+
version: { type: "string" },
|
|
532
|
+
file_stems: {
|
|
533
|
+
type: "array",
|
|
534
|
+
items: { type: "string" }
|
|
535
|
+
},
|
|
536
|
+
md_content: {
|
|
537
|
+
type: "string",
|
|
538
|
+
description: "Parsed markdown of the first file (truncated if exceeds maxMdOutputChars)."
|
|
539
|
+
},
|
|
540
|
+
md_truncated: {
|
|
541
|
+
type: "boolean",
|
|
542
|
+
description: "Whether md_content was truncated."
|
|
543
|
+
},
|
|
544
|
+
full_md_path: {
|
|
545
|
+
type: "string",
|
|
546
|
+
description: "Path to full markdown file if truncated."
|
|
547
|
+
},
|
|
548
|
+
raw_result_path: {
|
|
549
|
+
type: "string",
|
|
550
|
+
description: "Path to a JSON file with the full structured result (all files, all formats)."
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
render: textRender(renderResultOutput)
|
|
555
|
+
},
|
|
556
|
+
timeoutMs: 12e4,
|
|
557
|
+
execute: async (args, exec) => {
|
|
558
|
+
const a = args;
|
|
559
|
+
exec.signal.throwIfAborted();
|
|
560
|
+
const result = await client().getTaskResult(a.task_id, exec.signal);
|
|
561
|
+
const fileStems = Object.keys(result.results);
|
|
562
|
+
const firstStem = fileStems[0];
|
|
563
|
+
const mdContent = (firstStem !== void 0 ? result.results[firstStem] : void 0)?.md_content ?? "";
|
|
564
|
+
const rawResultPath = join(tmpdir(), `mineru-result-${a.task_id}.json`);
|
|
565
|
+
await writeFile(rawResultPath, JSON.stringify(result, null, 2), "utf8");
|
|
566
|
+
const { content, truncated, fullMdPath } = await maybeTruncateMd(mdContent, config().maxMdOutputChars, a.task_id);
|
|
567
|
+
const out = {
|
|
568
|
+
task_id: a.task_id,
|
|
569
|
+
raw_result_path: rawResultPath
|
|
570
|
+
};
|
|
571
|
+
if (result.backend) out.backend = result.backend;
|
|
572
|
+
if (result.version) out.version = result.version;
|
|
573
|
+
if (fileStems.length > 0) out.file_stems = fileStems;
|
|
574
|
+
if (content) out.md_content = content;
|
|
575
|
+
if (truncated) out.md_truncated = truncated;
|
|
576
|
+
if (fullMdPath) out.full_md_path = fullMdPath;
|
|
577
|
+
return out;
|
|
578
|
+
}
|
|
579
|
+
}));
|
|
580
|
+
ctx.tools.register(defineTool({
|
|
581
|
+
name: "mineru_parse_document",
|
|
582
|
+
description: "Parse a local document (PDF, image, DOCX, PPTX, or XLSX) via MinerU and return the extracted markdown. This is the recommended high-level tool: it submits the file, polls until parsing completes (up to poll_timeout_ms), and returns the markdown content inline. For large documents or when you need to interleave other work, use mineru_submit_parse_job + mineru_get_parse_status + mineru_get_parse_result instead. The file must be a local filesystem path; if you only have a URL, download it first (e.g., via bash curl). Default backend is 'pipeline' (hallucination-free, supports all languages).",
|
|
583
|
+
parameters: {
|
|
584
|
+
file_path: {
|
|
585
|
+
type: "string",
|
|
586
|
+
required: true,
|
|
587
|
+
description: "Local filesystem path to the document (PDF, PNG, JPG, DOCX, PPTX, or XLSX)."
|
|
588
|
+
},
|
|
589
|
+
backend: {
|
|
590
|
+
type: "string",
|
|
591
|
+
enum: MINERU_BACKENDS,
|
|
592
|
+
description: "Parsing backend. 'pipeline': hallucination-free, multi-language. 'hybrid-engine': requires VLM model. Default: 'pipeline'."
|
|
593
|
+
},
|
|
594
|
+
parse_method: {
|
|
595
|
+
type: "string",
|
|
596
|
+
enum: MINERU_PARSE_METHODS,
|
|
597
|
+
description: "Parse method (pipeline/hybrid only). 'auto': auto-detect. 'txt': text only (fast). 'ocr': force OCR."
|
|
598
|
+
},
|
|
599
|
+
lang_list: {
|
|
600
|
+
type: "array",
|
|
601
|
+
items: { type: "string" },
|
|
602
|
+
description: "Language codes for pipeline backend (e.g., 'ch'). Defaults to ['ch']."
|
|
603
|
+
},
|
|
604
|
+
formula_enable: {
|
|
605
|
+
type: "boolean",
|
|
606
|
+
description: "Enable formula parsing. Default: true."
|
|
607
|
+
},
|
|
608
|
+
table_enable: {
|
|
609
|
+
type: "boolean",
|
|
610
|
+
description: "Enable table parsing. Default: true."
|
|
611
|
+
},
|
|
612
|
+
start_page_id: {
|
|
613
|
+
type: "integer",
|
|
614
|
+
description: "PDF page range start (0-indexed). Default: 0."
|
|
615
|
+
},
|
|
616
|
+
end_page_id: {
|
|
617
|
+
type: "integer",
|
|
618
|
+
description: "PDF page range end (0-indexed, inclusive). Default: 99999 (all pages)."
|
|
619
|
+
},
|
|
620
|
+
return_middle_json: {
|
|
621
|
+
type: "boolean",
|
|
622
|
+
description: "Include middle JSON in the saved result file. Default: false."
|
|
623
|
+
},
|
|
624
|
+
return_content_list: {
|
|
625
|
+
type: "boolean",
|
|
626
|
+
description: "Include content list JSON in the saved result file. Default: false."
|
|
627
|
+
},
|
|
628
|
+
return_images: {
|
|
629
|
+
type: "boolean",
|
|
630
|
+
description: "Include extracted images in the saved result file. Default: false."
|
|
631
|
+
},
|
|
632
|
+
poll_timeout_ms: {
|
|
633
|
+
type: "number",
|
|
634
|
+
description: "Maximum time (ms) to wait for parsing before timing out. Default: 600000 (10 min)."
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
output: {
|
|
638
|
+
schema: {
|
|
639
|
+
type: "object",
|
|
640
|
+
additionalProperties: false,
|
|
641
|
+
properties: {
|
|
642
|
+
task_id: {
|
|
643
|
+
type: "string",
|
|
644
|
+
required: true
|
|
645
|
+
},
|
|
646
|
+
status: {
|
|
647
|
+
type: "string",
|
|
648
|
+
description: "Parse status: \"completed\" or \"failed\".",
|
|
649
|
+
required: true
|
|
650
|
+
},
|
|
651
|
+
backend: { type: "string" },
|
|
652
|
+
version: { type: "string" },
|
|
653
|
+
file_stems: {
|
|
654
|
+
type: "array",
|
|
655
|
+
items: { type: "string" }
|
|
656
|
+
},
|
|
657
|
+
md_content: {
|
|
658
|
+
type: "string",
|
|
659
|
+
description: "Parsed markdown content (truncated if very large)."
|
|
660
|
+
},
|
|
661
|
+
md_truncated: { type: "boolean" },
|
|
662
|
+
full_md_path: {
|
|
663
|
+
type: "string",
|
|
664
|
+
description: "Path to full markdown if truncated."
|
|
665
|
+
},
|
|
666
|
+
error: {
|
|
667
|
+
type: "string",
|
|
668
|
+
description: "Error message if status is \"failed\"."
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
render: textRender(renderParseDocOutput)
|
|
673
|
+
},
|
|
674
|
+
timeoutMs: 9e5,
|
|
675
|
+
execute: async (args, exec) => {
|
|
676
|
+
const a = args;
|
|
677
|
+
exec.signal.throwIfAborted();
|
|
678
|
+
const cfg = config();
|
|
679
|
+
const c = client();
|
|
680
|
+
const submit = await c.submitTask(a.file_path, toParseParams(a, cfg), exec.signal);
|
|
681
|
+
const pollTimeoutMs = a.poll_timeout_ms ?? cfg.pollTimeoutMs;
|
|
682
|
+
const finalStatus = await pollUntilDone(c, submit.task_id, {
|
|
683
|
+
intervalMs: cfg.pollIntervalMs,
|
|
684
|
+
timeoutMs: pollTimeoutMs,
|
|
685
|
+
signal: exec.signal
|
|
686
|
+
});
|
|
687
|
+
if (finalStatus.status === "failed") return {
|
|
688
|
+
task_id: submit.task_id,
|
|
689
|
+
status: "failed",
|
|
690
|
+
error: finalStatus.error ?? "Task failed without error message"
|
|
691
|
+
};
|
|
692
|
+
const result = await c.getTaskResult(submit.task_id, exec.signal);
|
|
693
|
+
const fileStems = Object.keys(result.results);
|
|
694
|
+
const firstStem = fileStems[0];
|
|
695
|
+
const { content, truncated, fullMdPath } = await maybeTruncateMd((firstStem !== void 0 ? result.results[firstStem] : void 0)?.md_content ?? "", cfg.maxMdOutputChars, submit.task_id);
|
|
696
|
+
const out = {
|
|
697
|
+
task_id: submit.task_id,
|
|
698
|
+
status: "completed"
|
|
699
|
+
};
|
|
700
|
+
if (result.backend) out.backend = result.backend;
|
|
701
|
+
if (result.version) out.version = result.version;
|
|
702
|
+
if (fileStems.length > 0) out.file_stems = fileStems;
|
|
703
|
+
if (content) out.md_content = content;
|
|
704
|
+
if (truncated) out.md_truncated = truncated;
|
|
705
|
+
if (fullMdPath) out.full_md_path = fullMdPath;
|
|
706
|
+
return out;
|
|
707
|
+
}
|
|
708
|
+
}));
|
|
709
|
+
}
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region src/rpc.ts
|
|
712
|
+
function ok(value) {
|
|
713
|
+
return {
|
|
714
|
+
ok: true,
|
|
715
|
+
value
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function fail(message) {
|
|
719
|
+
return {
|
|
720
|
+
ok: false,
|
|
721
|
+
error: {
|
|
722
|
+
code: "internal",
|
|
723
|
+
message,
|
|
724
|
+
details: {}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function toRuntimeConfig(resolved) {
|
|
729
|
+
return {
|
|
730
|
+
baseURL: resolved.baseURL,
|
|
731
|
+
apiKeyEnv: resolved.apiKeyEnv,
|
|
732
|
+
defaultBackend: resolved.defaultBackend,
|
|
733
|
+
defaultParseMethod: resolved.defaultParseMethod,
|
|
734
|
+
defaultLang: resolved.defaultLang,
|
|
735
|
+
pollIntervalMs: resolved.pollIntervalMs,
|
|
736
|
+
pollTimeoutMs: resolved.pollTimeoutMs,
|
|
737
|
+
requestTimeoutMs: resolved.requestTimeoutMs,
|
|
738
|
+
maxMdOutputChars: resolved.maxMdOutputChars
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function registerRpc(ctx, deps) {
|
|
742
|
+
ctx.logger.info("dsh-mineru: registering RPC channel /mineru-api");
|
|
743
|
+
ctx.connection.rpc.handle("/mineru-api", async (endpoint, payload) => {
|
|
744
|
+
switch (endpoint) {
|
|
745
|
+
case "mineru/config.get": return ok({ config: toRuntimeConfig(deps.getResolved()) });
|
|
746
|
+
case "mineru/config.set": {
|
|
747
|
+
const p = payload;
|
|
748
|
+
if (p === void 0 || typeof p !== "object" || p === null) return fail("payload must be { config: Partial<MineruRuntimeConfig> }");
|
|
749
|
+
const patch = p.config;
|
|
750
|
+
if (patch === void 0 || typeof patch !== "object") return fail("payload.config must be an object");
|
|
751
|
+
if (patch.baseURL !== void 0 && (typeof patch.baseURL !== "string" || patch.baseURL === "")) return fail("baseURL must be a non-empty string");
|
|
752
|
+
const current = deps.getResolved();
|
|
753
|
+
const next = {
|
|
754
|
+
baseURL: patch.baseURL ?? current.baseURL,
|
|
755
|
+
apiKeyEnv: patch.apiKeyEnv ?? current.apiKeyEnv,
|
|
756
|
+
defaultBackend: patch.defaultBackend ?? current.defaultBackend,
|
|
757
|
+
defaultParseMethod: patch.defaultParseMethod ?? current.defaultParseMethod,
|
|
758
|
+
defaultLang: patch.defaultLang ?? current.defaultLang,
|
|
759
|
+
pollIntervalMs: patch.pollIntervalMs ?? current.pollIntervalMs,
|
|
760
|
+
pollTimeoutMs: patch.pollTimeoutMs ?? current.pollTimeoutMs,
|
|
761
|
+
requestTimeoutMs: patch.requestTimeoutMs ?? current.requestTimeoutMs,
|
|
762
|
+
maxMdOutputChars: patch.maxMdOutputChars ?? current.maxMdOutputChars
|
|
763
|
+
};
|
|
764
|
+
deps.onConfigChanged(next);
|
|
765
|
+
return ok({ config: toRuntimeConfig(next) });
|
|
766
|
+
}
|
|
767
|
+
case "mineru/health": try {
|
|
768
|
+
const h = await deps.getClient().health(new AbortController().signal);
|
|
769
|
+
return ok({
|
|
770
|
+
status: h.status,
|
|
771
|
+
version: h.version,
|
|
772
|
+
queued_tasks: h.queued_tasks,
|
|
773
|
+
processing_tasks: h.processing_tasks,
|
|
774
|
+
completed_tasks: h.completed_tasks,
|
|
775
|
+
failed_tasks: h.failed_tasks,
|
|
776
|
+
max_concurrent_requests: h.max_concurrent_requests
|
|
777
|
+
});
|
|
778
|
+
} catch (err) {
|
|
779
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
780
|
+
}
|
|
781
|
+
default: return fail(`unknown endpoint: ${endpoint}`);
|
|
782
|
+
}
|
|
783
|
+
}, { authority: "trusted-host" });
|
|
784
|
+
}
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/index.ts
|
|
787
|
+
/**
|
|
788
|
+
* index.ts — dsh-mineru cordis plugin entry (host half).
|
|
789
|
+
*
|
|
790
|
+
* Dual-entry bundle: this is the host half (exports `.`). The browser half
|
|
791
|
+
* ships via `./client` (see `src/client/index.ts`).
|
|
792
|
+
*
|
|
793
|
+
* Architecture:
|
|
794
|
+
* - 5 model-facing tools (health, submit, status, result, parse_document)
|
|
795
|
+
* registered once at load; each tool reads the live client/config via
|
|
796
|
+
* getters, so RPC config mutations hot-reload without re-registration.
|
|
797
|
+
* - Settings namespace `mineru` persists user edits to `$DSH_HOME/settings.yaml`;
|
|
798
|
+
* cordis.yml `config:` is the composition base (first-boot seed).
|
|
799
|
+
* - RPC on `/api` channel: `mineru/config.get`/`.set`/`.health` for the
|
|
800
|
+
* browser settings page (bypasses the `WEB_SETTINGS_NAMESPACES` wire
|
|
801
|
+
* allowlist — same pattern as yet-another-subagent).
|
|
802
|
+
*/
|
|
803
|
+
const name = "dsh-mineru";
|
|
804
|
+
const inject = ["tools", "connection"];
|
|
805
|
+
const Config = z.object({
|
|
806
|
+
baseURL: z.string().description("MinerU API base URL (e.g. http://host:18000). Required."),
|
|
807
|
+
apiKeyEnv: z.string().role("credential-ref").default("MINERU_API_KEY"),
|
|
808
|
+
defaultBackend: z.union([
|
|
809
|
+
"pipeline",
|
|
810
|
+
"vlm-engine",
|
|
811
|
+
"hybrid-engine",
|
|
812
|
+
"vlm-http-client",
|
|
813
|
+
"hybrid-http-client"
|
|
814
|
+
]).default("pipeline"),
|
|
815
|
+
defaultParseMethod: z.union([
|
|
816
|
+
"auto",
|
|
817
|
+
"txt",
|
|
818
|
+
"ocr"
|
|
819
|
+
]).default("auto"),
|
|
820
|
+
defaultLang: z.string().default("ch"),
|
|
821
|
+
pollIntervalMs: z.number().default(2e3),
|
|
822
|
+
pollTimeoutMs: z.number().default(6e5),
|
|
823
|
+
requestTimeoutMs: z.number().default(6e4),
|
|
824
|
+
maxMdOutputChars: z.number().default(2e5)
|
|
825
|
+
});
|
|
826
|
+
function resolveConfig(config) {
|
|
827
|
+
if (typeof config.baseURL !== "string" || config.baseURL === "") throw new Error("dsh-mineru: config \"baseURL\" is required. Set it in the DSH GUI settings or cordis.patch.yml.");
|
|
828
|
+
return {
|
|
829
|
+
baseURL: config.baseURL,
|
|
830
|
+
apiKeyEnv: config.apiKeyEnv ?? "MINERU_API_KEY",
|
|
831
|
+
defaultBackend: config.defaultBackend ?? "pipeline",
|
|
832
|
+
defaultParseMethod: config.defaultParseMethod ?? "auto",
|
|
833
|
+
defaultLang: config.defaultLang ?? "ch",
|
|
834
|
+
pollIntervalMs: config.pollIntervalMs ?? 2e3,
|
|
835
|
+
pollTimeoutMs: config.pollTimeoutMs ?? 6e5,
|
|
836
|
+
requestTimeoutMs: config.requestTimeoutMs ?? 6e4,
|
|
837
|
+
maxMdOutputChars: config.maxMdOutputChars ?? 2e5
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function makeClient(ctx, resolved) {
|
|
841
|
+
return new MinerUClient({
|
|
842
|
+
baseURL: resolved.baseURL,
|
|
843
|
+
timeoutMs: resolved.requestTimeoutMs,
|
|
844
|
+
apiKeyResolver: async () => {
|
|
845
|
+
try {
|
|
846
|
+
const credentials = ctx.get("credentials");
|
|
847
|
+
if (credentials?.resolve) {
|
|
848
|
+
const hit = await credentials.resolve(resolved.apiKeyEnv);
|
|
849
|
+
if (hit?.value) return hit.value;
|
|
850
|
+
}
|
|
851
|
+
} catch {}
|
|
852
|
+
const envVal = process.env[resolved.apiKeyEnv];
|
|
853
|
+
return envVal && envVal.length > 0 ? envVal : void 0;
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
function apply(ctx, config = {}) {
|
|
858
|
+
let resolved = resolveConfig(config);
|
|
859
|
+
let client = makeClient(ctx, resolved);
|
|
860
|
+
const getResolved = () => resolved;
|
|
861
|
+
const getClient = () => client;
|
|
862
|
+
const onConfigChanged = (next) => {
|
|
863
|
+
resolved = next;
|
|
864
|
+
client = makeClient(ctx, resolved);
|
|
865
|
+
ctx.logger.info(`dsh-mineru: config updated, baseURL=${resolved.baseURL}`);
|
|
866
|
+
};
|
|
867
|
+
registerTools(ctx, getClient, getResolved);
|
|
868
|
+
registerRpc(ctx, {
|
|
869
|
+
getResolved,
|
|
870
|
+
getClient,
|
|
871
|
+
onConfigChanged
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
//#endregion
|
|
875
|
+
export { Config, apply, inject, name };
|