Test shorurl (#4686)

* Short-chain burying (#4678)

* TrackRegisterParams

* 新增工作流导入功能,支持从URL获取工作流JSON数据并创建应用。实现了URL验证、CORS处理、剪贴板读取等功能,确保用户能够顺利导入工作流数据。

* 更新工作流导入功能,将导入逻辑从utils模块迁移至workflow模块,并修正相关导入路径。此更改有助于代码结构的清晰和模块化。

* 优化工作流导入组件,重构导入逻辑,增加从URL获取工作流数据的功能,并实现JSON配置导入窗口。修复了状态管理和错误处理,提升用户体验。

* 更新工作流导入功能,增加对UTM参数的支持,优化从URL获取工作流数据的逻辑,并重构相关API接口。修复了状态管理和错误处理,提升了用户体验。

* 更新创建应用的API接口,将UTM参数的字段名称从`shorUrlId`和`projectCode`修改为`shorUrlPlatform`和`shorUrlProjectCode`,以提高代码的可读性和一致性。

* impoter json

* Optimize the logic

* delete some console

* fix

* perf: sem code

---------

Co-authored-by: dreamer6680 <1468683855@qq.com>
This commit is contained in:
Archer
2025-04-27 22:56:42 +08:00
committed by GitHub
parent 5357aa402b
commit 659b8b1106
17 changed files with 285 additions and 61 deletions

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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<string, any> }) => {
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

View File

@@ -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",

View File

@@ -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": "单轮对话中最大上传文件数量",

View File

@@ -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": "單輪對話中最大上傳檔案數量",

View File

@@ -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 }) => {
<MyModal
isOpen
onClose={onClose}
isLoading={isCreating}
isLoading={isCreating || isFetching}
title={t('app:type.Import from json')}
iconSrc="common/importLight"
iconColor={'primary.600'}

View File

@@ -13,7 +13,8 @@ import { checkIsWecomTerminal } from '@fastgpt/global/support/user/login/constan
import { getNanoid } from '@fastgpt/global/common/string/tools';
import Avatar from '@fastgpt/web/components/common/Avatar';
import dynamic from 'next/dynamic';
import { GET, POST } from '@/web/common/api/request';
import { POST } from '@/web/common/api/request';
import { getBdVId } from '@/web/support/marketing/utils';
interface Props {
children: React.ReactNode;
@@ -107,7 +108,7 @@ const FormLayout = ({ children, setPageType, pageType }: Props) => {
];
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]
);

View File

@@ -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',

View File

@@ -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<CreateAppBody>) {
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<CreateAppBody>) {
type,
uid: userId,
teamId,
tmbId
tmbId,
appId,
...utmParams
});
return appId;

View File

@@ -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<FetchWorkflowBody, FetchWorkflowQuery>,
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);

View File

@@ -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<EditFolderFormType>();
const {
isOpen: isOpenJsonImportModal,
onOpen: onOpenJsonImportModal,
onClose: onCloseJsonImportModal
} = useDisclosure();
const [editFolder, setEditFolder] = useState<EditFolderFormType>();
//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() {

View File

@@ -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;

View File

@@ -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({

View File

@@ -7,14 +7,28 @@ 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 {
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();
@@ -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,

View File

@@ -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<transitionWorkflowResponse>('/core/app/transitionWorkflow', data);
export const postCopyApp = (data: copyAppQuery) => POST<copyAppResponse>('/core/app/copy', data);
export const getFetchWorkflow = (data: FetchWorkflowQuery) =>
GET<FetchWorkflowResponseType>('/core/app/fetchWorkflow', data);

View File

@@ -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);
};