Commit 4fa14557 by siigure Committed by GitHub

folder-depth-limit (#7103)

* feat: implement folder depth management and error handling

- Added new error codes for folder depth limits in CommonErrEnum.
- Introduced folder depth validation logic in the parent folder management.
- Implemented API for fetching subtree maximum folder depth.
- Enhanced folder creation and movement operations to respect maximum folder depth constraints.
- Updated UI components to handle folder depth checks and display appropriate error messages.
- Localized new error messages in English and Chinese.

* refactor: simplify folder depth limit to backend validation

Remove frontend drag pre-check and subtreeDepth API; rename env to
MAX_FOLDER_DEPTH with strict IntSchema validation; keep create-folder
button hiding only.

* perf: limit tip

* chore: remove unrelated folder depth changes

---------

Co-authored-by: archer <545436317@qq.com>
parent 38b26a3b
...@@ -9,7 +9,9 @@ export enum CommonErrEnum { ...@@ -9,7 +9,9 @@ export enum CommonErrEnum {
fileNotFound = 'fileNotFound', fileNotFound = 'fileNotFound',
unAuthFile = 'unAuthFile', unAuthFile = 'unAuthFile',
missingParams = 'missingParams', missingParams = 'missingParams',
inheritPermissionError = 'inheritPermissionError' inheritPermissionError = 'inheritPermissionError',
folderDepthLimit = 'folderDepthLimit',
folderMoveDepthLimit = 'folderMoveDepthLimit'
} }
const datasetErr = [ const datasetErr = [
{ {
...@@ -35,6 +37,14 @@ const datasetErr = [ ...@@ -35,6 +37,14 @@ const datasetErr = [
{ {
statusText: CommonErrEnum.inheritPermissionError, statusText: CommonErrEnum.inheritPermissionError,
message: i18nT('common:error.inheritPermissionError') message: i18nT('common:error.inheritPermissionError')
},
{
statusText: CommonErrEnum.folderDepthLimit,
message: i18nT('common:error.folderDepthLimit')
},
{
statusText: CommonErrEnum.folderMoveDepthLimit,
message: i18nT('common:error.folderMoveDepthLimit')
} }
]; ];
export default datasetErr.reduce((acc, cur, index) => { export default datasetErr.reduce((acc, cur, index) => {
......
import type { ParentTreePathItemType } from './type';
/** 允许的最深文件夹层级,默认 4(根目录下最多 4 层文件夹)。 */
export const DEFAULT_MAX_FOLDER_DEPTH = 4;
/** 归一化路由 parentId:仅保留非空字符串,其余视为根目录。 */
export const normalizeParentId = (parentId: unknown): string | null => {
if (typeof parentId === 'string' && parentId.length > 0) {
return parentId;
}
return null;
};
/**
* 判断 paths 是否与当前 parentId 对齐。
* 切换目录时 useRequest 可能短暂保留上一层 paths,此时不应据此隐藏「新建文件夹」。
*/
export const isPathsSyncedWithParent = (
parentId: string | null | undefined,
paths: ReadonlyArray<Pick<ParentTreePathItemType, 'parentId'>>
) => {
const normalizedParentId = normalizeParentId(parentId);
if (!normalizedParentId) {
return paths.length === 0;
}
if (paths.length === 0) {
return false;
}
const lastPathId = paths[paths.length - 1]?.parentId;
return lastPathId != null && String(lastPathId) === normalizedParentId;
};
/**
* 根据 parentId 与路径数组计算当前所在文件夹层级。
* 根目录列表页为 0;进入第 n 层文件夹时等于 paths.length(paths 含当前文件夹)。
*/
export const getCurrentFolderLevel = (parentId: string | null | undefined, pathsLength: number) =>
normalizeParentId(parentId) ? pathsLength : 0;
/**
* 是否允许在当前位置新建子文件夹。
* 规则:新建后的文件夹层级 = 当前层级 + 1,不得超过 maxFolderLevel。
*/
export const canCreateFolderAtDepth = (currentFolderLevel: number, maxFolderLevel?: number) => {
const max = maxFolderLevel ?? DEFAULT_MAX_FOLDER_DEPTH;
return currentFolderLevel + 1 <= max;
};
/**
* 结合 parentId 与 paths 判断是否可新建子文件夹(前端列表页使用)。
* paths 未与 parentId 对齐时返回 true,避免 stale paths 误隐藏按钮;后端仍会兜底校验。
*/
export const canCreateSubFolder = (
parentId: string | null | undefined,
pathsOrLength: ReadonlyArray<Pick<ParentTreePathItemType, 'parentId'>> | number,
maxFolderLevel?: number
) => {
const normalizedParentId = normalizeParentId(parentId);
if (typeof pathsOrLength === 'number') {
return canCreateFolderAtDepth(
getCurrentFolderLevel(normalizedParentId, pathsOrLength),
maxFolderLevel
);
}
if (!isPathsSyncedWithParent(normalizedParentId, pathsOrLength)) {
return true;
}
return canCreateFolderAtDepth(
getCurrentFolderLevel(normalizedParentId, pathsOrLength.length),
maxFolderLevel
);
};
...@@ -113,6 +113,7 @@ export type FastGPTFeConfigsType = { ...@@ -113,6 +113,7 @@ export type FastGPTFeConfigsType = {
agentSandboxMaxSessionRuntime?: number; agentSandboxMaxSessionRuntime?: number;
agentSkillMaxUploadBytes?: number; agentSkillMaxUploadBytes?: number;
workflowParallelRunMaxConcurrency?: number; workflowParallelRunMaxConcurrency?: number;
maxFolderDepth?: number;
}; };
uploadFileMaxAmount: number; uploadFileMaxAmount: number;
......
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { serviceEnv } from '../../env';
type FolderResourceDoc = {
_id: unknown;
parentId?: string | null;
teamId?: unknown;
type?: string;
};
/** 兼容 App / Dataset / Skill 等 Mongoose Model 的最小查询接口。 */
type FolderResourceModel = {
findById: (
id: string,
select?: string
) => {
lean: <T = FolderResourceDoc>() => Promise<T | null>;
};
find: (
query: { parentId?: string; teamId?: string },
select?: string
) => {
lean: <T = FolderResourceDoc>() => Promise<T[]>;
};
};
type FolderTypeChecker = (type: string) => boolean;
type FolderDepthModelProps = {
model: FolderResourceModel;
teamId: string;
};
type CheckCreateFolderDepthProps = FolderDepthModelProps & {
parentId: ParentIdType;
};
type CheckMoveFolderDepthProps = FolderDepthModelProps & {
resourceId: string;
targetParentId: ParentIdType;
isFolderType: FolderTypeChecker;
};
/**
* 根据 parentId 向上追溯,计算父级目录深度。
* 根目录深度为 0;遇到 parentId 成环或父级不存在时拒绝请求。
*/
const getParentFolderDepth = async ({
parentId,
teamId,
model
}: FolderDepthModelProps & { parentId: ParentIdType }): Promise<number> => {
if (!parentId) return 0;
let depth = 0;
let currentId: string | null = String(parentId);
const visited = new Set<string>();
while (currentId) {
if (visited.has(currentId)) {
throw CommonErrEnum.invalidParams;
}
visited.add(currentId);
const doc: FolderResourceDoc | null = await model
.findById(currentId, 'parentId teamId')
.lean<FolderResourceDoc>();
if (!doc || String(doc.teamId) !== String(teamId)) {
throw CommonErrEnum.invalidParams;
}
depth += 1;
currentId = doc.parentId ? String(doc.parentId) : null;
}
return depth;
};
/**
* 计算被移动资源子树中文件夹的最大相对深度。
* 非文件夹资源返回 0;文件夹自身相对深度为 1。
*/
const getSubtreeMaxFolderDepth = async ({
resourceId,
teamId,
model,
isFolderType
}: FolderDepthModelProps & {
resourceId: string;
isFolderType: FolderTypeChecker;
}): Promise<number> => {
const resource = await model.findById(resourceId, 'type teamId').lean<FolderResourceDoc>();
if (!resource || String(resource.teamId) !== String(teamId)) {
throw CommonErrEnum.invalidResource;
}
if (!resource.type || !isFolderType(resource.type)) {
return 0;
}
let maxRelativeDepth = 1;
const queue: Array<{ id: string; depth: number }> = [{ id: resourceId, depth: 1 }];
const visited = new Set<string>([resourceId]);
while (queue.length > 0) {
const current = queue.shift();
if (!current) break;
maxRelativeDepth = Math.max(maxRelativeDepth, current.depth);
const children = await model
.find({ parentId: current.id, teamId }, '_id type')
.lean<FolderResourceDoc>();
for (const child of children) {
if (!child.type || !isFolderType(child.type)) continue;
const childId = String(child._id);
if (visited.has(childId)) continue;
visited.add(childId);
queue.push({ id: childId, depth: current.depth + 1 });
}
}
return maxRelativeDepth;
};
/** 判断 targetId 是否位于 ancestorId 的子树中(含自身)。 */
const isInSubtree = async ({
ancestorId,
targetId,
teamId,
model
}: FolderDepthModelProps & {
ancestorId: string;
targetId: string;
}): Promise<boolean> => {
if (ancestorId === targetId) return true;
let currentId: string | null = targetId;
const visited = new Set<string>();
while (currentId) {
if (currentId === ancestorId) return true;
if (visited.has(currentId)) return false;
visited.add(currentId);
const doc: FolderResourceDoc | null = await model
.findById(currentId, 'parentId teamId')
.lean<FolderResourceDoc>();
if (!doc || String(doc.teamId) !== String(teamId)) return false;
currentId = doc.parentId ? String(doc.parentId) : null;
}
return false;
};
/**
* 创建文件夹前校验:parentDepth + 1 不得超过最大深度。
* 权限校验应在本函数之前完成。
*/
export const checkCreateFolderDepth = async ({
parentId,
teamId,
model
}: CheckCreateFolderDepthProps) => {
const maxDepth = serviceEnv.MAX_FOLDER_DEPTH;
const parentDepth = await getParentFolderDepth({ parentId, teamId, model });
if (parentDepth + 1 > maxDepth) {
throw CommonErrEnum.folderDepthLimit;
}
};
/**
* 移动资源前校验:目标父级深度 + 子树最大文件夹深度不得超过最大深度。
* 同时阻止移动到自身或其子目录,避免 parentId 成环。
*/
export const checkMoveFolderDepth = async ({
resourceId,
targetParentId,
teamId,
model,
isFolderType
}: CheckMoveFolderDepthProps) => {
const maxDepth = serviceEnv.MAX_FOLDER_DEPTH;
if (targetParentId && String(targetParentId) === String(resourceId)) {
throw CommonErrEnum.invalidParams;
}
if (targetParentId) {
const movingIntoDescendant = await isInSubtree({
ancestorId: resourceId,
targetId: String(targetParentId),
teamId,
model
});
if (movingIntoDescendant) {
throw CommonErrEnum.invalidParams;
}
}
const [targetParentDepth, subtreeMaxFolderDepth] = await Promise.all([
getParentFolderDepth({ parentId: targetParentId, teamId, model }),
getSubtreeMaxFolderDepth({ resourceId, teamId, model, isFolderType })
]);
if (targetParentDepth + subtreeMaxFolderDepth > maxDepth) {
throw CommonErrEnum.folderMoveDepthLimit;
}
};
...@@ -24,7 +24,8 @@ export const initFastGPTConfig = (config?: FastGPTConfigFileType) => { ...@@ -24,7 +24,8 @@ export const initFastGPTConfig = (config?: FastGPTConfigFileType) => {
config.feConfigs.uploadFileMaxAmount = serviceEnv.UPLOAD_FILE_MAX_AMOUNT; config.feConfigs.uploadFileMaxAmount = serviceEnv.UPLOAD_FILE_MAX_AMOUNT;
config.feConfigs.limit = { config.feConfigs.limit = {
...config.feConfigs.limit, ...config.feConfigs.limit,
agentSkillMaxUploadBytes: serviceEnv.AGENT_SKILL_MAX_UPLOAD_SIZE * 1024 * 1024 agentSkillMaxUploadBytes: serviceEnv.AGENT_SKILL_MAX_UPLOAD_SIZE * 1024 * 1024,
maxFolderDepth: serviceEnv.MAX_FOLDER_DEPTH
}; };
global.feConfigs = config.feConfigs; global.feConfigs = config.feConfigs;
......
import { createEnv } from '@t3-oss/env-core'; import { createEnv } from '@t3-oss/env-core';
import z from 'zod'; import z from 'zod';
import { isPhaseProductionBuild } from '@fastgpt/global/common/system/constants'; import { isPhaseProductionBuild } from '@fastgpt/global/common/system/constants';
import { DEFAULT_MAX_FOLDER_DEPTH } from '@fastgpt/global/common/parentFolder/depth';
import { BoolSchema, IntSchema, NumSchema, UrlSchema } from '@fastgpt/global/common/zod'; import { BoolSchema, IntSchema, NumSchema, UrlSchema } from '@fastgpt/global/common/zod';
const defaultableIntSchema = (defaultValue: number) => const defaultableIntSchema = (defaultValue: number) =>
...@@ -9,6 +10,7 @@ const defaultableIntSchema = (defaultValue: number) => ...@@ -9,6 +10,7 @@ const defaultableIntSchema = (defaultValue: number) =>
z.coerce.number<number>().int().nonnegative() z.coerce.number<number>().int().nonnegative()
); );
// 系统最大字符串处理长度
const SYSTEM_STRING_LENGTH_UNIT = 1_000_000; const SYSTEM_STRING_LENGTH_UNIT = 1_000_000;
/** /**
...@@ -277,6 +279,9 @@ export const serviceEnv = createEnv({ ...@@ -277,6 +279,9 @@ export const serviceEnv = createEnv({
SERVICE_REQUEST_MAX_CONTENT_LENGTH: IntSchema.default(10).meta({ SERVICE_REQUEST_MAX_CONTENT_LENGTH: IntSchema.default(10).meta({
description: '服务器接收请求的最大大小(MB)' description: '服务器接收请求的最大大小(MB)'
}), }),
MAX_FOLDER_DEPTH: IntSchema.min(2).max(20).default(DEFAULT_MAX_FOLDER_DEPTH).meta({
description: '允许的最深文件夹层级,默认 4(根目录下最多 4 层文件夹)'
}),
APP_FOLDER_MAX_AMOUNT: IntSchema.default(1000).meta({ APP_FOLDER_MAX_AMOUNT: IntSchema.default(1000).meta({
description: '应用文件夹最大数量' description: '应用文件夹最大数量'
}), }),
......
...@@ -13,6 +13,7 @@ import type { IconNameType } from '../Icon/type'; ...@@ -13,6 +13,7 @@ import type { IconNameType } from '../Icon/type';
import { useSystem } from '../../../hooks/useSystem'; import { useSystem } from '../../../hooks/useSystem';
import Avatar from '../Avatar'; import Avatar from '../Avatar';
import MyPopover from '../MyPopover'; import MyPopover from '../MyPopover';
import MyTooltip from '../MyTooltip';
export type MenuItemType = 'primary' | 'danger' | 'gray' | 'grayBg'; export type MenuItemType = 'primary' | 'danger' | 'gray' | 'grayBg';
...@@ -29,6 +30,8 @@ export type MenuItemData = { ...@@ -29,6 +30,8 @@ export type MenuItemData = {
onClick?: () => any; onClick?: () => any;
menuItemStyles?: MenuItemProps; menuItemStyles?: MenuItemProps;
menuList?: MenuItemData[]; menuList?: MenuItemData[];
disabled?: boolean;
disabledTip?: string;
}>; }>;
}; };
...@@ -204,13 +207,19 @@ const MenuItem = ({ ...@@ -204,13 +207,19 @@ const MenuItem = ({
<Box <Box
px={3} px={3}
py={2} py={2}
cursor="pointer" cursor={item.disabled ? 'not-allowed' : 'pointer'}
opacity={item.disabled ? 0.6 : 1}
borderRadius="md" borderRadius="md"
_hover={{ _hover={
bg: 'primary.50', item.disabled
color: 'primary.600' ? {}
}} : {
bg: 'primary.50',
color: 'primary.600'
}
}
onClick={(e) => { onClick={(e) => {
if (item.disabled) return;
if (item.onClick) { if (item.onClick) {
item.onClick(); item.onClick();
} }
...@@ -235,7 +244,7 @@ const MenuItem = ({ ...@@ -235,7 +244,7 @@ const MenuItem = ({
> >
{item.label} {item.label}
</Box> </Box>
{item.description && ( {item.description && !item.disabled && (
<Box color={'myGray.500'} fontSize={'mini'}> <Box color={'myGray.500'} fontSize={'mini'}>
{item.description} {item.description}
</Box> </Box>
...@@ -297,6 +306,16 @@ const MultipleMenu = (props: Props) => { ...@@ -297,6 +306,16 @@ const MultipleMenu = (props: Props) => {
</Box> </Box>
)} )}
{group.children.map((item, index) => { {group.children.map((item, index) => {
const menuItem = <MenuItem item={item} size={size} onClose={onCloseFn} />;
if (item.disabled && item.disabledTip) {
return (
<MyTooltip shouldWrapChildren={false} key={index} label={item.disabledTip}>
<Box>{menuItem}</Box>
</MyTooltip>
);
}
return ( return (
<Box key={index}> <Box key={index}>
{item.menuList ? ( {item.menuList ? (
...@@ -306,15 +325,11 @@ const MultipleMenu = (props: Props) => { ...@@ -306,15 +325,11 @@ const MultipleMenu = (props: Props) => {
trigger={'hover'} trigger={'hover'}
menuList={item.menuList} menuList={item.menuList}
onClose={onCloseFn} onClose={onCloseFn}
Trigger={ Trigger={<Box>{menuItem}</Box>}
<Box>
<MenuItem item={item} size={size} onClose={onCloseFn} />
</Box>
}
hasArrow hasArrow
/> />
) : ( ) : (
<MenuItem item={item} size={size} onClose={onCloseFn} /> menuItem
)} )}
</Box> </Box>
); );
......
...@@ -361,7 +361,7 @@ const MyMenu = ({ ...@@ -361,7 +361,7 @@ const MyMenu = ({
if (child.disabled && child.disabledTip) { if (child.disabled && child.disabledTip) {
return ( return (
<MyTooltip shouldWrapChildren={false} key={index} label={child.disabledTip}> <MyTooltip shouldWrapChildren={false} key={index} label={child.disabledTip}>
{menuItem} <Box>{menuItem}</Box>
</MyTooltip> </MyTooltip>
); );
} }
......
...@@ -46,6 +46,7 @@ ...@@ -46,6 +46,7 @@
"File": "File", "File": "File",
"Finish": "Finish", "Finish": "Finish",
"Folder": "Folder", "Folder": "Folder",
"folder_depth_limit_tip": "Maximum folder depth reached",
"FullScreen": "FullScreen", "FullScreen": "FullScreen",
"FullScreenLight": "FullScreenLight", "FullScreenLight": "FullScreenLight",
"Import": "Import", "Import": "Import",
...@@ -789,6 +790,8 @@ ...@@ -789,6 +790,8 @@
"error.fileNotFound": "File not found~", "error.fileNotFound": "File not found~",
"error.file_upload_disabled": "File upload is disabled for the current app", "error.file_upload_disabled": "File upload is disabled for the current app",
"error.inheritPermissionError": "Inherit permission Error", "error.inheritPermissionError": "Inherit permission Error",
"error.folderDepthLimit": "Folder depth limit reached. Cannot create a subfolder here.",
"error.folderMoveDepthLimit": "Cannot move here. The folder would exceed the depth limit.",
"error.invalid_params": "Invalid parameter", "error.invalid_params": "Invalid parameter",
"error.llm_track_expired": "Request details expired", "error.llm_track_expired": "Request details expired",
"error.missingParams": "Insufficient parameters", "error.missingParams": "Insufficient parameters",
......
...@@ -46,6 +46,7 @@ ...@@ -46,6 +46,7 @@
"File": "文件", "File": "文件",
"Finish": "完成", "Finish": "完成",
"Folder": "文件夹", "Folder": "文件夹",
"folder_depth_limit_tip": "已达到最大目录深度",
"FullScreen": "全屏", "FullScreen": "全屏",
"FullScreenLight": "全屏预览", "FullScreenLight": "全屏预览",
"Import": "导入", "Import": "导入",
...@@ -789,6 +790,8 @@ ...@@ -789,6 +790,8 @@
"error.fileNotFound": "文件找不到了~", "error.fileNotFound": "文件找不到了~",
"error.file_upload_disabled": "当前应用未开启文件上传", "error.file_upload_disabled": "当前应用未开启文件上传",
"error.inheritPermissionError": "权限继承错误", "error.inheritPermissionError": "权限继承错误",
"error.folderDepthLimit": "文件夹层级已满,无法在此创建子文件夹",
"error.folderMoveDepthLimit": "无法移入:该位置会使文件夹超出层级上限",
"error.invalid_params": "参数无效", "error.invalid_params": "参数无效",
"error.llm_track_expired": "请求详情已过期", "error.llm_track_expired": "请求详情已过期",
"error.missingParams": "参数缺失", "error.missingParams": "参数缺失",
......
...@@ -45,6 +45,7 @@ ...@@ -45,6 +45,7 @@
"File": "檔案", "File": "檔案",
"Finish": "完成", "Finish": "完成",
"Folder": "資料夾", "Folder": "資料夾",
"folder_depth_limit_tip": "已達到最大目錄深度",
"FullScreen": "全屏", "FullScreen": "全屏",
"FullScreenLight": "全屏預覽", "FullScreenLight": "全屏預覽",
"Import": "匯入", "Import": "匯入",
...@@ -783,6 +784,8 @@ ...@@ -783,6 +784,8 @@
"error.fileNotFound": "找不到檔案", "error.fileNotFound": "找不到檔案",
"error.file_upload_disabled": "當前應用未開啟文件上傳", "error.file_upload_disabled": "當前應用未開啟文件上傳",
"error.inheritPermissionError": "繼承權限錯誤", "error.inheritPermissionError": "繼承權限錯誤",
"error.folderDepthLimit": "資料夾層級已滿,無法在此建立子資料夾",
"error.folderMoveDepthLimit": "無法移入:該位置會使資料夾超出層級上限",
"error.invalid_params": "參數無效", "error.invalid_params": "參數無效",
"error.llm_track_expired": "請求詳情已過期", "error.llm_track_expired": "請求詳情已過期",
"error.missingParams": "參數不足", "error.missingParams": "參數不足",
......
import React, { useState, type DragEvent, useCallback } from 'react'; import { useState, type DragEvent, useCallback } from 'react';
import type { BoxProps } from '@chakra-ui/react'; import type { BoxProps } from '@chakra-ui/react';
import { useBoolean } from 'ahooks'; import { useBoolean } from 'ahooks';
...@@ -19,7 +19,7 @@ export const useFolderDrag = ({ ...@@ -19,7 +19,7 @@ export const useFolderDrag = ({
draggable: true, draggable: true,
userSelect: 'none' as any, userSelect: 'none' as any,
'data-drag-id': isFolder ? dataId : undefined, 'data-drag-id': isFolder ? dataId : undefined,
onDragStart: (e: DragEvent<HTMLDivElement>) => { onDragStart: () => {
setDragId(dataId); setDragId(dataId);
}, },
onDragOver: (e: DragEvent<HTMLDivElement>) => { onDragOver: (e: DragEvent<HTMLDivElement>) => {
...@@ -41,7 +41,7 @@ export const useFolderDrag = ({ ...@@ -41,7 +41,7 @@ export const useFolderDrag = ({
if (targetId && dragId && targetId !== dragId) { if (targetId && dragId && targetId !== dragId) {
await onDrop(dragId, targetId); await onDrop(dragId, targetId);
} }
} catch (error) {} } catch {}
setTargetId(undefined); setTargetId(undefined);
setDragId(undefined); setDragId(undefined);
......
...@@ -4,6 +4,7 @@ import { useRequest } from '@fastgpt/web/hooks/useRequest'; ...@@ -4,6 +4,7 @@ import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { getSkillList, getSkillFolderPath, getSkillDetail } from '@/web/core/skill/api'; import { getSkillList, getSkillFolderPath, getSkillDetail } from '@/web/core/skill/api';
import type { ListSkillsResponse } from '@fastgpt/global/core/ai/skill/api'; import type { ListSkillsResponse } from '@fastgpt/global/core/ai/skill/api';
import type { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type'; import type { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { normalizeParentId } from '@fastgpt/global/common/parentFolder/depth';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { SkillPermission } from '@fastgpt/global/support/permission/skill/controller'; import type { SkillPermission } from '@fastgpt/global/support/permission/skill/controller';
...@@ -46,10 +47,7 @@ export const SkillListContext = createContext<SkillListContextType>({ ...@@ -46,10 +47,7 @@ export const SkillListContext = createContext<SkillListContextType>({
const SkillListContextProvider = ({ children }: { children: ReactNode }) => { const SkillListContextProvider = ({ children }: { children: ReactNode }) => {
const router = useRouter(); const router = useRouter();
// 归一化 parentId:非空字符串时取值,否则为 null const parentId = normalizeParentId(router.query.parentId);
const rawParentId = router.query.parentId;
const parentId: string | null =
typeof rawParentId === 'string' && rawParentId.length > 0 ? rawParentId : null;
const [searchKey, setSearchKey] = useState(''); const [searchKey, setSearchKey] = useState('');
......
...@@ -52,9 +52,9 @@ function List() { ...@@ -52,9 +52,9 @@ function List() {
searchKey, searchKey,
setSearchKey setSearchKey
} = useContextSelector(DatasetsContext, (v) => v); } = useContextSelector(DatasetsContext, (v) => v);
const [editPerDatasetId, setEditPerDatasetId] = useState<string>();
const router = useRouter(); const router = useRouter();
const { parentId = null } = router.query as { parentId?: string | null }; const { parentId = null } = router.query as { parentId?: string | null };
const [editPerDatasetId, setEditPerDatasetId] = useState<string>();
const formatDatasets = useMemo( const formatDatasets = useMemo(
() => () =>
......
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
type ParentIdType, type ParentIdType,
type ParentTreePathItemType type ParentTreePathItemType
} from '@fastgpt/global/common/parentFolder/type'; } from '@fastgpt/global/common/parentFolder/type';
import { normalizeParentId } from '@fastgpt/global/common/parentFolder/depth';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { createContext } from 'use-context-selector'; import { createContext } from 'use-context-selector';
...@@ -71,7 +72,7 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) { ...@@ -71,7 +72,7 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [moveDatasetId, setMoveDatasetId] = useState<string>(); const [moveDatasetId, setMoveDatasetId] = useState<string>();
const [searchKey, setSearchKey] = useState(''); const [searchKey, setSearchKey] = useState('');
const { parentId = null } = router.query as { parentId?: string | null }; const parentId = normalizeParentId(router.query.parentId);
const { const {
data: myDatasets = [], data: myDatasets = [],
...@@ -99,10 +100,13 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) { ...@@ -99,10 +100,13 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) {
); );
const { data: paths = [], runAsync: refetchPaths } = useRequest( const { data: paths = [], runAsync: refetchPaths } = useRequest(
async () => getDatasetPaths({ sourceId: parentId, type: 'current' }), async () => {
if (!parentId) return [];
return getDatasetPaths({ sourceId: parentId, type: 'current' });
},
{ {
manual: false, manual: false,
refreshDeps: [folderDetail] refreshDeps: [parentId]
} }
); );
......
...@@ -18,6 +18,8 @@ import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; ...@@ -18,6 +18,8 @@ import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { TeamSkillCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { TeamSkillCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import { checkCreateFolderDepth } from '@fastgpt/service/common/parentFolder/depth';
import { MongoAgentSkills } from '@fastgpt/service/core/ai/skill/model/schema';
async function handler(req: ApiRequestProps<CreateSkillFolderBody>) { async function handler(req: ApiRequestProps<CreateSkillFolderBody>) {
const { name, description, parentId } = parseApiInput({ const { name, description, parentId } = parseApiInput({
...@@ -45,6 +47,8 @@ async function handler(req: ApiRequestProps<CreateSkillFolderBody>) { ...@@ -45,6 +47,8 @@ async function handler(req: ApiRequestProps<CreateSkillFolderBody>) {
per: TeamSkillCreatePermissionVal per: TeamSkillCreatePermissionVal
}); });
await checkCreateFolderDepth({ parentId, teamId, model: MongoAgentSkills });
// Create the folder within a transaction and copy collaborators from parent // Create the folder within a transaction and copy collaborators from parent
const folderId = await mongoSessionRun(async (session) => { const folderId = await mongoSessionRun(async (session) => {
const folder = await createSkillFolder( const folder = await createSkillFolder(
......
...@@ -32,6 +32,7 @@ import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; ...@@ -32,6 +32,7 @@ import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { isValidObjectId } from 'mongoose'; import { isValidObjectId } from 'mongoose';
import type { ApiRequestProps } from '@fastgpt/service/type/next'; import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar'; import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar';
import { checkMoveFolderDepth } from '@fastgpt/service/common/parentFolder/depth';
async function handler(req: ApiRequestProps<UpdateSkillBody>) { async function handler(req: ApiRequestProps<UpdateSkillBody>) {
const { skillId, name, description, category, avatar, parentId } = parseApiInput({ const { skillId, name, description, category, avatar, parentId } = parseApiInput({
...@@ -102,6 +103,16 @@ async function handler(req: ApiRequestProps<UpdateSkillBody>) { ...@@ -102,6 +103,16 @@ async function handler(req: ApiRequestProps<UpdateSkillBody>) {
} }
} }
if (isMove) {
await checkMoveFolderDepth({
resourceId: skillId,
targetParentId: parentId,
teamId,
model: MongoAgentSkills,
isFolderType: (type) => type === AgentSkillTypeEnum.folder
});
}
if (!isMove) { if (!isMove) {
// Field validation for normal update // Field validation for normal update
if (name !== undefined) { if (name !== undefined) {
......
...@@ -18,6 +18,7 @@ import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; ...@@ -18,6 +18,7 @@ import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { checkTeamAppTypeLimit } from '@fastgpt/service/support/permission/teamLimit'; import { checkTeamAppTypeLimit } from '@fastgpt/service/support/permission/teamLimit';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import { checkCreateFolderDepth } from '@fastgpt/service/common/parentFolder/depth';
import { import {
CreateAppFolderBodySchema, CreateAppFolderBodySchema,
CreateAppFolderResponseSchema, CreateAppFolderResponseSchema,
...@@ -48,6 +49,8 @@ async function handler( ...@@ -48,6 +49,8 @@ async function handler(
await checkTeamAppTypeLimit({ teamId, appCheckType: 'folder' }); await checkTeamAppTypeLimit({ teamId, appCheckType: 'folder' });
await checkCreateFolderDepth({ parentId, teamId, model: MongoApp });
// Create app // Create app
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {
const app = await MongoApp.create({ const app = await MongoApp.create({
......
...@@ -28,6 +28,7 @@ import { i18nT } from '@fastgpt/global/common/i18n/utils'; ...@@ -28,6 +28,7 @@ import { i18nT } from '@fastgpt/global/common/i18n/utils';
import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar'; import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar';
import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller'; import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import { checkMoveFolderDepth } from '@fastgpt/service/common/parentFolder/depth';
import { import {
UpdateAppBodySchema, UpdateAppBodySchema,
UpdateAppQuerySchema, UpdateAppQuerySchema,
...@@ -111,6 +112,23 @@ async function handler(req: ApiRequestProps<UpdateAppBodyType, UpdateAppQueryTyp ...@@ -111,6 +112,23 @@ async function handler(req: ApiRequestProps<UpdateAppBodyType, UpdateAppQueryTyp
} }
} }
if (isMove) {
const isFolderType =
app.type === AppTypeEnum.toolFolder
? (type: string) => type === AppTypeEnum.toolFolder
: app.type === AppTypeEnum.folder
? (type: string) => type === AppTypeEnum.folder
: () => false;
await checkMoveFolderDepth({
resourceId: appId,
targetParentId: parentId,
teamId: app.teamId,
model: MongoApp,
isFolderType
});
}
const onUpdate = async (session?: ClientSession) => { const onUpdate = async (session?: ClientSession) => {
// format nodes data // format nodes data
// 1. dataset search limit, less than model quoteMaxToken // 1. dataset search limit, less than model quoteMaxToken
......
...@@ -17,6 +17,7 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; ...@@ -17,6 +17,7 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import { checkCreateFolderDepth } from '@fastgpt/service/common/parentFolder/depth';
import { import {
CreateDatasetFolderBodySchema, CreateDatasetFolderBodySchema,
type CreateDatasetFolderBody type CreateDatasetFolderBody
...@@ -45,6 +46,8 @@ async function handler(req: ApiRequestProps<CreateDatasetFolderBody>) { ...@@ -45,6 +46,8 @@ async function handler(req: ApiRequestProps<CreateDatasetFolderBody>) {
await checkTeamDatasetFolderLimit({ teamId }); await checkTeamDatasetFolderLimit({ teamId });
await checkCreateFolderDepth({ parentId, teamId, model: MongoDataset });
await mongoSessionRun(async (session) => { await mongoSessionRun(async (session) => {
const dataset = await MongoDataset.create({ const dataset = await MongoDataset.create({
...parseParentIdInMongo(parentId), ...parseParentIdInMongo(parentId),
......
...@@ -42,6 +42,8 @@ import { computedCollectionChunkSettings } from '@fastgpt/global/core/dataset/tr ...@@ -42,6 +42,8 @@ import { computedCollectionChunkSettings } from '@fastgpt/global/core/dataset/tr
import { getResourceOwnedClbs } from '@fastgpt/service/support/permission/controller'; import { getResourceOwnedClbs } from '@fastgpt/service/support/permission/controller';
import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar'; import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar';
import { isInternalAddress, PRIVATE_URL_TEXT } from '@fastgpt/service/common/system/utils'; import { isInternalAddress, PRIVATE_URL_TEXT } from '@fastgpt/service/common/system/utils';
import { checkMoveFolderDepth } from '@fastgpt/service/common/parentFolder/depth';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
// 更新知识库接口 // 更新知识库接口
// 包括如下功能: // 包括如下功能:
...@@ -55,20 +57,25 @@ import { isInternalAddress, PRIVATE_URL_TEXT } from '@fastgpt/service/common/sys ...@@ -55,20 +57,25 @@ import { isInternalAddress, PRIVATE_URL_TEXT } from '@fastgpt/service/common/sys
// (2) 目标目录的管理权限 // (2) 目标目录的管理权限
// (3) 如果从根目录移动或移动到根目录,需要有团队的应用创建权限 // (3) 如果从根目录移动或移动到根目录,需要有团队的应用创建权限
async function handler(req: ApiRequestProps<UpdateDatasetBody>) { async function handler(req: ApiRequestProps<UpdateDatasetBody>) {
let { const {
id, body: {
parentId, id,
name, parentId,
avatar, name,
intro, avatar,
agentModel, intro,
vlmModel, agentModel,
websiteConfig, vlmModel,
externalReadUrl, websiteConfig,
apiDatasetServer, externalReadUrl,
autoSync, apiDatasetServer,
chunkSettings autoSync,
} = UpdateDatasetBodySchema.parse(req.body); chunkSettings: rawChunkSettings
}
} = parseApiInput({
req,
bodySchema: UpdateDatasetBodySchema
});
if (websiteConfig?.url) { if (websiteConfig?.url) {
if (await isInternalAddress(websiteConfig.url)) { if (await isInternalAddress(websiteConfig.url)) {
...@@ -87,9 +94,9 @@ async function handler(req: ApiRequestProps<UpdateDatasetBody>) { ...@@ -87,9 +94,9 @@ async function handler(req: ApiRequestProps<UpdateDatasetBody>) {
let targetName = ''; let targetName = '';
chunkSettings = chunkSettings const chunkSettings = rawChunkSettings
? computedCollectionChunkSettings({ ? computedCollectionChunkSettings({
...chunkSettings, ...rawChunkSettings,
llmModel: getLLMModel(dataset.agentModel), llmModel: getLLMModel(dataset.agentModel),
vectorModel: getEmbeddingModel(dataset.vectorModel) vectorModel: getEmbeddingModel(dataset.vectorModel)
}) })
...@@ -130,6 +137,16 @@ async function handler(req: ApiRequestProps<UpdateDatasetBody>) { ...@@ -130,6 +137,16 @@ async function handler(req: ApiRequestProps<UpdateDatasetBody>) {
if (!permission.hasWritePer) return Promise.reject(DatasetErrEnum.unAuthDataset); if (!permission.hasWritePer) return Promise.reject(DatasetErrEnum.unAuthDataset);
} }
if (isMove) {
await checkMoveFolderDepth({
resourceId: id,
targetParentId: parentId,
teamId: dataset.teamId,
model: MongoDataset,
isFolderType: (type) => type === DatasetTypeEnum.folder
});
}
const isFolder = dataset.type === DatasetTypeEnum.folder; const isFolder = dataset.type === DatasetTypeEnum.folder;
updateTraining({ updateTraining({
......
...@@ -29,9 +29,15 @@ import { getUtmWorkflow } from '@/web/support/marketing/utils'; ...@@ -29,9 +29,15 @@ import { getUtmWorkflow } from '@/web/support/marketing/utils';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import {
canCreateSubFolder,
DEFAULT_MAX_FOLDER_DEPTH
} from '@fastgpt/global/common/parentFolder/depth';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; import { ReadRoleVal } from '@fastgpt/global/support/permission/constant';
import TemplateCreatePanel from '@/pageComponents/dashboard/agent/TemplateCreatePanel'; import TemplateCreatePanel from '@/pageComponents/dashboard/agent/TemplateCreatePanel';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const EditFolderModal = dynamic( const EditFolderModal = dynamic(
() => import('@fastgpt/web/components/common/MyModal/EditFolderModal') () => import('@fastgpt/web/components/common/MyModal/EditFolderModal')
...@@ -57,6 +63,10 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => { ...@@ -57,6 +63,10 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
} = useContextSelector(AppListContext, (v) => v); } = useContextSelector(AppListContext, (v) => v);
const [editFolder, setEditFolder] = useState<EditFolderFormType>(); const [editFolder, setEditFolder] = useState<EditFolderFormType>();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { feConfigs } = useSystemStore();
const maxFolderDepth = feConfigs?.limit?.maxFolderDepth ?? DEFAULT_MAX_FOLDER_DEPTH;
const canCreateFolder = canCreateSubFolder(parentId, paths, maxFolderDepth);
const folderDepthLimitTip = t('common:folder_depth_limit_tip');
const { const {
isOpen: isOpenJsonImportModal, isOpen: isOpenJsonImportModal,
...@@ -149,14 +159,17 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => { ...@@ -149,14 +159,17 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
{hasCreatePer && ( {hasCreatePer && (
<> <>
<Button <MyTooltip label={canCreateFolder ? '' : folderDepthLimitTip}>
variant={'grayBase'} <Button
leftIcon={<MyIcon name={'common/addLight'} w={'18px'} mr={-1} />} variant={'grayBase'}
onClick={() => setEditFolder({})} leftIcon={<MyIcon name={'common/addLight'} w={'18px'} mr={-1} />}
px={5} onClick={() => setEditFolder({})}
> isDisabled={!canCreateFolder}
{t('common:Folder')} px={5}
</Button> >
{t('common:Folder')}
</Button>
</MyTooltip>
<Button <Button
variant={'grayBase'} variant={'grayBase'}
leftIcon={<MyIcon name={'common/importLight'} w={'14px'} />} leftIcon={<MyIcon name={'common/importLight'} w={'14px'} />}
......
...@@ -14,6 +14,11 @@ import SkillListContextProvider, { ...@@ -14,6 +14,11 @@ import SkillListContextProvider, {
} from '@/pageComponents/dashboard/skill/context'; } from '@/pageComponents/dashboard/skill/context';
import List from '@/pageComponents/dashboard/skill/List'; import List from '@/pageComponents/dashboard/skill/List';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import {
canCreateSubFolder,
DEFAULT_MAX_FOLDER_DEPTH
} from '@fastgpt/global/common/parentFolder/depth';
import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { postCreateSkillFolder } from '@/web/core/skill/api'; import { postCreateSkillFolder } from '@/web/core/skill/api';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
...@@ -22,6 +27,7 @@ import FolderPath from '@/components/common/folder/Path'; ...@@ -22,6 +27,7 @@ import FolderPath from '@/components/common/folder/Path';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { useSkillSandboxOperationGuard } from '@/components/core/skill/useSkillSandboxOperationGuard'; import { useSkillSandboxOperationGuard } from '@/components/core/skill/useSkillSandboxOperationGuard';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const EditFolderModal = dynamic( const EditFolderModal = dynamic(
() => import('@fastgpt/web/components/common/MyModal/EditFolderModal') () => import('@fastgpt/web/components/common/MyModal/EditFolderModal')
...@@ -34,6 +40,7 @@ const SkillPageContent = ({ MenuIcon }: { MenuIcon: JSX.Element }) => { ...@@ -34,6 +40,7 @@ const SkillPageContent = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
const router = useRouter(); const router = useRouter();
const { isPc } = useSystem(); const { isPc } = useSystem();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { feConfigs } = useSystemStore();
const [editFolder, setEditFolder] = useState<EditFolderFormType>(); const [editFolder, setEditFolder] = useState<EditFolderFormType>();
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false); const [showImportModal, setShowImportModal] = useState(false);
...@@ -50,6 +57,9 @@ const SkillPageContent = ({ MenuIcon }: { MenuIcon: JSX.Element }) => { ...@@ -50,6 +57,9 @@ const SkillPageContent = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
paths, paths,
folderDetail folderDetail
} = useContextSelector(SkillListContext, (v) => v); } = useContextSelector(SkillListContext, (v) => v);
const maxFolderDepth = feConfigs?.limit?.maxFolderDepth ?? DEFAULT_MAX_FOLDER_DEPTH;
const canCreateFolder = canCreateSubFolder(parentId, paths, maxFolderDepth);
const folderDepthLimitTip = t('common:folder_depth_limit_tip');
const { runAsync: onCreateFolder } = useRequest(postCreateSkillFolder, { const { runAsync: onCreateFolder } = useRequest(postCreateSkillFolder, {
onSuccess() { onSuccess() {
...@@ -116,14 +126,17 @@ const SkillPageContent = ({ MenuIcon }: { MenuIcon: JSX.Element }) => { ...@@ -116,14 +126,17 @@ const SkillPageContent = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
{hasCreatePer && ( {hasCreatePer && (
<> <>
<Button <MyTooltip label={canCreateFolder ? '' : folderDepthLimitTip}>
variant={'grayBase'} <Button
leftIcon={<MyIcon name={'common/addLight'} w={'18px'} mr={-1} />} variant={'grayBase'}
onClick={() => setEditFolder({})} leftIcon={<MyIcon name={'common/addLight'} w={'18px'} mr={-1} />}
px={5} onClick={() => setEditFolder({})}
> isDisabled={!canCreateFolder}
{t('common:Folder')} px={5}
</Button> >
{t('common:Folder')}
</Button>
</MyTooltip>
<Button <Button
variant={'grayBase'} variant={'grayBase'}
leftIcon={<MyIcon name={'common/importLight'} w={'14px'} />} leftIcon={<MyIcon name={'common/importLight'} w={'14px'} />}
......
...@@ -30,6 +30,11 @@ import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; ...@@ -30,6 +30,11 @@ import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import MyBox from '@fastgpt/web/components/common/MyBox'; import MyBox from '@fastgpt/web/components/common/MyBox';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import {
canCreateSubFolder,
DEFAULT_MAX_FOLDER_DEPTH,
normalizeParentId
} from '@fastgpt/global/common/parentFolder/depth';
import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; import { ReadRoleVal } from '@fastgpt/global/support/permission/constant';
const EditFolderModal = dynamic( const EditFolderModal = dynamic(
...@@ -42,7 +47,7 @@ const Dataset = () => { ...@@ -42,7 +47,7 @@ const Dataset = () => {
const { isPc } = useSystem(); const { isPc } = useSystem();
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { parentId } = router.query as { parentId: string }; const parentId = normalizeParentId(router.query.parentId);
const { const {
myDatasets, myDatasets,
...@@ -60,6 +65,9 @@ const Dataset = () => { ...@@ -60,6 +65,9 @@ const Dataset = () => {
} = useContextSelector(DatasetsContext, (v) => v); } = useContextSelector(DatasetsContext, (v) => v);
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const maxFolderDepth = feConfigs?.limit?.maxFolderDepth ?? DEFAULT_MAX_FOLDER_DEPTH;
const canCreateFolder = canCreateSubFolder(parentId, paths, maxFolderDepth);
const folderDepthLimitTip = t('common:folder_depth_limit_tip');
const { toast } = useToast(); const { toast } = useToast();
const [editFolderData, setEditFolderData] = useState<EditFolderFormType>(); const [editFolderData, setEditFolderData] = useState<EditFolderFormType>();
const [createDatasetType, setCreateDatasetType] = useState<CreateDatasetType>(); const [createDatasetType, setCreateDatasetType] = useState<CreateDatasetType>();
...@@ -218,6 +226,8 @@ const Dataset = () => { ...@@ -218,6 +226,8 @@ const Dataset = () => {
{ {
icon: FolderIcon, icon: FolderIcon,
label: t('common:Folder'), label: t('common:Folder'),
disabled: !canCreateFolder,
disabledTip: folderDepthLimitTip,
onClick: () => setEditFolderData({}) onClick: () => setEditFolderData({})
} }
] ]
......
...@@ -128,7 +128,8 @@ const defaultFeConfigs: FastGPTFeConfigsType = { ...@@ -128,7 +128,8 @@ const defaultFeConfigs: FastGPTFeConfigsType = {
exportDatasetLimitMinutes: 0, exportDatasetLimitMinutes: 0,
websiteSyncLimitMinuted: 0, websiteSyncLimitMinuted: 0,
agentSkillMaxUploadBytes: serviceEnv.AGENT_SKILL_MAX_UPLOAD_SIZE * 1024 * 1024, agentSkillMaxUploadBytes: serviceEnv.AGENT_SKILL_MAX_UPLOAD_SIZE * 1024 * 1024,
workflowParallelRunMaxConcurrency: serviceEnv.WORKFLOW_PARALLEL_MAX_CONCURRENCY workflowParallelRunMaxConcurrency: serviceEnv.WORKFLOW_PARALLEL_MAX_CONCURRENCY,
maxFolderDepth: serviceEnv.MAX_FOLDER_DEPTH
}, },
scripts: [], scripts: [],
favicon: '/favicon.ico', favicon: '/favicon.ico',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment