mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 09:37:28 +00:00
♻️ refactor: remove chat-plugin-sdk (#13512)
* ♻️ refactor: remove @lobehub/chat-plugin-sdk dependency Plugins have been deprecated. This removes the SDK entirely: - Define built-in ToolManifest, ToolManifestSettings, ToolErrorType types - Delete src/features/PluginsUI/ (plugin iframe rendering) - Delete src/store/tool/slices/oldStore/ (deprecated plugin store) - Delete src/server/services/pluginGateway/ (plugin gateway) - Delete src/app/(backend)/webapi/plugin/gateway/ (plugin API route) - Migrate all ~50 files from SDK imports to @lobechat/types - Remove @lobehub/chat-plugin-sdk, @lobehub/chat-plugins-gateway deps - Remove @swagger-api/apidom-reference override and patch Fixes LOBE-6655 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: add missing getInstalledPlugins mock in customPlugin test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🔧 chore: increase Vercel build memory limit to 8192MB The 6144MB limit was causing OOM during Vite SPA chunk rendering. Aligned with other build commands that already use 8192MB. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ♻️ refactor: unify default tool type to builtin and fix CustomRender - Remove `invokeDefaultTypePlugin` — default type now falls through to builtin in both server and client execution paths - Fix `CustomRender` to actually render builtin tool components via `getBuiltinRender` instead of always returning null - Increase SPA build memory limit from 7168MB to 8192MB to fix OOM Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ♻️ refactor: remove legacy plugin gateway and type-specific invocations - Delete `runPluginApi`, `internal_callPluginApi`, `invokeMarkdownTypePlugin`, `invokeStandaloneTypePlugin` - Remove plugin gateway endpoint (`/webapi/plugin/gateway`) from URL config - Remove special `builtin → default` runtimeType mapping in plugin model - Clean up unused imports and related tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: add 'builtin' to runtimeType union to fix type error Use ToolManifestType instead of inline union for runtimeType fields so that 'builtin' is included as a valid type. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0dc8930750
commit
3415df3715
106 changed files with 310 additions and 3090 deletions
|
|
@ -179,7 +179,7 @@ This system is expected to be gradually deprecated
|
|||
in favor of the MCP tool system.
|
||||
|
||||
- Frontend calls them via the
|
||||
`invokeDefaultTypePlugin` method
|
||||
`invokeBuiltinTool` method
|
||||
- Retrieves plugin settings and manifest,
|
||||
creates authentication headers,
|
||||
and sends requests to the plugin gateway
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ while (state.status !== 'done' && state.status !== 'error') {
|
|||
**Plugin 工具**:传统插件体系,通过 API 网关调用。
|
||||
该体系预期将逐步废弃,由 MCP 工具体系替代。
|
||||
|
||||
- 前端通过 `invokeDefaultTypePlugin` 方法调用
|
||||
- 前端通过 `invokeBuiltinTool` 方法调用
|
||||
- 获取插件设置和清单、创建认证请求头、
|
||||
发送请求到插件网关
|
||||
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@
|
|||
"build:next": "cross-env NODE_OPTIONS=--max-old-space-size=7168 bun run build:next:raw",
|
||||
"build:next:raw": "next build",
|
||||
"build:raw": "bun run build:spa:raw && bun run build:spa:copy && bun run build:next:raw",
|
||||
"build:spa": "cross-env NODE_OPTIONS=--max-old-space-size=7168 pnpm run build:spa:raw",
|
||||
"build:spa": "cross-env NODE_OPTIONS=--max-old-space-size=8192 pnpm run build:spa:raw",
|
||||
"build:spa:copy": "tsx scripts/copySpaBuild.mts && tsx scripts/generateSpaTemplates.mts",
|
||||
"build:spa:mobile": "cross-env NODE_OPTIONS=--max-old-space-size=8192 MOBILE=true vite build",
|
||||
"build:spa:raw": "rm -rf public/_spa && vite build",
|
||||
"build:vercel": "cross-env-shell NODE_OPTIONS=--max-old-space-size=6144 \"bun run build:raw && bun run db:migrate\"",
|
||||
"build:vercel": "cross-env-shell NODE_OPTIONS=--max-old-space-size=8192 \"bun run build:raw && bun run db:migrate\"",
|
||||
"build-migrate-db": "bun run db:migrate",
|
||||
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
|
||||
"clean:node_modules": "bash -lc 'set -e; echo \"Removing all node_modules...\"; rm -rf node_modules; pnpm -r exec rm -rf node_modules; rm -rf apps/desktop/node_modules; echo \"All node_modules removed.\"'",
|
||||
|
|
@ -262,8 +262,6 @@
|
|||
"@lobechat/web-crawler": "workspace:*",
|
||||
"@lobehub/analytics": "^1.6.0",
|
||||
"@lobehub/charts": "^5.0.0",
|
||||
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
||||
"@lobehub/chat-plugins-gateway": "^1.9.0",
|
||||
"@lobehub/desktop-ipc-typings": "workspace:*",
|
||||
"@lobehub/editor": "^4.5.0",
|
||||
"@lobehub/icons": "^5.0.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { DEFAULT_PREFERENCE } from '@lobechat/const';
|
||||
import type { CustomPluginParams, UserAgentOnboarding, UserOnboarding } from '@lobechat/types';
|
||||
import type { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import type {
|
||||
CustomPluginParams,
|
||||
ToolManifest,
|
||||
UserAgentOnboarding,
|
||||
UserOnboarding,
|
||||
} from '@lobechat/types';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { boolean, index, jsonb, pgTable, primaryKey, text, varchar } from 'drizzle-orm/pg-core';
|
||||
|
||||
|
|
@ -95,7 +99,7 @@ export const userInstalledPlugins = pgTable(
|
|||
|
||||
identifier: text('identifier').notNull(),
|
||||
type: text('type', { enum: ['plugin', 'customPlugin'] }).notNull(),
|
||||
manifest: jsonb('manifest').$type<LobeChatPluginManifest>(),
|
||||
manifest: jsonb('manifest').$type<ToolManifest>(),
|
||||
settings: jsonb('settings'),
|
||||
customParams: jsonb('custom_params').$type<CustomPluginParams>(),
|
||||
source: varchar255('source'),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
"dependencies": {
|
||||
"@lobechat/python-interpreter": "workspace:*",
|
||||
"@lobechat/web-crawler": "workspace:*",
|
||||
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
||||
"@lobehub/market-sdk": "0.32.2",
|
||||
"@lobehub/market-types": "^1.12.3",
|
||||
"model-bank": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import type { LobeChatPluginMeta, Meta } from '@lobehub/chat-plugin-sdk/lib/types/market';
|
||||
import type { ToolManifest } from '../tool/manifest';
|
||||
|
||||
export enum PluginCategory {
|
||||
All = 'all',
|
||||
|
|
@ -24,7 +23,24 @@ export enum PluginSorts {
|
|||
Title = 'title',
|
||||
}
|
||||
|
||||
export interface DiscoverPluginItem extends Omit<LobeChatPluginMeta, 'meta'>, Meta {
|
||||
interface PluginMeta {
|
||||
avatar: string;
|
||||
description?: string;
|
||||
tags?: string[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface DiscoverPluginMeta {
|
||||
author: string;
|
||||
createdAt: string;
|
||||
homepage: string;
|
||||
identifier: string;
|
||||
manifest: string;
|
||||
meta: PluginMeta;
|
||||
schemaVersion: number;
|
||||
}
|
||||
|
||||
export interface DiscoverPluginItem extends Omit<DiscoverPluginMeta, 'meta'>, PluginMeta {
|
||||
category?: PluginCategory;
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +71,7 @@ export interface PluginListResponse {
|
|||
export type PluginSource = 'legacy' | 'market' | 'builtin';
|
||||
|
||||
export interface DiscoverPluginDetail extends Omit<DiscoverPluginItem, 'manifest'> {
|
||||
manifest?: LobeChatPluginManifest | string;
|
||||
manifest?: ToolManifest | string;
|
||||
related: DiscoverPluginItem[];
|
||||
/**
|
||||
* Plugin source type
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { IPluginErrorType } from '@lobehub/chat-plugin-sdk';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { ILobeAgentRuntimeErrorType } from '../../agentRuntime';
|
||||
import type { ErrorType } from '../../fetch';
|
||||
import type { IToolErrorType } from '../../tool/error';
|
||||
|
||||
/**
|
||||
* Chat message error object
|
||||
|
|
@ -10,7 +10,7 @@ import type { ErrorType } from '../../fetch';
|
|||
export interface ChatMessageError {
|
||||
body?: any;
|
||||
message?: string;
|
||||
type: ErrorType | IPluginErrorType | ILobeAgentRuntimeErrorType;
|
||||
type: ErrorType | IToolErrorType | ILobeAgentRuntimeErrorType;
|
||||
}
|
||||
|
||||
export const ChatMessageErrorSchema = z.object({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import type { IPluginErrorType } from '@lobehub/chat-plugin-sdk';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
|
@ -129,5 +128,5 @@ export const ChatToolPayloadSchema = z.object({
|
|||
export interface ChatMessagePluginError {
|
||||
body?: any;
|
||||
message: string;
|
||||
type: IPluginErrorType;
|
||||
type: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { type RuntimeStepContext } from '../stepContext';
|
|||
import { type HumanInterventionConfig, type HumanInterventionPolicy } from './intervention';
|
||||
import { HumanInterventionConfigSchema, HumanInterventionPolicySchema } from './intervention';
|
||||
|
||||
interface Meta {
|
||||
export interface Meta {
|
||||
/**
|
||||
* avatar
|
||||
* @desc Avatar of the plugin
|
||||
|
|
@ -35,7 +35,7 @@ interface Meta {
|
|||
title: string;
|
||||
}
|
||||
|
||||
const MetaSchema = z.object({
|
||||
export const MetaSchema = z.object({
|
||||
avatar: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
readme: z.string().optional(),
|
||||
|
|
|
|||
5
packages/types/src/tool/error.ts
Normal file
5
packages/types/src/tool/error.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export const ToolErrorType = {
|
||||
PluginSettingsInvalid: 'PluginSettingsInvalid',
|
||||
} as const;
|
||||
|
||||
export type IToolErrorType = (typeof ToolErrorType)[keyof typeof ToolErrorType];
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
import type { LobeChatPluginManifest, LobePluginType } from '@lobehub/chat-plugin-sdk';
|
||||
|
||||
import type { ToolManifest, ToolManifestType } from './manifest';
|
||||
import type { CustomPluginParams } from './plugin';
|
||||
import type { LobeToolType } from './tool';
|
||||
|
||||
export interface LobeTool {
|
||||
customParams?: CustomPluginParams | null;
|
||||
identifier: string;
|
||||
manifest?: LobeChatPluginManifest | null;
|
||||
manifest?: ToolManifest | null;
|
||||
/**
|
||||
* use for runtime
|
||||
*/
|
||||
runtimeType?: 'mcp' | 'default' | 'markdown' | 'standalone';
|
||||
runtimeType?: ToolManifestType;
|
||||
settings?: any;
|
||||
// TODO: remove type and then make it required
|
||||
source?: LobeToolType;
|
||||
|
|
@ -21,12 +20,14 @@ export interface LobeTool {
|
|||
type: LobeToolType;
|
||||
}
|
||||
|
||||
export type LobeToolRenderType = LobePluginType | 'builtin';
|
||||
export type LobeToolRenderType = ToolManifestType;
|
||||
|
||||
export * from './builtin';
|
||||
export * from './crawler';
|
||||
export * from './error';
|
||||
export * from './interpreter';
|
||||
export * from './intervention';
|
||||
export * from './manifest';
|
||||
export * from './plugin';
|
||||
export * from './search';
|
||||
export * from './tool';
|
||||
|
|
|
|||
58
packages/types/src/tool/manifest.ts
Normal file
58
packages/types/src/tool/manifest.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { type LobeChatPluginApi, LobeChatPluginApiSchema, type Meta, MetaSchema } from './builtin';
|
||||
|
||||
export type ToolManifestType = 'builtin' | 'default' | 'markdown' | 'mcp' | 'standalone';
|
||||
|
||||
export interface ToolManifestSettings {
|
||||
properties: Record<string, any>;
|
||||
required?: string[];
|
||||
type: 'object';
|
||||
}
|
||||
|
||||
export const ToolManifestSettingsSchema = z.object({
|
||||
properties: z.record(z.string(), z.any()),
|
||||
required: z.array(z.string()).optional(),
|
||||
type: z.literal('object'),
|
||||
});
|
||||
|
||||
export interface ToolManifest {
|
||||
$schema?: string;
|
||||
api: LobeChatPluginApi[];
|
||||
author?: string;
|
||||
createdAt?: string;
|
||||
gateway?: string;
|
||||
homepage?: string;
|
||||
identifier: string;
|
||||
meta: Meta;
|
||||
openapi?: string;
|
||||
settings?: ToolManifestSettings;
|
||||
systemRole?: string;
|
||||
type?: ToolManifestType;
|
||||
ui?: { height?: number; mode?: 'iframe' | 'module'; url: string; width?: number };
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export const ToolManifestSchema = z.object({
|
||||
$schema: z.string().optional(),
|
||||
api: z.array(LobeChatPluginApiSchema),
|
||||
author: z.string().optional(),
|
||||
createdAt: z.string().optional(),
|
||||
gateway: z.string().optional(),
|
||||
homepage: z.string().optional(),
|
||||
identifier: z.string(),
|
||||
meta: MetaSchema,
|
||||
openapi: z.string().optional(),
|
||||
settings: ToolManifestSettingsSchema.optional(),
|
||||
systemRole: z.string().optional(),
|
||||
type: z.enum(['default', 'standalone', 'markdown', 'builtin', 'mcp']).optional(),
|
||||
ui: z
|
||||
.object({
|
||||
height: z.number().optional(),
|
||||
mode: z.enum(['iframe', 'module']).optional(),
|
||||
url: z.string(),
|
||||
width: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
version: z.string().optional(),
|
||||
});
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import type { LobeChatPluginManifest, Meta } from '@lobehub/chat-plugin-sdk';
|
||||
|
||||
import type { Meta } from './builtin';
|
||||
import type { ToolManifest, ToolManifestType } from './manifest';
|
||||
import type { LobeToolType } from './tool';
|
||||
|
||||
export type PluginManifestMap = Record<string, LobeChatPluginManifest>;
|
||||
export type PluginManifestMap = Record<string, ToolManifest>;
|
||||
|
||||
export interface CustomPluginMetadata {
|
||||
avatar?: string;
|
||||
|
|
@ -53,7 +53,7 @@ export interface CustomPluginParams {
|
|||
export interface LobeToolCustomPlugin {
|
||||
customParams?: CustomPluginParams;
|
||||
identifier: string;
|
||||
manifest?: LobeChatPluginManifest;
|
||||
manifest?: ToolManifest;
|
||||
settings?: any;
|
||||
type: 'customPlugin';
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ export interface InstallPluginMeta extends Partial<Meta> {
|
|||
createdAt?: string;
|
||||
homepage?: string;
|
||||
identifier: string;
|
||||
runtimeType?: 'mcp' | 'default' | 'markdown' | 'standalone' | undefined;
|
||||
runtimeType?: ToolManifestType;
|
||||
type: LobeToolType;
|
||||
}
|
||||
|
||||
|
|
@ -71,3 +71,12 @@ export interface PluginInstallError {
|
|||
cause?: string;
|
||||
message: 'noManifest' | 'fetchError' | 'manifestInvalid' | 'urlError';
|
||||
}
|
||||
|
||||
export interface PluginRequestPayload {
|
||||
apiName: string;
|
||||
arguments?: string;
|
||||
identifier: string;
|
||||
indexUrl?: string;
|
||||
manifest?: ToolManifest;
|
||||
type?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@lobechat/const": "workspace:*",
|
||||
"@lobechat/ssrf-safe-fetch": "workspace:*",
|
||||
"@lobechat/types": "workspace:*",
|
||||
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
||||
"@vercel/functions": "^3.3.0",
|
||||
"brotli-wasm": "^3.0.1",
|
||||
"chroma-js": "^3.1.2",
|
||||
|
|
@ -33,7 +33,6 @@
|
|||
"remark": "^15.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-html": "^16.0.1",
|
||||
"@lobechat/ssrf-safe-fetch": "workspace:*",
|
||||
"tokenx": "^1.2.1",
|
||||
"ua-parser-js": "^1.0.41",
|
||||
"uuid": "^11.1.0",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import type { OpenAIPluginManifest } from '@lobechat/types';
|
||||
import type { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { pluginManifestSchema } from '@lobehub/chat-plugin-sdk';
|
||||
import type { ToolManifest } from '@lobechat/types';
|
||||
import { ToolManifestSchema } from '@lobechat/types';
|
||||
|
||||
import { API_ENDPOINTS } from '@/services/_url';
|
||||
|
||||
const fetchJSON = async <T = any>(url: string, proxy = false): Promise<T> => {
|
||||
// 2. Send request
|
||||
let res: Response;
|
||||
try {
|
||||
res = await (proxy ? fetch(API_ENDPOINTS.proxy, { body: url, method: 'POST' }) : fetch(url));
|
||||
|
|
@ -36,67 +34,17 @@ const fetchJSON = async <T = any>(url: string, proxy = false): Promise<T> => {
|
|||
return data;
|
||||
};
|
||||
|
||||
export const convertOpenAIManifestToLobeManifest = (
|
||||
data: OpenAIPluginManifest,
|
||||
): LobeChatPluginManifest => {
|
||||
const manifest: LobeChatPluginManifest = {
|
||||
api: [],
|
||||
homepage: data.legal_info_url,
|
||||
identifier: data.name_for_model,
|
||||
meta: {
|
||||
avatar: data.logo_url,
|
||||
description: data.description_for_human,
|
||||
title: data.name_for_human,
|
||||
},
|
||||
openapi: data.api.url,
|
||||
systemRole: data.description_for_model,
|
||||
type: 'default',
|
||||
version: '1',
|
||||
};
|
||||
switch (data.auth.type) {
|
||||
case 'none': {
|
||||
break;
|
||||
}
|
||||
case 'service_http': {
|
||||
manifest.settings = {
|
||||
properties: {
|
||||
apiAuthKey: {
|
||||
default: data.auth.verification_tokens['openai'],
|
||||
description: 'API Key',
|
||||
format: 'password',
|
||||
title: 'API Key',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return manifest;
|
||||
};
|
||||
|
||||
export const getToolManifest = async (
|
||||
url?: string,
|
||||
useProxy: boolean = false,
|
||||
): Promise<LobeChatPluginManifest> => {
|
||||
// 1. Validate plugin
|
||||
): Promise<ToolManifest> => {
|
||||
if (!url) {
|
||||
throw new TypeError('noManifest');
|
||||
}
|
||||
|
||||
// 2. Send request
|
||||
let data = await fetchJSON<LobeChatPluginManifest>(url, useProxy);
|
||||
const data = await fetchJSON<ToolManifest>(url, useProxy);
|
||||
|
||||
// @ts-ignore
|
||||
// if there is a description_for_model, it is an OpenAI plugin
|
||||
// we need convert to lobe plugin
|
||||
if (data['description_for_model']) {
|
||||
data = convertOpenAIManifestToLobeManifest(data as any);
|
||||
}
|
||||
// 3. Validate plugin file format specification
|
||||
const parser = pluginManifestSchema.safeParse(data);
|
||||
const parser = ToolManifestSchema.safeParse(data);
|
||||
|
||||
if (!parser.success) {
|
||||
throw new TypeError('manifestInvalid', { cause: parser.error });
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
diff --git a/node_modules/.cache/logger/umi.log b/node_modules/.cache/logger/umi.log
|
||||
new file mode 100644
|
||||
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
||||
diff --git a/src/parse/parsers/binary/index-node.cjs b/src/parse/parsers/binary/index-node.cjs
|
||||
index a88ba4d8a0ddff6f4b545ed9368739d94f893eb2..b9b78a29e3c228e7d78e0861b38d734bdef2c854 100644
|
||||
--- a/src/parse/parsers/binary/index-node.cjs
|
||||
+++ b/src/parse/parsers/binary/index-node.cjs
|
||||
@@ -3,7 +3,7 @@
|
||||
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
|
||||
exports.__esModule = true;
|
||||
exports.default = void 0;
|
||||
-var _buffer = require("#buffer");
|
||||
+var _buffer = require("buffer");
|
||||
var _apidomCore = require("@swagger-api/apidom-core");
|
||||
var _ParserError = _interopRequireDefault(require("../../../errors/ParserError.cjs"));
|
||||
var _Parser = _interopRequireDefault(require("../Parser.cjs"));
|
||||
diff --git a/src/parse/parsers/binary/index-node.mjs b/src/parse/parsers/binary/index-node.mjs
|
||||
index 3abce8c52fd5eb7b92f33b66fccf6896ccbc89fe..4a3f957cf127a71c7f2b4dfa56512271b6f785ab 100644
|
||||
--- a/src/parse/parsers/binary/index-node.mjs
|
||||
+++ b/src/parse/parsers/binary/index-node.mjs
|
||||
@@ -1,4 +1,4 @@
|
||||
-import { Buffer } from '#buffer'; // eslint-disable-line import/order
|
||||
+import { Buffer } from 'buffer'; // eslint-disable-line import/order
|
||||
import { ParseResultElement, StringElement } from '@swagger-api/apidom-core';
|
||||
import ParserError from "../../../errors/ParserError.mjs";
|
||||
import Parser from "../Parser.mjs";
|
||||
|
|
@ -9,8 +9,6 @@ onlyBuiltDependencies:
|
|||
- '@lobehub/editor'
|
||||
|
||||
overrides:
|
||||
'@lobehub/chat-plugin-sdk>swagger-client': 3.36.0
|
||||
'@swagger-api/apidom-reference': 1.1.0
|
||||
jose: ^6.1.3
|
||||
stylelint-config-clean-order: 7.0.0
|
||||
pdfjs-dist: 5.4.530
|
||||
|
|
@ -18,5 +16,4 @@ overrides:
|
|||
react-dom: 19.2.4
|
||||
|
||||
patchedDependencies:
|
||||
'@swagger-api/apidom-reference': patches/@swagger-api__apidom-reference.patch
|
||||
'@upstash/qstash': patches/@upstash__qstash.patch
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
import { AgentRuntimeError } from '@lobechat/model-runtime';
|
||||
import { ChatErrorType, TraceNameMap } from '@lobechat/types';
|
||||
import { type PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
|
||||
import { createGatewayOnEdgeRuntime } from '@lobehub/chat-plugins-gateway';
|
||||
|
||||
import { LOBE_CHAT_TRACE_ID } from '@/const/trace';
|
||||
import { getAppConfig } from '@/envs/app';
|
||||
import { LOBE_CHAT_AUTH_HEADER } from '@/envs/auth';
|
||||
import { TraceClient } from '@/libs/traces';
|
||||
import { parserPluginSettings } from '@/server/services/pluginGateway/settings';
|
||||
import { getTracePayload } from '@/utils/trace';
|
||||
|
||||
const { PLUGINS_INDEX_URL: pluginsIndexUrl, PLUGIN_SETTINGS } = getAppConfig();
|
||||
|
||||
const defaultPluginSettings = parserPluginSettings(PLUGIN_SETTINGS);
|
||||
|
||||
const handler = createGatewayOnEdgeRuntime({ defaultPluginSettings, pluginsIndexUrl });
|
||||
|
||||
export const POST = async (req: Request) => {
|
||||
// get Authorization from header
|
||||
const authorization = req.headers.get(LOBE_CHAT_AUTH_HEADER);
|
||||
if (!authorization) throw AgentRuntimeError.createError(ChatErrorType.Unauthorized);
|
||||
|
||||
// TODO: need to be replace by better telemetry system
|
||||
// add trace
|
||||
const tracePayload = getTracePayload(req);
|
||||
const traceClient = new TraceClient();
|
||||
const trace = traceClient.createTrace({
|
||||
id: tracePayload?.traceId,
|
||||
...tracePayload,
|
||||
});
|
||||
|
||||
const { manifest, indexUrl, ...input } = (await req.clone().json()) as PluginRequestPayload;
|
||||
|
||||
const span = trace?.span({
|
||||
input,
|
||||
metadata: { indexUrl, manifest },
|
||||
name: TraceNameMap.FetchPluginAPI,
|
||||
});
|
||||
|
||||
span?.update({ parentObservationId: tracePayload?.observationId });
|
||||
|
||||
const res = await handler(req);
|
||||
|
||||
span?.end({ output: await res.clone().text() });
|
||||
|
||||
if (trace?.id) {
|
||||
res.headers.set(LOBE_CHAT_TRACE_ID, trace.id);
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
|
@ -1,38 +1,19 @@
|
|||
import { Flexbox } from '@lobehub/ui';
|
||||
import { Switch } from 'antd';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useToolStore } from '@/store/tool';
|
||||
|
||||
import { useStore } from '../store';
|
||||
|
||||
const MarketList = memo<{ id: string }>(({ id }) => {
|
||||
const [toggleAgentPlugin, hasPlugin] = useStore((s) => [s.toggleAgentPlugin, !!s.config.plugins]);
|
||||
const plugins = useStore((s) => s.config.plugins || []);
|
||||
|
||||
const [useFetchPluginList, fetchPluginManifest] = useToolStore((s) => [
|
||||
s.useFetchPluginStore,
|
||||
s.installPlugin,
|
||||
]);
|
||||
|
||||
const pluginManifestLoading = useToolStore((s) => s.pluginInstallLoading, isEqual);
|
||||
|
||||
useFetchPluginList();
|
||||
|
||||
return (
|
||||
<Flexbox horizontal align={'center'} gap={8}>
|
||||
<Switch
|
||||
loading={pluginManifestLoading[id]}
|
||||
checked={
|
||||
// If loading, it means it's activated
|
||||
pluginManifestLoading[id] || !hasPlugin ? false : plugins.includes(id)
|
||||
}
|
||||
onChange={(checked) => {
|
||||
checked={!hasPlugin ? false : plugins.includes(id)}
|
||||
onChange={() => {
|
||||
toggleAgentPlugin(id);
|
||||
if (checked) {
|
||||
fetchPluginManifest(id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
|
|
|
|||
|
|
@ -59,20 +59,17 @@ export const useControls = ({ setUpdating }: { setUpdating: (updating: boolean)
|
|||
const userAgentSkills = useToolStore(agentSkillsSelectors.getUserAgentSkills, isEqual);
|
||||
|
||||
const [
|
||||
useFetchPluginStore,
|
||||
useFetchUserKlavisServers,
|
||||
useFetchLobehubSkillConnections,
|
||||
useFetchUninstalledBuiltinTools,
|
||||
useFetchAgentSkills,
|
||||
] = useToolStore((s) => [
|
||||
s.useFetchPluginStore,
|
||||
s.useFetchUserKlavisServers,
|
||||
s.useFetchLobehubSkillConnections,
|
||||
s.useFetchUninstalledBuiltinTools,
|
||||
s.useFetchAgentSkills,
|
||||
]);
|
||||
|
||||
useFetchPluginStore();
|
||||
useFetchInstalledPlugins();
|
||||
useFetchUninstalledBuiltinTools(true);
|
||||
useFetchAgentSkills(true);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
||||
import { type ILobeAgentRuntimeErrorType } from '@lobechat/model-runtime';
|
||||
import { AgentRuntimeErrorType } from '@lobechat/model-runtime';
|
||||
import { type ChatMessageError, type ErrorType } from '@lobechat/types';
|
||||
import { type ChatMessageError, type ErrorType, type IToolErrorType } from '@lobechat/types';
|
||||
import { ChatErrorType } from '@lobechat/types';
|
||||
import { type IPluginErrorType } from '@lobehub/chat-plugin-sdk';
|
||||
import { type AlertProps } from '@lobehub/ui';
|
||||
import { Block, Highlighter, Skeleton } from '@lobehub/ui';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
|
@ -52,7 +51,7 @@ const OllamaSetupGuide = dynamic(() => import('./OllamaSetupGuide'), {
|
|||
|
||||
// Config for the errorMessage display
|
||||
const getErrorAlertConfig = (
|
||||
errorType?: IPluginErrorType | ILobeAgentRuntimeErrorType | ErrorType,
|
||||
errorType?: IToolErrorType | ILobeAgentRuntimeErrorType | ErrorType,
|
||||
): AlertProps | undefined => {
|
||||
// OpenAIBizError / ZhipuBizError / GoogleBizError / ...
|
||||
if (typeof errorType === 'string' && (errorType.includes('Biz') || errorType.includes('Invalid')))
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { getBuiltinRender } from '@lobechat/builtin-tools/renders';
|
||||
import { type ChatPluginPayload } from '@lobechat/types';
|
||||
import { safeParseJSON } from '@lobechat/utils';
|
||||
import { Flexbox } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
|
||||
import PluginRender from '@/features/PluginsUI/Render';
|
||||
import { type ChatPluginPayload } from '@/types/index';
|
||||
|
||||
interface CustomRenderProps {
|
||||
content: string;
|
||||
/**
|
||||
|
|
@ -19,19 +19,21 @@ interface CustomRenderProps {
|
|||
}
|
||||
|
||||
const CustomRender = memo<CustomRenderProps>(
|
||||
({ toolCallId, messageId, content, pluginState, plugin }) => {
|
||||
({ content, messageId, plugin, pluginState, toolCallId }) => {
|
||||
const Render = getBuiltinRender(plugin?.identifier, plugin?.apiName);
|
||||
|
||||
if (!Render) return null;
|
||||
|
||||
return (
|
||||
<Flexbox gap={12} id={toolCallId} width={'100%'}>
|
||||
<PluginRender
|
||||
arguments={plugin?.arguments}
|
||||
<Render
|
||||
apiName={plugin?.apiName}
|
||||
args={safeParseJSON(plugin?.arguments)}
|
||||
content={content}
|
||||
identifier={plugin?.identifier}
|
||||
loading={false}
|
||||
messageId={messageId}
|
||||
payload={plugin}
|
||||
messageId={messageId!}
|
||||
pluginState={pluginState}
|
||||
toolCallId={toolCallId}
|
||||
type={plugin?.type}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifest } from '@lobechat/types';
|
||||
|
||||
import { safeParseJSON } from '@/utils/safeParseJSON';
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ interface McpServers {
|
|||
}
|
||||
|
||||
interface ParsedMcpInput {
|
||||
manifest?: LobeChatPluginManifest;
|
||||
manifest?: ToolManifest;
|
||||
mcpServers?: McpServers;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifest } from '@lobechat/types';
|
||||
import { Block, Button, Flexbox, Icon, Text } from '@lobehub/ui';
|
||||
import { type FormInstance } from 'antd';
|
||||
import { Form as AForm } from 'antd';
|
||||
|
|
@ -17,7 +17,7 @@ import PluginEmptyState from './EmptyState';
|
|||
|
||||
const PluginPreview = memo<{ form: FormInstance }>(({ form }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
const manifest: LobeChatPluginManifest = AForm.useWatch(['manifest'], form);
|
||||
const manifest: ToolManifest = AForm.useWatch(['manifest'], form);
|
||||
const meta = manifest?.meta;
|
||||
|
||||
if (!manifest)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { BRANDING_NAME } from '@lobechat/business-const';
|
||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifest } from '@lobechat/types';
|
||||
import { ActionIcon, Checkbox, Flexbox, FormItem, Input } from '@lobehub/ui';
|
||||
import { type FormInstance } from 'antd';
|
||||
import { Form } from 'antd';
|
||||
|
|
@ -39,7 +39,7 @@ const UrlManifestForm = memo<{ form: FormInstance; isEditMode: boolean }>(
|
|||
({ form, isEditMode }) => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
const [manifest, setManifest] = useState<LobeChatPluginManifest>();
|
||||
const [manifest, setManifest] = useState<ToolManifest>();
|
||||
|
||||
const urlKey = ['customParams', 'manifestUrl'];
|
||||
const proxyKey = ['customParams', 'useProxy'];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type PluginSchema } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifestSettings } from '@lobechat/types';
|
||||
import { Form, Markdown } from '@lobehub/ui';
|
||||
import { Form as AForm } from 'antd';
|
||||
import { createStaticStyles } from 'antd-style';
|
||||
|
|
@ -10,7 +10,7 @@ import { pluginSelectors } from '@/store/tool/selectors';
|
|||
|
||||
import ItemRender from '../../components/JSONSchemaConfig/ItemRender';
|
||||
|
||||
export const transformPluginSettings = (pluginSettings: PluginSchema) => {
|
||||
export const transformPluginSettings = (pluginSettings: ToolManifestSettings) => {
|
||||
if (!pluginSettings?.properties) return [];
|
||||
|
||||
return Object.entries(pluginSettings.properties).map(([name, i]) => ({
|
||||
|
|
@ -28,7 +28,7 @@ export const transformPluginSettings = (pluginSettings: PluginSchema) => {
|
|||
|
||||
interface PluginSettingsConfigProps {
|
||||
id: string;
|
||||
schema: PluginSchema;
|
||||
schema: ToolManifestSettings;
|
||||
}
|
||||
|
||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import BuiltinType from './index';
|
||||
|
||||
// Mock renders module
|
||||
const mockWebBrowsingRender = vi.fn(({ content }) => <div>WebBrowsingRender: {content}</div>);
|
||||
const mockCodeInterpreterRender = vi.fn(({ content }) => (
|
||||
<div>CodeInterpreterRender: {content}</div>
|
||||
));
|
||||
|
||||
vi.mock('@lobechat/builtin-tools/renders', () => ({
|
||||
getBuiltinRender: vi.fn((identifier, apiName) => {
|
||||
if (identifier === 'lobe-web-browsing') return mockWebBrowsingRender;
|
||||
if (identifier === 'lobe-code-interpreter') return mockCodeInterpreterRender;
|
||||
return undefined;
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock useParseContent hook
|
||||
vi.mock('../useParseContent', () => ({
|
||||
useParseContent: vi.fn((content) => ({ data: content })),
|
||||
}));
|
||||
|
||||
describe('BuiltinType', () => {
|
||||
it('should not render anything if identifier is not provided', () => {
|
||||
const { container } = render(<BuiltinType content="..." messageId="123" />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render anything if identifier is unknown', () => {
|
||||
const { container } = render(<BuiltinType content="{}" identifier="unknown" messageId="123" />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render the correct renderer for web browsing', () => {
|
||||
const content = '{"query":"test"}';
|
||||
render(<BuiltinType content={content} identifier="lobe-web-browsing" messageId="123" />);
|
||||
expect(screen.getByText(`WebBrowsingRender: ${content}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the correct renderer for code interpreter', () => {
|
||||
const content = '{"code":"print(1)"}';
|
||||
render(<BuiltinType content={content} identifier="lobe-code-interpreter" messageId="123" />);
|
||||
expect(screen.getByText(`CodeInterpreterRender: ${content}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should pass correct props to renderer', () => {
|
||||
const content = '{"test":"data"}';
|
||||
const args = '{"arg":"value"}';
|
||||
const pluginState = { state: 'value' };
|
||||
const pluginError = { error: 'test' };
|
||||
|
||||
render(
|
||||
<BuiltinType
|
||||
apiName="testApi"
|
||||
arguments={args}
|
||||
content={content}
|
||||
identifier="lobe-web-browsing"
|
||||
messageId="msg-123"
|
||||
pluginError={pluginError}
|
||||
pluginState={pluginState}
|
||||
toolCallId="tool-call-123"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(`WebBrowsingRender: ${content}`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import { getBuiltinRender } from '@lobechat/builtin-tools/renders';
|
||||
import { ToolRenderProvider } from '@lobechat/shared-tool-ui';
|
||||
import { safeParseJSON } from '@lobechat/utils';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useParseContent } from '../useParseContent';
|
||||
import { useToolRenderCaps } from './useToolRenderCaps';
|
||||
|
||||
export interface BuiltinTypeProps {
|
||||
apiName?: string;
|
||||
arguments?: string;
|
||||
content: string;
|
||||
identifier?: string;
|
||||
loading?: boolean;
|
||||
/**
|
||||
* The real message ID (tool message ID)
|
||||
*/
|
||||
messageId?: string;
|
||||
pluginError?: any;
|
||||
pluginState?: any;
|
||||
/**
|
||||
* The tool call ID from the assistant message
|
||||
*/
|
||||
toolCallId?: string;
|
||||
}
|
||||
|
||||
const BuiltinType = memo<BuiltinTypeProps>(
|
||||
({
|
||||
content,
|
||||
arguments: argumentsStr = '',
|
||||
pluginState,
|
||||
toolCallId,
|
||||
messageId,
|
||||
identifier,
|
||||
pluginError,
|
||||
apiName,
|
||||
}) => {
|
||||
const { data } = useParseContent(content);
|
||||
const caps = useToolRenderCaps();
|
||||
|
||||
const Render = getBuiltinRender(identifier, apiName);
|
||||
|
||||
if (!Render) return;
|
||||
|
||||
const args = safeParseJSON(argumentsStr);
|
||||
|
||||
return (
|
||||
<ToolRenderProvider value={caps}>
|
||||
<Render
|
||||
apiName={apiName}
|
||||
args={args || {}}
|
||||
content={data}
|
||||
identifier={identifier}
|
||||
messageId={messageId || toolCallId || ''}
|
||||
pluginError={pluginError}
|
||||
pluginState={pluginState}
|
||||
toolCallId={toolCallId}
|
||||
/>
|
||||
</ToolRenderProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default BuiltinType;
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import { type PluginRenderProps } from '@lobehub/chat-plugin-sdk/client';
|
||||
import { Skeleton } from '@lobehub/ui';
|
||||
import { memo, useRef, useState } from 'react';
|
||||
|
||||
import { useOnPluginReadyForInteraction } from '../../utils/iframeOnReady';
|
||||
import { useOnPluginFetchMessage } from '../../utils/listenToPlugin';
|
||||
import { sendMessageContentToPlugin } from '../../utils/postMessage';
|
||||
|
||||
interface IFrameRenderProps extends PluginRenderProps {
|
||||
height?: number;
|
||||
url: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
const IFrameRender = memo<IFrameRenderProps>(({ url, width = 800, height = 300, ...props }) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// When props change, proactively send data to the iframe
|
||||
useOnPluginReadyForInteraction(() => {
|
||||
const iframeWin = iframeRef.current?.contentWindow;
|
||||
|
||||
if (iframeWin) {
|
||||
sendMessageContentToPlugin(iframeWin, props);
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
// when get iframe fetch message ,send message content
|
||||
useOnPluginFetchMessage(() => {
|
||||
const iframeWin = iframeRef.current?.contentWindow;
|
||||
if (iframeWin) {
|
||||
sendMessageContentToPlugin(iframeWin, props);
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <Skeleton active style={{ maxWidth: '100%', width }} />}
|
||||
<iframe
|
||||
// @ts-ignore
|
||||
allowtransparency="true"
|
||||
height={height}
|
||||
hidden={loading}
|
||||
ref={iframeRef}
|
||||
src={url}
|
||||
width={width}
|
||||
style={{
|
||||
border: 0,
|
||||
// iframe cannot be transparent in color-scheme:dark mode
|
||||
// refs: https://www.jianshu.com/p/bc5a37bb6a7b
|
||||
colorScheme: 'light',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
onLoad={() => {
|
||||
setLoading(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
export default IFrameRender;
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { memo } from 'react';
|
||||
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/selectors';
|
||||
|
||||
import Loading from '../Loading';
|
||||
import { useParseContent } from '../useParseContent';
|
||||
import IFrameRender from './IFrameRender';
|
||||
|
||||
export interface PluginDefaultTypeProps {
|
||||
content: string;
|
||||
loading?: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const PluginDefaultType = memo<PluginDefaultTypeProps>(({ content, name, loading }) => {
|
||||
const manifest = useToolStore(pluginSelectors.getToolManifestById(name || ''));
|
||||
|
||||
const { isJSON, data } = useParseContent(content);
|
||||
|
||||
if (!isJSON) {
|
||||
return loading && <Loading />;
|
||||
}
|
||||
|
||||
if (!manifest?.ui) return;
|
||||
|
||||
const ui = manifest.ui;
|
||||
|
||||
if (!ui.url) return;
|
||||
|
||||
return (
|
||||
<IFrameRender
|
||||
content={data}
|
||||
height={ui.height}
|
||||
name={name || 'unknown'}
|
||||
url={ui.url}
|
||||
width={ui.width}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default PluginDefaultType;
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import { Flexbox } from '@lobehub/ui';
|
||||
import { createStaticStyles, keyframes } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const animloader = keyframes`
|
||||
0% {
|
||||
inset-inline-start: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
100% {
|
||||
inset-inline-start: 100%;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
`;
|
||||
|
||||
const styles = createStaticStyles(
|
||||
({ css, cssVar }) => css`
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
|
||||
width: 300px;
|
||||
height: 12px;
|
||||
border: 1px solid ${cssVar.colorBorder};
|
||||
border-radius: 10px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
|
||||
background: ${cssVar.colorPrimary};
|
||||
|
||||
animation: ${animloader} 2s linear infinite;
|
||||
}
|
||||
`,
|
||||
);
|
||||
const Loading = memo(() => {
|
||||
const { t } = useTranslation('plugin');
|
||||
|
||||
return (
|
||||
<Flexbox align={'center'} gap={8} padding={16}>
|
||||
<span className={styles} />
|
||||
{t('loading.content')}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default Loading;
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
import { Flexbox, Image, Markdown } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
|
||||
import Arguments from '@/features/Conversation/Messages/AssistantGroup/Tool/Detail/Arguments';
|
||||
import { type ToolCallResult } from '@/libs/mcp';
|
||||
|
||||
export interface MCPTypeProps {
|
||||
apiName?: string;
|
||||
arguments?: string;
|
||||
content: string;
|
||||
identifier?: string;
|
||||
loading?: boolean;
|
||||
/**
|
||||
* The real message ID (tool message ID)
|
||||
*/
|
||||
messageId?: string;
|
||||
pluginError?: any;
|
||||
pluginState?: ToolCallResult;
|
||||
/**
|
||||
* The tool call ID from the assistant message
|
||||
*/
|
||||
toolCallId?: string;
|
||||
}
|
||||
|
||||
const MCPType = memo<MCPTypeProps>(({ pluginState, arguments: args }) => {
|
||||
if (!pluginState) return;
|
||||
|
||||
const { content } = pluginState;
|
||||
|
||||
const hasImage = content.some((item) => item.type === 'image');
|
||||
return (
|
||||
<Flexbox
|
||||
gap={8}
|
||||
style={
|
||||
!hasImage ? { maxHeight: 400, overflow: 'scroll', padding: 8, width: '100%' } : undefined
|
||||
}
|
||||
>
|
||||
{args && <Arguments arguments={args} />}
|
||||
<Flexbox>
|
||||
<Flexbox>
|
||||
{content.map((item, index) => {
|
||||
switch (item.type) {
|
||||
case 'text': {
|
||||
return (
|
||||
<Markdown key={item.text} variant={'chat'}>
|
||||
{item.text}
|
||||
</Markdown>
|
||||
);
|
||||
}
|
||||
|
||||
case 'image': {
|
||||
return (
|
||||
<Image
|
||||
alt="MCP content"
|
||||
height={'auto'}
|
||||
key={`image-${index}`}
|
||||
src={item.data}
|
||||
width={'100%'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
})}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default MCPType;
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { Markdown } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { userGeneralSettingsSelectors } from '@/store/user/selectors';
|
||||
|
||||
import Loading from '../Loading';
|
||||
|
||||
export interface PluginMarkdownTypeProps {
|
||||
content: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const PluginMarkdownType = memo<PluginMarkdownTypeProps>(({ content, loading }) => {
|
||||
const fontSize = useUserStore(userGeneralSettingsSelectors.fontSize);
|
||||
if (loading) return <Loading />;
|
||||
|
||||
return (
|
||||
<Markdown fontSize={fontSize} variant={'chat'}>
|
||||
{content}
|
||||
</Markdown>
|
||||
);
|
||||
});
|
||||
|
||||
export default PluginMarkdownType;
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
import { type PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
|
||||
import { Skeleton } from '@lobehub/ui';
|
||||
import { memo, useRef, useState } from 'react';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { dbMessageSelectors } from '@/store/chat/selectors';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/selectors';
|
||||
|
||||
import { useOnPluginReadyForInteraction } from '../utils/iframeOnReady';
|
||||
import {
|
||||
useOnPluginCreateAssistantMessage,
|
||||
useOnPluginFetchMessage,
|
||||
useOnPluginFetchPluginSettings,
|
||||
useOnPluginFetchPluginState,
|
||||
useOnPluginFillContent,
|
||||
useOnPluginTriggerAIMessage,
|
||||
} from '../utils/listenToPlugin';
|
||||
import { useOnPluginSettingsUpdate } from '../utils/pluginSettings';
|
||||
import { useOnPluginStateUpdate } from '../utils/pluginState';
|
||||
import {
|
||||
sendMessageContentToPlugin,
|
||||
sendPayloadToPlugin,
|
||||
sendPluginSettingsToPlugin,
|
||||
sendPluginStateToPlugin,
|
||||
} from '../utils/postMessage';
|
||||
|
||||
// just to simplify code a little, don't use this pattern everywhere
|
||||
const getSettings = (identifier: string) =>
|
||||
pluginSelectors.getPluginSettingsById(identifier)(useToolStore.getState());
|
||||
const getMessage = (id: string) => dbMessageSelectors.getDbMessageById(id)(useChatStore.getState());
|
||||
|
||||
interface IFrameRenderProps {
|
||||
height?: number;
|
||||
id: string;
|
||||
payload?: PluginRequestPayload;
|
||||
url: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, height = 300 }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
// when payload change,send content to plugin
|
||||
useOnPluginReadyForInteraction(() => {
|
||||
const iframeWin = iframeRef.current?.contentWindow;
|
||||
|
||||
if (iframeWin && payload) {
|
||||
const settings = getSettings(payload.identifier);
|
||||
const message = getMessage(id);
|
||||
const state = message?.pluginState;
|
||||
|
||||
sendPayloadToPlugin(iframeWin, { payload, settings, state });
|
||||
}
|
||||
}, [payload]);
|
||||
|
||||
// when plugin wants to get message content, send it to plugin
|
||||
useOnPluginFetchMessage(() => {
|
||||
const iframeWin = iframeRef.current?.contentWindow;
|
||||
|
||||
if (iframeWin) {
|
||||
const message = dbMessageSelectors.getDbMessageById(id)(useChatStore.getState());
|
||||
if (!message) return;
|
||||
const props = { content: '' };
|
||||
|
||||
try {
|
||||
props.content = JSON.parse(message.content || '{}');
|
||||
} catch {
|
||||
props.content = message.content || '';
|
||||
}
|
||||
|
||||
sendMessageContentToPlugin(iframeWin, props);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// when plugin try to send back message, we should fill it to the message content
|
||||
const fillPluginContent = useChatStore((s) => s.fillPluginMessageContent);
|
||||
useOnPluginFillContent((content, triggerAiMessage) => {
|
||||
fillPluginContent(id, content, triggerAiMessage);
|
||||
});
|
||||
|
||||
// when plugin wants to get plugin state, send it to plugin
|
||||
useOnPluginFetchPluginState((key) => {
|
||||
const iframeWin = iframeRef.current?.contentWindow;
|
||||
|
||||
if (iframeWin) {
|
||||
const message = getMessage(id);
|
||||
if (!message) return;
|
||||
|
||||
sendPluginStateToPlugin(iframeWin, key, message.pluginState?.[key]);
|
||||
}
|
||||
});
|
||||
|
||||
// when plugin update state, we should update it to the message pluginState key
|
||||
const optimisticUpdatePluginState = useChatStore((s) => s.optimisticUpdatePluginState);
|
||||
useOnPluginStateUpdate((key, value) => {
|
||||
optimisticUpdatePluginState(id, { [key]: value });
|
||||
});
|
||||
|
||||
// when plugin wants to get plugin settings, send it to plugin
|
||||
useOnPluginFetchPluginSettings(() => {
|
||||
const iframeWin = iframeRef.current?.contentWindow;
|
||||
|
||||
if (iframeWin) {
|
||||
if (!payload?.identifier) return;
|
||||
|
||||
const settings = getSettings(payload.identifier);
|
||||
|
||||
sendPluginSettingsToPlugin(iframeWin, settings);
|
||||
}
|
||||
});
|
||||
|
||||
// when plugin update settings, we should update it to the plugin settings
|
||||
const updatePluginSettings = useToolStore((s) => s.updatePluginSettings);
|
||||
useOnPluginSettingsUpdate((value) => {
|
||||
if (!payload?.identifier) return;
|
||||
|
||||
updatePluginSettings(payload?.identifier, value);
|
||||
});
|
||||
|
||||
// when plugin want to trigger AI message
|
||||
const triggerAIMessage = useChatStore((s) => s.triggerAIMessage);
|
||||
useOnPluginTriggerAIMessage((messageId) => {
|
||||
// we need to know which message to trigger
|
||||
if (messageId !== id) return;
|
||||
|
||||
triggerAIMessage({ parentId: id });
|
||||
});
|
||||
|
||||
// when plugin want to create an assistant message
|
||||
const createAssistantMessage = useChatStore((s) => s.createAssistantMessageByPlugin);
|
||||
useOnPluginCreateAssistantMessage((content) => {
|
||||
createAssistantMessage(content, id);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <Skeleton active style={{ maxWidth: '100%', width }} />}
|
||||
<iframe
|
||||
// @ts-ignore
|
||||
allowtransparency="true"
|
||||
height={height}
|
||||
hidden={loading}
|
||||
ref={iframeRef}
|
||||
src={url}
|
||||
width={width}
|
||||
style={{
|
||||
border: 0,
|
||||
// iframe cannot be transparent in color-scheme:dark mode
|
||||
// refs: https://www.jianshu.com/p/bc5a37bb6a7b
|
||||
colorScheme: 'light',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
onLoad={() => {
|
||||
setLoading(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
export default IFrameRender;
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { type PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors } from '@/store/tool/slices/plugin/selectors';
|
||||
|
||||
import IFrameRender from './Iframe';
|
||||
|
||||
export interface PluginStandaloneTypeProps {
|
||||
id: string;
|
||||
name?: string;
|
||||
payload?: PluginRequestPayload;
|
||||
}
|
||||
|
||||
const PluginDefaultType = memo<PluginStandaloneTypeProps>(({ payload, id, name = 'unknown' }) => {
|
||||
const manifest = useToolStore(pluginSelectors.getToolManifestById(name));
|
||||
|
||||
if (!manifest?.ui) return;
|
||||
|
||||
const ui = manifest.ui;
|
||||
|
||||
if (!ui.url) return;
|
||||
// if the id start with "tmp", return directly to avoid duplicate rendering
|
||||
if (id.startsWith('tmp')) return;
|
||||
return (
|
||||
<IFrameRender
|
||||
height={ui.height}
|
||||
id={id}
|
||||
key={id}
|
||||
payload={payload}
|
||||
url={ui.url}
|
||||
width={ui.width}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default PluginDefaultType;
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
import { type LobeToolRenderType } from '@lobechat/types';
|
||||
import { type PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
|
||||
import { memo } from 'react';
|
||||
|
||||
import SafeBoundary from '@/components/ErrorBoundary';
|
||||
|
||||
import BuiltinType from './BuiltinType';
|
||||
import DefaultType from './DefaultType';
|
||||
import Markdown from './MarkdownType';
|
||||
import MCP from './MCPType';
|
||||
import Standalone from './StandaloneType';
|
||||
|
||||
export interface PluginRenderProps {
|
||||
arguments?: string;
|
||||
content: string;
|
||||
identifier?: string;
|
||||
loading?: boolean;
|
||||
/**
|
||||
* The real message ID (tool message ID)
|
||||
*/
|
||||
messageId?: string;
|
||||
payload?: PluginRequestPayload;
|
||||
pluginError?: any;
|
||||
pluginState?: any;
|
||||
/**
|
||||
* The tool call ID from the assistant message
|
||||
*/
|
||||
toolCallId?: string;
|
||||
type?: LobeToolRenderType;
|
||||
}
|
||||
|
||||
const PluginRender = memo<PluginRenderProps>(
|
||||
({
|
||||
content,
|
||||
arguments: argumentsStr = '',
|
||||
toolCallId,
|
||||
messageId,
|
||||
payload,
|
||||
pluginState,
|
||||
identifier,
|
||||
type,
|
||||
loading,
|
||||
pluginError,
|
||||
}) => {
|
||||
const renderContent = () => {
|
||||
switch (type) {
|
||||
case 'standalone': {
|
||||
return (
|
||||
<Standalone id={toolCallId || messageId || ''} name={identifier} payload={payload} />
|
||||
);
|
||||
}
|
||||
|
||||
case 'builtin': {
|
||||
return (
|
||||
<BuiltinType
|
||||
apiName={payload?.apiName}
|
||||
arguments={argumentsStr}
|
||||
content={content}
|
||||
identifier={identifier}
|
||||
loading={loading}
|
||||
messageId={messageId}
|
||||
pluginError={pluginError}
|
||||
pluginState={pluginState}
|
||||
toolCallId={toolCallId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-expect-error need to update types
|
||||
case 'mcp': {
|
||||
return (
|
||||
<MCP
|
||||
apiName={payload?.apiName}
|
||||
arguments={argumentsStr}
|
||||
content={content}
|
||||
identifier={identifier}
|
||||
loading={loading}
|
||||
messageId={messageId}
|
||||
pluginError={pluginError}
|
||||
pluginState={pluginState}
|
||||
toolCallId={toolCallId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'markdown': {
|
||||
return <Markdown content={content} loading={loading} />;
|
||||
}
|
||||
|
||||
default: {
|
||||
return <DefaultType content={content} loading={loading} name={identifier} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Use stable key to prevent ErrorBoundary from resetting on parent re-renders
|
||||
const boundaryKey = `${identifier}-${payload?.apiName}-${toolCallId || messageId}`;
|
||||
|
||||
return (
|
||||
<SafeBoundary
|
||||
key={boundaryKey}
|
||||
variant="alert"
|
||||
alertTitle={
|
||||
identifier
|
||||
? `${identifier}${payload?.apiName ? ` / ${payload.apiName}` : ''}`
|
||||
: 'Tool Render Error'
|
||||
}
|
||||
>
|
||||
{renderContent()}
|
||||
</SafeBoundary>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default PluginRender;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
export const useParseContent = (content: string) => {
|
||||
let isJSON = true;
|
||||
|
||||
try {
|
||||
JSON.parse(content);
|
||||
} catch {
|
||||
isJSON = false;
|
||||
}
|
||||
|
||||
const data = isJSON ? JSON.parse(content) : content;
|
||||
|
||||
return useMemo(() => ({ data, isJSON }), [content]);
|
||||
};
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useOnPluginReadyForInteraction } from './iframeOnReady';
|
||||
|
||||
describe('useOnPluginReadyForInteraction', () => {
|
||||
const mockOnReady = vi.fn();
|
||||
|
||||
afterEach(() => {
|
||||
mockOnReady.mockReset();
|
||||
// eslint-disable-next-line unicorn/no-invalid-remove-event-listener
|
||||
window.removeEventListener('message', () => {});
|
||||
});
|
||||
|
||||
it('sets readyForRender to true when a PluginChannel.pluginReadyForRender message is received', async () => {
|
||||
const { result } = renderHook(() => useOnPluginReadyForInteraction(mockOnReady));
|
||||
|
||||
expect(result.current).toBe(false); // Initially, readyForRender should be false
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: PluginChannel.pluginReadyForRender },
|
||||
});
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
|
||||
expect(result.current).toBe(true); // After the event, readyForRender should be true
|
||||
});
|
||||
|
||||
it('sets readyForRender to true when a PluginChannel.pluginReadyForRender message is received', async () => {
|
||||
const { result } = renderHook(() => useOnPluginReadyForInteraction(mockOnReady));
|
||||
|
||||
expect(result.current).toBe(false); // Initially, readyForRender should be false
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: PluginChannel.pluginReadyForRender },
|
||||
});
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
|
||||
expect(result.current).toBe(true); // After the event, readyForRender should be true
|
||||
expect(mockOnReady).toHaveBeenCalledTimes(1); // onReady should have been called once
|
||||
});
|
||||
|
||||
it('does not call onReady for non-pluginReadyForRender messages', async () => {
|
||||
renderHook(() => useOnPluginReadyForInteraction(mockOnReady));
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: 'nonPluginReadyMessage' },
|
||||
});
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
|
||||
expect(mockOnReady).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cleans up message event listener on unmount', () => {
|
||||
const { unmount } = renderHook(() => useOnPluginReadyForInteraction(mockOnReady));
|
||||
|
||||
unmount();
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: PluginChannel.pluginReadyForRender },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockOnReady).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useOnPluginReadyForInteraction = (onReady: () => void, deps: any[] = []) => {
|
||||
const [readyForRender, setReady] = useState(false);
|
||||
useEffect(() => {
|
||||
const fn = (e: MessageEvent) => {
|
||||
if (e.data.type === PluginChannel.pluginReadyForRender) {
|
||||
setReady(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', fn);
|
||||
return () => {
|
||||
window.removeEventListener('message', fn);
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (readyForRender) {
|
||||
onReady();
|
||||
}
|
||||
}, [readyForRender, ...deps]);
|
||||
|
||||
return readyForRender;
|
||||
};
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
useOnPluginCreateAssistantMessage,
|
||||
useOnPluginFetchMessage,
|
||||
useOnPluginFetchPluginSettings,
|
||||
useOnPluginFetchPluginState,
|
||||
useOnPluginFillContent,
|
||||
useOnPluginTriggerAIMessage,
|
||||
} from './listenToPlugin';
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('useOnPluginFetchMessage', () => {
|
||||
it('calls onRequest when a fetchPluginMessage is received', () => {
|
||||
const mockOnRequest = vi.fn();
|
||||
renderHook(() => useOnPluginFetchMessage(mockOnRequest));
|
||||
|
||||
const testData = { key: 'testData', type: PluginChannel.fetchPluginMessage };
|
||||
const event = new MessageEvent('message', {
|
||||
data: testData,
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockOnRequest).toHaveBeenCalledWith(testData);
|
||||
});
|
||||
|
||||
it('does not call onRequest for other message types', () => {
|
||||
const mockOnRequest = vi.fn();
|
||||
renderHook(() => useOnPluginFetchMessage(mockOnRequest));
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: 'otherMessageType' },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockOnRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useOnPluginFetchPluginState', () => {
|
||||
it('calls onRequest with the key when a fetchPluginState message is received', () => {
|
||||
const mockOnRequest = vi.fn();
|
||||
renderHook(() => useOnPluginFetchPluginState(mockOnRequest));
|
||||
|
||||
const testKey = 'testKey';
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: PluginChannel.fetchPluginState, key: testKey },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockOnRequest).toHaveBeenCalledWith(testKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useOnPluginFillContent', () => {
|
||||
it('calls callback with content when a fillStandalonePluginContent message is received', () => {
|
||||
const mockCallback = vi.fn();
|
||||
renderHook(() => useOnPluginFillContent(mockCallback));
|
||||
|
||||
const testContent = 'testContent';
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: PluginChannel.fillStandalonePluginContent, content: testContent },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith(testContent, undefined);
|
||||
});
|
||||
|
||||
it('calls callback with JSON stringified content if content is not a string', () => {
|
||||
const mockCallback = vi.fn();
|
||||
renderHook(() => useOnPluginFillContent(mockCallback));
|
||||
|
||||
const testContent = { some: 'data' };
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: PluginChannel.fillStandalonePluginContent, content: testContent },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith(JSON.stringify(testContent), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useOnPluginFetchPluginSettings', () => {
|
||||
it('calls onRequest when a fetchPluginSettings message is received', () => {
|
||||
const mockOnRequest = vi.fn();
|
||||
renderHook(() => useOnPluginFetchPluginSettings(mockOnRequest));
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: PluginChannel.fetchPluginSettings },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockOnRequest).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useOnPluginTriggerAIMessage', () => {
|
||||
it('calls callback with id when a triggerAIMessage is received', () => {
|
||||
const mockCallback = vi.fn();
|
||||
renderHook(() => useOnPluginTriggerAIMessage(mockCallback));
|
||||
|
||||
const testId = 'testId';
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: PluginChannel.triggerAIMessage, id: testId },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith(testId);
|
||||
});
|
||||
|
||||
it('does not call callback for other message types', () => {
|
||||
const mockCallback = vi.fn();
|
||||
renderHook(() => useOnPluginTriggerAIMessage(mockCallback));
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: 'otherMessageType', id: 'testId' },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useOnPluginCreateAssistantMessage', () => {
|
||||
it('calls callback with content when a createAssistantMessage is received', () => {
|
||||
const mockCallback = vi.fn();
|
||||
renderHook(() => useOnPluginCreateAssistantMessage(mockCallback));
|
||||
|
||||
const testContent = 'testContent';
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: PluginChannel.createAssistantMessage, content: testContent },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith(testContent);
|
||||
});
|
||||
|
||||
it('does not call callback for other message types', () => {
|
||||
const mockCallback = vi.fn();
|
||||
renderHook(() => useOnPluginCreateAssistantMessage(mockCallback));
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: 'otherMessageType', content: 'testContent' },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useOnPluginFetchMessage = (onRequest: (data: any) => void, deps: any[] = []) => {
|
||||
useEffect(() => {
|
||||
const fn = (e: MessageEvent) => {
|
||||
if (e.data.type === PluginChannel.fetchPluginMessage) {
|
||||
onRequest(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', fn);
|
||||
return () => {
|
||||
window.removeEventListener('message', fn);
|
||||
};
|
||||
}, deps);
|
||||
};
|
||||
|
||||
export const useOnPluginFetchPluginState = (onRequest: (key: string) => void) => {
|
||||
useEffect(() => {
|
||||
const fn = (e: MessageEvent) => {
|
||||
if (e.data.type === PluginChannel.fetchPluginState) {
|
||||
onRequest(e.data.key);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', fn);
|
||||
return () => {
|
||||
window.removeEventListener('message', fn);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useOnPluginFillContent = (
|
||||
callback: (content: string, triggerAiMessage?: boolean) => void,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const fn = (e: MessageEvent) => {
|
||||
if (e.data.type === PluginChannel.fillStandalonePluginContent) {
|
||||
const data = e.data.content;
|
||||
const triggerAiMessage = e.data.triggerAiMessage;
|
||||
const content = typeof data !== 'string' ? JSON.stringify(data) : data;
|
||||
|
||||
callback(content, triggerAiMessage);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', fn);
|
||||
return () => {
|
||||
window.removeEventListener('message', fn);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useOnPluginFetchPluginSettings = (onRequest: () => void) => {
|
||||
useEffect(() => {
|
||||
const fn = (e: MessageEvent) => {
|
||||
if (e.data.type === PluginChannel.fetchPluginSettings) {
|
||||
onRequest();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', fn);
|
||||
return () => {
|
||||
window.removeEventListener('message', fn);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useOnPluginTriggerAIMessage = (callback: (id: string) => void) => {
|
||||
useEffect(() => {
|
||||
const fn = (e: MessageEvent) => {
|
||||
if (e.data.type === PluginChannel.triggerAIMessage) {
|
||||
callback(e.data.id);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', fn);
|
||||
return () => {
|
||||
window.removeEventListener('message', fn);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useOnPluginCreateAssistantMessage = (callback: (content: string) => void) => {
|
||||
useEffect(() => {
|
||||
const fn = (e: MessageEvent) => {
|
||||
if (e.data.type === PluginChannel.createAssistantMessage) {
|
||||
callback(e.data.content);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', fn);
|
||||
return () => {
|
||||
window.removeEventListener('message', fn);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useOnPluginSettingsUpdate } from './pluginSettings';
|
||||
|
||||
describe('useOnPluginSettingsUpdate', () => {
|
||||
const mockCallback = vi.fn();
|
||||
|
||||
afterEach(() => {
|
||||
mockCallback.mockReset();
|
||||
// eslint-disable-next-line unicorn/no-invalid-remove-event-listener
|
||||
window.removeEventListener('message', () => {});
|
||||
});
|
||||
|
||||
it('calls the callback when a PluginChannel updatePluginSettings message is received', () => {
|
||||
renderHook(() => useOnPluginSettingsUpdate(mockCallback));
|
||||
|
||||
const testSettings = { theme: 'dark', notifications: true };
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: PluginChannel.updatePluginSettings, value: testSettings },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith(testSettings);
|
||||
});
|
||||
|
||||
it('does not call the callback for non-updatePluginSettings messages', () => {
|
||||
renderHook(() => useOnPluginSettingsUpdate(mockCallback));
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: 'nonPluginSettingsUpdate', value: { irrelevant: true } },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cleans up message event listener on unmount', () => {
|
||||
const { unmount } = renderHook(() => useOnPluginSettingsUpdate(mockCallback));
|
||||
|
||||
unmount();
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: PluginChannel.updatePluginSettings, value: {} },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useOnPluginSettingsUpdate = (callback: (settings: any) => void) => {
|
||||
useEffect(() => {
|
||||
const fn = (e: MessageEvent) => {
|
||||
if (e.data.type === PluginChannel.updatePluginSettings) {
|
||||
callback(e.data.value);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', fn);
|
||||
return () => {
|
||||
window.removeEventListener('message', fn);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useOnPluginStateUpdate } from './pluginState';
|
||||
|
||||
describe('useOnPluginStateUpdate', () => {
|
||||
// Mock for the callback function to be used in tests
|
||||
const mockCallback = vi.fn();
|
||||
|
||||
afterEach(() => {
|
||||
// Reset the mock callback after each test
|
||||
mockCallback.mockReset();
|
||||
// Ensure no event listeners are left hanging after each test
|
||||
// eslint-disable-next-line unicorn/no-invalid-remove-event-listener
|
||||
window.removeEventListener('message', () => {});
|
||||
});
|
||||
|
||||
it('calls the callback when a PluginChannel update message is received', () => {
|
||||
renderHook(() => useOnPluginStateUpdate(mockCallback));
|
||||
|
||||
const testKey = 'testKey';
|
||||
const testValue = 'testValue';
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: PluginChannel.updatePluginState, key: testKey, value: testValue },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith(testKey, testValue);
|
||||
});
|
||||
|
||||
it('does not call the callback for non-PluginChannel messages', () => {
|
||||
renderHook(() => useOnPluginStateUpdate(mockCallback));
|
||||
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type: 'nonPluginMessage', key: 'key', value: 'value' },
|
||||
});
|
||||
|
||||
window.dispatchEvent(event);
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useOnPluginStateUpdate = (callback: (key: string, value: any) => void) => {
|
||||
useEffect(() => {
|
||||
const fn = (e: MessageEvent) => {
|
||||
if (e.data.type === PluginChannel.updatePluginState) {
|
||||
const key = e.data.key;
|
||||
const value = e.data.value;
|
||||
|
||||
callback(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', fn);
|
||||
return () => {
|
||||
window.removeEventListener('message', fn);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
sendMessageContentToPlugin,
|
||||
sendPayloadToPlugin,
|
||||
sendPluginSettingsToPlugin,
|
||||
sendPluginStateToPlugin,
|
||||
} from './postMessage';
|
||||
|
||||
// Mock window object with a postMessage spy
|
||||
const mockWindow = {
|
||||
postMessage: vi.fn(),
|
||||
};
|
||||
|
||||
describe('plugin communication functions', () => {
|
||||
it('sendMessageContentToPlugin should call window.postMessage with correct arguments', () => {
|
||||
const props = { some: 'data' };
|
||||
sendMessageContentToPlugin(mockWindow as unknown as Window, props);
|
||||
expect(mockWindow.postMessage).toHaveBeenCalledWith(
|
||||
{ props, type: PluginChannel.renderPlugin },
|
||||
'*',
|
||||
);
|
||||
});
|
||||
|
||||
it('sendPayloadToPlugin should call window.postMessage with correct arguments', () => {
|
||||
const props = { payload: 'payload', settings: 'settings', state: 'state' };
|
||||
sendPayloadToPlugin(mockWindow as unknown as Window, props);
|
||||
expect(mockWindow.postMessage).toHaveBeenCalledWith(
|
||||
{
|
||||
type: PluginChannel.initStandalonePlugin,
|
||||
payload: props.payload,
|
||||
settings: props.settings,
|
||||
state: props.state,
|
||||
props: props.payload, // Note: This is due to the TODO in your code
|
||||
},
|
||||
'*',
|
||||
);
|
||||
});
|
||||
|
||||
it('sendPluginStateToPlugin should call window.postMessage with correct arguments', () => {
|
||||
const key = 'key';
|
||||
const value = 'value';
|
||||
sendPluginStateToPlugin(mockWindow as unknown as Window, key, value);
|
||||
expect(mockWindow.postMessage).toHaveBeenCalledWith(
|
||||
{ key, type: PluginChannel.renderPluginState, value },
|
||||
'*',
|
||||
);
|
||||
});
|
||||
|
||||
it('sendPluginSettingsToPlugin should call window.postMessage with correct arguments', () => {
|
||||
const settings = { setting1: 'value1' };
|
||||
sendPluginSettingsToPlugin(mockWindow as unknown as Window, settings);
|
||||
expect(mockWindow.postMessage).toHaveBeenCalledWith(
|
||||
{ type: PluginChannel.renderPluginSettings, value: settings },
|
||||
'*',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Reset the mock after each test
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { PluginChannel } from '@lobehub/chat-plugin-sdk/client';
|
||||
|
||||
export const sendMessageContentToPlugin = (window: Window, props: any) => {
|
||||
window.postMessage({ props, type: PluginChannel.renderPlugin }, '*');
|
||||
};
|
||||
|
||||
export const sendPayloadToPlugin = (
|
||||
window: Window,
|
||||
props: { payload: any; settings: any; state?: any },
|
||||
) => {
|
||||
window.postMessage(
|
||||
{
|
||||
type: PluginChannel.initStandalonePlugin,
|
||||
...props,
|
||||
// TODO: props need to deprecated
|
||||
props: props.payload,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
};
|
||||
|
||||
export const sendPluginStateToPlugin = (window: Window, key: string, value: any) => {
|
||||
window.postMessage({ key, type: PluginChannel.renderPluginState, value }, '*');
|
||||
};
|
||||
|
||||
export const sendPluginSettingsToPlugin = (window: Window, settings: any) => {
|
||||
window.postMessage({ type: PluginChannel.renderPluginSettings, value: settings }, '*');
|
||||
};
|
||||
|
|
@ -2,7 +2,6 @@ import { BuiltinToolsPortals } from '@lobechat/builtin-tools/portals';
|
|||
import isEqual from 'fast-deep-equal';
|
||||
import { memo } from 'react';
|
||||
|
||||
import PluginRender from '@/features/PluginsUI/Render';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { chatPortalSelectors, dbMessageSelectors } from '@/store/chat/selectors';
|
||||
import { safeParseJSON } from '@/utils/safeParseJSON';
|
||||
|
|
@ -25,19 +24,7 @@ const ToolRender = memo(() => {
|
|||
|
||||
const Render = BuiltinToolsPortals[plugin.identifier];
|
||||
|
||||
if (!Render)
|
||||
return (
|
||||
<PluginRender
|
||||
arguments={plugin.arguments}
|
||||
content={message.content}
|
||||
identifier={plugin.identifier}
|
||||
messageId={messageId}
|
||||
payload={plugin}
|
||||
pluginState={pluginState}
|
||||
toolCallId={message.tool_call_id}
|
||||
type={plugin?.type}
|
||||
/>
|
||||
);
|
||||
if (!Render) return null;
|
||||
|
||||
return (
|
||||
<Render
|
||||
|
|
|
|||
|
|
@ -116,19 +116,16 @@ const AgentTool = memo<AgentToolProps>(
|
|||
|
||||
// Fetch plugins
|
||||
const [
|
||||
useFetchPluginStore,
|
||||
useFetchUserKlavisServers,
|
||||
useFetchLobehubSkillConnections,
|
||||
useFetchUninstalledBuiltinTools,
|
||||
useFetchAgentSkills,
|
||||
] = useToolStore((s) => [
|
||||
s.useFetchPluginStore,
|
||||
s.useFetchUserKlavisServers,
|
||||
s.useFetchLobehubSkillConnections,
|
||||
s.useFetchUninstalledBuiltinTools,
|
||||
s.useFetchAgentSkills,
|
||||
]);
|
||||
useFetchPluginStore();
|
||||
useFetchInstalledPlugins();
|
||||
useFetchUninstalledBuiltinTools(true);
|
||||
useFetchAgentSkills(true);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const Item = memo<DiscoverMcpItem>(({ name, description, icon, identifier }) =>
|
|||
mcpStoreSelectors.isMCPInstalling(identifier)(s),
|
||||
s.installMCPPlugin,
|
||||
s.cancelInstallMCPPlugin,
|
||||
s.uninstallPlugin,
|
||||
s.uninstallMCPPlugin,
|
||||
mcpStoreSelectors.getPluginById(identifier)(s),
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const Item = memo<ItemProps>(({ identifier, title, description, avatar }) => {
|
|||
|
||||
const [customPlugin, uninstallPlugin, updateCustomPlugin, pluginManifest] = useToolStore((s) => [
|
||||
pluginSelectors.getCustomPluginById(identifier)(s),
|
||||
s.uninstallPlugin,
|
||||
s.uninstallCustomPlugin,
|
||||
s.updateCustomPlugin,
|
||||
pluginSelectors.getToolManifestById(identifier)(s),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifest } from '@lobechat/types';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { createAgentToolsEngine, createToolsEngine, getEnabledTools } from './index';
|
||||
|
|
@ -30,7 +30,7 @@ vi.mock('@/store/tool', () => ({
|
|||
avatar: '🔍',
|
||||
},
|
||||
type: 'builtin',
|
||||
} as unknown as LobeChatPluginManifest,
|
||||
} as unknown as ToolManifest,
|
||||
type: 'builtin' as const,
|
||||
},
|
||||
{
|
||||
|
|
@ -56,7 +56,7 @@ vi.mock('@/store/tool', () => ({
|
|||
avatar: '🌐',
|
||||
},
|
||||
type: 'builtin',
|
||||
} as unknown as LobeChatPluginManifest,
|
||||
} as unknown as ToolManifest,
|
||||
type: 'builtin' as const,
|
||||
},
|
||||
],
|
||||
|
|
@ -64,7 +64,7 @@ vi.mock('@/store/tool', () => ({
|
|||
}));
|
||||
|
||||
let mockGetInstalledPluginById: (id: string) => () => any = () => () => undefined;
|
||||
let mockInstalledPluginManifestList: () => LobeChatPluginManifest[] = () => [];
|
||||
let mockInstalledPluginManifestList: () => ToolManifest[] = () => [];
|
||||
|
||||
vi.mock('@/store/tool/selectors', () => ({
|
||||
pluginSelectors: {
|
||||
|
|
@ -270,7 +270,7 @@ describe('toolEngineering', () => {
|
|||
identifier: 'stdio-mcp-plugin',
|
||||
meta: { title: 'Stdio MCP', avatar: '🔧' },
|
||||
type: 'default',
|
||||
} as unknown as LobeChatPluginManifest,
|
||||
} as unknown as ToolManifest,
|
||||
];
|
||||
mockGetInstalledPluginById = (id: string) => () =>
|
||||
id === 'stdio-mcp-plugin'
|
||||
|
|
@ -305,7 +305,7 @@ describe('toolEngineering', () => {
|
|||
identifier: 'stdio-mcp-plugin',
|
||||
meta: { title: 'Stdio MCP', avatar: '🔧' },
|
||||
type: 'default',
|
||||
} as unknown as LobeChatPluginManifest;
|
||||
} as unknown as ToolManifest;
|
||||
|
||||
const httpMcpManifest = {
|
||||
api: [
|
||||
|
|
@ -318,7 +318,7 @@ describe('toolEngineering', () => {
|
|||
identifier: 'http-mcp-plugin',
|
||||
meta: { title: 'HTTP MCP', avatar: '🌐' },
|
||||
type: 'default',
|
||||
} as unknown as LobeChatPluginManifest;
|
||||
} as unknown as ToolManifest;
|
||||
|
||||
it('should filter stdio MCP tools in non-desktop environment', () => {
|
||||
mockInstalledPluginManifestList = () => [stdioMcpManifest];
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
|
|||
import { alwaysOnToolIds, defaultToolIds } from '@lobechat/builtin-tools';
|
||||
import { createEnableChecker, type PluginEnableChecker } from '@lobechat/context-engine';
|
||||
import { ToolsEngine } from '@lobechat/context-engine';
|
||||
import { type ChatCompletionTool, type WorkingModel } from '@lobechat/types';
|
||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ChatCompletionTool, type ToolManifest, type WorkingModel } from '@lobechat/types';
|
||||
|
||||
import { isToolAvailableInCurrentEnv } from '@/helpers/toolAvailability';
|
||||
import { getAgentStoreState } from '@/store/agent';
|
||||
|
|
@ -32,7 +31,7 @@ import { isCanUseFC } from '../isCanUseFC';
|
|||
*/
|
||||
export interface ToolsEngineConfig {
|
||||
/** Additional manifests to include beyond the standard ones */
|
||||
additionalManifests?: LobeChatPluginManifest[];
|
||||
additionalManifests?: ToolManifest[];
|
||||
/** Default tool IDs that will always be added to the end of the tools list */
|
||||
defaultToolIds?: string[];
|
||||
/** Custom enable checker for plugins */
|
||||
|
|
@ -51,20 +50,16 @@ export const createToolsEngine = (config: ToolsEngineConfig = {}): ToolsEngine =
|
|||
const pluginManifests = pluginSelectors.installedPluginManifestList(toolStoreState);
|
||||
|
||||
// Get all builtin tool manifests
|
||||
const builtinManifests = toolStoreState.builtinTools.map(
|
||||
(tool) => tool.manifest as LobeChatPluginManifest,
|
||||
);
|
||||
const builtinManifests = toolStoreState.builtinTools.map((tool) => tool.manifest as ToolManifest);
|
||||
|
||||
// Get Klavis tool manifests
|
||||
const klavisTools = klavisStoreSelectors.klavisAsLobeTools(toolStoreState);
|
||||
const klavisManifests = klavisTools
|
||||
.map((tool) => tool.manifest as LobeChatPluginManifest)
|
||||
.filter(Boolean);
|
||||
const klavisManifests = klavisTools.map((tool) => tool.manifest as ToolManifest).filter(Boolean);
|
||||
|
||||
// Get LobeHub Skill tool manifests
|
||||
const lobehubSkillTools = lobehubSkillStoreSelectors.lobehubSkillAsLobeTools(toolStoreState);
|
||||
const lobehubSkillManifests = lobehubSkillTools
|
||||
.map((tool) => tool.manifest as LobeChatPluginManifest)
|
||||
.map((tool) => tool.manifest as ToolManifest)
|
||||
.filter(Boolean);
|
||||
|
||||
// Combine all manifests
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { KLAVIS_SERVER_TYPES } from '@lobechat/const';
|
|||
import { ToolNameResolver } from '@lobechat/context-engine';
|
||||
import { type API } from '@lobechat/prompts';
|
||||
import { apiPrompt, toolPrompt } from '@lobechat/prompts';
|
||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifest } from '@lobechat/types';
|
||||
import { type IEditor } from '@lobehub/editor';
|
||||
import { INSERT_MENTION_COMMAND } from '@lobehub/editor';
|
||||
import { Icon, Image } from '@lobehub/ui';
|
||||
|
|
@ -41,7 +41,7 @@ const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label
|
|||
|
||||
const toolNameResolver = new ToolNameResolver();
|
||||
|
||||
const buildApiList = (identifier: string, manifest?: LobeChatPluginManifest): API[] => {
|
||||
const buildApiList = (identifier: string, manifest?: ToolManifest): API[] => {
|
||||
if (!manifest?.api) return [];
|
||||
|
||||
return manifest.api.map((api) => ({
|
||||
|
|
@ -58,7 +58,7 @@ const hydrateSystemRole = (systemRole?: string) => {
|
|||
|
||||
const resolveInstructions = (
|
||||
metadata: MentionMetadata,
|
||||
manifest?: LobeChatPluginManifest,
|
||||
manifest?: ToolManifest,
|
||||
fallbackDesc?: string,
|
||||
) => {
|
||||
if (metadata.instructions) return metadata.instructions;
|
||||
|
|
@ -70,7 +70,7 @@ const resolveInstructions = (
|
|||
|
||||
const resolveApiName = (
|
||||
metadata: MentionMetadata,
|
||||
manifest: LobeChatPluginManifest | undefined,
|
||||
manifest: ToolManifest | undefined,
|
||||
pluginId?: string,
|
||||
fallbackLabel?: string,
|
||||
) => {
|
||||
|
|
@ -93,7 +93,7 @@ const resolveApiName = (
|
|||
|
||||
const resolveApiDescription = (
|
||||
metadata: MentionMetadata,
|
||||
manifest: LobeChatPluginManifest | undefined,
|
||||
manifest: ToolManifest | undefined,
|
||||
pluginId: string | undefined,
|
||||
apiName?: string,
|
||||
) => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { KLAVIS_SERVER_TYPES } from '@lobechat/const';
|
|||
import { ToolNameResolver } from '@lobechat/context-engine';
|
||||
import { type API } from '@lobechat/prompts';
|
||||
import { apiPrompt, toolPrompt } from '@lobechat/prompts';
|
||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifest } from '@lobechat/types';
|
||||
import { type IEditor } from '@lobehub/editor';
|
||||
import { INSERT_MENTION_COMMAND } from '@lobehub/editor';
|
||||
import { Icon, Image } from '@lobehub/ui';
|
||||
|
|
@ -41,7 +41,7 @@ const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label
|
|||
|
||||
const toolNameResolver = new ToolNameResolver();
|
||||
|
||||
const buildApiList = (identifier: string, manifest?: LobeChatPluginManifest): API[] => {
|
||||
const buildApiList = (identifier: string, manifest?: ToolManifest): API[] => {
|
||||
if (!manifest?.api) return [];
|
||||
|
||||
return manifest.api.map((api) => ({
|
||||
|
|
@ -58,7 +58,7 @@ const hydrateSystemRole = (systemRole?: string) => {
|
|||
|
||||
const resolveInstructions = (
|
||||
metadata: MentionMetadata,
|
||||
manifest?: LobeChatPluginManifest,
|
||||
manifest?: ToolManifest,
|
||||
fallbackDesc?: string,
|
||||
) => {
|
||||
if (metadata.instructions) return metadata.instructions;
|
||||
|
|
@ -70,7 +70,7 @@ const resolveInstructions = (
|
|||
|
||||
const resolveApiName = (
|
||||
metadata: MentionMetadata,
|
||||
manifest: LobeChatPluginManifest | undefined,
|
||||
manifest: ToolManifest | undefined,
|
||||
pluginId?: string,
|
||||
fallbackLabel?: string,
|
||||
) => {
|
||||
|
|
@ -93,7 +93,7 @@ const resolveApiName = (
|
|||
|
||||
const resolveApiDescription = (
|
||||
metadata: MentionMetadata,
|
||||
manifest: LobeChatPluginManifest | undefined,
|
||||
manifest: ToolManifest | undefined,
|
||||
pluginId: string | undefined,
|
||||
apiName?: string,
|
||||
) => {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { useAgentStore } from '@/store/agent';
|
|||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { useServerConfigStore } from '@/store/serverConfig';
|
||||
import { pluginHelpers, useToolStore } from '@/store/tool';
|
||||
import { pluginSelectors, pluginStoreSelectors } from '@/store/tool/selectors';
|
||||
import { mcpStoreSelectors, pluginSelectors } from '@/store/tool/selectors';
|
||||
import { type LobeToolType } from '@/types/tool/tool';
|
||||
|
||||
import EditCustomPlugin from './EditCustomPlugin';
|
||||
|
|
@ -23,15 +23,12 @@ interface ActionsProps {
|
|||
|
||||
const Actions = memo<ActionsProps>(({ identifier, type, isMCP }) => {
|
||||
const mobile = useServerConfigStore((s) => s.isMobile);
|
||||
const [installed, installing, installPlugin, unInstallPlugin, installMCPPlugin] = useToolStore(
|
||||
(s) => [
|
||||
const [installed, installing, unInstallPlugin, installMCPPlugin] = useToolStore((s) => [
|
||||
pluginSelectors.isPluginInstalled(identifier)(s),
|
||||
pluginStoreSelectors.isPluginInstallLoading(identifier)(s),
|
||||
s.installPlugin,
|
||||
s.uninstallPlugin,
|
||||
mcpStoreSelectors.isPluginInstallLoading(identifier)(s),
|
||||
s.uninstallCustomPlugin,
|
||||
s.installMCPPlugin,
|
||||
],
|
||||
);
|
||||
]);
|
||||
|
||||
const isCustomPlugin = type === 'customPlugin';
|
||||
const { t } = useTranslation('plugin');
|
||||
|
|
@ -116,9 +113,6 @@ const Actions = memo<ActionsProps>(({ identifier, type, isMCP }) => {
|
|||
if (isMCP) {
|
||||
await installMCPPlugin(identifier);
|
||||
await togglePlugin(identifier);
|
||||
} else {
|
||||
await installPlugin(identifier);
|
||||
await togglePlugin(identifier);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifest } from '@lobechat/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { PluginModel } from '@/database/models/plugin';
|
||||
|
|
@ -49,7 +49,7 @@ export const klavisRouter = router({
|
|||
const tools = toolsResponse.tools || [];
|
||||
|
||||
// Save to database using the provided identifier (format: lowercase, spaces replaced with hyphens)
|
||||
const manifest: LobeChatPluginManifest = {
|
||||
const manifest: ToolManifest = {
|
||||
api: tools.map((tool: any) => ({
|
||||
description: tool.description || '',
|
||||
name: tool.name,
|
||||
|
|
@ -232,7 +232,7 @@ export const klavisRouter = router({
|
|||
const existingPlugin = await ctx.pluginModel.findById(identifier);
|
||||
|
||||
// Build manifest containing all tools
|
||||
const manifest: LobeChatPluginManifest = {
|
||||
const manifest: ToolManifest = {
|
||||
api: tools.map((tool) => ({
|
||||
description: tool.description || '',
|
||||
name: tool.name,
|
||||
|
|
|
|||
|
|
@ -57,14 +57,6 @@ vi.mock('@/server/services/search', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
// Mock plugin gateway service to avoid server-side env access
|
||||
vi.mock('@/server/services/pluginGateway', () => ({
|
||||
PluginGatewayService: vi.fn().mockImplementation(() => ({
|
||||
getPluginManifest: vi.fn(),
|
||||
executePlugin: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock factory and redis dependencies to break env import chains,
|
||||
// so the barrel can be imported with real AgentRuntimeCoordinator + InMemory backends
|
||||
vi.mock('@/server/modules/AgentRuntime/factory', async () => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { type RuntimeExecutorContext } from '@/server/modules/AgentRuntime/Runti
|
|||
import { createRuntimeExecutors } from '@/server/modules/AgentRuntime/RuntimeExecutors';
|
||||
import { type IStreamEventManager } from '@/server/modules/AgentRuntime/types';
|
||||
import { mcpService } from '@/server/services/mcp';
|
||||
import { PluginGatewayService } from '@/server/services/pluginGateway';
|
||||
import { QueueService } from '@/server/services/queue';
|
||||
import { LocalQueueServiceImpl } from '@/server/services/queue/impls';
|
||||
import { ToolExecutionService } from '@/server/services/toolExecution';
|
||||
|
|
@ -160,13 +159,11 @@ export class AgentRuntimeService {
|
|||
this.messageModel = new MessageModel(db, this.userId);
|
||||
|
||||
// Initialize ToolExecutionService with dependencies
|
||||
const pluginGatewayService = new PluginGatewayService();
|
||||
const builtinToolsExecutor = new BuiltinToolsExecutor(db, userId);
|
||||
|
||||
this.toolExecutionService = new ToolExecutionService({
|
||||
builtinToolsExecutor,
|
||||
mcpService,
|
||||
pluginGatewayService,
|
||||
});
|
||||
|
||||
// Setup local execution callback for LocalQueueServiceImpl
|
||||
|
|
|
|||
|
|
@ -36,14 +36,6 @@ vi.mock('@/server/services/search', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
// Mock plugin gateway service
|
||||
vi.mock('@/server/services/pluginGateway', () => ({
|
||||
PluginGatewayService: vi.fn().mockImplementation(() => ({
|
||||
executePlugin: vi.fn(),
|
||||
getPluginManifest: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock MCP service
|
||||
vi.mock('@/server/services/mcp', () => ({
|
||||
mcpService: {
|
||||
|
|
|
|||
|
|
@ -29,9 +29,6 @@ vi.mock('@/server/modules/AgentRuntime/RuntimeExecutors', () => ({
|
|||
createRuntimeExecutors: vi.fn(() => ({})),
|
||||
}));
|
||||
vi.mock('@/server/services/mcp', () => ({ mcpService: {} }));
|
||||
vi.mock('@/server/services/pluginGateway', () => ({
|
||||
PluginGatewayService: vi.fn().mockImplementation(() => ({})),
|
||||
}));
|
||||
vi.mock('@/server/services/queue', () => ({
|
||||
QueueService: vi.fn().mockImplementation(() => ({
|
||||
getImpl: vi.fn(() => ({})),
|
||||
|
|
|
|||
|
|
@ -36,14 +36,6 @@ vi.mock('@/server/services/search', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
// Mock plugin gateway service
|
||||
vi.mock('@/server/services/pluginGateway', () => ({
|
||||
PluginGatewayService: vi.fn().mockImplementation(() => ({
|
||||
getPluginManifest: vi.fn(),
|
||||
executePlugin: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock MCP service
|
||||
vi.mock('@/server/services/mcp', () => ({
|
||||
mcpService: {
|
||||
|
|
|
|||
|
|
@ -37,14 +37,6 @@ vi.mock('@/server/services/search', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
// Mock plugin gateway service
|
||||
vi.mock('@/server/services/pluginGateway', () => ({
|
||||
PluginGatewayService: vi.fn().mockImplementation(() => ({
|
||||
executePlugin: vi.fn(),
|
||||
getPluginManifest: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock MCP service
|
||||
vi.mock('@/server/services/mcp', () => ({
|
||||
mcpService: {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { type CheckMcpInstallResult, type CustomPluginMetadata } from '@lobechat/types';
|
||||
import { safeParseJSON } from '@lobechat/utils';
|
||||
import {
|
||||
type CheckMcpInstallResult,
|
||||
type CustomPluginMetadata,
|
||||
type LobeChatPluginApi,
|
||||
type LobeChatPluginManifest,
|
||||
type PluginSchema,
|
||||
} from '@lobehub/chat-plugin-sdk';
|
||||
type ToolManifest,
|
||||
type ToolManifestSettings,
|
||||
} from '@lobechat/types';
|
||||
import { safeParseJSON } from '@lobechat/utils';
|
||||
import { type DeploymentOption } from '@lobehub/market-sdk';
|
||||
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
|
@ -111,7 +112,7 @@ export class MCPService {
|
|||
// Assuming identifier is the unique name/id
|
||||
description: item.description,
|
||||
name: item.name,
|
||||
parameters: item.inputSchema as PluginSchema,
|
||||
parameters: item.inputSchema as ToolManifestSettings,
|
||||
}));
|
||||
} catch (error) {
|
||||
// Only retry for NoValidSessionId errors
|
||||
|
|
@ -355,7 +356,7 @@ export class MCPService {
|
|||
type: 'none' | 'bearer' | 'oauth2';
|
||||
},
|
||||
headers?: Record<string, string>,
|
||||
): Promise<LobeChatPluginManifest> {
|
||||
): Promise<ToolManifest> {
|
||||
const mcpParams = { name: identifier, type: 'http' as const, url };
|
||||
|
||||
// Add authentication info to parameters if available
|
||||
|
|
@ -390,7 +391,7 @@ export class MCPService {
|
|||
async getStdioMcpServerManifest(
|
||||
params: Omit<StdioMCPParams, 'type'>,
|
||||
metadata?: CustomPluginMetadata,
|
||||
): Promise<LobeChatPluginManifest> {
|
||||
): Promise<ToolManifest> {
|
||||
const mcpParams = {
|
||||
args: params.args,
|
||||
command: params.command,
|
||||
|
|
@ -424,7 +425,7 @@ export class MCPService {
|
|||
mcpParams,
|
||||
// TODO: temporary
|
||||
type: 'mcp' as any,
|
||||
} as LobeChatPluginManifest;
|
||||
} as ToolManifest;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -490,7 +491,7 @@ export class MCPService {
|
|||
// Assuming identifier is the unique name/id
|
||||
description: item.description,
|
||||
name: item.name,
|
||||
parameters: item.inputSchema as PluginSchema,
|
||||
parameters: item.inputSchema as ToolManifestSettings,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
import { type ChatToolPayload } from '@lobechat/types';
|
||||
import { safeParseJSON } from '@lobechat/utils';
|
||||
import { type PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
|
||||
import { type GatewaySuccessResponse } from '@lobehub/chat-plugins-gateway';
|
||||
import debug from 'debug';
|
||||
|
||||
import { getAppConfig } from '@/envs/app';
|
||||
import { parserPluginSettings } from '@/server/services/pluginGateway/settings';
|
||||
import { type ToolExecutionContext } from '@/server/services/toolExecution/types';
|
||||
|
||||
const log = debug('lobe-server:plugin-gateway-service');
|
||||
|
||||
export class PluginGatewayService {
|
||||
params: { PLUGINS_INDEX_URL: string; PLUGIN_SETTINGS: string | undefined };
|
||||
|
||||
constructor() {
|
||||
const { PLUGINS_INDEX_URL, PLUGIN_SETTINGS } = getAppConfig();
|
||||
|
||||
this.params = { PLUGINS_INDEX_URL, PLUGIN_SETTINGS };
|
||||
}
|
||||
|
||||
async execute(payload: ChatToolPayload, context: ToolExecutionContext) {
|
||||
const { identifier, apiName, arguments: argsStr } = payload;
|
||||
const args = safeParseJSON(argsStr) || {};
|
||||
|
||||
log('Executing plugin: %s:%s with args: %O', identifier, apiName, args, context);
|
||||
|
||||
try {
|
||||
// Construct plugin request
|
||||
const requestBody: PluginRequestPayload = {
|
||||
apiName,
|
||||
arguments: JSON.stringify(args),
|
||||
identifier,
|
||||
manifest: context.toolManifestMap[identifier] as any,
|
||||
};
|
||||
const { Gateway } = await import('@lobehub/chat-plugins-gateway');
|
||||
const gateway = new Gateway({
|
||||
defaultPluginSettings: parserPluginSettings(this.params.PLUGIN_SETTINGS),
|
||||
pluginsIndexUrl: this.params.PLUGINS_INDEX_URL,
|
||||
});
|
||||
|
||||
const response = await gateway.execute(requestBody);
|
||||
|
||||
log('Plugin execution result: %O', response);
|
||||
|
||||
return {
|
||||
content: (response as GatewaySuccessResponse).data,
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error executing plugin %s:%s: %O', identifier, apiName, error);
|
||||
return {
|
||||
content: (error as Error).message,
|
||||
error: {
|
||||
message: (error as Error).message,
|
||||
},
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { parserPluginSettings } from './settings';
|
||||
|
||||
describe('parserPluginSettings', () => {
|
||||
it('should return an empty object when input is undefined', () => {
|
||||
expect(parserPluginSettings()).toEqual({});
|
||||
});
|
||||
|
||||
it('should return an empty object when input is an empty string', () => {
|
||||
expect(parserPluginSettings('')).toEqual({});
|
||||
});
|
||||
|
||||
it('should parse plugin settings from a well-formed string', () => {
|
||||
const input = 'plugin1:key1=value1;key2=value2,plugin2:key3=value3';
|
||||
const expected = {
|
||||
plugin1: { key1: 'value1', key2: 'value2' },
|
||||
plugin2: { key3: 'value3' },
|
||||
};
|
||||
expect(parserPluginSettings(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle strings with Chinese commas', () => {
|
||||
const input = 'plugin1:key1=value1;key2=value2,plugin2:key3=value3';
|
||||
const expected = {
|
||||
plugin1: { key1: 'value1', key2: 'value2' },
|
||||
plugin2: { key3: 'value3' },
|
||||
};
|
||||
expect(parserPluginSettings(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should ignore empty segments', () => {
|
||||
const input = 'plugin1:key1=value1;key2=value2,,,plugin2:key3=value3';
|
||||
const expected = {
|
||||
plugin1: { key1: 'value1', key2: 'value2' },
|
||||
plugin2: { key3: 'value3' },
|
||||
};
|
||||
expect(parserPluginSettings(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should merge settings for the same pluginId', () => {
|
||||
const input = 'plugin1:key1=value1;key2=value2,plugin1:key3=value3;key4=value4';
|
||||
const expected = {
|
||||
plugin1: { key1: 'value1', key2: 'value2', key3: 'value3', key4: 'value4' },
|
||||
};
|
||||
expect(parserPluginSettings(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should override previous values if the same key appears again for the same pluginId', () => {
|
||||
const input = 'plugin1:key1=value1;key2=value2,plugin1:key2=newValue2;key3=value3';
|
||||
const expected = {
|
||||
plugin1: { key1: 'value1', key2: 'newValue2', key3: 'value3' },
|
||||
};
|
||||
expect(parserPluginSettings(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
describe('error senses', () => {
|
||||
it('should ignore settings with incorrect key-value format', () => {
|
||||
const input = 'plugin1:key1=value1;incorrectFormat,plugin2:key2=value2';
|
||||
const expected = {
|
||||
plugin1: { key1: 'value1' },
|
||||
plugin2: { key2: 'value2' },
|
||||
};
|
||||
expect(parserPluginSettings(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle extra separators gracefully', () => {
|
||||
const input = 'plugin1:key1=value1==value1.1;key2=value2;,plugin2:key3=value3';
|
||||
const expected = {
|
||||
plugin1: { key1: 'value1', key2: 'value2' },
|
||||
plugin2: { key3: 'value3' },
|
||||
};
|
||||
expect(parserPluginSettings(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should ignore settings with empty keys or values', () => {
|
||||
const input = 'plugin1:=value1;key2=,plugin2:key3=value3';
|
||||
const expected = {
|
||||
plugin2: { key3: 'value3' },
|
||||
};
|
||||
|
||||
expect(parserPluginSettings(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should ignore leading and trailing whitespace in keys and values', () => {
|
||||
const input = ' plugin1 : key1 = value1 ; key2 = value2 , plugin2 : key3=value3 ';
|
||||
const expected = {
|
||||
plugin1: { key1: 'value1', key2: 'value2' },
|
||||
plugin2: { key3: 'value3' },
|
||||
};
|
||||
expect(parserPluginSettings(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle special characters in keys and values', () => {
|
||||
const input = 'plugin1:key1=value1+special;key2=value2#special,plugin2:key3=value3/special';
|
||||
const expected = {
|
||||
plugin1: { key1: 'value1+special', key2: 'value2#special' },
|
||||
plugin2: { key3: 'value3/special' },
|
||||
};
|
||||
expect(parserPluginSettings(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
export const parserPluginSettings = (
|
||||
settingsStr?: string,
|
||||
): Record<string, Record<string, string>> => {
|
||||
if (!settingsStr) return {};
|
||||
|
||||
const settings = new Map<string, Record<string, string>>();
|
||||
|
||||
const array = settingsStr.split(/[,,]/).filter(Boolean);
|
||||
|
||||
for (const item of array) {
|
||||
const [id, pluginSettingsStr] = item.split(':');
|
||||
if (!id) continue;
|
||||
|
||||
const pluginSettingItems = pluginSettingsStr.split(';');
|
||||
|
||||
const cleanId = id.trim();
|
||||
|
||||
for (const item of pluginSettingItems) {
|
||||
const [key, value] = item.split('=');
|
||||
if (!key || !value) continue;
|
||||
const cleanKey = key.trim();
|
||||
const cleanValue = value.trim();
|
||||
|
||||
settings.set(cleanId, { ...settings.get(cleanId), [cleanKey]: cleanValue });
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(settings.entries());
|
||||
};
|
||||
|
|
@ -11,7 +11,6 @@ import {
|
|||
|
||||
import { DiscoverService } from '../discover';
|
||||
import { type MCPService } from '../mcp';
|
||||
import { type PluginGatewayService } from '../pluginGateway';
|
||||
import { type BuiltinToolsExecutor } from './builtin';
|
||||
import { classifyToolError } from './errorClassification';
|
||||
import {
|
||||
|
|
@ -25,7 +24,6 @@ const log = debug('lobe-server:tool-execution-service');
|
|||
interface ToolExecutionServiceDeps {
|
||||
builtinToolsExecutor: BuiltinToolsExecutor;
|
||||
mcpService: MCPService;
|
||||
pluginGatewayService: PluginGatewayService;
|
||||
}
|
||||
|
||||
const normalizeExecutionError = (error: unknown, fallbackMessage: string) => {
|
||||
|
|
@ -62,16 +60,10 @@ const normalizeExecutionError = (error: unknown, fallbackMessage: string) => {
|
|||
export class ToolExecutionService {
|
||||
private builtinToolsExecutor: BuiltinToolsExecutor;
|
||||
private mcpService: MCPService;
|
||||
private pluginGatewayService: PluginGatewayService;
|
||||
|
||||
constructor({
|
||||
mcpService,
|
||||
pluginGatewayService,
|
||||
builtinToolsExecutor,
|
||||
}: ToolExecutionServiceDeps) {
|
||||
constructor({ mcpService, builtinToolsExecutor }: ToolExecutionServiceDeps) {
|
||||
this.builtinToolsExecutor = builtinToolsExecutor;
|
||||
this.mcpService = mcpService;
|
||||
this.pluginGatewayService = pluginGatewayService;
|
||||
}
|
||||
|
||||
async executeTool(
|
||||
|
|
@ -87,19 +79,14 @@ export class ToolExecutionService {
|
|||
const typeStr = type as string;
|
||||
let data: ToolExecutionResult;
|
||||
switch (typeStr) {
|
||||
case 'builtin': {
|
||||
data = await this.builtinToolsExecutor.execute(payload, context);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'mcp': {
|
||||
data = await this.executeMCPTool(payload, context);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'builtin':
|
||||
default: {
|
||||
data = await this.pluginGatewayService.execute(payload, context);
|
||||
|
||||
data = await this.builtinToolsExecutor.execute(payload, context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ToolService > can parse the OpenAI plugin 1`] = `
|
||||
{
|
||||
"api": [],
|
||||
"homepage": "https://products.wolframalpha.com/api/commercial-termsofuse",
|
||||
"identifier": "Wolfram",
|
||||
"meta": {
|
||||
"avatar": "https://www.wolframcdn.com/images/icons/Wolfram.png",
|
||||
"description": "Access computation, math, curated knowledge & real-time data through Wolfram|Alpha and Wolfram Language.",
|
||||
"title": "Wolfram",
|
||||
},
|
||||
"openapi": "https://www.wolframalpha.com/.well-known/apispec.json",
|
||||
"settings": {
|
||||
"properties": {
|
||||
"apiAuthKey": {
|
||||
"default": "18c4412dec6846eda6ec2fa95f144e1f",
|
||||
"description": "API Key",
|
||||
"format": "password",
|
||||
"title": "API Key",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"systemRole": "Access dynamic computation and curated data from WolframAlpha and Wolfram Cloud.
|
||||
General guidelines:
|
||||
- Use only getWolframAlphaResults or getWolframCloudResults endpoints.
|
||||
- Prefer getWolframAlphaResults unless Wolfram Language code should be evaluated.
|
||||
- Use getWolframAlphaResults for natural-language queries in English; translate non-English queries before sending, then respond in the original language.
|
||||
- Use getWolframCloudResults for problems solvable with Wolfram Language code.
|
||||
- Suggest only Wolfram Language for external computation.
|
||||
- Inform users if information is not from Wolfram endpoints.
|
||||
- Display image URLs with Markdown syntax: ![URL]
|
||||
- ALWAYS use this exponent notation: \`6*10^14\`, NEVER \`6e14\`.
|
||||
- ALWAYS use {"input": query} structure for queries to Wolfram endpoints; \`query\` must ONLY be a single-line string.
|
||||
- ALWAYS use proper Markdown formatting for all math, scientific, and chemical formulas, symbols, etc.: '$$\\n[expression]\\n$$' for standalone cases and '\\( [expression] \\)' when inline.
|
||||
- Format inline Wolfram Language code with Markdown code formatting.
|
||||
- Never mention your knowledge cutoff date; Wolfram may return more recent data.
|
||||
getWolframAlphaResults guidelines:
|
||||
- Understands natural language queries about entities in chemistry, physics, geography, history, art, astronomy, and more.
|
||||
- Performs mathematical calculations, date and unit conversions, formula solving, etc.
|
||||
- Convert inputs to simplified keyword queries whenever possible (e.g. convert "how many people live in France" to "France population").
|
||||
- Use ONLY single-letter variable names, with or without integer subscript (e.g., n, n1, n_1).
|
||||
- Use named physical constants (e.g., 'speed of light') without numerical substitution.
|
||||
- Include a space between compound units (e.g., "Ω m" for "ohm*meter").
|
||||
- To solve for a variable in an equation with units, consider solving a corresponding equation without units; exclude counting units (e.g., books), include genuine units (e.g., kg).
|
||||
- If data for multiple properties is needed, make separate calls for each property.
|
||||
- If a Wolfram Alpha result is not relevant to the query:
|
||||
-- If Wolfram provides multiple 'Assumptions' for a query, choose the more relevant one(s) without explaining the initial result. If you are unsure, ask the user to choose.
|
||||
-- Re-send the exact same 'input' with NO modifications, and add the 'assumption' parameter, formatted as a list, with the relevant values.
|
||||
-- ONLY simplify or rephrase the initial query if a more relevant 'Assumption' or other input suggestions are not provided.
|
||||
-- Do not explain each step unless user input is needed. Proceed directly to making a better API call based on the available assumptions.
|
||||
getWolframCloudResults guidelines:
|
||||
- Accepts only syntactically correct Wolfram Language code.
|
||||
- Performs complex calculations, data analysis, plotting, data import, and information retrieval.
|
||||
- Before writing code that uses Entity, EntityProperty, EntityClass, etc. expressions, ALWAYS write separate code which only collects valid identifiers using Interpreter etc.; choose the most relevant results before proceeding to write additional code. Examples:
|
||||
-- Find the EntityType that represents countries: \`Interpreter["EntityType",AmbiguityFunction->All]["countries"]\`.
|
||||
-- Find the Entity for the Empire State Building: \`Interpreter["Building",AmbiguityFunction->All]["empire state"]\`.
|
||||
-- EntityClasses: Find the "Movie" entity class for Star Trek movies: \`Interpreter["MovieClass",AmbiguityFunction->All]["star trek"]\`.
|
||||
-- Find EntityProperties associated with "weight" of "Element" entities: \`Interpreter[Restricted["EntityProperty", "Element"],AmbiguityFunction->All]["weight"]\`.
|
||||
-- If all else fails, try to find any valid Wolfram Language representation of a given input: \`SemanticInterpretation["skyscrapers",_,Hold,AmbiguityFunction->All]\`.
|
||||
-- Prefer direct use of entities of a given type to their corresponding typeData function (e.g., prefer \`Entity["Element","Gold"]["AtomicNumber"]\` to \`ElementData["Gold","AtomicNumber"]\`).
|
||||
- When composing code:
|
||||
-- Use batching techniques to retrieve data for multiple entities in a single call, if applicable.
|
||||
-- Use Association to organize and manipulate data when appropriate.
|
||||
-- Optimize code for performance and minimize the number of calls to external sources (e.g., the Wolfram Knowledgebase)
|
||||
-- Use only camel case for variable names (e.g., variableName).
|
||||
-- Use ONLY double quotes around all strings, including plot labels, etc. (e.g., \`PlotLegends -> {"sin(x)", "cos(x)", "tan(x)"}\`).
|
||||
-- Avoid use of QuantityMagnitude.
|
||||
-- If unevaluated Wolfram Language symbols appear in API results, use \`EntityValue[Entity["WolframLanguageSymbol",symbol],{"PlaintextUsage","Options"}]\` to validate or retrieve usage information for relevant symbols; \`symbol\` may be a list of symbols.
|
||||
-- Apply Evaluate to complex expressions like integrals before plotting (e.g., \`Plot[Evaluate[Integrate[...]]]\`).
|
||||
- Remove all comments and formatting from code passed to the "input" parameter; for example: instead of \`square[x_] := Module[{result},\\n result = x^2 (* Calculate the square *)\\n]\`, send \`square[x_]:=Module[{result},result=x^2]\`.
|
||||
- In ALL responses that involve code, write ALL code in Wolfram Language; create Wolfram Language functions even if an implementation is already well known in another language.
|
||||
",
|
||||
"type": "default",
|
||||
"version": "1",
|
||||
}
|
||||
`;
|
||||
|
|
@ -6,7 +6,6 @@ describe('API_ENDPOINTS', () => {
|
|||
it('should return correct basePath URLs', () => {
|
||||
expect(API_ENDPOINTS.oauth).toBe('/api/auth');
|
||||
expect(API_ENDPOINTS.proxy).toBe('/webapi/proxy');
|
||||
expect(API_ENDPOINTS.gateway).toBe('/webapi/plugin/gateway');
|
||||
expect(API_ENDPOINTS.trace).toBe('/webapi/trace');
|
||||
expect(API_ENDPOINTS.stt).toBe('/webapi/stt/openai');
|
||||
expect(API_ENDPOINTS.edge).toBe('/webapi/tts/edge');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { toolService } from '../tool';
|
||||
import OpenAIPlugin from './openai/plugin.json';
|
||||
|
||||
// Mocking modules and functions
|
||||
|
||||
|
|
@ -31,7 +30,6 @@ describe('ToolService', () => {
|
|||
const manifestUrl = 'http://fake-url.com/manifest.json';
|
||||
|
||||
const fakeManifest = {
|
||||
$schema: '../node_modules/@lobehub/chat-plugin-sdk/schema.json',
|
||||
api: [
|
||||
{
|
||||
url: 'https://realtime-weather.chat-plugin.lobehub.com/api/v1',
|
||||
|
|
@ -136,10 +134,4 @@ describe('ToolService', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('can parse the OpenAI plugin', async () => {
|
||||
const manifest = toolService['convertOpenAIManifestToLobeManifest'](OpenAIPlugin as any);
|
||||
|
||||
expect(manifest).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ export const API_ENDPOINTS = {
|
|||
|
||||
proxy: withElectronProtocolIfElectron('/webapi/proxy'),
|
||||
|
||||
// plugins
|
||||
gateway: withElectronProtocolIfElectron('/webapi/plugin/gateway'),
|
||||
|
||||
// trace
|
||||
trace: withElectronProtocolIfElectron('/webapi/trace'),
|
||||
|
||||
|
|
|
|||
|
|
@ -1536,23 +1536,6 @@ describe('ChatService', () => {
|
|||
// Add more test cases to cover different scenarios and edge cases
|
||||
});
|
||||
|
||||
describe('runPluginApi', () => {
|
||||
it('should make a POST request and return the result text', async () => {
|
||||
const params = { identifier: 'test-plugin', apiName: '1' }; // Add more properties if needed
|
||||
const options = {};
|
||||
const mockResponse = new Response('Plugin Result', { status: 200 });
|
||||
|
||||
global.fetch = vi.fn(() => Promise.resolve(mockResponse));
|
||||
|
||||
const result = await chatService.runPluginApi(params, options);
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object));
|
||||
expect(result.text).toBe('Plugin Result');
|
||||
});
|
||||
|
||||
// Add more test cases to cover different scenarios and edge cases
|
||||
});
|
||||
|
||||
describe('fetchPresetTaskResult', () => {
|
||||
it('should handle successful chat completion response', async () => {
|
||||
// Mock getChatCompletion to simulate successful completion
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { AgentBuilderIdentifier } from '@lobechat/builtin-tool-agent-builder';
|
|||
import { KLAVIS_SERVER_TYPES, LOBEHUB_SKILL_PROVIDERS } from '@lobechat/const';
|
||||
import { type OfficialToolItem } from '@lobechat/context-engine';
|
||||
import { type FetchSSEOptions } from '@lobechat/fetch-sse';
|
||||
import { fetchSSE, getMessageError, standardizeAnimationStyle } from '@lobechat/fetch-sse';
|
||||
import { fetchSSE, standardizeAnimationStyle } from '@lobechat/fetch-sse';
|
||||
import { type ChatCompletionErrorPayload } from '@lobechat/model-runtime';
|
||||
import { AgentRuntimeError } from '@lobechat/model-runtime';
|
||||
import {
|
||||
|
|
@ -12,8 +12,6 @@ import {
|
|||
type UIChatMessage,
|
||||
} from '@lobechat/types';
|
||||
import { ChatErrorType, TraceTagMap } from '@lobechat/types';
|
||||
import { type PluginRequestPayload } from '@lobehub/chat-plugin-sdk';
|
||||
import { createHeadersWithPluginSettings } from '@lobehub/chat-plugin-sdk';
|
||||
import { merge } from 'es-toolkit/compat';
|
||||
import { ModelProvider } from 'model-bank';
|
||||
|
||||
|
|
@ -32,7 +30,6 @@ import {
|
|||
builtinToolSelectors,
|
||||
klavisStoreSelectors,
|
||||
lobehubSkillStoreSelectors,
|
||||
pluginSelectors,
|
||||
} from '@/store/tool/selectors';
|
||||
import { getUserStoreState, useUserStore } from '@/store/user';
|
||||
import {
|
||||
|
|
@ -42,7 +39,7 @@ import {
|
|||
} from '@/store/user/selectors';
|
||||
import { type ChatStreamPayload, type OpenAIChatMessage } from '@/types/openai/chat';
|
||||
import { createErrorResponse } from '@/utils/errorResponse';
|
||||
import { createTraceHeader, getTraceId } from '@/utils/trace';
|
||||
import { createTraceHeader } from '@/utils/trace';
|
||||
|
||||
import { createHeaderWithAuth } from '../_auth';
|
||||
import { API_ENDPOINTS } from '../_url';
|
||||
|
|
@ -446,40 +443,6 @@ class ChatService {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* run the plugin api to get result
|
||||
* @param params
|
||||
* @param options
|
||||
*/
|
||||
runPluginApi = async (params: PluginRequestPayload, options?: FetchOptions) => {
|
||||
const s = getToolStoreState();
|
||||
|
||||
const settings = pluginSelectors.getPluginSettingsById(params.identifier)(s);
|
||||
const manifest = pluginSelectors.getToolManifestById(params.identifier)(s);
|
||||
|
||||
const traceHeader = createTraceHeader(this.mapTrace(options?.trace, TraceTagMap.ToolCalling));
|
||||
|
||||
const headers = await createHeaderWithAuth({
|
||||
headers: { ...createHeadersWithPluginSettings(settings), ...traceHeader },
|
||||
});
|
||||
|
||||
const gatewayURL = manifest?.gateway ?? API_ENDPOINTS.gateway;
|
||||
|
||||
const res = await fetch(gatewayURL, {
|
||||
body: JSON.stringify({ ...params, manifest }),
|
||||
headers,
|
||||
method: 'POST',
|
||||
signal: options?.signal,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw await getMessageError(res);
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
return { text, traceId: getTraceId(res) };
|
||||
};
|
||||
|
||||
fetchPresetTaskResult = async ({
|
||||
params,
|
||||
onMessageHandle,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { type ChatToolPayload } from '@lobechat/types';
|
||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ChatToolPayload, type ToolManifest } from '@lobechat/types';
|
||||
import superjson from 'superjson';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
|
|
@ -438,7 +437,7 @@ describe('MCPService', () => {
|
|||
describe('getStreamableMcpServerManifest', () => {
|
||||
it('should use toolsClient for streamable URLs when not on desktop', async () => {
|
||||
const { toolsClient } = await import('@/libs/trpc/client');
|
||||
const mockManifest: LobeChatPluginManifest = {
|
||||
const mockManifest: ToolManifest = {
|
||||
identifier: 'streamable-server',
|
||||
version: '1',
|
||||
meta: { title: 'Streamable MCP Server', avatar: '🌐' },
|
||||
|
|
@ -470,7 +469,7 @@ describe('MCPService', () => {
|
|||
|
||||
it('should use toolsClient for remote URLs', async () => {
|
||||
const { toolsClient } = await import('@/libs/trpc/client');
|
||||
const mockManifest: LobeChatPluginManifest = {
|
||||
const mockManifest: ToolManifest = {
|
||||
identifier: 'remote-server',
|
||||
version: '1',
|
||||
meta: { title: 'Remote MCP Server', avatar: '🌍' },
|
||||
|
|
@ -507,7 +506,7 @@ describe('MCPService', () => {
|
|||
|
||||
it('should handle different URL formats correctly', async () => {
|
||||
const { toolsClient } = await import('@/libs/trpc/client');
|
||||
const mockManifest: LobeChatPluginManifest = {
|
||||
const mockManifest: ToolManifest = {
|
||||
identifier: 'server',
|
||||
version: '1',
|
||||
meta: { title: 'URL Test Server', avatar: '🔗' },
|
||||
|
|
@ -537,7 +536,7 @@ describe('MCPService', () => {
|
|||
|
||||
it('should handle OAuth2 authentication', async () => {
|
||||
const { toolsClient } = await import('@/libs/trpc/client');
|
||||
const mockManifest: LobeChatPluginManifest = {
|
||||
const mockManifest: ToolManifest = {
|
||||
identifier: 'oauth-server',
|
||||
version: '1',
|
||||
meta: { title: 'OAuth Server', avatar: '🔐' },
|
||||
|
|
@ -579,7 +578,7 @@ describe('MCPService', () => {
|
|||
|
||||
describe('getStdioMcpServerManifest', () => {
|
||||
it('should call ipc mcp.getStdioMcpServerManifest with stdio parameters', async () => {
|
||||
const mockManifest: LobeChatPluginManifest = {
|
||||
const mockManifest: ToolManifest = {
|
||||
identifier: 'stdio-server',
|
||||
version: '1',
|
||||
meta: { title: 'Stdio Server', avatar: '📦' },
|
||||
|
|
@ -616,7 +615,7 @@ describe('MCPService', () => {
|
|||
});
|
||||
|
||||
it('should handle abort signal for stdio manifest', async () => {
|
||||
const mockManifest: LobeChatPluginManifest = {
|
||||
const mockManifest: ToolManifest = {
|
||||
identifier: 'python-server',
|
||||
version: '1',
|
||||
meta: { title: 'Stdio Server', avatar: '🐍' },
|
||||
|
|
@ -650,7 +649,7 @@ describe('MCPService', () => {
|
|||
});
|
||||
|
||||
it('should work without optional parameters', async () => {
|
||||
const mockManifest: LobeChatPluginManifest = {
|
||||
const mockManifest: ToolManifest = {
|
||||
identifier: 'npm-server',
|
||||
version: '1',
|
||||
meta: { title: 'Simple Server', avatar: '📦' },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { type LobeTool } from '@lobechat/types';
|
||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type LobeTool, type ToolManifest } from '@lobechat/types';
|
||||
|
||||
import { lambdaClient } from '@/libs/trpc/client';
|
||||
import { type LobeToolCustomPlugin } from '@/types/tool/plugin';
|
||||
|
|
@ -7,7 +6,7 @@ import { type LobeToolCustomPlugin } from '@/types/tool/plugin';
|
|||
export interface InstallPluginParams {
|
||||
customParams?: Record<string, any>;
|
||||
identifier: string;
|
||||
manifest: LobeChatPluginManifest;
|
||||
manifest: ToolManifest;
|
||||
settings?: Record<string, any>;
|
||||
type: 'plugin' | 'customPlugin';
|
||||
}
|
||||
|
|
@ -38,7 +37,7 @@ export class PluginService {
|
|||
});
|
||||
};
|
||||
|
||||
updatePluginManifest = async (id: string, manifest: LobeChatPluginManifest): Promise<void> => {
|
||||
updatePluginManifest = async (id: string, manifest: ToolManifest): Promise<void> => {
|
||||
await lambdaClient.plugin.updatePlugin.mutate({ id, manifest });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { lambdaClient } from '@/libs/trpc/client';
|
||||
import { globalHelpers } from '@/store/global/helpers';
|
||||
import { type PluginQueryParams } from '@/types/discover';
|
||||
import { convertOpenAIManifestToLobeManifest, getToolManifest } from '@/utils/toolManifest';
|
||||
import { getToolManifest } from '@/utils/toolManifest';
|
||||
|
||||
class ToolService {
|
||||
getOldPluginList = async (params: PluginQueryParams): Promise<any> => {
|
||||
|
|
@ -16,7 +16,6 @@ class ToolService {
|
|||
};
|
||||
|
||||
getToolManifest = getToolManifest;
|
||||
convertOpenAIManifestToLobeManifest = convertOpenAIManifestToLobeManifest;
|
||||
}
|
||||
|
||||
export const toolService = new ToolService();
|
||||
|
|
|
|||
|
|
@ -6,15 +6,12 @@ import i18n from 'i18next';
|
|||
import { type Mock } from 'vitest';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { chatService } from '@/services/chat';
|
||||
import { messageService } from '@/services/message';
|
||||
import { chatSelectors } from '@/store/chat/selectors';
|
||||
import { useChatStore } from '@/store/chat/store';
|
||||
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
|
||||
const invokeStandaloneTypePlugin = useChatStore.getState().invokeStandaloneTypePlugin;
|
||||
|
||||
vi.mock('zustand/traditional');
|
||||
|
||||
// Mock messageService
|
||||
|
|
@ -189,75 +186,6 @@ describe('ChatPluginAction', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('invokeDefaultTypePlugin', () => {
|
||||
it('should run the default plugin type and update message content', async () => {
|
||||
const pluginPayload = { apiName: 'testApi', arguments: { key: 'value' } };
|
||||
const messageId = 'message-id';
|
||||
const pluginApiResponse = 'Plugin API response';
|
||||
|
||||
const storeState = useChatStore.getState();
|
||||
|
||||
vi.spyOn(storeState, 'refreshMessages');
|
||||
vi.spyOn(storeState, 'triggerAIMessage').mockResolvedValue(undefined);
|
||||
vi.spyOn(storeState, 'optimisticUpdateMessageContent').mockResolvedValue();
|
||||
|
||||
const runSpy = vi.spyOn(chatService, 'runPluginApi').mockResolvedValue({
|
||||
text: pluginApiResponse,
|
||||
traceId: '',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.invokeDefaultTypePlugin(messageId, pluginPayload);
|
||||
});
|
||||
|
||||
expect(runSpy).toHaveBeenCalledWith(pluginPayload, { signal: undefined, trace: {} });
|
||||
expect(storeState.optimisticUpdateMessageContent).toHaveBeenCalledWith(
|
||||
messageId,
|
||||
pluginApiResponse,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors when the plugin API call fails', async () => {
|
||||
const pluginPayload = { apiName: 'testApi', arguments: { key: 'value' } };
|
||||
const messageId = 'message-id';
|
||||
const error = new Error('API call failed');
|
||||
const mockMessages = [{ id: 'msg-1', content: 'test' }] as any;
|
||||
|
||||
// Mock the service to return messages
|
||||
(messageService.updateMessageError as Mock).mockResolvedValue({
|
||||
success: true,
|
||||
messages: mockMessages,
|
||||
});
|
||||
|
||||
const storeState = useChatStore.getState();
|
||||
const replaceMessagesSpy = vi.spyOn(storeState, 'replaceMessages');
|
||||
vi.spyOn(storeState, 'triggerAIMessage').mockResolvedValue(undefined);
|
||||
|
||||
vi.spyOn(chatService, 'runPluginApi').mockRejectedValue(error);
|
||||
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
await act(async () => {
|
||||
await result.current.invokeDefaultTypePlugin(messageId, pluginPayload);
|
||||
});
|
||||
|
||||
expect(chatService.runPluginApi).toHaveBeenCalledWith(pluginPayload, { trace: {} });
|
||||
// Context now includes groupId from the message
|
||||
expect(messageService.updateMessageError).toHaveBeenCalledWith(
|
||||
messageId,
|
||||
error,
|
||||
expect.objectContaining({ topicId: undefined }),
|
||||
);
|
||||
expect(replaceMessagesSpy).toHaveBeenCalledWith(mockMessages, {
|
||||
context: expect.objectContaining({ topicId: undefined }),
|
||||
});
|
||||
expect(storeState.triggerAIMessage).not.toHaveBeenCalled(); // 确保在错误情况下不调用此方法
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePluginState', () => {
|
||||
it('should update the plugin state for a message', async () => {
|
||||
const messageId = 'message-id';
|
||||
|
|
@ -670,84 +598,6 @@ describe('ChatPluginAction', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('invokeMarkdownTypePlugin', () => {
|
||||
it('should invoke a markdown type plugin', async () => {
|
||||
const payload = {
|
||||
apiName: 'markdownApi',
|
||||
identifier: 'abc',
|
||||
type: 'markdown',
|
||||
arguments: JSON.stringify({ key: 'value' }),
|
||||
} as ChatToolPayload;
|
||||
const messageId = 'message-id';
|
||||
|
||||
const runPluginApiMock = vi.fn();
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({ internal_callPluginApi: runPluginApiMock });
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.invokeMarkdownTypePlugin(messageId, payload);
|
||||
});
|
||||
|
||||
// Verify that the markdown type plugin was invoked
|
||||
expect(runPluginApiMock).toHaveBeenCalledWith(messageId, payload);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invokeStandaloneTypePlugin', () => {
|
||||
it('should update message with error and refresh messages if plugin settings are invalid', async () => {
|
||||
const messageId = 'message-id';
|
||||
const mockMessages = [{ id: 'msg-1', content: 'test' }] as any;
|
||||
|
||||
const payload = {
|
||||
identifier: 'pluginName',
|
||||
} as ChatToolPayload;
|
||||
|
||||
// Mock the service to return messages
|
||||
(messageService.updateMessageError as Mock).mockResolvedValue({
|
||||
success: true,
|
||||
messages: mockMessages,
|
||||
});
|
||||
|
||||
const replaceMessagesSpy = vi.fn();
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
validatePluginSettings: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ valid: false, errors: ['Invalid setting'] }),
|
||||
});
|
||||
|
||||
useChatStore.setState({ replaceMessages: replaceMessagesSpy, invokeStandaloneTypePlugin });
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.invokeStandaloneTypePlugin(messageId, payload);
|
||||
});
|
||||
|
||||
const call = vi.mocked(messageService.updateMessageError).mock.calls[0];
|
||||
|
||||
expect(call[1]).toEqual({
|
||||
body: {
|
||||
error: ['Invalid setting'],
|
||||
message: '[plugin] your settings is invalid with plugin manifest setting schema',
|
||||
},
|
||||
message: 'response.PluginSettingsInvalid',
|
||||
type: 'PluginSettingsInvalid',
|
||||
});
|
||||
|
||||
// Context now includes groupId from the message
|
||||
expect(replaceMessagesSpy).toHaveBeenCalledWith(mockMessages, {
|
||||
context: expect.objectContaining({ topicId: undefined }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reInvokeToolMessage', () => {
|
||||
it('should re-invoke a tool message', async () => {
|
||||
const messageId = 'message-id';
|
||||
|
|
@ -876,92 +726,6 @@ describe('ChatPluginAction', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('internal_callPluginApi', () => {
|
||||
it('should call plugin API and update message content', async () => {
|
||||
const messageId = 'message-id';
|
||||
const payload: ChatToolPayload = {
|
||||
id: 'tool-id',
|
||||
type: 'default',
|
||||
identifier: 'plugin-id',
|
||||
apiName: 'api-name',
|
||||
arguments: '{}',
|
||||
};
|
||||
const apiResponse = 'API response';
|
||||
|
||||
vi.spyOn(chatService, 'runPluginApi').mockResolvedValue({
|
||||
text: apiResponse,
|
||||
traceId: 'trace-id',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
optimisticUpdateMessageContent: vi.fn(),
|
||||
refreshMessages: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.internal_callPluginApi(messageId, payload);
|
||||
});
|
||||
|
||||
expect(chatService.runPluginApi).toHaveBeenCalledWith(payload, expect.any(Object));
|
||||
expect(result.current.optimisticUpdateMessageContent).toHaveBeenCalledWith(
|
||||
messageId,
|
||||
apiResponse,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(messageService.updateMessage).toHaveBeenCalledWith(messageId, { traceId: 'trace-id' });
|
||||
});
|
||||
|
||||
it('should handle API call errors', async () => {
|
||||
const messageId = 'message-id';
|
||||
const payload: ChatToolPayload = {
|
||||
id: 'tool-id',
|
||||
type: 'default',
|
||||
identifier: 'plugin-id',
|
||||
apiName: 'api-name',
|
||||
arguments: '{}',
|
||||
};
|
||||
const error = new Error('API call failed');
|
||||
const mockMessages = [{ id: 'msg-1', content: 'test' }] as any;
|
||||
|
||||
// Mock the service to return messages
|
||||
(messageService.updateMessageError as Mock).mockResolvedValue({
|
||||
success: true,
|
||||
messages: mockMessages,
|
||||
});
|
||||
|
||||
vi.spyOn(chatService, 'runPluginApi').mockRejectedValue(error);
|
||||
|
||||
const replaceMessagesSpy = vi.fn();
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState({
|
||||
replaceMessages: replaceMessagesSpy,
|
||||
});
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useChatStore());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.internal_callPluginApi(messageId, payload);
|
||||
});
|
||||
|
||||
// Context now includes groupId from the message
|
||||
expect(messageService.updateMessageError).toHaveBeenCalledWith(
|
||||
messageId,
|
||||
error,
|
||||
expect.objectContaining({ topicId: undefined }),
|
||||
);
|
||||
expect(replaceMessagesSpy).toHaveBeenCalledWith(mockMessages, {
|
||||
context: expect.objectContaining({ topicId: undefined }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('internal_transformToolCalls', () => {
|
||||
it('should transform tool calls correctly', () => {
|
||||
const toolCalls: MessageToolCall[] = [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { builtinTools } from '@lobechat/builtin-tools';
|
||||
import { ToolArgumentsRepairer, ToolNameResolver } from '@lobechat/context-engine';
|
||||
import { type ChatToolPayload, type MessageToolCall } from '@lobechat/types';
|
||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ChatToolPayload, type MessageToolCall, type ToolManifest } from '@lobechat/types';
|
||||
|
||||
import { type ChatStore } from '@/store/chat/store';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
|
|
@ -33,7 +32,7 @@ export class PluginInternalsActionImpl {
|
|||
|
||||
// Build manifests map from tool store
|
||||
const toolStoreState = useToolStore.getState();
|
||||
const manifests: Record<string, LobeChatPluginManifest> = {};
|
||||
const manifests: Record<string, ToolManifest> = {};
|
||||
|
||||
// Track source for each identifier
|
||||
const sourceMap: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill'> = {};
|
||||
|
|
@ -42,7 +41,7 @@ export class PluginInternalsActionImpl {
|
|||
const installedPlugins = pluginSelectors.installedPlugins(toolStoreState);
|
||||
for (const plugin of installedPlugins) {
|
||||
if (plugin.manifest) {
|
||||
manifests[plugin.identifier] = plugin.manifest as LobeChatPluginManifest;
|
||||
manifests[plugin.identifier] = plugin.manifest as ToolManifest;
|
||||
// Check if this plugin has MCP params
|
||||
sourceMap[plugin.identifier] = plugin.customParams?.mcp ? 'mcp' : 'plugin';
|
||||
}
|
||||
|
|
@ -51,7 +50,7 @@ export class PluginInternalsActionImpl {
|
|||
// Get all builtin tools
|
||||
for (const tool of builtinTools) {
|
||||
if (tool.manifest) {
|
||||
manifests[tool.identifier] = tool.manifest as LobeChatPluginManifest;
|
||||
manifests[tool.identifier] = tool.manifest as ToolManifest;
|
||||
sourceMap[tool.identifier] = 'builtin';
|
||||
}
|
||||
}
|
||||
|
|
@ -60,7 +59,7 @@ export class PluginInternalsActionImpl {
|
|||
const klavisTools = klavisStoreSelectors.klavisAsLobeTools(toolStoreState);
|
||||
for (const tool of klavisTools) {
|
||||
if (tool.manifest) {
|
||||
manifests[tool.identifier] = tool.manifest as LobeChatPluginManifest;
|
||||
manifests[tool.identifier] = tool.manifest as ToolManifest;
|
||||
sourceMap[tool.identifier] = 'klavis';
|
||||
}
|
||||
}
|
||||
|
|
@ -69,7 +68,7 @@ export class PluginInternalsActionImpl {
|
|||
const lobehubSkillTools = lobehubSkillStoreSelectors.lobehubSkillAsLobeTools(toolStoreState);
|
||||
for (const tool of lobehubSkillTools) {
|
||||
if (tool.manifest) {
|
||||
manifests[tool.identifier] = tool.manifest as LobeChatPluginManifest;
|
||||
manifests[tool.identifier] = tool.manifest as ToolManifest;
|
||||
sourceMap[tool.identifier] = 'lobehubSkill';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import { type ChatToolPayload, type RuntimeStepContext } from '@lobechat/types';
|
||||
import { PluginErrorType } from '@lobehub/chat-plugin-sdk';
|
||||
import debug from 'debug';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { type MCPToolCallResult } from '@/libs/mcp';
|
||||
import { truncateToolResult } from '@/server/utils/truncateToolResult';
|
||||
import { chatService } from '@/services/chat';
|
||||
import { mcpService } from '@/services/mcp';
|
||||
import { messageService } from '@/services/message';
|
||||
import { AI_RUNTIME_OPERATION_TYPES } from '@/store/chat/slices/operation';
|
||||
|
|
@ -185,16 +182,6 @@ export class PluginTypesActionImpl {
|
|||
};
|
||||
};
|
||||
|
||||
invokeDefaultTypePlugin = async (id: string, payload: any): Promise<string | undefined> => {
|
||||
const { internal_callPluginApi } = this.#get();
|
||||
|
||||
const data = await internal_callPluginApi(id, payload);
|
||||
|
||||
if (!data) return;
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
invokeKlavisTypePlugin = async (
|
||||
id: string,
|
||||
payload: ChatToolPayload,
|
||||
|
|
@ -219,45 +206,6 @@ export class PluginTypesActionImpl {
|
|||
);
|
||||
};
|
||||
|
||||
invokeMarkdownTypePlugin = async (id: string, payload: ChatToolPayload): Promise<void> => {
|
||||
const { internal_callPluginApi } = this.#get();
|
||||
|
||||
await internal_callPluginApi(id, payload);
|
||||
};
|
||||
|
||||
invokeStandaloneTypePlugin = async (id: string, payload: ChatToolPayload): Promise<void> => {
|
||||
const result = await useToolStore.getState().validatePluginSettings(payload.identifier);
|
||||
if (!result) return;
|
||||
|
||||
// if the plugin settings is not valid, then set the message with error type
|
||||
if (!result.valid) {
|
||||
// Get message to extract agentId/topicId
|
||||
const message = dbMessageSelectors.getDbMessageById(id)(this.#get());
|
||||
const updateResult = await messageService.updateMessageError(
|
||||
id,
|
||||
{
|
||||
body: {
|
||||
error: result.errors,
|
||||
message: '[plugin] your settings is invalid with plugin manifest setting schema',
|
||||
},
|
||||
message: t('response.PluginSettingsInvalid', { ns: 'error' }),
|
||||
type: PluginErrorType.PluginSettingsInvalid as any,
|
||||
},
|
||||
{
|
||||
agentId: message?.agentId,
|
||||
topicId: message?.topicId,
|
||||
},
|
||||
);
|
||||
|
||||
if (updateResult?.success && updateResult.messages) {
|
||||
this.#get().replaceMessages(updateResult.messages, {
|
||||
context: { agentId: message?.agentId || '', topicId: message?.topicId },
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
invokeMCPTypePlugin = async (
|
||||
id: string,
|
||||
payload: ChatToolPayload,
|
||||
|
|
@ -402,76 +350,6 @@ export class PluginTypesActionImpl {
|
|||
|
||||
return remoteContent;
|
||||
};
|
||||
|
||||
internal_callPluginApi = async (
|
||||
id: string,
|
||||
payload: ChatToolPayload,
|
||||
): Promise<string | undefined> => {
|
||||
const { optimisticUpdateMessageContent } = this.#get();
|
||||
let data: string;
|
||||
|
||||
// Get message to extract agentId/topicId
|
||||
const message = dbMessageSelectors.getDbMessageById(id)(this.#get());
|
||||
|
||||
// Get abort controller from operation
|
||||
const operationId = this.#get().messageOperationMap[id];
|
||||
const operation = operationId ? this.#get().operations[operationId] : undefined;
|
||||
const abortController = operation?.abortController;
|
||||
|
||||
log(
|
||||
'[internal_callPluginApi] messageId=%s, plugin=%s, operationId=%s, aborted=%s',
|
||||
id,
|
||||
payload.identifier,
|
||||
operationId,
|
||||
abortController?.signal.aborted,
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await chatService.runPluginApi(payload, {
|
||||
signal: abortController?.signal,
|
||||
trace: { observationId: message?.observationId, traceId: message?.traceId },
|
||||
});
|
||||
data = res.text;
|
||||
|
||||
// save traceId
|
||||
if (res.traceId) {
|
||||
await messageService.updateMessage(id, { traceId: res.traceId });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const err = error as Error;
|
||||
|
||||
// ignore the aborted request error
|
||||
if (err.message.includes('The user aborted a request.')) {
|
||||
log(
|
||||
'[internal_callPluginApi] Request aborted: messageId=%s, plugin=%s',
|
||||
id,
|
||||
payload.identifier,
|
||||
);
|
||||
} else {
|
||||
const result = await messageService.updateMessageError(id, error as any, {
|
||||
agentId: message?.agentId,
|
||||
topicId: message?.topicId,
|
||||
});
|
||||
if (result?.success && result.messages) {
|
||||
this.#get().replaceMessages(result.messages, {
|
||||
context: { agentId: message?.agentId || '', topicId: message?.topicId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
data = '';
|
||||
}
|
||||
// If error occurred, exit
|
||||
if (!data) return;
|
||||
|
||||
// operationId already declared above, reuse it
|
||||
const context = operationId ? { operationId } : undefined;
|
||||
|
||||
await optimisticUpdateMessageContent(id, data, undefined, context);
|
||||
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
export type PluginTypesAction = Pick<PluginTypesActionImpl, keyof PluginTypesActionImpl>;
|
||||
|
|
|
|||
|
|
@ -92,26 +92,15 @@ export class PluginPublicApiActionImpl {
|
|||
stepContext?: RuntimeStepContext,
|
||||
): Promise<any> => {
|
||||
switch (payload.type) {
|
||||
case 'standalone': {
|
||||
return await this.#get().invokeStandaloneTypePlugin(id, payload);
|
||||
}
|
||||
|
||||
case 'markdown': {
|
||||
return await this.#get().invokeMarkdownTypePlugin(id, payload);
|
||||
}
|
||||
|
||||
case 'builtin': {
|
||||
// Pass stepContext to builtin tools for dynamic state access
|
||||
return await this.#get().invokeBuiltinTool(id, payload, stepContext);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
case 'mcp': {
|
||||
return await this.#get().invokeMCPTypePlugin(id, payload);
|
||||
}
|
||||
|
||||
case 'builtin':
|
||||
default: {
|
||||
return await this.#get().invokeDefaultTypePlugin(id, payload);
|
||||
// Pass stepContext to builtin tools for dynamic state access
|
||||
return await this.#get().invokeBuiltinTool(id, payload, stepContext);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { type LobeTool } from '@lobechat/types';
|
||||
import { type PluginSchema } from '@lobehub/chat-plugin-sdk';
|
||||
import { type LobeTool, type ToolManifestSettings } from '@lobechat/types';
|
||||
|
||||
import { type MetaData } from '@/types/meta';
|
||||
|
||||
|
|
@ -14,7 +13,7 @@ const getPluginAvatar = (meta?: MetaData) => meta?.avatar || '🧩';
|
|||
const isCustomPlugin = (id: string, pluginList: LobeTool[]) =>
|
||||
pluginList.some((i) => i.identifier === id && i.type === 'customPlugin');
|
||||
|
||||
const isSettingSchemaNonEmpty = (schema?: PluginSchema) =>
|
||||
const isSettingSchemaNonEmpty = (schema?: ToolManifestSettings) =>
|
||||
schema?.properties && Object.keys(schema.properties).length > 0;
|
||||
|
||||
export const pluginHelpers = {
|
||||
|
|
|
|||
|
|
@ -10,12 +10,10 @@ import {
|
|||
type LobehubSkillStoreState,
|
||||
} from './slices/lobehubSkillStore/initialState';
|
||||
import { initialMCPStoreState, type MCPStoreState } from './slices/mcpStore/initialState';
|
||||
import { initialPluginStoreState, type PluginStoreState } from './slices/oldStore/initialState';
|
||||
import { initialPluginState, type PluginState } from './slices/plugin/initialState';
|
||||
|
||||
export type ToolStoreState = PluginState &
|
||||
CustomPluginState &
|
||||
PluginStoreState &
|
||||
BuiltinToolState &
|
||||
MCPStoreState &
|
||||
KlavisStoreState &
|
||||
|
|
@ -25,7 +23,6 @@ export type ToolStoreState = PluginState &
|
|||
export const initialState: ToolStoreState = {
|
||||
...initialPluginState,
|
||||
...initialCustomPluginState,
|
||||
...initialPluginStoreState,
|
||||
...initialBuiltinToolState,
|
||||
...initialMCPStoreState,
|
||||
...initialKlavisStoreState,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,5 @@ export { customPluginSelectors } from '../slices/customPlugin/selectors';
|
|||
export { klavisStoreSelectors } from '../slices/klavisStore/selectors';
|
||||
export { lobehubSkillStoreSelectors } from '../slices/lobehubSkillStore/selectors';
|
||||
export { mcpStoreSelectors } from '../slices/mcpStore/selectors';
|
||||
export { pluginStoreSelectors } from '../slices/oldStore/selectors';
|
||||
export { pluginSelectors } from '../slices/plugin/selectors';
|
||||
export { toolSelectors } from './tool';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifest } from '@lobechat/types';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { type ToolStoreState } from '../initialState';
|
||||
|
|
@ -28,7 +28,7 @@ const mockState = {
|
|||
createdAt: '2024-01-01',
|
||||
homepage: 'https://example.com/plugin-1',
|
||||
meta: { title: 'Plugin 1', description: 'Plugin 1 description' },
|
||||
} as LobeChatPluginManifest,
|
||||
} as ToolManifest,
|
||||
runtimeType: 'standalone',
|
||||
type: 'plugin',
|
||||
},
|
||||
|
|
@ -39,7 +39,7 @@ const mockState = {
|
|||
api: [{ name: 'api-2' }],
|
||||
author: 'Another Author',
|
||||
homepage: 'https://example.com/plugin-2',
|
||||
} as LobeChatPluginManifest,
|
||||
} as ToolManifest,
|
||||
runtimeType: 'default',
|
||||
type: 'plugin',
|
||||
},
|
||||
|
|
@ -67,7 +67,7 @@ const mockState = {
|
|||
identifier: 'builtin-1',
|
||||
api: [{ name: 'builtin-api-1' }],
|
||||
meta: { title: 'Builtin 1', description: 'Builtin 1 description' },
|
||||
} as LobeChatPluginManifest,
|
||||
} as ToolManifest,
|
||||
},
|
||||
],
|
||||
pluginInstallLoading: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { getKlavisServerByServerIdentifier, getLobehubSkillProviderById } from '@lobechat/const';
|
||||
import { type RenderDisplayControl } from '@lobechat/types';
|
||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type RenderDisplayControl, type ToolManifest } from '@lobechat/types';
|
||||
|
||||
import {
|
||||
isInstalledPluginAvailableInCurrentEnv,
|
||||
|
|
@ -42,10 +41,10 @@ const getMetaById =
|
|||
|
||||
const getManifestById =
|
||||
(id: string) =>
|
||||
(s: ToolStoreState): LobeChatPluginManifest | undefined =>
|
||||
(s: ToolStoreState): ToolManifest | undefined =>
|
||||
pluginSelectors
|
||||
.installedPluginManifestList(s)
|
||||
.concat(s.builtinTools.map((b) => b.manifest as LobeChatPluginManifest))
|
||||
.concat(s.builtinTools.map((b) => b.manifest as ToolManifest))
|
||||
.find((i) => i.identifier === id);
|
||||
|
||||
// Get plugin manifest loading status
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ vi.mock('@/services/plugin', () => ({
|
|||
createCustomPlugin: vi.fn(),
|
||||
uninstallPlugin: vi.fn(),
|
||||
updatePluginManifest: vi.fn(),
|
||||
getInstalledPlugins: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifest } from '@lobechat/types';
|
||||
import { merge } from 'es-toolkit/compat';
|
||||
import { t } from 'i18next';
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ export class CustomPluginActionImpl {
|
|||
|
||||
try {
|
||||
updateInstallLoadingState(id, true);
|
||||
let manifest: LobeChatPluginManifest;
|
||||
let manifest: ToolManifest;
|
||||
// mean this is a mcp plugin
|
||||
if (!!plugin.customParams?.mcp) {
|
||||
const url = plugin.customParams?.mcp?.url;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type LobeChatPluginManifest, type LobeChatPluginMeta } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifest } from '@lobechat/types';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { type ToolStoreState } from '../../initialState';
|
||||
|
|
@ -19,7 +19,7 @@ const mockState = {
|
|||
identifier: 'plugin-1',
|
||||
api: [{ name: 'api-1' }],
|
||||
type: 'default',
|
||||
} as LobeChatPluginManifest,
|
||||
} as ToolManifest,
|
||||
},
|
||||
{
|
||||
identifier: 'plugin-2',
|
||||
|
|
@ -38,7 +38,7 @@ const mockState = {
|
|||
createdAt: '2021-01-01',
|
||||
meta: { avatar: 'avatar-url-1', title: 'Plugin 1' },
|
||||
homepage: 'http://homepage-1.com',
|
||||
} as LobeChatPluginMeta,
|
||||
} as any,
|
||||
{
|
||||
identifier: 'plugin-2',
|
||||
author: 'Author 2',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type * as LobechatConstModule from '@lobechat/const';
|
||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifest } from '@lobechat/types';
|
||||
import { type PluginItem } from '@lobehub/market-sdk';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
|
@ -230,7 +230,7 @@ describe('mcpStore actions', () => {
|
|||
});
|
||||
|
||||
describe('testMcpConnection', () => {
|
||||
const mockManifest: LobeChatPluginManifest = {
|
||||
const mockManifest: ToolManifest = {
|
||||
api: [],
|
||||
gateway: '',
|
||||
identifier: 'test-plugin',
|
||||
|
|
@ -717,7 +717,7 @@ describe('mcpStore actions', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const mockServerManifest: LobeChatPluginManifest = {
|
||||
const mockServerManifest: ToolManifest = {
|
||||
api: [],
|
||||
gateway: '',
|
||||
identifier: 'test-plugin',
|
||||
|
|
@ -1170,7 +1170,7 @@ describe('mcpStore actions', () => {
|
|||
version: '1.5.0',
|
||||
};
|
||||
|
||||
const serverManifestWithVersion: LobeChatPluginManifest = {
|
||||
const serverManifestWithVersion: ToolManifest = {
|
||||
api: [],
|
||||
gateway: '',
|
||||
identifier: 'test-plugin',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { CURRENT_VERSION, isDesktop } from '@lobechat/const';
|
||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { type ToolManifest } from '@lobechat/types';
|
||||
import { type PluginItem, type PluginListResponse } from '@lobehub/market-sdk';
|
||||
import { type TRPCClientError } from '@trpc/client';
|
||||
import debug from 'debug';
|
||||
|
|
@ -73,7 +73,7 @@ const toNonEmptyStringRecord = (input?: Record<string, any>) => {
|
|||
const buildCloudMcpManifest = (params: {
|
||||
data: any;
|
||||
plugin: { description?: string; icon?: string; identifier: string };
|
||||
}): LobeChatPluginManifest => {
|
||||
}): ToolManifest => {
|
||||
const { data, plugin } = params;
|
||||
|
||||
log('Using cloud connection, building manifest from market data');
|
||||
|
|
@ -104,7 +104,7 @@ const buildCloudMcpManifest = (params: {
|
|||
}
|
||||
|
||||
// Build complete manifest
|
||||
const manifest: LobeChatPluginManifest = {
|
||||
const manifest: ToolManifest = {
|
||||
api: apiArray,
|
||||
author: data.author?.name || data.author || '',
|
||||
createAt: data.createdAt || new Date().toISOString(),
|
||||
|
|
@ -120,7 +120,7 @@ const buildCloudMcpManifest = (params: {
|
|||
name: data.name || plugin.identifier,
|
||||
type: 'mcp',
|
||||
version: data.version,
|
||||
} as unknown as LobeChatPluginManifest;
|
||||
} as unknown as ToolManifest;
|
||||
|
||||
log('[Cloud MCP] Final manifest built:', {
|
||||
apiCount: manifest.api?.length,
|
||||
|
|
@ -136,7 +136,7 @@ export interface TestMcpConnectionResult {
|
|||
error?: string;
|
||||
/** STDIO process output logs for debugging */
|
||||
errorLog?: string;
|
||||
manifest?: LobeChatPluginManifest;
|
||||
manifest?: ToolManifest;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -470,7 +470,7 @@ export class PluginMCPStoreActionImpl {
|
|||
return;
|
||||
}
|
||||
|
||||
let manifest: LobeChatPluginManifest | undefined;
|
||||
let manifest: ToolManifest | undefined;
|
||||
|
||||
if (connection?.type === 'stdio') {
|
||||
manifest = await mcpService.getStdioMcpServerManifest(
|
||||
|
|
@ -750,7 +750,7 @@ export class PluginMCPStoreActionImpl {
|
|||
);
|
||||
|
||||
try {
|
||||
let manifest: LobeChatPluginManifest;
|
||||
let manifest: ToolManifest;
|
||||
|
||||
if (connection.type === 'http') {
|
||||
if (!connection.url) {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ import { type PluginItem } from '@lobehub/market-sdk';
|
|||
|
||||
import { type MCPInstallProgressMap } from '@/types/plugins';
|
||||
|
||||
export type PluginStoreListType = 'installed' | 'mcp';
|
||||
|
||||
export interface MCPStoreState {
|
||||
activeMCPIdentifier?: string;
|
||||
categories: string[];
|
||||
currentPage: number;
|
||||
isLoadingMore?: boolean;
|
||||
isMcpListInit?: boolean;
|
||||
listType: PluginStoreListType;
|
||||
mcpInstallAbortControllers: Record<string, AbortController>;
|
||||
mcpInstallProgress: MCPInstallProgressMap;
|
||||
mcpPluginItems: PluginItem[];
|
||||
|
|
@ -25,6 +28,7 @@ export interface MCPStoreState {
|
|||
export const initialMCPStoreState: MCPStoreState = {
|
||||
categories: [],
|
||||
currentPage: 1,
|
||||
listType: 'mcp',
|
||||
mcpInstallAbortControllers: {},
|
||||
mcpInstallProgress: {},
|
||||
mcpPluginItems: [],
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { MCPInstallStep } from '@/types/plugins';
|
|||
|
||||
import { initialState } from '../../initialState';
|
||||
import { type ToolStoreState } from '../../initialState';
|
||||
import { PluginStoreTabs } from '../oldStore/initialState';
|
||||
import { mcpStoreSelectors } from './selectors';
|
||||
|
||||
const createMockPluginItem = (id: string, overrides: Partial<PluginItem> = {}): PluginItem =>
|
||||
|
|
@ -56,20 +55,20 @@ const baseState: ToolStoreState = {
|
|||
settings: {},
|
||||
},
|
||||
],
|
||||
listType: PluginStoreTabs.MCP,
|
||||
listType: 'mcp',
|
||||
};
|
||||
|
||||
describe('mcpStoreSelectors', () => {
|
||||
describe('mcpPluginList', () => {
|
||||
it('should return all mcp plugins when listType is MCP', () => {
|
||||
const state: ToolStoreState = { ...baseState, listType: PluginStoreTabs.MCP };
|
||||
const state: ToolStoreState = { ...baseState, listType: 'mcp' };
|
||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should return only installed plugins when listType is Installed', () => {
|
||||
const state: ToolStoreState = { ...baseState, listType: PluginStoreTabs.Installed };
|
||||
const state: ToolStoreState = { ...baseState, listType: 'installed' };
|
||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
|
|
@ -77,7 +76,7 @@ describe('mcpStoreSelectors', () => {
|
|||
});
|
||||
|
||||
it('should map plugin items to InstallPluginMeta format', () => {
|
||||
const state: ToolStoreState = { ...baseState, listType: PluginStoreTabs.MCP };
|
||||
const state: ToolStoreState = { ...baseState, listType: 'mcp' };
|
||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||
const item = result[0];
|
||||
|
||||
|
|
@ -99,7 +98,7 @@ describe('mcpStoreSelectors', () => {
|
|||
const state: ToolStoreState = {
|
||||
...baseState,
|
||||
installedPlugins: [],
|
||||
listType: PluginStoreTabs.Installed,
|
||||
listType: 'installed',
|
||||
};
|
||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||
|
||||
|
|
@ -130,7 +129,7 @@ describe('mcpStoreSelectors', () => {
|
|||
settings: {},
|
||||
},
|
||||
],
|
||||
listType: PluginStoreTabs.Installed,
|
||||
listType: 'installed',
|
||||
};
|
||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,249 +0,0 @@
|
|||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { type Mock } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { notification } from '@/components/AntdStaticMethods';
|
||||
import { pluginService } from '@/services/plugin';
|
||||
import { toolService } from '@/services/tool';
|
||||
import { type DiscoverPluginItem } from '@/types/discover';
|
||||
|
||||
import { useToolStore } from '../../store';
|
||||
|
||||
// Mock necessary modules and functions
|
||||
vi.mock('@/components/AntdStaticMethods', () => ({
|
||||
notification: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
// Mock the pluginService.getToolList method
|
||||
vi.mock('@/services/plugin', () => ({
|
||||
pluginService: {
|
||||
uninstallPlugin: vi.fn(),
|
||||
installPlugin: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/services/tool', () => ({
|
||||
toolService: {
|
||||
getToolManifest: vi.fn(),
|
||||
getToolList: vi.fn(),
|
||||
getOldPluginList: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock i18next
|
||||
vi.mock('i18next', () => ({
|
||||
t: vi.fn((key) => key),
|
||||
}));
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
const logError = console.error;
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
useToolStore.setState({
|
||||
oldPluginItems: [
|
||||
{
|
||||
identifier: 'plugin1',
|
||||
title: 'plugin1',
|
||||
avatar: '🍏',
|
||||
manifest: 'https://abc.com/manifest.json',
|
||||
} as DiscoverPluginItem,
|
||||
],
|
||||
});
|
||||
console.error = () => {};
|
||||
});
|
||||
afterEach(() => {
|
||||
console.error = logError;
|
||||
});
|
||||
|
||||
describe('useToolStore:pluginStore', () => {
|
||||
describe('loadPluginStore', () => {
|
||||
it('should load plugin list and update state', async () => {
|
||||
// Given
|
||||
const pluginListMock = [{ identifier: 'plugin1' }, { identifier: 'plugin2' }];
|
||||
(toolService.getOldPluginList as Mock).mockResolvedValue({ items: pluginListMock });
|
||||
|
||||
// When
|
||||
let pluginList;
|
||||
await act(async () => {
|
||||
pluginList = await useToolStore.getState().loadPluginStore();
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(toolService.getOldPluginList).toHaveBeenCalled();
|
||||
expect(pluginList).toEqual(pluginListMock);
|
||||
expect(useToolStore.getState().oldPluginItems).toEqual(pluginListMock);
|
||||
});
|
||||
|
||||
it('should handle errors when loading plugin list', async () => {
|
||||
// Given
|
||||
const error = new Error('Failed to load plugin list');
|
||||
(toolService.getOldPluginList as Mock).mockRejectedValue(error);
|
||||
|
||||
// When
|
||||
let pluginList;
|
||||
let errorOccurred = false;
|
||||
try {
|
||||
await act(async () => {
|
||||
pluginList = await useToolStore.getState().loadPluginStore();
|
||||
});
|
||||
} catch (e) {
|
||||
errorOccurred = true;
|
||||
}
|
||||
|
||||
// Then
|
||||
expect(toolService.getOldPluginList).toHaveBeenCalled();
|
||||
expect(errorOccurred).toBe(true);
|
||||
expect(pluginList).toBeUndefined();
|
||||
// Ensure the state is not updated with an undefined value
|
||||
expect(useToolStore.getState().oldPluginItems).not.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useFetchPluginStore', () => {
|
||||
it('should fetch plugin store data', async () => {
|
||||
// Given
|
||||
const pluginListMock = [{ identifier: 'plugin1' }, { identifier: 'plugin2' }];
|
||||
(toolService.getOldPluginList as Mock).mockResolvedValue({ items: pluginListMock });
|
||||
|
||||
// When
|
||||
const { result } = renderHook(() => useToolStore().useFetchPluginStore());
|
||||
|
||||
// Wait for SWR to fetch data
|
||||
await waitFor(() => {
|
||||
expect(result.current.data).toEqual(pluginListMock);
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(toolService.getOldPluginList).toHaveBeenCalled();
|
||||
expect(result.current.error).toBeUndefined();
|
||||
});
|
||||
|
||||
// Note: Error handling test is not included because SWR retries by default,
|
||||
// making error scenarios difficult to test in unit tests.
|
||||
// The underlying loadPluginStore error handling is tested separately above.
|
||||
});
|
||||
|
||||
describe('installPlugin', () => {
|
||||
it('should be deprecated and do nothing', async () => {
|
||||
// Old plugin system has been deprecated
|
||||
await act(async () => {
|
||||
await useToolStore.getState().installPlugin('plugin1');
|
||||
});
|
||||
|
||||
// Should not call any service
|
||||
expect(toolService.getToolManifest).not.toHaveBeenCalled();
|
||||
expect(pluginService.installPlugin).not.toHaveBeenCalled();
|
||||
expect(notification.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('installPlugins', () => {
|
||||
it('should be deprecated and do nothing', async () => {
|
||||
// Old plugin system has been deprecated
|
||||
await act(async () => {
|
||||
await useToolStore.getState().installPlugins(['plugin1', 'plugin2']);
|
||||
});
|
||||
|
||||
// Should not call any service
|
||||
expect(pluginService.installPlugin).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('unInstallPlugin', () => {
|
||||
it('should uninstall a plugin and remove its manifest', async () => {
|
||||
// Given
|
||||
const pluginIdentifier = 'plugin1';
|
||||
act(() => {
|
||||
useToolStore.setState({
|
||||
installedPlugins: [
|
||||
{
|
||||
identifier: pluginIdentifier,
|
||||
type: 'plugin',
|
||||
manifest: {
|
||||
identifier: pluginIdentifier,
|
||||
} as LobeChatPluginManifest,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// When
|
||||
act(() => {
|
||||
useToolStore.getState().uninstallPlugin(pluginIdentifier);
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(pluginService.uninstallPlugin).toBeCalledWith(pluginIdentifier);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateInstallLoadingState', () => {
|
||||
it('should update the loading state for a plugin', () => {
|
||||
const pluginIdentifier = 'abc';
|
||||
const loadingState = true;
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
act(() => {
|
||||
result.current.updateInstallLoadingState(pluginIdentifier, loadingState);
|
||||
});
|
||||
|
||||
expect(result.current.pluginInstallLoading[pluginIdentifier]).toBe(loadingState);
|
||||
});
|
||||
|
||||
it('should clear the loading state for a plugin', () => {
|
||||
// Given
|
||||
const pluginIdentifier = 'dddd';
|
||||
const loadingState = undefined;
|
||||
|
||||
act(() => {
|
||||
useToolStore.setState({ pluginInstallLoading: { [pluginIdentifier]: true } });
|
||||
});
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
// When
|
||||
act(() => {
|
||||
result.current.updateInstallLoadingState(pluginIdentifier, loadingState);
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result.current.pluginInstallLoading[pluginIdentifier]).toBe(loadingState);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
import { type LobeTool } from '@lobechat/types';
|
||||
import { uniqBy } from 'es-toolkit/compat';
|
||||
import { produce } from 'immer';
|
||||
import { type SWRResponse } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { mutate } from '@/libs/swr';
|
||||
import { pluginService } from '@/services/plugin';
|
||||
import { toolService } from '@/services/tool';
|
||||
import { globalHelpers } from '@/store/global/helpers';
|
||||
import { type StoreSetter } from '@/store/types';
|
||||
import {
|
||||
type DiscoverPluginItem,
|
||||
type PluginListResponse,
|
||||
type PluginQueryParams,
|
||||
} from '@/types/discover';
|
||||
import { setNamespace } from '@/utils/storeDebug';
|
||||
|
||||
import { type ToolStore } from '../../store';
|
||||
import { type PluginInstallProgress, type PluginStoreState } from './initialState';
|
||||
|
||||
const n = setNamespace('pluginStore');
|
||||
|
||||
const INSTALLED_PLUGINS = 'loadInstalledPlugins';
|
||||
|
||||
type Setter = StoreSetter<ToolStore>;
|
||||
export const createPluginStoreSlice = (set: Setter, get: () => ToolStore, _api?: unknown) =>
|
||||
new PluginStoreActionImpl(set, get, _api);
|
||||
|
||||
export class PluginStoreActionImpl {
|
||||
readonly #get: () => ToolStore;
|
||||
readonly #set: Setter;
|
||||
|
||||
constructor(set: Setter, get: () => ToolStore, _api?: unknown) {
|
||||
void _api;
|
||||
this.#set = set;
|
||||
this.#get = get;
|
||||
}
|
||||
|
||||
installOldPlugin = async (
|
||||
_name: string,
|
||||
_type: 'plugin' | 'customPlugin' = 'plugin',
|
||||
): Promise<void> => {
|
||||
// Old plugin system has been deprecated, skip installation silently
|
||||
};
|
||||
|
||||
installPlugin = async (
|
||||
_name: string,
|
||||
_type: 'plugin' | 'customPlugin' = 'plugin',
|
||||
): Promise<void> => {
|
||||
// Old plugin system has been deprecated, skip installation silently
|
||||
};
|
||||
|
||||
installPlugins = async (_plugins: string[]): Promise<void> => {
|
||||
// Old plugin system has been deprecated, skip installation silently
|
||||
};
|
||||
|
||||
loadMorePlugins = (): void => {
|
||||
const { oldPluginItems, pluginTotalCount, currentPluginPage } = this.#get();
|
||||
|
||||
// Check if there is more data to load
|
||||
if (oldPluginItems.length < (pluginTotalCount || 0)) {
|
||||
this.#set(
|
||||
produce((draft: PluginStoreState) => {
|
||||
draft.currentPluginPage = currentPluginPage + 1;
|
||||
}),
|
||||
false,
|
||||
n('loadMorePlugins'),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
loadPluginStore = async (): Promise<DiscoverPluginItem[]> => {
|
||||
const locale = globalHelpers.getCurrentLanguage();
|
||||
|
||||
const data = await toolService.getOldPluginList({
|
||||
locale,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
});
|
||||
|
||||
this.#set({ oldPluginItems: data.items }, false, n('loadPluginList'));
|
||||
|
||||
return data.items;
|
||||
};
|
||||
|
||||
refreshPlugins = async (): Promise<void> => {
|
||||
await mutate(INSTALLED_PLUGINS);
|
||||
};
|
||||
|
||||
resetPluginList = (keywords?: string): void => {
|
||||
this.#set(
|
||||
produce((draft: PluginStoreState) => {
|
||||
draft.oldPluginItems = [];
|
||||
draft.currentPluginPage = 1;
|
||||
draft.pluginSearchKeywords = keywords;
|
||||
}),
|
||||
false,
|
||||
n('resetPluginList'),
|
||||
);
|
||||
};
|
||||
|
||||
uninstallPlugin = async (identifier: string): Promise<void> => {
|
||||
await pluginService.uninstallPlugin(identifier);
|
||||
await this.#get().refreshPlugins();
|
||||
};
|
||||
|
||||
updateInstallLoadingState = (key: string, value: boolean | undefined): void => {
|
||||
this.#set(
|
||||
produce((draft: PluginStoreState) => {
|
||||
draft.pluginInstallLoading[key] = value;
|
||||
}),
|
||||
false,
|
||||
n('updateInstallLoadingState'),
|
||||
);
|
||||
};
|
||||
|
||||
updatePluginInstallProgress = (
|
||||
identifier: string,
|
||||
progress: PluginInstallProgress | undefined,
|
||||
): void => {
|
||||
this.#set(
|
||||
produce((draft: PluginStoreState) => {
|
||||
draft.pluginInstallProgress[identifier] = progress;
|
||||
}),
|
||||
false,
|
||||
n(`updatePluginInstallProgress/${progress?.step || 'clear'}`),
|
||||
);
|
||||
};
|
||||
|
||||
useFetchInstalledPlugins = (enabled: boolean): SWRResponse<LobeTool[]> => {
|
||||
return useSWR<LobeTool[]>(
|
||||
enabled ? INSTALLED_PLUGINS : null,
|
||||
pluginService.getInstalledPlugins,
|
||||
{
|
||||
fallbackData: [],
|
||||
onSuccess: (data) => {
|
||||
this.#set(
|
||||
{ installedPlugins: data, loadingInstallPlugins: false },
|
||||
false,
|
||||
n('useFetchInstalledPlugins'),
|
||||
);
|
||||
},
|
||||
revalidateOnFocus: false,
|
||||
suspense: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
useFetchPluginList = (params: PluginQueryParams): SWRResponse<PluginListResponse> => {
|
||||
const locale = globalHelpers.getCurrentLanguage();
|
||||
|
||||
return useSWR<PluginListResponse>(
|
||||
['useFetchPluginList', locale, ...Object.values(params)].filter(Boolean).join('-'),
|
||||
async () => toolService.getOldPluginList(params),
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
this.#set(
|
||||
produce((draft: PluginStoreState) => {
|
||||
draft.pluginSearchLoading = false;
|
||||
|
||||
// Set basic information
|
||||
if (!draft.isPluginListInit) {
|
||||
draft.activePluginIdentifier = data.items?.[0]?.identifier;
|
||||
draft.isPluginListInit = true;
|
||||
draft.pluginTotalCount = data.totalCount;
|
||||
}
|
||||
|
||||
// Accumulate data logic
|
||||
if (params.page === 1) {
|
||||
// First page, set directly
|
||||
draft.oldPluginItems = uniqBy(data.items, 'identifier');
|
||||
} else {
|
||||
// Subsequent pages, accumulate data
|
||||
draft.oldPluginItems = uniqBy(
|
||||
[...draft.oldPluginItems, ...data.items],
|
||||
'identifier',
|
||||
);
|
||||
}
|
||||
}),
|
||||
false,
|
||||
n('useFetchPluginList/onSuccess'),
|
||||
);
|
||||
},
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
useFetchPluginStore = (): SWRResponse<DiscoverPluginItem[]> => {
|
||||
return useSWR<DiscoverPluginItem[]>('loadPluginStore', this.#get().loadPluginStore, {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export type PluginStoreAction = Pick<PluginStoreActionImpl, keyof PluginStoreActionImpl>;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from './action';
|
||||
export * from './initialState';
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { type DiscoverPluginItem } from '@/types/discover';
|
||||
|
||||
export type PluginInstallLoadingMap = Record<string, boolean | undefined>;
|
||||
|
||||
export enum PluginStoreTabs {
|
||||
Installed = 'installed',
|
||||
MCP = 'mcp',
|
||||
Plugin = 'old',
|
||||
}
|
||||
|
||||
export enum PluginInstallStep {
|
||||
COMPLETED = 'COMPLETED',
|
||||
ERROR = 'ERROR',
|
||||
FETCHING_MANIFEST = 'FETCHING_MANIFEST',
|
||||
INSTALLING_PLUGIN = 'INSTALLING_PLUGIN',
|
||||
}
|
||||
|
||||
export interface PluginInstallProgress {
|
||||
// Error message
|
||||
error?: string;
|
||||
// 0-100
|
||||
progress: number;
|
||||
step: PluginInstallStep;
|
||||
}
|
||||
|
||||
export type PluginInstallProgressMap = Record<string, PluginInstallProgress | undefined>;
|
||||
|
||||
export interface PluginStoreState {
|
||||
activePluginIdentifier?: string;
|
||||
currentPluginPage: number;
|
||||
displayMode: 'grid' | 'list';
|
||||
isPluginListInit?: boolean;
|
||||
|
||||
listType: PluginStoreTabs;
|
||||
oldPluginItems: DiscoverPluginItem[];
|
||||
pluginInstallLoading: PluginInstallLoadingMap;
|
||||
pluginInstallProgress: PluginInstallProgressMap;
|
||||
pluginSearchKeywords?: string;
|
||||
pluginSearchLoading?: boolean;
|
||||
pluginTotalCount?: number;
|
||||
}
|
||||
|
||||
export const initialPluginStoreState: PluginStoreState = {
|
||||
// Plugin list state management initial values
|
||||
currentPluginPage: 1,
|
||||
displayMode: 'grid',
|
||||
listType: PluginStoreTabs.MCP,
|
||||
oldPluginItems: [],
|
||||
pluginInstallLoading: {},
|
||||
pluginInstallProgress: {},
|
||||
};
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { type ToolStoreState } from '../../initialState';
|
||||
import { initialState } from '../../initialState';
|
||||
import { pluginStoreSelectors } from './selectors';
|
||||
|
||||
const mockState = {
|
||||
...initialState,
|
||||
listType: 'old',
|
||||
oldPluginItems: [
|
||||
{
|
||||
identifier: 'plugin-1',
|
||||
author: 'Author 1',
|
||||
createdAt: '2021-01-01',
|
||||
avatar: 'avatar-url-1',
|
||||
title: 'Plugin 1',
|
||||
homepage: 'http://homepage-1.com',
|
||||
},
|
||||
{
|
||||
identifier: 'plugin-2',
|
||||
author: 'Author 2',
|
||||
createdAt: '2022-02-02',
|
||||
avatar: 'avatar-url-2',
|
||||
title: 'Plugin 2',
|
||||
homepage: 'http://homepage-2.com',
|
||||
},
|
||||
],
|
||||
} as ToolStoreState;
|
||||
|
||||
describe('pluginStoreSelectors', () => {
|
||||
describe('onlinePluginStore', () => {
|
||||
it('should return the online plugin list', () => {
|
||||
const result = pluginStoreSelectors.onlinePluginStore(mockState);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
identifier: 'plugin-1',
|
||||
author: 'Author 1',
|
||||
createdAt: '2021-01-01',
|
||||
meta: { avatar: 'avatar-url-1', title: 'Plugin 1' },
|
||||
homepage: 'http://homepage-1.com',
|
||||
type: 'plugin',
|
||||
},
|
||||
{
|
||||
identifier: 'plugin-2',
|
||||
author: 'Author 2',
|
||||
createdAt: '2022-02-02',
|
||||
meta: { avatar: 'avatar-url-2', title: 'Plugin 2' },
|
||||
homepage: 'http://homepage-2.com',
|
||||
type: 'plugin',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { PluginStoreTabs } from '@/store/tool/slices/oldStore/initialState';
|
||||
import { type InstallPluginMeta } from '@/types/tool/plugin';
|
||||
|
||||
import { type ToolStoreState } from '../../initialState';
|
||||
|
||||
const onlinePluginStore = (s: ToolStoreState) => {
|
||||
const installedPluginIds = new Set(s.installedPlugins.map((i) => i.identifier));
|
||||
const list =
|
||||
s.listType === PluginStoreTabs.Plugin
|
||||
? s.oldPluginItems
|
||||
: s.oldPluginItems.filter((p) => installedPluginIds.has(p.identifier));
|
||||
|
||||
return list.map<InstallPluginMeta>((p) => ({
|
||||
author: p.author,
|
||||
createdAt: p.createdAt,
|
||||
homepage: p.homepage,
|
||||
identifier: p.identifier,
|
||||
meta: {
|
||||
avatar: p.avatar,
|
||||
description: p.description,
|
||||
tags: p.tags,
|
||||
title: p.title,
|
||||
},
|
||||
type: 'plugin',
|
||||
}));
|
||||
};
|
||||
|
||||
const isPluginInstallLoading = (id: string) => (s: ToolStoreState) => s.pluginInstallLoading[id];
|
||||
|
||||
const getPluginInstallProgress = (id: string) => (s: ToolStoreState) => s.pluginInstallProgress[id];
|
||||
|
||||
const isOldPluginInInstallProgress = (id: string) => (s: ToolStoreState) =>
|
||||
!!s.pluginInstallProgress[id];
|
||||
|
||||
const getPluginById = (id: string) => (s: ToolStoreState) => {
|
||||
return s.oldPluginItems.find((i) => i.identifier === id);
|
||||
};
|
||||
|
||||
export const pluginStoreSelectors = {
|
||||
getPluginById,
|
||||
getPluginInstallProgress,
|
||||
isOldPluginInInstallProgress,
|
||||
isPluginInstallLoading,
|
||||
onlinePluginStore,
|
||||
};
|
||||
|
|
@ -9,6 +9,7 @@ import { useToolStore } from '../../store';
|
|||
|
||||
vi.mock('@/services/plugin', () => ({
|
||||
pluginService: {
|
||||
getInstalledPlugins: vi.fn().mockResolvedValue([]),
|
||||
updatePluginSettings: vi.fn(),
|
||||
removeAllPlugins: vi.fn(),
|
||||
},
|
||||
|
|
@ -23,13 +24,6 @@ describe('useToolStore:plugin', () => {
|
|||
describe('checkPluginsIsInstalled', () => {
|
||||
it('should be deprecated and do nothing', async () => {
|
||||
// Old plugin system has been deprecated
|
||||
const loadPluginStoreMock = vi.fn();
|
||||
const installPluginsMock = vi.fn();
|
||||
useToolStore.setState({
|
||||
loadPluginStore: loadPluginStoreMock,
|
||||
installPlugins: installPluginsMock,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useToolStore());
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -37,8 +31,6 @@ describe('useToolStore:plugin', () => {
|
|||
});
|
||||
|
||||
// Should not call any methods since old plugin system is deprecated
|
||||
expect(loadPluginStoreMock).not.toHaveBeenCalled();
|
||||
expect(installPluginsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue