@lobehub/lobehub 2.0.0-next.163 → 2.0.0-next.165
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 +31 -34
- package/.cursor/rules/desktop-local-tools-implement.mdc +3 -3
- package/.cursor/rules/desktop-window-management.mdc +56 -66
- package/CHANGELOG.md +52 -0
- package/README.md +6 -6
- package/README.zh-CN.md +6 -6
- package/apps/desktop/Development.md +42 -46
- package/apps/desktop/README.md +37 -1
- package/apps/desktop/README.zh-CN.md +26 -1
- package/apps/desktop/electron.vite.config.ts +1 -0
- package/apps/desktop/src/main/controllers/AuthCtr.ts +4 -3
- package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +33 -20
- package/apps/desktop/src/main/controllers/DevtoolsCtr.ts +4 -2
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +14 -13
- package/apps/desktop/src/main/controllers/MenuCtr.ts +5 -4
- package/apps/desktop/src/main/controllers/NetworkProxyCtr.ts +18 -19
- package/apps/desktop/src/main/controllers/NotificationCtr.ts +4 -3
- package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +5 -4
- package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +3 -2
- package/apps/desktop/src/main/controllers/ShellCommandCtr.ts +5 -4
- package/apps/desktop/src/main/controllers/ShortcutCtr.ts +4 -3
- package/apps/desktop/src/main/controllers/SystemCtr.ts +7 -37
- package/apps/desktop/src/main/controllers/SystemServerCtr.ts +38 -0
- package/apps/desktop/src/main/controllers/TrayMenuCtr.ts +5 -4
- package/apps/desktop/src/main/controllers/UpdaterCtr.ts +6 -5
- package/apps/desktop/src/main/controllers/UploadFileCtr.ts +3 -25
- package/apps/desktop/src/main/controllers/UploadFileServerCtr.ts +33 -0
- package/apps/desktop/src/main/controllers/__tests__/AuthCtr.test.ts +9 -1
- package/apps/desktop/src/main/controllers/__tests__/BrowserWindowsCtr.test.ts +29 -9
- package/apps/desktop/src/main/controllers/__tests__/DevtoolsCtr.test.ts +12 -3
- package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +7 -0
- package/apps/desktop/src/main/controllers/__tests__/MenuCtr.test.ts +10 -0
- package/apps/desktop/src/main/controllers/__tests__/NetworkProxyCtr.test.ts +10 -0
- package/apps/desktop/src/main/controllers/__tests__/NotificationCtr.test.ts +8 -0
- package/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts +8 -0
- package/apps/desktop/src/main/controllers/__tests__/RemoteServerSyncCtr.test.ts +1 -0
- package/apps/desktop/src/main/controllers/__tests__/ShellCommandCtr.test.ts +10 -0
- package/apps/desktop/src/main/controllers/__tests__/ShortcutCtr.test.ts +11 -0
- package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +43 -73
- package/apps/desktop/src/main/controllers/__tests__/SystemServerCtr.test.ts +75 -0
- package/apps/desktop/src/main/controllers/__tests__/TrayMenuCtr.test.ts +24 -13
- package/apps/desktop/src/main/controllers/__tests__/UpdaterCtr.test.ts +13 -2
- package/apps/desktop/src/main/controllers/__tests__/UploadFileCtr.test.ts +29 -108
- package/apps/desktop/src/main/controllers/__tests__/UploadFileServerCtr.test.ts +55 -0
- package/apps/desktop/src/main/controllers/_template.ts +2 -2
- package/apps/desktop/src/main/controllers/index.ts +5 -29
- package/apps/desktop/src/main/controllers/registry.ts +52 -0
- package/apps/desktop/src/main/core/App.ts +15 -47
- package/apps/desktop/src/main/core/__tests__/App.test.ts +5 -4
- package/apps/desktop/src/main/core/infrastructure/IoCContainer.ts +0 -5
- package/apps/desktop/src/main/core/infrastructure/__tests__/IoCContainer.test.ts +0 -50
- package/apps/desktop/src/main/exports.d.ts +8 -0
- package/apps/desktop/src/main/exports.ts +2 -0
- package/apps/desktop/src/main/global.d.ts +3 -0
- package/apps/desktop/src/main/modules/fileSearch/__tests__/macOS.integration.test.ts +17 -8
- package/apps/desktop/src/main/package.json +10 -0
- package/apps/desktop/src/main/services/fileSrv.ts +1 -1
- package/apps/desktop/src/main/utils/ipc/__tests__/base.test.ts +91 -0
- package/apps/desktop/src/main/utils/ipc/base.ts +170 -0
- package/apps/desktop/src/main/utils/ipc/index.ts +11 -0
- package/apps/desktop/src/main/utils/ipc/utility.ts +20 -0
- package/apps/desktop/src/preload/electronApi.ts +4 -1
- package/apps/desktop/src/preload/invoke.test.ts +13 -16
- package/apps/desktop/src/preload/invoke.ts +2 -5
- package/apps/desktop/src/preload/routeInterceptor.test.ts +13 -13
- package/apps/desktop/src/preload/routeInterceptor.ts +4 -4
- package/apps/desktop/tsconfig.json +15 -5
- package/changelog/v1.json +14 -0
- package/locales/ar/auth.json +3 -0
- package/locales/bg-BG/auth.json +3 -0
- package/locales/de-DE/auth.json +3 -0
- package/locales/en-US/auth.json +4 -1
- package/locales/es-ES/auth.json +3 -0
- package/locales/fa-IR/auth.json +3 -0
- package/locales/fr-FR/auth.json +3 -0
- package/locales/it-IT/auth.json +3 -0
- package/locales/ja-JP/auth.json +3 -0
- package/locales/ko-KR/auth.json +3 -0
- package/locales/nl-NL/auth.json +3 -0
- package/locales/pl-PL/auth.json +3 -0
- package/locales/pt-BR/auth.json +3 -0
- package/locales/ru-RU/auth.json +3 -0
- package/locales/tr-TR/auth.json +3 -0
- package/locales/vi-VN/auth.json +3 -0
- package/locales/zh-CN/auth.json +3 -0
- package/locales/zh-TW/auth.json +3 -0
- package/package.json +4 -3
- package/packages/electron-client-ipc/src/index.ts +1 -1
- package/packages/electron-client-ipc/src/ipc.test.ts +62 -0
- package/packages/electron-client-ipc/src/ipc.ts +63 -0
- package/packages/electron-client-ipc/src/streamInvoke.ts +7 -1
- package/packages/electron-client-ipc/src/types/dispatch.ts +1 -10
- package/packages/electron-client-ipc/vitest.config.mts +10 -0
- package/packages/electron-server-ipc/src/ipcClient.ts +1 -2
- package/packages/electron-server-ipc/src/ipcServer.ts +1 -2
- package/packages/electron-server-ipc/src/types/index.ts +1 -5
- package/pnpm-workspace.yaml +1 -1
- package/scripts/i18nWorkflow/const.ts +2 -2
- package/scripts/i18nWorkflow/i18nConfig.ts +7 -0
- package/scripts/i18nWorkflow/utils.ts +1 -1
- package/src/app/[variants]/(auth)/signup/[[...signup]]/BetterAuthSignUpForm.tsx +23 -0
- package/src/app/[variants]/(main)/(mobile)/me/(home)/features/UserBanner.tsx +3 -9
- package/src/app/[variants]/(main)/discover/(detail)/provider/features/Sidebar/ActionButton/ProviderConfig.tsx +2 -2
- package/src/app/[variants]/(main)/profile/(home)/Client.tsx +206 -138
- package/src/features/User/PlanTag.tsx +4 -4
- package/src/locales/default/auth.ts +3 -0
- package/src/locales/default/setting.ts +1 -0
- package/src/server/modules/ElectronIPCClient/index.ts +59 -13
- package/src/services/electron/__tests__/devtools.test.ts +10 -6
- package/src/services/electron/autoUpdate.ts +5 -5
- package/src/services/electron/desktopNotification.ts +4 -7
- package/src/services/electron/devtools.ts +2 -2
- package/src/services/electron/file.ts +3 -2
- package/src/services/electron/localFileService.ts +17 -16
- package/src/services/electron/remoteServer.ts +7 -6
- package/src/services/electron/settings.ts +9 -11
- package/src/services/electron/system.ts +8 -6
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +1 -1
- package/src/store/global/actions/general.ts +8 -10
- package/src/utils/electron/desktopRemoteRPCFetch.ts +3 -2
- package/src/utils/electron/ipc.ts +12 -0
- package/tsconfig.json +5 -0
- package/apps/desktop/src/main/types/ipcClientEvent.ts +0 -3
- 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
|
+
};
|
|
@@ -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'
|
|
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'
|
|
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'
|
|
63
|
-
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
129
|
-
invoke('event2'
|
|
130
|
-
invoke('event3'
|
|
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'
|
|
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 {
|
|
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
|
|
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
|
-
"@/*": [
|
|
16
|
-
|
|
17
|
+
"@/*": [
|
|
18
|
+
"src/main/*"
|
|
19
|
+
],
|
|
20
|
+
"~common/*": [
|
|
21
|
+
"src/common/*"
|
|
22
|
+
]
|
|
17
23
|
}
|
|
18
24
|
},
|
|
19
|
-
"include": [
|
|
20
|
-
|
|
25
|
+
"include": [
|
|
26
|
+
"src/main/**/*",
|
|
27
|
+
"src/preload/**/*",
|
|
28
|
+
"electron-builder.js"
|
|
29
|
+
]
|
|
30
|
+
}
|
package/changelog/v1.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {},
|
|
4
|
+
"date": "2025-12-09",
|
|
5
|
+
"version": "2.0.0-next.165"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"children": {
|
|
9
|
+
"improvements": [
|
|
10
|
+
"Update link handling in PlanTag component to use react-router-dom."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"date": "2025-12-08",
|
|
14
|
+
"version": "2.0.0-next.164"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"children": {
|
|
4
18
|
"fixes": [
|
package/locales/ar/auth.json
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"betterAuth": {
|
|
56
56
|
"errors": {
|
|
57
|
+
"confirmPasswordRequired": "يرجى تأكيد كلمة المرور",
|
|
57
58
|
"emailExists": "هذا البريد الإلكتروني مسجّل بالفعل، يرجى تسجيل الدخول مباشرة",
|
|
58
59
|
"emailInvalid": "يرجى إدخال عنوان بريد إلكتروني صالح",
|
|
59
60
|
"emailNotRegistered": "هذا البريد الإلكتروني غير مسجل",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"passwordFormat": "يجب أن تحتوي كلمة المرور على أحرف وأرقام",
|
|
66
67
|
"passwordMaxLength": "يجب ألا تتجاوز كلمة المرور 64 حرفًا",
|
|
67
68
|
"passwordMinLength": "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل",
|
|
69
|
+
"passwordMismatch": "كلمتا المرور غير متطابقتين",
|
|
68
70
|
"passwordRequired": "يرجى إدخال كلمة المرور",
|
|
69
71
|
"usernameNotRegistered": "اسم المستخدم هذا غير مسجل",
|
|
70
72
|
"usernameRequired": "يرجى إدخال اسم المستخدم"
|
|
@@ -125,6 +127,7 @@
|
|
|
125
127
|
"submit": "تسجيل الدخول"
|
|
126
128
|
},
|
|
127
129
|
"signup": {
|
|
130
|
+
"confirmPasswordPlaceholder": "يرجى تأكيد كلمة المرور",
|
|
128
131
|
"emailPlaceholder": "يرجى إدخال عنوان البريد الإلكتروني",
|
|
129
132
|
"error": "فشل التسجيل، يرجى المحاولة مرة أخرى",
|
|
130
133
|
"firstNamePlaceholder": "الاسم الأول",
|
package/locales/bg-BG/auth.json
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"betterAuth": {
|
|
56
56
|
"errors": {
|
|
57
|
+
"confirmPasswordRequired": "Моля, потвърдете паролата",
|
|
57
58
|
"emailExists": "Този имейл вече е регистриран. Моля, влезте директно.",
|
|
58
59
|
"emailInvalid": "Моля, въведете валиден имейл адрес",
|
|
59
60
|
"emailNotRegistered": "Този имейл все още не е регистриран",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"passwordFormat": "Паролата трябва да съдържа както букви, така и цифри",
|
|
66
67
|
"passwordMaxLength": "Паролата не може да надвишава 64 знака",
|
|
67
68
|
"passwordMinLength": "Паролата трябва да бъде поне 8 знака",
|
|
69
|
+
"passwordMismatch": "Въведените пароли не съвпадат",
|
|
68
70
|
"passwordRequired": "Моля, въведете парола",
|
|
69
71
|
"usernameNotRegistered": "Това потребителско име не е регистрирано",
|
|
70
72
|
"usernameRequired": "Моля, въведете потребителско име"
|
|
@@ -125,6 +127,7 @@
|
|
|
125
127
|
"submit": "Вход"
|
|
126
128
|
},
|
|
127
129
|
"signup": {
|
|
130
|
+
"confirmPasswordPlaceholder": "Моля, потвърдете паролата",
|
|
128
131
|
"emailPlaceholder": "Моля, въведете имейл адрес",
|
|
129
132
|
"error": "Регистрацията не бе успешна, моля опитайте отново",
|
|
130
133
|
"firstNamePlaceholder": "Собствено име",
|
package/locales/de-DE/auth.json
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"betterAuth": {
|
|
56
56
|
"errors": {
|
|
57
|
+
"confirmPasswordRequired": "Bitte bestätigen Sie Ihr Passwort",
|
|
57
58
|
"emailExists": "Diese E-Mail-Adresse ist bereits registriert. Bitte melden Sie sich direkt an.",
|
|
58
59
|
"emailInvalid": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
|
59
60
|
"emailNotRegistered": "Diese E-Mail-Adresse ist noch nicht registriert",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"passwordFormat": "Das Passwort muss Buchstaben und Zahlen enthalten",
|
|
66
67
|
"passwordMaxLength": "Das Passwort darf maximal 64 Zeichen lang sein",
|
|
67
68
|
"passwordMinLength": "Das Passwort muss mindestens 8 Zeichen lang sein",
|
|
69
|
+
"passwordMismatch": "Die beiden eingegebenen Passwörter stimmen nicht überein",
|
|
68
70
|
"passwordRequired": "Bitte geben Sie ein Passwort ein",
|
|
69
71
|
"usernameNotRegistered": "Dieser Benutzername ist noch nicht registriert",
|
|
70
72
|
"usernameRequired": "Bitte geben Sie einen Benutzernamen ein"
|
|
@@ -125,6 +127,7 @@
|
|
|
125
127
|
"submit": "Anmelden"
|
|
126
128
|
},
|
|
127
129
|
"signup": {
|
|
130
|
+
"confirmPasswordPlaceholder": "Bitte bestätigen Sie Ihr Passwort",
|
|
128
131
|
"emailPlaceholder": "Bitte geben Sie Ihre E-Mail-Adresse ein",
|
|
129
132
|
"error": "Registrierung fehlgeschlagen, bitte versuchen Sie es erneut",
|
|
130
133
|
"firstNamePlaceholder": "Vorname",
|
package/locales/en-US/auth.json
CHANGED
|
@@ -54,8 +54,9 @@
|
|
|
54
54
|
},
|
|
55
55
|
"betterAuth": {
|
|
56
56
|
"errors": {
|
|
57
|
-
"
|
|
57
|
+
"confirmPasswordRequired": "Please confirm your password",
|
|
58
58
|
"emailExists": "This email is already registered. Please sign in instead",
|
|
59
|
+
"emailInvalid": "Please enter a valid email address or username",
|
|
59
60
|
"emailNotRegistered": "This email or username is not registered",
|
|
60
61
|
"emailNotVerified": "Email not verified, please verify your email first",
|
|
61
62
|
"emailRequired": "Please enter your email address or username",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"passwordFormat": "Password must contain both letters and numbers",
|
|
66
67
|
"passwordMaxLength": "Password must not exceed 64 characters",
|
|
67
68
|
"passwordMinLength": "Password must be at least 8 characters",
|
|
69
|
+
"passwordMismatch": "The passwords do not match",
|
|
68
70
|
"passwordRequired": "Please enter your password",
|
|
69
71
|
"usernameNotRegistered": "This username is not registered",
|
|
70
72
|
"usernameRequired": "Please enter your username"
|
|
@@ -125,6 +127,7 @@
|
|
|
125
127
|
"submit": "Sign In"
|
|
126
128
|
},
|
|
127
129
|
"signup": {
|
|
130
|
+
"confirmPasswordPlaceholder": "Confirm your password",
|
|
128
131
|
"emailPlaceholder": "Enter your email address",
|
|
129
132
|
"error": "Sign up failed, please try again",
|
|
130
133
|
"firstNamePlaceholder": "First Name",
|
package/locales/es-ES/auth.json
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"betterAuth": {
|
|
56
56
|
"errors": {
|
|
57
|
+
"confirmPasswordRequired": "Por favor, confirma la contraseña",
|
|
57
58
|
"emailExists": "Este correo electrónico ya está registrado. Por favor, inicia sesión directamente.",
|
|
58
59
|
"emailInvalid": "Por favor, introduce una dirección de correo electrónico válida",
|
|
59
60
|
"emailNotRegistered": "Este correo electrónico no está registrado",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"passwordFormat": "La contraseña debe contener letras y números",
|
|
66
67
|
"passwordMaxLength": "La contraseña no puede tener más de 64 caracteres",
|
|
67
68
|
"passwordMinLength": "La contraseña debe tener al menos 8 caracteres",
|
|
69
|
+
"passwordMismatch": "Las contraseñas no coinciden",
|
|
68
70
|
"passwordRequired": "Por favor, introduce tu contraseña",
|
|
69
71
|
"usernameNotRegistered": "Este nombre de usuario no está registrado",
|
|
70
72
|
"usernameRequired": "Por favor, introduce tu nombre de usuario"
|
|
@@ -125,6 +127,7 @@
|
|
|
125
127
|
"submit": "Iniciar sesión"
|
|
126
128
|
},
|
|
127
129
|
"signup": {
|
|
130
|
+
"confirmPasswordPlaceholder": "Confirma tu contraseña",
|
|
128
131
|
"emailPlaceholder": "Introduce tu dirección de correo electrónico",
|
|
129
132
|
"error": "Error al registrarse. Inténtalo de nuevo",
|
|
130
133
|
"firstNamePlaceholder": "Nombre",
|