@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.
- 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 +25 -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 +9 -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/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,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
|
+
}
|