@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
@@ -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, ipcMain, protocol } from 'electron';
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 { buildDir, LOCAL_DATABASE_DIR, nextStandaloneDir } from '@/const/dir';
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 { IpcClientEventSender } from '@/types/ipcClientEvent';
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
- (import.meta as any).glob('@/controllers/*Ctr.ts', { eager: true }),
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
- (import.meta as any).glob('@/services/*Srv.ts', { eager: true }),
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.initializeIPCEvents();
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
- IoCContainer.controllers.get(ControllerClass)?.forEach((event) => {
331
- if (event.mode === 'client') {
332
- // Store all objects from event decorator in ipcClientEventMap
333
- this.ipcClientEventMap.set(event.name, {
334
- controller,
335
- methodName: event.methodName,
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 initializeIPCEvents() {
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
- (import.meta as any).glob = vi.fn(() => ({}));
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';
@@ -0,0 +1,2 @@
1
+ // Export types for renderer/server to use
2
+ export type { DesktopIpcServices, DesktopServerIpcServices } from './controllers/registry';
@@ -0,0 +1,3 @@
1
+ import 'vite/client';
2
+
3
+ export {};
@@ -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
- expect(results.length).toBeGreaterThan(0);
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
- expect(results.length).toBeGreaterThan(0);
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
- expect(results.length).toBeGreaterThan(0);
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
- expect(results.length).toBeGreaterThan(0);
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
- expect(results.length).toBeGreaterThan(0);
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
- expect(results.length).toBeGreaterThan(0);
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
- expect(lowerResults.length).toBeGreaterThan(0);
323
- expect(upperResults.length).toBeGreaterThan(0);
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,10 @@
1
+ {
2
+ "name": "@lobehub/desktop-ipc-typings",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "main": "./exports.d.ts",
6
+ "types": "./exports.d.ts",
7
+ "exports": {
8
+ ".": "./exports.d.ts"
9
+ }
10
+ }
@@ -37,7 +37,7 @@ interface UploadFileParams {
37
37
  type: string;
38
38
  }
39
39
 
40
- interface FileMetadata {
40
+ export interface FileMetadata {
41
41
  date: string;
42
42
  dirname: string;
43
43
  filename: string;
@@ -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
+ });