@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.
- package/.env.desktop +1 -2
- package/.github/workflows/{release-desktop.yml → desktop-pr-build.yml} +59 -137
- package/.github/workflows/release-desktop-beta.yml +196 -0
- package/CHANGELOG.md +50 -0
- package/apps/desktop/.i18nrc.js +31 -0
- package/apps/desktop/Development.md +47 -0
- package/apps/desktop/README.md +6 -0
- package/apps/desktop/build/Icon-beta.icns +0 -0
- package/apps/desktop/build/Icon-nightly.icns +0 -0
- package/apps/desktop/build/Icon.icns +0 -0
- package/apps/desktop/build/entitlements.mac.plist +12 -0
- package/apps/desktop/build/favicon.ico +0 -0
- package/apps/desktop/build/icon-beta.png +0 -0
- package/apps/desktop/build/icon-dev.png +0 -0
- package/apps/desktop/build/icon-nightly.ico +0 -0
- package/apps/desktop/build/icon-nightly.png +0 -0
- package/apps/desktop/build/icon.ico +0 -0
- package/apps/desktop/build/icon.png +0 -0
- package/apps/desktop/dev-app-update.yml +6 -0
- package/apps/desktop/electron-builder.js +92 -0
- package/apps/desktop/electron.vite.config.ts +40 -0
- package/apps/desktop/package.json +72 -0
- package/apps/desktop/pnpm-workspace.yaml +5 -0
- package/apps/desktop/resources/error.html +136 -0
- package/apps/desktop/resources/locales/ar/common.json +32 -0
- package/apps/desktop/resources/locales/ar/dialog.json +31 -0
- package/apps/desktop/resources/locales/ar/menu.json +70 -0
- package/apps/desktop/resources/locales/bg-BG/common.json +32 -0
- package/apps/desktop/resources/locales/bg-BG/dialog.json +31 -0
- package/apps/desktop/resources/locales/bg-BG/menu.json +70 -0
- package/apps/desktop/resources/locales/de-DE/common.json +32 -0
- package/apps/desktop/resources/locales/de-DE/dialog.json +31 -0
- package/apps/desktop/resources/locales/de-DE/menu.json +70 -0
- package/apps/desktop/resources/locales/en-US/common.json +32 -0
- package/apps/desktop/resources/locales/en-US/dialog.json +31 -0
- package/apps/desktop/resources/locales/en-US/menu.json +70 -0
- package/apps/desktop/resources/locales/es-ES/common.json +32 -0
- package/apps/desktop/resources/locales/es-ES/dialog.json +31 -0
- package/apps/desktop/resources/locales/es-ES/menu.json +70 -0
- package/apps/desktop/resources/locales/fa-IR/common.json +32 -0
- package/apps/desktop/resources/locales/fa-IR/dialog.json +31 -0
- package/apps/desktop/resources/locales/fa-IR/menu.json +70 -0
- package/apps/desktop/resources/locales/fr-FR/common.json +32 -0
- package/apps/desktop/resources/locales/fr-FR/dialog.json +31 -0
- package/apps/desktop/resources/locales/fr-FR/menu.json +70 -0
- package/apps/desktop/resources/locales/it-IT/common.json +32 -0
- package/apps/desktop/resources/locales/it-IT/dialog.json +31 -0
- package/apps/desktop/resources/locales/it-IT/menu.json +70 -0
- package/apps/desktop/resources/locales/ja-JP/common.json +32 -0
- package/apps/desktop/resources/locales/ja-JP/dialog.json +31 -0
- package/apps/desktop/resources/locales/ja-JP/menu.json +70 -0
- package/apps/desktop/resources/locales/ko-KR/common.json +32 -0
- package/apps/desktop/resources/locales/ko-KR/dialog.json +31 -0
- package/apps/desktop/resources/locales/ko-KR/menu.json +70 -0
- package/apps/desktop/resources/locales/nl-NL/common.json +32 -0
- package/apps/desktop/resources/locales/nl-NL/dialog.json +31 -0
- package/apps/desktop/resources/locales/nl-NL/menu.json +70 -0
- package/apps/desktop/resources/locales/pl-PL/common.json +32 -0
- package/apps/desktop/resources/locales/pl-PL/dialog.json +31 -0
- package/apps/desktop/resources/locales/pl-PL/menu.json +70 -0
- package/apps/desktop/resources/locales/pt-BR/common.json +32 -0
- package/apps/desktop/resources/locales/pt-BR/dialog.json +31 -0
- package/apps/desktop/resources/locales/pt-BR/menu.json +70 -0
- package/apps/desktop/resources/locales/ru-RU/common.json +32 -0
- package/apps/desktop/resources/locales/ru-RU/dialog.json +31 -0
- package/apps/desktop/resources/locales/ru-RU/menu.json +70 -0
- package/apps/desktop/resources/locales/tr-TR/common.json +32 -0
- package/apps/desktop/resources/locales/tr-TR/dialog.json +31 -0
- package/apps/desktop/resources/locales/tr-TR/menu.json +70 -0
- package/apps/desktop/resources/locales/vi-VN/common.json +32 -0
- package/apps/desktop/resources/locales/vi-VN/dialog.json +31 -0
- package/apps/desktop/resources/locales/vi-VN/menu.json +70 -0
- package/apps/desktop/resources/locales/zh-CN/common.json +32 -0
- package/apps/desktop/resources/locales/zh-CN/dialog.json +31 -0
- package/apps/desktop/resources/locales/zh-CN/menu.json +70 -0
- package/apps/desktop/resources/locales/zh-TW/common.json +32 -0
- package/apps/desktop/resources/locales/zh-TW/dialog.json +31 -0
- package/apps/desktop/resources/locales/zh-TW/menu.json +70 -0
- package/apps/desktop/resources/splash.html +88 -0
- package/apps/desktop/scripts/i18nWorkflow/const.ts +18 -0
- package/apps/desktop/scripts/i18nWorkflow/genDefaultLocale.ts +35 -0
- package/apps/desktop/scripts/i18nWorkflow/genDiff.ts +57 -0
- package/apps/desktop/scripts/i18nWorkflow/index.ts +35 -0
- package/apps/desktop/scripts/i18nWorkflow/utils.ts +54 -0
- package/apps/desktop/scripts/pglite-server.ts +14 -0
- package/apps/desktop/src/common/routes.ts +78 -0
- package/apps/desktop/src/main/appBrowsers.ts +47 -0
- package/apps/desktop/src/main/const/dir.ts +29 -0
- package/apps/desktop/src/main/const/env.ts +3 -0
- package/apps/desktop/src/main/const/store.ts +22 -0
- package/apps/desktop/src/main/controllers/AuthCtr.ts +390 -0
- package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +95 -0
- package/apps/desktop/src/main/controllers/DevtoolsCtr.ts +9 -0
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +380 -0
- package/apps/desktop/src/main/controllers/MenuCtr.ts +29 -0
- package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +335 -0
- package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +321 -0
- package/apps/desktop/src/main/controllers/ShortcutCtr.ts +19 -0
- package/apps/desktop/src/main/controllers/SystemCtr.ts +93 -0
- package/apps/desktop/src/main/controllers/UpdaterCtr.ts +43 -0
- package/apps/desktop/src/main/controllers/UploadFileCtr.ts +34 -0
- package/apps/desktop/src/main/controllers/_template.ts +9 -0
- package/apps/desktop/src/main/controllers/index.ts +58 -0
- package/apps/desktop/src/main/core/App.ts +370 -0
- package/apps/desktop/src/main/core/Browser.ts +345 -0
- package/apps/desktop/src/main/core/BrowserManager.ts +154 -0
- package/apps/desktop/src/main/core/I18nManager.ts +185 -0
- package/apps/desktop/src/main/core/IoCContainer.ts +12 -0
- package/apps/desktop/src/main/core/MenuManager.ts +64 -0
- package/apps/desktop/src/main/core/ShortcutManager.ts +173 -0
- package/apps/desktop/src/main/core/StoreManager.ts +89 -0
- package/apps/desktop/src/main/core/UpdaterManager.ts +321 -0
- package/apps/desktop/src/main/index.ts +5 -0
- package/apps/desktop/src/main/locales/default/common.ts +34 -0
- package/apps/desktop/src/main/locales/default/dialog.ts +33 -0
- package/apps/desktop/src/main/locales/default/index.ts +11 -0
- package/apps/desktop/src/main/locales/default/menu.ts +72 -0
- package/apps/desktop/src/main/locales/resources.ts +35 -0
- package/apps/desktop/src/main/menus/impls/BaseMenuPlatform.ts +10 -0
- package/apps/desktop/src/main/menus/impls/linux.ts +243 -0
- package/apps/desktop/src/main/menus/impls/macOS.ts +360 -0
- package/apps/desktop/src/main/menus/impls/windows.ts +226 -0
- package/apps/desktop/src/main/menus/index.ts +34 -0
- package/apps/desktop/src/main/menus/types.ts +28 -0
- package/apps/desktop/src/main/modules/fileSearch/impl/macOS.ts +577 -0
- package/apps/desktop/src/main/modules/fileSearch/index.ts +23 -0
- package/apps/desktop/src/main/modules/fileSearch/type.ts +27 -0
- package/apps/desktop/src/main/modules/updater/configs.ts +22 -0
- package/apps/desktop/src/main/modules/updater/utils.ts +33 -0
- package/apps/desktop/src/main/services/fileSearchSrv.ts +35 -0
- package/apps/desktop/src/main/services/fileSrv.ts +255 -0
- package/apps/desktop/src/main/services/index.ts +9 -0
- package/apps/desktop/src/main/shortcuts/config.ts +18 -0
- package/apps/desktop/src/main/shortcuts/index.ts +1 -0
- package/apps/desktop/src/main/types/fileSearch.ts +51 -0
- package/apps/desktop/src/main/types/store.ts +14 -0
- package/apps/desktop/src/main/utils/file-system.ts +15 -0
- package/apps/desktop/src/main/utils/logger.ts +44 -0
- package/apps/desktop/src/main/utils/next-electron-rsc.ts +383 -0
- package/apps/desktop/src/preload/electronApi.ts +18 -0
- package/apps/desktop/src/preload/index.ts +14 -0
- package/apps/desktop/src/preload/invoke.ts +10 -0
- package/apps/desktop/src/preload/routeInterceptor.ts +162 -0
- package/apps/desktop/tsconfig.json +21 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/events/remoteServer.ts +11 -4
- package/packages/electron-client-ipc/src/types/dataSync.ts +15 -0
- package/packages/electron-client-ipc/src/types/index.ts +2 -1
- package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +21 -0
- package/packages/electron-server-ipc/src/const.ts +3 -3
- package/packages/electron-server-ipc/src/ipcClient.test.ts +7 -6
- package/packages/electron-server-ipc/src/ipcClient.ts +17 -8
- package/packages/electron-server-ipc/src/ipcServer.ts +7 -3
- package/scripts/electronWorkflow/setDesktopVersion.ts +60 -43
- package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
- package/src/components/Analytics/Desktop.tsx +19 -0
- package/src/components/Analytics/index.tsx +3 -0
- package/src/config/aiModels/wenxin.ts +95 -8
- package/src/database/core/db-adaptor.ts +4 -1
- package/src/database/core/electron.ts +317 -0
- package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx → features/ElectronTitlebar/Connection/ConnectionMode.tsx} +24 -21
- package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Option.tsx +3 -5
- package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx → features/ElectronTitlebar/Connection/RemoteStatus.tsx} +10 -7
- package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/index.tsx +4 -4
- package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateModal.tsx +2 -1
- package/src/libs/trpc/client/async.ts +6 -0
- package/src/libs/trpc/client/edge.ts +6 -0
- package/src/libs/trpc/client/helpers/desktopRemoteRPCFetch.ts +72 -0
- package/src/libs/trpc/client/index.ts +1 -0
- package/src/libs/trpc/client/lambda.ts +10 -1
- package/src/libs/trpc/client/tools.ts +6 -0
- package/src/server/globalConfig/index.ts +0 -3
- package/src/server/modules/ElectronIPCClient/index.ts +3 -1
- package/src/server/routers/desktop/index.ts +2 -0
- package/src/server/routers/desktop/mcp.ts +47 -0
- package/src/server/routers/lambda/user.ts +38 -23
- package/src/server/routers/tools/mcp.ts +0 -6
- package/src/services/electron/remoteServer.ts +4 -4
- package/src/services/mcp.ts +17 -7
- package/src/services/upload.ts +9 -0
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +11 -2
- package/src/store/chat/slices/builtinTool/actions/localFile.ts +110 -53
- package/src/store/electron/actions/sync.ts +20 -19
- package/src/store/electron/initialState.ts +3 -3
- package/src/store/electron/selectors/sync.ts +6 -3
- package/src/store/electron/store.ts +2 -0
- package/src/store/file/slices/upload/action.ts +11 -3
- package/src/store/tool/selectors/tool.ts +10 -1
- package/src/utils/fetch/headers.ts +27 -0
- package/src/utils/fetch/index.ts +2 -0
- package/src/utils/fetch/request.ts +28 -0
- package/packages/electron-client-ipc/src/types/remoteServer.ts +0 -8
- /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Waiting.tsx +0 -0
- /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateNotification.tsx +0 -0
- /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
|
+
}
|