Skip to content

Commit 9a8cfcc

Browse files
committed
✨ feat: support openapi convertor
1 parent e295d99 commit 9a8cfcc

11 files changed

Lines changed: 2381 additions & 3 deletions

File tree

.prettierignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ coverage
2929
.eslintcache
3030
.stylelintcache
3131
test-output
32-
__snapshots__
32+
tests/__snapshots__
3333
*.snap
3434

3535
# production

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,12 @@
7575
]
7676
},
7777
"dependencies": {
78+
"@apidevtools/swagger-parser": "^10.1.0",
7879
"@babel/runtime": "^7.23.2",
7980
"@types/json-schema": "^7.0.14",
81+
"openapi-jsonschema-parameters": "^12.1.3",
82+
"openapi-types": "^12.1.3",
83+
"swagger-client": "^3.24.6",
8084
"zod": "^3.22.4"
8185
},
8286
"devDependencies": {

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './error';
2+
export * from './openapi';
23
export * from './request';
34
export * from './schema/manifest';
45
export * from './schema/market';

src/openapi/index.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import SwaggerParser from '@apidevtools/swagger-parser';
2+
import { convertParametersToJSONSchema } from 'openapi-jsonschema-parameters';
3+
import { OpenAPI, OpenAPIV3_1 } from 'openapi-types';
4+
5+
import { pluginApiSchema } from '../schema/manifest';
6+
import { LobeChatPluginApi, PluginSchema } from '../types';
7+
8+
export class OpenAPIConvertor {
9+
private readonly openapi: object;
10+
constructor(openapi: object) {
11+
this.openapi = openapi;
12+
}
13+
14+
convertOpenAPIToPluginSchema = async () => {
15+
const api = await SwaggerParser.dereference(this.openapi as OpenAPI.Document);
16+
17+
const paths = api.paths!;
18+
const methods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
19+
20+
const plugins: LobeChatPluginApi[] = [];
21+
22+
for (const [path, operations] of Object.entries(paths)) {
23+
for (const method of methods) {
24+
const operation = (operations as any)[method];
25+
if (operation) {
26+
const parametersSchema = convertParametersToJSONSchema(operation.parameters || []);
27+
const requestBodySchema = this.convertRequestBodyToSchema(operation.requestBody);
28+
29+
const parameters = this.mergeSchemas(
30+
...Object.values(parametersSchema),
31+
requestBodySchema,
32+
);
33+
34+
// 保留原始逻辑作为备选
35+
const name = operation.operationId || `${method.toUpperCase()} ${path}`;
36+
37+
const description = operation.summary || operation.description || name;
38+
39+
const plugin = { description, name, parameters } as LobeChatPluginApi;
40+
41+
const res = pluginApiSchema.safeParse(plugin);
42+
if (res.success) plugins.push(plugin);
43+
else {
44+
throw res.error;
45+
}
46+
}
47+
}
48+
}
49+
50+
return plugins;
51+
};
52+
53+
convertAuthToSettingsSchema = async (
54+
// eslint-disable-next-line unicorn/no-object-as-default-parameter
55+
rawSettingsSchema: PluginSchema = { properties: {}, type: 'object' },
56+
): Promise<PluginSchema> => {
57+
let settingsSchema = rawSettingsSchema;
58+
59+
// @ts-ignore
60+
const { default: SwaggerClient } = await import('swagger-client');
61+
62+
// 使用 SwaggerClient 解析 OpenAPI JSON
63+
const openAPI = await SwaggerClient.resolve({ spec: this.openapi });
64+
const api = openAPI.spec;
65+
66+
for (const entry of Object.entries(api.components?.securitySchemes || {})) {
67+
let authSchema = {} as PluginSchema;
68+
const [key, value] = entry as [string, any];
69+
70+
switch (value.type) {
71+
case 'apiKey': {
72+
authSchema = {
73+
properties: {
74+
[key]: {
75+
description: value.description || `${key} API Key`,
76+
format: 'password',
77+
title: value.name,
78+
type: 'string',
79+
},
80+
},
81+
required: [key],
82+
type: 'object',
83+
};
84+
break;
85+
}
86+
case 'http': {
87+
if (value.scheme === 'basic') {
88+
authSchema = {
89+
properties: {
90+
[key]: {
91+
description: 'Basic authentication credentials',
92+
format: 'password',
93+
type: 'string',
94+
},
95+
},
96+
required: [key],
97+
type: 'object',
98+
};
99+
} else if (value.scheme === 'bearer') {
100+
authSchema = {
101+
properties: {
102+
[key]: {
103+
description: value.description || `${key} Bearer token`,
104+
format: 'password',
105+
title: key,
106+
type: 'string',
107+
},
108+
},
109+
required: [key],
110+
type: 'object',
111+
};
112+
}
113+
break;
114+
}
115+
case 'oauth2': {
116+
authSchema = {
117+
properties: {
118+
[`${key}_clientId`]: {
119+
description: 'Client ID for OAuth2',
120+
type: 'string',
121+
},
122+
[`${key}_clientSecret`]: {
123+
description: 'Client Secret for OAuth2',
124+
format: 'password',
125+
type: 'string',
126+
},
127+
[`${key}_accessToken`]: {
128+
description: 'Access token for OAuth2',
129+
format: 'password',
130+
type: 'string',
131+
},
132+
},
133+
required: [`${key}_clientId`, `${key}_clientSecret`, `${key}_accessToken`],
134+
type: 'object',
135+
};
136+
break;
137+
}
138+
}
139+
140+
// 合并当前鉴权机制的 schema 到 settingsSchema
141+
Object.assign(settingsSchema.properties, authSchema.properties);
142+
143+
if (authSchema.required) {
144+
settingsSchema.required = [
145+
...new Set([...(settingsSchema.required || []), ...authSchema.required]),
146+
];
147+
}
148+
}
149+
150+
return settingsSchema;
151+
};
152+
153+
private convertRequestBodyToSchema(requestBody: OpenAPIV3_1.RequestBodyObject) {
154+
if (!requestBody || !requestBody.content) {
155+
return null;
156+
}
157+
158+
let requestBodySchema = {};
159+
160+
// 遍历所有的 content-type
161+
for (const [contentType, mediaType] of Object.entries(requestBody.content)) {
162+
if (mediaType.schema) {
163+
// 直接使用已解析的 Schema
164+
const resolvedSchema = mediaType.schema;
165+
166+
// 根据不同的 content-type,可以在这里添加特定的处理逻辑
167+
switch (contentType) {
168+
case 'application/json': {
169+
// 直接使用解析后的 Schema 作为 JSON 的请求体定义
170+
requestBodySchema = resolvedSchema;
171+
break;
172+
}
173+
case 'application/x-www-form-urlencoded':
174+
case 'multipart/form-data': {
175+
// 这些类型通常用于文件上传和表单数据,可能需要特别处理
176+
requestBodySchema = resolvedSchema;
177+
break;
178+
}
179+
// 其他 MIME 类型...
180+
default: {
181+
// 如果遇到未知的 content-type,可以选择忽略或抛出错误
182+
console.warn(`Unsupported content-type: ${contentType}`);
183+
break;
184+
}
185+
}
186+
}
187+
}
188+
189+
return requestBodySchema;
190+
}
191+
192+
private mergeSchemas(...schemas: any[]) {
193+
// 初始化合并后的 Schema
194+
const mergedSchema: PluginSchema = {
195+
properties: {},
196+
required: [],
197+
type: 'object',
198+
};
199+
200+
// 遍历每个参数的 Schema
201+
for (const schema of schemas) {
202+
if (schema && schema.properties) {
203+
// 合并属性
204+
Object.assign(mergedSchema.properties, schema.properties);
205+
206+
// 合并必需字段
207+
if (Array.isArray(schema.required)) {
208+
mergedSchema.required = [
209+
...new Set([...(mergedSchema.required || []), ...schema.required]),
210+
];
211+
}
212+
}
213+
}
214+
215+
// 如果没有任何必需字段,则删除 required 属性
216+
if (mergedSchema.required?.length === 0) {
217+
delete mergedSchema.required;
218+
}
219+
220+
return mergedSchema;
221+
}
222+
}

0 commit comments

Comments
 (0)