zhangjinyang
2025-05-08 422caea710dff865acd62cf95c38f9e50ef92bf4
feat: 导入导出工作流. close #IC59WP
4个文件已修改
181 ■■■■■ 已修改文件
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiWorkflowController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/components/CardPage/index.tsx 43 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/hooks/useApis.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/pages/ai/Workflow.tsx 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiWorkflowController.java
@@ -1,6 +1,8 @@
package tech.aiflowy.ai.controller;
import cn.hutool.core.io.IoUtil;
import dev.tinyflow.core.Tinyflow;
import org.springframework.web.multipart.MultipartFile;
import tech.aiflowy.ai.entity.AiWorkflow;
import tech.aiflowy.ai.service.AiKnowledgeService;
import tech.aiflowy.ai.service.AiLlmService;
@@ -13,7 +15,9 @@
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
@@ -38,6 +42,14 @@
        this.aiLlmService = aiLlmService;
    }
    @PostMapping("/importWorkFlow")
    public Result importWorkFlow(AiWorkflow workflow, MultipartFile jsonFile) throws Exception {
        InputStream is = jsonFile.getInputStream();
        String content = IoUtil.read(is, StandardCharsets.UTF_8);
        workflow.setContent(content);
        save(workflow);
        return Result.success();
    }
    @GetMapping("getRunningParameters")
    public Result getRunningParameters(@RequestParam BigInteger id) {
aiflowy-ui-react/src/components/CardPage/index.tsx
@@ -1,4 +1,4 @@
import React, {useEffect, useState} from 'react';
import React, {forwardRef, useEffect, useImperativeHandle, useState} from 'react';
import {ColumnsConfig} from "../AntdCrud";
import {Avatar, Button, Card, Col, Dropdown, Modal, Pagination, Row, Spin, Tooltip} from "antd";
import {
@@ -17,6 +17,7 @@
import {useUrlParams} from "../../hooks/useUrlParams.ts";
export type CardPageProps = {
    ref?: any,
    tableAlias: string,
    defaultPageSize?: number,
    editModalTitle?: string,
@@ -27,10 +28,11 @@
    defaultAvatarSrc?: string,
    titleKey?: string,
    descriptionKey?: string,
    customActions?: (data: any, existNodes: React.ReactNode[]) => React.ReactNode[]
    customActions?: (data: any, existNodes: React.ReactNode[]) => React.ReactNode[],
    customHandleButton?:() => React.ReactNode[],
}
const CardPage: React.FC<CardPageProps> = ({
const CardPage: React.FC<CardPageProps> = forwardRef(({
                                               tableAlias
                                               , defaultPageSize = 12
                                               , editModalTitle
@@ -41,8 +43,21 @@
                                               , defaultAvatarSrc
                                               , titleKey = "title"
                                               , descriptionKey = "description"
                                               , customActions = (_data, existNodes) => existNodes,
                                           }) => {
                                               , customActions = (_data: any, existNodes: any) => existNodes
                                               , customHandleButton = () => []
                                           },ref) => {
    useImperativeHandle(ref, () => ({
        refresh: () => {
            doGet({
                params: {
                    ...searchParams,
                    pageNumber: localPageNumber,
                    pageSize,
                }
            });
        }
    }));
    const {
        loading,
@@ -66,8 +81,19 @@
    // const [sortKey, setSortKey] = useState<string | undefined>()
    // const [sortType, setSortType] = useState<"asc" | "desc" | undefined>()
    useBreadcrumbRightEl(<Button type={"primary"} onClick={() => setIsEditOpen(true)}>
        <PlusOutlined/>{addButtonText}</Button>)
    useBreadcrumbRightEl(
        <>
            <div>
                {customHandleButton().map((item, index) =>
                    (<div key={index}
                          style={{display: "inline-block", marginRight: "5px", marginBottom: "5px"}}>{item}</div>))
                }
                <Button type={"primary"} onClick={() => setIsEditOpen(true)}>
                    <PlusOutlined/>{addButtonText}
                </Button>
            </div>
        </>
    )
    const closeEdit = () => {
        setIsEditOpen(false)
@@ -88,7 +114,6 @@
    return (
        <>
            <EditPage modalTitle={editModalTitle || ""}
                      tableAlias={tableAlias}
                      open={isEditOpen}
@@ -187,6 +212,6 @@
            </Spin>
        </>
    )
};
})
export default CardPage
aiflowy-ui-react/src/hooks/useApis.ts
@@ -173,6 +173,20 @@
    }, options)
}
export const usePostFile = (url: string, options?: Options) => {
    const [{loading},doPost] = useAxios({
        url: url,
        method: "POST",
        headers: {
            "Content-Type": "multipart/form-data"
        },
        ...options
    });
    return {
        loading,
        doPost
    }
}
export const useUpload = () => {
    const result = usePost("/api/v1/commons/upload");
aiflowy-ui-react/src/pages/ai/Workflow.tsx
@@ -1,10 +1,13 @@
import React from 'react';
import React, {useRef, useState} from 'react';
import {
    NodeIndexOutlined,
    DownloadOutlined,
    NodeIndexOutlined, UploadOutlined,
} from "@ant-design/icons";
import CardPage from "../../components/CardPage";
import {ColumnsConfig} from "../../components/AntdCrud";
import {Button, Form, Input, message, Modal, Upload} from "antd";
import TextArea from "antd/es/input/TextArea";
import {usePostFile} from "../../hooks/useApis.ts";
const columnsColumns: ColumnsConfig<any> = [
    {
@@ -41,9 +44,102 @@
const Workflow: React.FC<{ paramsToUrl: boolean }> = () => {
    const cardPageRef = useRef<any>(null);
    function exportWorkflow(item: any) {
        const filename = item.title + ".json";
        const text= item.content;
        const element = document.createElement('a');
        element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
        element.setAttribute('download', filename);
        element.style.display = 'none';
        document.body.appendChild(element);
        element.click();
        document.body.removeChild(element);
        message.success(`导出成功,请等待下载`);
    }
    const [importForm] = Form.useForm();
    const [isModalOpen, setIsModalOpen] = useState(false)
    const {loading: confirmLoading,doPost: postFile} = usePostFile("/api/v1/aiWorkflow/importWorkFlow")
    const handleOk = () => {
        importForm.validateFields().then((values:any) => {
            const formData = new FormData();
            formData.append('jsonFile', values.jsonFile.file);
            formData.append("title", values.title)
            formData.append("description", values.description??"")
            postFile({
                data:  formData
            }).then((res) => {
                if (res.data.errorCode === 0) {
                    handleCancel()
                    message.success(`导入成功`);
                    if (cardPageRef.current) {
                        cardPageRef.current.refresh();
                    }
                } else {
                    message.error(`导入失败`);
                }
            })
        }).catch(err => {
            console.log(err)
        })
    }
    const handleCancel = () => {
        importForm.resetFields()
        setIsModalOpen(false)
    }
    const beforeUpload = () => {
        // 返回 false 阻止自动上传
        return false;
    };
    return (
        <>
            <CardPage tableAlias={"aiWorkflow"}
            <Modal title="导入工作流"
                   open={isModalOpen}
                   onOk={handleOk}
                   onCancel={handleCancel}
                   confirmLoading={confirmLoading}
            >
                <Form
                    form={importForm}
                    name="basic"
                    labelCol={{ span: 5 }}
                    wrapperCol={{ span: 16 }}
                    autoComplete="off"
                >
                    <Form.Item
                        label="名称"
                        name="title"
                        rules={[{ required: true, message: '请输入工作流名称' }]}
                    >
                        <Input />
                    </Form.Item>
                    <Form.Item
                        label="描述"
                        name="description"
                    >
                        <TextArea rows={4} />
                    </Form.Item>
                    <Form.Item
                        label="文件"
                        name="jsonFile"
                        rules={[{ required: true, message: '请选择工作流文件' }]}
                    >
                        <Upload
                            name="jsonFile"
                            beforeUpload={beforeUpload}
                            maxCount={1}
                            accept=".json" // 可以指定接受的文件类型
                        >
                            <Button icon={<UploadOutlined />}>选择文件</Button>
                        </Upload>
                    </Form.Item>
                </Form>
            </Modal>
            <CardPage ref={cardPageRef}
                      tableAlias={"aiWorkflow"}
                      editModalTitle={"新增/编辑工作流"}
                      columnsConfig={columnsColumns}
                      addButtonText={"新增工作流"}
@@ -54,9 +150,17 @@
                              <NodeIndexOutlined title="设计工作流" onClick={() => {
                                  window.open(`/ai/workflow/design/${item.id}`, "_blank")
                              }}/>,
                              <DownloadOutlined title="导出工作流" onClick={() => {
                                  exportWorkflow(item)
                              }} />,
                              ...existNodes
                          ]
                      }}
                      customHandleButton={() => {
                          return [
                              <Button type={"primary"} onClick={() => {setIsModalOpen(true)}}><UploadOutlined />导入工作流</Button>,
                          ]
                      }}
            />
        </>
    )