Skip to content

Commit

Permalink
✨ feat: support openapi convertor
Browse files Browse the repository at this point in the history
  • Loading branch information
arvinxx committed Dec 15, 2023
1 parent e295d99 commit 9a8cfcc
Show file tree
Hide file tree
Showing 11 changed files with 2,381 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ coverage
.eslintcache
.stylelintcache
test-output
__snapshots__
tests/__snapshots__
*.snap

# production
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,12 @@
]
},
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@babel/runtime": "^7.23.2",
"@types/json-schema": "^7.0.14",
"openapi-jsonschema-parameters": "^12.1.3",
"openapi-types": "^12.1.3",
"swagger-client": "^3.24.6",
"zod": "^3.22.4"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './error';
export * from './openapi';
export * from './request';
export * from './schema/manifest';
export * from './schema/market';
Expand Down
222 changes: 222 additions & 0 deletions src/openapi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import SwaggerParser from '@apidevtools/swagger-parser';
import { convertParametersToJSONSchema } from 'openapi-jsonschema-parameters';
import { OpenAPI, OpenAPIV3_1 } from 'openapi-types';

import { pluginApiSchema } from '../schema/manifest';
import { LobeChatPluginApi, PluginSchema } from '../types';

export class OpenAPIConvertor {
private readonly openapi: object;
constructor(openapi: object) {
this.openapi = openapi;
}

convertOpenAPIToPluginSchema = async () => {
const api = await SwaggerParser.dereference(this.openapi as OpenAPI.Document);

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 parametersSchema = convertParametersToJSONSchema(operation.parameters || []);
const requestBodySchema = this.convertRequestBodyToSchema(operation.requestBody);

const parameters = this.mergeSchemas(
...Object.values(parametersSchema),
requestBodySchema,
);

// 保留原始逻辑作为备选
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;
};

convertAuthToSettingsSchema = async (
// eslint-disable-next-line unicorn/no-object-as-default-parameter
rawSettingsSchema: PluginSchema = { properties: {}, type: 'object' },
): Promise<PluginSchema> => {
let settingsSchema = rawSettingsSchema;

// @ts-ignore
const { default: SwaggerClient } = await import('swagger-client');

// 使用 SwaggerClient 解析 OpenAPI JSON
const openAPI = await SwaggerClient.resolve({ spec: this.openapi });
const api = openAPI.spec;

for (const entry of Object.entries(api.components?.securitySchemes || {})) {
let authSchema = {} as PluginSchema;
const [key, value] = entry as [string, any];

switch (value.type) {
case 'apiKey': {
authSchema = {
properties: {
[key]: {
description: value.description || `${key} API Key`,
format: 'password',
title: value.name,
type: 'string',
},
},
required: [key],
type: 'object',
};
break;
}
case 'http': {
if (value.scheme === 'basic') {
authSchema = {
properties: {
[key]: {
description: 'Basic authentication credentials',
format: 'password',
type: 'string',
},
},
required: [key],
type: 'object',
};
} else if (value.scheme === 'bearer') {
authSchema = {
properties: {
[key]: {
description: value.description || `${key} Bearer token`,
format: 'password',
title: key,
type: 'string',
},
},
required: [key],
type: 'object',
};
}
break;
}
case 'oauth2': {
authSchema = {
properties: {
[`${key}_clientId`]: {
description: 'Client ID for OAuth2',
type: 'string',
},
[`${key}_clientSecret`]: {
description: 'Client Secret for OAuth2',
format: 'password',
type: 'string',
},
[`${key}_accessToken`]: {
description: 'Access token for OAuth2',
format: 'password',
type: 'string',
},
},
required: [`${key}_clientId`, `${key}_clientSecret`, `${key}_accessToken`],
type: 'object',
};
break;
}
}

// 合并当前鉴权机制的 schema 到 settingsSchema
Object.assign(settingsSchema.properties, authSchema.properties);

if (authSchema.required) {
settingsSchema.required = [
...new Set([...(settingsSchema.required || []), ...authSchema.required]),
];
}
}

return settingsSchema;
};

private convertRequestBodyToSchema(requestBody: OpenAPIV3_1.RequestBodyObject) {
if (!requestBody || !requestBody.content) {
return null;
}

let requestBodySchema = {};

// 遍历所有的 content-type
for (const [contentType, mediaType] of Object.entries(requestBody.content)) {
if (mediaType.schema) {
// 直接使用已解析的 Schema
const resolvedSchema = mediaType.schema;

// 根据不同的 content-type,可以在这里添加特定的处理逻辑
switch (contentType) {
case 'application/json': {
// 直接使用解析后的 Schema 作为 JSON 的请求体定义
requestBodySchema = resolvedSchema;
break;
}
case 'application/x-www-form-urlencoded':
case 'multipart/form-data': {
// 这些类型通常用于文件上传和表单数据,可能需要特别处理
requestBodySchema = resolvedSchema;
break;
}
// 其他 MIME 类型...
default: {
// 如果遇到未知的 content-type,可以选择忽略或抛出错误
console.warn(`Unsupported content-type: ${contentType}`);
break;
}
}
}
}

return requestBodySchema;
}

private mergeSchemas(...schemas: any[]) {
// 初始化合并后的 Schema
const mergedSchema: PluginSchema = {
properties: {},
required: [],
type: 'object',
};

// 遍历每个参数的 Schema
for (const schema of schemas) {
if (schema && schema.properties) {
// 合并属性
Object.assign(mergedSchema.properties, schema.properties);

// 合并必需字段
if (Array.isArray(schema.required)) {
mergedSchema.required = [
...new Set([...(mergedSchema.required || []), ...schema.required]),
];
}
}
}

// 如果没有任何必需字段,则删除 required 属性
if (mergedSchema.required?.length === 0) {
delete mergedSchema.required;
}

return mergedSchema;
}
}
Loading

0 comments on commit 9a8cfcc

Please sign in to comment.