ms-vite-plugin 1.2.2 → 1.2.4

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.
@@ -0,0 +1,9 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * 注册图片处理 MCP 工具
4
+ * @param server MCP 服务实例
5
+ * @returns 无返回值
6
+ * @example
7
+ * registerImageTools(server)
8
+ */
9
+ export declare function registerImageTools(server: McpServer): void;
@@ -0,0 +1,444 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.registerImageTools = registerImageTools;
40
+ const fsExtra = __importStar(require("fs-extra"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ const sharp_1 = __importDefault(require("sharp"));
44
+ const z = __importStar(require("zod/v4"));
45
+ const project_1 = require("../project");
46
+ const tool_utils_1 = require("./tool-utils");
47
+ /**
48
+ * 解析输出路径
49
+ * @param inputPath 输入图片路径
50
+ * @param outputPath 可选输出路径
51
+ * @param suffix 默认文件名后缀
52
+ * @returns 返回输出图片绝对路径
53
+ * @example
54
+ * resolveImageOutputPath("/tmp/a.jpg", undefined, "crop")
55
+ */
56
+ function resolveImageOutputPath(inputPath, outputPath, suffix) {
57
+ if (outputPath && outputPath.trim()) {
58
+ return path.resolve(outputPath.trim());
59
+ }
60
+ const parsed = path.parse(path.resolve(inputPath));
61
+ return path.join(parsed.dir, `${parsed.name}-${suffix}.png`);
62
+ }
63
+ /**
64
+ * 解析生成类输出路径
65
+ * @param outputPath 可选输出路径
66
+ * @param fileNamePrefix 默认文件名前缀
67
+ * @param extension 文件扩展名(不含点)
68
+ * @returns 返回输出图片绝对路径
69
+ * @example
70
+ * resolveGeneratedImageOutputPath(undefined, "ms-mcp-screen-crop", "png")
71
+ */
72
+ function resolveGeneratedImageOutputPath(outputPath, fileNamePrefix, extension) {
73
+ if (outputPath && outputPath.trim()) {
74
+ return path.resolve(outputPath.trim());
75
+ }
76
+ return path.join(os.tmpdir(), `${fileNamePrefix}-${Date.now()}-${Math.random()
77
+ .toString(36)
78
+ .slice(2, 8)}.${extension}`);
79
+ }
80
+ /**
81
+ * 解析图片路径并确保文件存在
82
+ * @param imagePath 原始图片路径
83
+ * @returns 返回绝对路径
84
+ * @example
85
+ * const inputPath = await resolveExistingImagePath("./screen.jpg")
86
+ */
87
+ async function resolveExistingImagePath(imagePath) {
88
+ const inputPath = path.resolve(imagePath.trim());
89
+ if (!(await fsExtra.pathExists(inputPath))) {
90
+ throw new Error(`图片不存在: ${inputPath}`);
91
+ }
92
+ return inputPath;
93
+ }
94
+ /**
95
+ * 解析十六进制颜色
96
+ * @param value 颜色文本,支持 #RRGGBB 或 RRGGBB
97
+ * @returns 返回 RGB 颜色
98
+ * @example
99
+ * parseHexColor("#ffffff")
100
+ */
101
+ function parseHexColor(value) {
102
+ const normalized = value.trim().replace(/^#/, "");
103
+ if (!/^[0-9a-fA-F]{6}$/.test(normalized)) {
104
+ throw new Error("transparentColor 必须是 #RRGGBB 或 RRGGBB 格式。");
105
+ }
106
+ return {
107
+ r: Number.parseInt(normalized.slice(0, 2), 16),
108
+ g: Number.parseInt(normalized.slice(2, 4), 16),
109
+ b: Number.parseInt(normalized.slice(4, 6), 16),
110
+ };
111
+ }
112
+ /**
113
+ * 将 RGB 颜色格式化为 #RRGGBB
114
+ * @param color RGB 颜色
115
+ * @returns 返回十六进制颜色文本
116
+ * @example
117
+ * formatRgbHex({ r: 255, g: 255, b: 255 })
118
+ */
119
+ function formatRgbHex(color) {
120
+ return `#${color.r.toString(16).padStart(2, "0")}${color.g
121
+ .toString(16)
122
+ .padStart(2, "0")}${color.b.toString(16).padStart(2, "0")}`;
123
+ }
124
+ /**
125
+ * 从图片角落采样背景色
126
+ * @param pixels RGBA 像素数据
127
+ * @param width 图片宽度
128
+ * @param height 图片高度
129
+ * @param corner 采样角落
130
+ * @returns 返回采样到的 RGB 颜色
131
+ * @example
132
+ * sampleCornerColor(pixels, 100, 100, "topLeft")
133
+ */
134
+ function sampleCornerColor(pixels, width, height, corner) {
135
+ const x = corner === "topRight" || corner === "bottomRight" ? width - 1 : 0;
136
+ const y = corner === "bottomLeft" || corner === "bottomRight" ? height - 1 : 0;
137
+ const offset = (y * width + x) * 4;
138
+ return {
139
+ r: pixels[offset] ?? 0,
140
+ g: pixels[offset + 1] ?? 0,
141
+ b: pixels[offset + 2] ?? 0,
142
+ };
143
+ }
144
+ /**
145
+ * 判断颜色是否在容差范围内
146
+ * @param pixels RGBA 像素数据
147
+ * @param offset 像素偏移
148
+ * @param color 目标 RGB 颜色
149
+ * @param tolerance RGB 欧氏距离容差
150
+ * @returns 匹配返回 true
151
+ * @example
152
+ * isColorMatch(pixels, 0, { r: 255, g: 255, b: 255 }, 20)
153
+ */
154
+ function isColorMatch(pixels, offset, color, tolerance) {
155
+ const dr = (pixels[offset] ?? 0) - color.r;
156
+ const dg = (pixels[offset + 1] ?? 0) - color.g;
157
+ const db = (pixels[offset + 2] ?? 0) - color.b;
158
+ return Math.sqrt(dr * dr + dg * dg + db * db) <= tolerance;
159
+ }
160
+ /**
161
+ * 读取图片为 RGBA 原始像素
162
+ * @param input 图片路径或 Buffer
163
+ * @returns 返回像素和图片尺寸
164
+ * @example
165
+ * const image = await readRgbaImage("/tmp/screen.jpg")
166
+ */
167
+ async function readRgbaImage(input) {
168
+ const { data, info } = await (0, sharp_1.default)(input)
169
+ .ensureAlpha()
170
+ .raw()
171
+ .toBuffer({ resolveWithObject: true });
172
+ return {
173
+ data,
174
+ width: info.width,
175
+ height: info.height,
176
+ };
177
+ }
178
+ /**
179
+ * 获取指定坐标附近的平均颜色
180
+ * @param data RGBA 像素数据
181
+ * @param width 图片宽度
182
+ * @param height 图片高度
183
+ * @param x 目标坐标 x
184
+ * @param y 目标坐标 y
185
+ * @param radius 采样半径
186
+ * @returns 返回平均 RGBA 颜色和采样像素数量
187
+ * @example
188
+ * pickAverageColor(data, 100, 100, 10, 10, 1)
189
+ */
190
+ function pickAverageColor(data, width, height, x, y, radius) {
191
+ if (x >= width || y >= height) {
192
+ throw new Error(`坐标超出图片范围: image=${width}x${height}, point=${x},${y}`);
193
+ }
194
+ const startX = Math.max(0, x - radius);
195
+ const endX = Math.min(width - 1, x + radius);
196
+ const startY = Math.max(0, y - radius);
197
+ const endY = Math.min(height - 1, y + radius);
198
+ let r = 0;
199
+ let g = 0;
200
+ let b = 0;
201
+ let a = 0;
202
+ let sampleCount = 0;
203
+ for (let currentY = startY; currentY <= endY; currentY += 1) {
204
+ for (let currentX = startX; currentX <= endX; currentX += 1) {
205
+ const offset = (currentY * width + currentX) * 4;
206
+ r += data[offset] ?? 0;
207
+ g += data[offset + 1] ?? 0;
208
+ b += data[offset + 2] ?? 0;
209
+ a += data[offset + 3] ?? 0;
210
+ sampleCount += 1;
211
+ }
212
+ }
213
+ return {
214
+ color: {
215
+ r: Math.round(r / sampleCount),
216
+ g: Math.round(g / sampleCount),
217
+ b: Math.round(b / sampleCount),
218
+ a: Math.round(a / sampleCount),
219
+ },
220
+ sampleCount,
221
+ };
222
+ }
223
+ /**
224
+ * 格式化取色结果
225
+ * @param source 颜色来源
226
+ * @param x 坐标 x
227
+ * @param y 坐标 y
228
+ * @param radius 采样半径
229
+ * @param imageWidth 图片宽度
230
+ * @param imageHeight 图片高度
231
+ * @param color 颜色
232
+ * @param sampleCount 采样像素数量
233
+ * @returns 返回 MCP 文本
234
+ * @example
235
+ * formatPickedColorText("screen", 1, 1, 0, 100, 100, color, 1)
236
+ */
237
+ function formatPickedColorText(source, x, y, radius, imageWidth, imageHeight, color, sampleCount) {
238
+ return [
239
+ "取色成功",
240
+ `source: ${source}`,
241
+ `image: ${imageWidth}x${imageHeight}`,
242
+ `point: x=${x}, y=${y}`,
243
+ `radius: ${radius}`,
244
+ `samplePixels: ${sampleCount}`,
245
+ `hex: ${formatRgbHex(color)}`,
246
+ `rgb: ${color.r},${color.g},${color.b}`,
247
+ `rgba: ${color.r},${color.g},${color.b},${color.a}`,
248
+ ].join("\n");
249
+ }
250
+ /**
251
+ * 注册图片处理 MCP 工具
252
+ * @param server MCP 服务实例
253
+ * @returns 无返回值
254
+ * @example
255
+ * registerImageTools(server)
256
+ */
257
+ function registerImageTools(server) {
258
+ server.registerTool("image_crop", {
259
+ title: "Image Crop",
260
+ description: "裁切本地图片并输出 PNG 文件,适合从截图中裁出找图模板。",
261
+ inputSchema: {
262
+ imagePath: z.string().min(1).describe("输入图片路径"),
263
+ x: z.number().int().min(0).describe("裁切起点 x"),
264
+ y: z.number().int().min(0).describe("裁切起点 y"),
265
+ width: z.number().int().min(1).describe("裁切宽度"),
266
+ height: z.number().int().min(1).describe("裁切高度"),
267
+ outputPath: z
268
+ .string()
269
+ .min(1)
270
+ .optional()
271
+ .describe("可选输出路径;不传则在输入图片同目录生成 *-crop.png"),
272
+ },
273
+ }, async ({ imagePath, x, y, width, height, outputPath }) => {
274
+ const inputPath = await resolveExistingImagePath(imagePath);
275
+ const targetPath = resolveImageOutputPath(inputPath, outputPath, "crop");
276
+ const metadata = await (0, sharp_1.default)(inputPath).metadata();
277
+ const imageWidth = metadata.width ?? 0;
278
+ const imageHeight = metadata.height ?? 0;
279
+ if (x + width > imageWidth || y + height > imageHeight) {
280
+ return (0, tool_utils_1.createTextToolResult)(`裁切范围超出图片尺寸: image=${imageWidth}x${imageHeight}, crop=${x},${y},${width},${height}`, true);
281
+ }
282
+ await fsExtra.ensureDir(path.dirname(targetPath));
283
+ const info = await (0, sharp_1.default)(inputPath)
284
+ .extract({ left: x, top: y, width, height })
285
+ .png()
286
+ .toFile(targetPath);
287
+ return (0, tool_utils_1.createTextToolResult)([
288
+ "图片裁切成功",
289
+ `input: ${inputPath}`,
290
+ `output: ${targetPath}`,
291
+ `crop: x=${x}, y=${y}, width=${width}, height=${height}`,
292
+ `size: ${info.size} bytes`,
293
+ ].join("\n"));
294
+ });
295
+ server.registerTool("screen_crop", {
296
+ title: "Screen Crop",
297
+ description: "从当前默认设备截图中按坐标裁切 PNG 文件,适合自动截取找图模板。",
298
+ inputSchema: {
299
+ x: z.number().int().min(0).describe("裁切起点 x"),
300
+ y: z.number().int().min(0).describe("裁切起点 y"),
301
+ width: z.number().int().min(1).describe("裁切宽度"),
302
+ height: z.number().int().min(1).describe("裁切高度"),
303
+ outputPath: z
304
+ .string()
305
+ .min(1)
306
+ .optional()
307
+ .describe("可选输出路径;不传则写入系统临时目录"),
308
+ },
309
+ }, async ({ x, y, width, height, outputPath }) => {
310
+ const target = await (0, tool_utils_1.resolveRuntimeHttpTarget)();
311
+ const screenshot = await (0, project_1.getScreenshotOnDevice)((0, tool_utils_1.createRuntimeHttpRequestOptions)(target));
312
+ const metadata = await (0, sharp_1.default)(screenshot).metadata();
313
+ const imageWidth = metadata.width ?? 0;
314
+ const imageHeight = metadata.height ?? 0;
315
+ if (x + width > imageWidth || y + height > imageHeight) {
316
+ return (0, tool_utils_1.createTextToolResult)(`裁切范围超出截图尺寸: image=${imageWidth}x${imageHeight}, crop=${x},${y},${width},${height}`, true);
317
+ }
318
+ const targetPath = resolveGeneratedImageOutputPath(outputPath, "ms-mcp-screen-crop", "png");
319
+ await fsExtra.ensureDir(path.dirname(targetPath));
320
+ const info = await (0, sharp_1.default)(screenshot)
321
+ .extract({ left: x, top: y, width, height })
322
+ .png()
323
+ .toFile(targetPath);
324
+ return (0, tool_utils_1.createTextToolResult)([
325
+ "屏幕裁图成功",
326
+ `device: ${target.label}`,
327
+ `output: ${targetPath}`,
328
+ `screen: ${imageWidth}x${imageHeight}`,
329
+ `crop: x=${x}, y=${y}, width=${width}, height=${height}`,
330
+ `size: ${info.size} bytes`,
331
+ ].join("\n"));
332
+ });
333
+ server.registerTool("image_pick_color", {
334
+ title: "Image Pick Color",
335
+ description: "读取本地图片指定坐标颜色,返回 hex/rgb/rgba,适合生成找色代码。",
336
+ inputSchema: {
337
+ imagePath: z.string().min(1).describe("输入图片路径"),
338
+ x: z.number().int().min(0).describe("目标坐标 x"),
339
+ y: z.number().int().min(0).describe("目标坐标 y"),
340
+ radius: z
341
+ .number()
342
+ .int()
343
+ .min(0)
344
+ .max(20)
345
+ .optional()
346
+ .default(0)
347
+ .describe("采样半径,0 表示只取单点,默认 0"),
348
+ },
349
+ }, async ({ imagePath, x, y, radius }) => {
350
+ const inputPath = await resolveExistingImagePath(imagePath);
351
+ const image = await readRgbaImage(inputPath);
352
+ const picked = pickAverageColor(image.data, image.width, image.height, x, y, radius);
353
+ return (0, tool_utils_1.createTextToolResult)(formatPickedColorText(inputPath, x, y, radius, image.width, image.height, picked.color, picked.sampleCount));
354
+ });
355
+ server.registerTool("screen_pick_color", {
356
+ title: "Screen Pick Color",
357
+ description: "从当前默认设备截图中读取指定坐标颜色,返回 hex/rgb/rgba,适合自动生成找色代码。",
358
+ inputSchema: {
359
+ x: z.number().int().min(0).describe("目标坐标 x"),
360
+ y: z.number().int().min(0).describe("目标坐标 y"),
361
+ radius: z
362
+ .number()
363
+ .int()
364
+ .min(0)
365
+ .max(20)
366
+ .optional()
367
+ .default(0)
368
+ .describe("采样半径,0 表示只取单点,默认 0"),
369
+ },
370
+ }, async ({ x, y, radius }) => {
371
+ const target = await (0, tool_utils_1.resolveRuntimeHttpTarget)();
372
+ const screenshot = await (0, project_1.getScreenshotOnDevice)((0, tool_utils_1.createRuntimeHttpRequestOptions)(target));
373
+ const image = await readRgbaImage(screenshot);
374
+ const picked = pickAverageColor(image.data, image.width, image.height, x, y, radius);
375
+ return (0, tool_utils_1.createTextToolResult)(formatPickedColorText(target.label, x, y, radius, image.width, image.height, picked.color, picked.sampleCount));
376
+ });
377
+ server.registerTool("image_make_transparent", {
378
+ title: "Image Make Transparent",
379
+ description: "将图片中接近指定颜色或角落背景色的像素设为透明,并输出 PNG 文件,适合制作找图模板透明图。",
380
+ inputSchema: {
381
+ imagePath: z.string().min(1).describe("输入图片路径"),
382
+ outputPath: z
383
+ .string()
384
+ .min(1)
385
+ .optional()
386
+ .describe("可选输出路径;不传则在输入图片同目录生成 *-transparent.png"),
387
+ transparentColor: z
388
+ .string()
389
+ .min(1)
390
+ .optional()
391
+ .describe("要透明化的颜色,格式 #RRGGBB;不传则从指定角落采样"),
392
+ sampleCorner: z
393
+ .enum(["topLeft", "topRight", "bottomLeft", "bottomRight"])
394
+ .optional()
395
+ .default("topLeft")
396
+ .describe("未传 transparentColor 时采样的背景角落,默认 topLeft"),
397
+ tolerance: z
398
+ .number()
399
+ .int()
400
+ .min(0)
401
+ .max(441)
402
+ .optional()
403
+ .default(24)
404
+ .describe("RGB 欧氏距离容差,默认 24,最大 441"),
405
+ },
406
+ }, async ({ imagePath, outputPath, transparentColor, sampleCorner, tolerance, }) => {
407
+ const inputPath = await resolveExistingImagePath(imagePath);
408
+ const targetPath = resolveImageOutputPath(inputPath, outputPath, "transparent");
409
+ const { data, info } = await (0, sharp_1.default)(inputPath)
410
+ .ensureAlpha()
411
+ .raw()
412
+ .toBuffer({ resolveWithObject: true });
413
+ const targetColor = transparentColor
414
+ ? parseHexColor(transparentColor)
415
+ : sampleCornerColor(data, info.width, info.height, sampleCorner);
416
+ let transparentPixelCount = 0;
417
+ for (let offset = 0; offset < data.length; offset += 4) {
418
+ if (isColorMatch(data, offset, targetColor, tolerance)) {
419
+ data[offset + 3] = 0;
420
+ transparentPixelCount += 1;
421
+ }
422
+ }
423
+ await fsExtra.ensureDir(path.dirname(targetPath));
424
+ const output = await (0, sharp_1.default)(data, {
425
+ raw: {
426
+ width: info.width,
427
+ height: info.height,
428
+ channels: 4,
429
+ },
430
+ })
431
+ .png()
432
+ .toFile(targetPath);
433
+ return (0, tool_utils_1.createTextToolResult)([
434
+ "透明图制作成功",
435
+ `input: ${inputPath}`,
436
+ `output: ${targetPath}`,
437
+ `image: ${info.width}x${info.height}`,
438
+ `transparentColor: ${formatRgbHex(targetColor)}`,
439
+ `tolerance: ${tolerance}`,
440
+ `transparentPixels: ${transparentPixelCount}`,
441
+ `size: ${output.size} bytes`,
442
+ ].join("\n"));
443
+ });
444
+ }
@@ -0,0 +1,9 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ /**
3
+ * 注册 OCR MCP 工具
4
+ * @param server MCP 服务实例
5
+ * @returns 无返回值
6
+ * @example
7
+ * registerOcrTools(server)
8
+ */
9
+ export declare function registerOcrTools(server: McpServer): void;
@@ -0,0 +1,348 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.registerOcrTools = registerOcrTools;
37
+ const fsExtra = __importStar(require("fs-extra"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const z = __importStar(require("zod/v4"));
41
+ const tool_utils_1 = require("./tool-utils");
42
+ const APPLE_OCR_LANGUAGES = [
43
+ "en-US",
44
+ "fr-FR",
45
+ "it-IT",
46
+ "de-DE",
47
+ "es-ES",
48
+ "pt-BR",
49
+ "zh-Hans",
50
+ "zh-Hant",
51
+ ];
52
+ /**
53
+ * 生成安全的 JavaScript 字面量
54
+ * @param value 任意 JSON 兼容值
55
+ * @returns 返回可嵌入脚本的 JSON 字面量
56
+ * @example
57
+ * jsonLiteral(["zh-Hans", "en-US"])
58
+ */
59
+ function jsonLiteral(value) {
60
+ return JSON.stringify(value);
61
+ }
62
+ /**
63
+ * 解析 OCR 输出文件路径
64
+ * @param outputPath 用户指定的可选路径
65
+ * @returns 返回最终输出文件绝对路径
66
+ * @example
67
+ * resolveOcrOutputPath(undefined)
68
+ */
69
+ function resolveOcrOutputPath(outputPath) {
70
+ if (outputPath && outputPath.trim()) {
71
+ return path.resolve(outputPath.trim());
72
+ }
73
+ return path.join(os.tmpdir(), `ms-mcp-ocr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
74
+ }
75
+ /**
76
+ * 构建 Apple OCR 运行脚本
77
+ * @param mode OCR 模式
78
+ * @param input 输入源
79
+ * @param x 区域左上角 x
80
+ * @param y 区域左上角 y
81
+ * @param ex 区域右下角 x
82
+ * @param ey 区域右下角 y
83
+ * @param texts 查找文本数组
84
+ * @param languages 识别语言数组
85
+ * @returns 返回可交给 runScript 的 JavaScript 脚本
86
+ * @example
87
+ * buildAppleOcrScript("recognize", "screen", 0, 0, 0, 0)
88
+ */
89
+ function buildAppleOcrScript(mode, input, x, y, ex, ey, texts, languages) {
90
+ if (mode === "findText") {
91
+ if (!texts || texts.length === 0) {
92
+ throw new Error("findText 模式必须传 texts。");
93
+ }
94
+ return `appleOcr.findText(${jsonLiteral(input)}, ${jsonLiteral(texts)}, ${x}, ${y}, ${ex}, ${ey}, ${jsonLiteral(languages)});`;
95
+ }
96
+ if (mode === "numbers") {
97
+ return `appleOcr.recognizeNumbers(${jsonLiteral(input)}, ${x}, ${y}, ${ex}, ${ey});`;
98
+ }
99
+ return `appleOcr.recognize(${jsonLiteral(input)}, ${x}, ${y}, ${ex}, ${ey}, ${jsonLiteral(languages)});`;
100
+ }
101
+ /**
102
+ * 构建 Paddle OCR 运行脚本
103
+ * @param mode OCR 模式
104
+ * @param input 输入源
105
+ * @param x 区域左上角 x
106
+ * @param y 区域左上角 y
107
+ * @param ex 区域右下角 x
108
+ * @param ey 区域右下角 y
109
+ * @param texts 查找文本数组
110
+ * @param confidenceThreshold 置信度阈值
111
+ * @param maxSideLen 模型最大边长
112
+ * @param useGpu 是否使用 GPU
113
+ * @returns 返回可交给 runScript 的 JavaScript 脚本
114
+ * @example
115
+ * buildPaddleOcrScript("recognize", "screen", 0, 0, 0, 0)
116
+ */
117
+ function buildPaddleOcrScript(mode, input, x, y, ex, ey, texts, confidenceThreshold, maxSideLen, useGpu) {
118
+ if (mode === "numbers") {
119
+ throw new Error("PaddleOCR 文档未提供 numbers 模式,请使用 appleocr 引擎。");
120
+ }
121
+ const loadScript = `const __loaded = paddleOcr.loadV5(${maxSideLen}, ${useGpu});`;
122
+ const loadFailedScript = 'let __ocrResult; if (!__loaded) { __ocrResult = { success: false, error: "PaddleOCR loadV5 failed" }; }';
123
+ if (mode === "findText") {
124
+ if (!texts || texts.length === 0) {
125
+ throw new Error("findText 模式必须传 texts。");
126
+ }
127
+ return [
128
+ loadScript,
129
+ loadFailedScript,
130
+ `if (__loaded) { __ocrResult = paddleOcr.findText(${jsonLiteral(input)}, ${jsonLiteral(texts)}, ${x}, ${y}, ${ex}, ${ey}, ${confidenceThreshold}); }`,
131
+ "__ocrResult;",
132
+ ].join("\n");
133
+ }
134
+ return [
135
+ loadScript,
136
+ loadFailedScript,
137
+ `if (__loaded) { __ocrResult = paddleOcr.recognize(${jsonLiteral(input)}, ${x}, ${y}, ${ex}, ${ey}, ${confidenceThreshold}); }`,
138
+ "__ocrResult;",
139
+ ].join("\n");
140
+ }
141
+ /**
142
+ * 构建 OCR 运行脚本
143
+ * @param engine OCR 引擎
144
+ * @param mode OCR 模式
145
+ * @param input 输入源
146
+ * @param x 区域左上角 x
147
+ * @param y 区域左上角 y
148
+ * @param ex 区域右下角 x
149
+ * @param ey 区域右下角 y
150
+ * @param texts 查找文本数组
151
+ * @param languages Apple OCR 识别语言数组
152
+ * @param confidenceThreshold PaddleOCR 置信度阈值
153
+ * @param paddleMaxSideLen PaddleOCR 模型最大边长
154
+ * @param paddleUseGpu PaddleOCR 是否使用 GPU
155
+ * @returns 返回可交给 runScript 的 JavaScript 脚本
156
+ * @example
157
+ * buildOcrScript("appleocr", "recognize", "screen", 0, 0, 0, 0)
158
+ */
159
+ function buildOcrScript(engine, mode, input, x, y, ex, ey, texts, languages, confidenceThreshold, paddleMaxSideLen, paddleUseGpu) {
160
+ if (engine === "paddleocr") {
161
+ return buildPaddleOcrScript(mode, input, x, y, ex, ey, texts, confidenceThreshold, paddleMaxSideLen, paddleUseGpu);
162
+ }
163
+ return buildAppleOcrScript(mode, input, x, y, ex, ey, texts, languages);
164
+ }
165
+ /**
166
+ * 调用设备 runScript 接口
167
+ * @param ip 设备 IP
168
+ * @param port 设备端口
169
+ * @param script JavaScript 脚本文本
170
+ * @param timeoutMs 超时时间
171
+ * @returns 返回 runScript 的 JSON 响应
172
+ * @example
173
+ * await callRunScript("192.168.1.10", "9800", "1 + 1;", 30000)
174
+ */
175
+ async function callRunScript(ip, port, script, timeoutMs) {
176
+ const controller = new AbortController();
177
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
178
+ try {
179
+ const response = await fetch(`http://${ip}:${port}/api/runScript`, {
180
+ method: "POST",
181
+ signal: controller.signal,
182
+ headers: {
183
+ "Content-Type": "application/json",
184
+ },
185
+ body: JSON.stringify({ script }),
186
+ });
187
+ const text = await response.text();
188
+ let body;
189
+ try {
190
+ body = JSON.parse(text);
191
+ }
192
+ catch {
193
+ throw new Error(`runScript 响应不是有效 JSON: ${text}`);
194
+ }
195
+ if (!response.ok) {
196
+ throw new Error(`runScript HTTP ${response.status}: ${text}`);
197
+ }
198
+ return {
199
+ status: response.status,
200
+ body,
201
+ };
202
+ }
203
+ catch (error) {
204
+ if (error instanceof Error && error.name === "AbortError") {
205
+ throw new Error(`runScript 请求超时: ${timeoutMs}ms`);
206
+ }
207
+ throw error;
208
+ }
209
+ finally {
210
+ clearTimeout(timeout);
211
+ }
212
+ }
213
+ /**
214
+ * 格式化 OCR 工具结果
215
+ * @param response runScript 响应
216
+ * @param outputPath 可选输出文件路径
217
+ * @returns 返回 MCP 文本内容
218
+ * @example
219
+ * await formatOcrToolText({ success: true, result: [] })
220
+ */
221
+ async function formatOcrToolText(response, outputPath) {
222
+ const resultCount = Array.isArray(response.result)
223
+ ? response.result.length
224
+ : undefined;
225
+ const responseText = (0, tool_utils_1.formatRuntimeJsonText)(response);
226
+ if (outputPath || responseText.length > 12000) {
227
+ const targetPath = resolveOcrOutputPath(outputPath);
228
+ await fsExtra.ensureDir(path.dirname(targetPath));
229
+ await fsExtra.writeJson(targetPath, response, { spaces: 2 });
230
+ return [
231
+ "OCR 执行成功",
232
+ ...(resultCount === undefined ? [] : [`resultCount: ${resultCount}`]),
233
+ `resultType: ${response.resultType ?? "unknown"}`,
234
+ `output: ${targetPath}`,
235
+ ].join("\n");
236
+ }
237
+ return [
238
+ "OCR 执行成功",
239
+ ...(resultCount === undefined ? [] : [`resultCount: ${resultCount}`]),
240
+ `resultType: ${response.resultType ?? "unknown"}`,
241
+ "",
242
+ responseText,
243
+ ].join("\n");
244
+ }
245
+ /**
246
+ * 注册 OCR MCP 工具
247
+ * @param server MCP 服务实例
248
+ * @returns 无返回值
249
+ * @example
250
+ * registerOcrTools(server)
251
+ */
252
+ function registerOcrTools(server) {
253
+ server.registerTool("ocr_recognize", {
254
+ title: "OCR Recognize",
255
+ description: "通过设备 POST /api/runScript 执行快点JS OCR。支持 appleocr 与 paddleocr,默认 appleocr;OCR 没有独立 HTTP 接口时使用本工具。",
256
+ inputSchema: {
257
+ engine: z
258
+ .enum(["appleocr", "paddleocr"])
259
+ .optional()
260
+ .default("appleocr")
261
+ .describe("OCR 引擎:appleocr=Apple Vision,paddleocr=PaddleOCR"),
262
+ mode: z
263
+ .enum(["recognize", "numbers", "findText"])
264
+ .optional()
265
+ .default("recognize")
266
+ .describe("OCR 模式:recognize=识别文本,numbers=识别数字,findText=查找文本"),
267
+ input: z
268
+ .string()
269
+ .min(1)
270
+ .optional()
271
+ .default("screen")
272
+ .describe('输入源,默认 "screen",也可传图片路径、URL 或 imageId'),
273
+ x: z.number().int().min(0).optional().default(0).describe("区域左上角 x"),
274
+ y: z.number().int().min(0).optional().default(0).describe("区域左上角 y"),
275
+ ex: z
276
+ .number()
277
+ .int()
278
+ .min(0)
279
+ .optional()
280
+ .default(0)
281
+ .describe("区域右下角 x;全屏可传 0"),
282
+ ey: z
283
+ .number()
284
+ .int()
285
+ .min(0)
286
+ .optional()
287
+ .default(0)
288
+ .describe("区域右下角 y;全屏可传 0"),
289
+ texts: z
290
+ .array(z.string().min(1))
291
+ .optional()
292
+ .describe("findText 模式要查找的文本数组"),
293
+ languages: z
294
+ .array(z.enum(APPLE_OCR_LANGUAGES))
295
+ .optional()
296
+ .describe('Apple OCR 识别语言数组,默认由运行时使用 ["zh-Hans", "en-US"]'),
297
+ confidenceThreshold: z
298
+ .number()
299
+ .min(0)
300
+ .max(1)
301
+ .optional()
302
+ .default(0.6)
303
+ .describe("PaddleOCR 置信度阈值,默认 0.6"),
304
+ paddleMaxSideLen: z
305
+ .number()
306
+ .int()
307
+ .min(32)
308
+ .max(4096)
309
+ .optional()
310
+ .default(640)
311
+ .describe("PaddleOCR loadV5 最大边长,默认 640"),
312
+ paddleUseGpu: z
313
+ .boolean()
314
+ .optional()
315
+ .default(false)
316
+ .describe("PaddleOCR loadV5 是否使用 GPU,默认 false"),
317
+ outputPath: z
318
+ .string()
319
+ .min(1)
320
+ .optional()
321
+ .describe("可选输出 JSON 路径;不传且结果很长时写入系统临时目录"),
322
+ timeoutMs: z
323
+ .number()
324
+ .int()
325
+ .min(1000)
326
+ .max(600000)
327
+ .optional()
328
+ .default(60000)
329
+ .describe("runScript 请求超时时间,默认 60000 毫秒"),
330
+ },
331
+ }, async ({ engine, mode, input, x, y, ex, ey, texts, languages, confidenceThreshold, paddleMaxSideLen, paddleUseGpu, outputPath, timeoutMs, }) => {
332
+ const target = await (0, tool_utils_1.resolveRuntimeHttpTarget)();
333
+ const script = buildOcrScript(engine, mode, input, x, y, ex, ey, texts, languages, confidenceThreshold, paddleMaxSideLen, paddleUseGpu);
334
+ const response = await callRunScript(target.ip, target.port, script, timeoutMs);
335
+ const text = await formatOcrToolText(response.body, outputPath);
336
+ return (0, tool_utils_1.createTextToolResult)([
337
+ text,
338
+ "",
339
+ `device: ${target.label}`,
340
+ `engine: ${engine}`,
341
+ `mode: ${mode}`,
342
+ `input: ${input}`,
343
+ `region: x=${x}, y=${y}, ex=${ex}, ey=${ey}`,
344
+ `runScript: POST /api/runScript`,
345
+ `status: ${response.status}`,
346
+ ].join("\n"), response.body.success === false);
347
+ });
348
+ }
package/dist/mcp/tools.js CHANGED
@@ -8,6 +8,8 @@ const device_config_1 = require("./device-config");
8
8
  Object.defineProperty(exports, "DEFAULT_DEVICE_PORT", { enumerable: true, get: function () { return device_config_1.DEFAULT_DEVICE_PORT; } });
9
9
  const doc_tools_1 = require("./doc-tools");
10
10
  const httpapi_tools_1 = require("./httpapi-tools");
11
+ const image_tools_1 = require("./image-tools");
12
+ const ocr_tools_1 = require("./ocr-tools");
11
13
  const runtime_tools_1 = require("./runtime-tools");
12
14
  /**
13
15
  * 创建并注册 MCP 工具
@@ -23,6 +25,8 @@ function createMcpServer() {
23
25
  (0, doc_tools_1.registerDocResources)(server);
24
26
  (0, doc_tools_1.registerDocTools)(server);
25
27
  (0, httpapi_tools_1.registerHttpApiTools)(server);
28
+ (0, image_tools_1.registerImageTools)(server);
29
+ (0, ocr_tools_1.registerOcrTools)(server);
26
30
  (0, runtime_tools_1.registerRuntimeTools)(server);
27
31
  return server;
28
32
  }
package/docs/AGENTS.md ADDED
@@ -0,0 +1,191 @@
1
+ # AGENTS
2
+
3
+ ## 角色
4
+
5
+ 你是快点JS专用开发助手,只服务于快点JS这一特定执行环境。必须严格依据快点JS官方 API 文档、当前项目结构、当前 MCP 工具能力和项目自带 `ms` CLI 能力回答问题与编写代码。
6
+
7
+ 始终把快点JS当成特定 iOS 自动化环境,而不是普通 Node.js、浏览器或通用 Python 环境。不要臆造不存在的 API、对象、参数、行为或运行机制。遇到文档未确认、工具未提供或当前项目未实现的能力,必须明确说明限制,并给出保守方案。
8
+
9
+ ## 基本规则
10
+
11
+ - JavaScript 项目使用 JavaScript 文档与写法。
12
+ - Python 项目使用 Python 文档与写法。
13
+ - 不要混用 JavaScript 与 Python API。
14
+ - 不要把快点JS Python 脚本当成普通 CPython 脚本。
15
+ - 不要对快点JS Python 脚本运行 `python -m py_compile`、`py_compile` 或类似 CPython 编译校验。
16
+ - 不要创建、保留或同步 `__pycache__`、`.pyc` 作为项目改动。
17
+ - 文档未确认的能力必须明确说明无法确认,不要凭经验补全。
18
+
19
+ ## 目录职责
20
+
21
+ 严格遵守项目目录职责:
22
+
23
+ - `scripts`:脚本文件与运行逻辑。
24
+ - `ui`:UI 页面文件,如 HTML、页面 JavaScript。
25
+ - `res`:脚本运行真正依赖的资源文件,如图片、模板等。
26
+ - `screenshot`:调试、预览和问题定位产生的截图。
27
+
28
+ 不要把快点JS项目改写成通用 Web、Node.js 或 Python 项目结构。截图、节点树和长文本结果优先写入文件,避免 MCP 响应被截断。
29
+
30
+ 资源保存规则:
31
+
32
+ - 调试截图、预览截图、问题定位截图:保存到 `screenshot`。
33
+ - 找图模板、裁切后的目标图片、透明模板、脚本会读取或匹配的样本图:保存到 `res`。
34
+ - 节点 XML、日志快照和临时分析长文本:优先保存到临时文件或用户指定路径,不要混入 `res`。
35
+ - 使用 `image_crop`、`screen_crop`、`image_make_transparent` 生成脚本会用到的图片时,必须优先把 `outputPath` 指向 `res` 目录。
36
+
37
+ ## 文档优先
38
+
39
+ 回答快点JS API 问题或编写脚本前,必须先查询当前语言对应的快点JS文档。
40
+
41
+ MCP 可用时:
42
+
43
+ 1. 使用 `set_docs_language` 设置语言:`js`、`js_zh` 或 `python`。
44
+ 2. 使用 `search_api_docs` 搜索相关模块或 API。
45
+ 3. 使用 `read_api_doc` 读取完整文档。
46
+ 4. 只依据已确认的文档内容回答或编写代码。
47
+
48
+ MCP 不可用时:
49
+
50
+ 1. 查阅项目本地文档,例如 `node_modules/ms-vite-plugin/docs/api/`、`node_modules/ms-vite-plugin/docs/apicn/`、`node_modules/ms-vite-plugin/docs/apipython/`。
51
+ 2. 只使用文档中已经确认的 API。
52
+ 3. 在回复中说明使用了本地文档作为 MCP fallback。
53
+
54
+ ## HTTP API 工作流
55
+
56
+ 调用设备 HTTP API 前,必须先查询 HTTP API 文档。不要猜测接口路径、请求方法或参数名称。
57
+
58
+ MCP 可用时:
59
+
60
+ 1. 使用 `search_http_api_docs` 搜索目标能力、中文标题或接口路径。
61
+ 2. 使用 `read_http_api_doc` 读取目标接口片段。
62
+ 3. 确认 `method`、`path`、参数位置、参数类型和返回结构。
63
+ 4. 使用 `http_api_call` 传入文档返回的 `docSlug`、`method`、`path`、`query` 或 `body`。
64
+
65
+ `http_api_call` 只能调用 HTTP API 文档中已声明的接口,并且会校验 `docSlug`、`method`、`path` 是否匹配。
66
+
67
+ 命令 fallback 时:
68
+
69
+ 1. 先在本地 HTTP API 文档中确认接口存在。
70
+ 2. 再用 `curl` 调用明确存在的接口。
71
+ 3. 不要把未确认的路径当成可用接口。
72
+
73
+ ## 设备与运行
74
+
75
+ 设备相关操作以用户指定的设备为准。不要假设设备或工作区已经设置。
76
+
77
+ MCP 可用时:
78
+
79
+ 1. 使用 `get_device` 查看当前默认设备。
80
+ 2. 如果未设置设备,使用 `set_device` 设置设备 IP 和端口。
81
+ 3. 使用 `set_workspace` 设置快点JS项目根目录。
82
+ 4. 使用 `run_project` 运行脚本项目。
83
+ 5. 使用 `run_ui_project` 预览 UI。
84
+ 6. 使用 `stop_project` 停止当前设备上的项目。
85
+ 7. 使用 `package_project` 执行生产打包。
86
+
87
+ 命令可用时,可以在项目根目录使用:
88
+
89
+ ```bash
90
+ npx ms --help
91
+ npx ms run -i <device-ip> --port <port>
92
+ npx ms run-ui -i <device-ip> --port <port>
93
+ npx ms screenshot -i <device-ip> --port <port> --format file --output <path>
94
+ npx ms stop -i <device-ip> --port <port>
95
+ npx ms package
96
+ ```
97
+
98
+ 命令必须在快点JS项目根目录运行,项目根目录通常包含 `package.json` 和 `scripts/`。不要用未确认的通用 Node.js、浏览器或 Python 命令替代 MCP 或项目自带 `ms` CLI。
99
+
100
+ ## 工具分组
101
+
102
+ ### 文档
103
+
104
+ - `set_docs_language`
105
+ - `get_docs_language`
106
+ - `list_api_docs`
107
+ - `search_api_docs`
108
+ - `read_api_doc`
109
+ - `search_http_api_docs`
110
+ - `read_http_api_doc`
111
+
112
+ ### 工作区与运行
113
+
114
+ - `set_workspace`
115
+ - `get_workspace`
116
+ - `run_project`
117
+ - `run_ui_project`
118
+ - `stop_project`
119
+ - `package_project`
120
+
121
+ ### 设备与调试
122
+
123
+ - `set_device`
124
+ - `get_device`
125
+ - `get_logs`
126
+ - `take_screenshot`
127
+ - `get_node_source`
128
+ - `image_crop`
129
+ - `screen_crop`
130
+ - `image_pick_color`
131
+ - `screen_pick_color`
132
+ - `ocr_recognize`
133
+ - `image_make_transparent`
134
+
135
+ ### 通用 HTTP API
136
+
137
+ - `http_api_call`
138
+
139
+ 控制、HID、IME、镜像、配置、当前应用、运行脚本等普通设备 HTTP API 通过 `search_http_api_docs`、`read_http_api_doc` 和 `http_api_call` 使用,不要恢复为大量独立工具。
140
+
141
+ ## 工具选择规则
142
+
143
+ - 写快点JS脚本代码:先设置或确认文档语言,再查语言 API 文档。
144
+ - 调设备 HTTP API:先查 HTTP API 文档,再调用 `http_api_call` 或已确认接口的 `curl` fallback。
145
+ - 获取截图:优先使用 `take_screenshot`;命令 fallback 可用 `npx ms screenshot`。
146
+ - 获取节点 XML:优先使用 `get_node_source`。
147
+ - 查看日志:优先使用 `get_logs`。
148
+ - 从本地图片裁切找图模板:使用 `image_crop`,输出到 `res`。
149
+ - 从当前设备截图裁切找图模板:使用 `screen_crop`,输出到 `res`。
150
+ - 从本地图片指定坐标取色:使用 `image_pick_color`。
151
+ - 从当前设备截图指定坐标取色:使用 `screen_pick_color`。
152
+ - 制作透明找图模板:使用 `image_make_transparent`,输出到 `res`。
153
+ - 执行 OCR 识别、数字识别或查找文字:使用 `ocr_recognize`。
154
+ - 运行项目:优先使用 `run_project` 或 `run_ui_project`;命令 fallback 可用 `npx ms run` 或 `npx ms run-ui`。
155
+ - 停止项目:优先使用 `stop_project`;命令 fallback 可用 `npx ms stop`。
156
+ - 打包项目:优先使用 `package_project`;命令 fallback 可用 `npx ms package`。
157
+
158
+ ## OCR 规则
159
+
160
+ OCR 没有独立 HTTP 接口,使用 `ocr_recognize` 通过 `POST /api/runScript` 执行快点JS OCR 脚本。
161
+
162
+ - `ocr_recognize` 支持 `appleocr` 和 `paddleocr`。
163
+ - 默认使用 `appleocr`。
164
+ - 数字识别优先使用 `appleocr`,因为 PaddleOCR 文档未提供独立 numbers 模式。
165
+ - `runScript` 的规则是最后一行表达式或变量会作为结果返回,不需要写 `return`。
166
+
167
+ ## 验证要求
168
+
169
+ 设备行为改动通常需要真实设备验证:
170
+
171
+ 1. 使用 MCP `run_project` 或命令 `npx ms run -i <device-ip> --port <port>` 同步并运行项目。
172
+ 2. 使用 MCP `take_screenshot` 或命令 `npx ms screenshot ...` 获取截图。
173
+ 3. 必要时使用 `get_logs` 或日志 HTTP API 查看运行日志。
174
+ 4. 在回复中说明截图或日志是否确认了预期行为。
175
+
176
+ UI 预览发起后不需要长时间等待结果,可以通过截图查看当前界面效果。
177
+
178
+ 如果设备、MCP 或 CLI 不可用,必须明确说明无法验证的原因,以及可用后应运行的命令。
179
+
180
+ ## 禁止事项
181
+
182
+ - 不要在没有查询文档的情况下回答 API 用法或编写 API 调用代码。
183
+ - 不要臆造快点JS API、对象、参数、返回值或运行机制。
184
+ - 不要混用 JavaScript 与 Python API。
185
+ - 不要把快点JS当成普通 Node.js、浏览器或通用 Python 环境。
186
+ - 不要对快点JS Python 脚本运行 `py_compile` 或其他 CPython 编译校验。
187
+ - 不要在没有查询 HTTP API 文档的情况下调用 `http_api_call` 或 `curl`。
188
+ - 不要把完整 URL 传给 `http_api_call.path`,只能传 `/api/...`、`/logger/...`、`/mirror/...` 这类相对路径。
189
+ - 不要调用 HTTP API 文档中未声明的接口。
190
+ - 不要用 `http_api_call` 替代截图落文件、节点 XML 落文件、日志缓存、项目打包和项目运行等专用工具。
191
+ - 不要假设设备、工作区或运行环境已经准备好。
package/docs/SKILL.md CHANGED
@@ -59,7 +59,14 @@ npx ms stop -i <device-ip> --port <port>
59
59
  - `res`:脚本运行真正依赖的资源文件,如图片、模板等。
60
60
  - `screenshot`:调试、预览和问题定位产生的截图。
61
61
 
62
- 截图不属于脚本资源,用户需要保留时默认保存到 `screenshot` 目录。只有脚本运行真正依赖的资源才放入 `res`。节点树、截图、长文本结果优先写入文件后再读取,避免响应被截断。
62
+ 节点树、截图、长文本结果优先写入文件后再读取,避免响应被截断。
63
+
64
+ 资源保存规则:
65
+
66
+ - 调试截图、预览截图、问题定位截图:保存到 `screenshot`。
67
+ - 找图模板、裁切后的目标图片、透明模板、脚本会读取或匹配的样本图:保存到 `res`。
68
+ - 使用 `image_crop`、`screen_crop`、`image_make_transparent` 生成脚本会用到的图片时,必须优先把 `outputPath` 指向 `res` 目录。
69
+ - 只有脚本运行真正依赖的资源才放入 `res`,一次性观察材料不要放入 `res`。
63
70
 
64
71
  不要把 `__pycache__` 或 `.pyc` 当成项目产物保留或同步;如果本地工具意外生成,清理掉。
65
72
 
@@ -99,7 +106,7 @@ MCP 可用时:
99
106
 
100
107
  ## 设备工作流
101
108
 
102
- 设备相关操作以用户指定的设备为准。当前常用测试设备是 `192.168.31.152:9800`,但用户给出新设备时必须使用新设备。
109
+ 设备相关操作以用户指定的设备为准。不要假设设备已经设置;用户给出新设备时必须使用新设备。
103
110
 
104
111
  MCP 可用时:
105
112
 
@@ -163,6 +170,12 @@ MCP 可用时:
163
170
  - `get_logs`
164
171
  - `take_screenshot`
165
172
  - `get_node_source`
173
+ - `image_crop`
174
+ - `screen_crop`
175
+ - `image_pick_color`
176
+ - `screen_pick_color`
177
+ - `ocr_recognize`
178
+ - `image_make_transparent`
166
179
 
167
180
  ### 通用 HTTP API
168
181
 
@@ -170,6 +183,16 @@ MCP 可用时:
170
183
 
171
184
  控制、HID、IME、镜像、配置、当前应用、运行脚本等普通设备 HTTP API 通过 `search_http_api_docs`、`read_http_api_doc` 和 `http_api_call` 使用;命令 fallback 时可用已确认文档的 `curl` 调用。
172
185
 
186
+ OCR 没有独立 HTTP 接口,使用 `ocr_recognize` 通过 `POST /api/runScript` 执行快点JS OCR 脚本。`ocr_recognize` 支持 `appleocr` 和 `paddleocr`,默认 `appleocr`。`runScript` 的规则是最后一行表达式或变量会作为结果返回,不需要写 `return`。
187
+
188
+ 图片与识别工作流:
189
+
190
+ - 调试界面:先用 `take_screenshot` 保存到 `screenshot`,再观察界面。
191
+ - 生成找图模板:用 `screen_crop` 或 `image_crop`,输出路径放到 `res`。
192
+ - 生成透明找图模板:用 `image_make_transparent`,输出路径放到 `res`。
193
+ - 生成找色代码:用 `screen_pick_color` 或 `image_pick_color` 获取 `hex/rgb/rgba`。
194
+ - 生成 OCR 代码或定位文字:用 `ocr_recognize`,默认 `appleocr`,用户指定时使用 `paddleocr`。
195
+
173
196
  ## 禁止事项
174
197
 
175
198
  - 不要在没有查询文档的情况下回答 API 用法或编写 API 调用代码。
@@ -122,7 +122,14 @@ MCP 可用时:
122
122
  - `res`:脚本运行真正依赖的资源文件,如图片、模板等。
123
123
  - `screenshot`:调试、预览和问题定位产生的截图。
124
124
 
125
- 不要把快点JS项目改写成通用 Web、Node.js 或 Python 项目结构。截图、节点树和长文本结果优先写入文件,避免 MCP 响应被截断。截图不属于脚本资源,用户需要保留时默认建议保存到 `screenshot` 目录;只有脚本运行真正依赖的资源才放入 `res`。
125
+ 不要把快点JS项目改写成通用 Web、Node.js 或 Python 项目结构。截图、节点树和长文本结果优先写入文件,避免 MCP 响应被截断。
126
+
127
+ 资源保存规则:
128
+
129
+ - 调试截图、预览截图、问题定位截图:保存到 `screenshot`。
130
+ - 找图模板、裁切后的目标图片、透明模板、脚本会读取或匹配的样本图:保存到 `res`。
131
+ - 节点 XML、日志快照和临时分析长文本:优先保存到临时文件或用户指定路径,不要混入 `res`。
132
+ - 使用 `image_crop`、`screen_crop`、`image_make_transparent` 生成脚本会用到的图片时,必须优先把 `outputPath` 指向 `res` 目录。
126
133
 
127
134
  ## MCP 设备模型
128
135
 
@@ -163,6 +170,12 @@ UI 预览发起后不需要长时间等待结果,可以通过 `take_screenshot
163
170
  - `get_logs`
164
171
  - `take_screenshot`
165
172
  - `get_node_source`
173
+ - `image_crop`
174
+ - `screen_crop`
175
+ - `image_pick_color`
176
+ - `screen_pick_color`
177
+ - `ocr_recognize`
178
+ - `image_make_transparent`
166
179
 
167
180
  ### 通用 HTTP API 调用
168
181
 
@@ -175,6 +188,13 @@ UI 预览发起后不需要长时间等待结果,可以通过 `take_screenshot
175
188
  - 写快点JS脚本代码:先设置或确认文档语言,再查语言 API 文档。
176
189
  - 调设备 HTTP API:先查 HTTP API 文档,再调用 `http_api_call` 或已确认接口的 `curl` fallback。
177
190
  - 获取截图:优先使用 `take_screenshot`;命令 fallback 可用 `npx ms screenshot`。
191
+ - 从本地图片裁切找图模板:使用 `image_crop`,输出到 `res`。
192
+ - 从当前设备截图裁切找图模板:使用 `screen_crop`,输出到 `res`。
193
+ - 从本地图片指定坐标取色:使用 `image_pick_color`。
194
+ - 从当前设备截图指定坐标取色:使用 `screen_pick_color`。
195
+ - 执行 OCR 识别、数字识别或查找文字:使用 `ocr_recognize`;支持用户指定 `appleocr` 或 `paddleocr`,默认 `appleocr`。
196
+ - OCR 没有独立 HTTP 接口,通过 `POST /api/runScript` 执行快点JS OCR 脚本;`runScript` 的规则是最后一行表达式或变量会作为结果返回,不需要写 `return`。
197
+ - 制作透明找图模板:使用 `image_make_transparent`,输出到 `res`。
178
198
  - 获取节点 XML:优先使用 `get_node_source`。
179
199
  - 查看日志:优先使用 `get_logs`。
180
200
  - 运行项目:优先使用 `run_project` 或 `run_ui_project`;命令 fallback 可用 `npx ms run` 或 `npx ms run-ui`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ms-vite-plugin",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "type": "commonjs",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -26,6 +26,7 @@
26
26
  "commander": "^14.0.3",
27
27
  "crc": "^4.3.2",
28
28
  "fs-extra": "^11.3.4",
29
+ "sharp": "^0.34.5",
29
30
  "uuid": "^14.0.0",
30
31
  "vite": "^8.0.9",
31
32
  "vite-plugin-bundle-obfuscator": "^1.11.0",