image-vision-mcp 1.0.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 +21 -0
- package/README.md +252 -0
- package/dist/constants.d.ts +15 -0
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy/imageProxy.d.ts +1 -0
- package/dist/proxy/imageProxy.js +346 -0
- package/dist/proxy/imageProxy.js.map +1 -0
- package/dist/schemas.d.ts +26 -0
- package/dist/schemas.js +32 -0
- package/dist/schemas.js.map +1 -0
- package/dist/services/clipboard.d.ts +16 -0
- package/dist/services/clipboard.js +196 -0
- package/dist/services/clipboard.js.map +1 -0
- package/dist/services/image.d.ts +19 -0
- package/dist/services/image.js +216 -0
- package/dist/services/image.js.map +1 -0
- package/dist/services/moonshot.d.ts +9 -0
- package/dist/services/moonshot.js +71 -0
- package/dist/services/moonshot.js.map +1 -0
- package/dist/tools/describeClipboard.d.ts +2 -0
- package/dist/tools/describeClipboard.js +106 -0
- package/dist/tools/describeClipboard.js.map +1 -0
- package/dist/tools/describeImage.d.ts +2 -0
- package/dist/tools/describeImage.js +105 -0
- package/dist/tools/describeImage.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ImageLoadResult } from "../types.js";
|
|
2
|
+
export declare function isHttpUrl(input: string): boolean;
|
|
3
|
+
export interface UrlCheckResult {
|
|
4
|
+
allowed: boolean;
|
|
5
|
+
reason?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 校验图片 URL 是否安全(防止 SSRF 访问内网/元数据服务)
|
|
9
|
+
*
|
|
10
|
+
* 默认拒绝:loopback、私网、链路本地、CGNAT、已知元数据 hostname。
|
|
11
|
+
* 如需访问内网图片服务,设置环境变量 ALLOW_PRIVATE_NETWORK_IMAGES=1
|
|
12
|
+
*/
|
|
13
|
+
export declare function isAllowedUrl(url: string): UrlCheckResult;
|
|
14
|
+
export declare function detectMimeTypeFromPath(filePath: string): string;
|
|
15
|
+
export declare function detectMimeTypeFromContentType(contentType: string | undefined): string;
|
|
16
|
+
export declare function imageBufferToDataUrl(buffer: Buffer, mimeType: string): string;
|
|
17
|
+
export declare function loadLocalImage(filePath: string): Promise<ImageLoadResult>;
|
|
18
|
+
export declare function downloadImage(url: string): Promise<ImageLoadResult>;
|
|
19
|
+
export declare function loadImageAsDataUrl(input: string): Promise<ImageLoadResult>;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { URL } from "url";
|
|
4
|
+
import net from "net";
|
|
5
|
+
import http from "http";
|
|
6
|
+
import https from "https";
|
|
7
|
+
import dns from "dns";
|
|
8
|
+
import axios from "axios";
|
|
9
|
+
import { SUPPORTED_MIME_TYPES, IMAGE_DOWNLOAD_TIMEOUT_MS, IMAGE_MAX_SIZE_BYTES, } from "../constants.js";
|
|
10
|
+
export function isHttpUrl(input) {
|
|
11
|
+
return /^https?:\/\//i.test(input);
|
|
12
|
+
}
|
|
13
|
+
// ────────────────────── SSRF 防护 ──────────────────────
|
|
14
|
+
const BLOCKED_HOSTNAMES = new Set([
|
|
15
|
+
"localhost",
|
|
16
|
+
"ip6-localhost",
|
|
17
|
+
"ip6-loopback",
|
|
18
|
+
"metadata.google.internal", // GCP 元数据
|
|
19
|
+
"metadata", // 一些云环境的简写
|
|
20
|
+
]);
|
|
21
|
+
function isPrivateIPv4(ip) {
|
|
22
|
+
const parts = ip.split(".").map(Number);
|
|
23
|
+
if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const [a, b] = parts;
|
|
27
|
+
if (a === 10)
|
|
28
|
+
return true; // 10.0.0.0/8
|
|
29
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
30
|
+
return true; // 172.16.0.0/12
|
|
31
|
+
if (a === 192 && b === 168)
|
|
32
|
+
return true; // 192.168.0.0/16
|
|
33
|
+
if (a === 127)
|
|
34
|
+
return true; // 127.0.0.0/8 loopback
|
|
35
|
+
if (a === 169 && b === 254)
|
|
36
|
+
return true; // 169.254.0.0/16 link-local(含 AWS/GCP 元数据)
|
|
37
|
+
if (a === 0)
|
|
38
|
+
return true; // 0.0.0.0/8
|
|
39
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
40
|
+
return true; // 100.64.0.0/10 CGNAT
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function isPrivateIPv6(ip) {
|
|
44
|
+
const lower = ip.toLowerCase();
|
|
45
|
+
if (lower === "::1")
|
|
46
|
+
return true; // loopback
|
|
47
|
+
if (lower === "::")
|
|
48
|
+
return true; // 未指定
|
|
49
|
+
if (lower.startsWith("fc") || lower.startsWith("fd"))
|
|
50
|
+
return true; // fc00::/7 ULA
|
|
51
|
+
if (lower.startsWith("fe8") || lower.startsWith("fe9") ||
|
|
52
|
+
lower.startsWith("fea") || lower.startsWith("feb"))
|
|
53
|
+
return true; // fe80::/10 link-local
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 校验图片 URL 是否安全(防止 SSRF 访问内网/元数据服务)
|
|
58
|
+
*
|
|
59
|
+
* 默认拒绝:loopback、私网、链路本地、CGNAT、已知元数据 hostname。
|
|
60
|
+
* 如需访问内网图片服务,设置环境变量 ALLOW_PRIVATE_NETWORK_IMAGES=1
|
|
61
|
+
*/
|
|
62
|
+
export function isAllowedUrl(url) {
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = new URL(url);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return { allowed: false, reason: "URL 解析失败" };
|
|
69
|
+
}
|
|
70
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
71
|
+
return { allowed: false, reason: `不允许的协议: ${parsed.protocol}` };
|
|
72
|
+
}
|
|
73
|
+
const hostname = parsed.hostname.replace(/^\[|\]$/g, ""); // 去掉 IPv6 方括号
|
|
74
|
+
if (BLOCKED_HOSTNAMES.has(hostname.toLowerCase())) {
|
|
75
|
+
return { allowed: false, reason: `不允许的 hostname: ${hostname}` };
|
|
76
|
+
}
|
|
77
|
+
if (net.isIPv4(hostname)) {
|
|
78
|
+
if (isPrivateIPv4(hostname)) {
|
|
79
|
+
return { allowed: false, reason: `不允许的内网 IP: ${hostname}` };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (net.isIPv6(hostname)) {
|
|
83
|
+
if (isPrivateIPv6(hostname)) {
|
|
84
|
+
return { allowed: false, reason: `不允许的内网 IP: ${hostname}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { allowed: true };
|
|
88
|
+
}
|
|
89
|
+
function isPrivateIP(ip) {
|
|
90
|
+
if (net.isIPv4(ip))
|
|
91
|
+
return isPrivateIPv4(ip);
|
|
92
|
+
if (net.isIPv6(ip))
|
|
93
|
+
return isPrivateIPv6(ip);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 创建安全的 DNS lookup 函数:解析后校验所有 IP,拒绝内网地址。
|
|
98
|
+
*
|
|
99
|
+
* 防止 DNS rebinding 攻击:攻击者控制域名,第一次解析返回公网 IP 通过校验,
|
|
100
|
+
* 第二次解析返回内网 IP 绕过校验。本函数返回第一个校验过的 IP,强制 axios
|
|
101
|
+
* 用这个 IP 连接,不再重新解析。
|
|
102
|
+
*/
|
|
103
|
+
function createSafeLookup(allowPrivate) {
|
|
104
|
+
return (hostname, options, callback) => {
|
|
105
|
+
const lookupOpts = { ...options, all: true };
|
|
106
|
+
dns.lookup(hostname, lookupOpts, (err, addresses) => {
|
|
107
|
+
if (err) {
|
|
108
|
+
callback(err, "", 0);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const list = (addresses || []);
|
|
112
|
+
if (list.length === 0) {
|
|
113
|
+
callback(new Error(`DNS 解析无结果: ${hostname}`), "", 0);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!allowPrivate) {
|
|
117
|
+
for (const addr of list) {
|
|
118
|
+
if (isPrivateIP(addr.address)) {
|
|
119
|
+
callback(new Error(`域名 ${hostname} 解析到内网 IP: ${addr.address}`), "", 0);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const first = list[0];
|
|
125
|
+
callback(null, first.address, first.family);
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export function detectMimeTypeFromPath(filePath) {
|
|
130
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
131
|
+
return SUPPORTED_MIME_TYPES[ext] ?? "image/jpeg";
|
|
132
|
+
}
|
|
133
|
+
export function detectMimeTypeFromContentType(contentType) {
|
|
134
|
+
if (!contentType)
|
|
135
|
+
return "image/jpeg";
|
|
136
|
+
const base = contentType.split(";")[0].trim().toLowerCase();
|
|
137
|
+
const known = ["image/jpeg", "image/png", "image/gif", "image/webp", "image/bmp"];
|
|
138
|
+
return known.includes(base) ? base : "image/jpeg";
|
|
139
|
+
}
|
|
140
|
+
export function imageBufferToDataUrl(buffer, mimeType) {
|
|
141
|
+
return `data:${mimeType};base64,${buffer.toString("base64")}`;
|
|
142
|
+
}
|
|
143
|
+
export async function loadLocalImage(filePath) {
|
|
144
|
+
let stats;
|
|
145
|
+
try {
|
|
146
|
+
stats = await fs.stat(filePath);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
throw new Error(`无法访问本地文件: ${filePath} (${err.message})`);
|
|
150
|
+
}
|
|
151
|
+
if (!stats.isFile()) {
|
|
152
|
+
throw new Error(`路径不是文件: ${filePath}`);
|
|
153
|
+
}
|
|
154
|
+
const buffer = await fs.readFile(filePath);
|
|
155
|
+
const mimeType = detectMimeTypeFromPath(filePath);
|
|
156
|
+
return {
|
|
157
|
+
dataUrl: imageBufferToDataUrl(buffer, mimeType),
|
|
158
|
+
mimeType,
|
|
159
|
+
sizeBytes: buffer.length,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export async function downloadImage(url) {
|
|
163
|
+
// SSRF 防护:默认拒绝内网/loopback/元数据地址
|
|
164
|
+
const allowPrivateRaw = (process.env.ALLOW_PRIVATE_NETWORK_IMAGES || "").toLowerCase();
|
|
165
|
+
const allowPrivate = allowPrivateRaw === "1" || allowPrivateRaw === "true";
|
|
166
|
+
if (!allowPrivate) {
|
|
167
|
+
const check = isAllowedUrl(url);
|
|
168
|
+
if (!check.allowed) {
|
|
169
|
+
throw new Error(`${check.reason}(如需访问内网图片,设置环境变量 ALLOW_PRIVATE_NETWORK_IMAGES=1)`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
// 自定义 DNS 解析:校验所有解析到的 IP,防止 DNS rebinding 绕过 SSRF 防护。
|
|
174
|
+
// 通过 httpAgent/httpsAgent 传入,axios 会把它们传给底层 http.request。
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
+
const safeLookup = createSafeLookup(allowPrivate);
|
|
177
|
+
const httpAgent = new http.Agent({ lookup: safeLookup });
|
|
178
|
+
const httpsAgent = new https.Agent({ lookup: safeLookup });
|
|
179
|
+
const config = {
|
|
180
|
+
responseType: "arraybuffer",
|
|
181
|
+
timeout: IMAGE_DOWNLOAD_TIMEOUT_MS,
|
|
182
|
+
maxContentLength: IMAGE_MAX_SIZE_BYTES,
|
|
183
|
+
maxRedirects: 5,
|
|
184
|
+
httpAgent,
|
|
185
|
+
httpsAgent,
|
|
186
|
+
};
|
|
187
|
+
const response = await axios.get(url, config);
|
|
188
|
+
const buffer = Buffer.from(response.data);
|
|
189
|
+
const contentType = response.headers?.["content-type"];
|
|
190
|
+
const mimeType = detectMimeTypeFromContentType(typeof contentType === "string" ? contentType : undefined);
|
|
191
|
+
return {
|
|
192
|
+
dataUrl: imageBufferToDataUrl(buffer, mimeType),
|
|
193
|
+
mimeType,
|
|
194
|
+
sizeBytes: buffer.length,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
if (err instanceof axios.AxiosError) {
|
|
199
|
+
if (err.response) {
|
|
200
|
+
throw new Error(`下载图片失败: HTTP ${err.response.status} ${err.response.statusText} - ${url}`);
|
|
201
|
+
}
|
|
202
|
+
if (err.code === "ECONNABORTED") {
|
|
203
|
+
throw new Error(`下载图片超时 (${IMAGE_DOWNLOAD_TIMEOUT_MS}ms): ${url}`);
|
|
204
|
+
}
|
|
205
|
+
throw new Error(`下载图片失败: ${err.message} - ${url}`);
|
|
206
|
+
}
|
|
207
|
+
throw new Error(`下载图片失败: ${err.message} - ${url}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export async function loadImageAsDataUrl(input) {
|
|
211
|
+
if (isHttpUrl(input)) {
|
|
212
|
+
return downloadImage(input);
|
|
213
|
+
}
|
|
214
|
+
return loadLocalImage(input);
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/services/image.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MoonshotChatResponse, MoonshotMessage } from "../types.js";
|
|
2
|
+
export interface CallMoonshotParams {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
model?: string;
|
|
5
|
+
messages: MoonshotMessage[];
|
|
6
|
+
maxTokens?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function handleMoonshotError(error: unknown): string;
|
|
9
|
+
export declare function callMoonshot(params: CallMoonshotParams): Promise<MoonshotChatResponse>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import axios, { AxiosError } from "axios";
|
|
2
|
+
import { API_BASE_URL, DEFAULT_MODEL, REQUEST_TIMEOUT_MS } from "../constants.js";
|
|
3
|
+
export function handleMoonshotError(error) {
|
|
4
|
+
if (error instanceof AxiosError) {
|
|
5
|
+
if (error.response) {
|
|
6
|
+
const status = error.response.status;
|
|
7
|
+
const data = error.response.data;
|
|
8
|
+
const errMsg = data?.error?.message;
|
|
9
|
+
switch (status) {
|
|
10
|
+
case 401:
|
|
11
|
+
return `Error: Moonshot API 鉴权失败 (401)。请检查 MOONSHOT_API_KEY 是否正确。${errMsg ? ` 详情: ${errMsg}` : ""}`;
|
|
12
|
+
case 403:
|
|
13
|
+
return `Error: Moonshot API 拒绝访问 (403)。可能是 API key 无权限调用该模型。${errMsg ? ` 详情: ${errMsg}` : ""}`;
|
|
14
|
+
case 404:
|
|
15
|
+
return `Error: Moonshot API 模型不存在 (404)。请检查模型名是否正确 (当前: ${DEFAULT_MODEL})。${errMsg ? ` 详情: ${errMsg}` : ""}`;
|
|
16
|
+
case 429:
|
|
17
|
+
return `Error: Moonshot API 限流 (429)。请稍后重试。${errMsg ? ` 详情: ${errMsg}` : ""}`;
|
|
18
|
+
case 500:
|
|
19
|
+
case 502:
|
|
20
|
+
case 503:
|
|
21
|
+
return `Error: Moonshot API 服务端错误 (${status})。请稍后重试。${errMsg ? ` 详情: ${errMsg}` : ""}`;
|
|
22
|
+
default:
|
|
23
|
+
return `Error: Moonshot API 请求失败 (HTTP ${status})。${errMsg ? ` 详情: ${errMsg}` : ""}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (error.code === "ECONNABORTED") {
|
|
27
|
+
return `Error: Moonshot API 请求超时 (${REQUEST_TIMEOUT_MS}ms)。请稍后重试。`;
|
|
28
|
+
}
|
|
29
|
+
if (error.code === "ENOTFOUND" || error.code === "ECONNREFUSED") {
|
|
30
|
+
return `Error: 无法连接 Moonshot API (${error.code})。请检查网络。`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return `Error: 调用 Moonshot API 时发生意外错误: ${error instanceof Error ? error.message : String(error)}`;
|
|
34
|
+
}
|
|
35
|
+
const MAX_RETRIES = 2;
|
|
36
|
+
const INITIAL_RETRY_DELAY_MS = 1000;
|
|
37
|
+
export async function callMoonshot(params) {
|
|
38
|
+
const { apiKey, model = DEFAULT_MODEL, messages, maxTokens = 2048 } = params;
|
|
39
|
+
if (!apiKey) {
|
|
40
|
+
throw new Error("MOONSHOT_API_KEY 未设置");
|
|
41
|
+
}
|
|
42
|
+
let lastError;
|
|
43
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
44
|
+
try {
|
|
45
|
+
const response = await axios.post(`${API_BASE_URL}/chat/completions`, {
|
|
46
|
+
model,
|
|
47
|
+
messages,
|
|
48
|
+
max_tokens: maxTokens,
|
|
49
|
+
}, {
|
|
50
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
51
|
+
headers: {
|
|
52
|
+
"Content-Type": "application/json",
|
|
53
|
+
Authorization: `Bearer ${apiKey}`,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
return response.data;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
lastError = err;
|
|
60
|
+
// 仅对 429 限流重试,指数退避:1s, 2s
|
|
61
|
+
if (err instanceof AxiosError && err.response?.status === 429 && attempt < MAX_RETRIES) {
|
|
62
|
+
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw lastError;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=moonshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moonshot.js","sourceRoot":"","sources":["../../src/services/moonshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { DescribeClipboardInputSchema } from "../schemas.js";
|
|
2
|
+
import { DEFAULT_MODEL, CHARACTER_LIMIT } from "../constants.js";
|
|
3
|
+
import { readClipboardImage } from "../services/clipboard.js";
|
|
4
|
+
import { callMoonshot, handleMoonshotError } from "../services/moonshot.js";
|
|
5
|
+
export function registerDescribeClipboardTool(server, getApiKey) {
|
|
6
|
+
server.registerTool("kimi_describe_clipboard", {
|
|
7
|
+
title: "Kimi 识别剪贴板图片",
|
|
8
|
+
description: `使用 Moonshot Kimi 视觉模型 (kimi-k2.6) 识别剪贴板中的图片并返回文本描述。
|
|
9
|
+
|
|
10
|
+
直接从系统剪贴板读取图片,无需提供文件路径或 URL。
|
|
11
|
+
跨平台支持: Windows (PowerShell)、macOS (pngpaste/osascript)、Linux (xclip/xsel)。
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
- prompt (string, 可选): 询问图片的问题或识图指令,默认 "请详细描述这张图片的内容。"。最长 4000 字符
|
|
15
|
+
- max_tokens (number, 可选): 返回描述的最大 token 数,1-8192,默认 2048
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
文本内容,包含图片描述和 token 使用情况元信息。
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
1. 用户先复制一张图片到剪贴板 (Ctrl+C)
|
|
22
|
+
2. 调用此工具即可自动从剪贴板读取并识别
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
- 描述剪贴板截图: (无参数,使用默认 prompt)
|
|
26
|
+
- 提取剪贴板图片中的文字: prompt="请提取图片中的所有文字"
|
|
27
|
+
- 识别剪贴板中的图表: prompt="图表中显示的数据是什么"
|
|
28
|
+
|
|
29
|
+
Error Handling:
|
|
30
|
+
- "剪贴板中没有图片": 请先复制一张图片到剪贴板
|
|
31
|
+
- "不支持的平台": 当前操作系统不支持
|
|
32
|
+
- "Moonshot API 鉴权失败 (401)": API key 错误
|
|
33
|
+
- "Moonshot API 限流 (429)": 请求过于频繁,请稍后重试`,
|
|
34
|
+
inputSchema: DescribeClipboardInputSchema,
|
|
35
|
+
annotations: {
|
|
36
|
+
readOnlyHint: true,
|
|
37
|
+
destructiveHint: false,
|
|
38
|
+
idempotentHint: false,
|
|
39
|
+
openWorldHint: true,
|
|
40
|
+
},
|
|
41
|
+
}, async (params) => {
|
|
42
|
+
const apiKey = getApiKey();
|
|
43
|
+
// 1. 从剪贴板读取图片
|
|
44
|
+
let imageResult;
|
|
45
|
+
try {
|
|
46
|
+
imageResult = await readClipboardImage();
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: "text",
|
|
53
|
+
text: `Error: 读取剪贴板图片失败 - ${err.message}`,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// 2. 构造 Moonshot 请求
|
|
59
|
+
const content = [
|
|
60
|
+
{ type: "image_url", image_url: { url: imageResult.dataUrl } },
|
|
61
|
+
{ type: "text", text: params.prompt },
|
|
62
|
+
];
|
|
63
|
+
const messages = [{ role: "user", content }];
|
|
64
|
+
// 3. 调用 Moonshot API
|
|
65
|
+
let response;
|
|
66
|
+
try {
|
|
67
|
+
response = await callMoonshot({
|
|
68
|
+
apiKey,
|
|
69
|
+
model: DEFAULT_MODEL,
|
|
70
|
+
messages,
|
|
71
|
+
maxTokens: params.max_tokens,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: handleMoonshotError(err) }],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// 4. 提取描述
|
|
80
|
+
const description = response.choices?.[0]?.message?.content ?? "";
|
|
81
|
+
if (!description) {
|
|
82
|
+
return {
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
type: "text",
|
|
86
|
+
text: `Error: Moonshot API 返回空内容。完整响应: ${JSON.stringify(response)}`,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// 5. 构造输出
|
|
92
|
+
const meta = [
|
|
93
|
+
`模型: ${response.model}`,
|
|
94
|
+
`图片大小: ${(imageResult.sizeBytes / 1024).toFixed(2)} KB (${imageResult.mimeType})`,
|
|
95
|
+
`Token 使用: ${response.usage?.total_tokens ?? "未知"} (prompt: ${response.usage?.prompt_tokens ?? "?"}, completion: ${response.usage?.completion_tokens ?? "?"})`,
|
|
96
|
+
].join("\n");
|
|
97
|
+
let text = `${description}\n\n---\n${meta}`;
|
|
98
|
+
if (text.length > CHARACTER_LIMIT) {
|
|
99
|
+
text = text.slice(0, CHARACTER_LIMIT) + `\n\n[已截断,原文长度 ${text.length} 字符]`;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text }],
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=describeClipboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"describeClipboard.js","sourceRoot":"","sources":["../../src/tools/describeClipboard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { DescribeImageInputSchema } from "../schemas.js";
|
|
2
|
+
import { DEFAULT_MODEL, CHARACTER_LIMIT } from "../constants.js";
|
|
3
|
+
import { loadImageAsDataUrl } from "../services/image.js";
|
|
4
|
+
import { callMoonshot, handleMoonshotError } from "../services/moonshot.js";
|
|
5
|
+
export function registerDescribeImageTool(server, getApiKey) {
|
|
6
|
+
server.registerTool("kimi_describe_image", {
|
|
7
|
+
title: "Kimi 识图",
|
|
8
|
+
description: `使用 Moonshot Kimi 视觉模型 (kimi-k2.6) 识别图片并返回文本描述。
|
|
9
|
+
|
|
10
|
+
支持输入本地图片路径或 http(s):// 图片 URL。MCP 内部会自动将图片转换为 base64 后调用 Moonshot API(Moonshot 不支持直接传 URL)。
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
- image (string, 必填): 图片路径或 URL。
|
|
14
|
+
- 本地路径示例: "C:/Users/user/screenshot.png" 或 "/tmp/img.jpg"
|
|
15
|
+
- URL 示例: "https://example.com/img.png"
|
|
16
|
+
- prompt (string, 可选): 询问图片的问题或识图指令,默认 "请详细描述这张图片的内容。"。最长 4000 字符
|
|
17
|
+
- max_tokens (number, 可选): 返回描述的最大 token 数,1-8192,默认 2048
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
文本内容,包含图片描述和 token 使用情况元信息。
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
- 描述本地截图: image="C:/Users/user/screenshot.png"
|
|
24
|
+
- 描述网络图片: image="https://example.com/chart.png", prompt="图表中显示的数据是什么"
|
|
25
|
+
- 提取文字 (OCR): image="C:/Users/user/document.jpg", prompt="请提取图片中的所有文字"
|
|
26
|
+
- 识别物体: image="C:/Users/user/photo.jpg", prompt="图片里有哪些物体"
|
|
27
|
+
|
|
28
|
+
Error Handling:
|
|
29
|
+
- "无法访问本地文件": 文件路径错误或无权限
|
|
30
|
+
- "下载图片失败": URL 无法访问或超时
|
|
31
|
+
- "Moonshot API 鉴权失败 (401)": API key 错误
|
|
32
|
+
- "Moonshot API 限流 (429)": 请求过于频繁,请稍后重试`,
|
|
33
|
+
inputSchema: DescribeImageInputSchema,
|
|
34
|
+
annotations: {
|
|
35
|
+
readOnlyHint: true,
|
|
36
|
+
destructiveHint: false,
|
|
37
|
+
idempotentHint: true,
|
|
38
|
+
openWorldHint: true,
|
|
39
|
+
},
|
|
40
|
+
}, async (params) => {
|
|
41
|
+
const apiKey = getApiKey();
|
|
42
|
+
// 1. 加载图片为 data URL
|
|
43
|
+
let imageResult;
|
|
44
|
+
try {
|
|
45
|
+
imageResult = await loadImageAsDataUrl(params.image);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: `Error: 加载图片失败 - ${err.message}`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// 2. 构造 Moonshot 请求
|
|
58
|
+
const content = [
|
|
59
|
+
{ type: "image_url", image_url: { url: imageResult.dataUrl } },
|
|
60
|
+
{ type: "text", text: params.prompt },
|
|
61
|
+
];
|
|
62
|
+
const messages = [{ role: "user", content }];
|
|
63
|
+
// 3. 调用 Moonshot API
|
|
64
|
+
let response;
|
|
65
|
+
try {
|
|
66
|
+
response = await callMoonshot({
|
|
67
|
+
apiKey,
|
|
68
|
+
model: DEFAULT_MODEL,
|
|
69
|
+
messages,
|
|
70
|
+
maxTokens: params.max_tokens,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text", text: handleMoonshotError(err) }],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// 4. 提取描述
|
|
79
|
+
const description = response.choices?.[0]?.message?.content ?? "";
|
|
80
|
+
if (!description) {
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: `Error: Moonshot API 返回空内容。完整响应: ${JSON.stringify(response)}`,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// 5. 构造输出
|
|
91
|
+
const meta = [
|
|
92
|
+
`模型: ${response.model}`,
|
|
93
|
+
`图片大小: ${(imageResult.sizeBytes / 1024).toFixed(2)} KB (${imageResult.mimeType})`,
|
|
94
|
+
`Token 使用: ${response.usage?.total_tokens ?? "未知"} (prompt: ${response.usage?.prompt_tokens ?? "?"}, completion: ${response.usage?.completion_tokens ?? "?"})`,
|
|
95
|
+
].join("\n");
|
|
96
|
+
let text = `${description}\n\n---\n${meta}`;
|
|
97
|
+
if (text.length > CHARACTER_LIMIT) {
|
|
98
|
+
text = text.slice(0, CHARACTER_LIMIT) + `\n\n[已截断,原文长度 ${text.length} 字符]`;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: "text", text }],
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=describeImage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"describeImage.js","sourceRoot":"","sources":["../../src/tools/describeImage.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface MoonshotImageContent {
|
|
2
|
+
type: "image_url";
|
|
3
|
+
image_url: {
|
|
4
|
+
url: string;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export interface MoonshotTextContent {
|
|
8
|
+
type: "text";
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
export type MoonshotContent = MoonshotImageContent | MoonshotTextContent;
|
|
12
|
+
export interface MoonshotMessage {
|
|
13
|
+
role: "user" | "assistant" | "system";
|
|
14
|
+
content: string | MoonshotContent[];
|
|
15
|
+
}
|
|
16
|
+
export interface MoonshotChoice {
|
|
17
|
+
index: number;
|
|
18
|
+
message: {
|
|
19
|
+
role: string;
|
|
20
|
+
content: string;
|
|
21
|
+
};
|
|
22
|
+
finish_reason: string;
|
|
23
|
+
}
|
|
24
|
+
export interface MoonshotUsage {
|
|
25
|
+
prompt_tokens: number;
|
|
26
|
+
completion_tokens: number;
|
|
27
|
+
total_tokens: number;
|
|
28
|
+
}
|
|
29
|
+
export interface MoonshotChatResponse {
|
|
30
|
+
id: string;
|
|
31
|
+
object: string;
|
|
32
|
+
created: number;
|
|
33
|
+
model: string;
|
|
34
|
+
choices: MoonshotChoice[];
|
|
35
|
+
usage: MoonshotUsage;
|
|
36
|
+
}
|
|
37
|
+
export interface ImageLoadResult {
|
|
38
|
+
dataUrl: string;
|
|
39
|
+
mimeType: string;
|
|
40
|
+
sizeBytes: number;
|
|
41
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|