@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,335 @@
1
+ import { DataSyncConfig } from '@lobechat/electron-client-ipc';
2
+ import { safeStorage } from 'electron';
3
+ import querystring from 'node:querystring';
4
+ import { URL } from 'node:url';
5
+
6
+ import { OFFICIAL_CLOUD_SERVER } from '@/const/env';
7
+ import { createLogger } from '@/utils/logger';
8
+
9
+ import { ControllerModule, ipcClientEvent } from './index';
10
+
11
+ // Create logger
12
+ const logger = createLogger('controllers:RemoteServerConfigCtr');
13
+
14
+ /**
15
+ * Remote Server Configuration Controller
16
+ * Used to manage custom remote LobeChat server configuration
17
+ */
18
+ export default class RemoteServerConfigCtr extends ControllerModule {
19
+ /**
20
+ * Key used to store encrypted tokens in electron-store.
21
+ */
22
+ private readonly encryptedTokensKey = 'encryptedTokens';
23
+
24
+ /**
25
+ * Get remote server configuration
26
+ */
27
+ @ipcClientEvent('getRemoteServerConfig')
28
+ async getRemoteServerConfig() {
29
+ logger.debug('Getting remote server configuration');
30
+ const { storeManager } = this.app;
31
+
32
+ const config: DataSyncConfig = storeManager.get('dataSyncConfig');
33
+
34
+ logger.debug(
35
+ `Remote server config: active=${config.active}, storageMode=${config.storageMode}, url=${config.remoteServerUrl}`,
36
+ );
37
+
38
+ return config;
39
+ }
40
+
41
+ /**
42
+ * Set remote server configuration
43
+ */
44
+ @ipcClientEvent('setRemoteServerConfig')
45
+ async setRemoteServerConfig(config: Partial<DataSyncConfig>) {
46
+ logger.info(
47
+ `Setting remote server storageMode: active=${config.active}, storageMode=${config.storageMode}, url=${config.remoteServerUrl}`,
48
+ );
49
+ const { storeManager } = this.app;
50
+ const prev: DataSyncConfig = storeManager.get('dataSyncConfig');
51
+
52
+ // Save configuration
53
+ storeManager.set('dataSyncConfig', { ...prev, ...config });
54
+
55
+ return true;
56
+ }
57
+
58
+ /**
59
+ * Clear remote server configuration
60
+ */
61
+ @ipcClientEvent('clearRemoteServerConfig')
62
+ async clearRemoteServerConfig() {
63
+ logger.info('Clearing remote server configuration');
64
+ const { storeManager } = this.app;
65
+
66
+ // Clear instance configuration
67
+ storeManager.set('dataSyncConfig', { storageMode: 'local' });
68
+
69
+ // Clear tokens (if any)
70
+ await this.clearTokens();
71
+
72
+ return true;
73
+ }
74
+
75
+ /**
76
+ * Encrypted tokens
77
+ * Stored in memory for quick access, loaded from persistent storage on init.
78
+ */
79
+ private encryptedAccessToken?: string;
80
+ private encryptedRefreshToken?: string;
81
+
82
+ /**
83
+ * Promise representing the ongoing token refresh operation.
84
+ * Used to prevent concurrent refreshes and allow callers to wait.
85
+ */
86
+ private refreshPromise: Promise<{ error?: string; success: boolean }> | null = null;
87
+
88
+ /**
89
+ * Encrypt and store tokens
90
+ * @param accessToken Access token
91
+ * @param refreshToken Refresh token
92
+ */
93
+ async saveTokens(accessToken: string, refreshToken: string) {
94
+ logger.info('Saving encrypted tokens');
95
+
96
+ // If platform doesn't support secure storage, store raw tokens
97
+ if (!safeStorage.isEncryptionAvailable()) {
98
+ logger.warn('Safe storage not available, storing tokens unencrypted');
99
+ this.encryptedAccessToken = accessToken;
100
+ this.encryptedRefreshToken = refreshToken;
101
+ // Persist unencrypted tokens (consider security implications)
102
+ this.app.storeManager.set(this.encryptedTokensKey, {
103
+ accessToken: this.encryptedAccessToken,
104
+ refreshToken: this.encryptedRefreshToken,
105
+ });
106
+ return;
107
+ }
108
+
109
+ // Encrypt tokens
110
+ logger.debug('Encrypting tokens using safe storage');
111
+ this.encryptedAccessToken = Buffer.from(safeStorage.encryptString(accessToken)).toString(
112
+ 'base64',
113
+ );
114
+
115
+ this.encryptedRefreshToken = Buffer.from(safeStorage.encryptString(refreshToken)).toString(
116
+ 'base64',
117
+ );
118
+
119
+ // Persist encrypted tokens
120
+ logger.debug(`Persisting encrypted tokens to store key: ${this.encryptedTokensKey}`);
121
+ this.app.storeManager.set(this.encryptedTokensKey, {
122
+ accessToken: this.encryptedAccessToken,
123
+ refreshToken: this.encryptedRefreshToken,
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Get decrypted access token
129
+ */
130
+ async getAccessToken(): Promise<string | null> {
131
+ // Try loading from memory first
132
+ if (!this.encryptedAccessToken) {
133
+ logger.debug('Access token not in memory, trying to load from store...');
134
+ this.loadTokensFromStore(); // Attempt to load from persistent storage
135
+ }
136
+
137
+ if (!this.encryptedAccessToken) {
138
+ logger.debug('No access token found in memory or store.');
139
+ return null;
140
+ }
141
+
142
+ // If platform doesn't support secure storage, return stored token
143
+ if (!safeStorage.isEncryptionAvailable()) {
144
+ logger.debug(
145
+ 'Safe storage not available, returning potentially unencrypted token from memory/store',
146
+ );
147
+ return this.encryptedAccessToken;
148
+ }
149
+
150
+ try {
151
+ // Decrypt token
152
+ logger.debug('Decrypting access token');
153
+ const encryptedData = Buffer.from(this.encryptedAccessToken, 'base64');
154
+ return safeStorage.decryptString(encryptedData);
155
+ } catch (error) {
156
+ logger.error('Failed to decrypt access token:', error);
157
+ return null;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Get decrypted refresh token
163
+ */
164
+ async getRefreshToken(): Promise<string | null> {
165
+ // Try loading from memory first
166
+ if (!this.encryptedRefreshToken) {
167
+ logger.debug('Refresh token not in memory, trying to load from store...');
168
+ this.loadTokensFromStore(); // Attempt to load from persistent storage
169
+ }
170
+
171
+ if (!this.encryptedRefreshToken) {
172
+ logger.debug('No refresh token found in memory or store.');
173
+ return null;
174
+ }
175
+
176
+ // If platform doesn't support secure storage, return stored token
177
+ if (!safeStorage.isEncryptionAvailable()) {
178
+ logger.debug(
179
+ 'Safe storage not available, returning potentially unencrypted token from memory/store',
180
+ );
181
+ return this.encryptedRefreshToken;
182
+ }
183
+
184
+ try {
185
+ // Decrypt token
186
+ logger.debug('Decrypting refresh token');
187
+ const encryptedData = Buffer.from(this.encryptedRefreshToken, 'base64');
188
+ return safeStorage.decryptString(encryptedData);
189
+ } catch (error) {
190
+ logger.error('Failed to decrypt refresh token:', error);
191
+ return null;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Clear tokens
197
+ */
198
+ async clearTokens() {
199
+ logger.info('Clearing access and refresh tokens');
200
+ this.encryptedAccessToken = undefined;
201
+ this.encryptedRefreshToken = undefined;
202
+ // Also clear from persistent storage
203
+ logger.debug(`Deleting tokens from store key: ${this.encryptedTokensKey}`);
204
+ this.app.storeManager.delete(this.encryptedTokensKey);
205
+ }
206
+
207
+ /**
208
+ * 刷新访问令牌
209
+ * 使用存储的刷新令牌获取新的访问令牌
210
+ * Handles concurrent requests by returning the existing refresh promise if one is in progress.
211
+ */
212
+ @ipcClientEvent('refreshAccessToken')
213
+ async refreshAccessToken(): Promise<{ error?: string; success: boolean }> {
214
+ // If a refresh is already in progress, return the existing promise
215
+ if (this.refreshPromise) {
216
+ logger.debug('Token refresh already in progress, returning existing promise.');
217
+ return this.refreshPromise;
218
+ }
219
+
220
+ // Start a new refresh operation
221
+ logger.info('Initiating new token refresh operation.');
222
+ this.refreshPromise = this.performTokenRefresh();
223
+
224
+ // Return the promise so callers can wait
225
+ return this.refreshPromise;
226
+ }
227
+
228
+ /**
229
+ * Performs the actual token refresh logic.
230
+ * This method is called by refreshAccessToken and wrapped in a promise.
231
+ */
232
+ private async performTokenRefresh(): Promise<{ error?: string; success: boolean }> {
233
+ try {
234
+ // 获取配置信息
235
+ const config = await this.getRemoteServerConfig();
236
+
237
+ if (!config.remoteServerUrl || !config.active) {
238
+ logger.warn('Remote server not active or configured, skipping refresh.');
239
+ return { error: '远程服务器未激活或未配置', success: false };
240
+ }
241
+
242
+ // 获取刷新令牌
243
+ const refreshToken = await this.getRefreshToken();
244
+ if (!refreshToken) {
245
+ logger.error('No refresh token available for refresh operation.');
246
+ return { error: '没有可用的刷新令牌', success: false };
247
+ }
248
+
249
+ // 构造刷新请求
250
+ const remoteUrl = await this.getRemoteServerUrl(config);
251
+
252
+ const tokenUrl = new URL('/oidc/token', remoteUrl);
253
+
254
+ // 构造请求体
255
+ const body = querystring.stringify({
256
+ client_id: 'lobehub-desktop',
257
+ grant_type: 'refresh_token',
258
+ refresh_token: refreshToken,
259
+ });
260
+
261
+ logger.debug(`Sending token refresh request to ${tokenUrl.toString()}`);
262
+
263
+ // 发送请求
264
+ const response = await fetch(tokenUrl.toString(), {
265
+ body,
266
+ headers: {
267
+ 'Content-Type': 'application/x-www-form-urlencoded',
268
+ },
269
+ method: 'POST',
270
+ });
271
+
272
+ if (!response.ok) {
273
+ // 尝试解析错误响应
274
+ const errorData = await response.json().catch(() => ({}));
275
+ const errorMessage = `刷新令牌失败: ${response.status} ${response.statusText} ${
276
+ errorData.error_description || errorData.error || ''
277
+ }`.trim();
278
+ logger.error(errorMessage, errorData);
279
+ return { error: errorMessage, success: false };
280
+ }
281
+
282
+ // 解析响应
283
+ const data = await response.json();
284
+
285
+ // 检查响应中是否包含必要令牌
286
+ if (!data.access_token || !data.refresh_token) {
287
+ logger.error('Refresh response missing access_token or refresh_token', data);
288
+ return { error: '刷新响应中缺少令牌', success: false };
289
+ }
290
+
291
+ // 保存新令牌
292
+ logger.info('Token refresh successful, saving new tokens.');
293
+ await this.saveTokens(data.access_token, data.refresh_token);
294
+
295
+ return { success: true };
296
+ } catch (error) {
297
+ const errorMessage = error instanceof Error ? error.message : String(error);
298
+ logger.error('Exception during token refresh operation:', errorMessage, error);
299
+ return { error: `刷新令牌时发生异常: ${errorMessage}`, success: false };
300
+ } finally {
301
+ // Ensure the promise reference is cleared once the operation completes
302
+ logger.debug('Clearing the refresh promise reference.');
303
+ this.refreshPromise = null;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Load encrypted tokens from persistent storage (electron-store) into memory.
309
+ * This should be called during initialization or if memory tokens are missing.
310
+ */
311
+ private loadTokensFromStore() {
312
+ logger.debug(`Attempting to load tokens from store key: ${this.encryptedTokensKey}`);
313
+ const storedTokens = this.app.storeManager.get(this.encryptedTokensKey);
314
+
315
+ if (storedTokens && storedTokens.accessToken && storedTokens.refreshToken) {
316
+ logger.info('Successfully loaded tokens from store into memory.');
317
+ this.encryptedAccessToken = storedTokens.accessToken;
318
+ this.encryptedRefreshToken = storedTokens.refreshToken;
319
+ } else {
320
+ logger.debug('No valid tokens found in store.');
321
+ }
322
+ }
323
+
324
+ // Initialize by loading tokens from store when the controller is ready
325
+ // We might need a dedicated lifecycle method if constructor is too early for storeManager
326
+ afterAppReady() {
327
+ this.loadTokensFromStore();
328
+ }
329
+
330
+ async getRemoteServerUrl(config?: DataSyncConfig) {
331
+ const dataConfig = config ? config : await this.getRemoteServerConfig();
332
+
333
+ return dataConfig.storageMode === 'cloud' ? OFFICIAL_CLOUD_SERVER : dataConfig.remoteServerUrl;
334
+ }
335
+ }
@@ -0,0 +1,321 @@
1
+ import {
2
+ ProxyTRPCRequestParams,
3
+ ProxyTRPCRequestResult,
4
+ } from '@lobechat/electron-client-ipc/src/types/proxyTRPCRequest';
5
+ import { Buffer } from 'node:buffer';
6
+ import http, { IncomingMessage, OutgoingHttpHeaders } from 'node:http';
7
+ import https from 'node:https';
8
+ import { URL } from 'node:url';
9
+
10
+ import { createLogger } from '@/utils/logger';
11
+
12
+ import RemoteServerConfigCtr from './RemoteServerConfigCtr';
13
+ import { ControllerModule, ipcClientEvent } from './index';
14
+
15
+ // Create logger
16
+ const logger = createLogger('controllers:RemoteServerSyncCtr');
17
+
18
+ /**
19
+ * Remote Server Sync Controller
20
+ * For handling data synchronization with remote servers via IPC.
21
+ */
22
+ export default class RemoteServerSyncCtr extends ControllerModule {
23
+ /**
24
+ * Cached instance of RemoteServerConfigCtr
25
+ */
26
+ private _remoteServerConfigCtrInstance: RemoteServerConfigCtr | null = null;
27
+
28
+ /**
29
+ * Remote server configuration controller
30
+ */
31
+ private get remoteServerConfigCtr() {
32
+ if (!this._remoteServerConfigCtrInstance) {
33
+ this._remoteServerConfigCtrInstance = this.app.getController(RemoteServerConfigCtr);
34
+ }
35
+ return this._remoteServerConfigCtrInstance;
36
+ }
37
+
38
+ /**
39
+ * Controller initialization - No specific logic needed here now for request handling
40
+ */
41
+ afterAppReady() {
42
+ logger.info('RemoteServerSyncCtr initialized (IPC based)');
43
+ // No need to register protocol handler anymore
44
+ }
45
+
46
+ /**
47
+ * Helper function to perform the actual request forwarding to the remote server.
48
+ * Accepts arguments from IPC and returns response details.
49
+ */
50
+ private async forwardRequest(args: {
51
+ accessToken: string | null;
52
+ body?: string | ArrayBuffer;
53
+ headers: Record<string, string>;
54
+ method: string;
55
+ remoteServerUrl: string;
56
+ urlPath: string; // Pass the base URL
57
+ }): Promise<{
58
+ // Node headers type
59
+ body: Buffer;
60
+ headers: Record<string, string | string[] | undefined>;
61
+ status: number;
62
+ statusText: string; // Return body as Buffer
63
+ }> {
64
+ const {
65
+ urlPath,
66
+ method,
67
+ headers: originalHeaders,
68
+ body: requestBody,
69
+ accessToken,
70
+ remoteServerUrl,
71
+ } = args;
72
+
73
+ const logPrefix = `[ForwardRequest ${method} ${urlPath}]`; // Add prefix for easier correlation
74
+
75
+ if (!accessToken) {
76
+ logger.error(`${logPrefix} No access token provided`); // Enhanced log
77
+ return {
78
+ body: Buffer.from(''),
79
+ headers: {},
80
+ status: 401,
81
+ statusText: 'Authentication required, missing token',
82
+ };
83
+ }
84
+
85
+ // 1. Determine target URL and prepare request options
86
+ const targetUrl = new URL(urlPath, remoteServerUrl); // Combine base URL and path
87
+
88
+ logger.debug(`${logPrefix} Forwarding to ${targetUrl.toString()}`); // Enhanced log
89
+
90
+ // Prepare headers, cloning and adding Authorization
91
+ const requestHeaders: OutgoingHttpHeaders = { ...originalHeaders }; // Use OutgoingHttpHeaders
92
+ requestHeaders['Authorization'] = `Bearer ${accessToken}`;
93
+
94
+ // Let node handle Host, Content-Length etc. Remove potentially problematic headers
95
+ delete requestHeaders['host'];
96
+ delete requestHeaders['connection']; // Often causes issues
97
+ // delete requestHeaders['content-length']; // Let node handle it based on body
98
+
99
+ const requestOptions: https.RequestOptions | http.RequestOptions = {
100
+ // Use union type
101
+ headers: requestHeaders,
102
+ hostname: targetUrl.hostname,
103
+ method: method,
104
+ path: targetUrl.pathname + targetUrl.search,
105
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
106
+ protocol: targetUrl.protocol,
107
+ // agent: false, // Consider for keep-alive issues if they arise
108
+ };
109
+
110
+ const requester = targetUrl.protocol === 'https:' ? https : http;
111
+
112
+ // 2. Make the request and capture response
113
+ return new Promise((resolve) => {
114
+ const clientReq = requester.request(requestOptions, (clientRes: IncomingMessage) => {
115
+ const chunks: Buffer[] = [];
116
+ clientRes.on('data', (chunk) => {
117
+ chunks.push(chunk);
118
+ });
119
+
120
+ clientRes.on('end', () => {
121
+ const responseBody = Buffer.concat(chunks);
122
+ logger.debug(
123
+ `${logPrefix} Received response from ${targetUrl.toString()}: ${clientRes.statusCode}`,
124
+ ); // Enhanced log
125
+ resolve({
126
+ // These are IncomingHttpHeaders
127
+ body: responseBody,
128
+
129
+ headers: clientRes.headers,
130
+
131
+ status: clientRes.statusCode || 500,
132
+ statusText: clientRes.statusMessage || 'Unknown Status',
133
+ });
134
+ });
135
+
136
+ clientRes.on('error', (error) => {
137
+ // Error during response streaming
138
+ logger.error(
139
+ `${logPrefix} Error reading response stream from ${targetUrl.toString()}:`,
140
+ error,
141
+ ); // Enhanced log
142
+ // Rejecting might be better, but we need to resolve the outer promise for proxyTRPCRequest
143
+ resolve({
144
+ body: Buffer.from(`Error reading response stream: ${error.message}`),
145
+ headers: {},
146
+
147
+ status: 502,
148
+ // Bad Gateway
149
+ statusText: 'Error reading response stream',
150
+ });
151
+ });
152
+ });
153
+
154
+ clientReq.on('error', (error) => {
155
+ logger.error(`${logPrefix} Error forwarding request to ${targetUrl.toString()}:`, error); // Enhanced log
156
+ // Reject or resolve with error status for the outer promise
157
+ resolve({
158
+ body: Buffer.from(`Error forwarding request: ${error.message}`),
159
+ headers: {},
160
+
161
+ status: 502,
162
+ // Bad Gateway
163
+ statusText: 'Error forwarding request',
164
+ });
165
+ });
166
+
167
+ // 3. Send request body if present
168
+ if (requestBody) {
169
+ if (typeof requestBody === 'string') {
170
+ clientReq.write(requestBody, 'utf8'); // Specify encoding for strings
171
+ } else if (requestBody instanceof ArrayBuffer) {
172
+ clientReq.write(Buffer.from(requestBody)); // Convert ArrayBuffer to Buffer
173
+ } else {
174
+ // Should not happen based on type, but handle defensively
175
+ logger.warn(`${logPrefix} Unsupported request body type received:`, typeof requestBody); // Enhanced log
176
+ }
177
+ }
178
+
179
+ clientReq.end(); // Finalize the request
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Handles the 'proxy-trpc-request' IPC call from the renderer process.
185
+ * This method should be invoked by the ipcMain.handle setup in your main process entry point.
186
+ */
187
+ @ipcClientEvent('proxyTRPCRequest')
188
+ public async proxyTRPCRequest(args: ProxyTRPCRequestParams): Promise<ProxyTRPCRequestResult> {
189
+ logger.debug('Received proxyTRPCRequest IPC call:', {
190
+ headers: args.headers,
191
+ method: args.method,
192
+ urlPath: args.urlPath, // Log headers too for context
193
+ });
194
+
195
+ const logPrefix = `[ProxyTRPC ${args.method} ${args.urlPath}]`; // Prefix for this specific request
196
+
197
+ try {
198
+ const config = await this.remoteServerConfigCtr.getRemoteServerConfig();
199
+ if (!config.active || (config.storageMode === 'selfHost' && !config.remoteServerUrl)) {
200
+ logger.warn(
201
+ `${logPrefix} Remote server sync not active or configured. Rejecting proxy request.`,
202
+ ); // Enhanced log
203
+ return {
204
+ body: Buffer.from('Remote server sync not active or configured').buffer,
205
+ headers: {},
206
+
207
+ status: 503,
208
+ // Service Unavailable
209
+ statusText: 'Remote server sync not active or configured', // Return ArrayBuffer
210
+ };
211
+ }
212
+ const remoteServerUrl = await this.remoteServerConfigCtr.getRemoteServerUrl();
213
+
214
+ // Get initial token
215
+ let token = await this.remoteServerConfigCtr.getAccessToken();
216
+ logger.debug(
217
+ `${logPrefix} Initial token check: ${token ? 'Token exists' : 'No token found'}`,
218
+ ); // Added log
219
+
220
+ logger.info(`${logPrefix} Attempting to forward request...`); // Added log
221
+ let response = await this.forwardRequest({ ...args, accessToken: token, remoteServerUrl });
222
+
223
+ // Handle 401: Refresh token and retry if necessary
224
+ if (response.status === 401) {
225
+ logger.info(`${logPrefix} Received 401 from forwarded request. Attempting token refresh.`); // Enhanced log
226
+ const refreshed = await this.refreshTokenIfNeeded(logPrefix); // Pass prefix for context
227
+
228
+ if (refreshed) {
229
+ const newToken = await this.remoteServerConfigCtr.getAccessToken();
230
+ if (newToken) {
231
+ logger.info(`${logPrefix} Token refreshed successfully, retrying the request.`); // Enhanced log
232
+ response = await this.forwardRequest({
233
+ ...args,
234
+ accessToken: newToken,
235
+ remoteServerUrl,
236
+ });
237
+ } else {
238
+ logger.error(
239
+ `${logPrefix} Token refresh reported success, but failed to retrieve new token. Keeping original 401 response.`,
240
+ ); // Enhanced log
241
+ // Keep the original 401 response
242
+ }
243
+ } else {
244
+ logger.error(`${logPrefix} Token refresh failed. Keeping original 401 response.`); // Enhanced log
245
+ // Keep the original 401 response
246
+ }
247
+ }
248
+
249
+ // Convert headers and body to format defined in IPC event
250
+ const responseHeaders: Record<string, string> = {};
251
+ for (const [key, value] of Object.entries(response.headers)) {
252
+ if (value !== undefined) {
253
+ responseHeaders[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : value;
254
+ }
255
+ }
256
+
257
+ // Return the final response, ensuring body is serializable (string or ArrayBuffer)
258
+ const responseBody = response.body; // Buffer
259
+
260
+ // IMPORTANT: Check IPC limits. Large bodies might fail. Consider chunking if needed.
261
+ // Convert Buffer to ArrayBuffer for IPC
262
+ const finalBody = responseBody.buffer.slice(
263
+ responseBody.byteOffset,
264
+ responseBody.byteOffset + responseBody.byteLength,
265
+ );
266
+
267
+ logger.debug(`${logPrefix} Forwarding successful. Status: ${response.status}`); // Added log
268
+ return {
269
+ body: finalBody as ArrayBuffer,
270
+ headers: responseHeaders,
271
+ status: response.status,
272
+ statusText: response.statusText, // Return ArrayBuffer
273
+ };
274
+ } catch (error) {
275
+ logger.error(`${logPrefix} Unhandled error processing proxyTRPCRequest:`, error); // Enhanced log
276
+ // Ensure a serializable error response is returned
277
+ return {
278
+ body: Buffer.from(
279
+ `Internal Server Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
280
+ ).buffer,
281
+ headers: {},
282
+ status: 500,
283
+ statusText: 'Internal Server Error during proxy', // Return ArrayBuffer
284
+ };
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Attempts to refresh the access token by calling the RemoteServerConfigCtr.
290
+ * @returns Whether token refresh was successful
291
+ */
292
+ private async refreshTokenIfNeeded(callerLogPrefix: string = '[RefreshToken]'): Promise<boolean> {
293
+ // Added prefix parameter
294
+ const logPrefix = `${callerLogPrefix} [RefreshTrigger]`; // Updated prefix
295
+ logger.debug(`${logPrefix} Entered refreshTokenIfNeeded.`);
296
+
297
+ try {
298
+ logger.info(`${logPrefix} Triggering refreshAccessToken in RemoteServerConfigCtr.`);
299
+ const result = await this.remoteServerConfigCtr.refreshAccessToken();
300
+
301
+ if (result.success) {
302
+ logger.info(`${logPrefix} refreshAccessToken call completed successfully.`);
303
+ return true;
304
+ } else {
305
+ logger.error(`${logPrefix} refreshAccessToken call failed: ${result.error}`);
306
+ return false;
307
+ }
308
+ } catch (error) {
309
+ logger.error(`${logPrefix} Exception occurred while calling refreshAccessToken:`, error);
310
+ return false;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Clean up resources - No protocol handler to unregister anymore
316
+ */
317
+ destroy() {
318
+ logger.info('Destroying RemoteServerSyncCtr');
319
+ // Nothing specific to clean up here regarding request handling now
320
+ }
321
+ }
@@ -0,0 +1,19 @@
1
+ import { ControllerModule, ipcClientEvent } from '.';
2
+
3
+ export default class ShortcutController extends ControllerModule {
4
+ /**
5
+ * 获取所有快捷键配置
6
+ */
7
+ @ipcClientEvent('getShortcutsConfig')
8
+ getShortcutsConfig() {
9
+ return this.app.shortcutManager.getShortcutsConfig();
10
+ }
11
+
12
+ /**
13
+ * 更新单个快捷键配置
14
+ */
15
+ @ipcClientEvent('updateShortcutConfig')
16
+ updateShortcutConfig(id: string, accelerator: string): boolean {
17
+ return this.app.shortcutManager.updateShortcutConfig(id, accelerator);
18
+ }
19
+ }