@lobehub/lobehub 2.0.0-next.242 → 2.0.0-next.243
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/.cursor/rules/desktop-feature-implementation.mdc +2 -2
- package/.cursor/rules/desktop-local-tools-implement.mdc +2 -2
- package/CHANGELOG.md +27 -0
- package/apps/desktop/Development.md +1 -6
- package/apps/desktop/README.md +2 -17
- package/apps/desktop/README.zh-CN.md +1 -15
- package/apps/desktop/src/main/controllers/index.ts +1 -1
- package/apps/desktop/src/main/controllers/registry.ts +0 -9
- package/apps/desktop/src/main/core/App.ts +1 -11
- package/apps/desktop/src/main/core/browser/Browser.ts +278 -457
- package/apps/desktop/src/main/core/browser/WindowStateManager.ts +180 -0
- package/apps/desktop/src/main/core/browser/WindowThemeManager.ts +167 -0
- package/apps/desktop/src/main/core/browser/__tests__/WindowStateManager.test.ts +237 -0
- package/apps/desktop/src/main/core/browser/__tests__/WindowThemeManager.test.ts +240 -0
- package/apps/desktop/src/main/exports.d.ts +1 -1
- package/apps/desktop/src/main/exports.ts +1 -1
- package/apps/desktop/src/main/utils/__tests__/http-headers.test.ts +131 -0
- package/apps/desktop/src/main/utils/http-headers.ts +61 -0
- package/apps/desktop/src/main/utils/ipc/__tests__/base.test.ts +1 -22
- package/apps/desktop/src/main/utils/ipc/base.ts +0 -20
- package/apps/desktop/src/main/utils/ipc/index.ts +1 -9
- package/changelog/v1.json +5 -0
- package/package.json +1 -1
- package/src/features/ChatInput/InputEditor/Placeholder.tsx +4 -1
- package/apps/desktop/src/main/controllers/UploadFileServerCtr.ts +0 -33
- package/apps/desktop/src/main/controllers/__tests__/UploadFileServerCtr.test.ts +0 -55
- package/src/server/modules/ElectronIPCClient/index.ts +0 -92
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { WindowThemeManager } from '../WindowThemeManager';
|
|
4
|
+
|
|
5
|
+
// Use vi.hoisted to define mocks before hoisting
|
|
6
|
+
const { mockNativeTheme, mockBrowserWindow } = vi.hoisted(() => ({
|
|
7
|
+
mockBrowserWindow: {
|
|
8
|
+
isDestroyed: vi.fn().mockReturnValue(false),
|
|
9
|
+
setBackgroundColor: vi.fn(),
|
|
10
|
+
setTitleBarOverlay: vi.fn(),
|
|
11
|
+
},
|
|
12
|
+
mockNativeTheme: {
|
|
13
|
+
off: vi.fn(),
|
|
14
|
+
on: vi.fn(),
|
|
15
|
+
shouldUseDarkColors: false,
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('electron', () => ({
|
|
20
|
+
nativeTheme: mockNativeTheme,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock('@/utils/logger', () => ({
|
|
24
|
+
createLogger: () => ({
|
|
25
|
+
debug: vi.fn(),
|
|
26
|
+
error: vi.fn(),
|
|
27
|
+
info: vi.fn(),
|
|
28
|
+
warn: vi.fn(),
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('@/const/dir', () => ({
|
|
33
|
+
buildDir: '/mock/build',
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock('@/const/env', () => ({
|
|
37
|
+
isDev: false,
|
|
38
|
+
isWindows: true,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
vi.mock('@/const/theme', () => ({
|
|
42
|
+
BACKGROUND_DARK: '#1a1a1a',
|
|
43
|
+
BACKGROUND_LIGHT: '#ffffff',
|
|
44
|
+
SYMBOL_COLOR_DARK: '#ffffff',
|
|
45
|
+
SYMBOL_COLOR_LIGHT: '#000000',
|
|
46
|
+
THEME_CHANGE_DELAY: 0,
|
|
47
|
+
TITLE_BAR_HEIGHT: 32,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
describe('WindowThemeManager', () => {
|
|
51
|
+
let manager: WindowThemeManager;
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
vi.useFakeTimers();
|
|
56
|
+
|
|
57
|
+
mockNativeTheme.shouldUseDarkColors = false;
|
|
58
|
+
mockBrowserWindow.isDestroyed.mockReturnValue(false);
|
|
59
|
+
|
|
60
|
+
manager = new WindowThemeManager('test-window');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
vi.useRealTimers();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('isDarkMode', () => {
|
|
68
|
+
it('should return true when shouldUseDarkColors is true', () => {
|
|
69
|
+
mockNativeTheme.shouldUseDarkColors = true;
|
|
70
|
+
|
|
71
|
+
expect(manager.isDarkMode).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return false when shouldUseDarkColors is false', () => {
|
|
75
|
+
mockNativeTheme.shouldUseDarkColors = false;
|
|
76
|
+
|
|
77
|
+
expect(manager.isDarkMode).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('getPlatformConfig', () => {
|
|
82
|
+
it('should return Windows dark theme config when in dark mode', () => {
|
|
83
|
+
mockNativeTheme.shouldUseDarkColors = true;
|
|
84
|
+
|
|
85
|
+
const config = manager.getPlatformConfig();
|
|
86
|
+
|
|
87
|
+
expect(config).toEqual({
|
|
88
|
+
backgroundColor: '#1a1a1a',
|
|
89
|
+
icon: undefined,
|
|
90
|
+
titleBarOverlay: {
|
|
91
|
+
color: '#1a1a1a',
|
|
92
|
+
height: 32,
|
|
93
|
+
symbolColor: '#ffffff',
|
|
94
|
+
},
|
|
95
|
+
titleBarStyle: 'hidden',
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should return Windows light theme config when in light mode', () => {
|
|
100
|
+
mockNativeTheme.shouldUseDarkColors = false;
|
|
101
|
+
|
|
102
|
+
const config = manager.getPlatformConfig();
|
|
103
|
+
|
|
104
|
+
expect(config).toEqual({
|
|
105
|
+
backgroundColor: '#ffffff',
|
|
106
|
+
icon: undefined,
|
|
107
|
+
titleBarOverlay: {
|
|
108
|
+
color: '#ffffff',
|
|
109
|
+
height: 32,
|
|
110
|
+
symbolColor: '#000000',
|
|
111
|
+
},
|
|
112
|
+
titleBarStyle: 'hidden',
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('attach', () => {
|
|
118
|
+
it('should setup theme listener', () => {
|
|
119
|
+
manager.attach(mockBrowserWindow as any);
|
|
120
|
+
|
|
121
|
+
expect(mockNativeTheme.on).toHaveBeenCalledWith('updated', expect.any(Function));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should apply initial visual effects', () => {
|
|
125
|
+
manager.attach(mockBrowserWindow as any);
|
|
126
|
+
|
|
127
|
+
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalled();
|
|
128
|
+
expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should not setup duplicate listeners', () => {
|
|
132
|
+
manager.attach(mockBrowserWindow as any);
|
|
133
|
+
manager.attach(mockBrowserWindow as any);
|
|
134
|
+
|
|
135
|
+
expect(mockNativeTheme.on).toHaveBeenCalledTimes(1);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('cleanup', () => {
|
|
140
|
+
it('should remove theme listener', () => {
|
|
141
|
+
manager.attach(mockBrowserWindow as any);
|
|
142
|
+
manager.cleanup();
|
|
143
|
+
|
|
144
|
+
expect(mockNativeTheme.off).toHaveBeenCalledWith('updated', expect.any(Function));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should not throw if cleanup called without attach', () => {
|
|
148
|
+
expect(() => manager.cleanup()).not.toThrow();
|
|
149
|
+
expect(mockNativeTheme.off).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('handleAppThemeChange', () => {
|
|
154
|
+
it('should reapply visual effects after delay', () => {
|
|
155
|
+
manager.attach(mockBrowserWindow as any);
|
|
156
|
+
mockBrowserWindow.setBackgroundColor.mockClear();
|
|
157
|
+
mockBrowserWindow.setTitleBarOverlay.mockClear();
|
|
158
|
+
|
|
159
|
+
manager.handleAppThemeChange();
|
|
160
|
+
vi.advanceTimersByTime(0);
|
|
161
|
+
|
|
162
|
+
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalled();
|
|
163
|
+
expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('reapplyVisualEffects', () => {
|
|
168
|
+
it('should apply visual effects', () => {
|
|
169
|
+
manager.attach(mockBrowserWindow as any);
|
|
170
|
+
mockBrowserWindow.setBackgroundColor.mockClear();
|
|
171
|
+
|
|
172
|
+
manager.reapplyVisualEffects();
|
|
173
|
+
|
|
174
|
+
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('applyVisualEffects', () => {
|
|
179
|
+
it('should apply dark theme when in dark mode', () => {
|
|
180
|
+
mockNativeTheme.shouldUseDarkColors = true;
|
|
181
|
+
manager.attach(mockBrowserWindow as any);
|
|
182
|
+
|
|
183
|
+
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#1a1a1a');
|
|
184
|
+
expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalledWith({
|
|
185
|
+
color: '#1a1a1a',
|
|
186
|
+
height: 32,
|
|
187
|
+
symbolColor: '#ffffff',
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should apply light theme when in light mode', () => {
|
|
192
|
+
mockNativeTheme.shouldUseDarkColors = false;
|
|
193
|
+
manager.attach(mockBrowserWindow as any);
|
|
194
|
+
|
|
195
|
+
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalledWith('#ffffff');
|
|
196
|
+
expect(mockBrowserWindow.setTitleBarOverlay).toHaveBeenCalledWith({
|
|
197
|
+
color: '#ffffff',
|
|
198
|
+
height: 32,
|
|
199
|
+
symbolColor: '#000000',
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should not apply effects when window is destroyed', () => {
|
|
204
|
+
manager.attach(mockBrowserWindow as any);
|
|
205
|
+
mockBrowserWindow.setBackgroundColor.mockClear();
|
|
206
|
+
mockBrowserWindow.isDestroyed.mockReturnValue(true);
|
|
207
|
+
|
|
208
|
+
manager.reapplyVisualEffects();
|
|
209
|
+
|
|
210
|
+
expect(mockBrowserWindow.setBackgroundColor).not.toHaveBeenCalled();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should not apply effects when no window attached', () => {
|
|
214
|
+
// Manager without attached window
|
|
215
|
+
const freshManager = new WindowThemeManager('fresh-window');
|
|
216
|
+
|
|
217
|
+
// Should not throw
|
|
218
|
+
expect(() => freshManager.reapplyVisualEffects()).not.toThrow();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('theme change listener', () => {
|
|
223
|
+
it('should reapply visual effects on system theme change', () => {
|
|
224
|
+
manager.attach(mockBrowserWindow as any);
|
|
225
|
+
|
|
226
|
+
// Get the theme change handler
|
|
227
|
+
const themeHandler = mockNativeTheme.on.mock.calls.find((call) => call[0] === 'updated')?.[1];
|
|
228
|
+
|
|
229
|
+
expect(themeHandler).toBeDefined();
|
|
230
|
+
|
|
231
|
+
mockBrowserWindow.setBackgroundColor.mockClear();
|
|
232
|
+
|
|
233
|
+
// Simulate theme change
|
|
234
|
+
themeHandler();
|
|
235
|
+
vi.advanceTimersByTime(0);
|
|
236
|
+
|
|
237
|
+
expect(mockBrowserWindow.setBackgroundColor).toHaveBeenCalled();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -5,4 +5,4 @@ declare module '@lobechat/electron-client-ipc' {
|
|
|
5
5
|
interface DesktopIpcServicesMap extends DesktopIpcServices {}
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export { type DesktopIpcServices
|
|
8
|
+
export { type DesktopIpcServices } from './controllers/registry';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Export types for renderer/server to use
|
|
2
|
-
export type { DesktopIpcServices
|
|
2
|
+
export type { DesktopIpcServices } from './controllers/registry';
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
deleteResponseHeader,
|
|
5
|
+
getResponseHeader,
|
|
6
|
+
hasResponseHeader,
|
|
7
|
+
setResponseHeader,
|
|
8
|
+
} from '../http-headers';
|
|
9
|
+
|
|
10
|
+
describe('http-headers utilities', () => {
|
|
11
|
+
describe('setResponseHeader', () => {
|
|
12
|
+
it('should set a new header', () => {
|
|
13
|
+
const headers: Record<string, string[]> = {};
|
|
14
|
+
|
|
15
|
+
setResponseHeader(headers, 'Content-Type', 'application/json');
|
|
16
|
+
|
|
17
|
+
expect(headers['Content-Type']).toEqual(['application/json']);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should replace existing header with same case', () => {
|
|
21
|
+
const headers: Record<string, string[]> = {
|
|
22
|
+
'Content-Type': ['text/html'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
setResponseHeader(headers, 'Content-Type', 'application/json');
|
|
26
|
+
|
|
27
|
+
expect(headers['Content-Type']).toEqual(['application/json']);
|
|
28
|
+
expect(Object.keys(headers)).toHaveLength(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should replace existing header with different case', () => {
|
|
32
|
+
const headers: Record<string, string[]> = {
|
|
33
|
+
'content-type': ['text/html'],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
setResponseHeader(headers, 'Content-Type', 'application/json');
|
|
37
|
+
|
|
38
|
+
expect(headers['Content-Type']).toEqual(['application/json']);
|
|
39
|
+
expect(headers['content-type']).toBeUndefined();
|
|
40
|
+
expect(Object.keys(headers)).toHaveLength(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should handle array values', () => {
|
|
44
|
+
const headers: Record<string, string[]> = {};
|
|
45
|
+
|
|
46
|
+
setResponseHeader(headers, 'Set-Cookie', ['a=1', 'b=2']);
|
|
47
|
+
|
|
48
|
+
expect(headers['Set-Cookie']).toEqual(['a=1', 'b=2']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should replace multiple headers with different cases', () => {
|
|
52
|
+
const headers: Record<string, string[]> = {
|
|
53
|
+
'ACCESS-CONTROL-ALLOW-ORIGIN': ['*'],
|
|
54
|
+
'access-control-allow-origin': ['http://localhost'],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
setResponseHeader(headers, 'Access-Control-Allow-Origin', 'http://example.com');
|
|
58
|
+
|
|
59
|
+
expect(headers['Access-Control-Allow-Origin']).toEqual(['http://example.com']);
|
|
60
|
+
expect(Object.keys(headers)).toHaveLength(1);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('hasResponseHeader', () => {
|
|
65
|
+
it('should return true for existing header', () => {
|
|
66
|
+
const headers = { 'Content-Type': ['application/json'] };
|
|
67
|
+
|
|
68
|
+
expect(hasResponseHeader(headers, 'Content-Type')).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should return true for existing header with different case', () => {
|
|
72
|
+
const headers = { 'content-type': ['application/json'] };
|
|
73
|
+
|
|
74
|
+
expect(hasResponseHeader(headers, 'Content-Type')).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return false for non-existing header', () => {
|
|
78
|
+
const headers = { 'Content-Type': ['application/json'] };
|
|
79
|
+
|
|
80
|
+
expect(hasResponseHeader(headers, 'Authorization')).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('getResponseHeader', () => {
|
|
85
|
+
it('should get header value', () => {
|
|
86
|
+
const headers = { 'Content-Type': ['application/json'] };
|
|
87
|
+
|
|
88
|
+
expect(getResponseHeader(headers, 'Content-Type')).toEqual(['application/json']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should get header value with different case', () => {
|
|
92
|
+
const headers = { 'content-type': ['application/json'] };
|
|
93
|
+
|
|
94
|
+
expect(getResponseHeader(headers, 'Content-Type')).toEqual(['application/json']);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should return undefined for non-existing header', () => {
|
|
98
|
+
const headers = { 'Content-Type': ['application/json'] };
|
|
99
|
+
|
|
100
|
+
expect(getResponseHeader(headers, 'Authorization')).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('deleteResponseHeader', () => {
|
|
105
|
+
it('should delete existing header', () => {
|
|
106
|
+
const headers: Record<string, string[]> = { 'Content-Type': ['application/json'] };
|
|
107
|
+
|
|
108
|
+
const result = deleteResponseHeader(headers, 'Content-Type');
|
|
109
|
+
|
|
110
|
+
expect(result).toBe(true);
|
|
111
|
+
expect(headers['Content-Type']).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should delete header with different case', () => {
|
|
115
|
+
const headers: Record<string, string[]> = { 'content-type': ['application/json'] };
|
|
116
|
+
|
|
117
|
+
const result = deleteResponseHeader(headers, 'Content-Type');
|
|
118
|
+
|
|
119
|
+
expect(result).toBe(true);
|
|
120
|
+
expect(headers['content-type']).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should return false for non-existing header', () => {
|
|
124
|
+
const headers: Record<string, string[]> = { 'Content-Type': ['application/json'] };
|
|
125
|
+
|
|
126
|
+
const result = deleteResponseHeader(headers, 'Authorization');
|
|
127
|
+
|
|
128
|
+
expect(result).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP headers utilities for Electron webRequest
|
|
3
|
+
*
|
|
4
|
+
* Electron's webRequest responseHeaders is a plain JS object where keys are case-sensitive,
|
|
5
|
+
* but HTTP headers are case-insensitive per spec. These utilities handle this mismatch.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type ElectronResponseHeaders = Record<string, string[]>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Set a header value, replacing any existing header with the same name (case-insensitive)
|
|
12
|
+
*/
|
|
13
|
+
export function setResponseHeader(
|
|
14
|
+
headers: ElectronResponseHeaders,
|
|
15
|
+
name: string,
|
|
16
|
+
value: string | string[],
|
|
17
|
+
): void {
|
|
18
|
+
// Delete any existing header with same name (case-insensitive)
|
|
19
|
+
for (const key of Object.keys(headers)) {
|
|
20
|
+
if (key.toLowerCase() === name.toLowerCase()) {
|
|
21
|
+
delete headers[key];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
headers[name] = Array.isArray(value) ? value : [value];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if a header exists (case-insensitive)
|
|
29
|
+
*/
|
|
30
|
+
export function hasResponseHeader(headers: ElectronResponseHeaders, name: string): boolean {
|
|
31
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get a header value (case-insensitive)
|
|
36
|
+
*/
|
|
37
|
+
export function getResponseHeader(
|
|
38
|
+
headers: ElectronResponseHeaders,
|
|
39
|
+
name: string,
|
|
40
|
+
): string[] | undefined {
|
|
41
|
+
for (const key of Object.keys(headers)) {
|
|
42
|
+
if (key.toLowerCase() === name.toLowerCase()) {
|
|
43
|
+
return headers[key];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Delete a header (case-insensitive)
|
|
51
|
+
*/
|
|
52
|
+
export function deleteResponseHeader(headers: ElectronResponseHeaders, name: string): boolean {
|
|
53
|
+
let deleted = false;
|
|
54
|
+
for (const key of Object.keys(headers)) {
|
|
55
|
+
if (key.toLowerCase() === name.toLowerCase()) {
|
|
56
|
+
delete headers[key];
|
|
57
|
+
deleted = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return deleted;
|
|
61
|
+
}
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import type { IpcContext } from '../base';
|
|
4
|
-
import {
|
|
5
|
-
IpcMethod,
|
|
6
|
-
IpcServerMethod,
|
|
7
|
-
IpcService,
|
|
8
|
-
getIpcContext,
|
|
9
|
-
getServerMethodMetadata,
|
|
10
|
-
} from '../base';
|
|
4
|
+
import { IpcMethod, IpcService, getIpcContext } from '../base';
|
|
11
5
|
|
|
12
6
|
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
|
13
7
|
ipcMainHandleMock: vi.fn(),
|
|
@@ -73,19 +67,4 @@ describe('ipc service base', () => {
|
|
|
73
67
|
expect(service.invokedWith).toBe('test');
|
|
74
68
|
expect(ipcMainHandleMock).toHaveBeenCalledWith('direct.run', expect.any(Function));
|
|
75
69
|
});
|
|
76
|
-
|
|
77
|
-
it('collects server method metadata for decorators', () => {
|
|
78
|
-
class ServerService extends IpcService {
|
|
79
|
-
static readonly groupName = 'server';
|
|
80
|
-
|
|
81
|
-
@IpcServerMethod()
|
|
82
|
-
fetch(_: string) {
|
|
83
|
-
return 'ok';
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const metadata = getServerMethodMetadata(ServerService);
|
|
88
|
-
expect(metadata).toBeDefined();
|
|
89
|
-
expect(metadata?.get('fetch')).toBe('fetch');
|
|
90
|
-
});
|
|
91
70
|
});
|
|
@@ -10,7 +10,6 @@ export interface IpcContext {
|
|
|
10
10
|
|
|
11
11
|
// Metadata storage for decorated methods
|
|
12
12
|
const methodMetadata = new WeakMap<any, Map<string, string>>();
|
|
13
|
-
const serverMethodMetadata = new WeakMap<any, Map<string, string>>();
|
|
14
13
|
const ipcContextStorage = new AsyncLocalStorage<IpcContext>();
|
|
15
14
|
|
|
16
15
|
// Decorator for IPC methods
|
|
@@ -29,21 +28,6 @@ export function IpcMethod() {
|
|
|
29
28
|
};
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
export function IpcServerMethod(channelName?: string) {
|
|
33
|
-
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
34
|
-
const { constructor } = target;
|
|
35
|
-
|
|
36
|
-
if (!serverMethodMetadata.has(constructor)) {
|
|
37
|
-
serverMethodMetadata.set(constructor, new Map());
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const methods = serverMethodMetadata.get(constructor)!;
|
|
41
|
-
methods.set(propertyKey, channelName || propertyKey);
|
|
42
|
-
|
|
43
|
-
return descriptor;
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
31
|
// Handler registry for IPC methods
|
|
48
32
|
export class IpcHandler {
|
|
49
33
|
private static instance: IpcHandler;
|
|
@@ -157,10 +141,6 @@ export type CreateServicesResult<T extends readonly IpcServiceConstructor[]> = {
|
|
|
157
141
|
[K in T[number] as K['groupName']]: InstanceType<K>;
|
|
158
142
|
};
|
|
159
143
|
|
|
160
|
-
export function getServerMethodMetadata(target: IpcServiceConstructor) {
|
|
161
|
-
return serverMethodMetadata.get(target);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
144
|
export function getIpcContext() {
|
|
165
145
|
return ipcContextStorage.getStore();
|
|
166
146
|
}
|
|
@@ -1,11 +1,3 @@
|
|
|
1
1
|
export type { CreateServicesResult, IpcContext, IpcServiceConstructor } from './base';
|
|
2
|
-
export {
|
|
3
|
-
createServices,
|
|
4
|
-
getIpcContext,
|
|
5
|
-
getServerMethodMetadata,
|
|
6
|
-
IpcMethod,
|
|
7
|
-
IpcServerMethod,
|
|
8
|
-
IpcService,
|
|
9
|
-
runWithIpcContext,
|
|
10
|
-
} from './base';
|
|
2
|
+
export { createServices, getIpcContext, IpcMethod, IpcService, runWithIpcContext } from './base';
|
|
11
3
|
export type { ExtractServiceMethods, MergeIpcService } from './utility';
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.243",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent 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",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { KeyEnum } from '@lobechat/types';
|
|
2
2
|
import { Flexbox, Hotkey, combineKeys } from '@lobehub/ui';
|
|
3
3
|
import { memo } from 'react';
|
|
4
|
-
import { Trans } from 'react-i18next';
|
|
4
|
+
import { Trans, useTranslation } from 'react-i18next';
|
|
5
5
|
|
|
6
6
|
import { useUserStore } from '@/store/user';
|
|
7
7
|
import { preferenceSelectors } from '@/store/user/selectors';
|
|
@@ -12,6 +12,9 @@ const Placeholder = memo(() => {
|
|
|
12
12
|
? KeyEnum.Enter
|
|
13
13
|
: combineKeys([KeyEnum.Mod, KeyEnum.Enter]);
|
|
14
14
|
|
|
15
|
+
// Don't remove this line for i18n reactivity
|
|
16
|
+
void useTranslation('chat');
|
|
17
|
+
|
|
15
18
|
return (
|
|
16
19
|
<Flexbox align={'center'} as={'span'} gap={4} horizontal wrap={'wrap'}>
|
|
17
20
|
<Trans
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { CreateFileParams } from '@lobechat/electron-server-ipc';
|
|
2
|
-
|
|
3
|
-
import FileService from '@/services/fileSrv';
|
|
4
|
-
|
|
5
|
-
import { ControllerModule, IpcServerMethod } from './index';
|
|
6
|
-
|
|
7
|
-
export default class UploadFileServerCtr extends ControllerModule {
|
|
8
|
-
static override readonly groupName = 'upload';
|
|
9
|
-
|
|
10
|
-
private get fileService() {
|
|
11
|
-
return this.app.getService(FileService);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
@IpcServerMethod()
|
|
15
|
-
async getFileUrlById(id: string) {
|
|
16
|
-
return this.fileService.getFilePath(id);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
@IpcServerMethod()
|
|
20
|
-
async getFileHTTPURL(path: string) {
|
|
21
|
-
return this.fileService.getFileHTTPURL(path);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
@IpcServerMethod()
|
|
25
|
-
async deleteFiles(paths: string[]) {
|
|
26
|
-
return this.fileService.deleteFiles(paths);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
@IpcServerMethod()
|
|
30
|
-
async createFile(params: CreateFileParams) {
|
|
31
|
-
return this.fileService.uploadFile(params);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import type { App } from '@/core/App';
|
|
4
|
-
|
|
5
|
-
import UploadFileServerCtr from '../UploadFileServerCtr';
|
|
6
|
-
|
|
7
|
-
vi.mock('@/services/fileSrv', () => ({
|
|
8
|
-
default: class MockFileService {},
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
const mockFileService = {
|
|
12
|
-
getFileHTTPURL: vi.fn(),
|
|
13
|
-
getFilePath: vi.fn(),
|
|
14
|
-
deleteFiles: vi.fn(),
|
|
15
|
-
uploadFile: vi.fn(),
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const mockApp = {
|
|
19
|
-
getService: vi.fn(() => mockFileService),
|
|
20
|
-
} as unknown as App;
|
|
21
|
-
|
|
22
|
-
describe('UploadFileServerCtr', () => {
|
|
23
|
-
let controller: UploadFileServerCtr;
|
|
24
|
-
|
|
25
|
-
beforeEach(() => {
|
|
26
|
-
vi.clearAllMocks();
|
|
27
|
-
controller = new UploadFileServerCtr(mockApp);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('gets file path by id', async () => {
|
|
31
|
-
mockFileService.getFilePath.mockResolvedValue('path');
|
|
32
|
-
await expect(controller.getFileUrlById('id')).resolves.toBe('path');
|
|
33
|
-
expect(mockFileService.getFilePath).toHaveBeenCalledWith('id');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('gets HTTP URL', async () => {
|
|
37
|
-
mockFileService.getFileHTTPURL.mockResolvedValue('url');
|
|
38
|
-
await expect(controller.getFileHTTPURL('/path')).resolves.toBe('url');
|
|
39
|
-
expect(mockFileService.getFileHTTPURL).toHaveBeenCalledWith('/path');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('deletes files', async () => {
|
|
43
|
-
mockFileService.deleteFiles.mockResolvedValue(undefined);
|
|
44
|
-
await controller.deleteFiles(['a']);
|
|
45
|
-
expect(mockFileService.deleteFiles).toHaveBeenCalledWith(['a']);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('creates files via upload service', async () => {
|
|
49
|
-
const params = { filename: 'file' } as any;
|
|
50
|
-
mockFileService.uploadFile.mockResolvedValue({ success: true });
|
|
51
|
-
|
|
52
|
-
await expect(controller.createFile(params)).resolves.toEqual({ success: true });
|
|
53
|
-
expect(mockFileService.uploadFile).toHaveBeenCalledWith(params);
|
|
54
|
-
});
|
|
55
|
-
});
|