@lobehub/chat 1.12.13 → 1.12.14
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 +25 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/src/database/server/models/__tests__/message.test.ts +3 -2
- package/src/database/server/models/message.ts +8 -17
- package/src/server/routers/lambda/message.ts +1 -1
- package/src/store/file/slices/tts/action.test.ts +2 -34
- package/src/store/file/slices/tts/action.ts +10 -25
- package/src/store/file/slices/upload/action.ts +6 -6
- package/src/services/__tests__/upload_legacy.test.ts +0 -72
- package/src/services/upload_legacy.ts +0 -104
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 1.12.14](https://github.com/lobehub/lobe-chat/compare/v1.12.13...v1.12.14)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2024-08-24**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Fix tts file saving in server mode.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Fix tts file saving in server mode, closes [#3585](https://github.com/lobehub/lobe-chat/issues/3585) ([ab1cb47](https://github.com/lobehub/lobe-chat/commit/ab1cb47))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 1.12.13](https://github.com/lobehub/lobe-chat/compare/v1.12.12...v1.12.13)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2024-08-24**</sup>
|
package/README.md
CHANGED
|
@@ -268,12 +268,12 @@ Our marketplace is not just a showcase platform but also a collaborative space.
|
|
|
268
268
|
|
|
269
269
|
| Recent Submits | Description |
|
|
270
270
|
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
271
|
+
| [TypeScript Solution Architect](https://chat-preview.lobehub.com/market?agent=typescript-developer)<br/><sup>By **[swarfte](https://github.com/swarfte)** on **2024-08-24**</sup> | Expert in TypeScript, Node.js, Vue.js 3, Nuxt.js 3, Express.js, React.js, and modern UI libraries.<br/>`type-script` `java-script` `web-development` `coding-standards` `best-practices` |
|
|
271
272
|
| [Variable Name Conversion Expert](https://chat-preview.lobehub.com/market?agent=variable-name-conversion)<br/><sup>By **[zengyishou](https://github.com/zengyishou)** on **2024-08-21**</sup> | In software development, naming variables is a common yet relatively time-consuming task. This assistant can automatically convert Chinese variable names into English variable names that conform to camelCase, PascalCase, snake_case, kebab-case, and constant naming conventions based on specific naming rules. This not only improves code readability but also alleviates the frustration of variable naming.<br/>`software-development` `variable-naming` `chinese-to-english` `code-standards` `automatic-conversion` |
|
|
272
273
|
| [Prompt Engineering Expert](https://chat-preview.lobehub.com/market?agent=ai-prompts-assistant)<br/><sup>By **[cyicz123](https://github.com/cyicz123)** on **2024-08-12**</sup> | Specializing in prompt optimization and design<br/>`prompt-engineering` `ai-interaction` `writing` `optimization` `consulting` |
|
|
273
274
|
| [Commit Message Generator](https://chat-preview.lobehub.com/market?agent=commit-assistant)<br/><sup>By **[cyicz123](https://github.com/cyicz123)** on **2024-08-12**</sup> | Expert at generating precise Git commit messages<br/>`programming` `git` `commit-message` `code-review` |
|
|
274
|
-
| [RO-SCIRAW Prompt Word Expert](https://chat-preview.lobehub.com/market?agent=rosciraw)<br/><sup>By **[kirklin](https://github.com/kirklin)** on **2024-08-06**</sup> | The RO-SCIRAW framework, created by Kirk Lin, is a methodology for prompt words that provides a new paradigm for building highly precise and efficient prompt words. Please enter the information for the persona you want to create.<br/>`prompt-word-framework` |
|
|
275
275
|
|
|
276
|
-
> 📊 Total agents: [<kbd>**
|
|
276
|
+
> 📊 Total agents: [<kbd>**316**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
|
|
277
277
|
|
|
278
278
|
<!-- AGENT LIST -->
|
|
279
279
|
|
package/README.zh-CN.md
CHANGED
|
@@ -256,12 +256,12 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
|
|
|
256
256
|
|
|
257
257
|
| 最近新增 | 助手说明 |
|
|
258
258
|
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
259
|
+
| [TypeScript 解决方案架构师](https://chat-preview.lobehub.com/market?agent=typescript-developer)<br/><sup>By **[swarfte](https://github.com/swarfte)** on **2024-08-24**</sup> | 精通 TypeScript、Node.js、Vue.js 3、Nuxt.js 3、Express.js、React.js 和现代 UI 库。<br/>`类型脚本` `java-script` `网页开发` `编码标准` `最佳实践` |
|
|
259
260
|
| [开发变量名转换专家](https://chat-preview.lobehub.com/market?agent=variable-name-conversion)<br/><sup>By **[zengyishou](https://github.com/zengyishou)** on **2024-08-21**</sup> | 在软件开发过程中,命名变量是一项常见却相对耗时的任务。本助手能够根据特定的命名规则自动将中文变量名转换为符合小驼峰、大驼峰、下划线、横线以及常量命名规范的英文变量名。这不仅提高了代码的可读性,还解决了变量命名的苦恼。<br/>`软件开发` `变量命名` `中文转英文` `代码规范` `自动转换` |
|
|
260
261
|
| [提示工程专家](https://chat-preview.lobehub.com/market?agent=ai-prompts-assistant)<br/><sup>By **[cyicz123](https://github.com/cyicz123)** on **2024-08-12**</sup> | 专精 Prompt 优化与设计<br/>`提示工程` `ai交互` `写作` `优化` `咨询` |
|
|
261
262
|
| [提交信息生成器](https://chat-preview.lobehub.com/market?agent=commit-assistant)<br/><sup>By **[cyicz123](https://github.com/cyicz123)** on **2024-08-12**</sup> | 擅长生成精准的 Git 提交信息<br/>`编程` `git` `提交信息` `代码审查` |
|
|
262
|
-
| [RO-SCIRAW 提示词专家](https://chat-preview.lobehub.com/market?agent=rosciraw)<br/><sup>By **[kirklin](https://github.com/kirklin)** on **2024-08-06**</sup> | RO-SCIRAW 框架是由 Kirk Lin 开创的提示词方法论,为构建高度精确和高效的提示词提供了一个全新的范式。请输入你要创建的分身信息。<br/>`提示词框架` |
|
|
263
263
|
|
|
264
|
-
> 📊 Total agents: [<kbd>**
|
|
264
|
+
> 📊 Total agents: [<kbd>**316**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
|
|
265
265
|
|
|
266
266
|
<!-- AGENT LIST -->
|
|
267
267
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.14",
|
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -248,9 +248,10 @@ describe('MessageModel', () => {
|
|
|
248
248
|
|
|
249
249
|
// 断言结果
|
|
250
250
|
expect(result[0].extra.translate).toEqual({ content: 'translated', from: 'en', to: 'zh' });
|
|
251
|
-
// TODO: 确认是否需要包含 tts 字段
|
|
252
251
|
expect(result[0].extra.tts).toEqual({
|
|
253
|
-
|
|
252
|
+
contentMd5: 'md5',
|
|
253
|
+
file: 'f1',
|
|
254
|
+
voice: 'voice1',
|
|
254
255
|
});
|
|
255
256
|
});
|
|
256
257
|
|
|
@@ -89,11 +89,9 @@ export class MessageModel {
|
|
|
89
89
|
},
|
|
90
90
|
|
|
91
91
|
ttsId: messageTTS.id,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// ttsFile: messageTTS.fileId,
|
|
96
|
-
// ttsVoice: messageTTS.voice,
|
|
92
|
+
ttsContentMd5: messageTTS.contentMd5,
|
|
93
|
+
ttsFile: messageTTS.fileId,
|
|
94
|
+
ttsVoice: messageTTS.voice,
|
|
97
95
|
/* eslint-enable */
|
|
98
96
|
})
|
|
99
97
|
.from(messages)
|
|
@@ -113,7 +111,7 @@ export class MessageModel {
|
|
|
113
111
|
|
|
114
112
|
const messageIds = result.map((message) => message.id as string);
|
|
115
113
|
|
|
116
|
-
if (messageIds.length === 0) return
|
|
114
|
+
if (messageIds.length === 0) return [];
|
|
117
115
|
|
|
118
116
|
// 2. get relative files
|
|
119
117
|
const rawRelatedFileList = await serverDB
|
|
@@ -166,14 +164,7 @@ export class MessageModel {
|
|
|
166
164
|
.where(inArray(messageQueries.messageId, messageIds));
|
|
167
165
|
|
|
168
166
|
return result.map(
|
|
169
|
-
({
|
|
170
|
-
model,
|
|
171
|
-
provider,
|
|
172
|
-
translate,
|
|
173
|
-
ttsId,
|
|
174
|
-
// ttsFile, ttsId, ttsContentMd5, ttsVoice,
|
|
175
|
-
...item
|
|
176
|
-
}) => {
|
|
167
|
+
({ model, provider, translate, ttsId, ttsFile, ttsContentMd5, ttsVoice, ...item }) => {
|
|
177
168
|
const messageQuery = messageQueriesList.find((relation) => relation.messageId === item.id);
|
|
178
169
|
return {
|
|
179
170
|
...item,
|
|
@@ -185,9 +176,9 @@ export class MessageModel {
|
|
|
185
176
|
translate,
|
|
186
177
|
tts: ttsId
|
|
187
178
|
? {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
179
|
+
contentMd5: ttsContentMd5,
|
|
180
|
+
file: ttsFile,
|
|
181
|
+
voice: ttsVoice,
|
|
191
182
|
}
|
|
192
183
|
: undefined,
|
|
193
184
|
},
|
|
@@ -52,38 +52,6 @@ describe('TTSFileAction', () => {
|
|
|
52
52
|
expect(fileService.removeFile).toHaveBeenCalledWith(fileId);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
// Test for uploadTTSFile
|
|
56
|
-
it('uploadTTSFile should upload the file and return the file id', async () => {
|
|
57
|
-
const testFile = new File(['content'], 'test.mp3', { type: 'audio/mp3' });
|
|
58
|
-
const uploadedFileData = {
|
|
59
|
-
id: 'new-tts-file-id',
|
|
60
|
-
createdAt: testFile.lastModified,
|
|
61
|
-
data: await testFile.arrayBuffer(),
|
|
62
|
-
fileType: testFile.type,
|
|
63
|
-
name: testFile.name,
|
|
64
|
-
saveMode: 'local',
|
|
65
|
-
size: testFile.size,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// Mock the fileService.uploadFile to resolve with uploadedFileData
|
|
69
|
-
vi.spyOn(fileService, 'createFile').mockResolvedValue({ id: uploadedFileData.id, url: '' });
|
|
70
|
-
|
|
71
|
-
let fileId;
|
|
72
|
-
await act(async () => {
|
|
73
|
-
fileId = await useStore.getState().uploadTTSFile(testFile);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
expect(fileService.createFile).toHaveBeenCalledWith({
|
|
77
|
-
createdAt: testFile.lastModified,
|
|
78
|
-
data: await testFile.arrayBuffer(),
|
|
79
|
-
fileType: testFile.type,
|
|
80
|
-
name: testFile.name,
|
|
81
|
-
saveMode: 'local',
|
|
82
|
-
size: testFile.size,
|
|
83
|
-
});
|
|
84
|
-
expect(fileId).toBe(uploadedFileData.id);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
55
|
// Test for uploadTTSByArrayBuffers
|
|
88
56
|
it('uploadTTSByArrayBuffers should create a file and call uploadTTSFile', async () => {
|
|
89
57
|
const messageId = 'message-id';
|
|
@@ -93,8 +61,8 @@ describe('TTSFileAction', () => {
|
|
|
93
61
|
|
|
94
62
|
// Spy on uploadTTSFile to simulate a successful upload
|
|
95
63
|
const uploadTTSFileSpy = vi
|
|
96
|
-
.spyOn(useStore.getState(), '
|
|
97
|
-
.mockResolvedValue('new-tts-file-id');
|
|
64
|
+
.spyOn(useStore.getState(), 'uploadWithProgress')
|
|
65
|
+
.mockResolvedValue({ id: 'new-tts-file-id', url: '1' });
|
|
98
66
|
|
|
99
67
|
let fileId;
|
|
100
68
|
await act(async () => {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { SWRResponse } from 'swr';
|
|
2
2
|
import { StateCreator } from 'zustand/vanilla';
|
|
3
3
|
|
|
4
|
+
import { useClientDataSWR } from '@/libs/swr';
|
|
4
5
|
import { fileService } from '@/services/file';
|
|
5
|
-
import { legacyUploadService } from '@/services/upload_legacy';
|
|
6
6
|
import { FileItem } from '@/types/files';
|
|
7
7
|
|
|
8
8
|
import { FileStore } from '../../store';
|
|
9
9
|
|
|
10
|
+
const FETCH_TTS_FILE = 'fetchTTSFile';
|
|
11
|
+
|
|
10
12
|
export interface TTSFileAction {
|
|
11
13
|
removeTTSFile: (id: string) => Promise<void>;
|
|
12
14
|
|
|
@@ -15,8 +17,6 @@ export interface TTSFileAction {
|
|
|
15
17
|
arrayBuffers: ArrayBuffer[],
|
|
16
18
|
) => Promise<string | undefined>;
|
|
17
19
|
|
|
18
|
-
uploadTTSFile: (file: File) => Promise<string | undefined>;
|
|
19
|
-
|
|
20
20
|
useFetchTTSFile: (id: string | null) => SWRResponse<FileItem>;
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -38,26 +38,11 @@ export const createTTSFileSlice: StateCreator<
|
|
|
38
38
|
type: fileType,
|
|
39
39
|
};
|
|
40
40
|
const file = new File([blob], fileName, fileOptions);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const res = await legacyUploadService.uploadFile({
|
|
46
|
-
createdAt: file.lastModified,
|
|
47
|
-
data: await file.arrayBuffer(),
|
|
48
|
-
fileType: file.type,
|
|
49
|
-
name: file.name,
|
|
50
|
-
saveMode: 'local',
|
|
51
|
-
size: file.size,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const data = await fileService.createFile(res);
|
|
55
|
-
|
|
56
|
-
return data.id;
|
|
57
|
-
} catch (error) {
|
|
58
|
-
// 提示用户上传失败
|
|
59
|
-
console.error('upload error:', error);
|
|
60
|
-
}
|
|
41
|
+
|
|
42
|
+
const res = await get().uploadWithProgress({ file });
|
|
43
|
+
|
|
44
|
+
return res?.id;
|
|
61
45
|
},
|
|
62
|
-
useFetchTTSFile: (id) =>
|
|
46
|
+
useFetchTTSFile: (id) =>
|
|
47
|
+
useClientDataSWR(!!id ? [FETCH_TTS_FILE, id] : null, () => fileService.getFile(id!)),
|
|
63
48
|
});
|
|
@@ -16,7 +16,7 @@ const serverFileService = new ServerService();
|
|
|
16
16
|
interface UploadWithProgressParams {
|
|
17
17
|
file: File;
|
|
18
18
|
knowledgeBaseId?: string;
|
|
19
|
-
onStatusUpdate
|
|
19
|
+
onStatusUpdate?: (
|
|
20
20
|
data:
|
|
21
21
|
| {
|
|
22
22
|
id: string;
|
|
@@ -53,7 +53,7 @@ export const createFileUploadSlice: StateCreator<
|
|
|
53
53
|
> = (set, get) => ({
|
|
54
54
|
internal_uploadToClientDB: async ({ file, onStatusUpdate }) => {
|
|
55
55
|
if (!file.type.startsWith('image')) {
|
|
56
|
-
onStatusUpdate({ id: file.name, type: 'removeFile' });
|
|
56
|
+
onStatusUpdate?.({ id: file.name, type: 'removeFile' });
|
|
57
57
|
message.info({
|
|
58
58
|
content: t('upload.fileOnlySupportInServerMode', {
|
|
59
59
|
ext: file.name.split('.').pop(),
|
|
@@ -73,7 +73,7 @@ export const createFileUploadSlice: StateCreator<
|
|
|
73
73
|
file,
|
|
74
74
|
);
|
|
75
75
|
|
|
76
|
-
onStatusUpdate({
|
|
76
|
+
onStatusUpdate?.({
|
|
77
77
|
id: file.name,
|
|
78
78
|
type: 'updateFile',
|
|
79
79
|
value: {
|
|
@@ -99,7 +99,7 @@ export const createFileUploadSlice: StateCreator<
|
|
|
99
99
|
// 2. if file exist, just skip upload
|
|
100
100
|
if (checkStatus.isExist) {
|
|
101
101
|
metadata = checkStatus.metadata as FileMetadata;
|
|
102
|
-
onStatusUpdate({
|
|
102
|
+
onStatusUpdate?.({
|
|
103
103
|
id: file.name,
|
|
104
104
|
type: 'updateFile',
|
|
105
105
|
value: { status: 'processing', uploadState: { progress: 100, restTime: 0, speed: 0 } },
|
|
@@ -107,7 +107,7 @@ export const createFileUploadSlice: StateCreator<
|
|
|
107
107
|
} else {
|
|
108
108
|
// 2. if file don't exist, need upload files
|
|
109
109
|
metadata = await uploadService.uploadWithProgress(file, (status, upload) => {
|
|
110
|
-
onStatusUpdate({
|
|
110
|
+
onStatusUpdate?.({
|
|
111
111
|
id: file.name,
|
|
112
112
|
type: 'updateFile',
|
|
113
113
|
value: { status: status === 'success' ? 'processing' : status, uploadState: upload },
|
|
@@ -140,7 +140,7 @@ export const createFileUploadSlice: StateCreator<
|
|
|
140
140
|
knowledgeBaseId,
|
|
141
141
|
);
|
|
142
142
|
|
|
143
|
-
onStatusUpdate({
|
|
143
|
+
onStatusUpdate?.({
|
|
144
144
|
id: file.name,
|
|
145
145
|
type: 'updateFile',
|
|
146
146
|
value: {
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { DB_File } from '@/database/client/schemas/files';
|
|
4
|
-
import { edgeClient } from '@/libs/trpc/client';
|
|
5
|
-
import { API_ENDPOINTS } from '@/services/_url';
|
|
6
|
-
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
|
|
7
|
-
import { createServerConfigStore } from '@/store/serverConfig/store';
|
|
8
|
-
|
|
9
|
-
import { legacyUploadService as uploadService } from '../upload_legacy';
|
|
10
|
-
|
|
11
|
-
vi.mock('@/store/serverConfig/selectors');
|
|
12
|
-
vi.mock('@/libs/trpc/client', () => {
|
|
13
|
-
return {
|
|
14
|
-
edgeClient: {
|
|
15
|
-
upload: {
|
|
16
|
-
createS3PreSignedUrl: { mutate: vi.fn() },
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
beforeAll(() => {
|
|
23
|
-
createServerConfigStore();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe('UploadService', () => {
|
|
27
|
-
describe('uploadFile', () => {
|
|
28
|
-
it('should upload file to server when enableServer is true', async () => {
|
|
29
|
-
// Arrange
|
|
30
|
-
const file: DB_File = {
|
|
31
|
-
data: new ArrayBuffer(10),
|
|
32
|
-
fileType: 'text/plain',
|
|
33
|
-
metadata: {},
|
|
34
|
-
name: 'test.txt',
|
|
35
|
-
saveMode: 'local',
|
|
36
|
-
size: 10,
|
|
37
|
-
};
|
|
38
|
-
const mockCreateS3Url = vi.fn().mockResolvedValue('https://example.com');
|
|
39
|
-
vi.mocked(edgeClient.upload.createS3PreSignedUrl.mutate).mockImplementation(mockCreateS3Url);
|
|
40
|
-
vi.spyOn(serverConfigSelectors, 'enableUploadFileToServer').mockReturnValue(true);
|
|
41
|
-
global.fetch = vi.fn().mockResolvedValue({ ok: true } as Response);
|
|
42
|
-
|
|
43
|
-
// Act
|
|
44
|
-
const result = await uploadService.uploadFile(file);
|
|
45
|
-
|
|
46
|
-
// Assert
|
|
47
|
-
expect(mockCreateS3Url).toHaveBeenCalledTimes(1);
|
|
48
|
-
expect(fetch).toHaveBeenCalledTimes(1);
|
|
49
|
-
expect(result.url).toMatch(/\/\d+\/[a-f0-9-]+\.txt/);
|
|
50
|
-
expect(result.saveMode).toBe('url');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should save file locally when enableServer is false', async () => {
|
|
54
|
-
// Arrange
|
|
55
|
-
const file: DB_File = {
|
|
56
|
-
data: new ArrayBuffer(10),
|
|
57
|
-
fileType: 'text/plain',
|
|
58
|
-
metadata: {},
|
|
59
|
-
name: 'test.txt',
|
|
60
|
-
saveMode: 'local',
|
|
61
|
-
size: 10,
|
|
62
|
-
};
|
|
63
|
-
vi.spyOn(serverConfigSelectors, 'enableUploadFileToServer').mockReturnValue(false);
|
|
64
|
-
|
|
65
|
-
// Act
|
|
66
|
-
const result = await uploadService.uploadFile(file);
|
|
67
|
-
|
|
68
|
-
// Assert
|
|
69
|
-
expect(result).toEqual(file);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
});
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { fileEnv } from '@/config/file';
|
|
2
|
-
import { DB_File } from '@/database/client/schemas/files';
|
|
3
|
-
import { edgeClient } from '@/libs/trpc/client';
|
|
4
|
-
import { API_ENDPOINTS } from '@/services/_url';
|
|
5
|
-
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
|
|
6
|
-
import compressImage from '@/utils/compressImage';
|
|
7
|
-
import { uuid } from '@/utils/uuid';
|
|
8
|
-
|
|
9
|
-
class UploadService {
|
|
10
|
-
async uploadFile(file: DB_File) {
|
|
11
|
-
if (this.enableServer) {
|
|
12
|
-
const { data, ...params } = file;
|
|
13
|
-
const filename = `${uuid()}.${file.name.split('.').at(-1)}`;
|
|
14
|
-
|
|
15
|
-
// 精确到以 h 为单位的 path
|
|
16
|
-
const date = (Date.now() / 1000 / 60 / 60).toFixed(0);
|
|
17
|
-
const dirname = `${fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/${date}`;
|
|
18
|
-
const pathname = `${dirname}/${filename}`;
|
|
19
|
-
|
|
20
|
-
const url = await edgeClient.upload.createS3PreSignedUrl.mutate({ pathname });
|
|
21
|
-
|
|
22
|
-
const res = await fetch(url, {
|
|
23
|
-
body: data,
|
|
24
|
-
headers: { 'Content-Type': file.fileType },
|
|
25
|
-
method: 'PUT',
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
if (res.ok) {
|
|
29
|
-
return {
|
|
30
|
-
...params,
|
|
31
|
-
metadata: { date, dirname: dirname, filename: filename, path: pathname },
|
|
32
|
-
name: file.name,
|
|
33
|
-
saveMode: 'url',
|
|
34
|
-
url: pathname,
|
|
35
|
-
} as DB_File;
|
|
36
|
-
} else {
|
|
37
|
-
throw new Error('Upload Error');
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// 跳过图片上传测试
|
|
42
|
-
const isTestData = file.size === 1;
|
|
43
|
-
if (this.isImage(file.fileType) && !isTestData) {
|
|
44
|
-
return this.uploadImageFile(file);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// save to local storage
|
|
48
|
-
// we may want to save to a remote server later
|
|
49
|
-
return file;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @deprecated
|
|
54
|
-
* @param url
|
|
55
|
-
* @param file
|
|
56
|
-
*/
|
|
57
|
-
async uploadImageByUrl(url: string, file: Pick<DB_File, 'name' | 'metadata'>) {
|
|
58
|
-
const res = await fetch(API_ENDPOINTS.proxy, { body: url, method: 'POST' });
|
|
59
|
-
const data = await res.arrayBuffer();
|
|
60
|
-
const fileType = res.headers.get('content-type') || 'image/webp';
|
|
61
|
-
|
|
62
|
-
return this.uploadFile({
|
|
63
|
-
data,
|
|
64
|
-
fileType,
|
|
65
|
-
metadata: file.metadata,
|
|
66
|
-
name: file.name,
|
|
67
|
-
saveMode: 'local',
|
|
68
|
-
size: data.byteLength,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private isImage(fileType: string) {
|
|
73
|
-
const imageRegex = /^image\//;
|
|
74
|
-
return imageRegex.test(fileType);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private async uploadImageFile(file: DB_File) {
|
|
78
|
-
// 加载图片
|
|
79
|
-
const url = file.url || URL.createObjectURL(new Blob([file.data!]));
|
|
80
|
-
|
|
81
|
-
const img = new Image();
|
|
82
|
-
img.src = url;
|
|
83
|
-
await (() =>
|
|
84
|
-
new Promise((resolve) => {
|
|
85
|
-
img.addEventListener('load', resolve);
|
|
86
|
-
}))();
|
|
87
|
-
|
|
88
|
-
// 压缩图片
|
|
89
|
-
const base64String = compressImage({ img, type: file.fileType });
|
|
90
|
-
const binaryString = atob(base64String.split('base64,')[1]);
|
|
91
|
-
const uint8Array = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
|
|
92
|
-
file.data = uint8Array.buffer;
|
|
93
|
-
|
|
94
|
-
return file;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private get enableServer() {
|
|
98
|
-
return serverConfigSelectors.enableUploadFileToServer(
|
|
99
|
-
window.global_serverConfigStore.getState(),
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export const legacyUploadService = new UploadService();
|