feature: V4.11.1 (#5350)

* perf: system toolset & mcp (#5200)

* feat: support system toolset

* fix: type

* fix: system tool config

* chore: mcptool config migrate

* refactor: mcp toolset

* fix: fe type error

* fix: type error

* fix: show version

* chore: support extract tool's secretInputConfig out of inputs

* chore: compatible with old version mcp

* chore: adjust

* deps: update dependency @fastgpt-skd/plugin

* fix: version

* fix: some bug (#5316)

* chore: compatible with old version mcp

* fix: version

* fix: compatible bug

* fix: mcp object params

* fix: type error

* chore: update test cases

* chore: remove log

* fix: toolset node name

* optimize app logs sort (#5310)

* log keys config modal

* multiple select

* api

* fontsize

* code

* chatid

* fix build

* fix

* fix component

* change name

* log keys config

* fix

* delete unused

* fix

* perf: log code

* perf: send auth code modal enter press

* fix log (#5328)

* perf: mcp toolset comment

* perf: log ui

* remove log (#5347)

* doc

* fix: action

* remove log

* fix: Table Optimization (#5319)

* feat: table test: 1

* feat: table test: 2

* feat: table test: 3

* feat: table test: 4

* feat: table test : 5 把maxSize改回chunkSize

* feat: table test : 6 都删了,只看maxSize

* feat: table test : 7 恢复初始,接下来删除标签功能

* feat: table test : 8 删除标签功能

* feat: table test : 9 删除标签功能成功

* feat: table test : 10 继续调试,修改trainingStates

* feat: table test : 11 修改第一步

* feat: table test : 12 修改第二步

* feat: table test : 13 修改了HtmlTable2Md

* feat: table test : 14 修改表头分块规则

* feat: table test : 15 前面表格分的太细了

* feat: table test : 16 改着改着表头又不加了

* feat: table test : 17 用CUSTOM_SPLIT_SIGN不行,重新改

* feat: table test : 18 表头仍然还会多加,但现在分块搞的合理了终于

* feat: table test : 19 还是需要搞好表头问题,先保存一下调试情况

* feat: table test : 20 调试结束,看一下replace有没有问题,没问题就pr

* feat: table test : 21 先把注释删了

* feat: table test : 21 注释replace都改了,下面切main分支看看情况

* feat: table test : 22 修改旧文件

* feat: table test : 23 修改测试文件

* feat: table test : 24 xlsx表格处理

* feat: table test : 25 刚才没保存先com了

* feat: table test : 26 fix

* feat: table test : 27 先com一版调试

* feat: table test : 28 试试放format2csv里

* feat: table test : 29 xlsx解决

* feat: table test : 30 tablesplit解决

* feat: table test : 31

* feat: table test : 32

* perf: table split

* perf: mcp old version compatibility (#5342)

* fix: system-tool secret inputs

* fix: rewrite runtime node i18n for system tool

* perf: mcp old version compatibility

* fix: splitPluginId

* fix: old mcp toolId

* fix: filter secret key

* feat: support system toolset activation

* chore: remove log

* perf: mcp update

* perf: rewrite toolset

* fix:delete variable id (#5335)

* perf: variable update

* fix: multiple select ui

* perf: model config move to plugin

* fix: var conflit

* perf: variable checker

* Avoid empty number

* update doc time

* fix: test

* fix: mcp object

* update count app

* update count app

---------

Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: heheer <zhiyu44@qq.com>
Co-authored-by: colnii <1286949794@qq.com>
Co-authored-by: dreamer6680 <1468683855@qq.com>
This commit is contained in:
Archer
2025-08-01 16:08:20 +08:00
committed by GitHub
parent e0c21a949c
commit e25d7efb5b
143 changed files with 2596 additions and 4177 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app",
"version": "4.11.0",
"version": "4.11.1",
"private": false,
"scripts": {
"dev": "next dev",
@@ -35,10 +35,10 @@ import DndDrag, {
type DraggableProvided,
type DraggableStateSnapshot
} from '@fastgpt/web/components/common/DndDrag';
import { workflowSystemVariables } from '@/web/core/app/utils';
import { getNanoid } from '@fastgpt/global/common/string/tools';
export const defaultVariable: VariableItemType = {
id: getNanoid(6),
key: '',
label: '',
type: VariableInputEnum.input,
@@ -47,12 +47,8 @@ export const defaultVariable: VariableItemType = {
valueType: WorkflowIOValueTypeEnum.string
};
type InputItemType = VariableItemType & {
list: { label: string; value: string }[];
};
export const addVariable = () => {
const newVariable = { ...defaultVariable, key: '', id: '', list: [{ value: '', label: '' }] };
const newVariable = { ...defaultVariable, list: [{ value: '', label: '' }] };
return newVariable;
};
@@ -103,23 +99,47 @@ const VariableEdit = ({
});
}, [variables]);
/*
- New var: random key
- Update var: keep key
*/
const onSubmitSuccess = useCallback(
(data: InputItemType, action: 'confirm' | 'continue') => {
(data: VariableItemType, action: 'confirm' | 'continue') => {
data.label = data?.label?.trim();
if (!data.label) {
return toast({
status: 'warning',
title: t('app:variable_name_required')
});
}
const existingVariable = variables.find(
(item) => item.label === data.label && item.id !== data.id
);
// check if the variable already exists
const existingVariable = variables.find((item) => {
return item.key !== data.key && (data.label === item.label || data.label === item.key);
});
if (existingVariable) {
return toast({
status: 'warning',
title: t('app:variable_repeat')
});
}
// check if the variable is a system variable
if (
workflowSystemVariables.some(
(item) => item.key === data.label || t(item.label) === data.label
)
) {
toast({
status: 'warning',
title: t('common:core.module.variable.key already exists')
title: t('app:systemval_conflict_globalval')
});
return;
}
data.key = data.label;
data.enums = data.list;
if (data.type !== VariableInputEnum.select && data.list) {
delete data.list;
}
if (data.type === VariableInputEnum.custom) {
data.required = false;
@@ -127,16 +147,24 @@ const VariableEdit = ({
data.valueType = inputTypeList.find((item) => item.value === data.type)?.defaultValueType;
}
const onChangeVariable = [...variables];
if (data.id) {
const index = variables.findIndex((item) => item.id === data.id);
onChangeVariable[index] = data;
} else {
onChangeVariable.push({
...data,
id: getNanoid(6)
});
}
const onChangeVariable = (() => {
if (data.key) {
return variables.map((item) => {
if (item.key === data.key) {
return data;
}
return item;
});
}
return [
...variables,
{
...data,
key: getNanoid(8)
}
];
})();
if (action === 'confirm') {
onChange(onChangeVariable);
@@ -226,7 +254,7 @@ const VariableEdit = ({
{({ provided }) => (
<Tbody {...provided.droppableProps} ref={provided.innerRef}>
{formatVariables.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
<Draggable key={item.key} draggableId={item.key} index={index}>
{(provided, snapshot) => (
<TableItem
provided={provided}
@@ -235,7 +263,7 @@ const VariableEdit = ({
reset={reset}
onChange={onChange}
variables={variables}
key={item.id}
key={item.key}
/>
)}
</Draggable>
@@ -370,7 +398,7 @@ const TableItem = ({
<Td fontWeight={'medium'}>
<Flex alignItems={'center'}>
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.400'} mr={1} />
{item.key}
{item.label}
</Flex>
</Td>
<Td>
@@ -385,7 +413,10 @@ const TableItem = ({
onClick={() => {
const formattedItem = {
...item,
list: item.enums?.map((item) => ({ label: item.value, value: item.value })) || []
list:
item.list ||
item.enums?.map((item) => ({ label: item.value, value: item.value })) ||
[]
};
reset(formattedItem);
}}
@@ -393,7 +424,7 @@ const TableItem = ({
<MyIconButton
icon={'delete'}
hoverColor={'red.500'}
onClick={() => onChange(variables.filter((variable) => variable.id !== item.id))}
onClick={() => onChange(variables.filter((variable) => variable.key !== item.key))}
/>
</Flex>
</Td>
@@ -34,13 +34,15 @@ const LabelAndFormRender = ({
placeholder,
inputType,
variablesForm,
showValueType,
...props
}: {
formKey: string;
label: string;
label: string | React.ReactNode;
required?: boolean;
placeholder?: string;
variablesForm: UseFormReturn<any>;
showValueType?: boolean;
} & SpecificProps &
BoxProps) => {
const { control } = variablesForm;
@@ -48,7 +50,7 @@ const LabelAndFormRender = ({
return (
<Box _notLast={{ mb: 4 }}>
<Flex alignItems={'center'} mb={1}>
<FormLabel required={required}>{label}</FormLabel>
{typeof label === 'string' ? <FormLabel required={required}>{label}</FormLabel> : label}
{placeholder && <QuestionTip ml={1} label={placeholder} />}
</Flex>
@@ -11,7 +11,7 @@ import MultipleSelect, {
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import AIModelSelector from '../../../Select/AIModelSelector';
import FileSelector from '../../../Select/FileSelector';
import { useTranslation } from '@fastgpt/web/hooks/useSafeTranslation';
import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation';
const InputRender = (props: InputRenderProps) => {
const {
@@ -28,7 +28,7 @@ const InputRender = (props: InputRenderProps) => {
return <>{customRender(props)}</>;
}
const { t } = useTranslation();
const { t } = useSafeTranslation();
const {
value: selectedValue,
setValue,
@@ -81,6 +81,7 @@ const InputRender = (props: InputRenderProps) => {
return (
<MyNumberInput
{...commonProps}
value={value ?? ''}
min={props.min}
max={props.max}
bg={undefined}
@@ -42,6 +42,7 @@ const SendCodeAuthModal = ({
};
const handleEnterKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation();
if (e.key.toLowerCase() !== 'enter') return;
handleSubmit(onSubmit, onError)();
};
+2 -1
View File
@@ -7,7 +7,8 @@ export type GetAppChatLogsProps = {
dateStart: Date;
dateEnd: Date;
sources?: ChatSourceEnum[];
logTitle?: string;
tmbIds?: string[];
chatSearch?: string;
};
export type GetAppChatLogsParams = PaginationProps<GetAppChatLogsProps>;
@@ -185,7 +185,6 @@ const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => {
<DateRangePicker
defaultDate={filterProps.dateRange}
dateRange={filterProps.dateRange}
position="bottom"
onSuccess={(e) => setFilterProps({ ...filterProps, dateRange: e })}
/>
</Box>
@@ -374,7 +374,6 @@ const ModelDashboard = ({ Tab }: { Tab: React.ReactNode }) => {
<DateRangePicker
defaultDate={filterProps.dateRange}
dateRange={filterProps.dateRange}
position="bottom"
onSuccess={handleDateRangeChange}
/>
</Box>
@@ -15,12 +15,12 @@ import { type UsageItemType } from '@fastgpt/global/support/wallet/usage/type.d'
import dayjs from 'dayjs';
import { UsageSourceMap } from '@fastgpt/global/support/wallet/usage/constants';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation';
const UsageDetail = ({ usage, onClose }: { usage: UsageItemType; onClose: () => void }) => {
const { t } = useTranslation();
const { t } = useSafeTranslation();
const filterBillList = useMemo(
() => usage.list.filter((item) => item && item.moduleName),
[usage.list]
@@ -27,6 +27,7 @@ import { type UsageFilterParams } from './type';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { downloadFetch } from '@/web/common/system/utils';
import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation';
const UsageDetail = dynamic(() => import('./UsageDetail'));
@@ -39,7 +40,7 @@ const UsageTableList = ({
Selectors: React.ReactNode;
filterParams: UsageFilterParams;
}) => {
const { t } = useTranslation();
const { t } = useSafeTranslation();
const { dateRange, selectTmbIds, isSelectAllTmb, usageSources, isSelectAllSource, projectName } =
filterParams;
@@ -147,7 +148,9 @@ const UsageTableList = ({
</Flex>
</Td>
<Td>{t(UsageSourceMap[item.source]?.label as any) || '-'}</Td>
<Td>{t(item.appName as any) || '-'}</Td>
<Td className="textEllipsis" maxW={'400px'} title={t(item.appName as any)}>
{t(item.appName as any) || '-'}
</Td>
<Td>{formatNumber(item.totalPoints) || 0}</Td>
<Td>
<Button
@@ -134,6 +134,7 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
totalRecordsCount={totalRecordsCount}
title={title || ''}
chatModels={chatModels}
chatId={chatId}
/>
<Box flex={1} />
</>
@@ -0,0 +1,156 @@
import { Box, Button, Flex } from '@chakra-ui/react';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import { useTranslation } from 'next-i18next';
import { AppLogKeysEnumMap } from '@fastgpt/global/core/app/logs/constants';
import type {
DraggableProvided,
DraggableStateSnapshot
} from '@fastgpt/web/components/common/DndDrag';
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag';
import MyIcon from '@fastgpt/web/components/common/Icon';
import React from 'react';
import type { AppLogKeysType } from '@fastgpt/global/core/app/logs/type';
const LogKeysConfigPopover = ({
logKeysList,
setLogKeysList
}: {
logKeysList: AppLogKeysType[];
setLogKeysList: (logKeysList: AppLogKeysType[] | undefined) => void;
}) => {
const { t } = useTranslation();
return (
<MyPopover
placement="bottom-end"
w={'300px'}
closeOnBlur={true}
trigger="click"
Trigger={
<Button
size={'md'}
variant={'whiteBase'}
leftIcon={<MyIcon name={'common/setting'} w={'18px'} />}
>
{t('app:logs_key_config')}
</Button>
}
>
{({ onClose }) => {
return (
<Box p={4} overflowY={'auto'} maxH={['300px', '500px']}>
<DndDrag<AppLogKeysType>
onDragEndCb={setLogKeysList}
dataList={logKeysList}
renderClone={(provided, snapshot, rubric) => (
<DragItem
item={logKeysList[rubric.source.index]}
provided={provided}
snapshot={snapshot}
logKeys={logKeysList}
setLogKeys={setLogKeysList}
/>
)}
>
{({ provided }) => (
<Box {...provided.droppableProps} ref={provided.innerRef}>
{logKeysList.map((item, index) => (
<Draggable key={item.key} draggableId={item.key} index={index}>
{(provided, snapshot) => (
<>
<DragItem
item={item}
provided={provided}
snapshot={snapshot}
logKeys={logKeysList}
setLogKeys={setLogKeysList}
/>
{index !== logKeysList.length - 1 && <Box h={'1px'} bg={'myGray.200'} />}
</>
)}
</Draggable>
))}
</Box>
)}
</DndDrag>
</Box>
);
}}
</MyPopover>
);
};
export default LogKeysConfigPopover;
const DragItem = ({
item,
provided,
snapshot,
logKeys,
setLogKeys
}: {
item: AppLogKeysType;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
logKeys: AppLogKeysType[];
setLogKeys: (logKeys: AppLogKeysType[]) => void;
}) => {
const { t } = useTranslation();
return (
<Flex
ref={provided.innerRef}
{...provided.draggableProps}
style={{
...provided.draggableProps.style,
opacity: snapshot.isDragging ? 0.8 : 1
}}
alignItems={'center'}
py={1}
>
<Box {...provided.dragHandleProps}>
<MyIcon
name={'drag'}
p={2}
borderRadius={'md'}
_hover={{ color: 'primary.600' }}
w={'12px'}
color={'myGray.600'}
/>
</Box>
<Box fontSize={'14px'} color={'myGray.900'}>
{t(AppLogKeysEnumMap[item.key])}
</Box>
<Box flex={1} />
{item.enable ? (
<MyIcon
name={'visible'}
borderRadius={'md'}
w={4}
p={1}
cursor={'pointer'}
color={'primary.600'}
_hover={{ bg: 'myGray.50' }}
onClick={() => {
setLogKeys(
logKeys.map((key) => (key.key === item.key ? { ...key, enable: false } : key))
);
}}
/>
) : (
<MyIcon
name={'invisible'}
borderRadius={'md'}
w={4}
p={1}
cursor={'pointer'}
_hover={{ bg: 'myGray.50' }}
onClick={() => {
setLogKeys(
logKeys.map((key) => (key.key === item.key ? { ...key, enable: true } : key))
);
}}
/>
)}
</Flex>
);
};
@@ -0,0 +1,88 @@
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import { Box, Button, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import React from 'react';
import type { updateLogKeysBody } from '@/pages/api/core/app/logs/updateLogKeys';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { updateLogKeys } from '@/web/core/app/api/log';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import type { AppLogKeysType } from '@fastgpt/global/core/app/logs/type';
const SyncLogKeysPopover = ({
logKeys,
setLogKeys,
teamLogKeys,
fetchLogKeys
}: {
logKeys: AppLogKeysType[];
setLogKeys: (logKeys: AppLogKeysType[]) => void;
teamLogKeys: AppLogKeysType[];
fetchLogKeys: () => Promise<AppLogKeysType[]>;
}) => {
const { t } = useTranslation();
const appId = useContextSelector(AppContext, (v) => v.appId);
const { runAsync: updateList, loading: updateLoading } = useRequest2(
async (data: updateLogKeysBody) => {
await updateLogKeys(data);
},
{
manual: true,
onSuccess: async () => {
await fetchLogKeys();
}
}
);
return (
<MyPopover
placement="bottom-end"
w={'300px'}
closeOnBlur={true}
trigger="click"
Trigger={
<Flex alignItems={'center'} cursor={'pointer'}>
<MyIcon name="common/warn" w={4} color={'yellow.500'} />
</Flex>
}
>
{({ onClose }) => {
return (
<Box p={4}>
<Box mb={4}>{t('app:sync_log_keys_popover_text')}</Box>
<Flex justifyContent={'end'} gap={2}>
<Button
variant={'outline'}
size={'sm'}
onClick={() => {
setLogKeys(teamLogKeys);
onClose();
}}
>
{t('app:sync_team_app_log_keys')}
</Button>
<Button
size={'sm'}
isLoading={updateLoading}
onClick={async () => {
await updateList({
appId: appId,
logKeys
});
onClose();
}}
>
{t('app:save_team_app_log_keys')}
</Button>
</Flex>
</Box>
);
}}
</MyPopover>
);
};
export default SyncLogKeysPopover;
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useMemo, useState } from 'react';
import {
Flex,
Box,
@@ -10,12 +10,13 @@ import {
Td,
Tbody,
HStack,
Button
Button,
Input
} from '@chakra-ui/react';
import UserBox from '@fastgpt/web/components/common/UserBox';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { getAppChatLogs } from '@/web/core/app/api';
import { getAppChatLogs } from '@/web/core/app/api/log';
import dayjs from 'dayjs';
import { ChatSourceEnum, ChatSourceMap } from '@fastgpt/global/core/chat/constants';
import { addDays } from 'date-fns';
@@ -27,16 +28,26 @@ import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { cardStyles } from '../constants';
import dynamic from 'next/dynamic';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import MultipleSelect, {
useMultipleSelect
} from '@fastgpt/web/components/common/MySelect/MultipleSelect';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import PopoverConfirm from '@fastgpt/web/components/common/MyPopover/PopoverConfirm';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { downloadFetch } from '@/web/common/system/utils';
import LogKeysConfigPopover from './LogKeysConfigPopover';
import { getLogKeys } from '@/web/core/app/api/log';
import { AppLogKeysEnum } from '@fastgpt/global/core/app/logs/constants';
import { DefaultAppLogKeys } from '@fastgpt/global/core/app/logs/constants';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { getTeamMembers } from '@/web/support/user/team/api';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useLocalStorageState } from 'ahooks';
import type { AppLogKeysType } from '@fastgpt/global/core/app/logs/type';
import type { AppLogsListItemType } from '@/types/app';
import SyncLogKeysPopover from './SyncLogKeysPopover';
import { isEqual } from 'lodash';
const DetailLogsModal = dynamic(() => import('./DetailLogsModal'));
@@ -51,7 +62,105 @@ const Logs = () => {
});
const [detailLogsId, setDetailLogsId] = useState<string>();
const [logTitle, setLogTitle] = useState<string>();
const [tmbInputValue, setTmbInputValue] = useState('');
const [chatSearch, setChatSearch] = useState('');
const getCellRenderMap = (item: AppLogsListItemType) => ({
[AppLogKeysEnum.SOURCE]: (
<Td key={AppLogKeysEnum.SOURCE}>
{/* @ts-ignore */}
{item.sourceName || t(ChatSourceMap[item.source]?.name) || item.source}
</Td>
),
[AppLogKeysEnum.CREATED_TIME]: (
<Td key={AppLogKeysEnum.CREATED_TIME}>{dayjs(item.createTime).format('YYYY/MM/DD HH:mm')}</Td>
),
[AppLogKeysEnum.LAST_CONVERSATION_TIME]: (
<Td key={AppLogKeysEnum.LAST_CONVERSATION_TIME}>
{dayjs(item.updateTime).format('YYYY/MM/DD HH:mm')}
</Td>
),
[AppLogKeysEnum.USER]: (
<Td key={AppLogKeysEnum.USER}>
<Box>
{!!item.outLinkUid ? item.outLinkUid : <UserBox sourceMember={item.sourceMember} />}
</Box>
</Td>
),
[AppLogKeysEnum.TITLE]: (
<Td key={AppLogKeysEnum.TITLE} className="textEllipsis" maxW={'250px'}>
{item.customTitle || item.title}
</Td>
),
[AppLogKeysEnum.SESSION_ID]: (
<Td key={AppLogKeysEnum.SESSION_ID} className="textEllipsis" maxW={'200px'}>
{item.id || '-'}
</Td>
),
[AppLogKeysEnum.MESSAGE_COUNT]: <Td key={AppLogKeysEnum.MESSAGE_COUNT}>{item.messageCount}</Td>,
[AppLogKeysEnum.FEEDBACK]: (
<Td key={AppLogKeysEnum.FEEDBACK} w={'100px'}>
{!!item?.userGoodFeedbackCount && (
<Flex
mb={item?.userGoodFeedbackCount ? 1 : 0}
bg={'green.100'}
color={'green.600'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon mr={1} name={'core/chat/feedback/goodLight'} color={'green.600'} w={'14px'} />
{item.userGoodFeedbackCount}
</Flex>
)}
{!!item?.userBadFeedbackCount && (
<Flex
bg={'#FFF2EC'}
color={'#C96330'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon mr={1} name={'core/chat/feedback/badLight'} color={'#C96330'} w={'14px'} />
{item.userBadFeedbackCount}
</Flex>
)}
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
</Td>
),
[AppLogKeysEnum.CUSTOM_FEEDBACK]: (
<Td key={AppLogKeysEnum.CUSTOM_FEEDBACK}>{item.customFeedbacksCount || '-'}</Td>
),
[AppLogKeysEnum.ANNOTATED_COUNT]: (
<Td key={AppLogKeysEnum.ANNOTATED_COUNT}>{item.markCount}</Td>
),
[AppLogKeysEnum.RESPONSE_TIME]: (
<Td key={AppLogKeysEnum.RESPONSE_TIME}>
{item.averageResponseTime ? `${item.averageResponseTime.toFixed(2)}s` : '-'}
</Td>
),
[AppLogKeysEnum.ERROR_COUNT]: (
<Td key={AppLogKeysEnum.ERROR_COUNT}>{item.errorCount || '-'}</Td>
),
[AppLogKeysEnum.POINTS]: (
<Td key={AppLogKeysEnum.POINTS}>
{item.totalPoints ? `${item.totalPoints.toFixed(2)}` : '-'}
</Td>
)
});
const {
value: selectTmbIds,
setValue: setSelectTmbIds,
isSelectAll: isSelectAllTmb,
setIsSelectAll: setIsSelectAllTmb
} = useMultipleSelect<string>([], true);
const {
value: chatSources,
@@ -75,9 +184,19 @@ const Logs = () => {
dateStart: dateRange.from!,
dateEnd: dateRange.to!,
sources: isSelectAllSource ? undefined : chatSources,
logTitle
tmbIds: isSelectAllTmb ? undefined : selectTmbIds,
chatSearch
}),
[appId, chatSources, dateRange.from, dateRange.to, isSelectAllSource, logTitle]
[
appId,
chatSources,
dateRange.from,
dateRange.to,
isSelectAllSource,
selectTmbIds,
isSelectAllTmb,
chatSearch
]
);
const {
data: logs,
@@ -92,6 +211,66 @@ const Logs = () => {
refreshDeps: [params]
});
const [logKeys = DefaultAppLogKeys, setLogKeys] = useLocalStorageState<AppLogKeysType[]>(
`app_log_keys_${appId}`
);
const { runAsync: fetchLogKeys, data: teamLogKeys = [] } = useRequest2(
async () => {
const res = await getLogKeys({ appId });
const keys = res.logKeys.length > 0 ? res.logKeys : DefaultAppLogKeys;
setLogKeys(keys);
return keys;
},
{
manual: false,
refreshDeps: [appId]
}
);
const HeaderRenderMap = useMemo(
() => ({
[AppLogKeysEnum.SOURCE]: <Th key={AppLogKeysEnum.SOURCE}>{t('app:logs_keys_source')}</Th>,
[AppLogKeysEnum.CREATED_TIME]: (
<Th key={AppLogKeysEnum.CREATED_TIME}>{t('app:logs_keys_createdTime')}</Th>
),
[AppLogKeysEnum.LAST_CONVERSATION_TIME]: (
<Th key={AppLogKeysEnum.LAST_CONVERSATION_TIME}>
{t('app:logs_keys_lastConversationTime')}
</Th>
),
[AppLogKeysEnum.USER]: <Th key={AppLogKeysEnum.USER}>{t('app:logs_chat_user')}</Th>,
[AppLogKeysEnum.TITLE]: <Th key={AppLogKeysEnum.TITLE}>{t('app:logs_title')}</Th>,
[AppLogKeysEnum.SESSION_ID]: (
<Th key={AppLogKeysEnum.SESSION_ID}>{t('app:logs_keys_sessionId')}</Th>
),
[AppLogKeysEnum.MESSAGE_COUNT]: (
<Th key={AppLogKeysEnum.MESSAGE_COUNT}>{t('app:logs_message_total')}</Th>
),
[AppLogKeysEnum.FEEDBACK]: <Th key={AppLogKeysEnum.FEEDBACK}>{t('app:feedback_count')}</Th>,
[AppLogKeysEnum.CUSTOM_FEEDBACK]: (
<Th key={AppLogKeysEnum.CUSTOM_FEEDBACK}>
{t('common:core.app.feedback.Custom feedback')}
</Th>
),
[AppLogKeysEnum.ANNOTATED_COUNT]: (
<Th key={AppLogKeysEnum.ANNOTATED_COUNT}>
<Flex gap={1} alignItems={'center'}>
{t('app:mark_count')}
<QuestionTip label={t('common:core.chat.Mark Description')} />
</Flex>
</Th>
),
[AppLogKeysEnum.RESPONSE_TIME]: (
<Th key={AppLogKeysEnum.RESPONSE_TIME}>{t('app:logs_response_time')}</Th>
),
[AppLogKeysEnum.ERROR_COUNT]: (
<Th key={AppLogKeysEnum.ERROR_COUNT}>{t('app:logs_error_count')}</Th>
),
[AppLogKeysEnum.POINTS]: <Th key={AppLogKeysEnum.POINTS}>{t('app:logs_points')}</Th>
}),
[t]
);
const { runAsync: exportLogs } = useRequest2(
async () => {
await downloadFetch({
@@ -102,7 +281,8 @@ const Logs = () => {
dateStart: dateRange.from || new Date(),
dateEnd: addDays(dateRange.to || new Date(), 1),
sources: isSelectAllSource ? undefined : chatSources,
logTitle,
tmbIds: isSelectAllTmb ? undefined : selectTmbIds,
chatSearch,
title: t('app:logs_export_title'),
sourcesMap: Object.fromEntries(
@@ -117,10 +297,36 @@ const Logs = () => {
});
},
{
refreshDeps: [chatSources, logTitle]
refreshDeps: [chatSources]
}
);
const { data: members, ScrollData: TmbScrollData } = useScrollPagination(getTeamMembers, {
params: { searchKey: tmbInputValue },
refreshDeps: [tmbInputValue]
});
const tmbList = useMemo(
() =>
members.map((item) => ({
label: (
<HStack spacing={1}>
<Avatar src={item.avatar} w={'1.2rem'} rounded={'full'} />
<Box color={'myGray.900'} className="textEllipsis">
{item.memberName}
</Box>
</HStack>
),
value: item.tmbId
})),
[members]
);
const showSyncPopover = useMemo(() => {
const teamLogKeysList = teamLogKeys.filter((item) => item.enable);
const personalLogKeysList = logKeys.filter((item) => item.enable);
return !isEqual(teamLogKeysList, personalLogKeysList);
}, [teamLogKeys, logKeys]);
return (
<Flex
flexDirection={'column'}
@@ -131,49 +337,115 @@ const Logs = () => {
py={[4, 6]}
flex={'1 0 0'}
>
<Flex flexDir={['column', 'row']} alignItems={['flex-start', 'center']} gap={3}>
<Flex alignItems={'center'} gap={2}>
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'}>
{t('app:logs_source')}
</Box>
<Box>
<MultipleSelect<ChatSourceEnum>
list={sourceList}
value={chatSources}
onSelect={setChatSources}
isSelectAll={isSelectAllSource}
setIsSelectAll={setIsSelectAllSource}
itemWrap={false}
height={'32px'}
bg={'myGray.50'}
w={'160px'}
/>
</Box>
<Flex alignItems={'center'} flexWrap={'wrap'} gap={3}>
<Flex>
<MultipleSelect<ChatSourceEnum>
list={sourceList}
value={chatSources}
onSelect={setChatSources}
isSelectAll={isSelectAllSource}
setIsSelectAll={setIsSelectAllSource}
h={9}
w={'226px'}
rounded={'8px'}
tagStyle={{
px: 1,
py: 1,
borderRadius: 'sm',
bg: 'myGray.100',
color: 'myGray.900'
}}
borderColor={'myGray.200'}
formLabel={t('app:logs_source')}
/>
</Flex>
<Flex alignItems={'center'} gap={2}>
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'}>
{t('common:user.Time')}
</Box>
<Flex>
<DateRangePicker
defaultDate={dateRange}
position="bottom"
onSuccess={(date) => {
setDateRange(date);
}}
bg={'white'}
h={9}
w={'240px'}
rounded={'8px'}
borderColor={'myGray.200'}
formLabel={t('app:logs_date')}
_hover={{
borderColor: 'primary.300'
}}
/>
</Flex>
<Flex alignItems={'center'} gap={2}>
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'} whiteSpace={'nowrap'}>
{t('app:logs_title')}
<Flex>
<MultipleSelect<string>
list={tmbList}
value={selectTmbIds}
onSelect={(val) => {
setSelectTmbIds(val as string[]);
}}
ScrollData={TmbScrollData}
isSelectAll={isSelectAllTmb}
setIsSelectAll={setIsSelectAllTmb}
h={9}
w={'226px'}
rounded={'8px'}
formLabel={t('common:member')}
tagStyle={{
px: 1,
borderRadius: 'sm',
bg: 'myGray.100',
w: '76px'
}}
inputValue={tmbInputValue}
setInputValue={setTmbInputValue}
/>
</Flex>
<Flex
w={'226px'}
h={9}
alignItems={'center'}
rounded={'8px'}
border={'1px solid'}
borderColor={'myGray.200'}
_focusWithin={{
borderColor: 'primary.600',
boxShadow: '0 0 0 2.4px rgba(51, 112, 255, 0.15)'
}}
pl={3}
>
<Box rounded={'8px'} bg={'white'} fontSize={'sm'} border={'none'} whiteSpace={'nowrap'}>
{t('common:chat')}
</Box>
<SearchInput
placeholder={t('app:logs_title')}
w={'240px'}
value={logTitle}
onChange={(e) => setLogTitle(e.target.value)}
<Box w={'1px'} h={'12px'} bg={'myGray.200'} mx={2} />
<Input
placeholder={t('app:logs_search_chat')}
value={chatSearch}
onChange={(e) => setChatSearch(e.target.value)}
fontSize={'sm'}
border={'none'}
pl={0}
_focus={{
boxShadow: 'none'
}}
_placeholder={{
fontSize: 'sm'
}}
/>
</Flex>
<Box flex={'1'} />
{showSyncPopover && (
<SyncLogKeysPopover
logKeys={logKeys}
setLogKeys={setLogKeys}
teamLogKeys={teamLogKeys || []}
fetchLogKeys={fetchLogKeys}
/>
)}
<LogKeysConfigPopover
logKeysList={logKeys || DefaultAppLogKeys}
setLogKeysList={setLogKeys}
/>
<PopoverConfirm
Trigger={<Button size={'md'}>{t('common:Export')}</Button>}
showCancel
@@ -186,95 +458,28 @@ const Logs = () => {
<Table variant={'simple'} fontSize={'sm'}>
<Thead>
<Tr>
<Th>{t('common:core.app.logs.Source And Time')}</Th>
<Th>{t('app:logs_chat_user')}</Th>
<Th>{t('app:logs_title')}</Th>
<Th>{t('app:logs_message_total')}</Th>
<Th>{t('app:feedback_count')}</Th>
<Th>{t('common:core.app.feedback.Custom feedback')}</Th>
<Th>
<Flex gap={1} alignItems={'center'}>
{t('app:mark_count')}
<QuestionTip label={t('common:core.chat.Mark Description')} />
</Flex>
</Th>
{(logKeys || DefaultAppLogKeys)
.filter((logKey) => logKey.enable)
.map((logKey) => HeaderRenderMap[logKey.key])}
</Tr>
</Thead>
<Tbody fontSize={'xs'}>
{logs.map((item) => (
<Tr
key={item._id}
_hover={{ bg: 'myWhite.600' }}
cursor={'pointer'}
title={t('common:core.view_chat_detail')}
onClick={() => setDetailLogsId(item.id)}
>
<Td>
{/* @ts-ignore */}
<Box>{item.sourceName || t(ChatSourceMap[item.source]?.name) || item.source}</Box>
<Box color={'myGray.500'}>{dayjs(item.time).format('YYYY/MM/DD HH:mm')}</Box>
</Td>
<Td>
<Box>
{!!item.outLinkUid ? (
item.outLinkUid
) : (
<UserBox sourceMember={item.sourceMember} />
)}
</Box>
</Td>
<Td className="textEllipsis" maxW={'250px'}>
{item.customTitle || item.title}
</Td>
<Td>{item.messageCount}</Td>
<Td w={'100px'}>
{!!item?.userGoodFeedbackCount && (
<Flex
mb={item?.userGoodFeedbackCount ? 1 : 0}
bg={'green.100'}
color={'green.600'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon
mr={1}
name={'core/chat/feedback/goodLight'}
color={'green.600'}
w={'14px'}
/>
{item.userGoodFeedbackCount}
</Flex>
)}
{!!item?.userBadFeedbackCount && (
<Flex
bg={'#FFF2EC'}
color={'#C96330'}
px={3}
py={1}
alignItems={'center'}
justifyContent={'center'}
borderRadius={'md'}
fontWeight={'bold'}
>
<MyIcon
mr={1}
name={'core/chat/feedback/badLight'}
color={'#C96330'}
w={'14px'}
/>
{item.userBadFeedbackCount}
</Flex>
)}
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
</Td>
<Td>{item.customFeedbacksCount || '-'}</Td>
<Td>{item.markCount}</Td>
</Tr>
))}
{logs.map((item) => {
const cellRenderMap = getCellRenderMap(item);
return (
<Tr
key={item._id}
_hover={{ bg: 'myWhite.600' }}
cursor={'pointer'}
title={t('common:core.view_chat_detail')}
onClick={() => setDetailLogsId(item.id)}
>
{(logKeys || DefaultAppLogKeys)
.filter((logKey) => logKey.enable)
.map((logKey) => cellRenderMap[logKey.key])}
</Tr>
);
})}
</Tbody>
</Table>
{logs.length === 0 && !isLoading && <EmptyTip text={t('app:logs_empty')}></EmptyTip>}
@@ -4,20 +4,21 @@ import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import ChatItemContextProvider from '@/web/core/chat/context/chatItemContext';
import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext';
import { Box, Button, Flex } from '@chakra-ui/react';
import { Box, Button, Flex, HStack } from '@chakra-ui/react';
import { cardStyles } from '../constants';
import { useTranslation } from 'react-i18next';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { Controller, useForm } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import Markdown from '@/components/Markdown';
import { postRunMCPTool } from '@/web/core/app/api/plugin';
import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type';
import InputRender from '@/components/core/app/formRender';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { valueTypeToInputType } from '@/components/core/app/formRender/utils';
import { getNodeInputTypeFromSchemaInputType } from '@fastgpt/global/core/app/jsonschema';
import LabelAndFormRender from '@/components/core/app/formRender/LabelAndForm';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import ValueTypeLabel from '../WorkflowComponents/Flow/nodes/render/ValueTypeLabel';
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
const ChatTest = ({
currentTool,
@@ -32,7 +33,8 @@ const ChatTest = ({
const [output, setOutput] = useState<string>('');
const { control, handleSubmit, reset } = useForm();
const form = useForm();
const { handleSubmit, reset } = form;
useEffect(() => {
reset({});
@@ -42,6 +44,20 @@ const ChatTest = ({
const { runAsync: runTool, loading: isRunning } = useRequest2(
async (data: Record<string, any>) => {
if (!currentTool) return;
// Format type
Object.entries(currentTool?.inputSchema.properties || {}).forEach(
([paramName, paramInfo]) => {
const valueType = getNodeInputTypeFromSchemaInputType({
type: paramInfo.type,
arrayItems: paramInfo.items
});
if (data[paramName] !== undefined) {
data[paramName] = valueTypeFormat(data[paramName], valueType);
}
}
);
return await postRunMCPTool({
params: data,
url,
@@ -89,48 +105,35 @@ const ChatTest = ({
</Box>
<Box border={'1px solid'} borderColor={'myGray.200'} borderRadius={'8px'} p={3}>
{Object.entries(currentTool?.inputSchema.properties || {}).map(
([paramName, paramInfo]) => (
<Controller
key={paramName}
control={control}
name={paramName}
rules={{
validate: (value) => {
if (!currentTool?.inputSchema.required?.includes(paramName)) return true;
return !!value;
}
}}
render={({ field: { onChange, value }, fieldState: { error } }) => {
const inputType = valueTypeToInputType(
getNodeInputTypeFromSchemaInputType({ type: paramInfo.type })
);
([paramName, paramInfo]) => {
const inputType = valueTypeToInputType(
getNodeInputTypeFromSchemaInputType({ type: paramInfo.type })
);
const required = currentTool?.inputSchema.required?.includes(paramName);
return (
<Box _notLast={{ mb: 4 }}>
<Flex alignItems="center" mb={1}>
{currentTool?.inputSchema.required?.includes(paramName) && (
<Box mr={1} color="red.500">
*
</Box>
)}
<FormLabel fontSize="14px" fontWeight={'normal'} color="myGray.900">
{paramName}
<QuestionTip label={paramInfo.description} ml={1} />
</FormLabel>
</Flex>
<InputRender
inputType={inputType}
value={value}
onChange={onChange}
placeholder={paramInfo.description}
isInvalid={!!error}
return (
<LabelAndFormRender
label={
<HStack spacing={0} mr={2}>
<FormLabel required={required}>{paramName}</FormLabel>
<ValueTypeLabel
valueType={getNodeInputTypeFromSchemaInputType({
type: paramInfo.type,
arrayItems: paramInfo.items
})}
h={'auto'}
/>
</Box>
);
}}
/>
)
</HStack>
}
required={required}
key={paramName}
inputType={inputType}
formKey={paramName}
variablesForm={form}
placeholder={paramInfo.description}
/>
);
}
)}
</Box>
</>
@@ -6,7 +6,6 @@ import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type MCPToolSetData } from '@/pageComponents/dashboard/apps/MCPToolsEditModal';
import { type StoreSecretValueType } from '@fastgpt/global/common/secret/type';
const MCPTools = () => {
@@ -15,13 +14,15 @@ const MCPTools = () => {
const toolSetNode = appDetail.modules.find(
(item) => item.flowNodeType === FlowNodeTypeEnum.toolSet
);
return toolSetNode?.inputs[0].value as MCPToolSetData;
return toolSetNode?.toolConfig?.mcpToolSet ?? toolSetNode?.inputs[0].value;
}, [appDetail.modules]);
const [url, setUrl] = useState(toolSetData?.url || '');
const [toolList, setToolList] = useState<McpToolConfigType[]>(toolSetData?.toolList || []);
const [headerSecret, setHeaderSecret] = useState<StoreSecretValueType>(toolSetData?.headerSecret);
const [currentTool, setCurrentTool] = useState<McpToolConfigType>(toolSetData?.toolList[0]);
const [headerSecret, setHeaderSecret] = useState<StoreSecretValueType>(
toolSetData?.headerSecret ?? {}
);
const [currentTool, setCurrentTool] = useState<McpToolConfigType>(toolSetData.toolList[0]);
return (
<Flex h={'100%'} flexDirection={'column'} px={[3, 0]} pr={[3, 3]}>
@@ -22,18 +22,21 @@ import { useChatStore } from '@/web/core/chat/context/useChatStore';
import MyBox from '@fastgpt/web/components/common/MyBox';
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import VariablePopover from '@/components/core/chat/ChatContainer/ChatBox/components/VariablePopover';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
type Props = {
isOpen: boolean;
nodes?: StoreNodeItemType[];
edges?: StoreEdgeItemType[];
onClose: () => void;
chatId: string;
};
const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
const ChatTest = ({ isOpen, nodes = [], edges = [], onClose, chatId }: Props) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const isPlugin = appDetail.type === AppTypeEnum.plugin;
const { copyData } = useCopyData();
const { restartChat, ChatContainer, loading } = useChatTest({
nodes,
@@ -118,7 +121,16 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
>
<Flex fontSize={'16px'} fontWeight={'bold'} alignItems={'center'} mr={3}>
<MyIcon name={'common/paused'} w={'14px'} mr={2.5} />
{t('common:core.chat.Run test')}
<MyTooltip label={chatId ? t('common:chat_chatId', { chatId }) : ''}>
<Box
cursor={'pointer'}
onClick={() => {
copyData(chatId);
}}
>
{t('common:core.chat.Run test')}
</Box>
</MyTooltip>
</Flex>
{!isVariableVisible && <VariablePopover showExternalVariables />}
<Box flex={1} />
@@ -199,7 +211,7 @@ const Render = (Props: Props) => {
showNodeStatus
>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest {...Props} />
<ChatTest {...Props} chatId={chatId} />
</ChatRecordContextProvider>
</ChatItemContextProvider>
);
@@ -25,7 +25,7 @@ export type NodeTemplateListHeaderProps = {
parentId?: string;
type?: TemplateTypeEnum;
searchVal?: string;
}) => Promise<void>;
}) => Promise<any>;
onUpdateParentId: (parentId: string) => void;
};
@@ -56,7 +56,6 @@ export type TemplateListProps = {
const NodeTemplateListItem = ({
template,
templateType,
handleAddNode,
isPopover,
onUpdateParentId
@@ -128,11 +127,6 @@ const NodeTemplateListItem = ({
});
}}
onClick={() => {
if (template.isFolder && template.flowNodeType !== FlowNodeTypeEnum.toolSet) {
onUpdateParentId(template.id);
return;
}
const position =
isPopover && handleParams
? handleParams.addNodePosition
@@ -219,10 +213,6 @@ const NodeTemplateList = ({
template: NodeTemplateListItemType;
position: { x: number; y: number };
}) => {
if (template.isFolder && template.flowNodeType !== FlowNodeTypeEnum.toolSet) {
return;
}
try {
const templateNode = await (async () => {
try {
@@ -69,7 +69,7 @@ export const useNodeTemplates = () => {
const {
data: teamAndSystemApps,
loading: templatesIsLoading,
runAsync
runAsync: loadNodeTemplates
} = useRequest2(
async ({
parentId = '',
@@ -81,12 +81,13 @@ export const useNodeTemplates = () => {
searchVal?: string;
}) => {
if (type === TemplateTypeEnum.teamPlugin) {
// app, workflow-plugin, mcp
return getTeamPlugTemplates({
parentId,
searchKey: searchVal
parentId
}).then((res) => res.filter((app) => app.id !== appId));
}
if (type === TemplateTypeEnum.systemPlugin) {
// systemTool
return getSystemPlugTemplates({
searchKey: searchVal,
parentId
@@ -102,13 +103,6 @@ export const useNodeTemplates = () => {
}
);
const loadNodeTemplates = useCallback(
async (params: { parentId?: ParentIdType; type?: TemplateTypeEnum; searchVal?: string }) => {
await runAsync(params);
},
[runAsync]
);
const onUpdateParentId = useCallback(
(parentId: ParentIdType) => {
loadNodeTemplates({
@@ -20,7 +20,14 @@ export const useWorkflowUtils = () => {
}) => {
const nodeLength = nodeList.filter((node) => {
if (node.flowNodeType === flowNodeType) {
if (node.flowNodeType === FlowNodeTypeEnum.pluginModule) {
if (
[
FlowNodeTypeEnum.pluginModule,
FlowNodeTypeEnum.appModule,
FlowNodeTypeEnum.toolSet,
FlowNodeTypeEnum.tool
].includes(flowNodeType)
) {
return node.pluginId === pluginId;
} else {
return true;
@@ -5,18 +5,17 @@ import NodeCard from './render/NodeCard';
import IOTitle from '../components/IOTitle';
import Container from '../components/Container';
import { useTranslation } from 'react-i18next';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { Box, Flex } from '@chakra-ui/react';
const NodeToolSet = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { t } = useTranslation();
const { inputs } = data;
const toolList: McpToolConfigType[] = inputs[0]?.value?.toolList;
const { toolConfig } = data;
const toolList = toolConfig?.mcpToolSet?.toolList ?? toolConfig?.systemToolSet?.toolList ?? [];
return (
<NodeCard minW={'350px'} selected={selected} {...data}>
<Container>
<IOTitle text={t('app:MCP_tools_list')} />
<IOTitle text={t('app:MCP_tools_list')} {...data} catchError={undefined} />
<Box maxH={'500px'} overflowY={'auto'} className="nowheel">
{toolList?.map((tool, index) => (
<Flex
@@ -115,10 +115,12 @@ const NodeCard = (props: Props) => {
}, [nodeList, nodeId]);
const isAppNode = node && AppNodeFlowNodeTypeMap[node?.flowNodeType];
const showVersion = useMemo(() => {
// 1. Team app/System commercial plugin
// 1. MCP tool set do not have version
if (isAppNode && node.toolConfig?.mcpToolSet) return false;
// 2. Team app/System commercial plugin
if (isAppNode && node?.pluginId && !node?.pluginData?.error) return true;
// 2. System tool
if (isAppNode && node.toolConfig) return true;
// 3. System tool
if (isAppNode && node?.toolConfig?.systemTool) return true;
return false;
}, [isAppNode, node]);
@@ -1,4 +1,5 @@
import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant';
import type { BoxProps } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import type { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
@@ -7,11 +8,12 @@ import { useTranslation } from 'next-i18next';
const ValueTypeLabel = ({
valueType,
valueDesc
valueDesc,
...props
}: {
valueType?: WorkflowIOValueTypeEnum;
valueDesc?: string;
}) => {
} & BoxProps) => {
const valueTypeData = valueType ? FlowValueTypeMap[valueType] : undefined;
const { t } = useTranslation();
const label = valueTypeData?.label || '';
@@ -30,6 +32,7 @@ const ValueTypeLabel = ({
display={'flex'}
alignItems={'center'}
fontSize={'11px'}
{...props}
>
{t(label as any)}
</Box>
@@ -59,6 +59,7 @@ import WorkflowStatusContextProvider from './workflowStatusContext';
import { type ChatItemType, type UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { type WorkflowInteractiveResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
/*
Context
@@ -413,6 +414,8 @@ const WorkflowContextProvider = ({
const { t } = useTranslation();
const { toast } = useToast();
const { chatId } = useChatStore();
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const setAppDetail = useContextSelector(AppContext, (v) => v.setAppDetail);
const appId = appDetail._id;
@@ -1091,7 +1094,7 @@ const WorkflowContextProvider = ({
return (
<WorkflowContext.Provider value={value}>
{children}
<ChatTest isOpen={isOpenTest} {...workflowTestData} onClose={onCloseTest} />
<ChatTest isOpen={isOpenTest} {...workflowTestData} onClose={onCloseTest} chatId={chatId} />
</WorkflowContext.Provider>
);
};
@@ -186,7 +186,11 @@ const AppContextProvider = ({ children }: { children: ReactNode }) => {
return delAppById(appDetail._id);
},
{
onSuccess() {
onSuccess(data) {
data.forEach((appId) => {
localStorage.removeItem(`app_log_keys_${appId}`);
});
router.replace(`/dashboard/apps`);
},
successToast: t('common:delete_success'),
@@ -23,6 +23,7 @@ import { getMyApps } from '@/web/core/app/api';
import SelectOneResource from '@/components/common/folder/SelectOneResource';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import VariablePopover from '@/components/core/chat/ChatContainer/ChatBox/components/VariablePopover';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
const ChatHeader = ({
history,
@@ -59,6 +60,7 @@ const ChatHeader = ({
totalRecordsCount={totalRecordsCount}
title={chatData.title || t('common:core.chat.New Chat')}
chatModels={chatData.app.chatModels}
chatId={chatData.chatId || ''}
/>
<Box flex={1} />
</>
@@ -261,19 +263,33 @@ const MobileHeader = ({
export const PcHeader = ({
title,
chatModels,
totalRecordsCount
totalRecordsCount,
chatId
}: {
title: string;
chatModels?: string[];
totalRecordsCount: number;
chatId: string;
}) => {
const { t } = useTranslation();
const { copyData } = useCopyData();
return (
<>
<Box mr={3} maxW={'200px'} className="textEllipsis" color={'myGray.1000'}>
{title}
</Box>
<MyTooltip label={chatId ? t('common:chat_chatId', { chatId }) : ''}>
<Box
mr={3}
maxW={'200px'}
className="textEllipsis"
color={'myGray.1000'}
cursor={'pointer'}
onClick={() => {
copyData(chatId);
}}
>
{title}
</Box>
</MyTooltip>
<MyTag>
<MyIcon name={'history'} w={'14px'} />
<Box ml={1}>
@@ -98,7 +98,10 @@ const ListItem = () => {
return delAppById(id);
},
{
onSuccess() {
onSuccess(data) {
data.forEach((appId) => {
localStorage.removeItem(`app_log_keys_${appId}`);
});
loadMyApps();
},
successToast: t('common:delete_success'),
@@ -50,7 +50,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
});
const username = watch('username');
const { SendCodeBox } = useSendCode({ type: 'register' });
const { SendCodeBox, openCodeAuthModal } = useSendCode({ type: 'register' });
const { runAsync: onclickRegister, loading: requesting } = useRequest2(
async ({ username, password, code }: RegisterType) => {
@@ -122,7 +122,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
<Box
mt={9}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !requesting) {
if (!openCodeAuthModal && e.key === 'Enter' && !e.shiftKey && !requesting) {
handleSubmit(onclickRegister, onSubmitErr)();
}
}}
@@ -242,7 +242,7 @@ export const LoginContainer = ({
/>
)}
<Box position="relative" w="full" flex={'1 0 0'}>
<Flex flexDirection={'column'} w="full" flex={'1 0 0'}>
{/* main content area */}
<Box w={['100%', '380px']} flex={'1 0 0'}>
{pageType && DynamicComponent ? DynamicComponent : <Loading fixed={false} />}
@@ -270,7 +270,7 @@ export const LoginContainer = ({
{t('common:support.user.login.can_not_login')}
</Box>
)}
</Box>
</Flex>
<CookiesModal />
<ChineseRedirectModal />
@@ -111,12 +111,7 @@ const UsageTable = () => {
<Box fontSize={'mini'} fontWeight={'medium'} color={'myGray.900'}>
{t('common:user.Time')}
</Box>
<DateRangePicker
defaultDate={dateRange}
dateRange={dateRange}
position="bottom"
onSuccess={setDateRange}
/>
<DateRangePicker defaultDate={dateRange} dateRange={dateRange} onSuccess={setDateRange} />
{/* {usageTab === UsageTabEnum.dashboard && (
<MySelect<UnitType>
bg={'myGray.50'}
@@ -146,7 +141,7 @@ const UsageTable = () => {
setSelectTmbIds(val as string[]);
}}
itemWrap={false}
height={'32px'}
h={'32px'}
bg={'myGray.50'}
w={'160px'}
ScrollData={ScrollData}
@@ -3,8 +3,7 @@ import { NextAPI } from '@/service/middleware/entry';
import { MongoSystemModel } from '@fastgpt/service/core/ai/config/schema';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
import { findModelFromAlldata } from '@fastgpt/service/core/ai/model';
import { updateFastGPTConfigBuffer } from '@fastgpt/service/common/system/config/controller';
import { loadSystemModels, updatedReloadSystemModel } from '@fastgpt/service/core/ai/config/utils';
import { updatedReloadSystemModel } from '@fastgpt/service/core/ai/config/utils';
export type deleteQuery = {
model: string;
@@ -2,8 +2,7 @@ import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/nex
import { NextAPI } from '@/service/middleware/entry';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoSystemModel } from '@fastgpt/service/core/ai/config/schema';
import { loadSystemModels, updatedReloadSystemModel } from '@fastgpt/service/core/ai/config/utils';
import { updateFastGPTConfigBuffer } from '@fastgpt/service/common/system/config/controller';
import { updatedReloadSystemModel } from '@fastgpt/service/core/ai/config/utils';
import type { ModelTypeEnum } from '@fastgpt/global/core/ai/model';
import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth';
+4 -2
View File
@@ -8,7 +8,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';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
async function handler(req: NextApiRequest, res: NextApiResponse<string[]>) {
const { appId } = req.query as { appId: string };
if (!appId) {
@@ -23,7 +23,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
per: OwnerPermissionVal
});
await onDelOneApp({
const deletedAppIds = await onDelOneApp({
teamId,
appId
});
@@ -42,6 +42,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
// Tracks
pushTrack.countAppNodes({ teamId, tmbId, uid: userId, appId });
return deletedAppIds;
}
export default NextAPI(handler);
@@ -4,6 +4,7 @@ import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { rewriteAppWorkflowToDetail } from '@fastgpt/service/core/app/utils';
import { getLocale } from '@fastgpt/service/common/middle/i18n';
/* 获取应用详情 */
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -24,7 +25,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
nodes: app.modules,
teamId,
ownerTmbId: app.tmbId,
isRoot
isRoot,
lang: getLocale(req)
});
if (!app.permission.hasWritePer) {
@@ -37,8 +37,8 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
dateStart = addDays(new Date(), -7),
dateEnd = new Date(),
sources,
logTitle,
tmbIds,
chatSearch,
title,
sourcesMap
} = req.body;
@@ -80,10 +80,12 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
$lte: new Date(dateEnd)
},
...(sources && { source: { $in: sources } }),
...(logTitle && {
...(tmbIds && { tmbId: { $in: tmbIds } }),
...(chatSearch && {
$or: [
{ title: { $regex: new RegExp(`${replaceRegChars(logTitle)}`, 'i') } },
{ customTitle: { $regex: new RegExp(`${replaceRegChars(logTitle)}`, 'i') } }
{ chatId: { $regex: new RegExp(`${replaceRegChars(chatSearch)}`, 'i') } },
{ title: { $regex: new RegExp(`${replaceRegChars(chatSearch)}`, 'i') } },
{ customTitle: { $regex: new RegExp(`${replaceRegChars(chatSearch)}`, 'i') } }
]
})
};
@@ -12,10 +12,10 @@ import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import { type PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { addSourceMember } from '@fastgpt/service/support/user/utils';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
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 { replaceRegChars } from '@fastgpt/global/common/string/tools';
async function handler(
req: NextApiRequest,
@@ -26,7 +26,8 @@ async function handler(
dateStart = addDays(new Date(), -7),
dateEnd = new Date(),
sources,
logTitle
tmbIds,
chatSearch
} = req.body as GetAppChatLogsParams;
const { pageSize = 20, offset } = parsePaginationRequest(req);
@@ -51,10 +52,12 @@ async function handler(
$lte: new Date(dateEnd)
},
...(sources && { source: { $in: sources } }),
...(logTitle && {
...(tmbIds && { tmbId: { $in: tmbIds.map((item) => new Types.ObjectId(item)) } }),
...(chatSearch && {
$or: [
{ title: { $regex: new RegExp(`${replaceRegChars(logTitle)}`, 'i') } },
{ customTitle: { $regex: new RegExp(`${replaceRegChars(logTitle)}`, 'i') } }
{ chatId: { $regex: new RegExp(`${replaceRegChars(chatSearch)}`, 'i') } },
{ title: { $regex: new RegExp(`${replaceRegChars(chatSearch)}`, 'i') } },
{ customTitle: { $regex: new RegExp(`${replaceRegChars(chatSearch)}`, 'i') } }
]
})
};
@@ -123,6 +126,49 @@ async function handler(
0
]
}
},
totalResponseTime: {
$sum: {
$cond: [{ $eq: ['$obj', 'AI'] }, { $ifNull: ['$durationSeconds', 0] }, 0]
}
},
aiMessageCount: {
$sum: {
$cond: [{ $eq: ['$obj', 'AI'] }, 1, 0]
}
},
errorCount: {
$sum: {
$cond: [
{
$gt: [
{
$size: {
$filter: {
input: { $ifNull: ['$responseData', []] },
as: 'item',
cond: { $ne: [{ $ifNull: ['$$item.errorText', null] }, null] }
}
}
},
0
]
},
1,
0
]
}
},
totalPoints: {
$sum: {
$reduce: {
input: { $ifNull: ['$responseData', []] },
initialValue: 0,
in: {
$add: ['$$value', { $ifNull: ['$$this.totalPoints', 0] }]
}
}
}
}
}
}
@@ -142,7 +188,23 @@ async function handler(
customFeedbacksCount: {
$ifNull: [{ $arrayElemAt: ['$chatItemsData.customFeedback', 0] }, 0]
},
markCount: { $ifNull: [{ $arrayElemAt: ['$chatItemsData.adminMark', 0] }, 0] }
markCount: { $ifNull: [{ $arrayElemAt: ['$chatItemsData.adminMark', 0] }, 0] },
averageResponseTime: {
$cond: [
{
$gt: [{ $ifNull: [{ $arrayElemAt: ['$chatItemsData.aiMessageCount', 0] }, 0] }, 0]
},
{
$divide: [
{ $ifNull: [{ $arrayElemAt: ['$chatItemsData.totalResponseTime', 0] }, 0] },
{ $ifNull: [{ $arrayElemAt: ['$chatItemsData.aiMessageCount', 0] }, 1] }
]
},
0
]
},
errorCount: { $ifNull: [{ $arrayElemAt: ['$chatItemsData.errorCount', 0] }, 0] },
totalPoints: { $ifNull: [{ $arrayElemAt: ['$chatItemsData.totalPoints', 0] }, 0] }
}
},
{
@@ -153,12 +215,16 @@ async function handler(
customTitle: 1,
source: 1,
sourceName: 1,
time: '$updateTime',
updateTime: 1,
createTime: 1,
messageCount: 1,
userGoodFeedbackCount: 1,
userBadFeedbackCount: 1,
customFeedbacksCount: 1,
markCount: 1,
averageResponseTime: 1,
errorCount: 1,
totalPoints: 1,
outLinkUid: 1,
tmbId: 1
}
@@ -0,0 +1,36 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { MongoAppLogKeys } from '@fastgpt/service/core/app/logs/logkeysSchema';
import type { AppLogKeysSchemaType } from '@fastgpt/global/core/app/logs/type';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export type getLogKeysQuery = {
appId: string;
};
export type getLogKeysBody = {};
export type getLogKeysResponse = {
logKeys: AppLogKeysSchemaType['logKeys'];
};
async function handler(
req: ApiRequestProps<getLogKeysBody, getLogKeysQuery>,
res: ApiResponseType<any>
): Promise<getLogKeysResponse> {
const { appId } = req.query;
const { teamId } = await authApp({
req,
authToken: true,
appId,
per: WritePermissionVal
});
const result = await MongoAppLogKeys.findOne({ teamId, appId });
return { logKeys: result?.logKeys || [] };
}
export default NextAPI(handler);
@@ -0,0 +1,34 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { MongoAppLogKeys } from '@fastgpt/service/core/app/logs/logkeysSchema';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import type { AppLogKeysType } from '@fastgpt/global/core/app/logs/type';
export type updateLogKeysQuery = {};
export type updateLogKeysBody = {
appId: string;
logKeys: AppLogKeysType[];
};
export type updateLogKeysResponse = {};
async function handler(
req: ApiRequestProps<updateLogKeysBody, updateLogKeysQuery>,
res: ApiResponseType<any>
): Promise<updateLogKeysResponse> {
const { appId, logKeys } = req.body;
const { teamId } = await authApp({
req,
authToken: true,
appId,
per: WritePermissionVal
});
await MongoAppLogKeys.findOneAndUpdate({ teamId, appId }, { logKeys }, { upsert: true });
return {};
}
export default NextAPI(handler);
@@ -7,10 +7,7 @@ import { type CreateAppBody, onCreateApp } from '../create';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import {
getMCPToolRuntimeNode,
getMCPToolSetRuntimeNode
} from '@fastgpt/global/core/app/mcpTools/utils';
import { getMCPToolSetRuntimeNode } from '@fastgpt/global/core/app/mcpTools/utils';
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
import { checkTeamAppLimit } from '@fastgpt/service/support/permission/teamLimit';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
@@ -58,32 +55,13 @@ async function handler(
toolList,
name,
avatar,
headerSecret: formatedHeaderAuth
headerSecret: formatedHeaderAuth,
toolId: ''
})
],
session
});
for (const tool of toolList) {
await onCreateApp({
name: tool.name,
avatar,
parentId: mcpToolsId,
teamId,
tmbId,
type: AppTypeEnum.tool,
intro: tool.description,
modules: [
getMCPToolRuntimeNode({
tool,
url,
headerSecret: formatedHeaderAuth
})
],
session
});
}
return mcpToolsId;
});
@@ -0,0 +1,33 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import type { McpToolConfigType } from '@fastgpt/global/core/app/type';
import { UserError } from '@fastgpt/global/common/error/utils';
import { getMCPChildren } from '@fastgpt/service/core/app/mcp';
export type McpGetChildrenmQuery = {
id: string;
};
export type McpGetChildrenmBody = {};
export type McpGetChildrenmResponse = (McpToolConfigType & {
id: string;
avatar: string;
})[];
async function handler(
req: ApiRequestProps<McpGetChildrenmBody, McpGetChildrenmQuery>,
_res: ApiResponseType<any>
): Promise<McpGetChildrenmResponse> {
const { id } = req.query;
const app = await MongoApp.findOne({ _id: id }).lean();
if (!app) return Promise.reject(new UserError('No Mcp Toolset found'));
if (app.type !== AppTypeEnum.toolSet)
return Promise.reject(new UserError('the parent is not a mcp toolset'));
return getMCPChildren(app);
}
export default NextAPI(handler);
@@ -1,22 +1,12 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { type AppDetailType, type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { type McpToolConfigType } from '@fastgpt/global/core/app/type';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { isEqual } from 'lodash';
import { type ClientSession } from 'mongoose';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { onDelOneApp } from '@fastgpt/service/core/app/controller';
import { onCreateApp } from '../create';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import {
getMCPToolRuntimeNode,
getMCPToolSetRuntimeNode
} from '@fastgpt/global/core/app/mcpTools/utils';
import { type MCPToolSetData } from '@/pageComponents/dashboard/apps/MCPToolsEditModal';
import { getMCPToolSetRuntimeNode } from '@fastgpt/global/core/app/mcpTools/utils';
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';
@@ -39,149 +29,40 @@ async function handler(
const { appId, url, toolList, headerSecret } = req.body;
const { app } = await authApp({ req, authToken: true, appId, per: ManagePermissionVal });
const toolSetNode = app.modules.find((item) => item.flowNodeType === FlowNodeTypeEnum.toolSet);
const toolSetData = toolSetNode?.inputs[0].value as MCPToolSetData;
const formatedHeaderAuth = storeSecretValue(headerSecret);
// create tool set node
const toolSetRuntimeNode = getMCPToolSetRuntimeNode({
url,
toolList,
headerSecret: formatedHeaderAuth,
name: app.name,
avatar: app.avatar,
toolId: ''
});
await mongoSessionRun(async (session) => {
if (
!isEqual(toolSetData, {
url,
toolList
})
) {
await updateMCPChildrenTool({
parentApp: app,
toolSetData: {
url,
toolList,
headerSecret: formatedHeaderAuth
},
session
});
}
// create tool set node
const toolSetRuntimeNode = getMCPToolSetRuntimeNode({
url,
toolList,
headerSecret: formatedHeaderAuth,
name: app.name,
avatar: app.avatar
});
// update app and app version
await Promise.all([
MongoApp.updateOne(
{ _id: appId },
{
modules: [toolSetRuntimeNode],
updateTime: new Date()
},
{ session }
),
MongoAppVersion.updateOne(
{ appId },
{
$set: {
nodes: [toolSetRuntimeNode]
}
},
{ session }
)
]);
await MongoApp.updateOne(
{ _id: appId },
{
modules: [toolSetRuntimeNode],
updateTime: new Date()
},
{ session }
);
await MongoAppVersion.updateOne(
{ appId },
{
$set: {
nodes: [toolSetRuntimeNode]
}
},
{ session }
);
});
return {};
}
export default NextAPI(handler);
const updateMCPChildrenTool = async ({
parentApp,
toolSetData,
session
}: {
parentApp: AppDetailType;
toolSetData: {
url: string;
toolList: McpToolConfigType[];
headerSecret: StoreSecretValueType;
};
session: ClientSession;
}) => {
const { teamId, tmbId } = parentApp;
const dbTools = await MongoApp.find({
parentId: parentApp._id,
teamId
});
// 删除 DB 里有,新的工具列表里没有的工具
for await (const tool of dbTools) {
if (!toolSetData.toolList.find((t) => t.name === tool.name)) {
await onDelOneApp({
teamId,
appId: tool._id,
session
});
}
}
// 创建 DB 里没有,新的工具列表里有的工具
for await (const tool of toolSetData.toolList) {
if (!dbTools.find((t) => t.name === tool.name)) {
await onCreateApp({
name: tool.name,
avatar: parentApp.avatar,
parentId: parentApp._id,
teamId,
tmbId,
type: AppTypeEnum.tool,
intro: tool.description,
modules: [
getMCPToolRuntimeNode({
tool,
url: toolSetData.url,
headerSecret: toolSetData.headerSecret
})
],
session
});
}
}
// 更新 DB 里有的工具
for await (const tool of toolSetData.toolList) {
const dbTool = dbTools.find((t) => t.name === tool.name);
if (dbTool) {
await MongoApp.updateOne(
{ _id: dbTool._id },
{
modules: [
getMCPToolRuntimeNode({
tool,
url: toolSetData.url,
headerSecret: toolSetData.headerSecret
})
]
},
{ session }
);
await MongoAppVersion.updateOne(
{ appId: dbTool._id },
{
nodes: [
getMCPToolRuntimeNode({
tool,
url: toolSetData.url,
headerSecret: toolSetData.headerSecret
})
]
},
{ session }
);
}
}
};
@@ -3,16 +3,14 @@
*/
import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import {
getChildAppPreviewNode,
splitCombinePluginId
} from '@fastgpt/service/core/app/plugin/controller';
import { getChildAppPreviewNode } from '@fastgpt/service/core/app/plugin/controller';
import { type FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node.d';
import { NextAPI } from '@/service/middleware/entry';
import { type ApiRequestProps } from '@fastgpt/service/type/next';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import type { NextApiResponse } from 'next';
import { getLocale } from '@fastgpt/service/common/middle/i18n';
import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils';
export type GetPreviewNodeQuery = { appId: string; versionId?: string };
@@ -27,8 +25,7 @@ async function handler(
if (source === PluginSourceEnum.personal) {
await authApp({ req, authToken: true, appId: pluginId, per: ReadPermissionVal });
}
return getChildAppPreviewNode({ appId: pluginId, versionId, lang: getLocale(req) });
return getChildAppPreviewNode({ appId, versionId, lang: getLocale(req) });
}
export default NextAPI(handler);
@@ -9,7 +9,7 @@ import { getLocale } from '@fastgpt/service/common/middle/i18n';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import type { NextApiResponse } from 'next';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getSystemPlugins } from '@fastgpt/service/core/app/plugin/controller';
import { getSystemTools } from '@fastgpt/service/core/app/plugin/controller';
export type GetSystemPluginTemplatesBody = {
searchKey?: string;
@@ -25,7 +25,7 @@ async function handler(
const formatParentId = parentId || null;
const lang = getLocale(req);
const plugins = await getSystemPlugins();
const plugins = await getSystemTools();
return plugins // Just show the active plugins
.filter((item) => item.isActive)
@@ -33,7 +33,7 @@ async function handler(
...plugin,
parentId: plugin.parentId === undefined ? null : plugin.parentId,
templateType: plugin.templateType ?? FlowNodeTemplateTypeEnum.other,
flowNodeType: FlowNodeTypeEnum.tool,
flowNodeType: plugin.isFolder ? FlowNodeTypeEnum.toolSet : FlowNodeTypeEnum.tool,
name: parseI18nString(plugin.name, lang),
intro: parseI18nString(plugin.intro ?? '', lang)
}))
@@ -6,13 +6,12 @@ import { type ApiRequestProps } from '@fastgpt/service/type/next';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
import {
getSystemPluginByIdAndVersionId,
splitCombinePluginId
} from '@fastgpt/service/core/app/plugin/controller';
import { getSystemPluginByIdAndVersionId } from '@fastgpt/service/core/app/plugin/controller';
import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants';
import { PluginErrEnum } from '@fastgpt/global/common/error/code/plugin';
import { Types } from '@fastgpt/service/common/mongo';
import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils';
import { getMCPParentId } from '@fastgpt/global/core/app/mcpTools/utils';
export type getToolVersionListProps = PaginationProps<{
pluginId?: string;
@@ -36,6 +35,7 @@ async function handler(
list: []
};
}
const { source, pluginId: formatPluginId } = splitCombinePluginId(pluginId);
// System tool plugin
@@ -54,9 +54,10 @@ async function handler(
// Workflow plugin
const appId = await (async () => {
if (source === PluginSourceEnum.personal) {
if (source === PluginSourceEnum.personal || source === PluginSourceEnum.mcp) {
const appId = getMCPParentId(formatPluginId);
const { app } = await authApp({
appId: formatPluginId,
appId,
req,
per: ReadPermissionVal,
authToken: true
@@ -6,7 +6,7 @@ import {
} from '@fastgpt/global/common/parentFolder/type';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import { getLocale } from '@fastgpt/service/common/middle/i18n';
import { getSystemPlugins } from '@fastgpt/service/core/app/plugin/controller';
import { getSystemTools } from '@fastgpt/service/core/app/plugin/controller';
export type pathQuery = GetPathProps;
@@ -23,7 +23,7 @@ async function handler(
if (!pluginId) return [];
const plugins = await getSystemPlugins();
const plugins = await getSystemTools();
const plugin = plugins.find((item) => item.id === pluginId);
if (!plugin) return [];
@@ -24,7 +24,8 @@ async function handler(
})
});
return mcpClient.getTools();
const result = await mcpClient.getTools();
return result;
}
export default NextAPI(handler);
@@ -20,7 +20,6 @@ async function handler(
res: ApiResponseType<RunMCPToolResponse>
): Promise<RunMCPToolResponse> {
const { url, toolName, headerSecret, params } = req.body;
const mcpClient = new MCPClient({
url,
headers: getSecretValue({
@@ -33,7 +33,7 @@ async function handler(
MongoApp.countDocuments({
teamId,
type: {
$in: [AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin, AppTypeEnum.tool]
$in: [AppTypeEnum.simple, AppTypeEnum.workflow, AppTypeEnum.plugin, AppTypeEnum.toolSet]
}
}),
MongoDataset.countDocuments({
@@ -95,7 +95,11 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
errorToast: 'Error'
});
const { runAsync: onDeleFolder } = useRequest2(delAppById, {
onSuccess() {
onSuccess(data) {
data.forEach((appId) => {
localStorage.removeItem(`app_log_keys_${appId}`);
});
router.replace({
query: {
parentId: folderDetail?.parentId
@@ -13,6 +13,7 @@ import { addHours } from 'date-fns';
import { getScheduleTriggerApp } from '@/service/core/app/utils';
import { clearExpiredRawTextBufferCron } from '@fastgpt/service/common/buffer/rawText/controller';
import { clearExpiredDatasetImageCron } from '@fastgpt/service/core/dataset/image/controller';
import { cronRefreshModels } from '@fastgpt/service/core/ai/config/utils';
// Try to run train every minute
const setTrainingQueueCron = () => {
@@ -88,4 +89,5 @@ export const startCron = () => {
scheduleTriggerAppCron();
clearExpiredRawTextBufferCron();
clearExpiredDatasetImageCron();
cronRefreshModels();
};
+5 -1
View File
@@ -38,7 +38,8 @@ export type AppLogsListItemType = {
_id: string;
id: string;
source: string;
time: Date;
createTime: Date;
updateTime: Date;
title: string;
customTitle: string;
messageCount: number;
@@ -46,6 +47,9 @@ export type AppLogsListItemType = {
userBadFeedbackCount: number;
customFeedbacksCount: number;
markCount: number;
averageResponseTime: number;
errorCount: number;
totalPoints: number;
outLinkUid?: string;
tmbId: string;
sourceMember: SourceMember;
+2 -8
View File
@@ -1,11 +1,9 @@
import { GET, POST, DELETE, PUT } from '@/web/common/api/request';
import type { AppDetailType, AppListItemType } from '@fastgpt/global/core/app/type.d';
import type { GetAppChatLogsParams } from '@/global/core/api/appReq.d';
import type { AppUpdateParams, AppChangeOwnerBody } from '@/global/core/app/api';
import type { CreateAppBody } from '@/pages/api/core/app/create';
import type { ListAppBody } from '@/pages/api/core/app/list';
import type { AppLogsListItemType } from '@/types/app';
import type { PaginationResponse } from '@fastgpt/web/common/fetch/type';
import type { getBasicInfoResponse } from '@/pages/api/core/app/getBasicInfo';
/**
@@ -25,7 +23,7 @@ export const getMyAppsByTags = (data: {}) => POST(`/proApi/core/chat/team/getApp
/**
* 根据 ID 删除应用
*/
export const delAppById = (id: string) => DELETE(`/core/app/del?appId=${id}`);
export const delAppById = (id: string) => DELETE<string[]>(`/core/app/del?appId=${id}`);
/**
* 根据 ID 获取应用
@@ -44,10 +42,6 @@ export const putAppById = (id: string, data: AppUpdateParams) =>
export const getAppBasicInfoByIds = (ids: string[]) =>
POST<getBasicInfoResponse>(`/core/app/getBasicInfo`, { ids });
// =================== chat logs
export const getAppChatLogs = (data: GetAppChatLogsParams) =>
POST<PaginationResponse<AppLogsListItemType>>(`/core/app/getChatLogs`, data, { maxQuantity: 1 });
export const resumeInheritPer = (appId: string) =>
GET(`/core/app/resumeInheritPermission`, { appId });
+15
View File
@@ -0,0 +1,15 @@
import type { getLogKeysQuery, getLogKeysResponse } from '@/pages/api/core/app/logs/getLogKeys';
import type { updateLogKeysBody } from '@/pages/api/core/app/logs/updateLogKeys';
import { GET, POST } from '@/web/common/api/request';
import type { AppLogsListItemType } from '@/types/app';
import type { PaginationResponse } from '@fastgpt/web/common/fetch/type';
import type { GetAppChatLogsParams } from '@/global/core/api/appReq';
export const updateLogKeys = (data: updateLogKeysBody) =>
POST('/core/app/logs/updateLogKeys', data);
export const getLogKeys = (data: getLogKeysQuery) =>
GET<getLogKeysResponse>('/core/app/logs/getLogKeys', data);
export const getAppChatLogs = (data: GetAppChatLogsParams) =>
POST<PaginationResponse<AppLogsListItemType>>(`/core/app/getChatLogs`, data, { maxQuantity: 1 });
+24 -3
View File
@@ -5,7 +5,7 @@ import type {
FlowNodeTemplateType,
NodeTemplateListItemType
} from '@fastgpt/global/core/workflow/type/node';
import { getMyApps } from '../api';
import { getAppDetailById, getMyApps } from '../api';
import type { ListAppBody } from '@/pages/api/core/app/list';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
@@ -13,6 +13,7 @@ import type { GetPreviewNodeQuery } from '@/pages/api/core/app/plugin/getPreview
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import type {
GetPathProps,
ParentIdType,
ParentTreePathItemType
} from '@fastgpt/global/common/parentFolder/type';
import type { GetSystemPluginTemplatesBody } from '@/pages/api/core/app/plugin/getSystemPluginTemplates';
@@ -28,10 +29,26 @@ import type {
getToolVersionListProps,
getToolVersionResponse
} from '@/pages/api/core/app/plugin/getVersionList';
import type { McpGetChildrenmResponse } from '@/pages/api/core/app/mcpTools/getChildren';
/* ============ team plugin ============== */
export const getTeamPlugTemplates = (data?: ListAppBody) =>
getMyApps(data).then((res) =>
export const getTeamPlugTemplates = async (data?: {
parentId?: ParentIdType;
searchKey?: string;
}) => {
if (data?.parentId) {
// handle get mcptools
const app = await getAppDetailById(data.parentId);
if (app.type === AppTypeEnum.toolSet) {
const children = await getMcpChildren(data.parentId);
return children.map((item) => ({
...item,
flowNodeType: FlowNodeTypeEnum.tool,
templateType: FlowNodeTemplateTypeEnum.teamApp
}));
}
}
return getMyApps(data).then((res) =>
res.map((app) => ({
tmbId: app.tmbId,
id: app._id,
@@ -56,6 +73,7 @@ export const getTeamPlugTemplates = (data?: ListAppBody) =>
sourceMember: app.sourceMember
}))
);
};
/* ============ system plugin ============== */
export const getSystemPlugTemplates = (data: GetSystemPluginTemplatesBody) =>
@@ -91,6 +109,9 @@ export const getMCPTools = (data: getMCPToolsBody) =>
export const postRunMCPTool = (data: RunMCPToolBody) =>
POST('/support/mcp/client/runTool', data, { timeout: 300000 });
export const getMcpChildren = (id: string) =>
GET<McpGetChildrenmResponse>('/core/app/mcpTools/getChildren', { id });
/* ============ http plugin ============== */
export const postCreateHttpPlugin = (data: createHttpPluginBody) =>
POST('/core/app/httpPlugin/create', data);
@@ -19,6 +19,7 @@ type ContextProps = {
showNodeStatus: boolean;
};
type ChatBoxDataType = {
chatId?: string;
appId: string;
title?: string;
userAvatar?: string;
@@ -181,6 +181,11 @@ export const filterSensitiveNodesData = (nodes: StoreNodeItemType[]) => {
});
}
for (const input of node.inputs) {
if (input.key === NodeInputKeyEnum.systemInputConfig) {
input.value = undefined;
}
}
return node;
});
return cloneNodes;
@@ -105,7 +105,8 @@ export const useSendCode = ({ type }: { type: `${UserAuthTypeEnum}` }) => {
sendCode,
sendCodeText,
codeCountDown,
SendCodeBox
SendCodeBox,
openCodeAuthModal
};
};