@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.
- package/CHANGELOG.md +25 -0
- package/locales/ar/common.json +34 -6
- package/locales/ar/setting.json +36 -0
- package/locales/de-DE/common.json +34 -6
- package/locales/de-DE/setting.json +36 -0
- package/locales/en-US/common.json +34 -6
- package/locales/en-US/setting.json +36 -0
- package/locales/es-ES/common.json +34 -6
- package/locales/es-ES/setting.json +36 -0
- package/locales/fr-FR/common.json +34 -6
- package/locales/fr-FR/setting.json +36 -0
- package/locales/it-IT/common.json +34 -6
- package/locales/it-IT/setting.json +38 -0
- package/locales/ja-JP/common.json +34 -6
- package/locales/ja-JP/setting.json +38 -0
- package/locales/ko-KR/common.json +34 -6
- package/locales/ko-KR/setting.json +36 -0
- package/locales/nl-NL/common.json +34 -6
- package/locales/nl-NL/setting.json +38 -0
- package/locales/pl-PL/common.json +34 -6
- package/locales/pl-PL/setting.json +36 -0
- package/locales/pt-BR/common.json +34 -6
- package/locales/pt-BR/setting.json +36 -0
- package/locales/ru-RU/common.json +34 -6
- package/locales/ru-RU/setting.json +36 -0
- package/locales/tr-TR/common.json +34 -6
- package/locales/tr-TR/setting.json +36 -0
- package/locales/vi-VN/common.json +34 -6
- package/locales/vi-VN/setting.json +36 -0
- package/locales/zh-CN/common.json +34 -6
- package/locales/zh-CN/setting.json +36 -0
- package/locales/zh-TW/common.json +34 -6
- package/locales/zh-TW/setting.json +36 -0
- package/package.json +10 -5
- package/src/app/chat/(desktop)/features/SessionHeader.tsx +5 -1
- package/src/app/chat/(mobile)/features/SessionHeader.tsx +9 -4
- package/src/app/chat/features/SessionListContent/List/SkeletonList.tsx +0 -1
- package/src/app/settings/(desktop)/features/Header.tsx +11 -1
- package/src/app/settings/(mobile)/features/Header/index.tsx +12 -1
- package/src/app/settings/features/SettingList/index.tsx +2 -1
- package/src/app/settings/sync/Alert.tsx +39 -0
- package/src/app/settings/sync/DeviceInfo/Card.tsx +41 -0
- package/src/app/settings/sync/DeviceInfo/DeviceName.tsx +66 -0
- package/src/app/settings/sync/DeviceInfo/index.tsx +117 -0
- package/src/app/settings/sync/PageTitle.tsx +11 -0
- package/src/app/settings/sync/WebRTC/ChannelNameInput.tsx +46 -0
- package/src/app/settings/sync/WebRTC/index.tsx +97 -0
- package/src/app/settings/sync/components/SyncSwitch/index.css +237 -0
- package/src/app/settings/sync/components/SyncSwitch/index.tsx +79 -0
- package/src/app/settings/sync/components/SystemIcon.tsx +16 -0
- package/src/app/settings/sync/layout.tsx +9 -0
- package/src/app/settings/sync/page.tsx +23 -0
- package/src/app/settings/sync/util.ts +4 -0
- package/src/components/BrowserIcon/components/Brave.tsx +56 -0
- package/src/components/BrowserIcon/components/Chrome.tsx +14 -0
- package/src/components/BrowserIcon/components/Chromium.tsx +14 -0
- package/src/components/BrowserIcon/components/Edge.tsx +36 -0
- package/src/components/BrowserIcon/components/Firefox.tsx +38 -0
- package/src/components/BrowserIcon/components/Opera.tsx +19 -0
- package/src/components/BrowserIcon/components/Safari.tsx +23 -0
- package/src/components/BrowserIcon/components/Samsung.tsx +21 -0
- package/src/components/BrowserIcon/index.tsx +50 -0
- package/src/components/BrowserIcon/types.ts +8 -0
- package/src/const/settings.ts +6 -0
- package/src/database/core/__tests__/model.test.ts +2 -2
- package/src/database/core/db.ts +1 -1
- package/src/database/core/index.ts +1 -0
- package/src/database/core/model.ts +83 -5
- package/src/database/core/sync.ts +328 -0
- package/src/database/models/__tests__/message.test.ts +0 -1
- package/src/database/models/__tests__/plugin.test.ts +5 -2
- package/src/database/models/file.ts +1 -1
- package/src/database/models/message.ts +49 -30
- package/src/database/models/plugin.ts +6 -5
- package/src/database/models/session.ts +15 -16
- package/src/database/models/sessionGroup.ts +14 -8
- package/src/database/models/topic.ts +14 -21
- package/src/features/SyncStatusInspector/DisableSync.tsx +79 -0
- package/src/features/SyncStatusInspector/EnableSync.tsx +136 -0
- package/src/features/SyncStatusInspector/EnableTag.tsx +66 -0
- package/src/features/SyncStatusInspector/index.tsx +27 -0
- package/src/hooks/useSyncData.ts +48 -0
- package/src/layout/GlobalLayout/StoreHydration.tsx +5 -0
- package/src/locales/default/common.ts +27 -5
- package/src/locales/default/setting.ts +37 -1
- package/src/services/chat.ts +6 -2
- package/src/services/config.ts +1 -1
- package/src/services/global.ts +15 -0
- package/src/store/chat/slices/topic/action.test.ts +1 -1
- package/src/store/chat/slices/topic/action.ts +21 -10
- package/src/store/global/slices/common/action.ts +71 -1
- package/src/store/global/slices/common/initialState.ts +9 -0
- package/src/store/global/slices/common/selectors.ts +1 -0
- package/src/store/global/slices/preference/initialState.ts +2 -1
- package/src/store/global/slices/preference/selectors.ts +3 -0
- package/src/store/global/slices/settings/selectors/index.ts +1 -0
- package/src/store/global/slices/settings/selectors/sync.ts +14 -0
- package/src/types/settings/index.ts +3 -0
- package/src/types/settings/sync.ts +10 -0
- package/src/types/sync.ts +41 -0
- package/src/utils/platform.ts +9 -3
- 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
|
+
}
|
|
@@ -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 = {
|
|
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
|
|
|
@@ -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.
|
|
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
|
|
129
|
+
return super._deleteWithSync(id);
|
|
152
130
|
}
|
|
153
131
|
|
|
154
132
|
async clearTable() {
|
|
155
|
-
return this.
|
|
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.
|
|
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
|
|
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('
|
|
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.
|
|
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.
|
|
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.
|
|
41
|
+
return this._deleteWithSync(id);
|
|
43
42
|
}
|
|
44
43
|
clear() {
|
|
45
|
-
return this.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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)
|
|
57
|
+
if (session.group === id) {
|
|
58
|
+
await SessionModel.update(session.id, { group: 'default' });
|
|
59
|
+
}
|
|
57
60
|
});
|
|
61
|
+
|
|
58
62
|
if (!removeGroupItem) {
|
|
59
|
-
return this.
|
|
63
|
+
return this._deleteWithSync(id);
|
|
60
64
|
} else {
|
|
61
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
84
|
+
await this.update(id, { sort });
|
|
79
85
|
}
|
|
80
86
|
});
|
|
81
87
|
}
|