@lobehub/lobehub 2.0.0-next.194 → 2.0.0-next.196
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/setting.json +0 -3
- package/locales/bg-BG/setting.json +0 -3
- package/locales/de-DE/setting.json +0 -3
- package/locales/en-US/setting.json +0 -3
- package/locales/es-ES/setting.json +0 -3
- package/locales/fa-IR/setting.json +0 -3
- package/locales/fr-FR/setting.json +0 -3
- package/locales/it-IT/setting.json +0 -3
- package/locales/ja-JP/setting.json +0 -3
- package/locales/ko-KR/setting.json +0 -3
- package/locales/nl-NL/setting.json +0 -3
- package/locales/pl-PL/setting.json +0 -3
- package/locales/pt-BR/setting.json +0 -3
- package/locales/ru-RU/setting.json +0 -3
- package/locales/tr-TR/setting.json +0 -3
- package/locales/vi-VN/setting.json +0 -3
- package/locales/zh-CN/setting.json +0 -3
- package/locales/zh-TW/setting.json +0 -3
- package/package.json +1 -1
- package/packages/const/src/fetch.ts +1 -4
- package/packages/database/src/models/user.ts +8 -0
- package/packages/database/src/repositories/aiInfra/index.test.ts +11 -8
- package/packages/database/src/repositories/dataExporter/index.test.ts +11 -9
- package/packages/database/src/repositories/tableViewer/index.test.ts +13 -14
- package/packages/model-runtime/src/providers/zhipu/index.ts +6 -6
- package/packages/types/src/auth.ts +0 -4
- package/packages/utils/src/server/xor.test.ts +1 -2
- package/src/app/(backend)/_deprecated/createBizOpenAI/auth.test.ts +7 -41
- package/src/app/(backend)/_deprecated/createBizOpenAI/auth.ts +1 -15
- package/src/app/(backend)/_deprecated/createBizOpenAI/index.ts +2 -9
- package/src/app/(backend)/middleware/auth/index.ts +0 -1
- package/src/app/(backend)/middleware/auth/utils.test.ts +2 -42
- package/src/app/(backend)/middleware/auth/utils.ts +3 -17
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +0 -5
- package/src/app/(backend)/webapi/models/[provider]/route.test.ts +0 -6
- package/src/app/(backend)/webapi/plugin/gateway/route.ts +2 -32
- package/src/app/[variants]/(main)/settings/common/features/Common/Common.tsx +1 -16
- package/src/envs/app.ts +2 -0
- package/src/libs/trpc/lambda/middleware/index.ts +1 -0
- package/src/libs/trpc/lambda/middleware/telemetry.test.ts +237 -0
- package/src/libs/trpc/lambda/middleware/telemetry.ts +74 -0
- package/src/locales/default/setting.ts +0 -3
- package/src/server/routers/lambda/market/index.ts +1 -93
- package/src/server/routers/tools/_helpers/index.ts +1 -0
- package/src/server/routers/tools/_helpers/scheduleToolCallReport.ts +113 -0
- package/src/server/routers/tools/index.ts +2 -2
- package/src/server/routers/tools/market.ts +375 -0
- package/src/server/routers/tools/mcp.ts +77 -20
- package/src/services/chat/index.ts +0 -2
- package/src/services/codeInterpreter.ts +6 -6
- package/src/services/mcp.test.ts +60 -46
- package/src/services/mcp.ts +67 -48
- package/src/store/chat/slices/plugin/action.test.ts +191 -0
- package/src/store/chat/slices/plugin/actions/internals.ts +2 -18
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +31 -44
- package/packages/database/src/client/db.test.ts +0 -52
- package/packages/database/src/client/db.ts +0 -195
- package/packages/database/src/client/type.ts +0 -6
- package/src/server/routers/tools/codeInterpreter.ts +0 -255
|
@@ -1429,4 +1429,195 @@ describe('ChatPluginAction', () => {
|
|
|
1429
1429
|
});
|
|
1430
1430
|
});
|
|
1431
1431
|
});
|
|
1432
|
+
|
|
1433
|
+
describe('Plugin invoke functions use optimisticUpdateToolMessage', () => {
|
|
1434
|
+
const messageId = 'message-id';
|
|
1435
|
+
const payload: ChatToolPayload = {
|
|
1436
|
+
apiName: 'test-api',
|
|
1437
|
+
arguments: '{}',
|
|
1438
|
+
id: 'tool-call-id',
|
|
1439
|
+
identifier: 'test-plugin',
|
|
1440
|
+
type: 'default',
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
describe('invokeMCPTypePlugin', () => {
|
|
1444
|
+
it('should use optimisticUpdateToolMessage for successful result', async () => {
|
|
1445
|
+
const mockResult = {
|
|
1446
|
+
content: 'mcp result content',
|
|
1447
|
+
state: { content: [], isError: false },
|
|
1448
|
+
success: true,
|
|
1449
|
+
};
|
|
1450
|
+
|
|
1451
|
+
// Mock the mcpService
|
|
1452
|
+
const mcpService = await import('@/services/mcp');
|
|
1453
|
+
vi.spyOn(mcpService.mcpService, 'invokeMcpToolCall').mockResolvedValue(mockResult);
|
|
1454
|
+
|
|
1455
|
+
const optimisticUpdateToolMessageMock = vi.fn().mockResolvedValue(undefined);
|
|
1456
|
+
|
|
1457
|
+
act(() => {
|
|
1458
|
+
useChatStore.setState({
|
|
1459
|
+
activeAgentId: 'session-id',
|
|
1460
|
+
messagesMap: { [messageMapKey({ agentId: 'session-id' })]: [] },
|
|
1461
|
+
optimisticUpdateToolMessage: optimisticUpdateToolMessageMock,
|
|
1462
|
+
replaceMessages: vi.fn(),
|
|
1463
|
+
messageOperationMap: {},
|
|
1464
|
+
operations: {},
|
|
1465
|
+
});
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
const { result } = renderHook(() => useChatStore());
|
|
1469
|
+
|
|
1470
|
+
await act(async () => {
|
|
1471
|
+
await result.current.invokeMCPTypePlugin(messageId, payload);
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
expect(optimisticUpdateToolMessageMock).toHaveBeenCalledWith(
|
|
1475
|
+
messageId,
|
|
1476
|
+
{
|
|
1477
|
+
content: mockResult.content,
|
|
1478
|
+
pluginError: undefined,
|
|
1479
|
+
pluginState: mockResult.state,
|
|
1480
|
+
},
|
|
1481
|
+
undefined,
|
|
1482
|
+
);
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
it('should use optimisticUpdateToolMessage for error result', async () => {
|
|
1486
|
+
const mockResult = {
|
|
1487
|
+
content: 'error content',
|
|
1488
|
+
error: { message: 'test error' },
|
|
1489
|
+
state: { content: [], isError: true },
|
|
1490
|
+
success: false,
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
const mcpService = await import('@/services/mcp');
|
|
1494
|
+
vi.spyOn(mcpService.mcpService, 'invokeMcpToolCall').mockResolvedValue(mockResult);
|
|
1495
|
+
|
|
1496
|
+
const optimisticUpdateToolMessageMock = vi.fn().mockResolvedValue(undefined);
|
|
1497
|
+
|
|
1498
|
+
act(() => {
|
|
1499
|
+
useChatStore.setState({
|
|
1500
|
+
activeAgentId: 'session-id',
|
|
1501
|
+
messagesMap: { [messageMapKey({ agentId: 'session-id' })]: [] },
|
|
1502
|
+
optimisticUpdateToolMessage: optimisticUpdateToolMessageMock,
|
|
1503
|
+
replaceMessages: vi.fn(),
|
|
1504
|
+
messageOperationMap: {},
|
|
1505
|
+
operations: {},
|
|
1506
|
+
});
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
const { result } = renderHook(() => useChatStore());
|
|
1510
|
+
|
|
1511
|
+
await act(async () => {
|
|
1512
|
+
await result.current.invokeMCPTypePlugin(messageId, payload);
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
expect(optimisticUpdateToolMessageMock).toHaveBeenCalledWith(
|
|
1516
|
+
messageId,
|
|
1517
|
+
{
|
|
1518
|
+
content: mockResult.content,
|
|
1519
|
+
pluginError: mockResult.error,
|
|
1520
|
+
pluginState: undefined,
|
|
1521
|
+
},
|
|
1522
|
+
undefined,
|
|
1523
|
+
);
|
|
1524
|
+
});
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
describe('invokeKlavisTypePlugin', () => {
|
|
1528
|
+
it('should use optimisticUpdateToolMessage for successful result', async () => {
|
|
1529
|
+
const mockResult = {
|
|
1530
|
+
content: 'klavis result content',
|
|
1531
|
+
state: { data: 'test-data' },
|
|
1532
|
+
success: true,
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
// Mock useToolStore to return a server
|
|
1536
|
+
vi.spyOn(useToolStore, 'getState').mockReturnValue({
|
|
1537
|
+
servers: [{ identifier: 'test-plugin', serverUrl: 'http://test.com' }],
|
|
1538
|
+
callKlavisTool: vi.fn().mockResolvedValue({
|
|
1539
|
+
success: true,
|
|
1540
|
+
data: mockResult,
|
|
1541
|
+
}),
|
|
1542
|
+
} as any);
|
|
1543
|
+
|
|
1544
|
+
const optimisticUpdateToolMessageMock = vi.fn().mockResolvedValue(undefined);
|
|
1545
|
+
|
|
1546
|
+
act(() => {
|
|
1547
|
+
useChatStore.setState({
|
|
1548
|
+
activeAgentId: 'session-id',
|
|
1549
|
+
messagesMap: { [messageMapKey({ agentId: 'session-id' })]: [] },
|
|
1550
|
+
optimisticUpdateToolMessage: optimisticUpdateToolMessageMock,
|
|
1551
|
+
replaceMessages: vi.fn(),
|
|
1552
|
+
messageOperationMap: {},
|
|
1553
|
+
operations: {},
|
|
1554
|
+
});
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
const { result } = renderHook(() => useChatStore());
|
|
1558
|
+
|
|
1559
|
+
await act(async () => {
|
|
1560
|
+
await result.current.invokeKlavisTypePlugin(messageId, payload);
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
expect(optimisticUpdateToolMessageMock).toHaveBeenCalledWith(
|
|
1564
|
+
messageId,
|
|
1565
|
+
{
|
|
1566
|
+
content: mockResult.content,
|
|
1567
|
+
pluginError: undefined,
|
|
1568
|
+
pluginState: mockResult.state,
|
|
1569
|
+
},
|
|
1570
|
+
undefined,
|
|
1571
|
+
);
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
describe('invokeCloudCodeInterpreterTool', () => {
|
|
1576
|
+
it('should use optimisticUpdateToolMessage for successful result', async () => {
|
|
1577
|
+
const mockResult = {
|
|
1578
|
+
content: 'code interpreter result',
|
|
1579
|
+
state: { output: 'test output' },
|
|
1580
|
+
success: true,
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
// Mock CloudSandboxExecutionRuntime using doMock for dynamic mocking
|
|
1584
|
+
vi.doMock('@lobechat/builtin-tool-cloud-sandbox/executionRuntime', () => ({
|
|
1585
|
+
CloudSandboxExecutionRuntime: class {
|
|
1586
|
+
'test-api' = vi.fn().mockResolvedValue(mockResult);
|
|
1587
|
+
},
|
|
1588
|
+
}));
|
|
1589
|
+
|
|
1590
|
+
const optimisticUpdateToolMessageMock = vi.fn().mockResolvedValue(undefined);
|
|
1591
|
+
|
|
1592
|
+
act(() => {
|
|
1593
|
+
useChatStore.setState({
|
|
1594
|
+
activeAgentId: 'session-id',
|
|
1595
|
+
messagesMap: { [messageMapKey({ agentId: 'session-id' })]: [] },
|
|
1596
|
+
optimisticUpdateToolMessage: optimisticUpdateToolMessageMock,
|
|
1597
|
+
replaceMessages: vi.fn(),
|
|
1598
|
+
messageOperationMap: {},
|
|
1599
|
+
operations: {},
|
|
1600
|
+
});
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
const { result } = renderHook(() => useChatStore());
|
|
1604
|
+
|
|
1605
|
+
await act(async () => {
|
|
1606
|
+
await result.current.invokeCloudCodeInterpreterTool(messageId, payload);
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
expect(optimisticUpdateToolMessageMock).toHaveBeenCalledWith(
|
|
1610
|
+
messageId,
|
|
1611
|
+
{
|
|
1612
|
+
content: mockResult.content,
|
|
1613
|
+
pluginError: undefined,
|
|
1614
|
+
pluginState: mockResult.state,
|
|
1615
|
+
},
|
|
1616
|
+
undefined,
|
|
1617
|
+
);
|
|
1618
|
+
|
|
1619
|
+
vi.doUnmock('@lobechat/builtin-tool-cloud-sandbox/executionRuntime');
|
|
1620
|
+
});
|
|
1621
|
+
});
|
|
1622
|
+
});
|
|
1432
1623
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
|
|
2
2
|
import { ToolNameResolver } from '@lobechat/context-engine';
|
|
3
|
-
import { type ChatToolPayload, type MessageToolCall
|
|
3
|
+
import { type ChatToolPayload, type MessageToolCall } from '@lobechat/types';
|
|
4
4
|
import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
|
5
5
|
import { type StateCreator } from 'zustand/vanilla';
|
|
6
6
|
|
|
@@ -9,8 +9,6 @@ import { useToolStore } from '@/store/tool';
|
|
|
9
9
|
import { klavisStoreSelectors, pluginSelectors } from '@/store/tool/selectors';
|
|
10
10
|
import { builtinTools } from '@/tools';
|
|
11
11
|
|
|
12
|
-
import { displayMessageSelectors } from '../../message/selectors';
|
|
13
|
-
|
|
14
12
|
/**
|
|
15
13
|
* Internal utility methods and runtime state management
|
|
16
14
|
* These are building blocks used by other actions
|
|
@@ -20,11 +18,6 @@ export interface PluginInternalsAction {
|
|
|
20
18
|
* Transform tool calls from runtime format to storage format
|
|
21
19
|
*/
|
|
22
20
|
internal_transformToolCalls: (toolCalls: MessageToolCall[]) => ChatToolPayload[];
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Construct tools calling context for plugin invocation
|
|
26
|
-
*/
|
|
27
|
-
internal_constructToolsCallingContext: (id: string) => ToolsCallingContext | undefined;
|
|
28
21
|
}
|
|
29
22
|
|
|
30
23
|
export const pluginInternals: StateCreator<
|
|
@@ -32,7 +25,7 @@ export const pluginInternals: StateCreator<
|
|
|
32
25
|
[['zustand/devtools', never]],
|
|
33
26
|
[],
|
|
34
27
|
PluginInternalsAction
|
|
35
|
-
> = (
|
|
28
|
+
> = () => ({
|
|
36
29
|
internal_transformToolCalls: (toolCalls) => {
|
|
37
30
|
const toolNameResolver = new ToolNameResolver();
|
|
38
31
|
|
|
@@ -77,13 +70,4 @@ export const pluginInternals: StateCreator<
|
|
|
77
70
|
source: sourceMap[payload.identifier],
|
|
78
71
|
}));
|
|
79
72
|
},
|
|
80
|
-
|
|
81
|
-
internal_constructToolsCallingContext: (id: string) => {
|
|
82
|
-
const message = displayMessageSelectors.getDisplayMessageById(id)(get());
|
|
83
|
-
if (!message) return;
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
topicId: message.topicId,
|
|
87
|
-
};
|
|
88
|
-
},
|
|
89
73
|
});
|
|
@@ -261,12 +261,6 @@ export const pluginTypes: StateCreator<
|
|
|
261
261
|
},
|
|
262
262
|
|
|
263
263
|
invokeCloudCodeInterpreterTool: async (id, payload) => {
|
|
264
|
-
const {
|
|
265
|
-
optimisticUpdateMessageContent,
|
|
266
|
-
optimisticUpdatePluginState,
|
|
267
|
-
optimisticUpdateMessagePluginError,
|
|
268
|
-
} = get();
|
|
269
|
-
|
|
270
264
|
// Get message to extract topicId
|
|
271
265
|
const message = dbMessageSelectors.getDbMessageById(id)(get());
|
|
272
266
|
|
|
@@ -336,16 +330,16 @@ export const pluginTypes: StateCreator<
|
|
|
336
330
|
|
|
337
331
|
const context = operationId ? { operationId } : undefined;
|
|
338
332
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
333
|
+
// Use optimisticUpdateToolMessage to update content and state/error in a single call
|
|
334
|
+
await get().optimisticUpdateToolMessage(
|
|
335
|
+
id,
|
|
336
|
+
{
|
|
337
|
+
content: data.content,
|
|
338
|
+
pluginError: data.success ? undefined : data.error,
|
|
339
|
+
pluginState: data.success ? data.state : undefined,
|
|
340
|
+
},
|
|
341
|
+
context,
|
|
342
|
+
);
|
|
349
343
|
|
|
350
344
|
// Handle exportFile: save exported file and associate with assistant message (parent)
|
|
351
345
|
if (payload.apiName === 'exportFile' && data.success && data.state) {
|
|
@@ -422,12 +416,6 @@ export const pluginTypes: StateCreator<
|
|
|
422
416
|
},
|
|
423
417
|
|
|
424
418
|
invokeKlavisTypePlugin: async (id, payload) => {
|
|
425
|
-
const {
|
|
426
|
-
optimisticUpdateMessageContent,
|
|
427
|
-
optimisticUpdatePluginState,
|
|
428
|
-
optimisticUpdateMessagePluginError,
|
|
429
|
-
} = get();
|
|
430
|
-
|
|
431
419
|
let data: MCPToolCallResult | undefined;
|
|
432
420
|
|
|
433
421
|
// Get message to extract sessionId/topicId
|
|
@@ -510,13 +498,16 @@ export const pluginTypes: StateCreator<
|
|
|
510
498
|
// operationId already declared above, reuse it
|
|
511
499
|
const context = operationId ? { operationId } : undefined;
|
|
512
500
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
501
|
+
// Use optimisticUpdateToolMessage to update content and state/error in a single call
|
|
502
|
+
await get().optimisticUpdateToolMessage(
|
|
503
|
+
id,
|
|
504
|
+
{
|
|
505
|
+
content: data.content,
|
|
506
|
+
pluginError: data.success ? undefined : data.error,
|
|
507
|
+
pluginState: data.success ? data.state : undefined,
|
|
508
|
+
},
|
|
509
|
+
context,
|
|
510
|
+
);
|
|
520
511
|
|
|
521
512
|
return data.content;
|
|
522
513
|
},
|
|
@@ -561,12 +552,6 @@ export const pluginTypes: StateCreator<
|
|
|
561
552
|
},
|
|
562
553
|
|
|
563
554
|
invokeMCPTypePlugin: async (id, payload) => {
|
|
564
|
-
const {
|
|
565
|
-
optimisticUpdateMessageContent,
|
|
566
|
-
internal_constructToolsCallingContext,
|
|
567
|
-
optimisticUpdatePluginState,
|
|
568
|
-
optimisticUpdateMessagePluginError,
|
|
569
|
-
} = get();
|
|
570
555
|
let data: MCPToolCallResult | undefined;
|
|
571
556
|
|
|
572
557
|
// Get message to extract agentId/topicId
|
|
@@ -586,10 +571,9 @@ export const pluginTypes: StateCreator<
|
|
|
586
571
|
);
|
|
587
572
|
|
|
588
573
|
try {
|
|
589
|
-
const context = internal_constructToolsCallingContext(id);
|
|
590
574
|
const result = await mcpService.invokeMcpToolCall(payload, {
|
|
591
575
|
signal: abortController?.signal,
|
|
592
|
-
topicId:
|
|
576
|
+
topicId: message?.topicId,
|
|
593
577
|
});
|
|
594
578
|
|
|
595
579
|
if (!!result) data = result;
|
|
@@ -620,13 +604,16 @@ export const pluginTypes: StateCreator<
|
|
|
620
604
|
// operationId already declared above, reuse it
|
|
621
605
|
const context = operationId ? { operationId } : undefined;
|
|
622
606
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
607
|
+
// Use optimisticUpdateToolMessage to update content and state/error in a single call
|
|
608
|
+
await get().optimisticUpdateToolMessage(
|
|
609
|
+
id,
|
|
610
|
+
{
|
|
611
|
+
content: data.content,
|
|
612
|
+
pluginError: data.success ? undefined : data.error,
|
|
613
|
+
pluginState: data.success ? data.state : undefined,
|
|
614
|
+
},
|
|
615
|
+
context,
|
|
616
|
+
);
|
|
630
617
|
|
|
631
618
|
return data.content;
|
|
632
619
|
},
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { PGlite } from '@electric-sql/pglite';
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
vi.mock('@electric-sql/pglite', () => ({
|
|
5
|
-
PGlite: vi.fn(() => ({})),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
vi.mock('@electric-sql/pglite/vector', () => ({
|
|
9
|
-
vector: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
vi.mock('drizzle-orm/pglite', () => ({
|
|
13
|
-
drizzle: vi.fn(() => ({
|
|
14
|
-
dialect: {
|
|
15
|
-
migrate: vi.fn().mockResolvedValue(undefined),
|
|
16
|
-
},
|
|
17
|
-
})),
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
vi.clearAllMocks();
|
|
22
|
-
vi.resetModules();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe('DatabaseManager', () => {
|
|
26
|
-
describe('initializeDB', () => {
|
|
27
|
-
it('should initialize database with PGlite', async () => {
|
|
28
|
-
const { initializeDB } = await import('./db');
|
|
29
|
-
await initializeDB();
|
|
30
|
-
|
|
31
|
-
expect(PGlite).toHaveBeenCalledWith('idb://lobechat', {
|
|
32
|
-
extensions: { vector: expect.any(Function) },
|
|
33
|
-
relaxedDurability: true,
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should only initialize once when called multiple times', async () => {
|
|
38
|
-
const { initializeDB } = await import('./db');
|
|
39
|
-
await Promise.all([initializeDB(), initializeDB()]);
|
|
40
|
-
|
|
41
|
-
expect(PGlite).toHaveBeenCalledTimes(1);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe('clientDB proxy', () => {
|
|
46
|
-
it('should provide access to database after initialization', async () => {
|
|
47
|
-
const { clientDB, initializeDB } = await import('./db');
|
|
48
|
-
await initializeDB();
|
|
49
|
-
expect(clientDB).toBeDefined();
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
});
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { PGlite } from '@electric-sql/pglite';
|
|
2
|
-
import { vector } from '@electric-sql/pglite/vector';
|
|
3
|
-
import { sql } from 'drizzle-orm';
|
|
4
|
-
import { PgliteDatabase, drizzle } from 'drizzle-orm/pglite';
|
|
5
|
-
import { Md5 } from 'ts-md5';
|
|
6
|
-
|
|
7
|
-
import migrations from '../core/migrations.json';
|
|
8
|
-
import { DrizzleMigrationModel } from '../models/drizzleMigration';
|
|
9
|
-
import * as schema from '../schemas';
|
|
10
|
-
|
|
11
|
-
const pgliteSchemaHashCache = 'LOBE_CHAT_PGLITE_SCHEMA_HASH';
|
|
12
|
-
const DB_NAME = 'lobechat';
|
|
13
|
-
|
|
14
|
-
type DrizzleInstance = PgliteDatabase<typeof schema>;
|
|
15
|
-
|
|
16
|
-
class DatabaseManager {
|
|
17
|
-
private static instance: DatabaseManager;
|
|
18
|
-
private dbInstance: DrizzleInstance | null = null;
|
|
19
|
-
private initPromise: Promise<DrizzleInstance> | null = null;
|
|
20
|
-
private isLocalDBSchemaSynced = false;
|
|
21
|
-
|
|
22
|
-
private constructor() {}
|
|
23
|
-
|
|
24
|
-
static getInstance() {
|
|
25
|
-
if (!DatabaseManager.instance) {
|
|
26
|
-
DatabaseManager.instance = new DatabaseManager();
|
|
27
|
-
}
|
|
28
|
-
return DatabaseManager.instance;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
private async migrate(): Promise<DrizzleInstance> {
|
|
32
|
-
if (this.isLocalDBSchemaSynced) return this.db;
|
|
33
|
-
|
|
34
|
-
let hash: string | undefined;
|
|
35
|
-
if (typeof localStorage !== 'undefined') {
|
|
36
|
-
const cacheHash = localStorage.getItem(pgliteSchemaHashCache);
|
|
37
|
-
hash = Md5.hashStr(JSON.stringify(migrations));
|
|
38
|
-
// if hash is the same, no need to migrate
|
|
39
|
-
if (hash === cacheHash) {
|
|
40
|
-
try {
|
|
41
|
-
const drizzleMigration = new DrizzleMigrationModel(this.db as any);
|
|
42
|
-
|
|
43
|
-
// Check if tables exist in database
|
|
44
|
-
const tableCount = await drizzleMigration.getTableCounts();
|
|
45
|
-
|
|
46
|
-
// If table count > 0, consider database properly initialized
|
|
47
|
-
if (tableCount > 0) {
|
|
48
|
-
this.isLocalDBSchemaSynced = true;
|
|
49
|
-
return this.db;
|
|
50
|
-
}
|
|
51
|
-
} catch (error) {
|
|
52
|
-
console.warn('Error checking table existence, proceeding with migration', error);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const start = Date.now();
|
|
58
|
-
try {
|
|
59
|
-
// @ts-expect-error - migrate internal API
|
|
60
|
-
await this.db.dialect.migrate(migrations, this.db.session, {});
|
|
61
|
-
|
|
62
|
-
if (typeof localStorage !== 'undefined' && hash) {
|
|
63
|
-
localStorage.setItem(pgliteSchemaHashCache, hash);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
this.isLocalDBSchemaSynced = true;
|
|
67
|
-
console.info(`🗂 Migration success, take ${Date.now() - start}ms`);
|
|
68
|
-
} catch (cause) {
|
|
69
|
-
console.error('❌ Local database schema migration failed', cause);
|
|
70
|
-
throw cause;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return this.db;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async initialize(): Promise<DrizzleInstance> {
|
|
77
|
-
if (this.initPromise) return this.initPromise;
|
|
78
|
-
|
|
79
|
-
this.initPromise = (async () => {
|
|
80
|
-
if (this.dbInstance) return this.dbInstance;
|
|
81
|
-
|
|
82
|
-
const time = Date.now();
|
|
83
|
-
|
|
84
|
-
// 直接使用 pglite,自动处理 wasm 加载
|
|
85
|
-
const pglite = new PGlite(`idb://${DB_NAME}`, {
|
|
86
|
-
extensions: { vector },
|
|
87
|
-
relaxedDurability: true,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
this.dbInstance = drizzle({ client: pglite, schema });
|
|
91
|
-
|
|
92
|
-
await this.migrate();
|
|
93
|
-
|
|
94
|
-
console.log(`✅ Database initialized in ${Date.now() - time}ms`);
|
|
95
|
-
|
|
96
|
-
return this.dbInstance;
|
|
97
|
-
})();
|
|
98
|
-
|
|
99
|
-
return this.initPromise;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
get db(): DrizzleInstance {
|
|
103
|
-
if (!this.dbInstance) {
|
|
104
|
-
throw new Error('Database not initialized. Please call initialize() first.');
|
|
105
|
-
}
|
|
106
|
-
return this.dbInstance;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
createProxy(): DrizzleInstance {
|
|
110
|
-
return new Proxy({} as DrizzleInstance, {
|
|
111
|
-
get: (target, prop) => {
|
|
112
|
-
return this.db[prop as keyof DrizzleInstance];
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async resetDatabase(): Promise<void> {
|
|
118
|
-
// 1. Close existing PGlite connection
|
|
119
|
-
if (this.dbInstance) {
|
|
120
|
-
try {
|
|
121
|
-
// @ts-ignore
|
|
122
|
-
await (this.dbInstance.session as any).client.close();
|
|
123
|
-
console.log('PGlite instance closed successfully.');
|
|
124
|
-
} catch (e) {
|
|
125
|
-
console.error('Error closing PGlite instance:', e);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// 2. Reset database instance and initialization state
|
|
130
|
-
this.dbInstance = null;
|
|
131
|
-
this.initPromise = null;
|
|
132
|
-
this.isLocalDBSchemaSynced = false;
|
|
133
|
-
|
|
134
|
-
// 3. Delete IndexedDB database
|
|
135
|
-
return new Promise<void>((resolve, reject) => {
|
|
136
|
-
if (typeof indexedDB === 'undefined') {
|
|
137
|
-
console.warn('IndexedDB is not available, cannot delete database');
|
|
138
|
-
resolve();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const dbName = `/pglite/${DB_NAME}`;
|
|
143
|
-
const request = indexedDB.deleteDatabase(dbName);
|
|
144
|
-
|
|
145
|
-
request.onsuccess = () => {
|
|
146
|
-
console.log(`✅ Database '${dbName}' reset successfully`);
|
|
147
|
-
|
|
148
|
-
if (typeof localStorage !== 'undefined') {
|
|
149
|
-
localStorage.removeItem(pgliteSchemaHashCache);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
resolve();
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
|
156
|
-
request.onerror = (event) => {
|
|
157
|
-
const error = (event.target as IDBOpenDBRequest)?.error;
|
|
158
|
-
console.error(`❌ Error resetting database '${dbName}':`, error);
|
|
159
|
-
reject(
|
|
160
|
-
new Error(
|
|
161
|
-
`Failed to reset database '${dbName}'. Error: ${error?.message || 'Unknown error'}`,
|
|
162
|
-
),
|
|
163
|
-
);
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
request.onblocked = (event) => {
|
|
167
|
-
console.warn(`Deletion of database '${dbName}' is blocked.`, event);
|
|
168
|
-
reject(
|
|
169
|
-
new Error(
|
|
170
|
-
`Failed to reset database '${dbName}' because it is blocked by other open connections.`,
|
|
171
|
-
),
|
|
172
|
-
);
|
|
173
|
-
};
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Export singleton
|
|
179
|
-
const dbManager = DatabaseManager.getInstance();
|
|
180
|
-
|
|
181
|
-
export const clientDB = dbManager.createProxy();
|
|
182
|
-
|
|
183
|
-
export const initializeDB = () => dbManager.initialize();
|
|
184
|
-
|
|
185
|
-
export const resetClientDatabase = async () => {
|
|
186
|
-
await dbManager.resetDatabase();
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
export const updateMigrationRecord = async (migrationHash: string) => {
|
|
190
|
-
await clientDB.execute(
|
|
191
|
-
sql`INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at") VALUES (${migrationHash}, ${Date.now()});`,
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
await initializeDB();
|
|
195
|
-
};
|