ms-vite-plugin 1.4.15 → 1.4.17

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/dist/cli.js CHANGED
@@ -43,6 +43,12 @@ const project_1 = require("./project");
43
43
  const packager_1 = require("./packager");
44
44
  const ws_manager_1 = require("./ws-manager");
45
45
  const version_1 = require("./version");
46
+ const docs_service_1 = require("./mcp/docs-service");
47
+ const httpapi_docs_service_1 = require("./mcp/httpapi-docs-service");
48
+ const tool_utils_1 = require("./mcp/tool-utils");
49
+ const types_1 = require("./mcp/types");
50
+ const ocr_tools_1 = require("./mcp/ocr-tools");
51
+ const image_tools_1 = require("./mcp/image-tools");
46
52
  /**
47
53
  * WS 状态持久化文件路径(用于跨进程查询 ws-status)
48
54
  */
@@ -85,6 +91,305 @@ async function ensureValidKuaiJSProject(workspacePath) {
85
91
  console.error(" - scripts/ 目录");
86
92
  process.exit(1);
87
93
  }
94
+ /**
95
+ * 解析整数参数
96
+ * @param value 用户传入的参数文本
97
+ * @param defaultValue 默认值
98
+ * @param name 参数名称
99
+ * @param min 最小值
100
+ * @param max 最大值
101
+ * @returns 返回校验后的整数
102
+ * @example
103
+ * parseCliInteger("20", 10, "limit", 1, 100)
104
+ */
105
+ function parseCliInteger(value, defaultValue, name, min, max) {
106
+ const text = (value ?? String(defaultValue)).trim();
107
+ const numberValue = Number.parseInt(text, 10);
108
+ if (!Number.isInteger(numberValue) ||
109
+ numberValue < min ||
110
+ (max !== undefined && numberValue > max)) {
111
+ throw new Error(`无效 ${name}: ${text},允许范围 ${min}${max === undefined ? "+" : `..${max}`}`);
112
+ }
113
+ return numberValue;
114
+ }
115
+ /**
116
+ * 解析浮点数参数
117
+ * @param value 用户传入的参数文本
118
+ * @param defaultValue 默认值
119
+ * @param name 参数名称
120
+ * @param min 最小值
121
+ * @param max 最大值
122
+ * @returns 返回校验后的数字
123
+ * @example
124
+ * parseCliNumber("0.6", 0.6, "confidence", 0, 1)
125
+ */
126
+ function parseCliNumber(value, defaultValue, name, min, max) {
127
+ const text = (value ?? String(defaultValue)).trim();
128
+ const numberValue = Number.parseFloat(text);
129
+ if (!Number.isFinite(numberValue) || numberValue < min || numberValue > max) {
130
+ throw new Error(`无效 ${name}: ${text},允许范围 ${min}..${max}`);
131
+ }
132
+ return numberValue;
133
+ }
134
+ /**
135
+ * 解析枚举参数
136
+ * @param value 用户传入的参数文本
137
+ * @param defaultValue 默认值
138
+ * @param choices 允许的取值
139
+ * @param name 参数名称
140
+ * @returns 返回校验后的枚举值
141
+ * @example
142
+ * parseCliChoice("json", "text", ["text", "json"], "format")
143
+ */
144
+ function parseCliChoice(value, defaultValue, choices, name) {
145
+ const text = (value ?? defaultValue).trim();
146
+ if (!choices.includes(text)) {
147
+ throw new Error(`无效 ${name}: ${text},可选值: ${choices.join("|")}`);
148
+ }
149
+ return text;
150
+ }
151
+ /**
152
+ * 解析 HTTP API 请求方法
153
+ * @param value 用户传入的方法
154
+ * @returns 返回 HTTP API 方法
155
+ * @example
156
+ * parseHttpApiMethodOption("GET")
157
+ */
158
+ function parseHttpApiMethodOption(value) {
159
+ const method = value?.trim().toUpperCase();
160
+ return parseCliChoice(method, "GET", ["GET", "POST"], "method");
161
+ }
162
+ /**
163
+ * 解析设备 HTTP 地址
164
+ * @param options 设备命令选项
165
+ * @returns 返回设备 HTTP 目标
166
+ * @example
167
+ * parseDeviceHttpTarget({ ip: "192.168.1.10", port: "9800" })
168
+ */
169
+ function parseDeviceHttpTarget(options) {
170
+ const ip = options.ip?.trim();
171
+ if (!ip) {
172
+ throw new Error("请通过 --ip 指定设备 IP 地址");
173
+ }
174
+ const port = parseCliInteger(options.port, 9800, "port", 1, 65535);
175
+ return {
176
+ ip,
177
+ port: String(port),
178
+ label: `${ip}:${port}`,
179
+ };
180
+ }
181
+ /**
182
+ * 输出文本到终端或文件
183
+ * @param text 待输出文本
184
+ * @param output 用户指定输出路径
185
+ * @param savedLabel 保存成功提示
186
+ * @returns 输出完成后返回
187
+ * @example
188
+ * await writeOrPrintText("ok", "./out.txt", "结果已保存")
189
+ */
190
+ async function writeOrPrintText(text, output, savedLabel) {
191
+ if (output?.trim()) {
192
+ const outputPath = path.resolve(output.trim());
193
+ await fsExtra.ensureDir(path.dirname(outputPath));
194
+ await fsExtra.writeFile(outputPath, text, "utf8");
195
+ console.log(`✅ ${savedLabel}: ${outputPath}`);
196
+ return;
197
+ }
198
+ console.log(text);
199
+ }
200
+ /**
201
+ * 格式化 CLI 文档路径清单
202
+ * @returns 返回本地文档路径文本
203
+ * @example
204
+ * const text = formatCliDocsPathsText()
205
+ */
206
+ function formatCliDocsPathsText() {
207
+ const languages = ["js", "js_zh", "python"];
208
+ return [
209
+ "KuaiJS 文档路径:",
210
+ `docsRoot: ${(0, docs_service_1.getDocsRootDir)()}`,
211
+ ...languages.map((language) => `${language}: ${(0, docs_service_1.getDocsDirByLanguage)(language)} (${types_1.DOC_LANGUAGE_LABELS[language]})`),
212
+ `httpApi: ${(0, httpapi_docs_service_1.getHttpApiDocPath)()}`,
213
+ "说明: 直接读取上述 markdown 路径。",
214
+ ].join("\n");
215
+ }
216
+ /**
217
+ * 收集可重复字符串参数
218
+ * @param value 当前参数值
219
+ * @param previous 之前收集的值
220
+ * @returns 返回追加后的数组
221
+ * @example
222
+ * collectStringOption("登录", [])
223
+ */
224
+ function collectStringOption(value, previous) {
225
+ previous.push(value);
226
+ return previous;
227
+ }
228
+ /**
229
+ * 解析字符串数组参数
230
+ * @param value 用户传入文本
231
+ * @param name 参数名
232
+ * @returns 返回字符串数组或 undefined
233
+ * @example
234
+ * parseStringArrayOption("zh-Hans,en-US", "languages")
235
+ */
236
+ function parseStringArrayOption(value, name) {
237
+ const text = value?.trim();
238
+ if (!text) {
239
+ return undefined;
240
+ }
241
+ if (text.startsWith("[")) {
242
+ const parsed = JSON.parse(text);
243
+ if (!Array.isArray(parsed) ||
244
+ !parsed.every((item) => typeof item === "string" && item.trim())) {
245
+ throw new Error(`${name} 必须是字符串数组。`);
246
+ }
247
+ return parsed.map((item) => item.trim());
248
+ }
249
+ const items = text
250
+ .split(",")
251
+ .map((item) => item.trim())
252
+ .filter((item) => item.length > 0);
253
+ if (items.length === 0) {
254
+ throw new Error(`${name} 不能为空。`);
255
+ }
256
+ return items;
257
+ }
258
+ /**
259
+ * 解析 Apple OCR 语言数组
260
+ * @param value 用户传入语言文本
261
+ * @returns 返回语言数组或 undefined
262
+ * @example
263
+ * parseAppleOcrLanguages("zh-Hans,en-US")
264
+ */
265
+ function parseAppleOcrLanguages(value) {
266
+ const languages = parseStringArrayOption(value, "languages");
267
+ if (!languages) {
268
+ return undefined;
269
+ }
270
+ const allowed = new Set(ocr_tools_1.APPLE_OCR_LANGUAGES);
271
+ const invalid = languages.filter((language) => !allowed.has(language));
272
+ if (invalid.length > 0) {
273
+ throw new Error(`无效 languages: ${invalid.join(", ")},可选值: ${ocr_tools_1.APPLE_OCR_LANGUAGES.join(", ")}`);
274
+ }
275
+ return languages;
276
+ }
277
+ /**
278
+ * 解析 JSON 对象参数
279
+ * @param value 用户传入 JSON 文本
280
+ * @param name 参数名
281
+ * @returns 返回对象或 undefined
282
+ * @example
283
+ * parseJsonRecordOption('{"x":1}', "query")
284
+ */
285
+ function parseJsonRecordOption(value, name) {
286
+ const text = value?.trim();
287
+ if (!text) {
288
+ return undefined;
289
+ }
290
+ const parsed = JSON.parse(text);
291
+ if (!parsed ||
292
+ typeof parsed !== "object" ||
293
+ Array.isArray(parsed) ||
294
+ !Object.values(parsed).every((item) => typeof item === "string" ||
295
+ typeof item === "number" ||
296
+ typeof item === "boolean" ||
297
+ item === null)) {
298
+ throw new Error(`${name} 必须是 JSON 对象,且值只能是 string/number/boolean/null。`);
299
+ }
300
+ return parsed;
301
+ }
302
+ /**
303
+ * 解析 JSON 请求体参数
304
+ * @param value 用户传入请求体文本
305
+ * @returns 返回 JSON 值或 undefined
306
+ * @example
307
+ * parseJsonBodyOption('{"x":1}')
308
+ */
309
+ function parseJsonBodyOption(value) {
310
+ const text = value?.trim();
311
+ if (text === undefined || text.length === 0) {
312
+ return undefined;
313
+ }
314
+ try {
315
+ return JSON.parse(text);
316
+ }
317
+ catch {
318
+ return value;
319
+ }
320
+ }
321
+ /**
322
+ * 标准化 HTTP API 路径
323
+ * @param apiPath 用户传入路径
324
+ * @returns 返回相对路径
325
+ * @example
326
+ * normalizeCliHttpApiPath("/api/status")
327
+ */
328
+ function normalizeCliHttpApiPath(apiPath) {
329
+ const normalizedPath = apiPath.trim();
330
+ if (/^https?:\/\//i.test(normalizedPath)) {
331
+ throw new Error("path 只能传 HTTP API 相对路径,不能传完整 URL。");
332
+ }
333
+ if (!normalizedPath.startsWith("/")) {
334
+ throw new Error("path 必须以 / 开头,例如 /api/status。");
335
+ }
336
+ if (normalizedPath.includes("?")) {
337
+ throw new Error("path 不能包含 query string,请使用 --query 传递查询参数。");
338
+ }
339
+ return normalizedPath;
340
+ }
341
+ /**
342
+ * 追加 HTTP query 参数
343
+ * @param url 目标 URL
344
+ * @param query query 参数
345
+ * @returns 无返回值
346
+ * @example
347
+ * appendCliQueryParams(url, { x: 1 })
348
+ */
349
+ function appendCliQueryParams(url, query) {
350
+ if (!query) {
351
+ return;
352
+ }
353
+ for (const [key, value] of Object.entries(query)) {
354
+ if (value === null) {
355
+ continue;
356
+ }
357
+ url.searchParams.set(key, String(value));
358
+ }
359
+ }
360
+ /**
361
+ * 创建 HTTP API 请求体
362
+ * @param body 用户传入请求体
363
+ * @returns 返回 fetch 请求体和头
364
+ * @example
365
+ * createCliHttpRequestBody({ x: 1 })
366
+ */
367
+ function createCliHttpRequestBody(body) {
368
+ if (body === undefined) {
369
+ return {};
370
+ }
371
+ return {
372
+ headers: {
373
+ "Content-Type": "application/json",
374
+ },
375
+ requestBody: JSON.stringify(body),
376
+ };
377
+ }
378
+ /**
379
+ * 读取 HTTP API 响应文本
380
+ * @param response fetch 响应
381
+ * @param responseFormat 响应格式
382
+ * @returns 返回格式化文本
383
+ * @example
384
+ * await readCliHttpApiResponseText(response, "json")
385
+ */
386
+ async function readCliHttpApiResponseText(response, responseFormat) {
387
+ const responseText = await response.text();
388
+ if (responseFormat === "text") {
389
+ return responseText;
390
+ }
391
+ return (0, tool_utils_1.formatRuntimeJsonText)(JSON.parse(responseText));
392
+ }
88
393
  /**
89
394
  * 构建命令处理函数
90
395
  * @param options 命令选项
@@ -175,8 +480,6 @@ async function runUICommand(options) {
175
480
  */
176
481
  async function stopCommand(options) {
177
482
  try {
178
- const workspacePath = process.cwd();
179
- await ensureValidKuaiJSProject(workspacePath);
180
483
  await (0, project_1.stopOnDevice)(options);
181
484
  }
182
485
  catch (error) {
@@ -193,7 +496,6 @@ async function stopCommand(options) {
193
496
  */
194
497
  async function screenshotCommand(options) {
195
498
  try {
196
- await ensureValidKuaiJSProject(process.cwd());
197
499
  const format = options.format ?? "file";
198
500
  if (format === "base64") {
199
501
  const base64 = await (0, project_1.getScreenshotBase64OnDevice)(options);
@@ -218,22 +520,26 @@ async function screenshotCommand(options) {
218
520
  * @param options 命令选项
219
521
  * @returns 请求完成后退出
220
522
  * @example
221
- * ms source --ip 192.168.1.100 --port 9800 --max-depth 50 --timeout 120
523
+ * ms source --ip 192.168.1.100 --port 9800 --max-depth 50 --timeout 120 --mode 1
222
524
  */
223
525
  async function sourceCommand(options) {
224
526
  try {
225
- await ensureValidKuaiJSProject(process.cwd());
226
527
  const maxDepthText = (options.maxDepth ?? "50").trim();
227
528
  const timeoutText = (options.timeout ?? "120").trim();
529
+ const modeText = (options.mode ?? "1").trim();
228
530
  const maxDepth = Number.parseInt(maxDepthText, 10);
229
531
  const timeout = Number.parseInt(timeoutText, 10);
532
+ const mode = Number.parseInt(modeText, 10);
230
533
  if (!Number.isInteger(maxDepth) || maxDepth < 1) {
231
534
  throw new Error(`无效 max-depth: ${maxDepthText}`);
232
535
  }
233
536
  if (!Number.isInteger(timeout) || timeout < 1) {
234
537
  throw new Error(`无效 timeout: ${timeoutText}`);
235
538
  }
236
- const source = await (0, project_1.getSourceOnDevice)(options, maxDepth, timeout);
539
+ if (!Number.isInteger(mode) || (mode !== 1 && mode !== 2)) {
540
+ throw new Error(`无效 mode: ${modeText}`);
541
+ }
542
+ const source = await (0, project_1.getSourceOnDevice)(options, maxDepth, timeout, mode);
237
543
  if (options.output?.trim()) {
238
544
  const outputPath = path.resolve(options.output.trim());
239
545
  await fsExtra.ensureDir(path.dirname(outputPath));
@@ -248,6 +554,293 @@ async function sourceCommand(options) {
248
554
  process.exit(1);
249
555
  }
250
556
  }
557
+ /**
558
+ * logs 命令处理函数
559
+ * @param options 命令选项
560
+ * @returns 请求完成后退出
561
+ * @example
562
+ * ms logs --ip 192.168.1.100 --port 9800 --limit 200
563
+ */
564
+ async function logsCommand(options) {
565
+ try {
566
+ const limit = parseCliInteger(options.limit, 200, "limit", 1, 5000);
567
+ const format = parseCliChoice(options.format, "text", ["text", "json"], "format");
568
+ const result = await (0, project_1.getCurrentLogLinesOnDevice)(options, limit);
569
+ const text = format === "json"
570
+ ? (0, tool_utils_1.formatRuntimeJsonText)(result)
571
+ : result.lines.length > 0
572
+ ? result.lines.join("\n")
573
+ : "无日志";
574
+ await writeOrPrintText(text, options.output, "日志已保存");
575
+ if (format === "text" && result.hasMore && !options.output?.trim()) {
576
+ console.error(`还有更早日志,可增大 --limit,当前最多 ${limit} 行。`);
577
+ }
578
+ }
579
+ catch (error) {
580
+ console.error("❌ 获取日志失败:", error instanceof Error ? error.message : error);
581
+ process.exit(1);
582
+ }
583
+ }
584
+ /**
585
+ * docs paths 命令处理函数
586
+ * @param options 命令选项
587
+ * @returns 请求完成后退出
588
+ * @example
589
+ * ms docs paths
590
+ */
591
+ async function docsPathsCommand(options) {
592
+ try {
593
+ await writeOrPrintText(formatCliDocsPathsText(), options.output, "文档路径已保存");
594
+ }
595
+ catch (error) {
596
+ console.error("❌ 输出文档路径失败:", error instanceof Error ? error.message : error);
597
+ process.exit(1);
598
+ }
599
+ }
600
+ /**
601
+ * http-call 命令处理函数
602
+ * @param options 命令选项
603
+ * @returns 请求完成后退出
604
+ * @example
605
+ * ms http-call --ip 192.168.1.100 --method GET --path /api/status
606
+ */
607
+ async function httpCallCommand(options) {
608
+ try {
609
+ const target = parseDeviceHttpTarget(options);
610
+ const method = parseHttpApiMethodOption(options.method);
611
+ const apiPath = normalizeCliHttpApiPath(options.path ?? "");
612
+ const entry = await (0, httpapi_docs_service_1.findHttpApiDocEntry)(apiPath, method);
613
+ if (!entry) {
614
+ throw new Error(`HTTP API 文档中不存在该接口: ${method} ${apiPath}。请先读取 docs paths 返回的 HTTP API 文档路径。`);
615
+ }
616
+ const query = parseJsonRecordOption(options.query, "query");
617
+ const body = parseJsonBodyOption(options.body);
618
+ if (method === "GET" && body !== undefined) {
619
+ throw new Error("GET 接口不能传 body,请使用 --query。");
620
+ }
621
+ const responseFormat = parseCliChoice(options.responseFormat, "json", ["json", "text"], "response-format");
622
+ const timeoutMs = parseCliInteger(options.timeoutMs, 30000, "timeout-ms", 1000, 600000);
623
+ const url = new URL(`http://${target.ip}:${target.port}${apiPath}`);
624
+ appendCliQueryParams(url, query);
625
+ const controller = new AbortController();
626
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
627
+ try {
628
+ const { headers, requestBody } = createCliHttpRequestBody(body);
629
+ const response = await fetch(url.toString(), {
630
+ method,
631
+ signal: controller.signal,
632
+ headers,
633
+ body: requestBody,
634
+ });
635
+ const responseText = await readCliHttpApiResponseText(response, responseFormat);
636
+ const text = [
637
+ `HTTP API 调用${response.ok ? "成功" : "失败"}: ${method} ${apiPath}`,
638
+ `设备: ${target.label}`,
639
+ `文档: ${entry.title} (lines ${entry.startLine}-${entry.endLine})`,
640
+ `状态码: ${response.status}`,
641
+ "",
642
+ responseText,
643
+ ].join("\n");
644
+ await writeOrPrintText(text, options.output, "HTTP API 响应已保存");
645
+ if (!response.ok) {
646
+ process.exit(1);
647
+ }
648
+ }
649
+ finally {
650
+ clearTimeout(timeout);
651
+ }
652
+ }
653
+ catch (error) {
654
+ const message = error instanceof Error && error.name === "AbortError"
655
+ ? "请求超时"
656
+ : error instanceof Error
657
+ ? error.message
658
+ : String(error);
659
+ console.error("❌ HTTP API 调用失败:", message);
660
+ process.exit(1);
661
+ }
662
+ }
663
+ /**
664
+ * ocr 命令处理函数
665
+ * @param options 命令选项
666
+ * @returns 请求完成后退出
667
+ * @example
668
+ * ms ocr --ip 192.168.1.100 --mode recognize
669
+ */
670
+ async function ocrCommand(options) {
671
+ try {
672
+ const target = parseDeviceHttpTarget(options);
673
+ const engine = parseCliChoice(options.engine, "appleocr", ["appleocr", "paddleocr"], "engine");
674
+ const mode = parseCliChoice(options.mode, "recognize", ["recognize", "numbers", "findText"], "mode");
675
+ const input = options.input?.trim() || "screen";
676
+ const x = parseCliInteger(options.x, 0, "x", 0);
677
+ const y = parseCliInteger(options.y, 0, "y", 0);
678
+ const ex = parseCliInteger(options.ex, 0, "ex", 0);
679
+ const ey = parseCliInteger(options.ey, 0, "ey", 0);
680
+ const textItems = [
681
+ ...(parseStringArrayOption(options.texts, "texts") ?? []),
682
+ ...(options.text ?? []),
683
+ ];
684
+ const texts = textItems.length > 0 ? textItems : undefined;
685
+ const languages = parseAppleOcrLanguages(options.languages);
686
+ const confidenceThreshold = parseCliNumber(options.confidenceThreshold, 0.6, "confidence-threshold", 0, 1);
687
+ const timeoutMs = parseCliInteger(options.timeoutMs, 60000, "timeout-ms", 1000, 600000);
688
+ const format = parseCliChoice(options.format, "summary", ["summary", "json"], "format");
689
+ const result = await (0, ocr_tools_1.runOcrRecognitionOnDevice)(target, {
690
+ engine,
691
+ mode,
692
+ input,
693
+ x,
694
+ y,
695
+ ex,
696
+ ey,
697
+ texts,
698
+ languages,
699
+ confidenceThreshold,
700
+ exactMatch: options.exactMatch === true,
701
+ outputPath: format === "summary" ? options.output : undefined,
702
+ timeoutMs,
703
+ });
704
+ const text = format === "json" ? (0, tool_utils_1.formatRuntimeJsonText)(result.body) : result.text;
705
+ if (format === "json") {
706
+ await writeOrPrintText(text, options.output, "OCR 结果已保存");
707
+ }
708
+ else {
709
+ console.log([
710
+ text,
711
+ "",
712
+ `device: ${target.label}`,
713
+ `engine: ${engine}`,
714
+ `mode: ${mode}`,
715
+ `input: ${input}`,
716
+ `region: x=${x}, y=${y}, ex=${ex}, ey=${ey}`,
717
+ `status: ${result.status}`,
718
+ ].join("\n"));
719
+ }
720
+ if (result.body.success === false) {
721
+ process.exit(1);
722
+ }
723
+ }
724
+ catch (error) {
725
+ console.error("❌ OCR 执行失败:", error instanceof Error ? error.message : error);
726
+ process.exit(1);
727
+ }
728
+ }
729
+ /**
730
+ * image-crop 命令处理函数
731
+ * @param options 命令选项
732
+ * @returns 请求完成后退出
733
+ * @example
734
+ * ms image-crop --image ./screen.jpg --x 0 --y 0 --width 100 --height 100
735
+ */
736
+ async function imageCropCommand(options) {
737
+ try {
738
+ const imagePath = options.image?.trim();
739
+ if (!imagePath) {
740
+ throw new Error("请通过 --image 指定输入图片路径。");
741
+ }
742
+ const x = parseCliInteger(options.x, 0, "x", 0);
743
+ const y = parseCliInteger(options.y, 0, "y", 0);
744
+ const width = parseCliInteger(options.width, 1, "width", 1);
745
+ const height = parseCliInteger(options.height, 1, "height", 1);
746
+ const text = await (0, image_tools_1.cropLocalImage)(imagePath, x, y, width, height, options.output);
747
+ console.log(text);
748
+ }
749
+ catch (error) {
750
+ console.error("❌ 图片裁切失败:", error instanceof Error ? error.message : error);
751
+ process.exit(1);
752
+ }
753
+ }
754
+ /**
755
+ * screen-crop 命令处理函数
756
+ * @param options 命令选项
757
+ * @returns 请求完成后退出
758
+ * @example
759
+ * ms screen-crop -i 192.168.1.100 --x 0 --y 0 --width 100 --height 100
760
+ */
761
+ async function screenCropCommand(options) {
762
+ try {
763
+ const target = parseDeviceHttpTarget(options);
764
+ const x = parseCliInteger(options.x, 0, "x", 0);
765
+ const y = parseCliInteger(options.y, 0, "y", 0);
766
+ const width = parseCliInteger(options.width, 1, "width", 1);
767
+ const height = parseCliInteger(options.height, 1, "height", 1);
768
+ const text = await (0, image_tools_1.cropDeviceScreen)(target, x, y, width, height, options.output);
769
+ console.log(text);
770
+ }
771
+ catch (error) {
772
+ console.error("❌ 屏幕裁图失败:", error instanceof Error ? error.message : error);
773
+ process.exit(1);
774
+ }
775
+ }
776
+ /**
777
+ * image-pick-color 命令处理函数
778
+ * @param options 命令选项
779
+ * @returns 请求完成后退出
780
+ * @example
781
+ * ms image-pick-color --image ./screen.jpg --x 10 --y 10
782
+ */
783
+ async function imagePickColorCommand(options) {
784
+ try {
785
+ const imagePath = options.image?.trim();
786
+ if (!imagePath) {
787
+ throw new Error("请通过 --image 指定输入图片路径。");
788
+ }
789
+ const x = parseCliInteger(options.x, 0, "x", 0);
790
+ const y = parseCliInteger(options.y, 0, "y", 0);
791
+ const radius = parseCliInteger(options.radius, 0, "radius", 0, 20);
792
+ const text = await (0, image_tools_1.pickLocalImageColor)(imagePath, x, y, radius);
793
+ console.log(text);
794
+ }
795
+ catch (error) {
796
+ console.error("❌ 图片取色失败:", error instanceof Error ? error.message : error);
797
+ process.exit(1);
798
+ }
799
+ }
800
+ /**
801
+ * screen-pick-color 命令处理函数
802
+ * @param options 命令选项
803
+ * @returns 请求完成后退出
804
+ * @example
805
+ * ms screen-pick-color -i 192.168.1.100 --x 10 --y 10
806
+ */
807
+ async function screenPickColorCommand(options) {
808
+ try {
809
+ const target = parseDeviceHttpTarget(options);
810
+ const x = parseCliInteger(options.x, 0, "x", 0);
811
+ const y = parseCliInteger(options.y, 0, "y", 0);
812
+ const radius = parseCliInteger(options.radius, 0, "radius", 0, 20);
813
+ const text = await (0, image_tools_1.pickDeviceScreenColor)(target, x, y, radius);
814
+ console.log(text);
815
+ }
816
+ catch (error) {
817
+ console.error("❌ 屏幕取色失败:", error instanceof Error ? error.message : error);
818
+ process.exit(1);
819
+ }
820
+ }
821
+ /**
822
+ * image-transparent 命令处理函数
823
+ * @param options 命令选项
824
+ * @returns 请求完成后退出
825
+ * @example
826
+ * ms image-transparent --image ./button.png --transparent-color "#ffffff"
827
+ */
828
+ async function imageTransparentCommand(options) {
829
+ try {
830
+ const imagePath = options.image?.trim();
831
+ if (!imagePath) {
832
+ throw new Error("请通过 --image 指定输入图片路径。");
833
+ }
834
+ const sampleCorner = parseCliChoice(options.sampleCorner, "topLeft", ["topLeft", "topRight", "bottomLeft", "bottomRight"], "sample-corner");
835
+ const tolerance = parseCliInteger(options.tolerance, 24, "tolerance", 0, 441);
836
+ const text = await (0, image_tools_1.makeLocalImageTransparent)(imagePath, options.output, options.transparentColor, sampleCorner, tolerance);
837
+ console.log(text);
838
+ }
839
+ catch (error) {
840
+ console.error("❌ 透明图制作失败:", error instanceof Error ? error.message : error);
841
+ process.exit(1);
842
+ }
843
+ }
251
844
  /**
252
845
  * ws-start 命令处理函数
253
846
  * @param options 命令选项
@@ -257,7 +850,6 @@ async function sourceCommand(options) {
257
850
  */
258
851
  async function wsStartCommand(options) {
259
852
  try {
260
- await ensureValidKuaiJSProject(process.cwd());
261
853
  const wsPortText = (options.wsPort ?? "31111").trim();
262
854
  const wsPort = Number.parseInt(wsPortText, 10);
263
855
  if (!Number.isInteger(wsPort) || wsPort < 1 || wsPort > 65535) {
@@ -319,7 +911,6 @@ async function wsStartCommand(options) {
319
911
  */
320
912
  async function wsStopCommand() {
321
913
  try {
322
- await ensureValidKuaiJSProject(process.cwd());
323
914
  if (!(await fsExtra.pathExists(WS_PID_FILE))) {
324
915
  console.log("WS 服务未运行");
325
916
  return;
@@ -351,7 +942,6 @@ async function wsStopCommand() {
351
942
  * ms ws-status
352
943
  */
353
944
  async function wsStatusCommand() {
354
- await ensureValidKuaiJSProject(process.cwd());
355
945
  if (!(await fsExtra.pathExists(WS_PID_FILE))) {
356
946
  console.log(JSON.stringify({
357
947
  status: "stopped",
@@ -496,8 +1086,116 @@ commander_1.program
496
1086
  .option("--ws-wait-ms <wsWaitMs>", "WS 等待设备连接时间(毫秒)", "30000")
497
1087
  .option("--max-depth <maxDepth>", "节点最大深度", "50")
498
1088
  .option("--timeout <timeout>", "节点抓取超时秒数", "120")
1089
+ .option("--mode <mode>", "节点抓取模式: 1=模式1, 2=模式2", "1")
499
1090
  .option("--output <output>", "节点 XML 输出文件路径(可选)")
500
1091
  .action(sourceCommand);
1092
+ // 日志命令
1093
+ commander_1.program
1094
+ .command("logs")
1095
+ .description("获取设备当前日志最新行")
1096
+ .option("-i, --ip <ip>", "设备 IP 地址")
1097
+ .option("--port <port>", "设备端口", "9800")
1098
+ .option("--limit <limit>", "读取最新日志行数,范围 1..5000", "200")
1099
+ .option("--format <format>", "返回格式: text|json", "text")
1100
+ .option("--output <output>", "日志输出文件路径(可选)")
1101
+ .action(logsCommand);
1102
+ // OCR 命令
1103
+ commander_1.program
1104
+ .command("ocr")
1105
+ .description("在设备上执行 OCR 识别")
1106
+ .option("-i, --ip <ip>", "设备 IP 地址")
1107
+ .option("--port <port>", "设备端口", "9800")
1108
+ .option("--engine <engine>", "OCR 引擎: appleocr|paddleocr", "appleocr")
1109
+ .option("--mode <mode>", "OCR 模式: recognize|numbers|findText", "recognize")
1110
+ .option("--input <input>", '输入源,默认 "screen"', "screen")
1111
+ .option("--x <x>", "区域左上角 x", "0")
1112
+ .option("--y <y>", "区域左上角 y", "0")
1113
+ .option("--ex <ex>", "区域右下角 x;全屏可传 0", "0")
1114
+ .option("--ey <ey>", "区域右下角 y;全屏可传 0", "0")
1115
+ .option("--text <text>", "findText 文本,可重复", collectStringOption, [])
1116
+ .option("--texts <texts>", "findText 文本数组,支持 JSON 数组或逗号分隔")
1117
+ .option("--languages <languages>", "Apple OCR 语言,支持 JSON 数组或逗号分隔")
1118
+ .option("--exact-match", "findText 要求完整匹配文本")
1119
+ .option("--confidence-threshold <confidenceThreshold>", "PaddleOCR 置信度阈值", "0.6")
1120
+ .option("--timeout-ms <timeoutMs>", "请求超时时间(毫秒)", "60000")
1121
+ .option("--format <format>", "返回格式: summary|json", "summary")
1122
+ .option("--output <output>", "OCR 输出文件路径(可选)")
1123
+ .action(ocrCommand);
1124
+ // 本地图片裁切命令
1125
+ commander_1.program
1126
+ .command("image-crop")
1127
+ .description("裁切本地图片并输出 PNG")
1128
+ .requiredOption("--image <image>", "输入图片路径")
1129
+ .option("--x <x>", "裁切起点 x", "0")
1130
+ .option("--y <y>", "裁切起点 y", "0")
1131
+ .requiredOption("--width <width>", "裁切宽度")
1132
+ .requiredOption("--height <height>", "裁切高度")
1133
+ .option("--output <output>", "输出 PNG 路径(可选)")
1134
+ .action(imageCropCommand);
1135
+ // 设备截图裁切命令
1136
+ commander_1.program
1137
+ .command("screen-crop")
1138
+ .description("从设备截图中裁切 PNG")
1139
+ .option("-i, --ip <ip>", "设备 IP 地址")
1140
+ .option("--port <port>", "设备端口", "9800")
1141
+ .option("--x <x>", "裁切起点 x", "0")
1142
+ .option("--y <y>", "裁切起点 y", "0")
1143
+ .requiredOption("--width <width>", "裁切宽度")
1144
+ .requiredOption("--height <height>", "裁切高度")
1145
+ .option("--output <output>", "输出 PNG 路径(可选)")
1146
+ .action(screenCropCommand);
1147
+ // 本地图片取色命令
1148
+ commander_1.program
1149
+ .command("image-pick-color")
1150
+ .description("读取本地图片指定坐标颜色")
1151
+ .requiredOption("--image <image>", "输入图片路径")
1152
+ .option("--x <x>", "目标坐标 x", "0")
1153
+ .option("--y <y>", "目标坐标 y", "0")
1154
+ .option("--radius <radius>", "采样半径,范围 0..20", "0")
1155
+ .action(imagePickColorCommand);
1156
+ // 设备截图取色命令
1157
+ commander_1.program
1158
+ .command("screen-pick-color")
1159
+ .description("读取设备截图指定坐标颜色")
1160
+ .option("-i, --ip <ip>", "设备 IP 地址")
1161
+ .option("--port <port>", "设备端口", "9800")
1162
+ .option("--x <x>", "目标坐标 x", "0")
1163
+ .option("--y <y>", "目标坐标 y", "0")
1164
+ .option("--radius <radius>", "采样半径,范围 0..20", "0")
1165
+ .action(screenPickColorCommand);
1166
+ // 本地图片透明化命令
1167
+ commander_1.program
1168
+ .command("image-transparent")
1169
+ .description("将图片中接近指定颜色或角落背景色的像素设为透明")
1170
+ .requiredOption("--image <image>", "输入图片路径")
1171
+ .option("--output <output>", "输出 PNG 路径(可选)")
1172
+ .option("--transparent-color <transparentColor>", "透明化颜色,格式 #RRGGBB")
1173
+ .option("--sample-corner <sampleCorner>", "未传 transparent-color 时采样角落: topLeft|topRight|bottomLeft|bottomRight", "topLeft")
1174
+ .option("--tolerance <tolerance>", "RGB 欧氏距离容差,范围 0..441", "24")
1175
+ .action(imageTransparentCommand);
1176
+ // API 文档命令
1177
+ const docsCommand = commander_1.program
1178
+ .command("docs")
1179
+ .description("显示 KuaiJS 本地文档路径");
1180
+ docsCommand
1181
+ .command("paths")
1182
+ .description("显示 KuaiJS 本地文档路径")
1183
+ .option("--output <output>", "输出文件路径(可选)")
1184
+ .action(docsPathsCommand);
1185
+ // HTTP API 调用命令
1186
+ commander_1.program
1187
+ .command("http-call")
1188
+ .description("按 HTTP API 文档声明调用设备接口")
1189
+ .option("-i, --ip <ip>", "设备 IP 地址")
1190
+ .option("--port <port>", "设备端口", "9800")
1191
+ .requiredOption("--method <method>", "HTTP 方法: GET|POST")
1192
+ .requiredOption("--path <path>", "HTTP API 相对路径,例如 /api/status")
1193
+ .option("--query <query>", "JSON 对象 query 参数")
1194
+ .option("--body <body>", "JSON 请求体;普通字符串会作为 JSON 字符串提交")
1195
+ .option("--response-format <responseFormat>", "响应格式: json|text", "json")
1196
+ .option("--timeout-ms <timeoutMs>", "请求超时时间(毫秒)", "30000")
1197
+ .option("--output <output>", "响应输出文件路径(可选)")
1198
+ .action(httpCallCommand);
501
1199
  // WS 服务启动命令
502
1200
  commander_1.program
503
1201
  .command("ws-start")