mirror of
https://github.com/cs-lazy-tools/ChatGPT-On-CS.git
synced 2026-09-01 15:00:59 +08:00
add: 新增插件系统
This commit is contained in:
@@ -111,6 +111,7 @@
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.4",
|
||||
"emoji-picker-react": "^4.10.0",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^4.19.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
|
||||
Generated
+17
@@ -59,6 +59,9 @@ dependencies:
|
||||
electron-updater:
|
||||
specifier: ^6.1.4
|
||||
version: 6.1.8
|
||||
emoji-picker-react:
|
||||
specifier: ^4.10.0
|
||||
version: 4.10.0(react@18.3.0)
|
||||
exceljs:
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
@@ -7252,6 +7255,16 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/emoji-picker-react@4.10.0(react@18.3.0):
|
||||
resolution: {integrity: sha512-EfvOsGbyweMNcJ1F99XUv+XPdfkpa2NRAYkhwdIeYS6DWeISu3kHWX+iwvFLUVAc533aWbsGpETbxwbhzsiMnw==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: '>=16'
|
||||
dependencies:
|
||||
flairup: 0.0.39
|
||||
react: 18.3.0
|
||||
dev: false
|
||||
|
||||
/emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
@@ -8292,6 +8305,10 @@ packages:
|
||||
path-exists: 4.0.0
|
||||
dev: true
|
||||
|
||||
/flairup@0.0.39:
|
||||
resolution: {integrity: sha512-UVPkzZmZeBWBx1+Ovo++kYKk9Wi32Jxt+c7HsxnEY80ExwFV54w+NyquFziqMLS0BnGVE43yGD4OvIwaAm/WiQ==}
|
||||
dev: false
|
||||
|
||||
/flat-cache@3.2.0:
|
||||
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
|
||||
@@ -538,6 +538,88 @@ class BKServer {
|
||||
}
|
||||
});
|
||||
|
||||
this.app.get('/api/v1/plugin/list', async (req, res) => {
|
||||
const plugins = await this.configController.getAllCustomPlugins();
|
||||
const results = plugins.map((plugin) => {
|
||||
return {
|
||||
id: plugin.id,
|
||||
code: plugin.code,
|
||||
title: plugin.title,
|
||||
description: plugin.description,
|
||||
icon: plugin.icon,
|
||||
source: plugin.source,
|
||||
author: plugin.author,
|
||||
type: plugin.type,
|
||||
tags: JSON.parse(plugin.tags || '[]'),
|
||||
};
|
||||
});
|
||||
res.json({
|
||||
success: true,
|
||||
data: results,
|
||||
});
|
||||
});
|
||||
|
||||
this.app.get('/api/v1/plugin/detail', async (req, res) => {
|
||||
const { id } = req.query;
|
||||
const plugin = await this.configController.getPluginConfig(Number(id));
|
||||
if (!plugin) {
|
||||
res.json({
|
||||
success: false,
|
||||
data: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tags = JSON.parse(plugin.tags || '[]');
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: plugin.id,
|
||||
code: plugin.code,
|
||||
title: plugin.title,
|
||||
description: plugin.description,
|
||||
icon: plugin.icon,
|
||||
source: plugin.source,
|
||||
author: plugin.author,
|
||||
type: plugin.type,
|
||||
tags,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
this.app.post('/api/v1/plugin/create', async (req, res) => {
|
||||
const { code, source, author, description, icon, tags, title } = req.body;
|
||||
await this.configController.createCustomPlugin({
|
||||
code,
|
||||
source,
|
||||
author,
|
||||
description,
|
||||
icon,
|
||||
tags: JSON.stringify(tags),
|
||||
title,
|
||||
});
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
this.app.post('/api/v1/plugin/update', async (req, res) => {
|
||||
const { id, code, description, icon, tags, title } = req.body;
|
||||
await this.configController.updateCustomPlugin({
|
||||
pluginId: id,
|
||||
code,
|
||||
description,
|
||||
icon,
|
||||
tags: JSON.stringify(tags),
|
||||
title,
|
||||
});
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
this.app.post('/api/v1/plugin/delete', async (req, res) => {
|
||||
const { id } = req.body;
|
||||
await this.configController.deleteCustomPlugin(id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
// TODO: 后续需要根据通过 WS 去检查后端服务是否健康
|
||||
this.app.get('/api/v1/base/health', async (req, res) => {
|
||||
|
||||
@@ -74,16 +74,6 @@ export class ConfigController {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得插件配置
|
||||
* @param pluginId
|
||||
* @returns
|
||||
*/
|
||||
public async getPluginConfig(pluginId: number): Promise<Plugin | null> {
|
||||
const plugin = await Plugin.findByPk(pluginId);
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活或关闭配置
|
||||
* @param
|
||||
@@ -134,6 +124,106 @@ export class ConfigController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得自定义插件
|
||||
* @param
|
||||
* @returns
|
||||
*/
|
||||
public async getAllCustomPlugins(): Promise<Plugin[]> {
|
||||
const plugins = await Plugin.findAll();
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得插件配置
|
||||
* @param pluginId
|
||||
* @returns
|
||||
*/
|
||||
public async getPluginConfig(pluginId: number): Promise<Plugin | null> {
|
||||
const plugin = await Plugin.findByPk(pluginId);
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增自定义插件
|
||||
* @param
|
||||
* @returns
|
||||
*/
|
||||
public async createCustomPlugin({
|
||||
source,
|
||||
author,
|
||||
description,
|
||||
icon,
|
||||
tags,
|
||||
title,
|
||||
code,
|
||||
}: {
|
||||
source?: string;
|
||||
author?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
tags?: string;
|
||||
title: string;
|
||||
code: string;
|
||||
}) {
|
||||
const plugin = await Plugin.create({
|
||||
source: source || 'custom',
|
||||
author,
|
||||
description,
|
||||
icon,
|
||||
tags,
|
||||
type: 'plugin',
|
||||
title,
|
||||
code,
|
||||
});
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除自定义插件
|
||||
* @param
|
||||
* @returns
|
||||
*/
|
||||
public async deleteCustomPlugin(pluginId: number) {
|
||||
const plugin = await Plugin.findByPk(pluginId);
|
||||
if (plugin) {
|
||||
await plugin.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新自定义插件
|
||||
* @param
|
||||
* @returns
|
||||
*/
|
||||
public async updateCustomPlugin({
|
||||
pluginId,
|
||||
code,
|
||||
description,
|
||||
icon,
|
||||
tags,
|
||||
title,
|
||||
}: {
|
||||
pluginId: number;
|
||||
code: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
tags: string;
|
||||
title: string;
|
||||
}) {
|
||||
const plugin = await Plugin.findByPk(pluginId);
|
||||
if (plugin) {
|
||||
await plugin.update({
|
||||
code,
|
||||
description,
|
||||
icon,
|
||||
tags,
|
||||
title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查配置是否激活
|
||||
* @param
|
||||
@@ -254,18 +344,11 @@ export class ConfigController {
|
||||
}
|
||||
|
||||
if (type === 'plugin') {
|
||||
let pluginCode = '';
|
||||
|
||||
if (config?.plugin_id) {
|
||||
const plugin = await Plugin.findByPk(config?.plugin_id);
|
||||
pluginCode = plugin?.code || '';
|
||||
}
|
||||
|
||||
return {
|
||||
appId: config?.platform_id || '',
|
||||
instanceId: config?.instance_id || '',
|
||||
usePlugin: config?.use_plugin || false,
|
||||
pluginCode,
|
||||
pluginId: config?.plugin_id || 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -364,18 +447,7 @@ export class ConfigController {
|
||||
let pluginId = null;
|
||||
const config = cfg as PluginConfig;
|
||||
if (dbConfig.use_plugin) {
|
||||
let plugin = await Plugin.findByPk(dbConfig.plugin_id);
|
||||
if (!plugin) {
|
||||
plugin = await Plugin.create({
|
||||
code: config.pluginCode,
|
||||
});
|
||||
} else {
|
||||
await plugin.update({
|
||||
code: config.pluginCode,
|
||||
});
|
||||
}
|
||||
|
||||
pluginId = plugin.id;
|
||||
pluginId = config.pluginId;
|
||||
}
|
||||
|
||||
await dbConfig.update({
|
||||
|
||||
@@ -91,7 +91,7 @@ export async function checkAndAddFields(sequelize: Sequelize) {
|
||||
.addColumn('n_config', 'truncate_word_key', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: '...',
|
||||
defaultValue: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,92 @@ import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
export class Plugin extends Model {
|
||||
declare id: number;
|
||||
|
||||
declare name: string;
|
||||
|
||||
declare code: string;
|
||||
|
||||
declare platform: string;
|
||||
|
||||
declare platform_id: string;
|
||||
|
||||
declare instance_id: string; // 可能是作用于单个实例的插件
|
||||
|
||||
declare created_at: Date;
|
||||
|
||||
declare version: string;
|
||||
|
||||
declare source: string; // 自定义插件、官方内置插件、第三方插件
|
||||
|
||||
declare author: string; // 插件作者
|
||||
|
||||
declare description: string; // 插件描述
|
||||
|
||||
declare icon: string; // 插件图标
|
||||
|
||||
declare tags: string; // 插件标签
|
||||
|
||||
declare type: string; // 插件类型
|
||||
|
||||
declare title: string; // 插件标题
|
||||
}
|
||||
|
||||
export async function checkAndAddFields(sequelize: Sequelize) {
|
||||
const tableDescription = await Plugin.describe();
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.source) {
|
||||
await sequelize.getQueryInterface().addColumn('plugins', 'source', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: 'custom',
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.author) {
|
||||
await sequelize.getQueryInterface().addColumn('plugins', 'author', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: 'unknown',
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.description) {
|
||||
await sequelize.getQueryInterface().addColumn('plugins', 'description', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: '插件的描述信息~',
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.icon) {
|
||||
await sequelize.getQueryInterface().addColumn('plugins', 'icon', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: '😀',
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.tags) {
|
||||
await sequelize.getQueryInterface().addColumn('plugins', 'tags', {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
defaultValue: JSON.stringify([]),
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.type) {
|
||||
await sequelize.getQueryInterface().addColumn('plugins', 'type', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: 'plugin',
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!tableDescription.title) {
|
||||
await sequelize.getQueryInterface().addColumn('plugins', 'title', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: '插件标题',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function initPlugin(sequelize: Sequelize) {
|
||||
@@ -34,14 +107,17 @@ export function initPlugin(sequelize: Sequelize) {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
// @Deprecated
|
||||
platform: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
// @Deprecated
|
||||
platform_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
},
|
||||
// @Deprecated
|
||||
instance_id: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
@@ -52,9 +128,44 @@ export function initPlugin(sequelize: Sequelize) {
|
||||
},
|
||||
version: {
|
||||
type: DataTypes.STRING(255),
|
||||
defaultValue: '1.0.0',
|
||||
defaultValue: '1.1.0',
|
||||
allowNull: true,
|
||||
},
|
||||
source: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: 'custom',
|
||||
},
|
||||
author: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: 'unknown',
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: '插件的描述信息~',
|
||||
},
|
||||
icon: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: '😀',
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
defaultValue: JSON.stringify([]),
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: 'plugin',
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: '插件标题',
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
@@ -63,4 +174,6 @@ export function initPlugin(sequelize: Sequelize) {
|
||||
timestamps: false,
|
||||
},
|
||||
);
|
||||
|
||||
checkAndAddFields(sequelize);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export interface PluginConfig {
|
||||
appId: string;
|
||||
instanceId: string;
|
||||
usePlugin: boolean;
|
||||
pluginCode: string;
|
||||
pluginId: number;
|
||||
}
|
||||
|
||||
export interface DriverConfig {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DriverConfig,
|
||||
Message,
|
||||
Session,
|
||||
Plugin,
|
||||
MessageModel,
|
||||
} from './platform';
|
||||
import { GET, POST } from '../common/api/request';
|
||||
@@ -327,3 +328,29 @@ export async function exportMessageExcel() {
|
||||
const data = await GET('/api/v1/message/excel');
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getCustomPluginList() {
|
||||
const data = await GET<{
|
||||
data: Plugin[];
|
||||
}>('/api/v1/plugin/list');
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getCustomPluginDetail(id: number) {
|
||||
const data = await GET<{
|
||||
data: Plugin;
|
||||
}>('/api/v1/plugin/detail', { id });
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function addCustomPlugin(plugin: Plugin) {
|
||||
await POST('/api/v1/plugin/create', plugin);
|
||||
}
|
||||
|
||||
export async function updateCustomPlugin(plugin: Plugin) {
|
||||
await POST('/api/v1/plugin/update', plugin);
|
||||
}
|
||||
|
||||
export async function deleteCustomPlugin(id: number) {
|
||||
await POST('/api/v1/plugin/delete', { id });
|
||||
}
|
||||
|
||||
+12
-1
@@ -44,6 +44,17 @@ export interface LogBody {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
id?: number;
|
||||
code?: string;
|
||||
type: string;
|
||||
title: string;
|
||||
author?: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface GenericConfig {
|
||||
appId: string;
|
||||
instanceId: string;
|
||||
@@ -76,7 +87,7 @@ export interface PluginConfig {
|
||||
appId: string;
|
||||
instanceId: string;
|
||||
usePlugin: boolean;
|
||||
pluginCode: string;
|
||||
pluginId: number;
|
||||
}
|
||||
|
||||
export interface DriverConfig {
|
||||
|
||||
@@ -19,10 +19,12 @@ import {
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { loader } from '@monaco-editor/react';
|
||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import GeneralSettings from './components/Settings/GeneralSettings';
|
||||
import LLMSettings from './components/Settings/LLMSettings';
|
||||
import PluginSettings from './components/Settings/PluginSettings';
|
||||
import PluginPage from './pages/Plugin';
|
||||
import PluginEditorPage from './pages/PluginEditor';
|
||||
import AboutPage from './components/About';
|
||||
import { trackPageView } from '../common/services/analytics';
|
||||
import {
|
||||
@@ -199,18 +201,6 @@ const App = () => {
|
||||
{settings.appId || settings.instanceId ? '' : '全局'}插件设置
|
||||
</Tab>
|
||||
|
||||
{
|
||||
// {!settings.appId && (
|
||||
// <Tab
|
||||
// _selected={{ bg: 'gray.200' }}
|
||||
// _hover={{ bg: 'gray.300' }}
|
||||
// textAlign="left"
|
||||
// >
|
||||
// 使用激活码
|
||||
// </Tab>
|
||||
// )}
|
||||
}
|
||||
|
||||
{!settings.appId && (
|
||||
<Tab
|
||||
_selected={{ bg: 'gray.200' }}
|
||||
@@ -243,26 +233,30 @@ const App = () => {
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Heading as="h3" size="md" mb={4}>
|
||||
{settings.appId ? '' : '全局'}插件设置
|
||||
</Heading>
|
||||
<PluginSettings
|
||||
appId={settings.appId}
|
||||
instanceId={settings.instanceId}
|
||||
/>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/settings.html"
|
||||
element={
|
||||
<PluginPage
|
||||
appId={settings.appId}
|
||||
instanceId={settings.instanceId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings.html/editor"
|
||||
element={
|
||||
<PluginEditorPage
|
||||
appId={settings.appId}
|
||||
instanceId={settings.instanceId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Router>
|
||||
</TabPanel>
|
||||
{
|
||||
// {!settings.appId && (
|
||||
// <>
|
||||
// <TabPanel>
|
||||
// <Heading as="h3" size="md" mb={4}>
|
||||
// 账户设置
|
||||
// </Heading>
|
||||
// <AccountSettings />
|
||||
// </TabPanel>
|
||||
// </>
|
||||
// )}
|
||||
}
|
||||
|
||||
{!settings.appId && (
|
||||
<>
|
||||
<TabPanel>
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
import {
|
||||
PluginConfig,
|
||||
LogBody,
|
||||
} from '../../../common/services/platform/platform.d';
|
||||
} from '../../../common/services/platform/platform';
|
||||
import MessageModal from '../MessageModal';
|
||||
import { useSystemStore } from '../../stores/useSystemStore';
|
||||
|
||||
@@ -152,7 +152,7 @@ const PluginSettings = ({
|
||||
});
|
||||
}
|
||||
},
|
||||
[code, config], // 更新依赖项
|
||||
[code, config, appId, instanceId, toast], // 更新依赖项
|
||||
);
|
||||
|
||||
const handleDefaultCode = () => {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
Divider,
|
||||
Tag,
|
||||
Flex,
|
||||
IconButton,
|
||||
Checkbox,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaBook, FaPlus } from 'react-icons/fa';
|
||||
import { Plugin } from '../../../common/services/platform/platform';
|
||||
|
||||
const PluginCard = ({
|
||||
plugin,
|
||||
isActive,
|
||||
onActivate,
|
||||
onEdit,
|
||||
}: {
|
||||
plugin: Plugin;
|
||||
isActive: boolean;
|
||||
onActivate: () => void;
|
||||
onEdit?: () => void;
|
||||
}) => {
|
||||
const [selected, setSelected] = useState(false);
|
||||
const handleMouseEnter = () => setSelected(true);
|
||||
const handleMouseLeave = () => setSelected(false);
|
||||
|
||||
return (
|
||||
<Box
|
||||
position="relative"
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
overflow="hidden"
|
||||
p={4}
|
||||
bg={selected ? 'white' : 'gray.50'}
|
||||
boxShadow={selected ? 'lg' : 'none'}
|
||||
borderColor={selected ? 'blue.500' : 'gray.200'}
|
||||
borderStyle="solid"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={onEdit}
|
||||
>
|
||||
{plugin.type === 'plugin' && (
|
||||
<Flex position="absolute" top={2} right={2}>
|
||||
<Checkbox isChecked={isActive} onChange={onActivate} />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{isActive && plugin.type === 'plugin' && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
bg="green.500"
|
||||
color="white"
|
||||
px={2}
|
||||
py={1}
|
||||
borderBottomLeftRadius="md"
|
||||
>
|
||||
已激活
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{plugin.type === 'guide' && (
|
||||
<Flex
|
||||
direction="column"
|
||||
align="flex-start"
|
||||
justify="center"
|
||||
height="100%"
|
||||
>
|
||||
<Flex align="flex-start">
|
||||
<Text fontSize="4xl">{plugin.icon}</Text>
|
||||
<Text
|
||||
fontWeight="bold"
|
||||
textAlign="left"
|
||||
whiteSpace="pre-line"
|
||||
mt={2}
|
||||
>
|
||||
{plugin.title}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Divider my={4} />
|
||||
<Flex align="center">
|
||||
<FaBook />
|
||||
<Text ml={2}>查看指南</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{plugin.type === 'custom' && (
|
||||
<Flex
|
||||
direction="column"
|
||||
align="flex-start"
|
||||
justify="center"
|
||||
height="100%"
|
||||
>
|
||||
<Flex align="center">
|
||||
<IconButton
|
||||
icon={<FaPlus />}
|
||||
aria-label="Add Custom Tool"
|
||||
variant="outline"
|
||||
mr={2}
|
||||
/>
|
||||
<Text fontWeight="bold">创建自定义工具</Text>
|
||||
</Flex>
|
||||
<Divider my={4} />
|
||||
<Flex align="center">
|
||||
<FaBook />
|
||||
<Text ml={2}>查看帮助</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{plugin.type === 'plugin' && (
|
||||
<>
|
||||
<Flex>
|
||||
<Text fontSize="4xl">{plugin.icon}</Text>
|
||||
<Box ml={4}>
|
||||
<Text fontWeight="bold" textAlign="left">
|
||||
{plugin.title}
|
||||
</Text>
|
||||
<Text color="gray.500" textAlign="left">
|
||||
{plugin.author}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Text mt={4} textAlign="left">
|
||||
{plugin.description}
|
||||
</Text>
|
||||
<Flex mt={4} wrap="wrap">
|
||||
{plugin.tags.map((tag, idx) => (
|
||||
<Tag key={idx} mr={2} mt={2}>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginCard;
|
||||
@@ -0,0 +1,188 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
ChakraProvider,
|
||||
Grid,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Skeleton,
|
||||
Stack,
|
||||
} from '@chakra-ui/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Plugin } from '../../../common/services/platform/platform';
|
||||
import { getCustomPluginList as getLocalPluginList } from '../../../common/services/platform/controller';
|
||||
import PluginCard from './PluginCard';
|
||||
|
||||
const systemPlugins = [
|
||||
{
|
||||
type: 'guide',
|
||||
title: '我有兴趣为懒人客服\n贡献工具',
|
||||
description: '',
|
||||
tags: [],
|
||||
icon: '📘',
|
||||
},
|
||||
{
|
||||
type: 'plugin',
|
||||
title: '系统插件名称',
|
||||
author: '系统作者名',
|
||||
description: '这是一个系统插件的描述。',
|
||||
tags: ['Tag1', 'Tag2', 'Tag3'],
|
||||
icon: '😀',
|
||||
},
|
||||
// 其他系统插件数据...
|
||||
];
|
||||
|
||||
const userPlugins = [
|
||||
{
|
||||
type: 'plugin',
|
||||
title: '用户插件名称',
|
||||
author: '用户作者名',
|
||||
description: '这是一个用户插件的描述。',
|
||||
tags: ['Tag1', 'Tag2', 'Tag3'],
|
||||
icon: '😀',
|
||||
},
|
||||
// 其他用户插件数据...
|
||||
];
|
||||
|
||||
type PluginPageProps = {
|
||||
appId?: string;
|
||||
instanceId?: string;
|
||||
};
|
||||
|
||||
const PluginPage = ({ appId, instanceId }: PluginPageProps) => {
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [activePlugin, setActivePlugin] = useState<number | null>(null);
|
||||
const [customPlugins, setCustomPlugins] = useState<Plugin[]>([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
data: localPluginData,
|
||||
isLoading: isLocalLoading,
|
||||
// refetch: refetchLocalPluginList,
|
||||
} = useQuery(
|
||||
['localPlugins'],
|
||||
() => {
|
||||
return getLocalPluginList();
|
||||
},
|
||||
{
|
||||
retry: () => {
|
||||
return true;
|
||||
},
|
||||
retryDelay: () => {
|
||||
return 1000;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (localPluginData) {
|
||||
console.log(localPluginData);
|
||||
setCustomPlugins([
|
||||
{
|
||||
type: 'custom',
|
||||
title: '创建自定义工具',
|
||||
description: '',
|
||||
tags: [],
|
||||
},
|
||||
...localPluginData.data,
|
||||
]);
|
||||
}
|
||||
}, [localPluginData]);
|
||||
|
||||
if (isLocalLoading) {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton height="20px" />
|
||||
<Skeleton height="20px" />
|
||||
<Skeleton height="20px" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const handleActivate = (index: number) => {
|
||||
setActivePlugin(activePlugin === index ? null : index);
|
||||
};
|
||||
|
||||
const handleEdit = (plugin: Plugin) => {
|
||||
console.log('edit', plugin);
|
||||
if (plugin.type === 'custom' || plugin.type === 'plugin') {
|
||||
navigate(
|
||||
'/settings.html/editor',
|
||||
plugin.id
|
||||
? {
|
||||
state: { pluginId: plugin.id },
|
||||
}
|
||||
: {},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ChakraProvider>
|
||||
<Tabs index={tabIndex} onChange={(index) => setTabIndex(index)}>
|
||||
<TabList>
|
||||
<Tab>系统内置</Tab>
|
||||
<Tab>用户分享</Tab>
|
||||
<Tab>自定义</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Grid
|
||||
templateColumns="repeat(auto-fill, minmax(250px, 1fr))"
|
||||
gap={6}
|
||||
p={4}
|
||||
>
|
||||
{systemPlugins.map((plugin, index) => (
|
||||
<PluginCard
|
||||
key={index}
|
||||
plugin={plugin}
|
||||
isActive={activePlugin === index}
|
||||
onActivate={() => handleActivate(index)}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Grid
|
||||
templateColumns="repeat(auto-fill, minmax(250px, 1fr))"
|
||||
gap={6}
|
||||
p={4}
|
||||
>
|
||||
{userPlugins.map((plugin, index) => (
|
||||
<PluginCard
|
||||
key={index}
|
||||
plugin={plugin}
|
||||
isActive={activePlugin === index}
|
||||
onActivate={() => handleActivate(index)}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Grid
|
||||
templateColumns="repeat(auto-fill, minmax(250px, 1fr))"
|
||||
gap={6}
|
||||
p={4}
|
||||
>
|
||||
{customPlugins.map((plugin, index) => (
|
||||
<PluginCard
|
||||
key={index}
|
||||
plugin={plugin}
|
||||
isActive={activePlugin === index}
|
||||
onActivate={() => handleActivate(index)}
|
||||
onEdit={() => handleEdit(plugin)}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</ChakraProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginPage;
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
VStack,
|
||||
Text,
|
||||
Divider,
|
||||
Input,
|
||||
Textarea,
|
||||
Tag,
|
||||
TagLabel,
|
||||
TagCloseButton,
|
||||
HStack,
|
||||
Button,
|
||||
Box,
|
||||
} from '@chakra-ui/react';
|
||||
import EmojiPicker, { EmojiClickData, EmojiStyle } from 'emoji-picker-react';
|
||||
import { Plugin } from '../../../common/services/platform/platform';
|
||||
|
||||
type PluginBasicInfoProps = {
|
||||
plugin: Plugin;
|
||||
handleUpdateConfig: (config: Partial<Plugin>) => void;
|
||||
};
|
||||
|
||||
const PluginBasicInfo: React.FC<PluginBasicInfoProps> = ({
|
||||
plugin,
|
||||
handleUpdateConfig,
|
||||
}) => {
|
||||
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false);
|
||||
|
||||
const handleTagRemove = (tagToRemove: string) => {
|
||||
const updatedTags = plugin.tags.filter((tag) => tag !== tagToRemove);
|
||||
handleUpdateConfig({ tags: updatedTags });
|
||||
};
|
||||
|
||||
const handleTagAdd = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && e.currentTarget.value.trim()) {
|
||||
handleUpdateConfig({
|
||||
tags: [...plugin.tags, e.currentTarget.value.trim()],
|
||||
});
|
||||
e.currentTarget.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const onEmojiClick = (emojiObject: EmojiClickData) => {
|
||||
handleUpdateConfig({ icon: emojiObject.emoji });
|
||||
setIsEmojiPickerOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack spacing="4" align="start" width="100%">
|
||||
<Text fontSize="1xl" fontWeight="bold">
|
||||
插件基础信息
|
||||
</Text>
|
||||
<Divider />
|
||||
{plugin.author && <Text>作者: {plugin.author}</Text>}
|
||||
{plugin.type && <Text>插件类型: {plugin.type}</Text>}
|
||||
<HStack>
|
||||
<Input
|
||||
placeholder="插件标题"
|
||||
value={plugin.title}
|
||||
onChange={(e) => handleUpdateConfig({ title: e.target.value })}
|
||||
/>
|
||||
<Button onClick={() => setIsEmojiPickerOpen(!isEmojiPickerOpen)}>
|
||||
{plugin.icon || '😀'}
|
||||
</Button>
|
||||
{isEmojiPickerOpen && (
|
||||
<Box position="absolute" zIndex="1">
|
||||
<EmojiPicker
|
||||
onEmojiClick={onEmojiClick}
|
||||
emojiStyle={EmojiStyle.NATIVE}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<Textarea
|
||||
placeholder="描述"
|
||||
value={plugin.description}
|
||||
onChange={(e) => handleUpdateConfig({ description: e.target.value })}
|
||||
/>
|
||||
<VStack align="start" width="100%">
|
||||
<Text>标签:</Text>
|
||||
<HStack wrap="wrap">
|
||||
{plugin.tags.map((tag, idx) => (
|
||||
<Tag
|
||||
key={idx}
|
||||
size="md"
|
||||
borderRadius="full"
|
||||
variant="solid"
|
||||
colorScheme="teal"
|
||||
>
|
||||
<TagLabel>{tag}</TagLabel>
|
||||
<TagCloseButton onClick={() => handleTagRemove(tag)} />
|
||||
</Tag>
|
||||
))}
|
||||
</HStack>
|
||||
<Input placeholder="按 Enter 添加标签" onKeyDown={handleTagAdd} />
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginBasicInfo;
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
Text,
|
||||
Button,
|
||||
Divider,
|
||||
HStack,
|
||||
useDisclosure,
|
||||
AlertDialog,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
} from '@chakra-ui/react';
|
||||
import { FiSave } from 'react-icons/fi';
|
||||
import { RepeatIcon } from '@chakra-ui/icons';
|
||||
import Editor, { Monaco } from '@monaco-editor/react';
|
||||
import {
|
||||
PluginExampleCode,
|
||||
PluginExtraLib,
|
||||
} from '../../../common/utils/constants';
|
||||
|
||||
type PluginEditorProps = {
|
||||
code?: string;
|
||||
setCode: (code?: string) => void;
|
||||
handleSaveCode: (code?: string) => void;
|
||||
};
|
||||
|
||||
// 子组件:插件编辑页
|
||||
const PluginEditor = ({ code, setCode, handleSaveCode }: PluginEditorProps) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const cancelRef = useRef<any>();
|
||||
|
||||
const handleEditorWillMount = (monaco: Monaco) => {
|
||||
monaco.languages.registerCompletionItemProvider('javascript', {
|
||||
// @ts-ignore
|
||||
provideCompletionItems: () => {
|
||||
const suggestions = [
|
||||
{
|
||||
label: 'require',
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: 'require()',
|
||||
documentation: '引入模块',
|
||||
},
|
||||
{
|
||||
label: 'console',
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: 'console.log()',
|
||||
documentation: '打印日志',
|
||||
},
|
||||
];
|
||||
return { suggestions };
|
||||
},
|
||||
});
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
PluginExtraLib,
|
||||
'ts:filename/types.d.ts',
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack spacing="4" align="start" width="100%">
|
||||
<Text fontSize="1xl" fontWeight="bold">
|
||||
插件编辑页
|
||||
</Text>
|
||||
<Divider />
|
||||
<HStack>
|
||||
<Button
|
||||
leftIcon={<FiSave />}
|
||||
onClick={() => handleSaveCode()}
|
||||
colorScheme="teal"
|
||||
size="sm"
|
||||
>
|
||||
保存代码
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<RepeatIcon />}
|
||||
onClick={onOpen}
|
||||
colorScheme="red"
|
||||
size="sm"
|
||||
>
|
||||
重置代码
|
||||
</Button>
|
||||
</HStack>
|
||||
<Box width="100%" height="400px">
|
||||
<Editor
|
||||
height="100%"
|
||||
defaultLanguage="javascript"
|
||||
value={code}
|
||||
onChange={(value) => setCode(value)}
|
||||
beforeMount={handleEditorWillMount}
|
||||
theme="vs-dark"
|
||||
/>
|
||||
</Box>
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
重置代码
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogBody>
|
||||
你确定要重置代码吗?这将会清空当前的代码。
|
||||
</AlertDialogBody>
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="red"
|
||||
onClick={() => {
|
||||
setCode(PluginExampleCode);
|
||||
handleSaveCode(PluginExampleCode);
|
||||
onClose();
|
||||
}}
|
||||
ml={3}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginEditor;
|
||||
@@ -0,0 +1,311 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Input,
|
||||
Select,
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Th,
|
||||
Tr,
|
||||
Td,
|
||||
Divider,
|
||||
Text,
|
||||
Flex,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { RepeatIcon } from '@chakra-ui/icons';
|
||||
import { FiPlay } from 'react-icons/fi';
|
||||
import { checkPluginAvailability } from '../../../common/services/platform/controller';
|
||||
import { useSystemStore } from '../../stores/useSystemStore';
|
||||
import {
|
||||
Message,
|
||||
RoleType,
|
||||
MessageType,
|
||||
LogBody,
|
||||
} from '../../../common/services/platform/platform';
|
||||
import {
|
||||
ContextKeys,
|
||||
MockCtx,
|
||||
MockMessages,
|
||||
} from '../../../common/utils/constants';
|
||||
|
||||
const PluginTestPage = ({ code }: { code?: string }) => {
|
||||
const toast = useToast();
|
||||
const [consoleLogs, setConsoleLogs] = useState<LogBody[]>([]);
|
||||
const [newMessage, setNewMessage] = useState<Message>({
|
||||
sender: '',
|
||||
content: '',
|
||||
role: 'SELF',
|
||||
type: 'TEXT',
|
||||
});
|
||||
const [selectedContextKey, setSelectedContextKey] = useState<string>(
|
||||
ContextKeys[0],
|
||||
);
|
||||
const [contextValue, setContextValue] = useState<string>('');
|
||||
const { context, setContext, addMessage, messages, removeMessage } =
|
||||
useSystemStore();
|
||||
|
||||
const handleAddMessage = () => {
|
||||
addMessage(newMessage);
|
||||
setNewMessage({ sender: '', content: '', role: 'SELF', type: 'TEXT' });
|
||||
};
|
||||
|
||||
const handleSetContext = () => {
|
||||
setContext(selectedContextKey, contextValue);
|
||||
setContextValue('');
|
||||
};
|
||||
|
||||
const handleCheckPlugin = async () => {
|
||||
try {
|
||||
const resp = await checkPluginAvailability({
|
||||
code: code || '',
|
||||
ctx: context,
|
||||
messages,
|
||||
});
|
||||
setConsoleLogs(resp.consoleOutput || []);
|
||||
if (resp.status) {
|
||||
toast({
|
||||
title: '插件测试通过',
|
||||
position: 'top',
|
||||
description: resp.message,
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: '插件测试失败',
|
||||
position: 'top',
|
||||
description: resp.error,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast({
|
||||
title: '检查插件失败',
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetDefault = () => {
|
||||
// MockCtx 是一个 Map 对象
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [key, value] of MockCtx) {
|
||||
setContext(key, value);
|
||||
}
|
||||
|
||||
// 先清空所有消息
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
removeMessage(i);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const msg of MockMessages) {
|
||||
// @ts-ignore
|
||||
addMessage(msg);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack spacing="4" align="start" width="100%">
|
||||
<HStack>
|
||||
<Button
|
||||
leftIcon={<FiPlay />}
|
||||
onClick={handleCheckPlugin}
|
||||
colorScheme="green"
|
||||
>
|
||||
测试插件
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<RepeatIcon />}
|
||||
onClick={handleSetDefault}
|
||||
colorScheme="blue"
|
||||
>
|
||||
设置默认
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
<Flex width="100%" justifyContent="space-between">
|
||||
<Box width="48%">
|
||||
<FormControl>
|
||||
<FormLabel>测试上下文</FormLabel>
|
||||
<Select
|
||||
value={selectedContextKey}
|
||||
onChange={(e) => setSelectedContextKey(e.target.value)}
|
||||
>
|
||||
{ContextKeys.map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{key}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl mt={4}>
|
||||
<FormLabel>上下文的输入值</FormLabel>
|
||||
<Input
|
||||
value={contextValue}
|
||||
onChange={(e) => setContextValue(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
mt={4}
|
||||
onClick={handleSetContext}
|
||||
colorScheme="blue"
|
||||
size="sm"
|
||||
>
|
||||
设置上下文内容
|
||||
</Button>
|
||||
<Divider my={4} />
|
||||
<Table variant="simple">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>上下文主键</Th>
|
||||
<Th>上下文的值</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{Object.entries(context).map(([key, value], index) => (
|
||||
<Tr key={index}>
|
||||
<Td>{key}</Td>
|
||||
<Td>{value}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
<Box width="48%">
|
||||
<FormControl>
|
||||
<FormLabel>发送者</FormLabel>
|
||||
<Input
|
||||
value={newMessage.sender}
|
||||
onChange={(e) =>
|
||||
setNewMessage({ ...newMessage, sender: e.target.value })
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl mt={4}>
|
||||
<FormLabel>消息内容</FormLabel>
|
||||
<Input
|
||||
value={newMessage.content}
|
||||
onChange={(e) =>
|
||||
setNewMessage({ ...newMessage, content: e.target.value })
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<HStack mt={4}>
|
||||
<FormControl>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<Select
|
||||
value={newMessage.role}
|
||||
onChange={(e) =>
|
||||
setNewMessage({
|
||||
...newMessage,
|
||||
role: e.target.value as RoleType,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="SELF">自己的消息</option>
|
||||
<option value="OTHER">别人的消息</option>
|
||||
<option value="SYSTEM">系统消息</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>消息类型</FormLabel>
|
||||
<Select
|
||||
value={newMessage.type}
|
||||
onChange={(e) =>
|
||||
setNewMessage({
|
||||
...newMessage,
|
||||
type: e.target.value as MessageType,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="TEXT">文本</option>
|
||||
<option value="IMAGE">图片</option>
|
||||
<option value="VIDEO">视频</option>
|
||||
<option value="FILE">文件</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
<Button
|
||||
mt={4}
|
||||
onClick={handleAddMessage}
|
||||
colorScheme="blue"
|
||||
size="sm"
|
||||
>
|
||||
添加消息
|
||||
</Button>
|
||||
<Divider my={4} />
|
||||
<Box>
|
||||
{messages.map((msg, index) => (
|
||||
<HStack
|
||||
key={index}
|
||||
justify="space-between"
|
||||
mb={2}
|
||||
bg={msg.role === 'SELF' ? 'blue.100' : 'gray.100'}
|
||||
p={2}
|
||||
borderRadius="md"
|
||||
>
|
||||
<Box>
|
||||
<Text fontSize="sm" fontWeight="bold">
|
||||
{msg.sender}
|
||||
</Text>
|
||||
<Text fontSize="sm">{msg.content}</Text>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
({msg.role} - {msg.type})
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
onClick={() => removeMessage(index)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</HStack>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Divider />
|
||||
<Box width="100%">
|
||||
<HStack justify="space-between">
|
||||
<Text fontWeight="bold">查看日志</Text>
|
||||
<Button
|
||||
onClick={() => setConsoleLogs([])}
|
||||
colorScheme="red"
|
||||
size="sm"
|
||||
>
|
||||
清空日志
|
||||
</Button>
|
||||
</HStack>
|
||||
{consoleLogs && consoleLogs.length > 0 && (
|
||||
<Box height="200px" overflowY="auto" mt={2}>
|
||||
{consoleLogs.map((log, index) => (
|
||||
<Text key={index}>
|
||||
{log.level} {log.time} {log.message}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginTestPage;
|
||||
@@ -0,0 +1,326 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
VStack,
|
||||
Button,
|
||||
useToast,
|
||||
HStack,
|
||||
Stack,
|
||||
Skeleton,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Box,
|
||||
useDisclosure,
|
||||
AlertDialog,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
} from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
FiChevronLeft,
|
||||
FiShare2,
|
||||
FiTrash2,
|
||||
FiPlusCircle,
|
||||
} from 'react-icons/fi';
|
||||
import { PluginExampleCode } from '../../../common/utils/constants';
|
||||
import {
|
||||
getCustomPluginDetail,
|
||||
addCustomPlugin,
|
||||
updateCustomPlugin,
|
||||
deleteCustomPlugin,
|
||||
} from '../../../common/services/platform/controller';
|
||||
import { Plugin } from '../../../common/services/platform/platform';
|
||||
import PluginTestPage from './PluginTestPage';
|
||||
import PluginBasicInfo from './PluginBasicInfo';
|
||||
import PluginEditorCom from './PluginEditor';
|
||||
|
||||
type PluginEditorProps = {
|
||||
appId?: string;
|
||||
instanceId?: string;
|
||||
};
|
||||
|
||||
const PluginEditor = ({ appId, instanceId }: PluginEditorProps) => {
|
||||
const [code, setCode] = useState<string | undefined>(PluginExampleCode);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const cancelRef = useRef<any>();
|
||||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// 获取 navigate 传递的状态参数
|
||||
const { pluginId } = location.state || {};
|
||||
|
||||
const { data, isLoading } = useQuery(['pluginDetail', pluginId], async () => {
|
||||
try {
|
||||
if (!pluginId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resp = await getCustomPluginDetail(pluginId);
|
||||
return resp;
|
||||
} catch (error) {
|
||||
const errormsg =
|
||||
error instanceof Error ? error.message : JSON.stringify(error);
|
||||
toast({
|
||||
title: '获取插件失败',
|
||||
description: errormsg,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const [plugin, setPlugin] = useState<Plugin>({
|
||||
title: '新建插件',
|
||||
description: '这是一个自定义插件~',
|
||||
code: PluginExampleCode,
|
||||
icon: '😀',
|
||||
tags: [],
|
||||
type: 'plugin',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const obj = data.data as Plugin;
|
||||
setPlugin(obj);
|
||||
setCode(obj.code || PluginExampleCode);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const handleAddNewPlugin = async () => {
|
||||
try {
|
||||
await addCustomPlugin({
|
||||
...plugin,
|
||||
code: code || PluginExampleCode,
|
||||
});
|
||||
toast({
|
||||
title: '新增插件成功',
|
||||
position: 'top',
|
||||
description: '插件已添加',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} catch (error) {
|
||||
const errormsg =
|
||||
error instanceof Error ? error.message : JSON.stringify(error);
|
||||
toast({
|
||||
title: '新增插件失败',
|
||||
position: 'top',
|
||||
description: errormsg,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePlugin = async () => {
|
||||
if (!plugin || !plugin.id) return;
|
||||
try {
|
||||
await deleteCustomPlugin(plugin.id);
|
||||
toast({
|
||||
title: '删除插件成功',
|
||||
position: 'top',
|
||||
description: '插件已删除',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} catch (error) {
|
||||
const errormsg =
|
||||
error instanceof Error ? error.message : JSON.stringify(error);
|
||||
toast({
|
||||
title: '删除插件失败',
|
||||
position: 'top',
|
||||
description: errormsg,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateConfig = async (newConfig: Partial<Plugin>) => {
|
||||
if (!plugin) return;
|
||||
const updatedConfig = { ...plugin, ...newConfig };
|
||||
setPlugin(updatedConfig);
|
||||
try {
|
||||
await updateCustomPlugin(updatedConfig);
|
||||
} catch (error) {
|
||||
const errormsg =
|
||||
error instanceof Error ? error.message : JSON.stringify(error);
|
||||
toast({
|
||||
title: '更新插件失败',
|
||||
position: 'top',
|
||||
description: errormsg,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveCode = useCallback(
|
||||
async (inCode?: string) => {
|
||||
if (!plugin) return;
|
||||
try {
|
||||
await updateCustomPlugin({
|
||||
...plugin,
|
||||
code: inCode || code || PluginExampleCode,
|
||||
});
|
||||
toast({
|
||||
title: '代码已保存',
|
||||
position: 'top',
|
||||
description: '插件已更新',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} catch (error) {
|
||||
const errormsg =
|
||||
error instanceof Error ? error.message : JSON.stringify(error);
|
||||
toast({
|
||||
title: '更新插件失败',
|
||||
position: 'top',
|
||||
description: errormsg,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[code, plugin, toast],
|
||||
);
|
||||
|
||||
console.log('location 0002', location);
|
||||
|
||||
if (pluginId && (isLoading || !data || !plugin)) {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton height="20px" />
|
||||
<Skeleton height="20px" />
|
||||
<Skeleton height="20px" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
console.log('location 0003', location);
|
||||
|
||||
return (
|
||||
<VStack align="start" spacing="4" minHeight="100vh" position="relative">
|
||||
<Box position="fixed" top="10px" right="10px" zIndex={10}>
|
||||
<Button
|
||||
leftIcon={<FiChevronLeft />}
|
||||
colorScheme="teal"
|
||||
onClick={() => navigate('/settings.html')}
|
||||
>
|
||||
返回列表
|
||||
</Button>
|
||||
</Box>
|
||||
<Tabs width="70vw" flex="1">
|
||||
<TabList>
|
||||
<Tab>插件基础信息</Tab>
|
||||
<Tab>插件编辑</Tab>
|
||||
<Tab>测试插件</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<PluginBasicInfo
|
||||
plugin={plugin}
|
||||
handleUpdateConfig={handleUpdateConfig}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<PluginEditorCom
|
||||
code={code}
|
||||
setCode={setCode}
|
||||
handleSaveCode={handleSaveCode}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<PluginTestPage code={code} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<Box h={'30px'} />
|
||||
|
||||
<HStack
|
||||
spacing="4"
|
||||
position="fixed"
|
||||
bottom="0"
|
||||
width="100%"
|
||||
bg="white"
|
||||
p="4"
|
||||
boxShadow="md"
|
||||
>
|
||||
<Button
|
||||
leftIcon={<FiShare2 />}
|
||||
colorScheme="purple"
|
||||
onClick={() => {
|
||||
/* 发布插件到社区逻辑 */
|
||||
}}
|
||||
>
|
||||
发布社区
|
||||
</Button>
|
||||
{pluginId ? (
|
||||
<Button leftIcon={<FiTrash2 />} colorScheme="red" onClick={onOpen}>
|
||||
删除
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
leftIcon={<FiPlusCircle />}
|
||||
colorScheme="blue"
|
||||
onClick={handleAddNewPlugin}
|
||||
>
|
||||
新增
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
删除插件
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogBody>
|
||||
你确定要删除插件吗?这个操作不可逆。
|
||||
</AlertDialogBody>
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="red"
|
||||
onClick={async () => {
|
||||
await handleDeletePlugin();
|
||||
onClose();
|
||||
navigate('/settings.html');
|
||||
}}
|
||||
ml={3}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginEditor;
|
||||
Reference in New Issue
Block a user