@lobehub/chat 1.82.10 → 1.83.0

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 (195) hide show
  1. package/.env.desktop +1 -2
  2. package/.github/workflows/{release-desktop.yml → desktop-pr-build.yml} +59 -137
  3. package/.github/workflows/release-desktop-beta.yml +196 -0
  4. package/CHANGELOG.md +25 -0
  5. package/apps/desktop/.i18nrc.js +31 -0
  6. package/apps/desktop/Development.md +47 -0
  7. package/apps/desktop/README.md +6 -0
  8. package/apps/desktop/build/Icon-beta.icns +0 -0
  9. package/apps/desktop/build/Icon-nightly.icns +0 -0
  10. package/apps/desktop/build/Icon.icns +0 -0
  11. package/apps/desktop/build/entitlements.mac.plist +12 -0
  12. package/apps/desktop/build/favicon.ico +0 -0
  13. package/apps/desktop/build/icon-beta.png +0 -0
  14. package/apps/desktop/build/icon-dev.png +0 -0
  15. package/apps/desktop/build/icon-nightly.ico +0 -0
  16. package/apps/desktop/build/icon-nightly.png +0 -0
  17. package/apps/desktop/build/icon.ico +0 -0
  18. package/apps/desktop/build/icon.png +0 -0
  19. package/apps/desktop/dev-app-update.yml +6 -0
  20. package/apps/desktop/electron-builder.js +92 -0
  21. package/apps/desktop/electron.vite.config.ts +40 -0
  22. package/apps/desktop/package.json +72 -0
  23. package/apps/desktop/pnpm-workspace.yaml +5 -0
  24. package/apps/desktop/resources/error.html +136 -0
  25. package/apps/desktop/resources/locales/ar/common.json +32 -0
  26. package/apps/desktop/resources/locales/ar/dialog.json +31 -0
  27. package/apps/desktop/resources/locales/ar/menu.json +70 -0
  28. package/apps/desktop/resources/locales/bg-BG/common.json +32 -0
  29. package/apps/desktop/resources/locales/bg-BG/dialog.json +31 -0
  30. package/apps/desktop/resources/locales/bg-BG/menu.json +70 -0
  31. package/apps/desktop/resources/locales/de-DE/common.json +32 -0
  32. package/apps/desktop/resources/locales/de-DE/dialog.json +31 -0
  33. package/apps/desktop/resources/locales/de-DE/menu.json +70 -0
  34. package/apps/desktop/resources/locales/en-US/common.json +32 -0
  35. package/apps/desktop/resources/locales/en-US/dialog.json +31 -0
  36. package/apps/desktop/resources/locales/en-US/menu.json +70 -0
  37. package/apps/desktop/resources/locales/es-ES/common.json +32 -0
  38. package/apps/desktop/resources/locales/es-ES/dialog.json +31 -0
  39. package/apps/desktop/resources/locales/es-ES/menu.json +70 -0
  40. package/apps/desktop/resources/locales/fa-IR/common.json +32 -0
  41. package/apps/desktop/resources/locales/fa-IR/dialog.json +31 -0
  42. package/apps/desktop/resources/locales/fa-IR/menu.json +70 -0
  43. package/apps/desktop/resources/locales/fr-FR/common.json +32 -0
  44. package/apps/desktop/resources/locales/fr-FR/dialog.json +31 -0
  45. package/apps/desktop/resources/locales/fr-FR/menu.json +70 -0
  46. package/apps/desktop/resources/locales/it-IT/common.json +32 -0
  47. package/apps/desktop/resources/locales/it-IT/dialog.json +31 -0
  48. package/apps/desktop/resources/locales/it-IT/menu.json +70 -0
  49. package/apps/desktop/resources/locales/ja-JP/common.json +32 -0
  50. package/apps/desktop/resources/locales/ja-JP/dialog.json +31 -0
  51. package/apps/desktop/resources/locales/ja-JP/menu.json +70 -0
  52. package/apps/desktop/resources/locales/ko-KR/common.json +32 -0
  53. package/apps/desktop/resources/locales/ko-KR/dialog.json +31 -0
  54. package/apps/desktop/resources/locales/ko-KR/menu.json +70 -0
  55. package/apps/desktop/resources/locales/nl-NL/common.json +32 -0
  56. package/apps/desktop/resources/locales/nl-NL/dialog.json +31 -0
  57. package/apps/desktop/resources/locales/nl-NL/menu.json +70 -0
  58. package/apps/desktop/resources/locales/pl-PL/common.json +32 -0
  59. package/apps/desktop/resources/locales/pl-PL/dialog.json +31 -0
  60. package/apps/desktop/resources/locales/pl-PL/menu.json +70 -0
  61. package/apps/desktop/resources/locales/pt-BR/common.json +32 -0
  62. package/apps/desktop/resources/locales/pt-BR/dialog.json +31 -0
  63. package/apps/desktop/resources/locales/pt-BR/menu.json +70 -0
  64. package/apps/desktop/resources/locales/ru-RU/common.json +32 -0
  65. package/apps/desktop/resources/locales/ru-RU/dialog.json +31 -0
  66. package/apps/desktop/resources/locales/ru-RU/menu.json +70 -0
  67. package/apps/desktop/resources/locales/tr-TR/common.json +32 -0
  68. package/apps/desktop/resources/locales/tr-TR/dialog.json +31 -0
  69. package/apps/desktop/resources/locales/tr-TR/menu.json +70 -0
  70. package/apps/desktop/resources/locales/vi-VN/common.json +32 -0
  71. package/apps/desktop/resources/locales/vi-VN/dialog.json +31 -0
  72. package/apps/desktop/resources/locales/vi-VN/menu.json +70 -0
  73. package/apps/desktop/resources/locales/zh-CN/common.json +32 -0
  74. package/apps/desktop/resources/locales/zh-CN/dialog.json +31 -0
  75. package/apps/desktop/resources/locales/zh-CN/menu.json +70 -0
  76. package/apps/desktop/resources/locales/zh-TW/common.json +32 -0
  77. package/apps/desktop/resources/locales/zh-TW/dialog.json +31 -0
  78. package/apps/desktop/resources/locales/zh-TW/menu.json +70 -0
  79. package/apps/desktop/resources/splash.html +88 -0
  80. package/apps/desktop/scripts/i18nWorkflow/const.ts +18 -0
  81. package/apps/desktop/scripts/i18nWorkflow/genDefaultLocale.ts +35 -0
  82. package/apps/desktop/scripts/i18nWorkflow/genDiff.ts +57 -0
  83. package/apps/desktop/scripts/i18nWorkflow/index.ts +35 -0
  84. package/apps/desktop/scripts/i18nWorkflow/utils.ts +54 -0
  85. package/apps/desktop/scripts/pglite-server.ts +14 -0
  86. package/apps/desktop/src/common/routes.ts +78 -0
  87. package/apps/desktop/src/main/appBrowsers.ts +47 -0
  88. package/apps/desktop/src/main/const/dir.ts +29 -0
  89. package/apps/desktop/src/main/const/env.ts +3 -0
  90. package/apps/desktop/src/main/const/store.ts +22 -0
  91. package/apps/desktop/src/main/controllers/AuthCtr.ts +390 -0
  92. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +95 -0
  93. package/apps/desktop/src/main/controllers/DevtoolsCtr.ts +9 -0
  94. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +380 -0
  95. package/apps/desktop/src/main/controllers/MenuCtr.ts +29 -0
  96. package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +335 -0
  97. package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +321 -0
  98. package/apps/desktop/src/main/controllers/ShortcutCtr.ts +19 -0
  99. package/apps/desktop/src/main/controllers/SystemCtr.ts +93 -0
  100. package/apps/desktop/src/main/controllers/UpdaterCtr.ts +43 -0
  101. package/apps/desktop/src/main/controllers/UploadFileCtr.ts +34 -0
  102. package/apps/desktop/src/main/controllers/_template.ts +9 -0
  103. package/apps/desktop/src/main/controllers/index.ts +58 -0
  104. package/apps/desktop/src/main/core/App.ts +370 -0
  105. package/apps/desktop/src/main/core/Browser.ts +345 -0
  106. package/apps/desktop/src/main/core/BrowserManager.ts +154 -0
  107. package/apps/desktop/src/main/core/I18nManager.ts +185 -0
  108. package/apps/desktop/src/main/core/IoCContainer.ts +12 -0
  109. package/apps/desktop/src/main/core/MenuManager.ts +64 -0
  110. package/apps/desktop/src/main/core/ShortcutManager.ts +173 -0
  111. package/apps/desktop/src/main/core/StoreManager.ts +89 -0
  112. package/apps/desktop/src/main/core/UpdaterManager.ts +321 -0
  113. package/apps/desktop/src/main/index.ts +5 -0
  114. package/apps/desktop/src/main/locales/default/common.ts +34 -0
  115. package/apps/desktop/src/main/locales/default/dialog.ts +33 -0
  116. package/apps/desktop/src/main/locales/default/index.ts +11 -0
  117. package/apps/desktop/src/main/locales/default/menu.ts +72 -0
  118. package/apps/desktop/src/main/locales/resources.ts +35 -0
  119. package/apps/desktop/src/main/menus/impls/BaseMenuPlatform.ts +10 -0
  120. package/apps/desktop/src/main/menus/impls/linux.ts +243 -0
  121. package/apps/desktop/src/main/menus/impls/macOS.ts +360 -0
  122. package/apps/desktop/src/main/menus/impls/windows.ts +226 -0
  123. package/apps/desktop/src/main/menus/index.ts +34 -0
  124. package/apps/desktop/src/main/menus/types.ts +28 -0
  125. package/apps/desktop/src/main/modules/fileSearch/impl/macOS.ts +577 -0
  126. package/apps/desktop/src/main/modules/fileSearch/index.ts +23 -0
  127. package/apps/desktop/src/main/modules/fileSearch/type.ts +27 -0
  128. package/apps/desktop/src/main/modules/updater/configs.ts +22 -0
  129. package/apps/desktop/src/main/modules/updater/utils.ts +33 -0
  130. package/apps/desktop/src/main/services/fileSearchSrv.ts +35 -0
  131. package/apps/desktop/src/main/services/fileSrv.ts +255 -0
  132. package/apps/desktop/src/main/services/index.ts +9 -0
  133. package/apps/desktop/src/main/shortcuts/config.ts +18 -0
  134. package/apps/desktop/src/main/shortcuts/index.ts +1 -0
  135. package/apps/desktop/src/main/types/fileSearch.ts +51 -0
  136. package/apps/desktop/src/main/types/store.ts +14 -0
  137. package/apps/desktop/src/main/utils/file-system.ts +15 -0
  138. package/apps/desktop/src/main/utils/logger.ts +44 -0
  139. package/apps/desktop/src/main/utils/next-electron-rsc.ts +383 -0
  140. package/apps/desktop/src/preload/electronApi.ts +18 -0
  141. package/apps/desktop/src/preload/index.ts +14 -0
  142. package/apps/desktop/src/preload/invoke.ts +10 -0
  143. package/apps/desktop/src/preload/routeInterceptor.ts +162 -0
  144. package/apps/desktop/tsconfig.json +21 -0
  145. package/changelog/v1.json +9 -0
  146. package/package.json +1 -1
  147. package/packages/electron-client-ipc/src/events/remoteServer.ts +11 -4
  148. package/packages/electron-client-ipc/src/types/dataSync.ts +15 -0
  149. package/packages/electron-client-ipc/src/types/index.ts +2 -1
  150. package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +21 -0
  151. package/packages/electron-server-ipc/src/const.ts +3 -3
  152. package/packages/electron-server-ipc/src/ipcClient.test.ts +7 -6
  153. package/packages/electron-server-ipc/src/ipcClient.ts +17 -8
  154. package/packages/electron-server-ipc/src/ipcServer.ts +7 -3
  155. package/scripts/electronWorkflow/setDesktopVersion.ts +60 -43
  156. package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
  157. package/src/components/Analytics/Desktop.tsx +19 -0
  158. package/src/components/Analytics/index.tsx +3 -0
  159. package/src/database/core/db-adaptor.ts +4 -1
  160. package/src/database/core/electron.ts +317 -0
  161. package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx → features/ElectronTitlebar/Connection/ConnectionMode.tsx} +24 -21
  162. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Option.tsx +3 -5
  163. package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx → features/ElectronTitlebar/Connection/RemoteStatus.tsx} +10 -7
  164. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/index.tsx +4 -4
  165. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateModal.tsx +2 -1
  166. package/src/libs/trpc/client/async.ts +6 -0
  167. package/src/libs/trpc/client/edge.ts +6 -0
  168. package/src/libs/trpc/client/helpers/desktopRemoteRPCFetch.ts +72 -0
  169. package/src/libs/trpc/client/index.ts +1 -0
  170. package/src/libs/trpc/client/lambda.ts +10 -1
  171. package/src/libs/trpc/client/tools.ts +6 -0
  172. package/src/server/globalConfig/index.ts +0 -3
  173. package/src/server/modules/ElectronIPCClient/index.ts +3 -1
  174. package/src/server/routers/desktop/index.ts +2 -0
  175. package/src/server/routers/desktop/mcp.ts +47 -0
  176. package/src/server/routers/lambda/user.ts +38 -23
  177. package/src/server/routers/tools/mcp.ts +0 -6
  178. package/src/services/electron/remoteServer.ts +4 -4
  179. package/src/services/mcp.ts +17 -7
  180. package/src/services/upload.ts +9 -0
  181. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +11 -2
  182. package/src/store/chat/slices/builtinTool/actions/localFile.ts +110 -53
  183. package/src/store/electron/actions/sync.ts +20 -19
  184. package/src/store/electron/initialState.ts +3 -3
  185. package/src/store/electron/selectors/sync.ts +6 -3
  186. package/src/store/electron/store.ts +2 -0
  187. package/src/store/file/slices/upload/action.ts +11 -3
  188. package/src/store/tool/selectors/tool.ts +10 -1
  189. package/src/utils/fetch/headers.ts +27 -0
  190. package/src/utils/fetch/index.ts +2 -0
  191. package/src/utils/fetch/request.ts +28 -0
  192. package/packages/electron-client-ipc/src/types/remoteServer.ts +0 -8
  193. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Waiting.tsx +0 -0
  194. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateNotification.tsx +0 -0
  195. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/index.tsx +0 -0
@@ -0,0 +1,93 @@
1
+ import { ElectronAppState } from '@lobechat/electron-client-ipc';
2
+ import { app, systemPreferences } from 'electron';
3
+ import { macOS } from 'electron-is';
4
+ import { readFileSync, writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import process from 'node:process';
7
+
8
+ import { DB_SCHEMA_HASH_FILENAME, LOCAL_DATABASE_DIR, userDataDir } from '@/const/dir';
9
+
10
+ import { ControllerModule, ipcClientEvent, ipcServerEvent } from './index';
11
+
12
+ export default class SystemController extends ControllerModule {
13
+ /**
14
+ * Handles the 'getDesktopAppState' IPC request.
15
+ * Gathers essential application and system information.
16
+ */
17
+ @ipcClientEvent('getDesktopAppState')
18
+ async getAppState(): Promise<ElectronAppState> {
19
+ const platform = process.platform;
20
+ const arch = process.arch;
21
+
22
+ return {
23
+ // System Info
24
+ arch,
25
+ isLinux: platform === 'linux',
26
+ isMac: platform === 'darwin',
27
+ isWindows: platform === 'win32',
28
+ platform: platform as 'darwin' | 'win32' | 'linux',
29
+ userPath: {
30
+ // User Paths (ensure keys match UserPathData / DesktopAppState interface)
31
+ desktop: app.getPath('desktop'),
32
+ documents: app.getPath('documents'),
33
+ downloads: app.getPath('downloads'),
34
+ home: app.getPath('home'),
35
+ music: app.getPath('music'),
36
+ pictures: app.getPath('pictures'),
37
+ userData: app.getPath('userData'),
38
+ videos: app.getPath('videos'),
39
+ },
40
+ };
41
+ }
42
+
43
+ /**
44
+ * 检查可用性
45
+ */
46
+ @ipcClientEvent('checkSystemAccessibility')
47
+ checkAccessibilityForMacOS() {
48
+ if (!macOS()) return;
49
+ return systemPreferences.isTrustedAccessibilityClient(true);
50
+ }
51
+
52
+ /**
53
+ * 更新应用语言设置
54
+ */
55
+ @ipcClientEvent('updateLocale')
56
+ async updateLocale(locale: string) {
57
+ // 保存语言设置
58
+ this.app.storeManager.set('locale', locale);
59
+
60
+ // 更新i18n实例的语言
61
+ await this.app.i18n.changeLanguage(locale === 'auto' ? app.getLocale() : locale);
62
+
63
+ return { success: true };
64
+ }
65
+
66
+ @ipcServerEvent('getDatabasePath')
67
+ async getDatabasePath() {
68
+ return join(this.app.appStoragePath, LOCAL_DATABASE_DIR);
69
+ }
70
+
71
+ @ipcServerEvent('getDatabaseSchemaHash')
72
+ async getDatabaseSchemaHash() {
73
+ try {
74
+ return readFileSync(this.DB_SCHEMA_HASH_PATH, 'utf8');
75
+ } catch {
76
+ return undefined;
77
+ }
78
+ }
79
+
80
+ @ipcServerEvent('getUserDataPath')
81
+ async getUserDataPath() {
82
+ return userDataDir;
83
+ }
84
+
85
+ @ipcServerEvent('setDatabaseSchemaHash')
86
+ async setDatabaseSchemaHash(hash: string) {
87
+ writeFileSync(this.DB_SCHEMA_HASH_PATH, hash, 'utf8');
88
+ }
89
+
90
+ private get DB_SCHEMA_HASH_PATH() {
91
+ return join(this.app.appStoragePath, DB_SCHEMA_HASH_FILENAME);
92
+ }
93
+ }
@@ -0,0 +1,43 @@
1
+ import { createLogger } from '@/utils/logger';
2
+
3
+ import { ControllerModule, ipcClientEvent } from './index';
4
+
5
+ const logger = createLogger('controllers:UpdaterCtr');
6
+
7
+ export default class UpdaterCtr extends ControllerModule {
8
+ /**
9
+ * 检查更新
10
+ */
11
+ @ipcClientEvent('checkUpdate')
12
+ async checkForUpdates() {
13
+ logger.info('Check for updates requested');
14
+ await this.app.updaterManager.checkForUpdates();
15
+ }
16
+
17
+ /**
18
+ * 下载更新
19
+ */
20
+ @ipcClientEvent('downloadUpdate')
21
+ async downloadUpdate() {
22
+ logger.info('Download update requested');
23
+ await this.app.updaterManager.downloadUpdate();
24
+ }
25
+
26
+ /**
27
+ * 关闭应用并安装更新
28
+ */
29
+ @ipcClientEvent('installNow')
30
+ quitAndInstallUpdate() {
31
+ logger.info('Quit and install update requested');
32
+ this.app.updaterManager.installNow();
33
+ }
34
+
35
+ /**
36
+ * 下次启动时安装更新
37
+ */
38
+ @ipcClientEvent('installLater')
39
+ installLater() {
40
+ logger.info('Install later requested');
41
+ this.app.updaterManager.installLater();
42
+ }
43
+ }
@@ -0,0 +1,34 @@
1
+ import FileService from '@/services/fileSrv';
2
+
3
+ import { ControllerModule, ipcClientEvent, ipcServerEvent } from './index';
4
+
5
+ interface UploadFileParams {
6
+ content: ArrayBuffer;
7
+ filename: string;
8
+ hash: string;
9
+ path: string;
10
+ type: string;
11
+ }
12
+
13
+ export default class UploadFileCtr extends ControllerModule {
14
+ private get fileService() {
15
+ return this.app.getService(FileService);
16
+ }
17
+
18
+ @ipcClientEvent('createFile')
19
+ async uploadFile(params: UploadFileParams) {
20
+ return this.fileService.uploadFile(params);
21
+ }
22
+
23
+ // ======== server event
24
+
25
+ @ipcServerEvent('getStaticFilePath')
26
+ async getFileUrlById(id: string) {
27
+ return this.fileService.getFilePath(id);
28
+ }
29
+
30
+ @ipcServerEvent('deleteFiles')
31
+ async deleteFiles(paths: string[]) {
32
+ return this.fileService.deleteFiles(paths);
33
+ }
34
+ }
@@ -0,0 +1,9 @@
1
+ import { ControllerModule, ipcClientEvent } from './index';
2
+
3
+ export default class DevtoolsCtr extends ControllerModule {
4
+ @ipcClientEvent('openDevtools')
5
+ async openDevtools() {
6
+ const devtoolsBrowser = this.app.browserManager.retrieveByIdentifier('devtools');
7
+ devtoolsBrowser.show();
8
+ }
9
+ }
@@ -0,0 +1,58 @@
1
+ import type { ClientDispatchEvents } from '@lobechat/electron-client-ipc';
2
+ import type { ServerDispatchEvents } from '@lobechat/electron-server-ipc';
3
+
4
+ import type { App } from '@/core/App';
5
+ import { IoCContainer } from '@/core/IoCContainer';
6
+ 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
+ * controller 用的 ipc client event 装饰器
23
+ */
24
+ export const ipcClientEvent = (method: keyof ClientDispatchEvents) =>
25
+ ipcDecorator(method, 'client');
26
+
27
+ /**
28
+ * controller 用的 ipc server event 装饰器
29
+ */
30
+ export const ipcServerEvent = (method: keyof ServerDispatchEvents) =>
31
+ ipcDecorator(method, 'server');
32
+
33
+ const shortcutDecorator = (name: string) => (target: any, methodName: string, descriptor?: any) => {
34
+ const actions = IoCContainer.shortcuts.get(target.constructor) || [];
35
+ actions.push({ methodName, name });
36
+
37
+ IoCContainer.shortcuts.set(target.constructor, actions);
38
+
39
+ return descriptor;
40
+ };
41
+
42
+ /**
43
+ * shortcut inject decorator
44
+ */
45
+ export const shortcut = (method: ShortcutActionType) => shortcutDecorator(method);
46
+
47
+ interface IControllerModule {
48
+ afterAppReady?(): void;
49
+ app: App;
50
+ beforeAppReady?(): void;
51
+ }
52
+ export class ControllerModule implements IControllerModule {
53
+ constructor(public app: App) {
54
+ this.app = app;
55
+ }
56
+ }
57
+
58
+ export type IControlModule = typeof ControllerModule;
@@ -0,0 +1,370 @@
1
+ import { ElectronIPCEventHandler, ElectronIPCServer } from '@lobechat/electron-server-ipc';
2
+ import { Session, app, ipcMain, protocol } from 'electron';
3
+ import { macOS, windows } from 'electron-is';
4
+ import { join } from 'node:path';
5
+
6
+ import { name } from '@/../../package.json';
7
+ import { buildDir, nextStandaloneDir } from '@/const/dir';
8
+ import { isDev } from '@/const/env';
9
+ import { IControlModule } from '@/controllers';
10
+ import { IServiceModule } from '@/services';
11
+ import { createLogger } from '@/utils/logger';
12
+ import { CustomRequestHandler, createHandler } from '@/utils/next-electron-rsc';
13
+
14
+ import BrowserManager from './BrowserManager';
15
+ import { I18nManager } from './I18nManager';
16
+ import { IoCContainer } from './IoCContainer';
17
+ import MenuManager from './MenuManager';
18
+ import { ShortcutManager } from './ShortcutManager';
19
+ import { StoreManager } from './StoreManager';
20
+ import { UpdaterManager } from './UpdaterManager';
21
+
22
+ const logger = createLogger('core:App');
23
+
24
+ export type IPCEventMap = Map<string, { controller: any; methodName: string }>;
25
+ export type ShortcutMethodMap = Map<string, () => Promise<void>>;
26
+
27
+ type Class<T> = new (...args: any[]) => T;
28
+
29
+ const importAll = (r: any) => Object.values(r).map((v: any) => v.default);
30
+
31
+ export class App {
32
+ nextServerUrl = 'http://localhost:3015';
33
+
34
+ browserManager: BrowserManager;
35
+ menuManager: MenuManager;
36
+ i18n: I18nManager;
37
+ storeManager: StoreManager;
38
+ updaterManager: UpdaterManager;
39
+ shortcutManager: ShortcutManager;
40
+
41
+ /**
42
+ * whether app is in quiting
43
+ */
44
+ isQuiting: boolean = false;
45
+
46
+ get appStoragePath() {
47
+ const storagePath = this.storeManager.get('storagePath');
48
+
49
+ if (!storagePath) {
50
+ throw new Error('Storage path not found in store');
51
+ }
52
+
53
+ return storagePath;
54
+ }
55
+
56
+ constructor() {
57
+ logger.info('----------------------------------------------');
58
+ logger.info('Starting LobeHub...');
59
+
60
+ logger.debug('Initializing App');
61
+ // Initialize store manager
62
+ this.storeManager = new StoreManager(this);
63
+
64
+ // load controllers
65
+ const controllers: IControlModule[] = importAll(
66
+ (import.meta as any).glob('@/controllers/*Ctr.ts', { eager: true }),
67
+ );
68
+
69
+ logger.debug(`Loading ${controllers.length} controllers`);
70
+ controllers.forEach((controller) => this.addController(controller));
71
+
72
+ // load services
73
+ const services: IServiceModule[] = importAll(
74
+ (import.meta as any).glob('@/services/*Srv.ts', { eager: true }),
75
+ );
76
+
77
+ logger.debug(`Loading ${services.length} services`);
78
+ services.forEach((service) => this.addService(service));
79
+
80
+ this.initializeIPCEvents();
81
+
82
+ this.i18n = new I18nManager(this);
83
+ this.browserManager = new BrowserManager(this);
84
+ this.menuManager = new MenuManager(this);
85
+ this.updaterManager = new UpdaterManager(this);
86
+ this.shortcutManager = new ShortcutManager(this);
87
+
88
+ // register the schema to interceptor url
89
+ // it should register before app ready
90
+ this.registerNextHandler();
91
+
92
+ // 统一处理 before-quit 事件
93
+ app.on('before-quit', this.handleBeforeQuit);
94
+
95
+ logger.info('App initialization completed');
96
+ }
97
+
98
+ bootstrap = async () => {
99
+ logger.info('Bootstrapping application');
100
+ // make single instance
101
+ const isSingle = app.requestSingleInstanceLock();
102
+ if (!isSingle) {
103
+ logger.info('Another instance is already running, exiting');
104
+ app.exit(0);
105
+ }
106
+
107
+ this.initDevBranding();
108
+
109
+ // ==============
110
+ await this.ipcServer.start();
111
+ logger.debug('IPC server started');
112
+
113
+ // Initialize app
114
+ await this.makeAppReady();
115
+
116
+ // Initialize i18n. Note: app.getLocale() must be called after app.whenReady() to get the correct value
117
+ await this.i18n.init();
118
+ this.menuManager.initialize();
119
+
120
+ // Initialize global shortcuts: globalShortcut must be called after app.whenReady()
121
+ this.shortcutManager.initialize();
122
+
123
+ this.browserManager.initializeBrowsers();
124
+
125
+ // Initialize updater manager
126
+ await this.updaterManager.initialize();
127
+
128
+ // Set global application exit state
129
+ this.isQuiting = false;
130
+
131
+ app.on('window-all-closed', () => {
132
+ if (windows()) {
133
+ logger.info('All windows closed, quitting application (Windows)');
134
+ app.quit();
135
+ }
136
+ });
137
+
138
+ app.on('activate', this.onActivate);
139
+ logger.info('Application bootstrap completed');
140
+ };
141
+
142
+ getService<T>(serviceClass: Class<T>): T {
143
+ return this.services.get(serviceClass);
144
+ }
145
+
146
+ getController<T>(controllerClass: Class<T>): T {
147
+ return this.controllers.get(controllerClass);
148
+ }
149
+
150
+ private onActivate = () => {
151
+ logger.debug('Application activated');
152
+ this.browserManager.showMainWindow();
153
+ };
154
+
155
+ /**
156
+ * Call beforeAppReady method on all controllers before the application is ready
157
+ */
158
+ private makeAppReady = async () => {
159
+ logger.debug('Preparing application ready state');
160
+ this.controllers.forEach((controller) => {
161
+ if (typeof controller.beforeAppReady === 'function') {
162
+ try {
163
+ controller.beforeAppReady();
164
+ } catch (error) {
165
+ logger.error(`Error in controller.beforeAppReady:`, error);
166
+ console.error(`[App] Error in controller.beforeAppReady:`, error);
167
+ }
168
+ }
169
+ });
170
+
171
+ logger.debug('Waiting for app to be ready');
172
+ await app.whenReady();
173
+ logger.debug('Application ready');
174
+
175
+ this.controllers.forEach((controller) => {
176
+ if (typeof controller.afterAppReady === 'function') {
177
+ try {
178
+ controller.afterAppReady();
179
+ } catch (error) {
180
+ logger.error(`Error in controller.afterAppReady:`, error);
181
+ console.error(`[App] Error in controller.beforeAppReady:`, error);
182
+ }
183
+ }
184
+ });
185
+ logger.info('Application ready state completed');
186
+ };
187
+
188
+ // ============= helper ============= //
189
+
190
+ /**
191
+ * all controllers in app
192
+ */
193
+ private controllers = new Map<Class<any>, any>();
194
+ /**
195
+ * all services in app
196
+ */
197
+ private services = new Map<Class<any>, any>();
198
+
199
+ private ipcServer: ElectronIPCServer;
200
+ /**
201
+ * events dispatched from webview layer
202
+ */
203
+ private ipcClientEventMap: IPCEventMap = new Map();
204
+ private ipcServerEventMap: IPCEventMap = new Map();
205
+ shortcutMethodMap: ShortcutMethodMap = new Map();
206
+
207
+ /**
208
+ * use in next router interceptor in prod browser render
209
+ */
210
+ nextInterceptor: (params: { session: Session }) => () => void;
211
+
212
+ /**
213
+ * Collection of unregister functions for custom request handlers
214
+ */
215
+ private customHandlerUnregisterFns: Array<() => void> = [];
216
+
217
+ /**
218
+ * Function to register custom request handler
219
+ */
220
+ private registerCustomHandlerFn?: (handler: CustomRequestHandler) => () => void;
221
+
222
+ /**
223
+ * Register custom request handler
224
+ * @param handler Custom request handler function
225
+ * @returns Function to unregister the handler
226
+ */
227
+ registerRequestHandler = (handler: CustomRequestHandler): (() => void) => {
228
+ if (!this.registerCustomHandlerFn) {
229
+ logger.warn('Custom request handler registration is not available');
230
+ return () => {};
231
+ }
232
+
233
+ logger.debug('Registering custom request handler');
234
+ const unregisterFn = this.registerCustomHandlerFn(handler);
235
+ this.customHandlerUnregisterFns.push(unregisterFn);
236
+
237
+ return () => {
238
+ unregisterFn();
239
+ const index = this.customHandlerUnregisterFns.indexOf(unregisterFn);
240
+ if (index !== -1) {
241
+ this.customHandlerUnregisterFns.splice(index, 1);
242
+ }
243
+ };
244
+ };
245
+
246
+ /**
247
+ * Unregister all custom request handlers
248
+ */
249
+ unregisterAllRequestHandlers = () => {
250
+ this.customHandlerUnregisterFns.forEach((unregister) => unregister());
251
+ this.customHandlerUnregisterFns = [];
252
+ };
253
+
254
+ private addController = (ControllerClass: IControlModule) => {
255
+ const controller = new ControllerClass(this);
256
+ this.controllers.set(ControllerClass, controller);
257
+
258
+ IoCContainer.controllers.get(ControllerClass)?.forEach((event) => {
259
+ if (event.mode === 'client') {
260
+ // Store all objects from event decorator in ipcClientEventMap
261
+ this.ipcClientEventMap.set(event.name, {
262
+ controller,
263
+ methodName: event.methodName,
264
+ });
265
+ }
266
+
267
+ if (event.mode === 'server') {
268
+ // Store all objects from event decorator in ipcServerEventMap
269
+ this.ipcServerEventMap.set(event.name, {
270
+ controller,
271
+ methodName: event.methodName,
272
+ });
273
+ }
274
+ });
275
+
276
+ IoCContainer.shortcuts.get(ControllerClass)?.forEach((shortcut) => {
277
+ this.shortcutMethodMap.set(shortcut.name, async () => {
278
+ controller[shortcut.methodName]();
279
+ });
280
+ });
281
+ };
282
+
283
+ private addService = (ServiceClass: IServiceModule) => {
284
+ const service = new ServiceClass(this);
285
+ this.services.set(ServiceClass, service);
286
+ };
287
+
288
+ private initDevBranding = () => {
289
+ if (!isDev) return;
290
+
291
+ logger.debug('Setting up dev branding');
292
+ app.setName('lobehub-desktop-dev');
293
+ if (macOS()) {
294
+ app.dock!.setIcon(join(buildDir, 'icon-dev.png'));
295
+ }
296
+ };
297
+
298
+ private registerNextHandler() {
299
+ logger.debug('Registering Next.js handler');
300
+ const handler = createHandler({
301
+ debug: true,
302
+ localhostUrl: this.nextServerUrl,
303
+ protocol,
304
+ standaloneDir: nextStandaloneDir,
305
+ });
306
+
307
+ // Log output based on development or production mode
308
+ if (isDev) {
309
+ logger.info(
310
+ `Development mode: Custom request handler enabled, but Next.js interception disabled`,
311
+ );
312
+ } else {
313
+ logger.info(
314
+ `Production mode: ${this.nextServerUrl} will be intercepted to ${nextStandaloneDir}`,
315
+ );
316
+ }
317
+
318
+ this.nextInterceptor = handler.createInterceptor;
319
+
320
+ // Save custom handler registration function
321
+ if (handler.registerCustomHandler) {
322
+ this.registerCustomHandlerFn = handler.registerCustomHandler;
323
+ logger.debug('Custom request handler registration is available');
324
+ } else {
325
+ logger.warn('Custom request handler registration is not available');
326
+ }
327
+ }
328
+
329
+ private initializeIPCEvents() {
330
+ logger.debug('Initializing IPC events');
331
+ // Register batch controller client events for render side consumption
332
+ this.ipcClientEventMap.forEach((eventInfo, key) => {
333
+ const { controller, methodName } = eventInfo;
334
+
335
+ ipcMain.handle(key, async (e, ...data) => {
336
+ try {
337
+ return await controller[methodName](...data);
338
+ } catch (error) {
339
+ logger.error(`Error handling IPC event ${key}:`, error);
340
+ return { error: error.message };
341
+ }
342
+ });
343
+ });
344
+
345
+ // Batch register server events from controllers for next server consumption
346
+ const ipcServerEvents = {} as ElectronIPCEventHandler;
347
+
348
+ this.ipcServerEventMap.forEach((eventInfo, key) => {
349
+ const { controller, methodName } = eventInfo;
350
+
351
+ ipcServerEvents[key] = async (payload) => {
352
+ try {
353
+ return await controller[methodName](payload);
354
+ } catch (error) {
355
+ return { error: error.message };
356
+ }
357
+ };
358
+ });
359
+
360
+ this.ipcServer = new ElectronIPCServer(name, ipcServerEvents);
361
+ }
362
+
363
+ // 新增 before-quit 处理函数
364
+ private handleBeforeQuit = () => {
365
+ this.isQuiting = true; // 首先设置标志
366
+
367
+ // 执行清理操作
368
+ this.unregisterAllRequestHandlers();
369
+ };
370
+ }