diff --git a/packages/global/support/user/login/api.d.ts b/packages/global/support/user/login/api.d.ts index d994a24ec4a6..b8bbea7472a7 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; + shortUrlSource?: string; + shortUrlMedium?: string; + shortUrlContent?: string; }; sourceDomain?: string; }; diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index ca6c92a4746e..5119328ef7f5 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -182,9 +182,14 @@ "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", + "type.error.URLempty": "The URL cannot be empty", + "type.error.Workflow data is empty": "No workflow data was obtained", + "type.error.workflowresponseempty": "Response content is empty", "type_not_recognized": "App type not recognized", "upload_file_max_amount": "Maximum File Quantity", "upload_file_max_amount_tip": "Maximum number of files uploaded in a single round of conversation", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index e8aa2294213f..0f7fb64a96e4 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -189,11 +189,16 @@ "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": "插件", "type.Simple bot": "简易应用", "type.Workflow bot": "工作流", + "type.error.URLempty": "URL不能为空", + "type.error.Workflow data is empty": "没有获取到工作流数据", + "type.error.workflowresponseempty": "响应内容为空", "type_not_recognized": "未识别到应用类型", "upload_file_max_amount": "最大文件数量", "upload_file_max_amount_tip": "单轮对话中最大上传文件数量", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 93dd83401455..df5c474c16cf 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -182,9 +182,14 @@ "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": "工作流程", + "type.error.URLempty": "URL不能為空", + "type.error.Workflow data is empty": "沒有獲取到工作流數據", + "type.error.workflowresponseempty": "響應內容為空", "type_not_recognized": "未識別到應用程式類型", "upload_file_max_amount": "最大檔案數量", "upload_file_max_amount_tip": "單輪對話中最大上傳檔案數量", diff --git a/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx b/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx index 58367c3624e2..d662ea5a5267 100644 --- a/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx +++ b/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx @@ -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 { useToast } from '@fastgpt/web/hooks/useToast'; +import { getFetchWorkflow } from '@/web/core/app/api/app'; type FormType = { avatar: string; @@ -23,10 +25,26 @@ type FormType = { workflowStr: string; }; +type UTMParams = { + source?: string; + medium?: string; + content?: string; +}; + +const getUtmParams = () => { + try { + const params = JSON.parse(sessionStorage.getItem('utm_params') || '{}'); + return params as UTMParams; + } catch (error) { + return {} as UTMParams; + } +}; + const JsonImportModal = ({ onClose }: { onClose: () => void }) => { const { t } = useTranslation(); const { parentId, loadMyApps } = useContextSelector(AppListContext, (v) => v); const router = useRouter(); + const { toast } = useToast(); const { register, setValue, watch, handleSubmit } = useForm({ defaultValues: { @@ -37,6 +55,43 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => { }); const workflowStr = watch('workflowStr'); + const { runAsync: fetchWorkflow, loading: isFetching } = useRequest2( + async (url?: string) => { + if (!url) return Promise.reject(t('app:type.error.URLempty')); + + let fetchUrl = url.trim(); + if (fetchUrl.endsWith('/')) fetchUrl = fetchUrl.slice(0, -1); + if (!fetchUrl.startsWith('http')) fetchUrl = `https://${fetchUrl}`; + + return getFetchWorkflow({ url: fetchUrl }); + }, + { manual: false } + ); + + useEffect(() => { + const url = sessionStorage.getItem('utm_workflow'); + if (!url) return; + + toast({ title: t('app:type.Import from json_loading'), status: 'info' }); + + fetchWorkflow(url) + .then((workflowData) => { + if (!workflowData) return Promise.reject(t('app:type.error.Workflow data is empty')); + + setValue('workflowStr', JSON.stringify(workflowData, null, 2)); + + const utmParams = getUtmParams(); + if (utmParams.content) setValue('name', utmParams.content); + + sessionStorage.removeItem('utm_params'); + sessionStorage.removeItem('utm_workflow'); + }) + .catch(() => { + toast({ title: t('app:type.Import from json_error'), status: 'error' }); + onClose(); + }); + }, [fetchWorkflow, onClose, t, toast]); + const avatar = watch('avatar'); const { File, @@ -65,6 +120,9 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => { const { runAsync: onSubmit, loading: isCreating } = useRequest2( async ({ name, workflowStr }: FormType) => { + // 处理UTM参数 + const utmParams = getUtmParams(); + const { workflow, appType } = await (async () => { try { const workflow = JSON.parse(workflowStr); @@ -97,7 +155,11 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => { type: appType, modules: workflow.nodes, edges: workflow.edges, - chatConfig: workflow.chatConfig + chatConfig: workflow.chatConfig, + utmParams: { + utm_platform: utmParams?.medium, + utm_projectcode: utmParams?.content + } }); }, { @@ -116,7 +178,7 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => { ) { - const { parentId, name, avatar, type, modules, edges, chatConfig } = req.body; + const { parentId, name, avatar, type, modules, edges, chatConfig, utmParams } = req.body; if (!name || !type || !Array.isArray(modules)) { return Promise.reject(CommonErrEnum.inheritPermissionError); @@ -66,8 +70,11 @@ async function handler(req: ApiRequestProps) { type, uid: userId, teamId, - tmbId - }); + tmbId, + appId, + 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 new file mode 100644 index 000000000000..3ccdf491e048 --- /dev/null +++ b/projects/app/src/pages/api/core/app/fetchWorkflow.ts @@ -0,0 +1,51 @@ +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; +}; + +export type FetchWorkflowResponseType = ApiResponseType<{ + data: JSON; +}>; + +async function handler( + req: ApiRequestProps, + res: FetchWorkflowResponseType +) { + await authCert({ req, authToken: true }); + + const url = req.body?.url || req.query?.url; + + if (!url) { + return Promise.reject('app:type.error.URLempty'); + } + + 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 + }); + + const contentType = response.headers['content-type'] || ''; + + if (!response.data || response.data.length === 0) { + return Promise.reject('app:type.error.workflowresponseempty'); + } + + JSON.parse(JSON.stringify(response.data)); + + 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 dd333f7b1226..111751c78e8c 100644 --- a/projects/app/src/pages/dashboard/apps/index.tsx +++ b/projects/app/src/pages/dashboard/apps/index.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; import { Box, Flex, Button, useDisclosure, Input, InputGroup } from '@chakra-ui/react'; import { AddIcon } from '@chakra-ui/icons'; import { serviceSideProps } from '@/web/common/i18n/utils'; @@ -78,6 +78,14 @@ 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 hasWorkflowUrl = !!sessionStorage.getItem('utm_workflow'); + if (hasWorkflowUrl) { + onOpenJsonImportModal(); + } + }, [onOpenJsonImportModal]); + const { runAsync: onCreateFolder } = useRequest2(postCreateAppFolder, { onSuccess() { loadMyApps(); diff --git a/projects/app/src/pages/login/index.tsx b/projects/app/src/pages/login/index.tsx index 0917450770ab..366b9d44ea21 100644 --- a/projects/app/src/pages/login/index.tsx +++ b/projects/app/src/pages/login/index.tsx @@ -64,7 +64,7 @@ const Login = ({ ChineseRedirectUrl }: { ChineseRedirectUrl: string }) => { setUserInfo(res.user); const decodeLastRoute = decodeURIComponent(lastRoute); - // 检查是否是当前的 route + const navigateTo = decodeLastRoute && !decodeLastRoute.includes('/login') ? decodeLastRoute diff --git a/projects/app/src/web/context/useInitApp.ts b/projects/app/src/web/context/useInitApp.ts index 83d6b7f018e0..53f44bd4ff13 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([]); @@ -72,7 +77,21 @@ export const useInitApp = () => { useEffect(() => { 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 { + const utmParams: Record = {}; + if (utm_source) utmParams.source = utm_source; + if (utm_medium) utmParams.medium = utm_medium; + if (utm_content) utmParams.content = utm_content; + + if (Object.keys(utmParams).length > 0) { + sessionStorage.setItem('utm_params', JSON.stringify(utmParams)); + } + k && sessionStorage.setItem('fastgpt_sem', JSON.stringify({ keyword: k, ...utmParams })); + } catch (error) { + console.error('处理UTM参数出错:', error); + } const formatSourceDomain = (() => { if (sourceDomain) return sourceDomain; @@ -82,7 +101,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/api/app.ts b/projects/app/src/web/core/app/api/app.ts index 0dc719e2164b..2b36cca6aeb0 100644 --- a/projects/app/src/web/core/app/api/app.ts +++ b/projects/app/src/web/core/app/api/app.ts @@ -10,6 +10,10 @@ import type { } 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 +29,6 @@ export const postTransition2Workflow = (data: transitionWorkflowBody) => POST('/core/app/transitionWorkflow', data); export const postCopyApp = (data: copyAppQuery) => POST('/core/app/copy', data); + +export const getFetchWorkflow = (data: FetchWorkflowQuery) => + GET('/core/app/fetchWorkflow', data);