@lobehub/chat 1.142.2 → 1.142.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/changelog/v1.json +9 -0
- package/locales/ar/chat.json +4 -4
- package/locales/ar/file.json +1 -0
- package/locales/ar/models.json +1 -1
- package/locales/bg-BG/chat.json +4 -4
- package/locales/bg-BG/file.json +1 -0
- package/locales/bg-BG/models.json +1 -1
- package/locales/de-DE/chat.json +4 -4
- package/locales/de-DE/file.json +1 -0
- package/locales/de-DE/models.json +1 -1
- package/locales/en-US/chat.json +4 -4
- package/locales/en-US/file.json +1 -0
- package/locales/en-US/models.json +1 -1
- package/locales/es-ES/chat.json +4 -4
- package/locales/es-ES/file.json +1 -0
- package/locales/es-ES/models.json +1 -1
- package/locales/fa-IR/chat.json +4 -4
- package/locales/fa-IR/file.json +1 -0
- package/locales/fa-IR/models.json +1 -1
- package/locales/fr-FR/chat.json +4 -4
- package/locales/fr-FR/file.json +1 -0
- package/locales/fr-FR/models.json +1 -1
- package/locales/it-IT/chat.json +4 -4
- package/locales/it-IT/file.json +1 -0
- package/locales/ja-JP/chat.json +4 -4
- package/locales/ja-JP/file.json +1 -0
- package/locales/ja-JP/models.json +1 -1
- package/locales/ko-KR/chat.json +4 -4
- package/locales/ko-KR/file.json +1 -0
- package/locales/ko-KR/models.json +1 -1
- package/locales/nl-NL/chat.json +4 -4
- package/locales/nl-NL/file.json +1 -0
- package/locales/nl-NL/models.json +1 -1
- package/locales/pl-PL/chat.json +4 -4
- package/locales/pl-PL/file.json +1 -0
- package/locales/pl-PL/models.json +1 -1
- package/locales/pt-BR/chat.json +4 -4
- package/locales/pt-BR/file.json +1 -0
- package/locales/ru-RU/chat.json +4 -4
- package/locales/ru-RU/file.json +1 -0
- package/locales/ru-RU/models.json +1 -1
- package/locales/tr-TR/chat.json +4 -4
- package/locales/tr-TR/file.json +1 -0
- package/locales/tr-TR/models.json +1 -1
- package/locales/vi-VN/chat.json +4 -4
- package/locales/vi-VN/file.json +1 -0
- package/locales/vi-VN/models.json +1 -1
- package/locales/zh-CN/chat.json +4 -4
- package/locales/zh-CN/file.json +1 -0
- package/locales/zh-TW/chat.json +4 -4
- package/locales/zh-TW/file.json +1 -0
- package/locales/zh-TW/models.json +1 -1
- package/package.json +3 -2
- package/packages/const/src/file.ts +2 -0
- package/packages/obervability-otel/package.json +5 -5
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/CreateNewModelModal/index.tsx +7 -0
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelConfigModal/index.tsx +7 -0
- package/src/features/FileManager/FileList/FileListItem/index.tsx +3 -2
- package/src/features/FileManager/FileList/MasonryFileItem/MasonryItemWrapper.tsx +1 -1
- package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +2 -4
- package/src/features/FileManager/FileList/MasonrySkeleton.tsx +11 -5
- package/src/features/FileManager/FileList/ToolBar/MultiSelectActions.tsx +1 -1
- package/src/features/FileManager/FileList/index.tsx +51 -9
- package/src/locales/default/chat.ts +4 -4
- package/src/locales/default/file.ts +1 -0
- package/src/store/file/slices/fileManager/action.test.ts +136 -1
- package/src/store/file/slices/fileManager/action.ts +30 -8
- package/src/utils/unzipFile.test.ts +128 -0
- package/src/utils/unzipFile.ts +122 -0
- package/vitest.config.mts +1 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import pMap from 'p-map';
|
|
1
2
|
import { SWRResponse, mutate } from 'swr';
|
|
2
3
|
import { StateCreator } from 'zustand/vanilla';
|
|
3
4
|
|
|
4
|
-
import { FILE_UPLOAD_BLACKLIST } from '@/const/file';
|
|
5
|
+
import { FILE_UPLOAD_BLACKLIST, MAX_UPLOAD_FILE_COUNT } from '@/const/file';
|
|
5
6
|
import { useClientDataSWR } from '@/libs/swr';
|
|
6
7
|
import { fileService } from '@/services/file';
|
|
7
8
|
import { ServerService } from '@/services/file/server';
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
} from '@/store/file/reducers/uploadFileList';
|
|
13
14
|
import { FileListItem, QueryFileListParams } from '@/types/files';
|
|
14
15
|
import { isChunkingUnsupported } from '@/utils/isChunkingUnsupported';
|
|
16
|
+
import { unzipFile } from '@/utils/unzipFile';
|
|
15
17
|
|
|
16
18
|
import { FileStore } from '../../store';
|
|
17
19
|
import { fileManagerSelectors } from './selectors';
|
|
@@ -89,18 +91,37 @@ export const createFileManageSlice: StateCreator<
|
|
|
89
91
|
pushDockFileList: async (rawFiles, knowledgeBaseId) => {
|
|
90
92
|
const { dispatchDockFileList } = get();
|
|
91
93
|
|
|
92
|
-
// 0.
|
|
93
|
-
const
|
|
94
|
+
// 0. Process ZIP files and extract their contents
|
|
95
|
+
const filesToUpload: File[] = [];
|
|
96
|
+
for (const file of rawFiles) {
|
|
97
|
+
if (file.type === 'application/zip' || file.name.endsWith('.zip')) {
|
|
98
|
+
try {
|
|
99
|
+
const extractedFiles = await unzipFile(file);
|
|
100
|
+
filesToUpload.push(...extractedFiles);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error('Failed to extract ZIP file:', error);
|
|
103
|
+
// If extraction fails, treat it as a regular file
|
|
104
|
+
filesToUpload.push(file);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
filesToUpload.push(file);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
94
110
|
|
|
95
|
-
// 1.
|
|
111
|
+
// 1. skip file in blacklist
|
|
112
|
+
const files = filesToUpload.filter((file) => !FILE_UPLOAD_BLACKLIST.includes(file.name));
|
|
113
|
+
|
|
114
|
+
// 2. Add all files to dock
|
|
96
115
|
dispatchDockFileList({
|
|
97
116
|
atStart: true,
|
|
98
117
|
files: files.map((file) => ({ file, id: file.name, status: 'pending' })),
|
|
99
118
|
type: 'addFiles',
|
|
100
119
|
});
|
|
101
120
|
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
// 3. Upload files with concurrency limit using p-map
|
|
122
|
+
const uploadResults = await pMap(
|
|
123
|
+
files,
|
|
124
|
+
async (file) => {
|
|
104
125
|
const result = await get().uploadWithProgress({
|
|
105
126
|
file,
|
|
106
127
|
knowledgeBaseId,
|
|
@@ -110,10 +131,11 @@ export const createFileManageSlice: StateCreator<
|
|
|
110
131
|
await get().refreshFileList();
|
|
111
132
|
|
|
112
133
|
return { file, fileId: result?.id, fileType: file.type };
|
|
113
|
-
}
|
|
134
|
+
},
|
|
135
|
+
{ concurrency: MAX_UPLOAD_FILE_COUNT },
|
|
114
136
|
);
|
|
115
137
|
|
|
116
|
-
//
|
|
138
|
+
// 4. auto-embed files that support chunking
|
|
117
139
|
const fileIdsToEmbed = uploadResults
|
|
118
140
|
.filter(({ fileType, fileId }) => fileId && !isChunkingUnsupported(fileType))
|
|
119
141
|
.map(({ fileId }) => fileId!);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { zip } from 'fflate';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { unzipFile } from './unzipFile';
|
|
5
|
+
|
|
6
|
+
describe('unzipFile', () => {
|
|
7
|
+
it('should extract files from a ZIP archive', async () => {
|
|
8
|
+
// Create a mock ZIP file with test data
|
|
9
|
+
const testFiles = {
|
|
10
|
+
'test.txt': new TextEncoder().encode('Hello, World!'),
|
|
11
|
+
'folder/nested.txt': new TextEncoder().encode('Nested file content'),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const zipped = await new Promise<Uint8Array>((resolve, reject) => {
|
|
15
|
+
zip(testFiles, (error, data) => {
|
|
16
|
+
if (error) reject(error);
|
|
17
|
+
else resolve(data);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const zipFile = new File([new Uint8Array(zipped)], 'test.zip', { type: 'application/zip' });
|
|
22
|
+
|
|
23
|
+
const extractedFiles = await unzipFile(zipFile);
|
|
24
|
+
|
|
25
|
+
expect(extractedFiles).toHaveLength(2);
|
|
26
|
+
expect(extractedFiles[0].name).toBe('test.txt');
|
|
27
|
+
expect(extractedFiles[1].name).toBe('nested.txt');
|
|
28
|
+
|
|
29
|
+
// Verify file contents
|
|
30
|
+
const content1 = await extractedFiles[0].text();
|
|
31
|
+
expect(content1).toBe('Hello, World!');
|
|
32
|
+
|
|
33
|
+
const content2 = await extractedFiles[1].text();
|
|
34
|
+
expect(content2).toBe('Nested file content');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should skip directories in ZIP archive', async () => {
|
|
38
|
+
const testFiles = {
|
|
39
|
+
'file.txt': new TextEncoder().encode('File content'),
|
|
40
|
+
'folder/': new Uint8Array(0), // Directory entry
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const zipped = await new Promise<Uint8Array>((resolve, reject) => {
|
|
44
|
+
zip(testFiles, (error, data) => {
|
|
45
|
+
if (error) reject(error);
|
|
46
|
+
else resolve(data);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const zipFile = new File([new Uint8Array(zipped)], 'test.zip', { type: 'application/zip' });
|
|
51
|
+
|
|
52
|
+
const extractedFiles = await unzipFile(zipFile);
|
|
53
|
+
|
|
54
|
+
expect(extractedFiles).toHaveLength(1);
|
|
55
|
+
expect(extractedFiles[0].name).toBe('file.txt');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should skip hidden files and __MACOSX directories', async () => {
|
|
59
|
+
const testFiles = {
|
|
60
|
+
'.hidden': new TextEncoder().encode('Hidden file'),
|
|
61
|
+
'__MACOSX/._file.txt': new TextEncoder().encode('Mac metadata'),
|
|
62
|
+
'visible.txt': new TextEncoder().encode('Visible file'),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const zipped = await new Promise<Uint8Array>((resolve, reject) => {
|
|
66
|
+
zip(testFiles, (error, data) => {
|
|
67
|
+
if (error) reject(error);
|
|
68
|
+
else resolve(data);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const zipFile = new File([new Uint8Array(zipped)], 'test.zip', { type: 'application/zip' });
|
|
73
|
+
|
|
74
|
+
const extractedFiles = await unzipFile(zipFile);
|
|
75
|
+
|
|
76
|
+
expect(extractedFiles).toHaveLength(1);
|
|
77
|
+
expect(extractedFiles[0].name).toBe('visible.txt');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should set correct MIME types for extracted files', async () => {
|
|
81
|
+
const testFiles = {
|
|
82
|
+
'document.pdf': new TextEncoder().encode('PDF content'),
|
|
83
|
+
'image.png': new TextEncoder().encode('PNG content'),
|
|
84
|
+
'code.ts': new TextEncoder().encode('TypeScript code'),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const zipped = await new Promise<Uint8Array>((resolve, reject) => {
|
|
88
|
+
zip(testFiles, (error, data) => {
|
|
89
|
+
if (error) reject(error);
|
|
90
|
+
else resolve(data);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const zipFile = new File([new Uint8Array(zipped)], 'test.zip', { type: 'application/zip' });
|
|
95
|
+
|
|
96
|
+
const extractedFiles = await unzipFile(zipFile);
|
|
97
|
+
|
|
98
|
+
expect(extractedFiles).toHaveLength(3);
|
|
99
|
+
expect(extractedFiles.find((f) => f.name === 'document.pdf')?.type).toBe('application/pdf');
|
|
100
|
+
expect(extractedFiles.find((f) => f.name === 'image.png')?.type).toBe('image/png');
|
|
101
|
+
expect(extractedFiles.find((f) => f.name === 'code.ts')?.type).toBe('text/typescript');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should handle empty ZIP files', async () => {
|
|
105
|
+
const testFiles = {};
|
|
106
|
+
|
|
107
|
+
const zipped = await new Promise<Uint8Array>((resolve, reject) => {
|
|
108
|
+
zip(testFiles, (error, data) => {
|
|
109
|
+
if (error) reject(error);
|
|
110
|
+
else resolve(data);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const zipFile = new File([new Uint8Array(zipped)], 'empty.zip', { type: 'application/zip' });
|
|
115
|
+
|
|
116
|
+
const extractedFiles = await unzipFile(zipFile);
|
|
117
|
+
|
|
118
|
+
expect(extractedFiles).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should reject on invalid ZIP file', async () => {
|
|
122
|
+
const invalidFile = new File([new Uint8Array([1, 2, 3, 4])], 'invalid.zip', {
|
|
123
|
+
type: 'application/zip',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await expect(unzipFile(invalidFile)).rejects.toThrow();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { unzip } from 'fflate';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Determines the MIME type based on file extension
|
|
5
|
+
*/
|
|
6
|
+
const getFileType = (fileName: string): string => {
|
|
7
|
+
const extension = fileName.split('.').pop()?.toLowerCase() || '';
|
|
8
|
+
|
|
9
|
+
const mimeTypes: Record<string, string> = {
|
|
10
|
+
// Images
|
|
11
|
+
bmp: 'image/bmp',
|
|
12
|
+
|
|
13
|
+
// Code files
|
|
14
|
+
c: 'text/x-c',
|
|
15
|
+
|
|
16
|
+
cpp: 'text/x-c++',
|
|
17
|
+
|
|
18
|
+
cs: 'text/x-csharp',
|
|
19
|
+
|
|
20
|
+
css: 'text/css',
|
|
21
|
+
|
|
22
|
+
// Documents
|
|
23
|
+
csv: 'text/csv',
|
|
24
|
+
|
|
25
|
+
doc: 'application/msword',
|
|
26
|
+
|
|
27
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
28
|
+
|
|
29
|
+
gif: 'image/gif',
|
|
30
|
+
|
|
31
|
+
go: 'text/x-go',
|
|
32
|
+
|
|
33
|
+
html: 'text/html',
|
|
34
|
+
|
|
35
|
+
java: 'text/x-java',
|
|
36
|
+
|
|
37
|
+
jpeg: 'image/jpeg',
|
|
38
|
+
|
|
39
|
+
jpg: 'image/jpeg',
|
|
40
|
+
|
|
41
|
+
js: 'text/javascript',
|
|
42
|
+
|
|
43
|
+
json: 'application/json',
|
|
44
|
+
|
|
45
|
+
jsx: 'text/javascript',
|
|
46
|
+
|
|
47
|
+
md: 'text/markdown',
|
|
48
|
+
|
|
49
|
+
pdf: 'application/pdf',
|
|
50
|
+
|
|
51
|
+
php: 'application/x-httpd-php',
|
|
52
|
+
|
|
53
|
+
png: 'image/png',
|
|
54
|
+
|
|
55
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
56
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
57
|
+
py: 'text/x-python',
|
|
58
|
+
rb: 'text/x-ruby',
|
|
59
|
+
rs: 'text/x-rust',
|
|
60
|
+
rtf: 'application/rtf',
|
|
61
|
+
sh: 'application/x-sh',
|
|
62
|
+
svg: 'image/svg+xml',
|
|
63
|
+
ts: 'text/typescript',
|
|
64
|
+
tsx: 'text/typescript',
|
|
65
|
+
txt: 'text/plain',
|
|
66
|
+
webp: 'image/webp',
|
|
67
|
+
xls: 'application/vnd.ms-excel',
|
|
68
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
69
|
+
xml: 'application/xml',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return mimeTypes[extension] || 'application/octet-stream';
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extracts files from a ZIP archive
|
|
77
|
+
* @param zipFile - The ZIP file to extract
|
|
78
|
+
* @returns Promise that resolves to an array of extracted Files
|
|
79
|
+
*/
|
|
80
|
+
export const unzipFile = async (zipFile: File): Promise<File[]> => {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
zipFile
|
|
83
|
+
.arrayBuffer()
|
|
84
|
+
.then((arrayBuffer) => {
|
|
85
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
86
|
+
|
|
87
|
+
unzip(uint8Array, (error, unzipped) => {
|
|
88
|
+
if (error) {
|
|
89
|
+
reject(error);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const extractedFiles: File[] = [];
|
|
94
|
+
|
|
95
|
+
for (const [path, data] of Object.entries(unzipped)) {
|
|
96
|
+
// Skip directories and hidden files
|
|
97
|
+
if (path.endsWith('/') || path.includes('__MACOSX') || path.startsWith('.')) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get the filename from the path
|
|
102
|
+
const fileName = path.split('/').pop() || path;
|
|
103
|
+
|
|
104
|
+
// Create a File object from the extracted data
|
|
105
|
+
const blob = new Blob([new Uint8Array(data)], {
|
|
106
|
+
type: getFileType(fileName),
|
|
107
|
+
});
|
|
108
|
+
const file = new File([blob], fileName, {
|
|
109
|
+
type: getFileType(fileName),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
extractedFiles.push(file);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
resolve(extractedFiles);
|
|
116
|
+
});
|
|
117
|
+
})
|
|
118
|
+
.catch(() => {
|
|
119
|
+
reject(new Error('Failed to read ZIP file'));
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
};
|
package/vitest.config.mts
CHANGED
|
@@ -15,6 +15,7 @@ export default defineConfig({
|
|
|
15
15
|
'@/const/locale': resolve(__dirname, './src/const/locale'),
|
|
16
16
|
// TODO: after refactor the errorResponse, we can remove it
|
|
17
17
|
'@/utils/errorResponse': resolve(__dirname, './src/utils/errorResponse'),
|
|
18
|
+
'@/utils/unzipFile': resolve(__dirname, './src/utils/unzipFile'),
|
|
18
19
|
'@/utils': resolve(__dirname, './packages/utils/src'),
|
|
19
20
|
'@/types': resolve(__dirname, './packages/types/src'),
|
|
20
21
|
'@/const': resolve(__dirname, './packages/const/src'),
|