mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-02 01:02:05 +08:00
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:
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ export type NodeTemplateListHeaderProps = {
|
||||
parentId?: string;
|
||||
type?: TemplateTypeEnum;
|
||||
searchVal?: string;
|
||||
}) => Promise<void>;
|
||||
}) => Promise<any>;
|
||||
onUpdateParentId: (parentId: string) => void;
|
||||
};
|
||||
|
||||
|
||||
-10
@@ -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 {
|
||||
|
||||
+4
-10
@@ -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({
|
||||
|
||||
+8
-1
@@ -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;
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+5
-3
@@ -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]);
|
||||
|
||||
+5
-2
@@ -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';
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
Vendored
+5
-1
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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 });
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user