diff --git a/packages/service/core/app/controller.ts b/packages/service/core/app/controller.ts index fb07552a2..0a2402183 100644 --- a/packages/service/core/app/controller.ts +++ b/packages/service/core/app/controller.ts @@ -27,6 +27,8 @@ import { getS3ChatSource } from '../../common/s3/sources/chat'; import { MongoAppChatLog } from './logs/chatLogsSchema'; import { MongoAppRegistration } from '../../support/appRegistration/schema'; import { MongoMcpKey } from '../../support/mcp/schema'; +import { type ClientSession } from '../../common/mongo'; +import { MongoAppRecord } from './record/schema'; export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] }) => { if (!nodes) return; @@ -203,4 +205,29 @@ export const deleteAppsImmediate = async ({ '_id' ).lean(); await Promise.all(evalJobs.map((evalJob) => removeEvaluationJob(evalJob._id))); + + // Remove app record + await MongoAppRecord.deleteMany({ teamId, appId: { $in: appIds } }); }; + +export async function updateParentFoldersUpdateTime({ + parentId, + session +}: { + parentId?: string | null; + session?: ClientSession; +}): Promise { + if (!parentId) return; + + const parentApp = await MongoApp.findById(parentId, 'parentId updateTime'); + if (!parentApp) return; + + parentApp.updateTime = new Date(); + await parentApp.save({ session }); + + // Recursively update parent folders + await updateParentFoldersUpdateTime({ + parentId: parentApp.parentId, + session + }); +} diff --git a/packages/service/core/app/record/schema.ts b/packages/service/core/app/record/schema.ts new file mode 100644 index 000000000..1a04a5236 --- /dev/null +++ b/packages/service/core/app/record/schema.ts @@ -0,0 +1,45 @@ +import { + TeamCollectionName, + TeamMemberCollectionName +} from '@fastgpt/global/support/user/team/constant'; +import { getMongoModel, Schema } from '../../../common/mongo'; +import { AppCollectionName } from '../schema'; +import type { AppRecordType } from './type'; + +export const AppRecordCollectionName = 'app_records'; + +const AppRecordSchema = new Schema( + { + tmbId: { + type: Schema.Types.ObjectId, + ref: TeamMemberCollectionName, + required: true + }, + teamId: { + type: Schema.Types.ObjectId, + ref: TeamCollectionName, + required: true + }, + appId: { + type: Schema.Types.ObjectId, + ref: AppCollectionName, + required: true + }, + lastUsedTime: { + type: Date, + default: () => new Date() + } + }, + { + timestamps: false + } +); + +AppRecordSchema.index({ tmbId: 1, lastUsedTime: -1 }); // 查询用户最近使用的应用 +AppRecordSchema.index({ tmbId: 1, appId: 1 }, { unique: true }); // 防止重复记录 +AppRecordSchema.index({ teamId: 1, appId: 1 }); // 用于清理权限失效的记录 + +export const MongoAppRecord = getMongoModel( + AppRecordCollectionName, + AppRecordSchema +); diff --git a/packages/service/core/app/record/type.ts b/packages/service/core/app/record/type.ts new file mode 100644 index 000000000..127e6f44e --- /dev/null +++ b/packages/service/core/app/record/type.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +export const AppRecordSchemaZod = z.object({ + _id: z.string(), + tmbId: z.string(), + teamId: z.string(), + appId: z.string(), + lastUsedTime: z.date() +}); + +// TypeScript types inferred from Zod schemas +export type AppRecordType = z.infer; + +export const GetRecentlyUsedAppsResponseSchema = z.array( + z.object({ + _id: z.string(), + name: z.string(), + avatar: z.string() + }) +); +export type GetRecentlyUsedAppsResponseType = z.infer; diff --git a/packages/service/core/app/record/utils.ts b/packages/service/core/app/record/utils.ts new file mode 100644 index 000000000..c6ff3ee8e --- /dev/null +++ b/packages/service/core/app/record/utils.ts @@ -0,0 +1,41 @@ +import { mongoSessionRun } from '../../../common/mongo/sessionRun'; +import { MongoAppRecord } from './schema'; + +export const recordAppUsage = async ({ + appId, + tmbId, + teamId +}: { + appId: string; + tmbId: string; + teamId: string; +}) => { + await mongoSessionRun(async (session) => { + await MongoAppRecord.updateOne( + { tmbId, appId }, + { + $set: { + teamId, + lastUsedTime: new Date() + } + }, + { + upsert: true, + session + } + ); + + // 检查是否超过50条,如果超过则删除最旧的一条 + const count = await MongoAppRecord.countDocuments({ tmbId }, { session }); + + if (count > 50) { + await MongoAppRecord.deleteOne( + { tmbId }, + { + session, + sort: { lastUsedTime: 1 } + } + ); + } + }); +}; diff --git a/packages/service/core/chat/saveChat.ts b/packages/service/core/chat/saveChat.ts index f427a40b1..a18773fa1 100644 --- a/packages/service/core/chat/saveChat.ts +++ b/packages/service/core/chat/saveChat.ts @@ -35,7 +35,6 @@ export type Props = { nodes: StoreNodeItemType[]; appChatConfig?: AppChatConfigType; variables?: Record; - isUpdateUseTime: boolean; newTitle: string; source: `${ChatSourceEnum}`; sourceName?: string; @@ -219,7 +218,6 @@ export async function saveChat(props: Props) { nodes, appChatConfig, variables, - isUpdateUseTime, newTitle, source, sourceName, @@ -393,18 +391,6 @@ export async function saveChat(props: Props) { } catch (error) { addLog.error('Push chat log error', error); } - - if (isUpdateUseTime) { - await MongoApp.updateOne( - { _id: appId }, - { - updateTime: new Date() - }, - { - ...writePrimary - } - ).catch(); - } } catch (error) { addLog.error(`update chat history error`, error); } diff --git a/projects/app/src/pageComponents/chat/ChatFavouriteApp/index.tsx b/projects/app/src/pageComponents/chat/ChatFavouriteApp/index.tsx index 36c109a7b..8e8b78241 100644 --- a/projects/app/src/pageComponents/chat/ChatFavouriteApp/index.tsx +++ b/projects/app/src/pageComponents/chat/ChatFavouriteApp/index.tsx @@ -18,7 +18,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; import { useTranslation } from 'react-i18next'; import { useForm } from 'react-hook-form'; import { useContextSelector } from 'use-context-selector'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { useMemo } from 'react'; import Avatar from '@fastgpt/web/components/common/Avatar'; import { ChatSettingTabOptionEnum, ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; @@ -41,11 +41,11 @@ const ChatFavouriteApp = () => { const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); - const wideLogoUrl = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.wideLogoUrl); - const homeTabTitle = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.homeTabTitle); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); + const wideLogoUrl = useContextSelector(ChatPageContext, (v) => v.chatSettings?.wideLogoUrl); + const homeTabTitle = useContextSelector(ChatPageContext, (v) => v.chatSettings?.homeTabTitle); - const tags = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.favouriteTags || []); + const tags = useContextSelector(ChatPageContext, (v) => v.chatSettings?.favouriteTags || []); const tagCache = useMemo(() => { return tags.reduce( (acc, tag) => { diff --git a/projects/app/src/pageComponents/chat/ChatHeader.tsx b/projects/app/src/pageComponents/chat/ChatHeader.tsx index 4a37a7dc4..b54dff79c 100644 --- a/projects/app/src/pageComponents/chat/ChatHeader.tsx +++ b/projects/app/src/pageComponents/chat/ChatHeader.tsx @@ -14,7 +14,6 @@ import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constan import { useSystem } from '@fastgpt/web/hooks/useSystem'; import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs'; import { useRouter } from 'next/router'; -import { type AppListItemType } from '@fastgpt/global/core/app/type'; import { type GetResourceFolderListProps, type GetResourceListItemResponse @@ -24,7 +23,7 @@ import SelectOneResource from '@/components/common/folder/SelectOneResource'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import VariablePopover from '@/components/core/chat/ChatContainer/components/VariablePopover'; import { useCopyData } from '@fastgpt/web/hooks/useCopyData'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { ChatSidebarPaneEnum, DEFAULT_LOGO_BANNER_COLLAPSED_URL @@ -38,7 +37,6 @@ import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/const const ChatHeader = ({ history, showHistory, - apps, totalRecordsCount, pane, @@ -50,18 +48,15 @@ const ChatHeader = ({ history: ChatItemType[]; showHistory?: boolean; - apps?: AppListItemType[]; totalRecordsCount: number; reserveSpace?: boolean; }) => { const { t } = useTranslation(); const { isPc } = useSystem(); - const pathname = usePathname(); const { source } = useChatStore(); const chatData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible); - const isPlugin = chatData.app.type === AppTypeEnum.workflowTool; const isShare = source === 'share'; const chatType = isShare ? ChatTypeEnum.share : ChatTypeEnum.chat; @@ -87,7 +82,6 @@ const ChatHeader = ({ ) : ( void; - appId: string; - apps?: AppListItemType[]; -}) => { +const MobileDrawer = ({ onCloseDrawer, appId }: { onCloseDrawer: () => void; appId: string }) => { enum TabEnum { recently = 'recently', app = 'app' @@ -129,6 +115,7 @@ const MobileDrawer = ({ const { t } = useTranslation(); const { setChatId } = useChatStore(); + const myApps = useContextSelector(ChatPageContext, (v) => v.myApps); const [currentTab, setCurrentTab] = useState(TabEnum.recently); @@ -143,7 +130,7 @@ const MobileDrawer = ({ ); }, []); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); const onclickApp = (id: string) => { handlePaneChange(ChatSidebarPaneEnum.RECENTLY_USED_APPS, id); @@ -201,8 +188,8 @@ const MobileDrawer = ({ {/* history */} {currentTab === TabEnum.recently && ( - {Array.isArray(apps) && - apps.map((item) => ( + {Array.isArray(myApps) && + myApps.map((item) => ( { const router = useRouter(); @@ -290,9 +275,7 @@ const MobileHeader = ({ - {isOpenDrawer && !isShareChat && ( - - )} + {isOpenDrawer && !isShareChat && } ); }; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/DataDashboard.tsx b/projects/app/src/pageComponents/chat/ChatSetting/DataDashboard.tsx index d9e4ecca3..9cb47a951 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/DataDashboard.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/DataDashboard.tsx @@ -1,5 +1,5 @@ import LogChart from '@/pageComponents/app/detail/Logs/LogChart'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { Flex } from '@chakra-ui/react'; import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import type { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker'; @@ -13,7 +13,7 @@ type Props = { }; const LogDetails = ({ Header }: Props) => { - const appId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); + const appId = useContextSelector(ChatPageContext, (v) => v.chatSettings?.appId || ''); const [dateRange, setDateRange] = useState({ from: new Date(addDays(new Date(), -6).setHours(0, 0, 0, 0)), diff --git a/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/TagManageModal.tsx b/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/TagManageModal.tsx index 56e7cd9a3..2192f636a 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/TagManageModal.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/TagManageModal.tsx @@ -1,4 +1,4 @@ -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { AddIcon } from '@chakra-ui/icons'; import { Box, @@ -377,10 +377,10 @@ type Props = { const TagManageModal = ({ onClose, onRefresh }: Props) => { const { t } = useTranslation(); - const refreshChatSetting = useContextSelector(ChatSettingContext, (v) => v.refreshChatSetting); + const refreshChatSetting = useContextSelector(ChatPageContext, (v) => v.refreshChatSetting); // get tags from db - const tags = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.favouriteTags || []); + const tags = useContextSelector(ChatPageContext, (v) => v.chatSettings?.favouriteTags || []); // local editable tags list const [localTags, setLocalTags] = useState(tags); diff --git a/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/index.tsx b/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/index.tsx index 6e11dd166..3262bd666 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/index.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/FavouriteAppSetting/index.tsx @@ -1,20 +1,12 @@ -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { Button, - ButtonGroup, Flex, HStack, IconButton, Input, InputGroup, InputLeftElement, - Table, - TableContainer, - Tbody, - Td, - Th, - Thead, - Tr, useDisclosure } from '@chakra-ui/react'; import MySelect from '@fastgpt/web/components/common/MySelect'; @@ -68,7 +60,7 @@ const FavouriteAppSetting = ({ Header }: Props) => { const searchAppTagValue = watchSearchValue('tag'); // apps' tags options - const tagOptions = useContextSelector(ChatSettingContext, (v) => { + const tagOptions = useContextSelector(ChatPageContext, (v) => { const tags = v.chatSettings?.favouriteTags || []; return [ { label: t('chat:setting.favourite.category_all'), value: '' }, @@ -76,7 +68,7 @@ const FavouriteAppSetting = ({ Header }: Props) => { ]; }); // app's tags cache map - const tagMap = useContextSelector(ChatSettingContext, (v) => + const tagMap = useContextSelector(ChatPageContext, (v) => (v.chatSettings?.favouriteTags || []).reduce>( (acc, tag) => { acc[tag.id] = { ...tag }; diff --git a/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/index.tsx b/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/index.tsx index b224d69bc..bdafa29c6 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/index.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/HomepageSetting/index.tsx @@ -23,7 +23,7 @@ import Avatar from '@fastgpt/web/components/common/Avatar'; import type { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { useMount } from 'ahooks'; import { useContextSelector } from 'use-context-selector'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { DEFAULT_LOGO_BANNER_COLLAPSED_URL, DEFAULT_LOGO_BANNER_URL @@ -46,8 +46,8 @@ const HomepageSetting = ({ Header, onDiagramShow }: Props) => { const { t } = useTranslation(); const { feConfigs } = useSystemStore(); - const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); - const refreshChatSetting = useContextSelector(ChatSettingContext, (v) => v.refreshChatSetting); + const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings); + const refreshChatSetting = useContextSelector(ChatPageContext, (v) => v.refreshChatSetting); const chatSettings2Form = useCallback( (data?: ChatSettingType) => { diff --git a/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx b/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx index c7da09305..9c3bac4b5 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/LogDetails.tsx @@ -1,5 +1,5 @@ import LogTable from '@/pageComponents/app/detail/Logs/LogTable'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { Flex } from '@chakra-ui/react'; import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import type { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker'; @@ -16,7 +16,7 @@ type Props = { const chatSourceValues = Object.values(ChatSourceEnum); const LogDetails = ({ Header }: Props) => { - const appId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); + const appId = useContextSelector(ChatPageContext, (v) => v.chatSettings?.appId || ''); const [dateRange, setDateRange] = useState({ from: new Date(addDays(new Date(), -6).setHours(0, 0, 0, 0)), diff --git a/projects/app/src/pageComponents/chat/ChatSetting/index.tsx b/projects/app/src/pageComponents/chat/ChatSetting/index.tsx index 9587874f3..521c3dd00 100644 --- a/projects/app/src/pageComponents/chat/ChatSetting/index.tsx +++ b/projects/app/src/pageComponents/chat/ChatSetting/index.tsx @@ -9,7 +9,7 @@ import { useContextSelector } from 'use-context-selector'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { ChatContext } from '@/web/core/chat/context/chatContext'; import NextHead from '@/components/common/NextHead'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; import { useTranslation } from 'react-i18next'; import { useMount } from 'ahooks'; @@ -43,8 +43,8 @@ const ChatSetting = () => { ); const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); - const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); const handleTabChange = useCallback( (tab: ChatSettingTabOptionEnum) => { diff --git a/projects/app/src/pageComponents/chat/ChatTeamApp/List.tsx b/projects/app/src/pageComponents/chat/ChatTeamApp/List.tsx index 98fb90c69..f6d2c90d5 100644 --- a/projects/app/src/pageComponents/chat/ChatTeamApp/List.tsx +++ b/projects/app/src/pageComponents/chat/ChatTeamApp/List.tsx @@ -15,7 +15,7 @@ import AppTypeTag from '@/pageComponents/chat/ChatTeamApp/TypeTag'; import { formatTimeToChatTime } from '@fastgpt/global/common/string/time'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import UserBox from '@fastgpt/web/components/common/UserBox'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; const List = ({ appType }: { appType: AppTypeEnum | 'all' }) => { @@ -33,7 +33,7 @@ const List = ({ appType }: { appType: AppTypeEnum | 'all' }) => { ].includes(app.type) ) ); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); return ( <> diff --git a/projects/app/src/pageComponents/chat/ChatTeamApp/index.tsx b/projects/app/src/pageComponents/chat/ChatTeamApp/index.tsx index 709d0d23b..d688c5d23 100644 --- a/projects/app/src/pageComponents/chat/ChatTeamApp/index.tsx +++ b/projects/app/src/pageComponents/chat/ChatTeamApp/index.tsx @@ -13,7 +13,7 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { ChatContext } from '@/web/core/chat/context/chatContext'; import NextHead from '@/components/common/NextHead'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { getWebReqUrl } from '@fastgpt/web/common/system/utils'; @@ -29,7 +29,7 @@ const MyApps = () => { (v) => v ); - const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); + const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings); const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); diff --git a/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx b/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx index e0251ab60..2beed8662 100644 --- a/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx +++ b/projects/app/src/pageComponents/chat/ChatWindow/AppChatWindow.tsx @@ -7,7 +7,6 @@ import SideBar from '@/components/SideBar'; import { ChatContext } from '@/web/core/chat/context/chatContext'; import { useContextSelector } from 'use-context-selector'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; -import { type AppListItemType } from '@fastgpt/global/core/app/type'; import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants'; import { useCallback } from 'react'; import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type'; @@ -20,7 +19,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { getInitChatInfo } from '@/web/core/chat/api'; import { useUserStore } from '@/web/support/user/useUserStore'; import NextHead from '@/components/common/NextHead'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { ChatSidebarPaneEnum } from '../constants'; import ChatHistorySidebar from '@/pageComponents/chat/slider/ChatSliderSidebar'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; @@ -30,11 +29,7 @@ import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat'; const CustomPluginRunBox = dynamic(() => import('@/pageComponents/chat/CustomPluginRunBox')); -type Props = { - myApps: AppListItemType[]; -}; - -const AppChatWindow = ({ myApps }: Props) => { +const AppChatWindow = () => { const { userInfo } = useUserStore(); const { chatId, appId, outLinkAuthData } = useChatStore(); @@ -55,9 +50,10 @@ const AppChatWindow = ({ myApps }: Props) => { const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); - const pane = useContextSelector(ChatSettingContext, (v) => v.pane); - const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const pane = useContextSelector(ChatPageContext, (v) => v.pane); + const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); + const refreshRecentlyUsed = useContextSelector(ChatPageContext, (v) => v.refreshRecentlyUsed); const { loading } = useRequest2( async () => { @@ -122,9 +118,19 @@ const AppChatWindow = ({ myApps }: Props) => { title: newTitle })); + refreshRecentlyUsed(); + return { responseText, isNewChat: forbidLoadChat.current }; }, - [appId, chatId, onUpdateHistoryTitle, setChatBoxData, forbidLoadChat, isShowCite] + [ + appId, + chatId, + onUpdateHistoryTitle, + setChatBoxData, + forbidLoadChat, + isShowCite, + refreshRecentlyUsed + ] ); return ( @@ -158,7 +164,6 @@ const AppChatWindow = ({ myApps }: Props) => { pane={pane} chatSettings={chatSettings} showHistory - apps={myApps} history={chatRecords} totalRecordsCount={totalRecordsCount} /> diff --git a/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx b/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx index 062b176a8..131e72da3 100644 --- a/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx +++ b/projects/app/src/pageComponents/chat/ChatWindow/HomeChatWindow.tsx @@ -36,12 +36,8 @@ import { getDefaultAppForm } from '@fastgpt/global/core/app/utils'; import { getToolPreviewNode } from '@/web/core/app/api/tool'; import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node'; import { getWebLLMModel } from '@/web/common/system/utils'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; -import type { - AppFileSelectConfigType, - AppListItemType, - AppWhisperConfigType -} from '@fastgpt/global/core/app/type'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; +import type { AppFileSelectConfigType, AppWhisperConfigType } from '@fastgpt/global/core/app/type'; import ChatHeader from '@/pageComponents/chat/ChatHeader'; import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; import { ChatSidebarPaneEnum } from '../constants'; @@ -49,10 +45,6 @@ import ChatHistorySidebar from '@/pageComponents/chat/slider/ChatSliderSidebar'; import ChatSliderMobileDrawer from '@/pageComponents/chat/slider/ChatSliderMobileDrawer'; import { getWebReqUrl } from '@fastgpt/web/common/system/utils'; -type Props = { - myApps: AppListItemType[]; -}; - const defaultFileSelectConfig: AppFileSelectConfigType = { maxFiles: 20, canSelectFile: true, @@ -68,7 +60,7 @@ const defaultWhisperConfig: AppWhisperConfigType = { autoTTSResponse: false }; -const HomeChatWindow = ({ myApps }: Props) => { +const HomeChatWindow = () => { const { t } = useTranslation(); const { isPc } = useSystem(); @@ -86,10 +78,11 @@ const HomeChatWindow = ({ myApps }: Props) => { const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables); const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite); - const pane = useContextSelector(ChatSettingContext, (v) => v.pane); - const chatSettings = useContextSelector(ChatSettingContext, (v) => v.chatSettings); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); - const homeAppId = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.appId || ''); + const pane = useContextSelector(ChatPageContext, (v) => v.pane); + const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); + const homeAppId = useContextSelector(ChatPageContext, (v) => v.chatSettings?.appId || ''); + const refreshRecentlyUsed = useContextSelector(ChatPageContext, (v) => v.refreshRecentlyUsed); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); @@ -232,6 +225,8 @@ const HomeChatWindow = ({ myApps }: Props) => { title: newTitle })); + refreshRecentlyUsed(); + return { responseText, isNewChat: forbidLoadChat.current }; } @@ -281,6 +276,8 @@ const HomeChatWindow = ({ myApps }: Props) => { title: newTitle })); + refreshRecentlyUsed(); + return { responseText, isNewChat: forbidLoadChat.current }; } ); @@ -449,7 +446,6 @@ const HomeChatWindow = ({ myApps }: Props) => { pane={pane} chatSettings={chatSettings} showHistory - apps={myApps} history={chatRecords} totalRecordsCount={totalRecordsCount} /> diff --git a/projects/app/src/pageComponents/chat/slider/ChatSliderFooter.tsx b/projects/app/src/pageComponents/chat/slider/ChatSliderFooter.tsx index ceadb24d2..0464de8f0 100644 --- a/projects/app/src/pageComponents/chat/slider/ChatSliderFooter.tsx +++ b/projects/app/src/pageComponents/chat/slider/ChatSliderFooter.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; import { useContextSelector } from 'use-context-selector'; import { ChatContext } from '@/web/core/chat/context/chatContext'; @@ -15,8 +15,8 @@ const ChatSliderFooter = () => { const { feConfigs } = useSystemStore(); const onCloseSlider = useContextSelector(ChatContext, (v) => v.onCloseSlider); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); - const pane = useContextSelector(ChatSettingContext, (v) => v.pane); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); + const pane = useContextSelector(ChatPageContext, (v) => v.pane); const isAdmin = !!userInfo?.team.permission.hasManagePer; const isSettingPane = pane === ChatSidebarPaneEnum.SETTING; diff --git a/projects/app/src/pageComponents/chat/slider/ChatSliderHeader.tsx b/projects/app/src/pageComponents/chat/slider/ChatSliderHeader.tsx index 11fb7b672..4d924f16c 100644 --- a/projects/app/src/pageComponents/chat/slider/ChatSliderHeader.tsx +++ b/projects/app/src/pageComponents/chat/slider/ChatSliderHeader.tsx @@ -1,6 +1,6 @@ import { GridItem, Grid } from '@chakra-ui/react'; import React from 'react'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; import { useContextSelector } from 'use-context-selector'; import { ChatContext } from '@/web/core/chat/context/chatContext'; @@ -24,9 +24,9 @@ const ChatSliderHeader = ({ title, banner }: Props) => { const { isPc } = useSystem(); const { setChatId } = useChatStore(); - const pane = useContextSelector(ChatSettingContext, (v) => v.pane); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); - const enableHome = useContextSelector(ChatSettingContext, (v) => v.chatSettings?.enableHome); + const pane = useContextSelector(ChatPageContext, (v) => v.pane); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); + const enableHome = useContextSelector(ChatPageContext, (v) => v.chatSettings?.enableHome); const appName = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.name); const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.avatar); diff --git a/projects/app/src/pageComponents/chat/slider/index.tsx b/projects/app/src/pageComponents/chat/slider/index.tsx index d5837f146..5240427a3 100644 --- a/projects/app/src/pageComponents/chat/slider/index.tsx +++ b/projects/app/src/pageComponents/chat/slider/index.tsx @@ -17,12 +17,12 @@ import { } from '@/pageComponents/chat/constants'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useContextSelector } from 'use-context-selector'; -import { ChatSettingContext } from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext } from '@/web/core/chat/context/chatPageContext'; import { usePathname } from 'next/navigation'; +import type { GetRecentlyUsedAppsResponseType } from '@fastgpt/service/core/app/record/type'; type Props = { activeAppId: string; - apps: AppListItemType[]; }; const MotionBox = motion(Box); @@ -148,13 +148,13 @@ const AnimatedText: React.FC = ({ show, children, className, ); const LogoSection = () => { - const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); - const logos = useContextSelector(ChatSettingContext, (v) => v.logos); + const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1); + const logos = useContextSelector(ChatPageContext, (v) => v.logos); const isHomeActive = useContextSelector( - ChatSettingContext, + ChatPageContext, (v) => v.pane === ChatSidebarPaneEnum.HOME ); - const onTriggerCollapse = useContextSelector(ChatSettingContext, (v) => v.onTriggerCollapse); + const onTriggerCollapse = useContextSelector(ChatPageContext, (v) => v.onTriggerCollapse); const wideLogoSrc = logos.wideLogoUrl; const squareLogoSrc = logos.squareLogoUrl; @@ -256,24 +256,24 @@ const NavigationSection = () => { const { feConfigs } = useSystemStore(); const isEnableHome = useContextSelector( - ChatSettingContext, + ChatPageContext, (v) => v.chatSettings?.enableHome ?? true ); - const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); - const onTriggerCollapse = useContextSelector(ChatSettingContext, (v) => v.onTriggerCollapse); + const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1); + const onTriggerCollapse = useContextSelector(ChatPageContext, (v) => v.onTriggerCollapse); const isHomeActive = useContextSelector( - ChatSettingContext, + ChatPageContext, (v) => v.pane === ChatSidebarPaneEnum.HOME ); const isTeamAppsActive = useContextSelector( - ChatSettingContext, + ChatPageContext, (v) => v.pane === ChatSidebarPaneEnum.TEAM_APPS ); const isFavouriteAppsActive = useContextSelector( - ChatSettingContext, + ChatPageContext, (v) => v.pane === ChatSidebarPaneEnum.FAVORITE_APPS ); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); return ( @@ -365,12 +365,12 @@ const BottomSection = () => { const isAdmin = !!userInfo?.team.permission.hasManagePer; const isShare = pathname === '/chat/share'; - const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); + const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1); const isSettingActive = useContextSelector( - ChatSettingContext, + ChatPageContext, (v) => v.pane === ChatSidebarPaneEnum.SETTING ); - const onSettingClick = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const onSettingClick = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); return ( @@ -485,13 +485,14 @@ const BottomSection = () => { ); }; -const ChatSlider = ({ apps, activeAppId }: Props) => { +const ChatSlider = ({ activeAppId }: Props) => { const { t } = useTranslation(); - const isCollapsed = useContextSelector(ChatSettingContext, (v) => v.collapse === 1); - const pane = useContextSelector(ChatSettingContext, (v) => v.pane); + const isCollapsed = useContextSelector(ChatPageContext, (v) => v.collapse === 1); + const pane = useContextSelector(ChatPageContext, (v) => v.pane); + const myApps = useContextSelector(ChatPageContext, (v) => v.myApps); - const handlePaneChange = useContextSelector(ChatSettingContext, (v) => v.handlePaneChange); + const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange); return ( { - {apps.map((item) => ( + {myApps.map((item) => ( { - const { setSource, setAppId } = useChatStore(); - const { userInfo, initUserInfo } = useUserStore(); - - const [isInitedUser, setIsInitedUser] = useState(false); - - // get app list - const { data: myApps = [] } = useRequest2(() => getRecentlyUsedApps({ getRecentlyChat: true }), { - manual: false, - errorToast: '', - refreshDeps: [userInfo], - pollingInterval: 30000 - }); - - // initialize user info - useMount(async () => { - // ensure store has current appId before setting source (avoids fallback to lastChatAppId) - if (appId) setAppId(appId); - try { - await initUserInfo(); - } catch (error) { - console.log('User not logged in:', error); - } finally { - setSource('online'); - setIsInitedUser(true); - } - }); - - // sync appId to store as soon as route/appId changes - useEffect(() => { - if (appId) { - setAppId(appId); - } - }, [appId, setAppId, userInfo]); - - return { - isInitedUser, - userInfo, - myApps - }; -}; diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts index ea9265c5f..8246df691 100644 --- a/projects/app/src/pages/api/core/app/create.ts +++ b/projects/app/src/pages/api/core/app/create.ts @@ -33,6 +33,7 @@ import { isS3ObjectKey } from '@fastgpt/service/common/s3/utils'; import { MongoAppTemplate } from '@fastgpt/service/core/app/templates/templateSchema'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import path from 'node:path'; +import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller'; export type CreateAppBody = { parentId?: ParentIdType; @@ -243,6 +244,11 @@ export const onCreateApp = async ({ await getS3AvatarSource().refreshAvatar(_avatar, undefined, session); + await updateParentFoldersUpdateTime({ + parentId, + session + }); + (async () => { addAuditLog({ tmbId, diff --git a/projects/app/src/pages/api/core/app/httpTools/update.ts b/projects/app/src/pages/api/core/app/httpTools/update.ts index 6ee3a5cc9..344bbf2f4 100644 --- a/projects/app/src/pages/api/core/app/httpTools/update.ts +++ b/projects/app/src/pages/api/core/app/httpTools/update.ts @@ -10,6 +10,7 @@ import { MongoApp } from '@fastgpt/service/core/app/schema'; import type { StoreSecretValueType } from '@fastgpt/global/common/secret/type'; import { storeSecretValue } from '@fastgpt/service/common/secret/utils'; import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema'; +import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller'; export type UpdateHttpPluginBody = { appId: string; @@ -50,6 +51,12 @@ async function handler(req: ApiRequestProps, res: NextApiR }, { session } ); + + await updateParentFoldersUpdateTime({ + parentId: app.parentId, + session + }); + await MongoAppVersion.updateOne( { appId }, { diff --git a/projects/app/src/pages/api/core/app/list.ts b/projects/app/src/pages/api/core/app/list.ts index 949b86f63..65b1adeb8 100644 --- a/projects/app/src/pages/api/core/app/list.ts +++ b/projects/app/src/pages/api/core/app/list.ts @@ -23,7 +23,6 @@ import { sumPer } from '@fastgpt/global/support/permission/utils'; export type ListAppBody = { parentId?: ParentIdType; type?: AppTypeEnum | AppTypeEnum[]; - getRecentlyChat?: boolean; searchKey?: string; }; @@ -38,7 +37,7 @@ export type ListAppBody = { */ async function handler(req: ApiRequestProps): Promise { - const { parentId, type, getRecentlyChat, searchKey } = req.body; + const { parentId, type, searchKey } = req.body; // Auth user permission const [{ tmbId, teamId, permission: teamPer }] = await Promise.all([ @@ -94,14 +93,6 @@ async function handler(req: ApiRequestProps): Promise { - if (getRecentlyChat) { - return { - // get all chat app, excluding hidden apps and deleted apps - teamId, - type: { $in: [AppTypeEnum.workflow, AppTypeEnum.simple, AppTypeEnum.workflowTool] } - }; - } - // Filter apps by permission, if not owner, only get apps that I have permission to access const idList = { _id: { $in: myPerList.map((item) => item.resourceId) } }; const appPerQuery = teamPer.isOwner @@ -153,7 +144,6 @@ async function handler(req: ApiRequestProps): Promise { - if (getRecentlyChat) return 15; if (searchKey) return 50; return; })(); diff --git a/projects/app/src/pages/api/core/app/mcpTools/update.ts b/projects/app/src/pages/api/core/app/mcpTools/update.ts index df0cca86f..555a3646d 100644 --- a/projects/app/src/pages/api/core/app/mcpTools/update.ts +++ b/projects/app/src/pages/api/core/app/mcpTools/update.ts @@ -10,6 +10,7 @@ import { getMCPToolSetRuntimeNode } from '@fastgpt/global/core/app/tool/mcpTool/ import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema'; import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type'; import { storeSecretValue } from '@fastgpt/service/common/secret/utils'; +import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller'; export type updateMCPToolsQuery = {}; @@ -51,6 +52,12 @@ async function handler( }, { session } ); + + await updateParentFoldersUpdateTime({ + parentId: app.parentId, + session + }); + await MongoAppVersion.updateOne( { appId }, { diff --git a/projects/app/src/pages/api/core/app/recentlyUsed.ts b/projects/app/src/pages/api/core/app/recentlyUsed.ts new file mode 100644 index 000000000..495a1f6b9 --- /dev/null +++ b/projects/app/src/pages/api/core/app/recentlyUsed.ts @@ -0,0 +1,43 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { NextAPI } from '@/service/middleware/entry'; +import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { MongoAppRecord } from '@fastgpt/service/core/app/record/schema'; +import { MongoApp } from '@fastgpt/service/core/app/schema'; +import type { GetRecentlyUsedAppsResponseType } from '@fastgpt/service/core/app/record/type'; + +async function handler( + req: ApiRequestProps<{}, {}>, + _res: ApiResponseType +) { + const { tmbId } = await authUserPer({ + req, + authToken: true, + authApiKey: true + }); + + const recentRecords = await MongoAppRecord.find( + { tmbId }, + { appId: 1 }, + { sort: { lastUsedTime: -1 }, limit: 20 } + ).lean(); + + if (!recentRecords.length) return []; + + const apps = await MongoApp.find( + { _id: { $in: recentRecords.map((record) => record.appId) } }, + '_id name avatar' + ).lean(); + + const appMap = new Map(apps.map((app) => [String(app._id), app])); + + return recentRecords + .map((record) => appMap.get(String(record.appId))) + .filter((app) => app != null) + .map((app) => ({ + _id: String(app._id), + name: app.name, + avatar: app.avatar + })); +} + +export default NextAPI(handler); diff --git a/projects/app/src/pages/api/core/app/update.ts b/projects/app/src/pages/api/core/app/update.ts index 68a20455a..358045ec7 100644 --- a/projects/app/src/pages/api/core/app/update.ts +++ b/projects/app/src/pages/api/core/app/update.ts @@ -27,6 +27,7 @@ import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nAppType } from '@fastgpt/service/support/user/audit/util'; import { i18nT } from '@fastgpt/web/i18n/utils'; import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar'; +import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller'; export type AppUpdateQuery = { appId: string; @@ -117,7 +118,7 @@ async function handler(req: ApiRequestProps) { await getS3AvatarSource().refreshAvatar(avatar, app.avatar, session); - return MongoApp.findByIdAndUpdate( + const result = await MongoApp.findByIdAndUpdate( appId, { ...parseParentIdInMongo(parentId), @@ -137,6 +138,26 @@ async function handler(req: ApiRequestProps) { }, { session } ); + + if (isMove) { + // Update both old and new parent folders + await updateParentFoldersUpdateTime({ + parentId: app.parentId, + session + }); + await updateParentFoldersUpdateTime({ + parentId, + session + }); + } else { + // Update current parent folder + await updateParentFoldersUpdateTime({ + parentId: parentId || app.parentId, + session + }); + } + + return result; }; // Move diff --git a/projects/app/src/pages/api/core/app/version/publish.ts b/projects/app/src/pages/api/core/app/version/publish.ts index 6d804a085..d7a1e3873 100644 --- a/projects/app/src/pages/api/core/app/version/publish.ts +++ b/projects/app/src/pages/api/core/app/version/publish.ts @@ -13,6 +13,7 @@ import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nAppType } from '@fastgpt/service/support/user/audit/util'; import { i18nT } from '@fastgpt/web/i18n/utils'; +import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller'; async function handler(req: ApiRequestProps, res: NextApiResponse) { const { appId } = req.query as { appId: string }; @@ -29,6 +30,10 @@ async function handler(req: ApiRequestProps, res: NextApiRe nodes }); + await updateParentFoldersUpdateTime({ + parentId: app.parentId + }); + if (autoSave) { await mongoSessionRun(async (session) => { await MongoAppVersion.updateOne( diff --git a/projects/app/src/pages/api/core/chat/chatTest.ts b/projects/app/src/pages/api/core/chat/chatTest.ts index 8c934d397..c035395b1 100644 --- a/projects/app/src/pages/api/core/chat/chatTest.ts +++ b/projects/app/src/pages/api/core/chat/chatTest.ts @@ -248,7 +248,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { nodes, appChatConfig: chatConfig, variables: newVariables, - isUpdateUseTime: false, // owner update use time newTitle, source: ChatSourceEnum.test, userContent: userQuestion, diff --git a/projects/app/src/pages/api/core/chat/init.ts b/projects/app/src/pages/api/core/chat/init.ts index efd1fc3cf..840786906 100644 --- a/projects/app/src/pages/api/core/chat/init.ts +++ b/projects/app/src/pages/api/core/chat/init.ts @@ -1,6 +1,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@fastgpt/service/common/response'; import { authApp } from '@fastgpt/service/support/permission/app/auth'; +import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils'; import { getChatModelNameListByModules } from '@/service/core/app/workflow'; import type { InitChatProps, InitChatResponse } from '@/global/core/chat/api.d'; @@ -11,6 +12,8 @@ import { NextAPI } from '@/service/middleware/entry'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { presignVariablesFileUrls } from '@fastgpt/service/core/chat/utils'; +import { MongoAppRecord } from '@fastgpt/service/core/app/record/schema'; +import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; async function handler( req: NextApiRequest, @@ -25,57 +28,75 @@ async function handler( }); } - // auth app permission - const [{ app, tmbId }, chat] = await Promise.all([ - authApp({ - req, - authToken: true, - authApiKey: true, - appId, - per: ReadPermissionVal - }), - chatId ? MongoChat.findOne({ appId, chatId }) : undefined - ]); - - // auth chat permission - if (chat && !app.permission.hasReadChatLogPer && String(tmbId) !== String(chat?.tmbId)) { - return Promise.reject(ChatErrEnum.unAuthChat); - } - - // get app and history - const { nodes, chatConfig } = await getAppLatestVersion(app._id, app); - const pluginInputs = - chat?.pluginInputs ?? - nodes?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs ?? - []; - - const variables = await presignVariablesFileUrls({ - variables: chat?.variables, - variableConfig: chat?.variableList - }); - - return { - chatId, - appId, - title: chat?.title, - userAvatar: undefined, - variables, - app: { - chatConfig: getAppChatConfig({ - chatConfig, - systemConfigNode: getGuideModule(nodes), - storeVariables: chat?.variableList, - storeWelcomeText: chat?.welcomeText, - isPublicFetch: false + try { + // auth app permission + const [{ app, tmbId }, chat] = await Promise.all([ + authApp({ + req, + authToken: true, + authApiKey: true, + appId, + per: ReadPermissionVal }), - chatModels: getChatModelNameListByModules(nodes), - name: app.name, - avatar: app.avatar, - intro: app.intro, - type: app.type, - pluginInputs + chatId ? MongoChat.findOne({ appId, chatId }) : undefined + ]); + + // auth chat permission + if (chat && !app.permission.hasReadChatLogPer && String(tmbId) !== String(chat?.tmbId)) { + return Promise.reject(ChatErrEnum.unAuthChat); } - }; + + // get app and history + const { nodes, chatConfig } = await getAppLatestVersion(app._id, app); + const pluginInputs = + chat?.pluginInputs ?? + nodes?.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs ?? + []; + + const variables = await presignVariablesFileUrls({ + variables: chat?.variables, + variableConfig: chat?.variableList + }); + + return { + chatId, + appId, + title: chat?.title, + userAvatar: undefined, + variables, + app: { + chatConfig: getAppChatConfig({ + chatConfig, + systemConfigNode: getGuideModule(nodes), + storeVariables: chat?.variableList, + storeWelcomeText: chat?.welcomeText, + isPublicFetch: false + }), + chatModels: getChatModelNameListByModules(nodes), + name: app.name, + avatar: app.avatar, + intro: app.intro, + type: app.type, + pluginInputs + } + }; + } catch (error: any) { + if (error === AppErrEnum.unAuthApp) { + const { tmbId, teamId } = await authUserPer({ + req, + authToken: true, + authApiKey: true + }); + + await MongoAppRecord.deleteMany({ + tmbId, + teamId, + appId + }); + } + + return Promise.reject(error); + } } export default NextAPI(handler); diff --git a/projects/app/src/pages/api/v1/chat/completions.ts b/projects/app/src/pages/api/v1/chat/completions.ts index 68e9387e3..a42ea345b 100644 --- a/projects/app/src/pages/api/v1/chat/completions.ts +++ b/projects/app/src/pages/api/v1/chat/completions.ts @@ -27,6 +27,7 @@ import { } from '@fastgpt/service/core/chat/saveChat'; import { responseWrite } from '@fastgpt/service/common/response'; import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink'; +import { recordAppUsage } from '@fastgpt/service/core/app/record/utils'; import { pushResult2Remote, addOutLinkUsage } from '@fastgpt/service/support/outLink/tools'; import { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools'; import { authTeamSpaceToken } from '@/service/support/permission/auth/team'; @@ -326,7 +327,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { })(); // save chat - const isOwnerUse = !shareId && !spaceTeamId && String(tmbId) === String(app.tmbId); const source = (() => { if (shareId) { return ChatSourceEnum.share; @@ -363,7 +363,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { nodes, appChatConfig: chatConfig, variables: newVariables, - isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time newTitle, shareId, outLinkUid: outLinkUserId, @@ -383,6 +382,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { await saveChat(params); } + setImmediate(async () => { + await recordAppUsage({ + appId: String(app._id), + tmbId: String(tmbId), + teamId: String(teamId) + }); + }); + addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`); /* select fe response field */ diff --git a/projects/app/src/pages/api/v2/chat/completions.ts b/projects/app/src/pages/api/v2/chat/completions.ts index 8cfde3e15..3a36e7a8b 100644 --- a/projects/app/src/pages/api/v2/chat/completions.ts +++ b/projects/app/src/pages/api/v2/chat/completions.ts @@ -27,6 +27,7 @@ import { } from '@fastgpt/service/core/chat/saveChat'; import { responseWrite } from '@fastgpt/service/common/response'; import { authOutLinkChatStart } from '@/service/support/permission/auth/outLink'; +import { recordAppUsage } from '@fastgpt/service/core/app/record/utils'; import { pushResult2Remote, addOutLinkUsage } from '@fastgpt/service/support/outLink/tools'; import { getUsageSourceByAuthType } from '@fastgpt/global/support/wallet/usage/tools'; import { authTeamSpaceToken } from '@/service/support/permission/auth/team'; @@ -328,7 +329,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { })(); // save chat - const isOwnerUse = !shareId && !spaceTeamId && String(tmbId) === String(app.tmbId); const source = (() => { if (shareId) { return ChatSourceEnum.share; @@ -365,7 +365,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { nodes, appChatConfig: chatConfig, variables: newVariables, - isUpdateUseTime: isOwnerUse && source === ChatSourceEnum.online, // owner update use time newTitle, shareId, outLinkUid: outLinkUserId, @@ -385,6 +384,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { await saveChat(params); } + setImmediate(async () => { + await recordAppUsage({ + appId: String(app._id), + tmbId: String(tmbId), + teamId: String(teamId) + }); + }); + addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`); /* select fe response field */ diff --git a/projects/app/src/pages/chat/index.tsx b/projects/app/src/pages/chat/index.tsx index 536b4d19d..20ef86762 100644 --- a/projects/app/src/pages/chat/index.tsx +++ b/projects/app/src/pages/chat/index.tsx @@ -8,7 +8,6 @@ import { serviceSideProps } from '@/web/common/i18n/utils'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; import { GetChatTypeEnum } from '@/global/core/chat/constants'; import ChatContextProvider from '@/web/core/chat/context/chatContext'; -import { type AppListItemType } from '@fastgpt/global/core/app/type'; import { useContextSelector } from 'use-context-selector'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; @@ -18,13 +17,9 @@ import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList'; import LoginModal from '@/pageComponents/login/LoginModal'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import ChatSetting from '@/pageComponents/chat/ChatSetting'; -import { useChat } from '@/pageComponents/chat/useChat'; import AppChatWindow from '@/pageComponents/chat/ChatWindow/AppChatWindow'; import HomeChatWindow from '@/pageComponents/chat/ChatWindow/HomeChatWindow'; -import { - ChatSettingContext, - ChatSettingContextProvider -} from '@/web/core/chat/context/chatSettingContext'; +import { ChatPageContext, ChatPageContextProvider } from '@/web/core/chat/context/chatPageContext'; import ChatTeamApp from '@/pageComponents/chat/ChatTeamApp'; import ChatFavouriteApp from '@/pageComponents/chat/ChatFavouriteApp'; import { useUserStore } from '@/web/support/user/useUserStore'; @@ -33,7 +28,7 @@ import { MongoOutLink } from '@fastgpt/service/support/outLink/schema'; import { addLog } from '@fastgpt/service/common/system/log'; import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant'; -const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { +const Chat = () => { const { isPc } = useSystem(); const { appId } = useChatStore(); @@ -41,8 +36,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData); - const collapse = useContextSelector(ChatSettingContext, (v) => v.collapse); - const pane = useContextSelector(ChatSettingContext, (v) => v.pane); + const collapse = useContextSelector(ChatPageContext, (v) => v.collapse); + const pane = useContextSelector(ChatPageContext, (v) => v.pane); return ( @@ -55,14 +50,14 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { overflow={'hidden'} transition={'width 0.1s ease-in-out'} > - + )} {(!datasetCiteData || isPc) && ( {/* home chat window */} - {pane === ChatSidebarPaneEnum.HOME && } + {pane === ChatSidebarPaneEnum.HOME && } {/* favourite apps */} {pane === ChatSidebarPaneEnum.FAVORITE_APPS && } @@ -71,7 +66,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { {pane === ChatSidebarPaneEnum.TEAM_APPS && } {/* recently used apps chat window */} - {pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && } + {pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && } {/* setting */} {pane === ChatSidebarPaneEnum.SETTING && } @@ -91,19 +86,23 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { ); }; -const Render = (props: { +type ChatPageProps = { appId: string; isStandalone?: string; showRunningStatus: boolean; showCite: boolean; showFullText: boolean; canDownloadSource: boolean; -}) => { +}; + +const ChatContent = (props: ChatPageProps) => { const { appId, isStandalone } = props; const { chatId } = useChatStore(); const { setUserInfo } = useUserStore(); const { feConfigs } = useSystemStore(); - const { isInitedUser, userInfo, myApps } = useChat(appId); + + const isInitedUser = useContextSelector(ChatPageContext, (v) => v.isInitedUser); + const userInfo = useContextSelector(ChatPageContext, (v) => v.userInfo); const chatHistoryProviderParams = useMemo( () => ({ appId, source: ChatSourceEnum.online }), @@ -144,21 +143,27 @@ const Render = (props: { // show main chat interface return ( - - - - - - - - - + + + + + + + + ); +}; + +const Render = (props: ChatPageProps) => { + return ( + + + ); }; diff --git a/projects/app/src/service/core/app/utils.ts b/projects/app/src/service/core/app/utils.ts index e8f6685e5..cde645cab 100644 --- a/projects/app/src/service/core/app/utils.ts +++ b/projects/app/src/service/core/app/utils.ts @@ -101,7 +101,6 @@ export const getScheduleTriggerApp = async () => { nodes, appChatConfig: chatConfig, variables: {}, - isUpdateUseTime: false, // owner update use time newTitle: 'Cron Job', source: ChatSourceEnum.cronJob, userContent: { diff --git a/projects/app/src/service/support/mcp/utils.ts b/projects/app/src/service/support/mcp/utils.ts index 0eca67f80..fbf2645cd 100644 --- a/projects/app/src/service/support/mcp/utils.ts +++ b/projects/app/src/service/support/mcp/utils.ts @@ -257,7 +257,6 @@ export const callMcpServerTool = async ({ key, toolName, inputs }: toolCallProps nodes, appChatConfig: chatConfig, variables: newVariables, - isUpdateUseTime: false, // owner update use time newTitle, source: ChatSourceEnum.mcp, userContent: userQuestion, diff --git a/projects/app/src/web/core/app/api.ts b/projects/app/src/web/core/app/api.ts index 7d2a795e3..e925daa3b 100644 --- a/projects/app/src/web/core/app/api.ts +++ b/projects/app/src/web/core/app/api.ts @@ -5,6 +5,7 @@ import type { CreateAppBody } from '@/pages/api/core/app/create'; import type { ListAppBody } from '@/pages/api/core/app/list'; import type { getBasicInfoResponse } from '@/pages/api/core/app/getBasicInfo'; +import type { GetRecentlyUsedAppsResponseType } from '@fastgpt/service/core/app/record/type'; /** * 获取应用列表 @@ -14,10 +15,8 @@ export const getMyApps = (data?: ListAppBody) => maxQuantity: 1 }); -export const getRecentlyUsedApps = (data?: ListAppBody) => - POST('/core/app/list?t=0', data, { - maxQuantity: 1 - }); +export const getRecentlyUsedApps = () => + GET('/core/app/recentlyUsed'); /** * 创建一个应用 diff --git a/projects/app/src/web/core/chat/context/chatSettingContext.tsx b/projects/app/src/web/core/chat/context/chatPageContext.tsx similarity index 62% rename from projects/app/src/web/core/chat/context/chatSettingContext.tsx rename to projects/app/src/web/core/chat/context/chatPageContext.tsx index 4f2ea4365..e7255a9b0 100644 --- a/projects/app/src/web/core/chat/context/chatSettingContext.tsx +++ b/projects/app/src/web/core/chat/context/chatPageContext.tsx @@ -13,8 +13,14 @@ import { useRouter } from 'next/router'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { createContext } from 'use-context-selector'; import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance'; +import { getRecentlyUsedApps } from '@/web/core/app/api'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { useMount } from 'ahooks'; +import type { GetRecentlyUsedAppsResponseType } from '@fastgpt/service/core/app/record/type'; +import type { UserType } from '@fastgpt/global/support/user/type'; -export type ChatSettingContextValue = { +export type ChatPageContextValue = { + // Pane & collapse pane: ChatSidebarPaneEnum; handlePaneChange: ( pane: ChatSidebarPaneEnum, @@ -23,12 +29,18 @@ export type ChatSettingContextValue = { ) => void; collapse: CollapseStatusType; onTriggerCollapse: () => void; + // Chat settings chatSettings: ChatSettingType | undefined; refreshChatSetting: () => Promise; logos: { wideLogoUrl?: string; squareLogoUrl?: string }; + // User & apps + isInitedUser: boolean; + userInfo: UserType | null; + myApps: GetRecentlyUsedAppsResponseType; + refreshRecentlyUsed: () => void; }; -export const ChatSettingContext = createContext({ +export const ChatPageContext = createContext({ pane: ChatSidebarPaneEnum.HOME, handlePaneChange: () => {}, collapse: defaultCollapseStatus, @@ -37,19 +49,62 @@ export const ChatSettingContext = createContext({ logos: { wideLogoUrl: '', squareLogoUrl: '' }, refreshChatSetting: function (): Promise { throw new Error('Function not implemented.'); - } + }, + isInitedUser: false, + userInfo: null, + myApps: [], + refreshRecentlyUsed: () => {} }); -export const ChatSettingContextProvider = ({ children }: { children: React.ReactNode }) => { +export const ChatPageContextProvider = ({ + appId: routeAppId, + children +}: { + appId: string; + children: React.ReactNode; +}) => { const router = useRouter(); const { feConfigs } = useSystemStore(); - const { appId, setLastPane, setLastChatAppId, lastPane } = useChatStore(); + const { setSource, setAppId, setLastPane, setLastChatAppId, lastPane } = useChatStore(); + const { userInfo, initUserInfo } = useUserStore(); const { pane = lastPane || ChatSidebarPaneEnum.HOME } = router.query as { pane: ChatSidebarPaneEnum; }; const [collapse, setCollapse] = useState(defaultCollapseStatus); + const [isInitedUser, setIsInitedUser] = useState(false); + + // Get recently used apps + const { data: myApps = [], refresh: refreshRecentlyUsed } = useRequest2( + () => getRecentlyUsedApps(), + { + manual: false, + errorToast: '', + refreshDeps: [userInfo], + pollingInterval: 30000 + } + ); + + // Initialize user info + useMount(async () => { + if (routeAppId) setAppId(routeAppId); + try { + await initUserInfo(); + } catch (error) { + console.log('User not logged in:', error); + } finally { + setSource('online'); + setIsInitedUser(true); + } + }); + + // Sync appId to store as route/appId changes + useEffect(() => { + if (routeAppId) { + setAppId(routeAppId); + } + }, [routeAppId, setAppId, userInfo]); const { data: chatSettings, runAsync: refreshChatSetting } = useRequest2( async () => { @@ -69,8 +124,8 @@ export const ChatSettingContextProvider = ({ children }: { children: React.React if ( pane === ChatSidebarPaneEnum.HOME && - appId !== data.appId && - data.quickAppList.every((q) => q._id !== appId) + routeAppId !== data.appId && + data.quickAppList.every((q) => q._id !== routeAppId) ) { handlePaneChange(ChatSidebarPaneEnum.HOME, data.appId); } @@ -126,7 +181,7 @@ export const ChatSettingContextProvider = ({ children }: { children: React.React setCollapse(collapse === 0 ? 1 : 0); }, [collapse]); - const value: ChatSettingContextValue = useMemoEnhance( + const value: ChatPageContextValue = useMemoEnhance( () => ({ pane, handlePaneChange, @@ -134,10 +189,26 @@ export const ChatSettingContextProvider = ({ children }: { children: React.React onTriggerCollapse, chatSettings, refreshChatSetting, - logos + logos, + isInitedUser, + userInfo, + myApps, + refreshRecentlyUsed }), - [pane, handlePaneChange, collapse, chatSettings, refreshChatSetting, onTriggerCollapse, logos] + [ + pane, + handlePaneChange, + collapse, + onTriggerCollapse, + chatSettings, + refreshChatSetting, + logos, + isInitedUser, + userInfo, + myApps, + refreshRecentlyUsed + ] ); - return {children}; + return {children}; }; diff --git a/test/cases/service/core/chat/saveChat.test.ts b/test/cases/service/core/chat/saveChat.test.ts index 5fade7cf1..9c1308b93 100644 --- a/test/cases/service/core/chat/saveChat.test.ts +++ b/test/cases/service/core/chat/saveChat.test.ts @@ -31,7 +31,6 @@ const createMockProps = ( outputs: [] } ], - isUpdateUseTime: true, newTitle: 'Test Chat', source: 'online' as any, userContent: { @@ -228,7 +227,7 @@ describe('saveChat', () => { collectionId: 'collection-1', sourceId: 'source-1', sourceName: 'doc.pdf', - score: [{ type: 'embedding', value: 0.95, index: 0 }], + score: [{ type: 'embedding' as const, value: 0.95, index: 0 }], q: 'What is AI?', a: 'AI stands for Artificial Intelligence...', updateTime: new Date() @@ -283,36 +282,6 @@ describe('saveChat', () => { } }); - it('should update app use time when isUpdateUseTime is true', async () => { - const beforeTime = new Date(); - - const props = createMockProps( - { isUpdateUseTime: true }, - { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } - ); - - await saveChat(props); - - const app = await MongoApp.findById(testAppId); - expect(app?.updateTime).toBeDefined(); - expect(app!.updateTime.getTime()).toBeGreaterThanOrEqual(beforeTime.getTime()); - }); - - it('should not update app use time when isUpdateUseTime is false', async () => { - const app = await MongoApp.findById(testAppId); - const originalUpdateTime = app!.updateTime; - - const props = createMockProps( - { isUpdateUseTime: false }, - { appId: testAppId, teamId: testTeamId, tmbId: testTmbId } - ); - - await saveChat(props); - - const updatedApp = await MongoApp.findById(testAppId); - expect(updatedApp!.updateTime.getTime()).toBe(originalUpdateTime.getTime()); - }); - it('should create chat data log with error count when response has error', async () => { const props = createMockProps( {