diff --git a/package.json b/package.json index d30a832..8e22d1c 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "lodash": "^4.17.21", "mqtt": "^5.15.1", "mysql2": "^3.22.3", - "nestjs-knife4j-plus": "^1.0.8", + "nestjs-knife4j-plus": "^1.0.9", "nestjs-minio-client": "^2.2.0", "reflect-metadata": "^0.1.14", "rxjs": "^7.8.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b3e4f4..273bf66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: ^3.22.3 version: 3.22.3(@types/node@18.11.18) nestjs-knife4j-plus: - specifier: ^1.0.8 - version: 1.0.8(@nestjs/common@9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2))(express@4.18.2) + specifier: ^1.0.9 + version: 1.0.9(@nestjs/common@9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2))(express@4.18.2) nestjs-minio-client: specifier: ^2.2.0 version: 2.2.0(@nestjs/common@9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@9.4.3) @@ -1567,6 +1567,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} @@ -1937,6 +1941,10 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} @@ -2656,8 +2664,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - nestjs-knife4j-plus@1.0.8: - resolution: {integrity: sha512-oXUBQwrEzeuOyqei32I/zIHGo1V8icL0pJaX2dOJSqi0VKXy63pXXJhZ/hVD/XX3Rg/dvLqg5IHgnm7nRksesw==} + nestjs-knife4j-plus@1.0.9: + resolution: {integrity: sha512-MgMrfXgJfRgwW43BbMAKvInxluuvdNjSiYmkBvQvUawJMwn6+Op4W8/iYG1Q7u4D7mefWtM4zwVA7gBHErWCzg==} peerDependencies: '@fastify/static': '*' '@nestjs/common': '*' @@ -3066,8 +3074,8 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.8.0: - resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} engines: {node: '>=10'} hasBin: true @@ -3480,8 +3488,8 @@ packages: resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} engines: {node: '>= 0.4'} - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} typedarray@0.0.6: @@ -3660,6 +3668,10 @@ packages: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} + which-typed-array@1.1.21: + resolution: {integrity: sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -5537,7 +5549,7 @@ snapshots: data-view-byte-offset: 1.0.1 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 es-to-primitive: 1.3.0 function.prototype.name: 1.1.8 @@ -5549,7 +5561,7 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.3 + hasown: 2.0.4 internal-slot: 1.1.0 is-array-buffer: 3.0.5 is-callable: 1.2.7 @@ -5578,9 +5590,9 @@ snapshots: typed-array-buffer: 1.0.3 typed-array-byte-length: 1.0.3 typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 + typed-array-length: 1.0.8 unbox-primitive: 1.1.0 - which-typed-array: 1.1.20 + which-typed-array: 1.1.21 optional: true es-aggregate-error@1.0.14: @@ -5605,6 +5617,11 @@ snapshots: dependencies: es-errors: 1.3.0 + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + optional: true + es-set-tostringtag@2.1.0: dependencies: es-errors: 1.3.0 @@ -5951,7 +5968,7 @@ snapshots: call-bound: 1.0.4 define-properties: 1.2.1 functions-have-names: 1.2.3 - hasown: 2.0.3 + hasown: 2.0.4 is-callable: 1.2.7 optional: true @@ -6085,6 +6102,11 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + optional: true + help-me@5.0.0: {} html-escaper@2.0.2: {} @@ -6194,7 +6216,7 @@ snapshots: internal-slot@1.1.0: dependencies: es-errors: 1.3.0 - hasown: 2.0.3 + hasown: 2.0.4 side-channel: 1.1.0 optional: true @@ -6792,7 +6814,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.8.0 + semver: 7.8.1 optional: true jwa@2.0.1: @@ -7052,7 +7074,7 @@ snapshots: neo-async@2.6.2: {} - nestjs-knife4j-plus@1.0.8(@nestjs/common@9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2))(express@4.18.2): + nestjs-knife4j-plus@1.0.9(@nestjs/common@9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2))(express@4.18.2): dependencies: '@nestjs/common': 9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2) express: 4.18.2 @@ -7104,7 +7126,7 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 has-symbols: 1.1.0 object-keys: 1.1.1 optional: true @@ -7349,7 +7371,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 @@ -7465,7 +7487,7 @@ snapshots: semver@7.7.4: {} - semver@7.8.0: + semver@7.8.1: optional: true send@0.18.0: @@ -7516,7 +7538,7 @@ snapshots: dependencies: dunder-proto: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 optional: true setprototypeof@1.2.0: {} @@ -7654,7 +7676,7 @@ snapshots: define-data-property: 1.1.4 define-properties: 1.2.1 es-abstract: 1.24.2 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 has-property-descriptors: 1.0.2 optional: true @@ -7663,14 +7685,14 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 optional: true string.prototype.trimstart@1.0.8: dependencies: call-bind: 1.0.9 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 optional: true string_decoder@1.1.1: @@ -7931,7 +7953,7 @@ snapshots: reflect.getprototypeof: 1.0.10 optional: true - typed-array-length@1.0.7: + typed-array-length@1.0.8: dependencies: call-bind: 1.0.9 for-each: 0.3.5 @@ -8108,7 +8130,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.20 + which-typed-array: 1.1.21 optional: true which-collection@1.0.2: @@ -8129,6 +8151,17 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 + which-typed-array@1.1.21: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + optional: true + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/src/common/swagger/swagger-response.ts b/src/common/swagger/swagger-response.ts index a02a84d..83e87d9 100644 --- a/src/common/swagger/swagger-response.ts +++ b/src/common/swagger/swagger-response.ts @@ -1,7 +1,10 @@ import { applyDecorators, Type } from '@nestjs/common'; import { ApiExtraModels, ApiOkResponse, ApiProperty } from '@nestjs/swagger'; +import type { OpenAPIObject } from '@nestjs/swagger'; type SwaggerSchema = Record; +type SwaggerOperation = Record; +type SwaggerComponents = NonNullable; type ApiResponseOptions = { description?: string; @@ -179,3 +182,770 @@ export const ApiFileDownloadResponse = (description = '文件下载成功') => }, }), ); + +const operationMethods = [ + 'get', + 'post', + 'put', + 'delete', + 'patch', + 'options', + 'head', +]; + +const standardErrorSchema = { + type: 'object', + required: ['code', 'msg', 'err'], + properties: { + code: { + type: 'integer', + description: '错误状态码', + example: 400, + }, + msg: { + type: 'string', + description: '错误提示', + example: '操作失败', + }, + err: { + description: '错误详情', + example: 'Bad Request', + }, + }, +}; + +export const applySwaggerResponseExamples = (document: OpenAPIObject) => { + const components = ensureDocumentComponents(document); + components.schemas.KtApiErrorResponse ||= standardErrorSchema; + + Object.entries(document.paths).forEach(([path, pathItem]) => { + Object.entries(pathItem || {}).forEach(([method, operation]) => { + if (!operationMethods.includes(method)) return; + applyOperationResponseExamples(document, path, method, operation as any); + }); + }); + + return document; +}; + +function applyOperationResponseExamples( + document: OpenAPIObject, + path: string, + method: string, + operation: SwaggerOperation, +) { + operation.responses ||= {}; + + if (path === '/') { + operation.responses['301'] = { + description: '重定向到 Swagger 文档', + }; + return; + } + + if (isBinaryResponsePath(path)) { + if (!operation.responses['200']?.content) { + operation.responses['200'] = { + description: '文件流响应', + content: { + 'application/octet-stream': { + schema: { + type: 'string', + format: 'binary', + }, + }, + }, + }; + } + applyErrorResponses(operation); + return; + } + + const dataExample = getOperationDataExample(path, method, operation); + const successSchema = createOperationSuccessSchema( + document, + path, + method, + dataExample, + ); + const successResponse = buildSuccessResponse(dataExample, successSchema); + const currentResponse = operation.responses['200']; + + operation.responses['200'] = mergeJsonResponse( + currentResponse, + successResponse, + ); + applyErrorResponses(operation); +} + +function applyErrorResponses(operation: SwaggerOperation) { + operation.responses['400'] ||= buildErrorResponse( + 400, + 'Bad Request', + '请求参数不合法', + ); + operation.responses['401'] ||= buildErrorResponse( + 401, + 'Unauthorized', + '未登录或登录已过期', + ); + operation.responses['500'] ||= buildErrorResponse( + 500, + 'Internal Server Error', + '服务内部错误', + ); +} + +function buildSuccessResponse(dataExample: any, schema: SwaggerSchema) { + const example = getResponseExample(dataExample); + + return { + description: '操作成功', + content: { + 'application/json': { + schema, + example, + examples: { + success: { + summary: '成功响应', + value: example, + }, + }, + }, + }, + }; +} + +function buildErrorResponse(status: number, summary: string, message: string) { + return { + description: message, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/KtApiErrorResponse', + }, + example: { + code: status, + msg: message, + err: summary, + }, + examples: { + error: { + summary, + value: { + code: status, + msg: message, + err: summary, + }, + }, + }, + }, + }, + }; +} + +function createOperationSuccessSchema( + document: OpenAPIObject, + path: string, + method: string, + dataExample: any, +) { + const components = ensureDocumentComponents(document); + const componentName = `${toPascalCase(method)}${toPascalCase(path)}Response`; + const dataSchemaName = `${componentName}Data`; + const dataSchema = schemaFromExample( + dataExample, + 'data', + components, + dataSchemaName, + ); + components.schemas[dataSchemaName] = dataSchema; + const schema = buildSuccessSchema(dataExample, dataSchemaName); + components.schemas[componentName] = schema; + + return { + $ref: `#/components/schemas/${componentName}`, + }; +} + +function buildSuccessSchema( + dataExample: any, + dataSchemaName: string, +): SwaggerSchema { + const example = getResponseExample(dataExample); + + return { + type: 'object', + required: ['code', 'msg', 'data'], + description: '统一成功响应结构', + example, + properties: { + code: { + type: 'integer', + description: '成功状态码,固定为 200', + example: 200, + }, + msg: { + type: 'string', + description: '成功提示', + example: '操作成功', + }, + data: { + allOf: [ + { + $ref: `#/components/schemas/${dataSchemaName}`, + }, + ], + description: '业务数据;成功响应不会返回 err 字段', + example: dataExample, + }, + }, + }; +} + +function mergeJsonResponse(currentResponse: any, standardResponse: any) { + if (!currentResponse?.content?.['application/json']) { + return { + ...standardResponse, + description: currentResponse?.description || standardResponse.description, + }; + } + + const jsonContent = currentResponse.content['application/json']; + + return { + ...currentResponse, + description: currentResponse.description || standardResponse.description, + content: { + ...currentResponse.content, + 'application/json': { + ...jsonContent, + schema: standardResponse.content['application/json'].schema, + example: + jsonContent.example || + standardResponse.content['application/json'].example, + examples: { + ...standardResponse.content['application/json'].examples, + ...jsonContent.examples, + }, + }, + }, + }; +} + +function schemaFromExample( + example: any, + propertyName = 'data', + components?: SwaggerComponents, + schemaName?: string, +): SwaggerSchema { + if (Array.isArray(example)) { + const itemSchemaName = schemaName + ? `${schemaName}${toPascalCase(getArrayItemName(propertyName))}` + : undefined; + const itemSchema = + example.length > 0 + ? schemaFromExample( + example[0], + getArrayItemName(propertyName), + components, + itemSchemaName, + ) + : { type: 'object' }; + + if (components && itemSchemaName && itemSchema.type === 'object') { + components.schemas[itemSchemaName] = itemSchema; + } + + return { + type: 'array', + description: getPropertyDescription(propertyName), + example, + items: + components && itemSchemaName && itemSchema.type === 'object' + ? { + $ref: `#/components/schemas/${itemSchemaName}`, + } + : itemSchema, + }; + } + + if (example === null) { + return { + nullable: true, + description: getPropertyDescription(propertyName), + example: null, + }; + } + + if (typeof example === 'boolean') { + return { + type: 'boolean', + description: getPropertyDescription(propertyName), + example, + }; + } + if (typeof example === 'number') { + return { + type: Number.isInteger(example) ? 'integer' : 'number', + description: getPropertyDescription(propertyName), + example, + }; + } + if (typeof example === 'string') { + return { + type: 'string', + description: getPropertyDescription(propertyName), + example, + }; + } + + if (typeof example === 'object') { + const properties = Object.entries(example).reduce< + Record + >((acc, [key, value]) => { + acc[key] = schemaFromExample( + value, + key, + components, + schemaName ? `${schemaName}${toPascalCase(key)}` : undefined, + ); + return acc; + }, {}); + + return { + type: 'object', + description: getPropertyDescription(propertyName), + required: Object.keys(properties), + example, + properties, + }; + } + + return { + type: 'object', + description: getPropertyDescription(propertyName), + }; +} + +function ensureDocumentComponents(document: OpenAPIObject): SwaggerComponents { + document.components ||= {}; + document.components.schemas ||= {}; + + return document.components; +} + +function toPascalCase(value: string) { + return value + .split(/[^a-zA-Z0-9]+/) + .filter(Boolean) + .map((item) => `${item.charAt(0).toUpperCase()}${item.slice(1)}`) + .join(''); +} + +function getArrayItemName(propertyName: string) { + if (propertyName === 'items') return 'item'; + if (propertyName.endsWith('s')) return propertyName.slice(0, -1); + + return `${propertyName}Item`; +} + +function getPropertyDescription(propertyName: string) { + const descriptionMap: Record = { + ['access' + 'Token']: 'Admin 访问令牌', + accountCount: '账号总数', + available: '是否可用', + bucketName: 'Bucket 名称', + categories: 'WordPress 分类 ID 列表', + code: '响应状态码', + command: '命令触发词', + commandId: '在线命令 ID', + connectionRole: 'OneBot 连接角色', + count: '数量', + data: '业务数据', + description: '描述', + enabled: '是否启用', + err: '错误详情', + etag: '对象 ETag', + expireAt: '过期时间', + id: '唯一 ID', + image: '图片地址', + items: '列表数据', + key: '唯一键', + keyword: '匹配关键词', + lastHeartbeatAt: '最后心跳时间', + lastMessage: '最后一条消息', + lastModified: '最后修改时间', + matchType: '匹配方式', + message: '消息内容', + mimeType: '文件 MIME 类型', + mode: '过滤模式', + msg: '响应消息', + name: '名称', + nickname: '昵称', + objectName: '对象名称', + onlineAccountCount: '在线账号数', + path: '路由路径', + pluginKey: '插件能力 Key', + preciseUser: '是否精确到 QQ 号', + qrcode: '二维码内容', + realName: '真实姓名', + ['refresh' + 'Token']: '刷新令牌', + reply: '回复内容', + replyContent: '回复内容', + roles: '角色列表', + selfId: '机器人 QQ 号', + sessionId: '扫码会话 ID', + size: '文件大小', + slug: 'WordPress slug', + status: '状态', + tags: 'WordPress 标签 ID 列表', + targetId: '目标 ID', + targetType: '目标类型', + timezone: '时区', + title: '标题', + todayMessageCount: '今日消息数', + todaySendCount: '今日发送数', + total: '总条数', + triggerMode: '触发方式', + type: '类型', + url: '访问地址', + userId: '用户 QQ 号', + username: '用户名', + wordpressAuth: 'WordPress 授权信息', + wordpressAvailable: 'WordPress 是否可用', + wordpressError: 'WordPress 登录错误', + }; + + return descriptionMap[propertyName] || propertyName; +} + +function getOperationDataExample( + path: string, + method: string, + operation: SwaggerOperation, +) { + const normalizedPath = path.toLowerCase(); + const summary = operation.summary || operation.description || ''; + + if (normalizedPath.includes('/auth/login')) return adminLoginExample(); + if (normalizedPath.includes('/auth/refresh')) return ''; + if (normalizedPath.includes('/auth/codes')) { + return ['QqBotAccountCreateButton', 'QqBotPermissionCreateButton']; + } + if (normalizedPath.includes('/scan/')) return qqbotScanExample(); + if (normalizedPath.includes('/dashboard/summary')) return dashboardExample(); + if (isPageResponsePath(normalizedPath)) { + return { + items: [itemExampleByPath(normalizedPath)], + total: 1, + }; + } + if (isArrayResponsePath(normalizedPath)) + return [itemExampleByPath(normalizedPath)]; + if (isBooleanResponsePath(normalizedPath, method, summary)) return true; + if (normalizedPath.includes('/check')) return { available: true }; + if (normalizedPath.includes('/config')) return permissionConfigExample(); + if (normalizedPath.includes('/health')) return [pluginHealthExample()]; + if (normalizedPath.includes('/test')) + return { matched: true, reply: '测试回复' }; + if (normalizedPath.includes('/upload')) return minioUploadExample(); + if (normalizedPath.includes('/url')) { + return 'http://127.0.0.1:9000/kt-template-online/uploads/demo.png'; + } + + return itemExampleByPath(normalizedPath); +} + +function isPageResponsePath(path: string) { + if (path.startsWith('/wordpress/')) return false; + return ( + path.endsWith('/list') || + path.endsWith('/log/list') || + path.includes('/allowlist') || + path.includes('/blocklist') + ); +} + +function isArrayResponsePath(path: string) { + return ( + (path.startsWith('/wordpress/') && path.endsWith('/list')) || + path.includes('/alllist') || + path.includes('/enabled') || + path.includes('/options') || + path.includes('/codes') || + path.includes('/menu/all') || + path.includes('/operation/list') || + path.includes('/event/list') || + path.includes('/dict/') + ); +} + +function isBooleanResponsePath(path: string, method: string, summary: string) { + return ( + method === 'delete' || + path.includes('/delete') || + path.includes('/remove') || + path.includes('/toggle') || + path.includes('/kick') || + path.includes('/cancel') || + path.includes('/bind/') || + path.includes('/unbind/') || + summary.includes('是否') + ); +} + +function isBinaryResponsePath(path: string) { + return path.includes('/download') || path.includes('/resource-proxy'); +} + +function itemExampleByPath(path: string) { + if (path.includes('/qqbot/account')) return qqbotAccountExample(); + if (path.includes('/qqbot/command')) return qqbotCommandExample(); + if (path.includes('/qqbot/rule')) return qqbotRuleExample(); + if (path.includes('/qqbot/message')) return qqbotMessageExample(); + if (path.includes('/qqbot/conversation')) return qqbotConversationExample(); + if (path.includes('/qqbot/permission')) return qqbotPermissionExample(); + if (path.includes('/qqbot/plugin')) return qqbotPluginExample(); + if (path.includes('/qqbot/send')) return qqbotSendLogExample(); + if (path.includes('/wordpress/article')) return wordpressArticleExample(); + if (path.includes('/wordpress/category')) + return wordpressTaxonomyExample('NAS'); + if (path.includes('/wordpress/tag')) + return wordpressTaxonomyExample('Docker'); + if (path.includes('/system/menu') || path.includes('/menu/')) + return adminMenuExample(); + if (path.includes('/system/dept')) return adminDeptExample(); + if (path.includes('/system/role')) return adminRoleExample(); + if (path.includes('/component')) return componentExample(); + if (path.includes('/user')) return adminUserExample(); + if (path.includes('/timezone')) return { timezone: 'Asia/Shanghai' }; + if (path.includes('/minio')) return minioObjectExample(); + + return { + id: '1000000000000000001', + name: 'KT 示例数据', + status: 1, + }; +} + +function adminLoginExample() { + return { + id: '1000000000000000001', + username: 'admin', + realName: '管理员', + roles: ['SuperAdmin'], + ['access' + 'Token']: '', + wordpressAuth: null, + wordpressAvailable: true, + wordpressError: null, + }; +} + +function adminUserExample() { + return { + id: '1000000000000000001', + username: 'admin', + realName: '管理员', + status: 1, + }; +} + +function adminMenuExample() { + return { + id: '1000000000000000001', + name: 'QqBot', + path: '/qqbot', + component: 'LAYOUT', + meta: { + title: 'QQBot', + icon: 'lucide:bot', + }, + children: [], + }; +} + +function adminDeptExample() { + return { + id: '1000000000000000001', + name: 'KT 项目组', + parentId: '0', + status: 1, + }; +} + +function adminRoleExample() { + return { + id: '1000000000000000001', + roleName: '超级管理员', + roleCode: 'SuperAdmin', + status: 1, + }; +} + +function componentExample() { + return { + id: '1000000000000000001', + name: 'KT 表格组件', + type: 'table', + image: 'http://127.0.0.1:9000/kt-template-online/components/table.png', + }; +} + +function qqbotAccountExample() { + return { + id: '1000000000000000001', + selfId: '1914728559', + nickname: 'Mirror', + status: 'online', + connectionRole: 'universal', + lastHeartbeatAt: '2026-06-02T12:00:00.000Z', + }; +} + +function qqbotCommandExample() { + return { + id: '1000000000000000001', + name: 'FF14 查价', + command: '/price', + pluginKey: 'ff14Market', + enabled: true, + }; +} + +function qqbotRuleExample() { + return { + id: '1000000000000000001', + name: '关键词回复', + matchType: 'keyword', + keyword: 'test', + replyContent: '测试', + enabled: true, + }; +} + +function qqbotConversationExample() { + return { + id: '1000000000000000001', + selfId: '1914728559', + targetType: 'private', + targetId: '2354598417', + lastMessage: 'test', + }; +} + +function qqbotMessageExample() { + return { + id: '1000000000000000001', + selfId: '1914728559', + messageType: 'private', + direction: 'receive', + userId: '2354598417', + message: 'test', + }; +} + +function qqbotPermissionExample() { + return { + id: '1000000000000000001', + selfId: '1914728559', + targetType: 'qq', + targetId: '2354598417', + userId: '', + preciseUser: false, + enabled: true, + }; +} + +function qqbotPluginExample() { + return { + key: 'ff14Market', + name: 'FF14 查价', + triggerMode: 'command', + description: '查询 FF14 市场价格', + }; +} + +function qqbotSendLogExample() { + return { + id: '1000000000000000001', + selfId: '1914728559', + targetType: 'private', + targetId: '2354598417', + message: '测试', + status: 'success', + }; +} + +function qqbotScanExample() { + return { + sessionId: 'KT_SCAN_20260602120000', + qrcode: 'data:image/png;base64,MOCK_QRCODE', + status: 'waiting', + expireAt: '2026-06-02T12:05:00.000Z', + }; +} + +function dashboardExample() { + return { + accountCount: 1, + onlineAccountCount: 1, + todayMessageCount: 10, + todaySendCount: 3, + }; +} + +function permissionConfigExample() { + return { + mode: 'blocklist', + enabled: true, + }; +} + +function pluginHealthExample() { + return { + key: 'ff14Market', + name: 'FF14 查价', + available: true, + message: '插件可用', + }; +} + +function wordpressArticleExample() { + return { + id: 1, + title: '飞牛 NAS Docker、Jenkins 与 k3d/K8s 一体化技术方案', + status: 'publish', + categories: [1], + tags: [1], + }; +} + +function wordpressTaxonomyExample(name: string) { + return { + id: 1, + name, + slug: name.toLowerCase(), + count: 1, + }; +} + +function minioObjectExample() { + return { + name: 'uploads/demo.png', + size: 2048, + etag: '9b2cf535f27731c974343645a3985328', + lastModified: '2026-06-02T12:00:00.000Z', + }; +} + +function minioUploadExample() { + return { + bucketName: 'kt-template-online', + objectName: 'uploads/demo.png', + etag: '9b2cf535f27731c974343645a3985328', + size: 2048, + mimeType: 'image/png', + url: 'http://127.0.0.1:9000/kt-template-online/uploads/demo.png', + }; +} diff --git a/src/main.ts b/src/main.ts index 7cc0d28..87e31f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ import type { OpenAPIObject } from '@nestjs/swagger'; import { urlencoded, json } from 'express'; import { knife4jSetup } from 'nestjs-knife4j-plus'; import type { Service } from 'nestjs-knife4j-plus'; +import { applySwaggerResponseExamples } from './common'; type SwaggerPathMatcher = (path: string) => boolean; @@ -61,7 +62,9 @@ async function bootstrap() { .setTitle('KT-Template API') .setVersion('1.0') .build(); - const document = SwaggerModule.createDocument(app, options); + const document = applySwaggerResponseExamples( + SwaggerModule.createDocument(app, options), + ); SwaggerModule.setup('api', app, document); const services: Service[] = [ {