@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,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 {
|
|
5
|
-
import {
|
|
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.
|
|
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.
|
|
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
|
|
84
|
+
reqBody: iface.req_body_other || undefined,
|
|
84
85
|
resBodyType: iface.res_body_type,
|
|
85
|
-
resBody: iface.res_body
|
|
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.
|
|
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 =
|
|
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.
|
|
143
|
-
title: z.string().describe('接口标题,必填,YApi
|
|
144
|
-
path: z.string().describe('接口路径,必填,例如 /api/user/list
|
|
145
|
-
method: httpMethodEnum.default('GET').describe('HTTP 方法,必填枚举,默认 GET。可选值 GET/POST/PUT/DELETE/PATCH。'),
|
|
146
|
-
desc: optionalTrimmedText.describe('
|
|
147
|
-
status: z.enum(['done', 'undone']).optional().default('undone').describe('开发状态,可选枚举 done/undone,默认 undone。done
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
}, '
|
|
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.
|
|
190
|
-
projectId: z.
|
|
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.
|
|
248
|
-
index: z.
|
|
307
|
+
id: z.number().int().positive().describe('接口 ID,必填。'),
|
|
308
|
+
index: z.number().int().positive().describe('新排序索引,必填整数。在所属分类内的相对排序,YApi 通常按数字升序展示,越小越靠前。'),
|
|
309
|
+
...confirmSchema,
|
|
249
310
|
}, async (input) => {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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.
|
|
267
|
-
index: z.
|
|
329
|
+
catid: z.number().int().positive().describe('分类 ID,必填。'),
|
|
330
|
+
index: z.number().int().positive().describe('分类的新排序索引,必填整数。作用于项目内分类列表,YApi 按升序展示,越小越靠前。'),
|
|
331
|
+
...confirmSchema,
|
|
268
332
|
}, async (input) => {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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.
|
|
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
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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.
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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.
|
|
401
|
-
name: z.string().optional().describe('新名称,可选。name
|
|
482
|
+
catId: z.number().int().positive().describe('分类 ID,必填。'),
|
|
483
|
+
name: z.string().optional().describe('新名称,可选。name 和 desc 至少传一个,否则 CLI 会拒绝请求。'),
|
|
402
484
|
desc: z.string().optional().describe('新描述,可选。'),
|
|
403
|
-
|
|
485
|
+
...confirmSchema,
|
|
404
486
|
}, async (input) => {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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.
|
|
506
|
+
catId: z.number().int().positive().describe('分类 ID,必填。删除会同时清理该分类下所有接口,YApi 服务端不会二次确认,建议先用 interface-list-by-cat 查看影响。'),
|
|
507
|
+
...confirmSchema,
|
|
423
508
|
}, async (input) => {
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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.
|
|
522
|
+
interfaceId: z.number().int().positive().describe('接口 ID,必填。删除不可恢复,YApi 同时会清理关联的测试集合用例。'),
|
|
523
|
+
...confirmSchema,
|
|
435
524
|
}, async (input) => {
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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.
|
|
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.
|
|
467
|
-
catId: z.
|
|
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
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
}, '
|
|
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.
|
|
514
|
-
limit: z.
|
|
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(
|
|
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.
|
|
784
|
+
interfaceId: z.number().int().positive().describe('接口 ID,必填。返回接口摘要信息、请求/响应摘要与最近变更日志,请求/响应体若为 json 会截断到 300 字符,其他类型仅返回长度信息。'),
|
|
551
785
|
logLimit: z.coerce
|
|
552
786
|
.number()
|
|
553
787
|
.int()
|