From f4585573976a6f8d9c3a53de776d6cb1d4413c60 Mon Sep 17 00:00:00 2001
From: dreamer6680 <1468683855@qq.com>
Date: Fri, 25 Apr 2025 11:17:31 +0800
Subject: [PATCH 01/10] TrackRegisterParams
---
packages/global/support/user/login/api.d.ts | 5 +-
projects/app/src/components/Layout/index.tsx | 15 +-
.../app/src/components/WorkflowImporter.tsx | 133 ++++++++
projects/app/src/pages/login/fastlogin.tsx | 8 +
projects/app/src/pages/login/index.tsx | 9 +
projects/app/src/pages/login/provider.tsx | 6 +
projects/app/src/web/context/useInitApp.ts | 55 +++-
projects/app/src/web/core/app/utils.ts | 285 ++++++++++++++++++
8 files changed, 507 insertions(+), 9 deletions(-)
create mode 100644 projects/app/src/components/WorkflowImporter.tsx
diff --git a/packages/global/support/user/login/api.d.ts b/packages/global/support/user/login/api.d.ts
index d994a24ec4a6..19d0e9afac2e 100644
--- a/packages/global/support/user/login/api.d.ts
+++ b/packages/global/support/user/login/api.d.ts
@@ -7,7 +7,10 @@ export type TrackRegisterParams = {
inviterId?: string;
bd_vid?: string;
fastgpt_sem?: {
- keyword: string;
+ keyword?: string;
+ source?: string;
+ medium?: string;
+ content?: string;
};
sourceDomain?: string;
};
diff --git a/projects/app/src/components/Layout/index.tsx b/projects/app/src/components/Layout/index.tsx
index 78b59895f46b..49a8c010b2b4 100644
--- a/projects/app/src/components/Layout/index.tsx
+++ b/projects/app/src/components/Layout/index.tsx
@@ -15,6 +15,7 @@ import { useDebounceEffect, useMount } from 'ahooks';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
import WorkorderButton from './WorkorderButton';
+import WorkflowImporter from '@/components/WorkflowImporter';
const Navbar = dynamic(() => import('./navbar'));
const NavbarPhone = dynamic(() => import('./navbarPhone'));
@@ -164,7 +165,19 @@ const Layout = ({ children }: { children: JSX.Element }) => {
>
)}
-
+ {(() => {
+ // 添加浏览器环境检查,避免服务器端渲染时的错误
+ const isBrowser = typeof window !== 'undefined';
+ if (isBrowser && sessionStorage.getItem('utm_workflow')) {
+ console.log(
+ 'Layout: 准备渲染WorkflowImporter,userInfo =',
+ !!userInfo,
+ userInfo?.username
+ );
+ return !!userInfo && ;
+ }
+ return null;
+ })()}
>
diff --git a/projects/app/src/components/WorkflowImporter.tsx b/projects/app/src/components/WorkflowImporter.tsx
new file mode 100644
index 000000000000..5fb298146419
--- /dev/null
+++ b/projects/app/src/components/WorkflowImporter.tsx
@@ -0,0 +1,133 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { useRouter } from 'next/router';
+import { importWorkflowFromUrl } from '@/web/core/app/utils';
+import {
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ Flex,
+ Spinner,
+ Text,
+ useToast
+} from '@chakra-ui/react';
+import { useUserStore } from '@/web/support/user/useUserStore';
+import { useTranslation } from 'next-i18next';
+
+const WorkflowImporter = () => {
+ const { t } = useTranslation();
+ const router = useRouter();
+ const toast = useToast();
+ const { userInfo } = useUserStore();
+ const [isImporting, setIsImporting] = useState(false);
+ const [importStatus, setImportStatus] = useState('');
+
+ useEffect(() => {
+ // 添加浏览器环境检查
+ const isBrowser = typeof window !== 'undefined';
+ if (!isBrowser) return; // 如果不是浏览器环境,直接返回
+
+ // 只有用户已登录时检查
+ if (!userInfo?.username) {
+ return;
+ }
+
+ const checkAndImportWorkflow = async () => {
+ try {
+ let workflowUrl = sessionStorage.getItem('utm_workflow');
+
+ if (!workflowUrl) {
+ return;
+ }
+
+ sessionStorage.removeItem('utm_workflow');
+
+ setIsImporting(true);
+ setImportStatus('正在准备导入工作流...');
+
+ toast({
+ title: '发现工作流URL,正在导入...',
+ status: 'info',
+ duration: 5000,
+ isClosable: true
+ });
+
+ try {
+ setImportStatus('正在从URL获取工作流数据...');
+
+ // 获取utm_params中的source参数,如果有的话
+ let content = '未命名';
+ try {
+ const utmParams = localStorage.getItem('utm_params');
+ if (utmParams) {
+ const params = JSON.parse(utmParams);
+ if (params.content) content = params.content;
+ }
+ } catch (error) {
+ console.error('解析utm_params出错:', error);
+ }
+
+ // 导入工作流
+ const appId = await importWorkflowFromUrl({
+ url: workflowUrl,
+ name: `${content}`
+ });
+
+ setImportStatus('工作流导入成功!');
+
+ // 导入成功后提示并跳转
+ toast({
+ title: '工作流导入成功',
+ status: 'success',
+ duration: 5000,
+ isClosable: true
+ });
+
+ // 延迟一下再跳转,确保用户看到成功消息
+ setTimeout(() => {
+ setIsImporting(false);
+ router.push(`/app/detail?appId=${appId}`);
+ }, 1500);
+ } catch (error) {
+ console.error('导入工作流失败:', error);
+ setImportStatus(`导入失败:${error instanceof Error ? error.message : '未知错误'}`);
+
+ toast({
+ title: '导入工作流失败',
+ description: error instanceof Error ? error.message : '未知错误',
+ status: 'error',
+ duration: 5000,
+ isClosable: true
+ });
+
+ setTimeout(() => {
+ setIsImporting(false);
+ }, 2000);
+ }
+ } catch (error) {
+ console.error('检查工作流URL出错:', error);
+ setIsImporting(false);
+ }
+ };
+
+ checkAndImportWorkflow();
+ }, [userInfo, router, toast]);
+
+ return (
+ {}} closeOnOverlayClick={false} isCentered>
+
+
+ 导入工作流
+
+
+
+ {importStatus}
+
+
+
+
+ );
+};
+
+export default WorkflowImporter;
diff --git a/projects/app/src/pages/login/fastlogin.tsx b/projects/app/src/pages/login/fastlogin.tsx
index e353b73d5f8d..75b3cbd6bb15 100644
--- a/projects/app/src/pages/login/fastlogin.tsx
+++ b/projects/app/src/pages/login/fastlogin.tsx
@@ -26,6 +26,14 @@ const FastLogin = ({
(res: ResLogin) => {
setUserInfo(res.user);
+ // 检查是否有工作流需要导入
+ if (typeof window !== 'undefined' && sessionStorage.getItem('utm_workflow')) {
+ console.log('快速登录成功,检测到工作流导入链接,跳转到首页以触发导入逻辑');
+ // 跳转到首页,让Layout中的WorkflowImporter组件处理导入
+ router.push('/');
+ return;
+ }
+
setTimeout(() => {
router.push(decodeURIComponent(callbackUrl));
}, 100);
diff --git a/projects/app/src/pages/login/index.tsx b/projects/app/src/pages/login/index.tsx
index 0917450770ab..7bee12c616a0 100644
--- a/projects/app/src/pages/login/index.tsx
+++ b/projects/app/src/pages/login/index.tsx
@@ -64,6 +64,15 @@ const Login = ({ ChineseRedirectUrl }: { ChineseRedirectUrl: string }) => {
setUserInfo(res.user);
const decodeLastRoute = decodeURIComponent(lastRoute);
+
+ // 检查是否有工作流需要导入
+ if (typeof window !== 'undefined' && sessionStorage.getItem('utm_workflow')) {
+ console.log('登录成功,检测到工作流导入链接,跳转到首页以触发导入逻辑');
+ // 跳转到首页,让Layout中的WorkflowImporter组件处理导入
+ router.push('/');
+ return;
+ }
+
// 检查是否是当前的 route
const navigateTo =
decodeLastRoute && !decodeLastRoute.includes('/login')
diff --git a/projects/app/src/pages/login/provider.tsx b/projects/app/src/pages/login/provider.tsx
index a1327a7412ed..c4eb42c7daaf 100644
--- a/projects/app/src/pages/login/provider.tsx
+++ b/projects/app/src/pages/login/provider.tsx
@@ -26,6 +26,12 @@ const provider = () => {
(res: ResLogin) => {
setUserInfo(res.user);
+ if (typeof window !== 'undefined' && sessionStorage.getItem('utm_workflow')) {
+ console.log('OAuth登录成功,检测到工作流导入链接,跳转到首页以触发导入逻辑');
+ router.push('/');
+ return;
+ }
+
router.push(
loginStore?.lastRoute ? decodeURIComponent(loginStore?.lastRoute) : '/dashboard/apps'
);
diff --git a/projects/app/src/web/context/useInitApp.ts b/projects/app/src/web/context/useInitApp.ts
index 83d6b7f018e0..28c27eb02359 100644
--- a/projects/app/src/web/context/useInitApp.ts
+++ b/projects/app/src/web/context/useInitApp.ts
@@ -10,12 +10,17 @@ import { useUserStore } from '../support/user/useUserStore';
export const useInitApp = () => {
const router = useRouter();
- const { hiId, bd_vid, k, sourceDomain } = router.query as {
- hiId?: string;
- bd_vid?: string;
- k?: string;
- sourceDomain?: string;
- };
+ const { hiId, bd_vid, k, sourceDomain, utm_source, utm_medium, utm_content, utm_workflow } =
+ router.query as {
+ hiId?: string;
+ bd_vid?: string;
+ k?: string;
+ sourceDomain?: string;
+ utm_source?: string;
+ utm_medium?: string;
+ utm_content?: string;
+ utm_workflow?: string;
+ };
const { loadGitStar, setInitd, feConfigs } = useSystemStore();
const { userInfo } = useUserStore();
const [scripts, setScripts] = useState([]);
@@ -70,9 +75,45 @@ export const useInitApp = () => {
});
useEffect(() => {
+ // 添加浏览器环境检查
+ const isBrowser = typeof window !== 'undefined';
+ if (!isBrowser) return; // 如果不是浏览器环境,直接返回
+
hiId && localStorage.setItem('inviterId', hiId);
bd_vid && sessionStorage.setItem('bd_vid', bd_vid);
k && sessionStorage.setItem('fastgpt_sem', JSON.stringify({ keyword: k }));
+ utm_workflow && sessionStorage.setItem('utm_workflow', utm_workflow);
+
+ // 处理UTM参数,将除workflow外的所有参数合并到fastgpt_sem中
+ try {
+ // 创建UTM对象
+ const utmParams: Record = {};
+ if (utm_source) utmParams.source = utm_source;
+ if (utm_medium) utmParams.medium = utm_medium;
+ if (utm_content) utmParams.content = utm_content;
+
+ // 将UTM参数存入localStorage以便登录和注册时使用
+ if (Object.keys(utmParams).length > 0) {
+ localStorage.setItem('utm_params', JSON.stringify(utmParams));
+ }
+
+ // 获取现有的fastgpt_sem
+ const existingSem = sessionStorage.getItem('fastgpt_sem')
+ ? JSON.parse(sessionStorage.getItem('fastgpt_sem')!)
+ : {};
+
+ const newSem = {
+ ...existingSem,
+ ...utmParams
+ };
+
+ // 保存更新后的fastgpt_sem
+ if (Object.keys(newSem).length > 0) {
+ sessionStorage.setItem('fastgpt_sem', JSON.stringify(newSem));
+ }
+ } catch (error) {
+ console.error('处理UTM参数出错:', error);
+ }
const formatSourceDomain = (() => {
if (sourceDomain) return sourceDomain;
@@ -82,7 +123,7 @@ export const useInitApp = () => {
if (formatSourceDomain && !sessionStorage.getItem('sourceDomain')) {
sessionStorage.setItem('sourceDomain', formatSourceDomain);
}
- }, [bd_vid, hiId, k, sourceDomain]);
+ }, [bd_vid, hiId, k, utm_content, utm_medium, utm_source, utm_workflow, sourceDomain]);
return {
feConfigs,
diff --git a/projects/app/src/web/core/app/utils.ts b/projects/app/src/web/core/app/utils.ts
index 326d40c2bacc..302b69c3953b 100644
--- a/projects/app/src/web/core/app/utils.ts
+++ b/projects/app/src/web/core/app/utils.ts
@@ -40,6 +40,10 @@ import {
} from '@fastgpt/global/core/workflow/template/input';
import { workflowStartNodeId } from './constants';
import { getDefaultAppForm } from '@fastgpt/global/core/app/utils';
+import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
+import { getAppType } from '@fastgpt/global/core/app/utils';
+import { postCreateApp } from './api';
+import { appTypeMap } from '@/pageComponents/app/constants';
type WorkflowType = {
nodes: StoreNodeItemType[];
@@ -629,3 +633,284 @@ export const getAppQGuideCustomURL = (appDetail: AppDetailType | AppSchema): str
?.inputs.find((i) => i.key === NodeInputKeyEnum.chatInputGuide)?.value.customUrl || ''
);
};
+
+/**
+ * 从URL获取工作流JSON数据
+ */
+export const fetchWorkflowFromUrl = async (url: string) => {
+ // 自定义响应类型,用于手动处理剪贴板内容
+ type CustomResponse = {
+ text: () => Promise;
+ ok: boolean;
+ status: number;
+ headers: {
+ get: (name: string) => string | null;
+ };
+ };
+
+ try {
+ if (!url || typeof url !== 'string') {
+ throw new Error('WORKFLOW_IMPORT_ERROR: URL为空或格式错误');
+ }
+
+ // 清理URL
+ let fetchUrl = url.trim();
+
+ // 如果URL最后有斜杠,移除它
+ if (fetchUrl.endsWith('/')) {
+ fetchUrl = fetchUrl.slice(0, -1);
+ }
+
+ // 确保URL是绝对路径
+ if (!fetchUrl.startsWith('http://') && !fetchUrl.startsWith('https://')) {
+ fetchUrl = `https://${fetchUrl}`;
+ }
+
+ // 设置请求超时
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 30000); // 30秒超时
+
+ try {
+ // 首先检查这是否是本地请求(localhost或127.0.0.1)
+ const isLocalRequest = fetchUrl.includes('localhost') || fetchUrl.includes('127.0.0.1');
+
+ if (isLocalRequest) {
+ try {
+ // 尝试方法1: 使用相对路径(如果URL是指向同一个域的不同端口)
+ const urlObj = new URL(fetchUrl);
+ const path = urlObj.pathname + urlObj.search;
+ console.log(`尝试使用相对路径请求: ${path}`);
+ const relativeResponse = await fetch(path, {
+ signal: controller.signal,
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json'
+ }
+ }).catch((e) => {
+ console.log(`相对路径请求失败: ${e.message}`);
+ return null;
+ });
+
+ if (relativeResponse?.ok) {
+ clearTimeout(timeoutId);
+ const text = await relativeResponse.text();
+ return JSON.parse(text);
+ }
+ } catch (err: any) {
+ console.log(`相对路径方法失败: ${err.message}`);
+ }
+
+ // 尝试方法2: 如果是本地开发环境,建议用户手动复制JSON
+ const userConfirmed = window.confirm(
+ `由于CORS限制,无法直接从 ${fetchUrl} 获取工作流数据。\n\n` +
+ '请手动打开该URL,复制JSON内容,然后点击"确定"来粘贴。\n\n' +
+ '或者点击"取消"放弃导入。'
+ );
+
+ if (userConfirmed) {
+ try {
+ const clipboardText = await navigator.clipboard.readText().catch(() => '');
+ if (clipboardText) {
+ try {
+ return JSON.parse(clipboardText);
+ } catch (err) {
+ throw new Error('剪贴板内容不是有效的JSON格式,请确保复制了完整的JSON数据');
+ }
+ } else {
+ throw new Error('无法读取剪贴板内容,请确保已授予网站剪贴板权限');
+ }
+ } catch (err) {
+ console.error('读取剪贴板失败:', err);
+
+ // 如果剪贴板读取失败,提供手动输入选项
+ const jsonInput = window.prompt('无法自动读取剪贴板。请手动粘贴工作流JSON数据:', '');
+
+ if (jsonInput) {
+ try {
+ return JSON.parse(jsonInput);
+ } catch (err) {
+ throw new Error('输入的内容不是有效的JSON格式');
+ }
+ } else {
+ throw new Error('未提供JSON数据,导入已取消');
+ }
+ }
+ } else {
+ throw new Error('用户取消了导入操作');
+ }
+ }
+
+ // 如果不是本地请求,尝试正常请求
+ const response = await fetch(fetchUrl, {
+ signal: controller.signal,
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ mode: 'cors' // 尝试CORS
+ }).catch(async (err) => {
+ console.log(`直接请求失败: ${err.message},尝试替代方法`);
+
+ // 如果是CORS错误,尝试使用no-cors模式(但这会导致不能读取响应内容)
+ if (
+ err.message &&
+ (err.message.includes('CORS') ||
+ err.message.includes('网络') ||
+ err.message.includes('network'))
+ ) {
+ console.log('检测到CORS错误,提示用户手动获取JSON数据');
+
+ const userConfirmed = window.confirm(
+ `由于CORS限制,无法直接从 ${fetchUrl} 获取工作流数据。\n\n` +
+ '请手动打开该URL,复制JSON内容,然后点击"确定"来粘贴。\n\n' +
+ '或者点击"取消"放弃导入。'
+ );
+
+ if (userConfirmed) {
+ try {
+ const clipboardText = await navigator.clipboard.readText().catch(() => '');
+ if (clipboardText) {
+ try {
+ const customResponse: CustomResponse = {
+ text: () => Promise.resolve(clipboardText),
+ ok: true,
+ status: 200,
+ headers: {
+ get: (name: string) => (name === 'content-type' ? 'application/json' : null)
+ }
+ };
+ return customResponse;
+ } catch (err) {
+ throw new Error('剪贴板内容不是有效的JSON格式,请确保复制了完整的JSON数据');
+ }
+ } else {
+ throw new Error('无法读取剪贴板内容,请确保已授予网站剪贴板权限');
+ }
+ } catch (err) {
+ console.error('读取剪贴板失败:', err);
+
+ // 如果剪贴板读取失败,提供手动输入选项
+ const jsonInput = window.prompt('无法自动读取剪贴板。请手动粘贴工作流JSON数据:', '');
+
+ if (jsonInput) {
+ const customResponse: CustomResponse = {
+ text: () => Promise.resolve(jsonInput),
+ ok: true,
+ status: 200,
+ headers: {
+ get: (name: string) => (name === 'content-type' ? 'application/json' : null)
+ }
+ };
+ return customResponse;
+ } else {
+ throw new Error('未提供JSON数据,导入已取消');
+ }
+ }
+ } else {
+ throw new Error('用户取消了导入操作');
+ }
+ }
+
+ throw err;
+ });
+
+ clearTimeout(timeoutId);
+
+ console.log(`获取状态码: ${response.status}`);
+
+ if (!response.ok) {
+ throw new Error(`获取工作流失败,HTTP错误状态: ${response.status}`);
+ }
+
+ const contentType = response.headers.get('content-type');
+ console.log(`响应内容类型: ${contentType}`);
+
+ if (!contentType || !contentType.includes('application/json')) {
+ console.warn(`警告:响应内容类型不是JSON (${contentType})`);
+ }
+
+ const text = await response.text();
+ console.log(`获取到响应内容长度: ${text.length}`);
+
+ if (!text || text.trim() === '') {
+ throw new Error('获取的响应内容为空');
+ }
+
+ let data;
+ try {
+ data = JSON.parse(text);
+ } catch (jsonError) {
+ console.error('JSON解析失败:', jsonError);
+ throw new Error('无法解析响应内容为JSON,请确保URL返回有效的JSON数据');
+ }
+
+ console.log('工作流数据获取成功', data ? '数据有效' : '数据为空');
+
+ if (!data) {
+ throw new Error('获取的工作流数据为空');
+ }
+
+ return data;
+ } catch (fetchError: any) {
+ if (fetchError.name === 'AbortError') {
+ throw new Error('获取工作流超时,请检查URL是否正确或稍后重试');
+ } else if (fetchError.message && fetchError.message.includes('CORS')) {
+ throw new Error('CORS错误:无法访问该URL,请确保URL允许跨域请求或使用支持CORS的端点');
+ }
+ throw fetchError;
+ }
+ } catch (error) {
+ console.error('获取工作流数据失败:', error);
+ throw new Error(`获取工作流失败: ${error instanceof Error ? error.message : String(error)}`);
+ }
+};
+
+/**
+ * 从URL获取工作流JSON数据并创建应用
+ */
+export const importWorkflowFromUrl = async ({
+ url,
+ name,
+ parentId
+}: {
+ url: string;
+ name?: string;
+ parentId?: string;
+}) => {
+ try {
+ console.log(`开始从URL导入工作流: ${url}`);
+
+ // 获取工作流数据
+ const data = await fetchWorkflowFromUrl(url);
+
+ if (!data || !data.nodes || !data.edges) {
+ throw new Error('工作流数据格式不正确,缺少nodes或edges');
+ }
+
+ // 获取应用类型
+ const appType = getAppType(data);
+ if (!appType) {
+ throw new Error('无法识别应用类型,请确保导入的是有效的工作流JSON');
+ }
+
+ console.log(`识别到工作流类型: ${appType}`);
+
+ // 创建应用
+ const appId = await postCreateApp({
+ parentId,
+ avatar: appTypeMap[appType].avatar,
+ name: name || `未命名 ${new Date().toLocaleString()}`,
+ type: appType,
+ modules: data.nodes || [],
+ edges: data.edges || [],
+ chatConfig: data.chatConfig || {}
+ });
+
+ console.log(`工作流导入成功,创建的应用ID: ${appId}`);
+ return appId;
+ } catch (error) {
+ console.error('导入工作流失败:', error);
+ throw error;
+ }
+};
From a9da786f83df21182cff679e83c3bed911d70d16 Mon Sep 17 00:00:00 2001
From: dreamer6680 <1468683855@qq.com>
Date: Fri, 25 Apr 2025 14:45:04 +0800
Subject: [PATCH 02/10] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B7=A5=E4=BD=9C?=
=?UTF-8?q?=E6=B5=81=E5=AF=BC=E5=85=A5=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF?=
=?UTF-8?q?=E6=8C=81=E4=BB=8EURL=E8=8E=B7=E5=8F=96=E5=B7=A5=E4=BD=9C?=
=?UTF-8?q?=E6=B5=81JSON=E6=95=B0=E6=8D=AE=E5=B9=B6=E5=88=9B=E5=BB=BA?=
=?UTF-8?q?=E5=BA=94=E7=94=A8=E3=80=82=E5=AE=9E=E7=8E=B0=E4=BA=86URL?=
=?UTF-8?q?=E9=AA=8C=E8=AF=81=E3=80=81CORS=E5=A4=84=E7=90=86=E3=80=81?=
=?UTF-8?q?=E5=89=AA=E8=B4=B4=E6=9D=BF=E8=AF=BB=E5=8F=96=E7=AD=89=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=EF=BC=8C=E7=A1=AE=E4=BF=9D=E7=94=A8=E6=88=B7=E8=83=BD?=
=?UTF-8?q?=E5=A4=9F=E9=A1=BA=E5=88=A9=E5=AF=BC=E5=85=A5=E5=B7=A5=E4=BD=9C?=
=?UTF-8?q?=E6=B5=81=E6=95=B0=E6=8D=AE=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
projects/app/src/web/core/app/workflow.ts | 283 ++++++++++++++++++++++
1 file changed, 283 insertions(+)
create mode 100644 projects/app/src/web/core/app/workflow.ts
diff --git a/projects/app/src/web/core/app/workflow.ts b/projects/app/src/web/core/app/workflow.ts
new file mode 100644
index 000000000000..0a753b4888b5
--- /dev/null
+++ b/projects/app/src/web/core/app/workflow.ts
@@ -0,0 +1,283 @@
+import { getAppType } from '@fastgpt/global/core/app/utils';
+import { postCreateApp } from './api';
+import { appTypeMap } from '@/pageComponents/app/constants';
+/**
+ * 从URL获取工作流JSON数据
+ */
+export const fetchWorkflowFromUrl = async (url: string) => {
+ // 自定义响应类型,用于手动处理剪贴板内容
+ type CustomResponse = {
+ text: () => Promise;
+ ok: boolean;
+ status: number;
+ headers: {
+ get: (name: string) => string | null;
+ };
+ };
+
+ try {
+ if (!url || typeof url !== 'string') {
+ throw new Error('WORKFLOW_IMPORT_ERROR: URL为空或格式错误');
+ }
+
+ // 清理URL
+ let fetchUrl = url.trim();
+
+ // 如果URL最后有斜杠,移除它
+ if (fetchUrl.endsWith('/')) {
+ fetchUrl = fetchUrl.slice(0, -1);
+ }
+
+ // 确保URL是绝对路径
+ if (!fetchUrl.startsWith('http://') && !fetchUrl.startsWith('https://')) {
+ fetchUrl = `https://${fetchUrl}`;
+ }
+
+ // 设置请求超时
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 30000); // 30秒超时
+
+ try {
+ // 首先检查这是否是本地请求(localhost或127.0.0.1)
+ const isLocalRequest = fetchUrl.includes('localhost') || fetchUrl.includes('127.0.0.1');
+
+ if (isLocalRequest) {
+ try {
+ // 尝试方法1: 使用相对路径(如果URL是指向同一个域的不同端口)
+ const urlObj = new URL(fetchUrl);
+ const path = urlObj.pathname + urlObj.search;
+ console.log(`尝试使用相对路径请求: ${path}`);
+ const relativeResponse = await fetch(path, {
+ signal: controller.signal,
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json'
+ }
+ }).catch((e) => {
+ console.log(`相对路径请求失败: ${e.message}`);
+ return null;
+ });
+
+ if (relativeResponse?.ok) {
+ clearTimeout(timeoutId);
+ const text = await relativeResponse.text();
+ return JSON.parse(text);
+ }
+ } catch (err: any) {
+ console.log(`相对路径方法失败: ${err.message}`);
+ }
+
+ // 尝试方法2: 如果是本地开发环境,建议用户手动复制JSON
+ const userConfirmed = window.confirm(
+ `由于CORS限制,无法直接从 ${fetchUrl} 获取工作流数据。\n\n` +
+ '请手动打开该URL,复制JSON内容,然后点击"确定"来粘贴。\n\n' +
+ '或者点击"取消"放弃导入。'
+ );
+
+ if (userConfirmed) {
+ try {
+ const clipboardText = await navigator.clipboard.readText().catch(() => '');
+ if (clipboardText) {
+ try {
+ return JSON.parse(clipboardText);
+ } catch (err) {
+ throw new Error('剪贴板内容不是有效的JSON格式,请确保复制了完整的JSON数据');
+ }
+ } else {
+ throw new Error('无法读取剪贴板内容,请确保已授予网站剪贴板权限');
+ }
+ } catch (err) {
+ console.error('读取剪贴板失败:', err);
+
+ // 如果剪贴板读取失败,提供手动输入选项
+ const jsonInput = window.prompt('无法自动读取剪贴板。请手动粘贴工作流JSON数据:', '');
+
+ if (jsonInput) {
+ try {
+ return JSON.parse(jsonInput);
+ } catch (err) {
+ throw new Error('输入的内容不是有效的JSON格式');
+ }
+ } else {
+ throw new Error('未提供JSON数据,导入已取消');
+ }
+ }
+ } else {
+ throw new Error('用户取消了导入操作');
+ }
+ }
+
+ // 如果不是本地请求,尝试正常请求
+ const response = await fetch(fetchUrl, {
+ signal: controller.signal,
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ mode: 'cors' // 尝试CORS
+ }).catch(async (err) => {
+ console.log(`直接请求失败: ${err.message},尝试替代方法`);
+
+ // 如果是CORS错误,尝试使用no-cors模式(但这会导致不能读取响应内容)
+ if (
+ err.message &&
+ (err.message.includes('CORS') ||
+ err.message.includes('网络') ||
+ err.message.includes('network'))
+ ) {
+ console.log('检测到CORS错误,提示用户手动获取JSON数据');
+
+ const userConfirmed = window.confirm(
+ `由于CORS限制,无法直接从 ${fetchUrl} 获取工作流数据。\n\n` +
+ '请手动打开该URL,复制JSON内容,然后点击"确定"来粘贴。\n\n' +
+ '或者点击"取消"放弃导入。'
+ );
+
+ if (userConfirmed) {
+ try {
+ const clipboardText = await navigator.clipboard.readText().catch(() => '');
+ if (clipboardText) {
+ try {
+ const customResponse: CustomResponse = {
+ text: () => Promise.resolve(clipboardText),
+ ok: true,
+ status: 200,
+ headers: {
+ get: (name: string) => (name === 'content-type' ? 'application/json' : null)
+ }
+ };
+ return customResponse;
+ } catch (err) {
+ throw new Error('剪贴板内容不是有效的JSON格式,请确保复制了完整的JSON数据');
+ }
+ } else {
+ throw new Error('无法读取剪贴板内容,请确保已授予网站剪贴板权限');
+ }
+ } catch (err) {
+ console.error('读取剪贴板失败:', err);
+
+ // 如果剪贴板读取失败,提供手动输入选项
+ const jsonInput = window.prompt('无法自动读取剪贴板。请手动粘贴工作流JSON数据:', '');
+
+ if (jsonInput) {
+ const customResponse: CustomResponse = {
+ text: () => Promise.resolve(jsonInput),
+ ok: true,
+ status: 200,
+ headers: {
+ get: (name: string) => (name === 'content-type' ? 'application/json' : null)
+ }
+ };
+ return customResponse;
+ } else {
+ throw new Error('未提供JSON数据,导入已取消');
+ }
+ }
+ } else {
+ throw new Error('用户取消了导入操作');
+ }
+ }
+
+ throw err;
+ });
+
+ clearTimeout(timeoutId);
+
+ console.log(`获取状态码: ${response.status}`);
+
+ if (!response.ok) {
+ throw new Error(`获取工作流失败,HTTP错误状态: ${response.status}`);
+ }
+
+ const contentType = response.headers.get('content-type');
+ console.log(`响应内容类型: ${contentType}`);
+
+ if (!contentType || !contentType.includes('application/json')) {
+ console.warn(`警告:响应内容类型不是JSON (${contentType})`);
+ }
+
+ const text = await response.text();
+ console.log(`获取到响应内容长度: ${text.length}`);
+
+ if (!text || text.trim() === '') {
+ throw new Error('获取的响应内容为空');
+ }
+
+ let data;
+ try {
+ data = JSON.parse(text);
+ } catch (jsonError) {
+ console.error('JSON解析失败:', jsonError);
+ throw new Error('无法解析响应内容为JSON,请确保URL返回有效的JSON数据');
+ }
+
+ console.log('工作流数据获取成功', data ? '数据有效' : '数据为空');
+
+ if (!data) {
+ throw new Error('获取的工作流数据为空');
+ }
+
+ return data;
+ } catch (fetchError: any) {
+ if (fetchError.name === 'AbortError') {
+ throw new Error('获取工作流超时,请检查URL是否正确或稍后重试');
+ } else if (fetchError.message && fetchError.message.includes('CORS')) {
+ throw new Error('CORS错误:无法访问该URL,请确保URL允许跨域请求或使用支持CORS的端点');
+ }
+ throw fetchError;
+ }
+ } catch (error) {
+ console.error('获取工作流数据失败:', error);
+ throw new Error(`获取工作流失败: ${error instanceof Error ? error.message : String(error)}`);
+ }
+};
+
+/**
+ * 从URL获取工作流JSON数据并创建应用
+ */
+export const importWorkflowFromUrl = async ({
+ url,
+ name,
+ parentId
+}: {
+ url: string;
+ name?: string;
+ parentId?: string;
+}) => {
+ try {
+ console.log(`开始从URL导入工作流: ${url}`);
+
+ // 获取工作流数据
+ const data = await fetchWorkflowFromUrl(url);
+
+ if (!data || !data.nodes || !data.edges) {
+ throw new Error('工作流数据格式不正确,缺少nodes或edges');
+ }
+
+ // 获取应用类型
+ const appType = getAppType(data);
+ if (!appType) {
+ throw new Error('无法识别应用类型,请确保导入的是有效的工作流JSON');
+ }
+
+ console.log(`识别到工作流类型: ${appType}`);
+
+ // 创建应用
+ const appId = await postCreateApp({
+ parentId,
+ avatar: appTypeMap[appType].avatar,
+ name: name || `未命名 ${new Date().toLocaleString()}`,
+ type: appType,
+ modules: data.nodes || [],
+ edges: data.edges || [],
+ chatConfig: data.chatConfig || {}
+ });
+
+ console.log(`工作流导入成功,创建的应用ID: ${appId}`);
+ return appId;
+ } catch (error) {
+ console.error('导入工作流失败:', error);
+ throw error;
+ }
+};
From 65efde797e575af71fe2e68f07c797a911d908d3 Mon Sep 17 00:00:00 2001
From: dreamer6680 <1468683855@qq.com>
Date: Fri, 25 Apr 2025 16:48:07 +0800
Subject: [PATCH 03/10] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=B7=A5=E4=BD=9C?=
=?UTF-8?q?=E6=B5=81=E5=AF=BC=E5=85=A5=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=B0=86?=
=?UTF-8?q?=E5=AF=BC=E5=85=A5=E9=80=BB=E8=BE=91=E4=BB=8Eutils=E6=A8=A1?=
=?UTF-8?q?=E5=9D=97=E8=BF=81=E7=A7=BB=E8=87=B3workflow=E6=A8=A1=E5=9D=97?=
=?UTF-8?q?=EF=BC=8C=E5=B9=B6=E4=BF=AE=E6=AD=A3=E7=9B=B8=E5=85=B3=E5=AF=BC?=
=?UTF-8?q?=E5=85=A5=E8=B7=AF=E5=BE=84=E3=80=82=E6=AD=A4=E6=9B=B4=E6=94=B9?=
=?UTF-8?q?=E6=9C=89=E5=8A=A9=E4=BA=8E=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?=
=?UTF-8?q?=E7=9A=84=E6=B8=85=E6=99=B0=E5=92=8C=E6=A8=A1=E5=9D=97=E5=8C=96?=
=?UTF-8?q?=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../app/src/components/WorkflowImporter.tsx | 2 +-
projects/app/src/web/core/app/utils.ts | 281 ------------------
2 files changed, 1 insertion(+), 282 deletions(-)
diff --git a/projects/app/src/components/WorkflowImporter.tsx b/projects/app/src/components/WorkflowImporter.tsx
index 5fb298146419..e4167696738e 100644
--- a/projects/app/src/components/WorkflowImporter.tsx
+++ b/projects/app/src/components/WorkflowImporter.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useState, useRef } from 'react';
import { useRouter } from 'next/router';
-import { importWorkflowFromUrl } from '@/web/core/app/utils';
+import { importWorkflowFromUrl } from '@/web/core/app/workflow';
import {
Modal,
ModalOverlay,
diff --git a/projects/app/src/web/core/app/utils.ts b/projects/app/src/web/core/app/utils.ts
index 302b69c3953b..bb15a8244ed8 100644
--- a/projects/app/src/web/core/app/utils.ts
+++ b/projects/app/src/web/core/app/utils.ts
@@ -633,284 +633,3 @@ export const getAppQGuideCustomURL = (appDetail: AppDetailType | AppSchema): str
?.inputs.find((i) => i.key === NodeInputKeyEnum.chatInputGuide)?.value.customUrl || ''
);
};
-
-/**
- * 从URL获取工作流JSON数据
- */
-export const fetchWorkflowFromUrl = async (url: string) => {
- // 自定义响应类型,用于手动处理剪贴板内容
- type CustomResponse = {
- text: () => Promise;
- ok: boolean;
- status: number;
- headers: {
- get: (name: string) => string | null;
- };
- };
-
- try {
- if (!url || typeof url !== 'string') {
- throw new Error('WORKFLOW_IMPORT_ERROR: URL为空或格式错误');
- }
-
- // 清理URL
- let fetchUrl = url.trim();
-
- // 如果URL最后有斜杠,移除它
- if (fetchUrl.endsWith('/')) {
- fetchUrl = fetchUrl.slice(0, -1);
- }
-
- // 确保URL是绝对路径
- if (!fetchUrl.startsWith('http://') && !fetchUrl.startsWith('https://')) {
- fetchUrl = `https://${fetchUrl}`;
- }
-
- // 设置请求超时
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 30000); // 30秒超时
-
- try {
- // 首先检查这是否是本地请求(localhost或127.0.0.1)
- const isLocalRequest = fetchUrl.includes('localhost') || fetchUrl.includes('127.0.0.1');
-
- if (isLocalRequest) {
- try {
- // 尝试方法1: 使用相对路径(如果URL是指向同一个域的不同端口)
- const urlObj = new URL(fetchUrl);
- const path = urlObj.pathname + urlObj.search;
- console.log(`尝试使用相对路径请求: ${path}`);
- const relativeResponse = await fetch(path, {
- signal: controller.signal,
- headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json'
- }
- }).catch((e) => {
- console.log(`相对路径请求失败: ${e.message}`);
- return null;
- });
-
- if (relativeResponse?.ok) {
- clearTimeout(timeoutId);
- const text = await relativeResponse.text();
- return JSON.parse(text);
- }
- } catch (err: any) {
- console.log(`相对路径方法失败: ${err.message}`);
- }
-
- // 尝试方法2: 如果是本地开发环境,建议用户手动复制JSON
- const userConfirmed = window.confirm(
- `由于CORS限制,无法直接从 ${fetchUrl} 获取工作流数据。\n\n` +
- '请手动打开该URL,复制JSON内容,然后点击"确定"来粘贴。\n\n' +
- '或者点击"取消"放弃导入。'
- );
-
- if (userConfirmed) {
- try {
- const clipboardText = await navigator.clipboard.readText().catch(() => '');
- if (clipboardText) {
- try {
- return JSON.parse(clipboardText);
- } catch (err) {
- throw new Error('剪贴板内容不是有效的JSON格式,请确保复制了完整的JSON数据');
- }
- } else {
- throw new Error('无法读取剪贴板内容,请确保已授予网站剪贴板权限');
- }
- } catch (err) {
- console.error('读取剪贴板失败:', err);
-
- // 如果剪贴板读取失败,提供手动输入选项
- const jsonInput = window.prompt('无法自动读取剪贴板。请手动粘贴工作流JSON数据:', '');
-
- if (jsonInput) {
- try {
- return JSON.parse(jsonInput);
- } catch (err) {
- throw new Error('输入的内容不是有效的JSON格式');
- }
- } else {
- throw new Error('未提供JSON数据,导入已取消');
- }
- }
- } else {
- throw new Error('用户取消了导入操作');
- }
- }
-
- // 如果不是本地请求,尝试正常请求
- const response = await fetch(fetchUrl, {
- signal: controller.signal,
- headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest'
- },
- mode: 'cors' // 尝试CORS
- }).catch(async (err) => {
- console.log(`直接请求失败: ${err.message},尝试替代方法`);
-
- // 如果是CORS错误,尝试使用no-cors模式(但这会导致不能读取响应内容)
- if (
- err.message &&
- (err.message.includes('CORS') ||
- err.message.includes('网络') ||
- err.message.includes('network'))
- ) {
- console.log('检测到CORS错误,提示用户手动获取JSON数据');
-
- const userConfirmed = window.confirm(
- `由于CORS限制,无法直接从 ${fetchUrl} 获取工作流数据。\n\n` +
- '请手动打开该URL,复制JSON内容,然后点击"确定"来粘贴。\n\n' +
- '或者点击"取消"放弃导入。'
- );
-
- if (userConfirmed) {
- try {
- const clipboardText = await navigator.clipboard.readText().catch(() => '');
- if (clipboardText) {
- try {
- const customResponse: CustomResponse = {
- text: () => Promise.resolve(clipboardText),
- ok: true,
- status: 200,
- headers: {
- get: (name: string) => (name === 'content-type' ? 'application/json' : null)
- }
- };
- return customResponse;
- } catch (err) {
- throw new Error('剪贴板内容不是有效的JSON格式,请确保复制了完整的JSON数据');
- }
- } else {
- throw new Error('无法读取剪贴板内容,请确保已授予网站剪贴板权限');
- }
- } catch (err) {
- console.error('读取剪贴板失败:', err);
-
- // 如果剪贴板读取失败,提供手动输入选项
- const jsonInput = window.prompt('无法自动读取剪贴板。请手动粘贴工作流JSON数据:', '');
-
- if (jsonInput) {
- const customResponse: CustomResponse = {
- text: () => Promise.resolve(jsonInput),
- ok: true,
- status: 200,
- headers: {
- get: (name: string) => (name === 'content-type' ? 'application/json' : null)
- }
- };
- return customResponse;
- } else {
- throw new Error('未提供JSON数据,导入已取消');
- }
- }
- } else {
- throw new Error('用户取消了导入操作');
- }
- }
-
- throw err;
- });
-
- clearTimeout(timeoutId);
-
- console.log(`获取状态码: ${response.status}`);
-
- if (!response.ok) {
- throw new Error(`获取工作流失败,HTTP错误状态: ${response.status}`);
- }
-
- const contentType = response.headers.get('content-type');
- console.log(`响应内容类型: ${contentType}`);
-
- if (!contentType || !contentType.includes('application/json')) {
- console.warn(`警告:响应内容类型不是JSON (${contentType})`);
- }
-
- const text = await response.text();
- console.log(`获取到响应内容长度: ${text.length}`);
-
- if (!text || text.trim() === '') {
- throw new Error('获取的响应内容为空');
- }
-
- let data;
- try {
- data = JSON.parse(text);
- } catch (jsonError) {
- console.error('JSON解析失败:', jsonError);
- throw new Error('无法解析响应内容为JSON,请确保URL返回有效的JSON数据');
- }
-
- console.log('工作流数据获取成功', data ? '数据有效' : '数据为空');
-
- if (!data) {
- throw new Error('获取的工作流数据为空');
- }
-
- return data;
- } catch (fetchError: any) {
- if (fetchError.name === 'AbortError') {
- throw new Error('获取工作流超时,请检查URL是否正确或稍后重试');
- } else if (fetchError.message && fetchError.message.includes('CORS')) {
- throw new Error('CORS错误:无法访问该URL,请确保URL允许跨域请求或使用支持CORS的端点');
- }
- throw fetchError;
- }
- } catch (error) {
- console.error('获取工作流数据失败:', error);
- throw new Error(`获取工作流失败: ${error instanceof Error ? error.message : String(error)}`);
- }
-};
-
-/**
- * 从URL获取工作流JSON数据并创建应用
- */
-export const importWorkflowFromUrl = async ({
- url,
- name,
- parentId
-}: {
- url: string;
- name?: string;
- parentId?: string;
-}) => {
- try {
- console.log(`开始从URL导入工作流: ${url}`);
-
- // 获取工作流数据
- const data = await fetchWorkflowFromUrl(url);
-
- if (!data || !data.nodes || !data.edges) {
- throw new Error('工作流数据格式不正确,缺少nodes或edges');
- }
-
- // 获取应用类型
- const appType = getAppType(data);
- if (!appType) {
- throw new Error('无法识别应用类型,请确保导入的是有效的工作流JSON');
- }
-
- console.log(`识别到工作流类型: ${appType}`);
-
- // 创建应用
- const appId = await postCreateApp({
- parentId,
- avatar: appTypeMap[appType].avatar,
- name: name || `未命名 ${new Date().toLocaleString()}`,
- type: appType,
- modules: data.nodes || [],
- edges: data.edges || [],
- chatConfig: data.chatConfig || {}
- });
-
- console.log(`工作流导入成功,创建的应用ID: ${appId}`);
- return appId;
- } catch (error) {
- console.error('导入工作流失败:', error);
- throw error;
- }
-};
From 79eb1d42f49be410f682d326d86c4915658f5716 Mon Sep 17 00:00:00 2001
From: dreamer6680 <1468683855@qq.com>
Date: Sun, 27 Apr 2025 12:06:21 +0800
Subject: [PATCH 04/10] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=B7=A5=E4=BD=9C?=
=?UTF-8?q?=E6=B5=81=E5=AF=BC=E5=85=A5=E7=BB=84=E4=BB=B6=EF=BC=8C=E9=87=8D?=
=?UTF-8?q?=E6=9E=84=E5=AF=BC=E5=85=A5=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E?=
=?UTF-8?q?=E5=8A=A0=E4=BB=8EURL=E8=8E=B7=E5=8F=96=E5=B7=A5=E4=BD=9C?=
=?UTF-8?q?=E6=B5=81=E6=95=B0=E6=8D=AE=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=8C?=
=?UTF-8?q?=E5=B9=B6=E5=AE=9E=E7=8E=B0JSON=E9=85=8D=E7=BD=AE=E5=AF=BC?=
=?UTF-8?q?=E5=85=A5=E7=AA=97=E5=8F=A3=E3=80=82=E4=BF=AE=E5=A4=8D=E4=BA=86?=
=?UTF-8?q?=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E5=92=8C=E9=94=99=E8=AF=AF?=
=?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=8C=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7?=
=?UTF-8?q?=E4=BD=93=E9=AA=8C=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
projects/app/src/components/Layout/index.tsx | 14 --
.../app/src/components/WorkflowImporter.tsx | 223 ++++++++++++------
.../dashboard/apps/JsonImportModal.tsx | 127 +++++++---
.../app/src/pages/dashboard/apps/index.tsx | 12 +-
projects/app/src/web/core/app/workflow.ts | 18 +-
5 files changed, 266 insertions(+), 128 deletions(-)
diff --git a/projects/app/src/components/Layout/index.tsx b/projects/app/src/components/Layout/index.tsx
index 49a8c010b2b4..273b49330949 100644
--- a/projects/app/src/components/Layout/index.tsx
+++ b/projects/app/src/components/Layout/index.tsx
@@ -15,7 +15,6 @@ import { useDebounceEffect, useMount } from 'ahooks';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
import WorkorderButton from './WorkorderButton';
-import WorkflowImporter from '@/components/WorkflowImporter';
const Navbar = dynamic(() => import('./navbar'));
const NavbarPhone = dynamic(() => import('./navbarPhone'));
@@ -165,19 +164,6 @@ const Layout = ({ children }: { children: JSX.Element }) => {
>
)}
- {(() => {
- // 添加浏览器环境检查,避免服务器端渲染时的错误
- const isBrowser = typeof window !== 'undefined';
- if (isBrowser && sessionStorage.getItem('utm_workflow')) {
- console.log(
- 'Layout: 准备渲染WorkflowImporter,userInfo =',
- !!userInfo,
- userInfo?.username
- );
- return !!userInfo && ;
- }
- return null;
- })()}
>
diff --git a/projects/app/src/components/WorkflowImporter.tsx b/projects/app/src/components/WorkflowImporter.tsx
index e4167696738e..ea90b7a52148 100644
--- a/projects/app/src/components/WorkflowImporter.tsx
+++ b/projects/app/src/components/WorkflowImporter.tsx
@@ -1,27 +1,33 @@
-import React, { useEffect, useState, useRef } from 'react';
+import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
-import { importWorkflowFromUrl } from '@/web/core/app/workflow';
+import { importWorkflowFromUrl, fetchWorkflowFromUrl } from '@/web/core/app/workflow';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
+ ModalFooter,
Flex,
Spinner,
Text,
- useToast
+ useToast,
+ Button
} from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useTranslation } from 'next-i18next';
+import ImportAppConfigEditor from '@/pageComponents/app/ImportAppConfigEditor';
const WorkflowImporter = () => {
const { t } = useTranslation();
const router = useRouter();
const toast = useToast();
const { userInfo } = useUserStore();
- const [isImporting, setIsImporting] = useState(false);
- const [importStatus, setImportStatus] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [status, setStatus] = useState('');
+ const [showJsonImporter, setShowJsonImporter] = useState(false);
+ const [defaultJson, setDefaultJson] = useState('');
+ const [workflowUrl, setWorkflowUrl] = useState('');
useEffect(() => {
// 添加浏览器环境检查
@@ -33,68 +39,39 @@ const WorkflowImporter = () => {
return;
}
- const checkAndImportWorkflow = async () => {
+ const checkWorkflow = async () => {
try {
- let workflowUrl = sessionStorage.getItem('utm_workflow');
+ let url = sessionStorage.getItem('utm_workflow');
- if (!workflowUrl) {
+ if (!url) {
return;
}
- sessionStorage.removeItem('utm_workflow');
-
- setIsImporting(true);
- setImportStatus('正在准备导入工作流...');
-
- toast({
- title: '发现工作流URL,正在导入...',
- status: 'info',
- duration: 5000,
- isClosable: true
- });
+ // 保存URL,等待导入完成后再移除
+ setWorkflowUrl(url);
+ setIsLoading(true);
+ setStatus('正在获取工作流数据...');
try {
- setImportStatus('正在从URL获取工作流数据...');
-
- // 获取utm_params中的source参数,如果有的话
- let content = '未命名';
- try {
- const utmParams = localStorage.getItem('utm_params');
- if (utmParams) {
- const params = JSON.parse(utmParams);
- if (params.content) content = params.content;
- }
- } catch (error) {
- console.error('解析utm_params出错:', error);
- }
+ // 获取工作流数据
+ const workflowData = await fetchWorkflowFromUrl(url);
- // 导入工作流
- const appId = await importWorkflowFromUrl({
- url: workflowUrl,
- name: `${content}`
- });
-
- setImportStatus('工作流导入成功!');
+ if (!workflowData) {
+ throw new Error('无法获取工作流数据');
+ }
- // 导入成功后提示并跳转
- toast({
- title: '工作流导入成功',
- status: 'success',
- duration: 5000,
- isClosable: true
- });
+ // 将获取到的JSON数据设置为defaultJson
+ setDefaultJson(JSON.stringify(workflowData, null, 2));
- // 延迟一下再跳转,确保用户看到成功消息
- setTimeout(() => {
- setIsImporting(false);
- router.push(`/app/detail?appId=${appId}`);
- }, 1500);
+ // 显示JSON导入窗口
+ setIsLoading(false);
+ setShowJsonImporter(true);
} catch (error) {
- console.error('导入工作流失败:', error);
- setImportStatus(`导入失败:${error instanceof Error ? error.message : '未知错误'}`);
+ console.error('获取工作流数据失败:', error);
+ setStatus(`获取失败:${error instanceof Error ? error.message : '未知错误'}`);
toast({
- title: '导入工作流失败',
+ title: '获取工作流数据失败',
description: error instanceof Error ? error.message : '未知错误',
status: 'error',
duration: 5000,
@@ -102,31 +79,137 @@ const WorkflowImporter = () => {
});
setTimeout(() => {
- setIsImporting(false);
+ setIsLoading(false);
}, 2000);
}
} catch (error) {
console.error('检查工作流URL出错:', error);
- setIsImporting(false);
+ setIsLoading(false);
}
};
- checkAndImportWorkflow();
- }, [userInfo, router, toast]);
+ checkWorkflow();
+ }, [userInfo, toast]);
+
+ // 处理导入工作流
+ const handleImportWorkflow = async (jsonConfig) => {
+ try {
+ setShowJsonImporter(false);
+ setIsLoading(true);
+ setStatus('正在导入工作流...');
+
+ // 获取utm_params中的source参数,如果有的话
+ let content = '未命名';
+ try {
+ const utmParams = localStorage.getItem('utm_params');
+ if (utmParams) {
+ const params = JSON.parse(utmParams);
+ if (params.content) content = params.content;
+ }
+ } catch (error) {
+ console.error('解析utm_params出错:', error);
+ }
+
+ // 解析JSON
+ let workflowData;
+ try {
+ workflowData = JSON.parse(jsonConfig);
+ } catch (e) {
+ throw new Error('JSON格式无效,请检查配置');
+ }
+
+ // 创建应用
+ const appId = await importWorkflowFromUrl({
+ url: workflowUrl,
+ name: `${content}`,
+ data: workflowData // 直接使用已获取的数据
+ });
+
+ // 导入成功后提示并跳转
+ toast({
+ title: '工作流导入成功',
+ status: 'success',
+ duration: 5000,
+ isClosable: true
+ });
+
+ // 清除sessionStorage中的utm_workflow
+ sessionStorage.removeItem('utm_workflow');
+
+ // 延迟一下再跳转,确保用户看到成功消息
+ setTimeout(() => {
+ setIsLoading(false);
+ router.push(`/app/detail?appId=${appId}`);
+ }, 1500);
+ } catch (error) {
+ console.error('导入工作流失败:', error);
+ setStatus(`导入失败:${error instanceof Error ? error.message : '未知错误'}`);
+
+ toast({
+ title: '导入工作流失败',
+ description: error instanceof Error ? error.message : '未知错误',
+ status: 'error',
+ duration: 5000,
+ isClosable: true
+ });
+
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 2000);
+ }
+ };
+
+ // 取消导入
+ const handleCancel = () => {
+ setShowJsonImporter(false);
+ // 清除sessionStorage中的utm_workflow
+ sessionStorage.removeItem('utm_workflow');
+ };
return (
- {}} closeOnOverlayClick={false} isCentered>
-
-
- 导入工作流
-
-
-
- {importStatus}
-
-
-
-
+ <>
+ {/* 加载中状态弹窗 */}
+ {}} closeOnOverlayClick={false} isCentered>
+
+
+ 导入工作流
+
+
+
+ {status}
+
+
+
+
+
+ {/* JSON配置导入窗口 */}
+
+
+
+ 导入工作流配置
+
+
+ 已从URL获取工作流配置,您可以检查并编辑下方的配置,然后点击导入按钮完成导入。
+
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx b/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx
index 58367c3624e2..7509c5066c2b 100644
--- a/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx
+++ b/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx
@@ -1,5 +1,5 @@
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
-import { Box, Button, Flex, Input, ModalBody, ModalFooter } from '@chakra-ui/react';
+import { Box, Button, Flex, Input, ModalBody, ModalFooter, Spinner, Text } 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';
@@ -7,7 +7,7 @@ import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { appTypeMap } from '@/pageComponents/app/constants';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
-import { useMemo } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { getAppType } from '@fastgpt/global/core/app/utils';
import { useContextSelector } from 'use-context-selector';
import { AppListContext } from './context';
@@ -16,6 +16,8 @@ import { postCreateApp } from '@/web/core/app/api';
import { useRouter } from 'next/router';
import { form2AppWorkflow } from '@/web/core/app/utils';
import ImportAppConfigEditor from '@/pageComponents/app/ImportAppConfigEditor';
+import { fetchWorkflowFromUrl } from '@/web/core/app/workflow';
+import { useToast } from '@fastgpt/web/hooks/useToast';
type FormType = {
avatar: string;
@@ -27,6 +29,9 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
const router = useRouter();
+ const { toast } = useToast();
+ const [isLoading, setIsLoading] = useState(false);
+ const [loadingStatus, setLoadingStatus] = useState('');
const { register, setValue, watch, handleSubmit } = useForm({
defaultValues: {
@@ -37,6 +42,47 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => {
});
const workflowStr = watch('workflowStr');
+ // 检查并获取utm_workflow
+ useEffect(() => {
+ const isBrowser = typeof window !== 'undefined';
+ if (!isBrowser) return;
+
+ const checkWorkflow = async () => {
+ try {
+ const url = sessionStorage.getItem('utm_workflow');
+ if (!url) return;
+
+ try {
+ const workflowData = await fetchWorkflowFromUrl(url);
+
+ if (!workflowData) {
+ throw new Error('无法获取工作流数据');
+ }
+
+ setValue('workflowStr', JSON.stringify(workflowData, null, 2));
+
+ try {
+ const utmParams = localStorage.getItem('utm_params');
+ if (utmParams) {
+ const params = JSON.parse(utmParams);
+ if (params.content) setValue('name', params.content);
+ }
+ } catch (error) {
+ console.error('解析utm_params出错:', error);
+ }
+
+ sessionStorage.removeItem('utm_workflow');
+ } catch (error) {
+ console.error('获取工作流数据失败:', error);
+ }
+ } catch (error) {
+ console.error('检查工作流URL出错:', error);
+ }
+ };
+
+ checkWorkflow();
+ }, []);
+
const avatar = watch('avatar');
const {
File,
@@ -122,44 +168,55 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => {
iconColor={'primary.600'}
>
-
- {t('common:common.Set Name')}
-
-
-
-
-
-
-
-
- setValue('workflowStr', e)}
- rows={10}
- />
-
+ {isLoading ? (
+
+
+ {loadingStatus}
+
+ ) : (
+ <>
+
+ {t('common:common.Set Name')}
+
+
+
+
+
+
+
+
+ setValue('workflowStr', e)}
+ rows={10}
+ />
+
+ >
+ )}
-
+
{
} = useDisclosure();
const [editFolder, setEditFolder] = useState();
+ useEffect(() => {
+ const isBrowser = typeof window !== 'undefined';
+ if (!isBrowser) return;
+
+ const hasWorkflowUrl = !!sessionStorage.getItem('utm_workflow');
+ if (hasWorkflowUrl) {
+ onOpenJsonImportModal();
+ }
+ }, [onOpenJsonImportModal]);
+
const { runAsync: onCreateFolder } = useRequest2(postCreateAppFolder, {
onSuccess() {
loadMyApps();
diff --git a/projects/app/src/web/core/app/workflow.ts b/projects/app/src/web/core/app/workflow.ts
index 0a753b4888b5..4b91f57ab192 100644
--- a/projects/app/src/web/core/app/workflow.ts
+++ b/projects/app/src/web/core/app/workflow.ts
@@ -239,24 +239,26 @@ export const fetchWorkflowFromUrl = async (url: string) => {
export const importWorkflowFromUrl = async ({
url,
name,
- parentId
+ parentId,
+ data
}: {
url: string;
name?: string;
parentId?: string;
+ data?: any; // 可选参数,允许直接传入已获取的工作流数据
}) => {
try {
console.log(`开始从URL导入工作流: ${url}`);
- // 获取工作流数据
- const data = await fetchWorkflowFromUrl(url);
+ // 如果已提供data,则直接使用,否则从URL获取
+ const workflowData = data || (await fetchWorkflowFromUrl(url));
- if (!data || !data.nodes || !data.edges) {
+ if (!workflowData || !workflowData.nodes || !workflowData.edges) {
throw new Error('工作流数据格式不正确,缺少nodes或edges');
}
// 获取应用类型
- const appType = getAppType(data);
+ const appType = getAppType(workflowData);
if (!appType) {
throw new Error('无法识别应用类型,请确保导入的是有效的工作流JSON');
}
@@ -269,9 +271,9 @@ export const importWorkflowFromUrl = async ({
avatar: appTypeMap[appType].avatar,
name: name || `未命名 ${new Date().toLocaleString()}`,
type: appType,
- modules: data.nodes || [],
- edges: data.edges || [],
- chatConfig: data.chatConfig || {}
+ modules: workflowData.nodes || [],
+ edges: workflowData.edges || [],
+ chatConfig: workflowData.chatConfig || {}
});
console.log(`工作流导入成功,创建的应用ID: ${appId}`);
From 0d821d6d752a7c8a7696cc81de972ae118830a74 Mon Sep 17 00:00:00 2001
From: dreamer6680 <1468683855@qq.com>
Date: Sun, 27 Apr 2025 16:17:04 +0800
Subject: [PATCH 05/10] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=B7=A5=E4=BD=9C?=
=?UTF-8?q?=E6=B5=81=E5=AF=BC=E5=85=A5=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=A2=9E?=
=?UTF-8?q?=E5=8A=A0=E5=AF=B9UTM=E5=8F=82=E6=95=B0=E7=9A=84=E6=94=AF?=
=?UTF-8?q?=E6=8C=81=EF=BC=8C=E4=BC=98=E5=8C=96=E4=BB=8EURL=E8=8E=B7?=
=?UTF-8?q?=E5=8F=96=E5=B7=A5=E4=BD=9C=E6=B5=81=E6=95=B0=E6=8D=AE=E7=9A=84?=
=?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E5=B9=B6=E9=87=8D=E6=9E=84=E7=9B=B8?=
=?UTF-8?q?=E5=85=B3API=E6=8E=A5=E5=8F=A3=E3=80=82=E4=BF=AE=E5=A4=8D?=
=?UTF-8?q?=E4=BA=86=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E5=92=8C=E9=94=99?=
=?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=EF=BC=8C=E6=8F=90=E5=8D=87=E4=BA=86?=
=?UTF-8?q?=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../app/src/components/WorkflowImporter.tsx | 2 +-
.../dashboard/apps/JsonImportModal.tsx | 28 +-
projects/app/src/pages/api/core/app/create.ts | 21 +-
.../src/pages/api/core/app/fetchWorkflow.ts | 86 +++++++
.../app/src/pages/dashboard/apps/index.tsx | 1 +
projects/app/src/web/context/useInitApp.ts | 4 +-
projects/app/src/web/core/app/api/app.ts | 8 +-
projects/app/src/web/core/app/workflow.ts | 239 ++----------------
8 files changed, 163 insertions(+), 226 deletions(-)
create mode 100644 projects/app/src/pages/api/core/app/fetchWorkflow.ts
diff --git a/projects/app/src/components/WorkflowImporter.tsx b/projects/app/src/components/WorkflowImporter.tsx
index ea90b7a52148..697ed56a64f6 100644
--- a/projects/app/src/components/WorkflowImporter.tsx
+++ b/projects/app/src/components/WorkflowImporter.tsx
@@ -92,7 +92,7 @@ const WorkflowImporter = () => {
}, [userInfo, toast]);
// 处理导入工作流
- const handleImportWorkflow = async (jsonConfig) => {
+ const handleImportWorkflow = async (jsonConfig: string) => {
try {
setShowJsonImporter(false);
setIsLoading(true);
diff --git a/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx b/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx
index 7509c5066c2b..43ac47c4847b 100644
--- a/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx
+++ b/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx
@@ -25,6 +25,13 @@ type FormType = {
workflowStr: string;
};
+type UTMParams = {
+ keyword?: string;
+ source?: string;
+ medium?: string;
+ content?: string;
+};
+
const JsonImportModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v);
@@ -62,9 +69,9 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => {
setValue('workflowStr', JSON.stringify(workflowData, null, 2));
try {
- const utmParams = localStorage.getItem('utm_params');
- if (utmParams) {
- const params = JSON.parse(utmParams);
+ const utmParamsStr = localStorage.getItem('utm_params');
+ if (utmParamsStr) {
+ const params = JSON.parse(utmParamsStr) as UTMParams;
if (params.content) setValue('name', params.content);
}
} catch (error) {
@@ -111,6 +118,17 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => {
const { runAsync: onSubmit, loading: isCreating } = useRequest2(
async ({ name, workflowStr }: FormType) => {
+ const utmParamsStr = sessionStorage.getItem('utm_params');
+ let utmParams: UTMParams | null = null;
+ sessionStorage.removeItem('utm_params');
+ try {
+ if (utmParamsStr) {
+ utmParams = JSON.parse(utmParamsStr);
+ }
+ } catch (error) {
+ console.error('解析utm_params出错:', error);
+ }
+
const { workflow, appType } = await (async () => {
try {
const workflow = JSON.parse(workflowStr);
@@ -143,7 +161,9 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => {
type: appType,
modules: workflow.nodes,
edges: workflow.edges,
- chatConfig: workflow.chatConfig
+ chatConfig: workflow.chatConfig,
+ utm_platform: utmParams?.medium,
+ utm_projectcode: utmParams?.content
});
},
{
diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts
index b44be3ec9fe1..87a1db83f000 100644
--- a/projects/app/src/pages/api/core/app/create.ts
+++ b/projects/app/src/pages/api/core/app/create.ts
@@ -27,10 +27,22 @@ export type CreateAppBody = {
modules: AppSchema['modules'];
edges?: AppSchema['edges'];
chatConfig?: AppSchema['chatConfig'];
+ utm_platform?: string;
+ utm_projectcode?: string;
};
async function handler(req: ApiRequestProps) {
- const { parentId, name, avatar, type, modules, edges, chatConfig } = req.body;
+ const {
+ parentId,
+ name,
+ avatar,
+ type,
+ modules,
+ edges,
+ chatConfig,
+ utm_platform,
+ utm_projectcode
+ } = req.body;
if (!name || !type || !Array.isArray(modules)) {
return Promise.reject(CommonErrEnum.inheritPermissionError);
@@ -66,8 +78,11 @@ async function handler(req: ApiRequestProps) {
type,
uid: userId,
teamId,
- tmbId
- });
+ tmbId,
+ appId,
+ shorUrlId: utm_platform,
+ projectCode: utm_projectcode
+ } as any);
return appId;
}
diff --git a/projects/app/src/pages/api/core/app/fetchWorkflow.ts b/projects/app/src/pages/api/core/app/fetchWorkflow.ts
new file mode 100644
index 000000000000..3e454d96c406
--- /dev/null
+++ b/projects/app/src/pages/api/core/app/fetchWorkflow.ts
@@ -0,0 +1,86 @@
+import { NextAPI } from '@/service/middleware/entry';
+import { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
+import axios from 'axios';
+
+export type FetchWorkflowQuery = {
+ url: string;
+};
+
+export type FetchWorkflowResponseType = ApiResponseType<{
+ data: JSON;
+}>;
+
+async function handler(
+ req: ApiRequestProps<{}, FetchWorkflowQuery>,
+ res: FetchWorkflowResponseType
+) {
+ const { url } = req.query;
+
+ if (!url) {
+ console.error('[后端代理] URL参数为空');
+ return Promise.reject('URL参数不能为空');
+ }
+
+ // 解码URL(因为前端可能已经编码过)
+ let targetUrl = url;
+ try {
+ // 判断是否已经被编码
+ if (/%[0-9A-Fa-f]{2}/.test(url)) {
+ targetUrl = decodeURIComponent(url);
+ }
+ } catch (err) {
+ console.warn('[后端代理] URL解码失败,使用原始URL');
+ }
+
+ try {
+ // 使用axios发送请求
+ const response = await axios.get(targetUrl, {
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'Mozilla/5.0 (compatible; FastGPT/1.0)'
+ },
+ timeout: 30000, // 30秒超时
+ validateStatus: (status) => status < 500 // 只有5xx错误才会触发异常
+ });
+
+ // 检查状态码
+ if (response.status !== 200) {
+ console.error(`[后端代理] 请求返回非200状态码: ${response.status}`);
+ return Promise.reject(`请求返回非200状态码: ${response.status}`);
+ }
+
+ // 检查返回的数据类型
+ const contentType = response.headers['content-type'] || '';
+ if (!contentType.includes('application/json') && !contentType.includes('text/json')) {
+ console.warn(`[后端代理] 响应内容类型不是JSON: ${contentType}`);
+ try {
+ // 尝试解析响应数据
+ JSON.parse(JSON.stringify(response.data));
+ } catch (error) {
+ return Promise.reject(`响应内容不是有效的JSON格式: ${contentType}`);
+ }
+ }
+
+ return response.data;
+ } catch (error: any) {
+ console.error(`[后端代理] 获取工作流数据失败:`, error.message);
+
+ // 构建详细的错误信息
+ let errorMessage = '请求失败';
+
+ if (error.code === 'ECONNABORTED') {
+ errorMessage = '请求超时,请检查URL是否正确或稍后重试';
+ } else if (error.code === 'ENOTFOUND') {
+ errorMessage = '无法解析域名,请检查URL是否正确';
+ } else if (error.response) {
+ errorMessage = `请求失败,状态码: ${error.response.status}, 信息: ${error.message}`;
+ } else {
+ errorMessage = `请求失败: ${error.message}`;
+ }
+
+ return Promise.reject(errorMessage);
+ }
+}
+
+export default NextAPI(handler);
diff --git a/projects/app/src/pages/dashboard/apps/index.tsx b/projects/app/src/pages/dashboard/apps/index.tsx
index d144269ac422..11e59b81398d 100644
--- a/projects/app/src/pages/dashboard/apps/index.tsx
+++ b/projects/app/src/pages/dashboard/apps/index.tsx
@@ -33,6 +33,7 @@ import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import DashboardContainer from '@/pageComponents/dashboard/Container';
import List from '@/pageComponents/dashboard/apps/List';
import MCPToolsEditModal from '@/pageComponents/dashboard/apps/MCPToolsEditModal';
+import WorkflowImporter from '@/components/WorkflowImporter';
const CreateModal = dynamic(() => import('@/pageComponents/dashboard/apps/CreateModal'));
const EditFolderModal = dynamic(
diff --git a/projects/app/src/web/context/useInitApp.ts b/projects/app/src/web/context/useInitApp.ts
index 28c27eb02359..1a4831a493b2 100644
--- a/projects/app/src/web/context/useInitApp.ts
+++ b/projects/app/src/web/context/useInitApp.ts
@@ -92,9 +92,9 @@ export const useInitApp = () => {
if (utm_medium) utmParams.medium = utm_medium;
if (utm_content) utmParams.content = utm_content;
- // 将UTM参数存入localStorage以便登录和注册时使用
+ // 将UTM参数存入sessionStorage以便登录和注册时使用
if (Object.keys(utmParams).length > 0) {
- localStorage.setItem('utm_params', JSON.stringify(utmParams));
+ sessionStorage.setItem('utm_params', JSON.stringify(utmParams));
}
// 获取现有的fastgpt_sem
diff --git a/projects/app/src/web/core/app/api/app.ts b/projects/app/src/web/core/app/api/app.ts
index 0dc719e2164b..d6759cac9321 100644
--- a/projects/app/src/web/core/app/api/app.ts
+++ b/projects/app/src/web/core/app/api/app.ts
@@ -9,7 +9,10 @@ import type {
transitionWorkflowResponse
} from '@/pages/api/core/app/transitionWorkflow';
import type { copyAppQuery, copyAppResponse } from '@/pages/api/core/app/copy';
-
+import type {
+ FetchWorkflowQuery,
+ FetchWorkflowResponseType
+} from '@/pages/api/core/app/fetchWorkflow';
/* folder */
export const postCreateAppFolder = (data: CreateAppFolderBody) =>
POST('/core/app/folder/create', data);
@@ -25,3 +28,6 @@ export const postTransition2Workflow = (data: transitionWorkflowBody) =>
POST('/core/app/transitionWorkflow', data);
export const postCopyApp = (data: copyAppQuery) => POST('/core/app/copy', data);
+
+export const postFetchWorkflow = (data: FetchWorkflowQuery) =>
+ GET('/core/app/fetchWorkflow', data);
diff --git a/projects/app/src/web/core/app/workflow.ts b/projects/app/src/web/core/app/workflow.ts
index 4b91f57ab192..a5cd486f23a2 100644
--- a/projects/app/src/web/core/app/workflow.ts
+++ b/projects/app/src/web/core/app/workflow.ts
@@ -1,235 +1,50 @@
import { getAppType } from '@fastgpt/global/core/app/utils';
import { postCreateApp } from './api';
import { appTypeMap } from '@/pageComponents/app/constants';
-/**
- * 从URL获取工作流JSON数据
- */
-export const fetchWorkflowFromUrl = async (url: string) => {
- // 自定义响应类型,用于手动处理剪贴板内容
- type CustomResponse = {
- text: () => Promise;
- ok: boolean;
- status: number;
- headers: {
- get: (name: string) => string | null;
- };
- };
+import { postFetchWorkflow } from './api/app';
+export const fetchWorkflowFromUrl = async (url: string) => {
try {
if (!url || typeof url !== 'string') {
throw new Error('WORKFLOW_IMPORT_ERROR: URL为空或格式错误');
}
- // 清理URL
let fetchUrl = url.trim();
- // 如果URL最后有斜杠,移除它
if (fetchUrl.endsWith('/')) {
fetchUrl = fetchUrl.slice(0, -1);
}
- // 确保URL是绝对路径
if (!fetchUrl.startsWith('http://') && !fetchUrl.startsWith('https://')) {
fetchUrl = `https://${fetchUrl}`;
}
- // 设置请求超时
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 30000); // 30秒超时
-
try {
- // 首先检查这是否是本地请求(localhost或127.0.0.1)
- const isLocalRequest = fetchUrl.includes('localhost') || fetchUrl.includes('127.0.0.1');
-
- if (isLocalRequest) {
- try {
- // 尝试方法1: 使用相对路径(如果URL是指向同一个域的不同端口)
- const urlObj = new URL(fetchUrl);
- const path = urlObj.pathname + urlObj.search;
- console.log(`尝试使用相对路径请求: ${path}`);
- const relativeResponse = await fetch(path, {
- signal: controller.signal,
- headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json'
- }
- }).catch((e) => {
- console.log(`相对路径请求失败: ${e.message}`);
- return null;
- });
-
- if (relativeResponse?.ok) {
- clearTimeout(timeoutId);
- const text = await relativeResponse.text();
- return JSON.parse(text);
- }
- } catch (err: any) {
- console.log(`相对路径方法失败: ${err.message}`);
- }
-
- // 尝试方法2: 如果是本地开发环境,建议用户手动复制JSON
- const userConfirmed = window.confirm(
- `由于CORS限制,无法直接从 ${fetchUrl} 获取工作流数据。\n\n` +
- '请手动打开该URL,复制JSON内容,然后点击"确定"来粘贴。\n\n' +
- '或者点击"取消"放弃导入。'
- );
-
- if (userConfirmed) {
- try {
- const clipboardText = await navigator.clipboard.readText().catch(() => '');
- if (clipboardText) {
- try {
- return JSON.parse(clipboardText);
- } catch (err) {
- throw new Error('剪贴板内容不是有效的JSON格式,请确保复制了完整的JSON数据');
- }
- } else {
- throw new Error('无法读取剪贴板内容,请确保已授予网站剪贴板权限');
- }
- } catch (err) {
- console.error('读取剪贴板失败:', err);
-
- // 如果剪贴板读取失败,提供手动输入选项
- const jsonInput = window.prompt('无法自动读取剪贴板。请手动粘贴工作流JSON数据:', '');
-
- if (jsonInput) {
- try {
- return JSON.parse(jsonInput);
- } catch (err) {
- throw new Error('输入的内容不是有效的JSON格式');
- }
- } else {
- throw new Error('未提供JSON数据,导入已取消');
- }
- }
- } else {
- throw new Error('用户取消了导入操作');
- }
- }
-
- // 如果不是本地请求,尝试正常请求
- const response = await fetch(fetchUrl, {
- signal: controller.signal,
- headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest'
- },
- mode: 'cors' // 尝试CORS
- }).catch(async (err) => {
- console.log(`直接请求失败: ${err.message},尝试替代方法`);
-
- // 如果是CORS错误,尝试使用no-cors模式(但这会导致不能读取响应内容)
- if (
- err.message &&
- (err.message.includes('CORS') ||
- err.message.includes('网络') ||
- err.message.includes('network'))
- ) {
- console.log('检测到CORS错误,提示用户手动获取JSON数据');
-
- const userConfirmed = window.confirm(
- `由于CORS限制,无法直接从 ${fetchUrl} 获取工作流数据。\n\n` +
- '请手动打开该URL,复制JSON内容,然后点击"确定"来粘贴。\n\n' +
- '或者点击"取消"放弃导入。'
- );
-
- if (userConfirmed) {
- try {
- const clipboardText = await navigator.clipboard.readText().catch(() => '');
- if (clipboardText) {
- try {
- const customResponse: CustomResponse = {
- text: () => Promise.resolve(clipboardText),
- ok: true,
- status: 200,
- headers: {
- get: (name: string) => (name === 'content-type' ? 'application/json' : null)
- }
- };
- return customResponse;
- } catch (err) {
- throw new Error('剪贴板内容不是有效的JSON格式,请确保复制了完整的JSON数据');
- }
- } else {
- throw new Error('无法读取剪贴板内容,请确保已授予网站剪贴板权限');
- }
- } catch (err) {
- console.error('读取剪贴板失败:', err);
-
- // 如果剪贴板读取失败,提供手动输入选项
- const jsonInput = window.prompt('无法自动读取剪贴板。请手动粘贴工作流JSON数据:', '');
-
- if (jsonInput) {
- const customResponse: CustomResponse = {
- text: () => Promise.resolve(jsonInput),
- ok: true,
- status: 200,
- headers: {
- get: (name: string) => (name === 'content-type' ? 'application/json' : null)
- }
- };
- return customResponse;
- } else {
- throw new Error('未提供JSON数据,导入已取消');
- }
- }
- } else {
- throw new Error('用户取消了导入操作');
- }
- }
-
- throw err;
+ const encodedUrl = encodeURIComponent(fetchUrl);
+ console.log('正在通过后端代理获取工作流数据...');
+
+ const proxyResponse = await postFetchWorkflow({
+ url: encodedUrl
+ }).catch((e) => {
+ console.error(`获取失败: ${e.message || 'UNKNOWN_ERROR'}`);
+ return null;
});
- clearTimeout(timeoutId);
-
- console.log(`获取状态码: ${response.status}`);
-
- if (!response.ok) {
- throw new Error(`获取工作流失败,HTTP错误状态: ${response.status}`);
- }
-
- const contentType = response.headers.get('content-type');
- console.log(`响应内容类型: ${contentType}`);
-
- if (!contentType || !contentType.includes('application/json')) {
- console.warn(`警告:响应内容类型不是JSON (${contentType})`);
- }
-
- const text = await response.text();
- console.log(`获取到响应内容长度: ${text.length}`);
-
- if (!text || text.trim() === '') {
- throw new Error('获取的响应内容为空');
+ if (proxyResponse) {
+ console.log('工作流数据获取成功');
+ return proxyResponse;
+ } else {
+ throw new Error('后端代理请求返回空数据');
}
-
- let data;
- try {
- data = JSON.parse(text);
- } catch (jsonError) {
- console.error('JSON解析失败:', jsonError);
- throw new Error('无法解析响应内容为JSON,请确保URL返回有效的JSON数据');
- }
-
- console.log('工作流数据获取成功', data ? '数据有效' : '数据为空');
-
- if (!data) {
- throw new Error('获取的工作流数据为空');
- }
-
- return data;
- } catch (fetchError: any) {
- if (fetchError.name === 'AbortError') {
- throw new Error('获取工作流超时,请检查URL是否正确或稍后重试');
- } else if (fetchError.message && fetchError.message.includes('CORS')) {
- throw new Error('CORS错误:无法访问该URL,请确保URL允许跨域请求或使用支持CORS的端点');
- }
- throw fetchError;
+ } catch (err: any) {
+ console.error(`获取失败: ${err.message || 'UNKNOWN_ERROR'}`);
+ return Promise.reject(new Error('无法获取工作流数据'));
}
} catch (error) {
console.error('获取工作流数据失败:', error);
- throw new Error(`获取工作流失败: ${error instanceof Error ? error.message : String(error)}`);
+ return Promise.reject(
+ new Error(`获取工作流失败: ${error instanceof Error ? error.message : String(error)}`)
+ );
}
};
@@ -248,24 +63,19 @@ export const importWorkflowFromUrl = async ({
data?: any; // 可选参数,允许直接传入已获取的工作流数据
}) => {
try {
- console.log(`开始从URL导入工作流: ${url}`);
-
- // 如果已提供data,则直接使用,否则从URL获取
const workflowData = data || (await fetchWorkflowFromUrl(url));
if (!workflowData || !workflowData.nodes || !workflowData.edges) {
- throw new Error('工作流数据格式不正确,缺少nodes或edges');
+ return Promise.reject(new Error('工作流数据格式不正确,缺少nodes或edges'));
}
- // 获取应用类型
const appType = getAppType(workflowData);
if (!appType) {
- throw new Error('无法识别应用类型,请确保导入的是有效的工作流JSON');
+ return Promise.reject(new Error('无法识别应用类型,请确保导入的是有效的工作流JSON'));
}
console.log(`识别到工作流类型: ${appType}`);
- // 创建应用
const appId = await postCreateApp({
parentId,
avatar: appTypeMap[appType].avatar,
@@ -276,10 +86,9 @@ export const importWorkflowFromUrl = async ({
chatConfig: workflowData.chatConfig || {}
});
- console.log(`工作流导入成功,创建的应用ID: ${appId}`);
return appId;
} catch (error) {
console.error('导入工作流失败:', error);
- throw error;
+ return Promise.reject(error);
}
};
From ecff04c3ff82d05e86f8528d7134e882ae18aef7 Mon Sep 17 00:00:00 2001
From: dreamer6680 <1468683855@qq.com>
Date: Sun, 27 Apr 2025 16:19:40 +0800
Subject: [PATCH 06/10] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=88=9B=E5=BB=BA?=
=?UTF-8?q?=E5=BA=94=E7=94=A8=E7=9A=84API=E6=8E=A5=E5=8F=A3=EF=BC=8C?=
=?UTF-8?q?=E5=B0=86UTM=E5=8F=82=E6=95=B0=E7=9A=84=E5=AD=97=E6=AE=B5?=
=?UTF-8?q?=E5=90=8D=E7=A7=B0=E4=BB=8E`shorUrlId`=E5=92=8C`projectCode`?=
=?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=BA`shorUrlPlatform`=E5=92=8C`shorUrlPr?=
=?UTF-8?q?ojectCode`=EF=BC=8C=E4=BB=A5=E6=8F=90=E9=AB=98=E4=BB=A3?=
=?UTF-8?q?=E7=A0=81=E7=9A=84=E5=8F=AF=E8=AF=BB=E6=80=A7=E5=92=8C=E4=B8=80?=
=?UTF-8?q?=E8=87=B4=E6=80=A7=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
projects/app/src/pages/api/core/app/create.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts
index 87a1db83f000..c5094aef3087 100644
--- a/projects/app/src/pages/api/core/app/create.ts
+++ b/projects/app/src/pages/api/core/app/create.ts
@@ -80,8 +80,8 @@ async function handler(req: ApiRequestProps) {
teamId,
tmbId,
appId,
- shorUrlId: utm_platform,
- projectCode: utm_projectcode
+ shorUrlPlatform: utm_platform,
+ shorUrlProjectCode: utm_projectcode
} as any);
return appId;
From 789e9e9f9f211e75e43fa2a8056356b7e85d4742 Mon Sep 17 00:00:00 2001
From: dreamer6680 <1468683855@qq.com>
Date: Sun, 27 Apr 2025 17:11:10 +0800
Subject: [PATCH 07/10] impoter json
---
packages/web/i18n/en/app.json | 2 +
packages/web/i18n/zh-CN/app.json | 2 +
packages/web/i18n/zh-Hant/app.json | 2 +
.../app/src/components/WorkflowImporter.tsx | 216 ------------------
.../dashboard/apps/JsonImportModal.tsx | 98 ++++----
.../app/src/pages/dashboard/apps/index.tsx | 1 -
projects/app/src/pages/login/fastlogin.tsx | 14 +-
projects/app/src/pages/login/index.tsx | 14 +-
projects/app/src/pages/login/provider.tsx | 12 +-
projects/app/src/web/core/app/workflow.ts | 7 +-
10 files changed, 76 insertions(+), 292 deletions(-)
delete mode 100644 projects/app/src/components/WorkflowImporter.tsx
diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json
index ca6c92a4746e..5ff0c38be36d 100644
--- a/packages/web/i18n/en/app.json
+++ b/packages/web/i18n/en/app.json
@@ -182,6 +182,8 @@
"type.Http plugin": "HTTP Plugin",
"type.Import from json": "Import JSON",
"type.Import from json tip": "Create applications directly through JSON configuration files",
+ "type.Import from json_error": "Failed to get workflow data, please check the URL or manually paste the JSON data",
+ "type.Import from json_loading": "Workflow data is being retrieved, please wait...",
"type.Plugin": "Plugin",
"type.Simple bot": "Simple App",
"type.Workflow bot": "Workflow",
diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json
index e8aa2294213f..846af3ea642a 100644
--- a/packages/web/i18n/zh-CN/app.json
+++ b/packages/web/i18n/zh-CN/app.json
@@ -189,6 +189,8 @@
"type.Http plugin": "HTTP 插件",
"type.Import from json": "导入 JSON 配置",
"type.Import from json tip": "通过 JSON 配置文件,直接创建应用",
+ "type.Import from json_error": "获取工作流数据失败,请检查URL或手动粘贴JSON数据",
+ "type.Import from json_loading": "正在获取工作流数据,请稍候...",
"type.MCP tools": "MCP 工具集",
"type.MCP_tools_url": "MCP 地址",
"type.Plugin": "插件",
diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json
index 93dd83401455..526041cf3ee6 100644
--- a/packages/web/i18n/zh-Hant/app.json
+++ b/packages/web/i18n/zh-Hant/app.json
@@ -182,6 +182,8 @@
"type.Http plugin": "HTTP 外掛",
"type.Import from json": "匯入 JSON 設定",
"type.Import from json tip": "透過 JSON 設定文件,直接建立應用",
+ "type.Import from json_error": "獲取工作流數據失敗,請檢查URL或手動粘貼JSON數據",
+ "type.Import from json_loading": "正在獲取工作流數據,請稍候...",
"type.Plugin": "外掛",
"type.Simple bot": "簡易應用程式",
"type.Workflow bot": "工作流程",
diff --git a/projects/app/src/components/WorkflowImporter.tsx b/projects/app/src/components/WorkflowImporter.tsx
deleted file mode 100644
index 697ed56a64f6..000000000000
--- a/projects/app/src/components/WorkflowImporter.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { useRouter } from 'next/router';
-import { importWorkflowFromUrl, fetchWorkflowFromUrl } from '@/web/core/app/workflow';
-import {
- Modal,
- ModalOverlay,
- ModalContent,
- ModalHeader,
- ModalBody,
- ModalFooter,
- Flex,
- Spinner,
- Text,
- useToast,
- Button
-} from '@chakra-ui/react';
-import { useUserStore } from '@/web/support/user/useUserStore';
-import { useTranslation } from 'next-i18next';
-import ImportAppConfigEditor from '@/pageComponents/app/ImportAppConfigEditor';
-
-const WorkflowImporter = () => {
- const { t } = useTranslation();
- const router = useRouter();
- const toast = useToast();
- const { userInfo } = useUserStore();
- const [isLoading, setIsLoading] = useState(false);
- const [status, setStatus] = useState('');
- const [showJsonImporter, setShowJsonImporter] = useState(false);
- const [defaultJson, setDefaultJson] = useState('');
- const [workflowUrl, setWorkflowUrl] = useState('');
-
- useEffect(() => {
- // 添加浏览器环境检查
- const isBrowser = typeof window !== 'undefined';
- if (!isBrowser) return; // 如果不是浏览器环境,直接返回
-
- // 只有用户已登录时检查
- if (!userInfo?.username) {
- return;
- }
-
- const checkWorkflow = async () => {
- try {
- let url = sessionStorage.getItem('utm_workflow');
-
- if (!url) {
- return;
- }
-
- // 保存URL,等待导入完成后再移除
- setWorkflowUrl(url);
- setIsLoading(true);
- setStatus('正在获取工作流数据...');
-
- try {
- // 获取工作流数据
- const workflowData = await fetchWorkflowFromUrl(url);
-
- if (!workflowData) {
- throw new Error('无法获取工作流数据');
- }
-
- // 将获取到的JSON数据设置为defaultJson
- setDefaultJson(JSON.stringify(workflowData, null, 2));
-
- // 显示JSON导入窗口
- setIsLoading(false);
- setShowJsonImporter(true);
- } catch (error) {
- console.error('获取工作流数据失败:', error);
- setStatus(`获取失败:${error instanceof Error ? error.message : '未知错误'}`);
-
- toast({
- title: '获取工作流数据失败',
- description: error instanceof Error ? error.message : '未知错误',
- status: 'error',
- duration: 5000,
- isClosable: true
- });
-
- setTimeout(() => {
- setIsLoading(false);
- }, 2000);
- }
- } catch (error) {
- console.error('检查工作流URL出错:', error);
- setIsLoading(false);
- }
- };
-
- checkWorkflow();
- }, [userInfo, toast]);
-
- // 处理导入工作流
- const handleImportWorkflow = async (jsonConfig: string) => {
- try {
- setShowJsonImporter(false);
- setIsLoading(true);
- setStatus('正在导入工作流...');
-
- // 获取utm_params中的source参数,如果有的话
- let content = '未命名';
- try {
- const utmParams = localStorage.getItem('utm_params');
- if (utmParams) {
- const params = JSON.parse(utmParams);
- if (params.content) content = params.content;
- }
- } catch (error) {
- console.error('解析utm_params出错:', error);
- }
-
- // 解析JSON
- let workflowData;
- try {
- workflowData = JSON.parse(jsonConfig);
- } catch (e) {
- throw new Error('JSON格式无效,请检查配置');
- }
-
- // 创建应用
- const appId = await importWorkflowFromUrl({
- url: workflowUrl,
- name: `${content}`,
- data: workflowData // 直接使用已获取的数据
- });
-
- // 导入成功后提示并跳转
- toast({
- title: '工作流导入成功',
- status: 'success',
- duration: 5000,
- isClosable: true
- });
-
- // 清除sessionStorage中的utm_workflow
- sessionStorage.removeItem('utm_workflow');
-
- // 延迟一下再跳转,确保用户看到成功消息
- setTimeout(() => {
- setIsLoading(false);
- router.push(`/app/detail?appId=${appId}`);
- }, 1500);
- } catch (error) {
- console.error('导入工作流失败:', error);
- setStatus(`导入失败:${error instanceof Error ? error.message : '未知错误'}`);
-
- toast({
- title: '导入工作流失败',
- description: error instanceof Error ? error.message : '未知错误',
- status: 'error',
- duration: 5000,
- isClosable: true
- });
-
- setTimeout(() => {
- setIsLoading(false);
- }, 2000);
- }
- };
-
- // 取消导入
- const handleCancel = () => {
- setShowJsonImporter(false);
- // 清除sessionStorage中的utm_workflow
- sessionStorage.removeItem('utm_workflow');
- };
-
- return (
- <>
- {/* 加载中状态弹窗 */}
- {}} closeOnOverlayClick={false} isCentered>
-
-
- 导入工作流
-
-
-
- {status}
-
-
-
-
-
- {/* JSON配置导入窗口 */}
-
-
-
- 导入工作流配置
-
-
- 已从URL获取工作流配置,您可以检查并编辑下方的配置,然后点击导入按钮完成导入。
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-export default WorkflowImporter;
diff --git a/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx b/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx
index 43ac47c4847b..57d0bea280ec 100644
--- a/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx
+++ b/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx
@@ -59,31 +59,50 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => {
const url = sessionStorage.getItem('utm_workflow');
if (!url) return;
+ // 显示加载状态
+ setIsLoading(true);
+ toast({
+ title: t('app:type.Import from json_loading'),
+ status: 'info'
+ });
+
try {
const workflowData = await fetchWorkflowFromUrl(url);
if (!workflowData) {
- throw new Error('无法获取工作流数据');
+ return Promise.reject(new Error('无法获取工作流数据'));
}
setValue('workflowStr', JSON.stringify(workflowData, null, 2));
try {
- const utmParamsStr = localStorage.getItem('utm_params');
+ const utmParamsStr = sessionStorage.getItem('utm_params');
if (utmParamsStr) {
const params = JSON.parse(utmParamsStr) as UTMParams;
if (params.content) setValue('name', params.content);
}
+ sessionStorage.removeItem('utm_params');
} catch (error) {
console.error('解析utm_params出错:', error);
}
sessionStorage.removeItem('utm_workflow');
+
+ // 加载成功,关闭加载状态
+ setIsLoading(false);
} catch (error) {
console.error('获取工作流数据失败:', error);
+ // 显示错误信息
+ toast({
+ title: t('app:type.Import from json_error'),
+ status: 'error'
+ });
+ onClose();
+ setIsLoading(false);
}
} catch (error) {
console.error('检查工作流URL出错:', error);
+ setIsLoading(false);
}
};
@@ -182,53 +201,46 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => {
- {isLoading ? (
-
-
- {loadingStatus}
-
- ) : (
- <>
-
- {t('common:common.Set Name')}
-
-
-
-
-
-
+
+ {t('common:common.Set Name')}
+
+
+
+
-
-
- setValue('workflowStr', e)}
- rows={10}
- />
-
- >
- )}
+
+
+
+
+ setValue('workflowStr', e)}
+ rows={10}
+ />
+
+ >