@xlight-oss/visionary-dsh 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -11
- package/cordis.patch.yml +6 -2
- package/lib/image-bridge/core.mjs +21 -4
- package/lib/image-bridge/index.mjs +73 -3
- package/lib/image-bridge/persistence.mjs +66 -24
- package/lib/image-bridge/rewrite.mjs +13 -0
- package/lib/image-bridge/trust-fence.mjs +85 -0
- package/lib/index.mjs +189 -83
- package/lib/settings-card/client.js +607 -0
- package/lib/settings-card/index.mjs +18 -0
- package/lib/settings-card/package.json +36 -0
- package/lib/settings-route.mjs +215 -0
- package/package.json +5 -1
package/lib/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
16
16
|
import z from "@deepseek-ai/schemastery";
|
|
17
|
+
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
17
18
|
import { spawn } from "node:child_process";
|
|
18
19
|
import { statSync, readFileSync } from "node:fs";
|
|
19
20
|
import { promises as fs } from "node:fs";
|
|
@@ -23,9 +24,12 @@ import path from "node:path";
|
|
|
23
24
|
const name = "visionary-vision";
|
|
24
25
|
const inject = ["tools", "systemPrompt"];
|
|
25
26
|
|
|
27
|
+
/** Settings namespace(面板 / settings.yaml 双入口,规范:dsh-plugin 设置面板上传路径配置)。 */
|
|
28
|
+
const SETTINGS_NAMESPACE = settingsNamespace("visionary-vision");
|
|
29
|
+
|
|
26
30
|
// Keep in lockstep with the Rust binary's minor version: tools rely on the
|
|
27
31
|
// CLI's `--json` output shape. Bump when the binary's contract changes.
|
|
28
|
-
const COMPAT_MINOR = "0.
|
|
32
|
+
const COMPAT_MINOR = "0.7";
|
|
29
33
|
|
|
30
34
|
const Config = z.object({
|
|
31
35
|
binaryPath: z
|
|
@@ -43,11 +47,17 @@ const Config = z.object({
|
|
|
43
47
|
visionTimeoutMs: z
|
|
44
48
|
.number()
|
|
45
49
|
.default(300000)
|
|
46
|
-
.description("Per deepseek_vision call timeout in ms."),
|
|
50
|
+
.description("Per deepseek_vision / deepseek_ocr call timeout in ms."),
|
|
47
51
|
statusTimeoutMs: z
|
|
48
52
|
.number()
|
|
49
53
|
.default(60000)
|
|
50
54
|
.description("Per deepseek_vision_status / deepseek_vision_logout timeout in ms."),
|
|
55
|
+
modelType: z
|
|
56
|
+
.union([z.const("vision"), z.const("ocr")])
|
|
57
|
+
.default("vision")
|
|
58
|
+
.description(
|
|
59
|
+
"上传管道:vision(默认,完整多模态图像理解,携带 x-model-type: vision)| ocr(文本提取,不携带 x-model-type,走服务端 OCR 管道)。deepseek_vision 按此上传;deepseek_ocr 恒为 ocr。覆盖 CLI 默认配置,修改后即时生效。"
|
|
60
|
+
),
|
|
51
61
|
});
|
|
52
62
|
|
|
53
63
|
// --- binary resolution -------------------------------------------------------
|
|
@@ -222,6 +232,56 @@ async function materializeImage(image) {
|
|
|
222
232
|
return { arg: image, cleanup: null };
|
|
223
233
|
}
|
|
224
234
|
|
|
235
|
+
// --- CLI 参数构建 --------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
// 构建 vision / ocr 子命令的 argv(纯函数,便于测试)。
|
|
238
|
+
// 选项一律等号传参(`--prompt=<v>`),避免 clap 把 `-` 开头的空格形式值当 flag 拒绝。
|
|
239
|
+
// `modelType` 仅对 vision 子命令生效:ocr 时追加 `--model-type=ocr`(其余情况不追加,
|
|
240
|
+
// CLI 默认即 vision);`ocr` 子命令不暴露 `--model-type`(恒为 ocr)。
|
|
241
|
+
function buildImageCliArgs(subcommand, images, opts) {
|
|
242
|
+
const cliArgs = [subcommand, ...images, "--json"];
|
|
243
|
+
if (opts?.prompt) cliArgs.push(`--prompt=${opts.prompt}`);
|
|
244
|
+
if (opts?.thinking) cliArgs.push("--thinking");
|
|
245
|
+
if (opts?.sessionId) cliArgs.push(`--session-id=${opts.sessionId}`);
|
|
246
|
+
else if (opts?.continueConversation) cliArgs.push("--continue-conversation");
|
|
247
|
+
if (subcommand === "vision" && opts?.modelType === "ocr") cliArgs.push("--model-type=ocr");
|
|
248
|
+
return cliArgs;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// deepseek_vision / deepseek_ocr 共用参数 schema(原生工具面,规范对齐对应 MCP 工具)。
|
|
252
|
+
const imageToolParams = {
|
|
253
|
+
images: {
|
|
254
|
+
type: "array",
|
|
255
|
+
items: { type: "string" },
|
|
256
|
+
description: "One or more images: local file paths, base64, or data URIs. The model analyzes all of them together.",
|
|
257
|
+
},
|
|
258
|
+
image: {
|
|
259
|
+
type: "string",
|
|
260
|
+
description: "Single image (local path, base64, or data URI) — convenience form of `images` with one entry.",
|
|
261
|
+
},
|
|
262
|
+
prompt: {
|
|
263
|
+
type: "string",
|
|
264
|
+
description: "Question about the image(s) (default: detailed description in Chinese).",
|
|
265
|
+
},
|
|
266
|
+
thinking: {
|
|
267
|
+
type: "boolean",
|
|
268
|
+
description: "Enable DeepThink deep reasoning.",
|
|
269
|
+
},
|
|
270
|
+
continue_conversation: {
|
|
271
|
+
type: "boolean",
|
|
272
|
+
description: "Continue the previous session (multi-image comparison across turns).",
|
|
273
|
+
},
|
|
274
|
+
session_id: {
|
|
275
|
+
type: "string",
|
|
276
|
+
description: "Reuse an explicit session thread (takes precedence over continue_conversation).",
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const imageToolOutput = {
|
|
281
|
+
schema: { type: "string" },
|
|
282
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
283
|
+
};
|
|
284
|
+
|
|
225
285
|
// --- version probe (apply-time, fire-and-forget) -----------------------------
|
|
226
286
|
|
|
227
287
|
function parseMinor(v) {
|
|
@@ -232,18 +292,32 @@ function parseMinor(v) {
|
|
|
232
292
|
// --- tools -------------------------------------------------------------------
|
|
233
293
|
|
|
234
294
|
function apply(ctx, config) {
|
|
295
|
+
// 运行态配置:settings 面板 / settings.yaml 写入经 source() 即时生效(热重载),
|
|
296
|
+
// 无 settings 服务时回退到插件行 entry config。所有工具调用都从 runtime 读取,
|
|
297
|
+
// 而不是冻结的 config —— 设置面板切换 modelType(vision|ocr)无需重启 DSH。
|
|
298
|
+
let runtime = { ...config };
|
|
299
|
+
let source = () => config;
|
|
300
|
+
installSettingsSection(ctx, SETTINGS_NAMESPACE, Config, config, {
|
|
301
|
+
setSource: (thunk) => {
|
|
302
|
+
source = thunk;
|
|
303
|
+
},
|
|
304
|
+
onChange: () => {
|
|
305
|
+
runtime = { ...source() };
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
|
|
235
309
|
const loginSeconds = (() => {
|
|
236
310
|
const raw = Number(process.env.DEEPSEEK_LOGIN_TIMEOUT);
|
|
237
311
|
if (Number.isFinite(raw) && raw > 0) return raw;
|
|
238
|
-
return
|
|
312
|
+
return runtime.loginTimeoutSeconds > 0 ? runtime.loginTimeoutSeconds : 600;
|
|
239
313
|
})();
|
|
240
314
|
|
|
241
315
|
// 版本探测:apply 时 fire-and-forget(仅用于结果附带版本警告)。
|
|
242
316
|
// 注意:二进制路径【不在此缓存】——每次工具调用经 requireBinary()
|
|
243
|
-
// 重新 resolveBinaryPath(
|
|
317
|
+
// 重新 resolveBinaryPath(runtime),用户修改 PATH / DEEPSEEK_VISIONARY_BIN
|
|
244
318
|
// 后无需重启 DSH 即生效(懒解析,成本为数次 statSync)。
|
|
245
319
|
let versionInfo = { known: false, compatible: true, version: "" };
|
|
246
|
-
const probeBinary = resolveBinaryPath(
|
|
320
|
+
const probeBinary = resolveBinaryPath(runtime);
|
|
247
321
|
if (probeBinary) {
|
|
248
322
|
runCli(probeBinary, ["--version"], { timeoutMs: 5000 })
|
|
249
323
|
.then((r) => {
|
|
@@ -273,10 +347,12 @@ function apply(ctx, config) {
|
|
|
273
347
|
"",
|
|
274
348
|
"You have native vision tools backed by DeepSeek's web vision model (no API key):",
|
|
275
349
|
"- `deepseek_vision` — analyze one or more images (local path / base64 / data URI; use `images` for multiple)",
|
|
350
|
+
"- `deepseek_ocr` — extract raw text from an image (text extraction, not interpretation): document/PDF/terminal screenshots, code, signs",
|
|
276
351
|
"- `deepseek_vision_status` — check login state",
|
|
277
352
|
"- `deepseek_vision_login` — browser auto-login",
|
|
278
353
|
"- `deepseek_vision_logout` — clear saved credentials",
|
|
279
354
|
"",
|
|
355
|
+
"Use `deepseek_vision` for understanding an image; use `deepseek_ocr` when the user wants the text content of an image verbatim.",
|
|
280
356
|
"Prefer these native tools over invoking `visionary-server` through the shell: native tools run in the host process, so session continuation and login are not restricted by the bash sandbox.",
|
|
281
357
|
"The `image`/`images` paths passed to `deepseek_vision` are read and uploaded to the DeepSeek service — only pass paths the user intends to share.",
|
|
282
358
|
].join("\n"),
|
|
@@ -285,95 +361,110 @@ function apply(ctx, config) {
|
|
|
285
361
|
|
|
286
362
|
// 懒解析:每次工具调用重新定位二进制(PATH / 环境变量改动即时生效)。
|
|
287
363
|
const requireBinary = () => {
|
|
288
|
-
const binary = resolveBinaryPath(
|
|
364
|
+
const binary = resolveBinaryPath(runtime);
|
|
289
365
|
if (!binary) throw new Error(binaryMissingHelp());
|
|
290
366
|
return binary;
|
|
291
367
|
};
|
|
292
368
|
|
|
369
|
+
// deepseek_vision / deepseek_ocr 共享执行(materialize → spawn → 原子 JSON 解析)。
|
|
370
|
+
// `subcommand`:本轮 spawn 的 CLI 子命令;`modelType` 仅 vision 生效(见
|
|
371
|
+
// buildImageCliArgs)。ocr 工具恒为 ocr 子命令,不受 config.modelType 影响。
|
|
372
|
+
async function runImageAnalysis({ subcommand, name, imageInputs, args, exec }) {
|
|
373
|
+
const bin = requireBinary();
|
|
374
|
+
if (imageInputs.length === 0) {
|
|
375
|
+
throw new Error(`${name}: at least one image is required (\`images\` or \`image\`)`);
|
|
376
|
+
}
|
|
377
|
+
const materialized = [];
|
|
378
|
+
try {
|
|
379
|
+
for (const image of imageInputs) {
|
|
380
|
+
materialized.push(await materializeImage(image));
|
|
381
|
+
}
|
|
382
|
+
const cliArgs = buildImageCliArgs(
|
|
383
|
+
subcommand,
|
|
384
|
+
materialized.map((m) => m.arg),
|
|
385
|
+
{
|
|
386
|
+
prompt: args.prompt,
|
|
387
|
+
thinking: args.thinking,
|
|
388
|
+
sessionId: args.session_id,
|
|
389
|
+
continueConversation: args.continue_conversation,
|
|
390
|
+
modelType: subcommand === "vision" ? runtime.modelType : undefined,
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const r = await runCli(bin, cliArgs, {
|
|
395
|
+
timeoutMs: runtime.visionTimeoutMs,
|
|
396
|
+
signal: exec.signal,
|
|
397
|
+
});
|
|
398
|
+
if (r.killed) throw new Error(`${name} was aborted or timed out`);
|
|
399
|
+
let parsed = null;
|
|
400
|
+
try {
|
|
401
|
+
parsed = JSON.parse(r.stdout);
|
|
402
|
+
} catch {
|
|
403
|
+
parsed = null;
|
|
404
|
+
}
|
|
405
|
+
if (parsed && typeof parsed.error === "string") {
|
|
406
|
+
throw new Error(parsed.error);
|
|
407
|
+
}
|
|
408
|
+
if (parsed && typeof parsed.text === "string") {
|
|
409
|
+
const meta = [`session_id: ${parsed.session_id}`, `parent_message_id: ${parsed.parent_message_id}`]
|
|
410
|
+
.filter((s) => !s.endsWith(": null") && !s.endsWith(": undefined"))
|
|
411
|
+
.join(", ");
|
|
412
|
+
return withVersionWarning(meta ? `${parsed.text}\n\n[${meta}]` : parsed.text);
|
|
413
|
+
}
|
|
414
|
+
throw new Error(
|
|
415
|
+
`${name} failed (exit ${r.code}): ${(r.stderr || r.stdout).trim() || "unknown error"}`
|
|
416
|
+
);
|
|
417
|
+
} finally {
|
|
418
|
+
for (const m of materialized) if (m.cleanup) await m.cleanup();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
293
422
|
ctx.tools.register(
|
|
294
423
|
defineTool({
|
|
295
424
|
name: "deepseek_vision",
|
|
296
425
|
description:
|
|
297
426
|
"Analyze one or more images with DeepSeek's web vision model (local path / base64 / data URI). Pass multiple images via `images` to have the model analyze them together in one call (like the DeepSeek website). Use for screenshots, photos, or documents with images. Supports multi-turn conversation via continue_conversation / session_id.",
|
|
298
|
-
parameters:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
description: "Enable DeepThink deep reasoning.",
|
|
315
|
-
},
|
|
316
|
-
continue_conversation: {
|
|
317
|
-
type: "boolean",
|
|
318
|
-
description: "Continue the previous session (multi-image comparison across turns).",
|
|
319
|
-
},
|
|
320
|
-
session_id: {
|
|
321
|
-
type: "string",
|
|
322
|
-
description: "Reuse an explicit session thread (takes precedence over continue_conversation).",
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
output: {
|
|
326
|
-
schema: { type: "string" },
|
|
327
|
-
render: (_args, value) => [{ type: "text", text: value }],
|
|
427
|
+
parameters: imageToolParams,
|
|
428
|
+
output: imageToolOutput,
|
|
429
|
+
timeoutMs: runtime.visionTimeoutMs,
|
|
430
|
+
async execute(args, exec) {
|
|
431
|
+
const imageInputs = Array.isArray(args.images) && args.images.length > 0
|
|
432
|
+
? args.images
|
|
433
|
+
: args.image
|
|
434
|
+
? [args.image]
|
|
435
|
+
: [];
|
|
436
|
+
return runImageAnalysis({
|
|
437
|
+
subcommand: "vision",
|
|
438
|
+
name: "deepseek_vision",
|
|
439
|
+
imageInputs,
|
|
440
|
+
args,
|
|
441
|
+
exec,
|
|
442
|
+
});
|
|
328
443
|
},
|
|
329
|
-
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
ctx.tools.register(
|
|
448
|
+
defineTool({
|
|
449
|
+
name: "deepseek_ocr",
|
|
450
|
+
description:
|
|
451
|
+
"Extract raw text from one or more images with DeepSeek's OCR pipeline (local path / base64 / data URI), equivalent to `visionary-server ocr`. Use for the text content of a screenshot or document — document/PDF screenshots, code, signs, tables (not interpretation). Text is extracted verbatim by default. Supports multi-turn conversation via continue_conversation / session_id.",
|
|
452
|
+
parameters: imageToolParams,
|
|
453
|
+
output: imageToolOutput,
|
|
454
|
+
timeoutMs: runtime.visionTimeoutMs,
|
|
330
455
|
async execute(args, exec) {
|
|
331
|
-
const bin = requireBinary();
|
|
332
456
|
const imageInputs = Array.isArray(args.images) && args.images.length > 0
|
|
333
457
|
? args.images
|
|
334
458
|
: args.image
|
|
335
459
|
? [args.image]
|
|
336
460
|
: [];
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
const cliArgs = ["vision", ...materialized.map((m) => m.arg), "--json"];
|
|
346
|
-
if (args.prompt) cliArgs.push(`--prompt=${args.prompt}`);
|
|
347
|
-
if (args.thinking) cliArgs.push("--thinking");
|
|
348
|
-
if (args.session_id) cliArgs.push(`--session-id=${args.session_id}`);
|
|
349
|
-
else if (args.continue_conversation) cliArgs.push("--continue-conversation");
|
|
350
|
-
|
|
351
|
-
const r = await runCli(bin, cliArgs, {
|
|
352
|
-
timeoutMs: config.visionTimeoutMs,
|
|
353
|
-
signal: exec.signal,
|
|
354
|
-
});
|
|
355
|
-
if (r.killed) throw new Error("deepseek_vision was aborted or timed out");
|
|
356
|
-
let parsed = null;
|
|
357
|
-
try {
|
|
358
|
-
parsed = JSON.parse(r.stdout);
|
|
359
|
-
} catch {
|
|
360
|
-
parsed = null;
|
|
361
|
-
}
|
|
362
|
-
if (parsed && typeof parsed.error === "string") {
|
|
363
|
-
throw new Error(parsed.error);
|
|
364
|
-
}
|
|
365
|
-
if (parsed && typeof parsed.text === "string") {
|
|
366
|
-
const meta = [`session_id: ${parsed.session_id}`, `parent_message_id: ${parsed.parent_message_id}`]
|
|
367
|
-
.filter((s) => !s.endsWith(": null") && !s.endsWith(": undefined"))
|
|
368
|
-
.join(", ");
|
|
369
|
-
return withVersionWarning(meta ? `${parsed.text}\n\n[${meta}]` : parsed.text);
|
|
370
|
-
}
|
|
371
|
-
throw new Error(
|
|
372
|
-
`vision failed (exit ${r.code}): ${(r.stderr || r.stdout).trim() || "unknown error"}`
|
|
373
|
-
);
|
|
374
|
-
} finally {
|
|
375
|
-
for (const m of materialized) if (m.cleanup) await m.cleanup();
|
|
376
|
-
}
|
|
461
|
+
return runImageAnalysis({
|
|
462
|
+
subcommand: "ocr",
|
|
463
|
+
name: "deepseek_ocr",
|
|
464
|
+
imageInputs,
|
|
465
|
+
args,
|
|
466
|
+
exec,
|
|
467
|
+
});
|
|
377
468
|
},
|
|
378
469
|
})
|
|
379
470
|
);
|
|
@@ -388,11 +479,11 @@ function apply(ctx, config) {
|
|
|
388
479
|
schema: { type: "string" },
|
|
389
480
|
render: (_args, value) => [{ type: "text", text: value }],
|
|
390
481
|
},
|
|
391
|
-
timeoutMs:
|
|
482
|
+
timeoutMs: runtime.statusTimeoutMs,
|
|
392
483
|
async execute(_args, exec) {
|
|
393
484
|
const bin = requireBinary();
|
|
394
485
|
const r = await runCli(bin, ["status", "--json"], {
|
|
395
|
-
timeoutMs:
|
|
486
|
+
timeoutMs: runtime.statusTimeoutMs,
|
|
396
487
|
signal: exec.signal,
|
|
397
488
|
});
|
|
398
489
|
if (r.killed) throw new Error("deepseek_vision_status was aborted or timed out");
|
|
@@ -461,11 +552,11 @@ function apply(ctx, config) {
|
|
|
461
552
|
schema: { type: "string" },
|
|
462
553
|
render: (_args, value) => [{ type: "text", text: value }],
|
|
463
554
|
},
|
|
464
|
-
timeoutMs:
|
|
555
|
+
timeoutMs: runtime.statusTimeoutMs,
|
|
465
556
|
async execute(_args, exec) {
|
|
466
557
|
const bin = requireBinary();
|
|
467
558
|
const r = await runCli(bin, ["logout"], {
|
|
468
|
-
timeoutMs:
|
|
559
|
+
timeoutMs: runtime.statusTimeoutMs,
|
|
469
560
|
signal: exec.signal,
|
|
470
561
|
});
|
|
471
562
|
if (r.killed) throw new Error("deepseek_vision_logout was aborted or timed out");
|
|
@@ -478,4 +569,19 @@ function apply(ctx, config) {
|
|
|
478
569
|
);
|
|
479
570
|
}
|
|
480
571
|
|
|
481
|
-
export {
|
|
572
|
+
export {
|
|
573
|
+
name,
|
|
574
|
+
inject,
|
|
575
|
+
Config,
|
|
576
|
+
apply,
|
|
577
|
+
SETTINGS_NAMESPACE,
|
|
578
|
+
buildImageCliArgs,
|
|
579
|
+
resolveBinaryPath,
|
|
580
|
+
resolveFromNpmShim,
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// Internal reuse: the image-bridge plugin runs in the same package but is a
|
|
584
|
+
// separate plugin row; it needs the same binary resolution + subprocess
|
|
585
|
+
// plumbing to drive deterministic-mode analysis. Export the two pieces it
|
|
586
|
+
// reuses (not the public plugin surface).
|
|
587
|
+
export { runCli, binaryMissingHelp };
|