@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
@@ -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
|
+
});
|