diff --git a/packages/global/support/marketing/type.d.ts b/packages/global/support/marketing/type.d.ts new file mode 100644 index 000000000..a0f0e3082 --- /dev/null +++ b/packages/global/support/marketing/type.d.ts @@ -0,0 +1,14 @@ +export type ShortUrlParams = { + shortUrlSource?: string; // Article, video + shortUrlMedium?: string; // bilibili, youtube + shortUrlContent?: string; // project id +}; + +export type TrackRegisterParams = { + inviterId?: string; + bd_vid?: string; + fastgpt_sem?: { + keyword?: string; + } & ShortUrlParams; + sourceDomain?: string; +}; diff --git a/packages/global/support/user/login/api.d.ts b/packages/global/support/user/login/api.d.ts index d994a24ec..152b10b36 100644 --- a/packages/global/support/user/login/api.d.ts +++ b/packages/global/support/user/login/api.d.ts @@ -1,16 +1,10 @@ +import { TrackRegisterParams } from '../../marketing/type'; + export type GetWXLoginQRResponse = { code: string; codeUrl: string; }; -export type TrackRegisterParams = { - inviterId?: string; - bd_vid?: string; - fastgpt_sem?: { - keyword: string; - }; - sourceDomain?: string; -}; export type AccountRegisterBody = { username: string; code: string; diff --git a/packages/service/common/middle/tracks/utils.ts b/packages/service/common/middle/tracks/utils.ts index e56cd77c9..8711b941a 100644 --- a/packages/service/common/middle/tracks/utils.ts +++ b/packages/service/common/middle/tracks/utils.ts @@ -6,6 +6,7 @@ import { OAuthEnum } from '@fastgpt/global/support/user/constant'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { getAppLatestVersion } from '../../../core/app/version/controller'; +import { ShortUrlParams } from '@fastgpt/global/support/marketing/type'; const createTrack = ({ event, data }: { event: TrackEnum; data: Record }) => { if (!global.feConfigs?.isPlus) return; @@ -31,7 +32,13 @@ export const pushTrack = { data }); }, - createApp: (data: PushTrackCommonType & { type: AppTypeEnum }) => { + createApp: ( + data: PushTrackCommonType & + ShortUrlParams & { + type: AppTypeEnum; + appId: string; + } + ) => { return createTrack({ event: TrackEnum.createApp, data diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index ca6c92a47..5119328ef 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 e8aa22942..0f7fb64a9 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 93dd83401..df5c474c1 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 58367c362..3502c0477 100644 --- a/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx +++ b/projects/app/src/pageComponents/dashboard/apps/JsonImportModal.tsx @@ -16,6 +16,14 @@ 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'; +import { + getUtmParams, + getUtmWorkflow, + removeUtmParams, + removeUtmWorkflow +} from '@/web/support/marketing/utils'; type FormType = { avatar: string; @@ -37,6 +45,24 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => { }); const workflowStr = watch('workflowStr'); + const { loading: isFetching } = useRequest2( + async () => { + const url = getUtmWorkflow(); + if (!url) return; + + const workflowData = await getFetchWorkflow({ url }); + + setValue('workflowStr', JSON.stringify(workflowData, null, 2)); + + const utmParams = getUtmParams(); + if (utmParams.shortUrlContent) setValue('name', utmParams.shortUrlContent); + + removeUtmWorkflow(); + removeUtmParams(); + }, + { manual: false } + ); + const avatar = watch('avatar'); const { File, @@ -97,7 +123,8 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => { type: appType, modules: workflow.nodes, edges: workflow.edges, - chatConfig: workflow.chatConfig + chatConfig: workflow.chatConfig, + utmParams: getUtmParams() }); }, { @@ -116,7 +143,7 @@ const JsonImportModal = ({ onClose }: { onClose: () => void }) => { { ]; const show_oauth = useMemo( - () => !sessionStorage.getItem('bd_vid') && !!(feConfigs?.sso?.url || oAuthList.length > 0), + () => !getBdVId() && !!(feConfigs?.sso?.url || oAuthList.length > 0), [feConfigs?.sso?.url, oAuthList.length] ); diff --git a/projects/app/src/pageComponents/login/RegisterForm.tsx b/projects/app/src/pageComponents/login/RegisterForm.tsx index 20688b031..647241f14 100644 --- a/projects/app/src/pageComponents/login/RegisterForm.tsx +++ b/projects/app/src/pageComponents/login/RegisterForm.tsx @@ -12,6 +12,13 @@ import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useTranslation } from 'next-i18next'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { + getBdVId, + getFastGPTSem, + getInviterId, + getSourceDomain, + removeFastGPTSem +} from '@/web/support/marketing/utils'; interface Props { loginSuccess: (e: ResLogin) => void; @@ -50,20 +57,13 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { username, code, password, - inviterId: localStorage.getItem('inviterId') || undefined, - bd_vid: sessionStorage.getItem('bd_vid') || undefined, - fastgpt_sem: (() => { - try { - return sessionStorage.getItem('fastgpt_sem') - ? JSON.parse(sessionStorage.getItem('fastgpt_sem')!) - : undefined; - } catch { - return undefined; - } - })(), - sourceDomain: sessionStorage.getItem('sourceDomain') || undefined + inviterId: getInviterId(), + bd_vid: getBdVId(), + fastgpt_sem: getFastGPTSem(), + sourceDomain: getSourceDomain() }) ); + removeFastGPTSem(); toast({ status: 'success', diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts index b44be3ec9..bdf20c876 100644 --- a/projects/app/src/pages/api/core/app/create.ts +++ b/projects/app/src/pages/api/core/app/create.ts @@ -5,6 +5,7 @@ import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils' import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import type { AppSchema } from '@fastgpt/global/core/app/type'; import { defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant'; +import { ShortUrlParams } from '@fastgpt/global/support/marketing/type'; import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; @@ -27,10 +28,11 @@ export type CreateAppBody = { modules: AppSchema['modules']; edges?: AppSchema['edges']; chatConfig?: AppSchema['chatConfig']; + utmParams?: ShortUrlParams; }; async function handler(req: ApiRequestProps) { - 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,7 +68,9 @@ async function handler(req: ApiRequestProps) { type, uid: userId, teamId, - tmbId + tmbId, + appId, + ...utmParams }); 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 000000000..3ccdf491e --- /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 dd333f7b1..0e5a0d503 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'; @@ -33,6 +33,8 @@ 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 { getUtmWorkflow } from '@/web/support/marketing/utils'; +import { useMount } from 'ahooks'; const CreateModal = dynamic(() => import('@/pageComponents/dashboard/apps/CreateModal')); const EditFolderModal = dynamic( @@ -71,12 +73,20 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => { onOpen: onOpenCreateMCPTools, onClose: onCloseCreateMCPTools } = useDisclosure(); + + const [editFolder, setEditFolder] = useState(); + const { isOpen: isOpenJsonImportModal, onOpen: onOpenJsonImportModal, onClose: onCloseJsonImportModal } = useDisclosure(); - const [editFolder, setEditFolder] = useState(); + //if there is a workflow url in the session storage, open the json import modal and import the workflow + useMount(() => { + if (getUtmWorkflow()) { + onOpenJsonImportModal(); + } + }); const { runAsync: onCreateFolder } = useRequest2(postCreateAppFolder, { onSuccess() { diff --git a/projects/app/src/pages/login/index.tsx b/projects/app/src/pages/login/index.tsx index 091745077..97dc769a4 100644 --- a/projects/app/src/pages/login/index.tsx +++ b/projects/app/src/pages/login/index.tsx @@ -30,6 +30,7 @@ import { getDocPath } from '@/web/common/system/doc'; import { getWebReqUrl } from '@fastgpt/web/common/system/utils'; import LoginForm from '@/pageComponents/login/LoginForm/LoginForm'; import { useToast } from '@fastgpt/web/hooks/useToast'; +import { getBdVId } from '@/web/support/marketing/utils'; const RegisterForm = dynamic(() => import('@/pageComponents/login/RegisterForm')); const ForgetPasswordForm = dynamic(() => import('@/pageComponents/login/ForgetPasswordForm')); @@ -64,7 +65,7 @@ const Login = ({ ChineseRedirectUrl }: { ChineseRedirectUrl: string }) => { setUserInfo(res.user); const decodeLastRoute = decodeURIComponent(lastRoute); - // 检查是否是当前的 route + const navigateTo = decodeLastRoute && !decodeLastRoute.includes('/login') ? decodeLastRoute @@ -90,7 +91,7 @@ const Login = ({ ChineseRedirectUrl }: { ChineseRedirectUrl: string }) => { /* default login type */ useEffect(() => { - const bd_vid = sessionStorage.getItem('bd_vid'); + const bd_vid = getBdVId(); if (bd_vid) { setPageType(LoginPageTypeEnum.passwordLogin); return; diff --git a/projects/app/src/pages/login/provider.tsx b/projects/app/src/pages/login/provider.tsx index a1327a741..f240f31dc 100644 --- a/projects/app/src/pages/login/provider.tsx +++ b/projects/app/src/pages/login/provider.tsx @@ -11,6 +11,13 @@ import { serviceSideProps } from '@/web/common/i18n/utils'; import { getErrText } from '@fastgpt/global/common/error/utils'; import { useTranslation } from 'next-i18next'; import { OAuthEnum } from '@fastgpt/global/support/user/constant'; +import { + getBdVId, + getFastGPTSem, + getInviterId, + getSourceDomain, + removeFastGPTSem +} from '@/web/support/marketing/utils'; let isOauthLogging = false; @@ -40,18 +47,10 @@ const provider = () => { type: loginStore?.provider || OAuthEnum.sso, props, callbackUrl: `${location.origin}/login/provider`, - inviterId: localStorage.getItem('inviterId') || undefined, - bd_vid: sessionStorage.getItem('bd_vid') || undefined, - fastgpt_sem: (() => { - try { - return sessionStorage.getItem('fastgpt_sem') - ? JSON.parse(sessionStorage.getItem('fastgpt_sem')!) - : undefined; - } catch { - return undefined; - } - })(), - sourceDomain: sessionStorage.getItem('sourceDomain') || undefined + inviterId: getInviterId(), + bd_vid: getBdVId(), + fastgpt_sem: getFastGPTSem(), + sourceDomain: getSourceDomain() }); if (!res) { @@ -63,6 +62,8 @@ const provider = () => { router.replace('/login'); }, 1000); } + + removeFastGPTSem(); loginSuccess(res); } catch (error) { toast({ diff --git a/projects/app/src/web/context/useInitApp.ts b/projects/app/src/web/context/useInitApp.ts index 83d6b7f01..4af7fe92d 100644 --- a/projects/app/src/web/context/useInitApp.ts +++ b/projects/app/src/web/context/useInitApp.ts @@ -7,15 +7,29 @@ import { useMemoizedFn, useMount } from 'ahooks'; import { TrackEventName } from '../common/system/constants'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useUserStore } from '../support/user/useUserStore'; +import { + setBdVId, + setFastGPTSem, + setInviterId, + setSourceDomain, + setUtmParams, + setUtmWorkflow +} from '../support/marketing/utils'; +import { ShortUrlParams } from '@fastgpt/global/support/marketing/type'; 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([]); @@ -69,20 +83,21 @@ export const useInitApp = () => { pollingInterval: 300000 // 5 minutes refresh }); + // Marketing data track useEffect(() => { - hiId && localStorage.setItem('inviterId', hiId); - bd_vid && sessionStorage.setItem('bd_vid', bd_vid); - k && sessionStorage.setItem('fastgpt_sem', JSON.stringify({ keyword: k })); + setInviterId(hiId); + setBdVId(bd_vid); + setUtmWorkflow(utm_workflow); + setSourceDomain(sourceDomain); - const formatSourceDomain = (() => { - if (sourceDomain) return sourceDomain; - return document.referrer; - })(); - - if (formatSourceDomain && !sessionStorage.getItem('sourceDomain')) { - sessionStorage.setItem('sourceDomain', formatSourceDomain); - } - }, [bd_vid, hiId, k, sourceDomain]); + const utmParams: ShortUrlParams = { + ...(utm_source && { shortUrlSource: utm_source }), + ...(utm_medium && { shortUrlMedium: utm_medium }), + ...(utm_content && { shortUrlContent: utm_content }) + }; + setUtmParams(utmParams); + setFastGPTSem({ keyword: k, ...utmParams }); + }, [bd_vid, hiId, k, utm_workflow, sourceDomain, utm_source, utm_medium, utm_content]); 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 0dc719e21..2b36cca6a 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); diff --git a/projects/app/src/web/support/marketing/utils.ts b/projects/app/src/web/support/marketing/utils.ts new file mode 100644 index 000000000..c1631f999 --- /dev/null +++ b/projects/app/src/web/support/marketing/utils.ts @@ -0,0 +1,77 @@ +import { ShortUrlParams, TrackRegisterParams } from '@fastgpt/global/support/marketing/type'; + +export const getInviterId = () => { + return localStorage.getItem('inviterId') || undefined; +}; +export const setInviterId = (inviterId?: string) => { + if (!inviterId) return; + localStorage.setItem('inviterId', inviterId); +}; +export const removeInviterId = () => { + localStorage.removeItem('inviterId'); +}; + +export const getBdVId = () => { + return sessionStorage.getItem('bd_vid') || undefined; +}; +export const setBdVId = (bdVid?: string) => { + if (!bdVid) return; + sessionStorage.setItem('bd_vid', bdVid); +}; + +export const getUtmWorkflow = () => { + return sessionStorage.getItem('utm_workflow') || undefined; +}; +export const setUtmWorkflow = (utmWorkflow?: string) => { + if (!utmWorkflow) return; + sessionStorage.setItem('utm_workflow', utmWorkflow); +}; +export const removeUtmWorkflow = () => { + sessionStorage.removeItem('utm_workflow'); +}; + +export const getUtmParams = () => { + try { + const params = JSON.parse(localStorage.getItem('utm_params') || '{}'); + return params as ShortUrlParams; + } catch (error) { + return {} as ShortUrlParams; + } +}; +export const setUtmParams = (utmParams?: ShortUrlParams) => { + if (!utmParams || Object.keys(utmParams).length === 0) return; + localStorage.setItem('utm_params', JSON.stringify(utmParams)); +}; +export const removeUtmParams = () => { + localStorage.removeItem('utm_params'); +}; + +export const getFastGPTSem = () => { + try { + return localStorage.getItem('fastgpt_sem') + ? JSON.parse(localStorage.getItem('fastgpt_sem')!) + : undefined; + } catch { + return undefined; + } +}; +export const setFastGPTSem = (fastgptSem?: TrackRegisterParams['fastgpt_sem']) => { + if (!fastgptSem) return; + localStorage.setItem('fastgpt_sem', JSON.stringify(fastgptSem)); +}; +export const removeFastGPTSem = () => { + localStorage.removeItem('fastgpt_sem'); +}; + +export const getSourceDomain = () => { + return sessionStorage.getItem('sourceDomain') || undefined; +}; +export const setSourceDomain = (sourceDomain?: string) => { + const formatSourceDomain = (() => { + if (sourceDomain) return sourceDomain; + return document.referrer; + })(); + + if (!formatSourceDomain || getSourceDomain()) return; + sessionStorage.setItem('sourceDomain', formatSourceDomain); +};