From 2b84533c1124b716591ec7ff6f2557776b389745 Mon Sep 17 00:00:00 2001
From: heheer <1239331448@qq.com>
Date: Fri, 10 Jan 2025 12:08:52 +0800
Subject: [PATCH] feat: create app by json
---
packages/global/core/app/utils.ts | 19 +
.../web/components/common/Icon/constants.ts | 1 +
.../Icon/icons/core/app/type/jsonImport.svg | 19 +
.../common/Textarea/DragEditor/index.tsx | 140 +++++++
packages/web/i18n/en/app.json | 5 +
packages/web/i18n/en/common.json | 1 +
packages/web/i18n/zh-CN/app.json | 5 +
packages/web/i18n/zh-CN/common.json | 1 +
packages/web/i18n/zh-Hant/app.json | 5 +
packages/web/i18n/zh-Hant/common.json | 1 +
.../detail/components/SimpleApp/AppCard.tsx | 12 +
.../components/WorkflowComponents/AppCard.tsx | 62 ++--
.../Flow/ImportSettings.tsx | 125 +------
.../Flow/nodes/NodeHttp/CurlImportModal.tsx | 53 ++-
.../pages/app/list/components/CreateModal.tsx | 308 +++++++++-------
.../app/list/components/JsonImportModal.tsx | 176 +++++++++
.../list/components/TemplateMarketModal.tsx | 11 +-
projects/app/src/pages/app/list/index.tsx | 17 +
.../web/common/file/hooks/useSelectFile.tsx | 1 -
projects/app/src/web/core/app/templates.ts | 341 ++++++++++++++++++
projects/app/src/web/core/app/utils.ts | 8 +
21 files changed, 1026 insertions(+), 285 deletions(-)
create mode 100644 packages/web/components/common/Icon/icons/core/app/type/jsonImport.svg
create mode 100644 packages/web/components/common/Textarea/DragEditor/index.tsx
create mode 100644 projects/app/src/pages/app/list/components/JsonImportModal.tsx
diff --git a/packages/global/core/app/utils.ts b/packages/global/core/app/utils.ts
index c574cac5f90b..61d145347d0f 100644
--- a/packages/global/core/app/utils.ts
+++ b/packages/global/core/app/utils.ts
@@ -5,6 +5,8 @@ import type { FlowNodeInputItemType } from '../workflow/type/io.d';
import { getAppChatConfig } from '../workflow/utils';
import { StoreNodeItemType } from '../workflow/type/node';
import { DatasetSearchModeEnum } from '../dataset/constants';
+import { WorkflowTemplateBasicType } from 'core/workflow/type';
+import { AppTypeEnum } from './constants';
export const getDefaultAppForm = (): AppSimpleEditFormType => {
return {
@@ -127,3 +129,20 @@ export const appWorkflow2Form = ({
return defaultAppForm;
};
+
+export const getAppType = (config?: WorkflowTemplateBasicType | AppSimpleEditFormType) => {
+ if (!config) return '';
+
+ if ('aiSettings' in config) {
+ return AppTypeEnum.simple;
+ }
+
+ if (!('nodes' in config)) return '';
+ if (config.nodes.some((node) => node.flowNodeType === 'workflowStart')) {
+ return AppTypeEnum.workflow;
+ }
+ if (config.nodes.some((node) => node.flowNodeType === 'pluginInput')) {
+ return AppTypeEnum.plugin;
+ }
+ return '';
+};
diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts
index e7e87d5cd19e..01745bf14b34 100644
--- a/packages/web/components/common/Icon/constants.ts
+++ b/packages/web/components/common/Icon/constants.ts
@@ -142,6 +142,7 @@ export const iconPaths = {
'core/app/ttsFill': () => import('./icons/core/app/ttsFill.svg'),
'core/app/type/httpPlugin': () => import('./icons/core/app/type/httpPlugin.svg'),
'core/app/type/httpPluginFill': () => import('./icons/core/app/type/httpPluginFill.svg'),
+ 'core/app/type/jsonImport': () => import('./icons/core/app/type/jsonImport.svg'),
'core/app/type/plugin': () => import('./icons/core/app/type/plugin.svg'),
'core/app/type/pluginFill': () => import('./icons/core/app/type/pluginFill.svg'),
'core/app/type/pluginLight': () => import('./icons/core/app/type/pluginLight.svg'),
diff --git a/packages/web/components/common/Icon/icons/core/app/type/jsonImport.svg b/packages/web/components/common/Icon/icons/core/app/type/jsonImport.svg
new file mode 100644
index 000000000000..8ec2dc5847fd
--- /dev/null
+++ b/packages/web/components/common/Icon/icons/core/app/type/jsonImport.svg
@@ -0,0 +1,19 @@
+
\ No newline at end of file
diff --git a/packages/web/components/common/Textarea/DragEditor/index.tsx b/packages/web/components/common/Textarea/DragEditor/index.tsx
new file mode 100644
index 000000000000..4ad814af7144
--- /dev/null
+++ b/packages/web/components/common/Textarea/DragEditor/index.tsx
@@ -0,0 +1,140 @@
+import React, { DragEvent, useCallback, useState } from 'react';
+import { Box, Button, Flex, Textarea } from '@chakra-ui/react';
+import { useTranslation } from 'next-i18next';
+import { useSystem } from '../../../../hooks/useSystem';
+import MyIcon from '../../Icon';
+import { useToast } from '../../../../hooks/useToast';
+
+type Props = {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+ rows?: number;
+ File?: ({ onSelect }: { onSelect: (e: File[], sign?: any) => void }) => React.JSX.Element;
+ onOpen?: () => void;
+};
+
+const DragEditor = ({ value, onChange, placeholder, rows = 16, File, onOpen }: Props) => {
+ const { t } = useTranslation();
+ const { isPc } = useSystem();
+ const { toast } = useToast();
+ const [isDragging, setIsDragging] = useState(false);
+
+ const handleDragEnter = useCallback((e: DragEvent) => {
+ e.preventDefault();
+ setIsDragging(true);
+ }, []);
+
+ const handleDragLeave = useCallback((e: DragEvent) => {
+ e.preventDefault();
+ setIsDragging(false);
+ }, []);
+
+ const readJSONFile = useCallback(
+ (file: File) => {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ if (!file.name.endsWith('.json')) {
+ toast({
+ title: t('app:not_json_file'),
+ status: 'error'
+ });
+ return;
+ }
+ if (e.target) {
+ const res = JSON.parse(e.target.result as string);
+ onChange(JSON.stringify(res, null, 2));
+ }
+ };
+ reader.readAsText(file);
+ },
+ [t, toast]
+ );
+
+ const onSelectFile = useCallback(
+ async (e: File[]) => {
+ const file = e[0];
+ readJSONFile(file);
+ },
+ [readJSONFile]
+ );
+
+ const handleDrop = useCallback(
+ async (e: DragEvent) => {
+ e.preventDefault();
+ const file = e.dataTransfer.files[0];
+ console.log(file);
+ readJSONFile(file);
+ setIsDragging(false);
+ },
+ [readJSONFile]
+ );
+
+ return (
+ <>
+
+ {isDragging ? (
+ e.preventDefault()}
+ onDrop={handleDrop}
+ onDragLeave={handleDragLeave}
+ >
+
+
+
+ {t('app:file_recover')}
+
+
+
+ ) : (
+
+
+
+ {t('common:common.json_config')}
+
+
+
+ e.preventDefault()}
+ onDrop={handleDrop}
+ onDragLeave={handleDragLeave}
+ >
+
+
+ )}
+
+ {File && }
+ >
+ );
+};
+
+export default React.memo(DragEditor);
diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json
index d206af376447..e0445df335a8 100644
--- a/packages/web/i18n/en/app.json
+++ b/packages/web/i18n/en/app.json
@@ -69,6 +69,7 @@
"interval.6_hours": "Every 6 Hours",
"interval.per_hour": "Every Hour",
"intro": "A comprehensive model application orchestration system that offers out-of-the-box data processing and model invocation capabilities. It allows for rapid Dataset construction and workflow orchestration through Flow visualization, enabling complex Dataset scenarios!",
+ "invalid_json_format": "JSON format error",
"llm_not_support_vision": "This model does not support image recognition",
"llm_use_vision": "Vision",
"llm_use_vision_tip": "After clicking on the model selection, you can see whether the model supports image recognition and the ability to control whether to start image recognition. \nAfter starting image recognition, the model will read the image content in the file link, and if the user question is less than 500 words, it will automatically parse the image in the user question.",
@@ -94,6 +95,7 @@
"open_vision_function_tip": "Models with icon switches have image recognition capabilities. \nAfter being turned on, the model will parse the pictures in the file link and automatically parse the pictures in the user's question (user question ≤ 500 words).",
"or_drag_JSON": "or drag in JSON file",
"paste_config": "Paste Configuration",
+ "paste_config_or_drag": "Paste config or drag JSON file here",
"permission.des.manage": "Based on write permissions, you can configure publishing channels, view conversation logs, and assign permissions to the application.",
"permission.des.read": "Use the app to have conversations",
"permission.des.write": "Can view and edit apps",
@@ -150,9 +152,12 @@
"type.Create workflow bot": "Create Workflow",
"type.Create workflow tip": "Build complex multi-turn dialogue AI applications through low-code methods, recommended for advanced users.",
"type.Http plugin": "HTTP Plugin",
+ "type.Import from json": "Import JSON",
+ "type.Import from json tip": "Create an application with corresponding properties by importing a JSON file",
"type.Plugin": "Plugin",
"type.Simple bot": "Simple App",
"type.Workflow bot": "Workflow",
+ "type_not_recognized": "App type not recognized",
"upload_file_max_amount": "Maximum File Quantity",
"upload_file_max_amount_tip": "Maximum number of files uploaded in a single round of conversation",
"variable.select type_desc": "You can define a global variable that does not need to be filled in by the user.\n\nThe value of this variable can come from the API interface, the Query of the shared link, or assigned through the [Variable Update] module.",
diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json
index f36a3ea8aa3f..79e04a16253c 100644
--- a/packages/web/i18n/en/common.json
+++ b/packages/web/i18n/en/common.json
@@ -285,6 +285,7 @@
"core.app.Config schedule plan": "Configure Scheduled Execution",
"core.app.Config whisper": "Configure Voice Input",
"core.app.Config_auto_execute": "Click to configure automatic execution rules",
+ "core.app.Create app by curl": "Create by curl",
"core.app.Interval timer config": "Scheduled Execution Configuration",
"core.app.Interval timer run": "Scheduled Execution",
"core.app.Interval timer tip": "Can Execute App on Schedule",
diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json
index 4124839cb25b..d0aef585d3ed 100644
--- a/packages/web/i18n/zh-CN/app.json
+++ b/packages/web/i18n/zh-CN/app.json
@@ -69,6 +69,7 @@
"interval.6_hours": "每6小时",
"interval.per_hour": "每小时",
"intro": "是一个大模型应用编排系统,提供开箱即用的数据处理、模型调用等能力,可以快速的构建知识库并通过 Flow 可视化进行工作流编排,实现复杂的知识库场景!",
+ "invalid_json_format": "JSON 格式错误",
"llm_not_support_vision": "该模型不支持图片识别",
"llm_use_vision": "图片识别",
"llm_use_vision_tip": "点击模型选择后,可以看到模型是否支持图片识别以及控制是否启动图片识别的能力。启动图片识别后,模型会读取文件链接里图片内容,并且如果用户问题少于 500 字,会自动解析用户问题中的图片。",
@@ -94,6 +95,7 @@
"open_vision_function_tip": "有图示开关的模型即拥有图片识别能力。若开启,模型会解析文件链接里的图片,并自动解析用户问题中的图片(用户问题≤500字时生效)。",
"or_drag_JSON": "或拖入JSON文件",
"paste_config": "粘贴配置",
+ "paste_config_or_drag": "粘贴配置或拖入 JSON 文件",
"permission.des.manage": "写权限基础上,可配置发布渠道、查看对话日志、分配该应用权限",
"permission.des.read": "可使用该应用进行对话",
"permission.des.write": "可查看和编辑应用",
@@ -150,9 +152,12 @@
"type.Create workflow bot": "创建工作流",
"type.Create workflow tip": "通过低代码的方式,构建逻辑复杂的多轮对话 AI 应用,推荐高级玩家使用",
"type.Http plugin": "HTTP 插件",
+ "type.Import from json": "导入 JSON 配置",
+ "type.Import from json tip": "通过粘贴或导入 JSON 文件,直接创建对应属性的应用",
"type.Plugin": "插件",
"type.Simple bot": "简易应用",
"type.Workflow bot": "工作流",
+ "type_not_recognized": "未识别到应用类型",
"upload_file_max_amount": "最大文件数量",
"upload_file_max_amount_tip": "单轮对话中最大上传文件数量",
"variable.select type_desc": "可以为工作流定义全局变量,常用临时缓存。赋值的方式包括:\n1. 从对话页面的 query 参数获取。\n2. 通过 API 的 variables 对象传递。\n3. 通过【变量更新】节点进行赋值。",
diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json
index 7aca2c892cd2..c13f73f7c5a3 100644
--- a/packages/web/i18n/zh-CN/common.json
+++ b/packages/web/i18n/zh-CN/common.json
@@ -288,6 +288,7 @@
"core.app.Config schedule plan": "配置定时执行",
"core.app.Config whisper": "配置语音输入",
"core.app.Config_auto_execute": "点击配置自动执行规则",
+ "core.app.Create app by curl": "通过 curl 创建",
"core.app.Interval timer config": "定时执行配置",
"core.app.Interval timer run": "定时执行",
"core.app.Interval timer tip": "可定时执行应用",
diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json
index fa0bbd8c0767..b5254b8192cb 100644
--- a/packages/web/i18n/zh-Hant/app.json
+++ b/packages/web/i18n/zh-Hant/app.json
@@ -69,6 +69,7 @@
"interval.6_hours": "每 6 小時",
"interval.per_hour": "每小時",
"intro": "FastGPT 是一個基於大型語言模型的知識庫平臺,提供開箱即用的資料處理、向量檢索和視覺化 AI 工作流程編排等功能,讓您可以輕鬆開發和部署複雜的問答系統,而無需繁瑣的設定或配置。",
+ "invalid_json_format": "JSON 格式錯誤",
"llm_not_support_vision": "這個模型不支援圖片辨識",
"llm_use_vision": "圖片辨識",
"llm_use_vision_tip": "點選模型選擇後,可以看到模型是否支援圖片辨識以及控制是否啟用圖片辨識的功能。啟用圖片辨識後,模型會讀取檔案連結中的圖片內容,並且如果使用者問題少於 500 字,會自動解析使用者問題中的圖片。",
@@ -94,6 +95,7 @@
"open_vision_function_tip": "有圖示開關的模型即擁有圖片辨識功能。若開啟,模型會解析檔案連結中的圖片,並自動解析使用者問題中的圖片(使用者問題 ≤ 500 字時生效)。",
"or_drag_JSON": "或拖曳 JSON 檔案",
"paste_config": "貼上設定",
+ "paste_config_or_drag": "貼上配置或拖入 JSON 文件",
"permission.des.manage": "在寫入權限基礎上,可以設定發布通道、檢視對話紀錄、分配這個應用程式的權限",
"permission.des.read": "可以使用這個應用程式進行對話",
"permission.des.write": "可以檢視和編輯應用程式",
@@ -150,9 +152,12 @@
"type.Create workflow bot": "建立工作流程",
"type.Create workflow tip": "透過低程式碼的方式,建立邏輯複雜的多輪對話 AI 應用程式,建議進階使用者使用",
"type.Http plugin": "HTTP 外掛",
+ "type.Import from json": "導入 JSON 配置",
+ "type.Import from json tip": "透過貼上或匯入 JSON 文件,直接建立對應屬性的應用",
"type.Plugin": "外掛",
"type.Simple bot": "簡易應用程式",
"type.Workflow bot": "工作流程",
+ "type_not_recognized": "未識別到應用程式類型",
"upload_file_max_amount": "最大檔案數量",
"upload_file_max_amount_tip": "單輪對話中最大上傳檔案數量",
"variable.select type_desc": "可以為工作流程定義全域變數,常用於暫存。賦值的方式包括:\n1. 從對話頁面的 query 參數取得。\n2. 透過 API 的 variables 物件傳遞。\n3. 透過【變數更新】節點進行賦值。",
diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json
index dc3f4d736033..d7eb9e7294fe 100644
--- a/packages/web/i18n/zh-Hant/common.json
+++ b/packages/web/i18n/zh-Hant/common.json
@@ -285,6 +285,7 @@
"core.app.Config schedule plan": "設定排程執行",
"core.app.Config whisper": "設定語音輸入",
"core.app.Config_auto_execute": "點選配置自動執行規則",
+ "core.app.Create app by curl": "透過 curl 創建",
"core.app.Interval timer config": "排程執行設定",
"core.app.Interval timer run": "排程執行",
"core.app.Interval timer tip": "可排程執行應用程式",
diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/AppCard.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/AppCard.tsx
index 31226c5bc461..22176182f376 100644
--- a/projects/app/src/pages/app/detail/components/SimpleApp/AppCard.tsx
+++ b/projects/app/src/pages/app/detail/components/SimpleApp/AppCard.tsx
@@ -24,6 +24,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postTransition2Workflow } from '@/web/core/app/api/app';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { SimpleAppSnapshotType } from './useSnapshots';
+import { ExportPopover } from '../WorkflowComponents/AppCard';
const AppCard = ({
appForm,
@@ -118,6 +119,7 @@ const AppCard = ({
)}
{appDetail.permission.isOwner && (
+ {ExportPopover({
+ appName: appDetail.name,
+ appForm
+ })}
+
+ )
+ },
{
icon: 'core/app/type/workflow',
label: t('app:transition_to_workflow'),
diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx
index 70b4f796922e..0b1cca250043 100644
--- a/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx
+++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx
@@ -13,9 +13,10 @@ import MyTag from '@fastgpt/web/components/common/Tag/index';
import { publishStatusStyle } from '../constants';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import { fileDownload } from '@/web/common/file/utils';
-import { AppChatConfigType } from '@fastgpt/global/core/app/type';
+import { AppChatConfigType, AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useSystemStore } from '@/web/common/system/useSystemStore';
+import { filterSensitiveFormData } from '@/web/core/app/utils';
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
@@ -210,11 +211,13 @@ const AppCard = ({ showSaveStatus, isSaved }: { showSaveStatus: boolean; isSaved
return Render;
};
-function ExportPopover({
+export function ExportPopover({
chatConfig,
+ appForm,
appName
}: {
- chatConfig: AppChatConfigType;
+ chatConfig?: AppChatConfigType;
+ appForm?: AppSimpleEditFormType;
appName: string;
}) {
const { t } = useTranslation();
@@ -223,24 +226,15 @@ function ExportPopover({
const onExportWorkflow = useCallback(
async (mode: 'copy' | 'json') => {
- const data = flowData2StoreData();
- if (data) {
- if (mode === 'copy') {
- copyData(
- JSON.stringify(
- {
- nodes: filterSensitiveNodesData(data.nodes),
- edges: data.edges,
- chatConfig
- },
- null,
- 2
- ),
- t('app:export_config_successful')
- );
- } else if (mode === 'json') {
- fileDownload({
- text: JSON.stringify(
+ let config = '';
+
+ try {
+ if (appForm) {
+ config = JSON.stringify(filterSensitiveFormData(appForm), null, 2);
+ } else {
+ const data = flowData2StoreData();
+ if (data) {
+ config = JSON.stringify(
{
nodes: filterSensitiveNodesData(data.nodes),
edges: data.edges,
@@ -248,14 +242,28 @@ function ExportPopover({
},
null,
2
- ),
- type: 'application/json;charset=utf-8',
- filename: `${appName}.json`
- });
+ );
+ }
}
+ } catch (err) {
+ console.error(err);
+ }
+
+ if (!config) {
+ return;
+ }
+
+ if (mode === 'copy') {
+ copyData(config, t('app:export_config_successful'));
+ } else if (mode === 'json') {
+ fileDownload({
+ text: config,
+ type: 'application/json;charset=utf-8',
+ filename: `${appName}.json`
+ });
}
},
- [appName, chatConfig, copyData, flowData2StoreData, t]
+ [appForm, appName, chatConfig, copyData, flowData2StoreData, t]
);
return (
@@ -266,7 +274,7 @@ function ExportPopover({
trigger={'hover'}
w={'8.6rem'}
Trigger={
-
+
{t('app:export_configs')}
diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/ImportSettings.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/ImportSettings.tsx
index dd67eb8f0248..11867202cfe9 100644
--- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/ImportSettings.tsx
+++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/ImportSettings.tsx
@@ -1,13 +1,13 @@
-import React, { DragEvent, useCallback, useMemo, useState } from 'react';
-import { Textarea, Button, ModalBody, ModalFooter, Flex, Box } from '@chakra-ui/react';
+import React, { useCallback, useState } from 'react';
+import { Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../context';
import { useTranslation } from 'next-i18next';
-import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
+import DragEditor from '@fastgpt/web/components/common/Textarea/DragEditor';
type Props = {
onClose: () => void;
@@ -21,57 +21,8 @@ const ImportSettings = ({ onClose }: Props) => {
});
const { isPc } = useSystem();
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
- const [isDragging, setIsDragging] = useState(false);
- const [value, setValue] = useState('');
const { t } = useTranslation();
-
- const readJSONFile = useCallback(
- (file: File) => {
- const reader = new FileReader();
- reader.onload = (e) => {
- if (!file.name.endsWith('.json')) {
- toast({
- title: t('app:not_json_file'),
- status: 'error'
- });
- return;
- }
- if (e.target) {
- const res = JSON.parse(e.target.result as string);
- setValue(JSON.stringify(res, null, 2));
- }
- };
- reader.readAsText(file);
- },
- [t, toast]
- );
-
- const handleDragEnter = useCallback((e: DragEvent) => {
- e.preventDefault();
- setIsDragging(true);
- }, []);
-
- const handleDragLeave = useCallback((e: DragEvent) => {
- e.preventDefault();
- setIsDragging(false);
- }, []);
- const handleDrop = useCallback(
- async (e: DragEvent) => {
- e.preventDefault();
- const file = e.dataTransfer.files[0];
- readJSONFile(file);
- setIsDragging(false);
- },
- [readJSONFile]
- );
-
- const onSelectFile = useCallback(
- async (e: File[]) => {
- const file = e[0];
- readJSONFile(file);
- },
- [readJSONFile]
- );
+ const [value, setValue] = useState('');
return (
{
size={isPc ? 'lg' : 'md'}
>
-
- {isDragging ? (
- e.preventDefault()}
- onDrop={handleDrop}
- onDragLeave={handleDragLeave}
- >
-
-
-
- {t('app:file_recover')}
-
-
-
- ) : (
-
-
-
- {t('common:common.json_config')}
-
-
-
- e.preventDefault()}
- onDrop={handleDrop}
- onDragLeave={handleDragLeave}
- >
-
-
- )}
+
+ onclickCreate(data, item.templateId))}
+ >
+ {t('app:templateMarket.Use')}
+
+
-
-
- ))}
-
+
+ ))}
+
+ ) : (
+
+
+ )}
+ {currentCreateType !== 'template' && (
+
+
+ {t('common:common.Cancel')}
+
+ onclickCreate(data))}>
+ {t('common:common.Confirm')}
+
+
+ )}
onSelectImage(e, {
diff --git a/projects/app/src/pages/app/list/components/JsonImportModal.tsx b/projects/app/src/pages/app/list/components/JsonImportModal.tsx
new file mode 100644
index 000000000000..ff32c75a4587
--- /dev/null
+++ b/projects/app/src/pages/app/list/components/JsonImportModal.tsx
@@ -0,0 +1,176 @@
+import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
+import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
+import Avatar from '@fastgpt/web/components/common/Avatar';
+import MyModal from '@fastgpt/web/components/common/MyModal';
+import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
+import { useSystem } from '@fastgpt/web/hooks/useSystem';
+import { useTranslation } from 'next-i18next';
+import { useForm } from 'react-hook-form';
+import { appTypeMap } from './CreateModal';
+import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
+import { useEffect, useState } from 'react';
+import { useToast } from '@fastgpt/web/hooks/useToast';
+import { getAppType } from '@fastgpt/global/core/app/utils';
+import { useContextSelector } from 'use-context-selector';
+import { AppListContext } from './context';
+import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
+import { postCreateApp } from '@/web/core/app/api';
+import { useRouter } from 'next/router';
+import { form2AppWorkflow } from '@/web/core/app/utils';
+import DragEditor from '@fastgpt/web/components/common/Textarea/DragEditor';
+
+type FormType = {
+ avatar: string;
+ name: string;
+ type: AppTypeEnum | '';
+};
+
+const JsonImportModal = ({ onClose }: { onClose: () => void }) => {
+ const { toast } = useToast();
+ const { t } = useTranslation();
+ const { isPc } = useSystem();
+ const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
+ const router = useRouter();
+
+ const { register, setValue, watch, handleSubmit } = useForm({
+ defaultValues: {
+ avatar: appTypeMap[AppTypeEnum.simple].avatar,
+ name: '',
+ type: AppTypeEnum.simple
+ }
+ });
+ const avatar = watch('avatar');
+
+ const {
+ File,
+ onOpen: onOpenSelectFile,
+ onSelectImage
+ } = useSelectFile({
+ fileType: '.jpg,.png',
+ multiple: false
+ });
+
+ const { File: ConfigFile, onOpen: onOpenSelectConfigFile } = useSelectFile({
+ fileType: 'json',
+ multiple: false
+ });
+
+ const [workflowStr, setWorkflowStr] = useState('');
+
+ const { runAsync: onSubmit, loading: isCreating } = useRequest2(
+ async (data: FormType) => {
+ if (!data.type) {
+ return Promise.reject(t('app:type_not_recognized'));
+ }
+
+ let workflow;
+ try {
+ if (data.type === AppTypeEnum.simple) {
+ const appForm = JSON.parse(workflowStr);
+ workflow = form2AppWorkflow(appForm, t);
+ } else {
+ workflow = JSON.parse(workflowStr);
+ }
+ } catch (err) {
+ return Promise.reject(t('app:invalid_json_format'));
+ }
+
+ return postCreateApp({
+ parentId,
+ avatar: data.avatar,
+ name: data.name,
+ type: data.type,
+ modules: workflow.nodes,
+ edges: workflow.edges,
+ chatConfig: workflow.chatConfig
+ });
+ },
+ {
+ onSuccess(id: string) {
+ router.push(`/app/detail?appId=${id}`);
+ loadMyApps();
+ onClose();
+ },
+ successToast: t('common:common.Create Success')
+ }
+ );
+
+ useEffect(() => {
+ try {
+ const workflow = JSON.parse(workflowStr);
+ const type = getAppType(workflow);
+ setValue('type', type);
+ if (type && !avatar.startsWith('/')) {
+ setValue('avatar', appTypeMap[type].avatar);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }, [avatar, setValue, workflowStr]);
+
+ return (
+ <>
+
+
+
+ {t('common:common.Set Name')}
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('common:add_new')}
+
+
+
+ onSelectImage(e, {
+ maxH: 300,
+ maxW: 300,
+ callback: (e) => setValue('avatar', e)
+ })
+ }
+ />
+ >
+ );
+};
+
+export default JsonImportModal;
diff --git a/projects/app/src/pages/app/list/components/TemplateMarketModal.tsx b/projects/app/src/pages/app/list/components/TemplateMarketModal.tsx
index 8941c95e9c40..a68f64b327a6 100644
--- a/projects/app/src/pages/app/list/components/TemplateMarketModal.tsx
+++ b/projects/app/src/pages/app/list/components/TemplateMarketModal.tsx
@@ -37,6 +37,7 @@ import { webPushTrack } from '@/web/common/middle/tracks/utils';
import { AppTemplateSchemaType, TemplateTypeSchemaType } from '@fastgpt/global/core/app/type';
import { i18nT } from '@fastgpt/web/i18n/utils';
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
+import { form2AppWorkflow } from '@/web/core/app/utils';
type TemplateAppType = AppTypeEnum | 'all';
@@ -94,14 +95,18 @@ const TemplateMarketModal = ({
const { runAsync: onUseTemplate, loading: isCreating } = useRequest2(
async (template: AppTemplateSchemaType) => {
const templateDetail = await getTemplateMarketItemDetail(template.templateId);
+ let workflow = templateDetail.workflow;
+ if (templateDetail.type === AppTypeEnum.simple) {
+ workflow = form2AppWorkflow(workflow, t);
+ }
return postCreateApp({
parentId,
avatar: template.avatar,
name: template.name,
type: template.type as AppTypeEnum,
- modules: templateDetail.workflow.nodes || [],
- edges: templateDetail.workflow.edges || [],
- chatConfig: templateDetail.workflow.chatConfig
+ modules: workflow.nodes || [],
+ edges: workflow.edges || [],
+ chatConfig: workflow.chatConfig
}).then((res) => {
webPushTrack.useAppTemplate({
id: res,
diff --git a/projects/app/src/pages/app/list/index.tsx b/projects/app/src/pages/app/list/index.tsx
index 602652975f62..118674d884e2 100644
--- a/projects/app/src/pages/app/list/index.tsx
+++ b/projects/app/src/pages/app/list/index.tsx
@@ -30,6 +30,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
import MyIcon from '@fastgpt/web/components/common/Icon';
import TemplateMarketModal from './components/TemplateMarketModal';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
+import JsonImportModal from './components/JsonImportModal';
const CreateModal = dynamic(() => import('./components/CreateModal'));
const EditFolderModal = dynamic(
@@ -64,6 +65,11 @@ const MyApps = () => {
onOpen: onOpenCreateHttpPlugin,
onClose: onCloseCreateHttpPlugin
} = useDisclosure();
+ const {
+ isOpen: isOpenJsonImportModal,
+ onOpen: onOpenJsonImportModal,
+ onClose: onCloseJsonImportModal
+ } = useDisclosure();
const [editFolder, setEditFolder] = useState();
const [templateModalType, setTemplateModalType] = useState();
@@ -244,6 +250,16 @@ const MyApps = () => {
}
]
},
+ {
+ children: [
+ {
+ icon: 'core/app/type/jsonImport',
+ label: t('app:type.Import from json'),
+ description: t('app:type.Import from json tip'),
+ onClick: onOpenJsonImportModal
+ }
+ ]
+ },
...(isPc
? []
: [
@@ -375,6 +391,7 @@ const MyApps = () => {
defaultType={templateModalType}
/>
)}
+ {isOpenJsonImportModal && }
);
};
diff --git a/projects/app/src/web/common/file/hooks/useSelectFile.tsx b/projects/app/src/web/common/file/hooks/useSelectFile.tsx
index b2abdecb1d8b..72fabdccfc3b 100644
--- a/projects/app/src/web/common/file/hooks/useSelectFile.tsx
+++ b/projects/app/src/web/common/file/hooks/useSelectFile.tsx
@@ -74,7 +74,6 @@ export const useSelectFile = (props?: {
maxW,
maxH
});
- console.log(src, '--');
callback?.(src);
return src;
} catch (err: any) {
diff --git a/projects/app/src/web/core/app/templates.ts b/projects/app/src/web/core/app/templates.ts
index 9280ec0776bf..7a8c8eaba259 100644
--- a/projects/app/src/web/core/app/templates.ts
+++ b/projects/app/src/web/core/app/templates.ts
@@ -408,3 +408,344 @@ export const emptyTemplates: Record<
edges: []
}
};
+
+export const getCurlPlugin = ({
+ params,
+ headers,
+ body,
+ method,
+ url
+}: {
+ params: {
+ key: string;
+ value: string | undefined;
+ type: string;
+ }[];
+ headers: {
+ key: string;
+ value: string | undefined;
+ type: string;
+ }[];
+ body: string;
+ method: string;
+ url: string;
+}): {
+ nodes: AppSchema['modules'];
+ edges: AppSchema['edges'];
+ chatConfig: AppSchema['chatConfig'];
+} => {
+ return {
+ nodes: [
+ {
+ nodeId: 'pluginInput',
+ name: 'workflow:template.plugin_start',
+ intro: 'workflow:intro_plugin_input',
+ avatar: 'core/workflow/template/workflowStart',
+ flowNodeType: 'pluginInput',
+ showStatus: false,
+ position: {
+ x: 630.1191328382079,
+ y: -125.05298493910118
+ },
+ version: '481',
+ inputs: [],
+ outputs: []
+ },
+ {
+ nodeId: 'pluginOutput',
+ name: 'common:core.module.template.self_output',
+ intro: 'workflow:intro_custom_plugin_output',
+ avatar: 'core/workflow/template/pluginOutput',
+ flowNodeType: 'pluginOutput',
+ showStatus: false,
+ position: {
+ x: 1776.334576378706,
+ y: -179.2671413906911
+ },
+ version: '481',
+ inputs: [
+ {
+ renderTypeList: ['reference'],
+ valueType: 'any',
+ canEdit: true,
+ key: 'result',
+ label: 'result',
+ isToolOutput: false,
+ description: '',
+ required: true,
+ value: ['vumlECDQTjeC', 'httpRawResponse']
+ },
+ {
+ renderTypeList: ['reference'],
+ valueType: 'object',
+ canEdit: true,
+ key: 'error',
+ label: 'error',
+ isToolOutput: false,
+ description: '',
+ required: true,
+ value: ['vumlECDQTjeC', 'error']
+ }
+ ],
+ outputs: []
+ },
+ {
+ nodeId: 'pluginConfig',
+ name: 'common:core.module.template.system_config',
+ intro: '',
+ avatar: 'core/workflow/template/systemConfig',
+ flowNodeType: 'pluginConfig',
+ position: {
+ x: 182.48010602254573,
+ y: -190.55298493910115
+ },
+ version: '4811',
+ inputs: [],
+ outputs: []
+ },
+ {
+ nodeId: 'vumlECDQTjeC',
+ name: 'HTTP 请求',
+ intro: '可以发出一个 HTTP 请求,实现更为复杂的操作(联网搜索、数据库查询等)',
+ avatar: 'core/workflow/template/httpRequest',
+ flowNodeType: 'httpRequest468',
+ showStatus: true,
+ position: {
+ x: 1068.6226695001628,
+ y: -435.2671413906911
+ },
+ version: '481',
+ inputs: [
+ {
+ key: 'system_addInputParam',
+ renderTypeList: ['addInputParam'],
+ valueType: 'dynamic',
+ label: '',
+ required: false,
+ description: '接收前方节点的输出值作为变量,这些变量可以被 HTTP 请求参数使用。',
+ customInputConfig: {
+ selectValueTypeList: [
+ 'string',
+ 'number',
+ 'boolean',
+ 'object',
+ 'arrayString',
+ 'arrayNumber',
+ 'arrayBoolean',
+ 'arrayObject',
+ 'arrayAny',
+ 'any',
+ 'chatHistory',
+ 'datasetQuote',
+ 'dynamic',
+ 'selectApp',
+ 'selectDataset'
+ ],
+ showDescription: false,
+ showDefaultValue: true
+ },
+ valueDesc: '',
+ debugLabel: '',
+ toolDescription: ''
+ },
+ {
+ key: 'system_httpMethod',
+ renderTypeList: ['custom'],
+ valueType: 'string',
+ label: '',
+ value: method,
+ required: true,
+ valueDesc: '',
+ description: '',
+ debugLabel: '',
+ toolDescription: ''
+ },
+ {
+ key: 'system_httpTimeout',
+ renderTypeList: ['custom'],
+ valueType: 'number',
+ label: '',
+ value: 30,
+ min: 5,
+ max: 600,
+ required: true,
+ valueDesc: '',
+ description: '',
+ debugLabel: '',
+ toolDescription: ''
+ },
+ {
+ key: 'system_httpReqUrl',
+ renderTypeList: ['hidden'],
+ valueType: 'string',
+ label: '',
+ description:
+ '新的 HTTP 请求地址。如果出现两个"请求地址",可以删除该模块重新加入,会拉取最新的模块配置。',
+ placeholder: 'https://api.ai.com/getInventory',
+ required: false,
+ value: url,
+ valueDesc: '',
+ debugLabel: '',
+ toolDescription: ''
+ },
+ {
+ key: 'system_httpHeader',
+ renderTypeList: ['custom'],
+ valueType: 'any',
+ value: headers,
+ label: '',
+ description:
+ '自定义请求头,请严格填入 JSON 字符串。\n1. 确保最后一个属性没有逗号\n2. 确保 key 包含双引号\n例如:{"Authorization":"Bearer xxx"}',
+ placeholder: 'common:core.module.input.description.Http Request Header',
+ required: false,
+ valueDesc: '',
+ debugLabel: '',
+ toolDescription: ''
+ },
+ {
+ key: 'system_httpParams',
+ renderTypeList: ['hidden'],
+ valueType: 'any',
+ description:
+ '新的 HTTP 请求地址。如果出现两个“请求地址”,可以删除该模块重新加入,会拉取最新的模块配置。',
+ label: '',
+ required: false,
+ valueDesc: '',
+ description: '',
+ debugLabel: '',
+ toolDescription: ''
+ },
+ {
+ key: 'system_httpJsonBody',
+ renderTypeList: ['hidden'],
+ valueType: 'any',
+ value: body,
+ label: '',
+ required: false,
+ valueDesc: '',
+ description: '',
+ debugLabel: '',
+ toolDescription: ''
+ },
+ {
+ key: 'system_httpFormBody',
+ renderTypeList: ['hidden'],
+ valueType: 'any',
+ value: [],
+ label: '',
+ required: false,
+ valueDesc: '',
+ description: '',
+ debugLabel: '',
+ toolDescription: ''
+ },
+ {
+ key: 'system_httpContentType',
+ renderTypeList: ['hidden'],
+ valueType: 'string',
+ value: 'json',
+ label: '',
+ required: false,
+ valueDesc: '',
+ description: '',
+ debugLabel: '',
+ toolDescription: ''
+ }
+ ],
+ outputs: [
+ {
+ id: 'system_addOutputParam',
+ key: 'system_addOutputParam',
+ type: 'dynamic',
+ valueType: 'dynamic',
+ label: '输出字段提取',
+ customFieldConfig: {
+ selectValueTypeList: [
+ 'string',
+ 'number',
+ 'boolean',
+ 'object',
+ 'arrayString',
+ 'arrayNumber',
+ 'arrayBoolean',
+ 'arrayObject',
+ 'arrayAny',
+ 'any',
+ 'chatHistory',
+ 'datasetQuote',
+ 'dynamic',
+ 'selectApp',
+ 'selectDataset'
+ ],
+ showDescription: false,
+ showDefaultValue: false
+ },
+ description: '可以通过 JSONPath 语法来提取响应值中的指定字段',
+ valueDesc: ''
+ },
+ {
+ id: 'error',
+ key: 'error',
+ label: '请求错误',
+ description: 'HTTP请求错误信息,成功时返回空',
+ valueType: 'object',
+ type: 'static',
+ valueDesc: ''
+ },
+ {
+ id: 'httpRawResponse',
+ key: 'httpRawResponse',
+ required: true,
+ label: '原始响应',
+ description: 'HTTP请求的原始响应。只能接受字符串或JSON类型响应数据。',
+ valueType: 'any',
+ type: 'static',
+ valueDesc: ''
+ }
+ ]
+ }
+ ],
+ edges: [
+ {
+ source: 'pluginInput',
+ target: 'vumlECDQTjeC',
+ sourceHandle: 'pluginInput-source-right',
+ targetHandle: 'vumlECDQTjeC-target-left'
+ },
+ {
+ source: 'vumlECDQTjeC',
+ target: 'pluginOutput',
+ sourceHandle: 'vumlECDQTjeC-source-right',
+ targetHandle: 'pluginOutput-target-left'
+ }
+ ],
+ chatConfig: {
+ questionGuide: {
+ open: false,
+ model: 'gpt-4o-mini',
+ customPrompt:
+ "You are an AI assistant tasked with predicting the user's next question based on the conversation history. Your goal is to generate 3 potential questions that will guide the user to continue the conversation. When generating these questions, adhere to the following rules:\n\n1. Use the same language as the user's last question in the conversation history.\n2. Keep each question under 20 characters in length.\n\nAnalyze the conversation history provided to you and use it as context to generate relevant and engaging follow-up questions. Your predictions should be logical extensions of the current topic or related areas that the user might be interested in exploring further.\n\nRemember to maintain consistency in tone and style with the existing conversation while providing diverse options for the user to choose from. Your goal is to keep the conversation flowing naturally and help the user delve deeper into the subject matter or explore related topics."
+ },
+ ttsConfig: {
+ type: 'web'
+ },
+ whisperConfig: {
+ open: false,
+ autoSend: false,
+ autoTTSResponse: false
+ },
+ chatInputGuide: {
+ open: false,
+ textList: [],
+ customUrl: ''
+ },
+ instruction: '',
+ autoExecute: {
+ open: false,
+ defaultPrompt: ''
+ },
+ variables: [],
+ welcomeText: ''
+ }
+ };
+};
diff --git a/projects/app/src/web/core/app/utils.ts b/projects/app/src/web/core/app/utils.ts
index f0bd045c8271..5a8546a58b34 100644
--- a/projects/app/src/web/core/app/utils.ts
+++ b/projects/app/src/web/core/app/utils.ts
@@ -40,6 +40,7 @@ import {
Input_Template_UserChatInput
} from '@fastgpt/global/core/workflow/template/input';
import { workflowStartNodeId } from './constants';
+import { getDefaultAppForm } from '@fastgpt/global/core/app/utils';
type WorkflowType = {
nodes: StoreNodeItemType[];
@@ -515,6 +516,13 @@ export function form2AppWorkflow(
chatConfig: data.chatConfig
};
}
+export function filterSensitiveFormData(appForm: AppSimpleEditFormType) {
+ const defaultAppForm = getDefaultAppForm();
+ return {
+ ...appForm,
+ dataset: defaultAppForm.dataset
+ };
+}
export const workflowSystemVariables: EditorVariablePickerType[] = [
{