@lobehub/chat 1.82.9 → 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 (196) 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 +50 -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 +18 -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/config/aiModels/wenxin.ts +95 -8
  160. package/src/database/core/db-adaptor.ts +4 -1
  161. package/src/database/core/electron.ts +317 -0
  162. package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx → features/ElectronTitlebar/Connection/ConnectionMode.tsx} +24 -21
  163. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Option.tsx +3 -5
  164. package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx → features/ElectronTitlebar/Connection/RemoteStatus.tsx} +10 -7
  165. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/index.tsx +4 -4
  166. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateModal.tsx +2 -1
  167. package/src/libs/trpc/client/async.ts +6 -0
  168. package/src/libs/trpc/client/edge.ts +6 -0
  169. package/src/libs/trpc/client/helpers/desktopRemoteRPCFetch.ts +72 -0
  170. package/src/libs/trpc/client/index.ts +1 -0
  171. package/src/libs/trpc/client/lambda.ts +10 -1
  172. package/src/libs/trpc/client/tools.ts +6 -0
  173. package/src/server/globalConfig/index.ts +0 -3
  174. package/src/server/modules/ElectronIPCClient/index.ts +3 -1
  175. package/src/server/routers/desktop/index.ts +2 -0
  176. package/src/server/routers/desktop/mcp.ts +47 -0
  177. package/src/server/routers/lambda/user.ts +38 -23
  178. package/src/server/routers/tools/mcp.ts +0 -6
  179. package/src/services/electron/remoteServer.ts +4 -4
  180. package/src/services/mcp.ts +17 -7
  181. package/src/services/upload.ts +9 -0
  182. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +11 -2
  183. package/src/store/chat/slices/builtinTool/actions/localFile.ts +110 -53
  184. package/src/store/electron/actions/sync.ts +20 -19
  185. package/src/store/electron/initialState.ts +3 -3
  186. package/src/store/electron/selectors/sync.ts +6 -3
  187. package/src/store/electron/store.ts +2 -0
  188. package/src/store/file/slices/upload/action.ts +11 -3
  189. package/src/store/tool/selectors/tool.ts +10 -1
  190. package/src/utils/fetch/headers.ts +27 -0
  191. package/src/utils/fetch/index.ts +2 -0
  192. package/src/utils/fetch/request.ts +28 -0
  193. package/packages/electron-client-ipc/src/types/remoteServer.ts +0 -8
  194. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Waiting.tsx +0 -0
  195. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateNotification.tsx +0 -0
  196. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/index.tsx +0 -0
@@ -0,0 +1,345 @@
1
+ import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
2
+ import { BrowserWindow, BrowserWindowConstructorOptions, ipcMain } from 'electron';
3
+ import { join } from 'node:path';
4
+
5
+ import { createLogger } from '@/utils/logger';
6
+
7
+ import { preloadDir, resourcesDir } from '../const/dir';
8
+ import type { App } from './App';
9
+
10
+ // Create logger
11
+ const logger = createLogger('core:Browser');
12
+
13
+ export interface BrowserWindowOpts extends BrowserWindowConstructorOptions {
14
+ devTools?: boolean;
15
+ height?: number;
16
+ /**
17
+ * URL
18
+ */
19
+ identifier: string;
20
+ keepAlive?: boolean;
21
+ path: string;
22
+ showOnInit?: boolean;
23
+ title?: string;
24
+ width?: number;
25
+ }
26
+
27
+ export default class Browser {
28
+ private app: App;
29
+
30
+ /**
31
+ * Internal electron window
32
+ */
33
+ private _browserWindow?: BrowserWindow;
34
+
35
+ private stopInterceptHandler;
36
+ /**
37
+ * Identifier
38
+ */
39
+ identifier: string;
40
+
41
+ /**
42
+ * Options at creation
43
+ */
44
+ options: BrowserWindowOpts;
45
+
46
+ /**
47
+ * Key for storing window state in storeManager
48
+ */
49
+ private readonly windowStateKey: string;
50
+
51
+ /**
52
+ * Method to expose window externally
53
+ */
54
+ get browserWindow() {
55
+ return this.retrieveOrInitialize();
56
+ }
57
+
58
+ /**
59
+ * Method to construct BrowserWindows object
60
+ * @param options
61
+ * @param application
62
+ */
63
+ constructor(options: BrowserWindowOpts, application: App) {
64
+ logger.debug(`Creating Browser instance: ${options.identifier}`);
65
+ logger.debug(`Browser options: ${JSON.stringify(options)}`);
66
+ this.app = application;
67
+ this.identifier = options.identifier;
68
+ this.options = options;
69
+ this.windowStateKey = `windowSize_${this.identifier}`;
70
+
71
+ // Initialization
72
+ this.retrieveOrInitialize();
73
+ }
74
+
75
+ loadUrl = async (path: string) => {
76
+ const initUrl = this.app.nextServerUrl + path;
77
+
78
+ try {
79
+ logger.debug(`[${this.identifier}] Attempting to load URL: ${initUrl}`);
80
+ await this._browserWindow.loadURL(initUrl);
81
+ logger.debug(`[${this.identifier}] Successfully loaded URL: ${initUrl}`);
82
+ } catch (error) {
83
+ logger.error(`[${this.identifier}] Failed to load URL (${initUrl}):`, error);
84
+
85
+ // Try to load local error page
86
+ try {
87
+ logger.info(`[${this.identifier}] Attempting to load error page...`);
88
+ await this._browserWindow.loadFile(join(resourcesDir, 'error.html'));
89
+ logger.info(`[${this.identifier}] Error page loaded successfully.`);
90
+
91
+ // Remove previously set retry listeners to avoid duplicates
92
+ ipcMain.removeHandler('retry-connection');
93
+ logger.debug(`[${this.identifier}] Removed existing retry-connection handler if any.`);
94
+
95
+ // Set retry logic
96
+ ipcMain.handle('retry-connection', async () => {
97
+ logger.info(`[${this.identifier}] Retry connection requested for: ${initUrl}`);
98
+ try {
99
+ await this._browserWindow?.loadURL(initUrl);
100
+ logger.info(`[${this.identifier}] Reconnection successful to ${initUrl}`);
101
+ return { success: true };
102
+ } catch (err) {
103
+ logger.error(`[${this.identifier}] Retry connection failed for ${initUrl}:`, err);
104
+ // Reload error page
105
+ try {
106
+ logger.info(`[${this.identifier}] Reloading error page after failed retry...`);
107
+ await this._browserWindow?.loadFile(join(resourcesDir, 'error.html'));
108
+ logger.info(`[${this.identifier}] Error page reloaded.`);
109
+ } catch (loadErr) {
110
+ logger.error('[${this.identifier}] Failed to reload error page:', loadErr);
111
+ }
112
+ return { error: err.message, success: false };
113
+ }
114
+ });
115
+ logger.debug(`[${this.identifier}] Set up retry-connection handler.`);
116
+ } catch (err) {
117
+ logger.error(`[${this.identifier}] Failed to load error page:`, err);
118
+ // If even the error page can't be loaded, at least show a simple error message
119
+ try {
120
+ logger.warn(`[${this.identifier}] Attempting to load fallback error HTML string...`);
121
+ await this._browserWindow.loadURL(
122
+ 'data:text/html,<html><body><h1>Loading Failed</h1><p>Unable to connect to server, please restart the application</p></body></html>',
123
+ );
124
+ logger.info(`[${this.identifier}] Fallback error HTML string loaded.`);
125
+ } catch (finalErr) {
126
+ logger.error(`[${this.identifier}] Unable to display any page:`, finalErr);
127
+ }
128
+ }
129
+ }
130
+ };
131
+
132
+ loadPlaceholder = async () => {
133
+ logger.debug(`[${this.identifier}] Loading splash screen placeholder`);
134
+ // First load a local HTML loading page
135
+ await this._browserWindow.loadFile(join(resourcesDir, 'splash.html'));
136
+ logger.debug(`[${this.identifier}] Splash screen placeholder loaded.`);
137
+ };
138
+
139
+ show() {
140
+ logger.debug(`Showing window: ${this.identifier}`);
141
+ this.browserWindow.show();
142
+ }
143
+
144
+ hide() {
145
+ logger.debug(`Hiding window: ${this.identifier}`);
146
+ this.browserWindow.hide();
147
+ }
148
+
149
+ close() {
150
+ logger.debug(`Attempting to close window: ${this.identifier}`);
151
+ this.browserWindow.close();
152
+ }
153
+
154
+ /**
155
+ * Destroy instance
156
+ */
157
+ destroy() {
158
+ logger.debug(`Destroying window instance: ${this.identifier}`);
159
+ this.stopInterceptHandler?.();
160
+ this._browserWindow = undefined;
161
+ }
162
+
163
+ /**
164
+ * Initialize
165
+ */
166
+ retrieveOrInitialize() {
167
+ // When there is this window and it has not been destroyed
168
+ if (this._browserWindow && !this._browserWindow.isDestroyed()) {
169
+ logger.debug(`[${this.identifier}] Returning existing BrowserWindow instance.`);
170
+ return this._browserWindow;
171
+ }
172
+
173
+ const { path, title, width, height, devTools, showOnInit, ...res } = this.options;
174
+
175
+ // Load window state
176
+ const savedState = this.app.storeManager.get(this.windowStateKey as any) as
177
+ | { height?: number; width?: number }
178
+ | undefined; // Keep type for now, but only use w/h
179
+ logger.info(`Creating new BrowserWindow instance: ${this.identifier}`);
180
+ logger.debug(`[${this.identifier}] Options for new window: ${JSON.stringify(this.options)}`);
181
+ logger.debug(
182
+ `[${this.identifier}] Saved window state (only size used): ${JSON.stringify(savedState)}`,
183
+ );
184
+
185
+ const browserWindow = new BrowserWindow({
186
+ ...res,
187
+
188
+ height: savedState?.height || height,
189
+
190
+ show: false,
191
+
192
+ // Always create hidden first
193
+ title,
194
+
195
+ transparent: true,
196
+
197
+ webPreferences: {
198
+ // Context isolation environment
199
+ // https://www.electronjs.org/docs/tutorial/context-isolation
200
+ contextIsolation: true,
201
+ preload: join(preloadDir, 'index.js'),
202
+ // devTools: isDev,
203
+ },
204
+ // Use saved state if available, otherwise use options. Do not set x/y
205
+ // x: savedState?.x, // Don't restore x
206
+ // y: savedState?.y, // Don't restore y
207
+ width: savedState?.width || width,
208
+ });
209
+
210
+ this._browserWindow = browserWindow;
211
+ logger.debug(`[${this.identifier}] BrowserWindow instance created.`);
212
+
213
+ logger.debug(`[${this.identifier}] Setting up nextInterceptor.`);
214
+ this.stopInterceptHandler = this.app.nextInterceptor({
215
+ session: browserWindow.webContents.session,
216
+ });
217
+
218
+ // Windows 11 can use this new API
219
+ if (process.platform === 'win32' && browserWindow.setBackgroundMaterial) {
220
+ logger.debug(`[${this.identifier}] Setting window background material for Windows 11`);
221
+ browserWindow.setBackgroundMaterial('acrylic');
222
+ }
223
+
224
+ logger.debug(`[${this.identifier}] Initiating placeholder and URL loading sequence.`);
225
+ this.loadPlaceholder().then(() => {
226
+ this.loadUrl(path).catch((e) => {
227
+ logger.error(`[${this.identifier}] Initial loadUrl error for path '${path}':`, e);
228
+ });
229
+ });
230
+
231
+ // Show devtools if enabled
232
+ if (devTools) {
233
+ logger.debug(`[${this.identifier}] Opening DevTools because devTools option is true.`);
234
+ browserWindow.webContents.openDevTools();
235
+ }
236
+
237
+ logger.debug(`[${this.identifier}] Setting up 'ready-to-show' event listener.`);
238
+ browserWindow.once('ready-to-show', () => {
239
+ logger.debug(`[${this.identifier}] Window 'ready-to-show' event fired.`);
240
+ if (showOnInit) {
241
+ logger.debug(`Showing window ${this.identifier} because showOnInit is true.`);
242
+ browserWindow?.show();
243
+ } else {
244
+ logger.debug(
245
+ `Window ${this.identifier} not shown on 'ready-to-show' because showOnInit is false.`,
246
+ );
247
+ }
248
+ });
249
+
250
+ logger.debug(`[${this.identifier}] Setting up 'close' event listener.`);
251
+ browserWindow.on('close', (e) => {
252
+ logger.debug(`Window 'close' event triggered for: ${this.identifier}`);
253
+ logger.debug(
254
+ `[${this.identifier}] State during close event: isQuiting=${this.app.isQuiting}, keepAlive=${this.options.keepAlive}`,
255
+ );
256
+
257
+ // If in application quitting process, allow window to be closed
258
+ if (this.app.isQuiting) {
259
+ logger.debug(`[${this.identifier}] App is quitting, allowing window to close naturally.`);
260
+ // Save state before quitting
261
+ try {
262
+ const { width, height } = browserWindow.getBounds(); // Get only width and height
263
+ const sizeState = { height, width };
264
+ logger.debug(
265
+ `[${this.identifier}] Saving window size on quit: ${JSON.stringify(sizeState)}`,
266
+ );
267
+ this.app.storeManager.set(this.windowStateKey as any, sizeState); // Save only size
268
+ } catch (error) {
269
+ logger.error(`[${this.identifier}] Failed to save window state on quit:`, error);
270
+ }
271
+ // Need to clean up intercept handler
272
+ this.stopInterceptHandler?.();
273
+ return;
274
+ }
275
+
276
+ // Prevent window from being destroyed, just hide it (if marked as keepAlive)
277
+ if (this.options.keepAlive) {
278
+ logger.debug(
279
+ `[${this.identifier}] keepAlive is true, preventing default close and hiding window.`,
280
+ );
281
+ // Optionally save state when hiding if desired, but primary save is on actual close/quit
282
+ // try {
283
+ // const bounds = browserWindow.getBounds();
284
+ // logger.debug(`[${this.identifier}] Saving window state on hide: ${JSON.stringify(bounds)}`);
285
+ // this.app.storeManager.set(this.windowStateKey, bounds);
286
+ // } catch (error) {
287
+ // logger.error(`[${this.identifier}] Failed to save window state on hide:`, error);
288
+ // }
289
+ e.preventDefault();
290
+ browserWindow.hide();
291
+ } else {
292
+ // Window is actually closing (not keepAlive)
293
+ logger.debug(
294
+ `[${this.identifier}] keepAlive is false, allowing window to close. Saving size...`, // Updated log message
295
+ );
296
+ try {
297
+ const { width, height } = browserWindow.getBounds(); // Get only width and height
298
+ const sizeState = { height, width };
299
+ logger.debug(
300
+ `[${this.identifier}] Saving window size on close: ${JSON.stringify(sizeState)}`,
301
+ );
302
+ this.app.storeManager.set(this.windowStateKey as any, sizeState); // Save only size
303
+ } catch (error) {
304
+ logger.error(`[${this.identifier}] Failed to save window state on close:`, error);
305
+ }
306
+ // Need to clean up intercept handler
307
+ this.stopInterceptHandler?.();
308
+ }
309
+ });
310
+
311
+ logger.debug(`[${this.identifier}] retrieveOrInitialize completed.`);
312
+ return browserWindow;
313
+ }
314
+
315
+ moveToCenter() {
316
+ logger.debug(`Centering window: ${this.identifier}`);
317
+ this._browserWindow?.center();
318
+ }
319
+
320
+ setWindowSize(boundSize: { height?: number; width?: number }) {
321
+ logger.debug(
322
+ `Setting window size for ${this.identifier}: width=${boundSize.width}, height=${boundSize.height}`,
323
+ );
324
+ const windowSize = this._browserWindow.getBounds();
325
+ this._browserWindow?.setBounds({
326
+ height: boundSize.height || windowSize.height,
327
+ width: boundSize.width || windowSize.width,
328
+ });
329
+ }
330
+
331
+ broadcast = <T extends MainBroadcastEventKey>(channel: T, data?: MainBroadcastParams<T>) => {
332
+ logger.debug(`Broadcasting to window ${this.identifier}, channel: ${channel}`);
333
+ this._browserWindow.webContents.send(channel, data);
334
+ };
335
+
336
+ toggleVisible() {
337
+ logger.debug(`Toggling visibility for window: ${this.identifier}`);
338
+ if (this._browserWindow.isVisible() && this._browserWindow.isFocused()) {
339
+ this._browserWindow.hide();
340
+ } else {
341
+ this._browserWindow.show();
342
+ this._browserWindow.focus();
343
+ }
344
+ }
345
+ }
@@ -0,0 +1,154 @@
1
+ import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
2
+
3
+ import { createLogger } from '@/utils/logger';
4
+
5
+ import { AppBrowsersIdentifiers, appBrowsers } from '../appBrowsers';
6
+ import type { App } from './App';
7
+ import type { BrowserWindowOpts } from './Browser';
8
+ import Browser from './Browser';
9
+
10
+ // Create logger
11
+ const logger = createLogger('core:BrowserManager');
12
+
13
+ export default class BrowserManager {
14
+ app: App;
15
+
16
+ browsers: Map<AppBrowsersIdentifiers, Browser> = new Map();
17
+
18
+ constructor(app: App) {
19
+ logger.debug('Initializing BrowserManager');
20
+ this.app = app;
21
+ }
22
+
23
+ getMainWindow() {
24
+ return this.retrieveByIdentifier('chat');
25
+ }
26
+
27
+ showMainWindow() {
28
+ logger.debug('Showing main window');
29
+ const window = this.getMainWindow();
30
+ window.show();
31
+ }
32
+
33
+ showSettingsWindow() {
34
+ logger.debug('Showing settings window');
35
+ const window = this.retrieveByIdentifier('settings');
36
+ window.show();
37
+ return window;
38
+ }
39
+
40
+ broadcastToAllWindows = <T extends MainBroadcastEventKey>(
41
+ event: T,
42
+ data: MainBroadcastParams<T>,
43
+ ) => {
44
+ logger.debug(`Broadcasting event ${event} to all windows`);
45
+ this.browsers.forEach((browser) => {
46
+ browser.broadcast(event, data);
47
+ });
48
+ };
49
+
50
+ broadcastToWindow = <T extends MainBroadcastEventKey>(
51
+ identifier: AppBrowsersIdentifiers,
52
+ event: T,
53
+ data: MainBroadcastParams<T>,
54
+ ) => {
55
+ logger.debug(`Broadcasting event ${event} to window: ${identifier}`);
56
+ this.browsers.get(identifier).broadcast(event, data);
57
+ };
58
+
59
+ /**
60
+ * Display the settings window and navigate to a specific tab
61
+ * @param tab Settings window sub-path tab
62
+ */
63
+ async showSettingsWindowWithTab(tab?: string) {
64
+ logger.debug(`Showing settings window with tab: ${tab || 'default'}`);
65
+ // common is the main path for settings route
66
+ if (tab && tab !== 'common') {
67
+ const browser = await this.redirectToPage('settings', tab);
68
+
69
+ // make provider page more large
70
+ if (tab.startsWith('provider/')) {
71
+ logger.debug('Resizing window for provider settings');
72
+ browser.setWindowSize({ height: 1000, width: 1400 });
73
+ browser.moveToCenter();
74
+ }
75
+
76
+ return browser;
77
+ } else {
78
+ return this.showSettingsWindow();
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Navigate window to specific sub-path
84
+ * @param identifier Window identifier
85
+ * @param subPath Sub-path, such as 'agent', 'about', etc.
86
+ */
87
+ async redirectToPage(identifier: AppBrowsersIdentifiers, subPath?: string) {
88
+ try {
89
+ // Ensure window is retrieved or created
90
+ const browser = this.retrieveByIdentifier(identifier);
91
+ browser.hide();
92
+
93
+ const baseRoute = appBrowsers[identifier].path;
94
+
95
+ // Build complete URL path
96
+ const fullPath = subPath ? `${baseRoute}/${subPath}` : baseRoute;
97
+
98
+ logger.debug(`Redirecting to: ${fullPath}`);
99
+
100
+ // Load URL and show window
101
+ await browser.loadUrl(fullPath);
102
+ browser.show();
103
+
104
+ return browser;
105
+ } catch (error) {
106
+ logger.error(`Failed to redirect (${identifier}/${subPath}):`, error);
107
+ throw error;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * get Browser by identifier
113
+ */
114
+ retrieveByIdentifier(identifier: AppBrowsersIdentifiers) {
115
+ const browser = this.browsers.get(identifier);
116
+
117
+ if (browser) return browser;
118
+
119
+ logger.debug(`Browser ${identifier} not found, initializing new instance`);
120
+ return this.retrieveOrInitialize(appBrowsers[identifier]);
121
+ }
122
+
123
+ /**
124
+ * Initialize all browsers when app starts up
125
+ */
126
+ initializeBrowsers() {
127
+ logger.info('Initializing all browsers');
128
+ Object.values(appBrowsers).forEach((browser) => {
129
+ logger.debug(`Initializing browser: ${browser.identifier}`);
130
+ this.retrieveOrInitialize(browser);
131
+ });
132
+ }
133
+
134
+ // helper
135
+
136
+ /**
137
+ * Retrieve existing browser or initialize a new one
138
+ * @param options Browser window options
139
+ */
140
+ private retrieveOrInitialize(options: BrowserWindowOpts) {
141
+ let browser = this.browsers.get(options.identifier as AppBrowsersIdentifiers);
142
+ if (browser) {
143
+ logger.debug(`Retrieved existing browser: ${options.identifier}`);
144
+ return browser;
145
+ }
146
+
147
+ logger.debug(`Creating new browser: ${options.identifier}`);
148
+ browser = new Browser(options, this.app);
149
+
150
+ this.browsers.set(options.identifier as AppBrowsersIdentifiers, browser);
151
+
152
+ return browser;
153
+ }
154
+ }
@@ -0,0 +1,185 @@
1
+ import { app } from 'electron';
2
+ import i18next from 'i18next';
3
+
4
+ import { App } from '@/core/App';
5
+ import { loadResources } from '@/locales/resources';
6
+ import { createLogger } from '@/utils/logger';
7
+
8
+ // Create logger
9
+ const logger = createLogger('core:I18nManager');
10
+
11
+ export class I18nManager {
12
+ private i18n: typeof i18next;
13
+ private initialized: boolean = false;
14
+ private app: App;
15
+
16
+ constructor(app: App) {
17
+ logger.debug('Initializing I18nManager');
18
+ this.app = app;
19
+ this.i18n = i18next.createInstance();
20
+ }
21
+
22
+ /**
23
+ * Initialize i18next instance
24
+ */
25
+ async init(lang?: string) {
26
+ if (this.initialized) {
27
+ logger.debug('I18nManager already initialized, skipping');
28
+ return this.i18n;
29
+ }
30
+
31
+ // Priority: parameter language > stored locale > system language
32
+ const storedLocale = this.app.storeManager.get('locale', 'auto') as string;
33
+ const defaultLanguage =
34
+ lang || (storedLocale !== 'auto' ? storedLocale : app.getLocale()) || 'en-US';
35
+
36
+ logger.info(
37
+ `Initializing i18n, app locale: ${defaultLanguage}, stored locale: ${storedLocale}`,
38
+ );
39
+
40
+ await this.i18n.init({
41
+ defaultNS: 'menu',
42
+ fallbackLng: 'en-US',
43
+ // Load resources as needed
44
+ initAsync: true,
45
+ interpolation: {
46
+ escapeValue: false,
47
+ },
48
+
49
+ lng: defaultLanguage,
50
+
51
+ ns: ['menu', 'dialog', 'common'],
52
+ partialBundledLanguages: true,
53
+ });
54
+
55
+ logger.info(`i18n initialized, language: ${this.i18n.language}`);
56
+
57
+ // Preload base namespaces
58
+ await this.loadLocale(this.i18n.language);
59
+
60
+ this.initialized = true;
61
+
62
+ this.refreshMainUI();
63
+
64
+ // Listen for language change events
65
+ this.i18n.on('languageChanged', this.handleLanguageChanged);
66
+
67
+ return this.i18n;
68
+ }
69
+
70
+ /**
71
+ * Basic translation function
72
+ */
73
+ t = (key: string, options?: any) => {
74
+ const result = this.i18n.t(key, options) as string;
75
+
76
+ // If translation result is the same as key, translation might be missing
77
+ if (result === key) {
78
+ logger.warn(`${this.i18n.language} key: ${key} is not found`);
79
+ }
80
+
81
+ return result;
82
+ };
83
+
84
+ /**
85
+ * Create a translation function bound to a specific namespace
86
+ * @param namespace Namespace
87
+ * @returns Translation function bound to namespace
88
+ */
89
+ createNamespacedT(namespace: string) {
90
+ return (key: string, options: any = {}) => {
91
+ // Copy options to avoid modifying the original object
92
+ const mergedOptions = { ...options };
93
+ // Set namespace
94
+ mergedOptions.ns = namespace;
95
+
96
+ return this.t(key, mergedOptions);
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Get translation function by namespace
102
+ * Provides a more convenient calling method
103
+ */
104
+ ns = (namespace: string) => this.createNamespacedT(namespace);
105
+
106
+ /**
107
+ * Get current language
108
+ */
109
+ getCurrentLanguage() {
110
+ return this.i18n.language;
111
+ }
112
+
113
+ /**
114
+ * Change application language
115
+ * @param lng Target language
116
+ */
117
+ public async changeLanguage(lng: string): Promise<void> {
118
+ logger.info(`Changing language to: ${lng}`);
119
+
120
+ if (!this.initialized) {
121
+ await this.init();
122
+ }
123
+
124
+ await this.i18n.changeLanguage(lng);
125
+ // Language change event will trigger handleLanguageChanged
126
+ }
127
+
128
+ /**
129
+ * Handle language change event
130
+ */
131
+ private handleLanguageChanged = async (lang: string) => {
132
+ logger.info(`Language changed to: ${lang}`);
133
+ await this.loadLocale(lang);
134
+
135
+ // Notify other parts of main process to refresh UI
136
+ this.refreshMainUI();
137
+ };
138
+
139
+ /**
140
+ * Refresh main process UI (menus, etc.)
141
+ */
142
+ private refreshMainUI() {
143
+ logger.debug('Refreshing main UI after language change');
144
+ this.app.menuManager.refreshMenus();
145
+ }
146
+
147
+ /**
148
+ * Notify renderer process that language has changed
149
+ */
150
+ private notifyRendererProcess(lng: string) {
151
+ logger.debug(`Notifying renderer process of language change: ${lng}`);
152
+
153
+ // Send language change event to all windows
154
+ // const windows = this.app.browserManager.windows;
155
+ //
156
+ // if (windows && windows.length > 0) {
157
+ // windows.forEach((window) => {
158
+ // if (window?.webContents) {
159
+ // window.webContents.send('language-changed', lng);
160
+ // }
161
+ // });
162
+ // }
163
+ }
164
+
165
+ private async loadLocale(language: string) {
166
+ logger.debug(`Loading locale for language: ${language}`);
167
+ // Preload base namespaces
168
+ await Promise.all(['menu', 'dialog', 'common'].map((ns) => this.loadNamespace(language, ns)));
169
+ }
170
+
171
+ /**
172
+ * Load translation resources for specific namespace
173
+ */
174
+ private async loadNamespace(lng: string, ns: string) {
175
+ try {
176
+ logger.debug(`Loading namespace: ${lng}/${ns}`);
177
+ const resources = await loadResources(lng, ns);
178
+ this.i18n.addResourceBundle(lng, ns, resources, true, true);
179
+ return true;
180
+ } catch (error) {
181
+ logger.error(`Failed to load namespace: ${lng}/${ns}`, error);
182
+ return false;
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 存储应用中需要用装饰器的类
3
+ */
4
+ export class IoCContainer {
5
+ static controllers: WeakMap<
6
+ any,
7
+ { methodName: string; mode: 'client' | 'server'; name: string }[]
8
+ > = new WeakMap();
9
+
10
+ static shortcuts: WeakMap<any, { methodName: string; name: string }[]> = new WeakMap();
11
+ init() {}
12
+ }