Skip to content

Commit

Permalink
✅ test: add test for the gateway
Browse files Browse the repository at this point in the history
  • Loading branch information
arvinxx committed Dec 13, 2023
1 parent fff172f commit eafc3bb
Show file tree
Hide file tree
Showing 9 changed files with 736 additions and 101 deletions.
1 change: 0 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,5 @@ Dockerfile*

# misc
# add other ignore file below
tests
.env.example
.env
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
"prettier": "prettier -c --write \"**/**\"",
"release": "semantic-release",
"start": "vercel dev",
"test": "vitest --passWithNoTests",
"test:coverage": "vitest --coverage --passWithNoTests",
"test": "vitest",
"test:coverage": "vitest --coverage",
"test:update": "vitest -u",
"type-check": "tsc --noEmit"
},
"lint-staged": {
Expand Down Expand Up @@ -58,7 +59,7 @@
"@commitlint/cli": "^17",
"@lobehub/lint": "latest",
"@vercel/node": "^2",
"@vitest/coverage-v8": "latest",
"@vitest/coverage-v8": "0.34.6",
"commitlint": "^17",
"cross-env": "^7",
"eslint": "^8",
Expand All @@ -71,7 +72,7 @@
"semantic-release": "^21",
"typescript": "^5",
"vercel": "^28.20.0",
"vitest": "latest"
"vitest": "0.34.6"
},
"publishConfig": {
"access": "public",
Expand Down
186 changes: 104 additions & 82 deletions src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// https://github.com/vercel/next.js/discussions/47063#discussioncomment-5303951
import { Validator } from '@cfworker/json-schema';
import {
LobeChatPluginApi,
LobeChatPluginManifest,
LobeChatPluginsMarketIndex,
PluginErrorType,
Expand All @@ -17,7 +18,7 @@ import {

import { CorsOptions } from './cors';

const DEFAULT_PLUGINS_INDEX_URL = 'https://chat-plugins.lobehub.com';
export const DEFAULT_PLUGINS_INDEX_URL = 'https://chat-plugins.lobehub.com';

export interface GatewayOptions {
cors?: CorsOptions;
Expand All @@ -30,13 +31,13 @@ export interface GatewayOptions {
export class Gateway {
private pluginIndexUrl = DEFAULT_PLUGINS_INDEX_URL;

constructor(options: GatewayOptions) {
if (options.pluginsIndexUrl) {
constructor(options?: GatewayOptions) {
if (options?.pluginsIndexUrl) {
this.pluginIndexUrl = options.pluginsIndexUrl;
}
}

execute = async (payload: PluginRequestPayload, settings: any) => {
execute = async (payload: PluginRequestPayload, settings?: any) => {
// ========== 2. 校验请求入参基础格式 ========== //
const payloadParseResult = pluginRequestPayloadSchema.safeParse(payload);
if (!payloadParseResult.success)
Expand Down Expand Up @@ -79,7 +80,9 @@ export class Gateway {
message: '[gateway] plugin market index is invalid',
});

console.info('marketIndex:', marketIndex);
console.info(
`[marketIndex V${marketIndex.schemaVersion}] total ${marketIndex.plugins.length} plugins`,
);

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

Expand Down Expand Up @@ -184,83 +187,18 @@ export class Gateway {

// ========== 8. 兼容 OpenAPI 请求模式 ========== //
if (manifest.openapi) {
// @ts-ignore
const { default: SwaggerClient } = await import('swagger-client');

const authorizations = {} as {
[key: string]: any;
basicAuth?: any;
oauth2?: {
accessToken: string;
clientId: string;
clientSecret: string;
};
};

// 根据 settings 中的每个属性来构建 authorizations 对象
for (const [key, value] of Object.entries(settings)) {
if (key.endsWith('_username') && key.endsWith('_password')) {
// 处理 HTTP Basic Authentication
const username = settings[key];
const password = settings[key.replace('_username', '_password')];
authorizations.basicAuth = new SwaggerClient.PasswordAuthorization(username, password);
} else if (
key.endsWith('_clientId') &&
key.endsWith('_clientSecret') &&
key.endsWith('_accessToken')
) {
// 处理 OAuth2
const clientId = settings[key];
const clientSecret = settings[key.replace('_clientId', '_clientSecret')];
const accessToken = settings[key.replace('_clientId', '_accessToken')];
authorizations.oauth2 = { accessToken, clientId, clientSecret };
} else {
// 处理 API Key 和 Bearer Token
authorizations[key] = value as string;
}
}

let client;
try {
client = await SwaggerClient({ authorizations, url: manifest.openapi });
} catch (e) {
return createErrorResponse(PluginErrorType.PluginOpenApiInitError, {
error: e,
message: '[plugin] openapi client init error',
openapi: manifest.openapi,
});
}

const parameters = JSON.parse(args || '{}');

try {
const res = await client.execute({ operationId: apiName, parameters });

return new Response(res.text);
} catch (error) {
// 如果没有 status,说明没有发送请求,可能是 openapi 相关调用实现的问题
if (!(error as any).status) {
console.error(error);

return createErrorResponse(PluginErrorType.PluginGatewayError, {
authorizations,
apiName,
parameters,
error: (error as Error).message,
message:
'[plugin] there are problem with sending openapi request, please contact with LobeHub Team',
openapi: manifest.openapi,
});
}

// 如果是 401 则说明是鉴权问题
if ((error as Response).status === 401)
return createErrorResponse(PluginErrorType.PluginSettingsInvalid);

return createErrorResponse(PluginErrorType.PluginServerError, { error });
}
return await this.callOpenAPI(payload, settings, manifest);
}

return await this.callApi(api, args, settings, identifier);
};

private async callApi(
api: LobeChatPluginApi,
args: string | undefined,
settings: any,
identifier: string,
) {
if (!api.url)
return createErrorResponse(PluginErrorType.PluginApiParamsError, {
api,
Expand All @@ -283,9 +221,93 @@ export class Gateway {
console.log(`[${identifier}]`, args, `result:`, data.slice(0, 1000));

return new Response(data);
};
}

private async callOpenAPI(
payload: PluginRequestPayload,
settings: any,
manifest: LobeChatPluginManifest,
) {
const { arguments: args, apiName } = payload;

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

const authorizations = {} as {
[key: string]: any;
basicAuth?: any;
oauth2?: {
accessToken: string;
clientId: string;
clientSecret: string;
};
};

// 根据 settings 中的每个属性来构建 authorizations 对象
for (const [key, value] of Object.entries(settings)) {
if (key.endsWith('_username') && key.endsWith('_password')) {
// 处理 HTTP Basic Authentication
const username = settings[key];
const password = settings[key.replace('_username', '_password')];
authorizations.basicAuth = new SwaggerClient.PasswordAuthorization(username, password);
} else if (
key.endsWith('_clientId') &&
key.endsWith('_clientSecret') &&
key.endsWith('_accessToken')
) {
// 处理 OAuth2
const clientId = settings[key];
const clientSecret = settings[key.replace('_clientId', '_clientSecret')];
const accessToken = settings[key.replace('_clientId', '_accessToken')];
authorizations.oauth2 = { accessToken, clientId, clientSecret };
} else {
// 处理 API Key 和 Bearer Token
authorizations[key] = value as string;
}
}

let client;
try {
client = await SwaggerClient({ authorizations, url: manifest.openapi });
} catch (error) {
return createErrorResponse(PluginErrorType.PluginOpenApiInitError, {
error: error,
message: '[plugin] openapi client init error',
openapi: manifest.openapi,
});
}

const parameters = JSON.parse(args || '{}');

try {
const res = await client.execute({ operationId: apiName, parameters });

return new Response(res.text);
} catch (error) {
// 如果没有 status,说明没有发送请求,可能是 openapi 相关调用实现的问题
if (!(error as any).status) {
console.error(error);

return createErrorResponse(PluginErrorType.PluginGatewayError, {
apiName,
authorizations,
error: (error as Error).message,
message:
'[plugin] there are problem with sending openapi request, please contact with LobeHub Team',
openapi: manifest.openapi,
parameters,
});
}

// 如果是 401 则说明是鉴权问题
if ((error as Response).status === 401)
return createErrorResponse(PluginErrorType.PluginSettingsInvalid);

return createErrorResponse(PluginErrorType.PluginServerError, { error });
}
}

runOnEdge = async (req: Request) => {
runOnEdge = async (req: Request): Promise<Response> => {
// ========== 1. 校验请求方法 ========== //
if (req.method !== 'POST')
return createErrorResponse(PluginErrorType.MethodNotAllowed, {
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ export const createLobeChatPluginGateway = (options: GatewayOptions = {}) => {

return async (req: Request) => cors(req, await handler.runOnEdge(req), options.cors);
};

export * from './gateway';
Loading

0 comments on commit eafc3bb

Please sign in to comment.