feat: 将工作流与 FAQ 上传上限扩至 20MB

- 同步管理端、用户中心与后端工作流文件校验

- 扩展 FAQ 普通及分享上传限制并补充边界测试
This commit is contained in:
2026-07-30 17:39:13 +08:00
parent 2abff304ed
commit 100d744c25
12 changed files with 93 additions and 16 deletions

View File

@@ -47,7 +47,7 @@ import java.util.Set;
@UsePermission(moduleName = "/api/v1/documentCollection") @UsePermission(moduleName = "/api/v1/documentCollection")
public class FaqItemController extends BaseCurdController<FaqItemService, FaqItem> { public class FaqItemController extends BaseCurdController<FaqItemService, FaqItem> {
private static final long MAX_IMAGE_SIZE_BYTES = 5L * 1024L * 1024L; private static final long MAX_IMAGE_SIZE_BYTES = 20L * 1024L * 1024L;
private static final Set<String> ALLOWED_IMAGE_TYPES = new HashSet<>(Arrays.asList( private static final Set<String> ALLOWED_IMAGE_TYPES = new HashSet<>(Arrays.asList(
"image/jpeg", "image/jpeg",
"image/png", "image/png",
@@ -215,7 +215,7 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
throw new BusinessException("图片不能为空"); throw new BusinessException("图片不能为空");
} }
if (file.getSize() > MAX_IMAGE_SIZE_BYTES) { if (file.getSize() > MAX_IMAGE_SIZE_BYTES) {
throw new BusinessException("图片大小不能超过5MB"); throw new BusinessException("图片大小不能超过20MB");
} }
if (!isAllowedImageType(file)) { if (!isAllowedImageType(file)) {
throw new BusinessException("仅支持 JPG/PNG/WEBP/GIF 图片"); throw new BusinessException("仅支持 JPG/PNG/WEBP/GIF 图片");

View File

@@ -75,7 +75,7 @@ import java.util.Set;
@RequestMapping("/api/v1/share/knowledge") @RequestMapping("/api/v1/share/knowledge")
public class ShareKnowledgeController { public class ShareKnowledgeController {
private static final long MAX_IMAGE_SIZE_BYTES = 5L * 1024L * 1024L; private static final long MAX_IMAGE_SIZE_BYTES = 20L * 1024L * 1024L;
private static final Set<String> ALLOWED_IMAGE_TYPES = new HashSet<>(Arrays.asList( private static final Set<String> ALLOWED_IMAGE_TYPES = new HashSet<>(Arrays.asList(
"image/jpeg", "image/jpeg",
"image/png", "image/png",
@@ -785,7 +785,7 @@ public class ShareKnowledgeController {
throw new BusinessException("图片不能为空"); throw new BusinessException("图片不能为空");
} }
if (file.getSize() > MAX_IMAGE_SIZE_BYTES) { if (file.getSize() > MAX_IMAGE_SIZE_BYTES) {
throw new BusinessException("图片大小不能超过5MB"); throw new BusinessException("图片大小不能超过20MB");
} }
if (!isAllowedImageType(file)) { if (!isAllowedImageType(file)) {
throw new BusinessException("仅支持 JPG/PNG/WEBP/GIF 图片"); throw new BusinessException("仅支持 JPG/PNG/WEBP/GIF 图片");

View File

@@ -35,7 +35,7 @@ public class WorkflowRunningParameterResolver {
private static final String DEFAULT_START_FORM_DESCRIPTION = "请先补充必要信息,再开始执行工作流。"; private static final String DEFAULT_START_FORM_DESCRIPTION = "请先补充必要信息,再开始执行工作流。";
private static final String DEFAULT_START_FORM_SUBMIT_TEXT = "开始"; private static final String DEFAULT_START_FORM_SUBMIT_TEXT = "开始";
private static final int FILE_MAX_COUNT = 10; private static final int FILE_MAX_COUNT = 10;
private static final long FILE_MAX_SINGLE_SIZE = 5L * 1024 * 1024; private static final long FILE_MAX_SINGLE_SIZE = 20L * 1024 * 1024;
private static final long FILE_MAX_TOTAL_SIZE = 50L * 1024 * 1024; private static final long FILE_MAX_TOTAL_SIZE = 50L * 1024 * 1024;
@Resource @Resource
@@ -363,7 +363,7 @@ public class WorkflowRunningParameterResolver {
} }
Long size = parseLong(fileMap.get("size")); Long size = parseLong(fileMap.get("size"));
if (size != null && size > FILE_MAX_SINGLE_SIZE) { if (size != null && size > FILE_MAX_SINGLE_SIZE) {
throw new BusinessException("文件参数 " + parameterName + " 中单个文件不能超过 5MB"); throw new BusinessException("文件参数 " + parameterName + " 中单个文件不能超过 20MB");
} }
if (size != null && size > 0) { if (size != null && size > 0) {
totalSize += size; totalSize += size;

View File

@@ -34,7 +34,7 @@ import java.util.Set;
@Component @Component
public class DocNodeFileContentExtractor { public class DocNodeFileContentExtractor {
private static final int FILE_MAX_COUNT = 10; private static final int FILE_MAX_COUNT = 10;
private static final long FILE_MAX_SINGLE_SIZE = 5L * 1024 * 1024; private static final long FILE_MAX_SINGLE_SIZE = 20L * 1024 * 1024;
private static final long FILE_MAX_TOTAL_SIZE = 50L * 1024 * 1024; private static final long FILE_MAX_TOTAL_SIZE = 50L * 1024 * 1024;
private final DocumentParseBridgeService documentParseBridgeService; private final DocumentParseBridgeService documentParseBridgeService;
@@ -145,7 +145,7 @@ public class DocNodeFileContentExtractor {
} }
Long size = sourceRef.getSize(); Long size = sourceRef.getSize();
if (size != null && size > FILE_MAX_SINGLE_SIZE) { if (size != null && size > FILE_MAX_SINGLE_SIZE) {
throw new BusinessException("单个文件不能超过 5MB: " + sourceRef.getFileName()); throw new BusinessException("单个文件不能超过 20MB: " + sourceRef.getFileName());
} }
if (size != null && size > 0) { if (size != null && size > 0) {
totalSize += size; totalSize += size;

View File

@@ -8,6 +8,7 @@ import org.junit.Test;
import tech.easyflow.ai.entity.Workflow; import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.node.SearchDatasetNodeParser; import tech.easyflow.ai.node.SearchDatasetNodeParser;
import tech.easyflow.ai.node.WorkflowNodeParser; import tech.easyflow.ai.node.WorkflowNodeParser;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.math.BigInteger; import java.math.BigInteger;
@@ -178,6 +179,40 @@ public class WorkflowRunningParameterResolverTest {
Assert.assertEquals(2, ((List<?>) attachments).size()); Assert.assertEquals(2, ((List<?>) attachments).size());
} }
/**
* 文件参数应允许 20MB 边界值,并拒绝超过边界的文件。
*
* @throws Exception 反射注入失败
*/
@Test
public void testNormalizeRuntimeVariablesShouldEnforceTwentyMbSingleFileLimit() throws Exception {
WorkflowRunningParameterResolver resolver = newResolver();
Map<String, Object> variables = new LinkedHashMap<>();
variables.put("attachments", fileValue(
"accepted.pdf",
"/files/accepted.pdf",
20L * 1024L * 1024L
));
Map<String, Object> normalized = resolver.normalizeRuntimeVariables(
workflowContentWithStartParameters(),
variables
);
Assert.assertEquals(1, ((List<?>) normalized.get("attachments")).size());
variables.put("attachments", fileValue(
"oversized.pdf",
"/files/oversized.pdf",
20L * 1024L * 1024L + 1L
));
try {
resolver.normalizeRuntimeVariables(workflowContentWithStartParameters(), variables);
Assert.fail("expected BusinessException");
} catch (BusinessException exception) {
Assert.assertEquals("文件参数 attachments 中单个文件不能超过 20MB", exception.getMessage());
}
}
private static WorkflowRunningParameterResolver newResolver() throws Exception { private static WorkflowRunningParameterResolver newResolver() throws Exception {
WorkflowRunningParameterResolver resolver = new WorkflowRunningParameterResolver(); WorkflowRunningParameterResolver resolver = new WorkflowRunningParameterResolver();
ChainParser parser = ChainParser.builder() ChainParser parser = ChainParser.builder()

View File

@@ -263,6 +263,38 @@ public class DocNodeFileContentExtractorTest {
} }
} }
/**
* 验证文档节点允许 20MB 边界值,并拒绝超过边界的文件。
*/
@Test
public void shouldEnforceTwentyMbSingleFileLimit() {
DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
new RecordingDocumentParseBridgeService(),
new FakeFileStorageService(),
new FakeReaderManager("plain text")
);
Map<String, Object> accepted = buildFileValue(
"accepted.pdf",
"/files/accepted.pdf",
"application/pdf"
);
accepted.put("size", 20L * 1024L * 1024L);
Assert.assertEquals(1, extractor.toDocumentSourceRefs(accepted).size());
Map<String, Object> oversized = buildFileValue(
"oversized.pdf",
"/files/oversized.pdf",
"application/pdf"
);
oversized.put("size", 20L * 1024L * 1024L + 1L);
try {
extractor.toDocumentSourceRefs(oversized);
Assert.fail("expected BusinessException");
} catch (BusinessException exception) {
Assert.assertEquals("单个文件不能超过 20MB: oversized.pdf", exception.getMessage());
}
}
private Map<String, Object> buildFileValue(String fileName, String filePath, String contentType) { private Map<String, Object> buildFileValue(String fileName, String filePath, String contentType) {
Map<String, Object> value = new HashMap<String, Object>(); Map<String, Object> value = new HashMap<String, Object>();
value.put("fileName", fileName); value.put("fileName", fileName);

View File

@@ -179,7 +179,7 @@
"answerRequired": "Answer is required", "answerRequired": "Answer is required",
"collectionRequired": "Collection id is required", "collectionRequired": "Collection id is required",
"imageTypeInvalid": "Only JPG/PNG/WEBP/GIF images are supported", "imageTypeInvalid": "Only JPG/PNG/WEBP/GIF images are supported",
"imageSizeExceeded": "Image size must be less than 5MB", "imageSizeExceeded": "Image size must be less than 20MB",
"imageUploadFailed": "Image upload failed", "imageUploadFailed": "Image upload failed",
"import": { "import": {
"title": "Import FAQ", "title": "Import FAQ",

View File

@@ -179,7 +179,7 @@
"answerRequired": "答案不能为空", "answerRequired": "答案不能为空",
"collectionRequired": "知识库ID不能为空", "collectionRequired": "知识库ID不能为空",
"imageTypeInvalid": "仅支持 JPG/PNG/WEBP/GIF 图片", "imageTypeInvalid": "仅支持 JPG/PNG/WEBP/GIF 图片",
"imageSizeExceeded": "图片大小不能超过5MB", "imageSizeExceeded": "图片大小不能超过20MB",
"imageUploadFailed": "图片上传失败", "imageUploadFailed": "图片上传失败",
"import": { "import": {
"title": "导入FAQ", "title": "导入FAQ",

View File

@@ -45,7 +45,7 @@ const props = defineProps({
}); });
const emit = defineEmits(['submit', 'update:modelValue']); const emit = defineEmits(['submit', 'update:modelValue']);
const MAX_IMAGE_SIZE = 5 * 1024 * 1024; const MAX_IMAGE_SIZE = 20 * 1024 * 1024;
const ALLOWED_IMAGE_TYPES = new Set([ const ALLOWED_IMAGE_TYPES = new Set([
'image/gif', 'image/gif',
'image/jpeg', 'image/jpeg',

View File

@@ -127,11 +127,21 @@ describe('workflowFileValue', () => {
}); });
it('上传前校验单文件大小限制', () => { it('上传前校验单文件大小限制', () => {
const oversizedFile = new File([new Uint8Array(6 * 1024 * 1024)], 'large.pdf'); const acceptedFile = new File(
[new Uint8Array(20 * 1024 * 1024)],
'accepted.pdf',
);
const oversizedFile = new File(
[new Uint8Array(20 * 1024 * 1024 + 1)],
'large.pdf',
);
expect(() => expect(() =>
validateWorkflowFileSelection([], [oversizedFile]), validateWorkflowFileSelection([], [acceptedFile]),
).toThrow('单个文件不能超过 5.0 MB'); ).not.toThrow();
expect(() => validateWorkflowFileSelection([], [oversizedFile])).toThrow(
'单个文件不能超过 20.0 MB',
);
}); });
it('上传前校验总大小限制', () => { it('上传前校验总大小限制', () => {

View File

@@ -21,7 +21,7 @@ export interface WorkflowResourceLike {
export const WORKFLOW_FILE_LIMITS = { export const WORKFLOW_FILE_LIMITS = {
maxCount: 10, maxCount: 10,
maxSingleSize: 5 * 1024 * 1024, maxSingleSize: 20 * 1024 * 1024,
maxTotalSize: 50 * 1024 * 1024, maxTotalSize: 50 * 1024 * 1024,
} as const; } as const;

View File

@@ -21,7 +21,7 @@ export interface WorkflowResourceLike {
export const WORKFLOW_FILE_LIMITS = { export const WORKFLOW_FILE_LIMITS = {
maxCount: 10, maxCount: 10,
maxSingleSize: 5 * 1024 * 1024, maxSingleSize: 20 * 1024 * 1024,
maxTotalSize: 50 * 1024 * 1024, maxTotalSize: 50 * 1024 * 1024,
} as const; } as const;