@xlight-oss/visionary-dsh 0.5.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 +85 -0
- package/cordis.patch.yml +6 -0
- package/lib/index.mjs +400 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @xlight-oss/visionary-dsh
|
|
2
|
+
|
|
3
|
+
[DeepSeek Visionary](https://github.com/xlight/deepseek-visionary) 的 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)原生插件:把 `deepseek_vision` / `deepseek_vision_status` / `deepseek_vision_login` / `deepseek_vision_logout` 注册为 DSH 原生工具,由 `visionary-server` CLI 支撑(DeepSeek 网页版视觉模型,**无需 API key**)。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **原生工具** — 结构化参数 schema 注册到 `ctx.tools`,模型直接调用,无 MCP 中间层
|
|
8
|
+
- **复用 Rust 管道** — 每个工具 spawn `visionary-server`(PoW → 上传 → fork → HIF → SSE 全部在 Rust 侧),插件仅做参数映射与 JSON 解析
|
|
9
|
+
- **宿主级权限** — 工具在 DSH 宿主进程执行(不经 bash 沙箱),会话续聊与浏览器登录不受 workspace-write 限制
|
|
10
|
+
- **超时有界可取消** — 每个工具声明 `timeoutMs` 并转发 `exec.signal`(abort → kill 子进程)
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# npm 包(发布后)
|
|
16
|
+
dsh plugin --profile web add @xlight-oss/visionary-dsh
|
|
17
|
+
|
|
18
|
+
# 本地路径(开发验证)
|
|
19
|
+
dsh plugin --profile web add /path/to/packages/dsh-plugin
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`dsh plugin` 会把包安装进 profile 并通过 `dsh.bundle` 声明自动追加到 `dsh.profile.bundles` 层叠——**无需手写任何配置**。重启 DSH 后 4 个工具出现在工具目录。
|
|
23
|
+
|
|
24
|
+
验证:`dsh --profile web --dump-config` 应出现 `@xlight-oss/visionary-dsh` 层与 `visionary-vision` 插件行。
|
|
25
|
+
|
|
26
|
+
> **本地路径(开发)安装**:`dsh plugin add <path>` 以 link 方式安装,Node 从包的真实位置解析其 peer 依赖,因此本地开发需先在包目录 `pnpm install`(peer 已镜像为 devDependencies,见 `package.json`),否则加载时报 `Cannot find package '@deepseek-ai/dsh-tools'`。已发布的 npm 包无此要求(DSH 的 `profiles/node_modules` 兜底解析)。
|
|
27
|
+
|
|
28
|
+
## 前置要求
|
|
29
|
+
|
|
30
|
+
`visionary-server` 二进制需可被找到(三者任一):
|
|
31
|
+
|
|
32
|
+
1. `Config.binaryPath`(插件配置,绝对路径)
|
|
33
|
+
2. `DEEPSEEK_VISIONARY_BIN` 环境变量
|
|
34
|
+
3. 在 PATH 中
|
|
35
|
+
|
|
36
|
+
安装二进制见 [DeepSeek Visionary 安装章节](https://github.com/xlight/deepseek-visionary#安装)(install.sh / brew / npm)。未找到时工具返回含安装指引的错误。
|
|
37
|
+
|
|
38
|
+
## 配置
|
|
39
|
+
|
|
40
|
+
在 DSH profile 的 `cordis.patch.yml`(或 `$DSH_HOME/cordis.patch.yml`)给 `visionary-vision` 行补 `config`:
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
- id: visionary-vision
|
|
44
|
+
config:
|
|
45
|
+
binaryPath: /usr/local/bin/visionary-server
|
|
46
|
+
loginTimeoutSeconds: 900 # 不设则读 DEEPSEEK_LOGIN_TIMEOUT env(默认 600)
|
|
47
|
+
visionTimeoutMs: 300000
|
|
48
|
+
statusTimeoutMs: 60000
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> patch 层按 `id` 整行替换 `config`(不做键级深合并):覆盖 `visionary-vision` 时,未写出的字段回退到下方表格中的 schema 默认值,而非保留插件包内的配置。
|
|
52
|
+
|
|
53
|
+
| 字段 | 默认 | 说明 |
|
|
54
|
+
|------|------|------|
|
|
55
|
+
| `binaryPath` | `""`(env → PATH) | 二进制绝对路径 |
|
|
56
|
+
| `loginTimeoutSeconds` | 600(`DEEPSEEK_LOGIN_TIMEOUT` env 优先) | 登录等待超时(秒) |
|
|
57
|
+
| `visionTimeoutMs` | 300000 | `deepseek_vision` 单次超时 |
|
|
58
|
+
| `statusTimeoutMs` | 60000 | status / logout 超时 |
|
|
59
|
+
|
|
60
|
+
## 工具
|
|
61
|
+
|
|
62
|
+
| 工具 | 说明 |
|
|
63
|
+
|------|------|
|
|
64
|
+
| `deepseek_vision` | 识图(路径 / base64 / data URI),支持 `prompt` / `thinking` / `continue_conversation` / `session_id` 多轮续聊 |
|
|
65
|
+
| `deepseek_vision_status` | 登录状态检查(含真实 token 探针) |
|
|
66
|
+
| `deepseek_vision_login` | 浏览器自动登录(阻塞,超时可配) |
|
|
67
|
+
| `deepseek_vision_logout` | 清除保存的凭据 |
|
|
68
|
+
|
|
69
|
+
## 与其他接入路径的关系
|
|
70
|
+
|
|
71
|
+
| 路径 | 适用 |
|
|
72
|
+
|------|------|
|
|
73
|
+
| **本插件(推荐)** | DSH 用户:原生工具、结构化 schema、宿主级权限、续聊/登录不受沙箱限制 |
|
|
74
|
+
| skill + CLI(`init dsh` / `skill install`) | 任何能执行 shell 的 agent:零安装配置,模型经 bash 调 `visionary-server vision <image> --json`;DSH 下续聊/登录受 bash 沙箱写限制 |
|
|
75
|
+
| MCP(`mcp-stdio` + 各宿主配置) | 需要标准 MCP 工具面时(Zed / OpenCode / Codex / Claude Code 等) |
|
|
76
|
+
|
|
77
|
+
三者共用同一二进制与同一份凭据(`~/.deepseek-visionary/config.json`),可并存。
|
|
78
|
+
|
|
79
|
+
## 安全提示
|
|
80
|
+
|
|
81
|
+
`deepseek_vision` 的 `image` 参数指向的文件会被**读取并上传**至 chat.deepseek.com——仅传用户有意分享的路径。
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// DeepSeek Visionary — DeepSeek Harness native plugin.
|
|
2
|
+
//
|
|
3
|
+
// Registers `deepseek_vision` / `deepseek_vision_status` / `deepseek_vision_login`
|
|
4
|
+
// / `deepseek_vision_logout` as native tools on `ctx.tools`. Every tool spawns the
|
|
5
|
+
// `visionary-server` binary (CLI; `--json` atomic output where the subcommand
|
|
6
|
+
// supports it), so the heavy vision pipeline (PoW → upload → fork → HIF → SSE)
|
|
7
|
+
// stays in Rust. Tools run in the DSH host process — not through the bash
|
|
8
|
+
// sandbox — so session continuation and browser login are not restricted by the
|
|
9
|
+
// workspace-write file sandbox.
|
|
10
|
+
//
|
|
11
|
+
// Tool-call timeout is declared per tool (`ToolDefinition.timeoutMs`); DSH's
|
|
12
|
+
// timeout-policy enforces it cooperatively through `exec.signal`, which this
|
|
13
|
+
// plugin forwards to the spawned child (abort → kill).
|
|
14
|
+
|
|
15
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
16
|
+
import z from "@deepseek-ai/schemastery";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import { statSync } from "node:fs";
|
|
19
|
+
import { promises as fs } from "node:fs";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
|
|
23
|
+
const name = "visionary-vision";
|
|
24
|
+
const inject = ["tools", "systemPrompt"];
|
|
25
|
+
|
|
26
|
+
// Keep in lockstep with the Rust binary's minor version: tools rely on the
|
|
27
|
+
// CLI's `--json` output shape. Bump when the binary's contract changes.
|
|
28
|
+
const COMPAT_MINOR = "0.4";
|
|
29
|
+
|
|
30
|
+
const Config = z.object({
|
|
31
|
+
binaryPath: z
|
|
32
|
+
.string()
|
|
33
|
+
.default("")
|
|
34
|
+
.description(
|
|
35
|
+
"Absolute path to the visionary-server binary. Empty resolves via DEEPSEEK_VISIONARY_BIN, then PATH."
|
|
36
|
+
),
|
|
37
|
+
loginTimeoutSeconds: z
|
|
38
|
+
.number()
|
|
39
|
+
.default(600)
|
|
40
|
+
.description(
|
|
41
|
+
"Login wait timeout in seconds. DEEPSEEK_LOGIN_TIMEOUT env overrides when set (matching the CLI's own login deadline); schema default 600."
|
|
42
|
+
),
|
|
43
|
+
visionTimeoutMs: z
|
|
44
|
+
.number()
|
|
45
|
+
.default(300000)
|
|
46
|
+
.description("Per deepseek_vision call timeout in ms."),
|
|
47
|
+
statusTimeoutMs: z
|
|
48
|
+
.number()
|
|
49
|
+
.default(60000)
|
|
50
|
+
.description("Per deepseek_vision_status / deepseek_vision_logout timeout in ms."),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// --- binary resolution -------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
function resolveBinaryPath(config) {
|
|
56
|
+
if (config.binaryPath) return config.binaryPath;
|
|
57
|
+
const fromEnv = process.env.DEEPSEEK_VISIONARY_BIN;
|
|
58
|
+
if (fromEnv) return fromEnv;
|
|
59
|
+
const exe = process.platform === "win32" ? "visionary-server.exe" : "visionary-server";
|
|
60
|
+
for (const dir of (process.env.PATH || "").split(path.delimiter).filter(Boolean)) {
|
|
61
|
+
const candidate = path.join(dir, exe);
|
|
62
|
+
try {
|
|
63
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
64
|
+
} catch {
|
|
65
|
+
// keep looking
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const binaryMissingHelp = () =>
|
|
72
|
+
[
|
|
73
|
+
"visionary-server binary not found. Install it and retry:",
|
|
74
|
+
" - One-liner: curl -LsSf https://github.com/xlight/deepseek-visionary/releases/latest/download/visionary-server-installer.sh | sh",
|
|
75
|
+
" - Homebrew: brew install <tap>/visionary-server",
|
|
76
|
+
" - npm: npm install -g @xlight-oss/visionary-server",
|
|
77
|
+
"Or point the plugin at the binary via Config.binaryPath / DEEPSEEK_VISIONARY_BIN.",
|
|
78
|
+
].join("\n");
|
|
79
|
+
|
|
80
|
+
// --- subprocess --------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function runCli(binary, args, { timeoutMs, signal }) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
let child;
|
|
85
|
+
try {
|
|
86
|
+
child = spawn(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
87
|
+
} catch (err) {
|
|
88
|
+
reject(err);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let stdout = "";
|
|
92
|
+
let stderr = "";
|
|
93
|
+
let settled = false;
|
|
94
|
+
let killed = false;
|
|
95
|
+
child.stdout?.on("data", (d) => (stdout += d));
|
|
96
|
+
child.stderr?.on("data", (d) => (stderr += d));
|
|
97
|
+
|
|
98
|
+
const kill = () => {
|
|
99
|
+
killed = true;
|
|
100
|
+
child.kill("SIGKILL");
|
|
101
|
+
};
|
|
102
|
+
const timer = setTimeout(kill, timeoutMs);
|
|
103
|
+
const onAbort = () => kill();
|
|
104
|
+
if (signal) {
|
|
105
|
+
if (signal.aborted) kill();
|
|
106
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
child.on("error", (err) => {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
112
|
+
if (settled) return;
|
|
113
|
+
settled = true;
|
|
114
|
+
if (err.code === "ENOENT") reject(new Error(binaryMissingHelp()));
|
|
115
|
+
else reject(err);
|
|
116
|
+
});
|
|
117
|
+
child.on("close", (code) => {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
120
|
+
if (settled) return;
|
|
121
|
+
settled = true;
|
|
122
|
+
resolve({ code: code ?? -1, stdout, stderr, killed });
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- image input -------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
// Mirror the CLI's read_image semantics: "-" (stdin) is not passed by this
|
|
130
|
+
// plugin; "data:" prefixed input and long decodable base64 are materialized to
|
|
131
|
+
// a temp file so the payload never travels as a giant argv entry (Linux caps a
|
|
132
|
+
// single argument at 131072 bytes → E2BIG on real screenshots).
|
|
133
|
+
async function materializeImage(image) {
|
|
134
|
+
let data = null;
|
|
135
|
+
if (image.startsWith("data:")) {
|
|
136
|
+
const comma = image.indexOf(",");
|
|
137
|
+
if (comma === -1) throw new Error("invalid data URI for image");
|
|
138
|
+
const meta = image.slice(0, comma);
|
|
139
|
+
const payload = image.slice(comma + 1);
|
|
140
|
+
data = /;base64$/i.test(meta)
|
|
141
|
+
? Buffer.from(payload, "base64")
|
|
142
|
+
: Buffer.from(decodeURIComponent(payload), "utf8");
|
|
143
|
+
} else if (
|
|
144
|
+
image.length > 100 &&
|
|
145
|
+
image.length % 4 === 0 &&
|
|
146
|
+
/^[A-Za-z0-9+/=\r\n]+$/.test(image)
|
|
147
|
+
) {
|
|
148
|
+
try {
|
|
149
|
+
data = Buffer.from(image, "base64");
|
|
150
|
+
} catch {
|
|
151
|
+
data = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (data !== null) {
|
|
155
|
+
const tmp = path.join(
|
|
156
|
+
os.tmpdir(),
|
|
157
|
+
`visionary-${Date.now()}-${Math.random().toString(36).slice(2)}.img`
|
|
158
|
+
);
|
|
159
|
+
await fs.writeFile(tmp, data);
|
|
160
|
+
return { arg: tmp, cleanup: () => fs.rm(tmp, { force: true }).catch(() => {}) };
|
|
161
|
+
}
|
|
162
|
+
return { arg: image, cleanup: null };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- version probe (apply-time, fire-and-forget) -----------------------------
|
|
166
|
+
|
|
167
|
+
function parseMinor(v) {
|
|
168
|
+
const m = /(\d+)\.(\d+)/.exec(v || "");
|
|
169
|
+
return m ? `${m[1]}.${m[2]}` : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- tools -------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
function apply(ctx, config) {
|
|
175
|
+
const binary = resolveBinaryPath(config);
|
|
176
|
+
const loginSeconds = (() => {
|
|
177
|
+
const raw = Number(process.env.DEEPSEEK_LOGIN_TIMEOUT);
|
|
178
|
+
if (Number.isFinite(raw) && raw > 0) return raw;
|
|
179
|
+
return config.loginTimeoutSeconds > 0 ? config.loginTimeoutSeconds : 600;
|
|
180
|
+
})();
|
|
181
|
+
|
|
182
|
+
let versionInfo = { known: false, compatible: true, version: "" };
|
|
183
|
+
if (binary) {
|
|
184
|
+
runCli(binary, ["--version"], { timeoutMs: 5000 })
|
|
185
|
+
.then((r) => {
|
|
186
|
+
const version = (r.stdout || r.stderr).trim();
|
|
187
|
+
versionInfo = {
|
|
188
|
+
known: true,
|
|
189
|
+
compatible: parseMinor(version) === COMPAT_MINOR,
|
|
190
|
+
version,
|
|
191
|
+
};
|
|
192
|
+
})
|
|
193
|
+
.catch(() => {
|
|
194
|
+
versionInfo = { known: true, compatible: false, version: "unknown" };
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const withVersionWarning = (text) => {
|
|
199
|
+
if (!versionInfo.known || versionInfo.compatible) return text;
|
|
200
|
+
return `${text}\n\n[warning] visionary-server version "${versionInfo.version}" does not match the plugin's compatible version ${COMPAT_MINOR}.x — upgrade the binary or the plugin.`;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
if (ctx.systemPrompt?.section) {
|
|
204
|
+
ctx.systemPrompt.section({
|
|
205
|
+
name: "visionary-vision",
|
|
206
|
+
order: 150,
|
|
207
|
+
text: () => [
|
|
208
|
+
"## Vision (DeepSeek Visionary)",
|
|
209
|
+
"",
|
|
210
|
+
"You have native vision tools backed by DeepSeek's web vision model (no API key):",
|
|
211
|
+
"- `deepseek_vision` — analyze an image (local path / base64 / data URI)",
|
|
212
|
+
"- `deepseek_vision_status` — check login state",
|
|
213
|
+
"- `deepseek_vision_login` — browser auto-login",
|
|
214
|
+
"- `deepseek_vision_logout` — clear saved credentials",
|
|
215
|
+
"",
|
|
216
|
+
"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
|
+
"The `image` path passed to `deepseek_vision` is read and uploaded to the DeepSeek service — only pass paths the user intends to share.",
|
|
218
|
+
].join("\n"),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const requireBinary = () => {
|
|
223
|
+
if (!binary) throw new Error(binaryMissingHelp());
|
|
224
|
+
return binary;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
ctx.tools.register(
|
|
228
|
+
defineTool({
|
|
229
|
+
name: "deepseek_vision",
|
|
230
|
+
description:
|
|
231
|
+
"Analyze an image with DeepSeek's web vision model (local path / base64 / data URI). Use for screenshots, photos, or documents with images. Supports multi-turn conversation via continue_conversation / session_id.",
|
|
232
|
+
parameters: {
|
|
233
|
+
image: {
|
|
234
|
+
type: "string",
|
|
235
|
+
required: true,
|
|
236
|
+
description: "Image: local file path, base64, or data URI.",
|
|
237
|
+
},
|
|
238
|
+
prompt: {
|
|
239
|
+
type: "string",
|
|
240
|
+
description: "Question about the image (default: detailed description in Chinese).",
|
|
241
|
+
},
|
|
242
|
+
thinking: {
|
|
243
|
+
type: "boolean",
|
|
244
|
+
description: "Enable DeepThink deep reasoning.",
|
|
245
|
+
},
|
|
246
|
+
continue_conversation: {
|
|
247
|
+
type: "boolean",
|
|
248
|
+
description: "Continue the previous session (multi-image comparison).",
|
|
249
|
+
},
|
|
250
|
+
session_id: {
|
|
251
|
+
type: "string",
|
|
252
|
+
description: "Reuse an explicit session thread (takes precedence over continue_conversation).",
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
output: {
|
|
256
|
+
schema: { type: "string" },
|
|
257
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
258
|
+
},
|
|
259
|
+
timeoutMs: config.visionTimeoutMs,
|
|
260
|
+
async execute(args, exec) {
|
|
261
|
+
const bin = requireBinary();
|
|
262
|
+
const { arg, cleanup } = await materializeImage(args.image);
|
|
263
|
+
try {
|
|
264
|
+
const cliArgs = ["vision", arg, "--json"];
|
|
265
|
+
if (args.prompt) cliArgs.push(`--prompt=${args.prompt}`);
|
|
266
|
+
if (args.thinking) cliArgs.push("--thinking");
|
|
267
|
+
if (args.session_id) cliArgs.push(`--session-id=${args.session_id}`);
|
|
268
|
+
else if (args.continue_conversation) cliArgs.push("--continue");
|
|
269
|
+
|
|
270
|
+
const r = await runCli(bin, cliArgs, {
|
|
271
|
+
timeoutMs: config.visionTimeoutMs,
|
|
272
|
+
signal: exec.signal,
|
|
273
|
+
});
|
|
274
|
+
if (r.killed) throw new Error("deepseek_vision was aborted or timed out");
|
|
275
|
+
let parsed = null;
|
|
276
|
+
try {
|
|
277
|
+
parsed = JSON.parse(r.stdout);
|
|
278
|
+
} catch {
|
|
279
|
+
parsed = null;
|
|
280
|
+
}
|
|
281
|
+
if (parsed && typeof parsed.error === "string") {
|
|
282
|
+
throw new Error(parsed.error);
|
|
283
|
+
}
|
|
284
|
+
if (parsed && typeof parsed.text === "string") {
|
|
285
|
+
const meta = [`session_id: ${parsed.session_id}`, `parent_message_id: ${parsed.parent_message_id}`]
|
|
286
|
+
.filter((s) => !s.endsWith(": null") && !s.endsWith(": undefined"))
|
|
287
|
+
.join(", ");
|
|
288
|
+
return withVersionWarning(meta ? `${parsed.text}\n\n[${meta}]` : parsed.text);
|
|
289
|
+
}
|
|
290
|
+
throw new Error(
|
|
291
|
+
`vision failed (exit ${r.code}): ${(r.stderr || r.stdout).trim() || "unknown error"}`
|
|
292
|
+
);
|
|
293
|
+
} finally {
|
|
294
|
+
if (cleanup) await cleanup();
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
})
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
ctx.tools.register(
|
|
301
|
+
defineTool({
|
|
302
|
+
name: "deepseek_vision_status",
|
|
303
|
+
description:
|
|
304
|
+
"Check DeepSeek Vision login state (authenticated, token validity). Run before deepseek_vision if unsure whether the user is logged in.",
|
|
305
|
+
parameters: {},
|
|
306
|
+
output: {
|
|
307
|
+
schema: { type: "string" },
|
|
308
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
309
|
+
},
|
|
310
|
+
timeoutMs: config.statusTimeoutMs,
|
|
311
|
+
async execute(_args, exec) {
|
|
312
|
+
const bin = requireBinary();
|
|
313
|
+
const r = await runCli(bin, ["status", "--json"], {
|
|
314
|
+
timeoutMs: config.statusTimeoutMs,
|
|
315
|
+
signal: exec.signal,
|
|
316
|
+
});
|
|
317
|
+
if (r.killed) throw new Error("deepseek_vision_status was aborted or timed out");
|
|
318
|
+
// `status --json` prints a complete JSON document even when unauthenticated
|
|
319
|
+
// and exits non-zero — the JSON is authoritative, not the exit code.
|
|
320
|
+
let parsed = null;
|
|
321
|
+
try {
|
|
322
|
+
parsed = JSON.parse(r.stdout);
|
|
323
|
+
} catch {
|
|
324
|
+
parsed = null;
|
|
325
|
+
}
|
|
326
|
+
if (parsed) {
|
|
327
|
+
const state = parsed.token_valid
|
|
328
|
+
? "authenticated, token valid"
|
|
329
|
+
: parsed.authenticated
|
|
330
|
+
? "authenticated (live probe pending)"
|
|
331
|
+
: "not logged in";
|
|
332
|
+
const hint = parsed.token_valid
|
|
333
|
+
? ""
|
|
334
|
+
: "\nRun `deepseek_vision_login` to auto-login, or set DEEPSEEK_USER_TOKEN.";
|
|
335
|
+
return withVersionWarning(`DeepSeek Vision status: ${state}${hint}`);
|
|
336
|
+
}
|
|
337
|
+
throw new Error(
|
|
338
|
+
`status failed (exit ${r.code}): ${(r.stderr || r.stdout).trim() || "unknown error"}`
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
})
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
ctx.tools.register(
|
|
345
|
+
defineTool({
|
|
346
|
+
name: "deepseek_vision_login",
|
|
347
|
+
description:
|
|
348
|
+
"Browser auto-login to chat.deepseek.com (opens a browser window and waits for the user to log in). Blocks until login completes or the timeout elapses; the browser stays open on timeout so the user can finish and retry.",
|
|
349
|
+
parameters: {},
|
|
350
|
+
output: {
|
|
351
|
+
schema: { type: "string" },
|
|
352
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
353
|
+
},
|
|
354
|
+
timeoutMs: loginSeconds * 1000,
|
|
355
|
+
async execute(_args, exec) {
|
|
356
|
+
const bin = requireBinary();
|
|
357
|
+
const r = await runCli(bin, ["login"], {
|
|
358
|
+
timeoutMs: loginSeconds * 1000,
|
|
359
|
+
signal: exec.signal,
|
|
360
|
+
});
|
|
361
|
+
if (r.killed) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`deepseek_vision_login timed out after ${loginSeconds}s — the browser stays open; finish logging in and retry, or set DEEPSEEK_LOGIN_TIMEOUT for a longer wait.`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (r.code !== 0) {
|
|
367
|
+
throw new Error(`login failed (exit ${r.code}): ${(r.stderr || r.stdout).trim()}`);
|
|
368
|
+
}
|
|
369
|
+
return withVersionWarning(r.stdout.trim() || "Logged in.");
|
|
370
|
+
},
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
ctx.tools.register(
|
|
375
|
+
defineTool({
|
|
376
|
+
name: "deepseek_vision_logout",
|
|
377
|
+
description: "Clear saved DeepSeek Vision credentials.",
|
|
378
|
+
parameters: {},
|
|
379
|
+
output: {
|
|
380
|
+
schema: { type: "string" },
|
|
381
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
382
|
+
},
|
|
383
|
+
timeoutMs: config.statusTimeoutMs,
|
|
384
|
+
async execute(_args, exec) {
|
|
385
|
+
const bin = requireBinary();
|
|
386
|
+
const r = await runCli(bin, ["logout"], {
|
|
387
|
+
timeoutMs: config.statusTimeoutMs,
|
|
388
|
+
signal: exec.signal,
|
|
389
|
+
});
|
|
390
|
+
if (r.killed) throw new Error("deepseek_vision_logout was aborted or timed out");
|
|
391
|
+
if (r.code !== 0) {
|
|
392
|
+
throw new Error(`logout failed (exit ${r.code}): ${(r.stderr || r.stdout).trim()}`);
|
|
393
|
+
}
|
|
394
|
+
return withVersionWarning(r.stdout.trim() || "Logged out.");
|
|
395
|
+
},
|
|
396
|
+
})
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export { name, inject, Config, apply };
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xlight-oss/visionary-dsh",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "DeepSeek Visionary native plugin for DeepSeek Harness: deepseek_vision / status / login / logout native tools backed by the visionary-server CLI (DeepSeek web vision model, no API key).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"default": "./lib/index.mjs"
|
|
10
|
+
},
|
|
11
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib/index.mjs",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"dsh": {
|
|
20
|
+
"bundle": {
|
|
21
|
+
"patch": "./cordis.patch.yml"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
29
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
30
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
34
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
35
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/xlight/deepseek-visionary.git"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"deepseek-harness",
|
|
43
|
+
"dsh",
|
|
44
|
+
"cordis",
|
|
45
|
+
"vision",
|
|
46
|
+
"image-analysis",
|
|
47
|
+
"mcp",
|
|
48
|
+
"plugin"
|
|
49
|
+
],
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20"
|
|
53
|
+
}
|
|
54
|
+
}
|