@lobehub/chat 0.159.12 → 0.160.0
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +4 -2
- package/src/app/trpc/{[trpc] → edge/[trpc]}/route.ts +3 -3
- package/src/config/__tests__/server.test.ts +0 -11
- package/src/config/file.ts +34 -0
- package/src/config/server/app.ts +0 -8
- package/src/config/server/provider.ts +3 -3
- package/src/database/client/models/file.ts +2 -1
- package/src/database/client/schemas/files.ts +2 -2
- package/src/libs/agent-runtime/google/index.test.ts +20 -1
- package/src/libs/agent-runtime/google/index.ts +22 -9
- package/src/libs/agent-runtime/utils/uriParser.test.ts +29 -0
- package/src/libs/agent-runtime/utils/uriParser.ts +17 -9
- package/src/libs/trpc/client.ts +5 -3
- package/src/libs/trpc/index.ts +10 -34
- package/src/libs/trpc/init.ts +26 -0
- package/src/libs/trpc/middleware/password.test.ts +87 -0
- package/src/libs/trpc/middleware/password.ts +26 -0
- package/src/libs/trpc/middleware/userAuth.test.ts +44 -0
- package/src/libs/trpc/middleware/userAuth.ts +18 -0
- package/src/server/context.ts +28 -3
- package/src/server/files/s3.ts +58 -0
- package/src/server/globalConfig/index.ts +2 -0
- package/src/server/mock.ts +2 -2
- package/src/server/routers/{config → edge/config}/index.test.ts +1 -0
- package/src/server/routers/edge/upload.ts +16 -0
- package/src/server/routers/index.ts +5 -3
- package/src/services/__tests__/global.test.ts +4 -5
- package/src/services/__tests__/sync.test.ts +56 -0
- package/src/services/__tests__/upload.test.ts +72 -0
- package/src/services/_url.ts +2 -0
- package/src/services/file/client.test.ts +102 -34
- package/src/services/file/client.ts +24 -49
- package/src/services/file/type.ts +1 -2
- package/src/services/global.ts +3 -18
- package/src/services/sync.ts +19 -0
- package/src/services/upload.ts +99 -0
- package/src/store/chat/slices/builtinTool/action.test.ts +4 -2
- package/src/store/chat/slices/builtinTool/action.ts +6 -3
- package/src/store/file/slices/images/action.test.ts +10 -17
- package/src/store/file/slices/images/action.ts +4 -1
- package/src/store/file/slices/tts/action.test.ts +8 -14
- package/src/store/file/slices/tts/action.ts +4 -1
- package/src/store/serverConfig/selectors.ts +1 -0
- package/src/store/serverConfig/store.ts +10 -0
- package/src/store/user/slices/common/action.ts +26 -14
- package/src/store/user/slices/sync/action.test.ts +6 -6
- package/src/store/user/slices/sync/action.ts +3 -3
- package/src/types/serverConfig.ts +1 -0
- package/src/app/api/files/image/imgur.ts +0 -72
- package/src/app/api/files/image/route.ts +0 -42
- /package/src/server/routers/{config → edge/config}/__snapshots__/index.test.ts.snap +0 -0
- /package/src/server/routers/{config → edge/config}/index.ts +0 -0
package/src/services/global.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { DeepPartial } from 'utility-types';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { trpcClient } from '@/libs/trpc/client';
|
|
3
|
+
import { edgeClient } from '@/libs/trpc/client';
|
|
5
4
|
import { LobeAgentConfig } from '@/types/agent';
|
|
6
5
|
import { GlobalServerConfig } from '@/types/serverConfig';
|
|
7
|
-
import { StartDataSyncParams } from '@/types/sync';
|
|
8
6
|
|
|
9
7
|
const VERSION_URL = 'https://registry.npmmirror.com/@lobehub/chat';
|
|
10
8
|
|
|
@@ -20,24 +18,11 @@ class GlobalService {
|
|
|
20
18
|
};
|
|
21
19
|
|
|
22
20
|
getGlobalConfig = async (): Promise<GlobalServerConfig> => {
|
|
23
|
-
return
|
|
21
|
+
return edgeClient.config.getGlobalConfig.query();
|
|
24
22
|
};
|
|
25
23
|
|
|
26
24
|
getDefaultAgentConfig = async (): Promise<DeepPartial<LobeAgentConfig>> => {
|
|
27
|
-
return
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
enabledSync = async (params: StartDataSyncParams) => {
|
|
31
|
-
if (typeof window === 'undefined') return false;
|
|
32
|
-
|
|
33
|
-
await dataSync.startDataSync(params);
|
|
34
|
-
return true;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
disableSync = async () => {
|
|
38
|
-
await dataSync.disconnect();
|
|
39
|
-
|
|
40
|
-
return false;
|
|
25
|
+
return edgeClient.config.getDefaultAgentConfig.query();
|
|
41
26
|
};
|
|
42
27
|
}
|
|
43
28
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { dataSync } from '@/database/client/core';
|
|
2
|
+
import { StartDataSyncParams } from '@/types/sync';
|
|
3
|
+
|
|
4
|
+
class SyncService {
|
|
5
|
+
enabledSync = async (params: StartDataSyncParams) => {
|
|
6
|
+
if (typeof window === 'undefined') return false;
|
|
7
|
+
|
|
8
|
+
await dataSync.startDataSync(params);
|
|
9
|
+
return true;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
disableSync = async () => {
|
|
13
|
+
await dataSync.disconnect();
|
|
14
|
+
|
|
15
|
+
return false;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const syncService = new SyncService();
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
|
|
18
|
+
const pathname = `${fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/${date}/${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, filename: file.name },
|
|
32
|
+
name: filename,
|
|
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
|
+
async uploadImageByUrl(url: string, file: Pick<DB_File, 'name' | 'metadata'>) {
|
|
53
|
+
const res = await fetch(API_ENDPOINTS.proxy, { body: url, method: 'POST' });
|
|
54
|
+
const data = await res.arrayBuffer();
|
|
55
|
+
const fileType = res.headers.get('content-type') || 'image/webp';
|
|
56
|
+
|
|
57
|
+
return this.uploadFile({
|
|
58
|
+
data,
|
|
59
|
+
fileType,
|
|
60
|
+
metadata: file.metadata,
|
|
61
|
+
name: file.name,
|
|
62
|
+
saveMode: 'local',
|
|
63
|
+
size: data.byteLength,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private isImage(fileType: string) {
|
|
68
|
+
const imageRegex = /^image\//;
|
|
69
|
+
return imageRegex.test(fileType);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async uploadImageFile(file: DB_File) {
|
|
73
|
+
// 加载图片
|
|
74
|
+
const url = file.url || URL.createObjectURL(new Blob([file.data!]));
|
|
75
|
+
|
|
76
|
+
const img = new Image();
|
|
77
|
+
img.src = url;
|
|
78
|
+
await (() =>
|
|
79
|
+
new Promise((resolve) => {
|
|
80
|
+
img.addEventListener('load', resolve);
|
|
81
|
+
}))();
|
|
82
|
+
|
|
83
|
+
// 压缩图片
|
|
84
|
+
const base64String = compressImage({ img, type: file.fileType });
|
|
85
|
+
const binaryString = atob(base64String.split('base64,')[1]);
|
|
86
|
+
const uint8Array = Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
|
|
87
|
+
file.data = uint8Array.buffer;
|
|
88
|
+
|
|
89
|
+
return file;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private get enableServer() {
|
|
93
|
+
return serverConfigSelectors.enableUploadFileToServer(
|
|
94
|
+
window.global_serverConfigStore.getState(),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const uploadService = new UploadService();
|
|
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
|
|
4
4
|
import { fileService } from '@/services/file';
|
|
5
5
|
import { imageGenerationService } from '@/services/textToImage';
|
|
6
|
+
import { uploadService } from '@/services/upload';
|
|
6
7
|
import { chatSelectors } from '@/store/chat/selectors';
|
|
7
8
|
import { ChatMessage } from '@/types/message';
|
|
8
9
|
import { DallEImageItem } from '@/types/tool/dalle';
|
|
@@ -35,7 +36,8 @@ describe('chatToolSlice', () => {
|
|
|
35
36
|
const mockId = 'image-id';
|
|
36
37
|
|
|
37
38
|
vi.spyOn(imageGenerationService, 'generateImage').mockResolvedValue(mockUrl);
|
|
38
|
-
vi.spyOn(
|
|
39
|
+
vi.spyOn(uploadService, 'uploadImageByUrl').mockResolvedValue({} as any);
|
|
40
|
+
vi.spyOn(fileService, 'createFile').mockResolvedValue({ id: mockId });
|
|
39
41
|
vi.spyOn(result.current, 'toggleDallEImageLoading');
|
|
40
42
|
|
|
41
43
|
await act(async () => {
|
|
@@ -43,7 +45,7 @@ describe('chatToolSlice', () => {
|
|
|
43
45
|
});
|
|
44
46
|
// For each prompt, loading is toggled on and then off
|
|
45
47
|
expect(imageGenerationService.generateImage).toHaveBeenCalledTimes(prompts.length);
|
|
46
|
-
expect(
|
|
48
|
+
expect(uploadService.uploadImageByUrl).toHaveBeenCalledTimes(prompts.length);
|
|
47
49
|
|
|
48
50
|
expect(result.current.toggleDallEImageLoading).toHaveBeenCalledTimes(prompts.length * 2);
|
|
49
51
|
});
|
|
@@ -4,6 +4,7 @@ import { StateCreator } from 'zustand/vanilla';
|
|
|
4
4
|
|
|
5
5
|
import { fileService } from '@/services/file';
|
|
6
6
|
import { imageGenerationService } from '@/services/textToImage';
|
|
7
|
+
import { uploadService } from '@/services/upload';
|
|
7
8
|
import { chatSelectors } from '@/store/chat/selectors';
|
|
8
9
|
import { ChatStore } from '@/store/chat/store';
|
|
9
10
|
import { DallEImageItem } from '@/types/tool/dalle';
|
|
@@ -60,14 +61,16 @@ export const chatToolSlice: StateCreator<
|
|
|
60
61
|
|
|
61
62
|
toggleDallEImageLoading(messageId + params.prompt, false);
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
uploadService
|
|
64
65
|
.uploadImageByUrl(url, {
|
|
65
66
|
metadata: { ...params, originPrompt: originPrompt },
|
|
66
67
|
name: `${originPrompt || params.prompt}_${index}.png`,
|
|
67
68
|
})
|
|
68
|
-
.then((
|
|
69
|
+
.then(async (res) => {
|
|
70
|
+
const data = await fileService.createFile(res);
|
|
71
|
+
|
|
69
72
|
updateImageItem(messageId, (draft) => {
|
|
70
|
-
draft[index].imageId = id;
|
|
73
|
+
draft[index].imageId = data.id;
|
|
71
74
|
draft[index].previewUrl = undefined;
|
|
72
75
|
});
|
|
73
76
|
});
|
|
@@ -2,21 +2,14 @@ import { act, renderHook } from '@testing-library/react';
|
|
|
2
2
|
import useSWR from 'swr';
|
|
3
3
|
import { Mock, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
|
+
import { DB_File } from '@/database/client/schemas/files';
|
|
5
6
|
import { fileService } from '@/services/file';
|
|
7
|
+
import { uploadService } from '@/services/upload';
|
|
6
8
|
|
|
7
9
|
import { useFileStore as useStore } from '../../store';
|
|
8
10
|
|
|
9
11
|
vi.mock('zustand/traditional');
|
|
10
12
|
|
|
11
|
-
// Mocks for fileService
|
|
12
|
-
vi.mock('@/services/file', () => ({
|
|
13
|
-
fileService: {
|
|
14
|
-
removeFile: vi.fn(),
|
|
15
|
-
uploadFile: vi.fn(),
|
|
16
|
-
getFile: vi.fn(),
|
|
17
|
-
},
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
13
|
// Mock for useSWR
|
|
21
14
|
vi.mock('swr', () => ({
|
|
22
15
|
default: vi.fn(),
|
|
@@ -67,7 +60,7 @@ describe('useFileStore:images', () => {
|
|
|
67
60
|
const fileId = 'test-id';
|
|
68
61
|
|
|
69
62
|
// Mock the fileService.removeFile to resolve
|
|
70
|
-
(fileService
|
|
63
|
+
vi.spyOn(fileService, 'removeFile').mockResolvedValue(undefined);
|
|
71
64
|
|
|
72
65
|
// Populate the list to remove an item later
|
|
73
66
|
act(() => {
|
|
@@ -96,7 +89,7 @@ describe('useFileStore:images', () => {
|
|
|
96
89
|
};
|
|
97
90
|
|
|
98
91
|
// Mock the fileService.getFile to resolve with fileData
|
|
99
|
-
(fileService
|
|
92
|
+
vi.spyOn(fileService, 'getFile').mockResolvedValue(fileData as any);
|
|
100
93
|
|
|
101
94
|
// Mock useSWR to call the fetcher function immediately
|
|
102
95
|
const useSWRMock = vi.mocked(useSWR);
|
|
@@ -124,7 +117,7 @@ describe('useFileStore:images', () => {
|
|
|
124
117
|
|
|
125
118
|
// 模拟 fileService.uploadFile 抛出错误
|
|
126
119
|
const errorMessage = 'Upload failed';
|
|
127
|
-
(
|
|
120
|
+
vi.spyOn(uploadService, 'uploadFile').mockRejectedValue(new Error(errorMessage));
|
|
128
121
|
|
|
129
122
|
// Mock console.error for testing
|
|
130
123
|
const consoleErrorMock = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
@@ -133,7 +126,7 @@ describe('useFileStore:images', () => {
|
|
|
133
126
|
await result.current.uploadFile(testFile);
|
|
134
127
|
});
|
|
135
128
|
|
|
136
|
-
expect(
|
|
129
|
+
expect(uploadService.uploadFile).toHaveBeenCalledWith({
|
|
137
130
|
createdAt: testFile.lastModified,
|
|
138
131
|
data: await testFile.arrayBuffer(),
|
|
139
132
|
fileType: testFile.type,
|
|
@@ -156,7 +149,6 @@ describe('useFileStore:images', () => {
|
|
|
156
149
|
|
|
157
150
|
// 模拟 fileService.uploadFile 返回的数据
|
|
158
151
|
const uploadedFileData = {
|
|
159
|
-
id: 'new-file-id',
|
|
160
152
|
createdAt: testFile.lastModified,
|
|
161
153
|
data: await testFile.arrayBuffer(),
|
|
162
154
|
fileType: testFile.type,
|
|
@@ -166,13 +158,14 @@ describe('useFileStore:images', () => {
|
|
|
166
158
|
};
|
|
167
159
|
|
|
168
160
|
// Mock the fileService.uploadFile to resolve with uploadedFileData
|
|
169
|
-
(
|
|
161
|
+
vi.spyOn(uploadService, 'uploadFile').mockResolvedValue(uploadedFileData as DB_File);
|
|
162
|
+
vi.spyOn(fileService, 'createFile').mockResolvedValue({ id: 'new-file-id' });
|
|
170
163
|
|
|
171
164
|
await act(async () => {
|
|
172
165
|
await result.current.uploadFile(testFile);
|
|
173
166
|
});
|
|
174
167
|
|
|
175
|
-
expect(fileService.
|
|
168
|
+
expect(fileService.createFile).toHaveBeenCalledWith({
|
|
176
169
|
createdAt: testFile.lastModified,
|
|
177
170
|
data: await testFile.arrayBuffer(),
|
|
178
171
|
fileType: testFile.type,
|
|
@@ -180,7 +173,7 @@ describe('useFileStore:images', () => {
|
|
|
180
173
|
saveMode: 'local',
|
|
181
174
|
size: testFile.size,
|
|
182
175
|
});
|
|
183
|
-
expect(result.current.inputFilesList).toContain(
|
|
176
|
+
expect(result.current.inputFilesList).toContain('new-file-id');
|
|
184
177
|
});
|
|
185
178
|
});
|
|
186
179
|
});
|
|
@@ -3,6 +3,7 @@ import useSWR, { SWRResponse } from 'swr';
|
|
|
3
3
|
import { StateCreator } from 'zustand/vanilla';
|
|
4
4
|
|
|
5
5
|
import { fileService } from '@/services/file';
|
|
6
|
+
import { uploadService } from '@/services/upload';
|
|
6
7
|
import { FilePreview } from '@/types/files';
|
|
7
8
|
import { setNamespace } from '@/utils/storeDebug';
|
|
8
9
|
|
|
@@ -55,7 +56,7 @@ export const createFileSlice: StateCreator<
|
|
|
55
56
|
},
|
|
56
57
|
uploadFile: async (file) => {
|
|
57
58
|
try {
|
|
58
|
-
const
|
|
59
|
+
const result = await uploadService.uploadFile({
|
|
59
60
|
createdAt: file.lastModified,
|
|
60
61
|
data: await file.arrayBuffer(),
|
|
61
62
|
fileType: file.type,
|
|
@@ -64,6 +65,8 @@ export const createFileSlice: StateCreator<
|
|
|
64
65
|
size: file.size,
|
|
65
66
|
});
|
|
66
67
|
|
|
68
|
+
const data = await fileService.createFile(result);
|
|
69
|
+
|
|
67
70
|
set(
|
|
68
71
|
({ inputFilesList }) => ({ inputFilesList: [...inputFilesList, data.id] }),
|
|
69
72
|
false,
|
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import { act, renderHook } from '@testing-library/react';
|
|
2
2
|
import useSWR from 'swr';
|
|
3
|
-
import {
|
|
3
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
5
|
import { fileService } from '@/services/file';
|
|
6
|
+
import { createServerConfigStore } from '@/store/serverConfig/store';
|
|
6
7
|
|
|
7
8
|
import { useFileStore as useStore } from '../../store';
|
|
8
9
|
|
|
9
10
|
vi.mock('zustand/traditional');
|
|
10
11
|
|
|
11
|
-
// Mocks for fileService
|
|
12
|
-
vi.mock('@/services/file', () => ({
|
|
13
|
-
fileService: {
|
|
14
|
-
removeFile: vi.fn(),
|
|
15
|
-
uploadFile: vi.fn(),
|
|
16
|
-
getFile: vi.fn(),
|
|
17
|
-
},
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
12
|
// Mock for useSWR
|
|
21
13
|
vi.mock('swr', () => ({
|
|
22
14
|
default: vi.fn(),
|
|
@@ -36,6 +28,8 @@ beforeAll(() => {
|
|
|
36
28
|
});
|
|
37
29
|
},
|
|
38
30
|
});
|
|
31
|
+
|
|
32
|
+
createServerConfigStore();
|
|
39
33
|
});
|
|
40
34
|
|
|
41
35
|
beforeEach(() => {
|
|
@@ -49,7 +43,7 @@ describe('TTSFileAction', () => {
|
|
|
49
43
|
const fileId = 'tts-file-id';
|
|
50
44
|
|
|
51
45
|
// Mock the fileService.removeFile to resolve
|
|
52
|
-
(fileService
|
|
46
|
+
vi.spyOn(fileService, 'removeFile').mockResolvedValue(undefined);
|
|
53
47
|
|
|
54
48
|
await act(async () => {
|
|
55
49
|
await useStore.getState().removeTTSFile(fileId);
|
|
@@ -72,14 +66,14 @@ describe('TTSFileAction', () => {
|
|
|
72
66
|
};
|
|
73
67
|
|
|
74
68
|
// Mock the fileService.uploadFile to resolve with uploadedFileData
|
|
75
|
-
(fileService
|
|
69
|
+
vi.spyOn(fileService, 'createFile').mockResolvedValue(uploadedFileData);
|
|
76
70
|
|
|
77
71
|
let fileId;
|
|
78
72
|
await act(async () => {
|
|
79
73
|
fileId = await useStore.getState().uploadTTSFile(testFile);
|
|
80
74
|
});
|
|
81
75
|
|
|
82
|
-
expect(fileService.
|
|
76
|
+
expect(fileService.createFile).toHaveBeenCalledWith({
|
|
83
77
|
createdAt: testFile.lastModified,
|
|
84
78
|
data: await testFile.arrayBuffer(),
|
|
85
79
|
fileType: testFile.type,
|
|
@@ -127,7 +121,7 @@ describe('TTSFileAction', () => {
|
|
|
127
121
|
};
|
|
128
122
|
|
|
129
123
|
// Mock the fileService.getFile to resolve with fileData
|
|
130
|
-
(fileService
|
|
124
|
+
vi.spyOn(fileService, 'getFile').mockResolvedValue(fileData as any);
|
|
131
125
|
|
|
132
126
|
// Mock useSWR to call the fetcher function immediately
|
|
133
127
|
const useSWRMock = vi.mocked(useSWR);
|
|
@@ -2,6 +2,7 @@ import useSWR, { SWRResponse } from 'swr';
|
|
|
2
2
|
import { StateCreator } from 'zustand/vanilla';
|
|
3
3
|
|
|
4
4
|
import { fileService } from '@/services/file';
|
|
5
|
+
import { uploadService } from '@/services/upload';
|
|
5
6
|
import { FilePreview } from '@/types/files';
|
|
6
7
|
|
|
7
8
|
import { FileStore } from '../../store';
|
|
@@ -41,7 +42,7 @@ export const createTTSFileSlice: StateCreator<
|
|
|
41
42
|
},
|
|
42
43
|
uploadTTSFile: async (file) => {
|
|
43
44
|
try {
|
|
44
|
-
const
|
|
45
|
+
const res = await uploadService.uploadFile({
|
|
45
46
|
createdAt: file.lastModified,
|
|
46
47
|
data: await file.arrayBuffer(),
|
|
47
48
|
fileType: file.type,
|
|
@@ -50,6 +51,8 @@ export const createTTSFileSlice: StateCreator<
|
|
|
50
51
|
size: file.size,
|
|
51
52
|
});
|
|
52
53
|
|
|
54
|
+
const data = await fileService.createFile(res);
|
|
55
|
+
|
|
53
56
|
return data.id;
|
|
54
57
|
} catch (error) {
|
|
55
58
|
// 提示用户上传失败
|
|
@@ -6,6 +6,7 @@ export const featureFlagsSelectors = (s: ServerConfigStore) =>
|
|
|
6
6
|
mapFeatureFlagsEnvToState(s.featureFlags);
|
|
7
7
|
|
|
8
8
|
export const serverConfigSelectors = {
|
|
9
|
+
enableUploadFileToServer: (s: ServerConfigStore) => s.serverConfig.enableUploadFileToServer,
|
|
9
10
|
enabledAccessCode: (s: ServerConfigStore) => !!s.serverConfig?.enabledAccessCode,
|
|
10
11
|
enabledOAuthSSO: (s: ServerConfigStore) => s.serverConfig.enabledOAuthSSO,
|
|
11
12
|
enabledTelemetryChat: (s: ServerConfigStore) => s.serverConfig.telemetry.langfuse || false,
|
|
@@ -36,6 +36,12 @@ const createStore: CreateStore = (runtimeState) => () => ({
|
|
|
36
36
|
|
|
37
37
|
let store: StoreApi<ServerConfigStore>;
|
|
38
38
|
|
|
39
|
+
declare global {
|
|
40
|
+
interface Window {
|
|
41
|
+
global_serverConfigStore: StoreApi<ServerConfigStore>;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
export const initServerConfigStore = (initState: Partial<ServerConfigStore>) =>
|
|
40
46
|
createWithEqualityFn<ServerConfigStore>()(
|
|
41
47
|
devtools(createStore(initState || {}), {
|
|
@@ -53,6 +59,10 @@ export const createServerConfigStore = (initState?: Partial<ServerConfigStore>)
|
|
|
53
59
|
}),
|
|
54
60
|
shallow,
|
|
55
61
|
);
|
|
62
|
+
|
|
63
|
+
if (typeof window !== 'undefined') {
|
|
64
|
+
window.global_serverConfigStore = store;
|
|
65
|
+
}
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
return store;
|
|
@@ -2,7 +2,6 @@ import useSWR, { SWRResponse } from 'swr';
|
|
|
2
2
|
import { DeepPartial } from 'utility-types';
|
|
3
3
|
import type { StateCreator } from 'zustand/vanilla';
|
|
4
4
|
|
|
5
|
-
import { globalService } from '@/services/global';
|
|
6
5
|
import { messageService } from '@/services/message';
|
|
7
6
|
import { userService } from '@/services/user';
|
|
8
7
|
import type { UserStore } from '@/store/user';
|
|
@@ -53,22 +52,35 @@ export const createCommonSlice: StateCreator<
|
|
|
53
52
|
},
|
|
54
53
|
),
|
|
55
54
|
|
|
55
|
+
/**
|
|
56
|
+
* TODO: need to be removed in the future
|
|
57
|
+
* the serverConfig should be fetched only in the serverConfigStore
|
|
58
|
+
* @deprecated
|
|
59
|
+
*/
|
|
56
60
|
useFetchServerConfig: () =>
|
|
57
|
-
useSWR<GlobalServerConfig>(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
defaultAgent: data.defaultAgent,
|
|
62
|
-
languageModel: data.languageModel,
|
|
63
|
-
};
|
|
61
|
+
useSWR<GlobalServerConfig>(
|
|
62
|
+
'fetchGlobalConfig',
|
|
63
|
+
async () => {
|
|
64
|
+
const { globalService } = await import('@/services/global');
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
return globalService.getGlobalConfig();
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
onSuccess: (data) => {
|
|
70
|
+
if (data) {
|
|
71
|
+
const serverSettings: DeepPartial<GlobalSettings> = {
|
|
72
|
+
defaultAgent: data.defaultAgent,
|
|
73
|
+
languageModel: data.languageModel,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const defaultSettings = merge(get().defaultSettings, serverSettings);
|
|
66
77
|
|
|
67
|
-
|
|
78
|
+
set({ defaultSettings, serverConfig: data }, false, n('initGlobalConfig'));
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
get().refreshDefaultModelProviderList({ trigger: 'fetchServerConfig' });
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
revalidateOnFocus: false,
|
|
71
84
|
},
|
|
72
|
-
|
|
73
|
-
}),
|
|
85
|
+
),
|
|
74
86
|
});
|
|
@@ -2,10 +2,10 @@ import { act, renderHook, waitFor } from '@testing-library/react';
|
|
|
2
2
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import { withSWR } from '~test-utils';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { syncService } from '@/services/sync';
|
|
6
6
|
import { useUserStore } from '@/store/user';
|
|
7
|
-
import { userProfileSelectors } from '@/store/user/
|
|
8
|
-
import { syncSettingsSelectors } from '@/store/user/
|
|
7
|
+
import { userProfileSelectors } from '@/store/user/selectors';
|
|
8
|
+
import { syncSettingsSelectors } from '@/store/user/selectors';
|
|
9
9
|
|
|
10
10
|
vi.mock('zustand/traditional');
|
|
11
11
|
|
|
@@ -86,7 +86,7 @@ describe('createSyncSlice', () => {
|
|
|
86
86
|
enabled: true,
|
|
87
87
|
});
|
|
88
88
|
vi.spyOn(syncSettingsSelectors, 'deviceName').mockReturnValueOnce(deviceName);
|
|
89
|
-
const enabledSyncSpy = vi.spyOn(
|
|
89
|
+
const enabledSyncSpy = vi.spyOn(syncService, 'enabledSync').mockResolvedValueOnce(true);
|
|
90
90
|
const { result } = renderHook(() => useUserStore());
|
|
91
91
|
|
|
92
92
|
const data = await act(async () => {
|
|
@@ -114,8 +114,8 @@ describe('createSyncSlice', () => {
|
|
|
114
114
|
await waitFor(() => expect(result.current.data).toBe(false));
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
-
it('should call
|
|
118
|
-
const disableSyncSpy = vi.spyOn(
|
|
117
|
+
it('should call syncService.disableSync when userEnableSync is false', async () => {
|
|
118
|
+
const disableSyncSpy = vi.spyOn(syncService, 'disableSync').mockResolvedValueOnce(false);
|
|
119
119
|
|
|
120
120
|
const { result } = renderHook(
|
|
121
121
|
() => useUserStore().useEnabledSync(false, 'user-id', vi.fn()),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import useSWR, { SWRResponse } from 'swr';
|
|
2
2
|
import type { StateCreator } from 'zustand/vanilla';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { syncService } from '@/services/sync';
|
|
5
5
|
import type { UserStore } from '@/store/user';
|
|
6
6
|
import { OnSyncEvent, PeerSyncStatus } from '@/types/sync';
|
|
7
7
|
import { browserInfo } from '@/utils/platform';
|
|
@@ -50,7 +50,7 @@ export const createSyncSlice: StateCreator<
|
|
|
50
50
|
const defaultUserName = `My ${browserInfo.browser} (${browserInfo.os})`;
|
|
51
51
|
|
|
52
52
|
set({ syncStatus: PeerSyncStatus.Connecting });
|
|
53
|
-
return
|
|
53
|
+
return syncService.enabledSync({
|
|
54
54
|
channel: {
|
|
55
55
|
name: sync.channelName,
|
|
56
56
|
password: sync.channelPassword,
|
|
@@ -80,7 +80,7 @@ export const createSyncSlice: StateCreator<
|
|
|
80
80
|
if (!userId) return false;
|
|
81
81
|
|
|
82
82
|
// if user don't enable sync, stop sync
|
|
83
|
-
if (!userEnableSync) return
|
|
83
|
+
if (!userEnableSync) return syncService.disableSync();
|
|
84
84
|
|
|
85
85
|
return get().triggerEnableSync(userId, onEvent);
|
|
86
86
|
},
|
|
@@ -15,6 +15,7 @@ export interface ServerModelProviderConfig {
|
|
|
15
15
|
|
|
16
16
|
export interface GlobalServerConfig {
|
|
17
17
|
defaultAgent?: DeepPartial<GlobalDefaultAgent>;
|
|
18
|
+
enableUploadFileToServer?: boolean;
|
|
18
19
|
enabledAccessCode?: boolean;
|
|
19
20
|
enabledOAuthSSO?: boolean;
|
|
20
21
|
languageModel?: Partial<Record<GlobalLLMProviderKey, ServerModelProviderConfig>>;
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { getServerConfig } from '@/config/server';
|
|
2
|
-
|
|
3
|
-
interface UploadResponse {
|
|
4
|
-
data: UploadData;
|
|
5
|
-
status: number;
|
|
6
|
-
success: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface UploadData {
|
|
10
|
-
account_id: any;
|
|
11
|
-
account_url: any;
|
|
12
|
-
ad_type: any;
|
|
13
|
-
ad_url: any;
|
|
14
|
-
animated: boolean;
|
|
15
|
-
bandwidth: number;
|
|
16
|
-
datetime: number;
|
|
17
|
-
deletehash: string;
|
|
18
|
-
description: any;
|
|
19
|
-
favorite: boolean;
|
|
20
|
-
has_sound: boolean;
|
|
21
|
-
height: number;
|
|
22
|
-
hls: string;
|
|
23
|
-
id: string;
|
|
24
|
-
in_gallery: boolean;
|
|
25
|
-
in_most_viral: boolean;
|
|
26
|
-
is_ad: boolean;
|
|
27
|
-
link: string;
|
|
28
|
-
mp4: string;
|
|
29
|
-
name: string;
|
|
30
|
-
nsfw: any;
|
|
31
|
-
section: any;
|
|
32
|
-
size: number;
|
|
33
|
-
tags: any[];
|
|
34
|
-
title: any;
|
|
35
|
-
type: string;
|
|
36
|
-
views: number;
|
|
37
|
-
vote: any;
|
|
38
|
-
width: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export class Imgur {
|
|
42
|
-
clientId: string;
|
|
43
|
-
api = 'https://api.imgur.com/3';
|
|
44
|
-
|
|
45
|
-
constructor() {
|
|
46
|
-
this.clientId = getServerConfig().IMGUR_CLIENT_ID;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async upload(image: Blob) {
|
|
50
|
-
const formData = new FormData();
|
|
51
|
-
|
|
52
|
-
formData.append('image', image, 'image.png');
|
|
53
|
-
|
|
54
|
-
const res = await fetch(`${this.api}/upload`, {
|
|
55
|
-
body: formData,
|
|
56
|
-
headers: {
|
|
57
|
-
Authorization: `Client-ID ${this.clientId}`,
|
|
58
|
-
},
|
|
59
|
-
method: 'POST',
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
if (!res.ok) {
|
|
63
|
-
console.log(await res.text());
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const data: UploadResponse = await res.json();
|
|
67
|
-
if (data.success) {
|
|
68
|
-
return data.data.link;
|
|
69
|
-
}
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
}
|