@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/package.json +4 -2
  5. package/src/app/trpc/{[trpc] → edge/[trpc]}/route.ts +3 -3
  6. package/src/config/__tests__/server.test.ts +0 -11
  7. package/src/config/file.ts +34 -0
  8. package/src/config/server/app.ts +0 -8
  9. package/src/config/server/provider.ts +3 -3
  10. package/src/database/client/models/file.ts +2 -1
  11. package/src/database/client/schemas/files.ts +2 -2
  12. package/src/libs/agent-runtime/google/index.test.ts +20 -1
  13. package/src/libs/agent-runtime/google/index.ts +22 -9
  14. package/src/libs/agent-runtime/utils/uriParser.test.ts +29 -0
  15. package/src/libs/agent-runtime/utils/uriParser.ts +17 -9
  16. package/src/libs/trpc/client.ts +5 -3
  17. package/src/libs/trpc/index.ts +10 -34
  18. package/src/libs/trpc/init.ts +26 -0
  19. package/src/libs/trpc/middleware/password.test.ts +87 -0
  20. package/src/libs/trpc/middleware/password.ts +26 -0
  21. package/src/libs/trpc/middleware/userAuth.test.ts +44 -0
  22. package/src/libs/trpc/middleware/userAuth.ts +18 -0
  23. package/src/server/context.ts +28 -3
  24. package/src/server/files/s3.ts +58 -0
  25. package/src/server/globalConfig/index.ts +2 -0
  26. package/src/server/mock.ts +2 -2
  27. package/src/server/routers/{config → edge/config}/index.test.ts +1 -0
  28. package/src/server/routers/edge/upload.ts +16 -0
  29. package/src/server/routers/index.ts +5 -3
  30. package/src/services/__tests__/global.test.ts +4 -5
  31. package/src/services/__tests__/sync.test.ts +56 -0
  32. package/src/services/__tests__/upload.test.ts +72 -0
  33. package/src/services/_url.ts +2 -0
  34. package/src/services/file/client.test.ts +102 -34
  35. package/src/services/file/client.ts +24 -49
  36. package/src/services/file/type.ts +1 -2
  37. package/src/services/global.ts +3 -18
  38. package/src/services/sync.ts +19 -0
  39. package/src/services/upload.ts +99 -0
  40. package/src/store/chat/slices/builtinTool/action.test.ts +4 -2
  41. package/src/store/chat/slices/builtinTool/action.ts +6 -3
  42. package/src/store/file/slices/images/action.test.ts +10 -17
  43. package/src/store/file/slices/images/action.ts +4 -1
  44. package/src/store/file/slices/tts/action.test.ts +8 -14
  45. package/src/store/file/slices/tts/action.ts +4 -1
  46. package/src/store/serverConfig/selectors.ts +1 -0
  47. package/src/store/serverConfig/store.ts +10 -0
  48. package/src/store/user/slices/common/action.ts +26 -14
  49. package/src/store/user/slices/sync/action.test.ts +6 -6
  50. package/src/store/user/slices/sync/action.ts +3 -3
  51. package/src/types/serverConfig.ts +1 -0
  52. package/src/app/api/files/image/imgur.ts +0 -72
  53. package/src/app/api/files/image/route.ts +0 -42
  54. /package/src/server/routers/{config → edge/config}/__snapshots__/index.test.ts.snap +0 -0
  55. /package/src/server/routers/{config → edge/config}/index.ts +0 -0
@@ -1,10 +1,8 @@
1
1
  import { DeepPartial } from 'utility-types';
2
2
 
3
- import { dataSync } from '@/database/client/core';
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 trpcClient.config.getGlobalConfig.query();
21
+ return edgeClient.config.getGlobalConfig.query();
24
22
  };
25
23
 
26
24
  getDefaultAgentConfig = async (): Promise<DeepPartial<LobeAgentConfig>> => {
27
- return trpcClient.config.getDefaultAgentConfig.query();
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(fileService, 'uploadImageByUrl').mockResolvedValue({ id: mockId });
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(fileService.uploadImageByUrl).toHaveBeenCalledTimes(prompts.length);
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
- fileService
64
+ uploadService
64
65
  .uploadImageByUrl(url, {
65
66
  metadata: { ...params, originPrompt: originPrompt },
66
67
  name: `${originPrompt || params.prompt}_${index}.png`,
67
68
  })
68
- .then(({ id }) => {
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.removeFile as Mock).mockResolvedValue(undefined);
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.getFile as Mock).mockResolvedValue(fileData);
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
- (fileService.uploadFile as Mock).mockRejectedValue(new Error(errorMessage));
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(fileService.uploadFile).toHaveBeenCalledWith({
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
- (fileService.uploadFile as Mock).mockResolvedValue(uploadedFileData);
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.uploadFile).toHaveBeenCalledWith({
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(uploadedFileData.id);
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 data = await fileService.uploadFile({
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 { Mock, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
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.removeFile as Mock).mockResolvedValue(undefined);
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.uploadFile as Mock).mockResolvedValue(uploadedFileData);
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.uploadFile).toHaveBeenCalledWith({
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.getFile as Mock).mockResolvedValue(fileData);
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 data = await fileService.uploadFile({
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>('fetchGlobalConfig', globalService.getGlobalConfig, {
58
- onSuccess: (data) => {
59
- if (data) {
60
- const serverSettings: DeepPartial<GlobalSettings> = {
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
- const defaultSettings = merge(get().defaultSettings, serverSettings);
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
- set({ defaultSettings, serverConfig: data }, false, n('initGlobalConfig'));
78
+ set({ defaultSettings, serverConfig: data }, false, n('initGlobalConfig'));
68
79
 
69
- get().refreshDefaultModelProviderList({ trigger: 'fetchServerConfig' });
70
- }
80
+ get().refreshDefaultModelProviderList({ trigger: 'fetchServerConfig' });
81
+ }
82
+ },
83
+ revalidateOnFocus: false,
71
84
  },
72
- revalidateOnFocus: false,
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 { globalService } from '@/services/global';
5
+ import { syncService } from '@/services/sync';
6
6
  import { useUserStore } from '@/store/user';
7
- import { userProfileSelectors } from '@/store/user/slices/auth/selectors';
8
- import { syncSettingsSelectors } from '@/store/user/slices/settings/selectors';
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(globalService, 'enabledSync').mockResolvedValueOnce(true);
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 globalService.disableSync when userEnableSync is false', async () => {
118
- const disableSyncSpy = vi.spyOn(globalService, 'disableSync').mockResolvedValueOnce(false);
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 { globalService } from '@/services/global';
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 globalService.enabledSync({
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 globalService.disableSync();
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
- }