@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.
- 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 +50 -0
- package/Dockerfile +44 -52
- 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 +10 -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]/(main)/discover/(detail)/provider/features/Sidebar/ActionButton/ProviderConfig.tsx +2 -2
- 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
|
@@ -1,34 +1,7 @@
|
|
|
1
|
-
import type { ClientDispatchEvents } from '@lobechat/electron-client-ipc';
|
|
2
|
-
import type { ServerDispatchEvents } from '@lobechat/electron-server-ipc';
|
|
3
|
-
|
|
4
1
|
import type { App } from '@/core/App';
|
|
5
2
|
import { IoCContainer } from '@/core/infrastructure/IoCContainer';
|
|
6
3
|
import { ShortcutActionType } from '@/shortcuts';
|
|
7
|
-
|
|
8
|
-
const ipcDecorator =
|
|
9
|
-
(name: string, mode: 'client' | 'server') =>
|
|
10
|
-
(target: any, methodName: string, descriptor?: any) => {
|
|
11
|
-
const actions = IoCContainer.controllers.get(target.constructor) || [];
|
|
12
|
-
actions.push({
|
|
13
|
-
methodName,
|
|
14
|
-
mode,
|
|
15
|
-
name,
|
|
16
|
-
});
|
|
17
|
-
IoCContainer.controllers.set(target.constructor, actions);
|
|
18
|
-
return descriptor;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* IPC client event decorator for controllers
|
|
23
|
-
*/
|
|
24
|
-
export const ipcClientEvent = (method: keyof ClientDispatchEvents) =>
|
|
25
|
-
ipcDecorator(method, 'client');
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* IPC server event decorator for controllers
|
|
29
|
-
*/
|
|
30
|
-
export const ipcServerEvent = (method: keyof ServerDispatchEvents) =>
|
|
31
|
-
ipcDecorator(method, 'server');
|
|
4
|
+
import { IpcService } from '@/utils/ipc';
|
|
32
5
|
|
|
33
6
|
const shortcutDecorator = (name: string) => (target: any, methodName: string, descriptor?: any) => {
|
|
34
7
|
const actions = IoCContainer.shortcuts.get(target.constructor) || [];
|
|
@@ -68,10 +41,13 @@ interface IControllerModule {
|
|
|
68
41
|
beforeAppReady?(): void;
|
|
69
42
|
}
|
|
70
43
|
|
|
71
|
-
export class ControllerModule implements IControllerModule {
|
|
44
|
+
export class ControllerModule extends IpcService implements IControllerModule {
|
|
72
45
|
constructor(public app: App) {
|
|
46
|
+
super();
|
|
73
47
|
this.app = app;
|
|
74
48
|
}
|
|
75
49
|
}
|
|
76
50
|
|
|
77
51
|
export type IControlModule = typeof ControllerModule;
|
|
52
|
+
|
|
53
|
+
export { IpcMethod, IpcServerMethod } from '@/utils/ipc';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { CreateServicesResult, IpcServiceConstructor, MergeIpcService } from '@/utils/ipc';
|
|
2
|
+
|
|
3
|
+
import AuthCtr from './AuthCtr';
|
|
4
|
+
import BrowserWindowsCtr from './BrowserWindowsCtr';
|
|
5
|
+
import DevtoolsCtr from './DevtoolsCtr';
|
|
6
|
+
import LocalFileCtr from './LocalFileCtr';
|
|
7
|
+
import McpInstallCtr from './McpInstallCtr';
|
|
8
|
+
import MenuController from './MenuCtr';
|
|
9
|
+
import NetworkProxyCtr from './NetworkProxyCtr';
|
|
10
|
+
import NotificationCtr from './NotificationCtr';
|
|
11
|
+
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
|
|
12
|
+
import RemoteServerSyncCtr from './RemoteServerSyncCtr';
|
|
13
|
+
import ShellCommandCtr from './ShellCommandCtr';
|
|
14
|
+
import ShortcutController from './ShortcutCtr';
|
|
15
|
+
import SystemController from './SystemCtr';
|
|
16
|
+
import SystemServerCtr from './SystemServerCtr';
|
|
17
|
+
import TrayMenuCtr from './TrayMenuCtr';
|
|
18
|
+
import UpdaterCtr from './UpdaterCtr';
|
|
19
|
+
import UploadFileCtr from './UploadFileCtr';
|
|
20
|
+
import UploadFileServerCtr from './UploadFileServerCtr';
|
|
21
|
+
|
|
22
|
+
export const controllerIpcConstructors = [
|
|
23
|
+
AuthCtr,
|
|
24
|
+
BrowserWindowsCtr,
|
|
25
|
+
DevtoolsCtr,
|
|
26
|
+
LocalFileCtr,
|
|
27
|
+
McpInstallCtr,
|
|
28
|
+
MenuController,
|
|
29
|
+
NetworkProxyCtr,
|
|
30
|
+
NotificationCtr,
|
|
31
|
+
RemoteServerConfigCtr,
|
|
32
|
+
RemoteServerSyncCtr,
|
|
33
|
+
ShellCommandCtr,
|
|
34
|
+
ShortcutController,
|
|
35
|
+
SystemController,
|
|
36
|
+
TrayMenuCtr,
|
|
37
|
+
UpdaterCtr,
|
|
38
|
+
UploadFileCtr,
|
|
39
|
+
] as const satisfies readonly IpcServiceConstructor[];
|
|
40
|
+
|
|
41
|
+
type DesktopControllerIpcConstructors = typeof controllerIpcConstructors;
|
|
42
|
+
type DesktopControllerServices = CreateServicesResult<DesktopControllerIpcConstructors>;
|
|
43
|
+
export type DesktopIpcServices = MergeIpcService<DesktopControllerServices>;
|
|
44
|
+
|
|
45
|
+
export const controllerServerIpcConstructors = [
|
|
46
|
+
SystemServerCtr,
|
|
47
|
+
UploadFileServerCtr,
|
|
48
|
+
] as const satisfies readonly IpcServiceConstructor[];
|
|
49
|
+
|
|
50
|
+
type DesktopControllerServerConstructors = typeof controllerServerIpcConstructors;
|
|
51
|
+
type DesktopServerControllerServices = CreateServicesResult<DesktopControllerServerConstructors>;
|
|
52
|
+
export type DesktopServerIpcServices = MergeIpcService<DesktopServerControllerServices>;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { ElectronIPCEventHandler, ElectronIPCServer } from '@lobechat/electron-server-ipc';
|
|
2
|
-
import { Session, app,
|
|
2
|
+
import { Session, app, protocol } from 'electron';
|
|
3
3
|
import { macOS, windows } from 'electron-is';
|
|
4
4
|
import { pathExistsSync, remove } from 'fs-extra';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
|
|
8
8
|
import { name } from '@/../../package.json';
|
|
9
|
-
import {
|
|
9
|
+
import { LOCAL_DATABASE_DIR, buildDir, nextStandaloneDir } from '@/const/dir';
|
|
10
10
|
import { isDev } from '@/const/env';
|
|
11
11
|
import { IControlModule } from '@/controllers';
|
|
12
12
|
import { IServiceModule } from '@/services';
|
|
13
|
-
import {
|
|
13
|
+
import { getServerMethodMetadata } from '@/utils/ipc';
|
|
14
14
|
import { createLogger } from '@/utils/logger';
|
|
15
15
|
import { CustomRequestHandler, createHandler } from '@/utils/next-electron-rsc';
|
|
16
16
|
|
|
@@ -81,7 +81,7 @@ export class App {
|
|
|
81
81
|
|
|
82
82
|
// load controllers
|
|
83
83
|
const controllers: IControlModule[] = importAll(
|
|
84
|
-
|
|
84
|
+
import.meta.glob('@/controllers/*Ctr.ts', { eager: true }),
|
|
85
85
|
);
|
|
86
86
|
|
|
87
87
|
logger.debug(`Loading ${controllers.length} controllers`);
|
|
@@ -89,13 +89,13 @@ export class App {
|
|
|
89
89
|
|
|
90
90
|
// load services
|
|
91
91
|
const services: IServiceModule[] = importAll(
|
|
92
|
-
|
|
92
|
+
import.meta.glob('@/services/*Srv.ts', { eager: true }),
|
|
93
93
|
);
|
|
94
94
|
|
|
95
95
|
logger.debug(`Loading ${services.length} services`);
|
|
96
96
|
services.forEach((service) => this.addService(service));
|
|
97
97
|
|
|
98
|
-
this.
|
|
98
|
+
this.initializeServerIpcEvents();
|
|
99
99
|
|
|
100
100
|
this.i18n = new I18nManager(this);
|
|
101
101
|
this.browserManager = new BrowserManager(this);
|
|
@@ -268,10 +268,6 @@ export class App {
|
|
|
268
268
|
private services = new Map<Class<any>, any>();
|
|
269
269
|
|
|
270
270
|
private ipcServer: ElectronIPCServer;
|
|
271
|
-
/**
|
|
272
|
-
* events dispatched from webview layer
|
|
273
|
-
*/
|
|
274
|
-
private ipcClientEventMap: IPCEventMap = new Map();
|
|
275
271
|
private ipcServerEventMap: IPCEventMap = new Map();
|
|
276
272
|
shortcutMethodMap: ShortcutMethodMap = new Map();
|
|
277
273
|
protocolHandlerMap: ProtocolHandlerMap = new Map();
|
|
@@ -327,22 +323,13 @@ export class App {
|
|
|
327
323
|
const controller = new ControllerClass(this);
|
|
328
324
|
this.controllers.set(ControllerClass, controller);
|
|
329
325
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (event.mode === 'server') {
|
|
340
|
-
// Store all objects from event decorator in ipcServerEventMap
|
|
341
|
-
this.ipcServerEventMap.set(event.name, {
|
|
342
|
-
controller,
|
|
343
|
-
methodName: event.methodName,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
326
|
+
const serverMethods = getServerMethodMetadata(ControllerClass);
|
|
327
|
+
serverMethods?.forEach((methodName, propertyKey) => {
|
|
328
|
+
const channel = `${ControllerClass.groupName}.${methodName}`;
|
|
329
|
+
this.ipcServerEventMap.set(channel, {
|
|
330
|
+
controller,
|
|
331
|
+
methodName: propertyKey,
|
|
332
|
+
});
|
|
346
333
|
});
|
|
347
334
|
|
|
348
335
|
IoCContainer.shortcuts.get(ControllerClass)?.forEach((shortcut) => {
|
|
@@ -427,27 +414,8 @@ export class App {
|
|
|
427
414
|
}
|
|
428
415
|
}
|
|
429
416
|
|
|
430
|
-
private
|
|
431
|
-
logger.debug('Initializing IPC events');
|
|
432
|
-
// Register batch controller client events for render side consumption
|
|
433
|
-
this.ipcClientEventMap.forEach((eventInfo, key) => {
|
|
434
|
-
const { controller, methodName } = eventInfo;
|
|
435
|
-
|
|
436
|
-
ipcMain.handle(key, async (e, data) => {
|
|
437
|
-
// 从 WebContents 获取对应的 BrowserWindow id
|
|
438
|
-
const senderIdentifier = this.browserManager.getIdentifierByWebContents(e.sender);
|
|
439
|
-
try {
|
|
440
|
-
return await controller[methodName](data, {
|
|
441
|
-
identifier: senderIdentifier,
|
|
442
|
-
} as IpcClientEventSender);
|
|
443
|
-
} catch (error) {
|
|
444
|
-
logger.error(`Error handling IPC event ${key}:`, error);
|
|
445
|
-
return { error: error.message };
|
|
446
|
-
}
|
|
447
|
-
});
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
// Batch register server events from controllers for next server consumption
|
|
417
|
+
private initializeServerIpcEvents() {
|
|
418
|
+
logger.debug('Initializing IPC server events');
|
|
451
419
|
const ipcServerEvents = {} as ElectronIPCEventHandler;
|
|
452
420
|
|
|
453
421
|
this.ipcServerEventMap.forEach((eventInfo, key) => {
|
|
@@ -5,6 +5,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
5
5
|
|
|
6
6
|
import { LOCAL_DATABASE_DIR } from '@/const/dir';
|
|
7
7
|
|
|
8
|
+
// Import after mocks are set up
|
|
9
|
+
import { App } from '../App';
|
|
10
|
+
|
|
8
11
|
// Mock electron modules
|
|
9
12
|
vi.mock('electron', () => ({
|
|
10
13
|
app: {
|
|
@@ -24,6 +27,7 @@ vi.mock('electron', () => ({
|
|
|
24
27
|
},
|
|
25
28
|
ipcMain: {
|
|
26
29
|
handle: vi.fn(),
|
|
30
|
+
on: vi.fn(),
|
|
27
31
|
},
|
|
28
32
|
nativeTheme: {
|
|
29
33
|
on: vi.fn(),
|
|
@@ -166,9 +170,6 @@ vi.mock('@/utils/next-electron-rsc', () => ({
|
|
|
166
170
|
vi.mock('../../controllers/*Ctr.ts', () => ({}));
|
|
167
171
|
vi.mock('../../services/*Srv.ts', () => ({}));
|
|
168
172
|
|
|
169
|
-
// Import after mocks are set up
|
|
170
|
-
import { App } from '../App';
|
|
171
|
-
|
|
172
173
|
describe('App - Database Lock Cleanup', () => {
|
|
173
174
|
let appInstance: App;
|
|
174
175
|
let mockLockPath: string;
|
|
@@ -177,7 +178,7 @@ describe('App - Database Lock Cleanup', () => {
|
|
|
177
178
|
vi.clearAllMocks();
|
|
178
179
|
|
|
179
180
|
// Mock glob imports to return empty arrays
|
|
180
|
-
|
|
181
|
+
import.meta.glob = vi.fn(() => ({}));
|
|
181
182
|
|
|
182
183
|
mockLockPath = join('/mock/storage/path', LOCAL_DATABASE_DIR) + '.lock';
|
|
183
184
|
});
|
|
@@ -2,11 +2,6 @@
|
|
|
2
2
|
* 存储应用中需要用装饰器的类
|
|
3
3
|
*/
|
|
4
4
|
export class IoCContainer {
|
|
5
|
-
static controllers: WeakMap<
|
|
6
|
-
any,
|
|
7
|
-
{ methodName: string; mode: 'client' | 'server'; name: string }[]
|
|
8
|
-
> = new WeakMap();
|
|
9
|
-
|
|
10
5
|
static shortcuts: WeakMap<any, { methodName: string; name: string }[]> = new WeakMap();
|
|
11
6
|
|
|
12
7
|
static protocolHandlers: WeakMap<any, { action: string; methodName: string; urlType: string }[]> =
|
|
@@ -13,52 +13,6 @@ describe('IoCContainer', () => {
|
|
|
13
13
|
// For each test, use fresh class instances
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
describe('controllers WeakMap', () => {
|
|
17
|
-
it('should store controller metadata', () => {
|
|
18
|
-
const metadata = [{ methodName: 'handleMessage', mode: 'client' as const, name: 'message' }];
|
|
19
|
-
|
|
20
|
-
IoCContainer.controllers.set(TestController, metadata);
|
|
21
|
-
|
|
22
|
-
expect(IoCContainer.controllers.get(TestController)).toEqual(metadata);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('should allow multiple controllers', () => {
|
|
26
|
-
const metadata1 = [{ methodName: 'method1', mode: 'client' as const, name: 'action1' }];
|
|
27
|
-
const metadata2 = [{ methodName: 'method2', mode: 'server' as const, name: 'action2' }];
|
|
28
|
-
|
|
29
|
-
IoCContainer.controllers.set(TestController, metadata1);
|
|
30
|
-
IoCContainer.controllers.set(AnotherController, metadata2);
|
|
31
|
-
|
|
32
|
-
expect(IoCContainer.controllers.get(TestController)).toEqual(metadata1);
|
|
33
|
-
expect(IoCContainer.controllers.get(AnotherController)).toEqual(metadata2);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should allow overwriting controller metadata', () => {
|
|
37
|
-
const oldMetadata = [{ methodName: 'oldMethod', mode: 'client' as const, name: 'old' }];
|
|
38
|
-
const newMetadata = [{ methodName: 'newMethod', mode: 'server' as const, name: 'new' }];
|
|
39
|
-
|
|
40
|
-
IoCContainer.controllers.set(TestController, oldMetadata);
|
|
41
|
-
IoCContainer.controllers.set(TestController, newMetadata);
|
|
42
|
-
|
|
43
|
-
expect(IoCContainer.controllers.get(TestController)).toEqual(newMetadata);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should support multiple methods per controller', () => {
|
|
47
|
-
const metadata = [
|
|
48
|
-
{ methodName: 'method1', mode: 'client' as const, name: 'action1' },
|
|
49
|
-
{ methodName: 'method2', mode: 'server' as const, name: 'action2' },
|
|
50
|
-
{ methodName: 'method3', mode: 'client' as const, name: 'action3' },
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
IoCContainer.controllers.set(TestController, metadata);
|
|
54
|
-
|
|
55
|
-
const stored = IoCContainer.controllers.get(TestController);
|
|
56
|
-
expect(stored).toHaveLength(3);
|
|
57
|
-
expect(stored?.[0].mode).toBe('client');
|
|
58
|
-
expect(stored?.[1].mode).toBe('server');
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
16
|
describe('shortcuts WeakMap', () => {
|
|
63
17
|
it('should store shortcut metadata', () => {
|
|
64
18
|
const metadata = [{ methodName: 'toggleDarkMode', name: 'CmdOrCtrl+Shift+D' }];
|
|
@@ -141,10 +95,6 @@ describe('IoCContainer', () => {
|
|
|
141
95
|
});
|
|
142
96
|
|
|
143
97
|
describe('static properties', () => {
|
|
144
|
-
it('should have controllers as a WeakMap', () => {
|
|
145
|
-
expect(IoCContainer.controllers).toBeInstanceOf(WeakMap);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
98
|
it('should have shortcuts as a WeakMap', () => {
|
|
149
99
|
expect(IoCContainer.shortcuts).toBeInstanceOf(WeakMap);
|
|
150
100
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DesktopIpcServices } from './controllers/registry';
|
|
2
|
+
|
|
3
|
+
declare module '@lobechat/electron-client-ipc' {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
5
|
+
interface DesktopIpcServicesMap extends DesktopIpcServices {}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export { type DesktopIpcServices, type DesktopServerIpcServices } from './controllers/registry';
|
|
@@ -17,6 +17,12 @@ const repoRoot = path.resolve(__dirname, '../../../../..');
|
|
|
17
17
|
|
|
18
18
|
describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integration', () => {
|
|
19
19
|
const searchService = new MacOSSearchServiceImpl();
|
|
20
|
+
const ensureResults = (results: unknown[], context: string) => {
|
|
21
|
+
if (results.length > 0) return true;
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
console.warn(`⚠️ Spotlight returned 0 results for ${context} - indexing may be incomplete`);
|
|
24
|
+
return false;
|
|
25
|
+
};
|
|
20
26
|
|
|
21
27
|
describe('checkSearchServiceStatus', () => {
|
|
22
28
|
it('should verify Spotlight is available on macOS', async () => {
|
|
@@ -34,7 +40,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
|
|
34
40
|
onlyIn: repoRoot,
|
|
35
41
|
});
|
|
36
42
|
|
|
37
|
-
|
|
43
|
+
if (!ensureResults(results, 'package.json search')) return;
|
|
38
44
|
|
|
39
45
|
// Should find at least one package.json
|
|
40
46
|
const packageJson = results.find((r) => r.name === 'package.json');
|
|
@@ -49,7 +55,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
|
|
49
55
|
limit: 10,
|
|
50
56
|
onlyIn: repoRoot,
|
|
51
57
|
});
|
|
52
|
-
|
|
58
|
+
if (!ensureResults(results, 'README search')) return;
|
|
53
59
|
|
|
54
60
|
// Should contain markdown files
|
|
55
61
|
const mdFile = results.find((r) => r.type === 'md');
|
|
@@ -64,7 +70,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
|
|
64
70
|
onlyIn: repoRoot,
|
|
65
71
|
});
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
if (!ensureResults(results, 'TypeScript file search')) return;
|
|
68
74
|
|
|
69
75
|
// Should find the macOS.ts implementation file
|
|
70
76
|
const macOSFile = results.find((r) => r.name.includes('macOS') && r.type === 'ts');
|
|
@@ -106,7 +112,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
|
|
106
112
|
onlyIn: repoRoot,
|
|
107
113
|
});
|
|
108
114
|
|
|
109
|
-
|
|
115
|
+
if (!ensureResults(results, 'test file search')) return;
|
|
110
116
|
|
|
111
117
|
// Should find test files (can be in __tests__ directory or co-located with source files)
|
|
112
118
|
const testFile = results.find((r) => r.name.endsWith('.test.ts'));
|
|
@@ -161,6 +167,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
|
|
161
167
|
onlyIn: repoRoot,
|
|
162
168
|
});
|
|
163
169
|
|
|
170
|
+
if (!ensureResults(results, 'TypeScript identification')) return;
|
|
164
171
|
const tsFile = results.find((r) => r.name === 'LocalFileCtr.ts');
|
|
165
172
|
if (tsFile) {
|
|
166
173
|
expect(tsFile.type).toBe('ts');
|
|
@@ -176,6 +183,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
|
|
176
183
|
onlyIn: repoRoot,
|
|
177
184
|
});
|
|
178
185
|
|
|
186
|
+
if (!ensureResults(results, 'JSON identification')) return;
|
|
179
187
|
const jsonFile = results.find((r) => r.name.includes('tsconfig') && r.type === 'json');
|
|
180
188
|
if (jsonFile) {
|
|
181
189
|
expect(jsonFile.type).toBe('json');
|
|
@@ -191,6 +199,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
|
|
191
199
|
onlyIn: repoRoot,
|
|
192
200
|
});
|
|
193
201
|
|
|
202
|
+
if (!ensureResults(results, 'directory identification')) return;
|
|
194
203
|
const testDir = results.find((r) => r.name === '__tests__' && r.isDirectory);
|
|
195
204
|
if (testDir) {
|
|
196
205
|
expect(testDir.isDirectory).toBe(true);
|
|
@@ -221,7 +230,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
|
|
221
230
|
onlyIn: repoRoot,
|
|
222
231
|
});
|
|
223
232
|
|
|
224
|
-
|
|
233
|
+
if (!ensureResults(results, 'file metadata read')) return;
|
|
225
234
|
|
|
226
235
|
const file = results[0];
|
|
227
236
|
|
|
@@ -279,7 +288,7 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
|
|
279
288
|
onlyIn: repoRoot,
|
|
280
289
|
});
|
|
281
290
|
|
|
282
|
-
|
|
291
|
+
if (!ensureResults(results, 'fuzzy search accuracy')) return;
|
|
283
292
|
|
|
284
293
|
// Should find LocalFileCtr.ts or similar files
|
|
285
294
|
const found = results.some(
|
|
@@ -319,8 +328,8 @@ describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integrati
|
|
|
319
328
|
});
|
|
320
329
|
|
|
321
330
|
// Both searches should find similar files
|
|
322
|
-
|
|
323
|
-
|
|
331
|
+
if (!ensureResults(lowerResults, 'case-insensitive search (lower)')) return;
|
|
332
|
+
if (!ensureResults(upperResults, 'case-insensitive search (upper)')) return;
|
|
324
333
|
});
|
|
325
334
|
});
|
|
326
335
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { IpcContext } from '../base';
|
|
4
|
+
import {
|
|
5
|
+
IpcMethod,
|
|
6
|
+
IpcServerMethod,
|
|
7
|
+
IpcService,
|
|
8
|
+
getIpcContext,
|
|
9
|
+
getServerMethodMetadata,
|
|
10
|
+
} from '../base';
|
|
11
|
+
|
|
12
|
+
const { ipcMainHandleMock } = vi.hoisted(() => ({
|
|
13
|
+
ipcMainHandleMock: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('electron', () => ({
|
|
17
|
+
ipcMain: {
|
|
18
|
+
handle: ipcMainHandleMock,
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe('ipc service base', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
ipcMainHandleMock.mockClear();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('registers handlers and forwards payload/context correctly', async () => {
|
|
28
|
+
class TestService extends IpcService {
|
|
29
|
+
static readonly groupName = 'test';
|
|
30
|
+
public lastCall: { payload: string | undefined; context?: IpcContext } | null = null;
|
|
31
|
+
|
|
32
|
+
@IpcMethod()
|
|
33
|
+
ping(payload?: string) {
|
|
34
|
+
this.lastCall = { context: getIpcContext(), payload };
|
|
35
|
+
return 'pong';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const service = new TestService();
|
|
40
|
+
|
|
41
|
+
expect(service).toBeTruthy();
|
|
42
|
+
expect(ipcMainHandleMock).toHaveBeenCalledWith('test.ping', expect.any(Function));
|
|
43
|
+
|
|
44
|
+
const handler = ipcMainHandleMock.mock.calls[0][1];
|
|
45
|
+
const fakeSender = { id: 1 } as any;
|
|
46
|
+
const fakeEvent = { sender: fakeSender } as any;
|
|
47
|
+
|
|
48
|
+
const result = await handler(fakeEvent, 'hello');
|
|
49
|
+
|
|
50
|
+
expect(result).toBe('pong');
|
|
51
|
+
expect(service.lastCall).toEqual({
|
|
52
|
+
context: { event: fakeEvent, sender: fakeSender },
|
|
53
|
+
payload: 'hello',
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('allows direct method invocation without IPC context', () => {
|
|
58
|
+
class DirectCallService extends IpcService {
|
|
59
|
+
static readonly groupName = 'direct';
|
|
60
|
+
public invokedWith: string | null = null;
|
|
61
|
+
|
|
62
|
+
@IpcMethod()
|
|
63
|
+
run(payload: string) {
|
|
64
|
+
this.invokedWith = payload;
|
|
65
|
+
return payload.toUpperCase();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const service = new DirectCallService();
|
|
70
|
+
const result = service.run('test');
|
|
71
|
+
|
|
72
|
+
expect(result).toBe('TEST');
|
|
73
|
+
expect(service.invokedWith).toBe('test');
|
|
74
|
+
expect(ipcMainHandleMock).toHaveBeenCalledWith('direct.run', expect.any(Function));
|
|
75
|
+
});
|
|
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
|
+
});
|