@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,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
|
+
}
|