@cloudglab/yapi-cli 0.0.8 → 0.0.9

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +6 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.js +41 -77
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/changelog.js +7 -14
  7. package/dist/core/changelog.js.map +1 -1
  8. package/dist/core/cli-output.js +7 -6
  9. package/dist/core/cli-output.js.map +1 -1
  10. package/dist/core/cli-registry.js +28 -6
  11. package/dist/core/cli-registry.js.map +1 -1
  12. package/dist/core/manifest.js +14 -4
  13. package/dist/core/manifest.js.map +1 -1
  14. package/dist/core/output.d.ts +2 -2
  15. package/dist/core/output.js +12 -45
  16. package/dist/core/output.js.map +1 -1
  17. package/dist/core/tool-registry.js +5 -0
  18. package/dist/core/tool-registry.js.map +1 -1
  19. package/dist/core/update-probe.d.ts +3 -1
  20. package/dist/core/update-probe.js +9 -4
  21. package/dist/core/update-probe.js.map +1 -1
  22. package/dist/core/url-parser.js +10 -0
  23. package/dist/core/url-parser.js.map +1 -1
  24. package/dist/install.d.ts +9 -1
  25. package/dist/install.js +74 -20
  26. package/dist/install.js.map +1 -1
  27. package/dist/manifest.json +37 -1
  28. package/dist/services/yapi/api.d.ts +255 -79
  29. package/dist/services/yapi/api.js +162 -58
  30. package/dist/services/yapi/api.js.map +1 -1
  31. package/dist/services/yapi/auth.d.ts +8 -3
  32. package/dist/services/yapi/auth.js +10 -6
  33. package/dist/services/yapi/auth.js.map +1 -1
  34. package/dist/services/yapi/authCache.js +9 -2
  35. package/dist/services/yapi/authCache.js.map +1 -1
  36. package/dist/services/yapi/config.d.ts +6 -0
  37. package/dist/services/yapi/config.js +85 -8
  38. package/dist/services/yapi/config.js.map +1 -1
  39. package/dist/services/yapi/index.d.ts +3 -3
  40. package/dist/services/yapi/index.js +2 -3
  41. package/dist/services/yapi/index.js.map +1 -1
  42. package/dist/services/yapi/types.d.ts +38 -0
  43. package/dist/tools/shared.d.ts +17 -1
  44. package/dist/tools/shared.js +25 -6
  45. package/dist/tools/shared.js.map +1 -1
  46. package/dist/tools/yapi/docs-sync.js +3 -0
  47. package/dist/tools/yapi/docs-sync.js.map +1 -1
  48. package/dist/tools/yapi/groups.d.ts +1 -1
  49. package/dist/tools/yapi/groups.js +1 -1
  50. package/dist/tools/yapi/groups.js.map +1 -1
  51. package/dist/tools/yapi/register-auth.js +116 -104
  52. package/dist/tools/yapi/register-auth.js.map +1 -1
  53. package/dist/tools/yapi/register-group.js +118 -93
  54. package/dist/tools/yapi/register-group.js.map +1 -1
  55. package/dist/tools/yapi/register-interface.js +418 -238
  56. package/dist/tools/yapi/register-interface.js.map +1 -1
  57. package/dist/tools/yapi/register-mock.d.ts +2 -0
  58. package/dist/tools/yapi/register-mock.js +240 -0
  59. package/dist/tools/yapi/register-mock.js.map +1 -0
  60. package/dist/tools/yapi/register-project.js +344 -223
  61. package/dist/tools/yapi/register-project.js.map +1 -1
  62. package/dist/tools/yapi/register-test.js +33 -25
  63. package/dist/tools/yapi/register-test.js.map +1 -1
  64. package/dist/tools/yapi/register-util.js +444 -350
  65. package/dist/tools/yapi/register-util.js.map +1 -1
  66. package/dist/tools/yapi/register.js +3 -0
  67. package/dist/tools/yapi/register.js.map +1 -1
  68. package/dist/tools/yapi/utils.d.ts +49 -0
  69. package/dist/tools/yapi/utils.js +124 -2
  70. package/dist/tools/yapi/utils.js.map +1 -1
  71. package/dist/version.d.ts +1 -1
  72. package/dist/version.js +1 -1
  73. package/package.json +18 -5
  74. package/skills/yapi-cli/SKILL.md +22 -12
  75. package/skills/yapi-cli/reference/auth.md +34 -45
  76. package/skills/yapi-cli/reference/cheatsheet.md +156 -0
  77. package/skills/yapi-cli/reference/cli.md +39 -51
  78. package/skills/yapi-cli/reference/commands.md +35 -125
  79. package/skills/yapi-cli/reference/group.md +46 -19
  80. package/skills/yapi-cli/reference/index.md +32 -0
  81. package/skills/yapi-cli/reference/install.md +30 -6
  82. package/skills/yapi-cli/reference/interface.md +71 -155
  83. package/skills/yapi-cli/reference/mock.md +93 -0
  84. package/skills/yapi-cli/reference/overview.md +7 -5
  85. package/skills/yapi-cli/reference/project.md +67 -87
  86. package/skills/yapi-cli/reference/scenarios.md +184 -0
  87. package/skills/yapi-cli/reference/test.md +20 -17
  88. package/skills/yapi-cli/reference/tooling.md +89 -0
  89. package/dist/services/yapi/cache.d.ts +0 -27
  90. package/dist/services/yapi/cache.js +0 -88
  91. package/dist/services/yapi/cache.js.map +0 -1
@@ -1,10 +1,9 @@
1
- import { existsSync, mkdirSync, copyFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
1
  import { z } from 'zod';
5
2
  import { CliConfigError, CliError, CliValidationError } from '../../core/errors.js';
6
3
  import { parseYApiPageUrl } from '../../core/url-parser.js';
7
- import { exportFormatEnum, jsonResult, optionalBool, paginationSchema, projectIdSchema } from '../shared.js';
4
+ import { installSkill } from '../../install.js';
5
+ import { exportFormatEnum, jsonResult, optionalBool, paginationSchema, projectIdSchema, confirmSchema, runWithPreview } from '../shared.js';
6
+ import { previewOrAssertWriteAllowed } from '../../core/write-guard.js';
8
7
  import { createYApiServices } from './utils.js';
9
8
  import { CLI_VERSION } from '../../version.js';
10
9
  import { syncDocsToProject, watchAndSync, formatSyncResult } from './docs-sync.js';
@@ -15,6 +14,13 @@ const userBaseSchema = {
15
14
  const colBaseSchema = {
16
15
  ...projectIdSchema,
17
16
  };
17
+ /** 类型守卫:从 unknown 中提取带 list 数组字段的对象。 */
18
+ function isRecordWithList(value) {
19
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
20
+ return false;
21
+ const list = value.list;
22
+ return Array.isArray(list);
23
+ }
18
24
  export function registerUtilCommands(registry) {
19
25
  registry.tool('url-parse', {
20
26
  url: z.string().url().describe('浏览器里的 YApi 页面 URL,必填。CLI 会优先用当前配置的 YApi 服务器地址做匹配,再解析其中的 projectId / interfaceId / catId / colId / caseId / groupId / uid。'),
@@ -123,7 +129,7 @@ export function registerUtilCommands(registry) {
123
129
  nextBestTools: ['user-search', 'project-list'],
124
130
  });
125
131
  registry.tool('user-search', {
126
- query: z.string().describe('搜索关键词,必填。通常按用户名或邮箱做模糊匹配,大小写与命中规则由服务端决定。'),
132
+ query: z.string().trim().min(1).describe('搜索关键词,必填。通常按用户名或邮箱做模糊匹配,大小写与命中规则由服务端决定。'),
127
133
  }, async (input) => {
128
134
  const { api } = createYApiServices();
129
135
  const result = await api.searchUsers(input.query);
@@ -142,20 +148,29 @@ export function registerUtilCommands(registry) {
142
148
  });
143
149
  registry.tool('export', {
144
150
  ...projectIdSchema,
145
- format: exportFormatEnum.describe('导出格式,可选枚举 json / swagger / openapi3,默认 json。不同格式返回的数据结构不同。'),
151
+ format: exportFormatEnum.describe('导出格式,可选枚举 json / swagger / openapi3 / html / markdown,默认 json。html/markdown 走标准 YApi 导出插件;json/swagger/openapi3 走 GTest 扩展导出接口。'),
152
+ status: z.string().optional().describe('按接口状态过滤,可选。仅在 html/markdown/json(插件导出)格式下生效,常用值 done / undone。'),
146
153
  }, async (input) => {
147
154
  const { api } = createYApiServices();
148
- const data = input.format === 'json'
149
- ? await api.exportProjectJson(input.projectId)
150
- : input.format === 'swagger'
151
- ? await api.exportProjectSwagger(input.projectId)
152
- : await api.exportProjectOpenAPI3(input.projectId);
155
+ let data;
156
+ if (input.format === 'html' || input.format === 'markdown') {
157
+ data = await api.exportProjectData(input.projectId, input.format, input.status);
158
+ }
159
+ else if (input.format === 'json') {
160
+ data = await api.exportProjectJson(input.projectId);
161
+ }
162
+ else if (input.format === 'swagger') {
163
+ data = await api.exportProjectSwagger(input.projectId);
164
+ }
165
+ else {
166
+ data = await api.exportProjectOpenAPI3(input.projectId);
167
+ }
153
168
  return jsonResult({
154
169
  projectId: input.projectId,
155
170
  format: input.format,
156
171
  data,
157
172
  });
158
- }, '导出项目数据(JSON/Swagger/OpenAPI3)', {
173
+ }, '导出项目数据(JSON/Swagger/OpenAPI3/HTML/Markdown)', {
159
174
  costHint: 'low',
160
175
  nextBestTools: ['interface-list', 'project-get'],
161
176
  });
@@ -176,7 +191,7 @@ export function registerUtilCommands(registry) {
176
191
  });
177
192
  registry.tool('col-cases', {
178
193
  ...projectIdSchema,
179
- colId: z.coerce.number().describe('集合 ID,必填。返回该集合下的用例摘要列表,不会自动展开完整请求体。'),
194
+ colId: z.number().int().positive().describe('集合 ID,必填。返回该集合下的用例摘要列表,不会自动展开完整请求体。'),
180
195
  }, async (input) => {
181
196
  if (Number.isNaN(input.colId)) {
182
197
  throw new CliValidationError('无效的集合 ID', '请使用 --col-id <number>');
@@ -198,96 +213,121 @@ export function registerUtilCommands(registry) {
198
213
  });
199
214
  registry.tool('col-create', {
200
215
  ...projectIdSchema,
201
- name: z.string().describe('集合名称,必填。建议按场景或环境命名,例如 回归冒烟-生产。'),
216
+ name: z.string().trim().min(1).describe('集合名称,必填。建议按场景或环境命名,例如 回归冒烟-生产。'),
202
217
  desc: z.string().optional().describe('集合描述,可选,纯文本展示,用于说明这组用例的用途。'),
203
- }, async (input) => {
204
- const { api } = createYApiServices();
205
- const result = await api.createCollection({
206
- project_id: input.projectId,
207
- name: input.name,
208
- desc: input.desc,
209
- });
210
- return jsonResult({
211
- action: 'create',
212
- projectId: input.projectId,
213
- colId: result.data._id,
214
- name: input.name,
218
+ ...confirmSchema,
219
+ }, async (input) => {
220
+ const payload = input;
221
+ return runWithPreview('col-create', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
222
+ const { api } = createYApiServices();
223
+ const result = await api.createCollection({
224
+ project_id: input.projectId,
225
+ name: input.name,
226
+ desc: input.desc,
227
+ });
228
+ return jsonResult({
229
+ action: 'create',
230
+ projectId: input.projectId,
231
+ colId: result.data._id,
232
+ name: input.name,
233
+ });
215
234
  });
216
235
  }, '测试集合管理:创建集合', {
217
236
  costHint: 'medium',
218
237
  nextBestTools: ['col-list', 'col-cases'],
219
238
  });
220
239
  registry.tool('col-delete', {
221
- colId: z.coerce.number().describe('集合 ID,必填。删除后该集合与集合内用例的关联会被清理。'),
240
+ colId: z.number().int().positive().describe('集合 ID,必填。删除后该集合与集合内用例的关联会被清理。'),
241
+ ...confirmSchema,
222
242
  }, async (input) => {
223
- if (Number.isNaN(input.colId)) {
224
- throw new CliValidationError('无效的集合 ID', '请使用 --col-id <number>');
225
- }
226
- const { api } = createYApiServices();
227
- await api.deleteCollection(input.colId);
228
- return jsonResult({
229
- action: 'delete',
230
- colId: input.colId,
231
- deleted: true,
243
+ const payload = input;
244
+ return runWithPreview('col-delete', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
245
+ if (Number.isNaN(input.colId)) {
246
+ throw new CliValidationError('无效的集合 ID', '请使用 --col-id <number>');
247
+ }
248
+ const { api } = createYApiServices();
249
+ await api.deleteCollection(input.colId);
250
+ return jsonResult({
251
+ action: 'delete',
252
+ colId: input.colId,
253
+ deleted: true,
254
+ });
232
255
  });
233
256
  }, '测试集合管理:删除集合', {
234
257
  costHint: 'medium',
235
258
  nextBestTools: ['col-list', 'col-cases'],
236
259
  });
237
260
  registry.tool('case-add', {
238
- colId: z.coerce.number().describe('集合 ID,必填。新用例会被添加到这个测试集合。'),
239
- method: z.string().optional().default('GET').describe('HTTP 方法,默认 GET,作为该测试用例保存的请求方法。'),
240
- url: z.string().optional().describe('请求路径,可选,保存到用例的 req_url 字段,通常填写接口相对路径或完整 URL。'),
241
- }, async (input) => {
242
- if (Number.isNaN(input.colId)) {
243
- throw new CliValidationError('无效的集合 ID', '请使用 --col-id <number>');
244
- }
245
- const { api } = createYApiServices();
246
- const result = await api.addTestCase({
247
- col_id: input.colId,
248
- req_method: input.method,
249
- req_url: input.url,
250
- });
251
- return jsonResult({
252
- action: 'add',
253
- colId: input.colId,
254
- caseId: result.data._id,
255
- method: input.method,
256
- url: input.url,
261
+ ...projectIdSchema,
262
+ colId: z.number().int().positive().describe('集合 ID,必填。新用例会被添加到这个测试集合。'),
263
+ interfaceId: z.number().int().positive().describe('接口 ID,必填。测试用例关联的接口,YApi 会复用该接口的请求定义。'),
264
+ name: z.string().trim().min(1).describe('用例名称,必填。'),
265
+ env: z.string().optional().describe('用例环境标识,可选。对应接口所在项目的环境 name。'),
266
+ ...confirmSchema,
267
+ }, async (input) => {
268
+ const payload = input;
269
+ return runWithPreview('case-add', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
270
+ if (Number.isNaN(input.colId)) {
271
+ throw new CliValidationError('无效的集合 ID', '请使用 --col-id <number>');
272
+ }
273
+ const { api } = createYApiServices();
274
+ const result = await api.addTestCase({
275
+ project_id: input.projectId,
276
+ col_id: input.colId,
277
+ interface_id: input.interfaceId,
278
+ casename: input.name,
279
+ case_env: input.env,
280
+ });
281
+ return jsonResult({
282
+ action: 'add',
283
+ projectId: input.projectId,
284
+ colId: input.colId,
285
+ interfaceId: input.interfaceId,
286
+ caseId: result.data._id,
287
+ name: input.name,
288
+ });
257
289
  });
258
290
  }, '测试用例管理:添加用例', {
259
291
  costHint: 'medium',
260
292
  nextBestTools: ['col-cases', 'case-run'],
261
293
  });
262
294
  registry.tool('case-delete', {
263
- caseId: z.coerce.number().describe('用例 ID,必填。仅删除单条测试用例,不会删除所属集合。'),
295
+ caseId: z.number().int().positive().describe('用例 ID,必填。仅删除单条测试用例,不会删除所属集合。'),
296
+ ...confirmSchema,
264
297
  }, async (input) => {
265
- if (Number.isNaN(input.caseId)) {
266
- throw new CliValidationError('无效的用例 ID', '请使用 --case-id <number>');
267
- }
268
- const { api } = createYApiServices();
269
- await api.deleteTestCase(input.caseId);
270
- return jsonResult({
271
- action: 'delete',
272
- caseId: input.caseId,
273
- deleted: true,
298
+ const payload = input;
299
+ return runWithPreview('case-delete', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
300
+ if (Number.isNaN(input.caseId)) {
301
+ throw new CliValidationError('无效的用例 ID', '请使用 --case-id <number>');
302
+ }
303
+ const { api } = createYApiServices();
304
+ await api.deleteTestCase(input.caseId);
305
+ return jsonResult({
306
+ action: 'delete',
307
+ caseId: input.caseId,
308
+ deleted: true,
309
+ });
274
310
  });
275
311
  }, '测试用例管理:删除用例', {
276
312
  costHint: 'medium',
277
313
  nextBestTools: ['col-cases', 'case-run'],
278
314
  });
279
315
  registry.tool('case-run', {
280
- colId: z.coerce.number().describe('集合 ID,必填。会执行该集合绑定的测试脚本或用例。'),
316
+ colId: z.number().int().positive().describe('集合 ID,必填。会执行该集合绑定的测试脚本或用例。'),
317
+ ...confirmSchema,
281
318
  }, async (input) => {
282
- if (Number.isNaN(input.colId)) {
283
- throw new CliValidationError('无效的集合 ID', '请使用 --col-id <number>');
284
- }
285
- const { api } = createYApiServices();
286
- const result = await api.runTestCaseScript({ col_id: input.colId });
287
- return jsonResult({
288
- action: 'run',
289
- colId: input.colId,
290
- result: result.data,
319
+ const payload = input;
320
+ return runWithPreview('case-run', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
321
+ if (Number.isNaN(input.colId)) {
322
+ throw new CliValidationError('无效的集合 ID', '请使用 --col-id <number>');
323
+ }
324
+ const { api } = createYApiServices();
325
+ const result = await api.runTestCaseScript({ col_id: input.colId });
326
+ return jsonResult({
327
+ action: 'run',
328
+ colId: input.colId,
329
+ result: result.data,
330
+ });
291
331
  });
292
332
  }, '测试用例管理:运行集合脚本', {
293
333
  costHint: 'high',
@@ -296,6 +336,8 @@ export function registerUtilCommands(registry) {
296
336
  registry.tool('self-update', {
297
337
  channel: z.enum(['latest', 'next']).optional().default('latest').describe('npm 发布渠道'),
298
338
  }, async (input) => {
339
+ // self-update 只查询 npm registry 最新版本号,不执行任何写入,
340
+ // 因此不走写保护(confirmSchema / runWithPreview),避免误导调用方确认。
299
341
  const currentVersion = CLI_VERSION;
300
342
  const packageName = '@cloudglab/yapi-cli';
301
343
  const url = `https://registry.npmjs.org/${packageName}/${input.channel}`;
@@ -324,77 +366,79 @@ export function registerUtilCommands(registry) {
324
366
  nextBestTools: ['install', 'version'],
325
367
  });
326
368
  registry.tool('docs-sync', {
327
- file: z.string().describe('Markdown 文档路径'),
328
- projectId: z.coerce.number().describe('目标项目 ID'),
329
- catId: z.coerce.number().describe('目标分类(目录)ID'),
369
+ file: z.string().trim().min(1).describe('Markdown 文档路径'),
370
+ projectId: z.number().int().positive().describe('目标项目 ID'),
371
+ catId: z.number().int().positive().describe('目标分类(目录)ID'),
330
372
  merge: z.enum(['create', 'update', 'skip']).default('create').describe('已有接口处理策略'),
331
373
  watch: optionalBool.describe('监听文件变更'),
332
- }, async (input) => {
333
- const { api } = createYApiServices();
334
- if (input.watch) {
335
- process.stdout.write(`开始监听 ${input.file} ...\n`);
336
- await watchAndSync(api, input.projectId, input.catId, input.file, (result) => {
337
- process.stdout.write(`\n[${new Date().toLocaleTimeString()}] 同步完成:\n`);
338
- process.stdout.write(`${formatSyncResult(result)}\n`);
339
- });
340
- const first = await syncDocsToProject(api, input.projectId, input.catId, input.file, input.merge);
341
- process.stdout.write(`首次同步完成:\n${formatSyncResult(first)}\n`);
374
+ ...confirmSchema,
375
+ }, async (input) => {
376
+ const payload = input;
377
+ return runWithPreview('docs-sync', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
378
+ const { api } = createYApiServices();
379
+ if (input.watch) {
380
+ process.stdout.write(`开始监听 ${input.file} ...\n`);
381
+ const stopWatching = await watchAndSync(api, input.projectId, input.catId, input.file, (result) => {
382
+ process.stdout.write(`\n[${new Date().toLocaleTimeString()}] 同步完成:\n`);
383
+ process.stdout.write(`${formatSyncResult(result)}\n`);
384
+ });
385
+ const first = await syncDocsToProject(api, input.projectId, input.catId, input.file, input.merge);
386
+ process.stdout.write(`首次同步完成:\n${formatSyncResult(first)}\n`);
387
+ // 保存 disposer:Ctrl+C 退出时清理 watcher,避免文件描述符泄漏
388
+ process.on('SIGINT', () => {
389
+ try {
390
+ stopWatching();
391
+ }
392
+ catch {
393
+ // 忽略清理错误
394
+ }
395
+ process.exit(0);
396
+ });
397
+ return jsonResult({
398
+ watching: true,
399
+ message: `正在监听 ${input.file},按 Ctrl+C 停止`,
400
+ syncResult: first,
401
+ });
402
+ }
403
+ const result = await syncDocsToProject(api, input.projectId, input.catId, input.file, input.merge);
342
404
  return jsonResult({
343
- watching: true,
344
- message: `正在监听 ${input.file},按 Ctrl+C 停止`,
345
- syncResult: first,
405
+ details: result,
406
+ message: formatSyncResult(result),
346
407
  });
347
- }
348
- const result = await syncDocsToProject(api, input.projectId, input.catId, input.file, input.merge);
349
- return jsonResult({
350
- details: result,
351
- message: formatSyncResult(result),
352
408
  });
353
409
  }, '同步 Markdown 文档到 YApi 项目', {
354
410
  costHint: 'medium',
355
411
  nextBestTools: ['self-update', 'interface-list'],
356
412
  });
357
413
  registry.tool('install-skill', {
358
- skillPath: z.string().optional().describe('自定义 skill 文件路径'),
414
+ skillPath: z.string().optional().describe('自定义本地 skill 目录路径,省略则从已安装包安装'),
359
415
  yes: z.coerce.boolean().optional().default(false).describe('覆盖已有配置'),
360
- }, async (input) => {
361
- const moduleDir = dirname(fileURLToPath(import.meta.url));
362
- const opencodeAgentDir = join(process.env.HOME || '', '.opencode', 'agent');
363
- if (input.skillPath) {
364
- const target = join(opencodeAgentDir, 'yapi-cli.md');
365
- if (!existsSync(dirname(target))) {
366
- mkdirSync(dirname(target), { recursive: true });
367
- }
368
- copyFileSync(input.skillPath, target);
369
- return jsonResult({ message: `Skill 已安装到: ${target}` });
370
- }
371
- const possiblePaths = [
372
- join(moduleDir, '..', '..', '..', 'skills', 'yapi-cli', 'SKILL.md'),
373
- join(moduleDir, '..', '..', 'skills', 'yapi-cli', 'SKILL.md'),
374
- ];
375
- for (const skillPath of possiblePaths) {
376
- try {
377
- if (existsSync(skillPath)) {
378
- const target = join(opencodeAgentDir, 'yapi-cli.md');
379
- if (!existsSync(dirname(target))) {
380
- mkdirSync(dirname(target), { recursive: true });
381
- }
382
- copyFileSync(skillPath, target);
383
- return jsonResult({ message: `Skill 已安装到: ${target}` });
384
- }
385
- }
386
- catch {
387
- continue;
388
- }
389
- }
390
- throw new CliValidationError('未找到内置 skill 文件。可通过 --skillPath 指定路径');
416
+ ...confirmSchema,
417
+ }, async (input) => {
418
+ const payload = input;
419
+ return runWithPreview('install-skill', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
420
+ const options = {
421
+ cliOnly: false,
422
+ skillOnly: true,
423
+ skillSource: 'local',
424
+ skillLocalPath: input.skillPath,
425
+ yes: input.yes,
426
+ keepConfig: false,
427
+ };
428
+ await installSkill('安装', options);
429
+ return jsonResult({
430
+ message: input.skillPath
431
+ ? `已从 ${input.skillPath} 安装 yapi skill 到全局`
432
+ : '已安装 yapi skill 到全局',
433
+ });
434
+ });
391
435
  }, '安装 OpenCode skill 文件', {
392
436
  costHint: 'medium',
393
437
  nextBestTools: ['self-update', 'config-init'],
394
438
  });
395
439
  // ==================== 用户详情 ====================
396
440
  registry.tool('user-get', {
397
- uid: z.coerce.number().describe('用户 UID'),
441
+ uid: z.number().int().positive().describe('用户 UID'),
398
442
  }, async (input) => {
399
443
  const { api } = createYApiServices();
400
444
  const result = await api.findUserById(input.uid);
@@ -409,20 +453,24 @@ export function registerUtilCommands(registry) {
409
453
  });
410
454
  // ==================== 修改密码 ====================
411
455
  registry.tool('user-change-password', {
412
- oldPassword: z.string().describe('旧密码'),
413
- newPassword: z.string().describe('新密码'),
414
- }, async (input) => {
415
- if (input.oldPassword.length === 0 || input.newPassword.length === 0) {
416
- throw new CliValidationError('旧密码和新密码不能为空', '请同时提供 --old-password 和 --new-password');
417
- }
418
- const { api } = createYApiServices();
419
- await api.changePassword({
420
- old_password: input.oldPassword,
421
- new_password: input.newPassword,
422
- });
423
- return jsonResult({
424
- action: 'change-password',
425
- message: '密码已更新',
456
+ oldPassword: z.string().trim().min(1).describe('旧密码'),
457
+ newPassword: z.string().trim().min(1).describe('新密码'),
458
+ ...confirmSchema,
459
+ }, async (input) => {
460
+ const payload = input;
461
+ return runWithPreview('user-change-password', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
462
+ if (input.oldPassword.length === 0 || input.newPassword.length === 0) {
463
+ throw new CliValidationError('旧密码和新密码不能为空', '请同时提供 --old-password 和 --new-password');
464
+ }
465
+ const { api } = createYApiServices();
466
+ await api.changePassword({
467
+ old_password: input.oldPassword,
468
+ new_password: input.newPassword,
469
+ });
470
+ return jsonResult({
471
+ action: 'change-password',
472
+ message: '密码已更新',
473
+ });
426
474
  });
427
475
  }, '用户管理:修改当前用户密码', {
428
476
  costHint: 'medium',
@@ -430,37 +478,41 @@ export function registerUtilCommands(registry) {
430
478
  });
431
479
  // ==================== 用户注册 ====================
432
480
  registry.tool('user-register', {
433
- username: z.string().describe('用户名'),
434
- password: z.string().describe('密码'),
481
+ username: z.string().trim().min(1).describe('用户名'),
482
+ password: z.string().trim().min(1).describe('密码'),
435
483
  email: z.string().email().describe('邮箱'),
436
484
  role: z
437
485
  .enum(['admin', 'owner', 'dev', 'guest', 'viewer'])
438
486
  .optional()
439
487
  .describe('角色(admin/owner/dev/guest/viewer)'),
488
+ ...confirmSchema,
440
489
  }, async (input) => {
441
- if (input.username.length === 0) {
442
- throw new CliValidationError('用户名不能为空', '请提供 --username');
443
- }
444
- if (input.password.length === 0) {
445
- throw new CliValidationError('密码不能为空', '请提供 --password');
446
- }
447
- const { api } = createYApiServices();
448
- const result = await api.userRegister({
449
- username: input.username,
450
- password: input.password,
451
- email: input.email,
452
- role: input.role,
453
- });
454
- if (result.errcode !== 0) {
455
- throw new CliError(result.errmsg ?? '用户注册失败', 14, 'API_ERROR');
456
- }
457
- return jsonResult({
458
- action: 'user-register',
459
- userId: result.data?._id,
460
- username: input.username,
461
- email: input.email,
462
- role: input.role,
463
- message: `用户 ${input.username} 注册成功`,
490
+ const payload = input;
491
+ return runWithPreview('user-register', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
492
+ if (input.username.length === 0) {
493
+ throw new CliValidationError('用户名不能为空', '请提供 --username');
494
+ }
495
+ if (input.password.length === 0) {
496
+ throw new CliValidationError('密码不能为空', '请提供 --password');
497
+ }
498
+ const { api } = createYApiServices();
499
+ const result = await api.userRegister({
500
+ username: input.username,
501
+ password: input.password,
502
+ email: input.email,
503
+ role: input.role,
504
+ });
505
+ if (result.errcode !== 0) {
506
+ throw new CliError(result.errmsg ?? '用户注册失败', 14, 'API_ERROR');
507
+ }
508
+ return jsonResult({
509
+ action: 'user-register',
510
+ userId: result.data?._id,
511
+ username: input.username,
512
+ email: input.email,
513
+ role: input.role,
514
+ message: `用户 ${input.username} 注册成功`,
515
+ });
464
516
  });
465
517
  }, '用户管理:注册新用户(需要管理员权限)', {
466
518
  costHint: 'medium',
@@ -468,14 +520,18 @@ export function registerUtilCommands(registry) {
468
520
  });
469
521
  // ==================== 用户删除 ====================
470
522
  registry.tool('user-delete', {
471
- uid: z.coerce.number().describe('用户 UID'),
523
+ uid: z.number().int().positive().describe('用户 UID'),
524
+ ...confirmSchema,
472
525
  }, async (input) => {
473
- const { api } = createYApiServices();
474
- await api.userDelete(input.uid);
475
- return jsonResult({
476
- action: 'user-delete',
477
- uid: input.uid,
478
- message: `用户 ${input.uid} 已删除`,
526
+ const payload = input;
527
+ return runWithPreview('user-delete', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
528
+ const { api } = createYApiServices();
529
+ await api.userDelete(input.uid);
530
+ return jsonResult({
531
+ action: 'user-delete',
532
+ uid: input.uid,
533
+ message: `用户 ${input.uid} 已删除`,
534
+ });
479
535
  });
480
536
  }, '用户管理:删除用户(需要管理员权限,不可恢复)', {
481
537
  costHint: 'medium',
@@ -483,20 +539,24 @@ export function registerUtilCommands(registry) {
483
539
  });
484
540
  // ==================== Token 登录 ====================
485
541
  registry.tool('login-by-token', {
486
- token: z.string().describe('YApi 用户 token,必填。用于直接校验 token 是否有效,并按当前实现写入本地认证上下文。'),
542
+ token: z.string().trim().min(1).describe('YApi 用户 token,必填。用于直接校验 token 是否有效,并按当前实现写入本地认证上下文。'),
543
+ ...confirmSchema,
487
544
  }, async (input) => {
488
- if (input.token.length === 0) {
489
- throw new CliValidationError('token 不能为空', '请提供 --token');
490
- }
491
- const { api } = createYApiServices();
492
- const result = await api.loginByToken(input.token);
493
- if (result.errcode !== 0) {
494
- throw new CliError(result.errmsg ?? 'token 登录失败', 14, 'API_ERROR');
495
- }
496
- return jsonResult({
497
- action: 'login-by-token',
498
- user: result.data,
499
- message: `token 验证通过:${result.data?.username ?? ''}`,
545
+ const payload = input;
546
+ return runWithPreview('login-by-token', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
547
+ if (input.token.length === 0) {
548
+ throw new CliValidationError('token 不能为空', '请提供 --token');
549
+ }
550
+ const { api } = createYApiServices();
551
+ const result = await api.loginByToken(input.token);
552
+ if (result.errcode !== 0) {
553
+ throw new CliError(result.errmsg ?? 'token 登录失败', 14, 'API_ERROR');
554
+ }
555
+ return jsonResult({
556
+ action: 'login-by-token',
557
+ user: result.data,
558
+ message: `token 验证通过:${result.data?.username ?? ''}`,
559
+ });
500
560
  });
501
561
  }, '用户管理:使用 token 静默登录(校验 token 有效性)', {
502
562
  costHint: 'low',
@@ -504,7 +564,7 @@ export function registerUtilCommands(registry) {
504
564
  });
505
565
  // ==================== 用户头像 ====================
506
566
  registry.tool('user-avatar', {
507
- uid: z.coerce.number().describe('用户 UID,必填。返回该用户当前头像地址或头像资源信息。'),
567
+ uid: z.number().int().positive().describe('用户 UID,必填。返回该用户当前头像地址或头像资源信息。'),
508
568
  }, async (input) => {
509
569
  const { api } = createYApiServices();
510
570
  const result = await api.getUserAvatar(input.uid);
@@ -519,24 +579,28 @@ export function registerUtilCommands(registry) {
519
579
  });
520
580
  // ==================== 上传头像 ====================
521
581
  registry.tool('user-upload-avatar', {
522
- uid: z.coerce.number().describe('用户 UID,必填。通常只能更新当前用户或管理员可见用户。'),
523
- base64: z.string().describe('图片 base64 编码,必填,不含 data:image/...;base64, 前缀。建议传纯图片内容,避免额外前缀导致服务端解析失败。'),
524
- }, async (input) => {
525
- if (input.base64.length === 0) {
526
- throw new CliValidationError('base64 不能为空', '请提供 --base64');
527
- }
528
- const { api } = createYApiServices();
529
- const result = await api.uploadUserAvatar({
530
- uid: input.uid,
531
- base64: input.base64,
532
- });
533
- if (result.errcode !== 0) {
534
- throw new CliError(result.errmsg ?? '上传头像失败', 14, 'API_ERROR');
535
- }
536
- return jsonResult({
537
- action: 'user-upload-avatar',
538
- uid: input.uid,
539
- message: '头像上传成功',
582
+ uid: z.number().int().positive().describe('用户 UID,必填。通常只能更新当前用户或管理员可见用户。'),
583
+ base64: z.string().trim().min(1).describe('图片 base64 编码,必填,不含 data:image/...;base64, 前缀。建议传纯图片内容,避免额外前缀导致服务端解析失败。'),
584
+ ...confirmSchema,
585
+ }, async (input) => {
586
+ const payload = input;
587
+ return runWithPreview('user-upload-avatar', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
588
+ if (input.base64.length === 0) {
589
+ throw new CliValidationError('base64 不能为空', '请提供 --base64');
590
+ }
591
+ const { api } = createYApiServices();
592
+ const result = await api.uploadUserAvatar({
593
+ uid: input.uid,
594
+ basecode: input.base64,
595
+ });
596
+ if (result.errcode !== 0) {
597
+ throw new CliError(result.errmsg ?? '上传头像失败', 14, 'API_ERROR');
598
+ }
599
+ return jsonResult({
600
+ action: 'user-upload-avatar',
601
+ uid: input.uid,
602
+ message: '头像上传成功',
603
+ });
540
604
  });
541
605
  }, '用户管理:上传用户头像', {
542
606
  costHint: 'medium',
@@ -546,9 +610,7 @@ export function registerUtilCommands(registry) {
546
610
  registry.tool('user-projects', {}, async () => {
547
611
  const { api } = createYApiServices();
548
612
  const result = await api.listUserProjects();
549
- const projects = Array.isArray(result.data)
550
- ? result.data
551
- : result.data?.list ?? [];
613
+ const projects = Array.isArray(result.data) ? result.data : [];
552
614
  return jsonResult({
553
615
  action: 'user-projects',
554
616
  total: projects.length,
@@ -566,19 +628,19 @@ export function registerUtilCommands(registry) {
566
628
  });
567
629
  // ==================== 动态日志(按更新时间) ====================
568
630
  registry.tool('log-list-by-update', {
569
- apid: z.coerce.number().describe('接口 ID,必填。用于定位要查询日志的目标接口。'),
631
+ apid: z.number().int().positive().describe('接口 ID,必填。用于定位要查询日志的目标接口。'),
570
632
  type: z.string().optional().default('interface').describe('日志类型,默认 interface。若服务端支持其他类型,可按后端要求传值。'),
571
- limit: z.coerce.number().optional().describe('返回条数限制,可选。值越大返回的日志越多,但结构化输出也会更长。'),
633
+ limit: z.number().int().positive().optional().describe('返回条数限制,可选。值越大返回的日志越多,但结构化输出也会更长。'),
572
634
  }, async (input) => {
573
635
  const { api } = createYApiServices();
574
636
  const result = await api.listLogsByUpdate({
575
637
  type: input.type,
576
- apid: input.apid,
638
+ typeid: input.apid,
577
639
  limit: input.limit,
578
640
  });
579
641
  return jsonResult({
580
642
  action: 'logs-by-update',
581
- apid: input.apid,
643
+ typeid: input.apid,
582
644
  type: input.type,
583
645
  logs: result.data,
584
646
  });
@@ -589,7 +651,7 @@ export function registerUtilCommands(registry) {
589
651
  // ==================== 用例列表(按变量参数过滤) ====================
590
652
  registry.tool('col-case-list-by-var-params', {
591
653
  ...projectIdSchema,
592
- colId: z.coerce.number().describe('集合 ID'),
654
+ colId: z.number().int().positive().describe('集合 ID'),
593
655
  }, async (input) => {
594
656
  const { api } = createYApiServices();
595
657
  const result = await api.getCaseListByVariableParams(input.colId);
@@ -605,23 +667,27 @@ export function registerUtilCommands(registry) {
605
667
  });
606
668
  // ==================== 用例排序 ====================
607
669
  registry.tool('col-case-up-index', {
608
- colId: z.coerce.number().describe('集合 ID'),
609
- ids: z.string().describe('用例 ID 列表(逗号分隔,例如: 1,2,3)'),
610
- }, async (input) => {
611
- const parsed = input.ids
612
- .split(',')
613
- .map((s) => s.trim())
614
- .filter((s) => s.length > 0)
615
- .map((s) => Number(s));
616
- if (parsed.length === 0 || parsed.some((n) => Number.isNaN(n))) {
617
- throw new CliValidationError('ids 必须是合法的逗号分隔数字', '请提供 --ids 例如: --ids 1,2,3');
618
- }
619
- const { api } = createYApiServices();
620
- await api.upCaseIndex({ col_id: input.colId, ids: parsed });
621
- return jsonResult({
622
- action: 'case-up-index',
623
- colId: input.colId,
624
- ids: parsed,
670
+ colId: z.number().int().positive().describe('集合 ID'),
671
+ ids: z.string().trim().min(1).describe('用例 ID 列表(逗号分隔,例如: 1,2,3)'),
672
+ ...confirmSchema,
673
+ }, async (input) => {
674
+ const payload = input;
675
+ return runWithPreview('col-case-up-index', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
676
+ const parsed = input.ids
677
+ .split(',')
678
+ .map((s) => s.trim())
679
+ .filter((s) => s.length > 0)
680
+ .map((s) => Number(s));
681
+ if (parsed.length === 0 || parsed.some((n) => Number.isNaN(n))) {
682
+ throw new CliValidationError('ids 必须是合法的逗号分隔数字', '请提供 --ids 例如: --ids 1,2,3');
683
+ }
684
+ const { api } = createYApiServices();
685
+ await api.upCaseIndex({ col_id: input.colId, ids: parsed });
686
+ return jsonResult({
687
+ action: 'case-up-index',
688
+ colId: input.colId,
689
+ ids: parsed,
690
+ });
625
691
  });
626
692
  }, '测试用例:调整用例排序', {
627
693
  costHint: 'medium',
@@ -630,22 +696,26 @@ export function registerUtilCommands(registry) {
630
696
  // ==================== 集合排序 ====================
631
697
  registry.tool('col-up-index', {
632
698
  ...projectIdSchema,
633
- ids: z.string().describe('集合 ID 列表(逗号分隔,例如: 1,2,3)'),
634
- }, async (input) => {
635
- const parsed = input.ids
636
- .split(',')
637
- .map((s) => s.trim())
638
- .filter((s) => s.length > 0)
639
- .map((s) => Number(s));
640
- if (parsed.length === 0 || parsed.some((n) => Number.isNaN(n))) {
641
- throw new CliValidationError('ids 必须是合法的逗号分隔数字', '请提供 --ids 例如: --ids 1,2,3');
642
- }
643
- const { api } = createYApiServices();
644
- await api.upColIndex({ project_id: input.projectId, ids: parsed });
645
- return jsonResult({
646
- action: 'col-up-index',
647
- projectId: input.projectId,
648
- ids: parsed,
699
+ ids: z.string().trim().min(1).describe('集合 ID 列表(逗号分隔,例如: 1,2,3)'),
700
+ ...confirmSchema,
701
+ }, async (input) => {
702
+ const payload = input;
703
+ return runWithPreview('col-up-index', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
704
+ const parsed = input.ids
705
+ .split(',')
706
+ .map((s) => s.trim())
707
+ .filter((s) => s.length > 0)
708
+ .map((s) => Number(s));
709
+ if (parsed.length === 0 || parsed.some((n) => Number.isNaN(n))) {
710
+ throw new CliValidationError('ids 必须是合法的逗号分隔数字', '请提供 --ids 例如: --ids 1,2,3');
711
+ }
712
+ const { api } = createYApiServices();
713
+ await api.upColIndex({ project_id: input.projectId, ids: parsed });
714
+ return jsonResult({
715
+ action: 'col-up-index',
716
+ projectId: input.projectId,
717
+ ids: parsed,
718
+ });
649
719
  });
650
720
  }, '测试集合:调整集合排序', {
651
721
  costHint: 'medium',
@@ -653,22 +723,26 @@ export function registerUtilCommands(registry) {
653
723
  });
654
724
  // ==================== 更新集合 ====================
655
725
  registry.tool('col-update', {
656
- colId: z.coerce.number().describe('集合 ID,必填。'),
726
+ colId: z.number().int().positive().describe('集合 ID,必填。'),
657
727
  name: z.string().optional().describe('新名称,可选。name 与 desc 至少需要传一个。'),
658
728
  desc: z.string().optional().describe('新描述,可选。name 与 desc 至少需要传一个。'),
729
+ ...confirmSchema,
659
730
  }, async (input) => {
660
- if (input.name === undefined && input.desc === undefined) {
661
- throw new CliValidationError('至少需要提供 name desc 中的一个');
662
- }
663
- const { api } = createYApiServices();
664
- await api.updateCollection({
665
- id: input.colId,
666
- name: input.name,
667
- desc: input.desc,
668
- });
669
- return jsonResult({
670
- colId: input.colId,
671
- message: `集合 ${input.colId} 已更新`,
731
+ const payload = input;
732
+ return runWithPreview('col-update', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
733
+ if (input.name === undefined && input.desc === undefined) {
734
+ throw new CliValidationError('至少需要提供 name desc 中的一个');
735
+ }
736
+ const { api } = createYApiServices();
737
+ await api.updateCollection({
738
+ col_id: input.colId,
739
+ name: input.name,
740
+ desc: input.desc,
741
+ });
742
+ return jsonResult({
743
+ colId: input.colId,
744
+ message: `集合 ${input.colId} 已更新`,
745
+ });
672
746
  });
673
747
  }, '测试集合:更新集合信息', {
674
748
  costHint: 'medium',
@@ -676,7 +750,7 @@ export function registerUtilCommands(registry) {
676
750
  });
677
751
  // ==================== 用例详情 ====================
678
752
  registry.tool('case-get', {
679
- caseId: z.coerce.number().describe('用例 ID,必填。返回该用例保存的请求方法、URL、脚本等完整信息。'),
753
+ caseId: z.number().int().positive().describe('用例 ID,必填。返回该用例保存的请求方法、URL、脚本等完整信息。'),
680
754
  }, async (input) => {
681
755
  if (Number.isNaN(input.caseId)) {
682
756
  throw new CliValidationError('无效的用例 ID', '请使用 --case-id <number>');
@@ -693,24 +767,28 @@ export function registerUtilCommands(registry) {
693
767
  });
694
768
  // ==================== 更新用例 ====================
695
769
  registry.tool('case-update', {
696
- caseId: z.coerce.number().describe('用例 ID,必填。'),
770
+ caseId: z.number().int().positive().describe('用例 ID,必填。'),
697
771
  caseType: z.string().optional().describe('用例类型,可选,如 col/test。具体取值由服务端字段约束决定。'),
698
772
  method: z.string().optional().describe('HTTP 方法,可选。仅更新该用例的 req_method 字段。'),
699
773
  url: z.string().optional().describe('请求路径,可选。仅更新该用例的 req_url 字段。'),
774
+ ...confirmSchema,
700
775
  }, async (input) => {
701
- if (Number.isNaN(input.caseId)) {
702
- throw new CliValidationError('无效的用例 ID', '请使用 --case-id <number>');
703
- }
704
- const { api } = createYApiServices();
705
- await api.updateTestCase({
706
- id: input.caseId,
707
- case_type: input.caseType,
708
- req_method: input.method,
709
- req_url: input.url,
710
- });
711
- return jsonResult({
712
- caseId: input.caseId,
713
- message: `用例 ${input.caseId} 已更新`,
776
+ const payload = input;
777
+ return runWithPreview('case-update', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
778
+ if (Number.isNaN(input.caseId)) {
779
+ throw new CliValidationError('无效的用例 ID', '请使用 --case-id <number>');
780
+ }
781
+ const { api } = createYApiServices();
782
+ await api.updateTestCase({
783
+ id: input.caseId,
784
+ case_type: input.caseType,
785
+ req_method: input.method,
786
+ req_url: input.url,
787
+ });
788
+ return jsonResult({
789
+ caseId: input.caseId,
790
+ message: `用例 ${input.caseId} 已更新`,
791
+ });
714
792
  });
715
793
  }, '测试用例:更新用例信息', {
716
794
  costHint: 'medium',
@@ -718,32 +796,36 @@ export function registerUtilCommands(registry) {
718
796
  });
719
797
  // ==================== 批量添加用例 ====================
720
798
  registry.tool('case-add-list', {
721
- colId: z.coerce.number().describe('集合 ID,必填。批量添加的所有用例都会进入该集合。'),
799
+ colId: z.number().int().positive().describe('集合 ID,必填。批量添加的所有用例都会进入该集合。'),
722
800
  ...projectIdSchema,
723
- caseList: z.string().describe('用例数组 JSON 字符串,必填,例如: [{"req_method":"GET","req_url":"/api/demo"}]。CLI 会先校验它是 JSON 数组,再整体提交给服务端。'),
801
+ caseList: z.string().trim().min(1).describe('用例数组 JSON 字符串,必填,例如: [{"req_method":"GET","req_url":"/api/demo"}]。CLI 会先校验它是 JSON 数组,再整体提交给服务端。'),
802
+ ...confirmSchema,
724
803
  }, async (input) => {
725
- let caseList;
726
- try {
727
- const parsed = JSON.parse(input.caseList);
728
- if (!Array.isArray(parsed)) {
729
- throw new Error('caseList 必须是数组');
804
+ const payload = input;
805
+ return runWithPreview('case-add-list', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
806
+ let caseList;
807
+ try {
808
+ const parsed = JSON.parse(input.caseList);
809
+ if (!Array.isArray(parsed)) {
810
+ throw new Error('caseList 必须是数组');
811
+ }
812
+ caseList = parsed;
730
813
  }
731
- caseList = parsed;
732
- }
733
- catch (err) {
734
- throw new CliValidationError(`caseList 解析失败: ${err instanceof Error ? err.message : String(err)}`, '请提供合法的 JSON 数组,例如: --case-list \'[{"req_method":"GET","req_url":"/api/demo"}]\'');
735
- }
736
- const { api } = createYApiServices();
737
- const result = await api.addTestCaseList({
738
- col_id: input.colId,
739
- project_id: input.projectId,
740
- case_list: caseList,
741
- });
742
- return jsonResult({
743
- colId: input.colId,
744
- projectId: input.projectId,
745
- count: result.data?.count ?? caseList.length,
746
- message: `已批量添加 ${caseList.length} 个用例`,
814
+ catch (err) {
815
+ throw new CliValidationError(`caseList 解析失败: ${err instanceof Error ? err.message : String(err)}`, '请提供合法的 JSON 数组,例如: --case-list \'[{"req_method":"GET","req_url":"/api/demo"}]\'');
816
+ }
817
+ const { api } = createYApiServices();
818
+ const result = await api.addTestCaseList({
819
+ col_id: input.colId,
820
+ project_id: input.projectId,
821
+ case_list: caseList,
822
+ });
823
+ return jsonResult({
824
+ colId: input.colId,
825
+ projectId: input.projectId,
826
+ count: result.data?.count ?? caseList.length,
827
+ message: `已批量添加 ${caseList.length} 个用例`,
828
+ });
747
829
  });
748
830
  }, '测试用例:批量添加用例到集合', {
749
831
  costHint: 'medium',
@@ -751,22 +833,26 @@ export function registerUtilCommands(registry) {
751
833
  });
752
834
  // ==================== 克隆用例列表 ====================
753
835
  registry.tool('col-case-clone', {
754
- colId: z.coerce.number().describe('目标集合 ID,必填。克隆出的用例会写入这个集合。'),
836
+ colId: z.number().int().positive().describe('目标集合 ID,必填。克隆出的用例会写入这个集合。'),
755
837
  ...projectIdSchema,
756
- srcColId: z.coerce.number().describe('源集合 ID,必填。会从这个集合复制现有用例。'),
757
- }, async (input) => {
758
- const { api } = createYApiServices();
759
- const result = await api.cloneTestCaseList({
760
- col_id: input.colId,
761
- project_id: input.projectId,
762
- src_col_id: input.srcColId,
763
- });
764
- return jsonResult({
765
- srcColId: input.srcColId,
766
- colId: input.colId,
767
- projectId: input.projectId,
768
- count: result.data?.count ?? 0,
769
- message: `已从集合 ${input.srcColId} 克隆用例到 ${input.colId}`,
838
+ srcColId: z.number().int().positive().describe('源集合 ID,必填。会从这个集合复制现有用例。'),
839
+ ...confirmSchema,
840
+ }, async (input) => {
841
+ const payload = input;
842
+ return runWithPreview('col-case-clone', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
843
+ const { api } = createYApiServices();
844
+ const result = await api.cloneTestCaseList({
845
+ col_id: input.colId,
846
+ project_id: input.projectId,
847
+ src_col_id: input.srcColId,
848
+ });
849
+ return jsonResult({
850
+ srcColId: input.srcColId,
851
+ colId: input.colId,
852
+ projectId: input.projectId,
853
+ count: result.data?.count ?? 0,
854
+ message: `已从集合 ${input.srcColId} 克隆用例到 ${input.colId}`,
855
+ });
770
856
  });
771
857
  }, '测试集合:从另一个集合克隆用例列表', {
772
858
  costHint: 'medium',
@@ -774,7 +860,7 @@ export function registerUtilCommands(registry) {
774
860
  });
775
861
  // ==================== 用例环境变量列表 ====================
776
862
  registry.tool('case-env-list', {
777
- caseId: z.coerce.number().describe('用例 ID,必填。返回该用例关联的环境变量列表。'),
863
+ caseId: z.number().int().positive().describe('用例 ID,必填。返回该用例关联的环境变量列表。'),
778
864
  }, async (input) => {
779
865
  const { api } = createYApiServices();
780
866
  const result = await api.getTestCaseEnvList(input.caseId);
@@ -789,33 +875,37 @@ export function registerUtilCommands(registry) {
789
875
  // ==================== 导入数据 ====================
790
876
  registry.tool('import', {
791
877
  ...projectIdSchema,
792
- type: z.string().describe('导入类型,必填,如 swagger/json/har/postman。需要与 content 的真实格式一致。'),
793
- content: z.string().describe('导入内容,必填,JSON/Swagger 字符串。CLI 不做格式解析,直接交给服务端按 type 处理。'),
878
+ type: z.string().trim().min(1).describe('导入类型,必填,如 swagger/json/har/postman。需要与 content 的真实格式一致。'),
879
+ content: z.string().trim().min(1).describe('导入内容,必填,JSON/Swagger 字符串。CLI 不做格式解析,直接交给服务端按 type 处理。'),
794
880
  merge: z
795
881
  .enum(['normal', 'good', 'merge'])
796
882
  .optional()
797
883
  .default('normal')
798
884
  .describe('同名接口合并策略,可选枚举 normal/good/merge,默认 normal。是否真正覆盖或合并由服务端实现决定。'),
885
+ ...confirmSchema,
799
886
  }, async (input) => {
800
- if (input.content.trim().length === 0) {
801
- throw new CliValidationError('content 不能为空');
802
- }
803
- const { api } = createYApiServices();
804
- const result = await api.importData({
805
- project_id: input.projectId,
806
- type: input.type,
807
- content: input.content,
808
- merge: input.merge,
809
- });
810
- if (result.errcode !== 0) {
811
- throw new CliError(result.errmsg ?? '导入数据失败', 14, 'API_ERROR');
812
- }
813
- return jsonResult({
814
- projectId: input.projectId,
815
- type: input.type,
816
- merge: input.merge,
817
- data: result.data,
818
- message: '导入完成',
887
+ const payload = input;
888
+ return runWithPreview('import', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
889
+ if (input.content.trim().length === 0) {
890
+ throw new CliValidationError('content 不能为空');
891
+ }
892
+ const { api } = createYApiServices();
893
+ const result = await api.importData({
894
+ project_id: input.projectId,
895
+ type: input.type,
896
+ content: input.content,
897
+ merge: input.merge,
898
+ });
899
+ if (result.errcode !== 0) {
900
+ throw new CliError(result.errmsg ?? '导入数据失败', 14, 'API_ERROR');
901
+ }
902
+ return jsonResult({
903
+ projectId: input.projectId,
904
+ type: input.type,
905
+ merge: input.merge,
906
+ data: result.data,
907
+ message: '导入完成',
908
+ });
819
909
  });
820
910
  }, '导入数据到项目(支持 swagger/json/har/postman 等格式)', {
821
911
  costHint: 'medium',
@@ -823,27 +913,31 @@ export function registerUtilCommands(registry) {
823
913
  });
824
914
  // ==================== 更新用户 ====================
825
915
  registry.tool('user-update', {
826
- uid: z.coerce.number().describe('用户 UID'),
916
+ uid: z.number().int().positive().describe('用户 UID'),
827
917
  username: z.string().optional().describe('新用户名'),
828
918
  email: z.string().email().optional().describe('新邮箱'),
829
919
  role: z
830
920
  .enum(['admin', 'owner', 'dev', 'guest', 'viewer'])
831
921
  .optional()
832
922
  .describe('新角色(admin/owner/dev/guest/viewer)'),
923
+ ...confirmSchema,
833
924
  }, async (input) => {
834
- if (input.username === undefined && input.email === undefined && input.role === undefined) {
835
- throw new CliValidationError('至少需要提供 username、email role 中的一个');
836
- }
837
- const { api } = createYApiServices();
838
- await api.updateUser({
839
- id: input.uid,
840
- username: input.username,
841
- email: input.email,
842
- role: input.role,
843
- });
844
- return jsonResult({
845
- uid: input.uid,
846
- message: `用户 ${input.uid} 已更新`,
925
+ const payload = input;
926
+ return runWithPreview('user-update', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
927
+ if (input.username === undefined && input.email === undefined && input.role === undefined) {
928
+ throw new CliValidationError('至少需要提供 username、email role 中的一个');
929
+ }
930
+ const { api } = createYApiServices();
931
+ await api.updateUser({
932
+ id: input.uid,
933
+ username: input.username,
934
+ email: input.email,
935
+ role: input.role,
936
+ });
937
+ return jsonResult({
938
+ uid: input.uid,
939
+ message: `用户 ${input.uid} 已更新`,
940
+ });
847
941
  });
848
942
  }, '用户管理:更新用户信息(需要管理员权限)', {
849
943
  costHint: 'medium',