@floatingsk/dsh-vision 0.1.1 → 0.1.3
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 +39 -5
- package/lib/index.js +2 -1
- package/mcp/server.js +205 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
# dsh-vision
|
|
2
2
|
|
|
3
|
-
> 给 DeepSeek Harness(dsh
|
|
3
|
+
> 给 DeepSeek Harness(dsh)插件 与 Claude Code(MCP)提供视觉能力的工具:**本地 OCR(macOS / Windows)+ 云端 VLM(多供应商)图片理解**。
|
|
4
4
|
|
|
5
5
|
DeepSeek 的模型 API 目前不支持图像输入,`read_image` 因此无法使用。本插件提供两个工具绕过这个限制:
|
|
6
6
|
|
|
7
7
|
| 工具 | 能力 | 成本 |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| `read_image_text` | 识别图片中的**文字**(macOS
|
|
10
|
-
| `describe_image` | 理解图片的**画面内容**(云端 VLM
|
|
9
|
+
| `read_image_text` | 识别图片中的**文字**(macOS Vision / Windows 内置 OCR,免费离线,中英文) | 免费 |
|
|
10
|
+
| `describe_image` | 理解图片的**画面内容**(云端 VLM,多供应商,OpenAI 兼容端点) | 按量付费 |
|
|
11
11
|
|
|
12
12
|
## 特性
|
|
13
13
|
|
|
14
|
-
- 🔒 **本地 OCR
|
|
14
|
+
- 🔒 **本地 OCR**:macOS 基于 Vision.framework,Windows 基于内置 OCR 引擎,图片不出本机,隐私安全
|
|
15
15
|
- ☁️ **云端 VLM**:默认阿里云百炼 `qwen3-vl-flash`(快且便宜),OpenAI 兼容接口,可换成任意提供商
|
|
16
16
|
- 🖼️ **自动压缩**:VLM 调用前用 `sips` 把大图压到 2048px / JPEG 85%,省钱省流量
|
|
17
17
|
- 🔑 **灵活取 Key**:环境变量或 `~/.dsh/.credentials.yaml`
|
|
18
18
|
- 🧪 **零依赖单测**:核心逻辑用 Node 内置 `node:test` 覆盖(13 用例)
|
|
19
19
|
- 🌏 **多供应商 VLM**:内置百炼 / 硅基流动 / 智谱 / 火山方舟,OpenAI 兼容可加任意家
|
|
20
|
+
- 🔌 **双形态**:既是 dsh 插件,也是 MCP server(Claude Code 等 MCP 客户端直接用)
|
|
20
21
|
- 🪟 **Windows 支持**:附 PowerShell OCR 后端(Windows.Media.Ocr),VLM 通道跨平台
|
|
21
22
|
|
|
22
23
|
## 安装
|
|
23
24
|
|
|
24
|
-
### 方式 A:从 npm
|
|
25
|
+
### 方式 A:从 npm 安装(推荐)
|
|
25
26
|
|
|
26
27
|
```sh
|
|
27
28
|
dsh plugin --profile web add @floatingsk/dsh-vision
|
|
@@ -143,6 +144,39 @@ Agent 会自动选择合适的工具(读文字走 OCR,看画面走 VLM)。
|
|
|
143
144
|
| `qwen3-vl-plus` | 质量更高,稍慢稍贵 |
|
|
144
145
|
| `qwen-vl-ocr` | 纯文字识别专用,比本地 OCR 更强(需联网) |
|
|
145
146
|
|
|
147
|
+
## Claude Code / MCP 使用
|
|
148
|
+
|
|
149
|
+
本仓库附带一个零依赖的 **MCP server**(`mcp/server.js`),让 **Claude Code**(以及任何支持 MCP 的客户端)也能用这两个工具——即使你的 Claude Code 接的是不支持视觉的模型(如 DeepSeek)。
|
|
150
|
+
|
|
151
|
+
### 接入 Claude Code
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
# 全局接入(所有项目可用)
|
|
155
|
+
claude mcp add dsh-vision -- node /path/to/dsh-vision/mcp/server.js
|
|
156
|
+
|
|
157
|
+
# 或者只给当前项目(在项目根目录建 .mcp.json):
|
|
158
|
+
# {
|
|
159
|
+
# "mcpServers": {
|
|
160
|
+
# "dsh-vision": {
|
|
161
|
+
# "command": "node",
|
|
162
|
+
# "args": ["/path/to/dsh-vision/mcp/server.js"],
|
|
163
|
+
# "env": { "DASHSCOPE_API_KEY": "sk-xxx" }
|
|
164
|
+
# }
|
|
165
|
+
# }
|
|
166
|
+
# }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`describe_image` 的 API Key 读取优先级:环境变量 > `~/.dsh/.credentials.yaml`。用 `.mcp.json` 时可在 `env` 里直接配。
|
|
170
|
+
|
|
171
|
+
### 验证
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
claude mcp list # 应看到 dsh-vision
|
|
175
|
+
claude mcp test dsh-vision # 或直接问 Claude:看下 /path/to/xxx.png 里是什么
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
> MCP server 为纯 Node 实现(stdio JSON-RPC),无第三方依赖,Node >= 18 即可。
|
|
179
|
+
|
|
146
180
|
## 开发
|
|
147
181
|
|
|
148
182
|
```sh
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { execFile } from "node:child_process";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { readFile, unlink } from "node:fs/promises";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
import z from "@deepseek-ai/schemastery";
|
|
10
11
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
@@ -67,7 +68,7 @@ export const Config = z.object({
|
|
|
67
68
|
|
|
68
69
|
/** 用 sips 把图片压到最长边 <= maxDim 的 JPEG;失败时退回原图(不落盘) */
|
|
69
70
|
async function prepareImage(filePath, maxDim) {
|
|
70
|
-
const tmpOut = join(
|
|
71
|
+
const tmpOut = join(tmpdir(), `dsh-vision-vlm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.jpg`);
|
|
71
72
|
try {
|
|
72
73
|
await execFileAsync("sips", [
|
|
73
74
|
"-Z", String(maxDim),
|
package/mcp/server.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// dsh-vision MCP server — 给 Claude Code 等 MCP 客户端提供图片理解能力
|
|
2
|
+
//
|
|
3
|
+
// 提供两个工具(与 dsh-vision 插件一致):
|
|
4
|
+
// - read_image_text : 本地 OCR(macOS Vision / Windows),免费离线
|
|
5
|
+
// - describe_image : 云端 VLM(多供应商,OpenAI 兼容),默认阿里云百炼 qwen3-vl-flash
|
|
6
|
+
//
|
|
7
|
+
// 用法:
|
|
8
|
+
// node mcp/server.js
|
|
9
|
+
// 在 Claude Code 中接入:
|
|
10
|
+
// claude mcp add dsh-vision -- node /path/to/dsh-vision/mcp/server.js
|
|
11
|
+
//
|
|
12
|
+
// 依赖: 复用 ../lib/core.js(纯逻辑)与 ../bin/vision-ocr(OCR 二进制)
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
import { promisify } from "node:util";
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import {
|
|
20
|
+
VLM_TIMEOUT_MS,
|
|
21
|
+
VLM_MAX_IMAGE_DIM,
|
|
22
|
+
JPEG_QUALITY,
|
|
23
|
+
OCR_TIMEOUT_MS,
|
|
24
|
+
DEFAULT_DESCRIBE_PROMPT,
|
|
25
|
+
resolveApiKey,
|
|
26
|
+
extractDescription,
|
|
27
|
+
buildVLMRequestBody,
|
|
28
|
+
mimeTypeForPath,
|
|
29
|
+
} from "../lib/core.js";
|
|
30
|
+
|
|
31
|
+
const execFileAsync = promisify(execFile);
|
|
32
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const ocrBin = join(__dirname, "..", "bin", "vision-ocr");
|
|
34
|
+
|
|
35
|
+
// ── 内置供应商(与插件 Config 默认值一致) ──────────────────────────────────
|
|
36
|
+
const PROVIDERS = {
|
|
37
|
+
bailian: { baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", model: "qwen3-vl-flash", apiKeyEnv: "DASHSCOPE_API_KEY" },
|
|
38
|
+
siliconflow: { baseUrl: "https://api.siliconflow.cn/v1", model: "Qwen/Qwen2.5-VL-7B-Instruct", apiKeyEnv: "SILICONFLOW_API_KEY" },
|
|
39
|
+
zhipu: { baseUrl: "https://open.bigmodel.cn/api/paas/v4", model: "glm-4v-flash", apiKeyEnv: "ZHIPU_API_KEY" },
|
|
40
|
+
volcengine: { baseUrl: "https://ark.cn-beijing.volces.com/api/v3", model: "doubao-seed-1.6-vision", apiKeyEnv: "ARK_API_KEY" },
|
|
41
|
+
};
|
|
42
|
+
const DEFAULT_PROVIDER = "bailian";
|
|
43
|
+
|
|
44
|
+
// ── 工具实现 ─────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
async function runOCR(filePath) {
|
|
47
|
+
const { stdout } = await execFileAsync(ocrBin, [filePath, "--json"], { timeout: OCR_TIMEOUT_MS });
|
|
48
|
+
const parsed = JSON.parse(stdout);
|
|
49
|
+
const lines = parsed.lines.map((l) => l.text);
|
|
50
|
+
return {
|
|
51
|
+
path: filePath,
|
|
52
|
+
width: parsed.width,
|
|
53
|
+
height: parsed.height,
|
|
54
|
+
text: lines.length > 0 ? lines.join("\n") : "(未检测到文字)",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runVLM(filePath, question, providerId) {
|
|
59
|
+
const provider = PROVIDERS[providerId || DEFAULT_PROVIDER];
|
|
60
|
+
if (!provider) throw new Error(`未知供应商 "${providerId}",可用: ${Object.keys(PROVIDERS).join(", ")}`);
|
|
61
|
+
const apiKey = resolveApiKey(provider.apiKeyEnv);
|
|
62
|
+
if (!apiKey) {
|
|
63
|
+
throw new Error(`需要 ${provider.apiKeyEnv}(可在环境变量或 ~/.dsh/.credentials.yaml 配置)`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 压缩图片(sips,macOS);失败退回原图
|
|
67
|
+
let imagePath = filePath;
|
|
68
|
+
try {
|
|
69
|
+
const tmpOut = join(tmpdir(), `dsh-vision-mcp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.jpg`);
|
|
70
|
+
await execFileAsync("sips", ["-Z", String(VLM_MAX_IMAGE_DIM), "-s", "format", "jpeg", "-s", "formatOptions", String(JPEG_QUALITY), filePath, "--out", tmpOut]);
|
|
71
|
+
imagePath = tmpOut;
|
|
72
|
+
} catch { /* 保留原图 */ }
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const imageBytes = await readFile(imagePath);
|
|
76
|
+
const prompt = question && question.trim().length > 0 ? question : DEFAULT_DESCRIBE_PROMPT;
|
|
77
|
+
const body = buildVLMRequestBody({
|
|
78
|
+
model: provider.model,
|
|
79
|
+
prompt,
|
|
80
|
+
imageBase64: imageBytes.toString("base64"),
|
|
81
|
+
mimeType: mimeTypeForPath(imagePath),
|
|
82
|
+
});
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timer = setTimeout(() => controller.abort(), VLM_TIMEOUT_MS);
|
|
85
|
+
let res;
|
|
86
|
+
try {
|
|
87
|
+
res = await fetch(`${provider.baseUrl}/chat/completions`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
|
|
90
|
+
body: JSON.stringify(body),
|
|
91
|
+
signal: controller.signal,
|
|
92
|
+
});
|
|
93
|
+
} finally {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
const detail = (await res.text()).slice(0, 500);
|
|
98
|
+
throw new Error(`VLM HTTP ${res.status}: ${detail}`);
|
|
99
|
+
}
|
|
100
|
+
const description = extractDescription(await res.json());
|
|
101
|
+
return description || "(空回复)";
|
|
102
|
+
} finally {
|
|
103
|
+
if (imagePath !== filePath) {
|
|
104
|
+
const { unlink } = await import("node:fs/promises");
|
|
105
|
+
await unlink(imagePath).catch(() => {});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── MCP 工具定义 ─────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
const TOOLS = [
|
|
113
|
+
{
|
|
114
|
+
name: "read_image_text",
|
|
115
|
+
description: "识别图片中的文字(OCR)。macOS 使用本地 Vision 引擎,Windows 使用内置 OCR,免费离线,支持中英文。返回按阅读顺序排列的文本行与图片尺寸。",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
file_path: { type: "string", description: "图片文件路径(PNG/JPEG 等)" },
|
|
120
|
+
},
|
|
121
|
+
required: ["file_path"],
|
|
122
|
+
},
|
|
123
|
+
handler: async (args) => {
|
|
124
|
+
if (!args?.file_path) throw new Error("file_path 必填");
|
|
125
|
+
const r = await runOCR(String(args.file_path));
|
|
126
|
+
return `【图片文字识别】 ${r.path} (${r.width}x${r.height})\n${r.text}`;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "describe_image",
|
|
131
|
+
description: "理解图片的画面内容(视觉语言模型 VLM)。调用可配置的云端视觉模型(默认阿里云百炼 qwen3-vl-flash),返回对图片内容的自然语言描述或对指定问题的回答。需要配置 API Key(DASHSCOPE_API_KEY 等环境变量,或 ~/.dsh/.credentials.yaml)。",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
file_path: { type: "string", description: "图片文件路径(PNG/JPEG)" },
|
|
136
|
+
question: { type: "string", description: "可选的具体问题;留空则描述整体画面内容" },
|
|
137
|
+
provider: { type: "string", description: "可选:VLM 供应商(bailian/siliconflow/zhipu/volcengine),留空用默认" },
|
|
138
|
+
},
|
|
139
|
+
required: ["file_path"],
|
|
140
|
+
},
|
|
141
|
+
handler: async (args) => {
|
|
142
|
+
if (!args?.file_path) throw new Error("file_path 必填");
|
|
143
|
+
const provider = args.provider || DEFAULT_PROVIDER;
|
|
144
|
+
const description = await runVLM(String(args.file_path), args.question ? String(args.question) : undefined, String(provider));
|
|
145
|
+
return `【图片理解·${provider}】 ${args.file_path}\n${description}`;
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
// ── stdio JSON-RPC 循环(零依赖 MCP server) ─────────────────────────────────
|
|
151
|
+
|
|
152
|
+
function sendMessage(msg) {
|
|
153
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function handleRequest(msg) {
|
|
157
|
+
const { id, method, params } = msg;
|
|
158
|
+
switch (method) {
|
|
159
|
+
case "initialize":
|
|
160
|
+
return {
|
|
161
|
+
protocolVersion: params?.protocolVersion ?? "2024-11-05",
|
|
162
|
+
capabilities: { tools: {} },
|
|
163
|
+
serverInfo: { name: "dsh-vision", version: "0.1.0" },
|
|
164
|
+
};
|
|
165
|
+
case "tools/list":
|
|
166
|
+
return {
|
|
167
|
+
tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
|
|
168
|
+
};
|
|
169
|
+
case "tools/call": {
|
|
170
|
+
const tool = TOOLS.find((t) => t.name === params?.name);
|
|
171
|
+
if (!tool) throw new Error(`未知工具: ${params?.name}`);
|
|
172
|
+
const text = await tool.handler(params?.arguments ?? {});
|
|
173
|
+
return { content: [{ type: "text", text }] };
|
|
174
|
+
}
|
|
175
|
+
default:
|
|
176
|
+
throw new Error(`未知方法: ${method}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const readline = await import("node:readline");
|
|
181
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
182
|
+
rl.on("line", async (line) => {
|
|
183
|
+
if (!line.trim()) return;
|
|
184
|
+
let msg;
|
|
185
|
+
try {
|
|
186
|
+
msg = JSON.parse(line);
|
|
187
|
+
} catch {
|
|
188
|
+
return; // 忽略非法 JSON
|
|
189
|
+
}
|
|
190
|
+
if (msg.method === "notifications/initialized") return; // 通知无需响应
|
|
191
|
+
if (msg.id === undefined) return; // 忽略无 id 的消息
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const result = await handleRequest(msg);
|
|
195
|
+
sendMessage({ jsonrpc: "2.0", id: msg.id, result });
|
|
196
|
+
} catch (err) {
|
|
197
|
+
sendMessage({ jsonrpc: "2.0", id: msg.id, error: { code: -32603, message: String(err?.message ?? err) } });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 保持进程常驻:MCP server 生命周期由客户端(Claude Code)管理,
|
|
202
|
+
// 不能在 stdin 结束时立即退出(异步工具调用可能尚未完成)。
|
|
203
|
+
// 预留 SIGTERM/SIGINT 优雅退出,便于测试脚本终止。
|
|
204
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
205
|
+
process.on("SIGINT", () => process.exit(0));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floatingsk/dsh-vision",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "为 DeepSeek Harness
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "为 DeepSeek Harness(dsh 插件)与 Claude Code(MCP)提供图片理解能力:本地 OCR(macOS Vision / Windows 内置 OCR)+ 云端 VLM(多供应商,OpenAI 兼容)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"lib",
|
|
12
|
+
"mcp",
|
|
12
13
|
"bin/vision-ocr.swift",
|
|
13
14
|
"bin/vision-ocr.ps1",
|
|
14
15
|
"README.md",
|