@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.
- package/CHANGELOG.md +50 -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/config/modelProviders/moonshot.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
package/src/const/settings.ts
CHANGED
|
@@ -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['
|
|
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['
|
|
52
|
+
await expect(baseModel['_addWithSync'](invalidData)).rejects.toThrow(TypeError);
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
});
|
package/src/database/core/db.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
|