perf: tool call check (#4818)

* i18n

* tool call

* fix: mcp create permission;Plugin unauth tip

* fix: mcp create permission;Plugin unauth tip

* fix: Cite modal permission

* remove invalide cite

* perf: prompt

* filter fulltext search

* fix: ts

* fix: ts

* fix: ts
This commit is contained in:
Archer
2025-05-15 15:51:34 +08:00
committed by GitHub
parent a6c80684d1
commit 4e83840c14
48 changed files with 721 additions and 642 deletions

View File

@@ -22,135 +22,184 @@ import { getCollectionSourceData } from '@fastgpt/global/core/dataset/collection
import Markdown from '.';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import { Types } from 'mongoose';
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { useCreation } from 'ahooks';
const A = ({ children, chatAuthData, showAnimation, ...props }: any) => {
export type AProps = {
chatAuthData?: {
appId: string;
chatId: string;
chatItemDataId: string;
} & OutLinkChatAuthProps;
onOpenCiteModal?: (e?: {
collectionId?: string;
sourceId?: string;
sourceName?: string;
datasetId?: string;
quoteId?: string;
}) => void;
};
const EmptyHrefLink = function EmptyHrefLink({ content }: { content: string }) {
const { t } = useTranslation();
return (
<MyTooltip label={t('common:core.chat.markdown.Quick Question')}>
<Button
variant={'whitePrimary'}
size={'xs'}
borderRadius={'md'}
my={1}
onClick={() => eventBus.emit(EventNameEnum.sendQuestion, { text: content })}
>
{content}
</Button>
</MyTooltip>
);
};
const CiteLink = React.memo(function CiteLink({
id,
chatAuthData,
onOpenCiteModal,
showAnimation
}: { id: string; showAnimation?: boolean } & AProps) {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const content = useMemo(() => String(children), [children]);
if (!Types.ObjectId.isValid(id)) {
return <></>;
}
const {
data: datasetCiteData,
loading,
runAsync: getQuoteDataById
} = useRequest2((id: string) => getQuoteData({ id, ...chatAuthData }), {
manual: true
});
const sourceData = useMemo(
() => getCollectionSourceData(datasetCiteData?.collection),
[datasetCiteData?.collection]
);
const icon = useMemo(
() => getSourceNameIcon({ sourceId: sourceData.sourceId, sourceName: sourceData.sourceName }),
[sourceData]
);
return (
<Popover
isLazy
direction="rtl"
placement="bottom"
strategy={'fixed'}
isOpen={isOpen}
onClose={onClose}
onOpen={() => {
onOpen();
if (showAnimation) return;
getQuoteDataById(id);
}}
trigger={'hover'}
gutter={4}
>
<PopoverTrigger>
<Button variant={'unstyled'} minH={0} minW={0} h={'auto'}>
<MyIcon
name={'core/chat/quoteSign'}
w={'1rem'}
color={'primary.700'}
cursor={'pointer'}
/>
</Button>
</PopoverTrigger>
<PopoverContent boxShadow={'lg'} w={'500px'} maxW={'90vw'} py={4}>
<MyBox isLoading={loading || showAnimation}>
<PopoverArrow />
<PopoverBody py={0} px={0} fontSize={'sm'}>
<Flex px={4} pb={1} justifyContent={'space-between'}>
<Box
alignItems={'center'}
fontSize={'xs'}
border={'sm'}
borderRadius={'sm'}
overflow={'hidden'}
display={'inline-flex'}
height={6}
mr={1}
>
<Flex px={1.5}>
<MyIcon name={icon as any} mr={1} flexShrink={0} w={'12px'} />
<Box
className={'textEllipsis'}
wordBreak={'break-all'}
flex={'1 0 0'}
fontSize={'mini'}
color={'myGray.900'}
>
{sourceData.sourceName}
</Box>
</Flex>
</Box>
<Button
variant={'ghost'}
color={'primary.600'}
size={'xs'}
onClick={() => {
onClose();
onOpenCiteModal?.({
quoteId: id,
sourceId: sourceData.sourceId,
sourceName: sourceData.sourceName,
datasetId: datasetCiteData?.collection.datasetId,
collectionId: datasetCiteData?.collection._id
});
}}
>
{t('common:all_quotes')}
</Button>
</Flex>
<Box h={'300px'} overflow={'auto'} px={4}>
<Markdown source={datasetCiteData?.q} />
{datasetCiteData?.a && <Markdown source={datasetCiteData?.a} />}
</Box>
</PopoverBody>
</MyBox>
</PopoverContent>
</Popover>
);
});
const A = ({
children,
chatAuthData,
onOpenCiteModal,
showAnimation,
...props
}: AProps & {
children: any;
showAnimation: boolean;
[key: string]: any;
}) => {
const content = useCreation(() => String(children), [children]);
// empty href link
if (!props.href && typeof children?.[0] === 'string') {
return (
<MyTooltip label={t('common:core.chat.markdown.Quick Question')}>
<Button
variant={'whitePrimary'}
size={'xs'}
borderRadius={'md'}
my={1}
onClick={() => eventBus.emit(EventNameEnum.sendQuestion, { text: content })}
>
{content}
</Button>
</MyTooltip>
);
return <EmptyHrefLink content={content} />;
}
// Cite
if (
(props.href?.startsWith('CITE') || props.href?.startsWith('QUOTE')) &&
typeof children?.[0] === 'string'
typeof content === 'string'
) {
if (!Types.ObjectId.isValid(content)) {
return <></>;
}
const {
data: quoteData,
loading,
runAsync: getQuoteDataById
} = useRequest2((id: string) => getQuoteData({ id, ...chatAuthData }), {
manual: true
});
const sourceData = useMemo(
() => getCollectionSourceData(quoteData?.collection),
[quoteData?.collection]
);
const icon = useMemo(
() => getSourceNameIcon({ sourceId: sourceData.sourceId, sourceName: sourceData.sourceName }),
[sourceData]
);
return (
<Popover
isLazy
direction="rtl"
placement="bottom"
strategy={'fixed'}
isOpen={isOpen}
onClose={onClose}
onOpen={() => {
onOpen();
if (showAnimation) return;
getQuoteDataById(String(children));
}}
trigger={'hover'}
gutter={4}
>
<PopoverTrigger>
<Button variant={'unstyled'} minH={0} minW={0} h={'auto'}>
<MyIcon
name={'core/chat/quoteSign'}
w={'1rem'}
color={'primary.700'}
cursor={'pointer'}
/>
</Button>
</PopoverTrigger>
<PopoverContent boxShadow={'lg'} w={'500px'} maxW={'90vw'} py={4}>
<MyBox isLoading={loading || showAnimation}>
<PopoverArrow />
<PopoverBody py={0} px={0} fontSize={'sm'}>
<Flex px={4} pb={1} justifyContent={'space-between'}>
<Box
alignItems={'center'}
fontSize={'xs'}
border={'sm'}
borderRadius={'sm'}
overflow={'hidden'}
display={'inline-flex'}
height={6}
mr={1}
>
<Flex px={1.5}>
<MyIcon name={icon as any} mr={1} flexShrink={0} w={'12px'} />
<Box
className={'textEllipsis'}
wordBreak={'break-all'}
flex={'1 0 0'}
fontSize={'mini'}
color={'myGray.900'}
>
{sourceData.sourceName}
</Box>
</Flex>
</Box>
<Button
variant={'ghost'}
color={'primary.600'}
size={'xs'}
onClick={() => {
onClose();
eventBus.emit(EventNameEnum.openQuoteReader, {
quoteId: String(children),
sourceId: sourceData.sourceId,
sourceName: sourceData.sourceName,
datasetId: quoteData?.collection.datasetId,
collectionId: quoteData?.collection._id
});
}}
>
{t('common:all_quotes')}
</Button>
</Flex>
<Box h={'300px'} overflow={'auto'} px={4}>
<Markdown source={quoteData?.q} />
{quoteData?.a && <Markdown source={quoteData?.a} />}
</Box>
</PopoverBody>
</MyBox>
</PopoverContent>
</Popover>
<CiteLink
id={content}
chatAuthData={chatAuthData}
onOpenCiteModal={onOpenCiteModal}
showAnimation={showAnimation}
/>
);
}

View File

@@ -13,7 +13,7 @@ import dynamic from 'next/dynamic';
import { Box } from '@chakra-ui/react';
import { CodeClassNameEnum, mdTextFormat } from './utils';
import { useCreation } from 'ahooks';
import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import type { AProps } from './A';
const CodeLight = dynamic(() => import('./codeBlock/CodeLight'), { ssr: false });
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr: false });
@@ -33,12 +33,7 @@ type Props = {
showAnimation?: boolean;
isDisabled?: boolean;
forbidZhFormat?: boolean;
chatAuthData?: {
appId: string;
chatId: string;
chatItemDataId: string;
} & OutLinkChatAuthProps;
};
} & AProps;
const Markdown = (props: Props) => {
const source = props.source || '';
@@ -53,16 +48,25 @@ const MarkdownRender = ({
showAnimation,
isDisabled,
forbidZhFormat,
chatAuthData
chatAuthData,
onOpenCiteModal
}: Props) => {
const components = useCreation(() => {
return {
img: Image,
pre: RewritePre,
code: Code,
a: (props: any) => <A {...props} showAnimation={showAnimation} chatAuthData={chatAuthData} />
a: (props: any) => (
<A
{...props}
showAnimation={showAnimation}
chatAuthData={chatAuthData}
onOpenCiteModal={onOpenCiteModal}
/>
)
};
}, [chatAuthData, showAnimation]);
}, [chatAuthData, onOpenCiteModal, showAnimation]);
const formatSource = useMemo(() => {
if (showAnimation || forbidZhFormat) return source;

View File

@@ -28,10 +28,15 @@ import { isEqual } from 'lodash';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time';
import dayjs from 'dayjs';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
import {
ChatItemContext,
type OnOpenCiteModalProps
} from '@/web/core/chat/context/chatItemContext';
import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
import { useToast } from '@fastgpt/web/hooks/useToast';
import dynamic from 'next/dynamic';
import { useMemoizedFn } from 'ahooks';
const ResponseTags = dynamic(() => import('./ResponseTags'));
const colorMap = {
[ChatStatusEnum.loading]: {
@@ -88,13 +93,15 @@ const AIContentCard = React.memo(function AIContentCard({
dataId,
isLastChild,
isChatting,
questionGuides
questionGuides,
onOpenCiteModal
}: {
dataId: string;
chatValue: ChatItemValueItemType[];
isLastChild: boolean;
isChatting: boolean;
questionGuides: string[];
onOpenCiteModal: (e?: OnOpenCiteModalProps) => void;
}) {
return (
<Flex flexDirection={'column'} gap={2}>
@@ -108,6 +115,7 @@ const AIContentCard = React.memo(function AIContentCard({
value={value}
isLastResponseValue={isLastChild && i === chatValue.length - 1}
isChatting={isChatting}
onOpenCiteModal={onOpenCiteModal}
/>
);
})}
@@ -122,7 +130,6 @@ const ChatItem = (props: Props) => {
const { type, avatar, statusBoxData, children, isLastChild, questionGuides = [], chat } = props;
const { isPc } = useSystem();
const { toast } = useToast();
const styleMap: BoxProps = {
...(type === ChatRoleEnum.Human
@@ -150,7 +157,6 @@ const ChatItem = (props: Props) => {
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
const showNodeStatus = useContextSelector(ChatItemContext, (v) => v.showNodeStatus);
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
const appId = useContextSelector(ChatBoxContext, (v) => v.appId);
const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId);
const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData);
@@ -228,64 +234,48 @@ const ChatItem = (props: Props) => {
return groupedValues;
}, [chat.obj, chat.value, isChatting]);
const handleOpenQuoteReader = useCallback(
({
collectionId,
sourceId,
sourceName,
datasetId,
quoteId
}: {
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
const onOpenCiteModal = useMemoizedFn(
(item?: {
collectionId?: string;
sourceId?: string;
sourceName?: string;
datasetId?: string;
quoteId?: string;
}) => {
if (!setQuoteData) return;
const collectionIdList = collectionId
? [collectionId]
const collectionIdList = item?.collectionId
? [item.collectionId]
: [...new Set(quoteList.map((item) => item.collectionId))];
setQuoteData({
setCiteModalData({
rawSearch: quoteList,
metadata:
collectionId && isShowReadRawSource
item?.collectionId && isShowReadRawSource
? {
appId: appId,
chatId: chatId,
chatItemDataId: chat.dataId,
collectionId: collectionId,
collectionId: item.collectionId,
collectionIdList,
sourceId: sourceId || '',
sourceName: sourceName || '',
datasetId: datasetId || '',
sourceId: item.sourceId || '',
sourceName: item.sourceName || '',
datasetId: item.datasetId || '',
outLinkAuthData,
quoteId
quoteId: item.quoteId
}
: {
appId: appId,
chatId: chatId,
chatItemDataId: chat.dataId,
collectionIdList,
sourceId: sourceId,
sourceName: sourceName,
sourceId: item?.sourceId,
sourceName: item?.sourceName,
outLinkAuthData
}
});
},
[setQuoteData, quoteList, isShowReadRawSource, appId, chatId, chat.dataId, outLinkAuthData]
}
);
useEffect(() => {
if (chat.obj !== ChatRoleEnum.AI) return;
eventBus.on(EventNameEnum.openQuoteReader, handleOpenQuoteReader);
return () => {
eventBus.off(EventNameEnum.openQuoteReader);
};
}, [chat.obj, handleOpenQuoteReader]);
return (
<Box
_hover={{
@@ -362,13 +352,23 @@ const ChatItem = (props: Props) => {
>
{type === ChatRoleEnum.Human && <HumanContentCard chatValue={value} />}
{type === ChatRoleEnum.AI && (
<AIContentCard
chatValue={value}
dataId={chat.dataId}
isLastChild={isLastChild && i === splitAiResponseResults.length - 1}
isChatting={isChatting}
questionGuides={questionGuides}
/>
<>
<AIContentCard
chatValue={value}
dataId={chat.dataId}
isLastChild={isLastChild && i === splitAiResponseResults.length - 1}
isChatting={isChatting}
questionGuides={questionGuides}
onOpenCiteModal={onOpenCiteModal}
/>
{i === splitAiResponseResults.length - 1 && (
<ResponseTags
showTags={!isLastChild || !isChatting}
historyItem={chat}
onOpenCiteModal={onOpenCiteModal}
/>
)}
</>
)}
{/* Example: Response tags. A set of dialogs only needs to be displayed once*/}
{i === splitAiResponseResults.length - 1 && <>{children}</>}

View File

@@ -14,17 +14,24 @@ import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
import { useSize } from 'ahooks';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
const ContextModal = dynamic(() => import('./ContextModal'));
const WholeResponseModal = dynamic(() => import('../../../components/WholeResponseModal'));
const ResponseTags = ({
showTags,
historyItem
historyItem,
onOpenCiteModal
}: {
showTags: boolean;
historyItem: ChatSiteItemType;
onOpenCiteModal: (e?: {
collectionId?: string;
sourceId?: string;
sourceName?: string;
datasetId?: string;
quoteId?: string;
}) => void;
}) => {
const { isPc } = useSystem();
const { t } = useTranslation();
@@ -80,15 +87,6 @@ const ResponseTags = ({
}));
}, [quoteList]);
const openQuoteReader = (item?: {
collectionId?: string;
sourceId?: string;
sourceName?: string;
datasetId?: string;
}) => {
eventBus.emit(EventNameEnum.openQuoteReader, item);
};
const notEmptyTags =
quoteList.length > 0 ||
(llmModuleAccount === 1 && notSharePage) ||
@@ -161,7 +159,7 @@ const ResponseTags = ({
cursor={'pointer'}
onClick={(e) => {
e.stopPropagation();
openQuoteReader(item);
onOpenCiteModal(item);
}}
height={6}
>
@@ -216,7 +214,7 @@ const ResponseTags = ({
cursor={'pointer'}
onClick={(e) => {
e.stopPropagation();
openQuoteReader();
onOpenCiteModal();
}}
>
{t('chat:citations', { num: quoteList.length })}

View File

@@ -68,7 +68,6 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
const ResponseTags = dynamic(() => import('./components/ResponseTags'));
const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
const ReadFeedbackModal = dynamic(() => import('./components/ReadFeedbackModal'));
const SelectMarkCollection = dynamic(() => import('./components/SelectMarkCollection'));
@@ -1014,10 +1013,6 @@ const ChatBox = ({
onReadUserDislike: onReadUserDislike(item)
}}
>
<ResponseTags
showTags={index !== chatRecords.length - 1 || !isChatting}
historyItem={item}
/>
{/* custom feedback */}
{item.customFeedbacks && item.customFeedbacks.length > 0 && (
<Box>
@@ -1072,7 +1067,6 @@ const ChatBox = ({
chatType,
delOneMessage,
externalVariableList?.length,
isChatting,
onAddUserDislike,
onAddUserLike,
onCloseCustomFeedback,

View File

@@ -30,7 +30,7 @@ import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
import { SelectOptionsComponent, FormInputComponent } from './Interactive/InteractiveComponents';
import { extractDeepestInteractive } from '@fastgpt/global/core/workflow/runtime/utils';
import { useContextSelector } from 'use-context-selector';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { type OnOpenCiteModalProps } from '@/web/core/chat/context/chatItemContext';
import { ChatBoxContext } from '../ChatContainer/ChatBox/Provider';
import { useCreation } from 'ahooks';
@@ -90,11 +90,13 @@ const RenderResoningContent = React.memo(function RenderResoningContent({
const RenderText = React.memo(function RenderText({
showAnimation,
text,
chatItemDataId
chatItemDataId,
onOpenCiteModal
}: {
showAnimation: boolean;
text: string;
chatItemDataId: string;
onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void;
}) {
const appId = useContextSelector(ChatBoxContext, (v) => v.appId);
const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId);
@@ -111,7 +113,14 @@ const RenderText = React.memo(function RenderText({
return { appId, chatId, chatItemDataId, ...outLinkAuthData };
}, [appId, chatId, chatItemDataId, outLinkAuthData]);
return <Markdown source={source} showAnimation={showAnimation} chatAuthData={chatAuthData} />;
return (
<Markdown
source={source}
showAnimation={showAnimation}
chatAuthData={chatAuthData}
onOpenCiteModal={onOpenCiteModal}
/>
);
});
const RenderTool = React.memo(
@@ -240,12 +249,14 @@ const AIResponseBox = ({
chatItemDataId,
value,
isLastResponseValue,
isChatting
isChatting,
onOpenCiteModal
}: {
chatItemDataId: string;
value: UserChatItemValueItemType | AIChatItemValueItemType;
isLastResponseValue: boolean;
isChatting: boolean;
onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void;
}) => {
if (value.type === ChatItemValueTypeEnum.text && value.text) {
return (
@@ -253,6 +264,7 @@ const AIResponseBox = ({
chatItemDataId={chatItemDataId}
showAnimation={isChatting && isLastResponseValue}
text={value.text.content}
onOpenCiteModal={onOpenCiteModal}
/>
);
}