@lobehub/chat 1.36.46 → 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.
Files changed (78) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.ja-JP.md +8 -8
  3. package/README.md +8 -8
  4. package/README.zh-CN.md +8 -8
  5. package/changelog/v1.json +9 -0
  6. package/next.config.mjs +4 -1
  7. package/package.json +5 -3
  8. package/scripts/migrateClientDB/compile-migrations.ts +14 -0
  9. package/src/app/(main)/(mobile)/me/(home)/layout.tsx +2 -0
  10. package/src/app/(main)/chat/_layout/Desktop/index.tsx +3 -2
  11. package/src/app/(main)/chat/_layout/Mobile.tsx +5 -3
  12. package/src/app/(main)/chat/features/Migration/DBReader.ts +290 -0
  13. package/src/app/(main)/chat/features/Migration/UpgradeButton.tsx +4 -8
  14. package/src/app/(main)/chat/features/Migration/index.tsx +26 -15
  15. package/src/app/(main)/settings/_layout/Desktop/index.tsx +2 -0
  16. package/src/app/loading/Client/Content.tsx +11 -1
  17. package/src/app/loading/Client/Error.tsx +27 -0
  18. package/src/app/loading/stage.ts +8 -0
  19. package/src/components/FullscreenLoading/index.tsx +4 -3
  20. package/src/const/version.ts +1 -0
  21. package/src/database/client/db.test.ts +172 -0
  22. package/src/database/client/db.ts +246 -0
  23. package/src/database/client/migrations.json +289 -0
  24. package/src/features/InitClientDB/EnableModal.tsx +111 -0
  25. package/src/features/InitClientDB/ErrorResult.tsx +125 -0
  26. package/src/features/InitClientDB/InitIndicator.tsx +124 -0
  27. package/src/features/InitClientDB/PGliteSVG.tsx +22 -0
  28. package/src/features/InitClientDB/index.tsx +37 -0
  29. package/src/hooks/useCheckPluginsIsInstalled.ts +2 -2
  30. package/src/hooks/useFetchInstalledPlugins.ts +2 -2
  31. package/src/hooks/useFetchMessages.ts +2 -2
  32. package/src/hooks/useFetchSessions.ts +2 -2
  33. package/src/hooks/useFetchThreads.ts +2 -2
  34. package/src/hooks/useFetchTopics.ts +2 -2
  35. package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -2
  36. package/src/services/baseClientService/index.ts +9 -0
  37. package/src/services/debug.ts +32 -34
  38. package/src/services/file/index.ts +6 -2
  39. package/src/services/file/pglite.test.ts +198 -0
  40. package/src/services/file/pglite.ts +84 -0
  41. package/src/services/file/type.ts +4 -3
  42. package/src/services/github.ts +17 -0
  43. package/src/services/import/index.ts +6 -2
  44. package/src/services/import/pglite.test.ts +997 -0
  45. package/src/services/import/pglite.ts +34 -0
  46. package/src/services/message/client.ts +2 -0
  47. package/src/services/message/index.ts +6 -2
  48. package/src/services/message/pglite.test.ts +430 -0
  49. package/src/services/message/pglite.ts +118 -0
  50. package/src/services/message/server.ts +9 -9
  51. package/src/services/message/type.ts +3 -4
  52. package/src/services/plugin/index.ts +6 -2
  53. package/src/services/plugin/pglite.test.ts +175 -0
  54. package/src/services/plugin/pglite.ts +51 -0
  55. package/src/services/session/client.ts +1 -1
  56. package/src/services/session/index.ts +6 -2
  57. package/src/services/session/pglite.test.ts +411 -0
  58. package/src/services/session/pglite.ts +184 -0
  59. package/src/services/session/type.ts +14 -1
  60. package/src/services/topic/index.ts +6 -3
  61. package/src/services/topic/pglite.test.ts +212 -0
  62. package/src/services/topic/pglite.ts +85 -0
  63. package/src/services/user/client.test.ts +0 -1
  64. package/src/services/user/index.ts +8 -2
  65. package/src/services/user/pglite.test.ts +98 -0
  66. package/src/services/user/pglite.ts +92 -0
  67. package/src/store/chat/slices/builtinTool/action.test.ts +3 -4
  68. package/src/store/global/actions/clientDb.ts +51 -0
  69. package/src/store/global/initialState.ts +13 -0
  70. package/src/store/global/selectors.ts +24 -3
  71. package/src/store/global/store.ts +3 -1
  72. package/src/store/session/slices/sessionGroup/reducer.test.ts +6 -6
  73. package/src/store/user/slices/common/action.ts +2 -4
  74. package/src/types/clientDB.ts +29 -0
  75. package/src/types/importer.ts +17 -5
  76. package/src/types/meta.ts +0 -9
  77. package/src/types/session/sessionGroup.ts +3 -3
  78. package/src/services/message/index.test.ts +0 -48
@@ -6,6 +6,7 @@ import { memo, useRef } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { Flexbox } from 'react-layout-kit';
8
8
 
9
+ import InitClientDB from '@/features/InitClientDB';
9
10
  import Footer from '@/features/Setting/Footer';
10
11
  import SettingContainer from '@/features/Setting/SettingContainer';
11
12
  import { useActiveSettingsKey } from '@/hooks/useActiveSettingsKey';
@@ -45,6 +46,7 @@ const Layout = memo<LayoutProps>(({ children, category }) => {
45
46
  </Header>
46
47
  )}
47
48
  <SettingContainer addonAfter={<Footer />}>{children}</SettingContainer>
49
+ <InitClientDB />
48
50
  </Flexbox>
49
51
  );
50
52
  });
@@ -1,17 +1,25 @@
1
+ import dynamic from 'next/dynamic';
1
2
  import React, { memo } from 'react';
2
3
  import { useTranslation } from 'react-i18next';
3
4
 
4
5
  import FullscreenLoading from '@/components/FullscreenLoading';
5
6
  import { useGlobalStore } from '@/store/global';
6
7
  import { systemStatusSelectors } from '@/store/global/selectors';
8
+ import { DatabaseLoadingState } from '@/types/clientDB';
7
9
 
8
10
  import { CLIENT_LOADING_STAGES } from '../stage';
9
11
 
12
+ const InitError = dynamic(() => import('./Error'), { ssr: false });
13
+
10
14
  interface InitProps {
11
15
  setActiveStage: (value: string) => void;
12
16
  }
13
17
 
14
- const Init = memo<InitProps>(() => {
18
+ const Init = memo<InitProps>(({ setActiveStage }) => {
19
+ const useInitClientDB = useGlobalStore((s) => s.useInitClientDB);
20
+
21
+ useInitClientDB({ onStateChange: setActiveStage });
22
+
15
23
  return null;
16
24
  });
17
25
 
@@ -23,12 +31,14 @@ interface ContentProps {
23
31
  const Content = memo<ContentProps>(({ loadingStage, setActiveStage }) => {
24
32
  const { t } = useTranslation('common');
25
33
  const isPgliteNotInited = useGlobalStore(systemStatusSelectors.isPgliteNotInited);
34
+ const isError = useGlobalStore((s) => s.initClientDBStage === DatabaseLoadingState.Error);
26
35
 
27
36
  return (
28
37
  <>
29
38
  {isPgliteNotInited && <Init setActiveStage={setActiveStage} />}
30
39
  <FullscreenLoading
31
40
  activeStage={CLIENT_LOADING_STAGES.indexOf(loadingStage)}
41
+ contentRender={isError && <InitError />}
32
42
  stages={CLIENT_LOADING_STAGES.map((key) => t(`appLoading.${key}` as any))}
33
43
  />
34
44
  </>
@@ -0,0 +1,27 @@
1
+ import { Button } from 'antd';
2
+ import React from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Center } from 'react-layout-kit';
5
+
6
+ import ErrorResult from '@/features/InitClientDB/ErrorResult';
7
+
8
+ const InitError = () => {
9
+ const { t } = useTranslation('common');
10
+
11
+ return (
12
+ <ErrorResult>
13
+ {({ setOpen }) => (
14
+ <Center gap={8}>
15
+ {t('appLoading.failed')}
16
+ <div>
17
+ <Button onClick={() => setOpen(true)} type={'primary'}>
18
+ {t('appLoading.showDetail')}
19
+ </Button>
20
+ </div>
21
+ </Center>
22
+ )}
23
+ </ErrorResult>
24
+ );
25
+ };
26
+
27
+ export default InitError;
@@ -1,3 +1,5 @@
1
+ import { DatabaseLoadingState } from '@/types/clientDB';
2
+
1
3
  export enum AppLoadingStage {
2
4
  GoToChat = 'goToChat',
3
5
  Idle = 'appIdle',
@@ -17,6 +19,12 @@ export const SERVER_LOADING_STAGES = [
17
19
  export const CLIENT_LOADING_STAGES = [
18
20
  AppLoadingStage.Idle,
19
21
  AppLoadingStage.Initializing,
22
+ DatabaseLoadingState.Initializing,
23
+ DatabaseLoadingState.LoadingDependencies,
24
+ DatabaseLoadingState.LoadingWasm,
25
+ DatabaseLoadingState.Migrating,
26
+ DatabaseLoadingState.Finished,
27
+ DatabaseLoadingState.Ready,
20
28
  AppLoadingStage.InitUser,
21
29
  AppLoadingStage.GoToChat,
22
30
  ] as string[];
@@ -1,4 +1,4 @@
1
- import React, { memo } from 'react';
1
+ import React, { ReactNode, memo } from 'react';
2
2
  import { Center, Flexbox } from 'react-layout-kit';
3
3
 
4
4
  import { ProductLogo } from '@/components/Branding';
@@ -6,15 +6,16 @@ import InitProgress, { StageItem } from '@/components/InitProgress';
6
6
 
7
7
  interface FullscreenLoadingProps {
8
8
  activeStage: number;
9
+ contentRender?: ReactNode;
9
10
  stages: StageItem[];
10
11
  }
11
12
 
12
- const FullscreenLoading = memo<FullscreenLoadingProps>(({ activeStage, stages }) => {
13
+ const FullscreenLoading = memo<FullscreenLoadingProps>(({ activeStage, stages, contentRender }) => {
13
14
  return (
14
15
  <Flexbox height={'100%'} style={{ position: 'relative', userSelect: 'none' }} width={'100%'}>
15
16
  <Center flex={1} gap={16} width={'100%'}>
16
17
  <ProductLogo size={48} type={'combine'} />
17
- <InitProgress activeStage={activeStage} stages={stages} />
18
+ {contentRender ? contentRender : <InitProgress activeStage={activeStage} stages={stages} />}
18
19
  </Center>
19
20
  </Flexbox>
20
21
  );
@@ -6,6 +6,7 @@ import { BRANDING_NAME, ORG_NAME } from './branding';
6
6
  export const CURRENT_VERSION = pkg.version;
7
7
 
8
8
  export const isServerMode = getServerDBConfig().NEXT_PUBLIC_ENABLED_SERVER_SERVICE;
9
+ export const isUsePgliteDB = process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite';
9
10
 
10
11
  // @ts-ignore
11
12
  export const isCustomBranding = BRANDING_NAME !== 'LobeChat';
@@ -0,0 +1,172 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { ClientDBLoadingProgress, DatabaseLoadingState } from '@/types/clientDB';
4
+
5
+ import { DatabaseManager } from './db';
6
+
7
+ // Mock 所有外部依赖
8
+ vi.mock('@electric-sql/pglite', () => ({
9
+ default: vi.fn(),
10
+ IdbFs: vi.fn(),
11
+ PGlite: vi.fn(),
12
+ MemoryFS: vi.fn(),
13
+ }));
14
+
15
+ vi.mock('@electric-sql/pglite/vector', () => ({
16
+ default: vi.fn(),
17
+ vector: vi.fn(),
18
+ }));
19
+
20
+ vi.mock('drizzle-orm/pglite', () => ({
21
+ drizzle: vi.fn(() => ({
22
+ dialect: {
23
+ migrate: vi.fn().mockResolvedValue(undefined),
24
+ },
25
+ })),
26
+ }));
27
+
28
+ let manager: DatabaseManager;
29
+ let progressEvents: ClientDBLoadingProgress[] = [];
30
+ let stateChanges: DatabaseLoadingState[] = [];
31
+
32
+ let callbacks = {
33
+ onProgress: vi.fn((progress: ClientDBLoadingProgress) => {
34
+ progressEvents.push(progress);
35
+ }),
36
+ onStateChange: vi.fn((state: DatabaseLoadingState) => {
37
+ stateChanges.push(state);
38
+ }),
39
+ };
40
+
41
+ beforeEach(() => {
42
+ vi.clearAllMocks();
43
+ progressEvents = [];
44
+ stateChanges = [];
45
+
46
+ callbacks = {
47
+ onProgress: vi.fn((progress: ClientDBLoadingProgress) => {
48
+ progressEvents.push(progress);
49
+ }),
50
+ onStateChange: vi.fn((state: DatabaseLoadingState) => {
51
+ stateChanges.push(state);
52
+ }),
53
+ };
54
+ // @ts-expect-error
55
+ DatabaseManager['instance'] = undefined;
56
+ manager = DatabaseManager.getInstance();
57
+ });
58
+
59
+ describe('DatabaseManager', () => {
60
+ describe('Callback Handling', () => {
61
+ it('should properly track loading states', async () => {
62
+ await manager.initialize(callbacks);
63
+
64
+ // 验证状态转换顺序
65
+ expect(stateChanges).toEqual([
66
+ DatabaseLoadingState.Initializing,
67
+ DatabaseLoadingState.LoadingDependencies,
68
+ DatabaseLoadingState.LoadingWasm,
69
+ DatabaseLoadingState.Migrating,
70
+ DatabaseLoadingState.Finished,
71
+ DatabaseLoadingState.Ready,
72
+ ]);
73
+ });
74
+
75
+ it('should report dependencies loading progress', async () => {
76
+ await manager.initialize(callbacks);
77
+
78
+ // 验证依赖加载进度回调
79
+ const dependencyProgress = progressEvents.filter((e) => e.phase === 'dependencies');
80
+ expect(dependencyProgress.length).toBeGreaterThan(0);
81
+ expect(dependencyProgress[dependencyProgress.length - 1]).toEqual(
82
+ expect.objectContaining({
83
+ phase: 'dependencies',
84
+ progress: 100,
85
+ costTime: expect.any(Number),
86
+ }),
87
+ );
88
+ });
89
+
90
+ it('should report WASM loading progress', async () => {
91
+ await manager.initialize(callbacks);
92
+
93
+ // 验证 WASM 加载进度回调
94
+ const wasmProgress = progressEvents.filter((e) => e.phase === 'wasm');
95
+ // expect(wasmProgress.length).toBeGreaterThan(0);
96
+ expect(wasmProgress[wasmProgress.length - 1]).toEqual(
97
+ expect.objectContaining({
98
+ phase: 'wasm',
99
+ progress: 100,
100
+ costTime: expect.any(Number),
101
+ }),
102
+ );
103
+ });
104
+
105
+ it('should handle initialization errors', async () => {
106
+ // 模拟加载失败
107
+ vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));
108
+
109
+ await expect(manager.initialize(callbacks)).rejects.toThrow();
110
+ expect(stateChanges).toContain(DatabaseLoadingState.Error);
111
+ });
112
+
113
+ it('should only initialize once when called multiple times', async () => {
114
+ const firstInit = manager.initialize(callbacks);
115
+ const secondInit = manager.initialize(callbacks);
116
+
117
+ await Promise.all([firstInit, secondInit]);
118
+
119
+ // 验证回调只被调用一次
120
+ const readyStateCount = stateChanges.filter(
121
+ (state) => state === DatabaseLoadingState.Ready,
122
+ ).length;
123
+ expect(readyStateCount).toBe(1);
124
+ });
125
+ });
126
+
127
+ describe('Progress Calculation', () => {
128
+ it('should report progress between 0 and 100', async () => {
129
+ await manager.initialize(callbacks);
130
+
131
+ // 验证所有进度值都在有效范围内
132
+ progressEvents.forEach((event) => {
133
+ expect(event.progress).toBeGreaterThanOrEqual(0);
134
+ expect(event.progress).toBeLessThanOrEqual(100);
135
+ });
136
+ });
137
+
138
+ it('should include timing information', async () => {
139
+ await manager.initialize(callbacks);
140
+
141
+ // 验证最终进度回调包含耗时信息
142
+ const finalProgress = progressEvents[progressEvents.length - 1];
143
+ expect(finalProgress.costTime).toBeGreaterThan(0);
144
+ });
145
+ });
146
+
147
+ describe('Error Handling', () => {
148
+ it('should handle missing callbacks gracefully', async () => {
149
+ // 测试没有提供回调的情况
150
+ await expect(manager.initialize()).resolves.toBeDefined();
151
+ });
152
+
153
+ it('should handle partial callbacks', async () => {
154
+ // 只提供部分回调
155
+ await expect(manager.initialize({ onProgress: callbacks.onProgress })).resolves.toBeDefined();
156
+ await expect(
157
+ manager.initialize({ onStateChange: callbacks.onStateChange }),
158
+ ).resolves.toBeDefined();
159
+ });
160
+ });
161
+
162
+ describe('Database Access', () => {
163
+ it('should throw error when accessing database before initialization', () => {
164
+ expect(() => manager.db).toThrow('Database not initialized');
165
+ });
166
+
167
+ it('should provide access to database after initialization', async () => {
168
+ await manager.initialize();
169
+ expect(() => manager.db).not.toThrow();
170
+ });
171
+ });
172
+ });
@@ -0,0 +1,246 @@
1
+ import type { PgliteDatabase } from 'drizzle-orm/pglite';
2
+ import { Md5 } from 'ts-md5';
3
+
4
+ import { ClientDBLoadingProgress, DatabaseLoadingState } from '@/types/clientDB';
5
+ import { sleep } from '@/utils/sleep';
6
+
7
+ import * as schema from '../schemas';
8
+ import migrations from './migrations.json';
9
+
10
+ const pgliteSchemaHashCache = 'LOBE_CHAT_PGLITE_SCHEMA_HASH';
11
+
12
+ type DrizzleInstance = PgliteDatabase<typeof schema>;
13
+
14
+ export interface DatabaseLoadingCallbacks {
15
+ onError?: (error: Error) => void;
16
+ onProgress?: (progress: ClientDBLoadingProgress) => void;
17
+ onStateChange?: (state: DatabaseLoadingState) => void;
18
+ }
19
+
20
+ export class DatabaseManager {
21
+ private static instance: DatabaseManager;
22
+ private dbInstance: DrizzleInstance | null = null;
23
+ private initPromise: Promise<DrizzleInstance> | null = null;
24
+ private callbacks?: DatabaseLoadingCallbacks;
25
+ private isLocalDBSchemaSynced = false;
26
+
27
+ // CDN 配置
28
+ private static WASM_CDN_URL =
29
+ 'https://registry.npmmirror.com/@electric-sql/pglite/0.2.13/files/dist/postgres.wasm';
30
+
31
+ private constructor() {}
32
+
33
+ static getInstance() {
34
+ if (!DatabaseManager.instance) {
35
+ DatabaseManager.instance = new DatabaseManager();
36
+ }
37
+ return DatabaseManager.instance;
38
+ }
39
+
40
+ // 加载并编译 WASM 模块
41
+ private async loadWasmModule(): Promise<WebAssembly.Module> {
42
+ const start = Date.now();
43
+ this.callbacks?.onStateChange?.(DatabaseLoadingState.LoadingWasm);
44
+
45
+ const response = await fetch(DatabaseManager.WASM_CDN_URL);
46
+
47
+ const contentLength = Number(response.headers.get('Content-Length')) || 0;
48
+ const reader = response.body?.getReader();
49
+
50
+ if (!reader) throw new Error('Failed to start WASM download');
51
+
52
+ let receivedLength = 0;
53
+ const chunks: Uint8Array[] = [];
54
+
55
+ // 读取数据流
56
+ // eslint-disable-next-line no-constant-condition
57
+ while (true) {
58
+ const { done, value } = await reader.read();
59
+
60
+ if (done) break;
61
+
62
+ chunks.push(value);
63
+ receivedLength += value.length;
64
+
65
+ // 计算并报告进度
66
+ const progress = Math.min(Math.round((receivedLength / contentLength) * 100), 100);
67
+ this.callbacks?.onProgress?.({
68
+ phase: 'wasm',
69
+ progress,
70
+ });
71
+ }
72
+
73
+ // 合并数据块
74
+ const wasmBytes = new Uint8Array(receivedLength);
75
+ let position = 0;
76
+ for (const chunk of chunks) {
77
+ wasmBytes.set(chunk, position);
78
+ position += chunk.length;
79
+ }
80
+
81
+ this.callbacks?.onProgress?.({
82
+ costTime: Date.now() - start,
83
+ phase: 'wasm',
84
+ progress: 100,
85
+ });
86
+
87
+ // 编译 WASM 模块
88
+ return WebAssembly.compile(wasmBytes);
89
+ }
90
+
91
+ // 异步加载 PGlite 相关依赖
92
+ private async loadDependencies() {
93
+ const start = Date.now();
94
+ this.callbacks?.onStateChange?.(DatabaseLoadingState.LoadingDependencies);
95
+
96
+ const imports = [
97
+ import('@electric-sql/pglite').then((m) => ({
98
+ IdbFs: m.IdbFs,
99
+ MemoryFS: m.MemoryFS,
100
+ PGlite: m.PGlite,
101
+ })),
102
+ import('@electric-sql/pglite/vector'),
103
+ import('drizzle-orm/pglite'),
104
+ ];
105
+
106
+ let loaded = 0;
107
+ const results = await Promise.all(
108
+ imports.map(async (importPromise) => {
109
+ const result = await importPromise;
110
+ loaded += 1;
111
+
112
+ // 计算加载进度
113
+ this.callbacks?.onProgress?.({
114
+ phase: 'dependencies',
115
+ progress: Math.min(Math.round((loaded / imports.length) * 100), 100),
116
+ });
117
+ return result;
118
+ }),
119
+ );
120
+
121
+ this.callbacks?.onProgress?.({
122
+ costTime: Date.now() - start,
123
+ phase: 'dependencies',
124
+ progress: 100,
125
+ });
126
+
127
+ // @ts-ignore
128
+ const [{ PGlite, IdbFs, MemoryFS }, { vector }, { drizzle }] = results;
129
+
130
+ return { IdbFs, MemoryFS, PGlite, drizzle, vector };
131
+ }
132
+
133
+ // 数据库迁移方法
134
+ private async migrate(skipMultiRun = false): Promise<DrizzleInstance> {
135
+ if (this.isLocalDBSchemaSynced && skipMultiRun) return this.db;
136
+
137
+ const cacheHash = localStorage.getItem(pgliteSchemaHashCache);
138
+ const hash = Md5.hashStr(JSON.stringify(migrations));
139
+
140
+ // if hash is the same, no need to migrate
141
+ if (hash === cacheHash) {
142
+ this.isLocalDBSchemaSynced = true;
143
+ return this.db;
144
+ }
145
+
146
+ const start = Date.now();
147
+ try {
148
+ this.callbacks?.onStateChange?.(DatabaseLoadingState.Migrating);
149
+
150
+ // refs: https://github.com/drizzle-team/drizzle-orm/discussions/2532
151
+ // @ts-expect-error
152
+ await this.db.dialect.migrate(migrations, this.db.session, {});
153
+ localStorage.setItem(pgliteSchemaHashCache, hash);
154
+ this.isLocalDBSchemaSynced = true;
155
+
156
+ console.info(`🗂 Migration success, take ${Date.now() - start}ms`);
157
+ } catch (cause) {
158
+ console.error('❌ Local database schema migration failed', cause);
159
+ throw cause;
160
+ }
161
+
162
+ return this.db;
163
+ }
164
+
165
+ // 初始化数据库
166
+ async initialize(callbacks?: DatabaseLoadingCallbacks): Promise<DrizzleInstance> {
167
+ if (this.initPromise) return this.initPromise;
168
+
169
+ this.callbacks = callbacks;
170
+
171
+ this.initPromise = (async () => {
172
+ try {
173
+ if (this.dbInstance) return this.dbInstance;
174
+
175
+ const time = Date.now();
176
+ // 初始化数据库
177
+ this.callbacks?.onStateChange?.(DatabaseLoadingState.Initializing);
178
+
179
+ // 加载依赖
180
+ const { PGlite, vector, drizzle, IdbFs, MemoryFS } = await this.loadDependencies();
181
+
182
+ // 加载并编译 WASM 模块
183
+ const wasmModule = await this.loadWasmModule();
184
+
185
+ const db = new PGlite({
186
+ extensions: { vector },
187
+ fs: typeof window === 'undefined' ? new MemoryFS('lobechat') : new IdbFs('lobechat'),
188
+ relaxedDurability: true,
189
+ wasmModule,
190
+ });
191
+
192
+ this.dbInstance = drizzle({ client: db, schema });
193
+
194
+ await this.migrate(true);
195
+
196
+ this.callbacks?.onStateChange?.(DatabaseLoadingState.Finished);
197
+ console.log(`✅ Database initialized in ${Date.now() - time}ms`);
198
+
199
+ await sleep(50);
200
+
201
+ this.callbacks?.onStateChange?.(DatabaseLoadingState.Ready);
202
+
203
+ return this.dbInstance as DrizzleInstance;
204
+ } catch (e) {
205
+ this.initPromise = null;
206
+ this.callbacks?.onStateChange?.(DatabaseLoadingState.Error);
207
+ const error = e as Error;
208
+ this.callbacks?.onError?.({
209
+ message: error.message,
210
+ name: error.name,
211
+ stack: error.stack,
212
+ });
213
+ throw error;
214
+ }
215
+ })();
216
+
217
+ return this.initPromise;
218
+ }
219
+
220
+ // 获取数据库实例
221
+ get db(): DrizzleInstance {
222
+ if (!this.dbInstance) {
223
+ throw new Error('Database not initialized. Please call initialize() first.');
224
+ }
225
+ return this.dbInstance;
226
+ }
227
+
228
+ // 创建代理对象
229
+ createProxy(): DrizzleInstance {
230
+ return new Proxy({} as DrizzleInstance, {
231
+ get: (target, prop) => {
232
+ return this.db[prop as keyof DrizzleInstance];
233
+ },
234
+ });
235
+ }
236
+ }
237
+
238
+ // 导出单例
239
+ const dbManager = DatabaseManager.getInstance();
240
+
241
+ // 保持原有的 clientDB 导出不变
242
+ export const clientDB = dbManager.createProxy();
243
+
244
+ // 导出初始化方法,供应用启动时使用
245
+ export const initializeDB = (callbacks?: DatabaseLoadingCallbacks) =>
246
+ dbManager.initialize(callbacks);