@leju-gym/gym-mcp 0.0.4 → 0.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leju-gym/gym-mcp",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Gym MCP Server — 通过 MCP 协议调用 gym CLI,AI 助手可直接查询 KuavoDataHub 项目列表等数据",
5
5
  "bin": {
6
6
  "gym-mcp": "src/mcp.js"
@@ -3,7 +3,8 @@
3
3
  * @ModuleMethod: src/services/mcp
4
4
  * @ModuleUsage: MCP 工具注册与调用服务
5
5
  * @ModuleDesc: 通过 spawn gym CLI 调用本地 gym 命令,
6
- * 逻辑零重复,认证完全复用 gym config login
6
+ * 逻辑零重复,认证完全复用 gym config login
7
+ * 所有查询命令标记 readOnlyHint,写操作不做标记以触发用户确认。
7
8
  */
8
9
 
9
10
  const appHelper = require('../../AppHelper');
@@ -18,24 +19,24 @@ function gymBin() {
18
19
  }
19
20
 
20
21
  /**
21
- * 执行 gym CLI 命令,返回文本输出(不再要求 --json,直接返回原始输出)
22
+ * 执行 gym CLI 命令,返回清理 ANSI 后的纯文本输出
23
+ * @param {string[]} args - CLI 参数数组
24
+ * @param {number} [timeout=30000] - 超时毫秒,上传/下载需要更长时间
22
25
  */
23
- async function runGym(args = []) {
26
+ async function runGym(args = [], timeout = 30_000) {
24
27
  const bin = gymBin();
25
28
  appHelper.info('runGym', bin, args.join(' '));
26
29
  try {
27
30
  const { stdout, stderr } = await execFileAsync(bin, args, {
28
- timeout: 30_000,
31
+ timeout,
29
32
  maxBuffer: 1024 * 1024,
30
33
  });
31
34
  if (stderr) appHelper.warn('runGym stderr', stderr);
32
- // 清理 ANSI 转义序列,返回纯文本
33
35
  return cleanAnsi(stdout || '');
34
36
  } catch (e) {
35
37
  if (e.code === 'ENOENT') {
36
38
  return 'gym CLI 未安装或不在 PATH 中。请执行: npm i -g @leju-gym/gym-cli && gym config login';
37
39
  }
38
- // 输出错误信息
39
40
  const out = cleanAnsi(e.stdout || '') || e.stderr || e.message;
40
41
  return out;
41
42
  }
@@ -46,19 +47,22 @@ async function runGym(args = []) {
46
47
  */
47
48
  function cleanAnsi(str) {
48
49
  return str
49
- .replace(/\x1b\[[0-9;]*m/g, '') // 颜色/样式
50
+ .replace(/\x1b\[[0-9;]*m/g, '') // 颜色/样式
50
51
  .replace(/\x1b\[[0-9;]*[A-Za-z]/g, '') // 光标移动等
51
- .replace(/\x1b\]8;.*?\x1b\\/g, '') // 超链接
52
+ .replace(/\x1b\]8;.*?\x1b\\/g, '') // 超链接
52
53
  .trim();
53
54
  }
54
55
 
55
56
  // ─── 工具定义 ──────────────────────────────────────────────────
56
57
 
57
58
  const toolDefinitions = [
58
- // ── 连通性测试 ──
59
+ // ══════════════════════════════════════════════════════════════
60
+ // 连通性测试
61
+ // ══════════════════════════════════════════════════════════════
59
62
  {
60
63
  name: 'echo',
61
64
  description: '回显输入内容,用于测试连通性',
65
+ annotations: { readOnlyHint: true },
62
66
  inputSchema: {
63
67
  type: 'object',
64
68
  properties: {
@@ -68,11 +72,14 @@ const toolDefinitions = [
68
72
  },
69
73
  },
70
74
 
71
- // ── 项目管理 ──
75
+ // ══════════════════════════════════════════════════════════════
76
+ // 项目管理
77
+ // ══════════════════════════════════════════════════════════════
72
78
  {
73
79
  name: 'gym_project_list',
74
80
  description:
75
81
  '查询组织及其下属项目列表(调用本地 gym CLI,复用已登录的认证状态)。返回每个组织下的所有项目 ID、名称、编码。',
82
+ annotations: { readOnlyHint: true },
76
83
  inputSchema: {
77
84
  type: 'object',
78
85
  properties: {
@@ -82,11 +89,14 @@ const toolDefinitions = [
82
89
  },
83
90
  },
84
91
 
85
- // ── 任务管理 ──
92
+ // ══════════════════════════════════════════════════════════════
93
+ // 任务管理
94
+ // ══════════════════════════════════════════════════════════════
86
95
  {
87
96
  name: 'gym_task_list',
88
97
  description:
89
98
  '查询任务列表。支持按状态、任务名称、任务编码、项目、场景、标签等筛选,支持分页。',
99
+ annotations: { readOnlyHint: true },
90
100
  inputSchema: {
91
101
  type: 'object',
92
102
  properties: {
@@ -110,6 +120,7 @@ const toolDefinitions = [
110
120
  {
111
121
  name: 'gym_task_detail',
112
122
  description: '查看单个任务详情,包括采集/标注/审核进度等信息',
123
+ annotations: { readOnlyHint: true },
113
124
  inputSchema: {
114
125
  type: 'object',
115
126
  properties: {
@@ -121,6 +132,7 @@ const toolDefinitions = [
121
132
  {
122
133
  name: 'gym_task_list_bag',
123
134
  description: '按任务ID列出对应的 BAG 数据信息,支持分页和状态筛选',
135
+ annotations: { readOnlyHint: true },
124
136
  inputSchema: {
125
137
  type: 'object',
126
138
  properties: {
@@ -132,12 +144,51 @@ const toolDefinitions = [
132
144
  required: ['taskId'],
133
145
  },
134
146
  },
147
+ {
148
+ name: 'gym_task_tag_list',
149
+ description: '列出任务标签(标签与项目绑定)',
150
+ annotations: { readOnlyHint: true },
151
+ inputSchema: {
152
+ type: 'object',
153
+ properties: {
154
+ projectId: { type: 'string', description: '【必填】项目ID' },
155
+ },
156
+ required: ['projectId'],
157
+ },
158
+ },
159
+ {
160
+ name: 'gym_task_tag_bind',
161
+ description:
162
+ '给一批任务打标签(支持按任务ID列表或按过滤条件批量打标)。此操作会修改任务标签,需要用户确认。',
163
+ inputSchema: {
164
+ type: 'object',
165
+ properties: {
166
+ projectId: { type: 'string', description: '【必填】项目ID' },
167
+ labels: { type: 'string', description: '【必填】标签名称列表(逗号分隔)' },
168
+ taskIds: { type: 'string', description: '任务ID列表(逗号分隔),指定后按 SELECTED 模式打标' },
169
+ status: { type: 'string', description: '按状态筛选: 0=待处理 1=进行中 2=已完成' },
170
+ taskId: { type: 'string', description: '按任务ID(精确匹配)筛选' },
171
+ taskName: { type: 'string', description: '按任务名称(包含匹配)筛选' },
172
+ taskCode: { type: 'string', description: '按任务编码(精确匹配)筛选' },
173
+ startDate: { type: 'string', description: '创建开始日期 (YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD)' },
174
+ endDate: { type: 'string', description: '创建结束日期 (YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD)' },
175
+ scene1: { type: 'string', description: '按一级场景过滤' },
176
+ scene2: { type: 'string', description: '按二级场景过滤' },
177
+ scene3: { type: 'string', description: '按三级场景过滤' },
178
+ tag: { type: 'string', description: '按任务标签名过滤(用于缩小打标范围)' },
179
+ },
180
+ required: ['projectId', 'labels'],
181
+ },
182
+ },
135
183
 
136
- // ── 数据管理 ──
184
+ // ══════════════════════════════════════════════════════════════
185
+ // 数据管理
186
+ // ══════════════════════════════════════════════════════════════
137
187
  {
138
188
  name: 'gym_data_list',
139
189
  description:
140
190
  '查询数据管理列表。支持按状态、任务、项目、场景、标签、设备、采集员等筛选,支持分页。',
191
+ annotations: { readOnlyHint: true },
141
192
  inputSchema: {
142
193
  type: 'object',
143
194
  properties: {
@@ -164,6 +215,7 @@ const toolDefinitions = [
164
215
  {
165
216
  name: 'gym_data_detail',
166
217
  description: '查看单条数据详情,包括设备、采集员、标注员、审核员等信息',
218
+ annotations: { readOnlyHint: true },
167
219
  inputSchema: {
168
220
  type: 'object',
169
221
  properties: {
@@ -172,11 +224,121 @@ const toolDefinitions = [
172
224
  required: ['dataId'],
173
225
  },
174
226
  },
227
+ {
228
+ name: 'gym_data_tag_list',
229
+ description: '列出数据标签(标签与项目绑定)',
230
+ annotations: { readOnlyHint: true },
231
+ inputSchema: {
232
+ type: 'object',
233
+ properties: {
234
+ projectId: { type: 'string', description: '【必填】项目ID' },
235
+ },
236
+ required: ['projectId'],
237
+ },
238
+ },
239
+ {
240
+ name: 'gym_data_tag_create',
241
+ description:
242
+ '创建数据标签。此操作会修改数据标签,需要用户确认。',
243
+ inputSchema: {
244
+ type: 'object',
245
+ properties: {
246
+ projectId: { type: 'string', description: '【必填】项目ID' },
247
+ name: { type: 'string', description: '【必填】标签名称' },
248
+ },
249
+ required: ['projectId', 'name'],
250
+ },
251
+ },
252
+ {
253
+ name: 'gym_data_tag_bind',
254
+ description:
255
+ '给一批数据打标签(支持按数据ID列表或按过滤条件批量打标)。此操作会修改数据标签,需要用户确认。',
256
+ inputSchema: {
257
+ type: 'object',
258
+ properties: {
259
+ projectId: { type: 'string', description: '【必填】项目ID' },
260
+ labels: { type: 'string', description: '【必填】标签名称列表(逗号分隔)' },
261
+ dataIds: { type: 'string', description: '数据ID列表(逗号分隔),指定后按 SELECTED 模式打标' },
262
+ status: { type: 'string', description: '状态过滤: 0=待标注 1=待审核 2=审核失败 3=审核成功 4=待同步 5=同步失败 6=已标注 7=待验收 8=验收失败 9=验收成功 10=标注中' },
263
+ taskId: { type: 'string', description: '按任务ID(精确匹配)筛选' },
264
+ dataName: { type: 'string', description: '按数据文件名(包含匹配)筛选' },
265
+ taskName: { type: 'string', description: '按任务名(包含匹配)筛选' },
266
+ taskCode: { type: 'string', description: '按任务编码(精确匹配)筛选' },
267
+ startDate: { type: 'string', description: '上传开始日期 (YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD)' },
268
+ endDate: { type: 'string', description: '上传结束日期 (YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD)' },
269
+ scene1: { type: 'string', description: '按一级场景过滤' },
270
+ scene2: { type: 'string', description: '按二级场景过滤' },
271
+ scene3: { type: 'string', description: '按三级场景过滤' },
272
+ tag: { type: 'string', description: '按数据标签名过滤(用于缩小打标范围)' },
273
+ deviceSn: { type: 'string', description: '按设备序列号(包含匹配)筛选' },
274
+ collector: { type: 'string', description: '按采集员(包含匹配)筛选' },
275
+ dataType: { type: 'string', description: '按数据类型筛选: success/failure' },
276
+ },
277
+ required: ['projectId', 'labels'],
278
+ },
279
+ },
280
+ {
281
+ name: 'gym_data_download',
282
+ description:
283
+ '下载 bag 数据(支持单文件、批量、按任务/项目/标签过滤)。会写入本地磁盘,需要用户确认。',
284
+ inputSchema: {
285
+ type: 'object',
286
+ properties: {
287
+ dataId: { type: 'string', description: '单文件模式: 数据ID。不传则需使用 --batch 参数' },
288
+ batch: { type: 'boolean', description: '启用批量下载模式' },
289
+ output: { type: 'string', description: '输出目录,默认当前目录' },
290
+ taskId: { type: 'string', description: '批量模式: 按任务ID过滤(需配合 --batch)' },
291
+ projectId: { type: 'string', description: '批量模式: 按项目过滤' },
292
+ tag: { type: 'string', description: '批量模式: 按数据标签名过滤,支持多个逗号分隔' },
293
+ status: { type: 'string', description: '批量模式: 状态过滤。0=待标注 1=待审核 2=审核失败 3=审核成功 4=待同步 5=同步失败 6=已标注 7=待验收 8=验收失败 9=验收成功 10=标注中' },
294
+ dataName: { type: 'string', description: '批量模式: 文件名包含' },
295
+ startDate: { type: 'string', description: '批量模式: 开始日期' },
296
+ endDate: { type: 'string', description: '批量模式: 结束日期' },
297
+ scene1: { type: 'string', description: '批量模式: 按一级场景过滤' },
298
+ scene2: { type: 'string', description: '批量模式: 按二级场景过滤' },
299
+ scene3: { type: 'string', description: '批量模式: 按三级场景过滤' },
300
+ concurrency: { type: 'number', description: '批量模式: 并发数(1-10),默认 3' },
301
+ count: { type: 'number', description: '批量模式: 下载条数,默认 1000' },
302
+ page: { type: 'number', description: '批量模式: 起始页,默认 1' },
303
+ limit: { type: 'number', description: '批量模式: 每页拉取记录数,默认 50' },
304
+ force: { type: 'boolean', description: '强制覆盖下载(关闭断点续传)' },
305
+ yes: { type: 'boolean', description: '跳过确认直接开始下载' },
306
+ easy: { type: 'boolean', description: '简单模式: 仅下载 .bag 文件,不创建额外文件夹' },
307
+ metadata: { type: 'boolean', description: '同时下载 metadata.json' },
308
+ noOffline: { type: 'boolean', description: '禁用 NAS 离线下载' },
309
+ },
310
+ },
311
+ },
312
+ {
313
+ name: 'gym_data_upload',
314
+ description:
315
+ '上传 bag 包文件到云端存储。此操作会上传文件并注册数据记录,需要用户确认。',
316
+ inputSchema: {
317
+ type: 'object',
318
+ properties: {
319
+ taskId: { type: 'string', description: '【必填】任务 ID' },
320
+ bagPath: { type: 'string', description: '【必填】本地 bag 文件路径(支持单个文件或目录)' },
321
+ deviceSn: { type: 'string', description: '【必填】设备 SN 码(如 LB_11、11_200)' },
322
+ deviceType: { type: 'string', description: '【必填】设备类型: "Kuavo 4Pro" | "Kuavo LB" | "Kuavo 5" | "Kuavo 5W"' },
323
+ location: { type: 'string', description: '上传目标位置 (cloud/nas),默认 cloud' },
324
+ dataType: { type: 'string', description: '数据类型 (success/failure),默认 success' },
325
+ topicsVersion: { type: 'string', description: '机器人话题版本号,默认 unknown' },
326
+ lowerVersion: { type: 'string', description: '下位机版本,默认 unknown' },
327
+ lowerCommit: { type: 'string', description: '下位机提交记录' },
328
+ duration: { type: 'string', description: 'bag 包时长(秒),默认 0' },
329
+ remark: { type: 'string', description: '备注信息' },
330
+ },
331
+ required: ['taskId', 'bagPath', 'deviceSn', 'deviceType'],
332
+ },
333
+ },
175
334
 
176
- // ── 设备管理 ──
335
+ // ══════════════════════════════════════════════════════════════
336
+ // 设备管理
337
+ // ══════════════════════════════════════════════════════════════
177
338
  {
178
339
  name: 'gym_device_list',
179
340
  description: '分页查询设备列表',
341
+ annotations: { readOnlyHint: true },
180
342
  inputSchema: {
181
343
  type: 'object',
182
344
  properties: {
@@ -186,10 +348,13 @@ const toolDefinitions = [
186
348
  },
187
349
  },
188
350
 
189
- // ── 场景管理 ──
351
+ // ══════════════════════════════════════════════════════════════
352
+ // 场景管理
353
+ // ══════════════════════════════════════════════════════════════
190
354
  {
191
355
  name: 'gym_scene_list',
192
356
  description: '查询场景列表。可选一级/二级场景ID来查询子场景。',
357
+ annotations: { readOnlyHint: true },
193
358
  inputSchema: {
194
359
  type: 'object',
195
360
  properties: {
@@ -201,10 +366,13 @@ const toolDefinitions = [
201
366
  },
202
367
  },
203
368
 
204
- // ── 交付任务管理 ──
369
+ // ══════════════════════════════════════════════════════════════
370
+ // 交付任务管理
371
+ // ══════════════════════════════════════════════════════════════
205
372
  {
206
373
  name: 'gym_delivery_list',
207
374
  description: '查询交付任务列表(支持分页)',
375
+ annotations: { readOnlyHint: true },
208
376
  inputSchema: {
209
377
  type: 'object',
210
378
  properties: {
@@ -213,11 +381,80 @@ const toolDefinitions = [
213
381
  },
214
382
  },
215
383
  },
384
+ {
385
+ name: 'gym_delivery_download',
386
+ description:
387
+ '下载交付任务数据(支持 Token 免登录模式或任务ID+业务名模式)。会写入本地磁盘,需要用户确认。',
388
+ inputSchema: {
389
+ type: 'object',
390
+ properties: {
391
+ taskId: { type: 'string', description: '交付任务ID(非 Token 模式必填)' },
392
+ businessName: { type: 'string', description: '业务名称(非 Token 模式必填)' },
393
+ downloadType: { type: 'string', description: '下载方式: cloud / nas / mix(非 Token 模式必填)' },
394
+ token: { type: 'string', description: '交付 Token(外部免登录模式,有此参数时无需 taskId/businessName/downloadType)' },
395
+ saveDir: { type: 'string', description: '保存目录,默认 ./download' },
396
+ nasSn: { type: 'string', description: 'NAS 序列号(download-type=nas 时必填)' },
397
+ concurrency: { type: 'number', description: '并发下载通道数(默认 3, 最大 10)' },
398
+ env: { type: 'string', description: 'Token 模式: 指定环境 dev/test/prod,默认 prod' },
399
+ },
400
+ },
401
+ },
402
+
403
+ // ══════════════════════════════════════════════════════════════
404
+ // 文件管理
405
+ // ══════════════════════════════════════════════════════════════
406
+ {
407
+ name: 'gym_file_list',
408
+ description: '列出记录下的文件',
409
+ annotations: { readOnlyHint: true },
410
+ inputSchema: {
411
+ type: 'object',
412
+ properties: {
413
+ recordId: { type: 'string', description: '【必填】记录ID' },
414
+ prefix: { type: 'string', description: '路径前缀' },
415
+ limit: { type: 'number', description: '最大返回数量,默认 100' },
416
+ },
417
+ required: ['recordId'],
418
+ },
419
+ },
420
+ {
421
+ name: 'gym_file_upload',
422
+ description:
423
+ '上传文件或文件夹到记录。此操作会写入 OSS 并注册文件记录,需要用户确认。',
424
+ inputSchema: {
425
+ type: 'object',
426
+ properties: {
427
+ recordId: { type: 'string', description: '【必填】记录ID' },
428
+ source: { type: 'string', description: '【必填】一个或多个文件/文件夹路径(空格分隔)' },
429
+ path: { type: 'string', description: '目标路径(相对于记录根目录)' },
430
+ stallTimeout: { type: 'number', description: '停滞超时(秒),默认 300' },
431
+ },
432
+ required: ['recordId', 'source'],
433
+ },
434
+ },
435
+ {
436
+ name: 'gym_file_download',
437
+ description:
438
+ '下载记录下的文件(支持单文件和递归下载目录)。会写入本地磁盘,需要用户确认。',
439
+ inputSchema: {
440
+ type: 'object',
441
+ properties: {
442
+ recordId: { type: 'string', description: '【必填】记录ID' },
443
+ filePath: { type: 'string', description: '【必填】文件路径' },
444
+ output: { type: 'string', description: '本地保存路径,默认当前目录' },
445
+ recursive: { type: 'boolean', description: '递归下载目录下所有文件' },
446
+ },
447
+ required: ['recordId', 'filePath'],
448
+ },
449
+ },
216
450
 
217
- // ── 文件记录管理 ──
451
+ // ══════════════════════════════════════════════════════════════
452
+ // 文件记录管理
453
+ // ══════════════════════════════════════════════════════════════
218
454
  {
219
455
  name: 'gym_record_list',
220
456
  description: '分页查询文件记录列表',
457
+ annotations: { readOnlyHint: true },
221
458
  inputSchema: {
222
459
  type: 'object',
223
460
  properties: {
@@ -226,6 +463,22 @@ const toolDefinitions = [
226
463
  },
227
464
  },
228
465
  },
466
+ {
467
+ name: 'gym_record_create',
468
+ description:
469
+ '创建新的文件记录。此操作会在服务端创建记录,需要用户确认。',
470
+ inputSchema: {
471
+ type: 'object',
472
+ properties: {
473
+ title: { type: 'string', description: '【必填】记录标题' },
474
+ projectId: { type: 'string', description: '【必填】所属项目 ID' },
475
+ deviceId: { type: 'string', description: '关联设备 ID(可选)' },
476
+ description: { type: 'string', description: '记录描述(可选)' },
477
+ visibility: { type: 'string', description: '可见性: public / private,默认 private' },
478
+ },
479
+ required: ['title', 'projectId'],
480
+ },
481
+ },
229
482
  ];
230
483
 
231
484
  // ─── ToolService ────────────────────────────────────────────────
@@ -236,6 +489,7 @@ class ToolService {
236
489
  name: t.name,
237
490
  description: t.description,
238
491
  inputSchema: t.inputSchema,
492
+ annotations: t.annotations,
239
493
  }));
240
494
  }
241
495
 
@@ -249,128 +503,266 @@ class ToolService {
249
503
  appHelper.info('callTool', toolName, JSON.stringify(args));
250
504
 
251
505
  switch (toolName) {
506
+ // ── 连通性测试 ──
252
507
  case 'echo':
253
508
  return { ok: true, data: { echo: args.message || '' } };
254
509
 
255
510
  // ── 项目管理 ──
256
511
  case 'gym_project_list': {
257
- const page = args.page || 1;
258
- const limit = args.limit || 10;
259
512
  const result = await runGym([
260
513
  'project', 'list',
261
- '--page', String(page),
262
- '--limit', String(limit),
514
+ '--page', String(args.page || 1),
515
+ '--limit', String(args.limit || 10),
263
516
  ]);
264
517
  return { ok: true, data: result };
265
518
  }
266
519
 
267
520
  // ── 任务管理 ──
268
521
  case 'gym_task_list': {
269
- const cliArgs = ['task', 'list'];
270
- if (args.page) cliArgs.push('--page', String(args.page));
271
- if (args.limit) cliArgs.push('--limit', String(args.limit));
272
- if (args.status) cliArgs.push('--status', String(args.status));
273
- if (args.taskId) cliArgs.push('--task-id', String(args.taskId));
274
- if (args.taskName) cliArgs.push('--task-name', String(args.taskName));
275
- if (args.taskCode) cliArgs.push('--task-code', String(args.taskCode));
276
- if (args.startDate) cliArgs.push('--start-date', String(args.startDate));
277
- if (args.endDate) cliArgs.push('--end-date', String(args.endDate));
278
- if (args.tag) cliArgs.push('--tag', String(args.tag));
279
- // projectId 和 scene 是必填/可选参数
280
- cliArgs.push('--project-id', String(args.projectId));
281
- if (args.scene1) cliArgs.push('--scene1', String(args.scene1));
282
- if (args.scene2) cliArgs.push('--scene2', String(args.scene2));
283
- if (args.scene3) cliArgs.push('--scene3', String(args.scene3));
284
- const result = await runGym(cliArgs);
285
- return { ok: true, data: result };
522
+ const cliArgs = ['task', 'list', '--project-id', String(args.projectId)];
523
+ if (args.page) cliArgs.push('--page', String(args.page));
524
+ if (args.limit) cliArgs.push('--limit', String(args.limit));
525
+ if (args.status) cliArgs.push('--status', String(args.status));
526
+ if (args.taskId) cliArgs.push('--task-id', String(args.taskId));
527
+ if (args.taskName) cliArgs.push('--task-name', String(args.taskName));
528
+ if (args.taskCode) cliArgs.push('--task-code', String(args.taskCode));
529
+ if (args.startDate) cliArgs.push('--start-date', String(args.startDate));
530
+ if (args.endDate) cliArgs.push('--end-date', String(args.endDate));
531
+ if (args.tag) cliArgs.push('--tag', String(args.tag));
532
+ if (args.scene1) cliArgs.push('--scene1', String(args.scene1));
533
+ if (args.scene2) cliArgs.push('--scene2', String(args.scene2));
534
+ if (args.scene3) cliArgs.push('--scene3', String(args.scene3));
535
+ return { ok: true, data: await runGym(cliArgs) };
286
536
  }
287
537
 
288
- case 'gym_task_detail': {
289
- const result = await runGym(['task', 'detail', String(args.taskId)]);
290
- return { ok: true, data: result };
291
- }
538
+ case 'gym_task_detail':
539
+ return { ok: true, data: await runGym(['task', 'detail', String(args.taskId)]) };
292
540
 
293
541
  case 'gym_task_list_bag': {
294
542
  const cliArgs = ['task', 'list-bag', String(args.taskId)];
295
543
  if (args.page) cliArgs.push('--page', String(args.page));
296
544
  if (args.limit) cliArgs.push('--limit', String(args.limit));
297
545
  if (args.status !== undefined) cliArgs.push('--status', String(args.status));
298
- const result = await runGym(cliArgs);
299
- return { ok: true, data: result };
546
+ return { ok: true, data: await runGym(cliArgs) };
547
+ }
548
+
549
+ case 'gym_task_tag_list':
550
+ return { ok: true, data: await runGym(['task', 'tag', 'list', '--project-id', String(args.projectId)]) };
551
+
552
+ case 'gym_task_tag_bind': {
553
+ const cliArgs = ['task', 'tag', 'bind',
554
+ '--project-id', String(args.projectId),
555
+ '--labels', String(args.labels),
556
+ ];
557
+ if (args.taskIds) cliArgs.push('--task-ids', String(args.taskIds));
558
+ if (args.status) cliArgs.push('--status', String(args.status));
559
+ if (args.taskId) cliArgs.push('--task-id', String(args.taskId));
560
+ if (args.taskName) cliArgs.push('--task-name', String(args.taskName));
561
+ if (args.taskCode) cliArgs.push('--task-code', String(args.taskCode));
562
+ if (args.startDate) cliArgs.push('--start-date', String(args.startDate));
563
+ if (args.endDate) cliArgs.push('--end-date', String(args.endDate));
564
+ if (args.scene1) cliArgs.push('--scene1', String(args.scene1));
565
+ if (args.scene2) cliArgs.push('--scene2', String(args.scene2));
566
+ if (args.scene3) cliArgs.push('--scene3', String(args.scene3));
567
+ if (args.tag) cliArgs.push('--tag', String(args.tag));
568
+ return { ok: true, data: await runGym(cliArgs) };
300
569
  }
301
570
 
302
571
  // ── 数据管理 ──
303
572
  case 'gym_data_list': {
304
573
  const cliArgs = ['data', 'list'];
305
- if (args.page) cliArgs.push('--page', String(args.page));
306
- if (args.limit) cliArgs.push('--limit', String(args.limit));
307
- if (args.status) cliArgs.push('--status', String(args.status));
308
- if (args.taskId) cliArgs.push('--task-id', String(args.taskId));
309
- if (args.dataName) cliArgs.push('--data-name', String(args.dataName));
310
- if (args.taskName) cliArgs.push('--task-name', String(args.taskName));
311
- if (args.taskCode) cliArgs.push('--task-code', String(args.taskCode));
312
- if (args.startDate) cliArgs.push('--start-date', String(args.startDate));
313
- if (args.endDate) cliArgs.push('--end-date', String(args.endDate));
314
- if (args.tag) cliArgs.push('--tag', String(args.tag));
315
- if (args.deviceSn) cliArgs.push('--device-sn', String(args.deviceSn));
316
- if (args.collector) cliArgs.push('--collector', String(args.collector));
317
- if (args.dataType) cliArgs.push('--data-type', String(args.dataType));
318
- if (args.projectId) cliArgs.push('--project-id', String(args.projectId));
319
- if (args.scene1) cliArgs.push('--scene1', String(args.scene1));
320
- if (args.scene2) cliArgs.push('--scene2', String(args.scene2));
321
- if (args.scene3) cliArgs.push('--scene3', String(args.scene3));
322
- const result = await runGym(cliArgs);
323
- return { ok: true, data: result };
574
+ if (args.page) cliArgs.push('--page', String(args.page));
575
+ if (args.limit) cliArgs.push('--limit', String(args.limit));
576
+ if (args.status) cliArgs.push('--status', String(args.status));
577
+ if (args.taskId) cliArgs.push('--task-id', String(args.taskId));
578
+ if (args.dataName) cliArgs.push('--data-name', String(args.dataName));
579
+ if (args.taskName) cliArgs.push('--task-name', String(args.taskName));
580
+ if (args.taskCode) cliArgs.push('--task-code', String(args.taskCode));
581
+ if (args.startDate) cliArgs.push('--start-date', String(args.startDate));
582
+ if (args.endDate) cliArgs.push('--end-date', String(args.endDate));
583
+ if (args.tag) cliArgs.push('--tag', String(args.tag));
584
+ if (args.deviceSn) cliArgs.push('--device-sn', String(args.deviceSn));
585
+ if (args.collector) cliArgs.push('--collector', String(args.collector));
586
+ if (args.dataType) cliArgs.push('--data-type', String(args.dataType));
587
+ if (args.projectId) cliArgs.push('--project-id', String(args.projectId));
588
+ if (args.scene1) cliArgs.push('--scene1', String(args.scene1));
589
+ if (args.scene2) cliArgs.push('--scene2', String(args.scene2));
590
+ if (args.scene3) cliArgs.push('--scene3', String(args.scene3));
591
+ return { ok: true, data: await runGym(cliArgs) };
324
592
  }
325
593
 
326
- case 'gym_data_detail': {
327
- const result = await runGym(['data', 'detail', String(args.dataId)]);
328
- return { ok: true, data: result };
594
+ case 'gym_data_detail':
595
+ return { ok: true, data: await runGym(['data', 'detail', String(args.dataId)]) };
596
+
597
+ case 'gym_data_tag_list':
598
+ return { ok: true, data: await runGym(['data', 'tag', 'list', '--project-id', String(args.projectId)]) };
599
+
600
+ case 'gym_data_tag_create':
601
+ return { ok: true, data: await runGym(['data', 'tag', 'create',
602
+ '--project-id', String(args.projectId),
603
+ '--name', String(args.name),
604
+ ]) };
605
+
606
+ case 'gym_data_tag_bind': {
607
+ const cliArgs = ['data', 'tag', 'bind',
608
+ '--project-id', String(args.projectId),
609
+ '--labels', String(args.labels),
610
+ ];
611
+ if (args.dataIds) cliArgs.push('--data-ids', String(args.dataIds));
612
+ if (args.status) cliArgs.push('--status', String(args.status));
613
+ if (args.taskId) cliArgs.push('--task-id', String(args.taskId));
614
+ if (args.dataName) cliArgs.push('--data-name', String(args.dataName));
615
+ if (args.taskName) cliArgs.push('--task-name', String(args.taskName));
616
+ if (args.taskCode) cliArgs.push('--task-code', String(args.taskCode));
617
+ if (args.startDate) cliArgs.push('--start-date', String(args.startDate));
618
+ if (args.endDate) cliArgs.push('--end-date', String(args.endDate));
619
+ if (args.scene1) cliArgs.push('--scene1', String(args.scene1));
620
+ if (args.scene2) cliArgs.push('--scene2', String(args.scene2));
621
+ if (args.scene3) cliArgs.push('--scene3', String(args.scene3));
622
+ if (args.tag) cliArgs.push('--tag', String(args.tag));
623
+ if (args.deviceSn) cliArgs.push('--device-sn', String(args.deviceSn));
624
+ if (args.collector) cliArgs.push('--collector', String(args.collector));
625
+ if (args.dataType) cliArgs.push('--data-type', String(args.dataType));
626
+ return { ok: true, data: await runGym(cliArgs) };
627
+ }
628
+
629
+ case 'gym_data_download': {
630
+ const cliArgs = ['data', 'download'];
631
+ if (args.dataId) {
632
+ // 单文件模式:dataId 作为位置参数
633
+ cliArgs.splice(1, 0, String(args.dataId));
634
+ }
635
+ if (args.output) cliArgs.push('--output', String(args.output));
636
+ if (args.batch) cliArgs.push('--batch');
637
+ if (args.force) cliArgs.push('--force');
638
+ if (args.yes) cliArgs.push('--yes');
639
+ if (args.easy) cliArgs.push('--easy');
640
+ if (args.metadata) cliArgs.push('--metadata');
641
+ if (args.noOffline) cliArgs.push('--no-offline');
642
+ if (args.taskId) cliArgs.push('--task-id', String(args.taskId));
643
+ if (args.projectId) cliArgs.push('--project-id', String(args.projectId));
644
+ if (args.tag) cliArgs.push('--tag', String(args.tag));
645
+ if (args.status) cliArgs.push('--status', String(args.status));
646
+ if (args.dataName) cliArgs.push('--data-name', String(args.dataName));
647
+ if (args.startDate) cliArgs.push('--start', String(args.startDate));
648
+ if (args.endDate) cliArgs.push('--end', String(args.endDate));
649
+ if (args.scene1) cliArgs.push('--scene1', String(args.scene1));
650
+ if (args.scene2) cliArgs.push('--scene2', String(args.scene2));
651
+ if (args.scene3) cliArgs.push('--scene3', String(args.scene3));
652
+ if (args.count) cliArgs.push('--count', String(args.count));
653
+ if (args.concurrency) cliArgs.push('--concurrency', String(args.concurrency));
654
+ if (args.page) cliArgs.push('--page', String(args.page));
655
+ if (args.limit) cliArgs.push('--limit', String(args.limit));
656
+ // 下载超时 10 分钟
657
+ return { ok: true, data: await runGym(cliArgs, 600_000) };
658
+ }
659
+
660
+ case 'gym_data_upload': {
661
+ const cliArgs = ['data', 'upload',
662
+ '--task-id', String(args.taskId),
663
+ '--bag-path', String(args.bagPath),
664
+ '--device-sn', String(args.deviceSn),
665
+ '--device-type', String(args.deviceType),
666
+ ];
667
+ if (args.location) cliArgs.push('--location', String(args.location));
668
+ if (args.dataType) cliArgs.push('--data-type', String(args.dataType));
669
+ if (args.topicsVersion) cliArgs.push('--topics-version', String(args.topicsVersion));
670
+ if (args.lowerVersion) cliArgs.push('--lower-version', String(args.lowerVersion));
671
+ if (args.lowerCommit) cliArgs.push('--lower-commit', String(args.lowerCommit));
672
+ if (args.duration) cliArgs.push('--duration', String(args.duration));
673
+ if (args.remark) cliArgs.push('--remark', String(args.remark));
674
+ // 上传超时 30 分钟
675
+ return { ok: true, data: await runGym(cliArgs, 1_800_000) };
329
676
  }
330
677
 
331
678
  // ── 设备管理 ──
332
- case 'gym_device_list': {
333
- const page = args.page || 1;
334
- const limit = args.limit || 10;
335
- const result = await runGym([
679
+ case 'gym_device_list':
680
+ return { ok: true, data: await runGym([
336
681
  'device', 'list',
337
- '--page', String(page),
338
- '--limit', String(limit),
339
- ]);
340
- return { ok: true, data: result };
341
- }
682
+ '--page', String(args.page || 1),
683
+ '--limit', String(args.limit || 10),
684
+ ]) };
342
685
 
343
686
  // ── 场景管理 ──
344
687
  case 'gym_scene_list': {
345
688
  const cliArgs = ['scene', 'list', '--project', String(args.projectId)];
346
689
  if (args.scene1) cliArgs.push('--scene1', String(args.scene1));
347
690
  if (args.scene2) cliArgs.push('--scene2', String(args.scene2));
348
- const result = await runGym(cliArgs);
349
- return { ok: true, data: result };
691
+ return { ok: true, data: await runGym(cliArgs) };
350
692
  }
351
693
 
352
694
  // ── 交付任务管理 ──
353
- case 'gym_delivery_list': {
354
- const page = args.page || 1;
355
- const limit = args.limit || 10;
356
- const result = await runGym([
695
+ case 'gym_delivery_list':
696
+ return { ok: true, data: await runGym([
357
697
  'delivery', 'list',
358
- '--page', String(page),
359
- '--limit', String(limit),
360
- ]);
361
- return { ok: true, data: result };
698
+ '--page', String(args.page || 1),
699
+ '--limit', String(args.limit || 10),
700
+ ]) };
701
+
702
+ case 'gym_delivery_download': {
703
+ const cliArgs = ['delivery', 'download'];
704
+ if (args.token) {
705
+ cliArgs.push('--token', String(args.token));
706
+ if (args.env) cliArgs.push('--env', String(args.env));
707
+ } else {
708
+ cliArgs.push('--task-id', String(args.taskId));
709
+ cliArgs.push('--business-name', String(args.businessName));
710
+ cliArgs.push('--download-type', String(args.downloadType));
711
+ }
712
+ if (args.saveDir) cliArgs.push('--save-dir', String(args.saveDir));
713
+ if (args.nasSn) cliArgs.push('--nas-sn', String(args.nasSn));
714
+ if (args.concurrency) cliArgs.push('--concurrency', String(args.concurrency));
715
+ // 下载超时 30 分钟
716
+ return { ok: true, data: await runGym(cliArgs, 1_800_000) };
717
+ }
718
+
719
+ // ── 文件管理 ──
720
+ case 'gym_file_list': {
721
+ const cliArgs = ['file', 'list', '--record-id', String(args.recordId)];
722
+ if (args.prefix) cliArgs.push('--prefix', String(args.prefix));
723
+ if (args.limit) cliArgs.push('--limit', String(args.limit));
724
+ return { ok: true, data: await runGym(cliArgs) };
725
+ }
726
+
727
+ case 'gym_file_upload': {
728
+ const cliArgs = ['file', 'upload',
729
+ '--record-id', String(args.recordId),
730
+ '--source', String(args.source),
731
+ ];
732
+ if (args.path) cliArgs.push('--path', String(args.path));
733
+ if (args.stallTimeout) cliArgs.push('--stall-timeout', String(args.stallTimeout));
734
+ // 上传超时 30 分钟
735
+ return { ok: true, data: await runGym(cliArgs, 1_800_000) };
736
+ }
737
+
738
+ case 'gym_file_download': {
739
+ const cliArgs = ['file', 'download',
740
+ '--record-id', String(args.recordId),
741
+ '--file-path', String(args.filePath),
742
+ ];
743
+ if (args.output) cliArgs.push('--output', String(args.output));
744
+ if (args.recursive) cliArgs.push('--recursive');
745
+ // 下载超时 10 分钟
746
+ return { ok: true, data: await runGym(cliArgs, 600_000) };
362
747
  }
363
748
 
364
749
  // ── 文件记录管理 ──
365
- case 'gym_record_list': {
366
- const page = args.page || 1;
367
- const limit = args.limit || 10;
368
- const result = await runGym([
750
+ case 'gym_record_list':
751
+ return { ok: true, data: await runGym([
369
752
  'record', 'list',
370
- '--page', String(page),
371
- '--limit', String(limit),
372
- ]);
373
- return { ok: true, data: result };
753
+ '--page', String(args.page || 1),
754
+ '--limit', String(args.limit || 10),
755
+ ]) };
756
+
757
+ case 'gym_record_create': {
758
+ const cliArgs = ['record', 'create',
759
+ '--title', String(args.title),
760
+ '--project-id', String(args.projectId),
761
+ ];
762
+ if (args.deviceId) cliArgs.push('--device-id', String(args.deviceId));
763
+ if (args.description) cliArgs.push('--description', String(args.description));
764
+ if (args.visibility) cliArgs.push('--visibility', String(args.visibility));
765
+ return { ok: true, data: await runGym(cliArgs) };
374
766
  }
375
767
 
376
768
  default: