@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.
Files changed (131) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/common.json +13 -2
  4. package/locales/ar/error.json +10 -0
  5. package/locales/ar/models.json +9 -6
  6. package/locales/ar/setting.json +28 -0
  7. package/locales/bg-BG/common.json +13 -2
  8. package/locales/bg-BG/error.json +10 -0
  9. package/locales/bg-BG/models.json +9 -6
  10. package/locales/bg-BG/setting.json +28 -0
  11. package/locales/de-DE/common.json +13 -2
  12. package/locales/de-DE/error.json +10 -0
  13. package/locales/de-DE/models.json +9 -6
  14. package/locales/de-DE/setting.json +28 -0
  15. package/locales/en-US/common.json +13 -2
  16. package/locales/en-US/error.json +10 -0
  17. package/locales/en-US/models.json +9 -6
  18. package/locales/en-US/setting.json +28 -0
  19. package/locales/es-ES/common.json +13 -2
  20. package/locales/es-ES/error.json +10 -0
  21. package/locales/es-ES/models.json +9 -6
  22. package/locales/es-ES/setting.json +28 -0
  23. package/locales/fa-IR/common.json +13 -2
  24. package/locales/fa-IR/error.json +10 -0
  25. package/locales/fa-IR/models.json +9 -6
  26. package/locales/fa-IR/setting.json +28 -0
  27. package/locales/fr-FR/common.json +13 -2
  28. package/locales/fr-FR/error.json +10 -0
  29. package/locales/fr-FR/models.json +9 -6
  30. package/locales/fr-FR/setting.json +28 -0
  31. package/locales/it-IT/common.json +13 -2
  32. package/locales/it-IT/error.json +10 -0
  33. package/locales/it-IT/models.json +9 -6
  34. package/locales/it-IT/setting.json +28 -0
  35. package/locales/ja-JP/common.json +13 -2
  36. package/locales/ja-JP/error.json +10 -0
  37. package/locales/ja-JP/models.json +9 -6
  38. package/locales/ja-JP/setting.json +28 -0
  39. package/locales/ko-KR/common.json +13 -2
  40. package/locales/ko-KR/error.json +10 -0
  41. package/locales/ko-KR/models.json +9 -6
  42. package/locales/ko-KR/setting.json +28 -0
  43. package/locales/nl-NL/common.json +13 -2
  44. package/locales/nl-NL/error.json +10 -0
  45. package/locales/nl-NL/models.json +9 -6
  46. package/locales/nl-NL/setting.json +28 -0
  47. package/locales/pl-PL/common.json +13 -2
  48. package/locales/pl-PL/error.json +10 -0
  49. package/locales/pl-PL/models.json +9 -6
  50. package/locales/pl-PL/setting.json +28 -0
  51. package/locales/pt-BR/common.json +13 -2
  52. package/locales/pt-BR/error.json +10 -0
  53. package/locales/pt-BR/models.json +9 -6
  54. package/locales/pt-BR/setting.json +28 -0
  55. package/locales/ru-RU/common.json +13 -2
  56. package/locales/ru-RU/error.json +10 -0
  57. package/locales/ru-RU/models.json +9 -6
  58. package/locales/ru-RU/setting.json +28 -0
  59. package/locales/tr-TR/common.json +13 -2
  60. package/locales/tr-TR/error.json +10 -0
  61. package/locales/tr-TR/models.json +9 -6
  62. package/locales/tr-TR/setting.json +28 -0
  63. package/locales/vi-VN/common.json +13 -2
  64. package/locales/vi-VN/error.json +10 -0
  65. package/locales/vi-VN/models.json +9 -6
  66. package/locales/vi-VN/setting.json +28 -0
  67. package/locales/zh-CN/common.json +13 -2
  68. package/locales/zh-CN/error.json +10 -0
  69. package/locales/zh-CN/models.json +10 -7
  70. package/locales/zh-CN/setting.json +28 -0
  71. package/locales/zh-TW/common.json +13 -2
  72. package/locales/zh-TW/error.json +10 -0
  73. package/locales/zh-TW/models.json +9 -6
  74. package/locales/zh-TW/setting.json +28 -0
  75. package/package.json +1 -1
  76. package/src/app/[variants]/(main)/(mobile)/me/data/features/Category.tsx +2 -2
  77. package/src/app/[variants]/(main)/chat/features/Migration/UpgradeButton.tsx +2 -1
  78. package/src/app/[variants]/(main)/settings/common/features/Common.tsx +0 -44
  79. package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +40 -14
  80. package/src/app/[variants]/(main)/settings/storage/Advanced.tsx +108 -0
  81. package/src/app/[variants]/(main)/settings/storage/IndexedDBStorage.tsx +55 -0
  82. package/src/app/[variants]/(main)/settings/storage/page.tsx +17 -0
  83. package/src/components/GroupIcon/index.tsx +25 -0
  84. package/src/components/IndexCard/index.tsx +143 -0
  85. package/src/components/ProgressItem/index.tsx +75 -0
  86. package/src/database/models/__tests__/session.test.ts +21 -0
  87. package/src/database/repositories/dataExporter/index.test.ts +330 -0
  88. package/src/database/repositories/dataExporter/index.ts +216 -0
  89. package/src/database/repositories/dataImporter/__tests__/fixtures/agents.json +65 -0
  90. package/src/database/repositories/dataImporter/__tests__/fixtures/agentsToSessions.json +541 -0
  91. package/src/database/repositories/dataImporter/__tests__/fixtures/topic.json +269 -0
  92. package/src/database/repositories/dataImporter/__tests__/fixtures/userSettings.json +18 -0
  93. package/src/database/repositories/dataImporter/__tests__/fixtures/with-client-id.json +778 -0
  94. package/src/database/repositories/dataImporter/__tests__/index.test.ts +120 -880
  95. package/src/database/repositories/dataImporter/deprecated/__tests__/index.test.ts +940 -0
  96. package/src/database/repositories/dataImporter/deprecated/index.ts +326 -0
  97. package/src/database/repositories/dataImporter/index.ts +684 -289
  98. package/src/database/server/models/session.ts +85 -9
  99. package/src/features/DataImporter/ImportDetail.tsx +203 -0
  100. package/src/features/DataImporter/SuccessResult.tsx +22 -6
  101. package/src/features/DataImporter/_deprecated.ts +43 -0
  102. package/src/features/DataImporter/config.ts +21 -0
  103. package/src/features/DataImporter/index.tsx +112 -31
  104. package/src/features/DevPanel/PostgresViewer/DataTable/index.tsx +6 -0
  105. package/src/features/User/UserPanel/useMenu.tsx +1 -36
  106. package/src/features/User/__tests__/useMenu.test.tsx +0 -2
  107. package/src/locales/default/common.ts +12 -1
  108. package/src/locales/default/error.ts +10 -0
  109. package/src/locales/default/setting.ts +28 -0
  110. package/src/server/routers/lambda/exporter.ts +25 -0
  111. package/src/server/routers/lambda/importer.ts +19 -3
  112. package/src/server/routers/lambda/index.ts +2 -0
  113. package/src/services/config.ts +80 -135
  114. package/src/services/export/_deprecated.ts +155 -0
  115. package/src/services/export/client.ts +15 -0
  116. package/src/services/export/index.ts +6 -0
  117. package/src/services/export/server.ts +9 -0
  118. package/src/services/export/type.ts +5 -0
  119. package/src/services/import/_deprecated.ts +42 -1
  120. package/src/services/import/client.test.ts +1 -1
  121. package/src/services/import/client.ts +30 -1
  122. package/src/services/import/server.ts +70 -2
  123. package/src/services/import/type.ts +10 -0
  124. package/src/store/global/initialState.ts +1 -0
  125. package/src/types/export.ts +11 -0
  126. package/src/types/exportConfig.ts +2 -0
  127. package/src/types/importer.ts +15 -0
  128. package/src/utils/client/exportFile.ts +21 -0
  129. package/vitest.config.ts +1 -1
  130. package/src/utils/config.ts +0 -109
  131. /package/src/database/repositories/dataImporter/{__tests__ → deprecated/__tests__}/fixtures/messages.json +0 -0
@@ -0,0 +1,143 @@
1
+ import { ActionIcon } from '@lobehub/ui';
2
+ import { createStyles } from 'antd-style';
3
+ import { ChevronDown, ChevronRight } from 'lucide-react';
4
+ import { ReactNode, memo } from 'react';
5
+ import { Center, Flexbox, FlexboxProps } from 'react-layout-kit';
6
+
7
+ const useStyles = createStyles(({ css, token, responsive }) => ({
8
+ card: css`
9
+ position: relative;
10
+
11
+ overflow: hidden;
12
+
13
+ border: 1px solid ${token.colorBorderSecondary};
14
+ border-radius: ${token.borderRadiusLG}px;
15
+
16
+ background: ${token.colorBgContainer};
17
+ `,
18
+ desc: css`
19
+ font-size: 14px;
20
+ line-height: 1.4;
21
+ color: ${token.colorTextDescription};
22
+ ${responsive.mobile} {
23
+ font-size: 12px;
24
+ }
25
+ `,
26
+ expend: css`
27
+ position: absolute;
28
+ inset-block-end: -12px;
29
+ inset-inline-start: 50%;
30
+ transform: translateX(-50%);
31
+
32
+ border: 1px solid ${token.colorBorderSecondary};
33
+ border-radius: 50%;
34
+
35
+ background: ${token.colorBgContainer};
36
+ `,
37
+ header: css`
38
+ border-block-end: 1px solid ${token.colorBorderSecondary};
39
+ background: ${token.colorFillQuaternary};
40
+ `,
41
+ more: css`
42
+ border: 1px solid ${token.colorBorderSecondary};
43
+ `,
44
+ title: css`
45
+ font-size: 16px;
46
+ font-weight: bold;
47
+ line-height: 1.4;
48
+ ${responsive.mobile} {
49
+ font-size: 14px;
50
+ }
51
+ `,
52
+ }));
53
+
54
+ interface IndexCardProps extends Omit<FlexboxProps, 'title'> {
55
+ desc?: ReactNode;
56
+ expand?: boolean;
57
+ extra?: ReactNode;
58
+ icon?: ReactNode;
59
+ moreTooltip?: string;
60
+ onExpand?: () => void;
61
+ onMoreClick?: () => void;
62
+ title?: ReactNode;
63
+ }
64
+
65
+ const IndexCard = memo<IndexCardProps>(
66
+ ({
67
+ expand = true,
68
+ onExpand,
69
+ icon,
70
+ className,
71
+ onMoreClick,
72
+ title,
73
+ extra,
74
+ moreTooltip,
75
+ desc,
76
+ children,
77
+ ...rest
78
+ }) => {
79
+ const { styles } = useStyles();
80
+ return (
81
+ <Flexbox
82
+ style={{
83
+ marginBottom: !expand ? 12 : undefined,
84
+ position: 'relative',
85
+ }}
86
+ >
87
+ <Flexbox
88
+ className={styles.card}
89
+ style={{
90
+ paddingBottom: !expand ? 12 : undefined,
91
+ }}
92
+ >
93
+ {title && (
94
+ <Flexbox
95
+ align={'center'}
96
+ className={styles.header}
97
+ gap={16}
98
+ horizontal
99
+ justify={'space-between'}
100
+ padding={16}
101
+ >
102
+ <Flexbox align={'center'} gap={12} horizontal>
103
+ {icon}
104
+ <Flexbox>
105
+ <div className={styles.title}>{title}</div>
106
+ {desc && <div className={styles.desc}>{desc}</div>}
107
+ </Flexbox>
108
+ </Flexbox>
109
+ <Flexbox align={'center'} gap={8} horizontal>
110
+ {extra}
111
+ {onMoreClick && (
112
+ <ActionIcon
113
+ className={styles.more}
114
+ icon={ChevronRight}
115
+ onClick={onMoreClick}
116
+ size={{ blockSize: 32, borderRadius: '50%', fontSize: 16 }}
117
+ title={moreTooltip}
118
+ />
119
+ )}
120
+ </Flexbox>
121
+ </Flexbox>
122
+ )}
123
+ <Flexbox className={className} gap={16} padding={16} width={'100%'} {...rest}>
124
+ {children}
125
+ </Flexbox>
126
+ </Flexbox>
127
+ {!expand && (
128
+ <Center className={styles.expend} height={24} width={24}>
129
+ <ActionIcon
130
+ icon={ChevronDown}
131
+ onClick={onExpand}
132
+ size={{ blockSize: 24, borderRadius: '50%', fontSize: 16 }}
133
+ />
134
+ </Center>
135
+ )}
136
+ </Flexbox>
137
+ );
138
+ },
139
+ );
140
+
141
+ IndexCard.displayName = 'IndexCard';
142
+
143
+ export default IndexCard;
@@ -0,0 +1,75 @@
1
+ import { Progress } from 'antd';
2
+ import { createStyles, useResponsive } from 'antd-style';
3
+ import { CSSProperties, memo } from 'react';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ const useStyles = createStyles(({ css, token }) => ({
7
+ desc: css`
8
+ height: 20px;
9
+ font-size: 12px;
10
+ line-height: 20px;
11
+ color: ${token.colorTextTertiary};
12
+ `,
13
+ title: css`
14
+ font-size: 15px;
15
+ font-weight: bold;
16
+ color: ${token.colorTextSecondary};
17
+ `,
18
+ }));
19
+
20
+ interface ProgressItemProps {
21
+ className?: string;
22
+ desc?: string;
23
+ legend?: string;
24
+ padding?: number;
25
+ percent: number;
26
+ style?: CSSProperties;
27
+ title: string;
28
+ usage: {
29
+ total: string | number;
30
+ used: string | number;
31
+ };
32
+ }
33
+
34
+ const ProgressItem = memo<ProgressItemProps>(
35
+ ({ legend, title, desc, usage, percent, style, className }) => {
36
+ const { mobile } = useResponsive();
37
+ const { styles, theme } = useStyles();
38
+
39
+ return (
40
+ <Flexbox className={className} paddingInline={16} style={style} width={'100%'}>
41
+ <Flexbox align={'center'} horizontal justify={'space-between'} width={'100%'}>
42
+ <Flexbox align={'center'} gap={8} horizontal>
43
+ {legend && (
44
+ <Flexbox
45
+ height={8}
46
+ style={{
47
+ background: theme.geekblue,
48
+ borderRadius: '50%',
49
+ flex: 'none',
50
+ }}
51
+ width={8}
52
+ />
53
+ )}
54
+ <Flexbox align={'baseline'} gap={mobile ? 0 : 8} horizontal={!mobile}>
55
+ <div className={styles.title}>{title}</div>
56
+ {desc && <div className={styles.desc}>{desc}</div>}
57
+ </Flexbox>
58
+ </Flexbox>
59
+ <div>
60
+ <span style={{ fontWeight: 'bold' }}>{usage.used}</span>
61
+ {['', '/', usage.total].join(' ')}
62
+ </div>
63
+ </Flexbox>
64
+ <Progress
65
+ percent={percent}
66
+ showInfo={false}
67
+ size={'small'}
68
+ strokeColor={theme.colorPrimary}
69
+ />
70
+ </Flexbox>
71
+ );
72
+ },
73
+ );
74
+
75
+ export default ProgressItem;
@@ -489,6 +489,7 @@ describe('SessionModel', () => {
489
489
  it('should delete a session and its associated topics and messages', async () => {
490
490
  // Create a session
491
491
  const sessionId = '1';
492
+ await serverDB.insert(users).values([{ id: '456' }]);
492
493
  await serverDB.insert(sessions).values({ id: sessionId, userId });
493
494
 
494
495
  // Create some topics and messages associated with the session
@@ -500,6 +501,11 @@ describe('SessionModel', () => {
500
501
  { id: '1', sessionId, userId, role: 'user' },
501
502
  { id: '2', sessionId, userId, role: 'assistant' },
502
503
  ]);
504
+ await serverDB.insert(agents).values([
505
+ { id: 'a1', userId },
506
+ { id: 'a2', userId: '456' },
507
+ ]);
508
+ await serverDB.insert(agentsToSessions).values([{ agentId: 'a1', userId, sessionId: '1' }]);
503
509
 
504
510
  // Delete the session
505
511
  await sessionModel.delete(sessionId);
@@ -514,6 +520,7 @@ describe('SessionModel', () => {
514
520
  expect(
515
521
  await serverDB.select().from(messages).where(eq(messages.sessionId, sessionId)),
516
522
  ).toHaveLength(0);
523
+ expect(await serverDB.select().from(agents).where(eq(agents.userId, userId))).toHaveLength(0);
517
524
  });
518
525
 
519
526
  it('should not delete sessions belonging to other users', async () => {
@@ -576,6 +583,8 @@ describe('SessionModel', () => {
576
583
  // Create some sessions
577
584
  const sessionIds = ['1', '2', '3'];
578
585
  await serverDB.insert(sessions).values(sessionIds.map((id) => ({ id, userId })));
586
+ await serverDB.insert(agents).values([{ id: '1', userId }]);
587
+ await serverDB.insert(agentsToSessions).values([{ sessionId: '1', agentId: '1', userId }]);
579
588
 
580
589
  // Create some topics and messages associated with the sessions
581
590
  await serverDB.insert(topics).values([
@@ -602,6 +611,7 @@ describe('SessionModel', () => {
602
611
  expect(
603
612
  await serverDB.select().from(messages).where(inArray(messages.sessionId, sessionIds)),
604
613
  ).toHaveLength(0);
614
+ expect(await serverDB.select().from(agents)).toHaveLength(0);
605
615
  });
606
616
 
607
617
  it('should not delete sessions belonging to other users', async () => {
@@ -710,6 +720,14 @@ describe('SessionModel', () => {
710
720
  { id: 'm1', sessionId: '1', userId, role: 'user' },
711
721
  { id: 'm2', sessionId: '2', userId, role: 'assistant' },
712
722
  ]);
723
+ await trx.insert(agents).values([
724
+ { id: 'a1', userId },
725
+ { id: 'a2', userId },
726
+ ]);
727
+ await trx.insert(agentsToSessions).values([
728
+ { agentId: 'a1', sessionId: '1', userId },
729
+ { agentId: 'a2', sessionId: '2', userId },
730
+ ]);
713
731
  });
714
732
 
715
733
  await sessionModel.deleteAll();
@@ -723,6 +741,9 @@ describe('SessionModel', () => {
723
741
  .from(messages)
724
742
  .where(eq(messages.userId, userId));
725
743
  expect(remainingMessages).toHaveLength(0);
744
+
745
+ const agentsTopics = await serverDB.select().from(agents).where(eq(agents.userId, userId));
746
+ expect(agentsTopics).toHaveLength(0);
726
747
  });
727
748
  });
728
749
 
@@ -0,0 +1,330 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { clientDB, initializeDB } from '@/database/client/db';
4
+ import {
5
+ agents,
6
+ agentsKnowledgeBases,
7
+ agentsToSessions,
8
+ files,
9
+ filesToSessions,
10
+ globalFiles,
11
+ knowledgeBaseFiles,
12
+ knowledgeBases,
13
+ messages,
14
+ sessionGroups,
15
+ sessions,
16
+ topics,
17
+ userSettings,
18
+ users,
19
+ } from '@/database/schemas';
20
+ import { LobeChatDatabase } from '@/database/type';
21
+
22
+ import { DATA_EXPORT_CONFIG, DataExporterRepos } from './index';
23
+
24
+ let db = clientDB as LobeChatDatabase;
25
+
26
+ // 设置测试数据
27
+ describe('DataExporterRepos', () => {
28
+ // 测试数据 ID
29
+ const testIds = {
30
+ userId: 'test-user-id',
31
+ fileId: 'test-file-id',
32
+ fileHash: 'test-file-hash',
33
+ sessionId: 'test-session-id',
34
+ agentId: 'test-agent-id',
35
+ topicId: 'test-topic-id',
36
+ messageId: 'test-message-id',
37
+ knowledgeBaseId: 'test-kb-id',
38
+ };
39
+
40
+ // 设置测试环境
41
+ let userId: string = testIds.userId;
42
+
43
+ const setupTestData = async () => {
44
+ await db.transaction(async (trx) => {
45
+ // 用户数据
46
+ await trx.insert(users).values({
47
+ id: testIds.userId,
48
+ username: 'testuser',
49
+ email: 'test@example.com',
50
+ });
51
+
52
+ // 用户设置
53
+ await trx.insert(userSettings).values({
54
+ id: testIds.userId,
55
+ general: { theme: 'light' },
56
+ });
57
+
58
+ // 全局文件
59
+ await trx.insert(globalFiles).values({
60
+ hashId: testIds.fileHash,
61
+ fileType: 'text/plain',
62
+ size: 1024,
63
+ url: 'https://example.com/test-file.txt',
64
+ creator: testIds.userId,
65
+ });
66
+
67
+ // 文件数据
68
+ await trx.insert(files).values({
69
+ id: testIds.fileId,
70
+ userId: testIds.userId,
71
+ fileType: 'text/plain',
72
+ fileHash: testIds.fileHash,
73
+ name: 'test-file.txt',
74
+ size: 1024,
75
+ url: 'https://example.com/test-file.txt',
76
+ });
77
+
78
+ // 会话组
79
+ await trx.insert(sessionGroups).values({
80
+ name: 'Test Group',
81
+ userId: testIds.userId,
82
+ });
83
+
84
+ // 会话
85
+ await trx.insert(sessions).values({
86
+ id: testIds.sessionId,
87
+ slug: 'test-session',
88
+ title: 'Test Session',
89
+ userId: testIds.userId,
90
+ });
91
+
92
+ // 主题
93
+ await trx.insert(topics).values({
94
+ id: testIds.topicId,
95
+ title: 'Test Topic',
96
+ sessionId: testIds.sessionId,
97
+ userId: testIds.userId,
98
+ });
99
+
100
+ // 消息
101
+ await trx.insert(messages).values({
102
+ id: testIds.messageId,
103
+ role: 'user',
104
+ content: 'Hello, world!',
105
+ userId: testIds.userId,
106
+ sessionId: testIds.sessionId,
107
+ topicId: testIds.topicId,
108
+ });
109
+
110
+ // 代理
111
+ await trx.insert(agents).values({
112
+ id: testIds.agentId,
113
+ title: 'Test Agent',
114
+ userId: testIds.userId,
115
+ });
116
+
117
+ // 代理到会话的关联
118
+ await trx.insert(agentsToSessions).values({
119
+ agentId: testIds.agentId,
120
+ sessionId: testIds.sessionId,
121
+ userId: testIds.userId,
122
+ });
123
+
124
+ // 文件到会话的关联
125
+ await trx.insert(filesToSessions).values({
126
+ fileId: testIds.fileId,
127
+ sessionId: testIds.sessionId,
128
+ userId: testIds.userId,
129
+ });
130
+
131
+ // 知识库
132
+ await trx.insert(knowledgeBases).values({
133
+ id: testIds.knowledgeBaseId,
134
+ name: 'Test Knowledge Base',
135
+ userId: testIds.userId,
136
+ });
137
+
138
+ // 知识库文件
139
+ await trx.insert(knowledgeBaseFiles).values({
140
+ knowledgeBaseId: testIds.knowledgeBaseId,
141
+ fileId: testIds.fileId,
142
+ userId: testIds.userId,
143
+ });
144
+
145
+ // 代理知识库
146
+ await trx.insert(agentsKnowledgeBases).values({
147
+ agentId: testIds.agentId,
148
+ knowledgeBaseId: testIds.knowledgeBaseId,
149
+ userId: testIds.userId,
150
+ });
151
+ });
152
+ };
153
+
154
+ beforeEach(async () => {
155
+ // 创建内存数据库
156
+ await initializeDB();
157
+
158
+ // 插入测试数据
159
+ await setupTestData();
160
+ });
161
+
162
+ afterEach(async () => {
163
+ await db.delete(users);
164
+ await db.delete(globalFiles);
165
+
166
+ vi.restoreAllMocks();
167
+ });
168
+
169
+ describe('export', () => {
170
+ it('should export all user data correctly', async () => {
171
+ // 创建导出器实例
172
+ const dataExporter = new DataExporterRepos(db, userId);
173
+
174
+ // 执行导出
175
+ const result = await dataExporter.export();
176
+
177
+ // 验证基础表导出结果
178
+ // expect(result).toHaveProperty('users');
179
+ // expect(result.users).toHaveLength(1);
180
+ // expect(result.users[0]).toHaveProperty('id', testIds.userId);
181
+ // expect(result.users[0]).not.toHaveProperty('userId'); // userId 字段应该被移除
182
+
183
+ expect(result).toHaveProperty('userSettings');
184
+ expect(result.userSettings).toHaveLength(1);
185
+ expect(result.userSettings[0]).toHaveProperty('id', testIds.userId);
186
+
187
+ // expect(result).toHaveProperty('files');
188
+ // expect(result.files).toHaveLength(1);
189
+ // expect(result.files[0]).toHaveProperty('id', testIds.fileId);
190
+ // expect(result.files[0]).toHaveProperty('fileHash', testIds.fileHash);
191
+ // expect(result.files[0]).not.toHaveProperty('userId');
192
+
193
+ expect(result).toHaveProperty('sessions');
194
+ expect(result.sessions).toHaveLength(1);
195
+ expect(result.sessions[0]).toHaveProperty('id', testIds.sessionId);
196
+
197
+ expect(result).toHaveProperty('topics');
198
+ expect(result.topics).toHaveLength(1);
199
+ expect(result.topics[0]).toHaveProperty('id', testIds.topicId);
200
+
201
+ expect(result).toHaveProperty('messages');
202
+ expect(result.messages).toHaveLength(1);
203
+ expect(result.messages[0]).toHaveProperty('id', testIds.messageId);
204
+
205
+ expect(result).toHaveProperty('agents');
206
+ expect(result.agents).toHaveLength(1);
207
+ expect(result.agents[0]).toHaveProperty('id', testIds.agentId);
208
+
209
+ // expect(result).toHaveProperty('knowledgeBases');
210
+ // expect(result.knowledgeBases).toHaveLength(1);
211
+ // expect(result.knowledgeBases[0]).toHaveProperty('id', testIds.knowledgeBaseId);
212
+
213
+ // 验证关联表导出结果
214
+ // expect(result).toHaveProperty('globalFiles');
215
+ // expect(result.globalFiles).toHaveLength(1);
216
+ // expect(result.globalFiles[0]).toHaveProperty('hashId', testIds.fileHash);
217
+
218
+ expect(result).toHaveProperty('agentsToSessions');
219
+ expect(result.agentsToSessions).toHaveLength(1);
220
+ expect(result.agentsToSessions[0]).toHaveProperty('agentId', testIds.agentId);
221
+ expect(result.agentsToSessions[0]).toHaveProperty('sessionId', testIds.sessionId);
222
+
223
+ // expect(result).toHaveProperty('filesToSessions');
224
+ // expect(result.filesToSessions).toHaveLength(1);
225
+ // expect(result.filesToSessions[0]).toHaveProperty('fileId', testIds.fileId);
226
+ // expect(result.filesToSessions[0]).toHaveProperty('sessionId', testIds.sessionId);
227
+
228
+ // expect(result).toHaveProperty('knowledgeBaseFiles');
229
+ // expect(result.knowledgeBaseFiles).toHaveLength(1);
230
+ // expect(result.knowledgeBaseFiles[0]).toHaveProperty(
231
+ // 'knowledgeBaseId',
232
+ // testIds.knowledgeBaseId,
233
+ // );
234
+ // expect(result.knowledgeBaseFiles[0]).toHaveProperty('fileId', testIds.fileId);
235
+ });
236
+
237
+ it('should handle empty database gracefully', async () => {
238
+ // 清空数据库
239
+
240
+ await db.delete(users);
241
+ await db.delete(globalFiles);
242
+
243
+ // 创建导出器实例
244
+ const dataExporter = new DataExporterRepos(db, userId);
245
+
246
+ // 执行导出
247
+ const result = await dataExporter.export();
248
+
249
+ // 验证所有表都返回空数组
250
+ DATA_EXPORT_CONFIG.baseTables.forEach(({ table }) => {
251
+ expect(result).toHaveProperty(table);
252
+ expect(result[table]).toEqual([]);
253
+ });
254
+
255
+ DATA_EXPORT_CONFIG.relationTables.forEach(({ table }) => {
256
+ expect(result).toHaveProperty(table);
257
+ expect(result[table]).toEqual([]);
258
+ });
259
+ });
260
+
261
+ it('should handle database query errors', async () => {
262
+ // 模拟查询错误
263
+ // @ts-ignore
264
+ vi.spyOn(db.query.users, 'findMany').mockRejectedValueOnce(new Error('Database error'));
265
+
266
+ // 创建导出器实例
267
+ const dataExporter = new DataExporterRepos(db, userId);
268
+
269
+ // 执行导出
270
+ const result = await dataExporter.export();
271
+
272
+ // 验证其他表仍然被导出
273
+ expect(result).toHaveProperty('sessions');
274
+ expect(result.sessions).toHaveLength(1);
275
+ });
276
+
277
+ it.skip('should skip relation tables when source tables have no data', async () => {
278
+ // 删除文件数据,这将导致 globalFiles 表被跳过
279
+ await db.delete(files);
280
+
281
+ // 创建导出器实例
282
+ const dataExporter = new DataExporterRepos(db, userId);
283
+
284
+ // 执行导出
285
+ const result = await dataExporter.export();
286
+
287
+ // 验证文件表为空
288
+ // expect(result).toHaveProperty('files');
289
+ // expect(result.files).toEqual([]);
290
+
291
+ // 验证关联表也为空
292
+ // expect(result).toHaveProperty('globalFiles');
293
+ // expect(result.globalFiles).toEqual([]);
294
+ });
295
+
296
+ it('should export data for a different user', async () => {
297
+ // 创建另一个用户
298
+ const anotherUserId = 'another-user-id';
299
+ await db.transaction(async (trx) => {
300
+ await trx.insert(users).values({
301
+ id: anotherUserId,
302
+ username: 'anotheruser',
303
+ email: 'another@example.com',
304
+ });
305
+ await trx.insert(sessions).values({
306
+ id: 'another-session-id',
307
+ slug: 'another-session',
308
+ title: 'Another Session',
309
+ userId: anotherUserId,
310
+ });
311
+ });
312
+
313
+ // 创建导出器实例,使用另一个用户 ID
314
+ const dataExporter = new DataExporterRepos(db, anotherUserId);
315
+
316
+ // 执行导出
317
+ const result = await dataExporter.export();
318
+
319
+ // 验证只导出了另一个用户的数据
320
+ // expect(result).toHaveProperty('users');
321
+ // expect(result.users).toHaveLength(1);
322
+ // expect(result.users[0]).toHaveProperty('id', anotherUserId);
323
+
324
+ expect(result).toHaveProperty('sessions');
325
+ expect(result.sessions).toHaveLength(1);
326
+ expect(result.sessions[0]).not.toHaveProperty('userId', anotherUserId);
327
+ expect(result.sessions[0]).toHaveProperty('id', 'another-session-id');
328
+ });
329
+ });
330
+ });