fix: 统一文档解析文件格式校验

- 统一知识库和工作流支持格式并增加前后端上传拦截

- 拒绝 XLS 与伪装 XLSX,避免空内容解析成功
This commit is contained in:
2026-09-02 19:15:01 +08:00
parent 198d592dd6
commit d7b0d442eb
24 changed files with 658 additions and 101 deletions

View File

@@ -0,0 +1,52 @@
import { shallowMount } from '@vue/test-utils';
import { describe, expect, it, vi } from 'vitest';
import ImportKnowledgeFileContainer from './ImportKnowledgeFileContainer.vue';
vi.mock('@easyflow/hooks', () => ({
useAppConfig: () => ({ apiURL: '' }),
}));
vi.mock('@easyflow/locales', () => ({
$t: (key: string, params?: Record<string, string>) =>
key === 'documentCollection.importDoc.batchUploadDescription'
? `支持 ${params?.types} 文件`
: key,
}));
vi.mock('@easyflow/stores', () => ({
useAccessStore: () => ({ accessToken: 'test-token' }),
}));
vi.mock('#/api/request', () => ({ api: {} }));
vi.mock('#/locales', () => ({
$t: (key: string) => key,
}));
vi.mock('element-plus/es/components/table-v2/index.mjs', () => ({
ElAutoResizer: { template: '<div><slot :height="300" :width="600" /></div>' },
ElTableV2: { template: '<div />' },
}));
vi.mock('element-plus/es/components/table-v2/style/css.mjs', () => ({}));
describe('import knowledge file container', () => {
it('uses the shared document formats for file and folder selection', () => {
const wrapper = shallowMount(ImportKnowledgeFileContainer, {
props: { batchMode: true },
});
const inputs = wrapper.findAll('input[type="file"]');
expect(inputs).toHaveLength(2);
for (const input of inputs) {
expect(input.attributes('accept')).toBe(
'.txt,.pdf,.docx,.md,.pptx,.xlsx,.csv',
);
}
expect(wrapper.text()).toContain(
'支持 TXT、PDF、DOCX、MD、PPTX、XLSX、CSV 文件',
);
});
});

View File

@@ -22,6 +22,7 @@ import {
import { formatFileSize } from '#/api/common/file';
import { api } from '#/api/request';
import DragFileUpload from '#/components/upload/DragFileUpload.vue';
import { DocumentParseFilePolicy } from '#/utils/document-parse-file-policy';
import { resolveDocumentUploadResponse } from './document-import-upload-response';
@@ -80,16 +81,6 @@ const emit = defineEmits<{
const MAX_FILE_COUNT = 2000;
const MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024;
const MAX_TOTAL_SIZE_BYTES = 1024 * 1024 * 1024;
const SUPPORTED_EXTENSIONS = new Set([
'csv',
'docx',
'md',
'pdf',
'pptx',
'txt',
'xlsx',
]);
const fileData = ref<LegacyFileInfo[]>([]);
const filesPath = ref<any[]>([]);
const dragUploadRef = ref<InstanceType<typeof DragFileUpload>>();
@@ -331,8 +322,7 @@ async function prepareBatch(files: File[]) {
ignoredCount++;
continue;
}
const extension = file.name.split('.').pop()?.toLowerCase() || '';
if (!SUPPORTED_EXTENSIONS.has(extension)) {
if (!DocumentParseFilePolicy.supports(file.name)) {
ignoredCount++;
continue;
}
@@ -344,7 +334,11 @@ async function prepareBatch(files: File[]) {
accepted.push(file);
}
if (accepted.length === 0) {
ElMessage.warning($t('documentCollection.importDoc.noSupportedFiles'));
ElMessage.warning(
$t('documentCollection.importDoc.noSupportedFiles', {
types: DocumentParseFilePolicy.supportedTypeLabel,
}),
);
return;
}
if (accepted.length > MAX_FILE_COUNT) {
@@ -356,7 +350,17 @@ async function prepareBatch(files: File[]) {
return;
}
if (ignoredCount > 0) {
ElMessage.info($t('documentCollection.importDoc.unsupportedSkipped'));
ElMessage.info(
$t('documentCollection.importDoc.unsupportedSkipped', {
types: DocumentParseFilePolicy.supportedTypeLabel,
}),
);
}
try {
await DocumentParseFilePolicy.validateFiles(accepted);
} catch (error: any) {
ElMessage.warning(error?.message || $t('message.notSupported'));
return;
}
batchFiles.value = await Promise.all(
@@ -538,7 +542,7 @@ async function createClientFileKey(relativePath: string) {
class="native-file-input"
type="file"
multiple
accept=".txt,.pdf,.docx,.md,.pptx,.xlsx,.csv"
:accept="DocumentParseFilePolicy.accept"
@change="handleNativeSelection"
/>
<input
@@ -546,6 +550,7 @@ async function createClientFileKey(relativePath: string) {
class="native-file-input"
type="file"
multiple
:accept="DocumentParseFilePolicy.accept"
webkitdirectory
@change="handleNativeSelection"
/>
@@ -564,7 +569,11 @@ async function createClientFileKey(relativePath: string) {
{{ $t('documentCollection.importDoc.batchUploadTitle') }}
</div>
<div class="batch-drop-zone__description">
{{ $t('documentCollection.importDoc.batchUploadDescription') }}
{{
$t('documentCollection.importDoc.batchUploadDescription', {
types: DocumentParseFilePolicy.supportedTypeLabel,
})
}}
</div>
<ElButton
:icon="FolderOpened"