@lobehub/chat 1.36.45 → 1.37.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/README.ja-JP.md +8 -8
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/changelog/v1.json +18 -0
- package/next.config.mjs +4 -1
- package/package.json +5 -3
- package/scripts/migrateClientDB/compile-migrations.ts +14 -0
- package/src/app/(main)/(mobile)/me/(home)/layout.tsx +2 -0
- package/src/app/(main)/chat/_layout/Desktop/index.tsx +3 -2
- package/src/app/(main)/chat/_layout/Mobile.tsx +5 -3
- package/src/app/(main)/chat/features/Migration/DBReader.ts +290 -0
- package/src/app/(main)/chat/features/Migration/UpgradeButton.tsx +4 -8
- package/src/app/(main)/chat/features/Migration/index.tsx +26 -15
- package/src/app/(main)/settings/_layout/Desktop/index.tsx +2 -0
- package/src/app/loading/Client/Content.tsx +11 -1
- package/src/app/loading/Client/Error.tsx +27 -0
- package/src/app/loading/stage.ts +8 -0
- package/src/components/FullscreenLoading/index.tsx +4 -3
- package/src/const/version.ts +1 -0
- package/src/database/_deprecated/models/file.ts +17 -3
- package/src/database/client/db.test.ts +172 -0
- package/src/database/client/db.ts +246 -0
- package/src/database/client/migrations.json +289 -0
- package/src/features/InitClientDB/EnableModal.tsx +111 -0
- package/src/features/InitClientDB/ErrorResult.tsx +125 -0
- package/src/features/InitClientDB/InitIndicator.tsx +124 -0
- package/src/features/InitClientDB/PGliteSVG.tsx +22 -0
- package/src/features/InitClientDB/index.tsx +37 -0
- package/src/hooks/useCheckPluginsIsInstalled.ts +2 -2
- package/src/hooks/useFetchInstalledPlugins.ts +2 -2
- package/src/hooks/useFetchMessages.ts +2 -2
- package/src/hooks/useFetchSessions.ts +2 -2
- package/src/hooks/useFetchThreads.ts +2 -2
- package/src/hooks/useFetchTopics.ts +2 -2
- package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -2
- package/src/server/routers/lambda/file.ts +1 -3
- package/src/services/__tests__/upload.test.ts +175 -0
- package/src/services/baseClientService/index.ts +9 -0
- package/src/services/debug.ts +32 -34
- package/src/services/file/ClientS3/index.test.ts +115 -0
- package/src/services/file/ClientS3/index.ts +58 -0
- package/src/services/file/client.test.ts +9 -4
- package/src/services/file/client.ts +36 -8
- package/src/services/file/index.ts +6 -2
- package/src/services/file/pglite.test.ts +198 -0
- package/src/services/file/pglite.ts +84 -0
- package/src/services/file/type.ts +4 -3
- package/src/services/github.ts +17 -0
- package/src/services/import/index.ts +6 -2
- package/src/services/import/pglite.test.ts +997 -0
- package/src/services/import/pglite.ts +34 -0
- package/src/services/message/client.ts +2 -0
- package/src/services/message/index.ts +6 -2
- package/src/services/message/pglite.test.ts +430 -0
- package/src/services/message/pglite.ts +118 -0
- package/src/services/message/server.ts +9 -9
- package/src/services/message/type.ts +3 -4
- package/src/services/plugin/index.ts +6 -2
- package/src/services/plugin/pglite.test.ts +175 -0
- package/src/services/plugin/pglite.ts +51 -0
- package/src/services/session/client.ts +1 -1
- package/src/services/session/index.ts +6 -2
- package/src/services/session/pglite.test.ts +411 -0
- package/src/services/session/pglite.ts +184 -0
- package/src/services/session/type.ts +14 -1
- package/src/services/topic/index.ts +6 -3
- package/src/services/topic/pglite.test.ts +212 -0
- package/src/services/topic/pglite.ts +85 -0
- package/src/services/upload.ts +8 -16
- package/src/services/user/client.test.ts +0 -1
- package/src/services/user/index.ts +8 -2
- package/src/services/user/pglite.test.ts +98 -0
- package/src/services/user/pglite.ts +92 -0
- package/src/store/chat/slices/builtinTool/action.test.ts +12 -4
- package/src/store/file/slices/upload/action.ts +33 -67
- package/src/store/global/actions/clientDb.ts +51 -0
- package/src/store/global/initialState.ts +13 -0
- package/src/store/global/selectors.ts +24 -3
- package/src/store/global/store.ts +3 -1
- package/src/store/session/slices/sessionGroup/reducer.test.ts +6 -6
- package/src/store/user/slices/common/action.ts +2 -4
- package/src/types/clientDB.ts +29 -0
- package/src/types/files/upload.ts +8 -2
- package/src/types/importer.ts +17 -5
- package/src/types/meta.ts +0 -9
- package/src/types/session/sessionGroup.ts +3 -3
- package/src/services/message/index.test.ts +0 -48
@@ -0,0 +1,124 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { Progress } from 'antd';
|
4
|
+
import { createStyles } from 'antd-style';
|
5
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
6
|
+
import { rgba } from 'polished';
|
7
|
+
import { memo } from 'react';
|
8
|
+
import { useTranslation } from 'react-i18next';
|
9
|
+
import { Center, Flexbox } from 'react-layout-kit';
|
10
|
+
|
11
|
+
import { useGlobalStore } from '@/store/global';
|
12
|
+
import { ClientDatabaseInitStages, DatabaseLoadingState } from '@/types/clientDB';
|
13
|
+
|
14
|
+
import ErrorResult from './ErrorResult';
|
15
|
+
|
16
|
+
const useStyles = createStyles(({ css, token, prefixCls }) => ({
|
17
|
+
bg: css`
|
18
|
+
padding-block: 8px;
|
19
|
+
padding-inline: 8px 32px;
|
20
|
+
background: ${token.colorText};
|
21
|
+
border-radius: 40px;
|
22
|
+
`,
|
23
|
+
container: css`
|
24
|
+
position: fixed;
|
25
|
+
z-index: 1000;
|
26
|
+
`,
|
27
|
+
progress: css`
|
28
|
+
.${prefixCls}-progress-text {
|
29
|
+
font-size: 12px;
|
30
|
+
color: ${token.colorBgContainer} !important;
|
31
|
+
}
|
32
|
+
`,
|
33
|
+
progressReady: css`
|
34
|
+
.${prefixCls}-progress-text {
|
35
|
+
color: ${token.colorSuccessBorder} !important;
|
36
|
+
}
|
37
|
+
`,
|
38
|
+
|
39
|
+
text: css`
|
40
|
+
font-size: 15px;
|
41
|
+
color: ${token.colorBgContainer};
|
42
|
+
`,
|
43
|
+
}));
|
44
|
+
|
45
|
+
interface InitClientDBProps {
|
46
|
+
bottom?: number;
|
47
|
+
show: boolean;
|
48
|
+
}
|
49
|
+
|
50
|
+
const InitClientDB = memo<InitClientDBProps>(({ bottom = 80, show }) => {
|
51
|
+
const { styles, theme, cx } = useStyles();
|
52
|
+
const currentStage = useGlobalStore((s) => s.initClientDBStage || DatabaseLoadingState.Idle);
|
53
|
+
const { t } = useTranslation('common');
|
54
|
+
const useInitClientDB = useGlobalStore((s) => s.useInitClientDB);
|
55
|
+
|
56
|
+
useInitClientDB();
|
57
|
+
|
58
|
+
const getStateMessage = (state: DatabaseLoadingState) => {
|
59
|
+
switch (state) {
|
60
|
+
case DatabaseLoadingState.Finished:
|
61
|
+
case DatabaseLoadingState.Ready: {
|
62
|
+
return t('clientDB.initing.ready');
|
63
|
+
}
|
64
|
+
|
65
|
+
case DatabaseLoadingState.Idle: {
|
66
|
+
return t('clientDB.initing.idle');
|
67
|
+
}
|
68
|
+
case DatabaseLoadingState.Initializing: {
|
69
|
+
return t('clientDB.initing.initializing');
|
70
|
+
}
|
71
|
+
case DatabaseLoadingState.LoadingDependencies: {
|
72
|
+
return t('clientDB.initing.loadingDependencies');
|
73
|
+
}
|
74
|
+
|
75
|
+
case DatabaseLoadingState.LoadingWasm: {
|
76
|
+
return t('clientDB.initing.loadingWasmModule');
|
77
|
+
}
|
78
|
+
|
79
|
+
case DatabaseLoadingState.Migrating: {
|
80
|
+
return t('clientDB.initing.migrating');
|
81
|
+
}
|
82
|
+
}
|
83
|
+
};
|
84
|
+
|
85
|
+
const currentStageIndex = ClientDatabaseInitStages.indexOf(currentStage);
|
86
|
+
const isReady = currentStage === DatabaseLoadingState.Finished;
|
87
|
+
const isError = currentStage === DatabaseLoadingState.Error;
|
88
|
+
return (
|
89
|
+
<AnimatePresence>
|
90
|
+
{show && (
|
91
|
+
<Center className={styles.container} style={{ bottom }} width={'100%'}>
|
92
|
+
<motion.div
|
93
|
+
animate={{ opacity: 1, y: 0 }}
|
94
|
+
exit={{ opacity: 0, y: 30 }}
|
95
|
+
initial={{ opacity: 0, y: 30 }}
|
96
|
+
transition={{ duration: 0.3 }}
|
97
|
+
>
|
98
|
+
{isError ? (
|
99
|
+
<ErrorResult />
|
100
|
+
) : (
|
101
|
+
<Flexbox align={'center'} className={styles.bg} gap={12} horizontal>
|
102
|
+
<Progress
|
103
|
+
className={cx(styles.progress, isReady && styles.progressReady)}
|
104
|
+
format={isReady ? undefined : (percent) => percent}
|
105
|
+
percent={parseInt(
|
106
|
+
((currentStageIndex / (ClientDatabaseInitStages.length - 1)) * 100).toFixed(0),
|
107
|
+
)}
|
108
|
+
size={40}
|
109
|
+
strokeColor={isReady ? theme.colorSuccessActive : theme.colorBgContainer}
|
110
|
+
strokeLinecap={'round'}
|
111
|
+
strokeWidth={10}
|
112
|
+
trailColor={rgba(theme.colorBgContainer, 0.1)}
|
113
|
+
type={'circle'}
|
114
|
+
/>
|
115
|
+
<span className={styles.text}>{getStateMessage(currentStage)}</span>
|
116
|
+
</Flexbox>
|
117
|
+
)}
|
118
|
+
</motion.div>
|
119
|
+
</Center>
|
120
|
+
)}
|
121
|
+
</AnimatePresence>
|
122
|
+
);
|
123
|
+
});
|
124
|
+
export default InitClientDB;
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { LucideIcon, LucideProps } from 'lucide-react';
|
2
|
+
import { forwardRef } from 'react';
|
3
|
+
|
4
|
+
// @ts-expect-error 类型感觉不对,未来修正
|
5
|
+
export const PGliteSVG: LucideIcon = forwardRef<SVGElement, Partial<Omit<LucideProps, 'ref'>>>(
|
6
|
+
({ size }, ref) => (
|
7
|
+
<svg
|
8
|
+
fill="currentColor"
|
9
|
+
height={size}
|
10
|
+
ref={ref as any}
|
11
|
+
viewBox={'0 0 1024 1024'}
|
12
|
+
width={size}
|
13
|
+
xmlns="http://www.w3.org/2000/svg"
|
14
|
+
>
|
15
|
+
<path
|
16
|
+
clip-rule="evenodd"
|
17
|
+
d="M941.581 335.737v460.806c0 15.926-12.913 28.836-28.832 28.818l-115.283-.137c-15.243-.018-27.706-11.88-28.703-26.877.011-.569.018-1.138.018-1.711l-.004-172.904c0-47.745-38.736-86.451-86.454-86.451-46.245 0-84.052-36.359-86.342-82.068V191.496l201.708.149c79.484.058 143.892 64.553 143.892 144.092Zm-576-144.281v201.818c0 47.746 38.682 86.456 86.4 86.456h86.4v-5.796c0 66.816 54.13 120.98 120.902 120.98 28.617 0 51.815 23.213 51.815 51.848v149.644c0 .688.011 1.372.025 2.057-.943 15.065-13.453 26.992-28.746 26.992l-144.982-.007.986-201.586c.079-15.915-12.755-28.88-28.66-28.959-15.904-.079-28.861 12.763-28.94 28.678l-.986 201.741v.118l-172.174-.01V623.722c0-15.915-12.895-28.819-28.8-28.819-15.906 0-28.8 12.904-28.8 28.819v201.704l-143.642-.007c-15.905-.004-28.798-12.904-28.798-28.819V335.547c0-79.58 64.471-144.093 144.001-144.092l143.999.001Zm446.544 173.693c0-23.874-19.343-43.228-43.2-43.228-23.861 0-43.2 19.354-43.2 43.228 0 23.875 19.339 43.226 43.2 43.226 23.857 0 43.2-19.351 43.2-43.226Z"
|
18
|
+
fill-rule="evenodd"
|
19
|
+
/>
|
20
|
+
</svg>
|
21
|
+
),
|
22
|
+
);
|
@@ -0,0 +1,37 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { Spin } from 'antd';
|
4
|
+
import dynamic from 'next/dynamic';
|
5
|
+
import { memo } from 'react';
|
6
|
+
|
7
|
+
import { useGlobalStore } from '@/store/global';
|
8
|
+
import { systemStatusSelectors } from '@/store/global/selectors';
|
9
|
+
|
10
|
+
const Modal = dynamic(() => import('./EnableModal'), {
|
11
|
+
loading: () => <Spin fullscreen />,
|
12
|
+
ssr: false,
|
13
|
+
});
|
14
|
+
|
15
|
+
const InitIndicator = dynamic(() => import('./InitIndicator'), {
|
16
|
+
ssr: false,
|
17
|
+
});
|
18
|
+
|
19
|
+
interface InitClientDBProps {
|
20
|
+
bottom?: number;
|
21
|
+
}
|
22
|
+
|
23
|
+
const InitClientDB = memo<InitClientDBProps>(({ bottom }) => {
|
24
|
+
const isPgliteNotEnabled = useGlobalStore(systemStatusSelectors.isPgliteNotEnabled);
|
25
|
+
const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited);
|
26
|
+
|
27
|
+
return (
|
28
|
+
<>
|
29
|
+
{/* 当用户没有设置启用 pglite 时,强弹窗引导用户来开启弹窗 */}
|
30
|
+
{isPgliteNotEnabled && <Modal open={isPgliteNotEnabled} />}
|
31
|
+
{/* 当用户已经启用 pglite 但没有初始化时,展示初始化指示器 */}
|
32
|
+
{isPgliteNotInited && <InitIndicator bottom={bottom} show={isPgliteNotInited} />}
|
33
|
+
</>
|
34
|
+
);
|
35
|
+
});
|
36
|
+
|
37
|
+
export default InitClientDB;
|
@@ -3,8 +3,8 @@ import { systemStatusSelectors } from '@/store/global/selectors';
|
|
3
3
|
import { useToolStore } from '@/store/tool';
|
4
4
|
|
5
5
|
export const useCheckPluginsIsInstalled = (plugins: string[]) => {
|
6
|
-
const
|
6
|
+
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
7
7
|
const checkPluginsIsInstalled = useToolStore((s) => s.useCheckPluginsIsInstalled);
|
8
8
|
|
9
|
-
checkPluginsIsInstalled(
|
9
|
+
checkPluginsIsInstalled(isDBInited, plugins);
|
10
10
|
};
|
@@ -3,8 +3,8 @@ import { systemStatusSelectors } from '@/store/global/selectors';
|
|
3
3
|
import { useToolStore } from '@/store/tool';
|
4
4
|
|
5
5
|
export const useFetchInstalledPlugins = () => {
|
6
|
-
const
|
6
|
+
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
7
7
|
const [useFetchInstalledPlugins] = useToolStore((s) => [s.useFetchInstalledPlugins]);
|
8
8
|
|
9
|
-
return useFetchInstalledPlugins(
|
9
|
+
return useFetchInstalledPlugins(isDBInited);
|
10
10
|
};
|
@@ -4,12 +4,12 @@ import { systemStatusSelectors } from '@/store/global/selectors';
|
|
4
4
|
import { useSessionStore } from '@/store/session';
|
5
5
|
|
6
6
|
export const useFetchMessages = () => {
|
7
|
-
const
|
7
|
+
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
8
8
|
const [sessionId] = useSessionStore((s) => [s.activeId]);
|
9
9
|
const [activeTopicId, useFetchMessages] = useChatStore((s) => [
|
10
10
|
s.activeTopicId,
|
11
11
|
s.useFetchMessages,
|
12
12
|
]);
|
13
13
|
|
14
|
-
useFetchMessages(
|
14
|
+
useFetchMessages(isDBInited, sessionId, activeTopicId);
|
15
15
|
};
|
@@ -5,9 +5,9 @@ import { useUserStore } from '@/store/user';
|
|
5
5
|
import { authSelectors } from '@/store/user/slices/auth/selectors';
|
6
6
|
|
7
7
|
export const useFetchSessions = () => {
|
8
|
-
const
|
8
|
+
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
9
9
|
const isLogin = useUserStore(authSelectors.isLogin);
|
10
10
|
const useFetchSessions = useSessionStore((s) => s.useFetchSessions);
|
11
11
|
|
12
|
-
useFetchSessions(
|
12
|
+
useFetchSessions(isDBInited, isLogin);
|
13
13
|
};
|
@@ -3,9 +3,9 @@ import { useGlobalStore } from '@/store/global';
|
|
3
3
|
import { systemStatusSelectors } from '@/store/global/selectors';
|
4
4
|
|
5
5
|
export const useFetchThreads = (activeTopicId?: string) => {
|
6
|
-
const
|
6
|
+
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
7
7
|
|
8
8
|
const [useFetchThreads] = useChatStore((s) => [s.useFetchThreads]);
|
9
9
|
|
10
|
-
useFetchThreads(
|
10
|
+
useFetchThreads(isDBInited, activeTopicId);
|
11
11
|
};
|
@@ -10,8 +10,8 @@ import { useSessionStore } from '@/store/session';
|
|
10
10
|
export const useFetchTopics = () => {
|
11
11
|
const [sessionId] = useSessionStore((s) => [s.activeId]);
|
12
12
|
const [activeTopicId, useFetchTopics] = useChatStore((s) => [s.activeTopicId, s.useFetchTopics]);
|
13
|
-
const
|
13
|
+
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
14
14
|
|
15
|
-
useFetchTopics(
|
15
|
+
useFetchTopics(isDBInited, sessionId);
|
16
16
|
useFetchThreads(activeTopicId);
|
17
17
|
};
|
@@ -51,8 +51,8 @@ const StoreInitialization = memo(() => {
|
|
51
51
|
* But during initialization, the value of `enableAuth` might be incorrect cause of the async fetch.
|
52
52
|
* So we need to use `isSignedIn` only to determine whether request for the default agent config and user state.
|
53
53
|
*/
|
54
|
-
const
|
55
|
-
const isLoginOnInit =
|
54
|
+
const isDBInited = useGlobalStore(systemStatusSelectors.isDBInited);
|
55
|
+
const isLoginOnInit = isDBInited && (enableNextAuth ? isSignedIn : isLogin);
|
56
56
|
|
57
57
|
// init inbox agent and default agent config
|
58
58
|
useInitAgentStore(isLoginOnInit, serverConfig.defaultAgent?.config);
|
@@ -32,9 +32,7 @@ export const fileRouter = router({
|
|
32
32
|
}),
|
33
33
|
|
34
34
|
createFile: fileProcedure
|
35
|
-
.input(
|
36
|
-
UploadFileSchema.omit({ data: true, saveMode: true, url: true }).extend({ url: z.string() }),
|
37
|
-
)
|
35
|
+
.input(UploadFileSchema.omit({ url: true }).extend({ url: z.string() }))
|
38
36
|
.mutation(async ({ ctx, input }) => {
|
39
37
|
const { isExist } = await ctx.fileModel.checkHash(input.hash!);
|
40
38
|
|
@@ -0,0 +1,175 @@
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import { fileEnv } from '@/config/file';
|
4
|
+
import { edgeClient } from '@/libs/trpc/client';
|
5
|
+
import { API_ENDPOINTS } from '@/services/_url';
|
6
|
+
import { clientS3Storage } from '@/services/file/ClientS3';
|
7
|
+
|
8
|
+
import { UPLOAD_NETWORK_ERROR, uploadService } from '../upload';
|
9
|
+
|
10
|
+
// Mock dependencies
|
11
|
+
vi.mock('@/libs/trpc/client', () => ({
|
12
|
+
edgeClient: {
|
13
|
+
upload: {
|
14
|
+
createS3PreSignedUrl: {
|
15
|
+
mutate: vi.fn(),
|
16
|
+
},
|
17
|
+
},
|
18
|
+
},
|
19
|
+
}));
|
20
|
+
|
21
|
+
vi.mock('@/services/file/ClientS3', () => ({
|
22
|
+
clientS3Storage: {
|
23
|
+
putObject: vi.fn(),
|
24
|
+
},
|
25
|
+
}));
|
26
|
+
|
27
|
+
vi.mock('@/utils/uuid', () => ({
|
28
|
+
uuid: () => 'mock-uuid',
|
29
|
+
}));
|
30
|
+
|
31
|
+
describe('UploadService', () => {
|
32
|
+
const mockFile = new File(['test'], 'test.png', { type: 'image/png' });
|
33
|
+
const mockPreSignUrl = 'https://example.com/presign';
|
34
|
+
|
35
|
+
beforeEach(() => {
|
36
|
+
vi.clearAllMocks();
|
37
|
+
// Mock Date.now
|
38
|
+
vi.spyOn(Date, 'now').mockImplementation(() => 3600000); // 1 hour in milliseconds
|
39
|
+
});
|
40
|
+
|
41
|
+
describe('uploadWithProgress', () => {
|
42
|
+
beforeEach(() => {
|
43
|
+
// Mock XMLHttpRequest
|
44
|
+
const xhrMock = {
|
45
|
+
upload: {
|
46
|
+
addEventListener: vi.fn(),
|
47
|
+
},
|
48
|
+
open: vi.fn(),
|
49
|
+
send: vi.fn(),
|
50
|
+
setRequestHeader: vi.fn(),
|
51
|
+
addEventListener: vi.fn(),
|
52
|
+
status: 200,
|
53
|
+
};
|
54
|
+
global.XMLHttpRequest = vi.fn(() => xhrMock) as any;
|
55
|
+
|
56
|
+
// Mock createS3PreSignedUrl
|
57
|
+
(edgeClient.upload.createS3PreSignedUrl.mutate as any).mockResolvedValue(mockPreSignUrl);
|
58
|
+
});
|
59
|
+
|
60
|
+
it('should upload file successfully with progress', async () => {
|
61
|
+
const onProgress = vi.fn();
|
62
|
+
const xhr = new XMLHttpRequest();
|
63
|
+
|
64
|
+
// Simulate successful upload
|
65
|
+
vi.spyOn(xhr, 'addEventListener').mockImplementation((event, handler) => {
|
66
|
+
if (event === 'load') {
|
67
|
+
// @ts-ignore
|
68
|
+
handler({ target: { status: 200 } });
|
69
|
+
}
|
70
|
+
});
|
71
|
+
|
72
|
+
const result = await uploadService.uploadWithProgress(mockFile, { onProgress });
|
73
|
+
|
74
|
+
expect(result).toEqual({
|
75
|
+
date: '1',
|
76
|
+
dirname: `${fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/1`,
|
77
|
+
filename: 'mock-uuid.png',
|
78
|
+
path: `${fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/1/mock-uuid.png`,
|
79
|
+
});
|
80
|
+
});
|
81
|
+
|
82
|
+
it('should handle network error', async () => {
|
83
|
+
const xhr = new XMLHttpRequest();
|
84
|
+
|
85
|
+
// Simulate network error
|
86
|
+
vi.spyOn(xhr, 'addEventListener').mockImplementation((event, handler) => {
|
87
|
+
if (event === 'error') {
|
88
|
+
Object.assign(xhr, { status: 0 });
|
89
|
+
// @ts-ignore
|
90
|
+
handler({});
|
91
|
+
}
|
92
|
+
});
|
93
|
+
|
94
|
+
await expect(uploadService.uploadWithProgress(mockFile, {})).rejects.toBe(
|
95
|
+
UPLOAD_NETWORK_ERROR,
|
96
|
+
);
|
97
|
+
});
|
98
|
+
|
99
|
+
it('should handle upload error', async () => {
|
100
|
+
const xhr = new XMLHttpRequest();
|
101
|
+
|
102
|
+
// Simulate upload error
|
103
|
+
vi.spyOn(xhr, 'addEventListener').mockImplementation((event, handler) => {
|
104
|
+
if (event === 'load') {
|
105
|
+
Object.assign(xhr, { status: 400, statusText: 'Bad Request' });
|
106
|
+
|
107
|
+
// @ts-ignore
|
108
|
+
handler({});
|
109
|
+
}
|
110
|
+
});
|
111
|
+
|
112
|
+
await expect(uploadService.uploadWithProgress(mockFile, {})).rejects.toBe('Bad Request');
|
113
|
+
});
|
114
|
+
});
|
115
|
+
|
116
|
+
describe('uploadToClientS3', () => {
|
117
|
+
it('should upload file to client S3 successfully', async () => {
|
118
|
+
const hash = 'test-hash';
|
119
|
+
const expectedResult = {
|
120
|
+
date: '1',
|
121
|
+
dirname: '',
|
122
|
+
filename: mockFile.name,
|
123
|
+
path: `client-s3://${hash}`,
|
124
|
+
};
|
125
|
+
|
126
|
+
(clientS3Storage.putObject as any).mockResolvedValue(undefined);
|
127
|
+
|
128
|
+
const result = await uploadService.uploadToClientS3(hash, mockFile);
|
129
|
+
|
130
|
+
expect(clientS3Storage.putObject).toHaveBeenCalledWith(hash, mockFile);
|
131
|
+
expect(result).toEqual(expectedResult);
|
132
|
+
});
|
133
|
+
});
|
134
|
+
|
135
|
+
describe('getImageFileByUrlWithCORS', () => {
|
136
|
+
beforeEach(() => {
|
137
|
+
global.fetch = vi.fn();
|
138
|
+
});
|
139
|
+
|
140
|
+
it('should fetch and create file from URL', async () => {
|
141
|
+
const url = 'https://example.com/image.png';
|
142
|
+
const filename = 'test.png';
|
143
|
+
const mockArrayBuffer = new ArrayBuffer(8);
|
144
|
+
|
145
|
+
(global.fetch as any).mockResolvedValue({
|
146
|
+
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
|
147
|
+
});
|
148
|
+
|
149
|
+
const result = await uploadService.getImageFileByUrlWithCORS(url, filename);
|
150
|
+
|
151
|
+
expect(global.fetch).toHaveBeenCalledWith(API_ENDPOINTS.proxy, {
|
152
|
+
body: url,
|
153
|
+
method: 'POST',
|
154
|
+
});
|
155
|
+
expect(result).toBeInstanceOf(File);
|
156
|
+
expect(result.name).toBe(filename);
|
157
|
+
expect(result.type).toBe('image/png');
|
158
|
+
});
|
159
|
+
|
160
|
+
it('should handle custom file type', async () => {
|
161
|
+
const url = 'https://example.com/image.jpg';
|
162
|
+
const filename = 'test.jpg';
|
163
|
+
const fileType = 'image/jpeg';
|
164
|
+
const mockArrayBuffer = new ArrayBuffer(8);
|
165
|
+
|
166
|
+
(global.fetch as any).mockResolvedValue({
|
167
|
+
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
|
168
|
+
});
|
169
|
+
|
170
|
+
const result = await uploadService.getImageFileByUrlWithCORS(url, filename, fileType);
|
171
|
+
|
172
|
+
expect(result.type).toBe(fileType);
|
173
|
+
});
|
174
|
+
});
|
175
|
+
});
|
package/src/services/debug.ts
CHANGED
@@ -1,39 +1,37 @@
|
|
1
|
-
import { DEBUG_MODEL } from '@/database/_deprecated/models/__DEBUG';
|
2
|
-
|
3
1
|
class DebugService {
|
4
2
|
async insertLargeDataToDB() {
|
5
|
-
await DEBUG_MODEL.createRandomData({
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
});
|
11
|
-
|
12
|
-
console.log('已插入10w');
|
13
|
-
|
14
|
-
await DEBUG_MODEL.createRandomData({
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
});
|
20
|
-
console.log('已插入40w');
|
21
|
-
|
22
|
-
await DEBUG_MODEL.createRandomData({
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
});
|
28
|
-
console.log('已插入70w');
|
29
|
-
|
30
|
-
await DEBUG_MODEL.createRandomData({
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
});
|
36
|
-
console.log('已插入100w');
|
3
|
+
// await DEBUG_MODEL.createRandomData({
|
4
|
+
// messageCount: 100_000,
|
5
|
+
// sessionCount: 40,
|
6
|
+
// startIndex: 0,
|
7
|
+
// topicCount: 200,
|
8
|
+
// });
|
9
|
+
//
|
10
|
+
// console.log('已插入10w');
|
11
|
+
//
|
12
|
+
// await DEBUG_MODEL.createRandomData({
|
13
|
+
// messageCount: 300_000,
|
14
|
+
// sessionCount: 40,
|
15
|
+
// startIndex: 100_001,
|
16
|
+
// topicCount: 200,
|
17
|
+
// });
|
18
|
+
// console.log('已插入40w');
|
19
|
+
//
|
20
|
+
// await DEBUG_MODEL.createRandomData({
|
21
|
+
// messageCount: 300_000,
|
22
|
+
// sessionCount: 40,
|
23
|
+
// startIndex: 400_001,
|
24
|
+
// topicCount: 200,
|
25
|
+
// });
|
26
|
+
// console.log('已插入70w');
|
27
|
+
//
|
28
|
+
// await DEBUG_MODEL.createRandomData({
|
29
|
+
// messageCount: 300_000,
|
30
|
+
// sessionCount: 40,
|
31
|
+
// startIndex: 700_001,
|
32
|
+
// topicCount: 200,
|
33
|
+
// });
|
34
|
+
// console.log('已插入100w');
|
37
35
|
}
|
38
36
|
}
|
39
37
|
|
@@ -0,0 +1,115 @@
|
|
1
|
+
import { createStore, del, get, set } from 'idb-keyval';
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
3
|
+
|
4
|
+
import { BrowserS3Storage } from './index';
|
5
|
+
|
6
|
+
// Mock idb-keyval
|
7
|
+
vi.mock('idb-keyval', () => ({
|
8
|
+
createStore: vi.fn(),
|
9
|
+
set: vi.fn(),
|
10
|
+
get: vi.fn(),
|
11
|
+
del: vi.fn(),
|
12
|
+
}));
|
13
|
+
|
14
|
+
let storage: BrowserS3Storage;
|
15
|
+
let mockStore = {};
|
16
|
+
|
17
|
+
beforeEach(() => {
|
18
|
+
// Reset all mocks before each test
|
19
|
+
vi.clearAllMocks();
|
20
|
+
mockStore = {};
|
21
|
+
(createStore as any).mockReturnValue(mockStore);
|
22
|
+
storage = new BrowserS3Storage();
|
23
|
+
});
|
24
|
+
|
25
|
+
describe('BrowserS3Storage', () => {
|
26
|
+
describe('constructor', () => {
|
27
|
+
it('should create store when in browser environment', () => {
|
28
|
+
expect(createStore).toHaveBeenCalledWith('lobechat-local-s3', 'objects');
|
29
|
+
});
|
30
|
+
});
|
31
|
+
|
32
|
+
describe('putObject', () => {
|
33
|
+
it('should successfully put a file object', async () => {
|
34
|
+
const mockFile = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
35
|
+
const mockArrayBuffer = new ArrayBuffer(8);
|
36
|
+
vi.spyOn(mockFile, 'arrayBuffer').mockResolvedValue(mockArrayBuffer);
|
37
|
+
(set as any).mockResolvedValue(undefined);
|
38
|
+
|
39
|
+
await storage.putObject('1-test-key', mockFile);
|
40
|
+
|
41
|
+
expect(set).toHaveBeenCalledWith(
|
42
|
+
'1-test-key',
|
43
|
+
{
|
44
|
+
data: mockArrayBuffer,
|
45
|
+
name: 'test.txt',
|
46
|
+
type: 'text/plain',
|
47
|
+
},
|
48
|
+
mockStore,
|
49
|
+
);
|
50
|
+
});
|
51
|
+
|
52
|
+
it('should throw error when put operation fails', async () => {
|
53
|
+
const mockFile = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
54
|
+
const mockError = new Error('Storage error');
|
55
|
+
(set as any).mockRejectedValue(mockError);
|
56
|
+
|
57
|
+
await expect(storage.putObject('test-key', mockFile)).rejects.toThrow(
|
58
|
+
'Failed to put file test.txt: Storage error',
|
59
|
+
);
|
60
|
+
});
|
61
|
+
});
|
62
|
+
|
63
|
+
describe('getObject', () => {
|
64
|
+
it('should successfully get a file object', async () => {
|
65
|
+
const mockData = {
|
66
|
+
data: new ArrayBuffer(8),
|
67
|
+
name: 'test.txt',
|
68
|
+
type: 'text/plain',
|
69
|
+
};
|
70
|
+
(get as any).mockResolvedValue(mockData);
|
71
|
+
|
72
|
+
const result = await storage.getObject('test-key');
|
73
|
+
|
74
|
+
expect(result).toBeInstanceOf(File);
|
75
|
+
expect(result?.name).toBe('test.txt');
|
76
|
+
expect(result?.type).toBe('text/plain');
|
77
|
+
});
|
78
|
+
|
79
|
+
it('should return undefined when file not found', async () => {
|
80
|
+
(get as any).mockResolvedValue(undefined);
|
81
|
+
|
82
|
+
const result = await storage.getObject('test-key');
|
83
|
+
|
84
|
+
expect(result).toBeUndefined();
|
85
|
+
});
|
86
|
+
|
87
|
+
it('should throw error when get operation fails', async () => {
|
88
|
+
const mockError = new Error('Storage error');
|
89
|
+
(get as any).mockRejectedValue(mockError);
|
90
|
+
|
91
|
+
await expect(storage.getObject('test-key')).rejects.toThrow(
|
92
|
+
'Failed to get object (key=test-key): Storage error',
|
93
|
+
);
|
94
|
+
});
|
95
|
+
});
|
96
|
+
|
97
|
+
describe('deleteObject', () => {
|
98
|
+
it('should successfully delete a file object', async () => {
|
99
|
+
(del as any).mockResolvedValue(undefined);
|
100
|
+
|
101
|
+
await storage.deleteObject('test-key2');
|
102
|
+
|
103
|
+
expect(del).toHaveBeenCalledWith('test-key2', {});
|
104
|
+
});
|
105
|
+
|
106
|
+
it('should throw error when delete operation fails', async () => {
|
107
|
+
const mockError = new Error('Storage error');
|
108
|
+
(del as any).mockRejectedValue(mockError);
|
109
|
+
|
110
|
+
await expect(storage.deleteObject('test-key')).rejects.toThrow(
|
111
|
+
'Failed to delete object (key=test-key): Storage error',
|
112
|
+
);
|
113
|
+
});
|
114
|
+
});
|
115
|
+
});
|