@lobehub/chat 0.140.0 → 0.141.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 (103) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/locales/ar/common.json +34 -6
  3. package/locales/ar/setting.json +36 -0
  4. package/locales/de-DE/common.json +34 -6
  5. package/locales/de-DE/setting.json +36 -0
  6. package/locales/en-US/common.json +34 -6
  7. package/locales/en-US/setting.json +36 -0
  8. package/locales/es-ES/common.json +34 -6
  9. package/locales/es-ES/setting.json +36 -0
  10. package/locales/fr-FR/common.json +34 -6
  11. package/locales/fr-FR/setting.json +36 -0
  12. package/locales/it-IT/common.json +34 -6
  13. package/locales/it-IT/setting.json +38 -0
  14. package/locales/ja-JP/common.json +34 -6
  15. package/locales/ja-JP/setting.json +38 -0
  16. package/locales/ko-KR/common.json +34 -6
  17. package/locales/ko-KR/setting.json +36 -0
  18. package/locales/nl-NL/common.json +34 -6
  19. package/locales/nl-NL/setting.json +38 -0
  20. package/locales/pl-PL/common.json +34 -6
  21. package/locales/pl-PL/setting.json +36 -0
  22. package/locales/pt-BR/common.json +34 -6
  23. package/locales/pt-BR/setting.json +36 -0
  24. package/locales/ru-RU/common.json +34 -6
  25. package/locales/ru-RU/setting.json +36 -0
  26. package/locales/tr-TR/common.json +34 -6
  27. package/locales/tr-TR/setting.json +36 -0
  28. package/locales/vi-VN/common.json +34 -6
  29. package/locales/vi-VN/setting.json +36 -0
  30. package/locales/zh-CN/common.json +34 -6
  31. package/locales/zh-CN/setting.json +36 -0
  32. package/locales/zh-TW/common.json +34 -6
  33. package/locales/zh-TW/setting.json +36 -0
  34. package/package.json +10 -5
  35. package/src/app/chat/(desktop)/features/SessionHeader.tsx +5 -1
  36. package/src/app/chat/(mobile)/features/SessionHeader.tsx +9 -4
  37. package/src/app/chat/features/SessionListContent/List/SkeletonList.tsx +0 -1
  38. package/src/app/settings/(desktop)/features/Header.tsx +11 -1
  39. package/src/app/settings/(mobile)/features/Header/index.tsx +12 -1
  40. package/src/app/settings/features/SettingList/index.tsx +2 -1
  41. package/src/app/settings/sync/Alert.tsx +39 -0
  42. package/src/app/settings/sync/DeviceInfo/Card.tsx +41 -0
  43. package/src/app/settings/sync/DeviceInfo/DeviceName.tsx +66 -0
  44. package/src/app/settings/sync/DeviceInfo/index.tsx +117 -0
  45. package/src/app/settings/sync/PageTitle.tsx +11 -0
  46. package/src/app/settings/sync/WebRTC/ChannelNameInput.tsx +46 -0
  47. package/src/app/settings/sync/WebRTC/index.tsx +97 -0
  48. package/src/app/settings/sync/components/SyncSwitch/index.css +237 -0
  49. package/src/app/settings/sync/components/SyncSwitch/index.tsx +79 -0
  50. package/src/app/settings/sync/components/SystemIcon.tsx +16 -0
  51. package/src/app/settings/sync/layout.tsx +9 -0
  52. package/src/app/settings/sync/page.tsx +23 -0
  53. package/src/app/settings/sync/util.ts +4 -0
  54. package/src/components/BrowserIcon/components/Brave.tsx +56 -0
  55. package/src/components/BrowserIcon/components/Chrome.tsx +14 -0
  56. package/src/components/BrowserIcon/components/Chromium.tsx +14 -0
  57. package/src/components/BrowserIcon/components/Edge.tsx +36 -0
  58. package/src/components/BrowserIcon/components/Firefox.tsx +38 -0
  59. package/src/components/BrowserIcon/components/Opera.tsx +19 -0
  60. package/src/components/BrowserIcon/components/Safari.tsx +23 -0
  61. package/src/components/BrowserIcon/components/Samsung.tsx +21 -0
  62. package/src/components/BrowserIcon/index.tsx +50 -0
  63. package/src/components/BrowserIcon/types.ts +8 -0
  64. package/src/config/modelProviders/moonshot.ts +8 -0
  65. package/src/const/settings.ts +6 -0
  66. package/src/database/core/__tests__/model.test.ts +2 -2
  67. package/src/database/core/db.ts +1 -1
  68. package/src/database/core/index.ts +1 -0
  69. package/src/database/core/model.ts +83 -5
  70. package/src/database/core/sync.ts +328 -0
  71. package/src/database/models/__tests__/message.test.ts +0 -1
  72. package/src/database/models/__tests__/plugin.test.ts +5 -2
  73. package/src/database/models/file.ts +1 -1
  74. package/src/database/models/message.ts +49 -30
  75. package/src/database/models/plugin.ts +6 -5
  76. package/src/database/models/session.ts +15 -16
  77. package/src/database/models/sessionGroup.ts +14 -8
  78. package/src/database/models/topic.ts +14 -21
  79. package/src/features/SyncStatusInspector/DisableSync.tsx +79 -0
  80. package/src/features/SyncStatusInspector/EnableSync.tsx +136 -0
  81. package/src/features/SyncStatusInspector/EnableTag.tsx +66 -0
  82. package/src/features/SyncStatusInspector/index.tsx +27 -0
  83. package/src/hooks/useSyncData.ts +48 -0
  84. package/src/layout/GlobalLayout/StoreHydration.tsx +5 -0
  85. package/src/locales/default/common.ts +27 -5
  86. package/src/locales/default/setting.ts +37 -1
  87. package/src/services/chat.ts +6 -2
  88. package/src/services/config.ts +1 -1
  89. package/src/services/global.ts +15 -0
  90. package/src/store/chat/slices/topic/action.test.ts +1 -1
  91. package/src/store/chat/slices/topic/action.ts +21 -10
  92. package/src/store/global/slices/common/action.ts +71 -1
  93. package/src/store/global/slices/common/initialState.ts +9 -0
  94. package/src/store/global/slices/common/selectors.ts +1 -0
  95. package/src/store/global/slices/preference/initialState.ts +2 -1
  96. package/src/store/global/slices/preference/selectors.ts +3 -0
  97. package/src/store/global/slices/settings/selectors/index.ts +1 -0
  98. package/src/store/global/slices/settings/selectors/sync.ts +14 -0
  99. package/src/types/settings/index.ts +3 -0
  100. package/src/types/settings/sync.ts +10 -0
  101. package/src/types/sync.ts +41 -0
  102. package/src/utils/platform.ts +9 -3
  103. package/src/utils/responsive.ts +21 -0
@@ -6,6 +6,7 @@ import {
6
6
  GlobalDefaultAgent,
7
7
  GlobalLLMConfig,
8
8
  GlobalSettings,
9
+ GlobalSyncSettings,
9
10
  GlobalTTSConfig,
10
11
  } from '@/types/settings';
11
12
 
@@ -121,9 +122,14 @@ export const DEFAULT_TOOL_CONFIG = {
121
122
  },
122
123
  };
123
124
 
125
+ const DEFAULT_SYNC_CONFIG: GlobalSyncSettings = {
126
+ webrtc: { enabled: false },
127
+ };
128
+
124
129
  export const DEFAULT_SETTINGS: GlobalSettings = {
125
130
  defaultAgent: DEFAULT_AGENT,
126
131
  languageModel: DEFAULT_LLM_CONFIG,
132
+ sync: DEFAULT_SYNC_CONFIG,
127
133
  tool: DEFAULT_TOOL_CONFIG,
128
134
  tts: DEFAULT_TTS_CONFIG,
129
135
  ...DEFAULT_BASE_SETTINGS,
@@ -37,7 +37,7 @@ describe('BaseModel', () => {
37
37
  content: 'Hello, World!',
38
38
  };
39
39
 
40
- const result = await baseModel['_add'](validData);
40
+ const result = await baseModel['_addWithSync'](validData);
41
41
 
42
42
  expect(result).toHaveProperty('id');
43
43
  expect(console.error).not.toHaveBeenCalled();
@@ -49,7 +49,7 @@ describe('BaseModel', () => {
49
49
  content: 'Hello, World!',
50
50
  };
51
51
 
52
- await expect(baseModel['_add'](invalidData)).rejects.toThrow(TypeError);
52
+ await expect(baseModel['_addWithSync'](invalidData)).rejects.toThrow(TypeError);
53
53
  });
54
54
  });
55
55
  });
@@ -21,7 +21,7 @@ import {
21
21
  } from './schemas';
22
22
  import { DBModel, LOBE_CHAT_LOCAL_DB_NAME } from './types/db';
23
23
 
24
- interface LobeDBSchemaMap {
24
+ export interface LobeDBSchemaMap {
25
25
  files: DB_File;
26
26
  messages: DB_Message;
27
27
  plugins: DB_Plugin;
@@ -1,2 +1,3 @@
1
1
  export { LocalDBInstance } from './db';
2
2
  export * from './model';
3
+ export { dataSync } from './sync';
@@ -1,10 +1,11 @@
1
1
  import Dexie, { BulkError } from 'dexie';
2
2
  import { ZodObject } from 'zod';
3
3
 
4
- import { DBBaseFieldsSchema } from '@/database/core/types/db';
5
4
  import { nanoid } from '@/utils/uuid';
6
5
 
7
6
  import { LocalDB, LocalDBInstance, LocalDBSchema } from './db';
7
+ import { dataSync } from './sync';
8
+ import { DBBaseFieldsSchema } from './types/db';
8
9
 
9
10
  export class BaseModel<N extends keyof LocalDBSchema = any, T = LocalDBSchema[N]['table']> {
10
11
  protected readonly db: LocalDB;
@@ -21,10 +22,16 @@ export class BaseModel<N extends keyof LocalDBSchema = any, T = LocalDBSchema[N]
21
22
  return this.db[this._tableName] as Dexie.Table;
22
23
  }
23
24
 
25
+ get yMap() {
26
+ return dataSync.getYMap(this._tableName);
27
+ }
28
+
29
+ // **************** Create *************** //
30
+
24
31
  /**
25
32
  * create a new record
26
33
  */
27
- protected async _add<T = LocalDBSchema[N]['model']>(
34
+ protected async _addWithSync<T = LocalDBSchema[N]['model']>(
28
35
  data: T,
29
36
  id: string | number = nanoid(),
30
37
  primaryKey: string = 'id',
@@ -51,6 +58,9 @@ export class BaseModel<N extends keyof LocalDBSchema = any, T = LocalDBSchema[N]
51
58
 
52
59
  const newId = await this.db[tableName].add(record);
53
60
 
61
+ // sync data to yjs data map
62
+ this.updateYMapItem(newId);
63
+
54
64
  return { id: newId };
55
65
  }
56
66
 
@@ -69,6 +79,7 @@ export class BaseModel<N extends keyof LocalDBSchema = any, T = LocalDBSchema[N]
69
79
  */
70
80
  createWithNewId?: boolean;
71
81
  idGenerator?: () => string;
82
+ withSync?: boolean;
72
83
  } = {},
73
84
  ): Promise<{
74
85
  added: number;
@@ -77,8 +88,8 @@ export class BaseModel<N extends keyof LocalDBSchema = any, T = LocalDBSchema[N]
77
88
  skips: string[];
78
89
  success: boolean;
79
90
  }> {
80
- const { idGenerator = nanoid, createWithNewId = false } = options;
81
- const validatedData = [];
91
+ const { idGenerator = nanoid, createWithNewId = false, withSync = true } = options;
92
+ const validatedData: any[] = [];
82
93
  const errors = [];
83
94
  const skips: string[] = [];
84
95
 
@@ -123,6 +134,15 @@ export class BaseModel<N extends keyof LocalDBSchema = any, T = LocalDBSchema[N]
123
134
  try {
124
135
  await this.table.bulkAdd(validatedData);
125
136
 
137
+ if (withSync) {
138
+ dataSync.transact(() => {
139
+ const pools = validatedData.map(async (item) => {
140
+ await this.updateYMapItem(item.id);
141
+ });
142
+ Promise.all(pools);
143
+ });
144
+ }
145
+
126
146
  return {
127
147
  added: validatedData.length,
128
148
  ids: validatedData.map((item) => item.id),
@@ -144,7 +164,36 @@ export class BaseModel<N extends keyof LocalDBSchema = any, T = LocalDBSchema[N]
144
164
  }
145
165
  }
146
166
 
147
- protected async _update(id: string, data: Partial<T>) {
167
+ // **************** Delete *************** //
168
+
169
+ protected async _deleteWithSync(id: string) {
170
+ const result = await this.table.delete(id);
171
+ // sync delete data to yjs data map
172
+ this.yMap?.delete(id);
173
+ return result;
174
+ }
175
+
176
+ protected async _bulkDeleteWithSync(keys: string[]) {
177
+ await this.table.bulkDelete(keys);
178
+ // sync delete data to yjs data map
179
+
180
+ dataSync.transact(() => {
181
+ keys.forEach((id) => {
182
+ this.yMap?.delete(id);
183
+ });
184
+ });
185
+ }
186
+
187
+ protected async _clearWithSync() {
188
+ const result = await this.table.clear();
189
+ // sync clear data to yjs data map
190
+ this.yMap?.clear();
191
+ return result;
192
+ }
193
+
194
+ // **************** Update *************** //
195
+
196
+ protected async _updateWithSync(id: string, data: Partial<T>) {
148
197
  // we need to check whether the data is valid
149
198
  // pick data related schema from the full schema
150
199
  const keys = Object.keys(data);
@@ -162,6 +211,35 @@ export class BaseModel<N extends keyof LocalDBSchema = any, T = LocalDBSchema[N]
162
211
 
163
212
  const success = await this.table.update(id, { ...data, updatedAt: Date.now() });
164
213
 
214
+ // sync data to yjs data map
215
+ this.updateYMapItem(id);
216
+
165
217
  return { success };
166
218
  }
219
+
220
+ protected async _putWithSync(data: any, id: string) {
221
+ const result = await this.table.put(data, id);
222
+
223
+ // sync data to yjs data map
224
+ this.updateYMapItem(id);
225
+
226
+ return result;
227
+ }
228
+
229
+ protected async _bulkPutWithSync(items: T[]) {
230
+ await this.table.bulkPut(items);
231
+
232
+ await dataSync.transact(() => {
233
+ items.forEach((items) => {
234
+ this.updateYMapItem((items as any).id);
235
+ });
236
+ });
237
+ }
238
+
239
+ // **************** Helper *************** //
240
+
241
+ private updateYMapItem = async (id: string) => {
242
+ const newData = await this.table.get(id);
243
+ this.yMap?.set(id, newData);
244
+ };
167
245
  }
@@ -0,0 +1,328 @@
1
+ import Debug from 'debug';
2
+ import { throttle, uniqBy } from 'lodash-es';
3
+ import type { WebrtcProvider } from 'y-webrtc';
4
+ import type { Doc, Transaction } from 'yjs';
5
+
6
+ import {
7
+ OnAwarenessChange,
8
+ OnSyncEvent,
9
+ OnSyncStatusChange,
10
+ PeerSyncStatus,
11
+ StartDataSyncParams,
12
+ } from '@/types/sync';
13
+
14
+ import { LobeDBSchemaMap, LocalDBInstance } from './db';
15
+
16
+ const LOG_NAME_SPACE = 'DataSync';
17
+
18
+ class DataSync {
19
+ private _ydoc: Doc | null = null;
20
+ private provider: WebrtcProvider | null = null;
21
+
22
+ private syncParams!: StartDataSyncParams;
23
+ private onAwarenessChange!: OnAwarenessChange;
24
+
25
+ private waitForConnecting: any;
26
+
27
+ logger = Debug(LOG_NAME_SPACE);
28
+
29
+ transact(fn: (transaction: Transaction) => unknown) {
30
+ this._ydoc?.transact(fn);
31
+ }
32
+
33
+ getYMap = (tableKey: keyof LobeDBSchemaMap) => {
34
+ return this._ydoc?.getMap(tableKey);
35
+ };
36
+
37
+ startDataSync = async (params: StartDataSyncParams) => {
38
+ this.syncParams = params;
39
+ this.onAwarenessChange = params.onAwarenessChange;
40
+
41
+ // 开发时由于存在 fast refresh 全局实例会缓存在运行时中
42
+ // 因此需要在每次重新连接时清理上一次的实例
43
+ if (window.__ONLY_USE_FOR_CLEANUP_IN_DEV) {
44
+ await this.cleanConnection(window.__ONLY_USE_FOR_CLEANUP_IN_DEV);
45
+ }
46
+
47
+ await this.connect(params);
48
+ };
49
+
50
+ connect = async (params: StartDataSyncParams) => {
51
+ const {
52
+ channel,
53
+ onSyncEvent,
54
+ onSyncStatusChange,
55
+ user,
56
+ onAwarenessChange,
57
+ signaling = 'wss://y-webrtc-signaling.lobehub.com',
58
+ } = params;
59
+ // ====== 1. init yjs doc ====== //
60
+
61
+ await this.initYDoc();
62
+
63
+ this.logger('[YJS] start to listen sync event...');
64
+ this.initYjsObserve(onSyncEvent, onSyncStatusChange);
65
+
66
+ // ====== 2. init webrtc provider ====== //
67
+ this.logger(`[WebRTC] init provider... room: ${channel.name}`);
68
+ const { WebrtcProvider } = await import('y-webrtc');
69
+
70
+ // clients connected to the same room-name share document updates
71
+ this.provider = new WebrtcProvider(channel.name, this._ydoc!, {
72
+ password: channel.password,
73
+ signaling: [signaling],
74
+ });
75
+
76
+ // when fast refresh in dev, the provider will be cached in window
77
+ // so we need to clean it in destory
78
+ if (process.env.NODE_ENV === 'development') {
79
+ window.__ONLY_USE_FOR_CLEANUP_IN_DEV = this.provider;
80
+ }
81
+
82
+ this.logger(`[WebRTC] provider init success`);
83
+
84
+ // ====== 3. check signaling server connection ====== //
85
+
86
+ // 当本地设备正确连接到 WebRTC Provider 后,触发 status 事件
87
+ // 当开始连接,则开始监听事件
88
+ this.provider.on('status', async ({ connected }) => {
89
+ this.logger('[WebRTC] peer status:', connected);
90
+ if (connected) {
91
+ // this.initObserve(onSyncEvent, onSyncStatusChange);
92
+ onSyncStatusChange?.(PeerSyncStatus.Connecting);
93
+ }
94
+ });
95
+
96
+ // check the connection with signaling server
97
+ let connectionCheckCount = 0;
98
+
99
+ this.waitForConnecting = setInterval(() => {
100
+ const signalingConnection: IWebsocketClient = this.provider!.signalingConns[0];
101
+
102
+ if (signalingConnection.connected) {
103
+ onSyncStatusChange?.(PeerSyncStatus.Ready);
104
+ clearInterval(this.waitForConnecting);
105
+ return;
106
+ }
107
+
108
+ connectionCheckCount += 1;
109
+
110
+ // check for 5 times, or make it failed
111
+ if (connectionCheckCount > 5) {
112
+ onSyncStatusChange?.(PeerSyncStatus.Unconnected);
113
+ clearInterval(this.waitForConnecting);
114
+ }
115
+ }, 2000);
116
+
117
+ // ====== 4. handle data sync ====== //
118
+
119
+ // 当各方的数据均完成同步后,YJS 对象之间的数据已经一致时,触发 synced 事件
120
+ this.provider.on('synced', async ({ synced }) => {
121
+ this.logger('[WebRTC] peer sync status:', synced);
122
+ if (synced) {
123
+ this.logger('[WebRTC] start to init yjs data...');
124
+ onSyncStatusChange?.(PeerSyncStatus.Syncing);
125
+ await this.initSync();
126
+ onSyncStatusChange?.(PeerSyncStatus.Synced);
127
+ this.logger('[WebRTC] yjs data init success');
128
+ } else {
129
+ this.logger('[WebRTC] data not sync, try to reconnect in 1s...');
130
+ // await this.reconnect(params);
131
+ setTimeout(() => {
132
+ onSyncStatusChange?.(PeerSyncStatus.Syncing);
133
+ this.reconnect(params);
134
+ }, 1000);
135
+ }
136
+ });
137
+
138
+ // ====== 5. handle awareness ====== //
139
+
140
+ this.initAwareness({ onAwarenessChange, user });
141
+
142
+ return this.provider;
143
+ };
144
+
145
+ reconnect = async (params: StartDataSyncParams) => {
146
+ await this.cleanConnection(this.provider);
147
+
148
+ await this.connect(params);
149
+ };
150
+
151
+ async disconnect() {
152
+ await this.cleanConnection(this.provider);
153
+ }
154
+
155
+ private initYDoc = async () => {
156
+ if (typeof window === 'undefined') return;
157
+
158
+ this.logger('[YJS] init YDoc...');
159
+ const { Doc } = await import('yjs');
160
+ this._ydoc = new Doc();
161
+ };
162
+
163
+ private async cleanConnection(provider: WebrtcProvider | null) {
164
+ if (provider) {
165
+ this.logger(`[WebRTC] clean Connection...`);
166
+ this.logger(`[WebRTC] clean awareness...`);
167
+ provider.awareness.destroy();
168
+
169
+ this.logger(`[WebRTC] clean room...`);
170
+ provider.room?.disconnect();
171
+ provider.room?.destroy();
172
+
173
+ this.logger(`[WebRTC] clean provider...`);
174
+ provider.disconnect();
175
+ provider.destroy();
176
+
177
+ this.logger(`[WebRTC] clean yjs doc...`);
178
+ this._ydoc?.destroy();
179
+
180
+ this.logger(`[WebRTC] -------------------`);
181
+ }
182
+ }
183
+
184
+ private initSync = async () => {
185
+ await Promise.all(
186
+ ['sessions', 'sessionGroups', 'topics', 'messages', 'plugins'].map(async (tableKey) =>
187
+ this.loadDataFromDBtoYjs(tableKey as keyof LobeDBSchemaMap),
188
+ ),
189
+ );
190
+ };
191
+
192
+ private initYjsObserve = (onEvent: OnSyncEvent, onSyncStatusChange: OnSyncStatusChange) => {
193
+ ['sessions', 'sessionGroups', 'topics', 'messages', 'plugins'].forEach((tableKey) => {
194
+ // listen yjs change
195
+ this.observeYMapChange(tableKey as keyof LobeDBSchemaMap, onEvent, onSyncStatusChange);
196
+ });
197
+ };
198
+
199
+ private observeYMapChange = (
200
+ tableKey: keyof LobeDBSchemaMap,
201
+ onEvent: OnSyncEvent,
202
+ onSyncStatusChange: OnSyncStatusChange,
203
+ ) => {
204
+ const table = LocalDBInstance[tableKey];
205
+ const yItemMap = this.getYMap(tableKey);
206
+ const updateSyncEvent = throttle(onEvent, 1000);
207
+
208
+ // 定义一个变量来保存定时器的ID
209
+ // eslint-disable-next-line no-undef
210
+ let debounceTimer: NodeJS.Timeout;
211
+
212
+ yItemMap?.observe(async (event) => {
213
+ // abort local change
214
+ if (event.transaction.local) return;
215
+
216
+ // 每次有变更时,都先清除之前的定时器(如果有的话),然后设置新的定时器
217
+ clearTimeout(debounceTimer);
218
+
219
+ onSyncStatusChange(PeerSyncStatus.Syncing);
220
+
221
+ this.logger(`[YJS] observe ${tableKey} changes:`, event.keysChanged.size);
222
+ const pools = Array.from(event.keys).map(async ([id, payload]) => {
223
+ const item: any = yItemMap.get(id);
224
+
225
+ switch (payload.action) {
226
+ case 'add':
227
+ case 'update': {
228
+ await table.put(item, id);
229
+
230
+ break;
231
+ }
232
+
233
+ case 'delete': {
234
+ await table.delete(id);
235
+ break;
236
+ }
237
+ }
238
+ });
239
+
240
+ await Promise.all(pools);
241
+
242
+ updateSyncEvent(tableKey);
243
+
244
+ // 设置定时器,2000ms 后更新状态为'synced'
245
+ debounceTimer = setTimeout(() => {
246
+ onSyncStatusChange(PeerSyncStatus.Synced);
247
+ }, 2000);
248
+ });
249
+ };
250
+
251
+ private loadDataFromDBtoYjs = async (tableKey: keyof LobeDBSchemaMap) => {
252
+ const table = LocalDBInstance[tableKey];
253
+ const items = await table.toArray();
254
+ const yItemMap = this.getYMap(tableKey);
255
+
256
+ // 定义每批次最多包含的数据条数
257
+ const batchSize = 50;
258
+
259
+ // 计算总批次数
260
+ const totalBatches = Math.ceil(items.length / batchSize);
261
+
262
+ for (let i = 0; i < totalBatches; i++) {
263
+ // 计算当前批次的起始和结束索引
264
+ const start = i * batchSize;
265
+ const end = start + batchSize;
266
+
267
+ // 获取当前批次的数据
268
+ const batchItems = items.slice(start, end);
269
+
270
+ // 将当前批次的数据推送到 Yjs 中
271
+ this._ydoc?.transact(() => {
272
+ batchItems.forEach((item) => {
273
+ yItemMap!.set(item.id, item);
274
+ });
275
+ });
276
+ }
277
+
278
+ this.logger('[DB]:', tableKey, yItemMap?.size);
279
+ };
280
+
281
+ private initAwareness = ({ user }: Pick<StartDataSyncParams, 'user' | 'onAwarenessChange'>) => {
282
+ if (!this.provider) return;
283
+
284
+ const awareness = this.provider.awareness;
285
+
286
+ awareness.setLocalState({ clientID: awareness.clientID, user });
287
+ this.onAwarenessChange?.([{ ...user, clientID: awareness.clientID, current: true }]);
288
+
289
+ awareness.on('change', () => this.syncAwarenessToUI());
290
+ };
291
+
292
+ private syncAwarenessToUI = async () => {
293
+ const awareness = this.provider?.awareness;
294
+
295
+ if (!awareness) return;
296
+
297
+ const state = Array.from(awareness.getStates().values()).map((s) => ({
298
+ ...s.user,
299
+ clientID: s.clientID,
300
+ current: s.clientID === awareness.clientID,
301
+ }));
302
+
303
+ this.onAwarenessChange?.(uniqBy(state, 'id'));
304
+ };
305
+ }
306
+
307
+ export const dataSync = new DataSync();
308
+
309
+ interface IWebsocketClient {
310
+ binaryType: 'arraybuffer' | 'blob' | null;
311
+ connect(): void;
312
+ connected: boolean;
313
+ connecting: boolean;
314
+ destroy(): void;
315
+ disconnect(): void;
316
+ lastMessageReceived: number;
317
+ send(message: any): void;
318
+ shouldConnect: boolean;
319
+ unsuccessfulReconnects: number;
320
+ url: string;
321
+ ws: WebSocket;
322
+ }
323
+
324
+ declare global {
325
+ interface Window {
326
+ __ONLY_USE_FOR_CLEANUP_IN_DEV?: WebrtcProvider | null;
327
+ }
328
+ }
@@ -1,6 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
2
 
3
- import { DB_Message } from '@/database/schemas/message';
4
3
  import { ChatMessage } from '@/types/message';
5
4
 
6
5
  import { CreateMessageParams, MessageModel } from '../message';
@@ -59,11 +59,14 @@ describe('PluginModel', () => {
59
59
  describe('update', () => {
60
60
  it('should update a plugin', async () => {
61
61
  await PluginModel.create(pluginData);
62
- const updatedPluginData: DB_Plugin = { ...pluginData, type: 'customPlugin' };
62
+ const updatedPluginData: DB_Plugin = {
63
+ ...pluginData,
64
+ type: 'customPlugin',
65
+ };
63
66
  await PluginModel.update(pluginData.identifier, updatedPluginData);
64
67
  const plugins = await PluginModel.getList();
65
68
  expect(plugins).toHaveLength(1);
66
- expect(plugins[0]).toEqual(updatedPluginData);
69
+ expect(plugins[0]).toEqual({ ...updatedPluginData, updatedAt: expect.any(Number) });
67
70
  });
68
71
  });
69
72
 
@@ -11,7 +11,7 @@ class _FileModel extends BaseModel<'files'> {
11
11
  async create(file: DB_File) {
12
12
  const id = nanoid();
13
13
 
14
- return this._add(file, `file-${id}`);
14
+ return this._addWithSync(file, `file-${id}`);
15
15
  }
16
16
 
17
17
  async findById(id: string) {
@@ -107,13 +107,13 @@ class _MessageModel extends BaseModel {
107
107
 
108
108
  const messageData: DB_Message = this.mapChatMessageToDBMessage(data as ChatMessage);
109
109
 
110
- return this._add(messageData, id);
110
+ return this._addWithSync(messageData, id);
111
111
  }
112
112
 
113
113
  async batchCreate(messages: ChatMessage[]) {
114
114
  const data: DB_Message[] = messages.map((m) => this.mapChatMessageToDBMessage(m));
115
115
 
116
- return this._batchAdd(data);
116
+ return this._batchAdd(data, { withSync: true });
117
117
  }
118
118
 
119
119
  async duplicateMessages(messages: ChatMessage[]): Promise<ChatMessage[]> {
@@ -123,36 +123,14 @@ class _MessageModel extends BaseModel {
123
123
  return duplicatedMessages;
124
124
  }
125
125
 
126
- async createDuplicateMessages(messages: ChatMessage[]): Promise<ChatMessage[]> {
127
- // 创建一个映射来存储原始消息ID和复制消息ID之间的关系
128
- const idMapping = new Map<string, string>();
129
-
130
- // 首先复制所有消息,并为每个复制的消息生成新的ID
131
- const duplicatedMessages = messages.map((originalMessage) => {
132
- const newId = nanoid();
133
- idMapping.set(originalMessage.id, newId);
134
-
135
- return { ...originalMessage, id: newId };
136
- });
137
-
138
- // 更新 parentId 为复制后的新ID
139
- for (const duplicatedMessage of duplicatedMessages) {
140
- if (duplicatedMessage.parentId && idMapping.has(duplicatedMessage.parentId)) {
141
- duplicatedMessage.parentId = idMapping.get(duplicatedMessage.parentId);
142
- }
143
- }
144
-
145
- return duplicatedMessages;
146
- }
147
-
148
126
  // **************** Delete *************** //
149
127
 
150
128
  async delete(id: string) {
151
- return this.table.delete(id);
129
+ return super._deleteWithSync(id);
152
130
  }
153
131
 
154
132
  async clearTable() {
155
- return this.table.clear();
133
+ return this._clearWithSync();
156
134
  }
157
135
 
158
136
  /**
@@ -178,13 +156,32 @@ class _MessageModel extends BaseModel {
178
156
  const messageIds = await query.primaryKeys();
179
157
 
180
158
  // Use the bulkDelete method to delete all selected messages in bulk
181
- return this.table.bulkDelete(messageIds);
159
+ return this._bulkDeleteWithSync(messageIds);
160
+ }
161
+
162
+ async batchDeleteBySessionId(sessionId: string): Promise<void> {
163
+ // If topicId is specified, use both assistantId and topicId as the filter criteria in the query.
164
+ // Otherwise, filter by assistantId and require that topicId is undefined.
165
+ const messageIds = await this.table.where('sessionId').equals(sessionId).primaryKeys();
166
+
167
+ // Use the bulkDelete method to delete all selected messages in bulk
168
+ return this._bulkDeleteWithSync(messageIds);
169
+ }
170
+
171
+ /**
172
+ * Delete all messages associated with the topicId
173
+ * @param topicId
174
+ */
175
+ async batchDeleteByTopicId(topicId: string): Promise<void> {
176
+ const messageIds = await this.table.where('topicId').equals(topicId).primaryKeys();
177
+
178
+ return this._bulkDeleteWithSync(messageIds);
182
179
  }
183
180
 
184
181
  // **************** Update *************** //
185
182
 
186
183
  async update(id: string, data: DeepPartial<DB_Message>) {
187
- return this._update(id, data);
184
+ return super._updateWithSync(id, data);
188
185
  }
189
186
 
190
187
  async updatePluginState(id: string, key: string, value: any) {
@@ -202,7 +199,7 @@ class _MessageModel extends BaseModel {
202
199
  */
203
200
  async batchUpdate(messageIds: string[], updateFields: Partial<DB_Message>): Promise<number> {
204
201
  // Retrieve the messages by their IDs
205
- const messagesToUpdate = await this.table.where(':id').anyOf(messageIds).toArray();
202
+ const messagesToUpdate = await this.table.where('id').anyOf(messageIds).toArray();
206
203
 
207
204
  // Update the specified fields of each message
208
205
  const updatedMessages = messagesToUpdate.map((message) => ({
@@ -211,13 +208,35 @@ class _MessageModel extends BaseModel {
211
208
  }));
212
209
 
213
210
  // Use the bulkPut method to update the messages in bulk
214
- await this.table.bulkPut(updatedMessages);
211
+ await this._bulkPutWithSync(updatedMessages);
215
212
 
216
213
  return updatedMessages.length;
217
214
  }
218
215
 
219
216
  // **************** Helper *************** //
220
217
 
218
+ private async createDuplicateMessages(messages: ChatMessage[]): Promise<ChatMessage[]> {
219
+ // 创建一个映射来存储原始消息ID和复制消息ID之间的关系
220
+ const idMapping = new Map<string, string>();
221
+
222
+ // 首先复制所有消息,并为每个复制的消息生成新的ID
223
+ const duplicatedMessages = messages.map((originalMessage) => {
224
+ const newId = nanoid();
225
+ idMapping.set(originalMessage.id, newId);
226
+
227
+ return { ...originalMessage, id: newId };
228
+ });
229
+
230
+ // 更新 parentId 为复制后的新ID
231
+ for (const duplicatedMessage of duplicatedMessages) {
232
+ if (duplicatedMessage.parentId && idMapping.has(duplicatedMessage.parentId)) {
233
+ duplicatedMessage.parentId = idMapping.get(duplicatedMessage.parentId);
234
+ }
235
+ }
236
+
237
+ return duplicatedMessages;
238
+ }
239
+
221
240
  private mapChatMessageToDBMessage(message: ChatMessage): DB_Message {
222
241
  const { extra, ...messageData } = message;
223
242