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 {
fileNotFound = 'fileNotFound',
unAuthFile = 'unAuthFile',
missingParams = 'missingParams',
inheritPermissionError = 'inheritPermissionError'
inheritPermissionError = 'inheritPermissionError',
folderDepthLimit = 'folderDepthLimit',
folderMoveDepthLimit = 'folderMoveDepthLimit'
}
const datasetErr = [
{
......@@ -35,6 +37,14 @@ const datasetErr = [
{
statusText: CommonErrEnum.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) => {
......
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 = {
agentSandboxMaxSessionRuntime?: number;
agentSkillMaxUploadBytes?: number;
workflowParallelRunMaxConcurrency?: number;
maxFolderDepth?: 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) => {
config.feConfigs.uploadFileMaxAmount = serviceEnv.UPLOAD_FILE_MAX_AMOUNT;
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;
......
import { createEnv } from '@t3-oss/env-core';
import z from 'zod';
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';
const defaultableIntSchema = (defaultValue: number) =>
......@@ -9,6 +10,7 @@ const defaultableIntSchema = (defaultValue: number) =>
z.coerce.number<number>().int().nonnegative()
);
// 系统最大字符串处理长度
const SYSTEM_STRING_LENGTH_UNIT = 1_000_000;
/**
......@@ -277,6 +279,9 @@ export const serviceEnv = createEnv({
SERVICE_REQUEST_MAX_CONTENT_LENGTH: IntSchema.default(10).meta({
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({
description: '应用文件夹最大数量'
}),
......
......@@ -13,6 +13,7 @@ import type { IconNameType } from '../Icon/type';
import { useSystem } from '../../../hooks/useSystem';
import Avatar from '../Avatar';
import MyPopover from '../MyPopover';
import MyTooltip from '../MyTooltip';
export type MenuItemType = 'primary' | 'danger' | 'gray' | 'grayBg';
......@@ -29,6 +30,8 @@ export type MenuItemData = {
onClick?: () => any;
menuItemStyles?: MenuItemProps;
menuList?: MenuItemData[];
disabled?: boolean;
disabledTip?: string;
}>;
};
......@@ -204,13 +207,19 @@ const MenuItem = ({
<Box
px={3}
py={2}
cursor="pointer"
cursor={item.disabled ? 'not-allowed' : 'pointer'}
opacity={item.disabled ? 0.6 : 1}
borderRadius="md"
_hover={{
bg: 'primary.50',
color: 'primary.600'
}}
_hover={
item.disabled
? {}
: {
bg: 'primary.50',
color: 'primary.600'
}
}
onClick={(e) => {
if (item.disabled) return;
if (item.onClick) {
item.onClick();
}
......@@ -235,7 +244,7 @@ const MenuItem = ({
>
{item.label}
</Box>
{item.description && (
{item.description && !item.disabled && (
<Box color={'myGray.500'} fontSize={'mini'}>
{item.description}
</Box>
......@@ -297,6 +306,16 @@ const MultipleMenu = (props: Props) => {
</Box>
)}
{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 (
<Box key={index}>
{item.menuList ? (
......@@ -306,15 +325,11 @@ const MultipleMenu = (props: Props) => {
trigger={'hover'}
menuList={item.menuList}
onClose={onCloseFn}
Trigger={
<Box>
<MenuItem item={item} size={size} onClose={onCloseFn} />
</Box>
}
Trigger={<Box>{menuItem}</Box>}
hasArrow
/>
) : (
<MenuItem item={item} size={size} onClose={onCloseFn} />
menuItem
)}
</Box>
);
......
......@@ -361,7 +361,7 @@ const MyMenu = ({
if (child.disabled && child.disabledTip) {
return (
<MyTooltip shouldWrapChildren={false} key={index} label={child.disabledTip}>
{menuItem}
<Box>{menuItem}</Box>
</MyTooltip>
);
}
......
......@@ -46,6 +46,7 @@
"File": "File",
"Finish": "Finish",
"Folder": "Folder",
"folder_depth_limit_tip": "Maximum folder depth reached",
"FullScreen": "FullScreen",
"FullScreenLight": "FullScreenLight",
"Import": "Import",
......@@ -789,6 +790,8 @@
"error.fileNotFound": "File not found~",
"error.file_upload_disabled": "File upload is disabled for the current app",
"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.llm_track_expired": "Request details expired",
"error.missingParams": "Insufficient parameters",
......
......@@ -46,6 +46,7 @@
"File": "文件",
"Finish": "完成",
"Folder": "文件夹",
"folder_depth_limit_tip": "已达到最大目录深度",
"FullScreen": "全屏",
"FullScreenLight": "全屏预览",
"Import": "导入",
......@@ -789,6 +790,8 @@
"error.fileNotFound": "文件找不到了~",
"error.file_upload_disabled": "当前应用未开启文件上传",
"error.inheritPermissionError": "权限继承错误",
"error.folderDepthLimit": "文件夹层级已满,无法在此创建子文件夹",
"error.folderMoveDepthLimit": "无法移入:该位置会使文件夹超出层级上限",
"error.invalid_params": "参数无效",
"error.llm_track_expired": "请求详情已过期",
"error.missingParams": "参数缺失",
......
......@@ -45,6 +45,7 @@
"File": "檔案",
"Finish": "完成",
"Folder": "資料夾",
"folder_depth_limit_tip": "已達到最大目錄深度",
"FullScreen": "全屏",
"FullScreenLight": "全屏預覽",
"Import": "匯入",
......@@ -783,6 +784,8 @@
"error.fileNotFound": "找不到檔案",
"error.file_upload_disabled": "當前應用未開啟文件上傳",
"error.inheritPermissionError": "繼承權限錯誤",
"error.folderDepthLimit": "資料夾層級已滿,無法在此建立子資料夾",
"error.folderMoveDepthLimit": "無法移入:該位置會使資料夾超出層級上限",
"error.invalid_params": "參數無效",
"error.llm_track_expired": "請求詳情已過期",
"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 { useBoolean } from 'ahooks';
......@@ -19,7 +19,7 @@ export const useFolderDrag = ({
draggable: true,
userSelect: 'none' as any,
'data-drag-id': isFolder ? dataId : undefined,
onDragStart: (e: DragEvent<HTMLDivElement>) => {
onDragStart: () => {
setDragId(dataId);
},
onDragOver: (e: DragEvent<HTMLDivElement>) => {
......@@ -41,7 +41,7 @@ export const useFolderDrag = ({
if (targetId && dragId && targetId !== dragId) {
await onDrop(dragId, targetId);
}
} catch (error) {}
} catch {}
setTargetId(undefined);
setDragId(undefined);
......
......@@ -4,6 +4,7 @@ import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { getSkillList, getSkillFolderPath, getSkillDetail } from '@/web/core/skill/api';
import type { ListSkillsResponse } from '@fastgpt/global/core/ai/skill/api';
import type { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { normalizeParentId } from '@fastgpt/global/common/parentFolder/depth';
import { useRouter } from 'next/router';
import type { SkillPermission } from '@fastgpt/global/support/permission/skill/controller';
......@@ -46,10 +47,7 @@ export const SkillListContext = createContext<SkillListContextType>({
const SkillListContextProvider = ({ children }: { children: ReactNode }) => {
const router = useRouter();
// 归一化 parentId:非空字符串时取值,否则为 null
const rawParentId = router.query.parentId;
const parentId: string | null =
typeof rawParentId === 'string' && rawParentId.length > 0 ? rawParentId : null;
const parentId = normalizeParentId(router.query.parentId);
const [searchKey, setSearchKey] = useState('');
......
......@@ -52,9 +52,9 @@ function List() {
searchKey,
setSearchKey
} = useContextSelector(DatasetsContext, (v) => v);
const [editPerDatasetId, setEditPerDatasetId] = useState<string>();
const router = useRouter();
const { parentId = null } = router.query as { parentId?: string | null };
const [editPerDatasetId, setEditPerDatasetId] = useState<string>();
const formatDatasets = useMemo(
() =>
......
......@@ -11,6 +11,7 @@ import {
type ParentIdType,
type ParentTreePathItemType
} from '@fastgpt/global/common/parentFolder/type';
import { normalizeParentId } from '@fastgpt/global/common/parentFolder/depth';
import { useRouter } from 'next/router';
import React, { useCallback, useState } from 'react';
import { createContext } from 'use-context-selector';
......@@ -71,7 +72,7 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) {
const { t } = useTranslation();
const [moveDatasetId, setMoveDatasetId] = useState<string>();
const [searchKey, setSearchKey] = useState('');
const { parentId = null } = router.query as { parentId?: string | null };
const parentId = normalizeParentId(router.query.parentId);
const {
data: myDatasets = [],
......@@ -99,10 +100,13 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) {
);
const { data: paths = [], runAsync: refetchPaths } = useRequest(
async () => getDatasetPaths({ sourceId: parentId, type: 'current' }),
async () => {
if (!parentId) return [];
return getDatasetPaths({ sourceId: parentId, type: 'current' });
},
{
manual: false,
refreshDeps: [folderDetail]
refreshDeps: [parentId]
}
);
......
......@@ -18,6 +18,8 @@ import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { TeamSkillCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
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>) {
const { name, description, parentId } = parseApiInput({
......@@ -45,6 +47,8 @@ async function handler(req: ApiRequestProps<CreateSkillFolderBody>) {
per: TeamSkillCreatePermissionVal
});
await checkCreateFolderDepth({ parentId, teamId, model: MongoAgentSkills });
// Create the folder within a transaction and copy collaborators from parent
const folderId = await mongoSessionRun(async (session) => {
const folder = await createSkillFolder(
......
......@@ -32,6 +32,7 @@ import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { isValidObjectId } from 'mongoose';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar';
import { checkMoveFolderDepth } from '@fastgpt/service/common/parentFolder/depth';
async function handler(req: ApiRequestProps<UpdateSkillBody>) {
const { skillId, name, description, category, avatar, parentId } = parseApiInput({
......@@ -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) {
// Field validation for normal update
if (name !== undefined) {
......
......@@ -18,6 +18,7 @@ import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { checkTeamAppTypeLimit } from '@fastgpt/service/support/permission/teamLimit';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import { checkCreateFolderDepth } from '@fastgpt/service/common/parentFolder/depth';
import {
CreateAppFolderBodySchema,
CreateAppFolderResponseSchema,
......@@ -48,6 +49,8 @@ async function handler(
await checkTeamAppTypeLimit({ teamId, appCheckType: 'folder' });
await checkCreateFolderDepth({ parentId, teamId, model: MongoApp });
// Create app
await mongoSessionRun(async (session) => {
const app = await MongoApp.create({
......
......@@ -28,6 +28,7 @@ import { i18nT } from '@fastgpt/global/common/i18n/utils';
import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar';
import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import { checkMoveFolderDepth } from '@fastgpt/service/common/parentFolder/depth';
import {
UpdateAppBodySchema,
UpdateAppQuerySchema,
......@@ -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) => {
// format nodes data
// 1. dataset search limit, less than model quoteMaxToken
......
......@@ -17,6 +17,7 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import { checkCreateFolderDepth } from '@fastgpt/service/common/parentFolder/depth';
import {
CreateDatasetFolderBodySchema,
type CreateDatasetFolderBody
......@@ -45,6 +46,8 @@ async function handler(req: ApiRequestProps<CreateDatasetFolderBody>) {
await checkTeamDatasetFolderLimit({ teamId });
await checkCreateFolderDepth({ parentId, teamId, model: MongoDataset });
await mongoSessionRun(async (session) => {
const dataset = await MongoDataset.create({
...parseParentIdInMongo(parentId),
......
......@@ -42,6 +42,8 @@ import { computedCollectionChunkSettings } from '@fastgpt/global/core/dataset/tr
import { getResourceOwnedClbs } from '@fastgpt/service/support/permission/controller';
import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar';
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
// (2) 目标目录的管理权限
// (3) 如果从根目录移动或移动到根目录,需要有团队的应用创建权限
async function handler(req: ApiRequestProps<UpdateDatasetBody>) {
let {
id,
parentId,
name,
avatar,
intro,
agentModel,
vlmModel,
websiteConfig,
externalReadUrl,
apiDatasetServer,
autoSync,
chunkSettings
} = UpdateDatasetBodySchema.parse(req.body);
const {
body: {
id,
parentId,
name,
avatar,
intro,
agentModel,
vlmModel,
websiteConfig,
externalReadUrl,
apiDatasetServer,
autoSync,
chunkSettings: rawChunkSettings
}
} = parseApiInput({
req,
bodySchema: UpdateDatasetBodySchema
});
if (websiteConfig?.url) {
if (await isInternalAddress(websiteConfig.url)) {
......@@ -87,9 +94,9 @@ async function handler(req: ApiRequestProps<UpdateDatasetBody>) {
let targetName = '';
chunkSettings = chunkSettings
const chunkSettings = rawChunkSettings
? computedCollectionChunkSettings({
...chunkSettings,
...rawChunkSettings,
llmModel: getLLMModel(dataset.agentModel),
vectorModel: getEmbeddingModel(dataset.vectorModel)
})
......@@ -130,6 +137,16 @@ async function handler(req: ApiRequestProps<UpdateDatasetBody>) {
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;
updateTraining({
......
......@@ -29,9 +29,15 @@ import { getUtmWorkflow } from '@/web/support/marketing/utils';
import { useMount } from 'ahooks';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
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 { ReadRoleVal } from '@fastgpt/global/support/permission/constant';
import TemplateCreatePanel from '@/pageComponents/dashboard/agent/TemplateCreatePanel';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const EditFolderModal = dynamic(
() => import('@fastgpt/web/components/common/MyModal/EditFolderModal')
......@@ -57,6 +63,10 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
} = useContextSelector(AppListContext, (v) => v);
const [editFolder, setEditFolder] = useState<EditFolderFormType>();
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 {
isOpen: isOpenJsonImportModal,
......@@ -149,14 +159,17 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
{hasCreatePer && (
<>
<Button
variant={'grayBase'}
leftIcon={<MyIcon name={'common/addLight'} w={'18px'} mr={-1} />}
onClick={() => setEditFolder({})}
px={5}
>
{t('common:Folder')}
</Button>
<MyTooltip label={canCreateFolder ? '' : folderDepthLimitTip}>
<Button
variant={'grayBase'}
leftIcon={<MyIcon name={'common/addLight'} w={'18px'} mr={-1} />}
onClick={() => setEditFolder({})}
isDisabled={!canCreateFolder}
px={5}
>
{t('common:Folder')}
</Button>
</MyTooltip>
<Button
variant={'grayBase'}
leftIcon={<MyIcon name={'common/importLight'} w={'14px'} />}
......
......@@ -14,6 +14,11 @@ import SkillListContextProvider, {
} from '@/pageComponents/dashboard/skill/context';
import List from '@/pageComponents/dashboard/skill/List';
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 { postCreateSkillFolder } from '@/web/core/skill/api';
import dynamic from 'next/dynamic';
......@@ -22,6 +27,7 @@ import FolderPath from '@/components/common/folder/Path';
import { useRouter } from 'next/router';
import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type';
import { useSkillSandboxOperationGuard } from '@/components/core/skill/useSkillSandboxOperationGuard';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const EditFolderModal = dynamic(
() => import('@fastgpt/web/components/common/MyModal/EditFolderModal')
......@@ -34,6 +40,7 @@ const SkillPageContent = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
const router = useRouter();
const { isPc } = useSystem();
const { userInfo } = useUserStore();
const { feConfigs } = useSystemStore();
const [editFolder, setEditFolder] = useState<EditFolderFormType>();
const [showCreateModal, setShowCreateModal] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
......@@ -50,6 +57,9 @@ const SkillPageContent = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
paths,
folderDetail
} = 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, {
onSuccess() {
......@@ -116,14 +126,17 @@ const SkillPageContent = ({ MenuIcon }: { MenuIcon: JSX.Element }) => {
{hasCreatePer && (
<>
<Button
variant={'grayBase'}
leftIcon={<MyIcon name={'common/addLight'} w={'18px'} mr={-1} />}
onClick={() => setEditFolder({})}
px={5}
>
{t('common:Folder')}
</Button>
<MyTooltip label={canCreateFolder ? '' : folderDepthLimitTip}>
<Button
variant={'grayBase'}
leftIcon={<MyIcon name={'common/addLight'} w={'18px'} mr={-1} />}
onClick={() => setEditFolder({})}
isDisabled={!canCreateFolder}
px={5}
>
{t('common:Folder')}
</Button>
</MyTooltip>
<Button
variant={'grayBase'}
leftIcon={<MyIcon name={'common/importLight'} w={'14px'} />}
......
......@@ -30,6 +30,11 @@ import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { useToast } from '@fastgpt/web/hooks/useToast';
import MyBox from '@fastgpt/web/components/common/MyBox';
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';
const EditFolderModal = dynamic(
......@@ -42,7 +47,7 @@ const Dataset = () => {
const { isPc } = useSystem();
const { t } = useTranslation();
const router = useRouter();
const { parentId } = router.query as { parentId: string };
const parentId = normalizeParentId(router.query.parentId);
const {
myDatasets,
......@@ -60,6 +65,9 @@ const Dataset = () => {
} = useContextSelector(DatasetsContext, (v) => v);
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 { toast } = useToast();
const [editFolderData, setEditFolderData] = useState<EditFolderFormType>();
const [createDatasetType, setCreateDatasetType] = useState<CreateDatasetType>();
......@@ -218,6 +226,8 @@ const Dataset = () => {
{
icon: FolderIcon,
label: t('common:Folder'),
disabled: !canCreateFolder,
disabledTip: folderDepthLimitTip,
onClick: () => setEditFolderData({})
}
]
......
......@@ -128,7 +128,8 @@ const defaultFeConfigs: FastGPTFeConfigsType = {
exportDatasetLimitMinutes: 0,
websiteSyncLimitMinuted: 0,
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: [],
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