Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/web/i18n/en/workflow.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,5 +272,8 @@
"workflow.Switch_success": "Switch Successful",
"workflow.Team cloud": "Team Cloud",
"workflow.exit_tips": "Your changes have not been saved. 'Exit directly' will not save your edits.",
"workflow.local_draft_account_mismatch": "A workflow draft from another account was detected and skipped.",
"workflow.local_draft_restore_failed": "An unsaved workflow draft was detected, but auto-save failed. The draft has been kept. Please log in again and retry.",
"workflow.local_draft_restore_success": "An unsaved workflow draft was detected and auto-saved.",
"params_setting": "Parameters"
}
3 changes: 3 additions & 0 deletions packages/web/i18n/zh-CN/workflow.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,5 +272,8 @@
"workflow.Switch_success": "切换成功",
"workflow.Team cloud": "团队云端",
"workflow.exit_tips": "您的更改尚未保存,「直接退出」将不会保存您的编辑记录。",
"workflow.local_draft_account_mismatch": "检测到其他账号的未保存工作流草稿,已跳过恢复。",
"workflow.local_draft_restore_failed": "检测到未保存的工作流草稿,自动保存失败。草稿已保留,请重新登录后重试。",
"workflow.local_draft_restore_success": "检测到未保存的工作流草稿,已自动保存。",
"params_setting": "参数设置"
}
3 changes: 3 additions & 0 deletions packages/web/i18n/zh-Hant/workflow.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,5 +272,8 @@
"workflow.Switch_success": "切換成功",
"workflow.Team cloud": "團隊雲端",
"workflow.exit_tips": "您的變更尚未儲存,「直接結束」將不會儲存您的編輯紀錄。",
"workflow.local_draft_account_mismatch": "偵測到其他帳號的未儲存工作流草稿,已略過恢復。",
"workflow.local_draft_restore_failed": "偵測到未儲存的工作流草稿,自動儲存失敗。草稿已保留,請重新登入後再試。",
"workflow.local_draft_restore_success": "偵測到未儲存的工作流草稿,已自動儲存。",
"params_setting": "參數設定"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@ import React, {
useState
} from 'react';
import { createContext, useContextSelector } from 'use-context-selector';
import { useDebounceEffect, useMemoizedFn, useUnmount } from 'ahooks';
import { useDebounceEffect, useUnmount } from 'ahooks';
import { WorkflowBufferDataContext, WorkflowInitContext } from './workflowInitContext';
import { compareSnapshot } from '@/web/core/workflow/utils';
import { AppContext } from '@/pageComponents/app/detail/context';
import { WorkflowSnapshotContext } from './workflowSnapshotContext';
import { WorkflowUtilsContext } from './workflowUtilsContext';
import { isProduction } from '@fastgpt/global/common/system/constants';
import { useTranslation } from 'next-i18next';
import { useBeforeunload } from '@fastgpt/web/hooks/useBeforeunload';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
getWorkflowLocalDraftIdentity,
removeWorkflowLocalDraftByApp,
saveWorkflowLocalDraft,
WORKFLOW_AUTH_INVALID_EVENT
} from '@/web/core/workflow/localDraft';

// 创建 Context
type WorkflowPersistenceContextValue = {
Expand Down Expand Up @@ -52,6 +58,38 @@ export const WorkflowPersistenceProvider: React.FC<PropsWithChildren> = ({ child
const [isSaved, setIsSaved] = useState(true);
// 离开保存标志
const leaveSaveSign = useRef(true);
const userInfo = useUserStore((state) => state.userInfo);
const flowData2StoreData = useContextSelector(WorkflowUtilsContext, (v) => v.flowData2StoreData);
const onSaveApp = useContextSelector(AppContext, (v) => v.onSaveApp);

const saveLocalDraft = useCallback(() => {
const identity = getWorkflowLocalDraftIdentity(userInfo);
const data = flowData2StoreData();

if (!data) {
removeWorkflowLocalDraftByApp({
appId: appDetail._id,
identity
});
return false;
}

return saveWorkflowLocalDraft({
appId: appDetail._id,
identity,
data: {
...data,
chatConfig: appDetail.chatConfig
}
});
}, [appDetail._id, appDetail.chatConfig, flowData2StoreData, userInfo]);

const removeCurrentLocalDraft = useCallback(() => {
removeWorkflowLocalDraftByApp({
appId: appDetail._id,
identity: getWorkflowLocalDraftIdentity(userInfo)
});
}, [appDetail._id, userInfo]);

/**
* 计算 isSaved 状态 - 防抖 500ms
Expand All @@ -76,8 +114,14 @@ export const WorkflowPersistenceProvider: React.FC<PropsWithChildren> = ({ child
}
);
setIsSaved(val);

if (val) {
removeCurrentLocalDraft();
} else {
saveLocalDraft();
}
},
[future, past, nodes, edges, appDetail.chatConfig],
[future, past, nodes, edges, appDetail.chatConfig, removeCurrentLocalDraft, saveLocalDraft],
{
wait: 500
}
Expand All @@ -89,11 +133,10 @@ export const WorkflowPersistenceProvider: React.FC<PropsWithChildren> = ({ child
* 1. 手动调用
* 2. 离开页面前
*/
const flowData2StoreData = useContextSelector(WorkflowUtilsContext, (v) => v.flowData2StoreData);
const onSaveApp = useContextSelector(AppContext, (v) => v.onSaveApp);
const autoSaveFn = useCallback(async () => {
if (isSaved || !leaveSaveSign.current) return;
console.log('Leave auto save');
saveLocalDraft();
const data = flowData2StoreData();
if (!data || data.nodes.length === 0) return;
await onSaveApp({
Expand All @@ -102,7 +145,22 @@ export const WorkflowPersistenceProvider: React.FC<PropsWithChildren> = ({ child
chatConfig: appDetail.chatConfig,
autoSave: true
});
}, [appDetail.chatConfig, flowData2StoreData, isSaved, onSaveApp]);
removeCurrentLocalDraft();
}, [
appDetail.chatConfig,
flowData2StoreData,
isSaved,
onSaveApp,
removeCurrentLocalDraft,
saveLocalDraft
]);

useEffect(() => {
window.addEventListener(WORKFLOW_AUTH_INVALID_EVENT, saveLocalDraft);
return () => {
window.removeEventListener(WORKFLOW_AUTH_INVALID_EVENT, saveLocalDraft);
};
}, [saveLocalDraft]);

// 页面关闭前自动保存
useUnmount(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useCallback } from 'react';
import { useTranslation } from 'next-i18next';
import { useToast } from '@fastgpt/web/hooks/useToast';
import type { UserType } from '@fastgpt/global/support/user/type';
import { postPublishApp } from '@/web/core/app/api/version';
import { checkWorkflowLocalDraft, removeWorkflowLocalDraft } from '@/web/core/workflow/localDraft';

export const restoreWorkflowLocalDraftAfterLogin = async ({
user,
fallbackRoute,
saveDraft,
onAccountMismatch,
onRestoreSuccess,
onRestoreFailed
}: {
user: UserType;
fallbackRoute: string;
saveDraft: typeof postPublishApp;
onAccountMismatch?: () => void;
onRestoreSuccess?: () => void;
onRestoreFailed?: () => void;
}) => {
const draftResult = checkWorkflowLocalDraft({
user,
route: fallbackRoute
});

if (draftResult.status === 'account-mismatch') {
onAccountMismatch?.();
return fallbackRoute;
}

if (draftResult.status !== 'matched') {
return fallbackRoute;
}

try {
await saveDraft(draftResult.draft.appId, {
...draftResult.draft.data,
isPublish: false,
autoSave: true
});
removeWorkflowLocalDraft();
onRestoreSuccess?.();
} catch (error) {
onRestoreFailed?.();
}

return draftResult.route;
};

export const useWorkflowLocalDraftRestore = () => {
const { t } = useTranslation();
const { toast } = useToast();

return useCallback(
async ({ user, fallbackRoute }: { user: UserType; fallbackRoute: string }): Promise<string> => {
return restoreWorkflowLocalDraftAfterLogin({
user,
fallbackRoute,
saveDraft: postPublishApp,
onAccountMismatch: () =>
toast({
status: 'warning',
title: t('workflow:workflow.local_draft_account_mismatch')
}),
onRestoreSuccess: () =>
toast({
status: 'success',
title: t('workflow:workflow.local_draft_restore_success')
}),
onRestoreFailed: () =>
toast({
status: 'warning',
title: t('workflow:workflow.local_draft_restore_failed')
})
});
},
[t, toast]
);
};
18 changes: 14 additions & 4 deletions projects/app/src/pages/login/fastlogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { useTranslation } from 'next-i18next';
import { validateRedirectUrl } from '@/web/common/utils/uri';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
import { useWorkflowLocalDraftRestore } from '@/pageComponents/login/hooks/useWorkflowLocalDraftRestore';
import { clearAuthRedirecting } from '@/web/common/api/request';

const FastLogin = ({
code,
Expand All @@ -24,15 +26,23 @@ const FastLogin = ({
const router = useRouter();
const { toast } = useToast();
const { t } = useTranslation();
const restoreWorkflowLocalDraft = useWorkflowLocalDraftRestore();
const loginSuccess = useCallback(
(res: LoginSuccessResponseType) => {
async (res: LoginSuccessResponseType) => {
setUserInfo(res.user);
clearAuthRedirecting();

const safeCallbackUrl = validateRedirectUrl(callbackUrl);
const targetRoute = await restoreWorkflowLocalDraft({
user: res.user,
fallbackRoute: safeCallbackUrl
});

setTimeout(() => {
router.push(validateRedirectUrl(callbackUrl));
router.push(targetRoute);
}, 100);
},
[setUserInfo, router, callbackUrl]
[callbackUrl, restoreWorkflowLocalDraft, router, setUserInfo]
);

const authCode = useCallback(
Expand All @@ -51,7 +61,7 @@ const FastLogin = ({
router.replace('/login');
}, 1000);
}
loginSuccess(res);
await loginSuccess(res);
} catch (error) {
toast({
status: 'warning',
Expand Down
14 changes: 12 additions & 2 deletions projects/app/src/pages/login/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import { subRoute } from '@fastgpt/web/common/system/utils';
import { validateRedirectUrl } from '@/web/common/utils/uri';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
import { useWorkflowLocalDraftRestore } from '@/pageComponents/login/hooks/useWorkflowLocalDraftRestore';
import { clearAuthRedirecting } from '@/web/common/api/request';

const Login = () => {
const router = useRouter();
const { lastRoute = '' } = router.query as { lastRoute: string };
const { t } = useTranslation();
const { toast } = useToast();
const { setUserInfo } = useUserStore();
const restoreWorkflowLocalDraft = useWorkflowLocalDraftRestore();

const loginSuccess = useCallback(
async (res: LoginSuccessResponseType) => {
setUserInfo(res.user);
clearAuthRedirecting();

const decodeLastRoute = validateRedirectUrl(lastRoute);

Expand All @@ -45,9 +49,15 @@ const Login = () => {
return decodeLastRoute;
})();

navigateTo && router.replace(navigateTo);
if (navigateTo) {
const targetRoute = await restoreWorkflowLocalDraft({
user: res.user,
fallbackRoute: navigateTo
});
router.replace(targetRoute);
}
},
[lastRoute, router, setUserInfo, t, toast]
[lastRoute, restoreWorkflowLocalDraft, router, setUserInfo, t, toast]
);

useMount(() => {
Expand Down
14 changes: 12 additions & 2 deletions projects/app/src/pages/login/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { retryFn } from '@fastgpt/global/common/system/utils';
import type { LangEnum } from '@fastgpt/global/common/i18n/type';
import { validateRedirectUrl } from '@/web/common/utils/uri';
import type { LoginSuccessResponseType } from '@fastgpt/global/openapi/support/user/account/login/api';
import { useWorkflowLocalDraftRestore } from '@/pageComponents/login/hooks/useWorkflowLocalDraftRestore';
import { clearAuthRedirecting } from '@/web/common/api/request';

let isOauthLogging = false;

Expand All @@ -33,6 +35,7 @@ const provider = () => {
const router = useRouter();
const { state, error, ...props } = router.query as Record<string, string>;
const { toast } = useToast();
const restoreWorkflowLocalDraft = useWorkflowLocalDraftRestore();

const lastRoute = loginStore?.lastRoute
? validateRedirectUrl(loginStore.lastRoute)
Expand All @@ -43,6 +46,7 @@ const provider = () => {
async (res: LoginSuccessResponseType) => {
const decodeLastRoute = validateRedirectUrl(lastRoute);
setUserInfo(res.user);
clearAuthRedirecting();

const navigateTo = await (async () => {
if (res.user.team.status !== 'active') {
Expand All @@ -61,9 +65,15 @@ const provider = () => {
return decodeLastRoute;
})();

navigateTo && router.replace(navigateTo);
if (navigateTo) {
const targetRoute = await restoreWorkflowLocalDraft({
user: res.user,
fallbackRoute: navigateTo
});
router.replace(targetRoute);
}
},
[setUserInfo, router, lastRoute, t, toast]
[lastRoute, restoreWorkflowLocalDraft, router, setUserInfo, t, toast]
);

const authProps = useCallback(
Expand Down
Loading
Loading