@lobehub/chat 1.76.1 → 1.77.1
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/changelog/v1.json +18 -0
- package/locales/ar/common.json +13 -2
- package/locales/ar/error.json +10 -0
- package/locales/ar/models.json +9 -6
- package/locales/ar/setting.json +28 -0
- package/locales/bg-BG/common.json +13 -2
- package/locales/bg-BG/error.json +10 -0
- package/locales/bg-BG/models.json +9 -6
- package/locales/bg-BG/setting.json +28 -0
- package/locales/de-DE/common.json +13 -2
- package/locales/de-DE/error.json +10 -0
- package/locales/de-DE/models.json +9 -6
- package/locales/de-DE/setting.json +28 -0
- package/locales/en-US/common.json +13 -2
- package/locales/en-US/error.json +10 -0
- package/locales/en-US/models.json +9 -6
- package/locales/en-US/setting.json +28 -0
- package/locales/es-ES/common.json +13 -2
- package/locales/es-ES/error.json +10 -0
- package/locales/es-ES/models.json +9 -6
- package/locales/es-ES/setting.json +28 -0
- package/locales/fa-IR/common.json +13 -2
- package/locales/fa-IR/error.json +10 -0
- package/locales/fa-IR/models.json +9 -6
- package/locales/fa-IR/setting.json +28 -0
- package/locales/fr-FR/common.json +13 -2
- package/locales/fr-FR/error.json +10 -0
- package/locales/fr-FR/models.json +9 -6
- package/locales/fr-FR/setting.json +28 -0
- package/locales/it-IT/common.json +13 -2
- package/locales/it-IT/error.json +10 -0
- package/locales/it-IT/models.json +9 -6
- package/locales/it-IT/setting.json +28 -0
- package/locales/ja-JP/common.json +13 -2
- package/locales/ja-JP/error.json +10 -0
- package/locales/ja-JP/models.json +9 -6
- package/locales/ja-JP/setting.json +28 -0
- package/locales/ko-KR/common.json +13 -2
- package/locales/ko-KR/error.json +10 -0
- package/locales/ko-KR/models.json +9 -6
- package/locales/ko-KR/setting.json +28 -0
- package/locales/nl-NL/common.json +13 -2
- package/locales/nl-NL/error.json +10 -0
- package/locales/nl-NL/models.json +9 -6
- package/locales/nl-NL/setting.json +28 -0
- package/locales/pl-PL/common.json +13 -2
- package/locales/pl-PL/error.json +10 -0
- package/locales/pl-PL/models.json +9 -6
- package/locales/pl-PL/setting.json +28 -0
- package/locales/pt-BR/common.json +13 -2
- package/locales/pt-BR/error.json +10 -0
- package/locales/pt-BR/models.json +9 -6
- package/locales/pt-BR/setting.json +28 -0
- package/locales/ru-RU/common.json +13 -2
- package/locales/ru-RU/error.json +10 -0
- package/locales/ru-RU/models.json +9 -6
- package/locales/ru-RU/setting.json +28 -0
- package/locales/tr-TR/common.json +13 -2
- package/locales/tr-TR/error.json +10 -0
- package/locales/tr-TR/models.json +9 -6
- package/locales/tr-TR/setting.json +28 -0
- package/locales/vi-VN/common.json +13 -2
- package/locales/vi-VN/error.json +10 -0
- package/locales/vi-VN/models.json +9 -6
- package/locales/vi-VN/setting.json +28 -0
- package/locales/zh-CN/common.json +13 -2
- package/locales/zh-CN/error.json +10 -0
- package/locales/zh-CN/models.json +10 -7
- package/locales/zh-CN/setting.json +28 -0
- package/locales/zh-TW/common.json +13 -2
- package/locales/zh-TW/error.json +10 -0
- package/locales/zh-TW/models.json +9 -6
- package/locales/zh-TW/setting.json +28 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/(mobile)/me/data/features/Category.tsx +2 -2
- package/src/app/[variants]/(main)/chat/features/Migration/UpgradeButton.tsx +2 -1
- package/src/app/[variants]/(main)/settings/common/features/Common.tsx +0 -44
- package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +40 -14
- package/src/app/[variants]/(main)/settings/storage/Advanced.tsx +108 -0
- package/src/app/[variants]/(main)/settings/storage/IndexedDBStorage.tsx +55 -0
- package/src/app/[variants]/(main)/settings/storage/page.tsx +17 -0
- package/src/components/GroupIcon/index.tsx +25 -0
- package/src/components/IndexCard/index.tsx +143 -0
- package/src/components/ProgressItem/index.tsx +75 -0
- package/src/database/models/__tests__/session.test.ts +21 -0
- package/src/database/repositories/dataExporter/index.test.ts +330 -0
- package/src/database/repositories/dataExporter/index.ts +216 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/agents.json +65 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/agentsToSessions.json +541 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/topic.json +269 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/userSettings.json +18 -0
- package/src/database/repositories/dataImporter/__tests__/fixtures/with-client-id.json +778 -0
- package/src/database/repositories/dataImporter/__tests__/index.test.ts +120 -880
- package/src/database/repositories/dataImporter/deprecated/__tests__/index.test.ts +940 -0
- package/src/database/repositories/dataImporter/deprecated/index.ts +326 -0
- package/src/database/repositories/dataImporter/index.ts +684 -289
- package/src/database/server/models/session.ts +85 -9
- package/src/features/DataImporter/ImportDetail.tsx +203 -0
- package/src/features/DataImporter/SuccessResult.tsx +22 -6
- package/src/features/DataImporter/_deprecated.ts +43 -0
- package/src/features/DataImporter/config.ts +21 -0
- package/src/features/DataImporter/index.tsx +112 -31
- package/src/features/DevPanel/PostgresViewer/DataTable/index.tsx +6 -0
- package/src/features/User/UserPanel/useMenu.tsx +1 -36
- package/src/features/User/__tests__/useMenu.test.tsx +0 -2
- package/src/locales/default/common.ts +12 -1
- package/src/locales/default/error.ts +10 -0
- package/src/locales/default/setting.ts +28 -0
- package/src/server/routers/lambda/exporter.ts +25 -0
- package/src/server/routers/lambda/importer.ts +19 -3
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/services/config.ts +80 -135
- package/src/services/export/_deprecated.ts +155 -0
- package/src/services/export/client.ts +15 -0
- package/src/services/export/index.ts +6 -0
- package/src/services/export/server.ts +9 -0
- package/src/services/export/type.ts +5 -0
- package/src/services/import/_deprecated.ts +42 -1
- package/src/services/import/client.test.ts +1 -1
- package/src/services/import/client.ts +30 -1
- package/src/services/import/server.ts +70 -2
- package/src/services/import/type.ts +10 -0
- package/src/store/global/initialState.ts +1 -0
- package/src/types/export.ts +11 -0
- package/src/types/exportConfig.ts +2 -0
- package/src/types/importer.ts +15 -0
- package/src/utils/client/exportFile.ts +21 -0
- package/vitest.config.ts +1 -1
- package/src/utils/config.ts +0 -109
- /package/src/database/repositories/dataImporter/{__tests__ → deprecated/__tests__}/fixtures/messages.json +0 -0
@@ -1,326 +1,721 @@
|
|
1
|
-
import { sql } from 'drizzle-orm';
|
2
1
|
import { and, eq, inArray } from 'drizzle-orm/expressions';
|
3
2
|
|
4
|
-
import
|
5
|
-
agents,
|
6
|
-
agentsToSessions,
|
7
|
-
messagePlugins,
|
8
|
-
messageTranslates,
|
9
|
-
messages,
|
10
|
-
sessionGroups,
|
11
|
-
sessions,
|
12
|
-
topics,
|
13
|
-
} from '@/database/schemas';
|
3
|
+
import * as EXPORT_TABLES from '@/database/schemas';
|
14
4
|
import { LobeChatDatabase } from '@/database/type';
|
15
|
-
import {
|
16
|
-
import { ImporterEntryData } from '@/types/importer';
|
17
|
-
import {
|
5
|
+
import { ImportPgDataStructure } from '@/types/export';
|
6
|
+
import { ImportResultData, ImporterEntryData } from '@/types/importer';
|
7
|
+
import { uuid } from '@/utils/uuid';
|
8
|
+
|
9
|
+
import { DeprecatedDataImporterRepos } from './deprecated';
|
10
|
+
|
11
|
+
interface ImportResult {
|
12
|
+
added: number;
|
13
|
+
errors: number;
|
14
|
+
skips: number;
|
15
|
+
updated?: number;
|
16
|
+
}
|
17
|
+
|
18
|
+
type ConflictStrategy = 'skip' | 'override' | 'merge';
|
19
|
+
|
20
|
+
interface TableImportConfig {
|
21
|
+
// 冲突处理策略
|
22
|
+
conflictStrategy?: ConflictStrategy;
|
23
|
+
// 字段处理函数
|
24
|
+
fieldProcessors?: {
|
25
|
+
[field: string]: (value: any) => any;
|
26
|
+
};
|
27
|
+
// 是否使用复合主键(没有单独的id字段)
|
28
|
+
isCompositeKey?: boolean;
|
29
|
+
// 是否保留原始ID
|
30
|
+
preserveId?: boolean;
|
31
|
+
// 关系字段定义
|
32
|
+
relations?: {
|
33
|
+
field: string;
|
34
|
+
sourceField?: string;
|
35
|
+
sourceTable: string;
|
36
|
+
}[];
|
37
|
+
// 自引用字段
|
38
|
+
selfReferences?: {
|
39
|
+
field: string;
|
40
|
+
sourceField?: string;
|
41
|
+
}[];
|
42
|
+
// 表名
|
43
|
+
table: string;
|
44
|
+
// 唯一约束字段
|
45
|
+
uniqueConstraints?: string[];
|
46
|
+
}
|
47
|
+
|
48
|
+
// 导入表配置
|
49
|
+
const IMPORT_TABLE_CONFIG: TableImportConfig[] = [
|
50
|
+
{
|
51
|
+
conflictStrategy: 'merge',
|
52
|
+
preserveId: true,
|
53
|
+
// 特殊表,ID与用户ID相同
|
54
|
+
table: 'userSettings',
|
55
|
+
uniqueConstraints: ['id'],
|
56
|
+
},
|
57
|
+
{
|
58
|
+
conflictStrategy: 'merge',
|
59
|
+
isCompositeKey: true,
|
60
|
+
table: 'userInstalledPlugins',
|
61
|
+
uniqueConstraints: ['identifier'],
|
62
|
+
},
|
63
|
+
{
|
64
|
+
conflictStrategy: 'skip',
|
65
|
+
preserveId: true,
|
66
|
+
table: 'aiProviders',
|
67
|
+
uniqueConstraints: ['id'],
|
68
|
+
},
|
69
|
+
{
|
70
|
+
conflictStrategy: 'skip',
|
71
|
+
preserveId: true, // 需要保留原始ID
|
72
|
+
relations: [
|
73
|
+
{
|
74
|
+
field: 'providerId',
|
75
|
+
sourceTable: 'aiProviders',
|
76
|
+
},
|
77
|
+
],
|
78
|
+
table: 'aiModels',
|
79
|
+
uniqueConstraints: ['id', 'providerId'],
|
80
|
+
},
|
81
|
+
{
|
82
|
+
table: 'sessionGroups',
|
83
|
+
uniqueConstraints: [],
|
84
|
+
},
|
85
|
+
{
|
86
|
+
fieldProcessors: {
|
87
|
+
slug: (value) => (value ? `${value}-${uuid().slice(0, 8)}` : null),
|
88
|
+
},
|
89
|
+
table: 'agents',
|
90
|
+
uniqueConstraints: ['slug'],
|
91
|
+
},
|
92
|
+
{
|
93
|
+
// 对slug字段进行特殊处理
|
94
|
+
fieldProcessors: {
|
95
|
+
slug: (value) => `${value}-${uuid().slice(0, 8)}`,
|
96
|
+
},
|
97
|
+
relations: [
|
98
|
+
{
|
99
|
+
field: 'groupId',
|
100
|
+
sourceTable: 'sessionGroups',
|
101
|
+
},
|
102
|
+
],
|
103
|
+
table: 'sessions',
|
104
|
+
uniqueConstraints: ['slug'],
|
105
|
+
},
|
106
|
+
{
|
107
|
+
relations: [
|
108
|
+
{
|
109
|
+
field: 'sessionId',
|
110
|
+
sourceTable: 'sessions',
|
111
|
+
},
|
112
|
+
],
|
113
|
+
table: 'topics',
|
114
|
+
},
|
115
|
+
{
|
116
|
+
conflictStrategy: 'skip',
|
117
|
+
isCompositeKey: true, // 使用复合主键 [agentId, sessionId]
|
118
|
+
relations: [
|
119
|
+
{
|
120
|
+
field: 'agentId',
|
121
|
+
sourceTable: 'agents',
|
122
|
+
},
|
123
|
+
{
|
124
|
+
field: 'sessionId',
|
125
|
+
sourceTable: 'sessions',
|
126
|
+
},
|
127
|
+
],
|
128
|
+
table: 'agentsToSessions',
|
129
|
+
uniqueConstraints: ['agentId', 'sessionId'],
|
130
|
+
},
|
131
|
+
{
|
132
|
+
relations: [
|
133
|
+
{
|
134
|
+
field: 'topicId',
|
135
|
+
sourceTable: 'topics',
|
136
|
+
},
|
137
|
+
],
|
138
|
+
selfReferences: [
|
139
|
+
{
|
140
|
+
field: 'parentThreadId',
|
141
|
+
},
|
142
|
+
],
|
143
|
+
table: 'threads',
|
144
|
+
},
|
145
|
+
{
|
146
|
+
relations: [
|
147
|
+
{
|
148
|
+
field: 'sessionId',
|
149
|
+
sourceTable: 'sessions',
|
150
|
+
},
|
151
|
+
{
|
152
|
+
field: 'topicId',
|
153
|
+
sourceTable: 'topics',
|
154
|
+
},
|
155
|
+
{
|
156
|
+
field: 'agentId',
|
157
|
+
sourceTable: 'agents',
|
158
|
+
},
|
159
|
+
{
|
160
|
+
field: 'threadId',
|
161
|
+
sourceTable: 'threads',
|
162
|
+
},
|
163
|
+
],
|
164
|
+
selfReferences: [
|
165
|
+
{
|
166
|
+
field: 'parentId',
|
167
|
+
},
|
168
|
+
{
|
169
|
+
field: 'quotaId',
|
170
|
+
},
|
171
|
+
],
|
172
|
+
table: 'messages',
|
173
|
+
},
|
174
|
+
{
|
175
|
+
conflictStrategy: 'skip',
|
176
|
+
preserveId: true, // 使用消息ID作为主键
|
177
|
+
relations: [
|
178
|
+
{
|
179
|
+
field: 'id',
|
180
|
+
sourceTable: 'messages',
|
181
|
+
},
|
182
|
+
],
|
183
|
+
table: 'messagePlugins',
|
184
|
+
},
|
185
|
+
{
|
186
|
+
isCompositeKey: true, // 使用复合主键 [messageId, chunkId]
|
187
|
+
relations: [
|
188
|
+
{
|
189
|
+
field: 'messageId',
|
190
|
+
sourceTable: 'messages',
|
191
|
+
},
|
192
|
+
{
|
193
|
+
field: 'chunkId',
|
194
|
+
sourceTable: 'chunks',
|
195
|
+
},
|
196
|
+
],
|
197
|
+
table: 'messageChunks',
|
198
|
+
},
|
199
|
+
{
|
200
|
+
isCompositeKey: true, // 使用复合主键 [id, queryId, chunkId]
|
201
|
+
relations: [
|
202
|
+
{
|
203
|
+
field: 'id',
|
204
|
+
sourceTable: 'messages',
|
205
|
+
},
|
206
|
+
{
|
207
|
+
field: 'queryId',
|
208
|
+
sourceTable: 'messageQueries',
|
209
|
+
},
|
210
|
+
{
|
211
|
+
field: 'chunkId',
|
212
|
+
sourceTable: 'chunks',
|
213
|
+
},
|
214
|
+
],
|
215
|
+
table: 'messageQueryChunks',
|
216
|
+
},
|
217
|
+
// {
|
218
|
+
// relations: [
|
219
|
+
// {
|
220
|
+
// field: 'messageId',
|
221
|
+
// sourceTable: 'messages',
|
222
|
+
// },
|
223
|
+
// {
|
224
|
+
// field: 'embeddingsId',
|
225
|
+
// sourceTable: 'embeddings',
|
226
|
+
// },
|
227
|
+
// ],
|
228
|
+
// table: 'messageQueries',
|
229
|
+
// },
|
230
|
+
{
|
231
|
+
conflictStrategy: 'skip',
|
232
|
+
preserveId: true, // 使用消息ID作为主键
|
233
|
+
relations: [
|
234
|
+
{
|
235
|
+
field: 'id',
|
236
|
+
sourceTable: 'messages',
|
237
|
+
},
|
238
|
+
],
|
239
|
+
table: 'messageTranslates',
|
240
|
+
},
|
241
|
+
// {
|
242
|
+
// conflictStrategy: 'skip',
|
243
|
+
// preserveId: true, // 使用消息ID作为主键
|
244
|
+
// relations: [
|
245
|
+
// {
|
246
|
+
// field: 'id',
|
247
|
+
// sourceTable: 'messages',
|
248
|
+
// },
|
249
|
+
// {
|
250
|
+
// field: 'fileId',
|
251
|
+
// sourceTable: 'files',
|
252
|
+
// },
|
253
|
+
// ],
|
254
|
+
// table: 'messageTTS',
|
255
|
+
// },
|
256
|
+
];
|
18
257
|
|
19
258
|
export class DataImporterRepos {
|
20
259
|
private userId: string;
|
21
260
|
private db: LobeChatDatabase;
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
*/
|
26
|
-
supportVersion = 7;
|
261
|
+
private deprecatedDataImporterRepos: DeprecatedDataImporterRepos;
|
262
|
+
private idMaps: Record<string, Record<string, string>> = {};
|
263
|
+
private conflictRecords: Record<string, { field: string; value: any }[]> = {};
|
27
264
|
|
28
265
|
constructor(db: LobeChatDatabase, userId: string) {
|
29
266
|
this.userId = userId;
|
30
267
|
this.db = db;
|
268
|
+
this.deprecatedDataImporterRepos = new DeprecatedDataImporterRepos(db, userId);
|
31
269
|
}
|
32
270
|
|
33
|
-
importData = async (data: ImporterEntryData) => {
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
.
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
271
|
+
importData = async (data: ImporterEntryData): Promise<ImportResultData> => {
|
272
|
+
const results = await this.deprecatedDataImporterRepos.importData(data);
|
273
|
+
return { results, success: true };
|
274
|
+
};
|
275
|
+
|
276
|
+
/**
|
277
|
+
* 导入PostgreSQL数据
|
278
|
+
*/
|
279
|
+
async importPgData(
|
280
|
+
dbData: ImportPgDataStructure,
|
281
|
+
conflictStrategy: ConflictStrategy = 'skip',
|
282
|
+
): Promise<ImportResultData> {
|
283
|
+
const results: Record<string, ImportResult> = {};
|
284
|
+
const { data } = dbData;
|
285
|
+
|
286
|
+
// 初始化ID映射表和冲突记录
|
287
|
+
this.idMaps = {};
|
288
|
+
this.conflictRecords = {};
|
289
|
+
|
290
|
+
try {
|
291
|
+
await this.db.transaction(async (trx) => {
|
292
|
+
// 按配置顺序导入表
|
293
|
+
for (const config of IMPORT_TABLE_CONFIG) {
|
294
|
+
const { table: tableName } = config;
|
295
|
+
|
296
|
+
// @ts-ignore
|
297
|
+
const tableData = data[tableName];
|
298
|
+
|
299
|
+
if (!tableData || tableData.length === 0) {
|
300
|
+
continue;
|
301
|
+
}
|
302
|
+
|
303
|
+
// 使用统一的导入方法
|
304
|
+
const result = await this.importTableData(trx, config, tableData, conflictStrategy);
|
305
|
+
console.log(`imported table: ${tableName}, records: ${tableData.length}`);
|
306
|
+
|
307
|
+
if (Object.values(result).some((value) => value > 0)) {
|
308
|
+
results[tableName] = result;
|
309
|
+
}
|
310
|
+
}
|
311
|
+
});
|
312
|
+
|
313
|
+
return { results, success: true };
|
314
|
+
} catch (error) {
|
315
|
+
console.error('Import failed:', error);
|
316
|
+
|
317
|
+
return {
|
318
|
+
error: {
|
319
|
+
details: this.extractErrorDetails(error),
|
320
|
+
message: (error as any).message,
|
321
|
+
},
|
322
|
+
results,
|
323
|
+
success: false,
|
324
|
+
};
|
325
|
+
}
|
326
|
+
}
|
327
|
+
|
328
|
+
/**
|
329
|
+
* 从错误中提取详细信息
|
330
|
+
*/
|
331
|
+
private extractErrorDetails(error: any) {
|
332
|
+
if (error.code === '23505') {
|
333
|
+
// PostgreSQL 唯一约束错误码
|
334
|
+
const match = error.detail?.match(/Key \((.+?)\)=\((.+?)\) already exists/);
|
335
|
+
if (match) {
|
336
|
+
return {
|
337
|
+
constraintType: 'unique',
|
338
|
+
field: match[1],
|
339
|
+
value: match[2],
|
340
|
+
};
|
80
341
|
}
|
342
|
+
}
|
81
343
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
const
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
const agentMapArray = await trx
|
127
|
-
.insert(agents)
|
128
|
-
.values(
|
129
|
-
shouldInsertSessionAgents.map(({ config, meta }) => ({
|
130
|
-
...config,
|
131
|
-
...meta,
|
132
|
-
userId: this.userId,
|
133
|
-
})),
|
134
|
-
)
|
135
|
-
.returning({ id: agents.id });
|
136
|
-
|
137
|
-
await trx.insert(agentsToSessions).values(
|
138
|
-
shouldInsertSessionAgents.map(({ id }, index) => ({
|
139
|
-
agentId: agentMapArray[index].id,
|
140
|
-
sessionId: sessionIdMap[id],
|
141
|
-
userId: this.userId,
|
142
|
-
})),
|
143
|
-
);
|
344
|
+
return error.detail || 'Unknown error details';
|
345
|
+
}
|
346
|
+
|
347
|
+
/**
|
348
|
+
* 统一的表数据导入函数 - 处理所有类型的表
|
349
|
+
*/
|
350
|
+
private async importTableData(
|
351
|
+
trx: any,
|
352
|
+
config: TableImportConfig,
|
353
|
+
tableData: any[],
|
354
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
355
|
+
_userConflictStrategy: ConflictStrategy,
|
356
|
+
): Promise<ImportResult> {
|
357
|
+
const {
|
358
|
+
table: tableName,
|
359
|
+
preserveId,
|
360
|
+
isCompositeKey = false,
|
361
|
+
uniqueConstraints = [],
|
362
|
+
conflictStrategy = 'override',
|
363
|
+
fieldProcessors = {},
|
364
|
+
relations = [],
|
365
|
+
selfReferences = [],
|
366
|
+
} = config;
|
367
|
+
|
368
|
+
// @ts-ignore
|
369
|
+
const table = EXPORT_TABLES[tableName];
|
370
|
+
const result: ImportResult = { added: 0, errors: 0, skips: 0, updated: 0 };
|
371
|
+
|
372
|
+
// 初始化该表的ID映射
|
373
|
+
if (!this.idMaps[tableName]) {
|
374
|
+
this.idMaps[tableName] = {};
|
375
|
+
}
|
376
|
+
|
377
|
+
try {
|
378
|
+
// 1. 查找已存在的记录(基于clientId和userId)
|
379
|
+
let existingRecords: any[] = [];
|
380
|
+
|
381
|
+
if ('clientId' in table && 'userId' in table) {
|
382
|
+
const clientIds = tableData.map((item) => item.clientId || item.id).filter(Boolean);
|
383
|
+
|
384
|
+
if (clientIds.length > 0) {
|
385
|
+
existingRecords = await trx.query[tableName].findMany({
|
386
|
+
where: and(eq(table.userId, this.userId), inArray(table.clientId, clientIds)),
|
387
|
+
});
|
144
388
|
}
|
145
389
|
}
|
146
390
|
|
147
|
-
//
|
148
|
-
if (
|
149
|
-
const
|
150
|
-
|
151
|
-
|
152
|
-
inArray(
|
153
|
-
|
154
|
-
|
391
|
+
// 如果需要保留原始ID,还需要检查ID是否已存在
|
392
|
+
if (preserveId && !isCompositeKey) {
|
393
|
+
const ids = tableData.map((item) => item.id).filter(Boolean);
|
394
|
+
if (ids.length > 0) {
|
395
|
+
const idExistingRecords = await trx.query[tableName].findMany({
|
396
|
+
where: inArray(table.id, ids),
|
397
|
+
});
|
398
|
+
|
399
|
+
// 合并到已存在记录集合中
|
400
|
+
existingRecords = [
|
401
|
+
...existingRecords,
|
402
|
+
...idExistingRecords.filter(
|
403
|
+
(record: any) => !existingRecords.some((existing) => existing.id === record.id),
|
155
404
|
),
|
156
|
-
|
157
|
-
}
|
158
|
-
topicResult.skips = skipQuery.length;
|
159
|
-
|
160
|
-
const mapArray = await trx
|
161
|
-
.insert(topics)
|
162
|
-
.values(
|
163
|
-
data.topics.map(({ id, createdAt, updatedAt, sessionId, favorite, ...res }) => ({
|
164
|
-
...res,
|
165
|
-
clientId: id,
|
166
|
-
createdAt: new Date(createdAt),
|
167
|
-
favorite: Boolean(favorite),
|
168
|
-
sessionId: sessionId ? sessionIdMap[sessionId] : null,
|
169
|
-
updatedAt: new Date(updatedAt),
|
170
|
-
userId: this.userId,
|
171
|
-
})),
|
172
|
-
)
|
173
|
-
.onConflictDoUpdate({
|
174
|
-
set: { updatedAt: new Date() },
|
175
|
-
target: [topics.clientId, topics.userId],
|
176
|
-
})
|
177
|
-
.returning({ clientId: topics.clientId, id: topics.id });
|
178
|
-
|
179
|
-
topicIdMap = Object.fromEntries(mapArray.map(({ clientId, id }) => [clientId, id]));
|
180
|
-
|
181
|
-
topicResult.added = mapArray.length - skipQuery.length;
|
405
|
+
];
|
406
|
+
}
|
182
407
|
}
|
183
408
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
messageResult.skips = skipQuery.length;
|
200
|
-
|
201
|
-
// filter out existing messages, only insert new ones
|
202
|
-
const shouldInsertMessages = data.messages.filter((s) =>
|
203
|
-
skipQuery.every((q) => q.clientId !== s.id),
|
204
|
-
);
|
205
|
-
|
206
|
-
// 2. insert messages
|
207
|
-
if (shouldInsertMessages.length > 0) {
|
208
|
-
const inertValues = shouldInsertMessages.map(
|
209
|
-
({ id, extra, createdAt, updatedAt, sessionId, topicId, content, ...res }) => ({
|
210
|
-
...res,
|
211
|
-
clientId: id,
|
212
|
-
content: sanitizeUTF8(content),
|
213
|
-
createdAt: new Date(createdAt),
|
214
|
-
model: extra?.fromModel,
|
215
|
-
parentId: null,
|
216
|
-
provider: extra?.fromProvider,
|
217
|
-
sessionId: sessionId ? sessionIdMap[sessionId] : null,
|
218
|
-
topicId: topicId ? topicIdMap[topicId] : null, // 暂时设为 NULL
|
219
|
-
updatedAt: new Date(updatedAt),
|
220
|
-
userId: this.userId,
|
221
|
-
}),
|
409
|
+
result.skips = existingRecords.length;
|
410
|
+
|
411
|
+
// 2. 为已存在的记录建立ID映射
|
412
|
+
for (const record of existingRecords) {
|
413
|
+
// 只有非复合主键表才需要ID映射
|
414
|
+
if (!isCompositeKey) {
|
415
|
+
this.idMaps[tableName][record.id] = record.id;
|
416
|
+
if (record.clientId) {
|
417
|
+
this.idMaps[tableName][record.clientId] = record.id;
|
418
|
+
}
|
419
|
+
|
420
|
+
// 记录中可能使用的任何其他ID标识符
|
421
|
+
const originalRecord = tableData.find(
|
422
|
+
(item) => item.id === record.id || item.clientId === record.clientId,
|
222
423
|
);
|
223
424
|
|
224
|
-
|
225
|
-
|
425
|
+
if (originalRecord) {
|
426
|
+
// 确保原始记录ID也映射到数据库记录ID
|
427
|
+
this.idMaps[tableName][originalRecord.id] = record.id;
|
428
|
+
}
|
429
|
+
}
|
430
|
+
}
|
431
|
+
|
432
|
+
// 3. 筛选出需要插入的记录
|
433
|
+
const recordsToInsert = tableData.filter(
|
434
|
+
(item) =>
|
435
|
+
!existingRecords.some(
|
436
|
+
(record) =>
|
437
|
+
(record.clientId === (item.clientId || item.id) && record.clientId) ||
|
438
|
+
(preserveId && !isCompositeKey && record.id === item.id),
|
439
|
+
),
|
440
|
+
);
|
441
|
+
|
442
|
+
if (recordsToInsert.length === 0) {
|
443
|
+
return result;
|
444
|
+
}
|
445
|
+
|
446
|
+
// 4. 准备导入数据
|
447
|
+
const preparedData = recordsToInsert.map((item) => {
|
448
|
+
const originalId = item.id;
|
449
|
+
|
450
|
+
// 处理日期字段
|
451
|
+
const dateFields: any = {};
|
452
|
+
if (item.createdAt) dateFields.createdAt = new Date(item.createdAt);
|
453
|
+
if (item.updatedAt) dateFields.updatedAt = new Date(item.updatedAt);
|
454
|
+
if (item.accessedAt) dateFields.accessedAt = new Date(item.accessedAt);
|
455
|
+
|
456
|
+
// 创建新记录对象
|
457
|
+
let newRecord: any = {};
|
458
|
+
|
459
|
+
// 根据是否复合主键和是否保留ID决定如何处理
|
460
|
+
if (isCompositeKey) {
|
461
|
+
// 对于复合主键表,不包含id字段
|
462
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
463
|
+
const { id: _, ...rest } = item;
|
464
|
+
newRecord = {
|
465
|
+
...rest,
|
466
|
+
...dateFields,
|
467
|
+
clientId: item.clientId || item.id,
|
468
|
+
userId: this.userId,
|
469
|
+
};
|
470
|
+
} else {
|
471
|
+
// 非复合主键表处理
|
472
|
+
newRecord = {
|
473
|
+
...(preserveId ? item : { ...item, id: undefined }),
|
474
|
+
...dateFields,
|
475
|
+
clientId: item.clientId || item.id,
|
476
|
+
userId: this.userId,
|
477
|
+
};
|
478
|
+
}
|
226
479
|
|
227
|
-
|
228
|
-
|
229
|
-
|
480
|
+
// 应用字段处理器
|
481
|
+
for (const field in fieldProcessors) {
|
482
|
+
if (newRecord[field] !== undefined) {
|
483
|
+
newRecord[field] = fieldProcessors[field](newRecord[field]);
|
230
484
|
}
|
485
|
+
}
|
231
486
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
.from(messages)
|
237
|
-
.where(
|
238
|
-
and(
|
239
|
-
eq(messages.userId, this.userId),
|
240
|
-
inArray(
|
241
|
-
messages.clientId,
|
242
|
-
data.messages.map(({ id }) => id),
|
243
|
-
),
|
244
|
-
),
|
245
|
-
);
|
246
|
-
|
247
|
-
const messageIdMap = Object.fromEntries(
|
248
|
-
messageIdArray.map(({ clientId, id }) => [clientId, id]),
|
249
|
-
);
|
487
|
+
// 特殊表处理
|
488
|
+
if (tableName === 'userSettings') {
|
489
|
+
newRecord.id = this.userId;
|
490
|
+
}
|
250
491
|
|
251
|
-
|
252
|
-
|
253
|
-
const
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
return sql`WHEN ${messages.clientId} = ${msg.id} THEN ${messageIdMap[msg.parentId as string]} `;
|
258
|
-
|
259
|
-
return undefined;
|
260
|
-
})
|
261
|
-
.filter(Boolean);
|
262
|
-
|
263
|
-
if (parentIdUpdates.length > 0) {
|
264
|
-
await trx
|
265
|
-
.update(messages)
|
266
|
-
.set({
|
267
|
-
parentId: sql`CASE ${sql.join(parentIdUpdates)} END`,
|
268
|
-
})
|
269
|
-
.where(
|
270
|
-
inArray(
|
271
|
-
messages.clientId,
|
272
|
-
data.messages.map((msg) => msg.id),
|
273
|
-
),
|
274
|
-
);
|
492
|
+
// 处理关系字段(外键引用)
|
493
|
+
for (const relation of relations) {
|
494
|
+
const { field, sourceTable } = relation;
|
495
|
+
|
496
|
+
if (newRecord[field] && this.idMaps[sourceTable]) {
|
497
|
+
const mappedId = this.idMaps[sourceTable][newRecord[field]];
|
275
498
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
499
|
+
if (mappedId) {
|
500
|
+
newRecord[field] = mappedId;
|
501
|
+
} else {
|
502
|
+
// 找不到映射,设为null
|
503
|
+
console.warn(
|
504
|
+
`Could not find mapped ID for ${field}=${newRecord[field]} in table ${sourceTable}`,
|
505
|
+
);
|
506
|
+
newRecord[field] = null;
|
507
|
+
}
|
280
508
|
}
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
apiName: msg.plugin?.apiName,
|
289
|
-
arguments: msg.plugin?.arguments,
|
290
|
-
id: messageIdMap[msg.id],
|
291
|
-
identifier: msg.plugin?.identifier,
|
292
|
-
state: msg.pluginState,
|
293
|
-
toolCallId: msg.tool_call_id,
|
294
|
-
type: msg.plugin?.type,
|
295
|
-
userId: this.userId,
|
296
|
-
})),
|
297
|
-
);
|
509
|
+
}
|
510
|
+
|
511
|
+
// 简化处理自引用字段 - 直接设为null
|
512
|
+
for (const selfRef of selfReferences) {
|
513
|
+
const { field } = selfRef;
|
514
|
+
if (newRecord[field] !== undefined) {
|
515
|
+
newRecord[field] = null;
|
298
516
|
}
|
517
|
+
}
|
518
|
+
|
519
|
+
return { newRecord, originalId };
|
520
|
+
});
|
299
521
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
522
|
+
// 5. 检查唯一约束并应用冲突策略
|
523
|
+
for (const record of preparedData) {
|
524
|
+
if (isCompositeKey && uniqueConstraints.length > 0) {
|
525
|
+
// 对于复合主键表,将所有唯一约束字段作为一个组合条件
|
526
|
+
const whereConditions = uniqueConstraints
|
527
|
+
.filter((field) => record.newRecord[field] !== undefined)
|
528
|
+
.map((field) => eq(table[field], record.newRecord[field]));
|
529
|
+
|
530
|
+
// 添加userId条件(如果表有userId字段)
|
531
|
+
if ('userId' in table) {
|
532
|
+
whereConditions.push(eq(table.userId, this.userId));
|
310
533
|
}
|
311
534
|
|
312
|
-
|
535
|
+
if (whereConditions.length > 0) {
|
536
|
+
const exists = await trx.query[tableName].findFirst({
|
537
|
+
where: and(...whereConditions),
|
538
|
+
});
|
539
|
+
|
540
|
+
if (exists) {
|
541
|
+
// 记录冲突
|
542
|
+
if (!this.conflictRecords[tableName]) this.conflictRecords[tableName] = [];
|
543
|
+
this.conflictRecords[tableName].push({
|
544
|
+
field: uniqueConstraints.join(','),
|
545
|
+
value: uniqueConstraints
|
546
|
+
.map((field) => `${field}=${record.newRecord[field]}`)
|
547
|
+
.join(','),
|
548
|
+
});
|
549
|
+
|
550
|
+
// 应用冲突策略
|
551
|
+
switch (conflictStrategy) {
|
552
|
+
case 'skip': {
|
553
|
+
record.newRecord._skip = true;
|
554
|
+
result.skips++;
|
555
|
+
|
556
|
+
// 关键改进:即使跳过,也建立ID映射关系
|
557
|
+
if (!isCompositeKey) {
|
558
|
+
this.idMaps[tableName][record.originalId] = exists.id;
|
559
|
+
if (record.newRecord.clientId) {
|
560
|
+
this.idMaps[tableName][record.newRecord.clientId] = exists.id;
|
561
|
+
}
|
562
|
+
}
|
563
|
+
break;
|
564
|
+
}
|
565
|
+
case 'override': {
|
566
|
+
// 不需要额外操作,插入时会覆盖
|
567
|
+
break;
|
568
|
+
}
|
569
|
+
case 'merge': {
|
570
|
+
// 合并数据
|
571
|
+
await trx
|
572
|
+
.update(table)
|
573
|
+
.set(record.newRecord)
|
574
|
+
.where(and(...whereConditions));
|
575
|
+
record.newRecord._skip = true;
|
576
|
+
if (result.updated) result.updated++;
|
577
|
+
else {
|
578
|
+
result.updated = 1;
|
579
|
+
}
|
580
|
+
break;
|
581
|
+
}
|
582
|
+
}
|
583
|
+
}
|
584
|
+
}
|
585
|
+
} else {
|
586
|
+
// 处理唯一约束
|
587
|
+
for (const field of uniqueConstraints) {
|
588
|
+
if (!record.newRecord[field]) continue;
|
589
|
+
|
590
|
+
// 检查字段值是否已存在
|
591
|
+
const exists = await trx.query[tableName].findFirst({
|
592
|
+
where: eq(table[field], record.newRecord[field]),
|
593
|
+
});
|
594
|
+
|
595
|
+
if (exists) {
|
596
|
+
// 记录冲突
|
597
|
+
if (!this.conflictRecords[tableName]) this.conflictRecords[tableName] = [];
|
598
|
+
this.conflictRecords[tableName].push({
|
599
|
+
field,
|
600
|
+
value: record.newRecord[field],
|
601
|
+
});
|
602
|
+
|
603
|
+
// 应用冲突策略
|
604
|
+
switch (conflictStrategy) {
|
605
|
+
case 'skip': {
|
606
|
+
record.newRecord._skip = true;
|
607
|
+
result.skips++;
|
608
|
+
|
609
|
+
// 关键改进:即使跳过,也建立ID映射关系
|
610
|
+
if (!isCompositeKey) {
|
611
|
+
this.idMaps[tableName][record.originalId] = exists.id;
|
612
|
+
if (record.newRecord.clientId) {
|
613
|
+
this.idMaps[tableName][record.newRecord.clientId] = exists.id;
|
614
|
+
}
|
615
|
+
}
|
616
|
+
break;
|
617
|
+
}
|
618
|
+
case 'override': {
|
619
|
+
// 应用字段处理器
|
620
|
+
if (field in fieldProcessors) {
|
621
|
+
record.newRecord[field] = fieldProcessors[field](record.newRecord[field]);
|
622
|
+
}
|
623
|
+
break;
|
624
|
+
}
|
625
|
+
|
626
|
+
case 'merge': {
|
627
|
+
// 合并数据
|
628
|
+
await trx
|
629
|
+
.update(table)
|
630
|
+
.set(record.newRecord)
|
631
|
+
.where(eq(table[field], record.newRecord[field]));
|
632
|
+
record.newRecord._skip = true;
|
633
|
+
if (result.updated) result.updated++;
|
634
|
+
else {
|
635
|
+
result.updated = 1;
|
636
|
+
}
|
637
|
+
break;
|
638
|
+
}
|
639
|
+
}
|
640
|
+
}
|
641
|
+
}
|
313
642
|
}
|
643
|
+
}
|
644
|
+
|
645
|
+
// 过滤掉标记为跳过的记录
|
646
|
+
const filteredData = preparedData.filter((record) => !record.newRecord._skip);
|
647
|
+
|
648
|
+
// 清除临时标记
|
649
|
+
filteredData.forEach((record) => delete record.newRecord._skip);
|
650
|
+
|
651
|
+
// 6. 批量插入数据
|
652
|
+
const BATCH_SIZE = 100;
|
653
|
+
|
654
|
+
for (let i = 0; i < filteredData.length; i += BATCH_SIZE) {
|
655
|
+
const batch = filteredData.slice(i, i + BATCH_SIZE).filter(Boolean);
|
656
|
+
|
657
|
+
const itemsToInsert = batch.map((item) => item.newRecord);
|
658
|
+
const originalIds = batch.map((item) => item.originalId);
|
659
|
+
|
660
|
+
try {
|
661
|
+
// 插入并返回结果
|
662
|
+
const insertQuery = trx.insert(table).values(itemsToInsert);
|
663
|
+
|
664
|
+
let insertResult;
|
665
|
+
|
666
|
+
// 只对非复合主键表需要返回ID
|
667
|
+
if (!isCompositeKey) {
|
668
|
+
const res = await insertQuery.returning();
|
669
|
+
insertResult = res.map((item: any) => ({
|
670
|
+
clientId: 'clientId' in item ? item.clientId : undefined,
|
671
|
+
id: item.id,
|
672
|
+
}));
|
673
|
+
} else {
|
674
|
+
await insertQuery;
|
675
|
+
insertResult = itemsToInsert.map(() => ({})); // 创建空结果以维持计数
|
676
|
+
}
|
677
|
+
|
678
|
+
result.added += insertResult.length;
|
679
|
+
|
680
|
+
// 建立ID映射关系 (只对非复合主键表)
|
681
|
+
if (!isCompositeKey) {
|
682
|
+
for (const [j, newRecord] of insertResult.entries()) {
|
683
|
+
const originalId = originalIds[j];
|
684
|
+
this.idMaps[tableName][originalId] = newRecord.id;
|
685
|
+
|
686
|
+
// 同时确保clientId也能映射到正确的ID
|
687
|
+
const originalRecord = tableData.find((item) => item.id === originalId);
|
688
|
+
if (originalRecord && originalRecord.clientId) {
|
689
|
+
this.idMaps[tableName][originalRecord.clientId] = newRecord.id;
|
690
|
+
}
|
691
|
+
}
|
692
|
+
}
|
693
|
+
} catch (error) {
|
694
|
+
console.error(`Error batch inserting ${tableName}:`, error);
|
695
|
+
|
696
|
+
// 处理错误并记录
|
697
|
+
if ((error as any).code === '23505') {
|
698
|
+
const match = (error as any).detail?.match(/Key \((.+?)\)=\((.+?)\) already exists/);
|
699
|
+
if (match) {
|
700
|
+
const conflictField = match[1];
|
701
|
+
|
702
|
+
if (!this.conflictRecords[tableName]) this.conflictRecords[tableName] = [];
|
703
|
+
this.conflictRecords[tableName].push({
|
704
|
+
field: conflictField,
|
705
|
+
value: match[2],
|
706
|
+
});
|
707
|
+
}
|
708
|
+
}
|
314
709
|
|
315
|
-
|
710
|
+
result.errors += batch.length;
|
711
|
+
}
|
316
712
|
}
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
};
|
713
|
+
|
714
|
+
return result;
|
715
|
+
} catch (error) {
|
716
|
+
console.error(`Error importing table ${tableName}:`, error);
|
717
|
+
result.errors = tableData.length;
|
718
|
+
return result;
|
719
|
+
}
|
720
|
+
}
|
326
721
|
}
|