diff --git a/package.json b/package.json index f8563c1593abe..9c20866613489 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "nanoid": "^5", "next": "^14.0.2", "openai": "^4.17.3", + "openapi-jsonschema-parameters": "^12.1.3", "polished": "^4", "posthog-js": "^1", "query-string": "^8", @@ -117,6 +118,7 @@ "remark-html": "^15", "semver": "^7", "sharp": "^0.33", + "swagger-client": "^3.24.5", "swr": "^2", "systemjs": "^6", "ts-md5": "^1", diff --git a/src/locales/default/plugin.ts b/src/locales/default/plugin.ts index cb3a91843d171..3b9d5bc46612b 100644 --- a/src/locales/default/plugin.ts +++ b/src/locales/default/plugin.ts @@ -91,8 +91,9 @@ export default { error: { fetchError: '请求该 manifest 链接失败,请确保链接的有效性,并检查链接是否允许跨域访问', installError: '插件 {{name}} 安装失败', - manifestInvalid: ' manifest 不符合规范,校验结果: \n\n {{error}}', + manifestInvalid: 'manifest 不符合规范,校验结果: \n\n {{error}}', noManifest: '描述文件不存在', + openAPIInvalid: 'OpenAPI 解析失败,错误: \n\n {{error}}', reinstallError: '插件 {{name}} 刷新失败', urlError: '该链接没有返回 JSON 格式的内容, 请确保是有效的链接', }, diff --git a/src/services/__tests__/__snapshots__/plugin.test.ts.snap b/src/services/__tests__/__snapshots__/plugin.test.ts.snap new file mode 100644 index 0000000000000..5b74908da3882 --- /dev/null +++ b/src/services/__tests__/__snapshots__/plugin.test.ts.snap @@ -0,0 +1,347 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`PluginService > convertOpenAPIToPluginSchema > can convert OpenAPI v2 MJ openAPI 1`] = ` +[ + { + "description": "查询所有账号", + "name": "listUsingGET", + "parameters": { + "properties": {}, + "type": "object", + }, + }, + { + "description": "指定ID获取账号", + "name": "fetchUsingGET", + "parameters": { + "properties": { + "id": { + "description": "账号ID", + "type": "string", + }, + }, + "required": [], + "type": "object", + }, + }, + { + "description": "提交Blend任务", + "name": "blendUsingPOST", + "parameters": { + "$$ref": "http://localhost:3000/#/definitions/Blend提交参数", + "properties": { + "base64Array": { + "description": "图片base64数组", + "example": [ + "data:image/png;base64,xxx1", + "data:image/png;base64,xxx2", + ], + "items": { + "type": "string", + }, + "refType": "string", + "type": "array", + }, + "dimensions": { + "description": "比例: PORTRAIT(2:3); SQUARE(1:1); LANDSCAPE(3:2)", + "enum": [ + "PORTRAIT", + "SQUARE", + "LANDSCAPE", + ], + "example": "SQUARE", + "refType": null, + "type": "string", + }, + "notifyHook": { + "description": "回调地址, 为空时使用全局notifyHook", + "refType": null, + "type": "string", + }, + "state": { + "description": "自定义参数", + "refType": null, + "type": "string", + }, + }, + "required": [ + "base64Array", + ], + "title": "Blend提交参数", + "type": "object", + }, + }, + { + "description": "绘图变化", + "name": "changeUsingPOST", + "parameters": { + "$$ref": "http://localhost:3000/#/definitions/变化任务提交参数", + "properties": { + "action": { + "description": "UPSCALE(放大); VARIATION(变换); REROLL(重新生成)", + "enum": [ + "UPSCALE", + "VARIATION", + "REROLL", + ], + "example": "UPSCALE", + "type": "string", + }, + "index": { + "description": "序号(1~4), action为UPSCALE,VARIATION时必传", + "example": 1, + "exclusiveMaximum": false, + "exclusiveMinimum": false, + "format": "int32", + "maximum": 4, + "minimum": 1, + "type": "integer", + }, + "notifyHook": { + "description": "回调地址, 为空时使用全局notifyHook", + "type": "string", + }, + "state": { + "description": "自定义参数", + "type": "string", + }, + "taskId": { + "description": "任务ID", + "example": "1320098173412546", + "type": "string", + }, + }, + "required": [ + "action", + "taskId", + ], + "title": "变化任务提交参数", + "type": "object", + }, + }, + { + "description": "提交Describe任务", + "name": "describeUsingPOST", + "parameters": { + "$$ref": "http://localhost:3000/#/definitions/Describe提交参数", + "properties": { + "base64": { + "description": "图片base64", + "example": "data:image/png;base64,xxx", + "type": "string", + }, + "notifyHook": { + "description": "回调地址, 为空时使用全局notifyHook", + "type": "string", + }, + "state": { + "description": "自定义参数", + "type": "string", + }, + }, + "required": [ + "base64", + ], + "title": "Describe提交参数", + "type": "object", + }, + }, + { + "description": "提交Imagine任务", + "name": "imagineUsingPOST", + "parameters": { + "$$ref": "http://localhost:3000/#/definitions/Imagine提交参数", + "properties": { + "base64Array": { + "description": "垫图base64数组", + "items": { + "type": "string", + }, + "type": "array", + }, + "notifyHook": { + "description": "回调地址, 为空时使用全局notifyHook", + "type": "string", + }, + "prompt": { + "description": "提示词", + "example": "Cat", + "type": "string", + }, + "state": { + "description": "自定义参数", + "type": "string", + }, + }, + "required": [ + "prompt", + ], + "title": "Imagine提交参数", + "type": "object", + }, + }, + { + "description": "绘图变化-simple", + "name": "simpleChangeUsingPOST", + "parameters": { + "$$ref": "http://localhost:3000/#/definitions/变化任务提交参数-simple", + "properties": { + "content": { + "description": "变化描述: ID $action$index", + "example": "1320098173412546 U2", + "type": "string", + }, + "notifyHook": { + "description": "回调地址, 为空时使用全局notifyHook", + "type": "string", + }, + "state": { + "description": "自定义参数", + "type": "string", + }, + }, + "required": [ + "content", + ], + "title": "变化任务提交参数-simple", + "type": "object", + }, + }, + { + "description": "查询所有任务", + "name": "listUsingGET_1", + "parameters": { + "properties": {}, + "type": "object", + }, + }, + { + "description": "根据ID列表查询任务", + "name": "listByIdsUsingPOST", + "parameters": { + "$$ref": "http://localhost:3000/#/definitions/任务查询参数", + "properties": { + "ids": { + "items": { + "type": "string", + }, + "type": "array", + }, + }, + "title": "任务查询参数", + "type": "object", + }, + }, + { + "description": "查询任务队列", + "name": "queueUsingGET", + "parameters": { + "properties": {}, + "type": "object", + }, + }, + { + "description": "指定ID获取任务", + "name": "fetchUsingGET_1", + "parameters": { + "properties": { + "id": { + "description": "任务ID", + "type": "string", + }, + }, + "required": [], + "type": "object", + }, + }, +] +`; + +exports[`PluginService > convertOpenAPIToPluginSchema > can convert OpenAPI v3.1 to lobe apis 1`] = ` +[ + { + "description": "Read Course Segments", + "name": "read_course_segments_course_segments__get", + "parameters": { + "properties": {}, + "type": "object", + }, + }, + { + "description": "Read Problem Set Item", + "name": "read_problem_set_item_problem_set__problem_set_id___question_number__get", + "parameters": { + "properties": { + "problem_set_id": { + "title": "Problem Set Id", + "type": "integer", + }, + "question_number": { + "title": "Question Number", + "type": "integer", + }, + }, + "required": [ + "problem_set_id", + "question_number", + ], + "type": "object", + }, + }, + { + "description": "Read Random Problem Set Items", + "name": "read_random_problem_set_items_problem_set_random__problem_set_id___n_items__get", + "parameters": { + "properties": { + "n_items": { + "title": "N Items", + "type": "integer", + }, + "problem_set_id": { + "title": "Problem Set Id", + "type": "integer", + }, + }, + "required": [ + "problem_set_id", + "n_items", + ], + "type": "object", + }, + }, + { + "description": "Read Range Of Problem Set Items", + "name": "read_range_of_problem_set_items_problem_set_range__problem_set_id___start___end__get", + "parameters": { + "properties": { + "end": { + "title": "End", + "type": "integer", + }, + "problem_set_id": { + "title": "Problem Set Id", + "type": "integer", + }, + "start": { + "title": "Start", + "type": "integer", + }, + }, + "required": [ + "problem_set_id", + "start", + "end", + ], + "type": "object", + }, + }, + { + "description": "Read User Id", + "name": "read_user_id_user__get", + "parameters": { + "properties": {}, + "type": "object", + }, + }, +] +`; diff --git a/src/services/__tests__/openapi/OpenAPI_V2.json b/src/services/__tests__/openapi/OpenAPI_V2.json new file mode 100644 index 0000000000000..033805a7dff25 --- /dev/null +++ b/src/services/__tests__/openapi/OpenAPI_V2.json @@ -0,0 +1,1012 @@ +{ + "basePath": "/mj", + "definitions": { + "Blend提交参数": { + "type": "object", + "required": ["base64Array"], + "properties": { + "base64Array": { + "type": "array", + "example": ["data:image/png;base64,xxx1", "data:image/png;base64,xxx2"], + "description": "图片base64数组", + "items": { + "type": "string" + }, + "refType": "string" + }, + "dimensions": { + "type": "string", + "example": "SQUARE", + "description": "比例: PORTRAIT(2:3); SQUARE(1:1); LANDSCAPE(3:2)", + "enum": ["PORTRAIT", "SQUARE", "LANDSCAPE"], + "refType": null + }, + "notifyHook": { + "type": "string", + "description": "回调地址, 为空时使用全局notifyHook", + "refType": null + }, + "state": { + "type": "string", + "description": "自定义参数", + "refType": null + } + }, + "title": "Blend提交参数" + }, + "Describe提交参数": { + "type": "object", + "required": ["base64"], + "properties": { + "base64": { + "type": "string", + "example": "data:image/png;base64,xxx", + "description": "图片base64" + }, + "notifyHook": { + "type": "string", + "description": "回调地址, 为空时使用全局notifyHook" + }, + "state": { + "type": "string", + "description": "自定义参数" + } + }, + "title": "Describe提交参数" + }, + "Discord账号": { + "type": "object", + "properties": { + "channelId": { + "type": "string", + "description": "频道ID" + }, + "coreSize": { + "type": "integer", + "format": "int32", + "description": "并发数" + }, + "enable": { + "type": "boolean", + "description": "是否可用" + }, + "guildId": { + "type": "string", + "description": "服务器ID" + }, + "id": { + "type": "string", + "description": "ID" + }, + "properties": { + "type": "object" + }, + "queueSize": { + "type": "integer", + "format": "int32", + "description": "等待队列长度" + }, + "timeoutMinutes": { + "type": "integer", + "format": "int32", + "description": "任务超时时间(分钟)" + }, + "userAgent": { + "type": "string", + "description": "用户UserAgent" + }, + "userToken": { + "type": "string", + "description": "用户Token" + } + }, + "title": "Discord账号" + }, + "Imagine提交参数": { + "type": "object", + "required": ["prompt"], + "properties": { + "base64Array": { + "type": "array", + "description": "垫图base64数组", + "items": { + "type": "string" + } + }, + "notifyHook": { + "type": "string", + "description": "回调地址, 为空时使用全局notifyHook" + }, + "prompt": { + "type": "string", + "example": "Cat", + "description": "提示词" + }, + "state": { + "type": "string", + "description": "自定义参数" + } + }, + "title": "Imagine提交参数" + }, + "任务": { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "任务类型", + "enum": ["IMAGINE", "UPSCALE", "VARIATION", "REROLL", "DESCRIBE", "BLEND"] + }, + "description": { + "type": "string", + "description": "任务描述" + }, + "failReason": { + "type": "string", + "description": "失败原因" + }, + "finishTime": { + "type": "integer", + "format": "int64", + "description": "结束时间" + }, + "id": { + "type": "string", + "description": "ID" + }, + "imageUrl": { + "type": "string", + "description": "图片url" + }, + "progress": { + "type": "string", + "description": "任务进度" + }, + "prompt": { + "type": "string", + "description": "提示词" + }, + "promptEn": { + "type": "string", + "description": "提示词-英文" + }, + "properties": { + "type": "object" + }, + "startTime": { + "type": "integer", + "format": "int64", + "description": "开始执行时间" + }, + "state": { + "type": "string", + "description": "自定义参数" + }, + "status": { + "type": "string", + "description": "任务状态", + "enum": ["NOT_START", "SUBMITTED", "IN_PROGRESS", "FAILURE", "SUCCESS"] + }, + "submitTime": { + "type": "integer", + "format": "int64", + "description": "提交时间" + } + }, + "title": "任务" + }, + "任务查询参数": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "title": "任务查询参数" + }, + "变化任务提交参数": { + "type": "object", + "required": ["action", "taskId"], + "properties": { + "action": { + "type": "string", + "example": "UPSCALE", + "description": "UPSCALE(放大); VARIATION(变换); REROLL(重新生成)", + "enum": ["UPSCALE", "VARIATION", "REROLL"] + }, + "index": { + "type": "integer", + "format": "int32", + "example": 1, + "description": "序号(1~4), action为UPSCALE,VARIATION时必传", + "minimum": 1, + "maximum": 4, + "exclusiveMinimum": false, + "exclusiveMaximum": false + }, + "notifyHook": { + "type": "string", + "description": "回调地址, 为空时使用全局notifyHook" + }, + "state": { + "type": "string", + "description": "自定义参数" + }, + "taskId": { + "type": "string", + "example": "1320098173412546", + "description": "任务ID" + } + }, + "title": "变化任务提交参数" + }, + "变化任务提交参数-simple": { + "type": "object", + "required": ["content"], + "properties": { + "content": { + "type": "string", + "example": "1320098173412546 U2", + "description": "变化描述: ID $action$index" + }, + "notifyHook": { + "type": "string", + "description": "回调地址, 为空时使用全局notifyHook" + }, + "state": { + "type": "string", + "description": "自定义参数" + } + }, + "title": "变化任务提交参数-simple" + }, + "提交结果": { + "type": "object", + "required": ["code", "description"], + "properties": { + "code": { + "type": "integer", + "format": "int32", + "example": 1, + "description": "状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误)" + }, + "description": { + "type": "string", + "example": "提交成功", + "description": "描述" + }, + "properties": { + "type": "object", + "description": "扩展字段" + }, + "result": { + "type": "string", + "example": 1320098173412546, + "description": "任务ID" + } + }, + "title": "提交结果" + } + }, + "host": "localhost:8080", + "info": { + "description": "代理 MidJourney 的discord频道,实现api形式调用AI绘图", + "version": "v2.5.4", + "title": "Midjourney Proxy API文档", + "termsOfService": "https://github.com/novicezk/midjourney-proxy", + "contact": { + "name": "novicezk", + "url": "https://github.com/novicezk/midjourney-proxy" + } + }, + "paths": { + "/mj/account/list": { + "get": { + "tags": ["账号查询"], + "summary": "查询所有账号", + "operationId": "listUsingGET", + "produces": ["*/*"], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "originalRef": "Discord账号", + "$ref": "#/definitions/Discord账号" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "responsesObject": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "originalRef": "Discord账号", + "$ref": "#/definitions/Discord账号" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": false, + "x-order": "2147483647" + } + }, + "/mj/account/{id}/fetch": { + "get": { + "tags": ["账号查询"], + "summary": "指定ID获取账号", + "operationId": "fetchUsingGET", + "produces": ["*/*"], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "账号ID", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "originalRef": "Discord账号", + "$ref": "#/definitions/Discord账号" + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "responsesObject": { + "200": { + "description": "OK", + "schema": { + "originalRef": "Discord账号", + "$ref": "#/definitions/Discord账号" + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": false, + "x-order": "2147483647" + } + }, + "/mj/submit/blend": { + "post": { + "tags": ["任务提交"], + "summary": "提交Blend任务", + "operationId": "blendUsingPOST", + "consumes": ["application/json"], + "produces": ["*/*"], + "parameters": [ + { + "in": "body", + "name": "blendDTO", + "description": "blendDTO", + "required": true, + "schema": { + "originalRef": "Blend提交参数", + "$ref": "#/definitions/Blend提交参数" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "originalRef": "提交结果", + "$ref": "#/definitions/提交结果" + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "responsesObject": { + "200": { + "description": "OK", + "schema": { + "originalRef": "提交结果", + "$ref": "#/definitions/提交结果" + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": false, + "x-order": "2147483647" + } + }, + "/mj/submit/change": { + "post": { + "tags": ["任务提交"], + "summary": "绘图变化", + "operationId": "changeUsingPOST", + "consumes": ["application/json"], + "produces": ["*/*"], + "parameters": [ + { + "in": "body", + "name": "changeDTO", + "description": "changeDTO", + "required": true, + "schema": { + "originalRef": "变化任务提交参数", + "$ref": "#/definitions/变化任务提交参数" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "originalRef": "提交结果", + "$ref": "#/definitions/提交结果" + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "responsesObject": { + "200": { + "description": "OK", + "schema": { + "originalRef": "提交结果", + "$ref": "#/definitions/提交结果" + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": false, + "x-order": "2147483647" + } + }, + "/mj/submit/describe": { + "post": { + "tags": ["任务提交"], + "summary": "提交Describe任务", + "operationId": "describeUsingPOST", + "consumes": ["application/json"], + "produces": ["*/*"], + "parameters": [ + { + "in": "body", + "name": "describeDTO", + "description": "describeDTO", + "required": true, + "schema": { + "originalRef": "Describe提交参数", + "$ref": "#/definitions/Describe提交参数" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "originalRef": "提交结果", + "$ref": "#/definitions/提交结果" + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "responsesObject": { + "200": { + "description": "OK", + "schema": { + "originalRef": "提交结果", + "$ref": "#/definitions/提交结果" + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": false, + "x-order": "2147483647" + } + }, + "/mj/submit/imagine": { + "post": { + "tags": ["任务提交"], + "summary": "提交Imagine任务", + "operationId": "imagineUsingPOST", + "consumes": ["application/json"], + "produces": ["*/*"], + "parameters": [ + { + "in": "body", + "name": "imagineDTO", + "description": "imagineDTO", + "required": true, + "schema": { + "originalRef": "Imagine提交参数", + "$ref": "#/definitions/Imagine提交参数" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "originalRef": "提交结果", + "$ref": "#/definitions/提交结果" + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "responsesObject": { + "200": { + "description": "OK", + "schema": { + "originalRef": "提交结果", + "$ref": "#/definitions/提交结果" + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": false, + "x-order": "2147483647" + } + }, + "/mj/submit/simple-change": { + "post": { + "tags": ["任务提交"], + "summary": "绘图变化-simple", + "operationId": "simpleChangeUsingPOST", + "consumes": ["application/json"], + "produces": ["*/*"], + "parameters": [ + { + "in": "body", + "name": "simpleChangeDTO", + "description": "simpleChangeDTO", + "required": true, + "schema": { + "originalRef": "变化任务提交参数-simple", + "$ref": "#/definitions/变化任务提交参数-simple" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "originalRef": "提交结果", + "$ref": "#/definitions/提交结果" + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "responsesObject": { + "200": { + "description": "OK", + "schema": { + "originalRef": "提交结果", + "$ref": "#/definitions/提交结果" + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": false, + "x-order": "2147483647" + } + }, + "/mj/task/list": { + "get": { + "tags": ["任务查询"], + "summary": "查询所有任务", + "operationId": "listUsingGET_1", + "produces": ["*/*"], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "originalRef": "任务", + "$ref": "#/definitions/任务" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "responsesObject": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "originalRef": "任务", + "$ref": "#/definitions/任务" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": false, + "x-order": "2147483647" + } + }, + "/mj/task/list-by-condition": { + "post": { + "tags": ["任务查询"], + "summary": "根据ID列表查询任务", + "operationId": "listByIdsUsingPOST", + "consumes": ["application/json"], + "produces": ["*/*"], + "parameters": [ + { + "in": "body", + "name": "conditionDTO", + "description": "conditionDTO", + "required": true, + "schema": { + "originalRef": "任务查询参数", + "$ref": "#/definitions/任务查询参数" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "originalRef": "任务", + "$ref": "#/definitions/任务" + } + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "responsesObject": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "originalRef": "任务", + "$ref": "#/definitions/任务" + } + } + }, + "201": { + "description": "Created" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": false, + "x-order": "2147483647" + } + }, + "/mj/task/queue": { + "get": { + "tags": ["任务查询"], + "summary": "查询任务队列", + "operationId": "queueUsingGET", + "produces": ["*/*"], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "originalRef": "任务", + "$ref": "#/definitions/任务" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "responsesObject": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "originalRef": "任务", + "$ref": "#/definitions/任务" + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": false, + "x-order": "2147483647" + } + }, + "/mj/task/{id}/fetch": { + "get": { + "tags": ["任务查询"], + "summary": "指定ID获取任务", + "operationId": "fetchUsingGET_1", + "produces": ["*/*"], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "任务ID", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "originalRef": "任务", + "$ref": "#/definitions/任务" + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "responsesObject": { + "200": { + "description": "OK", + "schema": { + "originalRef": "任务", + "$ref": "#/definitions/任务" + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Not Found" + } + }, + "deprecated": false, + "x-order": "2147483647" + } + } + }, + "swagger": "2.0", + "tags": [ + { + "name": "任务提交", + "x-order": "2147483647" + }, + { + "name": "任务查询", + "x-order": "2147483647" + }, + { + "name": "账号查询", + "x-order": "2147483647" + } + ], + "x-openapi": { + "x-markdownFiles": null, + "x-setting": { + "language": "zh-CN", + "enableSwaggerModels": true, + "swaggerModelName": "Swagger Models", + "enableReloadCacheParameter": false, + "enableAfterScript": true, + "enableDocumentManage": true, + "enableVersion": false, + "enableRequestCache": true, + "enableFilterMultipartApis": false, + "enableFilterMultipartApiMethodType": "POST", + "enableHost": false, + "enableHostText": "", + "enableDynamicParameter": false, + "enableDebug": true, + "enableFooter": true, + "enableFooterCustom": false, + "footerCustomContent": null, + "enableSearch": true, + "enableOpenApi": true, + "enableHomeCustom": false, + "homeCustomLocation": null, + "enableGroup": true, + "enableResponseCode": true + } + } +} diff --git a/src/services/__tests__/openapi/OpenAPI_V3.json b/src/services/__tests__/openapi/OpenAPI_V3.json new file mode 100644 index 0000000000000..65423dd874265 --- /dev/null +++ b/src/services/__tests__/openapi/OpenAPI_V3.json @@ -0,0 +1,245 @@ +{ + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError" + } + }, + "securitySchemes": { + "HTTPBearer": { + "type": "http", + "scheme": "bearer" + } + } + }, + "paths": { + "/course-segments/": { + "get": { + "summary": "Read Course Segments", + "operationId": "read_course_segments_course_segments__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/problem-set/{problem_set_id}/{question_number}": { + "get": { + "summary": "Read Problem Set Item", + "operationId": "read_problem_set_item_problem_set__problem_set_id___question_number__get", + "parameters": [ + { + "name": "problem_set_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Problem Set Id" + } + }, + { + "name": "question_number", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Question Number" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/problem-set/random/{problem_set_id}/{n_items}": { + "get": { + "summary": "Read Random Problem Set Items", + "operationId": "read_random_problem_set_items_problem_set_random__problem_set_id___n_items__get", + "parameters": [ + { + "name": "problem_set_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Problem Set Id" + } + }, + { + "name": "n_items", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "N Items" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/problem-set/range/{problem_set_id}/{start}/{end}": { + "get": { + "summary": "Read Range Of Problem Set Items", + "operationId": "read_range_of_problem_set_items_problem_set_range__problem_set_id___start___end__get", + "parameters": [ + { + "name": "problem_set_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Problem Set Id" + } + }, + { + "name": "start", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Start" + } + }, + { + "name": "end", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "End" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/user/": { + "get": { + "summary": "Read User Id", + "operationId": "read_user_id_user__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + } + }, + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "openapi": "3.1.0" +} diff --git a/src/services/__tests__/plugin.test.ts b/src/services/__tests__/plugin.test.ts index 3d120c8b0008a..566b9eb2d689b 100644 --- a/src/services/__tests__/plugin.test.ts +++ b/src/services/__tests__/plugin.test.ts @@ -9,6 +9,8 @@ import { LobeTool } from '@/types/tool'; import { LobeToolCustomPlugin } from '@/types/tool/plugin'; import { InstallPluginParams, pluginService } from '../plugin'; +import OpenAPIV2 from './openapi/OpenAPI_V2.json'; +import openAPIV3 from './openapi/OpenAPI_V3.json'; // Mocking modules and functions vi.mock('@/const/url', () => ({ @@ -329,5 +331,17 @@ describe('PluginService', () => { }); }); - // Add any additional tests to cover edge cases or error handling as needed. + describe('convertOpenAPIToPluginSchema', () => { + it('can convert OpenAPI v3.1 to lobe apis', async () => { + const plugins = await pluginService.convertOpenAPIToPluginSchema(openAPIV3); + + expect(plugins).toMatchSnapshot(); + }); + + it('can convert OpenAPI v2 MJ openAPI', async () => { + const plugins = await pluginService.convertOpenAPIToPluginSchema(OpenAPIV2); + + expect(plugins).toMatchSnapshot(); + }); + }); }); diff --git a/src/services/plugin.ts b/src/services/plugin.ts index 556bf307409b3..51411115ad1ae 100644 --- a/src/services/plugin.ts +++ b/src/services/plugin.ts @@ -1,14 +1,18 @@ import { + LobeChatPluginApi, LobeChatPluginManifest, LobeChatPluginsMarketIndex, + pluginApiSchema, pluginManifestSchema, } from '@lobehub/chat-plugin-sdk'; +import { convertParametersToJSONSchema } from 'openapi-jsonschema-parameters'; import { getPluginIndexJSON } from '@/const/url'; import { PluginModel } from '@/database/models/plugin'; import { globalHelpers } from '@/store/global/helpers'; import { LobeTool } from '@/types/tool'; import { LobeToolCustomPlugin } from '@/types/tool/plugin'; +import { merge } from '@/utils/merge'; export interface InstallPluginParams { identifier: string; @@ -16,6 +20,28 @@ export interface InstallPluginParams { type: 'plugin' | 'customPlugin'; } class PluginService { + private _fetchJSON = async (url: string): Promise => { + // 2. 发送请求 + let res: Response; + try { + res = await fetch(url); + } catch { + throw new TypeError('fetchError'); + } + + if (!res.ok) { + throw new TypeError('fetchError'); + } + + let data; + try { + data = await res.json(); + } catch { + throw new TypeError('urlError'); + } + + return data; + }; /** * get plugin list from store */ @@ -36,23 +62,8 @@ class PluginService { } // 2. 发送请求 - let res: Response; - try { - res = await fetch(url); - } catch { - throw new TypeError('fetchError'); - } - if (!res.ok) { - throw new TypeError('fetchError'); - } - - let data; - try { - data = await res.json(); - } catch { - throw new TypeError('urlError'); - } + const data = await this._fetchJSON(url); // 3. 校验插件文件格式规范 const parser = pluginManifestSchema.safeParse(data); @@ -61,6 +72,17 @@ class PluginService { throw new TypeError('manifestInvalid', { cause: parser.error }); } + // 4. if exist OpenAPI api, merge the openAPIs with apis + if (parser.data.openapi) { + const openapiJson = await this._fetchJSON(parser.data.openapi); + try { + const openAPIs = await this.convertOpenAPIToPluginSchema(openapiJson); + data.api = [...data.api, ...openAPIs]; + } catch (error) { + throw new TypeError('openAPIInvalid', { cause: error }); + } + } + return data; }; @@ -94,6 +116,48 @@ class PluginService { async updatePluginSettings(id: string, settings: any) { return PluginModel.update(id, { settings }); } + + convertOpenAPIToPluginSchema = async (openApiJson: object) => { + // 使用 SwaggerClient 解析 OpenAPI JSON + // @ts-ignore + const SwaggerClient = await import('swagger-client'); + const openAPI = await SwaggerClient.resolve({ spec: openApiJson }); + + const api = openAPI.spec; + const paths = api.paths; + const methods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']; + + const plugins: LobeChatPluginApi[] = []; + + for (const [path, operations] of Object.entries(paths)) { + for (const method of methods) { + const operation = (operations as any)[method]; + if (operation) { + const schemas = convertParametersToJSONSchema(operation.parameters || []); + + let parameters = { properties: {}, type: 'object' }; + for (const schema of Object.values(schemas)) { + parameters = merge(parameters, schema); + } + + // 保留原始逻辑作为备选 + const name = operation.operationId || `${method.toUpperCase()} ${path}`; + + const description = operation.summary || operation.description || name; + + const plugin = { description, name, parameters } as LobeChatPluginApi; + + const res = pluginApiSchema.safeParse(plugin); + if (res.success) plugins.push(plugin); + else { + throw res.error; + } + } + } + } + + return plugins; + }; } export const pluginService = new PluginService();