@lobehub/chat 1.77.2 → 1.77.4
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 +58 -0
- package/changelog/v1.json +18 -0
- package/docs/self-hosting/environment-variables/model-provider.mdx +1 -1
- package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +1 -1
- package/package.json +3 -3
- package/packages/electron-server-ipc/README.md +54 -0
- package/packages/electron-server-ipc/package.json +7 -0
- package/packages/electron-server-ipc/src/const.ts +5 -0
- package/packages/electron-server-ipc/src/index.ts +3 -0
- package/packages/electron-server-ipc/src/ipcClient.test.ts +211 -0
- package/packages/electron-server-ipc/src/ipcClient.ts +179 -0
- package/packages/electron-server-ipc/src/ipcServer.ts +126 -0
- package/packages/electron-server-ipc/src/types/event.ts +18 -0
- package/packages/electron-server-ipc/src/types/index.ts +1 -0
- package/scripts/migrateServerDB/index.ts +1 -1
- package/src/app/[variants]/(main)/repos/[id]/@menu/default.tsx +1 -1
- package/src/app/[variants]/(main)/repos/[id]/page.tsx +1 -1
- package/src/components/Branding/ProductLogo/index.tsx +3 -3
- package/src/database/core/db-adaptor.ts +6 -0
- package/src/database/{server/core → core}/dbForTest.ts +2 -2
- package/src/database/{server/core/db.ts → core/web-server.ts} +4 -5
- package/src/database/models/__tests__/_test_template.ts +4 -3
- package/src/database/models/__tests__/_util.ts +2 -2
- package/src/database/models/__tests__/agent.test.ts +1 -1
- package/src/database/models/__tests__/aiModel.test.ts +1 -1
- package/src/database/models/__tests__/aiProvider.test.ts +1 -1
- package/src/database/models/__tests__/asyncTask.test.ts +1 -1
- package/src/database/models/__tests__/chunk.test.ts +1 -1
- package/src/database/models/__tests__/file.test.ts +1 -1
- package/src/database/models/__tests__/knowledgeBase.test.ts +1 -1
- package/src/database/models/__tests__/message.test.ts +1 -1
- package/src/database/models/__tests__/plugin.test.ts +1 -1
- package/src/database/models/__tests__/session.test.ts +1 -2
- package/src/database/models/__tests__/sessionGroup.test.ts +1 -1
- package/src/database/models/__tests__/topic.test.ts +1 -1
- package/src/database/{server/models → models}/_template.ts +1 -1
- package/src/database/{server/models → models}/aiModel.ts +1 -1
- package/src/database/{server/models → models}/aiProvider.ts +1 -1
- package/src/database/{server/models → models}/asyncTask.ts +1 -1
- package/src/database/{server/models → models}/chunk.ts +1 -1
- package/src/database/{server/models → models}/embedding.ts +1 -1
- package/src/database/{server/models → models}/file.ts +1 -1
- package/src/database/{server/models → models}/knowledgeBase.ts +1 -1
- package/src/database/{server/models → models}/message.ts +1 -1
- package/src/database/{server/models → models}/plugin.ts +11 -4
- package/src/database/{server/models → models}/session.ts +1 -1
- package/src/database/{server/models → models}/sessionGroup.ts +1 -1
- package/src/database/{server/models → models}/thread.ts +1 -1
- package/src/database/{server/models → models}/topic.ts +1 -1
- package/src/database/{server/models → models}/user.ts +1 -1
- package/src/database/repositories/aiInfra/index.ts +2 -2
- package/src/database/repositories/dataImporter/deprecated/__tests__/index.test.ts +1 -1
- package/src/database/server/index.ts +1 -1
- package/src/database/server/models/__tests__/nextauth.test.ts +1 -1
- package/src/database/server/models/__tests__/user.test.ts +3 -3
- package/src/libs/next-auth/adapter/index.ts +1 -1
- package/src/libs/trpc/async/asyncAuth.ts +1 -1
- package/src/server/routers/async/file.ts +4 -4
- package/src/server/routers/async/ragEval.ts +3 -3
- package/src/server/routers/lambda/_template.ts +1 -1
- package/src/server/routers/lambda/agent.test.ts +10 -10
- package/src/server/routers/lambda/agent.ts +5 -5
- package/src/server/routers/lambda/aiModel.test.ts +4 -4
- package/src/server/routers/lambda/aiModel.ts +2 -2
- package/src/server/routers/lambda/aiProvider.test.ts +4 -4
- package/src/server/routers/lambda/aiProvider.ts +2 -2
- package/src/server/routers/lambda/chunk.ts +5 -5
- package/src/server/routers/lambda/file.ts +3 -3
- package/src/server/routers/lambda/knowledgeBase.ts +1 -1
- package/src/server/routers/lambda/message.ts +1 -1
- package/src/server/routers/lambda/plugin.ts +1 -1
- package/src/server/routers/lambda/ragEval.ts +1 -1
- package/src/server/routers/lambda/session.ts +2 -2
- package/src/server/routers/lambda/sessionGroup.ts +1 -1
- package/src/server/routers/lambda/thread.ts +2 -2
- package/src/server/routers/lambda/topic.ts +1 -1
- package/src/server/routers/lambda/user.test.ts +6 -6
- package/src/server/routers/lambda/user.ts +3 -3
- package/src/server/services/agent/index.test.ts +2 -2
- package/src/server/services/agent/index.ts +1 -1
- package/src/server/services/chunk/index.ts +2 -2
- package/src/server/services/nextAuthUser/index.test.ts +2 -2
- package/src/server/services/nextAuthUser/index.ts +1 -1
- package/src/server/services/user/index.test.ts +2 -2
- package/src/server/services/user/index.ts +1 -1
- package/src/services/aiModel/client.ts +1 -1
- package/src/services/aiProvider/client.ts +1 -1
- package/src/services/file/client.ts +1 -1
- package/src/services/message/client.ts +1 -1
- package/src/services/plugin/client.ts +1 -1
- package/src/services/session/client.ts +2 -2
- package/src/services/thread/client.ts +2 -2
- package/src/services/topic/client.ts +1 -1
- package/src/services/user/client.ts +3 -3
- /package/src/database/{server/models → models}/agent.ts +0 -0
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,64 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
### [Version 1.77.4](https://github.com/lobehub/lobe-chat/compare/v1.77.3...v1.77.4)
|
6
|
+
|
7
|
+
<sup>Released on **2025-03-31**</sup>
|
8
|
+
|
9
|
+
#### ♻ Code Refactoring
|
10
|
+
|
11
|
+
- **misc**: Refactor db core.
|
12
|
+
|
13
|
+
#### 💄 Styles
|
14
|
+
|
15
|
+
- **misc**: Update branding.
|
16
|
+
|
17
|
+
<br/>
|
18
|
+
|
19
|
+
<details>
|
20
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
21
|
+
|
22
|
+
#### Code refactoring
|
23
|
+
|
24
|
+
- **misc**: Refactor db core, closes [#7245](https://github.com/lobehub/lobe-chat/issues/7245) ([5c71db6](https://github.com/lobehub/lobe-chat/commit/5c71db6))
|
25
|
+
|
26
|
+
#### Styles
|
27
|
+
|
28
|
+
- **misc**: Update branding, closes [#7224](https://github.com/lobehub/lobe-chat/issues/7224) ([481cab0](https://github.com/lobehub/lobe-chat/commit/481cab0))
|
29
|
+
|
30
|
+
</details>
|
31
|
+
|
32
|
+
<div align="right">
|
33
|
+
|
34
|
+
[](#readme-top)
|
35
|
+
|
36
|
+
</div>
|
37
|
+
|
38
|
+
### [Version 1.77.3](https://github.com/lobehub/lobe-chat/compare/v1.77.2...v1.77.3)
|
39
|
+
|
40
|
+
<sup>Released on **2025-03-29**</sup>
|
41
|
+
|
42
|
+
#### ♻ Code Refactoring
|
43
|
+
|
44
|
+
- **misc**: Move general db models to database folder.
|
45
|
+
|
46
|
+
<br/>
|
47
|
+
|
48
|
+
<details>
|
49
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
50
|
+
|
51
|
+
#### Code refactoring
|
52
|
+
|
53
|
+
- **misc**: Move general db models to database folder, closes [#7222](https://github.com/lobehub/lobe-chat/issues/7222) ([f831d86](https://github.com/lobehub/lobe-chat/commit/f831d86))
|
54
|
+
|
55
|
+
</details>
|
56
|
+
|
57
|
+
<div align="right">
|
58
|
+
|
59
|
+
[](#readme-top)
|
60
|
+
|
61
|
+
</div>
|
62
|
+
|
5
63
|
### [Version 1.77.2](https://github.com/lobehub/lobe-chat/compare/v1.77.1...v1.77.2)
|
6
64
|
|
7
65
|
<sup>Released on **2025-03-29**</sup>
|
package/changelog/v1.json
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
[
|
2
|
+
{
|
3
|
+
"children": {
|
4
|
+
"improvements": [
|
5
|
+
"Update branding."
|
6
|
+
]
|
7
|
+
},
|
8
|
+
"date": "2025-03-31",
|
9
|
+
"version": "1.77.4"
|
10
|
+
},
|
11
|
+
{
|
12
|
+
"children": {
|
13
|
+
"improvements": [
|
14
|
+
"Move general db models to database folder."
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"date": "2025-03-29",
|
18
|
+
"version": "1.77.3"
|
19
|
+
},
|
2
20
|
{
|
3
21
|
"children": {
|
4
22
|
"fixes": [
|
@@ -578,7 +578,7 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
|
|
578
578
|
- Type: Optional
|
579
579
|
- Description: Used to control the model list, use `+` to add a model, use `-` to hide a model, use `model_name->deploymentName=display_name` to customize the display name of a model, separated by commas. Definition syntax rules see [model-list][model-list]
|
580
580
|
- Default: `-`
|
581
|
-
- Example: `-all,+deepseek-r1->deepseek-r1-250120,+deepseek-v3->deepseek-v3-
|
581
|
+
- Example: `-all,+deepseek-r1->deepseek-r1-250120,+deepseek-v3->deepseek-v3-250324,+doubao-1.5-pro-256k->doubao-1-5-pro-256k-250115,+doubao-1.5-pro-32k->doubao-1-5-pro-32k-250115,+doubao-1.5-lite-32k->doubao-1-5-lite-32k-250115`
|
582
582
|
|
583
583
|
### `VOLCENGINE_PROXY_URL`
|
584
584
|
|
@@ -577,7 +577,7 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
|
|
577
577
|
- 类型:可选
|
578
578
|
- 描述:用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名->部署名=展示名<扩展配置>` 来自定义模型的展示名,用英文逗号隔开。模型定义语法规则见 [模型列表][model-list]
|
579
579
|
- 默认值:`-`
|
580
|
-
- 示例:`-all,+deepseek-r1->deepseek-r1-250120,+deepseek-v3->deepseek-v3-
|
580
|
+
- 示例:`-all,+deepseek-r1->deepseek-r1-250120,+deepseek-v3->deepseek-v3-250324,+doubao-1.5-pro-256k->doubao-1-5-pro-256k-250115,+doubao-1.5-pro-32k->doubao-1-5-pro-32k-250115,+doubao-1.5-lite-32k->doubao-1-5-lite-32k-250115`
|
581
581
|
|
582
582
|
### `VOLCENGINE_PROXY_URL`
|
583
583
|
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.77.
|
3
|
+
"version": "1.77.4",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -136,7 +136,7 @@
|
|
136
136
|
"@lobehub/icons": "^1.73.1",
|
137
137
|
"@lobehub/tts": "^1.28.0",
|
138
138
|
"@lobehub/ui": "^1.169.2",
|
139
|
-
"@neondatabase/serverless": "^0.
|
139
|
+
"@neondatabase/serverless": "^1.0.0",
|
140
140
|
"@next/third-parties": "15.2.3",
|
141
141
|
"@react-spring/web": "^9.7.5",
|
142
142
|
"@sentry/nextjs": "^7.120.2",
|
@@ -182,7 +182,7 @@
|
|
182
182
|
"langfuse": "3.29.1",
|
183
183
|
"langfuse-core": "3.29.1",
|
184
184
|
"lodash-es": "^4.17.21",
|
185
|
-
"lucide-react": "^0.
|
185
|
+
"lucide-react": "^0.485.0",
|
186
186
|
"mammoth": "^1.9.0",
|
187
187
|
"mdast-util-to-markdown": "^2.1.2",
|
188
188
|
"modern-screenshot": "^4.5.5",
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# @lobechat/electron-server-ipc
|
2
|
+
|
3
|
+
LobeHub 的 Electron 应用与服务端之间的 IPC(进程间通信)模块,提供可靠的跨进程通信能力。
|
4
|
+
|
5
|
+
## 📝 简介
|
6
|
+
|
7
|
+
`@lobechat/electron-server-ipc` 是 LobeHub 桌面应用的核心组件,负责处理 Electron 进程与 nextjs 服务端之间的通信。它提供了一套简单而健壮的 API,用于在不同进程间传递数据和执行远程方法调用。
|
8
|
+
|
9
|
+
## 🛠️ 核心功能
|
10
|
+
|
11
|
+
- **可靠的 IPC 通信**: 基于 Socket 的通信机制,确保跨进程通信的稳定性和可靠性
|
12
|
+
- **自动重连机制**: 客户端具备断线重连功能,提高应用稳定性
|
13
|
+
- **类型安全**: 使用 TypeScript 提供完整的类型定义,确保 API 调用的类型安全
|
14
|
+
- **跨平台支持**: 同时支持 Windows、macOS 和 Linux 平台
|
15
|
+
|
16
|
+
## 🧩 核心组件
|
17
|
+
|
18
|
+
### IPC 服务端 (ElectronIPCServer)
|
19
|
+
|
20
|
+
负责监听客户端请求并响应,通常运行在 Electron 的主进程中:
|
21
|
+
|
22
|
+
```typescript
|
23
|
+
import { ElectronIPCEventHandler, ElectronIPCServer } from '@lobechat/electron-server-ipc';
|
24
|
+
|
25
|
+
// 定义处理函数
|
26
|
+
const eventHandler: ElectronIPCEventHandler = {
|
27
|
+
getDatabasePath: async () => {
|
28
|
+
return '/path/to/database';
|
29
|
+
},
|
30
|
+
// 其他处理函数...
|
31
|
+
};
|
32
|
+
|
33
|
+
// 创建并启动服务器
|
34
|
+
const server = new ElectronIPCServer(eventHandler);
|
35
|
+
server.start();
|
36
|
+
```
|
37
|
+
|
38
|
+
### IPC 客户端 (ElectronIpcClient)
|
39
|
+
|
40
|
+
负责连接到服务端并发送请求,通常在服务端(如 Next.js 服务)中使用:
|
41
|
+
|
42
|
+
```typescript
|
43
|
+
import { ElectronIPCMethods, ElectronIpcClient } from '@lobechat/electron-server-ipc';
|
44
|
+
|
45
|
+
// 创建客户端
|
46
|
+
const client = new ElectronIpcClient();
|
47
|
+
|
48
|
+
// 发送请求
|
49
|
+
const dbPath = await client.sendRequest(ElectronIPCMethods.getDatabasePath);
|
50
|
+
```
|
51
|
+
|
52
|
+
## 📌 说明
|
53
|
+
|
54
|
+
这是 LobeHub 的内部模块 (`"private": true`),专为 LobeHub 桌面应用设计,不作为独立包发布。
|
@@ -0,0 +1,211 @@
|
|
1
|
+
import fs from 'node:fs';
|
2
|
+
import net from 'node:net';
|
3
|
+
import os from 'node:os';
|
4
|
+
import path from 'node:path';
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
6
|
+
|
7
|
+
import { ElectronIpcClient } from './ipcClient';
|
8
|
+
import { ElectronIPCMethods } from './types';
|
9
|
+
|
10
|
+
// Mock node modules
|
11
|
+
vi.mock('node:fs');
|
12
|
+
vi.mock('node:net');
|
13
|
+
vi.mock('node:os');
|
14
|
+
vi.mock('node:path');
|
15
|
+
|
16
|
+
describe('ElectronIpcClient', () => {
|
17
|
+
// Mock data
|
18
|
+
const mockTempDir = '/mock/temp/dir';
|
19
|
+
const mockSocketInfoPath = '/mock/temp/dir/lobehub-electron-ipc-info.json';
|
20
|
+
const mockSocketInfo = { socketPath: '/mock/socket/path' };
|
21
|
+
|
22
|
+
// Mock socket
|
23
|
+
const mockSocket = {
|
24
|
+
on: vi.fn(),
|
25
|
+
write: vi.fn(),
|
26
|
+
end: vi.fn(),
|
27
|
+
};
|
28
|
+
|
29
|
+
beforeEach(() => {
|
30
|
+
// Use fake timers
|
31
|
+
vi.useFakeTimers();
|
32
|
+
|
33
|
+
// Reset all mocks
|
34
|
+
vi.resetAllMocks();
|
35
|
+
|
36
|
+
// Setup common mocks
|
37
|
+
vi.mocked(os.tmpdir).mockReturnValue(mockTempDir);
|
38
|
+
vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
|
39
|
+
vi.mocked(net.createConnection).mockReturnValue(mockSocket as unknown as net.Socket);
|
40
|
+
|
41
|
+
// Mock console methods
|
42
|
+
vi.spyOn(console, 'error').mockImplementation(() => {});
|
43
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
44
|
+
});
|
45
|
+
|
46
|
+
afterEach(() => {
|
47
|
+
vi.restoreAllMocks();
|
48
|
+
vi.useRealTimers();
|
49
|
+
});
|
50
|
+
|
51
|
+
describe('initialization', () => {
|
52
|
+
it('should initialize with socket path from info file if it exists', () => {
|
53
|
+
// Setup
|
54
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
55
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSocketInfo));
|
56
|
+
|
57
|
+
// Execute
|
58
|
+
new ElectronIpcClient();
|
59
|
+
|
60
|
+
// Verify
|
61
|
+
expect(fs.existsSync).toHaveBeenCalledWith(mockSocketInfoPath);
|
62
|
+
expect(fs.readFileSync).toHaveBeenCalledWith(mockSocketInfoPath, 'utf8');
|
63
|
+
});
|
64
|
+
|
65
|
+
it('should initialize with default socket path if info file does not exist', () => {
|
66
|
+
// Setup
|
67
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
68
|
+
|
69
|
+
// Execute
|
70
|
+
new ElectronIpcClient();
|
71
|
+
|
72
|
+
// Verify
|
73
|
+
expect(fs.existsSync).toHaveBeenCalledWith(mockSocketInfoPath);
|
74
|
+
expect(fs.readFileSync).not.toHaveBeenCalled();
|
75
|
+
|
76
|
+
// Test platform-specific behavior
|
77
|
+
const originalPlatform = process.platform;
|
78
|
+
Object.defineProperty(process, 'platform', { value: 'win32' });
|
79
|
+
new ElectronIpcClient();
|
80
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
81
|
+
});
|
82
|
+
|
83
|
+
it('should handle initialization errors gracefully', () => {
|
84
|
+
// Setup - Mock the error
|
85
|
+
vi.mocked(fs.existsSync).mockImplementation(() => {
|
86
|
+
throw new Error('Mock file system error');
|
87
|
+
});
|
88
|
+
|
89
|
+
// Execute
|
90
|
+
new ElectronIpcClient();
|
91
|
+
|
92
|
+
// Verify
|
93
|
+
expect(console.error).toHaveBeenCalledWith(
|
94
|
+
'Failed to initialize IPC client:',
|
95
|
+
expect.objectContaining({ message: 'Mock file system error' }),
|
96
|
+
);
|
97
|
+
});
|
98
|
+
});
|
99
|
+
|
100
|
+
describe('connection and request handling', () => {
|
101
|
+
let client: ElectronIpcClient;
|
102
|
+
|
103
|
+
beforeEach(() => {
|
104
|
+
// Setup a client with a known socket path
|
105
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
106
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSocketInfo));
|
107
|
+
client = new ElectronIpcClient();
|
108
|
+
|
109
|
+
// Reset socket mocks for each test
|
110
|
+
mockSocket.on.mockReset();
|
111
|
+
mockSocket.write.mockReset();
|
112
|
+
|
113
|
+
// Default implementation for socket.on
|
114
|
+
mockSocket.on.mockImplementation((event, callback) => {
|
115
|
+
return mockSocket;
|
116
|
+
});
|
117
|
+
|
118
|
+
// Default implementation for socket.write
|
119
|
+
mockSocket.write.mockImplementation((data, callback) => {
|
120
|
+
if (callback) callback();
|
121
|
+
return true;
|
122
|
+
});
|
123
|
+
});
|
124
|
+
|
125
|
+
it('should handle connection errors', async () => {
|
126
|
+
// Start request - but don't await it yet
|
127
|
+
const requestPromise = client.sendRequest(ElectronIPCMethods.getDatabasePath);
|
128
|
+
|
129
|
+
// Find the error event handler
|
130
|
+
const errorCallArgs = mockSocket.on.mock.calls.find((call) => call[0] === 'error');
|
131
|
+
if (errorCallArgs && typeof errorCallArgs[1] === 'function') {
|
132
|
+
const errorHandler = errorCallArgs[1];
|
133
|
+
|
134
|
+
// Trigger the error handler
|
135
|
+
errorHandler(new Error('Connection error'));
|
136
|
+
}
|
137
|
+
|
138
|
+
// Now await the promise
|
139
|
+
await expect(requestPromise).rejects.toThrow('Connection error');
|
140
|
+
});
|
141
|
+
|
142
|
+
it('should handle write errors', async () => {
|
143
|
+
// Setup connection callback
|
144
|
+
let connectionCallback: Function | undefined;
|
145
|
+
vi.mocked(net.createConnection).mockImplementation((path, callback) => {
|
146
|
+
connectionCallback = callback as Function;
|
147
|
+
return mockSocket as unknown as net.Socket;
|
148
|
+
});
|
149
|
+
|
150
|
+
// Setup write to fail
|
151
|
+
mockSocket.write.mockImplementation((data, callback) => {
|
152
|
+
if (callback) callback(new Error('Write error'));
|
153
|
+
return true;
|
154
|
+
});
|
155
|
+
|
156
|
+
// Start request
|
157
|
+
const requestPromise = client.sendRequest(ElectronIPCMethods.getDatabasePath);
|
158
|
+
|
159
|
+
// Simulate connection established
|
160
|
+
if (connectionCallback) connectionCallback();
|
161
|
+
|
162
|
+
// Now await the promise
|
163
|
+
await expect(requestPromise).rejects.toThrow('Write error');
|
164
|
+
});
|
165
|
+
});
|
166
|
+
|
167
|
+
describe('close method', () => {
|
168
|
+
let client: ElectronIpcClient;
|
169
|
+
|
170
|
+
beforeEach(() => {
|
171
|
+
// Setup a client with a known socket path
|
172
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
173
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSocketInfo));
|
174
|
+
client = new ElectronIpcClient();
|
175
|
+
|
176
|
+
// Setup socket.on
|
177
|
+
mockSocket.on.mockImplementation((event, callback) => {
|
178
|
+
return mockSocket;
|
179
|
+
});
|
180
|
+
});
|
181
|
+
|
182
|
+
it('should close the socket connection', async () => {
|
183
|
+
// Setup connection callback
|
184
|
+
let connectionCallback: Function | undefined;
|
185
|
+
vi.mocked(net.createConnection).mockImplementation((path, callback) => {
|
186
|
+
connectionCallback = callback as Function;
|
187
|
+
return mockSocket as unknown as net.Socket;
|
188
|
+
});
|
189
|
+
|
190
|
+
// Start a request to establish connection (but don't wait for it)
|
191
|
+
const requestPromise = client.sendRequest(ElectronIPCMethods.getDatabasePath).catch(() => {}); // Ignore any errors
|
192
|
+
|
193
|
+
// Simulate connection
|
194
|
+
if (connectionCallback) connectionCallback();
|
195
|
+
|
196
|
+
// Close the connection
|
197
|
+
client.close();
|
198
|
+
|
199
|
+
// Verify
|
200
|
+
expect(mockSocket.end).toHaveBeenCalled();
|
201
|
+
});
|
202
|
+
|
203
|
+
it('should handle close when not connected', () => {
|
204
|
+
// Close without connecting
|
205
|
+
client.close();
|
206
|
+
|
207
|
+
// Verify no errors
|
208
|
+
expect(mockSocket.end).not.toHaveBeenCalled();
|
209
|
+
});
|
210
|
+
});
|
211
|
+
});
|
@@ -0,0 +1,179 @@
|
|
1
|
+
import fs from 'node:fs';
|
2
|
+
import net from 'node:net';
|
3
|
+
import os from 'node:os';
|
4
|
+
import path from 'node:path';
|
5
|
+
|
6
|
+
import { SOCK_FILE, SOCK_INFO_FILE, WINDOW_PIPE_FILE } from './const';
|
7
|
+
import { IElectronIPCMethods } from './types';
|
8
|
+
|
9
|
+
export class ElectronIpcClient {
|
10
|
+
private socketPath: string | null = null;
|
11
|
+
private connected: boolean = false;
|
12
|
+
private socket: net.Socket | null = null;
|
13
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
14
|
+
private requestQueue: Map<string, { reject: Function; resolve: Function }> = new Map();
|
15
|
+
// eslint-disable-next-line no-undef
|
16
|
+
private reconnectTimeout: NodeJS.Timeout | null = null;
|
17
|
+
private connectionAttempts: number = 0;
|
18
|
+
private maxConnectionAttempts: number = 5;
|
19
|
+
|
20
|
+
constructor() {
|
21
|
+
this.initialize();
|
22
|
+
}
|
23
|
+
|
24
|
+
// 初始化客户端
|
25
|
+
private initialize() {
|
26
|
+
try {
|
27
|
+
// 从临时文件读取套接字路径
|
28
|
+
const tempDir = os.tmpdir();
|
29
|
+
const socketInfoPath = path.join(tempDir, SOCK_INFO_FILE);
|
30
|
+
|
31
|
+
if (fs.existsSync(socketInfoPath)) {
|
32
|
+
const socketInfo = JSON.parse(fs.readFileSync(socketInfoPath, 'utf8'));
|
33
|
+
this.socketPath = socketInfo.socketPath;
|
34
|
+
} else {
|
35
|
+
// 如果找不到套接字信息,使用默认路径
|
36
|
+
this.socketPath =
|
37
|
+
process.platform === 'win32' ? WINDOW_PIPE_FILE : path.join(os.tmpdir(), SOCK_FILE);
|
38
|
+
}
|
39
|
+
} catch (err) {
|
40
|
+
console.error('Failed to initialize IPC client:', err);
|
41
|
+
this.socketPath = null;
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
// 连接到 Electron IPC 服务器
|
46
|
+
private connect(): Promise<void> {
|
47
|
+
if (this.connected || !this.socketPath) {
|
48
|
+
return Promise.resolve();
|
49
|
+
}
|
50
|
+
|
51
|
+
return new Promise((resolve, reject) => {
|
52
|
+
try {
|
53
|
+
this.socket = net.createConnection(this.socketPath!, () => {
|
54
|
+
this.connected = true;
|
55
|
+
this.connectionAttempts = 0;
|
56
|
+
console.log('Connected to Electron IPC server');
|
57
|
+
resolve();
|
58
|
+
});
|
59
|
+
|
60
|
+
this.socket.on('data', (data) => {
|
61
|
+
try {
|
62
|
+
const response = JSON.parse(data.toString());
|
63
|
+
const { id, result, error } = response;
|
64
|
+
|
65
|
+
const pending = this.requestQueue.get(id);
|
66
|
+
if (pending) {
|
67
|
+
this.requestQueue.delete(id);
|
68
|
+
if (error) {
|
69
|
+
pending.reject(new Error(error));
|
70
|
+
} else {
|
71
|
+
pending.resolve(result);
|
72
|
+
}
|
73
|
+
}
|
74
|
+
} catch (err) {
|
75
|
+
console.error('Failed to parse response:', err);
|
76
|
+
}
|
77
|
+
});
|
78
|
+
|
79
|
+
this.socket.on('error', (err) => {
|
80
|
+
console.error('Socket error:', err);
|
81
|
+
this.connected = false;
|
82
|
+
this.handleDisconnect();
|
83
|
+
reject(err);
|
84
|
+
});
|
85
|
+
|
86
|
+
this.socket.on('close', () => {
|
87
|
+
console.log('Socket closed');
|
88
|
+
this.connected = false;
|
89
|
+
this.handleDisconnect();
|
90
|
+
});
|
91
|
+
} catch (err) {
|
92
|
+
console.error('Failed to connect to IPC server:', err);
|
93
|
+
this.handleDisconnect();
|
94
|
+
reject(err);
|
95
|
+
}
|
96
|
+
});
|
97
|
+
}
|
98
|
+
|
99
|
+
// 处理断开连接
|
100
|
+
private handleDisconnect() {
|
101
|
+
// 清除重连定时器
|
102
|
+
if (this.reconnectTimeout) {
|
103
|
+
clearTimeout(this.reconnectTimeout);
|
104
|
+
this.reconnectTimeout = null;
|
105
|
+
}
|
106
|
+
|
107
|
+
// 拒绝所有待处理的请求
|
108
|
+
for (const [, { reject }] of this.requestQueue) {
|
109
|
+
reject(new Error('Connection to Electron IPC server lost'));
|
110
|
+
}
|
111
|
+
this.requestQueue.clear();
|
112
|
+
|
113
|
+
// 尝试重新连接
|
114
|
+
if (this.connectionAttempts < this.maxConnectionAttempts) {
|
115
|
+
this.connectionAttempts++;
|
116
|
+
const delay = Math.min(1000 * Math.pow(2, this.connectionAttempts - 1), 30_000);
|
117
|
+
|
118
|
+
this.reconnectTimeout = setTimeout(() => {
|
119
|
+
this.connect().catch((err) => {
|
120
|
+
console.error(`Reconnection attempt ${this.connectionAttempts} failed:`, err);
|
121
|
+
});
|
122
|
+
}, delay);
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
// 发送请求到 Electron IPC 服务器
|
127
|
+
public async sendRequest<T>(method: IElectronIPCMethods, params: any = {}): Promise<T> {
|
128
|
+
if (!this.socketPath) {
|
129
|
+
throw new Error('Electron IPC connection not available');
|
130
|
+
}
|
131
|
+
|
132
|
+
// 如果未连接,先连接
|
133
|
+
if (!this.connected) {
|
134
|
+
await this.connect();
|
135
|
+
}
|
136
|
+
|
137
|
+
return new Promise<T>((resolve, reject) => {
|
138
|
+
try {
|
139
|
+
const id = Math.random().toString(36).slice(2, 15);
|
140
|
+
const request = { id, method, params };
|
141
|
+
|
142
|
+
// 将请求添加到队列
|
143
|
+
this.requestQueue.set(id, { reject, resolve });
|
144
|
+
|
145
|
+
// 设置超时
|
146
|
+
const timeout = setTimeout(() => {
|
147
|
+
this.requestQueue.delete(id);
|
148
|
+
reject(new Error(`Request ${method} timed out`));
|
149
|
+
}, 10_000);
|
150
|
+
|
151
|
+
// 发送请求
|
152
|
+
this.socket!.write(JSON.stringify(request), (err) => {
|
153
|
+
if (err) {
|
154
|
+
clearTimeout(timeout);
|
155
|
+
this.requestQueue.delete(id);
|
156
|
+
reject(err);
|
157
|
+
}
|
158
|
+
});
|
159
|
+
} catch (err) {
|
160
|
+
reject(err);
|
161
|
+
}
|
162
|
+
});
|
163
|
+
}
|
164
|
+
|
165
|
+
// 关闭连接
|
166
|
+
public close() {
|
167
|
+
if (this.reconnectTimeout) {
|
168
|
+
clearTimeout(this.reconnectTimeout);
|
169
|
+
this.reconnectTimeout = null;
|
170
|
+
}
|
171
|
+
|
172
|
+
if (this.socket) {
|
173
|
+
this.socket.end();
|
174
|
+
this.socket = null;
|
175
|
+
}
|
176
|
+
|
177
|
+
this.connected = false;
|
178
|
+
}
|
179
|
+
}
|