Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

插件网关 1.0 #2

Merged
merged 5 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export default async (req: VercelRequest, response: VercelResponse) => {
response.json({
status: 'ok',
});
return 'hello';
};
8 changes: 8 additions & 0 deletions api/v1/_validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from 'zod';

export const payloadSchema = z.object({
arguments: z.string().optional(),
name: z.string(),
});

export type PluginPayload = z.infer<typeof payloadSchema>;
167 changes: 136 additions & 31 deletions api/v1/runner.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,160 @@
import { LobeChatPlugin, LobeChatPluginsMarketIndex } from '@lobehub/chat-plugin-sdk';

import { OpenAIPluginPayload } from '../../types/plugins';
// reason to use cfworker json schema:
// https://github.com/vercel/next.js/discussions/47063#discussioncomment-5303951
import { Validator } from '@cfworker/json-schema';
import {
ErrorType,
LobeChatPlugin,
LobeChatPluginsMarketIndex,
createErrorResponse,
marketIndexSchema,
pluginManifestSchema,
pluginMetaSchema,
} from '@lobehub/chat-plugin-sdk';

import { PluginPayload, payloadSchema } from './_validator';

export const config = {
runtime: 'edge',
};

const INDEX_URL = `https://registry.npmmirror.com/@lobehub/lobe-chat-plugins/~1.4/files`;
const INDEX_URL = `https://registry.npmmirror.com/@lobehub/lobe-chat-plugins/latest/files`;

export default async (req: Request) => {
if (req.method !== 'POST') return new Response('Method Not Allowed', { status: 405 });
// ========== 1. 校验请求方法 ========== //
if (req.method !== 'POST')
return createErrorResponse(ErrorType.MethodNotAllowed, {
message: '[gateway] only allow POST method',
});

// ========== 2. 校验请求入参基础格式 ========== //
const requestPayload = (await req.json()) as PluginPayload;

const indexRes = await fetch(INDEX_URL);
const manifest: LobeChatPluginsMarketIndex = await indexRes.json();
console.log('manifest:', manifest);
const payloadParseResult = payloadSchema.safeParse(requestPayload);

const { name, arguments: args } = (await req.json()) as OpenAIPluginPayload;
if (!payloadParseResult.success)
return createErrorResponse(ErrorType.BadRequest, payloadParseResult.error);

console.log(`检测到 functionCall: ${name}`);
const { name, arguments: args } = requestPayload;

const item = manifest.plugins.find((i) => i.name === name);
console.info(`plugin call: ${name}`);

if (!item) return;
// ========== 3. 获取插件市场索引 ========== //

// 兼容 V0 版本的代码
if ((manifest.version as number) === 0) {
// 先通过插件资产 endpoint 路径查询
const res = await fetch((item as any).runtime.endpoint, { body: args, method: 'post' });
const data = await res.text();
console.log(`[${name}]`, args, `result:`, data.slice(0, 3600));
return new Response(data);
let marketIndex: LobeChatPluginsMarketIndex | undefined;
try {
const indexRes = await fetch(INDEX_URL);
marketIndex = await indexRes.json();
} catch (error) {
console.error(error);
marketIndex = undefined;
}

// 新版 V1 的代码
else if (manifest.version === 1) {
// 先通过插件资产 endpoint 路径查询
// 插件市场索引不存在
if (!marketIndex)
return createErrorResponse(ErrorType.PluginMarketIndexNotFound, {
indexUrl: INDEX_URL,
message: '[gateway] plugin market index not found',
});

// 插件市场索引解析失败
const indexParseResult = marketIndexSchema.safeParse(marketIndex);

if (!indexParseResult.success)
return createErrorResponse(ErrorType.PluginMarketIndexInvalid, {
error: indexParseResult.error,
indexUrl: INDEX_URL,
marketIndex,
message: '[gateway] plugin market index is invalid',
});

console.info('marketIndex:', marketIndex);

// ========== 4. 校验插件 meta 完备性 ========== //

const pluginMeta = marketIndex.plugins.find((i) => i.name === name);

// 一个不规范的插件示例
// const pluginMeta = {
// createAt: '2023-08-12',
// homepage: 'https://github.com/lobehub/chat-plugin-real-time-weather',
// manifest: 'https://registry.npmmirror.com/@lobehub/lobe-chat-plugins/latest/files',
// meta: {
// avatar: '☂️',
// tags: ['weather', 'realtime'],
// },
// name: 'realtimeWeather',
// schemaVersion: 'v1',
// };

// 校验插件是否存在
if (!pluginMeta)
return createErrorResponse(ErrorType.PluginMetaNotFound, {
message: `[gateway] plugin '${name}' is not found,please check the plugin list in ${INDEX_URL}, or create an issue to [lobe-chat-plugins](https://github.com/lobehub/lobe-chat-plugins/issues)`,
name,
});

const metaParseResult = pluginMetaSchema.safeParse(pluginMeta);

if (!metaParseResult.success)
return createErrorResponse(ErrorType.PluginMetaInvalid, {
error: metaParseResult.error,
message: '[plugin] plugin meta is invalid',
pluginMeta,
});

// ========== 5. 校验插件 manifest 完备性 ========== //

// 获取插件的 manifest
let manifest: LobeChatPlugin | undefined;
try {
const pluginRes = await fetch(pluginMeta.manifest);
manifest = (await pluginRes.json()) as LobeChatPlugin;
} catch (error) {
console.error(error);
manifest = undefined;
}

if (!item.manifest) return;
if (!manifest)
return createErrorResponse(ErrorType.PluginManifestNotFound, {
manifestUrl: pluginMeta.manifest,
message: '[plugin] plugin manifest not found',
});

// 获取插件的 manifest
const pluginRes = await fetch(item.manifest);
const chatPlugin = (await pluginRes.json()) as LobeChatPlugin;
const manifestParseResult = pluginManifestSchema.safeParse(manifest);

const response = await fetch(chatPlugin.server.url, { body: args, method: 'post' });
if (!manifestParseResult.success)
return createErrorResponse(ErrorType.PluginManifestInvalid, {
error: manifestParseResult.error,
manifest: manifest,
message: '[plugin] plugin manifest is invalid',
});

const data = await response.text();
console.log(`[${name}] plugin manifest:`, manifest);

console.log(`[${name}]`, args, `result:`, data.slice(0, 3600));
// ========== 6. 校验请求入参与 manifest 要求一致性 ========== //

return new Response(data);
if (args) {
const v = new Validator(manifest.schema.parameters as any);
const validator = v.validate(JSON.parse(args!));

if (!validator.valid)
return createErrorResponse(ErrorType.BadRequest, {
error: validator.errors,
manifest,
message: '[plugin] args is invalid with plugin manifest schema',
});
}

return;
// ========== 7. 发送请求 ========== //

const response = await fetch(manifest.server.url, { body: args, method: 'post' });

// 不正常的错误,直接返回请求
if (!response.ok) return response;

const data = await response.text();

console.log(`[${name}]`, args, `result:`, data.slice(0, 3600));

return new Response(data);
};
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
"not ie <= 10"
],
"dependencies": {
"@lobehub/chat-plugin-sdk": "^1.0.1"
"@cfworker/json-schema": "^1",
"@lobehub/chat-plugin-sdk": "latest",
"zod": "^3"
},
"devDependencies": {
"@lobehub/lint": "latest",
Expand All @@ -45,7 +47,7 @@
"prettier": "^2",
"semantic-release": "^21",
"typescript": "^5",
"vercel": "^31.2.3",
"vercel": "^29",
"vitest": "latest"
}
}