apiskill 0.1.0
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/MCP.md +12 -0
- package/README.ja.md +108 -0
- package/README.ko.md +108 -0
- package/README.md +119 -0
- package/README.zh.md +119 -0
- package/dist/assets/index-DH0wsJCI.js +299 -0
- package/dist/assets/index-vocUDpcf.css +1 -0
- package/dist/index.html +13 -0
- package/docs/cli.ja.md +90 -0
- package/docs/cli.ko.md +90 -0
- package/docs/cli.md +117 -0
- package/docs/cli.zh.md +117 -0
- package/docs/mcp.ja.md +79 -0
- package/docs/mcp.ko.md +79 -0
- package/docs/mcp.md +79 -0
- package/docs/mcp.zh.md +79 -0
- package/docs/web.ja.md +44 -0
- package/docs/web.ko.md +44 -0
- package/docs/web.md +57 -0
- package/docs/web.zh.md +57 -0
- package/index.html +12 -0
- package/mcp-config.example.json +13 -0
- package/package.json +44 -0
- package/scripts/apiskill-cli.mjs +372 -0
- package/scripts/lib/apiskill-core.mjs +520 -0
- package/scripts/lib/mock-server.mjs +262 -0
- package/scripts/lib/openapi-importer.mjs +169 -0
- package/scripts/lib/openapi-store.mjs +542 -0
- package/scripts/mcp-server.mjs +408 -0
- package/skills/apiskill/SKILL.md +71 -0
- package/skills/apiskill/agents/openai.yaml +4 -0
- package/src/AddApiDialog.tsx +590 -0
- package/src/App.tsx +2570 -0
- package/src/DocumentVersionManager.tsx +264 -0
- package/src/main.tsx +10 -0
- package/src/manualApiConfig.ts +401 -0
- package/src/styles.css +2101 -0
- package/src/swagger.ts +664 -0
- package/src/types.ts +115 -0
- package/tsconfig.json +21 -0
- package/vite.config.ts +1380 -0
package/src/swagger.ts
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Endpoint,
|
|
3
|
+
FieldRow,
|
|
4
|
+
HttpMethod,
|
|
5
|
+
SwaggerDocument,
|
|
6
|
+
SwaggerOperation,
|
|
7
|
+
SwaggerParameter,
|
|
8
|
+
SwaggerSchema,
|
|
9
|
+
} from './types';
|
|
10
|
+
import { formatManualApiCliConfig } from './manualApiConfig';
|
|
11
|
+
import type { ManualApiFieldConfig, ManualApiOperationConfig, ManualApiResponseConfig } from './manualApiConfig';
|
|
12
|
+
|
|
13
|
+
export const IMPORT_API_URL = '/api/openapi/import';
|
|
14
|
+
export const DOCUMENT_CREATE_API_URL = '/api/openapi/document';
|
|
15
|
+
export const AUTH_CHECK_API_URL = '/api/openapi/auth-check';
|
|
16
|
+
export const CACHE_API_URL = '/api/openapi/cache';
|
|
17
|
+
export const VERSIONS_API_URL = '/api/openapi/versions';
|
|
18
|
+
export const VERSION_SELECT_API_URL = '/api/openapi/version-select';
|
|
19
|
+
export const VERSION_META_API_URL = '/api/openapi/version-meta';
|
|
20
|
+
export const VERSION_DELETE_API_URL = '/api/openapi/version-delete';
|
|
21
|
+
export const VERSION_EXPORT_API_URL = '/api/openapi/version-export';
|
|
22
|
+
export const MOCK_START_API_URL = '/api/mock/start';
|
|
23
|
+
export const MOCK_STATUS_API_URL = '/api/mock/status';
|
|
24
|
+
export const MOCK_STOP_API_URL = '/api/mock/stop';
|
|
25
|
+
export const CUSTOM_OPERATION_API_URL = '/api/openapi/custom-operation';
|
|
26
|
+
export const DELETE_OPERATION_API_URL = '/api/openapi/custom-operation/delete';
|
|
27
|
+
export const OPERATION_LINKS_API_URL = '/api/openapi/operation-links';
|
|
28
|
+
export const STORAGE_SYNC_KEY = 'apiskill.swagger.syncedAt.v1';
|
|
29
|
+
export const STORAGE_SOURCE_KEY = 'apiskill.swagger.sourceUrl.v1';
|
|
30
|
+
|
|
31
|
+
const HTTP_METHODS: HttpMethod[] = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'];
|
|
32
|
+
const MAX_DEPTH = 12;
|
|
33
|
+
|
|
34
|
+
export function listEndpoints(doc: SwaggerDocument): Endpoint[] {
|
|
35
|
+
const paths = doc.paths ?? {};
|
|
36
|
+
|
|
37
|
+
return Object.entries(paths)
|
|
38
|
+
.flatMap(([path, pathItem]) =>
|
|
39
|
+
HTTP_METHODS.flatMap((method) => {
|
|
40
|
+
const operation = pathItem?.[method];
|
|
41
|
+
if (!operation) return [];
|
|
42
|
+
|
|
43
|
+
const tags = operation.tags?.length ? operation.tags : ['未分组'];
|
|
44
|
+
const summary = operation.summary || operation.description || '未命名接口';
|
|
45
|
+
const params = operation.parameters ?? [];
|
|
46
|
+
const searchable = [
|
|
47
|
+
method,
|
|
48
|
+
path,
|
|
49
|
+
summary,
|
|
50
|
+
operation.description,
|
|
51
|
+
operation.operationId,
|
|
52
|
+
tags.join(' '),
|
|
53
|
+
params.map((item) => `${item.name} ${item.description ?? ''}`).join(' '),
|
|
54
|
+
]
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join(' ')
|
|
57
|
+
.toLowerCase();
|
|
58
|
+
|
|
59
|
+
return [
|
|
60
|
+
{
|
|
61
|
+
id: `${method.toUpperCase()} ${path}`,
|
|
62
|
+
path,
|
|
63
|
+
method,
|
|
64
|
+
operation,
|
|
65
|
+
tags,
|
|
66
|
+
summary,
|
|
67
|
+
hasBody: operationHasBody(operation),
|
|
68
|
+
searchable,
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function listTags(endpoints: Endpoint[]) {
|
|
77
|
+
const tagMap = new Map<string, number>();
|
|
78
|
+
endpoints.forEach((endpoint) => {
|
|
79
|
+
endpoint.tags.forEach((tag) => tagMap.set(tag, (tagMap.get(tag) ?? 0) + 1));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return [...tagMap.entries()]
|
|
83
|
+
.map(([name, count]) => ({ name, count }))
|
|
84
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getSchemaName(ref?: string) {
|
|
88
|
+
if (!ref) return '';
|
|
89
|
+
return decodeURIComponent(ref.split('/').pop() ?? ref);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function resolveSchema(doc: SwaggerDocument, schema?: SwaggerSchema, seen = new Set<string>()): SwaggerSchema | undefined {
|
|
93
|
+
if (!schema) return undefined;
|
|
94
|
+
|
|
95
|
+
if (schema.$ref) {
|
|
96
|
+
if (seen.has(schema.$ref)) {
|
|
97
|
+
return {
|
|
98
|
+
type: 'object',
|
|
99
|
+
description: `循环引用: ${getSchemaName(schema.$ref)}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
seen.add(schema.$ref);
|
|
103
|
+
const resolved = schema.$ref.startsWith('#/')
|
|
104
|
+
? schema.$ref
|
|
105
|
+
.slice(2)
|
|
106
|
+
.split('/')
|
|
107
|
+
.reduce<unknown>((current, key) => {
|
|
108
|
+
if (current && typeof current === 'object') {
|
|
109
|
+
return (current as Record<string, unknown>)[decodeURIComponent(key)];
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}, doc)
|
|
113
|
+
: undefined;
|
|
114
|
+
return resolveSchema(doc, resolved as SwaggerSchema | undefined, seen);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (schema.allOf?.length) {
|
|
118
|
+
return mergeSchemas(doc, schema.allOf, schema);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return schema;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function mergeSchemas(doc: SwaggerDocument, schemas: SwaggerSchema[], base: SwaggerSchema): SwaggerSchema {
|
|
125
|
+
return schemas.reduce<SwaggerSchema>(
|
|
126
|
+
(merged, item) => {
|
|
127
|
+
const resolved = resolveSchema(doc, item) ?? {};
|
|
128
|
+
return {
|
|
129
|
+
...merged,
|
|
130
|
+
...resolved,
|
|
131
|
+
description: [merged.description, resolved.description].filter(Boolean).join(';') || merged.description,
|
|
132
|
+
required: [...new Set([...(merged.required ?? []), ...(resolved.required ?? [])])],
|
|
133
|
+
properties: {
|
|
134
|
+
...(merged.properties ?? {}),
|
|
135
|
+
...(resolved.properties ?? {}),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
{ ...base, allOf: undefined },
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function schemaType(doc: SwaggerDocument, schema?: SwaggerSchema): string {
|
|
144
|
+
if (!schema) return '-';
|
|
145
|
+
if (schema.$ref) return getSchemaName(schema.$ref);
|
|
146
|
+
|
|
147
|
+
const resolved = resolveSchema(doc, schema) ?? schema;
|
|
148
|
+
if (resolved.enum?.length) {
|
|
149
|
+
const enumText = resolved.enum.map(formatValue).join(' | ');
|
|
150
|
+
return `${resolved.type ?? 'enum'}<${enumText}>`;
|
|
151
|
+
}
|
|
152
|
+
if (resolved.type === 'array') return `${schemaType(doc, resolved.items)}[]`;
|
|
153
|
+
if (resolved.anyOf?.length) return resolved.anyOf.map((item) => schemaType(doc, item)).join(' | ');
|
|
154
|
+
if (resolved.oneOf?.length) return resolved.oneOf.map((item) => schemaType(doc, item)).join(' | ');
|
|
155
|
+
if (resolved.additionalProperties && typeof resolved.additionalProperties === 'object') {
|
|
156
|
+
return `Record<string, ${schemaType(doc, resolved.additionalProperties)}>`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return [resolved.type, resolved.format].filter(Boolean).join(':') || 'object';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function schemaDescription(schema?: SwaggerSchema) {
|
|
163
|
+
return schema?.description?.trim() || '';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function flattenSchema(
|
|
167
|
+
doc: SwaggerDocument,
|
|
168
|
+
schema?: SwaggerSchema,
|
|
169
|
+
options: {
|
|
170
|
+
prefix?: string;
|
|
171
|
+
requiredFields?: string[];
|
|
172
|
+
depth?: number;
|
|
173
|
+
seen?: Set<string>;
|
|
174
|
+
location?: string;
|
|
175
|
+
} = {},
|
|
176
|
+
): FieldRow[] {
|
|
177
|
+
const depth = options.depth ?? 0;
|
|
178
|
+
if (!schema || depth > MAX_DEPTH) return [];
|
|
179
|
+
|
|
180
|
+
if (schema.$ref) {
|
|
181
|
+
if (options.seen?.has(schema.$ref)) {
|
|
182
|
+
return [
|
|
183
|
+
{
|
|
184
|
+
name: options.prefix || getSchemaName(schema.$ref),
|
|
185
|
+
path: options.prefix || getSchemaName(schema.$ref),
|
|
186
|
+
location: options.location,
|
|
187
|
+
required: false,
|
|
188
|
+
type: getSchemaName(schema.$ref),
|
|
189
|
+
description: '循环引用,已停止展开',
|
|
190
|
+
defaultValue: '',
|
|
191
|
+
enumValue: '',
|
|
192
|
+
depth,
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
const nextSeen = new Set(options.seen ?? []);
|
|
197
|
+
nextSeen.add(schema.$ref);
|
|
198
|
+
return flattenSchema(doc, resolveSchema(doc, schema), { ...options, seen: nextSeen });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const resolved = resolveSchema(doc, schema) ?? schema;
|
|
202
|
+
const rows: FieldRow[] = [];
|
|
203
|
+
const properties = resolved.properties ?? {};
|
|
204
|
+
const requiredFields = resolved.required ?? options.requiredFields ?? [];
|
|
205
|
+
|
|
206
|
+
if (resolved.type === 'array') {
|
|
207
|
+
const name = options.prefix || '[]';
|
|
208
|
+
rows.push(toRow(doc, name, name, resolved, false, depth, options.location));
|
|
209
|
+
rows.push(
|
|
210
|
+
...flattenSchema(doc, resolved.items, {
|
|
211
|
+
...options,
|
|
212
|
+
prefix: `${name}[]`,
|
|
213
|
+
depth: depth + 1,
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
return rows;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (Object.keys(properties).length === 0) {
|
|
220
|
+
if (options.prefix) {
|
|
221
|
+
rows.push(toRow(doc, options.prefix.split('.').pop() ?? options.prefix, options.prefix, resolved, false, depth, options.location));
|
|
222
|
+
}
|
|
223
|
+
return rows;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
sortedEntries(properties).forEach(([name, property]) => {
|
|
227
|
+
const path = options.prefix ? `${options.prefix}.${name}` : name;
|
|
228
|
+
const propertyRequired = requiredFields.includes(name);
|
|
229
|
+
rows.push(toRow(doc, name, path, property, propertyRequired, depth, options.location));
|
|
230
|
+
|
|
231
|
+
const child = resolveSchema(doc, property) ?? property;
|
|
232
|
+
const childHasNested =
|
|
233
|
+
child.type === 'array' || Boolean(child.properties && Object.keys(child.properties).length) || Boolean(child.$ref);
|
|
234
|
+
if (childHasNested) {
|
|
235
|
+
if (child.type === 'array') {
|
|
236
|
+
const item = child.items;
|
|
237
|
+
const itemResolved = resolveSchema(doc, item) ?? item;
|
|
238
|
+
rows.push(
|
|
239
|
+
...flattenSchema(doc, item, {
|
|
240
|
+
...options,
|
|
241
|
+
prefix: `${path}[]`,
|
|
242
|
+
requiredFields: itemResolved?.required,
|
|
243
|
+
depth: depth + 1,
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
rows.push(
|
|
250
|
+
...flattenSchema(doc, property, {
|
|
251
|
+
...options,
|
|
252
|
+
prefix: path,
|
|
253
|
+
requiredFields: child.required,
|
|
254
|
+
depth: depth + 1,
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return rows;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function parameterRows(doc: SwaggerDocument, parameters: SwaggerParameter[] = []): FieldRow[] {
|
|
264
|
+
return sortFieldRows(parameters
|
|
265
|
+
.filter((parameter) => parameter.in !== 'body')
|
|
266
|
+
.map((parameter) => {
|
|
267
|
+
const schema = parameterSchema(parameter);
|
|
268
|
+
return {
|
|
269
|
+
name: parameter.name,
|
|
270
|
+
path: parameter.name,
|
|
271
|
+
location: parameter.in,
|
|
272
|
+
required: Boolean(parameter.required),
|
|
273
|
+
type: schemaType(doc, schema),
|
|
274
|
+
description: parameter.description || schemaDescription(schema) || '',
|
|
275
|
+
defaultValue: schema?.default === undefined ? '' : formatValue(schema.default),
|
|
276
|
+
enumValue: schema?.enum?.map(formatValue).join(', ') ?? '',
|
|
277
|
+
depth: 0,
|
|
278
|
+
};
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function requestBodyRows(doc: SwaggerDocument, operation: SwaggerOperation): Record<string, FieldRow[]> {
|
|
283
|
+
const content = operation.requestBody?.content ?? {};
|
|
284
|
+
const openApiRows = Object.fromEntries(
|
|
285
|
+
Object.entries(content).map(([mediaType, media]) => [
|
|
286
|
+
mediaType,
|
|
287
|
+
sortFieldRows(flattenSchema(doc, media.schema).map((row) => ({
|
|
288
|
+
...row,
|
|
289
|
+
location: 'body',
|
|
290
|
+
}))),
|
|
291
|
+
]),
|
|
292
|
+
);
|
|
293
|
+
if (Object.keys(openApiRows).length) return openApiRows;
|
|
294
|
+
|
|
295
|
+
const bodyParameter = operation.parameters?.find((parameter) => parameter.in === 'body');
|
|
296
|
+
if (!bodyParameter?.schema) return {};
|
|
297
|
+
const mediaTypes = operation.consumes?.length ? operation.consumes : ['application/json'];
|
|
298
|
+
return Object.fromEntries(
|
|
299
|
+
mediaTypes.map((mediaType) => [
|
|
300
|
+
mediaType,
|
|
301
|
+
sortFieldRows(flattenSchema(doc, bodyParameter.schema).map((row) => ({
|
|
302
|
+
...row,
|
|
303
|
+
location: 'body',
|
|
304
|
+
}))),
|
|
305
|
+
]),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function responseRows(doc: SwaggerDocument, operation: SwaggerOperation): Record<string, Record<string, FieldRow[]>> {
|
|
310
|
+
const responses = operation.responses ?? {};
|
|
311
|
+
return Object.fromEntries(
|
|
312
|
+
Object.entries(responses).map(([status, response]) => {
|
|
313
|
+
const content = response.content ?? {};
|
|
314
|
+
const openApiRows = Object.fromEntries(
|
|
315
|
+
Object.entries(content).map(([mediaType, media]) => [
|
|
316
|
+
mediaType,
|
|
317
|
+
sortFieldRows(flattenSchema(doc, media.schema).map((row) => ({
|
|
318
|
+
...row,
|
|
319
|
+
location: `response ${status}`,
|
|
320
|
+
}))),
|
|
321
|
+
]),
|
|
322
|
+
);
|
|
323
|
+
if (Object.keys(openApiRows).length) return [status, openApiRows];
|
|
324
|
+
|
|
325
|
+
const schema = response.schema ?? response.responseSchema;
|
|
326
|
+
if (!schema) return [status, {}];
|
|
327
|
+
const mediaTypes = operation.produces?.length ? operation.produces : ['application/json'];
|
|
328
|
+
return [
|
|
329
|
+
status,
|
|
330
|
+
Object.fromEntries(
|
|
331
|
+
mediaTypes.map((mediaType) => [
|
|
332
|
+
mediaType,
|
|
333
|
+
sortFieldRows(flattenSchema(doc, schema).map((row) => ({
|
|
334
|
+
...row,
|
|
335
|
+
location: `response ${status}`,
|
|
336
|
+
}))),
|
|
337
|
+
]),
|
|
338
|
+
),
|
|
339
|
+
];
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function buildAiMarkdown(doc: SwaggerDocument, endpoint: Endpoint) {
|
|
345
|
+
const op = endpoint.operation;
|
|
346
|
+
const parameterText = rowsToMarkdown(parameterRows(doc, op.parameters), '无参数');
|
|
347
|
+
const requestBodies = requestBodyRows(doc, op);
|
|
348
|
+
const responses = responseRows(doc, op);
|
|
349
|
+
|
|
350
|
+
const requestText = Object.keys(requestBodies).length
|
|
351
|
+
? Object.entries(requestBodies)
|
|
352
|
+
.map(([mediaType, rows]) => `### Request Body: ${mediaType}\n${rowsToMarkdown(rows, '空对象或无字段说明')}`)
|
|
353
|
+
.join('\n\n')
|
|
354
|
+
: '### Request Body\n无请求体';
|
|
355
|
+
|
|
356
|
+
const responseText = Object.entries(op.responses ?? {})
|
|
357
|
+
.map(([status, response]) => {
|
|
358
|
+
const medias = responses[status] ?? {};
|
|
359
|
+
const rows = Object.entries(medias);
|
|
360
|
+
if (!rows.length) {
|
|
361
|
+
return `### Response ${status}\n- Description: ${response.description || '无'}\n- Fields: 无字段说明`;
|
|
362
|
+
}
|
|
363
|
+
return rows
|
|
364
|
+
.map(
|
|
365
|
+
([mediaType, fields]) =>
|
|
366
|
+
`### Response ${status}: ${mediaType}\n- Description: ${response.description || '无'}\n${rowsToMarkdown(fields, '空对象或无字段说明')}`,
|
|
367
|
+
)
|
|
368
|
+
.join('\n\n');
|
|
369
|
+
})
|
|
370
|
+
.join('\n\n');
|
|
371
|
+
|
|
372
|
+
return [
|
|
373
|
+
`# API: ${endpoint.method.toUpperCase()} ${endpoint.path}`,
|
|
374
|
+
`- Summary: ${endpoint.summary}`,
|
|
375
|
+
`- Tags: ${endpoint.tags.join(', ')}`,
|
|
376
|
+
op.operationId ? `- OperationId: ${op.operationId}` : '',
|
|
377
|
+
op.description ? `- Description: ${op.description}` : '',
|
|
378
|
+
'',
|
|
379
|
+
'## Request Parameters',
|
|
380
|
+
parameterText,
|
|
381
|
+
'',
|
|
382
|
+
requestText,
|
|
383
|
+
'',
|
|
384
|
+
'## Responses',
|
|
385
|
+
responseText || '无响应说明',
|
|
386
|
+
]
|
|
387
|
+
.filter((line) => line !== '')
|
|
388
|
+
.join('\n');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function endpointToManualApiOperationConfig(doc: SwaggerDocument, endpoint: Endpoint): ManualApiOperationConfig {
|
|
392
|
+
const op = endpoint.operation;
|
|
393
|
+
const firstRequestBody = firstRequestBodySchema(op);
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
method: endpoint.method,
|
|
397
|
+
path: endpoint.path,
|
|
398
|
+
summary: endpoint.summary,
|
|
399
|
+
description: op.description || '',
|
|
400
|
+
operationId: op.operationId || '',
|
|
401
|
+
tags: endpoint.tags,
|
|
402
|
+
parameters: parameterRows(doc, op.parameters).map(rowToManualField),
|
|
403
|
+
requestBody: {
|
|
404
|
+
required: firstRequestBody?.required ?? false,
|
|
405
|
+
contentType: firstRequestBody?.contentType || 'application/json',
|
|
406
|
+
fields: firstRequestBody?.schema ? schemaToManualFields(doc, firstRequestBody.schema) : [],
|
|
407
|
+
},
|
|
408
|
+
responses: Object.entries(op.responses ?? {}).map(([status, response]) => {
|
|
409
|
+
const firstMedia = firstResponseSchema(op, response);
|
|
410
|
+
return {
|
|
411
|
+
status,
|
|
412
|
+
description: response.description || '',
|
|
413
|
+
contentType: firstMedia?.contentType || 'application/json',
|
|
414
|
+
fields: firstMedia?.schema ? schemaToManualFields(doc, firstMedia.schema) : [],
|
|
415
|
+
};
|
|
416
|
+
}),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function buildApiCliText(doc: SwaggerDocument, endpoint: Endpoint) {
|
|
421
|
+
return formatManualApiCliConfig(endpointToManualApiOperationConfig(doc, endpoint));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function buildRawSchemaJson(endpoint: Endpoint) {
|
|
425
|
+
return JSON.stringify(
|
|
426
|
+
{
|
|
427
|
+
method: endpoint.method.toUpperCase(),
|
|
428
|
+
path: endpoint.path,
|
|
429
|
+
tags: endpoint.tags,
|
|
430
|
+
summary: endpoint.summary,
|
|
431
|
+
operation: endpoint.operation,
|
|
432
|
+
},
|
|
433
|
+
null,
|
|
434
|
+
2,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function rowToManualField(row: FieldRow): ManualApiFieldConfig {
|
|
439
|
+
return {
|
|
440
|
+
name: row.path || row.name,
|
|
441
|
+
location: normalizeFieldLocation(row.location),
|
|
442
|
+
required: row.required,
|
|
443
|
+
type: normalizeFieldType(row.type),
|
|
444
|
+
description: row.description || '',
|
|
445
|
+
defaultValue: row.defaultValue || '',
|
|
446
|
+
enumValue: row.enumValue || '',
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function firstRequestBodySchema(operation: SwaggerOperation) {
|
|
451
|
+
const content = operation.requestBody?.content ?? {};
|
|
452
|
+
const firstContent = Object.entries(content)[0];
|
|
453
|
+
if (firstContent?.[1].schema) {
|
|
454
|
+
return {
|
|
455
|
+
required: Boolean(operation.requestBody?.required),
|
|
456
|
+
contentType: firstContent[0],
|
|
457
|
+
schema: firstContent[1].schema,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const bodyParameter = operation.parameters?.find((parameter) => parameter.in === 'body');
|
|
462
|
+
if (!bodyParameter?.schema) return undefined;
|
|
463
|
+
return {
|
|
464
|
+
required: Boolean(bodyParameter.required),
|
|
465
|
+
contentType: operation.consumes?.find((mediaType) => mediaType.includes('json')) ?? operation.consumes?.[0] ?? 'application/json',
|
|
466
|
+
schema: bodyParameter.schema,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function firstResponseSchema(
|
|
471
|
+
operation: SwaggerOperation,
|
|
472
|
+
response: NonNullable<SwaggerOperation['responses']>[string],
|
|
473
|
+
) {
|
|
474
|
+
const firstContent = Object.entries(response.content ?? {})[0];
|
|
475
|
+
if (firstContent?.[1].schema) {
|
|
476
|
+
return {
|
|
477
|
+
contentType: firstContent[0],
|
|
478
|
+
schema: firstContent[1].schema,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const schema = response.schema ?? response.responseSchema;
|
|
483
|
+
if (!schema) return undefined;
|
|
484
|
+
return {
|
|
485
|
+
contentType: operation.produces?.find((mediaType) => mediaType.includes('json')) ?? operation.produces?.[0] ?? 'application/json',
|
|
486
|
+
schema,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function schemaToManualFields(doc: SwaggerDocument, schema: SwaggerSchema, seen = new Set<string>()): ManualApiFieldConfig[] {
|
|
491
|
+
const resolved = resolveSchema(doc, schema) ?? schema;
|
|
492
|
+
if (schema.$ref) {
|
|
493
|
+
if (seen.has(schema.$ref)) return [];
|
|
494
|
+
const nextSeen = new Set(seen);
|
|
495
|
+
nextSeen.add(schema.$ref);
|
|
496
|
+
return schemaToManualFields(doc, resolved, nextSeen);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (resolved.type === 'array') {
|
|
500
|
+
return [
|
|
501
|
+
{
|
|
502
|
+
name: 'items',
|
|
503
|
+
location: 'body',
|
|
504
|
+
type: 'array',
|
|
505
|
+
description: schemaDescription(schema) || schemaDescription(resolved),
|
|
506
|
+
children: resolved.items ? schemaToManualFields(doc, resolved.items, seen) : [],
|
|
507
|
+
},
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return Object.entries(resolved.properties ?? {}).map(([name, property]) =>
|
|
512
|
+
schemaPropertyToManualField(doc, name, property, (resolved.required ?? []).includes(name), seen),
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function schemaPropertyToManualField(
|
|
517
|
+
doc: SwaggerDocument,
|
|
518
|
+
name: string,
|
|
519
|
+
schema: SwaggerSchema,
|
|
520
|
+
required: boolean,
|
|
521
|
+
seen: Set<string>,
|
|
522
|
+
): ManualApiFieldConfig {
|
|
523
|
+
const resolved = resolveSchema(doc, schema) ?? schema;
|
|
524
|
+
const type = manualFieldType(doc, schema);
|
|
525
|
+
const field: ManualApiFieldConfig = {
|
|
526
|
+
name,
|
|
527
|
+
location: 'body',
|
|
528
|
+
required,
|
|
529
|
+
type,
|
|
530
|
+
format: resolved.format || schema.format || '',
|
|
531
|
+
description: schemaDescription(schema) || schemaDescription(resolved),
|
|
532
|
+
defaultValue: resolved.default === undefined ? '' : formatValue(resolved.default),
|
|
533
|
+
enumValue: resolved.enum?.map(formatValue).join(', ') ?? '',
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
if (type === 'object') {
|
|
537
|
+
field.children = schema.$ref && seen.has(schema.$ref) ? [] : schemaToManualFields(doc, resolved, nextSeen(schema, seen));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (type === 'array') {
|
|
541
|
+
const item = resolved.items;
|
|
542
|
+
field.children = item ? schemaToManualFields(doc, item, nextSeen(schema, seen)) : [];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return field;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function nextSeen(schema: SwaggerSchema, seen: Set<string>) {
|
|
549
|
+
if (!schema.$ref) return seen;
|
|
550
|
+
const next = new Set(seen);
|
|
551
|
+
next.add(schema.$ref);
|
|
552
|
+
return next;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function manualFieldType(doc: SwaggerDocument, schema: SwaggerSchema) {
|
|
556
|
+
const resolved = resolveSchema(doc, schema) ?? schema;
|
|
557
|
+
if (resolved.type === 'array') return 'array';
|
|
558
|
+
if (resolved.type === 'object' || resolved.properties || schema.$ref) return 'object';
|
|
559
|
+
if (resolved.type === 'integer') return 'integer';
|
|
560
|
+
if (resolved.type === 'number') return 'number';
|
|
561
|
+
if (resolved.type === 'boolean') return 'boolean';
|
|
562
|
+
return 'string';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function normalizeFieldLocation(value?: string): ManualApiFieldConfig['location'] {
|
|
566
|
+
if (value === 'path' || value === 'query' || value === 'header' || value === 'cookie' || value === 'body') return value;
|
|
567
|
+
return 'body';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function normalizeFieldType(value: string) {
|
|
571
|
+
const normalized = value.toLowerCase();
|
|
572
|
+
if (normalized.includes('[]') || normalized.includes('array')) return 'array';
|
|
573
|
+
if (normalized.includes('object')) return 'object';
|
|
574
|
+
if (normalized.includes('integer')) return 'integer';
|
|
575
|
+
if (normalized.includes('number')) return 'number';
|
|
576
|
+
if (normalized.includes('boolean')) return 'boolean';
|
|
577
|
+
return 'string';
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function operationHasBody(operation: SwaggerOperation) {
|
|
581
|
+
return Boolean(operation.requestBody) || Boolean(operation.parameters?.some((parameter) => parameter.in === 'body'));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function sortedEntries<T>(record: Record<string, T>) {
|
|
585
|
+
return Object.entries(record).sort(([left], [right]) => left.localeCompare(right, undefined, { sensitivity: 'base' }));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function sortFieldRows(rows: FieldRow[]) {
|
|
589
|
+
return [...rows].sort((left, right) => {
|
|
590
|
+
const leftKey = left.path || left.name;
|
|
591
|
+
const rightKey = right.path || right.name;
|
|
592
|
+
return leftKey.localeCompare(rightKey, undefined, { sensitivity: 'base' });
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function parameterSchema(parameter: SwaggerParameter): SwaggerSchema | undefined {
|
|
597
|
+
if (parameter.schema) return parameter.schema;
|
|
598
|
+
if (!parameter.type && !parameter.format && !parameter.items && !parameter.enum?.length && parameter.default === undefined) return undefined;
|
|
599
|
+
return {
|
|
600
|
+
type: parameter.type,
|
|
601
|
+
format: parameter.format,
|
|
602
|
+
items: parameter.items,
|
|
603
|
+
enum: parameter.enum,
|
|
604
|
+
default: parameter.default,
|
|
605
|
+
description: parameter.description,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function rowsToMarkdown(rows: FieldRow[], emptyText: string) {
|
|
610
|
+
if (!rows.length) return emptyText;
|
|
611
|
+
|
|
612
|
+
return [
|
|
613
|
+
'| 字段 | 位置 | 必填 | 类型 | 默认值 | 枚举 | 说明 |',
|
|
614
|
+
'| --- | --- | --- | --- | --- | --- | --- |',
|
|
615
|
+
...rows.map((row) => {
|
|
616
|
+
const indent = row.depth > 0 ? `${' '.repeat(row.depth)}${row.name}` : row.name;
|
|
617
|
+
return [
|
|
618
|
+
escapeCell(indent),
|
|
619
|
+
escapeCell(row.location || '-'),
|
|
620
|
+
row.required ? '是' : '否',
|
|
621
|
+
escapeCell(row.type),
|
|
622
|
+
escapeCell(row.defaultValue || '-'),
|
|
623
|
+
escapeCell(row.enumValue || '-'),
|
|
624
|
+
escapeCell(row.description || '-'),
|
|
625
|
+
].join(' | ');
|
|
626
|
+
}),
|
|
627
|
+
]
|
|
628
|
+
.map((line) => (line.startsWith('|') ? line : `| ${line} |`))
|
|
629
|
+
.join('\n');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function toRow(
|
|
633
|
+
doc: SwaggerDocument,
|
|
634
|
+
name: string,
|
|
635
|
+
path: string,
|
|
636
|
+
schema: SwaggerSchema,
|
|
637
|
+
required: boolean,
|
|
638
|
+
depth: number,
|
|
639
|
+
location?: string,
|
|
640
|
+
): FieldRow {
|
|
641
|
+
const resolved = resolveSchema(doc, schema) ?? schema;
|
|
642
|
+
return {
|
|
643
|
+
name,
|
|
644
|
+
path,
|
|
645
|
+
location,
|
|
646
|
+
required,
|
|
647
|
+
type: schemaType(doc, schema),
|
|
648
|
+
description: schemaDescription(schema) || schemaDescription(resolved),
|
|
649
|
+
defaultValue: resolved.default === undefined ? '' : formatValue(resolved.default),
|
|
650
|
+
enumValue: resolved.enum?.map(formatValue).join(', ') ?? '',
|
|
651
|
+
depth,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function formatValue(value: unknown) {
|
|
656
|
+
if (value === undefined || value === null) return '';
|
|
657
|
+
if (typeof value === 'string') return value;
|
|
658
|
+
const json = JSON.stringify(value);
|
|
659
|
+
return json === undefined ? String(value) : json;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function escapeCell(value: unknown) {
|
|
663
|
+
return String(value ?? '').replace(/\|/g, '\\|').replace(/\n/g, '<br />');
|
|
664
|
+
}
|