diff --git a/.prettierignore b/.prettierignore index 12b160c..6dc47b8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -59,6 +59,5 @@ Dockerfile* # misc # add other ignore file below -tests .env.example .env diff --git a/package.json b/package.json index 9708f21..cd30481 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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", @@ -71,7 +72,7 @@ "semantic-release": "^21", "typescript": "^5", "vercel": "^28.20.0", - "vitest": "latest" + "vitest": "0.34.6" }, "publishConfig": { "access": "public", diff --git a/src/gateway.ts b/src/gateway.ts index 12a913d..6acf38d 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -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, @@ -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; @@ -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) @@ -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 完备性 ========== // @@ -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, @@ -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 => { // ========== 1. 校验请求方法 ========== // if (req.method !== 'POST') return createErrorResponse(PluginErrorType.MethodNotAllowed, { diff --git a/src/index.ts b/src/index.ts index 9d7ea09..d2d435f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,3 +18,5 @@ export const createLobeChatPluginGateway = (options: GatewayOptions = {}) => { return async (req: Request) => cors(req, await handler.runOnEdge(req), options.cors); }; + +export * from './gateway'; diff --git a/tests/__snapshots__/gateway.test.ts.snap b/tests/__snapshots__/gateway.test.ts.snap new file mode 100644 index 0000000..e939323 --- /dev/null +++ b/tests/__snapshots__/gateway.test.ts.snap @@ -0,0 +1,291 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Gateway > execute > should return PluginApiNotFound error when apiName is not found in manifest 1`] = ` +{ + "body": { + "manifest": { + "api": [ + { + "description": "", + "name": "test-api", + "parameters": { + "properties": {}, + "type": "object", + }, + "url": "https://test-api-url.com", + }, + ], + "identifier": "abc", + "meta": {}, + }, + "message": "[plugin] api not found", + "request": { + "apiName": "unknown-api", + "identifier": "test-plugin", + }, + }, + "errorType": "PluginApiNotFound", +} +`; + +exports[`Gateway > execute > should return PluginApiParamsError when api parameters are invalid 1`] = ` +{ + "body": { + "api": { + "description": "", + "name": "test-api", + "parameters": { + "properties": { + "a": { + "type": "string", + }, + }, + "required": [ + "a", + ], + "type": "object", + }, + "url": "https://test-api-url.com", + }, + "error": [ + { + "error": "Instance does not have required property \\"a\\".", + "instanceLocation": "#", + "keyword": "required", + "keywordLocation": "#/required", + }, + ], + "message": "[plugin] args is invalid with plugin manifest api schema", + "request": { + "invalid": "params", + }, + }, + "errorType": "PluginApiParamsError", +} +`; + +exports[`Gateway > execute > should return PluginApiParamsError when api url is missing 1`] = ` +{ + "body": { + "api": { + "description": "", + "name": "test-api", + "parameters": { + "properties": {}, + "type": "object", + }, + }, + "message": "[plugin] missing api url", + }, + "errorType": "PluginApiParamsError", +} +`; + +exports[`Gateway > execute > should return PluginManifestInvalid error when plugin manifest is invalid 1`] = ` +{ + "body": { + "error": { + "issues": [ + { + "code": "invalid_type", + "expected": "array", + "message": "Required", + "path": [ + "api", + ], + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "string", + "message": "Required", + "path": [ + "identifier", + ], + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "object", + "message": "Required", + "path": [ + "meta", + ], + "received": "undefined", + }, + ], + "name": "ZodError", + }, + "manifest": { + "invalid": "manifest", + }, + "message": "[plugin] plugin manifest is invalid", + }, + "errorType": "PluginManifestInvalid", +} +`; + +exports[`Gateway > execute > should return PluginManifestNotFound error when plugin manifest is unreachable 1`] = ` +{ + "body": { + "manifestUrl": "https://test-plugin-url.com/manifest.json", + "message": "[plugin] plugin manifest not found", + }, + "errorType": "PluginManifestNotFound", +} +`; + +exports[`Gateway > execute > should return PluginMarketIndexInvalid error when market index is invalid 1`] = ` +{ + "body": { + "error": { + "issues": [ + { + "code": "invalid_type", + "expected": "array", + "message": "Required", + "path": [ + "plugins", + ], + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "number", + "message": "Required", + "path": [ + "schemaVersion", + ], + "received": "undefined", + }, + ], + "name": "ZodError", + }, + "indexUrl": "https://test-market-index-url.com", + "marketIndex": { + "invalid": "index", + }, + "message": "[gateway] plugin market index is invalid", + }, + "errorType": "PluginMarketIndexInvalid", +} +`; + +exports[`Gateway > execute > should return PluginMarketIndexNotFound error when market index is unreachable 1`] = ` +{ + "body": { + "indexUrl": "https://test-market-index-url.com", + "message": "[gateway] plugin market index not found", + }, + "errorType": "PluginMarketIndexNotFound", +} +`; + +exports[`Gateway > execute > should return PluginMetaInvalid error when plugin meta is invalid 1`] = ` +{ + "body": { + "error": { + "issues": [ + { + "code": "invalid_type", + "expected": "string", + "message": "Required", + "path": [ + "author", + ], + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "string", + "message": "Required", + "path": [ + "createAt", + ], + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "string", + "message": "Required", + "path": [ + "homepage", + ], + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "string", + "message": "Required", + "path": [ + "manifest", + ], + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "object", + "message": "Required", + "path": [ + "meta", + ], + "received": "undefined", + }, + { + "code": "invalid_type", + "expected": "number", + "message": "Required", + "path": [ + "schemaVersion", + ], + "received": "undefined", + }, + ], + "name": "ZodError", + }, + "message": "[plugin] plugin meta is invalid", + "pluginMeta": { + "identifier": "test-plugin", + "invalidMeta": true, + }, + }, + "errorType": "PluginMetaInvalid", +} +`; + +exports[`Gateway > execute > should return PluginSettingsInvalid error when provided empty settings 1`] = ` +{ + "body": { + "error": [ + { + "error": "Instance does not have required property \\"abc\\".", + "instanceLocation": "#", + "keyword": "required", + "keywordLocation": "#/required", + }, + ], + "message": "[plugin] your settings is invalid with plugin manifest setting schema", + }, + "errorType": "PluginSettingsInvalid", +} +`; + +exports[`Gateway > execute > should return PluginSettingsInvalid error when provided settings are invalid 1`] = ` +{ + "body": { + "error": [ + { + "error": "Instance does not have required property \\"abc\\".", + "instanceLocation": "#", + "keyword": "required", + "keywordLocation": "#/required", + }, + ], + "message": "[plugin] your settings is invalid with plugin manifest setting schema", + "settings": { + "invalid": "settings", + }, + }, + "errorType": "PluginSettingsInvalid", +} +`; diff --git a/tests/api.test.ts b/tests/api.test.ts deleted file mode 100644 index b03c2f7..0000000 --- a/tests/api.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Api from '../api/index'; -import type { VercelRequest, VercelResponse } from '@vercel/node'; - -test('Api', async () => { - const data = await Api( - ({}), - ({ - json: () => {}, - }), - ); - expect(data).toEqual('hello'); -}); diff --git a/tests/gateway.test.ts b/tests/gateway.test.ts new file mode 100644 index 0000000..ea40305 --- /dev/null +++ b/tests/gateway.test.ts @@ -0,0 +1,321 @@ +import { + LobeChatPluginManifest, + PluginErrorType, + PluginRequestPayload, +} from '@lobehub/chat-plugin-sdk'; +import { Gateway } from '@lobehub/chat-plugins-gateway'; +import { Mock, beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.stubGlobal('fetch', vi.fn()); + +// 模拟响应数据 +const mockMarketIndex = { + plugins: [ + { + identifier: 'test-plugin', + manifest: 'https://test-plugin-url.com/manifest.json', + }, + ], + schemaVersion: 1, +}; + +const mockManifest = { + api: [ + { + description: '', + name: 'test-api', + parameters: { properties: {}, type: 'object' }, + url: 'https://test-api-url.com', + }, + ], + identifier: 'abc', + meta: {}, +} as LobeChatPluginManifest; + +const mockPluginRequestPayload: PluginRequestPayload = { + apiName: 'test-api', + arguments: '{}', + identifier: 'test-plugin', + indexUrl: 'https://test-market-index-url.com', + manifest: mockManifest, +}; + +let gateway: Gateway; + +beforeEach(() => { + // Reset the mocked fetch before each test + vi.resetAllMocks(); + gateway = new Gateway(); +}); + +describe('Gateway', () => { + describe('execute', () => { + it('should execute successfully when provided with correct payload and settings', async () => { + (fetch as Mock).mockImplementation(async (url) => { + if (url === 'https://test-market-index-url.com') + return { + json: async () => mockMarketIndex, + ok: true, + }; + + if (url === mockMarketIndex) + return { + json: async () => mockManifest, + ok: true, + }; + + return new Response(JSON.stringify({ success: true }), { status: 200 }); + }); + + const response = await gateway.execute(mockPluginRequestPayload); + const data = await response.json(); + expect(response.status).toBe(200); + expect(data).toEqual({ success: true }); + }); + + it('should return BadRequest error when payload is invalid', async () => { + const invalidPayload = { ...mockPluginRequestPayload, identifier: null }; // Invalid payload + const response = await gateway.execute(invalidPayload as unknown as PluginRequestPayload); + expect(response.status).toBe(PluginErrorType.BadRequest); + }); + + it('should return PluginMarketIndexNotFound error when market index is unreachable', async () => { + (fetch as Mock).mockRejectedValue(new Error('Network error')); + const response = await gateway.execute({ ...mockPluginRequestPayload, manifest: undefined }); + expect(response.status).toBe(590); + expect(await response.json()).toMatchSnapshot(); + }); + + it('should return PluginMarketIndexInvalid error when market index is invalid', async () => { + (fetch as Mock).mockResolvedValueOnce({ + json: async () => ({ invalid: 'index' }), + ok: true, // Invalid market index + }); + const response = await gateway.execute({ ...mockPluginRequestPayload, manifest: undefined }); + expect(response.status).toBe(490); + expect(await response.json()).toMatchSnapshot(); + }); + + it('should return PluginMetaNotFound error when plugin meta does not exist', async () => { + (fetch as Mock).mockResolvedValueOnce({ + json: async () => mockMarketIndex, + ok: true, + }); + const invalidPayload = { + ...mockPluginRequestPayload, + identifier: 'unknown-plugin', + manifest: undefined, + }; + const response = await gateway.execute(invalidPayload); + + expect(response.status).toEqual(404); + expect(await response.json()).toEqual({ + body: { + identifier: 'unknown-plugin', + message: + "[gateway] plugin 'unknown-plugin' is not found,please check the plugin list in https://test-market-index-url.com, or create an issue to [lobe-chat-plugins](https://github.com/lobehub/lobe-chat-plugins/issues)", + }, + errorType: 'PluginMetaNotFound', + }); + }); + + it('should return PluginMetaInvalid error when plugin meta is invalid', async () => { + (fetch as Mock).mockResolvedValueOnce({ + json: async () => ({ + ...mockMarketIndex, + plugins: [{ identifier: 'test-plugin', invalidMeta: true }], + }), + ok: true, + }); + const response = await gateway.execute({ ...mockPluginRequestPayload, manifest: undefined }); + expect(response.status).toBe(490); + expect(await response.json()).toMatchSnapshot(); + }); + + it('should return PluginManifestNotFound error when plugin manifest is unreachable', async () => { + (fetch as Mock).mockImplementation(async (url) => { + if (url === mockPluginRequestPayload.indexUrl) + return { + json: async () => ({ + ...mockMarketIndex, + plugins: [ + { + author: 'test-plugin', + createAt: '2023-08-12', + homepage: 'https://github.com/lobehub/chat-plugin-real-time-weather', + identifier: 'test-plugin', + manifest: 'https://test-plugin-url.com/manifest.json', + meta: { + avatar: '☂️', + tags: ['weather', 'realtime'], + title: 'realtimeWeather', + }, + schemaVersion: 1, + }, + ], + }), + ok: true, + }; + + if (url === 'https://test-plugin-url.com/manifest.json') + return { + json: async () => { + throw new Error('Network error'); + }, + ok: true, + }; + + throw new Error('Network error'); + }); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { manifest: _, ...payload } = mockPluginRequestPayload; + + const response = await gateway.execute(payload); + + expect(response.status).toBe(404); + expect(await response.json()).toMatchSnapshot(); + }); + + it('should return PluginManifestInvalid error when plugin manifest is invalid', async () => { + (fetch as Mock).mockImplementation(async (url) => { + if (url === mockPluginRequestPayload.indexUrl) + return { + json: async () => ({ + ...mockMarketIndex, + plugins: [ + { + author: 'test-plugin', + createAt: '2023-08-12', + homepage: 'https://github.com/lobehub/chat-plugin-real-time-weather', + identifier: 'test-plugin', + manifest: 'https://test-plugin-url.com/manifest.json', + meta: { + avatar: '☂️', + tags: ['weather', 'realtime'], + title: 'realtimeWeather', + }, + schemaVersion: 1, + }, + ], + }), + ok: true, + }; + + if (url === 'https://test-plugin-url.com/manifest.json') + return { + json: async () => ({ invalid: 'manifest' }), + ok: true, + }; + + throw new Error('Network error'); + }); + + const response = await gateway.execute({ ...mockPluginRequestPayload, manifest: undefined }); + expect(response.status).toBe(490); + expect(await response.json()).toMatchSnapshot(); + }); + + it('should return PluginSettingsInvalid error when provided settings are invalid', async () => { + const settings = { invalid: 'settings' }; + + const payload = { + ...mockPluginRequestPayload, + manifest: { + ...mockManifest, + settings: { properties: { abc: { type: 'string' } }, required: ['abc'], type: 'object' }, + } as LobeChatPluginManifest, + }; + + const response = await gateway.execute(payload, settings); + expect(response.status).toBe(422); + expect(await response.json()).toMatchSnapshot(); + }); + + it('should return PluginSettingsInvalid error when provided empty settings', async () => { + const payload = { + ...mockPluginRequestPayload, + manifest: { + ...mockManifest, + settings: { properties: { abc: { type: 'string' } }, required: ['abc'], type: 'object' }, + } as LobeChatPluginManifest, + }; + + const response = await gateway.execute(payload); + expect(response.status).toBe(422); + expect(await response.json()).toMatchSnapshot(); + }); + + it('should return PluginApiNotFound error when apiName is not found in manifest', async () => { + const payload = { ...mockPluginRequestPayload, apiName: 'unknown-api' }; + const response = await gateway.execute(payload); + expect(response.status).toBe(404); + expect(await response.json()).toMatchSnapshot(); + }); + + it('should return PluginApiParamsError when api parameters are invalid', async () => { + const payload: PluginRequestPayload = { + ...mockPluginRequestPayload, + // Invalid parameters + arguments: '{"invalid": "params"}', + manifest: { + ...mockManifest, + api: [ + { + description: '', + name: 'test-api', + parameters: { + properties: { a: { type: 'string' } }, + required: ['a'], + type: 'object', + }, + url: 'https://test-api-url.com', + }, + ], + } as LobeChatPluginManifest, + }; + + const response = await gateway.execute(payload); + expect(response.status).toBe(422); + expect(await response.json()).toMatchSnapshot(); + }); + + it('should return PluginApiParamsError when api url is missing', async () => { + const manifestWithoutUrl = { + ...mockManifest, + api: [{ ...mockManifest.api[0], url: undefined }], + }; + const response = await gateway.execute({ + ...mockPluginRequestPayload, + manifest: manifestWithoutUrl, + }); + expect(response.status).toBe(422); + expect(await response.json()).toMatchSnapshot(); + }); + + it('should return correct response when API request is successful', async () => { + (fetch as Mock).mockResolvedValueOnce( + new Response(JSON.stringify({ success: true }), { status: 200 }), + ); + + const response = await gateway.execute({ + ...mockPluginRequestPayload, + manifest: mockManifest, + }); + const data = await response.json(); + expect(response.status).toBe(200); + expect(data).toEqual({ success: true }); + }); + + it('should return error response when API request fails', async () => { + (fetch as Mock).mockResolvedValueOnce(new Response('Internal Server Error', { status: 500 })); + + const response = await gateway.execute({ + ...mockPluginRequestPayload, + manifest: mockManifest, + }); + expect(response.status).toBe(500); + expect(await response.text()).toBe('Internal Server Error'); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 534c8a9..5bb331a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,10 @@ "noUnusedLocals": true, "resolveJsonModule": true, "skipLibCheck": true, - "strict": true + "strict": true, + "paths": { + "@lobehub/chat-plugins-gateway": ["src"] + } }, - "include": ["types", "api", "src", "*.ts"] + "include": ["types", "api", "src", "*.ts", "tests"] } diff --git a/vitest.config.ts b/vitest.config.ts index 014f97e..60c1085 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,6 +2,14 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { + alias: { + '@': 'src', + '@lobehub/chat-plugins-gateway': 'src', + }, + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'lcov', 'text-summary'], + }, environment: 'node', globals: true, },