ms-vite-plugin 1.4.15 → 1.4.16

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,366 @@ 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
+ * 解析文档语言参数
153
+ * @param value 用户传入的语言
154
+ * @returns 返回文档语言
155
+ * @example
156
+ * parseDocsLanguageOption("js_zh")
157
+ */
158
+ function parseDocsLanguageOption(value) {
159
+ return parseCliChoice(value, "js_zh", ["js", "js_zh", "python"], "language");
160
+ }
161
+ /**
162
+ * 解析 HTTP API 请求方法
163
+ * @param value 用户传入的方法
164
+ * @returns 返回 HTTP API 方法
165
+ * @example
166
+ * parseHttpApiMethodOption("GET")
167
+ */
168
+ function parseHttpApiMethodOption(value) {
169
+ const method = value?.trim().toUpperCase();
170
+ return parseCliChoice(method, "GET", ["GET", "POST"], "method");
171
+ }
172
+ /**
173
+ * 解析可选 HTTP API 请求方法
174
+ * @param value 用户传入的方法
175
+ * @returns 返回 HTTP API 方法或 undefined
176
+ * @example
177
+ * parseOptionalHttpApiMethodOption("POST")
178
+ */
179
+ function parseOptionalHttpApiMethodOption(value) {
180
+ if (!value?.trim()) {
181
+ return undefined;
182
+ }
183
+ return parseHttpApiMethodOption(value);
184
+ }
185
+ /**
186
+ * 解析设备 HTTP 地址
187
+ * @param options 设备命令选项
188
+ * @returns 返回设备 HTTP 目标
189
+ * @example
190
+ * parseDeviceHttpTarget({ ip: "192.168.1.10", port: "9800" })
191
+ */
192
+ function parseDeviceHttpTarget(options) {
193
+ const ip = options.ip?.trim();
194
+ if (!ip) {
195
+ throw new Error("请通过 --ip 指定设备 IP 地址");
196
+ }
197
+ const port = parseCliInteger(options.port, 9800, "port", 1, 65535);
198
+ return {
199
+ ip,
200
+ port: String(port),
201
+ label: `${ip}:${port}`,
202
+ };
203
+ }
204
+ /**
205
+ * 输出文本到终端或文件
206
+ * @param text 待输出文本
207
+ * @param output 用户指定输出路径
208
+ * @param savedLabel 保存成功提示
209
+ * @returns 输出完成后返回
210
+ * @example
211
+ * await writeOrPrintText("ok", "./out.txt", "结果已保存")
212
+ */
213
+ async function writeOrPrintText(text, output, savedLabel) {
214
+ if (output?.trim()) {
215
+ const outputPath = path.resolve(output.trim());
216
+ await fsExtra.ensureDir(path.dirname(outputPath));
217
+ await fsExtra.writeFile(outputPath, text, "utf8");
218
+ console.log(`✅ ${savedLabel}: ${outputPath}`);
219
+ return;
220
+ }
221
+ console.log(text);
222
+ }
223
+ /**
224
+ * 格式化 API 文档摘要
225
+ * @param language 文档语言
226
+ * @param title 文档标题
227
+ * @param slug 文档 slug
228
+ * @param index 序号
229
+ * @param filePath 文档文件绝对路径
230
+ * @returns 返回摘要文本
231
+ * @example
232
+ * formatCliApiDocSummary("js_zh", "设备", "device", 0, "/tmp/device.md")
233
+ */
234
+ function formatCliApiDocSummary(language, title, slug, index, filePath) {
235
+ return [
236
+ `${index + 1}. ${title}`,
237
+ `slug: ${slug}`,
238
+ `uri: ${(0, docs_service_1.getDocUri)(language, slug)}`,
239
+ ...(filePath ? [`path: ${filePath}`] : []),
240
+ ].join("\n");
241
+ }
242
+ /**
243
+ * 格式化 HTTP API 文档摘要
244
+ * @param entry HTTP API 文档条目
245
+ * @param index 序号
246
+ * @returns 返回摘要文本
247
+ * @example
248
+ * formatCliHttpApiDocSummary(entry, 0)
249
+ */
250
+ function formatCliHttpApiDocSummary(entry, index) {
251
+ return [
252
+ `${index + 1}. ${entry.title}`,
253
+ `method: ${entry.method}`,
254
+ `path: ${entry.path}`,
255
+ `slug: ${entry.slug}`,
256
+ `section: ${entry.section || "未分组"}`,
257
+ `lines: ${entry.startLine}-${entry.endLine}`,
258
+ `file: ${entry.filePath}`,
259
+ ].join("\n");
260
+ }
261
+ /**
262
+ * 格式化 CLI 文档路径清单
263
+ * @returns 返回本地文档路径文本
264
+ * @example
265
+ * const text = formatCliDocsPathsText()
266
+ */
267
+ function formatCliDocsPathsText() {
268
+ const languages = ["js", "js_zh", "python"];
269
+ return [
270
+ "KuaiJS 文档路径:",
271
+ `docsRoot: ${(0, docs_service_1.getDocsRootDir)()}`,
272
+ ...languages.map((language) => `${language}: ${(0, docs_service_1.getDocsDirByLanguage)(language)} (${types_1.DOC_LANGUAGE_LABELS[language]})`),
273
+ `httpApi: ${(0, httpapi_docs_service_1.getHttpApiDocPath)()}`,
274
+ "说明: 直接读取上述 markdown 路径;docs/http-docs 命令用于定位具体文档。",
275
+ ].join("\n");
276
+ }
277
+ /**
278
+ * 收集可重复字符串参数
279
+ * @param value 当前参数值
280
+ * @param previous 之前收集的值
281
+ * @returns 返回追加后的数组
282
+ * @example
283
+ * collectStringOption("登录", [])
284
+ */
285
+ function collectStringOption(value, previous) {
286
+ previous.push(value);
287
+ return previous;
288
+ }
289
+ /**
290
+ * 解析字符串数组参数
291
+ * @param value 用户传入文本
292
+ * @param name 参数名
293
+ * @returns 返回字符串数组或 undefined
294
+ * @example
295
+ * parseStringArrayOption("zh-Hans,en-US", "languages")
296
+ */
297
+ function parseStringArrayOption(value, name) {
298
+ const text = value?.trim();
299
+ if (!text) {
300
+ return undefined;
301
+ }
302
+ if (text.startsWith("[")) {
303
+ const parsed = JSON.parse(text);
304
+ if (!Array.isArray(parsed) ||
305
+ !parsed.every((item) => typeof item === "string" && item.trim())) {
306
+ throw new Error(`${name} 必须是字符串数组。`);
307
+ }
308
+ return parsed.map((item) => item.trim());
309
+ }
310
+ const items = text
311
+ .split(",")
312
+ .map((item) => item.trim())
313
+ .filter((item) => item.length > 0);
314
+ if (items.length === 0) {
315
+ throw new Error(`${name} 不能为空。`);
316
+ }
317
+ return items;
318
+ }
319
+ /**
320
+ * 解析 Apple OCR 语言数组
321
+ * @param value 用户传入语言文本
322
+ * @returns 返回语言数组或 undefined
323
+ * @example
324
+ * parseAppleOcrLanguages("zh-Hans,en-US")
325
+ */
326
+ function parseAppleOcrLanguages(value) {
327
+ const languages = parseStringArrayOption(value, "languages");
328
+ if (!languages) {
329
+ return undefined;
330
+ }
331
+ const allowed = new Set(ocr_tools_1.APPLE_OCR_LANGUAGES);
332
+ const invalid = languages.filter((language) => !allowed.has(language));
333
+ if (invalid.length > 0) {
334
+ throw new Error(`无效 languages: ${invalid.join(", ")},可选值: ${ocr_tools_1.APPLE_OCR_LANGUAGES.join(", ")}`);
335
+ }
336
+ return languages;
337
+ }
338
+ /**
339
+ * 解析 JSON 对象参数
340
+ * @param value 用户传入 JSON 文本
341
+ * @param name 参数名
342
+ * @returns 返回对象或 undefined
343
+ * @example
344
+ * parseJsonRecordOption('{"x":1}', "query")
345
+ */
346
+ function parseJsonRecordOption(value, name) {
347
+ const text = value?.trim();
348
+ if (!text) {
349
+ return undefined;
350
+ }
351
+ const parsed = JSON.parse(text);
352
+ if (!parsed ||
353
+ typeof parsed !== "object" ||
354
+ Array.isArray(parsed) ||
355
+ !Object.values(parsed).every((item) => typeof item === "string" ||
356
+ typeof item === "number" ||
357
+ typeof item === "boolean" ||
358
+ item === null)) {
359
+ throw new Error(`${name} 必须是 JSON 对象,且值只能是 string/number/boolean/null。`);
360
+ }
361
+ return parsed;
362
+ }
363
+ /**
364
+ * 解析 JSON 请求体参数
365
+ * @param value 用户传入请求体文本
366
+ * @returns 返回 JSON 值或 undefined
367
+ * @example
368
+ * parseJsonBodyOption('{"x":1}')
369
+ */
370
+ function parseJsonBodyOption(value) {
371
+ const text = value?.trim();
372
+ if (text === undefined || text.length === 0) {
373
+ return undefined;
374
+ }
375
+ try {
376
+ return JSON.parse(text);
377
+ }
378
+ catch {
379
+ return value;
380
+ }
381
+ }
382
+ /**
383
+ * 标准化 HTTP API 路径
384
+ * @param apiPath 用户传入路径
385
+ * @returns 返回相对路径
386
+ * @example
387
+ * normalizeCliHttpApiPath("/api/status")
388
+ */
389
+ function normalizeCliHttpApiPath(apiPath) {
390
+ const normalizedPath = apiPath.trim();
391
+ if (/^https?:\/\//i.test(normalizedPath)) {
392
+ throw new Error("path 只能传 HTTP API 相对路径,不能传完整 URL。");
393
+ }
394
+ if (!normalizedPath.startsWith("/")) {
395
+ throw new Error("path 必须以 / 开头,例如 /api/status。");
396
+ }
397
+ if (normalizedPath.includes("?")) {
398
+ throw new Error("path 不能包含 query string,请使用 --query 传递查询参数。");
399
+ }
400
+ return normalizedPath;
401
+ }
402
+ /**
403
+ * 追加 HTTP query 参数
404
+ * @param url 目标 URL
405
+ * @param query query 参数
406
+ * @returns 无返回值
407
+ * @example
408
+ * appendCliQueryParams(url, { x: 1 })
409
+ */
410
+ function appendCliQueryParams(url, query) {
411
+ if (!query) {
412
+ return;
413
+ }
414
+ for (const [key, value] of Object.entries(query)) {
415
+ if (value === null) {
416
+ continue;
417
+ }
418
+ url.searchParams.set(key, String(value));
419
+ }
420
+ }
421
+ /**
422
+ * 创建 HTTP API 请求体
423
+ * @param body 用户传入请求体
424
+ * @returns 返回 fetch 请求体和头
425
+ * @example
426
+ * createCliHttpRequestBody({ x: 1 })
427
+ */
428
+ function createCliHttpRequestBody(body) {
429
+ if (body === undefined) {
430
+ return {};
431
+ }
432
+ return {
433
+ headers: {
434
+ "Content-Type": "application/json",
435
+ },
436
+ requestBody: JSON.stringify(body),
437
+ };
438
+ }
439
+ /**
440
+ * 读取 HTTP API 响应文本
441
+ * @param response fetch 响应
442
+ * @param responseFormat 响应格式
443
+ * @returns 返回格式化文本
444
+ * @example
445
+ * await readCliHttpApiResponseText(response, "json")
446
+ */
447
+ async function readCliHttpApiResponseText(response, responseFormat) {
448
+ const responseText = await response.text();
449
+ if (responseFormat === "text") {
450
+ return responseText;
451
+ }
452
+ return (0, tool_utils_1.formatRuntimeJsonText)(JSON.parse(responseText));
453
+ }
88
454
  /**
89
455
  * 构建命令处理函数
90
456
  * @param options 命令选项
@@ -175,8 +541,6 @@ async function runUICommand(options) {
175
541
  */
176
542
  async function stopCommand(options) {
177
543
  try {
178
- const workspacePath = process.cwd();
179
- await ensureValidKuaiJSProject(workspacePath);
180
544
  await (0, project_1.stopOnDevice)(options);
181
545
  }
182
546
  catch (error) {
@@ -193,7 +557,6 @@ async function stopCommand(options) {
193
557
  */
194
558
  async function screenshotCommand(options) {
195
559
  try {
196
- await ensureValidKuaiJSProject(process.cwd());
197
560
  const format = options.format ?? "file";
198
561
  if (format === "base64") {
199
562
  const base64 = await (0, project_1.getScreenshotBase64OnDevice)(options);
@@ -218,22 +581,26 @@ async function screenshotCommand(options) {
218
581
  * @param options 命令选项
219
582
  * @returns 请求完成后退出
220
583
  * @example
221
- * ms source --ip 192.168.1.100 --port 9800 --max-depth 50 --timeout 120
584
+ * ms source --ip 192.168.1.100 --port 9800 --max-depth 50 --timeout 120 --mode 1
222
585
  */
223
586
  async function sourceCommand(options) {
224
587
  try {
225
- await ensureValidKuaiJSProject(process.cwd());
226
588
  const maxDepthText = (options.maxDepth ?? "50").trim();
227
589
  const timeoutText = (options.timeout ?? "120").trim();
590
+ const modeText = (options.mode ?? "1").trim();
228
591
  const maxDepth = Number.parseInt(maxDepthText, 10);
229
592
  const timeout = Number.parseInt(timeoutText, 10);
593
+ const mode = Number.parseInt(modeText, 10);
230
594
  if (!Number.isInteger(maxDepth) || maxDepth < 1) {
231
595
  throw new Error(`无效 max-depth: ${maxDepthText}`);
232
596
  }
233
597
  if (!Number.isInteger(timeout) || timeout < 1) {
234
598
  throw new Error(`无效 timeout: ${timeoutText}`);
235
599
  }
236
- const source = await (0, project_1.getSourceOnDevice)(options, maxDepth, timeout);
600
+ if (!Number.isInteger(mode) || (mode !== 1 && mode !== 2)) {
601
+ throw new Error(`无效 mode: ${modeText}`);
602
+ }
603
+ const source = await (0, project_1.getSourceOnDevice)(options, maxDepth, timeout, mode);
237
604
  if (options.output?.trim()) {
238
605
  const outputPath = path.resolve(options.output.trim());
239
606
  await fsExtra.ensureDir(path.dirname(outputPath));
@@ -248,6 +615,482 @@ async function sourceCommand(options) {
248
615
  process.exit(1);
249
616
  }
250
617
  }
618
+ /**
619
+ * logs 命令处理函数
620
+ * @param options 命令选项
621
+ * @returns 请求完成后退出
622
+ * @example
623
+ * ms logs --ip 192.168.1.100 --port 9800 --limit 200
624
+ */
625
+ async function logsCommand(options) {
626
+ try {
627
+ const limit = parseCliInteger(options.limit, 200, "limit", 1, 5000);
628
+ const format = parseCliChoice(options.format, "text", ["text", "json"], "format");
629
+ const result = await (0, project_1.getCurrentLogLinesOnDevice)(options, limit);
630
+ const text = format === "json"
631
+ ? (0, tool_utils_1.formatRuntimeJsonText)(result)
632
+ : result.lines.length > 0
633
+ ? result.lines.join("\n")
634
+ : "无日志";
635
+ await writeOrPrintText(text, options.output, "日志已保存");
636
+ if (format === "text" && result.hasMore && !options.output?.trim()) {
637
+ console.error(`还有更早日志,可增大 --limit,当前最多 ${limit} 行。`);
638
+ }
639
+ }
640
+ catch (error) {
641
+ console.error("❌ 获取日志失败:", error instanceof Error ? error.message : error);
642
+ process.exit(1);
643
+ }
644
+ }
645
+ /**
646
+ * docs list 命令处理函数
647
+ * @param options 命令选项
648
+ * @returns 请求完成后退出
649
+ * @example
650
+ * ms docs list --language js_zh
651
+ */
652
+ async function docsListCommand(options) {
653
+ try {
654
+ const language = parseDocsLanguageOption(options.language);
655
+ const docs = await (0, docs_service_1.getApiDocsByLanguage)(language);
656
+ const lines = docs.map((doc, index) => formatCliApiDocSummary(language, doc.title, doc.slug, index, doc.filePath));
657
+ const text = [
658
+ `文档语言: ${language} (${types_1.DOC_LANGUAGE_LABELS[language]})`,
659
+ "",
660
+ ...(lines.length > 0 ? lines : ["无可用文档"]),
661
+ ].join("\n");
662
+ await writeOrPrintText(text, options.output, "文档列表已保存");
663
+ }
664
+ catch (error) {
665
+ console.error("❌ 列出文档失败:", error instanceof Error ? error.message : error);
666
+ process.exit(1);
667
+ }
668
+ }
669
+ /**
670
+ * docs paths 命令处理函数
671
+ * @param options 命令选项
672
+ * @returns 请求完成后退出
673
+ * @example
674
+ * ms docs paths
675
+ */
676
+ async function docsPathsCommand(options) {
677
+ try {
678
+ await writeOrPrintText(formatCliDocsPathsText(), options.output, "文档路径已保存");
679
+ }
680
+ catch (error) {
681
+ console.error("❌ 输出文档路径失败:", error instanceof Error ? error.message : error);
682
+ process.exit(1);
683
+ }
684
+ }
685
+ /**
686
+ * docs search 命令处理函数
687
+ * @param query 搜索词
688
+ * @param options 命令选项
689
+ * @returns 请求完成后退出
690
+ * @example
691
+ * ms docs search 点击 --language js_zh --limit 5
692
+ */
693
+ async function docsSearchCommand(query, options) {
694
+ try {
695
+ const language = parseDocsLanguageOption(options.language);
696
+ const limit = parseCliInteger(options.limit, 5, "limit", 1, 20);
697
+ const normalizedQuery = query.toLowerCase();
698
+ const docs = await (0, docs_service_1.getApiDocsByLanguage)(language);
699
+ const matches = docs
700
+ .map((doc) => ({ doc, score: (0, docs_service_1.scoreApiDoc)(doc, normalizedQuery) }))
701
+ .filter((item) => item.score > 0)
702
+ .sort((a, b) => b.score - a.score || a.doc.slug.localeCompare(b.doc.slug))
703
+ .slice(0, limit)
704
+ .map((item) => item.doc);
705
+ const text = matches.length === 0
706
+ ? `文档语言: ${language} (${types_1.DOC_LANGUAGE_LABELS[language]})\n未找到与 "${query}" 匹配的文档。`
707
+ : [
708
+ `文档语言: ${language} (${types_1.DOC_LANGUAGE_LABELS[language]})`,
709
+ "",
710
+ ...matches.map((doc, index) => formatCliApiDocSummary(language, doc.title, doc.slug, index, doc.filePath)),
711
+ ].join("\n");
712
+ await writeOrPrintText(text, options.output, "文档搜索结果已保存");
713
+ }
714
+ catch (error) {
715
+ console.error("❌ 搜索文档失败:", error instanceof Error ? error.message : error);
716
+ process.exit(1);
717
+ }
718
+ }
719
+ /**
720
+ * docs read 命令处理函数
721
+ * @param slug 文档 slug
722
+ * @param options 命令选项
723
+ * @returns 请求完成后退出
724
+ * @example
725
+ * ms docs read action --language js_zh
726
+ */
727
+ async function docsReadCommand(slug, options) {
728
+ try {
729
+ const language = parseDocsLanguageOption(options.language);
730
+ const docs = await (0, docs_service_1.getApiDocsByLanguage)(language);
731
+ const target = docs.find((doc) => doc.slug === slug);
732
+ if (!target) {
733
+ throw new Error(`未找到文档 ${slug}.md(语言: ${language})。`);
734
+ }
735
+ const text = [
736
+ `标题: ${target.title}`,
737
+ `语言: ${language}`,
738
+ `URI: ${(0, docs_service_1.getDocUri)(language, target.slug)}`,
739
+ `path: ${target.filePath}`,
740
+ "",
741
+ target.content,
742
+ ].join("\n");
743
+ await writeOrPrintText(text, options.output, "文档已保存");
744
+ }
745
+ catch (error) {
746
+ console.error("❌ 读取文档失败:", error instanceof Error ? error.message : error);
747
+ process.exit(1);
748
+ }
749
+ }
750
+ /**
751
+ * http-docs list 命令处理函数
752
+ * @param options 命令选项
753
+ * @returns 请求完成后退出
754
+ * @example
755
+ * ms http-docs list
756
+ */
757
+ async function httpDocsListCommand(options) {
758
+ try {
759
+ const entries = await (0, httpapi_docs_service_1.getHttpApiDocEntries)();
760
+ const text = [
761
+ "HTTP API 文档列表:",
762
+ "",
763
+ ...entries.map((entry, index) => formatCliHttpApiDocSummary(entry, index)),
764
+ ].join("\n");
765
+ await writeOrPrintText(text, options.output, "HTTP API 文档列表已保存");
766
+ }
767
+ catch (error) {
768
+ console.error("❌ 列出 HTTP API 文档失败:", error instanceof Error ? error.message : error);
769
+ process.exit(1);
770
+ }
771
+ }
772
+ /**
773
+ * http-docs search 命令处理函数
774
+ * @param query 搜索词
775
+ * @param options 命令选项
776
+ * @returns 请求完成后退出
777
+ * @example
778
+ * ms http-docs search 截图 --limit 8
779
+ */
780
+ async function httpDocsSearchCommand(query, options) {
781
+ try {
782
+ const limit = parseCliInteger(options.limit, 8, "limit", 1, 20);
783
+ const normalizedQuery = query.toLowerCase();
784
+ const entries = await (0, httpapi_docs_service_1.getHttpApiDocEntries)();
785
+ const matches = entries
786
+ .map((entry) => ({
787
+ entry,
788
+ score: (0, httpapi_docs_service_1.scoreHttpApiDocEntry)(entry, normalizedQuery),
789
+ }))
790
+ .filter((item) => item.score > 0)
791
+ .sort((a, b) => b.score - a.score ||
792
+ a.entry.path.localeCompare(b.entry.path) ||
793
+ a.entry.method.localeCompare(b.entry.method))
794
+ .slice(0, limit)
795
+ .map((item) => item.entry);
796
+ const text = matches.length === 0
797
+ ? `未找到与 "${query}" 匹配的 HTTP API 文档。`
798
+ : [
799
+ "HTTP API 文档匹配结果:",
800
+ "",
801
+ ...matches.map((entry, index) => formatCliHttpApiDocSummary(entry, index)),
802
+ ].join("\n");
803
+ await writeOrPrintText(text, options.output, "HTTP API 文档搜索结果已保存");
804
+ }
805
+ catch (error) {
806
+ console.error("❌ 搜索 HTTP API 文档失败:", error instanceof Error ? error.message : error);
807
+ process.exit(1);
808
+ }
809
+ }
810
+ /**
811
+ * http-docs read 命令处理函数
812
+ * @param identifier slug 或接口路径
813
+ * @param options 命令选项
814
+ * @returns 请求完成后退出
815
+ * @example
816
+ * ms http-docs read /api/source --method GET
817
+ */
818
+ async function httpDocsReadCommand(identifier, options) {
819
+ try {
820
+ const method = parseOptionalHttpApiMethodOption(options.method);
821
+ const entry = await (0, httpapi_docs_service_1.findHttpApiDocEntry)(identifier, method);
822
+ if (!entry) {
823
+ throw new Error(`未找到 HTTP API 文档: ${identifier}`);
824
+ }
825
+ const text = [
826
+ `title: ${entry.title}`,
827
+ `method: ${entry.method}`,
828
+ `path: ${entry.path}`,
829
+ `slug: ${entry.slug}`,
830
+ `section: ${entry.section || "未分组"}`,
831
+ `lines: ${entry.startLine}-${entry.endLine}`,
832
+ `file: ${entry.filePath}`,
833
+ "",
834
+ entry.content,
835
+ ].join("\n");
836
+ await writeOrPrintText(text, options.output, "HTTP API 文档已保存");
837
+ }
838
+ catch (error) {
839
+ console.error("❌ 读取 HTTP API 文档失败:", error instanceof Error ? error.message : error);
840
+ process.exit(1);
841
+ }
842
+ }
843
+ /**
844
+ * http-call 命令处理函数
845
+ * @param options 命令选项
846
+ * @returns 请求完成后退出
847
+ * @example
848
+ * ms http-call --ip 192.168.1.100 --method GET --path /api/status --doc-slug get-api-status
849
+ */
850
+ async function httpCallCommand(options) {
851
+ try {
852
+ const target = parseDeviceHttpTarget(options);
853
+ const method = parseHttpApiMethodOption(options.method);
854
+ const apiPath = normalizeCliHttpApiPath(options.path ?? "");
855
+ const docSlug = options.docSlug?.trim();
856
+ if (!docSlug) {
857
+ throw new Error("请通过 --doc-slug 指定 HTTP API 文档 slug。");
858
+ }
859
+ const entry = await (0, httpapi_docs_service_1.findHttpApiDocEntry)(docSlug);
860
+ if (!entry) {
861
+ throw new Error(`HTTP API 文档中不存在该 docSlug: ${docSlug}`);
862
+ }
863
+ if (entry.method !== method || entry.path !== apiPath) {
864
+ throw new Error(`HTTP API 调用与文档条目不匹配。传入 ${method} ${apiPath},文档 ${entry.method} ${entry.path}。`);
865
+ }
866
+ const query = parseJsonRecordOption(options.query, "query");
867
+ const body = parseJsonBodyOption(options.body);
868
+ if (method === "GET" && body !== undefined) {
869
+ throw new Error("GET 接口不能传 body,请使用 --query。");
870
+ }
871
+ const responseFormat = parseCliChoice(options.responseFormat, "json", ["json", "text"], "response-format");
872
+ const timeoutMs = parseCliInteger(options.timeoutMs, 30000, "timeout-ms", 1000, 600000);
873
+ const url = new URL(`http://${target.ip}:${target.port}${apiPath}`);
874
+ appendCliQueryParams(url, query);
875
+ const controller = new AbortController();
876
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
877
+ try {
878
+ const { headers, requestBody } = createCliHttpRequestBody(body);
879
+ const response = await fetch(url.toString(), {
880
+ method,
881
+ signal: controller.signal,
882
+ headers,
883
+ body: requestBody,
884
+ });
885
+ const responseText = await readCliHttpApiResponseText(response, responseFormat);
886
+ const text = [
887
+ `HTTP API 调用${response.ok ? "成功" : "失败"}: ${method} ${apiPath}`,
888
+ `设备: ${target.label}`,
889
+ `文档: ${entry.title} (${entry.slug}, lines ${entry.startLine}-${entry.endLine})`,
890
+ `状态码: ${response.status}`,
891
+ "",
892
+ responseText,
893
+ ].join("\n");
894
+ await writeOrPrintText(text, options.output, "HTTP API 响应已保存");
895
+ if (!response.ok) {
896
+ process.exit(1);
897
+ }
898
+ }
899
+ finally {
900
+ clearTimeout(timeout);
901
+ }
902
+ }
903
+ catch (error) {
904
+ const message = error instanceof Error && error.name === "AbortError"
905
+ ? "请求超时"
906
+ : error instanceof Error
907
+ ? error.message
908
+ : String(error);
909
+ console.error("❌ HTTP API 调用失败:", message);
910
+ process.exit(1);
911
+ }
912
+ }
913
+ /**
914
+ * ocr 命令处理函数
915
+ * @param options 命令选项
916
+ * @returns 请求完成后退出
917
+ * @example
918
+ * ms ocr --ip 192.168.1.100 --mode recognize
919
+ */
920
+ async function ocrCommand(options) {
921
+ try {
922
+ const target = parseDeviceHttpTarget(options);
923
+ const engine = parseCliChoice(options.engine, "appleocr", ["appleocr", "paddleocr"], "engine");
924
+ const mode = parseCliChoice(options.mode, "recognize", ["recognize", "numbers", "findText"], "mode");
925
+ const input = options.input?.trim() || "screen";
926
+ const x = parseCliInteger(options.x, 0, "x", 0);
927
+ const y = parseCliInteger(options.y, 0, "y", 0);
928
+ const ex = parseCliInteger(options.ex, 0, "ex", 0);
929
+ const ey = parseCliInteger(options.ey, 0, "ey", 0);
930
+ const textItems = [
931
+ ...(parseStringArrayOption(options.texts, "texts") ?? []),
932
+ ...(options.text ?? []),
933
+ ];
934
+ const texts = textItems.length > 0 ? textItems : undefined;
935
+ const languages = parseAppleOcrLanguages(options.languages);
936
+ const confidenceThreshold = parseCliNumber(options.confidenceThreshold, 0.6, "confidence-threshold", 0, 1);
937
+ const timeoutMs = parseCliInteger(options.timeoutMs, 60000, "timeout-ms", 1000, 600000);
938
+ const format = parseCliChoice(options.format, "summary", ["summary", "json"], "format");
939
+ const result = await (0, ocr_tools_1.runOcrRecognitionOnDevice)(target, {
940
+ engine,
941
+ mode,
942
+ input,
943
+ x,
944
+ y,
945
+ ex,
946
+ ey,
947
+ texts,
948
+ languages,
949
+ confidenceThreshold,
950
+ exactMatch: options.exactMatch === true,
951
+ outputPath: format === "summary" ? options.output : undefined,
952
+ timeoutMs,
953
+ });
954
+ const text = format === "json" ? (0, tool_utils_1.formatRuntimeJsonText)(result.body) : result.text;
955
+ if (format === "json") {
956
+ await writeOrPrintText(text, options.output, "OCR 结果已保存");
957
+ }
958
+ else {
959
+ console.log([
960
+ text,
961
+ "",
962
+ `device: ${target.label}`,
963
+ `engine: ${engine}`,
964
+ `mode: ${mode}`,
965
+ `input: ${input}`,
966
+ `region: x=${x}, y=${y}, ex=${ex}, ey=${ey}`,
967
+ `status: ${result.status}`,
968
+ ].join("\n"));
969
+ }
970
+ if (result.body.success === false) {
971
+ process.exit(1);
972
+ }
973
+ }
974
+ catch (error) {
975
+ console.error("❌ OCR 执行失败:", error instanceof Error ? error.message : error);
976
+ process.exit(1);
977
+ }
978
+ }
979
+ /**
980
+ * image-crop 命令处理函数
981
+ * @param options 命令选项
982
+ * @returns 请求完成后退出
983
+ * @example
984
+ * ms image-crop --image ./screen.jpg --x 0 --y 0 --width 100 --height 100
985
+ */
986
+ async function imageCropCommand(options) {
987
+ try {
988
+ const imagePath = options.image?.trim();
989
+ if (!imagePath) {
990
+ throw new Error("请通过 --image 指定输入图片路径。");
991
+ }
992
+ const x = parseCliInteger(options.x, 0, "x", 0);
993
+ const y = parseCliInteger(options.y, 0, "y", 0);
994
+ const width = parseCliInteger(options.width, 1, "width", 1);
995
+ const height = parseCliInteger(options.height, 1, "height", 1);
996
+ const text = await (0, image_tools_1.cropLocalImage)(imagePath, x, y, width, height, options.output);
997
+ console.log(text);
998
+ }
999
+ catch (error) {
1000
+ console.error("❌ 图片裁切失败:", error instanceof Error ? error.message : error);
1001
+ process.exit(1);
1002
+ }
1003
+ }
1004
+ /**
1005
+ * screen-crop 命令处理函数
1006
+ * @param options 命令选项
1007
+ * @returns 请求完成后退出
1008
+ * @example
1009
+ * ms screen-crop -i 192.168.1.100 --x 0 --y 0 --width 100 --height 100
1010
+ */
1011
+ async function screenCropCommand(options) {
1012
+ try {
1013
+ const target = parseDeviceHttpTarget(options);
1014
+ const x = parseCliInteger(options.x, 0, "x", 0);
1015
+ const y = parseCliInteger(options.y, 0, "y", 0);
1016
+ const width = parseCliInteger(options.width, 1, "width", 1);
1017
+ const height = parseCliInteger(options.height, 1, "height", 1);
1018
+ const text = await (0, image_tools_1.cropDeviceScreen)(target, x, y, width, height, options.output);
1019
+ console.log(text);
1020
+ }
1021
+ catch (error) {
1022
+ console.error("❌ 屏幕裁图失败:", error instanceof Error ? error.message : error);
1023
+ process.exit(1);
1024
+ }
1025
+ }
1026
+ /**
1027
+ * image-pick-color 命令处理函数
1028
+ * @param options 命令选项
1029
+ * @returns 请求完成后退出
1030
+ * @example
1031
+ * ms image-pick-color --image ./screen.jpg --x 10 --y 10
1032
+ */
1033
+ async function imagePickColorCommand(options) {
1034
+ try {
1035
+ const imagePath = options.image?.trim();
1036
+ if (!imagePath) {
1037
+ throw new Error("请通过 --image 指定输入图片路径。");
1038
+ }
1039
+ const x = parseCliInteger(options.x, 0, "x", 0);
1040
+ const y = parseCliInteger(options.y, 0, "y", 0);
1041
+ const radius = parseCliInteger(options.radius, 0, "radius", 0, 20);
1042
+ const text = await (0, image_tools_1.pickLocalImageColor)(imagePath, x, y, radius);
1043
+ console.log(text);
1044
+ }
1045
+ catch (error) {
1046
+ console.error("❌ 图片取色失败:", error instanceof Error ? error.message : error);
1047
+ process.exit(1);
1048
+ }
1049
+ }
1050
+ /**
1051
+ * screen-pick-color 命令处理函数
1052
+ * @param options 命令选项
1053
+ * @returns 请求完成后退出
1054
+ * @example
1055
+ * ms screen-pick-color -i 192.168.1.100 --x 10 --y 10
1056
+ */
1057
+ async function screenPickColorCommand(options) {
1058
+ try {
1059
+ const target = parseDeviceHttpTarget(options);
1060
+ const x = parseCliInteger(options.x, 0, "x", 0);
1061
+ const y = parseCliInteger(options.y, 0, "y", 0);
1062
+ const radius = parseCliInteger(options.radius, 0, "radius", 0, 20);
1063
+ const text = await (0, image_tools_1.pickDeviceScreenColor)(target, x, y, radius);
1064
+ console.log(text);
1065
+ }
1066
+ catch (error) {
1067
+ console.error("❌ 屏幕取色失败:", error instanceof Error ? error.message : error);
1068
+ process.exit(1);
1069
+ }
1070
+ }
1071
+ /**
1072
+ * image-transparent 命令处理函数
1073
+ * @param options 命令选项
1074
+ * @returns 请求完成后退出
1075
+ * @example
1076
+ * ms image-transparent --image ./button.png --transparent-color "#ffffff"
1077
+ */
1078
+ async function imageTransparentCommand(options) {
1079
+ try {
1080
+ const imagePath = options.image?.trim();
1081
+ if (!imagePath) {
1082
+ throw new Error("请通过 --image 指定输入图片路径。");
1083
+ }
1084
+ const sampleCorner = parseCliChoice(options.sampleCorner, "topLeft", ["topLeft", "topRight", "bottomLeft", "bottomRight"], "sample-corner");
1085
+ const tolerance = parseCliInteger(options.tolerance, 24, "tolerance", 0, 441);
1086
+ const text = await (0, image_tools_1.makeLocalImageTransparent)(imagePath, options.output, options.transparentColor, sampleCorner, tolerance);
1087
+ console.log(text);
1088
+ }
1089
+ catch (error) {
1090
+ console.error("❌ 透明图制作失败:", error instanceof Error ? error.message : error);
1091
+ process.exit(1);
1092
+ }
1093
+ }
251
1094
  /**
252
1095
  * ws-start 命令处理函数
253
1096
  * @param options 命令选项
@@ -257,7 +1100,6 @@ async function sourceCommand(options) {
257
1100
  */
258
1101
  async function wsStartCommand(options) {
259
1102
  try {
260
- await ensureValidKuaiJSProject(process.cwd());
261
1103
  const wsPortText = (options.wsPort ?? "31111").trim();
262
1104
  const wsPort = Number.parseInt(wsPortText, 10);
263
1105
  if (!Number.isInteger(wsPort) || wsPort < 1 || wsPort > 65535) {
@@ -319,7 +1161,6 @@ async function wsStartCommand(options) {
319
1161
  */
320
1162
  async function wsStopCommand() {
321
1163
  try {
322
- await ensureValidKuaiJSProject(process.cwd());
323
1164
  if (!(await fsExtra.pathExists(WS_PID_FILE))) {
324
1165
  console.log("WS 服务未运行");
325
1166
  return;
@@ -351,7 +1192,6 @@ async function wsStopCommand() {
351
1192
  * ms ws-status
352
1193
  */
353
1194
  async function wsStatusCommand() {
354
- await ensureValidKuaiJSProject(process.cwd());
355
1195
  if (!(await fsExtra.pathExists(WS_PID_FILE))) {
356
1196
  console.log(JSON.stringify({
357
1197
  status: "stopped",
@@ -496,8 +1336,161 @@ commander_1.program
496
1336
  .option("--ws-wait-ms <wsWaitMs>", "WS 等待设备连接时间(毫秒)", "30000")
497
1337
  .option("--max-depth <maxDepth>", "节点最大深度", "50")
498
1338
  .option("--timeout <timeout>", "节点抓取超时秒数", "120")
1339
+ .option("--mode <mode>", "节点抓取模式: 1=模式1, 2=模式2", "1")
499
1340
  .option("--output <output>", "节点 XML 输出文件路径(可选)")
500
1341
  .action(sourceCommand);
1342
+ // 日志命令
1343
+ commander_1.program
1344
+ .command("logs")
1345
+ .description("获取设备当前日志最新行")
1346
+ .option("-i, --ip <ip>", "设备 IP 地址")
1347
+ .option("--port <port>", "设备端口", "9800")
1348
+ .option("--limit <limit>", "读取最新日志行数,范围 1..5000", "200")
1349
+ .option("--format <format>", "返回格式: text|json", "text")
1350
+ .option("--output <output>", "日志输出文件路径(可选)")
1351
+ .action(logsCommand);
1352
+ // OCR 命令
1353
+ commander_1.program
1354
+ .command("ocr")
1355
+ .description("在设备上执行 OCR 识别")
1356
+ .option("-i, --ip <ip>", "设备 IP 地址")
1357
+ .option("--port <port>", "设备端口", "9800")
1358
+ .option("--engine <engine>", "OCR 引擎: appleocr|paddleocr", "appleocr")
1359
+ .option("--mode <mode>", "OCR 模式: recognize|numbers|findText", "recognize")
1360
+ .option("--input <input>", '输入源,默认 "screen"', "screen")
1361
+ .option("--x <x>", "区域左上角 x", "0")
1362
+ .option("--y <y>", "区域左上角 y", "0")
1363
+ .option("--ex <ex>", "区域右下角 x;全屏可传 0", "0")
1364
+ .option("--ey <ey>", "区域右下角 y;全屏可传 0", "0")
1365
+ .option("--text <text>", "findText 文本,可重复", collectStringOption, [])
1366
+ .option("--texts <texts>", "findText 文本数组,支持 JSON 数组或逗号分隔")
1367
+ .option("--languages <languages>", "Apple OCR 语言,支持 JSON 数组或逗号分隔")
1368
+ .option("--exact-match", "findText 要求完整匹配文本")
1369
+ .option("--confidence-threshold <confidenceThreshold>", "PaddleOCR 置信度阈值", "0.6")
1370
+ .option("--timeout-ms <timeoutMs>", "请求超时时间(毫秒)", "60000")
1371
+ .option("--format <format>", "返回格式: summary|json", "summary")
1372
+ .option("--output <output>", "OCR 输出文件路径(可选)")
1373
+ .action(ocrCommand);
1374
+ // 本地图片裁切命令
1375
+ commander_1.program
1376
+ .command("image-crop")
1377
+ .description("裁切本地图片并输出 PNG")
1378
+ .requiredOption("--image <image>", "输入图片路径")
1379
+ .option("--x <x>", "裁切起点 x", "0")
1380
+ .option("--y <y>", "裁切起点 y", "0")
1381
+ .requiredOption("--width <width>", "裁切宽度")
1382
+ .requiredOption("--height <height>", "裁切高度")
1383
+ .option("--output <output>", "输出 PNG 路径(可选)")
1384
+ .action(imageCropCommand);
1385
+ // 设备截图裁切命令
1386
+ commander_1.program
1387
+ .command("screen-crop")
1388
+ .description("从设备截图中裁切 PNG")
1389
+ .option("-i, --ip <ip>", "设备 IP 地址")
1390
+ .option("--port <port>", "设备端口", "9800")
1391
+ .option("--x <x>", "裁切起点 x", "0")
1392
+ .option("--y <y>", "裁切起点 y", "0")
1393
+ .requiredOption("--width <width>", "裁切宽度")
1394
+ .requiredOption("--height <height>", "裁切高度")
1395
+ .option("--output <output>", "输出 PNG 路径(可选)")
1396
+ .action(screenCropCommand);
1397
+ // 本地图片取色命令
1398
+ commander_1.program
1399
+ .command("image-pick-color")
1400
+ .description("读取本地图片指定坐标颜色")
1401
+ .requiredOption("--image <image>", "输入图片路径")
1402
+ .option("--x <x>", "目标坐标 x", "0")
1403
+ .option("--y <y>", "目标坐标 y", "0")
1404
+ .option("--radius <radius>", "采样半径,范围 0..20", "0")
1405
+ .action(imagePickColorCommand);
1406
+ // 设备截图取色命令
1407
+ commander_1.program
1408
+ .command("screen-pick-color")
1409
+ .description("读取设备截图指定坐标颜色")
1410
+ .option("-i, --ip <ip>", "设备 IP 地址")
1411
+ .option("--port <port>", "设备端口", "9800")
1412
+ .option("--x <x>", "目标坐标 x", "0")
1413
+ .option("--y <y>", "目标坐标 y", "0")
1414
+ .option("--radius <radius>", "采样半径,范围 0..20", "0")
1415
+ .action(screenPickColorCommand);
1416
+ // 本地图片透明化命令
1417
+ commander_1.program
1418
+ .command("image-transparent")
1419
+ .description("将图片中接近指定颜色或角落背景色的像素设为透明")
1420
+ .requiredOption("--image <image>", "输入图片路径")
1421
+ .option("--output <output>", "输出 PNG 路径(可选)")
1422
+ .option("--transparent-color <transparentColor>", "透明化颜色,格式 #RRGGBB")
1423
+ .option("--sample-corner <sampleCorner>", "未传 transparent-color 时采样角落: topLeft|topRight|bottomLeft|bottomRight", "topLeft")
1424
+ .option("--tolerance <tolerance>", "RGB 欧氏距离容差,范围 0..441", "24")
1425
+ .action(imageTransparentCommand);
1426
+ // API 文档命令
1427
+ const docsCommand = commander_1.program
1428
+ .command("docs")
1429
+ .description("查询 KuaiJS JS/中文/Python API 文档");
1430
+ docsCommand
1431
+ .command("paths")
1432
+ .description("显示 KuaiJS 本地文档路径")
1433
+ .option("--output <output>", "输出文件路径(可选)")
1434
+ .action(docsPathsCommand);
1435
+ docsCommand
1436
+ .command("list")
1437
+ .description("列出 API 文档")
1438
+ .option("--language <language>", "文档语言: js|js_zh|python", "js_zh")
1439
+ .option("--output <output>", "输出文件路径(可选)")
1440
+ .action(docsListCommand);
1441
+ docsCommand
1442
+ .command("search")
1443
+ .description("搜索 API 文档")
1444
+ .argument("<query>", "搜索关键字")
1445
+ .option("--language <language>", "文档语言: js|js_zh|python", "js_zh")
1446
+ .option("--limit <limit>", "返回条数上限,范围 1..20", "5")
1447
+ .option("--output <output>", "输出文件路径(可选)")
1448
+ .action(docsSearchCommand);
1449
+ docsCommand
1450
+ .command("read")
1451
+ .description("读取 API 文档")
1452
+ .argument("<slug>", "文档 slug(文件名,不含 .md)")
1453
+ .option("--language <language>", "文档语言: js|js_zh|python", "js_zh")
1454
+ .option("--output <output>", "输出文件路径(可选)")
1455
+ .action(docsReadCommand);
1456
+ // HTTP API 文档命令
1457
+ const httpDocsCommand = commander_1.program
1458
+ .command("http-docs")
1459
+ .description("查询 KuaiJS 设备 HTTP API 文档");
1460
+ httpDocsCommand
1461
+ .command("list")
1462
+ .description("列出 HTTP API 文档条目")
1463
+ .option("--output <output>", "输出文件路径(可选)")
1464
+ .action(httpDocsListCommand);
1465
+ httpDocsCommand
1466
+ .command("search")
1467
+ .description("搜索 HTTP API 文档")
1468
+ .argument("<query>", "搜索关键字、标题或接口路径")
1469
+ .option("--limit <limit>", "返回条数上限,范围 1..20", "8")
1470
+ .option("--output <output>", "输出文件路径(可选)")
1471
+ .action(httpDocsSearchCommand);
1472
+ httpDocsCommand
1473
+ .command("read")
1474
+ .description("读取 HTTP API 文档条目")
1475
+ .argument("<identifier>", "接口 slug 或 path")
1476
+ .option("--method <method>", "HTTP 方法,用于同路径多方法时消歧: GET|POST")
1477
+ .option("--output <output>", "输出文件路径(可选)")
1478
+ .action(httpDocsReadCommand);
1479
+ // HTTP API 调用命令
1480
+ commander_1.program
1481
+ .command("http-call")
1482
+ .description("按 HTTP API 文档声明调用设备接口")
1483
+ .option("-i, --ip <ip>", "设备 IP 地址")
1484
+ .option("--port <port>", "设备端口", "9800")
1485
+ .requiredOption("--method <method>", "HTTP 方法: GET|POST")
1486
+ .requiredOption("--path <path>", "HTTP API 相对路径,例如 /api/status")
1487
+ .requiredOption("--doc-slug <docSlug>", "http-docs 返回的接口 slug")
1488
+ .option("--query <query>", "JSON 对象 query 参数")
1489
+ .option("--body <body>", "JSON 请求体;普通字符串会作为 JSON 字符串提交")
1490
+ .option("--response-format <responseFormat>", "响应格式: json|text", "json")
1491
+ .option("--timeout-ms <timeoutMs>", "请求超时时间(毫秒)", "30000")
1492
+ .option("--output <output>", "响应输出文件路径(可选)")
1493
+ .action(httpCallCommand);
501
1494
  // WS 服务启动命令
502
1495
  commander_1.program
503
1496
  .command("ws-start")