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 +707 -9
- package/dist/mcp/doc-tools.d.ts +0 -8
- package/dist/mcp/doc-tools.js +17 -188
- package/dist/mcp/docs-service.d.ts +1 -50
- package/dist/mcp/docs-service.js +0 -105
- package/dist/mcp/httpapi-docs-service.d.ts +8 -32
- package/dist/mcp/httpapi-docs-service.js +8 -89
- package/dist/mcp/httpapi-tools.d.ts +1 -1
- package/dist/mcp/httpapi-tools.js +7 -155
- package/dist/mcp/image-tools.d.ts +69 -0
- package/dist/mcp/image-tools.js +163 -0
- package/dist/mcp/ocr-tools.d.ts +50 -0
- package/dist/mcp/ocr-tools.js +59 -48
- package/dist/mcp/runtime-tools.js +12 -4
- package/dist/mcp/tool-utils.d.ts +0 -12
- package/dist/mcp/tool-utils.js +0 -15
- package/dist/mcp/tools.js +0 -1
- package/dist/mcp/types.d.ts +0 -13
- package/dist/project.d.ts +3 -2
- package/dist/project.js +8 -2
- package/docs/AGENTS.md +54 -52
- package/docs/SKILL.md +52 -45
- package/docs/api/paddleocr.md +11 -33
- package/docs/apicn/paddleocr.md +5 -29
- package/docs/apipython/paddleocr.md +34 -55
- package/docs/mcp-agent-description.md +58 -73
- package/package.json +1 -1
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
|
-
|
|
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")
|