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.
|
in favor of the MCP tool system.
|
||||||
|
|
||||||
- Frontend calls them via the
|
- Frontend calls them via the
|
||||||
`invokeDefaultTypePlugin` method
|
`invokeBuiltinTool` method
|
||||||
- Retrieves plugin settings and manifest,
|
- Retrieves plugin settings and manifest,
|
||||||
creates authentication headers,
|
creates authentication headers,
|
||||||
and sends requests to the plugin gateway
|
and sends requests to the plugin gateway
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ while (state.status !== 'done' && state.status !== 'error') {
|
||||||
**Plugin 工具**:传统插件体系,通过 API 网关调用。
|
**Plugin 工具**:传统插件体系,通过 API 网关调用。
|
||||||
该体系预期将逐步废弃,由 MCP 工具体系替代。
|
该体系预期将逐步废弃,由 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": "cross-env NODE_OPTIONS=--max-old-space-size=7168 bun run build:next:raw",
|
||||||
"build:next:raw": "next build",
|
"build:next:raw": "next build",
|
||||||
"build:raw": "bun run build:spa:raw && bun run build:spa:copy && bun run build:next:raw",
|
"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: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:mobile": "cross-env NODE_OPTIONS=--max-old-space-size=8192 MOBILE=true vite build",
|
||||||
"build:spa:raw": "rm -rf public/_spa && 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-migrate-db": "bun run db:migrate",
|
||||||
"build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts",
|
"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.\"'",
|
"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:*",
|
"@lobechat/web-crawler": "workspace:*",
|
||||||
"@lobehub/analytics": "^1.6.0",
|
"@lobehub/analytics": "^1.6.0",
|
||||||
"@lobehub/charts": "^5.0.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/desktop-ipc-typings": "workspace:*",
|
||||||
"@lobehub/editor": "^4.5.0",
|
"@lobehub/editor": "^4.5.0",
|
||||||
"@lobehub/icons": "^5.0.0",
|
"@lobehub/icons": "^5.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { DEFAULT_PREFERENCE } from '@lobechat/const';
|
import { DEFAULT_PREFERENCE } from '@lobechat/const';
|
||||||
import type { CustomPluginParams, UserAgentOnboarding, UserOnboarding } from '@lobechat/types';
|
import type {
|
||||||
import type { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
CustomPluginParams,
|
||||||
|
ToolManifest,
|
||||||
|
UserAgentOnboarding,
|
||||||
|
UserOnboarding,
|
||||||
|
} from '@lobechat/types';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
import { boolean, index, jsonb, pgTable, primaryKey, text, varchar } from 'drizzle-orm/pg-core';
|
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(),
|
identifier: text('identifier').notNull(),
|
||||||
type: text('type', { enum: ['plugin', 'customPlugin'] }).notNull(),
|
type: text('type', { enum: ['plugin', 'customPlugin'] }).notNull(),
|
||||||
manifest: jsonb('manifest').$type<LobeChatPluginManifest>(),
|
manifest: jsonb('manifest').$type<ToolManifest>(),
|
||||||
settings: jsonb('settings'),
|
settings: jsonb('settings'),
|
||||||
customParams: jsonb('custom_params').$type<CustomPluginParams>(),
|
customParams: jsonb('custom_params').$type<CustomPluginParams>(),
|
||||||
source: varchar255('source'),
|
source: varchar255('source'),
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lobechat/python-interpreter": "workspace:*",
|
"@lobechat/python-interpreter": "workspace:*",
|
||||||
"@lobechat/web-crawler": "workspace:*",
|
"@lobechat/web-crawler": "workspace:*",
|
||||||
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
|
||||||
"@lobehub/market-sdk": "0.32.2",
|
"@lobehub/market-sdk": "0.32.2",
|
||||||
"@lobehub/market-types": "^1.12.3",
|
"@lobehub/market-types": "^1.12.3",
|
||||||
"model-bank": "workspace:*",
|
"model-bank": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import type { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
import type { ToolManifest } from '../tool/manifest';
|
||||||
import type { LobeChatPluginMeta, Meta } from '@lobehub/chat-plugin-sdk/lib/types/market';
|
|
||||||
|
|
||||||
export enum PluginCategory {
|
export enum PluginCategory {
|
||||||
All = 'all',
|
All = 'all',
|
||||||
|
|
@ -24,7 +23,24 @@ export enum PluginSorts {
|
||||||
Title = 'title',
|
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;
|
category?: PluginCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,7 +71,7 @@ export interface PluginListResponse {
|
||||||
export type PluginSource = 'legacy' | 'market' | 'builtin';
|
export type PluginSource = 'legacy' | 'market' | 'builtin';
|
||||||
|
|
||||||
export interface DiscoverPluginDetail extends Omit<DiscoverPluginItem, 'manifest'> {
|
export interface DiscoverPluginDetail extends Omit<DiscoverPluginItem, 'manifest'> {
|
||||||
manifest?: LobeChatPluginManifest | string;
|
manifest?: ToolManifest | string;
|
||||||
related: DiscoverPluginItem[];
|
related: DiscoverPluginItem[];
|
||||||
/**
|
/**
|
||||||
* Plugin source type
|
* Plugin source type
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { IPluginErrorType } from '@lobehub/chat-plugin-sdk';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { ILobeAgentRuntimeErrorType } from '../../agentRuntime';
|
import type { ILobeAgentRuntimeErrorType } from '../../agentRuntime';
|
||||||
import type { ErrorType } from '../../fetch';
|
import type { ErrorType } from '../../fetch';
|
||||||
|
import type { IToolErrorType } from '../../tool/error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chat message error object
|
* Chat message error object
|
||||||
|
|
@ -10,7 +10,7 @@ import type { ErrorType } from '../../fetch';
|
||||||
export interface ChatMessageError {
|
export interface ChatMessageError {
|
||||||
body?: any;
|
body?: any;
|
||||||
message?: string;
|
message?: string;
|
||||||
type: ErrorType | IPluginErrorType | ILobeAgentRuntimeErrorType;
|
type: ErrorType | IToolErrorType | ILobeAgentRuntimeErrorType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatMessageErrorSchema = z.object({
|
export const ChatMessageErrorSchema = z.object({
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import type { IPluginErrorType } from '@lobehub/chat-plugin-sdk';
|
|
||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
|
@ -129,5 +128,5 @@ export const ChatToolPayloadSchema = z.object({
|
||||||
export interface ChatMessagePluginError {
|
export interface ChatMessagePluginError {
|
||||||
body?: any;
|
body?: any;
|
||||||
message: string;
|
message: string;
|
||||||
type: IPluginErrorType;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { type RuntimeStepContext } from '../stepContext';
|
||||||
import { type HumanInterventionConfig, type HumanInterventionPolicy } from './intervention';
|
import { type HumanInterventionConfig, type HumanInterventionPolicy } from './intervention';
|
||||||
import { HumanInterventionConfigSchema, HumanInterventionPolicySchema } from './intervention';
|
import { HumanInterventionConfigSchema, HumanInterventionPolicySchema } from './intervention';
|
||||||
|
|
||||||
interface Meta {
|
export interface Meta {
|
||||||
/**
|
/**
|
||||||
* avatar
|
* avatar
|
||||||
* @desc Avatar of the plugin
|
* @desc Avatar of the plugin
|
||||||
|
|
@ -35,7 +35,7 @@ interface Meta {
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MetaSchema = z.object({
|
export const MetaSchema = z.object({
|
||||||
avatar: z.string().optional(),
|
avatar: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
readme: 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 { CustomPluginParams } from './plugin';
|
||||||
import type { LobeToolType } from './tool';
|
import type { LobeToolType } from './tool';
|
||||||
|
|
||||||
export interface LobeTool {
|
export interface LobeTool {
|
||||||
customParams?: CustomPluginParams | null;
|
customParams?: CustomPluginParams | null;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
manifest?: LobeChatPluginManifest | null;
|
manifest?: ToolManifest | null;
|
||||||
/**
|
/**
|
||||||
* use for runtime
|
* use for runtime
|
||||||
*/
|
*/
|
||||||
runtimeType?: 'mcp' | 'default' | 'markdown' | 'standalone';
|
runtimeType?: ToolManifestType;
|
||||||
settings?: any;
|
settings?: any;
|
||||||
// TODO: remove type and then make it required
|
// TODO: remove type and then make it required
|
||||||
source?: LobeToolType;
|
source?: LobeToolType;
|
||||||
|
|
@ -21,12 +20,14 @@ export interface LobeTool {
|
||||||
type: LobeToolType;
|
type: LobeToolType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LobeToolRenderType = LobePluginType | 'builtin';
|
export type LobeToolRenderType = ToolManifestType;
|
||||||
|
|
||||||
export * from './builtin';
|
export * from './builtin';
|
||||||
export * from './crawler';
|
export * from './crawler';
|
||||||
|
export * from './error';
|
||||||
export * from './interpreter';
|
export * from './interpreter';
|
||||||
export * from './intervention';
|
export * from './intervention';
|
||||||
|
export * from './manifest';
|
||||||
export * from './plugin';
|
export * from './plugin';
|
||||||
export * from './search';
|
export * from './search';
|
||||||
export * from './tool';
|
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';
|
import type { LobeToolType } from './tool';
|
||||||
|
|
||||||
export type PluginManifestMap = Record<string, LobeChatPluginManifest>;
|
export type PluginManifestMap = Record<string, ToolManifest>;
|
||||||
|
|
||||||
export interface CustomPluginMetadata {
|
export interface CustomPluginMetadata {
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
|
@ -53,7 +53,7 @@ export interface CustomPluginParams {
|
||||||
export interface LobeToolCustomPlugin {
|
export interface LobeToolCustomPlugin {
|
||||||
customParams?: CustomPluginParams;
|
customParams?: CustomPluginParams;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
manifest?: LobeChatPluginManifest;
|
manifest?: ToolManifest;
|
||||||
settings?: any;
|
settings?: any;
|
||||||
type: 'customPlugin';
|
type: 'customPlugin';
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +63,7 @@ export interface InstallPluginMeta extends Partial<Meta> {
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
homepage?: string;
|
homepage?: string;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
runtimeType?: 'mcp' | 'default' | 'markdown' | 'standalone' | undefined;
|
runtimeType?: ToolManifestType;
|
||||||
type: LobeToolType;
|
type: LobeToolType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,3 +71,12 @@ export interface PluginInstallError {
|
||||||
cause?: string;
|
cause?: string;
|
||||||
message: 'noManifest' | 'fetchError' | 'manifestInvalid' | 'urlError';
|
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": {
|
"dependencies": {
|
||||||
"@lobechat/const": "workspace:*",
|
"@lobechat/const": "workspace:*",
|
||||||
|
"@lobechat/ssrf-safe-fetch": "workspace:*",
|
||||||
"@lobechat/types": "workspace:*",
|
"@lobechat/types": "workspace:*",
|
||||||
"@lobehub/chat-plugin-sdk": "^1.32.4",
|
|
||||||
"@vercel/functions": "^3.3.0",
|
"@vercel/functions": "^3.3.0",
|
||||||
"brotli-wasm": "^3.0.1",
|
"brotli-wasm": "^3.0.1",
|
||||||
"chroma-js": "^3.1.2",
|
"chroma-js": "^3.1.2",
|
||||||
|
|
@ -33,7 +33,6 @@
|
||||||
"remark": "^15.0.1",
|
"remark": "^15.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-html": "^16.0.1",
|
"remark-html": "^16.0.1",
|
||||||
"@lobechat/ssrf-safe-fetch": "workspace:*",
|
|
||||||
"tokenx": "^1.2.1",
|
"tokenx": "^1.2.1",
|
||||||
"ua-parser-js": "^1.0.41",
|
"ua-parser-js": "^1.0.41",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
|
|
@ -42,4 +41,4 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest-canvas-mock": "^1.1.3"
|
"vitest-canvas-mock": "^1.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import type { OpenAIPluginManifest } from '@lobechat/types';
|
import type { ToolManifest } from '@lobechat/types';
|
||||||
import type { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
import { ToolManifestSchema } from '@lobechat/types';
|
||||||
import { pluginManifestSchema } from '@lobehub/chat-plugin-sdk';
|
|
||||||
|
|
||||||
import { API_ENDPOINTS } from '@/services/_url';
|
import { API_ENDPOINTS } from '@/services/_url';
|
||||||
|
|
||||||
const fetchJSON = async <T = any>(url: string, proxy = false): Promise<T> => {
|
const fetchJSON = async <T = any>(url: string, proxy = false): Promise<T> => {
|
||||||
// 2. Send request
|
|
||||||
let res: Response;
|
let res: Response;
|
||||||
try {
|
try {
|
||||||
res = await (proxy ? fetch(API_ENDPOINTS.proxy, { body: url, method: 'POST' }) : fetch(url));
|
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;
|
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 (
|
export const getToolManifest = async (
|
||||||
url?: string,
|
url?: string,
|
||||||
useProxy: boolean = false,
|
useProxy: boolean = false,
|
||||||
): Promise<LobeChatPluginManifest> => {
|
): Promise<ToolManifest> => {
|
||||||
// 1. Validate plugin
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
throw new TypeError('noManifest');
|
throw new TypeError('noManifest');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Send request
|
const data = await fetchJSON<ToolManifest>(url, useProxy);
|
||||||
let data = await fetchJSON<LobeChatPluginManifest>(url, useProxy);
|
|
||||||
|
|
||||||
// @ts-ignore
|
const parser = ToolManifestSchema.safeParse(data);
|
||||||
// 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);
|
|
||||||
|
|
||||||
if (!parser.success) {
|
if (!parser.success) {
|
||||||
throw new TypeError('manifestInvalid', { cause: parser.error });
|
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'
|
- '@lobehub/editor'
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
'@lobehub/chat-plugin-sdk>swagger-client': 3.36.0
|
|
||||||
'@swagger-api/apidom-reference': 1.1.0
|
|
||||||
jose: ^6.1.3
|
jose: ^6.1.3
|
||||||
stylelint-config-clean-order: 7.0.0
|
stylelint-config-clean-order: 7.0.0
|
||||||
pdfjs-dist: 5.4.530
|
pdfjs-dist: 5.4.530
|
||||||
|
|
@ -18,5 +16,4 @@ overrides:
|
||||||
react-dom: 19.2.4
|
react-dom: 19.2.4
|
||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
'@swagger-api/apidom-reference': patches/@swagger-api__apidom-reference.patch
|
|
||||||
'@upstash/qstash': patches/@upstash__qstash.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 { Flexbox } from '@lobehub/ui';
|
||||||
import { Switch } from 'antd';
|
import { Switch } from 'antd';
|
||||||
import isEqual from 'fast-deep-equal';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { useToolStore } from '@/store/tool';
|
|
||||||
|
|
||||||
import { useStore } from '../store';
|
import { useStore } from '../store';
|
||||||
|
|
||||||
const MarketList = memo<{ id: string }>(({ id }) => {
|
const MarketList = memo<{ id: string }>(({ id }) => {
|
||||||
const [toggleAgentPlugin, hasPlugin] = useStore((s) => [s.toggleAgentPlugin, !!s.config.plugins]);
|
const [toggleAgentPlugin, hasPlugin] = useStore((s) => [s.toggleAgentPlugin, !!s.config.plugins]);
|
||||||
const plugins = useStore((s) => 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 (
|
return (
|
||||||
<Flexbox horizontal align={'center'} gap={8}>
|
<Flexbox horizontal align={'center'} gap={8}>
|
||||||
<Switch
|
<Switch
|
||||||
loading={pluginManifestLoading[id]}
|
checked={!hasPlugin ? false : plugins.includes(id)}
|
||||||
checked={
|
onChange={() => {
|
||||||
// If loading, it means it's activated
|
|
||||||
pluginManifestLoading[id] || !hasPlugin ? false : plugins.includes(id)
|
|
||||||
}
|
|
||||||
onChange={(checked) => {
|
|
||||||
toggleAgentPlugin(id);
|
toggleAgentPlugin(id);
|
||||||
if (checked) {
|
|
||||||
fetchPluginManifest(id);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
|
|
|
||||||
|
|
@ -59,20 +59,17 @@ export const useControls = ({ setUpdating }: { setUpdating: (updating: boolean)
|
||||||
const userAgentSkills = useToolStore(agentSkillsSelectors.getUserAgentSkills, isEqual);
|
const userAgentSkills = useToolStore(agentSkillsSelectors.getUserAgentSkills, isEqual);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
useFetchPluginStore,
|
|
||||||
useFetchUserKlavisServers,
|
useFetchUserKlavisServers,
|
||||||
useFetchLobehubSkillConnections,
|
useFetchLobehubSkillConnections,
|
||||||
useFetchUninstalledBuiltinTools,
|
useFetchUninstalledBuiltinTools,
|
||||||
useFetchAgentSkills,
|
useFetchAgentSkills,
|
||||||
] = useToolStore((s) => [
|
] = useToolStore((s) => [
|
||||||
s.useFetchPluginStore,
|
|
||||||
s.useFetchUserKlavisServers,
|
s.useFetchUserKlavisServers,
|
||||||
s.useFetchLobehubSkillConnections,
|
s.useFetchLobehubSkillConnections,
|
||||||
s.useFetchUninstalledBuiltinTools,
|
s.useFetchUninstalledBuiltinTools,
|
||||||
s.useFetchAgentSkills,
|
s.useFetchAgentSkills,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useFetchPluginStore();
|
|
||||||
useFetchInstalledPlugins();
|
useFetchInstalledPlugins();
|
||||||
useFetchUninstalledBuiltinTools(true);
|
useFetchUninstalledBuiltinTools(true);
|
||||||
useFetchAgentSkills(true);
|
useFetchAgentSkills(true);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const';
|
||||||
import { type ILobeAgentRuntimeErrorType } from '@lobechat/model-runtime';
|
import { type ILobeAgentRuntimeErrorType } from '@lobechat/model-runtime';
|
||||||
import { AgentRuntimeErrorType } 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 { ChatErrorType } from '@lobechat/types';
|
||||||
import { type IPluginErrorType } from '@lobehub/chat-plugin-sdk';
|
|
||||||
import { type AlertProps } from '@lobehub/ui';
|
import { type AlertProps } from '@lobehub/ui';
|
||||||
import { Block, Highlighter, Skeleton } from '@lobehub/ui';
|
import { Block, Highlighter, Skeleton } from '@lobehub/ui';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
|
|
@ -52,7 +51,7 @@ const OllamaSetupGuide = dynamic(() => import('./OllamaSetupGuide'), {
|
||||||
|
|
||||||
// Config for the errorMessage display
|
// Config for the errorMessage display
|
||||||
const getErrorAlertConfig = (
|
const getErrorAlertConfig = (
|
||||||
errorType?: IPluginErrorType | ILobeAgentRuntimeErrorType | ErrorType,
|
errorType?: IToolErrorType | ILobeAgentRuntimeErrorType | ErrorType,
|
||||||
): AlertProps | undefined => {
|
): AlertProps | undefined => {
|
||||||
// OpenAIBizError / ZhipuBizError / GoogleBizError / ...
|
// OpenAIBizError / ZhipuBizError / GoogleBizError / ...
|
||||||
if (typeof errorType === 'string' && (errorType.includes('Biz') || errorType.includes('Invalid')))
|
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 { Flexbox } from '@lobehub/ui';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import PluginRender from '@/features/PluginsUI/Render';
|
|
||||||
import { type ChatPluginPayload } from '@/types/index';
|
|
||||||
|
|
||||||
interface CustomRenderProps {
|
interface CustomRenderProps {
|
||||||
content: string;
|
content: string;
|
||||||
/**
|
/**
|
||||||
|
|
@ -19,19 +19,21 @@ interface CustomRenderProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomRender = memo<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 (
|
return (
|
||||||
<Flexbox gap={12} id={toolCallId} width={'100%'}>
|
<Flexbox gap={12} id={toolCallId} width={'100%'}>
|
||||||
<PluginRender
|
<Render
|
||||||
arguments={plugin?.arguments}
|
apiName={plugin?.apiName}
|
||||||
|
args={safeParseJSON(plugin?.arguments)}
|
||||||
content={content}
|
content={content}
|
||||||
identifier={plugin?.identifier}
|
identifier={plugin?.identifier}
|
||||||
loading={false}
|
messageId={messageId!}
|
||||||
messageId={messageId}
|
|
||||||
payload={plugin}
|
|
||||||
pluginState={pluginState}
|
pluginState={pluginState}
|
||||||
toolCallId={toolCallId}
|
toolCallId={toolCallId}
|
||||||
type={plugin?.type}
|
|
||||||
/>
|
/>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
import { type ToolManifest } from '@lobechat/types';
|
||||||
|
|
||||||
import { safeParseJSON } from '@/utils/safeParseJSON';
|
import { safeParseJSON } from '@/utils/safeParseJSON';
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ interface McpServers {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedMcpInput {
|
interface ParsedMcpInput {
|
||||||
manifest?: LobeChatPluginManifest;
|
manifest?: ToolManifest;
|
||||||
mcpServers?: McpServers;
|
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 { Block, Button, Flexbox, Icon, Text } from '@lobehub/ui';
|
||||||
import { type FormInstance } from 'antd';
|
import { type FormInstance } from 'antd';
|
||||||
import { Form as AForm } from 'antd';
|
import { Form as AForm } from 'antd';
|
||||||
|
|
@ -17,7 +17,7 @@ import PluginEmptyState from './EmptyState';
|
||||||
|
|
||||||
const PluginPreview = memo<{ form: FormInstance }>(({ form }) => {
|
const PluginPreview = memo<{ form: FormInstance }>(({ form }) => {
|
||||||
const { t } = useTranslation('plugin');
|
const { t } = useTranslation('plugin');
|
||||||
const manifest: LobeChatPluginManifest = AForm.useWatch(['manifest'], form);
|
const manifest: ToolManifest = AForm.useWatch(['manifest'], form);
|
||||||
const meta = manifest?.meta;
|
const meta = manifest?.meta;
|
||||||
|
|
||||||
if (!manifest)
|
if (!manifest)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { BRANDING_NAME } from '@lobechat/business-const';
|
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 { ActionIcon, Checkbox, Flexbox, FormItem, Input } from '@lobehub/ui';
|
||||||
import { type FormInstance } from 'antd';
|
import { type FormInstance } from 'antd';
|
||||||
import { Form } from 'antd';
|
import { Form } from 'antd';
|
||||||
|
|
@ -39,7 +39,7 @@ const UrlManifestForm = memo<{ form: FormInstance; isEditMode: boolean }>(
|
||||||
({ form, isEditMode }) => {
|
({ form, isEditMode }) => {
|
||||||
const { t } = useTranslation('plugin');
|
const { t } = useTranslation('plugin');
|
||||||
|
|
||||||
const [manifest, setManifest] = useState<LobeChatPluginManifest>();
|
const [manifest, setManifest] = useState<ToolManifest>();
|
||||||
|
|
||||||
const urlKey = ['customParams', 'manifestUrl'];
|
const urlKey = ['customParams', 'manifestUrl'];
|
||||||
const proxyKey = ['customParams', 'useProxy'];
|
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, Markdown } from '@lobehub/ui';
|
||||||
import { Form as AForm } from 'antd';
|
import { Form as AForm } from 'antd';
|
||||||
import { createStaticStyles } from 'antd-style';
|
import { createStaticStyles } from 'antd-style';
|
||||||
|
|
@ -10,7 +10,7 @@ import { pluginSelectors } from '@/store/tool/selectors';
|
||||||
|
|
||||||
import ItemRender from '../../components/JSONSchemaConfig/ItemRender';
|
import ItemRender from '../../components/JSONSchemaConfig/ItemRender';
|
||||||
|
|
||||||
export const transformPluginSettings = (pluginSettings: PluginSchema) => {
|
export const transformPluginSettings = (pluginSettings: ToolManifestSettings) => {
|
||||||
if (!pluginSettings?.properties) return [];
|
if (!pluginSettings?.properties) return [];
|
||||||
|
|
||||||
return Object.entries(pluginSettings.properties).map(([name, i]) => ({
|
return Object.entries(pluginSettings.properties).map(([name, i]) => ({
|
||||||
|
|
@ -28,7 +28,7 @@ export const transformPluginSettings = (pluginSettings: PluginSchema) => {
|
||||||
|
|
||||||
interface PluginSettingsConfigProps {
|
interface PluginSettingsConfigProps {
|
||||||
id: string;
|
id: string;
|
||||||
schema: PluginSchema;
|
schema: ToolManifestSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = createStaticStyles(({ css, cssVar }) => ({
|
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 isEqual from 'fast-deep-equal';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import PluginRender from '@/features/PluginsUI/Render';
|
|
||||||
import { useChatStore } from '@/store/chat';
|
import { useChatStore } from '@/store/chat';
|
||||||
import { chatPortalSelectors, dbMessageSelectors } from '@/store/chat/selectors';
|
import { chatPortalSelectors, dbMessageSelectors } from '@/store/chat/selectors';
|
||||||
import { safeParseJSON } from '@/utils/safeParseJSON';
|
import { safeParseJSON } from '@/utils/safeParseJSON';
|
||||||
|
|
@ -25,19 +24,7 @@ const ToolRender = memo(() => {
|
||||||
|
|
||||||
const Render = BuiltinToolsPortals[plugin.identifier];
|
const Render = BuiltinToolsPortals[plugin.identifier];
|
||||||
|
|
||||||
if (!Render)
|
if (!Render) return null;
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Render
|
<Render
|
||||||
|
|
|
||||||
|
|
@ -116,19 +116,16 @@ const AgentTool = memo<AgentToolProps>(
|
||||||
|
|
||||||
// Fetch plugins
|
// Fetch plugins
|
||||||
const [
|
const [
|
||||||
useFetchPluginStore,
|
|
||||||
useFetchUserKlavisServers,
|
useFetchUserKlavisServers,
|
||||||
useFetchLobehubSkillConnections,
|
useFetchLobehubSkillConnections,
|
||||||
useFetchUninstalledBuiltinTools,
|
useFetchUninstalledBuiltinTools,
|
||||||
useFetchAgentSkills,
|
useFetchAgentSkills,
|
||||||
] = useToolStore((s) => [
|
] = useToolStore((s) => [
|
||||||
s.useFetchPluginStore,
|
|
||||||
s.useFetchUserKlavisServers,
|
s.useFetchUserKlavisServers,
|
||||||
s.useFetchLobehubSkillConnections,
|
s.useFetchLobehubSkillConnections,
|
||||||
s.useFetchUninstalledBuiltinTools,
|
s.useFetchUninstalledBuiltinTools,
|
||||||
s.useFetchAgentSkills,
|
s.useFetchAgentSkills,
|
||||||
]);
|
]);
|
||||||
useFetchPluginStore();
|
|
||||||
useFetchInstalledPlugins();
|
useFetchInstalledPlugins();
|
||||||
useFetchUninstalledBuiltinTools(true);
|
useFetchUninstalledBuiltinTools(true);
|
||||||
useFetchAgentSkills(true);
|
useFetchAgentSkills(true);
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ const Item = memo<DiscoverMcpItem>(({ name, description, icon, identifier }) =>
|
||||||
mcpStoreSelectors.isMCPInstalling(identifier)(s),
|
mcpStoreSelectors.isMCPInstalling(identifier)(s),
|
||||||
s.installMCPPlugin,
|
s.installMCPPlugin,
|
||||||
s.cancelInstallMCPPlugin,
|
s.cancelInstallMCPPlugin,
|
||||||
s.uninstallPlugin,
|
s.uninstallMCPPlugin,
|
||||||
mcpStoreSelectors.getPluginById(identifier)(s),
|
mcpStoreSelectors.getPluginById(identifier)(s),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ const Item = memo<ItemProps>(({ identifier, title, description, avatar }) => {
|
||||||
|
|
||||||
const [customPlugin, uninstallPlugin, updateCustomPlugin, pluginManifest] = useToolStore((s) => [
|
const [customPlugin, uninstallPlugin, updateCustomPlugin, pluginManifest] = useToolStore((s) => [
|
||||||
pluginSelectors.getCustomPluginById(identifier)(s),
|
pluginSelectors.getCustomPluginById(identifier)(s),
|
||||||
s.uninstallPlugin,
|
s.uninstallCustomPlugin,
|
||||||
s.updateCustomPlugin,
|
s.updateCustomPlugin,
|
||||||
pluginSelectors.getToolManifestById(identifier)(s),
|
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 { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { createAgentToolsEngine, createToolsEngine, getEnabledTools } from './index';
|
import { createAgentToolsEngine, createToolsEngine, getEnabledTools } from './index';
|
||||||
|
|
@ -30,7 +30,7 @@ vi.mock('@/store/tool', () => ({
|
||||||
avatar: '🔍',
|
avatar: '🔍',
|
||||||
},
|
},
|
||||||
type: 'builtin',
|
type: 'builtin',
|
||||||
} as unknown as LobeChatPluginManifest,
|
} as unknown as ToolManifest,
|
||||||
type: 'builtin' as const,
|
type: 'builtin' as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -56,7 +56,7 @@ vi.mock('@/store/tool', () => ({
|
||||||
avatar: '🌐',
|
avatar: '🌐',
|
||||||
},
|
},
|
||||||
type: 'builtin',
|
type: 'builtin',
|
||||||
} as unknown as LobeChatPluginManifest,
|
} as unknown as ToolManifest,
|
||||||
type: 'builtin' as const,
|
type: 'builtin' as const,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -64,7 +64,7 @@ vi.mock('@/store/tool', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let mockGetInstalledPluginById: (id: string) => () => any = () => () => undefined;
|
let mockGetInstalledPluginById: (id: string) => () => any = () => () => undefined;
|
||||||
let mockInstalledPluginManifestList: () => LobeChatPluginManifest[] = () => [];
|
let mockInstalledPluginManifestList: () => ToolManifest[] = () => [];
|
||||||
|
|
||||||
vi.mock('@/store/tool/selectors', () => ({
|
vi.mock('@/store/tool/selectors', () => ({
|
||||||
pluginSelectors: {
|
pluginSelectors: {
|
||||||
|
|
@ -270,7 +270,7 @@ describe('toolEngineering', () => {
|
||||||
identifier: 'stdio-mcp-plugin',
|
identifier: 'stdio-mcp-plugin',
|
||||||
meta: { title: 'Stdio MCP', avatar: '🔧' },
|
meta: { title: 'Stdio MCP', avatar: '🔧' },
|
||||||
type: 'default',
|
type: 'default',
|
||||||
} as unknown as LobeChatPluginManifest,
|
} as unknown as ToolManifest,
|
||||||
];
|
];
|
||||||
mockGetInstalledPluginById = (id: string) => () =>
|
mockGetInstalledPluginById = (id: string) => () =>
|
||||||
id === 'stdio-mcp-plugin'
|
id === 'stdio-mcp-plugin'
|
||||||
|
|
@ -305,7 +305,7 @@ describe('toolEngineering', () => {
|
||||||
identifier: 'stdio-mcp-plugin',
|
identifier: 'stdio-mcp-plugin',
|
||||||
meta: { title: 'Stdio MCP', avatar: '🔧' },
|
meta: { title: 'Stdio MCP', avatar: '🔧' },
|
||||||
type: 'default',
|
type: 'default',
|
||||||
} as unknown as LobeChatPluginManifest;
|
} as unknown as ToolManifest;
|
||||||
|
|
||||||
const httpMcpManifest = {
|
const httpMcpManifest = {
|
||||||
api: [
|
api: [
|
||||||
|
|
@ -318,7 +318,7 @@ describe('toolEngineering', () => {
|
||||||
identifier: 'http-mcp-plugin',
|
identifier: 'http-mcp-plugin',
|
||||||
meta: { title: 'HTTP MCP', avatar: '🌐' },
|
meta: { title: 'HTTP MCP', avatar: '🌐' },
|
||||||
type: 'default',
|
type: 'default',
|
||||||
} as unknown as LobeChatPluginManifest;
|
} as unknown as ToolManifest;
|
||||||
|
|
||||||
it('should filter stdio MCP tools in non-desktop environment', () => {
|
it('should filter stdio MCP tools in non-desktop environment', () => {
|
||||||
mockInstalledPluginManifestList = () => [stdioMcpManifest];
|
mockInstalledPluginManifestList = () => [stdioMcpManifest];
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
|
||||||
import { alwaysOnToolIds, defaultToolIds } from '@lobechat/builtin-tools';
|
import { alwaysOnToolIds, defaultToolIds } from '@lobechat/builtin-tools';
|
||||||
import { createEnableChecker, type PluginEnableChecker } from '@lobechat/context-engine';
|
import { createEnableChecker, type PluginEnableChecker } from '@lobechat/context-engine';
|
||||||
import { ToolsEngine } from '@lobechat/context-engine';
|
import { ToolsEngine } from '@lobechat/context-engine';
|
||||||
import { type ChatCompletionTool, type WorkingModel } from '@lobechat/types';
|
import { type ChatCompletionTool, type ToolManifest, type WorkingModel } from '@lobechat/types';
|
||||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
|
||||||
|
|
||||||
import { isToolAvailableInCurrentEnv } from '@/helpers/toolAvailability';
|
import { isToolAvailableInCurrentEnv } from '@/helpers/toolAvailability';
|
||||||
import { getAgentStoreState } from '@/store/agent';
|
import { getAgentStoreState } from '@/store/agent';
|
||||||
|
|
@ -32,7 +31,7 @@ import { isCanUseFC } from '../isCanUseFC';
|
||||||
*/
|
*/
|
||||||
export interface ToolsEngineConfig {
|
export interface ToolsEngineConfig {
|
||||||
/** Additional manifests to include beyond the standard ones */
|
/** 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 */
|
/** Default tool IDs that will always be added to the end of the tools list */
|
||||||
defaultToolIds?: string[];
|
defaultToolIds?: string[];
|
||||||
/** Custom enable checker for plugins */
|
/** Custom enable checker for plugins */
|
||||||
|
|
@ -51,20 +50,16 @@ export const createToolsEngine = (config: ToolsEngineConfig = {}): ToolsEngine =
|
||||||
const pluginManifests = pluginSelectors.installedPluginManifestList(toolStoreState);
|
const pluginManifests = pluginSelectors.installedPluginManifestList(toolStoreState);
|
||||||
|
|
||||||
// Get all builtin tool manifests
|
// Get all builtin tool manifests
|
||||||
const builtinManifests = toolStoreState.builtinTools.map(
|
const builtinManifests = toolStoreState.builtinTools.map((tool) => tool.manifest as ToolManifest);
|
||||||
(tool) => tool.manifest as LobeChatPluginManifest,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get Klavis tool manifests
|
// Get Klavis tool manifests
|
||||||
const klavisTools = klavisStoreSelectors.klavisAsLobeTools(toolStoreState);
|
const klavisTools = klavisStoreSelectors.klavisAsLobeTools(toolStoreState);
|
||||||
const klavisManifests = klavisTools
|
const klavisManifests = klavisTools.map((tool) => tool.manifest as ToolManifest).filter(Boolean);
|
||||||
.map((tool) => tool.manifest as LobeChatPluginManifest)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
// Get LobeHub Skill tool manifests
|
// Get LobeHub Skill tool manifests
|
||||||
const lobehubSkillTools = lobehubSkillStoreSelectors.lobehubSkillAsLobeTools(toolStoreState);
|
const lobehubSkillTools = lobehubSkillStoreSelectors.lobehubSkillAsLobeTools(toolStoreState);
|
||||||
const lobehubSkillManifests = lobehubSkillTools
|
const lobehubSkillManifests = lobehubSkillTools
|
||||||
.map((tool) => tool.manifest as LobeChatPluginManifest)
|
.map((tool) => tool.manifest as ToolManifest)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
// Combine all manifests
|
// Combine all manifests
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { KLAVIS_SERVER_TYPES } from '@lobechat/const';
|
||||||
import { ToolNameResolver } from '@lobechat/context-engine';
|
import { ToolNameResolver } from '@lobechat/context-engine';
|
||||||
import { type API } from '@lobechat/prompts';
|
import { type API } from '@lobechat/prompts';
|
||||||
import { apiPrompt, toolPrompt } 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 { type IEditor } from '@lobehub/editor';
|
||||||
import { INSERT_MENTION_COMMAND } from '@lobehub/editor';
|
import { INSERT_MENTION_COMMAND } from '@lobehub/editor';
|
||||||
import { Icon, Image } from '@lobehub/ui';
|
import { Icon, Image } from '@lobehub/ui';
|
||||||
|
|
@ -41,7 +41,7 @@ const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label
|
||||||
|
|
||||||
const toolNameResolver = new ToolNameResolver();
|
const toolNameResolver = new ToolNameResolver();
|
||||||
|
|
||||||
const buildApiList = (identifier: string, manifest?: LobeChatPluginManifest): API[] => {
|
const buildApiList = (identifier: string, manifest?: ToolManifest): API[] => {
|
||||||
if (!manifest?.api) return [];
|
if (!manifest?.api) return [];
|
||||||
|
|
||||||
return manifest.api.map((api) => ({
|
return manifest.api.map((api) => ({
|
||||||
|
|
@ -58,7 +58,7 @@ const hydrateSystemRole = (systemRole?: string) => {
|
||||||
|
|
||||||
const resolveInstructions = (
|
const resolveInstructions = (
|
||||||
metadata: MentionMetadata,
|
metadata: MentionMetadata,
|
||||||
manifest?: LobeChatPluginManifest,
|
manifest?: ToolManifest,
|
||||||
fallbackDesc?: string,
|
fallbackDesc?: string,
|
||||||
) => {
|
) => {
|
||||||
if (metadata.instructions) return metadata.instructions;
|
if (metadata.instructions) return metadata.instructions;
|
||||||
|
|
@ -70,7 +70,7 @@ const resolveInstructions = (
|
||||||
|
|
||||||
const resolveApiName = (
|
const resolveApiName = (
|
||||||
metadata: MentionMetadata,
|
metadata: MentionMetadata,
|
||||||
manifest: LobeChatPluginManifest | undefined,
|
manifest: ToolManifest | undefined,
|
||||||
pluginId?: string,
|
pluginId?: string,
|
||||||
fallbackLabel?: string,
|
fallbackLabel?: string,
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -93,7 +93,7 @@ const resolveApiName = (
|
||||||
|
|
||||||
const resolveApiDescription = (
|
const resolveApiDescription = (
|
||||||
metadata: MentionMetadata,
|
metadata: MentionMetadata,
|
||||||
manifest: LobeChatPluginManifest | undefined,
|
manifest: ToolManifest | undefined,
|
||||||
pluginId: string | undefined,
|
pluginId: string | undefined,
|
||||||
apiName?: string,
|
apiName?: string,
|
||||||
) => {
|
) => {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { KLAVIS_SERVER_TYPES } from '@lobechat/const';
|
||||||
import { ToolNameResolver } from '@lobechat/context-engine';
|
import { ToolNameResolver } from '@lobechat/context-engine';
|
||||||
import { type API } from '@lobechat/prompts';
|
import { type API } from '@lobechat/prompts';
|
||||||
import { apiPrompt, toolPrompt } 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 { type IEditor } from '@lobehub/editor';
|
||||||
import { INSERT_MENTION_COMMAND } from '@lobehub/editor';
|
import { INSERT_MENTION_COMMAND } from '@lobehub/editor';
|
||||||
import { Icon, Image } from '@lobehub/ui';
|
import { Icon, Image } from '@lobehub/ui';
|
||||||
|
|
@ -41,7 +41,7 @@ const KlavisIcon = memo<Pick<KlavisServerType, 'icon' | 'label'>>(({ icon, label
|
||||||
|
|
||||||
const toolNameResolver = new ToolNameResolver();
|
const toolNameResolver = new ToolNameResolver();
|
||||||
|
|
||||||
const buildApiList = (identifier: string, manifest?: LobeChatPluginManifest): API[] => {
|
const buildApiList = (identifier: string, manifest?: ToolManifest): API[] => {
|
||||||
if (!manifest?.api) return [];
|
if (!manifest?.api) return [];
|
||||||
|
|
||||||
return manifest.api.map((api) => ({
|
return manifest.api.map((api) => ({
|
||||||
|
|
@ -58,7 +58,7 @@ const hydrateSystemRole = (systemRole?: string) => {
|
||||||
|
|
||||||
const resolveInstructions = (
|
const resolveInstructions = (
|
||||||
metadata: MentionMetadata,
|
metadata: MentionMetadata,
|
||||||
manifest?: LobeChatPluginManifest,
|
manifest?: ToolManifest,
|
||||||
fallbackDesc?: string,
|
fallbackDesc?: string,
|
||||||
) => {
|
) => {
|
||||||
if (metadata.instructions) return metadata.instructions;
|
if (metadata.instructions) return metadata.instructions;
|
||||||
|
|
@ -70,7 +70,7 @@ const resolveInstructions = (
|
||||||
|
|
||||||
const resolveApiName = (
|
const resolveApiName = (
|
||||||
metadata: MentionMetadata,
|
metadata: MentionMetadata,
|
||||||
manifest: LobeChatPluginManifest | undefined,
|
manifest: ToolManifest | undefined,
|
||||||
pluginId?: string,
|
pluginId?: string,
|
||||||
fallbackLabel?: string,
|
fallbackLabel?: string,
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -93,7 +93,7 @@ const resolveApiName = (
|
||||||
|
|
||||||
const resolveApiDescription = (
|
const resolveApiDescription = (
|
||||||
metadata: MentionMetadata,
|
metadata: MentionMetadata,
|
||||||
manifest: LobeChatPluginManifest | undefined,
|
manifest: ToolManifest | undefined,
|
||||||
pluginId: string | undefined,
|
pluginId: string | undefined,
|
||||||
apiName?: string,
|
apiName?: string,
|
||||||
) => {
|
) => {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { useAgentStore } from '@/store/agent';
|
||||||
import { agentSelectors } from '@/store/agent/selectors';
|
import { agentSelectors } from '@/store/agent/selectors';
|
||||||
import { useServerConfigStore } from '@/store/serverConfig';
|
import { useServerConfigStore } from '@/store/serverConfig';
|
||||||
import { pluginHelpers, useToolStore } from '@/store/tool';
|
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 { type LobeToolType } from '@/types/tool/tool';
|
||||||
|
|
||||||
import EditCustomPlugin from './EditCustomPlugin';
|
import EditCustomPlugin from './EditCustomPlugin';
|
||||||
|
|
@ -23,15 +23,12 @@ interface ActionsProps {
|
||||||
|
|
||||||
const Actions = memo<ActionsProps>(({ identifier, type, isMCP }) => {
|
const Actions = memo<ActionsProps>(({ identifier, type, isMCP }) => {
|
||||||
const mobile = useServerConfigStore((s) => s.isMobile);
|
const mobile = useServerConfigStore((s) => s.isMobile);
|
||||||
const [installed, installing, installPlugin, unInstallPlugin, installMCPPlugin] = useToolStore(
|
const [installed, installing, unInstallPlugin, installMCPPlugin] = useToolStore((s) => [
|
||||||
(s) => [
|
pluginSelectors.isPluginInstalled(identifier)(s),
|
||||||
pluginSelectors.isPluginInstalled(identifier)(s),
|
mcpStoreSelectors.isPluginInstallLoading(identifier)(s),
|
||||||
pluginStoreSelectors.isPluginInstallLoading(identifier)(s),
|
s.uninstallCustomPlugin,
|
||||||
s.installPlugin,
|
s.installMCPPlugin,
|
||||||
s.uninstallPlugin,
|
]);
|
||||||
s.installMCPPlugin,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isCustomPlugin = type === 'customPlugin';
|
const isCustomPlugin = type === 'customPlugin';
|
||||||
const { t } = useTranslation('plugin');
|
const { t } = useTranslation('plugin');
|
||||||
|
|
@ -116,9 +113,6 @@ const Actions = memo<ActionsProps>(({ identifier, type, isMCP }) => {
|
||||||
if (isMCP) {
|
if (isMCP) {
|
||||||
await installMCPPlugin(identifier);
|
await installMCPPlugin(identifier);
|
||||||
await togglePlugin(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 { z } from 'zod';
|
||||||
|
|
||||||
import { PluginModel } from '@/database/models/plugin';
|
import { PluginModel } from '@/database/models/plugin';
|
||||||
|
|
@ -49,7 +49,7 @@ export const klavisRouter = router({
|
||||||
const tools = toolsResponse.tools || [];
|
const tools = toolsResponse.tools || [];
|
||||||
|
|
||||||
// Save to database using the provided identifier (format: lowercase, spaces replaced with hyphens)
|
// Save to database using the provided identifier (format: lowercase, spaces replaced with hyphens)
|
||||||
const manifest: LobeChatPluginManifest = {
|
const manifest: ToolManifest = {
|
||||||
api: tools.map((tool: any) => ({
|
api: tools.map((tool: any) => ({
|
||||||
description: tool.description || '',
|
description: tool.description || '',
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
|
|
@ -232,7 +232,7 @@ export const klavisRouter = router({
|
||||||
const existingPlugin = await ctx.pluginModel.findById(identifier);
|
const existingPlugin = await ctx.pluginModel.findById(identifier);
|
||||||
|
|
||||||
// Build manifest containing all tools
|
// Build manifest containing all tools
|
||||||
const manifest: LobeChatPluginManifest = {
|
const manifest: ToolManifest = {
|
||||||
api: tools.map((tool) => ({
|
api: tools.map((tool) => ({
|
||||||
description: tool.description || '',
|
description: tool.description || '',
|
||||||
name: tool.name,
|
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,
|
// Mock factory and redis dependencies to break env import chains,
|
||||||
// so the barrel can be imported with real AgentRuntimeCoordinator + InMemory backends
|
// so the barrel can be imported with real AgentRuntimeCoordinator + InMemory backends
|
||||||
vi.mock('@/server/modules/AgentRuntime/factory', async () => {
|
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 { createRuntimeExecutors } from '@/server/modules/AgentRuntime/RuntimeExecutors';
|
||||||
import { type IStreamEventManager } from '@/server/modules/AgentRuntime/types';
|
import { type IStreamEventManager } from '@/server/modules/AgentRuntime/types';
|
||||||
import { mcpService } from '@/server/services/mcp';
|
import { mcpService } from '@/server/services/mcp';
|
||||||
import { PluginGatewayService } from '@/server/services/pluginGateway';
|
|
||||||
import { QueueService } from '@/server/services/queue';
|
import { QueueService } from '@/server/services/queue';
|
||||||
import { LocalQueueServiceImpl } from '@/server/services/queue/impls';
|
import { LocalQueueServiceImpl } from '@/server/services/queue/impls';
|
||||||
import { ToolExecutionService } from '@/server/services/toolExecution';
|
import { ToolExecutionService } from '@/server/services/toolExecution';
|
||||||
|
|
@ -160,13 +159,11 @@ export class AgentRuntimeService {
|
||||||
this.messageModel = new MessageModel(db, this.userId);
|
this.messageModel = new MessageModel(db, this.userId);
|
||||||
|
|
||||||
// Initialize ToolExecutionService with dependencies
|
// Initialize ToolExecutionService with dependencies
|
||||||
const pluginGatewayService = new PluginGatewayService();
|
|
||||||
const builtinToolsExecutor = new BuiltinToolsExecutor(db, userId);
|
const builtinToolsExecutor = new BuiltinToolsExecutor(db, userId);
|
||||||
|
|
||||||
this.toolExecutionService = new ToolExecutionService({
|
this.toolExecutionService = new ToolExecutionService({
|
||||||
builtinToolsExecutor,
|
builtinToolsExecutor,
|
||||||
mcpService,
|
mcpService,
|
||||||
pluginGatewayService,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup local execution callback for LocalQueueServiceImpl
|
// 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
|
// Mock MCP service
|
||||||
vi.mock('@/server/services/mcp', () => ({
|
vi.mock('@/server/services/mcp', () => ({
|
||||||
mcpService: {
|
mcpService: {
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,6 @@ vi.mock('@/server/modules/AgentRuntime/RuntimeExecutors', () => ({
|
||||||
createRuntimeExecutors: vi.fn(() => ({})),
|
createRuntimeExecutors: vi.fn(() => ({})),
|
||||||
}));
|
}));
|
||||||
vi.mock('@/server/services/mcp', () => ({ mcpService: {} }));
|
vi.mock('@/server/services/mcp', () => ({ mcpService: {} }));
|
||||||
vi.mock('@/server/services/pluginGateway', () => ({
|
|
||||||
PluginGatewayService: vi.fn().mockImplementation(() => ({})),
|
|
||||||
}));
|
|
||||||
vi.mock('@/server/services/queue', () => ({
|
vi.mock('@/server/services/queue', () => ({
|
||||||
QueueService: vi.fn().mockImplementation(() => ({
|
QueueService: vi.fn().mockImplementation(() => ({
|
||||||
getImpl: vi.fn(() => ({})),
|
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
|
// Mock MCP service
|
||||||
vi.mock('@/server/services/mcp', () => ({
|
vi.mock('@/server/services/mcp', () => ({
|
||||||
mcpService: {
|
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
|
// Mock MCP service
|
||||||
vi.mock('@/server/services/mcp', () => ({
|
vi.mock('@/server/services/mcp', () => ({
|
||||||
mcpService: {
|
mcpService: {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { type CheckMcpInstallResult, type CustomPluginMetadata } from '@lobechat/types';
|
|
||||||
import { safeParseJSON } from '@lobechat/utils';
|
|
||||||
import {
|
import {
|
||||||
|
type CheckMcpInstallResult,
|
||||||
|
type CustomPluginMetadata,
|
||||||
type LobeChatPluginApi,
|
type LobeChatPluginApi,
|
||||||
type LobeChatPluginManifest,
|
type ToolManifest,
|
||||||
type PluginSchema,
|
type ToolManifestSettings,
|
||||||
} from '@lobehub/chat-plugin-sdk';
|
} from '@lobechat/types';
|
||||||
|
import { safeParseJSON } from '@lobechat/utils';
|
||||||
import { type DeploymentOption } from '@lobehub/market-sdk';
|
import { type DeploymentOption } from '@lobehub/market-sdk';
|
||||||
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
import { McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
|
|
@ -111,7 +112,7 @@ export class MCPService {
|
||||||
// Assuming identifier is the unique name/id
|
// Assuming identifier is the unique name/id
|
||||||
description: item.description,
|
description: item.description,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
parameters: item.inputSchema as PluginSchema,
|
parameters: item.inputSchema as ToolManifestSettings,
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Only retry for NoValidSessionId errors
|
// Only retry for NoValidSessionId errors
|
||||||
|
|
@ -355,7 +356,7 @@ export class MCPService {
|
||||||
type: 'none' | 'bearer' | 'oauth2';
|
type: 'none' | 'bearer' | 'oauth2';
|
||||||
},
|
},
|
||||||
headers?: Record<string, string>,
|
headers?: Record<string, string>,
|
||||||
): Promise<LobeChatPluginManifest> {
|
): Promise<ToolManifest> {
|
||||||
const mcpParams = { name: identifier, type: 'http' as const, url };
|
const mcpParams = { name: identifier, type: 'http' as const, url };
|
||||||
|
|
||||||
// Add authentication info to parameters if available
|
// Add authentication info to parameters if available
|
||||||
|
|
@ -390,7 +391,7 @@ export class MCPService {
|
||||||
async getStdioMcpServerManifest(
|
async getStdioMcpServerManifest(
|
||||||
params: Omit<StdioMCPParams, 'type'>,
|
params: Omit<StdioMCPParams, 'type'>,
|
||||||
metadata?: CustomPluginMetadata,
|
metadata?: CustomPluginMetadata,
|
||||||
): Promise<LobeChatPluginManifest> {
|
): Promise<ToolManifest> {
|
||||||
const mcpParams = {
|
const mcpParams = {
|
||||||
args: params.args,
|
args: params.args,
|
||||||
command: params.command,
|
command: params.command,
|
||||||
|
|
@ -424,7 +425,7 @@ export class MCPService {
|
||||||
mcpParams,
|
mcpParams,
|
||||||
// TODO: temporary
|
// TODO: temporary
|
||||||
type: 'mcp' as any,
|
type: 'mcp' as any,
|
||||||
} as LobeChatPluginManifest;
|
} as ToolManifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -490,7 +491,7 @@ export class MCPService {
|
||||||
// Assuming identifier is the unique name/id
|
// Assuming identifier is the unique name/id
|
||||||
description: item.description,
|
description: item.description,
|
||||||
name: item.name,
|
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 { DiscoverService } from '../discover';
|
||||||
import { type MCPService } from '../mcp';
|
import { type MCPService } from '../mcp';
|
||||||
import { type PluginGatewayService } from '../pluginGateway';
|
|
||||||
import { type BuiltinToolsExecutor } from './builtin';
|
import { type BuiltinToolsExecutor } from './builtin';
|
||||||
import { classifyToolError } from './errorClassification';
|
import { classifyToolError } from './errorClassification';
|
||||||
import {
|
import {
|
||||||
|
|
@ -25,7 +24,6 @@ const log = debug('lobe-server:tool-execution-service');
|
||||||
interface ToolExecutionServiceDeps {
|
interface ToolExecutionServiceDeps {
|
||||||
builtinToolsExecutor: BuiltinToolsExecutor;
|
builtinToolsExecutor: BuiltinToolsExecutor;
|
||||||
mcpService: MCPService;
|
mcpService: MCPService;
|
||||||
pluginGatewayService: PluginGatewayService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeExecutionError = (error: unknown, fallbackMessage: string) => {
|
const normalizeExecutionError = (error: unknown, fallbackMessage: string) => {
|
||||||
|
|
@ -62,16 +60,10 @@ const normalizeExecutionError = (error: unknown, fallbackMessage: string) => {
|
||||||
export class ToolExecutionService {
|
export class ToolExecutionService {
|
||||||
private builtinToolsExecutor: BuiltinToolsExecutor;
|
private builtinToolsExecutor: BuiltinToolsExecutor;
|
||||||
private mcpService: MCPService;
|
private mcpService: MCPService;
|
||||||
private pluginGatewayService: PluginGatewayService;
|
|
||||||
|
|
||||||
constructor({
|
constructor({ mcpService, builtinToolsExecutor }: ToolExecutionServiceDeps) {
|
||||||
mcpService,
|
|
||||||
pluginGatewayService,
|
|
||||||
builtinToolsExecutor,
|
|
||||||
}: ToolExecutionServiceDeps) {
|
|
||||||
this.builtinToolsExecutor = builtinToolsExecutor;
|
this.builtinToolsExecutor = builtinToolsExecutor;
|
||||||
this.mcpService = mcpService;
|
this.mcpService = mcpService;
|
||||||
this.pluginGatewayService = pluginGatewayService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeTool(
|
async executeTool(
|
||||||
|
|
@ -87,19 +79,14 @@ export class ToolExecutionService {
|
||||||
const typeStr = type as string;
|
const typeStr = type as string;
|
||||||
let data: ToolExecutionResult;
|
let data: ToolExecutionResult;
|
||||||
switch (typeStr) {
|
switch (typeStr) {
|
||||||
case 'builtin': {
|
|
||||||
data = await this.builtinToolsExecutor.execute(payload, context);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'mcp': {
|
case 'mcp': {
|
||||||
data = await this.executeMCPTool(payload, context);
|
data = await this.executeMCPTool(payload, context);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'builtin':
|
||||||
default: {
|
default: {
|
||||||
data = await this.pluginGatewayService.execute(payload, context);
|
data = await this.builtinToolsExecutor.execute(payload, context);
|
||||||
|
|
||||||
break;
|
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', () => {
|
it('should return correct basePath URLs', () => {
|
||||||
expect(API_ENDPOINTS.oauth).toBe('/api/auth');
|
expect(API_ENDPOINTS.oauth).toBe('/api/auth');
|
||||||
expect(API_ENDPOINTS.proxy).toBe('/webapi/proxy');
|
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.trace).toBe('/webapi/trace');
|
||||||
expect(API_ENDPOINTS.stt).toBe('/webapi/stt/openai');
|
expect(API_ENDPOINTS.stt).toBe('/webapi/stt/openai');
|
||||||
expect(API_ENDPOINTS.edge).toBe('/webapi/tts/edge');
|
expect(API_ENDPOINTS.edge).toBe('/webapi/tts/edge');
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { toolService } from '../tool';
|
import { toolService } from '../tool';
|
||||||
import OpenAIPlugin from './openai/plugin.json';
|
|
||||||
|
|
||||||
// Mocking modules and functions
|
// Mocking modules and functions
|
||||||
|
|
||||||
|
|
@ -31,7 +30,6 @@ describe('ToolService', () => {
|
||||||
const manifestUrl = 'http://fake-url.com/manifest.json';
|
const manifestUrl = 'http://fake-url.com/manifest.json';
|
||||||
|
|
||||||
const fakeManifest = {
|
const fakeManifest = {
|
||||||
$schema: '../node_modules/@lobehub/chat-plugin-sdk/schema.json',
|
|
||||||
api: [
|
api: [
|
||||||
{
|
{
|
||||||
url: 'https://realtime-weather.chat-plugin.lobehub.com/api/v1',
|
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'),
|
proxy: withElectronProtocolIfElectron('/webapi/proxy'),
|
||||||
|
|
||||||
// plugins
|
|
||||||
gateway: withElectronProtocolIfElectron('/webapi/plugin/gateway'),
|
|
||||||
|
|
||||||
// trace
|
// trace
|
||||||
trace: withElectronProtocolIfElectron('/webapi/trace'),
|
trace: withElectronProtocolIfElectron('/webapi/trace'),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1536,23 +1536,6 @@ describe('ChatService', () => {
|
||||||
// Add more test cases to cover different scenarios and edge cases
|
// 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', () => {
|
describe('fetchPresetTaskResult', () => {
|
||||||
it('should handle successful chat completion response', async () => {
|
it('should handle successful chat completion response', async () => {
|
||||||
// Mock getChatCompletion to simulate successful completion
|
// 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 { KLAVIS_SERVER_TYPES, LOBEHUB_SKILL_PROVIDERS } from '@lobechat/const';
|
||||||
import { type OfficialToolItem } from '@lobechat/context-engine';
|
import { type OfficialToolItem } from '@lobechat/context-engine';
|
||||||
import { type FetchSSEOptions } from '@lobechat/fetch-sse';
|
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 { type ChatCompletionErrorPayload } from '@lobechat/model-runtime';
|
||||||
import { AgentRuntimeError } from '@lobechat/model-runtime';
|
import { AgentRuntimeError } from '@lobechat/model-runtime';
|
||||||
import {
|
import {
|
||||||
|
|
@ -12,8 +12,6 @@ import {
|
||||||
type UIChatMessage,
|
type UIChatMessage,
|
||||||
} from '@lobechat/types';
|
} from '@lobechat/types';
|
||||||
import { ChatErrorType, TraceTagMap } 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 { merge } from 'es-toolkit/compat';
|
||||||
import { ModelProvider } from 'model-bank';
|
import { ModelProvider } from 'model-bank';
|
||||||
|
|
||||||
|
|
@ -32,7 +30,6 @@ import {
|
||||||
builtinToolSelectors,
|
builtinToolSelectors,
|
||||||
klavisStoreSelectors,
|
klavisStoreSelectors,
|
||||||
lobehubSkillStoreSelectors,
|
lobehubSkillStoreSelectors,
|
||||||
pluginSelectors,
|
|
||||||
} from '@/store/tool/selectors';
|
} from '@/store/tool/selectors';
|
||||||
import { getUserStoreState, useUserStore } from '@/store/user';
|
import { getUserStoreState, useUserStore } from '@/store/user';
|
||||||
import {
|
import {
|
||||||
|
|
@ -42,7 +39,7 @@ import {
|
||||||
} from '@/store/user/selectors';
|
} from '@/store/user/selectors';
|
||||||
import { type ChatStreamPayload, type OpenAIChatMessage } from '@/types/openai/chat';
|
import { type ChatStreamPayload, type OpenAIChatMessage } from '@/types/openai/chat';
|
||||||
import { createErrorResponse } from '@/utils/errorResponse';
|
import { createErrorResponse } from '@/utils/errorResponse';
|
||||||
import { createTraceHeader, getTraceId } from '@/utils/trace';
|
import { createTraceHeader } from '@/utils/trace';
|
||||||
|
|
||||||
import { createHeaderWithAuth } from '../_auth';
|
import { createHeaderWithAuth } from '../_auth';
|
||||||
import { API_ENDPOINTS } from '../_url';
|
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 ({
|
fetchPresetTaskResult = async ({
|
||||||
params,
|
params,
|
||||||
onMessageHandle,
|
onMessageHandle,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { type ChatToolPayload } from '@lobechat/types';
|
import { type ChatToolPayload, type ToolManifest } from '@lobechat/types';
|
||||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
|
||||||
import superjson from 'superjson';
|
import superjson from 'superjson';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
|
@ -438,7 +437,7 @@ describe('MCPService', () => {
|
||||||
describe('getStreamableMcpServerManifest', () => {
|
describe('getStreamableMcpServerManifest', () => {
|
||||||
it('should use toolsClient for streamable URLs when not on desktop', async () => {
|
it('should use toolsClient for streamable URLs when not on desktop', async () => {
|
||||||
const { toolsClient } = await import('@/libs/trpc/client');
|
const { toolsClient } = await import('@/libs/trpc/client');
|
||||||
const mockManifest: LobeChatPluginManifest = {
|
const mockManifest: ToolManifest = {
|
||||||
identifier: 'streamable-server',
|
identifier: 'streamable-server',
|
||||||
version: '1',
|
version: '1',
|
||||||
meta: { title: 'Streamable MCP Server', avatar: '🌐' },
|
meta: { title: 'Streamable MCP Server', avatar: '🌐' },
|
||||||
|
|
@ -470,7 +469,7 @@ describe('MCPService', () => {
|
||||||
|
|
||||||
it('should use toolsClient for remote URLs', async () => {
|
it('should use toolsClient for remote URLs', async () => {
|
||||||
const { toolsClient } = await import('@/libs/trpc/client');
|
const { toolsClient } = await import('@/libs/trpc/client');
|
||||||
const mockManifest: LobeChatPluginManifest = {
|
const mockManifest: ToolManifest = {
|
||||||
identifier: 'remote-server',
|
identifier: 'remote-server',
|
||||||
version: '1',
|
version: '1',
|
||||||
meta: { title: 'Remote MCP Server', avatar: '🌍' },
|
meta: { title: 'Remote MCP Server', avatar: '🌍' },
|
||||||
|
|
@ -507,7 +506,7 @@ describe('MCPService', () => {
|
||||||
|
|
||||||
it('should handle different URL formats correctly', async () => {
|
it('should handle different URL formats correctly', async () => {
|
||||||
const { toolsClient } = await import('@/libs/trpc/client');
|
const { toolsClient } = await import('@/libs/trpc/client');
|
||||||
const mockManifest: LobeChatPluginManifest = {
|
const mockManifest: ToolManifest = {
|
||||||
identifier: 'server',
|
identifier: 'server',
|
||||||
version: '1',
|
version: '1',
|
||||||
meta: { title: 'URL Test Server', avatar: '🔗' },
|
meta: { title: 'URL Test Server', avatar: '🔗' },
|
||||||
|
|
@ -537,7 +536,7 @@ describe('MCPService', () => {
|
||||||
|
|
||||||
it('should handle OAuth2 authentication', async () => {
|
it('should handle OAuth2 authentication', async () => {
|
||||||
const { toolsClient } = await import('@/libs/trpc/client');
|
const { toolsClient } = await import('@/libs/trpc/client');
|
||||||
const mockManifest: LobeChatPluginManifest = {
|
const mockManifest: ToolManifest = {
|
||||||
identifier: 'oauth-server',
|
identifier: 'oauth-server',
|
||||||
version: '1',
|
version: '1',
|
||||||
meta: { title: 'OAuth Server', avatar: '🔐' },
|
meta: { title: 'OAuth Server', avatar: '🔐' },
|
||||||
|
|
@ -579,7 +578,7 @@ describe('MCPService', () => {
|
||||||
|
|
||||||
describe('getStdioMcpServerManifest', () => {
|
describe('getStdioMcpServerManifest', () => {
|
||||||
it('should call ipc mcp.getStdioMcpServerManifest with stdio parameters', async () => {
|
it('should call ipc mcp.getStdioMcpServerManifest with stdio parameters', async () => {
|
||||||
const mockManifest: LobeChatPluginManifest = {
|
const mockManifest: ToolManifest = {
|
||||||
identifier: 'stdio-server',
|
identifier: 'stdio-server',
|
||||||
version: '1',
|
version: '1',
|
||||||
meta: { title: 'Stdio Server', avatar: '📦' },
|
meta: { title: 'Stdio Server', avatar: '📦' },
|
||||||
|
|
@ -616,7 +615,7 @@ describe('MCPService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle abort signal for stdio manifest', async () => {
|
it('should handle abort signal for stdio manifest', async () => {
|
||||||
const mockManifest: LobeChatPluginManifest = {
|
const mockManifest: ToolManifest = {
|
||||||
identifier: 'python-server',
|
identifier: 'python-server',
|
||||||
version: '1',
|
version: '1',
|
||||||
meta: { title: 'Stdio Server', avatar: '🐍' },
|
meta: { title: 'Stdio Server', avatar: '🐍' },
|
||||||
|
|
@ -650,7 +649,7 @@ describe('MCPService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work without optional parameters', async () => {
|
it('should work without optional parameters', async () => {
|
||||||
const mockManifest: LobeChatPluginManifest = {
|
const mockManifest: ToolManifest = {
|
||||||
identifier: 'npm-server',
|
identifier: 'npm-server',
|
||||||
version: '1',
|
version: '1',
|
||||||
meta: { title: 'Simple Server', avatar: '📦' },
|
meta: { title: 'Simple Server', avatar: '📦' },
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { type LobeTool } from '@lobechat/types';
|
import { type LobeTool, type ToolManifest } from '@lobechat/types';
|
||||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
|
||||||
|
|
||||||
import { lambdaClient } from '@/libs/trpc/client';
|
import { lambdaClient } from '@/libs/trpc/client';
|
||||||
import { type LobeToolCustomPlugin } from '@/types/tool/plugin';
|
import { type LobeToolCustomPlugin } from '@/types/tool/plugin';
|
||||||
|
|
@ -7,7 +6,7 @@ import { type LobeToolCustomPlugin } from '@/types/tool/plugin';
|
||||||
export interface InstallPluginParams {
|
export interface InstallPluginParams {
|
||||||
customParams?: Record<string, any>;
|
customParams?: Record<string, any>;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
manifest: LobeChatPluginManifest;
|
manifest: ToolManifest;
|
||||||
settings?: Record<string, any>;
|
settings?: Record<string, any>;
|
||||||
type: 'plugin' | 'customPlugin';
|
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 });
|
await lambdaClient.plugin.updatePlugin.mutate({ id, manifest });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { lambdaClient } from '@/libs/trpc/client';
|
import { lambdaClient } from '@/libs/trpc/client';
|
||||||
import { globalHelpers } from '@/store/global/helpers';
|
import { globalHelpers } from '@/store/global/helpers';
|
||||||
import { type PluginQueryParams } from '@/types/discover';
|
import { type PluginQueryParams } from '@/types/discover';
|
||||||
import { convertOpenAIManifestToLobeManifest, getToolManifest } from '@/utils/toolManifest';
|
import { getToolManifest } from '@/utils/toolManifest';
|
||||||
|
|
||||||
class ToolService {
|
class ToolService {
|
||||||
getOldPluginList = async (params: PluginQueryParams): Promise<any> => {
|
getOldPluginList = async (params: PluginQueryParams): Promise<any> => {
|
||||||
|
|
@ -16,7 +16,6 @@ class ToolService {
|
||||||
};
|
};
|
||||||
|
|
||||||
getToolManifest = getToolManifest;
|
getToolManifest = getToolManifest;
|
||||||
convertOpenAIManifestToLobeManifest = convertOpenAIManifestToLobeManifest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const toolService = new ToolService();
|
export const toolService = new ToolService();
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,12 @@ import i18n from 'i18next';
|
||||||
import { type Mock } from 'vitest';
|
import { type Mock } from 'vitest';
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { chatService } from '@/services/chat';
|
|
||||||
import { messageService } from '@/services/message';
|
import { messageService } from '@/services/message';
|
||||||
import { chatSelectors } from '@/store/chat/selectors';
|
import { chatSelectors } from '@/store/chat/selectors';
|
||||||
import { useChatStore } from '@/store/chat/store';
|
import { useChatStore } from '@/store/chat/store';
|
||||||
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
||||||
import { useToolStore } from '@/store/tool';
|
import { useToolStore } from '@/store/tool';
|
||||||
|
|
||||||
const invokeStandaloneTypePlugin = useChatStore.getState().invokeStandaloneTypePlugin;
|
|
||||||
|
|
||||||
vi.mock('zustand/traditional');
|
vi.mock('zustand/traditional');
|
||||||
|
|
||||||
// Mock messageService
|
// 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', () => {
|
describe('updatePluginState', () => {
|
||||||
it('should update the plugin state for a message', async () => {
|
it('should update the plugin state for a message', async () => {
|
||||||
const messageId = 'message-id';
|
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', () => {
|
describe('reInvokeToolMessage', () => {
|
||||||
it('should re-invoke a tool message', async () => {
|
it('should re-invoke a tool message', async () => {
|
||||||
const messageId = 'message-id';
|
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', () => {
|
describe('internal_transformToolCalls', () => {
|
||||||
it('should transform tool calls correctly', () => {
|
it('should transform tool calls correctly', () => {
|
||||||
const toolCalls: MessageToolCall[] = [
|
const toolCalls: MessageToolCall[] = [
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { builtinTools } from '@lobechat/builtin-tools';
|
import { builtinTools } from '@lobechat/builtin-tools';
|
||||||
import { ToolArgumentsRepairer, ToolNameResolver } from '@lobechat/context-engine';
|
import { ToolArgumentsRepairer, ToolNameResolver } from '@lobechat/context-engine';
|
||||||
import { type ChatToolPayload, type MessageToolCall } from '@lobechat/types';
|
import { type ChatToolPayload, type MessageToolCall, type ToolManifest } from '@lobechat/types';
|
||||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
|
||||||
|
|
||||||
import { type ChatStore } from '@/store/chat/store';
|
import { type ChatStore } from '@/store/chat/store';
|
||||||
import { useToolStore } from '@/store/tool';
|
import { useToolStore } from '@/store/tool';
|
||||||
|
|
@ -33,7 +32,7 @@ export class PluginInternalsActionImpl {
|
||||||
|
|
||||||
// Build manifests map from tool store
|
// Build manifests map from tool store
|
||||||
const toolStoreState = useToolStore.getState();
|
const toolStoreState = useToolStore.getState();
|
||||||
const manifests: Record<string, LobeChatPluginManifest> = {};
|
const manifests: Record<string, ToolManifest> = {};
|
||||||
|
|
||||||
// Track source for each identifier
|
// Track source for each identifier
|
||||||
const sourceMap: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill'> = {};
|
const sourceMap: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill'> = {};
|
||||||
|
|
@ -42,7 +41,7 @@ export class PluginInternalsActionImpl {
|
||||||
const installedPlugins = pluginSelectors.installedPlugins(toolStoreState);
|
const installedPlugins = pluginSelectors.installedPlugins(toolStoreState);
|
||||||
for (const plugin of installedPlugins) {
|
for (const plugin of installedPlugins) {
|
||||||
if (plugin.manifest) {
|
if (plugin.manifest) {
|
||||||
manifests[plugin.identifier] = plugin.manifest as LobeChatPluginManifest;
|
manifests[plugin.identifier] = plugin.manifest as ToolManifest;
|
||||||
// Check if this plugin has MCP params
|
// Check if this plugin has MCP params
|
||||||
sourceMap[plugin.identifier] = plugin.customParams?.mcp ? 'mcp' : 'plugin';
|
sourceMap[plugin.identifier] = plugin.customParams?.mcp ? 'mcp' : 'plugin';
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +50,7 @@ export class PluginInternalsActionImpl {
|
||||||
// Get all builtin tools
|
// Get all builtin tools
|
||||||
for (const tool of builtinTools) {
|
for (const tool of builtinTools) {
|
||||||
if (tool.manifest) {
|
if (tool.manifest) {
|
||||||
manifests[tool.identifier] = tool.manifest as LobeChatPluginManifest;
|
manifests[tool.identifier] = tool.manifest as ToolManifest;
|
||||||
sourceMap[tool.identifier] = 'builtin';
|
sourceMap[tool.identifier] = 'builtin';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +59,7 @@ export class PluginInternalsActionImpl {
|
||||||
const klavisTools = klavisStoreSelectors.klavisAsLobeTools(toolStoreState);
|
const klavisTools = klavisStoreSelectors.klavisAsLobeTools(toolStoreState);
|
||||||
for (const tool of klavisTools) {
|
for (const tool of klavisTools) {
|
||||||
if (tool.manifest) {
|
if (tool.manifest) {
|
||||||
manifests[tool.identifier] = tool.manifest as LobeChatPluginManifest;
|
manifests[tool.identifier] = tool.manifest as ToolManifest;
|
||||||
sourceMap[tool.identifier] = 'klavis';
|
sourceMap[tool.identifier] = 'klavis';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +68,7 @@ export class PluginInternalsActionImpl {
|
||||||
const lobehubSkillTools = lobehubSkillStoreSelectors.lobehubSkillAsLobeTools(toolStoreState);
|
const lobehubSkillTools = lobehubSkillStoreSelectors.lobehubSkillAsLobeTools(toolStoreState);
|
||||||
for (const tool of lobehubSkillTools) {
|
for (const tool of lobehubSkillTools) {
|
||||||
if (tool.manifest) {
|
if (tool.manifest) {
|
||||||
manifests[tool.identifier] = tool.manifest as LobeChatPluginManifest;
|
manifests[tool.identifier] = tool.manifest as ToolManifest;
|
||||||
sourceMap[tool.identifier] = 'lobehubSkill';
|
sourceMap[tool.identifier] = 'lobehubSkill';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
import { type ChatToolPayload, type RuntimeStepContext } from '@lobechat/types';
|
import { type ChatToolPayload, type RuntimeStepContext } from '@lobechat/types';
|
||||||
import { PluginErrorType } from '@lobehub/chat-plugin-sdk';
|
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import { t } from 'i18next';
|
|
||||||
|
|
||||||
import { type MCPToolCallResult } from '@/libs/mcp';
|
import { type MCPToolCallResult } from '@/libs/mcp';
|
||||||
import { truncateToolResult } from '@/server/utils/truncateToolResult';
|
import { truncateToolResult } from '@/server/utils/truncateToolResult';
|
||||||
import { chatService } from '@/services/chat';
|
|
||||||
import { mcpService } from '@/services/mcp';
|
import { mcpService } from '@/services/mcp';
|
||||||
import { messageService } from '@/services/message';
|
import { messageService } from '@/services/message';
|
||||||
import { AI_RUNTIME_OPERATION_TYPES } from '@/store/chat/slices/operation';
|
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 (
|
invokeKlavisTypePlugin = async (
|
||||||
id: string,
|
id: string,
|
||||||
payload: ChatToolPayload,
|
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 (
|
invokeMCPTypePlugin = async (
|
||||||
id: string,
|
id: string,
|
||||||
payload: ChatToolPayload,
|
payload: ChatToolPayload,
|
||||||
|
|
@ -402,76 +350,6 @@ export class PluginTypesActionImpl {
|
||||||
|
|
||||||
return remoteContent;
|
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>;
|
export type PluginTypesAction = Pick<PluginTypesActionImpl, keyof PluginTypesActionImpl>;
|
||||||
|
|
|
||||||
|
|
@ -92,26 +92,15 @@ export class PluginPublicApiActionImpl {
|
||||||
stepContext?: RuntimeStepContext,
|
stepContext?: RuntimeStepContext,
|
||||||
): Promise<any> => {
|
): Promise<any> => {
|
||||||
switch (payload.type) {
|
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
|
// @ts-ignore
|
||||||
case 'mcp': {
|
case 'mcp': {
|
||||||
return await this.#get().invokeMCPTypePlugin(id, payload);
|
return await this.#get().invokeMCPTypePlugin(id, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'builtin':
|
||||||
default: {
|
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 LobeTool, type ToolManifestSettings } from '@lobechat/types';
|
||||||
import { type PluginSchema } from '@lobehub/chat-plugin-sdk';
|
|
||||||
|
|
||||||
import { type MetaData } from '@/types/meta';
|
import { type MetaData } from '@/types/meta';
|
||||||
|
|
||||||
|
|
@ -14,7 +13,7 @@ const getPluginAvatar = (meta?: MetaData) => meta?.avatar || '🧩';
|
||||||
const isCustomPlugin = (id: string, pluginList: LobeTool[]) =>
|
const isCustomPlugin = (id: string, pluginList: LobeTool[]) =>
|
||||||
pluginList.some((i) => i.identifier === id && i.type === 'customPlugin');
|
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;
|
schema?.properties && Object.keys(schema.properties).length > 0;
|
||||||
|
|
||||||
export const pluginHelpers = {
|
export const pluginHelpers = {
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,10 @@ import {
|
||||||
type LobehubSkillStoreState,
|
type LobehubSkillStoreState,
|
||||||
} from './slices/lobehubSkillStore/initialState';
|
} from './slices/lobehubSkillStore/initialState';
|
||||||
import { initialMCPStoreState, type MCPStoreState } from './slices/mcpStore/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';
|
import { initialPluginState, type PluginState } from './slices/plugin/initialState';
|
||||||
|
|
||||||
export type ToolStoreState = PluginState &
|
export type ToolStoreState = PluginState &
|
||||||
CustomPluginState &
|
CustomPluginState &
|
||||||
PluginStoreState &
|
|
||||||
BuiltinToolState &
|
BuiltinToolState &
|
||||||
MCPStoreState &
|
MCPStoreState &
|
||||||
KlavisStoreState &
|
KlavisStoreState &
|
||||||
|
|
@ -25,7 +23,6 @@ export type ToolStoreState = PluginState &
|
||||||
export const initialState: ToolStoreState = {
|
export const initialState: ToolStoreState = {
|
||||||
...initialPluginState,
|
...initialPluginState,
|
||||||
...initialCustomPluginState,
|
...initialCustomPluginState,
|
||||||
...initialPluginStoreState,
|
|
||||||
...initialBuiltinToolState,
|
...initialBuiltinToolState,
|
||||||
...initialMCPStoreState,
|
...initialMCPStoreState,
|
||||||
...initialKlavisStoreState,
|
...initialKlavisStoreState,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,5 @@ export { customPluginSelectors } from '../slices/customPlugin/selectors';
|
||||||
export { klavisStoreSelectors } from '../slices/klavisStore/selectors';
|
export { klavisStoreSelectors } from '../slices/klavisStore/selectors';
|
||||||
export { lobehubSkillStoreSelectors } from '../slices/lobehubSkillStore/selectors';
|
export { lobehubSkillStoreSelectors } from '../slices/lobehubSkillStore/selectors';
|
||||||
export { mcpStoreSelectors } from '../slices/mcpStore/selectors';
|
export { mcpStoreSelectors } from '../slices/mcpStore/selectors';
|
||||||
export { pluginStoreSelectors } from '../slices/oldStore/selectors';
|
|
||||||
export { pluginSelectors } from '../slices/plugin/selectors';
|
export { pluginSelectors } from '../slices/plugin/selectors';
|
||||||
export { toolSelectors } from './tool';
|
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 { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { type ToolStoreState } from '../initialState';
|
import { type ToolStoreState } from '../initialState';
|
||||||
|
|
@ -28,7 +28,7 @@ const mockState = {
|
||||||
createdAt: '2024-01-01',
|
createdAt: '2024-01-01',
|
||||||
homepage: 'https://example.com/plugin-1',
|
homepage: 'https://example.com/plugin-1',
|
||||||
meta: { title: 'Plugin 1', description: 'Plugin 1 description' },
|
meta: { title: 'Plugin 1', description: 'Plugin 1 description' },
|
||||||
} as LobeChatPluginManifest,
|
} as ToolManifest,
|
||||||
runtimeType: 'standalone',
|
runtimeType: 'standalone',
|
||||||
type: 'plugin',
|
type: 'plugin',
|
||||||
},
|
},
|
||||||
|
|
@ -39,7 +39,7 @@ const mockState = {
|
||||||
api: [{ name: 'api-2' }],
|
api: [{ name: 'api-2' }],
|
||||||
author: 'Another Author',
|
author: 'Another Author',
|
||||||
homepage: 'https://example.com/plugin-2',
|
homepage: 'https://example.com/plugin-2',
|
||||||
} as LobeChatPluginManifest,
|
} as ToolManifest,
|
||||||
runtimeType: 'default',
|
runtimeType: 'default',
|
||||||
type: 'plugin',
|
type: 'plugin',
|
||||||
},
|
},
|
||||||
|
|
@ -67,7 +67,7 @@ const mockState = {
|
||||||
identifier: 'builtin-1',
|
identifier: 'builtin-1',
|
||||||
api: [{ name: 'builtin-api-1' }],
|
api: [{ name: 'builtin-api-1' }],
|
||||||
meta: { title: 'Builtin 1', description: 'Builtin 1 description' },
|
meta: { title: 'Builtin 1', description: 'Builtin 1 description' },
|
||||||
} as LobeChatPluginManifest,
|
} as ToolManifest,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
pluginInstallLoading: {
|
pluginInstallLoading: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { getKlavisServerByServerIdentifier, getLobehubSkillProviderById } from '@lobechat/const';
|
import { getKlavisServerByServerIdentifier, getLobehubSkillProviderById } from '@lobechat/const';
|
||||||
import { type RenderDisplayControl } from '@lobechat/types';
|
import { type RenderDisplayControl, type ToolManifest } from '@lobechat/types';
|
||||||
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isInstalledPluginAvailableInCurrentEnv,
|
isInstalledPluginAvailableInCurrentEnv,
|
||||||
|
|
@ -42,10 +41,10 @@ const getMetaById =
|
||||||
|
|
||||||
const getManifestById =
|
const getManifestById =
|
||||||
(id: string) =>
|
(id: string) =>
|
||||||
(s: ToolStoreState): LobeChatPluginManifest | undefined =>
|
(s: ToolStoreState): ToolManifest | undefined =>
|
||||||
pluginSelectors
|
pluginSelectors
|
||||||
.installedPluginManifestList(s)
|
.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);
|
.find((i) => i.identifier === id);
|
||||||
|
|
||||||
// Get plugin manifest loading status
|
// Get plugin manifest loading status
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ vi.mock('@/services/plugin', () => ({
|
||||||
createCustomPlugin: vi.fn(),
|
createCustomPlugin: vi.fn(),
|
||||||
uninstallPlugin: vi.fn(),
|
uninstallPlugin: vi.fn(),
|
||||||
updatePluginManifest: 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 { merge } from 'es-toolkit/compat';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
|
|
||||||
|
|
@ -46,7 +46,7 @@ export class CustomPluginActionImpl {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
updateInstallLoadingState(id, true);
|
updateInstallLoadingState(id, true);
|
||||||
let manifest: LobeChatPluginManifest;
|
let manifest: ToolManifest;
|
||||||
// mean this is a mcp plugin
|
// mean this is a mcp plugin
|
||||||
if (!!plugin.customParams?.mcp) {
|
if (!!plugin.customParams?.mcp) {
|
||||||
const url = plugin.customParams?.mcp?.url;
|
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 { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { type ToolStoreState } from '../../initialState';
|
import { type ToolStoreState } from '../../initialState';
|
||||||
|
|
@ -19,7 +19,7 @@ const mockState = {
|
||||||
identifier: 'plugin-1',
|
identifier: 'plugin-1',
|
||||||
api: [{ name: 'api-1' }],
|
api: [{ name: 'api-1' }],
|
||||||
type: 'default',
|
type: 'default',
|
||||||
} as LobeChatPluginManifest,
|
} as ToolManifest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identifier: 'plugin-2',
|
identifier: 'plugin-2',
|
||||||
|
|
@ -38,7 +38,7 @@ const mockState = {
|
||||||
createdAt: '2021-01-01',
|
createdAt: '2021-01-01',
|
||||||
meta: { avatar: 'avatar-url-1', title: 'Plugin 1' },
|
meta: { avatar: 'avatar-url-1', title: 'Plugin 1' },
|
||||||
homepage: 'http://homepage-1.com',
|
homepage: 'http://homepage-1.com',
|
||||||
} as LobeChatPluginMeta,
|
} as any,
|
||||||
{
|
{
|
||||||
identifier: 'plugin-2',
|
identifier: 'plugin-2',
|
||||||
author: 'Author 2',
|
author: 'Author 2',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type * as LobechatConstModule from '@lobechat/const';
|
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 { type PluginItem } from '@lobehub/market-sdk';
|
||||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
@ -230,7 +230,7 @@ describe('mcpStore actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('testMcpConnection', () => {
|
describe('testMcpConnection', () => {
|
||||||
const mockManifest: LobeChatPluginManifest = {
|
const mockManifest: ToolManifest = {
|
||||||
api: [],
|
api: [],
|
||||||
gateway: '',
|
gateway: '',
|
||||||
identifier: 'test-plugin',
|
identifier: 'test-plugin',
|
||||||
|
|
@ -717,7 +717,7 @@ describe('mcpStore actions', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockServerManifest: LobeChatPluginManifest = {
|
const mockServerManifest: ToolManifest = {
|
||||||
api: [],
|
api: [],
|
||||||
gateway: '',
|
gateway: '',
|
||||||
identifier: 'test-plugin',
|
identifier: 'test-plugin',
|
||||||
|
|
@ -1170,7 +1170,7 @@ describe('mcpStore actions', () => {
|
||||||
version: '1.5.0',
|
version: '1.5.0',
|
||||||
};
|
};
|
||||||
|
|
||||||
const serverManifestWithVersion: LobeChatPluginManifest = {
|
const serverManifestWithVersion: ToolManifest = {
|
||||||
api: [],
|
api: [],
|
||||||
gateway: '',
|
gateway: '',
|
||||||
identifier: 'test-plugin',
|
identifier: 'test-plugin',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { CURRENT_VERSION, isDesktop } from '@lobechat/const';
|
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 PluginItem, type PluginListResponse } from '@lobehub/market-sdk';
|
||||||
import { type TRPCClientError } from '@trpc/client';
|
import { type TRPCClientError } from '@trpc/client';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
|
@ -73,7 +73,7 @@ const toNonEmptyStringRecord = (input?: Record<string, any>) => {
|
||||||
const buildCloudMcpManifest = (params: {
|
const buildCloudMcpManifest = (params: {
|
||||||
data: any;
|
data: any;
|
||||||
plugin: { description?: string; icon?: string; identifier: string };
|
plugin: { description?: string; icon?: string; identifier: string };
|
||||||
}): LobeChatPluginManifest => {
|
}): ToolManifest => {
|
||||||
const { data, plugin } = params;
|
const { data, plugin } = params;
|
||||||
|
|
||||||
log('Using cloud connection, building manifest from market data');
|
log('Using cloud connection, building manifest from market data');
|
||||||
|
|
@ -104,7 +104,7 @@ const buildCloudMcpManifest = (params: {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build complete manifest
|
// Build complete manifest
|
||||||
const manifest: LobeChatPluginManifest = {
|
const manifest: ToolManifest = {
|
||||||
api: apiArray,
|
api: apiArray,
|
||||||
author: data.author?.name || data.author || '',
|
author: data.author?.name || data.author || '',
|
||||||
createAt: data.createdAt || new Date().toISOString(),
|
createAt: data.createdAt || new Date().toISOString(),
|
||||||
|
|
@ -120,7 +120,7 @@ const buildCloudMcpManifest = (params: {
|
||||||
name: data.name || plugin.identifier,
|
name: data.name || plugin.identifier,
|
||||||
type: 'mcp',
|
type: 'mcp',
|
||||||
version: data.version,
|
version: data.version,
|
||||||
} as unknown as LobeChatPluginManifest;
|
} as unknown as ToolManifest;
|
||||||
|
|
||||||
log('[Cloud MCP] Final manifest built:', {
|
log('[Cloud MCP] Final manifest built:', {
|
||||||
apiCount: manifest.api?.length,
|
apiCount: manifest.api?.length,
|
||||||
|
|
@ -136,7 +136,7 @@ export interface TestMcpConnectionResult {
|
||||||
error?: string;
|
error?: string;
|
||||||
/** STDIO process output logs for debugging */
|
/** STDIO process output logs for debugging */
|
||||||
errorLog?: string;
|
errorLog?: string;
|
||||||
manifest?: LobeChatPluginManifest;
|
manifest?: ToolManifest;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -470,7 +470,7 @@ export class PluginMCPStoreActionImpl {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let manifest: LobeChatPluginManifest | undefined;
|
let manifest: ToolManifest | undefined;
|
||||||
|
|
||||||
if (connection?.type === 'stdio') {
|
if (connection?.type === 'stdio') {
|
||||||
manifest = await mcpService.getStdioMcpServerManifest(
|
manifest = await mcpService.getStdioMcpServerManifest(
|
||||||
|
|
@ -750,7 +750,7 @@ export class PluginMCPStoreActionImpl {
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let manifest: LobeChatPluginManifest;
|
let manifest: ToolManifest;
|
||||||
|
|
||||||
if (connection.type === 'http') {
|
if (connection.type === 'http') {
|
||||||
if (!connection.url) {
|
if (!connection.url) {
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,15 @@ import { type PluginItem } from '@lobehub/market-sdk';
|
||||||
|
|
||||||
import { type MCPInstallProgressMap } from '@/types/plugins';
|
import { type MCPInstallProgressMap } from '@/types/plugins';
|
||||||
|
|
||||||
|
export type PluginStoreListType = 'installed' | 'mcp';
|
||||||
|
|
||||||
export interface MCPStoreState {
|
export interface MCPStoreState {
|
||||||
activeMCPIdentifier?: string;
|
activeMCPIdentifier?: string;
|
||||||
categories: string[];
|
categories: string[];
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
isLoadingMore?: boolean;
|
isLoadingMore?: boolean;
|
||||||
isMcpListInit?: boolean;
|
isMcpListInit?: boolean;
|
||||||
|
listType: PluginStoreListType;
|
||||||
mcpInstallAbortControllers: Record<string, AbortController>;
|
mcpInstallAbortControllers: Record<string, AbortController>;
|
||||||
mcpInstallProgress: MCPInstallProgressMap;
|
mcpInstallProgress: MCPInstallProgressMap;
|
||||||
mcpPluginItems: PluginItem[];
|
mcpPluginItems: PluginItem[];
|
||||||
|
|
@ -25,6 +28,7 @@ export interface MCPStoreState {
|
||||||
export const initialMCPStoreState: MCPStoreState = {
|
export const initialMCPStoreState: MCPStoreState = {
|
||||||
categories: [],
|
categories: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
|
listType: 'mcp',
|
||||||
mcpInstallAbortControllers: {},
|
mcpInstallAbortControllers: {},
|
||||||
mcpInstallProgress: {},
|
mcpInstallProgress: {},
|
||||||
mcpPluginItems: [],
|
mcpPluginItems: [],
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { MCPInstallStep } from '@/types/plugins';
|
||||||
|
|
||||||
import { initialState } from '../../initialState';
|
import { initialState } from '../../initialState';
|
||||||
import { type ToolStoreState } from '../../initialState';
|
import { type ToolStoreState } from '../../initialState';
|
||||||
import { PluginStoreTabs } from '../oldStore/initialState';
|
|
||||||
import { mcpStoreSelectors } from './selectors';
|
import { mcpStoreSelectors } from './selectors';
|
||||||
|
|
||||||
const createMockPluginItem = (id: string, overrides: Partial<PluginItem> = {}): PluginItem =>
|
const createMockPluginItem = (id: string, overrides: Partial<PluginItem> = {}): PluginItem =>
|
||||||
|
|
@ -56,20 +55,20 @@ const baseState: ToolStoreState = {
|
||||||
settings: {},
|
settings: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
listType: PluginStoreTabs.MCP,
|
listType: 'mcp',
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('mcpStoreSelectors', () => {
|
describe('mcpStoreSelectors', () => {
|
||||||
describe('mcpPluginList', () => {
|
describe('mcpPluginList', () => {
|
||||||
it('should return all mcp plugins when listType is MCP', () => {
|
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);
|
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||||
|
|
||||||
expect(result).toHaveLength(3);
|
expect(result).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return only installed plugins when listType is Installed', () => {
|
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);
|
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||||
|
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
|
|
@ -77,7 +76,7 @@ describe('mcpStoreSelectors', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should map plugin items to InstallPluginMeta format', () => {
|
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 result = mcpStoreSelectors.mcpPluginList(state);
|
||||||
const item = result[0];
|
const item = result[0];
|
||||||
|
|
||||||
|
|
@ -99,7 +98,7 @@ describe('mcpStoreSelectors', () => {
|
||||||
const state: ToolStoreState = {
|
const state: ToolStoreState = {
|
||||||
...baseState,
|
...baseState,
|
||||||
installedPlugins: [],
|
installedPlugins: [],
|
||||||
listType: PluginStoreTabs.Installed,
|
listType: 'installed',
|
||||||
};
|
};
|
||||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
const result = mcpStoreSelectors.mcpPluginList(state);
|
||||||
|
|
||||||
|
|
@ -130,7 +129,7 @@ describe('mcpStoreSelectors', () => {
|
||||||
settings: {},
|
settings: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
listType: PluginStoreTabs.Installed,
|
listType: 'installed',
|
||||||
};
|
};
|
||||||
const result = mcpStoreSelectors.mcpPluginList(state);
|
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', () => ({
|
vi.mock('@/services/plugin', () => ({
|
||||||
pluginService: {
|
pluginService: {
|
||||||
|
getInstalledPlugins: vi.fn().mockResolvedValue([]),
|
||||||
updatePluginSettings: vi.fn(),
|
updatePluginSettings: vi.fn(),
|
||||||
removeAllPlugins: vi.fn(),
|
removeAllPlugins: vi.fn(),
|
||||||
},
|
},
|
||||||
|
|
@ -23,13 +24,6 @@ describe('useToolStore:plugin', () => {
|
||||||
describe('checkPluginsIsInstalled', () => {
|
describe('checkPluginsIsInstalled', () => {
|
||||||
it('should be deprecated and do nothing', async () => {
|
it('should be deprecated and do nothing', async () => {
|
||||||
// Old plugin system has been deprecated
|
// Old plugin system has been deprecated
|
||||||
const loadPluginStoreMock = vi.fn();
|
|
||||||
const installPluginsMock = vi.fn();
|
|
||||||
useToolStore.setState({
|
|
||||||
loadPluginStore: loadPluginStoreMock,
|
|
||||||
installPlugins: installPluginsMock,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useToolStore());
|
const { result } = renderHook(() => useToolStore());
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|
@ -37,8 +31,6 @@ describe('useToolStore:plugin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should not call any methods since old plugin system is deprecated
|
// 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