This commit is contained in:
heheer 2025-12-23 11:14:39 +08:00 committed by GitHub
commit 209336b472
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 525 additions and 321 deletions

View File

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

View File

@ -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<AppRecordType>(
AppRecordCollectionName,
AppRecordSchema
);

View File

@ -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<typeof AppRecordSchemaZod>;
export const GetRecentlyUsedAppsResponseSchema = z.array(
z.object({
_id: z.string(),
name: z.string(),
avatar: z.string()
})
);
export type GetRecentlyUsedAppsResponseType = z.infer<typeof GetRecentlyUsedAppsResponseSchema>;

View File

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

View File

@ -35,7 +35,6 @@ export type Props = {
nodes: StoreNodeItemType[];
appChatConfig?: AppChatConfigType;
variables?: Record<string, any>;
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);
}

View File

@ -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) => {

View File

@ -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 = ({
</>
) : (
<MobileHeader
apps={apps}
appId={chatData.appId}
name={
pane === ChatSidebarPaneEnum.HOME && !isShare
@ -113,15 +107,7 @@ const ChatHeader = ({
);
};
const MobileDrawer = ({
onCloseDrawer,
appId,
apps
}: {
onCloseDrawer: () => 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>(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 && (
<Box px={3} overflow={'auto'} h={'100%'}>
{Array.isArray(apps) &&
apps.map((item) => (
{Array.isArray(myApps) &&
myApps.map((item) => (
<Flex justify={'center'} key={item._id}>
<Flex
py={2.5}
@ -247,13 +234,11 @@ const MobileHeader = ({
showHistory,
name,
avatar,
appId,
apps
appId
}: {
showHistory?: boolean;
avatar: string;
name: string;
apps?: AppListItemType[];
appId: string;
}) => {
const router = useRouter();
@ -290,9 +275,7 @@ const MobileHeader = ({
</Flex>
</Flex>
{isOpenDrawer && !isShareChat && (
<MobileDrawer apps={apps} appId={appId} onCloseDrawer={onCloseDrawer} />
)}
{isOpenDrawer && !isShareChat && <MobileDrawer appId={appId} onCloseDrawer={onCloseDrawer} />}
</>
);
};

View File

@ -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<DateRangeType>({
from: new Date(addDays(new Date(), -6).setHours(0, 0, 0, 0)),

View File

@ -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<ChatFavouriteTagType[]>(tags);

View File

@ -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<Record<string, ChatFavouriteTagType>>(
(acc, tag) => {
acc[tag.id] = { ...tag };

View File

@ -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) => {

View File

@ -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<DateRangeType>({
from: new Date(addDays(new Date(), -6).setHours(0, 0, 0, 0)),

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<AnimatedTextProps> = ({ 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 (
<Flex mt={4} flexDirection={'column'} gap={1} px={4}>
@ -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 (
<MotionBox mt={'auto'} px={3} py={4} layout={false}>
@ -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 (
<MotionFlex
@ -531,7 +532,7 @@ const ChatSlider = ({ apps, activeAppId }: Props) => {
</HStack>
<MyBox flex={'1 0 0'} h={0} overflow={'overlay'} px={4} position={'relative'}>
{apps.map((item) => (
{myApps.map((item) => (
<Flex
key={item._id}
py={2}

View File

@ -1,48 +0,0 @@
import { getRecentlyUsedApps } from '@/web/core/app/api';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useMount } from 'ahooks';
import { useState, useEffect } from 'react';
export const useChat = (appId: string) => {
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
};
};

View File

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

View File

@ -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<UpdateHttpPluginBody>, res: NextApiR
},
{ session }
);
await updateParentFoldersUpdateTime({
parentId: app.parentId,
session
});
await MongoAppVersion.updateOne(
{ appId },
{

View File

@ -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<ListAppBody>): Promise<AppListItemType[]> {
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<ListAppBody>): Promise<AppListItemTy
);
const findAppsQuery = (() => {
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<ListAppBody>): Promise<AppListItemTy
};
})();
const limit = (() => {
if (getRecentlyChat) return 15;
if (searchKey) return 50;
return;
})();

View File

@ -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 },
{

View File

@ -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<GetRecentlyUsedAppsResponseType>
) {
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);

View File

@ -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<AppUpdateBody, AppUpdateQuery>) {
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<AppUpdateBody, AppUpdateQuery>) {
},
{ 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

View File

@ -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<PostPublishAppProps>, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string };
@ -29,6 +30,10 @@ async function handler(req: ApiRequestProps<PostPublishAppProps>, res: NextApiRe
nodes
});
await updateParentFoldersUpdateTime({
parentId: app.parentId
});
if (autoSave) {
await mongoSessionRun(async (session) => {
await MongoAppVersion.updateOne(

View File

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

View File

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

View File

@ -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 */

View File

@ -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 */

View File

@ -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 (
<Flex h={'100%'}>
@ -55,14 +50,14 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
overflow={'hidden'}
transition={'width 0.1s ease-in-out'}
>
<ChatSlider apps={myApps} activeAppId={appId} />
<ChatSlider activeAppId={appId} />
</Box>
)}
{(!datasetCiteData || isPc) && (
<PageContainer flex="1 0 0" w={0} position="relative">
{/* home chat window */}
{pane === ChatSidebarPaneEnum.HOME && <HomeChatWindow myApps={myApps} />}
{pane === ChatSidebarPaneEnum.HOME && <HomeChatWindow />}
{/* favourite apps */}
{pane === ChatSidebarPaneEnum.FAVORITE_APPS && <ChatFavouriteApp />}
@ -71,7 +66,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
{pane === ChatSidebarPaneEnum.TEAM_APPS && <ChatTeamApp />}
{/* recently used apps chat window */}
{pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && <AppChatWindow myApps={myApps} />}
{pane === ChatSidebarPaneEnum.RECENTLY_USED_APPS && <AppChatWindow />}
{/* setting */}
{pane === ChatSidebarPaneEnum.SETTING && <ChatSetting />}
@ -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 (
<ChatSettingContextProvider>
<ChatContextProvider params={chatHistoryProviderParams}>
<ChatItemContextProvider
showRouteToDatasetDetail={isStandalone !== '1'}
showRunningStatus={props.showRunningStatus}
canDownloadSource={props.canDownloadSource}
isShowCite={props.showCite}
isShowFullText={props.showFullText}
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<Chat myApps={myApps} />
</ChatRecordContextProvider>
</ChatItemContextProvider>
</ChatContextProvider>
</ChatSettingContextProvider>
<ChatContextProvider params={chatHistoryProviderParams}>
<ChatItemContextProvider
showRouteToDatasetDetail={isStandalone !== '1'}
showRunningStatus={props.showRunningStatus}
canDownloadSource={props.canDownloadSource}
isShowCite={props.showCite}
isShowFullText={props.showFullText}
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<Chat />
</ChatRecordContextProvider>
</ChatItemContextProvider>
</ChatContextProvider>
);
};
const Render = (props: ChatPageProps) => {
return (
<ChatPageContextProvider appId={props.appId}>
<ChatContent {...props} />
</ChatPageContextProvider>
);
};

View File

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

View File

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

View File

@ -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<AppListItemType[]>('/core/app/list?t=0', data, {
maxQuantity: 1
});
export const getRecentlyUsedApps = () =>
GET<GetRecentlyUsedAppsResponseType>('/core/app/recentlyUsed');
/**
*

View File

@ -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<ChatSettingType | undefined>;
logos: { wideLogoUrl?: string; squareLogoUrl?: string };
// User & apps
isInitedUser: boolean;
userInfo: UserType | null;
myApps: GetRecentlyUsedAppsResponseType;
refreshRecentlyUsed: () => void;
};
export const ChatSettingContext = createContext<ChatSettingContextValue>({
export const ChatPageContext = createContext<ChatPageContextValue>({
pane: ChatSidebarPaneEnum.HOME,
handlePaneChange: () => {},
collapse: defaultCollapseStatus,
@ -37,19 +49,62 @@ export const ChatSettingContext = createContext<ChatSettingContextValue>({
logos: { wideLogoUrl: '', squareLogoUrl: '' },
refreshChatSetting: function (): Promise<ChatSettingType | undefined> {
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<CollapseStatusType>(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 <ChatSettingContext.Provider value={value}>{children}</ChatSettingContext.Provider>;
return <ChatPageContext.Provider value={value}>{children}</ChatPageContext.Provider>;
};

View File

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