🐛 fix: updown the old lobehub plugins (#12674)

* fix: updown the old lobehub plugins

* fix: update test.ts
This commit is contained in:
LiJian
2026-03-04 18:05:43 +08:00
committed by GitHub
parent 6ba657e6d0
commit bf5d6ce2f8
8 changed files with 27 additions and 505 deletions

View File

@@ -102,26 +102,5 @@ export const getToolManifest = async (
throw new TypeError('manifestInvalid', { cause: parser.error });
}
// 4. if exist OpenAPI api, merge the OpenAPIs to api
if (parser.data.openapi) {
const openapiJson = await fetchJSON(parser.data.openapi, useProxy);
// avoid https://github.com/lobehub/lobe-chat/issues/9059
if (typeof window !== 'undefined') {
try {
const { OpenAPIConvertor } = await import('@lobehub/chat-plugin-sdk/openapi');
const convertor = new OpenAPIConvertor(openapiJson);
const openAPIs = await convertor.convertOpenAPIToPluginSchema();
data.api = [...data.api, ...openAPIs];
data.settings = await convertor.convertAuthToSettingsSchema(data.settings);
} catch (error) {
throw new TypeError('openAPIInvalid', { cause: error });
}
}
}
return data;
};

View File

@@ -5,13 +5,11 @@ import type { Plugin } from 'vite';
*
* - `node:stream`: dynamically imported in azureai provider behind `typeof window === 'undefined'`
* guard — dead code in browser but Rollup still resolves it.
* - `@lobehub/chat-plugin-sdk/openapi`: dynamically imported in toolManifest, pulls in
* @apidevtools/swagger-parser which depends on Node built-ins (util, path).
* - `node-fetch`: dynamically imported by klavis SDK's getFetchFn behind a runtime
* Node.js version check — dead code in browser since native fetch is available.
*/
export function viteNodeModuleStub(): Plugin {
const stubbedModules = new Set(['node:stream', 'node-fetch', '@lobehub/chat-plugin-sdk/openapi']);
const stubbedModules = new Set(['node:stream', 'node-fetch']);
const VIRTUAL_PREFIX = '\0node-stub:';
return {

View File

@@ -77,128 +77,3 @@ getWolframCloudResults guidelines:
"version": "1",
}
`;
exports[`ToolService > getToolManifest > support OpenAPI manifest > should get plugin manifest 1`] = `
{
"$schema": "../node_modules/@lobehub/chat-plugin-sdk/schema.json",
"api": [
{
"description": "Read Course Segments",
"name": "read_course_segments_course_segments__get",
"parameters": {
"properties": {},
"type": "object",
},
},
{
"description": "Read Problem Set Item",
"name": "read_problem_set_item_problem_set__problem_set_id___question_number__get",
"parameters": {
"properties": {
"problem_set_id": {
"title": "Problem Set Id",
"type": "integer",
},
"question_number": {
"title": "Question Number",
"type": "integer",
},
},
"required": [
"problem_set_id",
"question_number",
],
"type": "object",
},
},
{
"description": "Read Random Problem Set Items",
"name": "read_random_problem_set_items_problem_set_random__problem_set_id___n_items__get",
"parameters": {
"properties": {
"n_items": {
"title": "N Items",
"type": "integer",
},
"problem_set_id": {
"title": "Problem Set Id",
"type": "integer",
},
},
"required": [
"problem_set_id",
"n_items",
],
"type": "object",
},
},
{
"description": "Read Range Of Problem Set Items",
"name": "read_range_of_problem_set_items_problem_set_range__problem_set_id___start___end__get",
"parameters": {
"properties": {
"end": {
"title": "End",
"type": "integer",
},
"problem_set_id": {
"title": "Problem Set Id",
"type": "integer",
},
"start": {
"title": "Start",
"type": "integer",
},
},
"required": [
"problem_set_id",
"start",
"end",
],
"type": "object",
},
},
{
"description": "Read User Id",
"name": "read_user_id_user__get",
"parameters": {
"properties": {},
"type": "object",
},
},
],
"author": "LobeHub",
"createAt": "2023-08-12",
"homepage": "https://github.com/lobehub/chat-plugin-realtime-weather",
"identifier": "realtime-weather",
"meta": {
"avatar": "🌈",
"description": "Get realtime weather information",
"tags": [
"weather",
"realtime",
],
"title": "Realtime Weather",
},
"openapi": "http://fake-url.com/openapiUrl.json",
"settings": {
"properties": {
"HTTPBearer": {
"description": "HTTPBearer Bearer token",
"format": "password",
"title": "HTTPBearer",
"type": "string",
},
},
"required": [
"HTTPBearer",
],
"type": "object",
},
"ui": {
"height": 310,
"url": "https://realtime-weather.chat-plugin.lobehub.com/iframe",
},
"version": "1",
}
`;

View File

@@ -1,7 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { toolService } from '../tool';
import openAPIV3 from './openai/OpenAPI_V3.json';
import OpenAIPlugin from './openai/plugin.json';
// Mocking modules and functions
@@ -136,85 +135,6 @@ describe('ToolService', () => {
expect(e).toEqual(new TypeError('fetchError'));
}
});
describe('support OpenAPI manifest', () => {
it('should get plugin manifest', async () => {
const manifestUrl = 'http://fake-url.com/manifest.json';
const openapiUrl = 'http://fake-url.com/openapiUrl.json';
const fakeManifest = {
$schema: '../node_modules/@lobehub/chat-plugin-sdk/schema.json',
api: [],
openapi: openapiUrl,
author: 'LobeHub',
createAt: '2023-08-12',
homepage: 'https://github.com/lobehub/chat-plugin-realtime-weather',
identifier: 'realtime-weather',
meta: {
avatar: '🌈',
tags: ['weather', 'realtime'],
title: 'Realtime Weather',
description: 'Get realtime weather information',
},
ui: {
url: 'https://realtime-weather.chat-plugin.lobehub.com/iframe',
height: 310,
},
version: '1',
};
global.fetch = vi.fn((url) =>
Promise.resolve({
ok: true,
headers: new Headers({ 'content-type': 'application/json' }),
json: () => Promise.resolve(url === openapiUrl ? openAPIV3 : fakeManifest),
}),
) as any;
const manifest = await toolService.getToolManifest(manifestUrl);
expect(manifest).toMatchSnapshot();
});
it('should return error on openAPIInvalid', async () => {
const openapiUrl = 'http://fake-url.com/openapiUrl.json';
const manifestUrl = 'http://fake-url.com/manifest.json';
const fakeManifest = {
$schema: '../node_modules/@lobehub/chat-plugin-sdk/schema.json',
api: [],
openapi: openapiUrl,
author: 'LobeHub',
createAt: '2023-08-12',
homepage: 'https://github.com/lobehub/chat-plugin-realtime-weather',
identifier: 'realtime-weather',
meta: {
avatar: '🌈',
tags: ['weather', 'realtime'],
title: 'Realtime Weather',
description: 'Get realtime weather information',
},
ui: {
url: 'https://realtime-weather.chat-plugin.lobehub.com/iframe',
height: 310,
},
version: '1',
};
global.fetch = vi.fn((url) =>
Promise.resolve({
ok: true,
headers: new Headers({ 'content-type': 'application/json' }),
json: () => Promise.resolve(url === openapiUrl ? [] : fakeManifest),
}),
) as any;
try {
await toolService.getToolManifest(manifestUrl);
} catch (e) {
expect(e).toEqual(new TypeError('openAPIInvalid'));
}
});
});
});
it('can parse the OpenAI plugin', async () => {

View File

@@ -161,137 +161,28 @@ describe('useToolStore:pluginStore', () => {
});
describe('installPlugin', () => {
it('should install a plugin with valid manifest', async () => {
const pluginIdentifier = 'plugin1';
const originalUpdateInstallLoadingState = useToolStore.getState().updateInstallLoadingState;
const updateInstallLoadingStateMock = vi.fn();
act(() => {
useToolStore.setState({
updateInstallLoadingState: updateInstallLoadingStateMock,
});
});
const pluginManifestMock = {
$schema: '../node_modules/@lobehub/chat-plugin-sdk/schema.json',
api: [
{
url: 'https://realtime-weather.chat-plugin.lobehub.com/api/v1',
name: 'fetchCurrentWeather',
description: '获取当前天气情况',
parameters: {
properties: {
city: {
description: '城市名称',
type: 'string',
},
},
required: ['city'],
type: 'object',
},
},
],
author: 'LobeHub',
createAt: '2023-08-12',
homepage: 'https://github.com/lobehub/chat-plugin-realtime-weather',
identifier: 'realtime-weather',
meta: {
avatar: '🌈',
tags: ['weather', 'realtime'],
title: 'Realtime Weather',
description: 'Get realtime weather information',
},
ui: {
url: 'https://realtime-weather.chat-plugin.lobehub.com/iframe',
height: 310,
},
version: '1',
};
(toolService.getToolManifest as Mock).mockResolvedValue(pluginManifestMock);
await act(async () => {
await useToolStore.getState().installPlugin(pluginIdentifier);
});
// Then
expect(toolService.getToolManifest).toHaveBeenCalled();
expect(notification.error).not.toHaveBeenCalled();
expect(updateInstallLoadingStateMock).toHaveBeenCalledTimes(2);
expect(pluginService.installPlugin).toHaveBeenCalledWith({
identifier: 'plugin1',
type: 'plugin',
manifest: pluginManifestMock,
});
act(() => {
useToolStore.setState({
updateInstallLoadingState: originalUpdateInstallLoadingState,
});
});
});
it('should throw error with no error', async () => {
// Given
const error = new TypeError('noManifest');
// Mock necessary modules and functions
(toolService.getToolManifest as Mock).mockRejectedValue(error);
useToolStore.setState({
oldPluginItems: [
{
identifier: 'plugin1',
title: 'plugin1',
avatar: '🍏',
} as DiscoverPluginItem,
],
});
it('should be deprecated and do nothing', async () => {
// Old plugin system has been deprecated
await act(async () => {
await useToolStore.getState().installPlugin('plugin1');
});
expect(notification.error).toHaveBeenCalledWith({
description: 'error.noManifest',
message: 'error.installError',
});
// Should not call any service
expect(toolService.getToolManifest).not.toHaveBeenCalled();
expect(pluginService.installPlugin).not.toHaveBeenCalled();
expect(notification.error).not.toHaveBeenCalled();
});
});
describe('installPlugins', () => {
it('should install multiple plugins', async () => {
// Given
act(() => {
useToolStore.setState({
oldPluginItems: [
{
identifier: 'plugin1',
title: 'plugin1',
avatar: '🍏',
manifest: 'https://abc.com/manifest.json',
} as DiscoverPluginItem,
{
identifier: 'plugin2',
title: 'plugin2',
avatar: '🍏',
manifest: 'https://abc.com/manifest.json',
} as DiscoverPluginItem,
],
});
});
const plugins = ['plugin1', 'plugin2'];
(toolService.getToolManifest as Mock).mockResolvedValue(pluginManifestMock);
// When
it('should be deprecated and do nothing', async () => {
// Old plugin system has been deprecated
await act(async () => {
await useToolStore.getState().installPlugins(plugins);
await useToolStore.getState().installPlugins(['plugin1', 'plugin2']);
});
expect(pluginService.installPlugin).toHaveBeenCalledTimes(2);
// Should not call any service
expect(pluginService.installPlugin).not.toHaveBeenCalled();
});
});

View File

@@ -1,29 +1,23 @@
import { type LobeTool } from '@lobechat/types';
import { uniqBy } from 'es-toolkit/compat';
import { t } from 'i18next';
import { produce } from 'immer';
import { type SWRResponse } from 'swr';
import useSWR from 'swr';
import { notification } from '@/components/AntdStaticMethods';
import { mutate } from '@/libs/swr';
import { pluginService } from '@/services/plugin';
import { toolService } from '@/services/tool';
import { globalHelpers } from '@/store/global/helpers';
import { pluginStoreSelectors } from '@/store/tool/selectors';
import { type StoreSetter } from '@/store/types';
import {
type DiscoverPluginItem,
type PluginListResponse,
type PluginQueryParams,
} from '@/types/discover';
import { type PluginInstallError } from '@/types/tool/plugin';
import { sleep } from '@/utils/sleep';
import { setNamespace } from '@/utils/storeDebug';
import { type ToolStore } from '../../store';
import { type PluginInstallProgress, type PluginStoreState } from './initialState';
import { PluginInstallStep } from './initialState';
const n = setNamespace('pluginStore');
@@ -44,107 +38,21 @@ export class PluginStoreActionImpl {
}
installOldPlugin = async (
name: string,
type: 'plugin' | 'customPlugin' = 'plugin',
_name: string,
_type: 'plugin' | 'customPlugin' = 'plugin',
): Promise<void> => {
const plugin = pluginStoreSelectors.getPluginById(name)(this.#get());
if (!plugin) return;
const { updateInstallLoadingState, refreshPlugins, updatePluginInstallProgress } = this.#get();
try {
// Start installation process
updateInstallLoadingState(name, true);
// Step 1: Fetch plugin manifest
updatePluginInstallProgress(name, {
progress: 25,
step: PluginInstallStep.FETCHING_MANIFEST,
});
const data = await toolService.getToolManifest(plugin.manifest);
// Step 2: Install plugin
updatePluginInstallProgress(name, {
progress: 60,
step: PluginInstallStep.INSTALLING_PLUGIN,
});
await pluginService.installPlugin({ identifier: plugin.identifier, manifest: data, type });
updatePluginInstallProgress(name, {
progress: 85,
step: PluginInstallStep.INSTALLING_PLUGIN,
});
await refreshPlugins();
// Step 4: Complete installation
updatePluginInstallProgress(name, {
progress: 100,
step: PluginInstallStep.COMPLETED,
});
// Briefly show completion status then clear progress
await sleep(1000);
updatePluginInstallProgress(name, undefined);
updateInstallLoadingState(name, undefined);
} catch (error) {
console.error(error);
const err = error as PluginInstallError;
// Set error state
updatePluginInstallProgress(name, {
error: err.message,
progress: 0,
step: PluginInstallStep.ERROR,
});
updateInstallLoadingState(name, undefined);
notification.error({
description: t(`error.${err.message}`, { ns: 'plugin' }),
message: t('error.installError', { name: plugin.title, ns: 'plugin' }),
});
}
// Old plugin system has been deprecated, skip installation silently
};
installPlugin = async (
name: string,
type: 'plugin' | 'customPlugin' = 'plugin',
_name: string,
_type: 'plugin' | 'customPlugin' = 'plugin',
): Promise<void> => {
const plugin = pluginStoreSelectors.getPluginById(name)(this.#get());
if (!plugin) return;
const { updateInstallLoadingState, refreshPlugins } = this.#get();
try {
updateInstallLoadingState(name, true);
const data = await toolService.getToolManifest(plugin.manifest);
await pluginService.installPlugin({ identifier: plugin.identifier, manifest: data, type });
await refreshPlugins();
updateInstallLoadingState(name, undefined);
} catch (error) {
console.error(error);
const err = error as PluginInstallError;
updateInstallLoadingState(name, undefined);
notification.error({
description: t(`error.${err.message}`, { ns: 'plugin' }),
message: t('error.installError', { name: plugin.title, ns: 'plugin' }),
});
}
// Old plugin system has been deprecated, skip installation silently
};
installPlugins = async (plugins: string[]): Promise<void> => {
const { installPlugin } = this.#get();
await Promise.all(plugins.map((identifier) => installPlugin(identifier)));
installPlugins = async (_plugins: string[]): Promise<void> => {
// Old plugin system has been deprecated, skip installation silently
};
loadMorePlugins = (): void => {

View File

@@ -3,7 +3,6 @@ import { act, renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { pluginService } from '@/services/plugin';
import { type DiscoverPluginItem } from '@/types/discover';
import { merge } from '@/utils/merge';
import { useToolStore } from '../../store';
@@ -22,24 +21,8 @@ beforeEach(() => {
describe('useToolStore:plugin', () => {
describe('checkPluginsIsInstalled', () => {
it('should not perform any operations if the plugin list is empty', async () => {
const installPluginsMock = vi.fn();
useToolStore.setState({
loadPluginStore: vi.fn(),
installPlugins: installPluginsMock,
});
const { result } = renderHook(() => useToolStore());
await act(async () => {
await result.current.checkPluginsIsInstalled([]);
});
expect(installPluginsMock).not.toHaveBeenCalled();
});
it('should load the plugin store and install plugins if necessary', async () => {
const plugins = ['plugin1', 'plugin2'];
it('should be deprecated and do nothing', async () => {
// Old plugin system has been deprecated
const loadPluginStoreMock = vi.fn();
const installPluginsMock = vi.fn();
useToolStore.setState({
@@ -50,32 +33,12 @@ describe('useToolStore:plugin', () => {
const { result } = renderHook(() => useToolStore());
await act(async () => {
await result.current.checkPluginsIsInstalled(plugins);
});
expect(loadPluginStoreMock).toHaveBeenCalled();
expect(installPluginsMock).toHaveBeenCalledWith(plugins);
});
it('should not load the plugin store and install plugins', async () => {
const plugins = ['plugin1', 'plugin2'];
const loadPluginStoreMock = vi.fn();
const installPluginsMock = vi.fn();
useToolStore.setState({
loadPluginStore: loadPluginStoreMock,
installPlugins: installPluginsMock,
installedPlugins: [{ identifier: 'abc' }] as LobeTool[],
oldPluginItems: [{ identifier: 'abc' }] as DiscoverPluginItem[],
});
const { result } = renderHook(() => useToolStore());
await act(async () => {
await result.current.checkPluginsIsInstalled(plugins);
await result.current.checkPluginsIsInstalled(['plugin1', 'plugin2']);
});
// Should not call any methods since old plugin system is deprecated
expect(loadPluginStoreMock).not.toHaveBeenCalled();
expect(installPluginsMock).toHaveBeenCalledWith(plugins);
expect(installPluginsMock).not.toHaveBeenCalled();
});
});

View File

@@ -8,7 +8,6 @@ import { type StoreSetter } from '@/store/types';
import { merge } from '@/utils/merge';
import { type ToolStore } from '../../store';
import { pluginStoreSelectors } from '../oldStore/selectors';
import { pluginSelectors } from './selectors';
/**
@@ -29,19 +28,8 @@ export class PluginActionImpl {
this.#get = get;
}
checkPluginsIsInstalled = async (plugins: string[]): Promise<void> => {
// if there is no plugins, just skip.
if (plugins.length === 0) return;
const { loadPluginStore, installPlugins } = this.#get();
// check if the store is empty
// if it is, we need to load the plugin store
if (pluginStoreSelectors.onlinePluginStore(this.#get()).length === 0) {
await loadPluginStore();
}
await installPlugins(plugins);
checkPluginsIsInstalled = async (_plugins: string[]): Promise<void> => {
// Old plugin system has been deprecated, skip auto-installation
};
removeAllPlugins = async (): Promise<void> => {