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} + /> + + - + ) { - const { - parentId, - name, - avatar, - type, - modules, - edges, - chatConfig, - utm_platform, - utm_projectcode - } = req.body; + const { parentId, name, avatar, type, modules, edges, chatConfig, utmParams } = req.body; if (!name || !type || !Array.isArray(modules)) { return Promise.reject(CommonErrEnum.inheritPermissionError); @@ -80,8 +72,8 @@ async function handler(req: ApiRequestProps) { teamId, tmbId, appId, - shorUrlPlatform: utm_platform, - shorUrlProjectCode: utm_projectcode + shorUrlPlatform: utmParams?.utm_platform, + shorUrlProjectCode: utmParams?.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 index 483b911c67dc..3ccdf491e048 100644 --- a/projects/app/src/pages/api/core/app/fetchWorkflow.ts +++ b/projects/app/src/pages/api/core/app/fetchWorkflow.ts @@ -1,6 +1,11 @@ import { NextAPI } from '@/service/middleware/entry'; import { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import axios from 'axios'; +import { authCert } from '@fastgpt/service/support/permission/auth/common'; + +export type FetchWorkflowBody = { + url: string; +}; export type FetchWorkflowQuery = { url: string; @@ -11,69 +16,36 @@ export type FetchWorkflowResponseType = ApiResponseType<{ }>; async function handler( - req: ApiRequestProps<{}, FetchWorkflowQuery>, + req: ApiRequestProps, res: FetchWorkflowResponseType ) { - const { url } = req.query; + await authCert({ req, authToken: true }); - if (!url) { - console.error('[后端代理] URL参数为空'); - return Promise.reject('URL参数不能为空'); - } + const url = req.body?.url || req.query?.url; - let targetUrl = url; - try { - if (/%[0-9A-Fa-f]{2}/.test(url)) { - targetUrl = decodeURIComponent(url); - } - } catch (err) { - console.warn('[后端代理] URL解码失败,使用原始URL'); + if (!url) { + return Promise.reject('app:type.error.URLempty'); } - try { - 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, - validateStatus: (status) => status < 500 // - }); - - 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}`); - } - } + const response = await axios.get(url, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0 (compatible; FastGPT/1.0)' + }, + timeout: 30000, + validateStatus: (status) => status < 500 + }); - return response.data; - } catch (error: any) { - console.error(`[后端代理] 获取工作流数据失败:`, error.message); + const contentType = response.headers['content-type'] || ''; - let errorMessage = '请求失败'; + if (!response.data || response.data.length === 0) { + return Promise.reject('app:type.error.workflowresponseempty'); + } - 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}`; - } + JSON.parse(JSON.stringify(response.data)); - return Promise.reject(errorMessage); - } + return response.data; } 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..111751c78e8c 100644 --- a/projects/app/src/pages/dashboard/apps/index.tsx +++ b/projects/app/src/pages/dashboard/apps/index.tsx @@ -78,10 +78,8 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => { } = useDisclosure(); const [editFolder, setEditFolder] = useState(); + //if there is a workflow url in the session storage, open the json import modal and import the workflow useEffect(() => { - const isBrowser = typeof window !== 'undefined'; - if (!isBrowser) return; - const hasWorkflowUrl = !!sessionStorage.getItem('utm_workflow'); if (hasWorkflowUrl) { onOpenJsonImportModal(); diff --git a/projects/app/src/pages/login/fastlogin.tsx b/projects/app/src/pages/login/fastlogin.tsx index 420b607eb4fe..e353b73d5f8d 100644 --- a/projects/app/src/pages/login/fastlogin.tsx +++ b/projects/app/src/pages/login/fastlogin.tsx @@ -27,11 +27,7 @@ const FastLogin = ({ setUserInfo(res.user); setTimeout(() => { - router.push( - sessionStorage.getItem('utm_workflow') - ? '/dashboard/apps' - : decodeURIComponent(callbackUrl) - ); + router.push(decodeURIComponent(callbackUrl)); }, 100); }, [setUserInfo, router, callbackUrl] diff --git a/projects/app/src/pages/login/index.tsx b/projects/app/src/pages/login/index.tsx index 3eea5fbc126d..366b9d44ea21 100644 --- a/projects/app/src/pages/login/index.tsx +++ b/projects/app/src/pages/login/index.tsx @@ -65,9 +65,8 @@ const Login = ({ ChineseRedirectUrl }: { ChineseRedirectUrl: string }) => { const decodeLastRoute = decodeURIComponent(lastRoute); - const navigateTo = sessionStorage.getItem('utm_workflow') - ? '/dashboard/apps' - : decodeLastRoute && !decodeLastRoute.includes('/login') + const navigateTo = + decodeLastRoute && !decodeLastRoute.includes('/login') ? decodeLastRoute : '/dashboard/apps'; router.push(navigateTo); diff --git a/projects/app/src/pages/login/provider.tsx b/projects/app/src/pages/login/provider.tsx index 45b57016d5bd..a1327a7412ed 100644 --- a/projects/app/src/pages/login/provider.tsx +++ b/projects/app/src/pages/login/provider.tsx @@ -27,11 +27,7 @@ const provider = () => { setUserInfo(res.user); router.push( - sessionStorage.getItem('utm_workflow') - ? '/dashboard/apps' - : loginStore?.lastRoute - ? decodeURIComponent(loginStore?.lastRoute) - : '/dashboard/apps' + loginStore?.lastRoute ? decodeURIComponent(loginStore?.lastRoute) : '/dashboard/apps' ); }, [setUserInfo, router, loginStore?.lastRoute] diff --git a/projects/app/src/web/context/useInitApp.ts b/projects/app/src/web/context/useInitApp.ts index 3862596a57ed..53f44bd4ff13 100644 --- a/projects/app/src/web/context/useInitApp.ts +++ b/projects/app/src/web/context/useInitApp.ts @@ -75,12 +75,8 @@ 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); try { @@ -92,19 +88,7 @@ export const useInitApp = () => { if (Object.keys(utmParams).length > 0) { sessionStorage.setItem('utm_params', JSON.stringify(utmParams)); } - - const existingSem = sessionStorage.getItem('fastgpt_sem') - ? JSON.parse(sessionStorage.getItem('fastgpt_sem')!) - : {}; - - const newSem = { - ...existingSem, - ...utmParams - }; - - if (Object.keys(newSem).length > 0) { - sessionStorage.setItem('fastgpt_sem', JSON.stringify(newSem)); - } + k && sessionStorage.setItem('fastgpt_sem', JSON.stringify({ keyword: k, ...utmParams })); } catch (error) { console.error('处理UTM参数出错:', error); } diff --git a/projects/app/src/web/core/app/api/app.ts b/projects/app/src/web/core/app/api/app.ts index d6759cac9321..2b36cca6aeb0 100644 --- a/projects/app/src/web/core/app/api/app.ts +++ b/projects/app/src/web/core/app/api/app.ts @@ -9,6 +9,7 @@ import type { transitionWorkflowResponse } from '@/pages/api/core/app/transitionWorkflow'; import type { copyAppQuery, copyAppResponse } from '@/pages/api/core/app/copy'; + import type { FetchWorkflowQuery, FetchWorkflowResponseType @@ -29,5 +30,5 @@ export const postTransition2Workflow = (data: transitionWorkflowBody) => export const postCopyApp = (data: copyAppQuery) => POST('/core/app/copy', data); -export const postFetchWorkflow = (data: FetchWorkflowQuery) => +export const getFetchWorkflow = (data: FetchWorkflowQuery) => GET('/core/app/fetchWorkflow', data); diff --git a/projects/app/src/web/core/app/utils.ts b/projects/app/src/web/core/app/utils.ts index bb15a8244ed8..326d40c2bacc 100644 --- a/projects/app/src/web/core/app/utils.ts +++ b/projects/app/src/web/core/app/utils.ts @@ -40,10 +40,6 @@ 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[];