@lobehub/chat 1.82.10 → 1.83.1
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 +194 -0
- package/CHANGELOG.md +42 -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 +14 -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,29 @@
|
|
1
|
+
import { app } from 'electron';
|
2
|
+
import { join } from 'node:path';
|
3
|
+
|
4
|
+
export const mainDir = join(__dirname);
|
5
|
+
|
6
|
+
export const preloadDir = join(mainDir, '../preload');
|
7
|
+
|
8
|
+
export const resourcesDir = join(mainDir, '../../resources');
|
9
|
+
|
10
|
+
export const buildDir = join(mainDir, '../../build');
|
11
|
+
|
12
|
+
const appPath = app.getAppPath();
|
13
|
+
|
14
|
+
export const nextStandaloneDir = join(appPath, 'dist', 'next');
|
15
|
+
|
16
|
+
export const userDataDir = app.getPath('userData');
|
17
|
+
|
18
|
+
export const appStorageDir = join(userDataDir, 'lobehub-storage');
|
19
|
+
|
20
|
+
// ------ Application storage directory ---- //
|
21
|
+
|
22
|
+
// db schema hash
|
23
|
+
export const DB_SCHEMA_HASH_FILENAME = 'lobehub-local-db-schema-hash';
|
24
|
+
// pglite database dir
|
25
|
+
export const LOCAL_DATABASE_DIR = 'lobehub-local-db';
|
26
|
+
// 本地存储文件(模拟 S3)
|
27
|
+
export const FILE_STORAGE_DIR = 'file-storage';
|
28
|
+
// Plugin 安装目录
|
29
|
+
export const INSTALL_PLUGINS_DIR = 'plugins';
|
@@ -0,0 +1,22 @@
|
|
1
|
+
/**
|
2
|
+
* 应用设置存储相关常量
|
3
|
+
*/
|
4
|
+
import { appStorageDir } from '@/const/dir';
|
5
|
+
import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
|
6
|
+
import { ElectronMainStore } from '@/types/store';
|
7
|
+
|
8
|
+
/**
|
9
|
+
* 存储名称
|
10
|
+
*/
|
11
|
+
export const STORE_NAME = 'lobehub-settings';
|
12
|
+
|
13
|
+
/**
|
14
|
+
* 存储默认值
|
15
|
+
*/
|
16
|
+
export const STORE_DEFAULTS: ElectronMainStore = {
|
17
|
+
dataSyncConfig: { storageMode: 'local' },
|
18
|
+
encryptedTokens: {},
|
19
|
+
locale: 'auto',
|
20
|
+
shortcuts: DEFAULT_SHORTCUTS_CONFIG,
|
21
|
+
storagePath: appStorageDir,
|
22
|
+
};
|
@@ -0,0 +1,390 @@
|
|
1
|
+
import { DataSyncConfig } from '@lobechat/electron-client-ipc';
|
2
|
+
import { BrowserWindow, app, shell } from 'electron';
|
3
|
+
import crypto from 'node:crypto';
|
4
|
+
import querystring from 'node:querystring';
|
5
|
+
import { URL } from 'node:url';
|
6
|
+
|
7
|
+
import { name } from '@/../../package.json';
|
8
|
+
import { createLogger } from '@/utils/logger';
|
9
|
+
|
10
|
+
import RemoteServerConfigCtr from './RemoteServerConfigCtr';
|
11
|
+
import { ControllerModule, ipcClientEvent } from './index';
|
12
|
+
|
13
|
+
// Create logger
|
14
|
+
const logger = createLogger('controllers:AuthCtr');
|
15
|
+
|
16
|
+
const protocolPrefix = `com.lobehub.${name}`;
|
17
|
+
/**
|
18
|
+
* Authentication Controller
|
19
|
+
* Used to implement the OAuth authorization flow
|
20
|
+
*/
|
21
|
+
export default class AuthCtr extends ControllerModule {
|
22
|
+
/**
|
23
|
+
* 远程服务器配置控制器
|
24
|
+
*/
|
25
|
+
private get remoteServerConfigCtr() {
|
26
|
+
return this.app.getController(RemoteServerConfigCtr);
|
27
|
+
}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* 当前的 PKCE 参数
|
31
|
+
*/
|
32
|
+
private codeVerifier: string | null = null;
|
33
|
+
private authRequestState: string | null = null;
|
34
|
+
|
35
|
+
beforeAppReady = () => {
|
36
|
+
this.registerProtocolHandler();
|
37
|
+
};
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Request OAuth authorization
|
41
|
+
*/
|
42
|
+
@ipcClientEvent('requestAuthorization')
|
43
|
+
async requestAuthorization(config: DataSyncConfig) {
|
44
|
+
const remoteUrl = await this.remoteServerConfigCtr.getRemoteServerUrl(config);
|
45
|
+
|
46
|
+
logger.info(
|
47
|
+
`Requesting OAuth authorization, storageMode:${config.storageMode} server URL: ${remoteUrl}`,
|
48
|
+
);
|
49
|
+
try {
|
50
|
+
// Generate PKCE parameters
|
51
|
+
logger.debug('Generating PKCE parameters');
|
52
|
+
const codeVerifier = this.generateCodeVerifier();
|
53
|
+
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
|
54
|
+
this.codeVerifier = codeVerifier;
|
55
|
+
|
56
|
+
// Generate state parameter to prevent CSRF attacks
|
57
|
+
this.authRequestState = crypto.randomBytes(16).toString('hex');
|
58
|
+
logger.debug(`Generated state parameter: ${this.authRequestState}`);
|
59
|
+
|
60
|
+
// Construct authorization URL
|
61
|
+
const authUrl = new URL('/oidc/auth', remoteUrl);
|
62
|
+
|
63
|
+
// Add query parameters
|
64
|
+
authUrl.search = querystring.stringify({
|
65
|
+
client_id: 'lobehub-desktop',
|
66
|
+
code_challenge: codeChallenge,
|
67
|
+
code_challenge_method: 'S256',
|
68
|
+
prompt: 'consent',
|
69
|
+
redirect_uri: `${protocolPrefix}://auth/callback`,
|
70
|
+
response_type: 'code',
|
71
|
+
scope: 'profile email offline_access',
|
72
|
+
state: this.authRequestState,
|
73
|
+
});
|
74
|
+
|
75
|
+
logger.info(`Constructed authorization URL: ${authUrl.toString()}`);
|
76
|
+
|
77
|
+
// Open authorization URL in the default browser
|
78
|
+
await shell.openExternal(authUrl.toString());
|
79
|
+
logger.debug('Opening authorization URL in default browser');
|
80
|
+
|
81
|
+
return { success: true };
|
82
|
+
} catch (error) {
|
83
|
+
logger.error('Authorization request failed:', error);
|
84
|
+
return { error: error.message, success: false };
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Handle authorization callback
|
90
|
+
* This method is called when the browser redirects to our custom protocol
|
91
|
+
*/
|
92
|
+
async handleAuthCallback(callbackUrl: string) {
|
93
|
+
logger.info(`Handling authorization callback: ${callbackUrl}`);
|
94
|
+
try {
|
95
|
+
const url = new URL(callbackUrl);
|
96
|
+
const params = new URLSearchParams(url.search);
|
97
|
+
|
98
|
+
// Get authorization code
|
99
|
+
const code = params.get('code');
|
100
|
+
const state = params.get('state');
|
101
|
+
logger.debug(`Got parameters from callback URL: code=${code}, state=${state}`);
|
102
|
+
|
103
|
+
// Validate state parameter to prevent CSRF attacks
|
104
|
+
if (state !== this.authRequestState) {
|
105
|
+
logger.error(
|
106
|
+
`Invalid state parameter: expected ${this.authRequestState}, received ${state}`,
|
107
|
+
);
|
108
|
+
throw new Error('Invalid state parameter');
|
109
|
+
}
|
110
|
+
logger.debug('State parameter validation passed');
|
111
|
+
|
112
|
+
if (!code) {
|
113
|
+
logger.error('No authorization code received');
|
114
|
+
throw new Error('No authorization code received');
|
115
|
+
}
|
116
|
+
|
117
|
+
// Get configuration information
|
118
|
+
const config = await this.remoteServerConfigCtr.getRemoteServerConfig();
|
119
|
+
logger.debug(`Getting remote server configuration: url=${config.remoteServerUrl}`);
|
120
|
+
|
121
|
+
if (!config.remoteServerUrl) {
|
122
|
+
logger.error('Server URL not configured');
|
123
|
+
throw new Error('No server URL configured');
|
124
|
+
}
|
125
|
+
|
126
|
+
// Get the previously saved code_verifier
|
127
|
+
const codeVerifier = this.codeVerifier;
|
128
|
+
if (!codeVerifier) {
|
129
|
+
logger.error('Code verifier not found');
|
130
|
+
throw new Error('No code verifier found');
|
131
|
+
}
|
132
|
+
logger.debug('Found code verifier');
|
133
|
+
|
134
|
+
// Exchange authorization code for token
|
135
|
+
logger.debug('Starting to exchange authorization code for token');
|
136
|
+
const result = await this.exchangeCodeForToken(code, codeVerifier);
|
137
|
+
|
138
|
+
if (result.success) {
|
139
|
+
logger.info('Authorization successful');
|
140
|
+
// Notify render process of successful authorization
|
141
|
+
this.broadcastAuthorizationSuccessful();
|
142
|
+
} else {
|
143
|
+
logger.warn(`Authorization failed: ${result.error || 'Unknown error'}`);
|
144
|
+
// Notify render process of failed authorization
|
145
|
+
this.broadcastAuthorizationFailed(result.error || 'Unknown error');
|
146
|
+
}
|
147
|
+
|
148
|
+
return result;
|
149
|
+
} catch (error) {
|
150
|
+
logger.error('Handling authorization callback failed:', error);
|
151
|
+
|
152
|
+
// Notify render process of failed authorization
|
153
|
+
this.broadcastAuthorizationFailed(error.message);
|
154
|
+
|
155
|
+
return { error: error.message, success: false };
|
156
|
+
} finally {
|
157
|
+
// Clear authorization request state
|
158
|
+
logger.debug('Clearing authorization request state');
|
159
|
+
this.authRequestState = null;
|
160
|
+
this.codeVerifier = null;
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
/**
|
165
|
+
* Refresh access token
|
166
|
+
*/
|
167
|
+
@ipcClientEvent('refreshAccessToken')
|
168
|
+
async refreshAccessToken() {
|
169
|
+
logger.info('Starting to refresh access token');
|
170
|
+
try {
|
171
|
+
// Call the centralized refresh logic in RemoteServerConfigCtr
|
172
|
+
const result = await this.remoteServerConfigCtr.refreshAccessToken();
|
173
|
+
|
174
|
+
if (result.success) {
|
175
|
+
logger.info('Token refresh successful via AuthCtr call.');
|
176
|
+
// Notify render process that token has been refreshed
|
177
|
+
this.broadcastTokenRefreshed();
|
178
|
+
return { success: true };
|
179
|
+
} else {
|
180
|
+
// Throw an error to be caught by the catch block below
|
181
|
+
// This maintains the existing behavior of clearing tokens on failure
|
182
|
+
logger.error(`Token refresh failed via AuthCtr call: ${result.error}`);
|
183
|
+
throw new Error(result.error || 'Token refresh failed');
|
184
|
+
}
|
185
|
+
} catch (error) {
|
186
|
+
// Keep the existing logic to clear tokens and require re-auth on failure
|
187
|
+
logger.error('Token refresh operation failed via AuthCtr, initiating cleanup:', error);
|
188
|
+
|
189
|
+
// Refresh failed, clear tokens and disable remote server
|
190
|
+
logger.warn('Refresh failed, clearing tokens and disabling remote server');
|
191
|
+
await this.remoteServerConfigCtr.clearTokens();
|
192
|
+
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: false });
|
193
|
+
|
194
|
+
// Notify render process that re-authorization is required
|
195
|
+
this.broadcastAuthorizationRequired();
|
196
|
+
|
197
|
+
return { error: error.message, success: false };
|
198
|
+
}
|
199
|
+
}
|
200
|
+
|
201
|
+
/**
|
202
|
+
* Register custom protocol handler
|
203
|
+
*/
|
204
|
+
private registerProtocolHandler() {
|
205
|
+
logger.info(`Registering custom protocol handler ${protocolPrefix}://`);
|
206
|
+
app.setAsDefaultProtocolClient(protocolPrefix);
|
207
|
+
|
208
|
+
// Register custom protocol handler
|
209
|
+
if (process.platform === 'darwin') {
|
210
|
+
// Handle open-url event on macOS
|
211
|
+
logger.debug('Registering open-url event handler for macOS');
|
212
|
+
app.on('open-url', (event, url) => {
|
213
|
+
event.preventDefault();
|
214
|
+
logger.info(`Received open-url event: ${url}`);
|
215
|
+
this.handleAuthCallback(url);
|
216
|
+
});
|
217
|
+
} else {
|
218
|
+
// Handle protocol callback via second-instance event on Windows and Linux
|
219
|
+
logger.debug('Registering second-instance event handler for Windows/Linux');
|
220
|
+
app.on('second-instance', (event, commandLine) => {
|
221
|
+
// Find the URL from command line arguments
|
222
|
+
const url = commandLine.find((arg) => arg.startsWith(`${protocolPrefix}://`));
|
223
|
+
if (url) {
|
224
|
+
logger.info(`Found URL from second-instance command line arguments: ${url}`);
|
225
|
+
this.handleAuthCallback(url);
|
226
|
+
} else {
|
227
|
+
logger.warn('Protocol URL not found in second-instance command line arguments');
|
228
|
+
}
|
229
|
+
});
|
230
|
+
}
|
231
|
+
|
232
|
+
logger.info(`Registered ${protocolPrefix}:// custom protocol handler`);
|
233
|
+
}
|
234
|
+
|
235
|
+
/**
|
236
|
+
* Exchange authorization code for token
|
237
|
+
*/
|
238
|
+
private async exchangeCodeForToken(code: string, codeVerifier: string) {
|
239
|
+
const remoteUrl = await this.remoteServerConfigCtr.getRemoteServerUrl();
|
240
|
+
logger.info('Starting to exchange authorization code for token');
|
241
|
+
try {
|
242
|
+
const tokenUrl = new URL('/oidc/token', remoteUrl);
|
243
|
+
logger.debug(`Constructed token exchange URL: ${tokenUrl.toString()}`);
|
244
|
+
|
245
|
+
// Construct request body
|
246
|
+
const body = querystring.stringify({
|
247
|
+
client_id: 'lobehub-desktop',
|
248
|
+
code,
|
249
|
+
code_verifier: codeVerifier,
|
250
|
+
grant_type: 'authorization_code',
|
251
|
+
redirect_uri: `${protocolPrefix}://auth/callback`,
|
252
|
+
});
|
253
|
+
|
254
|
+
logger.debug('Sending token exchange request');
|
255
|
+
// Send request to get token
|
256
|
+
const response = await fetch(tokenUrl.toString(), {
|
257
|
+
body,
|
258
|
+
headers: {
|
259
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
260
|
+
},
|
261
|
+
method: 'POST',
|
262
|
+
});
|
263
|
+
|
264
|
+
if (!response.ok) {
|
265
|
+
// Try parsing the error response
|
266
|
+
const errorData = await response.json().catch(() => ({}));
|
267
|
+
const errorMessage = `Failed to get token: ${response.status} ${response.statusText} ${errorData.error_description || errorData.error || ''}`;
|
268
|
+
logger.error(errorMessage);
|
269
|
+
throw new Error(errorMessage);
|
270
|
+
}
|
271
|
+
|
272
|
+
// Parse response
|
273
|
+
const data = await response.json();
|
274
|
+
logger.debug('Successfully received token exchange response');
|
275
|
+
// console.log(data); // Keep original log for debugging, or remove/change to logger.debug as needed
|
276
|
+
|
277
|
+
// Ensure response contains necessary fields
|
278
|
+
if (!data.access_token || !data.refresh_token) {
|
279
|
+
logger.error('Invalid token response: missing access_token or refresh_token');
|
280
|
+
throw new Error('Invalid token response: missing required fields');
|
281
|
+
}
|
282
|
+
|
283
|
+
// Save tokens
|
284
|
+
logger.debug('Starting to save exchanged tokens');
|
285
|
+
await this.remoteServerConfigCtr.saveTokens(data.access_token, data.refresh_token);
|
286
|
+
logger.info('Successfully saved exchanged tokens');
|
287
|
+
|
288
|
+
// Set server to active state
|
289
|
+
logger.debug(`Setting remote server to active state: ${remoteUrl}`);
|
290
|
+
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: true });
|
291
|
+
|
292
|
+
return { success: true };
|
293
|
+
} catch (error) {
|
294
|
+
logger.error('Exchanging authorization code failed:', error);
|
295
|
+
return { error: error.message, success: false };
|
296
|
+
}
|
297
|
+
}
|
298
|
+
|
299
|
+
/**
|
300
|
+
* Broadcast token refreshed event
|
301
|
+
*/
|
302
|
+
private broadcastTokenRefreshed() {
|
303
|
+
logger.debug('Broadcasting tokenRefreshed event to all windows');
|
304
|
+
const allWindows = BrowserWindow.getAllWindows();
|
305
|
+
|
306
|
+
for (const win of allWindows) {
|
307
|
+
if (!win.isDestroyed()) {
|
308
|
+
win.webContents.send('tokenRefreshed');
|
309
|
+
}
|
310
|
+
}
|
311
|
+
}
|
312
|
+
|
313
|
+
/**
|
314
|
+
* Broadcast authorization successful event
|
315
|
+
*/
|
316
|
+
private broadcastAuthorizationSuccessful() {
|
317
|
+
logger.debug('Broadcasting authorizationSuccessful event to all windows');
|
318
|
+
const allWindows = BrowserWindow.getAllWindows();
|
319
|
+
|
320
|
+
for (const win of allWindows) {
|
321
|
+
if (!win.isDestroyed()) {
|
322
|
+
win.webContents.send('authorizationSuccessful');
|
323
|
+
}
|
324
|
+
}
|
325
|
+
}
|
326
|
+
|
327
|
+
/**
|
328
|
+
* Broadcast authorization failed event
|
329
|
+
*/
|
330
|
+
private broadcastAuthorizationFailed(error: string) {
|
331
|
+
logger.debug(`Broadcasting authorizationFailed event to all windows, error: ${error}`);
|
332
|
+
const allWindows = BrowserWindow.getAllWindows();
|
333
|
+
|
334
|
+
for (const win of allWindows) {
|
335
|
+
if (!win.isDestroyed()) {
|
336
|
+
win.webContents.send('authorizationFailed', { error });
|
337
|
+
}
|
338
|
+
}
|
339
|
+
}
|
340
|
+
|
341
|
+
/**
|
342
|
+
* Broadcast authorization required event
|
343
|
+
*/
|
344
|
+
private broadcastAuthorizationRequired() {
|
345
|
+
logger.debug('Broadcasting authorizationRequired event to all windows');
|
346
|
+
const allWindows = BrowserWindow.getAllWindows();
|
347
|
+
|
348
|
+
for (const win of allWindows) {
|
349
|
+
if (!win.isDestroyed()) {
|
350
|
+
win.webContents.send('authorizationRequired');
|
351
|
+
}
|
352
|
+
}
|
353
|
+
}
|
354
|
+
|
355
|
+
/**
|
356
|
+
* Generate PKCE codeVerifier
|
357
|
+
*/
|
358
|
+
private generateCodeVerifier(): string {
|
359
|
+
logger.debug('Generating PKCE code verifier');
|
360
|
+
// Generate a random string of at least 43 characters
|
361
|
+
const verifier = crypto
|
362
|
+
.randomBytes(32)
|
363
|
+
.toString('base64')
|
364
|
+
.replaceAll('+', '-')
|
365
|
+
.replaceAll('/', '_')
|
366
|
+
.replace(/=+$/, '');
|
367
|
+
logger.debug('Generated code verifier (partial): ' + verifier.slice(0, 10) + '...'); // Avoid logging full sensitive info
|
368
|
+
return verifier;
|
369
|
+
}
|
370
|
+
|
371
|
+
/**
|
372
|
+
* Generate codeChallenge from codeVerifier (S256 method)
|
373
|
+
*/
|
374
|
+
private async generateCodeChallenge(codeVerifier: string): Promise<string> {
|
375
|
+
logger.debug('Generating PKCE code challenge (S256)');
|
376
|
+
// Hash codeVerifier using SHA-256
|
377
|
+
const encoder = new TextEncoder();
|
378
|
+
const data = encoder.encode(codeVerifier);
|
379
|
+
const digest = await crypto.subtle.digest('SHA-256', data);
|
380
|
+
|
381
|
+
// Convert hash result to base64url encoding
|
382
|
+
const challenge = Buffer.from(digest)
|
383
|
+
.toString('base64')
|
384
|
+
.replaceAll('+', '-')
|
385
|
+
.replaceAll('/', '_')
|
386
|
+
.replace(/=+$/, '');
|
387
|
+
logger.debug('Generated code challenge (partial): ' + challenge.slice(0, 10) + '...'); // Avoid logging full sensitive info
|
388
|
+
return challenge;
|
389
|
+
}
|
390
|
+
}
|
@@ -0,0 +1,95 @@
|
|
1
|
+
import { InterceptRouteParams } from '@lobechat/electron-client-ipc';
|
2
|
+
import { extractSubPath, findMatchingRoute } from '~common/routes';
|
3
|
+
|
4
|
+
import { AppBrowsersIdentifiers, BrowsersIdentifiers } from '@/appBrowsers';
|
5
|
+
|
6
|
+
import { ControllerModule, ipcClientEvent, shortcut } from './index';
|
7
|
+
|
8
|
+
export default class BrowserWindowsCtr extends ControllerModule {
|
9
|
+
@shortcut('toggleMainWindow')
|
10
|
+
async toggleMainWindow() {
|
11
|
+
const mainWindow = this.app.browserManager.getMainWindow();
|
12
|
+
mainWindow.toggleVisible();
|
13
|
+
}
|
14
|
+
|
15
|
+
@ipcClientEvent('openSettingsWindow')
|
16
|
+
async openSettingsWindow(tab?: string) {
|
17
|
+
console.log('[BrowserWindowsCtr] Received request to open settings window', tab);
|
18
|
+
|
19
|
+
try {
|
20
|
+
await this.app.browserManager.showSettingsWindowWithTab(tab);
|
21
|
+
|
22
|
+
return { success: true };
|
23
|
+
} catch (error) {
|
24
|
+
console.error('[BrowserWindowsCtr] Failed to open settings window:', error);
|
25
|
+
return { error: error.message, success: false };
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Handle route interception requests
|
31
|
+
* Responsible for handling route interception requests from the renderer process
|
32
|
+
*/
|
33
|
+
@ipcClientEvent('interceptRoute')
|
34
|
+
async interceptRoute(params: InterceptRouteParams) {
|
35
|
+
const { path, source } = params;
|
36
|
+
console.log(
|
37
|
+
`[BrowserWindowsCtr] Received route interception request: ${path}, source: ${source}`,
|
38
|
+
);
|
39
|
+
|
40
|
+
// Find matching route configuration
|
41
|
+
const matchedRoute = findMatchingRoute(path);
|
42
|
+
|
43
|
+
// If no matching route found, return not intercepted
|
44
|
+
if (!matchedRoute) {
|
45
|
+
console.log(`[BrowserWindowsCtr] No matching route configuration found: ${path}`);
|
46
|
+
return { intercepted: false, path, source };
|
47
|
+
}
|
48
|
+
|
49
|
+
console.log(
|
50
|
+
`[BrowserWindowsCtr] Intercepted route: ${path}, target window: ${matchedRoute.targetWindow}`,
|
51
|
+
);
|
52
|
+
|
53
|
+
try {
|
54
|
+
if (matchedRoute.targetWindow === BrowsersIdentifiers.settings) {
|
55
|
+
const subPath = extractSubPath(path, matchedRoute.pathPrefix);
|
56
|
+
|
57
|
+
await this.app.browserManager.showSettingsWindowWithTab(subPath);
|
58
|
+
|
59
|
+
return {
|
60
|
+
intercepted: true,
|
61
|
+
path,
|
62
|
+
source,
|
63
|
+
subPath,
|
64
|
+
targetWindow: matchedRoute.targetWindow,
|
65
|
+
};
|
66
|
+
} else {
|
67
|
+
await this.openTargetWindow(matchedRoute.targetWindow as AppBrowsersIdentifiers);
|
68
|
+
|
69
|
+
return {
|
70
|
+
intercepted: true,
|
71
|
+
path,
|
72
|
+
source,
|
73
|
+
targetWindow: matchedRoute.targetWindow,
|
74
|
+
};
|
75
|
+
}
|
76
|
+
} catch (error) {
|
77
|
+
console.error('[BrowserWindowsCtr] Error while processing route interception:', error);
|
78
|
+
return {
|
79
|
+
error: error.message,
|
80
|
+
intercepted: false,
|
81
|
+
path,
|
82
|
+
source,
|
83
|
+
};
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
/**
|
88
|
+
* Open target window and navigate to specified sub-path
|
89
|
+
*/
|
90
|
+
private async openTargetWindow(targetWindow: AppBrowsersIdentifiers) {
|
91
|
+
// Ensure the window can always be created or reopened
|
92
|
+
const browser = this.app.browserManager.retrieveByIdentifier(targetWindow);
|
93
|
+
browser.show();
|
94
|
+
}
|
95
|
+
}
|
@@ -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
|
+
}
|