@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +31 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.js +43 -5
  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 +61 -8
  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 +42 -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 +433 -199
  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 +37 -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 +43 -39
  78. package/skills/yapi-cli/reference/commands.md +35 -124
  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 -145
  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,14 +1,15 @@
1
1
  import { z } from 'zod';
2
2
  import { CliConfigError, CliError, CliNetworkError, CliValidationError } from '../../core/errors.js';
3
- import { jsonResult, optionalTrimmedText, httpMethodEnum, paginationSchema, projectIdSchema, optionalIdFilter, } from '../shared.js';
4
- import { createYApiServices, formatMethod, truncate } from './utils.js';
5
- import { loadConfig, YAPI_HOME, YApiAuthService, YApiService } from '../../services/yapi/index.js';
3
+ import { confirmSchema, jsonResult, optionalTrimmedText, httpMethodEnum, paginationSchema, projectIdSchema, optionalIdFilter, runWithPreview, } from '../shared.js';
4
+ import { previewOrAssertWriteAllowed } from '../../core/write-guard.js';
5
+ import { createYApiServices, formatMethod, getAuthService, truncate, buildJsonSchemaFlags, parseJsonArrayParam, inferInterfaceStyle } from './utils.js';
6
+ import { loadConfig } from '../../services/yapi/index.js';
6
7
  export function registerInterfaceCommands(registry) {
7
8
  // ==================== 搜索接口 ====================
8
9
  registry.tool('search', {
9
- q: z.string().describe('搜索关键词,必填。YApi 服务端在接口标题和路径上做模糊匹配,区分大小写由服务端决定。空字符串可能返回空结果或报错。'),
10
+ q: z.string().trim().min(1).describe('搜索关键词,必填。YApi 服务端在接口标题和路径上做模糊匹配,区分大小写由服务端决定。空字符串可能返回空结果或报错。'),
10
11
  ...optionalIdFilter,
11
- limit: z.coerce.number().optional().default(20).describe('返回接口数量上限,默认 20。YApi 服务端有上限保护,建议不超过 50;与 groupId/projectId 联用时可缩小搜索范围。'),
12
+ limit: z.number().int().positive().optional().default(20).describe('返回接口数量上限,默认 20。YApi 服务端有上限保护,建议不超过 50;与 groupId/projectId 联用时可缩小搜索范围。'),
12
13
  }, async (input) => {
13
14
  const { api } = createYApiServices();
14
15
  const result = await api.searchInterfaces({
@@ -61,7 +62,7 @@ export function registerInterfaceCommands(registry) {
61
62
  }, '列出项目中的接口', { costHint: 'low', nextBestTools: ['interface-get', 'project-get', 'category-list'] });
62
63
  // ==================== 接口详情 ====================
63
64
  registry.tool('interface-get', {
64
- interfaceId: z.coerce.number().describe('接口 ID,必填。返回接口完整字段,请求/响应体仅返回前 500 字符,长内容可用 interface-snapshot。'),
65
+ interfaceId: z.number().int().positive().describe('接口 ID,必填。返回接口完整字段,请求/响应体会原样返回。'),
65
66
  }, async (input) => {
66
67
  const { api } = createYApiServices();
67
68
  const result = await api.getInterfaceDetail(input.interfaceId);
@@ -80,25 +81,26 @@ export function registerInterfaceCommands(registry) {
80
81
  reqQuery: iface.req_query,
81
82
  reqHeaders: iface.req_headers,
82
83
  reqBodyType: iface.req_body_type,
83
- reqBody: iface.req_body_other ? truncate(iface.req_body_other, 500) : undefined,
84
+ reqBody: iface.req_body_other || undefined,
84
85
  resBodyType: iface.res_body_type,
85
- resBody: iface.res_body ? truncate(iface.res_body, 500) : undefined,
86
+ resBody: iface.res_body || undefined,
87
+ isMockOpen: iface.is_mock_open,
88
+ mockCustomScript: iface.mock_custom_script,
86
89
  },
87
90
  });
88
91
  }, '查看接口详情', { costHint: 'low', nextBestTools: ['interface-list', 'log', 'request'] });
89
92
  // ==================== 原始请求 ====================
90
93
  registry.tool('request', {
91
- path: z.string().describe('YApi API 路径,必填,例如 /api/project/list。可以传以 / 开头或不带 / 的路径,CLI 内部会统一补齐前导 /。'),
94
+ path: z.string().trim().min(1).describe('YApi API 路径,必填,例如 /api/project/list。可以传以 / 开头或不带 / 的路径,CLI 内部会统一补齐前导 /。'),
92
95
  method: httpMethodEnum.optional().default('GET').describe('HTTP 方法,可选枚举 GET/POST/PUT/DELETE/PATCH,默认 GET。YApi 私有接口通常要求 GET 或 POST,其他方法可能被服务端拒绝。'),
93
96
  body: optionalTrimmedText.describe('请求体 JSON 字符串,可选。仅在 method 为 POST/PUT/PATCH 时会真正发送到服务端,其他方法传入会被忽略。需是合法 JSON,空字符串视为未传。'),
94
- projectId: z.coerce.number().optional().describe('项目 ID,可选,YApi 内部数字 ID。当 YApi 接口要求 _yapi_token 或需要注入项目专属 Authorization 时使用,不传时仅使用全局登录 token。'),
97
+ projectId: z.number().int().positive().optional().describe('项目 ID,可选,YApi 内部数字 ID。当 YApi 接口要求 _yapi_token 或需要注入项目专属 Authorization 时使用,不传时仅使用全局登录 token。'),
95
98
  }, async (input) => {
96
99
  const config = loadConfig();
97
100
  if (!config) {
98
101
  throw new CliConfigError('请先运行 `yapi config-init` 初始化配置');
99
102
  }
100
- const auth = new YApiAuthService(YAPI_HOME);
101
- const api = new YApiService(config.url, () => auth.getAuthHeaders(), () => auth.getToken() ?? '');
103
+ const auth = getAuthService();
102
104
  const path = input.path.startsWith('/') ? input.path : `/${input.path}`;
103
105
  const url = `${config.url.replace(/\/+$/, '')}${path}`;
104
106
  const headers = {
@@ -112,17 +114,26 @@ export function registerInterfaceCommands(registry) {
112
114
  if (input.body && (input.method === 'POST' || input.method === 'PUT' || input.method === 'PATCH')) {
113
115
  parsedBody = input.body;
114
116
  }
117
+ // 加 30s 超时,避免裸 fetch 在服务端无响应时无限挂起
118
+ const controller = new AbortController();
119
+ const timeoutId = setTimeout(() => controller.abort(), 30000);
115
120
  let response;
116
121
  try {
117
122
  response = await fetch(url, {
118
123
  method: input.method,
119
124
  headers,
120
125
  body: parsedBody,
126
+ signal: controller.signal,
121
127
  });
122
128
  }
123
129
  catch (error) {
130
+ clearTimeout(timeoutId);
131
+ if (error instanceof Error && error.name === 'AbortError') {
132
+ throw new CliNetworkError(`请求超时(30s): ${url}`);
133
+ }
124
134
  throw new CliNetworkError(`请求失败: ${error instanceof Error ? error.message : String(error)}`);
125
135
  }
136
+ clearTimeout(timeoutId);
126
137
  const text = await response.text();
127
138
  let data;
128
139
  try {
@@ -139,36 +150,85 @@ export function registerInterfaceCommands(registry) {
139
150
  // ==================== 接口创建 ====================
140
151
  registry.tool('interface-create', {
141
152
  ...projectIdSchema,
142
- catId: z.coerce.number().describe('接口所属分类 ID,必填,可通过 category-list 获取。同分类下接口不可重复 path+method'),
143
- title: z.string().describe('接口标题,必填,YApi 前端展示用,建议带业务前缀,例如 用户登录-手机号密码。'),
144
- path: z.string().describe('接口路径,必填,例如 /api/user/list。建议以 / 开头,YApi 在生成 Mock URL 时会拼上项目 basepath。'),
145
- method: httpMethodEnum.default('GET').describe('HTTP 方法,必填枚举,默认 GET。可选值 GET/POST/PUT/DELETE/PATCH。'),
146
- desc: optionalTrimmedText.describe('接口描述,可选。仅作为展示文案,YApi 后端不做格式校验,建议补充参数/返回值/错误码等关键信息。'),
147
- status: z.enum(['done', 'undone']).optional().default('undone').describe('开发状态,可选枚举 done/undone,默认 undone。done 表示接口已联调通过,undone 表示仅完成设计。'),
148
- reqBodyType: z.string().optional().describe('请求体类型,可选。常用值 json / form / file / raw 等,具体合法值由 YApi 服务端决定。'),
149
- reqBody: optionalTrimmedText.describe('请求体示例,可选。reqBodyType json 时建议传入合法 JSON 字符串,其他类型为示例字符串。'),
150
- resBodyType: z.string().optional().describe('响应体类型,可选,常用 json。建议与 reqBodyType 保持一致以便阅读。'),
151
- resBody: optionalTrimmedText.describe('响应体示例,可选,JSON 字符串。建议写入一个最小可用的返回示例,便于联调对照。'),
153
+ catId: z.number().int().positive().describe('接口所属分类 ID,必填,可通过 category-list 获取。CLI 会拉取该分类下已有接口分析风格(路径前缀、method 偏好、请求/响应体字段结构),作为新接口的参考。'),
154
+ title: z.string().trim().min(1).describe('接口标题,必填,YApi 前端展示用。CLI 会参考该分类下已有接口的命名风格给出建议,建议按「业务模块-接口名」格式。'),
155
+ path: z.string().trim().min(1).describe('接口路径,必填,例如 /api/user/list。CLI 会参考该分类下已有接口的公共路径前缀,建议保持一致。'),
156
+ method: httpMethodEnum.default('GET').describe('HTTP 方法,必填枚举,默认 GET。可选值 GET/POST/PUT/DELETE/PATCH。CLI 会按该分类下最常用的 method 设置默认值。'),
157
+ desc: optionalTrimmedText.describe('接口描述,可选。仅作为展示文案,建议补充参数/返回值/错误码等关键信息。'),
158
+ status: z.enum(['done', 'undone']).optional().default('undone').describe('开发状态,可选枚举 done/undone,默认 undone。done 表示接口已联调通过。'),
159
+ tags: z.string().optional().describe('标签,可选,逗号分隔字符串。CLI 会按逗号拆分并 trim 后整体写入,不传或空字符串表示不设置标签。'),
160
+ reqQuery: z.string().optional().describe('请求查询参数 JSON 数组字符串,可选。例如 \'[{"name":"page","example":"1","desc":"页码","required":"1"}]\'。'),
161
+ reqHeaders: z.string().optional().describe('请求头 JSON 数组字符串,可选。例如 \'[{"name":"Authorization","value":"Bearer xxx","desc":"认证令牌"}]\'。'),
162
+ reqParams: z.string().optional().describe('URL 路径参数 JSON 数组字符串,可选。例如 \'[{"name":"id","example":"123","desc":"用户ID"}]\'。'),
163
+ reqBodyType: z.string().optional().describe('请求体类型,可选。常用值 json / form / file / raw 等。'),
164
+ reqBodyForm: z.string().optional().describe('请求体表单字段 JSON 数组字符串,可选(reqBodyType=form 时使用)。例如 \'[{"name":"username","type":"string","required":"1","desc":"用户名"}]\'。'),
165
+ reqBody: optionalTrimmedText.describe('请求体示例,可选。reqBodyType 为 json 时建议传入合法 JSON 字符串。CLI 会参考该分类下已有接口的请求体字段结构。'),
166
+ resBodyType: z.string().optional().describe('响应体类型,可选,常用 json。建议与 reqBodyType 保持一致。'),
167
+ resBody: optionalTrimmedText.describe('响应体示例,可选,JSON 字符串。CLI 会参考该分类下已有接口的响应体字段结构。'),
168
+ markdown: z.string().optional().describe('接口文档 Markdown 字符串,可选。YApi 会将其渲染为富文本说明。'),
169
+ apiOpened: z.boolean().optional().describe('是否对外开放此接口,可选。对外开放后可通过 Open API 访问。'),
170
+ ...confirmSchema,
152
171
  }, async (input) => {
172
+ const payload = input;
153
173
  const { api } = createYApiServices();
154
- const result = await api.createInterface({
155
- project_id: input.projectId,
156
- catid: input.catId,
157
- title: input.title,
158
- path: input.path,
159
- method: input.method,
160
- status: input.status,
161
- desc: input.desc,
162
- req_body_type: input.reqBodyType,
163
- req_body_other: input.reqBody,
164
- res_body_type: input.resBodyType,
165
- res_body: input.resBody,
166
- });
167
- return jsonResult({
168
- interfaceId: result.data._id,
169
- message: `接口「${input.title}」创建成功`,
174
+ const styleHints = await inferInterfaceStyle(api, input.projectId, input.catId, 10);
175
+ if (styleHints) {
176
+ payload._styleHints = {
177
+ commonPathPrefix: styleHints.commonPathPrefix,
178
+ commonMethod: styleHints.commonMethod,
179
+ reqBodyFields: styleHints.reqBodyFields,
180
+ resBodyFields: styleHints.resBodyFields,
181
+ catName: styleHints.catName,
182
+ };
183
+ }
184
+ return runWithPreview('interface-create', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
185
+ const tag = input.tags
186
+ ? input.tags
187
+ .split(',')
188
+ .map((t) => t.trim())
189
+ .filter((t) => t.length > 0)
190
+ : undefined;
191
+ let reqQuery;
192
+ let reqHeaders;
193
+ let reqParams;
194
+ let reqBodyForm;
195
+ try {
196
+ reqQuery = parseJsonArrayParam(input.reqQuery);
197
+ reqHeaders = parseJsonArrayParam(input.reqHeaders);
198
+ reqParams = parseJsonArrayParam(input.reqParams);
199
+ reqBodyForm = parseJsonArrayParam(input.reqBodyForm);
200
+ }
201
+ catch (err) {
202
+ throw new CliValidationError(err instanceof Error ? err.message : String(err));
203
+ }
204
+ const result = await api.createInterface({
205
+ project_id: input.projectId,
206
+ catid: input.catId,
207
+ title: input.title,
208
+ path: input.path,
209
+ method: input.method,
210
+ status: input.status,
211
+ tag,
212
+ desc: input.desc,
213
+ req_query: reqQuery,
214
+ req_headers: reqHeaders,
215
+ req_params: reqParams,
216
+ req_body_type: input.reqBodyType,
217
+ req_body_form: reqBodyForm,
218
+ req_body_other: input.reqBody,
219
+ res_body_type: input.resBodyType,
220
+ res_body: input.resBody,
221
+ markdown: input.markdown,
222
+ api_opened: input.apiOpened,
223
+ ...buildJsonSchemaFlags(input.reqBodyType, input.resBodyType),
224
+ });
225
+ return jsonResult({
226
+ interfaceId: result.data._id,
227
+ styleHints,
228
+ message: `接口「${input.title}」创建成功`,
229
+ });
170
230
  });
171
- }, '创建新接口', { costHint: 'medium', nextBestTools: ['interface-list', 'interface-get', 'category-list'] });
231
+ }, '创建新接口(CLI 会自动分析目标分类风格,作为命名和字段参考)', { costHint: 'medium', nextBestTools: ['interface-list', 'interface-get', 'category-list'] });
172
232
  // ==================== 分类列表 ====================
173
233
  registry.tool('category-list', {
174
234
  ...projectIdSchema,
@@ -186,8 +246,8 @@ export function registerInterfaceCommands(registry) {
186
246
  }, '列出项目中的分类(目录)', { costHint: 'low', nextBestTools: ['interface-list', 'interface-get'] });
187
247
  // ==================== 接口变更日志 ====================
188
248
  registry.tool('log', {
189
- interfaceId: z.coerce.number().describe('接口 ID,必填,YApi 内部数字 ID。'),
190
- projectId: z.coerce.number().describe('项目 ID,必填,YApi 内部数字 ID。YApi 服务端的接口日志接口要求同时传接口和所属项目,缺一会被拒绝。'),
249
+ interfaceId: z.number().int().positive().describe('接口 ID,必填,YApi 内部数字 ID。'),
250
+ projectId: z.number().int().positive().describe('项目 ID,必填,YApi 内部数字 ID。YApi 服务端的接口日志接口要求同时传接口和所属项目,缺一会被拒绝。'),
191
251
  ...paginationSchema,
192
252
  }, async (input) => {
193
253
  const { api } = createYApiServices();
@@ -244,65 +304,75 @@ export function registerInterfaceCommands(registry) {
244
304
  }, '获取项目的自定义字段配置', { costHint: 'low', nextBestTools: ['project-get', 'interface-list', 'interface-get'] });
245
305
  // ==================== 更新接口排序 ====================
246
306
  registry.tool('interface-up-index', {
247
- id: z.coerce.number().describe('接口 ID,必填。'),
248
- index: z.coerce.number().describe('新排序索引,必填整数。在所属分类内的相对排序,YApi 通常按数字升序展示,越小越靠前。'),
307
+ id: z.number().int().positive().describe('接口 ID,必填。'),
308
+ index: z.number().int().positive().describe('新排序索引,必填整数。在所属分类内的相对排序,YApi 通常按数字升序展示,越小越靠前。'),
309
+ ...confirmSchema,
249
310
  }, async (input) => {
250
- const { api } = createYApiServices();
251
- const result = await api.upInterfaceIndex({
252
- id: input.id,
253
- index: input.index,
254
- });
255
- if (result.errcode !== 0) {
256
- throw new CliError(result.errmsg ?? '更新接口排序失败', 14, 'API_ERROR');
257
- }
258
- return jsonResult({
259
- id: input.id,
260
- index: input.index,
261
- message: `接口 ${input.id} 排序已更新为 ${input.index}`,
311
+ const payload = input;
312
+ return runWithPreview('interface-up-index', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
313
+ const { api } = createYApiServices();
314
+ const result = await api.upInterfaceIndex([
315
+ { id: input.id, index: input.index },
316
+ ]);
317
+ if (result.errcode !== 0) {
318
+ throw new CliError(result.errmsg ?? '更新接口排序失败', 14, 'API_ERROR');
319
+ }
320
+ return jsonResult({
321
+ id: input.id,
322
+ index: input.index,
323
+ message: `接口 ${input.id} 排序已更新为 ${input.index}`,
324
+ });
262
325
  });
263
326
  }, '更新接口排序', { costHint: 'medium', nextBestTools: ['interface-list', 'interface-get'] });
264
327
  // ==================== 更新分类排序 ====================
265
328
  registry.tool('interface-up-cat-index', {
266
- catid: z.coerce.number().describe('分类 ID,必填。'),
267
- index: z.coerce.number().describe('分类的新排序索引,必填整数。作用于项目内分类列表,YApi 按升序展示,越小越靠前。'),
329
+ catid: z.number().int().positive().describe('分类 ID,必填。'),
330
+ index: z.number().int().positive().describe('分类的新排序索引,必填整数。作用于项目内分类列表,YApi 按升序展示,越小越靠前。'),
331
+ ...confirmSchema,
268
332
  }, async (input) => {
269
- const { api } = createYApiServices();
270
- const result = await api.upCatIndex({
271
- catid: input.catid,
272
- index: input.index,
273
- });
274
- if (result.errcode !== 0) {
275
- throw new CliError(result.errmsg ?? '更新分类排序失败', 14, 'API_ERROR');
276
- }
277
- return jsonResult({
278
- catid: input.catid,
279
- index: input.index,
280
- message: `分类 ${input.catid} 排序已更新为 ${input.index}`,
333
+ const payload = input;
334
+ return runWithPreview('interface-up-cat-index', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
335
+ const { api } = createYApiServices();
336
+ const result = await api.upCatIndex([
337
+ { id: input.catid, index: input.index },
338
+ ]);
339
+ if (result.errcode !== 0) {
340
+ throw new CliError(result.errmsg ?? '更新分类排序失败', 14, 'API_ERROR');
341
+ }
342
+ return jsonResult({
343
+ catid: input.catid,
344
+ index: input.index,
345
+ message: `分类 ${input.catid} 排序已更新为 ${input.index}`,
346
+ });
281
347
  });
282
348
  }, '更新分类排序', { costHint: 'medium', nextBestTools: ['interface-list', 'category-list'] });
283
349
  // ==================== 导入接口 ====================
284
350
  registry.tool('interface-upload', {
285
351
  ...projectIdSchema,
286
- content: z.string().describe('导入内容,必填,JSON/Swagger 字符串。可以是 raw 字符串也可以是 URL 服务端拉取,内容过长时建议走文件后再粘贴。CLI 内部不做格式校验,YApi 服务端按 type 解析。'),
352
+ content: z.string().trim().min(1).describe('导入内容,必填,JSON/Swagger 字符串。可以是 raw 字符串也可以是 URL 服务端拉取,内容过长时建议走文件后再粘贴。CLI 内部不做格式校验,YApi 服务端按 type 解析。'),
287
353
  type: z.string().optional().default('swagger').describe('导入类型,可选,默认 swagger。常用值 swagger / json / har / postman,YApi 服务端按值路由到对应解析器。'),
354
+ ...confirmSchema,
288
355
  }, async (input) => {
289
- if (!input.content.trim()) {
290
- throw new CliError('导入内容不能为空', 9, 'VALIDATION_ERROR');
291
- }
292
- const { api } = createYApiServices();
293
- const result = await api.interUpload({
294
- project_id: input.projectId,
295
- content: input.content,
296
- type: input.type ?? 'swagger',
297
- });
298
- if (result.errcode !== 0) {
299
- throw new CliError(result.errmsg ?? '导入接口失败', 14, 'API_ERROR');
300
- }
301
- return jsonResult({
302
- projectId: input.projectId,
303
- type: input.type ?? 'swagger',
304
- data: result.data,
305
- message: '接口导入完成',
356
+ const payload = input;
357
+ return runWithPreview('interface-upload', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
358
+ if (!input.content.trim()) {
359
+ throw new CliError('导入内容不能为空', 9, 'VALIDATION_ERROR');
360
+ }
361
+ const { api } = createYApiServices();
362
+ const result = await api.interUpload({
363
+ project_id: input.projectId,
364
+ content: input.content,
365
+ type: input.type ?? 'swagger',
366
+ });
367
+ if (result.errcode !== 0) {
368
+ throw new CliError(result.errmsg ?? '导入接口失败', 14, 'API_ERROR');
369
+ }
370
+ return jsonResult({
371
+ projectId: input.projectId,
372
+ type: input.type ?? 'swagger',
373
+ data: result.data,
374
+ message: '接口导入完成',
375
+ });
306
376
  });
307
377
  }, '导入接口(支持 swagger/json 等格式)', { costHint: 'medium', nextBestTools: ['interface-list', 'project-get'] });
308
378
  // ==================== 下载浏览器扩展 CRX ====================
@@ -317,47 +387,55 @@ export function registerInterfaceCommands(registry) {
317
387
  }, '接口工具:下载浏览器扩展 CRX 安装包', { costHint: 'low', nextBestTools: ['install', 'interface-get'] });
318
388
  // ==================== 接口 Mock 开关 ====================
319
389
  registry.tool('interface-mock-toggle', {
320
- interfaceId: z.coerce.number().describe('接口 ID,必填。'),
390
+ interfaceId: z.number().int().positive().describe('接口 ID,必填。'),
321
391
  open: z
322
392
  .union([z.literal('true'), z.literal('false')])
323
393
  .default('true')
324
394
  .describe('是否开启该接口的 Mock,可选枚举 true/false,默认 true。受项目级 is_mock_open 父开关影响,父开关关闭时本开关即使开启也不生效。'),
395
+ ...confirmSchema,
325
396
  }, async (input) => {
326
- const { api } = createYApiServices();
327
- const result = await api.updateInterface({
328
- id: input.interfaceId,
329
- is_mock_open: input.open === 'true',
330
- });
331
- if (result.errcode !== 0) {
332
- throw new CliError(result.errmsg ?? '切换 Mock 开关失败', 14, 'API_ERROR');
333
- }
334
- return jsonResult({
335
- interfaceId: input.interfaceId,
336
- is_mock_open: input.open === 'true',
337
- message: `接口 ${input.interfaceId} Mock 已${input.open === 'true' ? '开启' : '关闭'}`,
397
+ const payload = input;
398
+ return runWithPreview('interface-mock-toggle', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
399
+ const { api } = createYApiServices();
400
+ const result = await api.updateInterface({
401
+ id: input.interfaceId,
402
+ is_mock_open: input.open === 'true',
403
+ });
404
+ if (result.errcode !== 0) {
405
+ throw new CliError(result.errmsg ?? '切换 Mock 开关失败', 14, 'API_ERROR');
406
+ }
407
+ return jsonResult({
408
+ interfaceId: input.interfaceId,
409
+ is_mock_open: input.open === 'true',
410
+ message: `接口 ${input.interfaceId} Mock 已${input.open === 'true' ? '开启' : '关闭'}`,
411
+ });
338
412
  });
339
413
  }, '切换接口 Mock 开关', { costHint: 'medium', nextBestTools: ['interface-mock-script', 'interface-get'] });
340
414
  // ==================== 接口 Mock 脚本 ====================
341
415
  registry.tool('interface-mock-script', {
342
- interfaceId: z.coerce.number().describe('接口 ID,必填。'),
343
- script: z.string().describe('Mock 脚本内容,必填,JavaScript 字符串。由 YApi 服务端在 vm 沙箱内执行,作用于该接口的所有 mock 请求。空字符串相当于清空脚本。'),
416
+ interfaceId: z.number().int().positive().describe('接口 ID,必填。'),
417
+ script: z.string().describe('Mock 脚本内容,必填,JavaScript 字符串。由 YApi 服务端在 vm 沙箱内执行,作用于该接口的所有 mock 请求。传空字符串可清空脚本。'),
418
+ ...confirmSchema,
344
419
  }, async (input) => {
345
- const { api } = createYApiServices();
346
- const result = await api.updateInterface({
347
- id: input.interfaceId,
348
- mock_custom_script: input.script,
349
- });
350
- if (result.errcode !== 0) {
351
- throw new CliError(result.errmsg ?? '更新 Mock 脚本失败', 14, 'API_ERROR');
352
- }
353
- return jsonResult({
354
- interfaceId: input.interfaceId,
355
- message: `接口 ${input.interfaceId} Mock 脚本已更新`,
420
+ const payload = input;
421
+ return runWithPreview('interface-mock-script', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
422
+ const { api } = createYApiServices();
423
+ const result = await api.updateInterface({
424
+ id: input.interfaceId,
425
+ mock_custom_script: input.script,
426
+ });
427
+ if (result.errcode !== 0) {
428
+ throw new CliError(result.errmsg ?? '更新 Mock 脚本失败', 14, 'API_ERROR');
429
+ }
430
+ return jsonResult({
431
+ interfaceId: input.interfaceId,
432
+ message: `接口 ${input.interfaceId} Mock 脚本已更新`,
433
+ });
356
434
  });
357
435
  }, '设置接口自定义 Mock 脚本', { costHint: 'medium', nextBestTools: ['interface-mock-toggle', 'interface-get'] });
358
436
  // ==================== 分类详情 ====================
359
437
  registry.tool('category-get', {
360
- catId: z.coerce.number().describe('分类 ID,必填。返回该分类的 id/name/desc/projectId 与接口数量。'),
438
+ catId: z.number().int().positive().describe('分类 ID,必填。返回该分类的 id/name/desc/projectId 与接口数量。'),
361
439
  }, async (input) => {
362
440
  const { api } = createYApiServices();
363
441
  const result = await api.getCategory(input.catId);
@@ -374,76 +452,90 @@ export function registerInterfaceCommands(registry) {
374
452
  // ==================== 创建分类 ====================
375
453
  registry.tool('category-create', {
376
454
  ...projectIdSchema,
377
- name: z.string().describe('分类名称'),
455
+ name: z.string().trim().min(1).describe('分类名称'),
378
456
  desc: z.string().optional().describe('分类描述'),
457
+ ...confirmSchema,
379
458
  }, async (input) => {
380
- if (!input.name.trim()) {
381
- throw new CliValidationError('分类名称不能为空');
382
- }
383
- const { api } = createYApiServices();
384
- const result = await api.addCategory({
385
- name: input.name.trim(),
386
- project_id: input.projectId,
387
- desc: input.desc,
388
- });
389
- if (result.errcode !== 0) {
390
- throw new CliError(result.errmsg ?? '创建分类失败', 14, 'API_ERROR');
391
- }
392
- return jsonResult({
393
- catId: result.data._id,
394
- name: input.name,
395
- message: `分类「${input.name}」创建成功`,
459
+ const payload = input;
460
+ return runWithPreview('category-create', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
461
+ if (!input.name.trim()) {
462
+ throw new CliValidationError('分类名称不能为空');
463
+ }
464
+ const { api } = createYApiServices();
465
+ const result = await api.addCategory({
466
+ name: input.name.trim(),
467
+ project_id: input.projectId,
468
+ desc: input.desc,
469
+ });
470
+ if (result.errcode !== 0) {
471
+ throw new CliError(result.errmsg ?? '创建分类失败', 14, 'API_ERROR');
472
+ }
473
+ return jsonResult({
474
+ catId: result.data._id,
475
+ name: input.name,
476
+ message: `分类「${input.name}」创建成功`,
477
+ });
396
478
  });
397
479
  }, '创建接口分类(目录)', { costHint: 'medium', nextBestTools: ['category-list', 'interface-create'] });
398
480
  // ==================== 更新分类 ====================
399
481
  registry.tool('category-update', {
400
- catId: z.coerce.number().describe('分类 ID,必填。'),
401
- name: z.string().optional().describe('新名称,可选。name/desc/status 至少传一个,否则 CLI 会拒绝请求。'),
482
+ catId: z.number().int().positive().describe('分类 ID,必填。'),
483
+ name: z.string().optional().describe('新名称,可选。namedesc 至少传一个,否则 CLI 会拒绝请求。'),
402
484
  desc: z.string().optional().describe('新描述,可选。'),
403
- status: z.enum(['open', 'closed']).optional().describe('状态,可选枚举。open 表示开放编辑,closed 表示只读。'),
485
+ ...confirmSchema,
404
486
  }, async (input) => {
405
- if (input.name === undefined && input.desc === undefined && input.status === undefined) {
406
- throw new CliValidationError('至少需要提供 name、desc status 中的一个');
407
- }
408
- const { api } = createYApiServices();
409
- await api.updateCategory({
410
- catid: input.catId,
411
- name: input.name,
412
- desc: input.desc,
413
- status: input.status,
414
- });
415
- return jsonResult({
416
- catId: input.catId,
417
- message: `分类 ${input.catId} 已更新`,
487
+ const payload = input;
488
+ return runWithPreview('category-update', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
489
+ if (input.name === undefined && input.desc === undefined) {
490
+ throw new CliValidationError('至少需要提供 name desc 中的一个');
491
+ }
492
+ const { api } = createYApiServices();
493
+ await api.updateCategory({
494
+ catid: input.catId,
495
+ name: input.name,
496
+ desc: input.desc,
497
+ });
498
+ return jsonResult({
499
+ catId: input.catId,
500
+ message: `分类 ${input.catId} 已更新`,
501
+ });
418
502
  });
419
503
  }, '更新分类信息', { costHint: 'medium', nextBestTools: ['category-get', 'category-list'] });
420
504
  // ==================== 删除分类 ====================
421
505
  registry.tool('category-delete', {
422
- catId: z.coerce.number().describe('分类 ID,必填。删除会同时清理该分类下所有接口,YApi 服务端不会二次确认,建议先用 interface-list-by-cat 查看影响。'),
506
+ catId: z.number().int().positive().describe('分类 ID,必填。删除会同时清理该分类下所有接口,YApi 服务端不会二次确认,建议先用 interface-list-by-cat 查看影响。'),
507
+ ...confirmSchema,
423
508
  }, async (input) => {
424
- const { api } = createYApiServices();
425
- await api.deleteCategory(input.catId);
426
- return jsonResult({
427
- action: 'delete',
428
- catId: input.catId,
429
- deleted: true,
509
+ const payload = input;
510
+ return runWithPreview('category-delete', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
511
+ const { api } = createYApiServices();
512
+ await api.deleteCategory(input.catId);
513
+ return jsonResult({
514
+ action: 'delete',
515
+ catId: input.catId,
516
+ deleted: true,
517
+ });
430
518
  });
431
519
  }, '删除分类(不可恢复)', { costHint: 'medium', nextBestTools: ['category-list', 'category-get'] });
432
520
  // ==================== 删除接口 ====================
433
521
  registry.tool('interface-delete', {
434
- interfaceId: z.coerce.number().describe('接口 ID,必填。删除不可恢复,YApi 同时会清理关联的测试集合用例。'),
522
+ interfaceId: z.number().int().positive().describe('接口 ID,必填。删除不可恢复,YApi 同时会清理关联的测试集合用例。'),
523
+ ...confirmSchema,
435
524
  }, async (input) => {
436
- const { api } = createYApiServices();
437
- await api.deleteInterface(input.interfaceId);
438
- return jsonResult({
439
- action: 'delete',
440
- interfaceId: input.interfaceId,
441
- deleted: true,
525
+ const payload = input;
526
+ return runWithPreview('interface-delete', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
527
+ const { api } = createYApiServices();
528
+ await api.deleteInterface(input.interfaceId);
529
+ return jsonResult({
530
+ action: 'delete',
531
+ interfaceId: input.interfaceId,
532
+ deleted: true,
533
+ });
442
534
  });
443
535
  }, '删除接口(不可恢复)', { costHint: 'medium', nextBestTools: ['interface-list', 'interface-get'] });
444
536
  // ==================== 按分类列出接口 ====================
445
537
  registry.tool('interface-list-by-cat', {
446
- catId: z.coerce.number().describe('分类 ID,必填。返回该分类下的接口列表(不含分页)。'),
538
+ catId: z.number().int().positive().describe('分类 ID,必填。返回该分类下的接口列表(不含分页)。'),
447
539
  }, async (input) => {
448
540
  const { api } = createYApiServices();
449
541
  const result = await api.listInterfacesByCategory(input.catId);
@@ -460,58 +552,196 @@ export function registerInterfaceCommands(registry) {
460
552
  })),
461
553
  });
462
554
  }, '按分类 ID 列出接口', { costHint: 'low', nextBestTools: ['category-get', 'interface-list'] });
555
+ // ==================== 更新接口(部分更新) ====================
556
+ registry.tool('interface-update', {
557
+ interfaceId: z.number().int().positive().describe('接口 ID,必填。目标接口必须已存在,可用 interface-get 先确认。本命令走 /interface/up 部分更新语义,未传字段保持原值,不会覆盖。'),
558
+ title: z.string().optional().describe('接口标题,可选。'),
559
+ path: z.string().optional().describe('接口路径,可选,例如 /api/user/list。建议以 / 开头。'),
560
+ method: httpMethodEnum.optional().describe('HTTP 方法,可选枚举 GET/POST/PUT/DELETE/PATCH。不传则保持原值。'),
561
+ catid: z.number().int().positive().optional().describe('分类 ID,可选。移动接口到其它分类时使用。'),
562
+ status: z.enum(['done', 'undone']).optional().describe('开发状态,可选枚举 done/undone。'),
563
+ tags: z.string().optional().describe('标签,可选,逗号分隔字符串。CLI 按逗号拆分并 trim 后整体写入;传空串会清空标签。'),
564
+ desc: optionalTrimmedText.describe('接口描述,可选。'),
565
+ reqQuery: z.string().optional().describe('请求查询参数 JSON 数组字符串,可选。会整体覆盖现有 req_query(不是追加)。'),
566
+ reqHeaders: z.string().optional().describe('请求头 JSON 数组字符串,可选。会整体覆盖现有 req_headers。'),
567
+ reqParams: z.string().optional().describe('URL 路径参数 JSON 数组字符串,可选。会整体覆盖现有 req_params。'),
568
+ reqBodyType: z.string().optional().describe('请求体类型,可选,常用 json / form / file / raw。'),
569
+ reqBodyForm: z.string().optional().describe('请求体表单字段 JSON 数组字符串,可选(reqBodyType=form 时覆盖)。'),
570
+ reqBody: optionalTrimmedText.describe('请求体示例,可选。reqBodyType 为 json 时建议传入合法 JSON 字符串。'),
571
+ resBodyType: z.string().optional().describe('响应体类型,可选,常用 json。'),
572
+ resBody: optionalTrimmedText.describe('响应体示例,可选 JSON 字符串。YApi 后端会原样存储,建议先 interface-get 取当前 resBody 用 jq fromjson 展开、修改后再整体回传。'),
573
+ markdown: z.string().optional().describe('接口文档 Markdown 字符串,可选。覆盖现有 markdown 说明。'),
574
+ apiOpened: z.boolean().optional().describe('是否对外开放此接口,可选。'),
575
+ customFieldValue: z.string().optional().describe('自定义字段值,可选。仅项目启用了自定义字段时有效。'),
576
+ switchNotice: z.boolean().optional().describe('更新时是否发送变更通知给关注者,可选。需要服务端配置消息通知。'),
577
+ message: z.string().optional().describe('更新备注信息,可选。会记录在接口变更日志中。'),
578
+ ...confirmSchema,
579
+ }, async (input) => {
580
+ const payload = input;
581
+ return runWithPreview('interface-update', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
582
+ const provided = [
583
+ input.title, input.path, input.method, input.catid,
584
+ input.status, input.tags, input.desc,
585
+ input.reqQuery, input.reqHeaders, input.reqParams,
586
+ input.reqBodyType, input.reqBody, input.reqBodyForm,
587
+ input.resBodyType, input.resBody,
588
+ input.markdown, input.apiOpened, input.customFieldValue,
589
+ input.switchNotice, input.message,
590
+ ];
591
+ if (provided.every((v) => v === undefined || v === '')) {
592
+ throw new CliValidationError('至少需要提供 title/path/method/catid/status/tags/desc 或请求/响应相关参数中的一个');
593
+ }
594
+ const tag = input.tags !== undefined
595
+ ? input.tags
596
+ .split(',')
597
+ .map((t) => t.trim())
598
+ .filter((t) => t.length > 0)
599
+ : undefined;
600
+ let reqQuery;
601
+ let reqHeaders;
602
+ let reqParams;
603
+ let reqBodyForm;
604
+ try {
605
+ reqQuery = parseJsonArrayParam(input.reqQuery);
606
+ reqHeaders = parseJsonArrayParam(input.reqHeaders);
607
+ reqParams = parseJsonArrayParam(input.reqParams);
608
+ reqBodyForm = parseJsonArrayParam(input.reqBodyForm);
609
+ }
610
+ catch (err) {
611
+ throw new CliValidationError(err instanceof Error ? err.message : String(err));
612
+ }
613
+ const { api } = createYApiServices();
614
+ const result = await api.updateInterface({
615
+ id: input.interfaceId,
616
+ title: input.title,
617
+ path: input.path,
618
+ method: input.method,
619
+ catid: input.catid,
620
+ status: input.status,
621
+ tag,
622
+ desc: input.desc,
623
+ req_query: reqQuery,
624
+ req_headers: reqHeaders,
625
+ req_params: reqParams,
626
+ req_body_type: input.reqBodyType,
627
+ req_body_form: reqBodyForm,
628
+ req_body_other: input.reqBody,
629
+ res_body_type: input.resBodyType,
630
+ res_body: input.resBody,
631
+ markdown: input.markdown,
632
+ api_opened: input.apiOpened,
633
+ custom_field_value: input.customFieldValue,
634
+ switch_notice: input.switchNotice,
635
+ message: input.message,
636
+ ...buildJsonSchemaFlags(input.reqBodyType, input.resBodyType, !!input.reqBody),
637
+ });
638
+ if (result.errcode !== 0) {
639
+ throw new CliError(result.errmsg ?? '更新接口失败', 14, 'API_ERROR');
640
+ }
641
+ return jsonResult({
642
+ interfaceId: input.interfaceId,
643
+ action: 'update',
644
+ message: `接口 ${input.interfaceId} 已更新`,
645
+ });
646
+ });
647
+ }, '更新接口(部分更新,未传字段保持原值)', { costHint: 'medium', nextBestTools: ['interface-get', 'interface-list', 'interface-save'] });
463
648
  // ==================== 保存接口(upsert) ====================
464
649
  registry.tool('interface-save', {
465
650
  ...projectIdSchema,
466
- interfaceId: z.coerce.number().optional().describe('接口 ID,可选。提供则走更新分支,不提供则走创建分支。CLI 不校验 ID 是否已存在,服务端按 ID 命中后覆盖。'),
467
- catId: z.coerce.number().describe('接口所属分类 ID,必填。'),
468
- title: z.string().describe('接口标题,必填。'),
469
- path: z.string().describe('接口路径,必填,例如 /api/user/list。建议以 / 开头。'),
651
+ interfaceId: z.number().int().positive().optional().describe('接口 ID,可选。提供则走更新分支,不提供则走创建分支。'),
652
+ catId: z.number().int().positive().describe('接口所属分类 ID,必填。'),
653
+ title: z.string().trim().min(1).describe('接口标题,必填。'),
654
+ path: z.string().trim().min(1).describe('接口路径,必填,例如 /api/user/list。建议以 / 开头。'),
470
655
  method: httpMethodEnum.default('GET').describe('HTTP 方法,必填枚举,默认 GET。'),
471
656
  status: z.enum(['done', 'undone']).optional().default('undone').describe('开发状态,可选枚举,默认 undone。'),
472
657
  tags: z.string().optional().describe('标签,可选,逗号分隔字符串。CLI 会按逗号拆分并 trim 后整体写入。'),
473
658
  desc: optionalTrimmedText.describe('接口描述,可选。'),
659
+ reqQuery: z.string().optional().describe('请求查询参数 JSON 数组字符串,可选。例如 \'[{"name":"page","example":"1","required":"1"}]\'。'),
660
+ reqHeaders: z.string().optional().describe('请求头 JSON 数组字符串,可选。'),
661
+ reqParams: z.string().optional().describe('URL 路径参数 JSON 数组字符串,可选。'),
474
662
  reqBodyType: z.string().optional().describe('请求体类型,可选,常用 json / form。'),
663
+ reqBodyForm: z.string().optional().describe('请求体表单字段 JSON 数组字符串,可选。'),
475
664
  reqBody: optionalTrimmedText.describe('请求体示例,可选,reqBodyType 为 json 时建议传入合法 JSON。'),
476
665
  resBodyType: z.string().optional().describe('响应体类型,可选,常用 json。'),
477
666
  resBody: optionalTrimmedText.describe('响应体示例,可选 JSON 字符串。'),
667
+ markdown: z.string().optional().describe('接口文档 Markdown 字符串,可选。'),
668
+ apiOpened: z.boolean().optional().describe('是否对外开放此接口,可选。'),
669
+ customFieldValue: z.string().optional().describe('自定义字段值,可选。'),
670
+ ...confirmSchema,
478
671
  }, async (input) => {
479
- const tag = input.tags
480
- ? input.tags
481
- .split(',')
482
- .map((t) => t.trim())
483
- .filter((t) => t.length > 0)
484
- : undefined;
672
+ const payload = input;
485
673
  const { api } = createYApiServices();
486
- const result = await api.saveInterface({
487
- id: input.interfaceId,
488
- project_id: input.projectId,
489
- catid: input.catId,
490
- title: input.title,
491
- path: input.path,
492
- method: input.method,
493
- status: input.status,
494
- tag,
495
- desc: input.desc,
496
- req_body_type: input.reqBodyType,
497
- req_body_other: input.reqBody,
498
- res_body_type: input.resBodyType,
499
- res_body: input.resBody,
500
- });
501
- return jsonResult({
502
- interfaceId: result.data?._id ?? input.interfaceId,
503
- action: input.interfaceId ? 'update' : 'create',
504
- message: input.interfaceId
505
- ? `接口 ${input.interfaceId} 已保存`
506
- : `接口「${input.title}」创建成功`,
674
+ const styleHints = !input.interfaceId
675
+ ? await inferInterfaceStyle(api, input.projectId, input.catId, 5)
676
+ : null;
677
+ if (styleHints) {
678
+ payload._styleHints = {
679
+ commonPathPrefix: styleHints.commonPathPrefix,
680
+ commonMethod: styleHints.commonMethod,
681
+ reqBodyFields: styleHints.reqBodyFields,
682
+ resBodyFields: styleHints.resBodyFields,
683
+ catName: styleHints.catName,
684
+ };
685
+ }
686
+ return runWithPreview('interface-save', payload.confirm, payload, previewOrAssertWriteAllowed, async () => {
687
+ const tag = input.tags
688
+ ? input.tags
689
+ .split(',')
690
+ .map((t) => t.trim())
691
+ .filter((t) => t.length > 0)
692
+ : undefined;
693
+ let reqQuery;
694
+ let reqHeaders;
695
+ let reqParams;
696
+ let reqBodyForm;
697
+ try {
698
+ reqQuery = parseJsonArrayParam(input.reqQuery);
699
+ reqHeaders = parseJsonArrayParam(input.reqHeaders);
700
+ reqParams = parseJsonArrayParam(input.reqParams);
701
+ reqBodyForm = parseJsonArrayParam(input.reqBodyForm);
702
+ }
703
+ catch (err) {
704
+ throw new CliValidationError(err instanceof Error ? err.message : String(err));
705
+ }
706
+ const result = await api.saveInterface({
707
+ id: input.interfaceId,
708
+ project_id: input.projectId,
709
+ catid: input.catId,
710
+ title: input.title,
711
+ path: input.path,
712
+ method: input.method,
713
+ status: input.status,
714
+ tag,
715
+ desc: input.desc,
716
+ req_query: reqQuery,
717
+ req_headers: reqHeaders,
718
+ req_params: reqParams,
719
+ req_body_type: input.reqBodyType,
720
+ req_body_form: reqBodyForm,
721
+ req_body_other: input.reqBody,
722
+ res_body_type: input.resBodyType,
723
+ res_body: input.resBody,
724
+ markdown: input.markdown,
725
+ api_opened: input.apiOpened,
726
+ custom_field_value: input.customFieldValue,
727
+ ...buildJsonSchemaFlags(input.reqBodyType, input.resBodyType),
728
+ });
729
+ return jsonResult({
730
+ interfaceId: result.data?._id ?? input.interfaceId,
731
+ action: input.interfaceId ? 'update' : 'create',
732
+ styleHints,
733
+ message: input.interfaceId
734
+ ? `接口 ${input.interfaceId} 已保存`
735
+ : `接口「${input.title}」创建成功`,
736
+ });
507
737
  });
508
- }, '保存接口(存在则更新,不存在则创建)', { costHint: 'medium', nextBestTools: ['interface-get', 'interface-list'] });
738
+ }, '保存接口(存在则更新,不存在则创建;新建时自动分析目标分类风格)', { costHint: 'medium', nextBestTools: ['interface-get', 'interface-list'] });
509
739
  // ==================== 公开项目接口列表 ====================
510
740
  registry.tool('interface-list-public', {
511
741
  ...projectIdSchema,
512
742
  keyword: z.string().optional().describe('搜索关键词,可选。在公开项目的接口标题/路径上做模糊匹配,大小写敏感由服务端决定。'),
513
- page: z.coerce.number().optional().default(1).describe('页码,可选,从 1 开始,默认 1。'),
514
- limit: z.coerce.number().optional().default(20).describe('每页条数,可选,默认 20。建议不超过 50。'),
743
+ page: z.number().int().positive().optional().default(1).describe('页码,可选,从 1 开始,默认 1。'),
744
+ limit: z.number().int().positive().optional().default(20).describe('每页条数,可选,默认 20。建议不超过 50。'),
515
745
  }, async (input) => {
516
746
  const { api } = createYApiServices();
517
747
  const result = await api.listPublicInterfaces({
@@ -534,20 +764,24 @@ export function registerInterfaceCommands(registry) {
534
764
  }, '列出公开项目的接口(无需登录)', { costHint: 'low', nextBestTools: ['interface-list', 'interface-get'] });
535
765
  // ==================== schema 转 JSON ====================
536
766
  registry.tool('schema-to-json', {
537
- schema: z.string().describe('JSON Schema 字符串,必填。由 YApi 服务端转换为 JSON 示例,CLI 不做本地解析,空字符串会被 CLI 拒绝。'),
767
+ schema: z.string().trim().min(1).describe('JSON Schema 字符串,必填。由 YApi 服务端转换为 JSON 示例,CLI 不做本地解析,空字符串会被 CLI 拒绝。'),
768
+ required: z.boolean().optional().describe('是否生成必填字段的示例值,可选。默认按服务端逻辑(通常默认生成)。对应后端 schema2json 的 required 参数。'),
538
769
  }, async (input) => {
539
770
  if (input.schema.trim().length === 0) {
540
771
  throw new CliValidationError('schema 不能为空');
541
772
  }
542
773
  const { api } = createYApiServices();
543
- const result = await api.schemaToJson(input.schema);
774
+ const result = await api.schemaToJson({
775
+ schema: input.schema,
776
+ required: input.required,
777
+ });
544
778
  return jsonResult({
545
779
  schema: result.data,
546
780
  });
547
781
  }, '将 JSON Schema 转换为 JSON 示例', { costHint: 'low', nextBestTools: ['interface-create', 'interface-save'] });
548
782
  // ==================== 接口快照(一次调用聚合接口详情 + 最近变更) ====================
549
783
  registry.tool('interface-snapshot', {
550
- interfaceId: z.coerce.number().describe('接口 ID,必填。返回接口摘要信息、请求/响应摘要与最近变更日志,请求/响应体若为 json 会截断到 300 字符,其他类型仅返回长度信息。'),
784
+ interfaceId: z.number().int().positive().describe('接口 ID,必填。返回接口摘要信息、请求/响应摘要与最近变更日志,请求/响应体若为 json 会截断到 300 字符,其他类型仅返回长度信息。'),
551
785
  logLimit: z.coerce
552
786
  .number()
553
787
  .int()