@xlight-oss/visionary-dsh 0.6.0 → 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 +42 -14
- 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 +263 -91
- 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,8 +14,9 @@
|
|
|
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
|
-
import { statSync } from "node:fs";
|
|
19
|
+
import { statSync, readFileSync } from "node:fs";
|
|
19
20
|
import { promises as fs } from "node:fs";
|
|
20
21
|
import os from "node:os";
|
|
21
22
|
import path from "node:path";
|
|
@@ -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,15 +47,58 @@ 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 -------------------------------------------------------
|
|
54
64
|
|
|
65
|
+
// npm 全局安装(@xlight-oss/visionary-server)在 Windows 上只在 PATH 生成
|
|
66
|
+
// .cmd/.ps1 shim(node 包装脚本),真实 exe 在包内 node_modules/.bin_real/。
|
|
67
|
+
// shim 层用 spawnSync + stdio:"inherit" 转发——直接 spawn shim 会丢 stdout 管道、
|
|
68
|
+
// kill 链路断裂(孤儿进程)。故:解析 shim 文本定位 exe 真身,spawn 真身。
|
|
69
|
+
const NPM_PKG_SHIM_RE =
|
|
70
|
+
/node_modules[\\/]@xlight-oss[\\/]visionary-server[\\/]run-visionary-server\.js/;
|
|
71
|
+
|
|
72
|
+
// 从 npm shim(.cmd/.ps1)文本中解析出 exe 真身路径。
|
|
73
|
+
// shim 内容形如:... "%dp0%\node_modules\@xlight-oss\visionary-server\run-visionary-server.js" ...
|
|
74
|
+
// 包目录 = <shim目录>\node_modules\@xlight-oss\visionary-server
|
|
75
|
+
// 真身 = <包目录>\node_modules\.bin_real\visionary-server.exe
|
|
76
|
+
function resolveFromNpmShim(shimPath) {
|
|
77
|
+
let text;
|
|
78
|
+
try {
|
|
79
|
+
text = readFileSync(shimPath, "utf8");
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const m = NPM_PKG_SHIM_RE.exec(text);
|
|
84
|
+
if (!m) return null;
|
|
85
|
+
// shim 文本使用反斜杠分隔符(Windows 产物);手动切分保证在任意平台
|
|
86
|
+
// (包括测试跑的 macOS/Linux)都能解析,不依赖 path.dirname 的分隔符语义。
|
|
87
|
+
const pkgParts = m[0].split(/[\\/]+/).filter(Boolean);
|
|
88
|
+
if (pkgParts.length < 3) return null;
|
|
89
|
+
// node_modules\@xlight-oss\visionary-server\run-visionary-server.js → 去掉末段(文件名)
|
|
90
|
+
// npm shim 是 Windows 产物(.cmd/.ps1),包内 exe 恒为 visionary-server.exe,
|
|
91
|
+
// 与 process.platform 无关——固定扩展名保证任意平台测试/解析一致。
|
|
92
|
+
const pkgDir = path.join(path.dirname(shimPath), ...pkgParts.slice(0, -1));
|
|
93
|
+
const candidate = path.join(pkgDir, "node_modules", ".bin_real", "visionary-server.exe");
|
|
94
|
+
try {
|
|
95
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
96
|
+
} catch {
|
|
97
|
+
// keep looking
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
55
102
|
function resolveBinaryPath(config) {
|
|
56
103
|
if (config.binaryPath) return config.binaryPath;
|
|
57
104
|
const fromEnv = process.env.DEEPSEEK_VISIONARY_BIN;
|
|
@@ -65,17 +112,40 @@ function resolveBinaryPath(config) {
|
|
|
65
112
|
// keep looking
|
|
66
113
|
}
|
|
67
114
|
}
|
|
115
|
+
// win32 追加:npm 全局包 shim(.cmd / .ps1)→ 解析 exe 真身
|
|
116
|
+
if (process.platform === "win32") {
|
|
117
|
+
for (const shimName of ["visionary-server.cmd", "visionary-server.ps1"]) {
|
|
118
|
+
for (const dir of (process.env.PATH || "").split(path.delimiter).filter(Boolean)) {
|
|
119
|
+
const shim = path.join(dir, shimName);
|
|
120
|
+
try {
|
|
121
|
+
if (statSync(shim).isFile()) {
|
|
122
|
+
const resolved = resolveFromNpmShim(shim);
|
|
123
|
+
if (resolved) return resolved;
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// keep looking
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
68
131
|
return null;
|
|
69
132
|
}
|
|
70
133
|
|
|
71
134
|
const binaryMissingHelp = () =>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
135
|
+
process.platform === "win32"
|
|
136
|
+
? [
|
|
137
|
+
"visionary-server binary not found. Install it and retry:",
|
|
138
|
+
" - npm: npm install -g @xlight-oss/visionary-server (then restart DSH)",
|
|
139
|
+
" - or download from GitHub Releases: https://github.com/xlight/deepseek-visionary/releases/latest",
|
|
140
|
+
"Or point the plugin at the binary via Config.binaryPath / DEEPSEEK_VISIONARY_BIN.",
|
|
141
|
+
].join("\n")
|
|
142
|
+
: [
|
|
143
|
+
"visionary-server binary not found. Install it and retry:",
|
|
144
|
+
" - One-liner: curl -LsSf https://github.com/xlight/deepseek-visionary/releases/latest/download/visionary-server-installer.sh | sh",
|
|
145
|
+
" - Homebrew: brew install <tap>/visionary-server",
|
|
146
|
+
" - npm: npm install -g @xlight-oss/visionary-server",
|
|
147
|
+
"Or point the plugin at the binary via Config.binaryPath / DEEPSEEK_VISIONARY_BIN.",
|
|
148
|
+
].join("\n");
|
|
79
149
|
|
|
80
150
|
// --- subprocess --------------------------------------------------------------
|
|
81
151
|
|
|
@@ -162,6 +232,56 @@ async function materializeImage(image) {
|
|
|
162
232
|
return { arg: image, cleanup: null };
|
|
163
233
|
}
|
|
164
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
|
+
|
|
165
285
|
// --- version probe (apply-time, fire-and-forget) -----------------------------
|
|
166
286
|
|
|
167
287
|
function parseMinor(v) {
|
|
@@ -172,16 +292,34 @@ function parseMinor(v) {
|
|
|
172
292
|
// --- tools -------------------------------------------------------------------
|
|
173
293
|
|
|
174
294
|
function apply(ctx, config) {
|
|
175
|
-
|
|
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
|
+
|
|
176
309
|
const loginSeconds = (() => {
|
|
177
310
|
const raw = Number(process.env.DEEPSEEK_LOGIN_TIMEOUT);
|
|
178
311
|
if (Number.isFinite(raw) && raw > 0) return raw;
|
|
179
|
-
return
|
|
312
|
+
return runtime.loginTimeoutSeconds > 0 ? runtime.loginTimeoutSeconds : 600;
|
|
180
313
|
})();
|
|
181
314
|
|
|
315
|
+
// 版本探测:apply 时 fire-and-forget(仅用于结果附带版本警告)。
|
|
316
|
+
// 注意:二进制路径【不在此缓存】——每次工具调用经 requireBinary()
|
|
317
|
+
// 重新 resolveBinaryPath(runtime),用户修改 PATH / DEEPSEEK_VISIONARY_BIN
|
|
318
|
+
// 后无需重启 DSH 即生效(懒解析,成本为数次 statSync)。
|
|
182
319
|
let versionInfo = { known: false, compatible: true, version: "" };
|
|
183
|
-
|
|
184
|
-
|
|
320
|
+
const probeBinary = resolveBinaryPath(runtime);
|
|
321
|
+
if (probeBinary) {
|
|
322
|
+
runCli(probeBinary, ["--version"], { timeoutMs: 5000 })
|
|
185
323
|
.then((r) => {
|
|
186
324
|
const version = (r.stdout || r.stderr).trim();
|
|
187
325
|
versionInfo = {
|
|
@@ -209,105 +347,124 @@ function apply(ctx, config) {
|
|
|
209
347
|
"",
|
|
210
348
|
"You have native vision tools backed by DeepSeek's web vision model (no API key):",
|
|
211
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",
|
|
212
351
|
"- `deepseek_vision_status` — check login state",
|
|
213
352
|
"- `deepseek_vision_login` — browser auto-login",
|
|
214
353
|
"- `deepseek_vision_logout` — clear saved credentials",
|
|
215
354
|
"",
|
|
355
|
+
"Use `deepseek_vision` for understanding an image; use `deepseek_ocr` when the user wants the text content of an image verbatim.",
|
|
216
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.",
|
|
217
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.",
|
|
218
358
|
].join("\n"),
|
|
219
359
|
});
|
|
220
360
|
}
|
|
221
361
|
|
|
362
|
+
// 懒解析:每次工具调用重新定位二进制(PATH / 环境变量改动即时生效)。
|
|
222
363
|
const requireBinary = () => {
|
|
364
|
+
const binary = resolveBinaryPath(runtime);
|
|
223
365
|
if (!binary) throw new Error(binaryMissingHelp());
|
|
224
366
|
return binary;
|
|
225
367
|
};
|
|
226
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
|
+
|
|
227
422
|
ctx.tools.register(
|
|
228
423
|
defineTool({
|
|
229
424
|
name: "deepseek_vision",
|
|
230
425
|
description:
|
|
231
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.",
|
|
232
|
-
parameters:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
description: "Enable DeepThink deep reasoning.",
|
|
249
|
-
},
|
|
250
|
-
continue_conversation: {
|
|
251
|
-
type: "boolean",
|
|
252
|
-
description: "Continue the previous session (multi-image comparison across turns).",
|
|
253
|
-
},
|
|
254
|
-
session_id: {
|
|
255
|
-
type: "string",
|
|
256
|
-
description: "Reuse an explicit session thread (takes precedence over continue_conversation).",
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
output: {
|
|
260
|
-
schema: { type: "string" },
|
|
261
|
-
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
|
+
});
|
|
262
443
|
},
|
|
263
|
-
|
|
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,
|
|
264
455
|
async execute(args, exec) {
|
|
265
|
-
const bin = requireBinary();
|
|
266
456
|
const imageInputs = Array.isArray(args.images) && args.images.length > 0
|
|
267
457
|
? args.images
|
|
268
458
|
: args.image
|
|
269
459
|
? [args.image]
|
|
270
460
|
: [];
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
const cliArgs = ["vision", ...materialized.map((m) => m.arg), "--json"];
|
|
280
|
-
if (args.prompt) cliArgs.push(`--prompt=${args.prompt}`);
|
|
281
|
-
if (args.thinking) cliArgs.push("--thinking");
|
|
282
|
-
if (args.session_id) cliArgs.push(`--session-id=${args.session_id}`);
|
|
283
|
-
else if (args.continue_conversation) cliArgs.push("--continue-conversation");
|
|
284
|
-
|
|
285
|
-
const r = await runCli(bin, cliArgs, {
|
|
286
|
-
timeoutMs: config.visionTimeoutMs,
|
|
287
|
-
signal: exec.signal,
|
|
288
|
-
});
|
|
289
|
-
if (r.killed) throw new Error("deepseek_vision was aborted or timed out");
|
|
290
|
-
let parsed = null;
|
|
291
|
-
try {
|
|
292
|
-
parsed = JSON.parse(r.stdout);
|
|
293
|
-
} catch {
|
|
294
|
-
parsed = null;
|
|
295
|
-
}
|
|
296
|
-
if (parsed && typeof parsed.error === "string") {
|
|
297
|
-
throw new Error(parsed.error);
|
|
298
|
-
}
|
|
299
|
-
if (parsed && typeof parsed.text === "string") {
|
|
300
|
-
const meta = [`session_id: ${parsed.session_id}`, `parent_message_id: ${parsed.parent_message_id}`]
|
|
301
|
-
.filter((s) => !s.endsWith(": null") && !s.endsWith(": undefined"))
|
|
302
|
-
.join(", ");
|
|
303
|
-
return withVersionWarning(meta ? `${parsed.text}\n\n[${meta}]` : parsed.text);
|
|
304
|
-
}
|
|
305
|
-
throw new Error(
|
|
306
|
-
`vision failed (exit ${r.code}): ${(r.stderr || r.stdout).trim() || "unknown error"}`
|
|
307
|
-
);
|
|
308
|
-
} finally {
|
|
309
|
-
for (const m of materialized) if (m.cleanup) await m.cleanup();
|
|
310
|
-
}
|
|
461
|
+
return runImageAnalysis({
|
|
462
|
+
subcommand: "ocr",
|
|
463
|
+
name: "deepseek_ocr",
|
|
464
|
+
imageInputs,
|
|
465
|
+
args,
|
|
466
|
+
exec,
|
|
467
|
+
});
|
|
311
468
|
},
|
|
312
469
|
})
|
|
313
470
|
);
|
|
@@ -322,11 +479,11 @@ function apply(ctx, config) {
|
|
|
322
479
|
schema: { type: "string" },
|
|
323
480
|
render: (_args, value) => [{ type: "text", text: value }],
|
|
324
481
|
},
|
|
325
|
-
timeoutMs:
|
|
482
|
+
timeoutMs: runtime.statusTimeoutMs,
|
|
326
483
|
async execute(_args, exec) {
|
|
327
484
|
const bin = requireBinary();
|
|
328
485
|
const r = await runCli(bin, ["status", "--json"], {
|
|
329
|
-
timeoutMs:
|
|
486
|
+
timeoutMs: runtime.statusTimeoutMs,
|
|
330
487
|
signal: exec.signal,
|
|
331
488
|
});
|
|
332
489
|
if (r.killed) throw new Error("deepseek_vision_status was aborted or timed out");
|
|
@@ -395,11 +552,11 @@ function apply(ctx, config) {
|
|
|
395
552
|
schema: { type: "string" },
|
|
396
553
|
render: (_args, value) => [{ type: "text", text: value }],
|
|
397
554
|
},
|
|
398
|
-
timeoutMs:
|
|
555
|
+
timeoutMs: runtime.statusTimeoutMs,
|
|
399
556
|
async execute(_args, exec) {
|
|
400
557
|
const bin = requireBinary();
|
|
401
558
|
const r = await runCli(bin, ["logout"], {
|
|
402
|
-
timeoutMs:
|
|
559
|
+
timeoutMs: runtime.statusTimeoutMs,
|
|
403
560
|
signal: exec.signal,
|
|
404
561
|
});
|
|
405
562
|
if (r.killed) throw new Error("deepseek_vision_logout was aborted or timed out");
|
|
@@ -412,4 +569,19 @@ function apply(ctx, config) {
|
|
|
412
569
|
);
|
|
413
570
|
}
|
|
414
571
|
|
|
415
|
-
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 };
|