@lobehub/lobehub 2.0.0-next.164 → 2.0.0-next.166

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 (102) hide show
  1. package/.cursor/rules/desktop-feature-implementation.mdc +31 -34
  2. package/.cursor/rules/desktop-local-tools-implement.mdc +3 -3
  3. package/.cursor/rules/desktop-window-management.mdc +56 -66
  4. package/CHANGELOG.md +50 -0
  5. package/Dockerfile +44 -52
  6. package/README.md +6 -6
  7. package/README.zh-CN.md +6 -6
  8. package/apps/desktop/Development.md +42 -46
  9. package/apps/desktop/README.md +37 -1
  10. package/apps/desktop/README.zh-CN.md +26 -1
  11. package/apps/desktop/electron.vite.config.ts +1 -0
  12. package/apps/desktop/src/main/controllers/AuthCtr.ts +4 -3
  13. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +33 -20
  14. package/apps/desktop/src/main/controllers/DevtoolsCtr.ts +4 -2
  15. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +14 -13
  16. package/apps/desktop/src/main/controllers/MenuCtr.ts +5 -4
  17. package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +18 -19
  18. package/apps/desktop/src/main/controllers/NotificationCtr.ts +4 -3
  19. package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +5 -4
  20. package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +3 -2
  21. package/apps/desktop/src/main/controllers/ShellCommandCtr.ts +5 -4
  22. package/apps/desktop/src/main/controllers/ShortcutCtr.ts +4 -3
  23. package/apps/desktop/src/main/controllers/SystemCtr.ts +7 -37
  24. package/apps/desktop/src/main/controllers/SystemServerCtr.ts +38 -0
  25. package/apps/desktop/src/main/controllers/TrayMenuCtr.ts +5 -4
  26. package/apps/desktop/src/main/controllers/UpdaterCtr.ts +6 -5
  27. package/apps/desktop/src/main/controllers/UploadFileCtr.ts +3 -25
  28. package/apps/desktop/src/main/controllers/UploadFileServerCtr.ts +33 -0
  29. package/apps/desktop/src/main/controllers/__tests__/AuthCtr.test.ts +9 -1
  30. package/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts +29 -9
  31. package/apps/desktop/src/main/controllers/__tests__/DevtoolsCtr.test.ts +12 -3
  32. package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +7 -0
  33. package/apps/desktop/src/main/controllers/__tests__/MenuCtr.test.ts +10 -0
  34. package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +10 -0
  35. package/apps/desktop/src/main/controllers/__tests__/NotificationCtr.test.ts +8 -0
  36. package/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts +8 -0
  37. package/apps/desktop/src/main/controllers/__tests__/RemoteServerSyncCtr.test.ts +1 -0
  38. package/apps/desktop/src/main/controllers/__tests__/ShellCommandCtr.test.ts +10 -0
  39. package/apps/desktop/src/main/controllers/__tests__/ShortcutCtr.test.ts +11 -0
  40. package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +43 -73
  41. package/apps/desktop/src/main/controllers/__tests__/SystemServerCtr.test.ts +75 -0
  42. package/apps/desktop/src/main/controllers/__tests__/TrayMenuCtr.test.ts +24 -13
  43. package/apps/desktop/src/main/controllers/__tests__/UpdaterCtr.test.ts +13 -2
  44. package/apps/desktop/src/main/controllers/__tests__/UploadFileCtr.test.ts +29 -108
  45. package/apps/desktop/src/main/controllers/__tests__/UploadFileServerCtr.test.ts +55 -0
  46. package/apps/desktop/src/main/controllers/_template.ts +2 -2
  47. package/apps/desktop/src/main/controllers/index.ts +5 -29
  48. package/apps/desktop/src/main/controllers/registry.ts +52 -0
  49. package/apps/desktop/src/main/core/App.ts +15 -47
  50. package/apps/desktop/src/main/core/__tests__/App.test.ts +5 -4
  51. package/apps/desktop/src/main/core/infrastructure/IoCContainer.ts +0 -5
  52. package/apps/desktop/src/main/core/infrastructure/__tests__/IoCContainer.test.ts +0 -50
  53. package/apps/desktop/src/main/exports.d.ts +8 -0
  54. package/apps/desktop/src/main/exports.ts +2 -0
  55. package/apps/desktop/src/main/global.d.ts +3 -0
  56. package/apps/desktop/src/main/modules/fileSearch/__tests__/macOS.integration.test.ts +17 -8
  57. package/apps/desktop/src/main/package.json +10 -0
  58. package/apps/desktop/src/main/services/fileSrv.ts +1 -1
  59. package/apps/desktop/src/main/utils/ipc/__tests__/base.test.ts +91 -0
  60. package/apps/desktop/src/main/utils/ipc/base.ts +170 -0
  61. package/apps/desktop/src/main/utils/ipc/index.ts +11 -0
  62. package/apps/desktop/src/main/utils/ipc/utility.ts +20 -0
  63. package/apps/desktop/src/preload/electronApi.ts +4 -1
  64. package/apps/desktop/src/preload/invoke.test.ts +13 -16
  65. package/apps/desktop/src/preload/invoke.ts +2 -5
  66. package/apps/desktop/src/preload/routeInterceptor.test.ts +13 -13
  67. package/apps/desktop/src/preload/routeInterceptor.ts +4 -4
  68. package/apps/desktop/tsconfig.json +15 -5
  69. package/changelog/v1.json +10 -0
  70. package/package.json +4 -3
  71. package/packages/electron-client-ipc/src/index.ts +1 -1
  72. package/packages/electron-client-ipc/src/ipc.test.ts +62 -0
  73. package/packages/electron-client-ipc/src/ipc.ts +63 -0
  74. package/packages/electron-client-ipc/src/streamInvoke.ts +7 -1
  75. package/packages/electron-client-ipc/src/types/dispatch.ts +1 -10
  76. package/packages/electron-client-ipc/vitest.config.mts +10 -0
  77. package/packages/electron-server-ipc/src/ipcClient.ts +1 -2
  78. package/packages/electron-server-ipc/src/ipcServer.ts +1 -2
  79. package/packages/electron-server-ipc/src/types/index.ts +1 -5
  80. package/pnpm-workspace.yaml +1 -1
  81. package/scripts/i18nWorkflow/const.ts +2 -2
  82. package/scripts/i18nWorkflow/i18nConfig.ts +7 -0
  83. package/scripts/i18nWorkflow/utils.ts +1 -1
  84. package/src/app/[variants]/(main)/discover/(detail)/provider/features/Sidebar/ActionButton/ProviderConfig.tsx +2 -2
  85. package/src/locales/default/setting.ts +1 -0
  86. package/src/server/modules/ElectronIPCClient/index.ts +59 -13
  87. package/src/services/electron/__tests__/devtools.test.ts +10 -6
  88. package/src/services/electron/autoUpdate.ts +5 -5
  89. package/src/services/electron/desktopNotification.ts +4 -7
  90. package/src/services/electron/devtools.ts +2 -2
  91. package/src/services/electron/file.ts +3 -2
  92. package/src/services/electron/localFileService.ts +17 -16
  93. package/src/services/electron/remoteServer.ts +7 -6
  94. package/src/services/electron/settings.ts +9 -11
  95. package/src/services/electron/system.ts +8 -6
  96. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +1 -1
  97. package/src/store/global/actions/general.ts +8 -10
  98. package/src/utils/electron/desktopRemoteRPCFetch.ts +3 -2
  99. package/src/utils/electron/ipc.ts +12 -0
  100. package/tsconfig.json +5 -0
  101. package/apps/desktop/src/main/types/ipcClientEvent.ts +0 -3
  102. package/packages/electron-client-ipc/src/dispatch.ts +0 -41
@@ -0,0 +1,170 @@
1
+ import type { IpcMainInvokeEvent, WebContents } from 'electron';
2
+ import { ipcMain } from 'electron';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+
5
+ // Base context for IPC methods
6
+ export interface IpcContext {
7
+ event: IpcMainInvokeEvent;
8
+ sender: WebContents;
9
+ }
10
+
11
+ // Metadata storage for decorated methods
12
+ const methodMetadata = new WeakMap<any, Map<string, string>>();
13
+ const serverMethodMetadata = new WeakMap<any, Map<string, string>>();
14
+ const ipcContextStorage = new AsyncLocalStorage<IpcContext>();
15
+
16
+ // Decorator for IPC methods
17
+ export function IpcMethod() {
18
+ return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
19
+ const { constructor } = target;
20
+
21
+ if (!methodMetadata.has(constructor)) {
22
+ methodMetadata.set(constructor, new Map());
23
+ }
24
+
25
+ const methods = methodMetadata.get(constructor)!;
26
+ methods.set(propertyKey, propertyKey);
27
+
28
+ return descriptor;
29
+ };
30
+ }
31
+
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
+ // Handler registry for IPC methods
48
+ export class IpcHandler {
49
+ private static instance: IpcHandler;
50
+ private registeredChannels = new Set<string>();
51
+
52
+ static getInstance(): IpcHandler {
53
+ if (!IpcHandler.instance) {
54
+ IpcHandler.instance = new IpcHandler();
55
+ }
56
+ return IpcHandler.instance;
57
+ }
58
+
59
+ registerMethod<TArgs extends unknown[], TOutput>(
60
+ channel: string,
61
+ handler: (...args: TArgs) => Promise<TOutput> | TOutput,
62
+ ) {
63
+ if (this.registeredChannels.has(channel)) {
64
+ return; // Already registered
65
+ }
66
+
67
+ this.registeredChannels.add(channel);
68
+
69
+ ipcMain.handle(channel, async (event: IpcMainInvokeEvent, ...args: any[]) => {
70
+ const context: IpcContext = {
71
+ event,
72
+ sender: event.sender,
73
+ };
74
+
75
+ return ipcContextStorage.run(context, async () => {
76
+ try {
77
+ const typedArgs = args as TArgs;
78
+ return await handler(...typedArgs);
79
+ } catch (error) {
80
+ console.error(`Error in IPC method ${channel}:`, error);
81
+ throw error;
82
+ }
83
+ });
84
+ });
85
+ }
86
+
87
+ // Send events to renderer
88
+ sendToRenderer<T = any>(webContents: WebContents, channel: string, data: T) {
89
+ webContents.send(channel, data);
90
+ }
91
+ }
92
+
93
+ // Base class for IPC service groups
94
+ export abstract class IpcService {
95
+ protected handler = IpcHandler.getInstance();
96
+ static readonly groupName: string;
97
+
98
+ constructor() {
99
+ this.registerMethods();
100
+ }
101
+
102
+ protected registerMethods(): void {
103
+ const { constructor } = this;
104
+ const methods = methodMetadata.get(constructor);
105
+
106
+ if (methods) {
107
+ methods.forEach((methodName, propertyKey) => {
108
+ const method = (this as any)[propertyKey];
109
+ if (typeof method === 'function') {
110
+ this.registerMethod(methodName, method.bind(this));
111
+ }
112
+ });
113
+ }
114
+ }
115
+
116
+ protected registerMethod<TArgs extends unknown[], TOutput>(
117
+ methodName: string,
118
+ handler: (...args: TArgs) => Promise<TOutput> | TOutput,
119
+ ) {
120
+ const groupName = (this.constructor as typeof IpcService).groupName;
121
+ const channel = `${groupName}.${methodName}`;
122
+ this.handler.registerMethod(channel, handler);
123
+ }
124
+ }
125
+
126
+ // Service constructor with groupName
127
+ export interface IpcServiceConstructor {
128
+ new (...args: any[]): IpcService;
129
+ readonly groupName: string;
130
+ }
131
+
132
+ // Create services function that infers types from service constructors
133
+ export function createServices<T extends readonly IpcServiceConstructor[]>(
134
+ serviceConstructors: T,
135
+ ...constructorArgs: any[]
136
+ ): CreateServicesResult<T> {
137
+ const services = {} as any;
138
+
139
+ for (const ServiceConstructor of serviceConstructors) {
140
+ const instance = new ServiceConstructor(...constructorArgs);
141
+ const groupName = ServiceConstructor.groupName;
142
+
143
+ if (!groupName) {
144
+ throw new Error(
145
+ `Service ${ServiceConstructor.name} must define a static readonly groupName property`,
146
+ );
147
+ }
148
+
149
+ services[groupName] = instance;
150
+ }
151
+
152
+ return services;
153
+ }
154
+
155
+ // Helper type for createServices return type
156
+ export type CreateServicesResult<T extends readonly IpcServiceConstructor[]> = {
157
+ [K in T[number] as K['groupName']]: InstanceType<K>;
158
+ };
159
+
160
+ export function getServerMethodMetadata(target: IpcServiceConstructor) {
161
+ return serverMethodMetadata.get(target);
162
+ }
163
+
164
+ export function getIpcContext() {
165
+ return ipcContextStorage.getStore();
166
+ }
167
+
168
+ export function runWithIpcContext<T>(context: IpcContext, callback: () => T): T {
169
+ return ipcContextStorage.run(context, callback);
170
+ }
@@ -0,0 +1,11 @@
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';
11
+ export type { ExtractServiceMethods, MergeIpcService } from './utility';
@@ -0,0 +1,20 @@
1
+ // Extract method signatures from service classes
2
+ type ExtractMethodSignature<T> = T extends (...args: infer Args) => infer Output
3
+ ? (...args: Args) => AlwaysPromise<Output>
4
+ : never;
5
+
6
+ export type ExtractServiceMethods<T> = {
7
+ [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: ExtractMethodSignature<T[K]>;
8
+ };
9
+
10
+ type AlwaysPromise<T> = Promise<Awaited<T>>;
11
+
12
+ // TypeScript utility type to automatically merge IPC services
13
+ // This version works with both the old object format and new createServices format
14
+ export type MergeIpcService<T> = {
15
+ [K in keyof T]: T[K] extends new (...args: any[]) => infer Instance
16
+ ? ExtractServiceMethods<Instance>
17
+ : T[K] extends infer Instance
18
+ ? ExtractServiceMethods<Instance>
19
+ : never;
20
+ };
@@ -15,5 +15,8 @@ export const setupElectronApi = () => {
15
15
  console.error(error);
16
16
  }
17
17
 
18
- contextBridge.exposeInMainWorld('electronAPI', { invoke, onStreamInvoke });
18
+ contextBridge.exposeInMainWorld('electronAPI', {
19
+ invoke,
20
+ onStreamInvoke,
21
+ });
19
22
  };
@@ -1,4 +1,3 @@
1
- import { ClientDispatchEventKey } from '@lobechat/electron-client-ipc';
2
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
3
2
 
4
3
  // Mock electron module
@@ -21,9 +20,9 @@ describe('invoke', () => {
21
20
  const expectedResult = { success: true };
22
21
  mockIpcRendererInvoke.mockResolvedValue(expectedResult);
23
22
 
24
- const result = await invoke('getAppVersion' as ClientDispatchEventKey);
23
+ const result = await invoke('system.getAppVersion');
25
24
 
26
- expect(mockIpcRendererInvoke).toHaveBeenCalledWith('getAppVersion');
25
+ expect(mockIpcRendererInvoke).toHaveBeenCalledWith('system.getAppVersion');
27
26
  expect(mockIpcRendererInvoke).toHaveBeenCalledTimes(1);
28
27
  expect(result).toEqual(expectedResult);
29
28
  });
@@ -33,9 +32,9 @@ describe('invoke', () => {
33
32
  const expectedResult = { navigated: true };
34
33
  mockIpcRendererInvoke.mockResolvedValue(expectedResult);
35
34
 
36
- const result = await invoke('interceptRoute' as ClientDispatchEventKey, eventData);
35
+ const result = await invoke('windows.interceptRoute', eventData);
37
36
 
38
- expect(mockIpcRendererInvoke).toHaveBeenCalledWith('interceptRoute', eventData);
37
+ expect(mockIpcRendererInvoke).toHaveBeenCalledWith('windows.interceptRoute', eventData);
39
38
  expect(mockIpcRendererInvoke).toHaveBeenCalledTimes(1);
40
39
  expect(result).toEqual(expectedResult);
41
40
  });
@@ -59,16 +58,14 @@ describe('invoke', () => {
59
58
  const error = new Error('IPC communication failed');
60
59
  mockIpcRendererInvoke.mockRejectedValue(error);
61
60
 
62
- await expect(invoke('getAppVersion' as ClientDispatchEventKey)).rejects.toThrow(
63
- 'IPC communication failed',
64
- );
65
- expect(mockIpcRendererInvoke).toHaveBeenCalledWith('getAppVersion');
61
+ await expect(invoke('system.getAppVersion')).rejects.toThrow('IPC communication failed');
62
+ expect(mockIpcRendererInvoke).toHaveBeenCalledWith('system.getAppVersion');
66
63
  });
67
64
 
68
65
  it('should handle ipcRenderer returning undefined', async () => {
69
66
  mockIpcRendererInvoke.mockResolvedValue(undefined);
70
67
 
71
- const result = await invoke('someEvent' as ClientDispatchEventKey);
68
+ const result = await invoke('someEvent');
72
69
 
73
70
  expect(mockIpcRendererInvoke).toHaveBeenCalledWith('someEvent');
74
71
  expect(result).toBeUndefined();
@@ -77,7 +74,7 @@ describe('invoke', () => {
77
74
  it('should handle ipcRenderer returning null', async () => {
78
75
  mockIpcRendererInvoke.mockResolvedValue(null);
79
76
 
80
- const result = await invoke('someEvent' as ClientDispatchEventKey);
77
+ const result = await invoke('someEvent');
81
78
 
82
79
  expect(mockIpcRendererInvoke).toHaveBeenCalledWith('someEvent');
83
80
  expect(result).toBeNull();
@@ -96,7 +93,7 @@ describe('invoke', () => {
96
93
  };
97
94
  mockIpcRendererInvoke.mockResolvedValue(complexData);
98
95
 
99
- const result = await invoke('getData' as ClientDispatchEventKey);
96
+ const result = await invoke('getData');
100
97
 
101
98
  expect(result).toEqual(complexData);
102
99
  });
@@ -125,9 +122,9 @@ describe('invoke', () => {
125
122
  .mockResolvedValueOnce({ id: 3 });
126
123
 
127
124
  const [result1, result2, result3] = await Promise.all([
128
- invoke('event1' as ClientDispatchEventKey),
129
- invoke('event2' as ClientDispatchEventKey),
130
- invoke('event3' as ClientDispatchEventKey),
125
+ invoke('event1'),
126
+ invoke('event2'),
127
+ invoke('event3'),
131
128
  ]);
132
129
 
133
130
  expect(result1).toEqual({ id: 1 });
@@ -139,7 +136,7 @@ describe('invoke', () => {
139
136
  it('should handle empty string as data parameter', async () => {
140
137
  mockIpcRendererInvoke.mockResolvedValue({ received: '' });
141
138
 
142
- const result = await invoke('sendData' as ClientDispatchEventKey, '');
139
+ const result = await invoke('sendData', '');
143
140
 
144
141
  expect(mockIpcRendererInvoke).toHaveBeenCalledWith('sendData', '');
145
142
  expect(result).toEqual({ received: '' });
@@ -1,10 +1,7 @@
1
- import { ClientDispatchEventKey, DispatchInvoke } from '@lobechat/electron-client-ipc';
1
+ import { DispatchInvoke } from '@lobechat/electron-client-ipc';
2
2
  import { ipcRenderer } from 'electron';
3
3
 
4
4
  /**
5
5
  * Client-side method to invoke electron main process
6
6
  */
7
- export const invoke: DispatchInvoke = async <T extends ClientDispatchEventKey>(
8
- event: T,
9
- ...data: any[]
10
- ) => ipcRenderer.invoke(event, ...data);
7
+ export const invoke: DispatchInvoke = async (event, ...data) => ipcRenderer.invoke(event, ...data);
@@ -46,7 +46,7 @@ describe('setupRouteInterceptors', () => {
46
46
  const externalUrl = 'https://google.com';
47
47
  const result = window.open(externalUrl, '_blank');
48
48
 
49
- expect(invoke).toHaveBeenCalledWith('openExternalLink', externalUrl);
49
+ expect(invoke).toHaveBeenCalledWith('system.openExternalLink', externalUrl);
50
50
  expect(result).toBeNull();
51
51
  });
52
52
 
@@ -56,7 +56,7 @@ describe('setupRouteInterceptors', () => {
56
56
  const externalUrl = new URL('https://github.com');
57
57
  const result = window.open(externalUrl, '_blank');
58
58
 
59
- expect(invoke).toHaveBeenCalledWith('openExternalLink', 'https://github.com/');
59
+ expect(invoke).toHaveBeenCalledWith('system.openExternalLink', 'https://github.com/');
60
60
  expect(result).toBeNull();
61
61
  });
62
62
 
@@ -69,7 +69,7 @@ describe('setupRouteInterceptors', () => {
69
69
  // We can't fully test the original behavior in happy-dom, but we can verify invoke is not called
70
70
  window.open(internalUrl);
71
71
 
72
- expect(invoke).not.toHaveBeenCalledWith('openExternalLink', expect.anything());
72
+ expect(invoke).not.toHaveBeenCalledWith('system.openExternalLink', expect.anything());
73
73
  });
74
74
 
75
75
  it('should handle relative URL that resolves as internal link', () => {
@@ -81,7 +81,7 @@ describe('setupRouteInterceptors', () => {
81
81
  window.open(relativeUrl);
82
82
 
83
83
  // Since it's internal, it won't call invoke for external link
84
- expect(invoke).not.toHaveBeenCalledWith('openExternalLink', expect.anything());
84
+ expect(invoke).not.toHaveBeenCalledWith('system.openExternalLink', expect.anything());
85
85
  });
86
86
  });
87
87
 
@@ -102,7 +102,7 @@ describe('setupRouteInterceptors', () => {
102
102
  // Wait for async handling
103
103
  await new Promise((resolve) => setTimeout(resolve, 0));
104
104
 
105
- expect(invoke).toHaveBeenCalledWith('openExternalLink', 'https://example.com/');
105
+ expect(invoke).toHaveBeenCalledWith('system.openExternalLink', 'https://example.com/');
106
106
  expect(preventDefaultSpy).toHaveBeenCalled();
107
107
  expect(stopPropagationSpy).toHaveBeenCalled();
108
108
  });
@@ -129,7 +129,7 @@ describe('setupRouteInterceptors', () => {
129
129
  await new Promise((resolve) => setTimeout(resolve, 0));
130
130
 
131
131
  expect(findMatchingRoute).toHaveBeenCalledWith('/desktop/devtools');
132
- expect(invoke).toHaveBeenCalledWith('interceptRoute', {
132
+ expect(invoke).toHaveBeenCalledWith('windows.interceptRoute', {
133
133
  path: '/desktop/devtools',
134
134
  source: 'link-click',
135
135
  url: 'http://localhost:3000/desktop/devtools',
@@ -166,7 +166,7 @@ describe('setupRouteInterceptors', () => {
166
166
  await new Promise((resolve) => setTimeout(resolve, 0));
167
167
 
168
168
  expect(preventDefaultSpy).not.toHaveBeenCalled();
169
- expect(invoke).not.toHaveBeenCalledWith('interceptRoute', expect.anything());
169
+ expect(invoke).not.toHaveBeenCalledWith('windows.interceptRoute', expect.anything());
170
170
  });
171
171
 
172
172
  it('should handle non-HTTP link protocols as external links', async () => {
@@ -184,7 +184,7 @@ describe('setupRouteInterceptors', () => {
184
184
  await new Promise((resolve) => setTimeout(resolve, 0));
185
185
 
186
186
  // mailto: links are treated as external links by the URL constructor
187
- expect(invoke).toHaveBeenCalledWith('openExternalLink', 'mailto:test@example.com');
187
+ expect(invoke).toHaveBeenCalledWith('system.openExternalLink', 'mailto:test@example.com');
188
188
  expect(preventDefaultSpy).toHaveBeenCalled();
189
189
  });
190
190
  });
@@ -205,7 +205,7 @@ describe('setupRouteInterceptors', () => {
205
205
  history.pushState({}, '', '/desktop/devtools');
206
206
 
207
207
  expect(findMatchingRoute).toHaveBeenCalledWith('/desktop/devtools');
208
- expect(invoke).toHaveBeenCalledWith('interceptRoute', {
208
+ expect(invoke).toHaveBeenCalledWith('windows.interceptRoute', {
209
209
  path: '/desktop/devtools',
210
210
  source: 'push-state',
211
211
  url: 'http://localhost:3000/desktop/devtools',
@@ -245,7 +245,7 @@ describe('setupRouteInterceptors', () => {
245
245
 
246
246
  history.pushState({}, '', '/chat/new');
247
247
 
248
- expect(invoke).not.toHaveBeenCalledWith('interceptRoute', expect.anything());
248
+ expect(invoke).not.toHaveBeenCalledWith('windows.interceptRoute', expect.anything());
249
249
  });
250
250
 
251
251
  it('should handle pushState errors gracefully', () => {
@@ -279,7 +279,7 @@ describe('setupRouteInterceptors', () => {
279
279
  history.replaceState({}, '', '/desktop/devtools');
280
280
 
281
281
  expect(findMatchingRoute).toHaveBeenCalledWith('/desktop/devtools');
282
- expect(invoke).toHaveBeenCalledWith('interceptRoute', {
282
+ expect(invoke).toHaveBeenCalledWith('windows.interceptRoute', {
283
283
  path: '/desktop/devtools',
284
284
  source: 'replace-state',
285
285
  url: 'http://localhost:3000/desktop/devtools',
@@ -317,7 +317,7 @@ describe('setupRouteInterceptors', () => {
317
317
 
318
318
  history.replaceState({}, '', '/chat/session-123');
319
319
 
320
- expect(invoke).not.toHaveBeenCalledWith('interceptRoute', expect.anything());
320
+ expect(invoke).not.toHaveBeenCalledWith('windows.interceptRoute', expect.anything());
321
321
  });
322
322
  });
323
323
 
@@ -385,7 +385,7 @@ describe('setupRouteInterceptors', () => {
385
385
 
386
386
  await new Promise((resolve) => setTimeout(resolve, 0));
387
387
 
388
- expect(invoke).toHaveBeenCalledWith('interceptRoute', {
388
+ expect(invoke).toHaveBeenCalledWith('windows.interceptRoute', {
389
389
  path: '/desktop/devtools',
390
390
  source: 'push-state',
391
391
  url: 'http://localhost:3000/desktop/devtools',
@@ -11,7 +11,7 @@ const interceptRoute = async (
11
11
 
12
12
  // Use electron-client-ipc's dispatch method
13
13
  try {
14
- await invoke('interceptRoute', { path, source, url });
14
+ await invoke('windows.interceptRoute', { path, source, url });
15
15
  } catch (e) {
16
16
  console.error(`[preload] Route interception (${source}) call failed`, e);
17
17
  }
@@ -37,14 +37,14 @@ export const setupRouteInterceptors = function () {
37
37
  if (urlObj.origin !== window.location.origin) {
38
38
  console.log(`[preload] Intercepted window.open for external URL:`, urlString);
39
39
  // Call main process to handle external link
40
- invoke('openExternalLink', urlString);
40
+ invoke('system.openExternalLink', urlString);
41
41
  return null; // Return null to indicate no window was opened
42
42
  }
43
43
  } catch (error) {
44
44
  // Handle invalid URL or special protocol
45
45
  console.error(`[preload] Intercepted window.open for special protocol:`, url);
46
46
  console.error(error);
47
- invoke('openExternalLink', typeof url === 'string' ? url : url.toString());
47
+ invoke('system.openExternalLink', typeof url === 'string' ? url : url.toString());
48
48
  return null;
49
49
  }
50
50
  }
@@ -69,7 +69,7 @@ export const setupRouteInterceptors = function () {
69
69
  e.preventDefault();
70
70
  e.stopPropagation();
71
71
  // Call main process to handle external link
72
- await invoke('openExternalLink', url.href);
72
+ await invoke('system.openExternalLink', url.href);
73
73
  return false; // Explicitly prevent subsequent processing
74
74
  }
75
75
 
@@ -3,18 +3,28 @@
3
3
  "allowJs": true,
4
4
  "skipLibCheck": true,
5
5
  "forceConsistentCasingInFileNames": true,
6
- "noEmit": true,
7
6
  "target": "ESNext",
7
+ "emitDeclarationOnly": true,
8
8
  "esModuleInterop": true,
9
9
  "emitDecoratorMetadata": true,
10
+ "composite": true,
11
+ "baseUrl": ".",
10
12
  "experimentalDecorators": true,
11
13
  "module": "esnext",
12
14
  "moduleResolution": "bundler",
13
15
  "resolveJsonModule": true,
14
16
  "paths": {
15
- "@/*": ["src/main/*"],
16
- "~common/*": ["src/common/*"]
17
+ "@/*": [
18
+ "src/main/*"
19
+ ],
20
+ "~common/*": [
21
+ "src/common/*"
22
+ ]
17
23
  }
18
24
  },
19
- "include": ["src/main/**/*", "src/preload/**/*", "electron-builder.js"]
20
- }
25
+ "include": [
26
+ "src/main/**/*",
27
+ "src/preload/**/*",
28
+ "electron-builder.js"
29
+ ]
30
+ }
package/changelog/v1.json CHANGED
@@ -1,4 +1,14 @@
1
1
  [
2
+ {
3
+ "children": {},
4
+ "date": "2025-12-09",
5
+ "version": "2.0.0-next.166"
6
+ },
7
+ {
8
+ "children": {},
9
+ "date": "2025-12-09",
10
+ "version": "2.0.0-next.165"
11
+ },
2
12
  {
3
13
  "children": {
4
14
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.164",
3
+ "version": "2.0.0-next.166",
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",
@@ -27,7 +27,8 @@
27
27
  "sideEffects": false,
28
28
  "workspaces": [
29
29
  "packages/*",
30
- "e2e"
30
+ "e2e",
31
+ "apps/desktop/src/main"
31
32
  ],
32
33
  "scripts": {
33
34
  "prebuild": "tsx scripts/prebuild.mts && npm run lint",
@@ -169,6 +170,7 @@
169
170
  "@lobehub/charts": "^2.1.2",
170
171
  "@lobehub/chat-plugin-sdk": "^1.32.4",
171
172
  "@lobehub/chat-plugins-gateway": "^1.9.0",
173
+ "@lobehub/desktop-ipc-typings": "workspace:*",
172
174
  "@lobehub/editor": "^1.23.1",
173
175
  "@lobehub/icons": "^2.43.1",
174
176
  "@lobehub/market-sdk": "^0.23.2",
@@ -338,7 +340,6 @@
338
340
  "@types/crypto-js": "^4.2.2",
339
341
  "@types/debug": "^4.1.12",
340
342
  "@types/fs-extra": "^11.0.4",
341
- "@types/ioredis": "^5.0.0",
342
343
  "@types/ip": "^1.1.3",
343
344
  "@types/json-schema": "^7.0.15",
344
345
  "@types/lodash": "^4.17.21",
@@ -1,5 +1,5 @@
1
- export * from './dispatch';
2
1
  export * from './events';
2
+ export * from './ipc';
3
3
  export * from './streamInvoke';
4
4
  export * from './types';
5
5
  export * from './useWatchBroadcast';
@@ -0,0 +1,62 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const restoreWindow = (() => {
4
+ const originalWindow = (globalThis as any).window;
5
+
6
+ return () => {
7
+ if (originalWindow === undefined) {
8
+ Reflect.deleteProperty(globalThis as any, 'window');
9
+ return;
10
+ }
11
+ (globalThis as any).window = originalWindow;
12
+ };
13
+ })();
14
+
15
+ describe('getElectronIpc', () => {
16
+ beforeEach(() => {
17
+ vi.resetModules();
18
+ restoreWindow();
19
+ });
20
+
21
+ afterEach(() => {
22
+ restoreWindow();
23
+ });
24
+
25
+ it('returns null when window is not defined', async () => {
26
+ Reflect.deleteProperty(globalThis as any, 'window');
27
+ const { getElectronIpc } = await import('./ipc');
28
+
29
+ expect(getElectronIpc()).toBeNull();
30
+ });
31
+
32
+ it('returns null when invoke is missing on electronAPI', async () => {
33
+ (globalThis as any).window = { electronAPI: {} };
34
+ const { getElectronIpc } = await import('./ipc');
35
+
36
+ expect(getElectronIpc()).toBeNull();
37
+ });
38
+
39
+ it('creates a cached proxy and forwards payloads to invoke', async () => {
40
+ const invoke = vi.fn();
41
+ (globalThis as any).window = {
42
+ electronAPI: {
43
+ invoke,
44
+ onStreamInvoke: vi.fn(),
45
+ },
46
+ };
47
+
48
+ const { getElectronIpc } = await import('./ipc');
49
+
50
+ const ipc = getElectronIpc();
51
+ expect(ipc).not.toBeNull();
52
+
53
+ await (ipc as any).system.updateLocale('en-US');
54
+ expect(invoke).toHaveBeenCalledWith('system.updateLocale', 'en-US');
55
+
56
+ await (ipc as any).windows.closeWindow();
57
+ expect(invoke).toHaveBeenCalledWith('windows.closeWindow');
58
+
59
+ const cached = getElectronIpc();
60
+ expect(cached).toBe(ipc);
61
+ });
62
+ });