@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.
Files changed (195) hide show
  1. package/.env.desktop +1 -2
  2. package/.github/workflows/{release-desktop.yml → desktop-pr-build.yml} +59 -137
  3. package/.github/workflows/release-desktop-beta.yml +194 -0
  4. package/CHANGELOG.md +42 -0
  5. package/apps/desktop/.i18nrc.js +31 -0
  6. package/apps/desktop/Development.md +47 -0
  7. package/apps/desktop/README.md +6 -0
  8. package/apps/desktop/build/Icon-beta.icns +0 -0
  9. package/apps/desktop/build/Icon-nightly.icns +0 -0
  10. package/apps/desktop/build/Icon.icns +0 -0
  11. package/apps/desktop/build/entitlements.mac.plist +12 -0
  12. package/apps/desktop/build/favicon.ico +0 -0
  13. package/apps/desktop/build/icon-beta.png +0 -0
  14. package/apps/desktop/build/icon-dev.png +0 -0
  15. package/apps/desktop/build/icon-nightly.ico +0 -0
  16. package/apps/desktop/build/icon-nightly.png +0 -0
  17. package/apps/desktop/build/icon.ico +0 -0
  18. package/apps/desktop/build/icon.png +0 -0
  19. package/apps/desktop/dev-app-update.yml +6 -0
  20. package/apps/desktop/electron-builder.js +92 -0
  21. package/apps/desktop/electron.vite.config.ts +40 -0
  22. package/apps/desktop/package.json +72 -0
  23. package/apps/desktop/pnpm-workspace.yaml +5 -0
  24. package/apps/desktop/resources/error.html +136 -0
  25. package/apps/desktop/resources/locales/ar/common.json +32 -0
  26. package/apps/desktop/resources/locales/ar/dialog.json +31 -0
  27. package/apps/desktop/resources/locales/ar/menu.json +70 -0
  28. package/apps/desktop/resources/locales/bg-BG/common.json +32 -0
  29. package/apps/desktop/resources/locales/bg-BG/dialog.json +31 -0
  30. package/apps/desktop/resources/locales/bg-BG/menu.json +70 -0
  31. package/apps/desktop/resources/locales/de-DE/common.json +32 -0
  32. package/apps/desktop/resources/locales/de-DE/dialog.json +31 -0
  33. package/apps/desktop/resources/locales/de-DE/menu.json +70 -0
  34. package/apps/desktop/resources/locales/en-US/common.json +32 -0
  35. package/apps/desktop/resources/locales/en-US/dialog.json +31 -0
  36. package/apps/desktop/resources/locales/en-US/menu.json +70 -0
  37. package/apps/desktop/resources/locales/es-ES/common.json +32 -0
  38. package/apps/desktop/resources/locales/es-ES/dialog.json +31 -0
  39. package/apps/desktop/resources/locales/es-ES/menu.json +70 -0
  40. package/apps/desktop/resources/locales/fa-IR/common.json +32 -0
  41. package/apps/desktop/resources/locales/fa-IR/dialog.json +31 -0
  42. package/apps/desktop/resources/locales/fa-IR/menu.json +70 -0
  43. package/apps/desktop/resources/locales/fr-FR/common.json +32 -0
  44. package/apps/desktop/resources/locales/fr-FR/dialog.json +31 -0
  45. package/apps/desktop/resources/locales/fr-FR/menu.json +70 -0
  46. package/apps/desktop/resources/locales/it-IT/common.json +32 -0
  47. package/apps/desktop/resources/locales/it-IT/dialog.json +31 -0
  48. package/apps/desktop/resources/locales/it-IT/menu.json +70 -0
  49. package/apps/desktop/resources/locales/ja-JP/common.json +32 -0
  50. package/apps/desktop/resources/locales/ja-JP/dialog.json +31 -0
  51. package/apps/desktop/resources/locales/ja-JP/menu.json +70 -0
  52. package/apps/desktop/resources/locales/ko-KR/common.json +32 -0
  53. package/apps/desktop/resources/locales/ko-KR/dialog.json +31 -0
  54. package/apps/desktop/resources/locales/ko-KR/menu.json +70 -0
  55. package/apps/desktop/resources/locales/nl-NL/common.json +32 -0
  56. package/apps/desktop/resources/locales/nl-NL/dialog.json +31 -0
  57. package/apps/desktop/resources/locales/nl-NL/menu.json +70 -0
  58. package/apps/desktop/resources/locales/pl-PL/common.json +32 -0
  59. package/apps/desktop/resources/locales/pl-PL/dialog.json +31 -0
  60. package/apps/desktop/resources/locales/pl-PL/menu.json +70 -0
  61. package/apps/desktop/resources/locales/pt-BR/common.json +32 -0
  62. package/apps/desktop/resources/locales/pt-BR/dialog.json +31 -0
  63. package/apps/desktop/resources/locales/pt-BR/menu.json +70 -0
  64. package/apps/desktop/resources/locales/ru-RU/common.json +32 -0
  65. package/apps/desktop/resources/locales/ru-RU/dialog.json +31 -0
  66. package/apps/desktop/resources/locales/ru-RU/menu.json +70 -0
  67. package/apps/desktop/resources/locales/tr-TR/common.json +32 -0
  68. package/apps/desktop/resources/locales/tr-TR/dialog.json +31 -0
  69. package/apps/desktop/resources/locales/tr-TR/menu.json +70 -0
  70. package/apps/desktop/resources/locales/vi-VN/common.json +32 -0
  71. package/apps/desktop/resources/locales/vi-VN/dialog.json +31 -0
  72. package/apps/desktop/resources/locales/vi-VN/menu.json +70 -0
  73. package/apps/desktop/resources/locales/zh-CN/common.json +32 -0
  74. package/apps/desktop/resources/locales/zh-CN/dialog.json +31 -0
  75. package/apps/desktop/resources/locales/zh-CN/menu.json +70 -0
  76. package/apps/desktop/resources/locales/zh-TW/common.json +32 -0
  77. package/apps/desktop/resources/locales/zh-TW/dialog.json +31 -0
  78. package/apps/desktop/resources/locales/zh-TW/menu.json +70 -0
  79. package/apps/desktop/resources/splash.html +88 -0
  80. package/apps/desktop/scripts/i18nWorkflow/const.ts +18 -0
  81. package/apps/desktop/scripts/i18nWorkflow/genDefaultLocale.ts +35 -0
  82. package/apps/desktop/scripts/i18nWorkflow/genDiff.ts +57 -0
  83. package/apps/desktop/scripts/i18nWorkflow/index.ts +35 -0
  84. package/apps/desktop/scripts/i18nWorkflow/utils.ts +54 -0
  85. package/apps/desktop/scripts/pglite-server.ts +14 -0
  86. package/apps/desktop/src/common/routes.ts +78 -0
  87. package/apps/desktop/src/main/appBrowsers.ts +47 -0
  88. package/apps/desktop/src/main/const/dir.ts +29 -0
  89. package/apps/desktop/src/main/const/env.ts +3 -0
  90. package/apps/desktop/src/main/const/store.ts +22 -0
  91. package/apps/desktop/src/main/controllers/AuthCtr.ts +390 -0
  92. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +95 -0
  93. package/apps/desktop/src/main/controllers/DevtoolsCtr.ts +9 -0
  94. package/apps/desktop/src/main/controllers/LocalFileCtr.ts +380 -0
  95. package/apps/desktop/src/main/controllers/MenuCtr.ts +29 -0
  96. package/apps/desktop/src/main/controllers/RemoteServerConfigCtr.ts +335 -0
  97. package/apps/desktop/src/main/controllers/RemoteServerSyncCtr.ts +321 -0
  98. package/apps/desktop/src/main/controllers/ShortcutCtr.ts +19 -0
  99. package/apps/desktop/src/main/controllers/SystemCtr.ts +93 -0
  100. package/apps/desktop/src/main/controllers/UpdaterCtr.ts +43 -0
  101. package/apps/desktop/src/main/controllers/UploadFileCtr.ts +34 -0
  102. package/apps/desktop/src/main/controllers/_template.ts +9 -0
  103. package/apps/desktop/src/main/controllers/index.ts +58 -0
  104. package/apps/desktop/src/main/core/App.ts +370 -0
  105. package/apps/desktop/src/main/core/Browser.ts +345 -0
  106. package/apps/desktop/src/main/core/BrowserManager.ts +154 -0
  107. package/apps/desktop/src/main/core/I18nManager.ts +185 -0
  108. package/apps/desktop/src/main/core/IoCContainer.ts +12 -0
  109. package/apps/desktop/src/main/core/MenuManager.ts +64 -0
  110. package/apps/desktop/src/main/core/ShortcutManager.ts +173 -0
  111. package/apps/desktop/src/main/core/StoreManager.ts +89 -0
  112. package/apps/desktop/src/main/core/UpdaterManager.ts +321 -0
  113. package/apps/desktop/src/main/index.ts +5 -0
  114. package/apps/desktop/src/main/locales/default/common.ts +34 -0
  115. package/apps/desktop/src/main/locales/default/dialog.ts +33 -0
  116. package/apps/desktop/src/main/locales/default/index.ts +11 -0
  117. package/apps/desktop/src/main/locales/default/menu.ts +72 -0
  118. package/apps/desktop/src/main/locales/resources.ts +35 -0
  119. package/apps/desktop/src/main/menus/impls/BaseMenuPlatform.ts +10 -0
  120. package/apps/desktop/src/main/menus/impls/linux.ts +243 -0
  121. package/apps/desktop/src/main/menus/impls/macOS.ts +360 -0
  122. package/apps/desktop/src/main/menus/impls/windows.ts +226 -0
  123. package/apps/desktop/src/main/menus/index.ts +34 -0
  124. package/apps/desktop/src/main/menus/types.ts +28 -0
  125. package/apps/desktop/src/main/modules/fileSearch/impl/macOS.ts +577 -0
  126. package/apps/desktop/src/main/modules/fileSearch/index.ts +23 -0
  127. package/apps/desktop/src/main/modules/fileSearch/type.ts +27 -0
  128. package/apps/desktop/src/main/modules/updater/configs.ts +22 -0
  129. package/apps/desktop/src/main/modules/updater/utils.ts +33 -0
  130. package/apps/desktop/src/main/services/fileSearchSrv.ts +35 -0
  131. package/apps/desktop/src/main/services/fileSrv.ts +255 -0
  132. package/apps/desktop/src/main/services/index.ts +9 -0
  133. package/apps/desktop/src/main/shortcuts/config.ts +18 -0
  134. package/apps/desktop/src/main/shortcuts/index.ts +1 -0
  135. package/apps/desktop/src/main/types/fileSearch.ts +51 -0
  136. package/apps/desktop/src/main/types/store.ts +14 -0
  137. package/apps/desktop/src/main/utils/file-system.ts +15 -0
  138. package/apps/desktop/src/main/utils/logger.ts +44 -0
  139. package/apps/desktop/src/main/utils/next-electron-rsc.ts +383 -0
  140. package/apps/desktop/src/preload/electronApi.ts +18 -0
  141. package/apps/desktop/src/preload/index.ts +14 -0
  142. package/apps/desktop/src/preload/invoke.ts +10 -0
  143. package/apps/desktop/src/preload/routeInterceptor.ts +162 -0
  144. package/apps/desktop/tsconfig.json +21 -0
  145. package/changelog/v1.json +14 -0
  146. package/package.json +1 -1
  147. package/packages/electron-client-ipc/src/events/remoteServer.ts +11 -4
  148. package/packages/electron-client-ipc/src/types/dataSync.ts +15 -0
  149. package/packages/electron-client-ipc/src/types/index.ts +2 -1
  150. package/packages/electron-client-ipc/src/types/proxyTRPCRequest.ts +21 -0
  151. package/packages/electron-server-ipc/src/const.ts +3 -3
  152. package/packages/electron-server-ipc/src/ipcClient.test.ts +7 -6
  153. package/packages/electron-server-ipc/src/ipcClient.ts +17 -8
  154. package/packages/electron-server-ipc/src/ipcServer.ts +7 -3
  155. package/scripts/electronWorkflow/setDesktopVersion.ts +60 -43
  156. package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
  157. package/src/components/Analytics/Desktop.tsx +19 -0
  158. package/src/components/Analytics/index.tsx +3 -0
  159. package/src/database/core/db-adaptor.ts +4 -1
  160. package/src/database/core/electron.ts +317 -0
  161. package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx → features/ElectronTitlebar/Connection/ConnectionMode.tsx} +24 -21
  162. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Option.tsx +3 -5
  163. package/src/{app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx → features/ElectronTitlebar/Connection/RemoteStatus.tsx} +10 -7
  164. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/index.tsx +4 -4
  165. package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateModal.tsx +2 -1
  166. package/src/libs/trpc/client/async.ts +6 -0
  167. package/src/libs/trpc/client/edge.ts +6 -0
  168. package/src/libs/trpc/client/helpers/desktopRemoteRPCFetch.ts +72 -0
  169. package/src/libs/trpc/client/index.ts +1 -0
  170. package/src/libs/trpc/client/lambda.ts +10 -1
  171. package/src/libs/trpc/client/tools.ts +6 -0
  172. package/src/server/globalConfig/index.ts +0 -3
  173. package/src/server/modules/ElectronIPCClient/index.ts +3 -1
  174. package/src/server/routers/desktop/index.ts +2 -0
  175. package/src/server/routers/desktop/mcp.ts +47 -0
  176. package/src/server/routers/lambda/user.ts +38 -23
  177. package/src/server/routers/tools/mcp.ts +0 -6
  178. package/src/services/electron/remoteServer.ts +4 -4
  179. package/src/services/mcp.ts +17 -7
  180. package/src/services/upload.ts +9 -0
  181. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +11 -2
  182. package/src/store/chat/slices/builtinTool/actions/localFile.ts +110 -53
  183. package/src/store/electron/actions/sync.ts +20 -19
  184. package/src/store/electron/initialState.ts +3 -3
  185. package/src/store/electron/selectors/sync.ts +6 -3
  186. package/src/store/electron/store.ts +2 -0
  187. package/src/store/file/slices/upload/action.ts +11 -3
  188. package/src/store/tool/selectors/tool.ts +10 -1
  189. package/src/utils/fetch/headers.ts +27 -0
  190. package/src/utils/fetch/index.ts +2 -0
  191. package/src/utils/fetch/request.ts +28 -0
  192. package/packages/electron-client-ipc/src/types/remoteServer.ts +0 -8
  193. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/Connection/Waiting.tsx +0 -0
  194. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/UpdateNotification.tsx +0 -0
  195. /package/src/{app/[variants]/(main)/_layout/Desktop → features}/ElectronTitlebar/index.tsx +0 -0
@@ -0,0 +1,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,3 @@
1
+ export const isDev = process.env.NODE_ENV === 'development';
2
+
3
+ export const OFFICIAL_CLOUD_SERVER = process.env.OFFICIAL_CLOUD_SERVER || 'https://lobechat.com';
@@ -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
+ }