@lobehub/chat 0.140.1 → 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 (102) hide show
  1. package/CHANGELOG.md +25 -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/const/settings.ts +6 -0
  65. package/src/database/core/__tests__/model.test.ts +2 -2
  66. package/src/database/core/db.ts +1 -1
  67. package/src/database/core/index.ts +1 -0
  68. package/src/database/core/model.ts +83 -5
  69. package/src/database/core/sync.ts +328 -0
  70. package/src/database/models/__tests__/message.test.ts +0 -1
  71. package/src/database/models/__tests__/plugin.test.ts +5 -2
  72. package/src/database/models/file.ts +1 -1
  73. package/src/database/models/message.ts +49 -30
  74. package/src/database/models/plugin.ts +6 -5
  75. package/src/database/models/session.ts +15 -16
  76. package/src/database/models/sessionGroup.ts +14 -8
  77. package/src/database/models/topic.ts +14 -21
  78. package/src/features/SyncStatusInspector/DisableSync.tsx +79 -0
  79. package/src/features/SyncStatusInspector/EnableSync.tsx +136 -0
  80. package/src/features/SyncStatusInspector/EnableTag.tsx +66 -0
  81. package/src/features/SyncStatusInspector/index.tsx +27 -0
  82. package/src/hooks/useSyncData.ts +48 -0
  83. package/src/layout/GlobalLayout/StoreHydration.tsx +5 -0
  84. package/src/locales/default/common.ts +27 -5
  85. package/src/locales/default/setting.ts +37 -1
  86. package/src/services/chat.ts +6 -2
  87. package/src/services/config.ts +1 -1
  88. package/src/services/global.ts +15 -0
  89. package/src/store/chat/slices/topic/action.test.ts +1 -1
  90. package/src/store/chat/slices/topic/action.ts +21 -10
  91. package/src/store/global/slices/common/action.ts +71 -1
  92. package/src/store/global/slices/common/initialState.ts +9 -0
  93. package/src/store/global/slices/common/selectors.ts +1 -0
  94. package/src/store/global/slices/preference/initialState.ts +2 -1
  95. package/src/store/global/slices/preference/selectors.ts +3 -0
  96. package/src/store/global/slices/settings/selectors/index.ts +1 -0
  97. package/src/store/global/slices/settings/selectors/sync.ts +14 -0
  98. package/src/types/settings/index.ts +3 -0
  99. package/src/types/settings/sync.ts +10 -0
  100. package/src/types/sync.ts +41 -0
  101. package/src/utils/platform.ts +9 -3
  102. package/src/utils/responsive.ts +21 -0
@@ -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
 
@@ -21,14 +21,13 @@ class _PluginModel extends BaseModel {
21
21
  getList = async (): Promise<DB_Plugin[]> => {
22
22
  return this.table.toArray();
23
23
  };
24
-
25
24
  // **************** Create *************** //
26
25
 
27
26
  create = async (plugin: InstallPluginParams) => {
28
27
  const old = await this.table.get(plugin.identifier);
29
28
  const dbPlugin = this.mapToDBPlugin(plugin);
30
29
 
31
- return this.table.put(merge(old, dbPlugin), plugin.identifier);
30
+ return this._putWithSync(merge(old, dbPlugin), plugin.identifier);
32
31
  };
33
32
 
34
33
  batchCreate = async (plugins: LobeTool[]) => {
@@ -39,16 +38,18 @@ class _PluginModel extends BaseModel {
39
38
  // **************** Delete *************** //
40
39
 
41
40
  delete(id: string) {
42
- return this.table.delete(id);
41
+ return this._deleteWithSync(id);
43
42
  }
44
43
  clear() {
45
- return this.table.clear();
44
+ return this._clearWithSync();
46
45
  }
47
46
 
48
47
  // **************** Update *************** //
49
48
 
50
49
  update: (id: string, value: Partial<DB_Plugin>) => Promise<number> = async (id, value) => {
51
- return this.table.update(id, value);
50
+ const { success } = await this._updateWithSync(id, value);
51
+
52
+ return success;
52
53
  };
53
54
 
54
55
  // **************** Helper *************** //
@@ -3,7 +3,6 @@ import { DeepPartial } from 'utility-types';
3
3
  import { DEFAULT_AGENT_LOBE_SESSION } from '@/const/session';
4
4
  import { BaseModel } from '@/database/core';
5
5
  import { DBModel } from '@/database/core/types/db';
6
- import { SessionGroupModel } from '@/database/models/sessionGroup';
7
6
  import { DB_Session, DB_SessionSchema } from '@/database/schemas/session';
8
7
  import { LobeAgentConfig } from '@/types/agent';
9
8
  import {
@@ -16,6 +15,10 @@ import {
16
15
  import { merge } from '@/utils/merge';
17
16
  import { uuid } from '@/utils/uuid';
18
17
 
18
+ import { MessageModel } from './message';
19
+ import { SessionGroupModel } from './sessionGroup';
20
+ import { TopicModel } from './topic';
21
+
19
22
  class _SessionModel extends BaseModel {
20
23
  constructor() {
21
24
  super('sessions', DB_SessionSchema);
@@ -173,7 +176,7 @@ class _SessionModel extends BaseModel {
173
176
  async create(type: 'agent' | 'group', defaultValue: Partial<LobeAgentSession>, id = uuid()) {
174
177
  const data = merge(DEFAULT_AGENT_LOBE_SESSION, { type, ...defaultValue });
175
178
  const dataDB = this.mapToDB_Session(data);
176
- return this._add(dataDB, id);
179
+ return this._addWithSync(dataDB, id);
177
180
  }
178
181
 
179
182
  async batchCreate(sessions: LobeAgentSession[]) {
@@ -200,7 +203,7 @@ class _SessionModel extends BaseModel {
200
203
 
201
204
  const newSession = merge(session, { meta: { title: newTitle } });
202
205
 
203
- return this._add(newSession, uuid());
206
+ return this._addWithSync(newSession, uuid());
204
207
  }
205
208
 
206
209
  // **************** Delete *************** //
@@ -211,32 +214,28 @@ class _SessionModel extends BaseModel {
211
214
  async delete(id: string) {
212
215
  return this.db.transaction('rw', [this.table, this.db.topics, this.db.messages], async () => {
213
216
  // Delete all topics associated with the session
214
- const topics = await this.db.topics.where('sessionId').equals(id).toArray();
215
- const topicIds = topics.map((topic) => topic.id);
216
- if (topicIds.length > 0) {
217
- await this.db.topics.bulkDelete(topicIds);
218
- }
217
+ await TopicModel.batchDeleteBySessionId(id);
219
218
 
220
219
  // Delete all messages associated with the session
221
- const messages = await this.db.messages.where('sessionId').equals(id).toArray();
222
- const messageIds = messages.map((message) => message.id);
223
- if (messageIds.length > 0) {
224
- await this.db.messages.bulkDelete(messageIds);
225
- }
220
+ await MessageModel.batchDeleteBySessionId(id);
226
221
 
227
222
  // Finally, delete the session itself
228
- await this.table.delete(id);
223
+ await this._deleteWithSync(id);
229
224
  });
230
225
  }
231
226
 
227
+ async batchDelete(ids: string[]) {
228
+ return this._bulkDeleteWithSync(ids);
229
+ }
230
+
232
231
  async clearTable() {
233
- return this.table.clear();
232
+ return this._clearWithSync();
234
233
  }
235
234
 
236
235
  // **************** Update *************** //
237
236
 
238
237
  async update(id: string, data: Partial<DB_Session>) {
239
- return this._update(id, data);
238
+ return super._updateWithSync(id, data);
240
239
  }
241
240
 
242
241
  async updatePinned(id: string, pinned: boolean) {
@@ -42,7 +42,7 @@ class _SessionGroupModel extends BaseModel {
42
42
  // **************** Create *************** //
43
43
 
44
44
  async create(name: string, sort?: number, id = nanoid()) {
45
- return this._add({ name, sort }, id);
45
+ return this._addWithSync({ name, sort }, id);
46
46
  }
47
47
 
48
48
  async batchCreate(groups: SessionGroups) {
@@ -51,31 +51,37 @@ class _SessionGroupModel extends BaseModel {
51
51
 
52
52
  // **************** Delete *************** //
53
53
  async delete(id: string, removeGroupItem: boolean = false) {
54
- this.db.sessions.toCollection().modify((session) => {
54
+ const { SessionModel } = await import('./session');
55
+ this.db.sessions.toCollection().modify(async (session) => {
55
56
  // update all session associated with the sessionGroup to default
56
- if (session.group === id) session.group = 'default';
57
+ if (session.group === id) {
58
+ await SessionModel.update(session.id, { group: 'default' });
59
+ }
57
60
  });
61
+
58
62
  if (!removeGroupItem) {
59
- return this.table.delete(id);
63
+ return this._deleteWithSync(id);
60
64
  } else {
61
- return this.db.sessions.where('group').equals(id).delete();
65
+ const sessionIds = await this.db.sessions.where('group').equals(id).primaryKeys();
66
+
67
+ return await SessionModel.batchDelete(sessionIds);
62
68
  }
63
69
  }
64
70
 
65
71
  async clear() {
66
- this.table.clear();
72
+ await this._clearWithSync();
67
73
  }
68
74
 
69
75
  // **************** Update *************** //
70
76
 
71
77
  async update(id: string, data: Partial<DB_SessionGroup>) {
72
- return super._update(id, data);
78
+ return super._updateWithSync(id, data);
73
79
  }
74
80
 
75
81
  async updateOrder(sortMap: { id: string; sort: number }[]) {
76
82
  return this.db.transaction('rw', this.table, async () => {
77
83
  for (const { id, sort } of sortMap) {
78
- await this.table.update(id, { sort });
84
+ await this.update(id, { sort });
79
85
  }
80
86
  });
81
87
  }