feat: 导入导出工作流. close #IC59WP
| | |
| | | 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; |
| | |
| | | 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; |
| | | |
| | |
| | | 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) { |
| | |
| | | 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 { |
| | |
| | | import {useUrlParams} from "../../hooks/useUrlParams.ts"; |
| | | |
| | | export type CardPageProps = { |
| | | ref?: any, |
| | | tableAlias: string, |
| | | defaultPageSize?: number, |
| | | editModalTitle?: string, |
| | |
| | | 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 |
| | |
| | | , 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, |
| | |
| | | // 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) |
| | |
| | | |
| | | return ( |
| | | <> |
| | | |
| | | <EditPage modalTitle={editModalTitle || ""} |
| | | tableAlias={tableAlias} |
| | | open={isEditOpen} |
| | |
| | | </Spin> |
| | | </> |
| | | ) |
| | | }; |
| | | }) |
| | | |
| | | export default CardPage |
| | |
| | | }, 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"); |
| | |
| | | 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> = [ |
| | | { |
| | |
| | | |
| | | |
| | | 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={"新增工作流"} |
| | |
| | | <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>, |
| | | ] |
| | | }} |
| | | /> |
| | | </> |
| | | ) |