18586361686
2025-04-27 1e5a217f868026e5e3ecf3d09ebd36fe2208e8cd
feat: 新增插件工具前端页面和后端接口
1 文件已重命名
9个文件已添加
742 ■■■■■ 已修改文件
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiPluginToolController.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/AiPluginTool.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/base/AiPluginToolBase.java 183 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/mapper/AiPluginToolMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/service/AiPluginToolService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/service/impl/AiPluginToolServiceImpl.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/pages/ai/plugin/Plugin.tsx 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/pages/ai/plugin/PluginTool.tsx 224 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/pages/ai/plugin/PluginToolEdit.tsx 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/pages/ai/plugin/less/pluginToolEdit.less 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiPluginToolController.java
New file
@@ -0,0 +1,42 @@
package tech.aiflowy.ai.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.aiflowy.ai.entity.AiPluginTool;
import tech.aiflowy.ai.service.AiPluginToolService;
import tech.aiflowy.common.domain.Result;
import tech.aiflowy.common.web.controller.BaseCurdController;
import tech.aiflowy.common.web.jsonbody.JsonBody;
import javax.annotation.Resource;
/**
 *  控制层。
 *
 * @author WangGangqiang
 * @since 2025-04-27
 */
@RestController
@RequestMapping("/api/v1/aiPluginTool")
public class AiPluginToolController extends BaseCurdController<AiPluginToolService, AiPluginTool> {
    public AiPluginToolController(AiPluginToolService service) {
        super(service);
    }
    @Resource
    private AiPluginToolService aiPluginToolService;
    @PostMapping("/tool/save")
    public Result savePlugin(@JsonBody AiPluginTool aiPluginTool){
        return aiPluginToolService.savePluginTool(aiPluginTool);
    }
    // 插件工具修改页面查询
    @PostMapping("/tool/search")
    public Result searchPlugin(@JsonBody(value = "aiPluginToolId", required = true) String aiPluginToolId){
        return aiPluginToolService.searchPlugin(aiPluginToolId);
    }
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/AiPluginTool.java
New file
@@ -0,0 +1,15 @@
package tech.aiflowy.ai.entity;
import com.mybatisflex.annotation.Table;
import tech.aiflowy.ai.entity.base.AiPluginToolBase;
/**
 *  实体类。
 *
 * @author Administrator
 * @since 2025-04-27
 */
@Table("tb_ai_plugin_tool")
public class AiPluginTool extends AiPluginToolBase {
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/base/AiPluginToolBase.java
New file
@@ -0,0 +1,183 @@
package tech.aiflowy.ai.entity.base;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import java.io.Serializable;
import java.util.Date;
public class AiPluginToolBase implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 插件工具id
     */
    @Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "插件工具id")
    private Long id;
    /**
     * 插件id
     */
    @Column(comment = "插件id")
    private Long pluginId;
    /**
     * 名称
     */
    @Column(comment = "名称")
    private String name;
    /**
     * 描述
     */
    @Column(comment = "描述")
    private String description;
    /**
     * 基础路径
     */
    @Column(comment = "基础路径")
    private String basePath;
    /**
     * 创建时间
     */
    @Column(comment = "创建时间")
    private Date created;
    /**
     * 是否启用
     */
    @Column(comment = "是否启用")
    private Integer status;
    /**
     * 输入参数
     */
    @Column(comment = "输入参数")
    private String inputData;
    /**
     * 输出参数
     */
    @Column(comment = "输出参数")
    private String outputData;
    /**
     * 请求方式【Post, Get, Put, Delete】
     */
    @Column(comment = "请求方式【Post, Get, Put, Delete】")
    private String requestMethod;
    /**
     * 服务状态[0 下线 1 上线]
     */
    @Column(comment = "服务状态[0 下线 1 上线]")
    private int serviceStatus;
    /**
     * 调试状态【0失败 1成功】
     */
    @Column(comment = "调试状态【0失败 1成功】")
    private int debugStatus;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Long getPluginId() {
        return pluginId;
    }
    public void setPluginId(Long pluginId) {
        this.pluginId = pluginId;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public String getBasePath() {
        return basePath;
    }
    public void setBasePath(String basePath) {
        this.basePath = basePath;
    }
    public Date getCreated() {
        return created;
    }
    public void setCreated(Date created) {
        this.created = created;
    }
    public Integer getStatus() {
        return status;
    }
    public void setStatus(Integer status) {
        this.status = status;
    }
    public String getInputData() {
        return inputData;
    }
    public void setInputData(String inputData) {
        this.inputData = inputData;
    }
    public String getOutputData() {
        return outputData;
    }
    public void setOutputData(String outputData) {
        this.outputData = outputData;
    }
    public String getRequestMethod() {
        return requestMethod;
    }
    public void setRequestMethod(String requestMethod) {
        this.requestMethod = requestMethod;
    }
    public int getServiceStatus() {
        return serviceStatus;
    }
    public void setServiceStatus(int serviceStatus) {
        this.serviceStatus = serviceStatus;
    }
    public int getDebugStatus() {
        return debugStatus;
    }
    public void setDebugStatus(int debugStatus) {
        this.debugStatus = debugStatus;
    }
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/mapper/AiPluginToolMapper.java
New file
@@ -0,0 +1,14 @@
package tech.aiflowy.ai.mapper;
import com.mybatisflex.core.BaseMapper;
import tech.aiflowy.ai.entity.AiPluginTool;
/**
 *  映射层。
 *
 * @author Administrator
 * @since 2025-04-27
 */
public interface AiPluginToolMapper extends BaseMapper<AiPluginTool> {
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/service/AiPluginToolService.java
New file
@@ -0,0 +1,19 @@
package tech.aiflowy.ai.service;
import com.mybatisflex.core.service.IService;
import tech.aiflowy.ai.entity.AiPlugin;
import tech.aiflowy.ai.entity.AiPluginTool;
import tech.aiflowy.common.domain.Result;
/**
 *  服务层。
 *
 * @author WangGangqiang
 * @since 2025-04-27
 */
public interface AiPluginToolService extends IService<AiPluginTool> {
    Result savePluginTool(AiPluginTool aiPluginTool);
    Result searchPlugin(String aiPluginToolId);
}
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/service/impl/AiPluginToolServiceImpl.java
New file
@@ -0,0 +1,62 @@
package tech.aiflowy.ai.service.impl;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import tech.aiflowy.ai.entity.AiPlugin;
import tech.aiflowy.ai.entity.AiPluginTool;
import tech.aiflowy.ai.mapper.AiPluginMapper;
import tech.aiflowy.ai.mapper.AiPluginToolMapper;
import tech.aiflowy.ai.service.AiPluginToolService;
import org.springframework.stereotype.Service;
import tech.aiflowy.common.domain.Result;
import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
 *  服务层实现。
 *
 * @author WangGangqiang
 * @since 2025-04-27
 */
@Service
public class AiPluginToolServiceImpl extends ServiceImpl<AiPluginToolMapper, AiPluginTool>  implements AiPluginToolService{
    @Resource
    private AiPluginToolMapper aiPluginToolMapper;
    @Resource
    private AiPluginMapper aiPluginMapper;
    @Override
    public Result savePluginTool(AiPluginTool aiPluginTool) {
        aiPluginTool.setCreated(new Date());
        aiPluginTool.setRequestMethod("Post");
        int insert = aiPluginToolMapper.insert(aiPluginTool);
        if (insert <= 0){
            return Result.fail(1, "插入失败");
        }
        return Result.success();
    }
    @Override
    public Result searchPlugin(String aiPluginToolId) {
        //查询当前插件工具
        QueryWrapper queryAiPluginToolWrapper = QueryWrapper.create()
                .select("*")
                .from("tb_ai_plugin_tool")
                .where("id = ? ", aiPluginToolId);
        AiPluginTool aiPluginTool = aiPluginToolMapper.selectOneByQuery(queryAiPluginToolWrapper);
        // 查询当前的插件信息
        QueryWrapper queryAiPluginWrapper = QueryWrapper.create()
                .select("*")
                .from("tb_ai_plugin");
        AiPlugin aiPlugin = aiPluginMapper.selectOneByQuery(queryAiPluginWrapper);
        Map<String, Object> result = new HashMap<>();
        result.put("data", aiPluginTool);
        result.put("aiPlugin", aiPlugin);
        return Result.success(result);
    }
}
aiflowy-ui-react/src/pages/ai/plugin/Plugin.tsx
File was renamed from aiflowy-ui-react/src/pages/ai/Plugin.tsx
@@ -3,42 +3,39 @@
    DeleteOutlined,
    EditOutlined,
    EllipsisOutlined,
    MenuUnfoldOutlined,
    MinusCircleOutlined,
    PlusOutlined,
    SettingOutlined
    PlusOutlined
} from '@ant-design/icons';
import {
    Avatar,
    Button,
    Card,
    Checkbox,
    Col,
    Dropdown,
    Form,
    FormProps,
    Input,
    message,
    Modal,
    Radio,
    Row,
    Select,
    Space,
    message,
    Dropdown
    Space
} from 'antd';
import {useGetManual, usePage, usePostManual} from "../../hooks/useApis.ts";
import SearchForm from "../../components/AntdCrud/SearchForm.tsx";
import {ColumnsConfig} from "../../components/AntdCrud";
import {useBreadcrumbRightEl} from "../../hooks/useBreadcrumbRightEl.tsx";
import ImageUploader from "../../components/ImageUploader";
import {usePage, usePostManual} from "../../../hooks/useApis.ts";
import SearchForm from "../../../components/AntdCrud/SearchForm.tsx";
import {ColumnsConfig} from "../../../components/AntdCrud";
import {useBreadcrumbRightEl} from "../../../hooks/useBreadcrumbRightEl.tsx";
import ImageUploader from "../../../components/ImageUploader";
import TextArea from "antd/es/input/TextArea";
import {CheckboxGroupProps} from "antd/es/checkbox";
import {useNavigate} from "react-router-dom";
const actions: React.ReactNode[] = [
    <EditOutlined key="edit" />,
    <SettingOutlined key="setting" />,
    <EllipsisOutlined key="ellipsis" />,
];
const Plugin: React.FC = () => {
    const navigate = useNavigate();
    const columnsConfig: ColumnsConfig<any> = [
        {
            hidden: true,
@@ -250,6 +247,14 @@
                    <Col span={6}>
                        <Card  actions={
                            [
                                <MenuUnfoldOutlined title="工具列表" onClick={() => {
                                    navigate('/ai/pluginTool', {
                                        state: {
                                            id: item.id,
                                            pluginTitle: item.name
                                        }
                                    })
                                }}/>,
                                <EditOutlined key="edit" onClick={() =>{
                                    console.log('item')
                                    console.log(item)
@@ -273,7 +278,6 @@
                                    setAuthType(item.authType)
                                    setAddPluginIsOpen(true)
                                }} />,
                                <SettingOutlined key="setting" />,
                                <Dropdown menu={{
                                    items: [
                                        {
aiflowy-ui-react/src/pages/ai/plugin/PluginTool.tsx
New file
@@ -0,0 +1,224 @@
import React, {useEffect} from 'react';
import {Button, Form, FormProps, Input, message, Modal, Space, Table, TableProps, Tag} from 'antd';
import {useLocation, useNavigate} from "react-router-dom";
import {useLayout} from "../../../hooks/useLayout.tsx";
import {useBreadcrumbRightEl} from "../../../hooks/useBreadcrumbRightEl.tsx";
import {DeleteOutlined, EditOutlined, PlusOutlined} from "@ant-design/icons";
import TextArea from "antd/es/input/TextArea";
import {usePage, usePostManual} from "../../../hooks/useApis.ts";
import {convertDatetimeUtil} from "../../../libs/changeDatetimeUtil.tsx";
interface DataType {
    id: string;
    key: string;
    name: string;
    age: number;
    address: string;
    tags: string[];
}
const PluginTool: React.FC = () =>{
    type FieldType = {
        name?: string;
        description?: string;
    };
    const location = useLocation();
    // 获取路由参数 插件id
    const { id, pluginTitle } = location.state || {};
    // 创建表单实例
    const [form] = Form.useForm();
    const {setOptions} = useLayout();
    const navigate = useNavigate();
    // 控制创建工具模态框的显示与隐藏
    const [isAddPluginToolModalOpen, setAddPluginToolIsOpen] = React.useState(false);
    useBreadcrumbRightEl(<Button type={"primary"} onClick={() => {
        setAddPluginToolIsOpen(true);
    }}>
        <PlusOutlined/>创建工具</Button>)
    const {
        loading,
        result,
        doGet: doGetPage
    } = usePage('aiPluginTool', {}, {manual: true})
    const {doPost: doPostSavePluginTool} = usePostManual('/api/v1/aiPluginTool/tool/save')
    useEffect(() => {
        setOptions({
            showBreadcrumb: true,
            breadcrumbs: [
                {title: '首页'},
                {title: '插件', href: `/ai/plugin`},
                {title: pluginTitle},
            ],
        })
        doGetPage({
            params: {
                pageNumber: 1,
                pageSize: 10,
            }
        })
        return () => {
            setOptions({
                showBreadcrumb: true,
                breadcrumbs: []
            })
        }
    }, [])
    const columns: TableProps<DataType>['columns'] = [
        {
            title: 'id',
            dataIndex: 'id',
            key: 'id',
            hidden: true
        },
        {
            title: '工具名称',
            dataIndex: 'name',
            key: 'name'
        },
        {
            title: '输入参数',
            dataIndex: 'inputParams',
            key: 'inputParams',
        },
        {
            title: '调试状态',
            dataIndex: 'debugStatus',
            key: 'debugStatus',
            render: (item: number) =>{
                if (item === 0){
                    return  <Tag color="error">失败</Tag>
                }  else if(item === 1){
                    return  <Tag color="success">成功</Tag>
                }
            }
        },
        {
            title: '创建时间',
            dataIndex: 'created',
            key: 'created',
            render:convertDatetimeUtil
        },
        {
            title: '操作',
            key: 'action',
            render: (_:any, record:DataType) => (
                <Space size="middle">
                    <EditOutlined onClick={() =>{
                        navigate('/ai/pluginToolEdit', {
                            state: {
                                id: record.id,
                                // 插件名称
                                pluginTitle:pluginTitle,
                                // 插件工具名称
                                pluginToolTitle: record.name,
                                title: record.name
                            }
                        })
                    }}/>
                    <DeleteOutlined onClick={() =>{
                    }}/>
                </Space>
            ),
        },
    ];
    const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
        doPostSavePluginTool({
            data: {
                pluginId: id,
                name: values.name,
                description: values.description
            }
        }).then(r =>{
            if (r.data.errorCode == 0){
                message.success("创建成功!")
                form.resetFields()
                setAddPluginToolIsOpen(false)
                doGetPage({
                    params: {
                        pageNumber: 1,
                        pageSize: 10,
                    }
                })
            } else if (r.data.errorCode >= 1){
                message.error("创建失败!")
            }
        })
    };
    const onFinishFailed: FormProps<FieldType>['onFinishFailed'] = (errorInfo) => {
        console.log('Failed:', errorInfo);
    };
    const handleAddPluginToolOk = () => {
        // setAddPluginToolIsOpen(false);
    };
    const handleAddPluginToolCancel = () => {
        setAddPluginToolIsOpen(false);
    };
    return (
        <div>
            <Table<DataType> columns={columns} dataSource={result?.data?.records} />
            <Modal title="创建工具" open={isAddPluginToolModalOpen} onOk={handleAddPluginToolOk}
                   onCancel={handleAddPluginToolCancel}
                   footer={null}
            >
                <Form
                    form={form}
                    name="basic"
                    layout="vertical"
                    style={{ maxWidth: 600 }}
                    initialValues={{ remember: true }}
                    onFinish={onFinish}
                    onFinishFailed={onFinishFailed}
                    autoComplete="off"
                >
                    <Form.Item<FieldType>
                        label="工具名称"
                        name="name"
                        rules={[{ required: true, message: '请输入工具名称' }]}
                    >
                        <Input maxLength={40} showCount placeholder={'请输入工具名称'}/>
                    </Form.Item>
                    <Form.Item<FieldType>
                        label="描述"
                        name="description"
                        rules={[{ required: true, message: '请输入工具描述' }]}
                    >
                        <TextArea
                            showCount
                            maxLength={500}
                            placeholder="请输入工具描述"
                            style={{ height: 80, resize: 'none' }}
                        />
                    </Form.Item>
                    <Form.Item label={null}>
                        <Space style={{ display: 'flex', justifyContent: 'flex-end' }} >
                            {/* 取消按钮 */}
                            <Button onClick={handleAddPluginToolCancel}>取消</Button>
                            {/* 确定按钮 */}
                            <Button type="primary" htmlType="submit" style={{ marginRight: 8 }}>
                                确定
                            </Button>
                        </Space>
                    </Form.Item>
                </Form>
            </Modal>
        </div>
    );
};
export default {
    path: "/ai/pluginTool",
    element: PluginTool
};
aiflowy-ui-react/src/pages/ai/plugin/PluginToolEdit.tsx
New file
@@ -0,0 +1,135 @@
import React, {useEffect, useState} from 'react';
import { useLayout } from "../../../hooks/useLayout.tsx";
import { useLocation } from "react-router-dom";
import { Collapse, Spin } from "antd";
import { usePost } from "../../../hooks/useApis.ts";
import './less/pluginToolEdit.less'
import {EditOutlined, SettingOutlined} from "@ant-design/icons";
const text = `
  A dog is a type of domesticated animal.
  Known for its loyalty and faithfulness,
  it can be found as a welcome guest in many households across the world.
`
const PluginToolEdit: React.FC = () => {
    const { setOptions } = useLayout();
    const location = useLocation();
    const { id, pluginTitle, pluginToolTitle } = location.state || {};
    const { result: pluginToolInfo, doPost: doPostSearch, loading } = usePost('/api/v1/aiPluginTool/tool/search');
    const [showLoading, setShowLoading] = useState(true);
    useEffect(() => {
        setOptions({
            showBreadcrumb: true,
            breadcrumbs: [
                { title: '首页' },
                { title: '插件', href: `/ai/plugin` },
                { title: pluginTitle, href: `/ai/plugin` },
                { title: pluginToolTitle, href: `/ai/pluginTool` },
                { title: '修改' },
            ],
        });
        doPostSearch({
            data: {
                aiPluginToolId: id
            }
        }).then(() => {
            // 即使数据加载完成,仍然保持 loading 状态 1 秒
            setTimeout(() => {
                setShowLoading(false);
            }, 1000); // 1000ms = 1秒
        });
        return () => {
            setOptions({
                showBreadcrumb: true,
                breadcrumbs: []
            });
        };
    }, []);
    const onChange = (key: string | string[]) => {
        console.log(key);
    };
    // 如果正在加载,直接返回 null,不渲染任何内容
    if (showLoading) {
        return  (
            <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
                <Spin size="large"/>
            </div>
        )
    }
    const editPluginTool = (index: string) => (
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }} onClick={(event) => {
            // If you don't want click extra trigger collapse, you can prevent this:
            event.stopPropagation();
        }}>
            <EditOutlined/>
            <span>编辑</span>
        </div>
    );
    const collapseItems = [
        {
            key: '1',
            label: '基本信息',
            children: (
                <div className="basic-info">
                    <div>工具名称:</div>
                    <p>{pluginToolInfo?.data?.data?.name}</p>
                    <div>工具描述:</div>
                    <p>{pluginToolInfo?.data?.data?.description}</p>
                    <div>工具路径:</div>
                    {pluginToolInfo?.data?.data?.basePath ? (
                        <p>{pluginToolInfo?.data?.aiPlugin.baseUrl}/{pluginToolInfo?.data?.data?.basePath}</p>
                    ) : (
                        <p>{pluginToolInfo?.data?.aiPlugin.baseUrl}/{pluginToolInfo?.data?.data?.name}</p>
                    )}
                    <div>请求方法:</div>
                    <p>{pluginToolInfo?.data?.data.requestMethod}</p>
                </div>
            ),
            extra: editPluginTool('1')
        },
        {
            key: '2',
            label: '配置输入参数',
            children: <p>{text}</p>,
            extra: editPluginTool('2')
        },
        {
            key: '3',
            label: '配置输出参数',
            children: <p>{text}</p>,
            extra: editPluginTool('3')
        },
    ];
    return (
        <div style={{ backgroundColor: '#F5F5F5' }}>
            <Collapse
                bordered={false}
                defaultActiveKey={['1', '2', '3']}
                onChange={onChange}
                items={collapseItems.map(item => ({
                    ...item,
                    style: {
                        header: { backgroundColor: '#F7F7FA' },
                        body: { backgroundColor: '#F5F5F5' },
                    },
                }))}
            />
        </div>
    );
};
export default {
    path: "/ai/pluginToolEdit",
    element: PluginToolEdit
};
aiflowy-ui-react/src/pages/ai/plugin/less/pluginToolEdit.less
New file
@@ -0,0 +1,10 @@
.basic-info div{
  color: #383743;
  font-weight: bold;
  font-size: 14px;
}
.basic-info p{
  color: #383743;
  font-size: 14px;
}