@cloudglab/yapi-cli 0.0.7 → 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.
- package/CHANGELOG.md +25 -0
- package/README.md +31 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +43 -5
- package/dist/cli.js.map +1 -1
- package/dist/core/changelog.js +7 -14
- package/dist/core/changelog.js.map +1 -1
- package/dist/core/cli-output.js +7 -6
- package/dist/core/cli-output.js.map +1 -1
- package/dist/core/cli-registry.js +61 -8
- package/dist/core/cli-registry.js.map +1 -1
- package/dist/core/manifest.js +14 -4
- package/dist/core/manifest.js.map +1 -1
- package/dist/core/output.d.ts +2 -2
- package/dist/core/output.js +12 -45
- package/dist/core/output.js.map +1 -1
- package/dist/core/tool-registry.js +5 -0
- package/dist/core/tool-registry.js.map +1 -1
- package/dist/core/update-probe.d.ts +3 -1
- package/dist/core/update-probe.js +9 -4
- package/dist/core/update-probe.js.map +1 -1
- package/dist/core/url-parser.js +10 -0
- package/dist/core/url-parser.js.map +1 -1
- package/dist/install.d.ts +9 -1
- package/dist/install.js +74 -20
- package/dist/install.js.map +1 -1
- package/dist/manifest.json +42 -1
- package/dist/services/yapi/api.d.ts +255 -79
- package/dist/services/yapi/api.js +162 -58
- package/dist/services/yapi/api.js.map +1 -1
- package/dist/services/yapi/auth.d.ts +8 -3
- package/dist/services/yapi/auth.js +10 -6
- package/dist/services/yapi/auth.js.map +1 -1
- package/dist/services/yapi/authCache.js +9 -2
- package/dist/services/yapi/authCache.js.map +1 -1
- package/dist/services/yapi/config.d.ts +6 -0
- package/dist/services/yapi/config.js +85 -8
- package/dist/services/yapi/config.js.map +1 -1
- package/dist/services/yapi/index.d.ts +3 -3
- package/dist/services/yapi/index.js +2 -3
- package/dist/services/yapi/index.js.map +1 -1
- package/dist/services/yapi/types.d.ts +38 -0
- package/dist/tools/shared.d.ts +17 -1
- package/dist/tools/shared.js +25 -6
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools/yapi/docs-sync.js +3 -0
- package/dist/tools/yapi/docs-sync.js.map +1 -1
- package/dist/tools/yapi/groups.d.ts +1 -1
- package/dist/tools/yapi/groups.js +1 -1
- package/dist/tools/yapi/groups.js.map +1 -1
- package/dist/tools/yapi/register-auth.js +116 -104
- package/dist/tools/yapi/register-auth.js.map +1 -1
- package/dist/tools/yapi/register-group.js +118 -93
- package/dist/tools/yapi/register-group.js.map +1 -1
- package/dist/tools/yapi/register-interface.js +433 -199
- package/dist/tools/yapi/register-interface.js.map +1 -1
- package/dist/tools/yapi/register-mock.d.ts +2 -0
- package/dist/tools/yapi/register-mock.js +240 -0
- package/dist/tools/yapi/register-mock.js.map +1 -0
- package/dist/tools/yapi/register-project.js +344 -223
- package/dist/tools/yapi/register-project.js.map +1 -1
- package/dist/tools/yapi/register-test.js +33 -25
- package/dist/tools/yapi/register-test.js.map +1 -1
- package/dist/tools/yapi/register-util.js +444 -350
- package/dist/tools/yapi/register-util.js.map +1 -1
- package/dist/tools/yapi/register.js +3 -0
- package/dist/tools/yapi/register.js.map +1 -1
- package/dist/tools/yapi/utils.d.ts +49 -0
- package/dist/tools/yapi/utils.js +124 -2
- package/dist/tools/yapi/utils.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +18 -5
- package/skills/yapi-cli/SKILL.md +37 -12
- package/skills/yapi-cli/reference/auth.md +34 -45
- package/skills/yapi-cli/reference/cheatsheet.md +156 -0
- package/skills/yapi-cli/reference/cli.md +43 -39
- package/skills/yapi-cli/reference/commands.md +35 -124
- package/skills/yapi-cli/reference/group.md +46 -19
- package/skills/yapi-cli/reference/index.md +32 -0
- package/skills/yapi-cli/reference/install.md +30 -6
- package/skills/yapi-cli/reference/interface.md +71 -145
- package/skills/yapi-cli/reference/mock.md +93 -0
- package/skills/yapi-cli/reference/overview.md +7 -5
- package/skills/yapi-cli/reference/project.md +67 -87
- package/skills/yapi-cli/reference/scenarios.md +184 -0
- package/skills/yapi-cli/reference/test.md +20 -17
- package/skills/yapi-cli/reference/tooling.md +89 -0
- package/dist/services/yapi/cache.d.ts +0 -27
- package/dist/services/yapi/cache.js +0 -88
- 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 {
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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.
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
240
|
+
colId: z.number().int().positive().describe('集合 ID,必填。删除后该集合与集合内用例的关联会被清理。'),
|
|
241
|
+
...confirmSchema,
|
|
222
242
|
}, async (input) => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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.
|
|
295
|
+
caseId: z.number().int().positive().describe('用例 ID,必填。仅删除单条测试用例,不会删除所属集合。'),
|
|
296
|
+
...confirmSchema,
|
|
264
297
|
}, async (input) => {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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.
|
|
316
|
+
colId: z.number().int().positive().describe('集合 ID,必填。会执行该集合绑定的测试脚本或用例。'),
|
|
317
|
+
...confirmSchema,
|
|
281
318
|
}, async (input) => {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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.
|
|
329
|
-
catId: z.
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
process.stdout.write(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
344
|
-
message:
|
|
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('
|
|
414
|
+
skillPath: z.string().optional().describe('自定义本地 skill 目录路径,省略则从已安装包安装'),
|
|
359
415
|
yes: z.coerce.boolean().optional().default(false).describe('覆盖已有配置'),
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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.
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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.
|
|
523
|
+
uid: z.number().int().positive().describe('用户 UID'),
|
|
524
|
+
...confirmSchema,
|
|
472
525
|
}, async (input) => {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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.
|
|
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.
|
|
523
|
-
base64: z.string().describe('图片 base64 编码,必填,不含 data:image/...;base64, 前缀。建议传纯图片内容,避免额外前缀导致服务端解析失败。'),
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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.
|
|
631
|
+
apid: z.number().int().positive().describe('接口 ID,必填。用于定位要查询日志的目标接口。'),
|
|
570
632
|
type: z.string().optional().default('interface').describe('日志类型,默认 interface。若服务端支持其他类型,可按后端要求传值。'),
|
|
571
|
-
limit: z.
|
|
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
|
-
|
|
638
|
+
typeid: input.apid,
|
|
577
639
|
limit: input.limit,
|
|
578
640
|
});
|
|
579
641
|
return jsonResult({
|
|
580
642
|
action: 'logs-by-update',
|
|
581
|
-
|
|
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.
|
|
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.
|
|
609
|
-
ids: z.string().describe('用例 ID 列表(逗号分隔,例如: 1,2,3)'),
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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.
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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.
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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.
|
|
836
|
+
colId: z.number().int().positive().describe('目标集合 ID,必填。克隆出的用例会写入这个集合。'),
|
|
755
837
|
...projectIdSchema,
|
|
756
|
-
srcColId: z.
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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.
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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.
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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',
|