@lobehub/chat 1.103.1 → 1.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/apps/desktop/build/icon-beta.ico +0 -0
  3. package/apps/desktop/build/icon-dev.ico +0 -0
  4. package/apps/desktop/build/icon-nightly.ico +0 -0
  5. package/apps/desktop/build/icon.ico +0 -0
  6. package/apps/desktop/electron.vite.config.ts +4 -2
  7. package/apps/desktop/package.json +1 -0
  8. package/apps/desktop/src/main/appBrowsers.ts +2 -2
  9. package/apps/desktop/src/main/const/env.ts +5 -4
  10. package/apps/desktop/src/main/const/store.ts +1 -0
  11. package/apps/desktop/src/main/const/theme.ts +11 -0
  12. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +1 -1
  13. package/apps/desktop/src/main/controllers/NotificationCtr.ts +2 -4
  14. package/apps/desktop/src/main/controllers/ShortcutCtr.ts +9 -1
  15. package/apps/desktop/src/main/controllers/SystemCtr.ts +4 -0
  16. package/apps/desktop/src/main/controllers/TrayMenuCtr.ts +5 -9
  17. package/apps/desktop/src/main/controllers/__tests__/ShortcutCtr.test.ts +14 -11
  18. package/apps/desktop/src/main/controllers/index.ts +1 -1
  19. package/apps/desktop/src/main/core/App.ts +9 -10
  20. package/apps/desktop/src/main/core/{Browser.ts → browser/Browser.ts} +129 -88
  21. package/apps/desktop/src/main/core/{BrowserManager.ts → browser/BrowserManager.ts} +13 -3
  22. package/apps/desktop/src/main/core/{StaticFileServerManager.ts → infrastructure/StaticFileServerManager.ts} +13 -7
  23. package/apps/desktop/src/main/core/{StoreManager.ts → infrastructure/StoreManager.ts} +1 -1
  24. package/apps/desktop/src/main/core/{UpdaterManager.ts → infrastructure/UpdaterManager.ts} +1 -1
  25. package/apps/desktop/src/main/core/{MenuManager.ts → ui/MenuManager.ts} +2 -2
  26. package/apps/desktop/src/main/core/{ShortcutManager.ts → ui/ShortcutManager.ts} +78 -6
  27. package/apps/desktop/src/main/core/{Tray.ts → ui/Tray.ts} +61 -59
  28. package/apps/desktop/src/main/core/{TrayManager.ts → ui/TrayManager.ts} +5 -5
  29. package/apps/desktop/src/main/shortcuts/config.ts +4 -2
  30. package/apps/desktop/src/main/types/store.ts +1 -0
  31. package/changelog/v1.json +21 -0
  32. package/docs/development/basic/add-new-image-model.mdx +162 -0
  33. package/docs/development/basic/add-new-image-model.zh-CN.mdx +162 -0
  34. package/docs/usage/providers/fal.mdx +1 -1
  35. package/docs/usage/providers/fal.zh-CN.mdx +1 -1
  36. package/locales/ar/hotkey.json +10 -4
  37. package/locales/ar/setting.json +12 -1
  38. package/locales/bg-BG/hotkey.json +10 -4
  39. package/locales/bg-BG/setting.json +12 -1
  40. package/locales/de-DE/hotkey.json +10 -4
  41. package/locales/de-DE/setting.json +12 -1
  42. package/locales/en-US/hotkey.json +10 -4
  43. package/locales/en-US/setting.json +12 -1
  44. package/locales/es-ES/hotkey.json +10 -4
  45. package/locales/es-ES/setting.json +12 -1
  46. package/locales/fa-IR/hotkey.json +10 -4
  47. package/locales/fa-IR/setting.json +12 -1
  48. package/locales/fr-FR/hotkey.json +10 -4
  49. package/locales/fr-FR/setting.json +12 -1
  50. package/locales/it-IT/hotkey.json +10 -4
  51. package/locales/it-IT/setting.json +12 -1
  52. package/locales/ja-JP/hotkey.json +10 -4
  53. package/locales/ja-JP/setting.json +12 -1
  54. package/locales/ko-KR/hotkey.json +10 -4
  55. package/locales/ko-KR/setting.json +12 -1
  56. package/locales/nl-NL/hotkey.json +10 -4
  57. package/locales/nl-NL/setting.json +12 -1
  58. package/locales/pl-PL/hotkey.json +10 -4
  59. package/locales/pl-PL/setting.json +12 -1
  60. package/locales/pt-BR/hotkey.json +10 -4
  61. package/locales/pt-BR/setting.json +12 -1
  62. package/locales/ru-RU/hotkey.json +10 -4
  63. package/locales/ru-RU/setting.json +12 -1
  64. package/locales/tr-TR/hotkey.json +10 -4
  65. package/locales/tr-TR/setting.json +12 -1
  66. package/locales/vi-VN/hotkey.json +10 -4
  67. package/locales/vi-VN/setting.json +12 -1
  68. package/locales/zh-CN/hotkey.json +10 -4
  69. package/locales/zh-CN/setting.json +12 -1
  70. package/locales/zh-TW/hotkey.json +10 -4
  71. package/locales/zh-TW/setting.json +12 -1
  72. package/package.json +66 -66
  73. package/packages/electron-client-ipc/src/events/shortcut.ts +3 -1
  74. package/packages/electron-client-ipc/src/types/shortcut.ts +11 -0
  75. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/Portal.tsx +3 -1
  76. package/src/app/[variants]/(main)/chat/(workspace)/features/AgentSettings/index.tsx +4 -2
  77. package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +1 -1
  78. package/src/app/[variants]/(main)/image/features/GenerationFeed/BatchItem.tsx +39 -3
  79. package/src/app/[variants]/(main)/image/features/GenerationFeed/ReferenceImages.tsx +122 -0
  80. package/src/app/[variants]/(main)/settings/hotkey/features/Conversation.tsx +3 -11
  81. package/src/app/[variants]/(main)/settings/hotkey/features/Desktop.tsx +92 -0
  82. package/src/app/[variants]/(main)/settings/hotkey/features/Essential.tsx +3 -11
  83. package/src/app/[variants]/(main)/settings/hotkey/page.tsx +3 -0
  84. package/src/config/aiModels/fal.ts +31 -7
  85. package/src/config/aiModels/openai.ts +10 -1
  86. package/src/const/desktop.ts +9 -0
  87. package/src/const/hotkeys.ts +20 -16
  88. package/src/features/ElectronTitlebar/WinControl/index.tsx +85 -90
  89. package/src/features/ElectronTitlebar/hooks/useWatchThemeUpdate.ts +10 -5
  90. package/src/features/ImageTopicPanel/index.tsx +0 -1
  91. package/src/features/PluginDevModal/index.tsx +3 -1
  92. package/src/features/User/UserPanel/useMenu.tsx +2 -2
  93. package/src/features/User/__tests__/UserAvatar.test.tsx +5 -4
  94. package/src/libs/model-runtime/fal/index.ts +1 -1
  95. package/src/libs/model-runtime/types/image.ts +1 -1
  96. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +1 -1
  97. package/src/libs/model-runtime/utils/response.ts +2 -0
  98. package/src/libs/model-runtime/utils/streams/google-ai.test.ts +46 -0
  99. package/src/libs/model-runtime/utils/streams/google-ai.ts +4 -4
  100. package/src/libs/model-runtime/utils/streams/vertex-ai.ts +6 -8
  101. package/src/libs/standard-parameters/{meta-schema.test.ts → index.test.ts} +1 -1
  102. package/src/libs/standard-parameters/index.ts +152 -1
  103. package/src/locales/default/hotkey.ts +13 -5
  104. package/src/locales/default/setting.ts +11 -0
  105. package/src/server/ld.test.ts +4 -3
  106. package/src/server/routers/async/image.ts +1 -1
  107. package/src/services/__tests__/chat.test.ts +3 -4
  108. package/src/services/electron/settings.ts +19 -1
  109. package/src/store/chat/slices/message/selectors.test.ts +2 -3
  110. package/src/store/chat/slices/plugin/action.test.ts +2 -1
  111. package/src/store/electron/actions/settings.ts +42 -1
  112. package/src/store/electron/initialState.ts +9 -1
  113. package/src/store/electron/selectors/__tests__/desktopState.test.ts +6 -17
  114. package/src/store/electron/selectors/hotkey.ts +11 -0
  115. package/src/store/electron/selectors/index.ts +1 -0
  116. package/src/store/image/slices/generationConfig/action.test.ts +2 -2
  117. package/src/store/image/slices/generationConfig/action.ts +1 -1
  118. package/src/store/image/slices/generationConfig/hooks.test.ts +2 -2
  119. package/src/store/image/slices/generationConfig/hooks.ts +1 -4
  120. package/src/store/image/slices/generationConfig/initialState.ts +2 -2
  121. package/src/store/image/slices/generationConfig/selectors.test.ts +2 -2
  122. package/src/store/image/slices/generationConfig/selectors.ts +1 -1
  123. package/src/store/user/slices/auth/selectors.test.ts +3 -2
  124. package/src/types/generation/index.ts +1 -0
  125. package/src/types/hotkey.ts +18 -4
  126. package/docs/development/basic/add-new-ai-image-model.mdx +0 -36
  127. package/docs/development/basic/add-new-ai-image-model.zh-CN.mdx +0 -0
  128. package/src/config/paramsSchemas/fal/flux-kontext-dev.ts +0 -8
  129. package/src/config/paramsSchemas/fal/flux-pro-kontext.ts +0 -11
  130. package/src/config/paramsSchemas/fal/flux-schnell.ts +0 -9
  131. package/src/config/paramsSchemas/fal/imagen4.ts +0 -10
  132. package/src/config/paramsSchemas/openai/gpt-image-1.ts +0 -10
  133. package/src/libs/standard-parameters/meta-schema.ts +0 -147
  134. /package/apps/desktop/src/main/core/{I18nManager.ts → infrastructure/I18nManager.ts} +0 -0
  135. /package/apps/desktop/src/main/core/{IoCContainer.ts → infrastructure/IoCContainer.ts} +0 -0
@@ -6,13 +6,21 @@ import {
6
6
  nativeTheme,
7
7
  screen,
8
8
  } from 'electron';
9
- import os from 'node:os';
10
9
  import { join } from 'node:path';
11
10
 
11
+ import { buildDir, preloadDir, resourcesDir } from '@/const/dir';
12
+ import { isDev, isWindows } from '@/const/env';
13
+ import {
14
+ BACKGROUND_DARK,
15
+ BACKGROUND_LIGHT,
16
+ SYMBOL_COLOR_DARK,
17
+ SYMBOL_COLOR_LIGHT,
18
+ THEME_CHANGE_DELAY,
19
+ TITLE_BAR_HEIGHT,
20
+ } from '@/const/theme';
12
21
  import { createLogger } from '@/utils/logger';
13
22
 
14
- import { preloadDir, resourcesDir } from '../const/dir';
15
- import type { App } from './App';
23
+ import type { App } from '../App';
16
24
 
17
25
  // Create logger
18
26
  const logger = createLogger('core:Browser');
@@ -20,9 +28,6 @@ const logger = createLogger('core:Browser');
20
28
  export interface BrowserWindowOpts extends BrowserWindowConstructorOptions {
21
29
  devTools?: boolean;
22
30
  height?: number;
23
- /**
24
- * URL
25
- */
26
31
  identifier: string;
27
32
  keepAlive?: boolean;
28
33
  parentIdentifier?: string;
@@ -34,38 +39,18 @@ export interface BrowserWindowOpts extends BrowserWindowConstructorOptions {
34
39
 
35
40
  export default class Browser {
36
41
  private app: App;
37
-
38
- /**
39
- * Internal electron window
40
- */
41
42
  private _browserWindow?: BrowserWindow;
42
-
43
+ private themeListenerSetup = false;
43
44
  private stopInterceptHandler;
44
- /**
45
- * Identifier
46
- */
47
45
  identifier: string;
48
-
49
- /**
50
- * Options at creation
51
- */
52
46
  options: BrowserWindowOpts;
53
-
54
- /**
55
- * Key for storing window state in storeManager
56
- */
57
47
  private readonly windowStateKey: string;
58
48
 
59
- /**
60
- * Method to expose window externally
61
- */
62
49
  get browserWindow() {
63
50
  return this.retrieveOrInitialize();
64
51
  }
65
-
66
52
  get webContents() {
67
53
  if (this._browserWindow.isDestroyed()) return null;
68
-
69
54
  return this._browserWindow.webContents;
70
55
  }
71
56
 
@@ -86,6 +71,101 @@ export default class Browser {
86
71
  this.retrieveOrInitialize();
87
72
  }
88
73
 
74
+ /**
75
+ * Get platform-specific theme configuration for window creation
76
+ */
77
+ private getPlatformThemeConfig(isDarkMode?: boolean): Record<string, any> {
78
+ const darkMode = isDarkMode ?? nativeTheme.shouldUseDarkColors;
79
+
80
+ if (isWindows) {
81
+ return this.getWindowsThemeConfig(darkMode);
82
+ }
83
+
84
+ return {};
85
+ }
86
+
87
+ /**
88
+ * Get Windows-specific theme configuration
89
+ */
90
+ private getWindowsThemeConfig(isDarkMode: boolean) {
91
+ return {
92
+ backgroundColor: isDarkMode ? BACKGROUND_DARK : BACKGROUND_LIGHT,
93
+ icon: isDev ? join(buildDir, 'icon-dev.ico') : undefined,
94
+ titleBarOverlay: {
95
+ color: isDarkMode ? BACKGROUND_DARK : BACKGROUND_LIGHT,
96
+ height: TITLE_BAR_HEIGHT,
97
+ symbolColor: isDarkMode ? SYMBOL_COLOR_DARK : SYMBOL_COLOR_LIGHT,
98
+ },
99
+ titleBarStyle: 'hidden' as const,
100
+ };
101
+ }
102
+
103
+ private setupThemeListener(): void {
104
+ if (this.themeListenerSetup) return;
105
+
106
+ nativeTheme.on('updated', this.handleThemeChange);
107
+ this.themeListenerSetup = true;
108
+ }
109
+
110
+ private handleThemeChange = (): void => {
111
+ logger.debug(`[${this.identifier}] System theme changed, reapplying visual effects.`);
112
+ setTimeout(() => {
113
+ this.applyVisualEffects();
114
+ }, THEME_CHANGE_DELAY);
115
+ };
116
+
117
+ /**
118
+ * Handle application theme mode change (called from BrowserManager)
119
+ */
120
+ handleAppThemeChange = (): void => {
121
+ logger.debug(`[${this.identifier}] App theme mode changed, reapplying visual effects.`);
122
+ setTimeout(() => {
123
+ this.applyVisualEffects();
124
+ }, THEME_CHANGE_DELAY);
125
+ };
126
+
127
+ private applyVisualEffects(): void {
128
+ if (!this._browserWindow || this._browserWindow.isDestroyed()) return;
129
+
130
+ logger.debug(`[${this.identifier}] Applying visual effects for platform`);
131
+ const isDarkMode = this.isDarkMode;
132
+
133
+ try {
134
+ if (isWindows) {
135
+ this.applyWindowsVisualEffects(isDarkMode);
136
+ }
137
+
138
+ logger.debug(
139
+ `[${this.identifier}] Visual effects applied successfully (dark mode: ${isDarkMode})`,
140
+ );
141
+ } catch (error) {
142
+ logger.error(`[${this.identifier}] Failed to apply visual effects:`, error);
143
+ }
144
+ }
145
+
146
+ private applyWindowsVisualEffects(isDarkMode: boolean): void {
147
+ const config = this.getWindowsThemeConfig(isDarkMode);
148
+
149
+ this._browserWindow.setBackgroundColor(config.backgroundColor);
150
+ this._browserWindow.setTitleBarOverlay(config.titleBarOverlay);
151
+ }
152
+
153
+ private cleanupThemeListener(): void {
154
+ if (this.themeListenerSetup) {
155
+ // Note: nativeTheme listeners are global, consider using a centralized theme manager
156
+ nativeTheme.off('updated', this.handleThemeChange);
157
+ // for multiple windows to avoid duplicate listeners
158
+ this.themeListenerSetup = false;
159
+ }
160
+ }
161
+
162
+ private get isDarkMode() {
163
+ const themeMode = this.app.storeManager.get('themeMode');
164
+ if (themeMode === 'auto') return nativeTheme.shouldUseDarkColors;
165
+
166
+ return themeMode === 'dark';
167
+ }
168
+
89
169
  loadUrl = async (path: string) => {
90
170
  const initUrl = this.app.nextServerUrl + path;
91
171
 
@@ -203,6 +283,7 @@ export default class Browser {
203
283
  destroy() {
204
284
  logger.debug(`Destroying window instance: ${this.identifier}`);
205
285
  this.stopInterceptHandler?.();
286
+ this.cleanupThemeListener();
206
287
  this._browserWindow = undefined;
207
288
  }
208
289
 
@@ -228,45 +309,37 @@ export default class Browser {
228
309
  `[${this.identifier}] Saved window state (only size used): ${JSON.stringify(savedState)}`,
229
310
  );
230
311
 
231
- const { isWindows11, isWindows } = this.getWindowsVersion();
232
312
  const isDarkMode = nativeTheme.shouldUseDarkColors;
233
313
 
234
314
  const browserWindow = new BrowserWindow({
235
315
  ...res,
236
- ...(isWindows
237
- ? {
238
- titleBarStyle: 'hidden',
239
- }
240
- : {}),
241
- ...(isWindows11
242
- ? {
243
- backgroundMaterial: isDarkMode ? 'mica' : 'acrylic',
244
- vibrancy: 'under-window',
245
- visualEffectState: 'active',
246
- }
247
- : {}),
248
316
  autoHideMenuBar: true,
249
317
  backgroundColor: '#00000000',
318
+ darkTheme: isDarkMode,
250
319
  frame: false,
251
-
252
320
  height: savedState?.height || height,
253
- // Always create hidden first
254
321
  show: false,
255
322
  title,
256
-
323
+ vibrancy: 'sidebar',
324
+ visualEffectState: 'active',
257
325
  webPreferences: {
258
- // Context isolation environment
259
- // https://www.electronjs.org/docs/tutorial/context-isolation
326
+ backgroundThrottling: false,
260
327
  contextIsolation: true,
261
328
  preload: join(preloadDir, 'index.js'),
262
329
  },
263
330
  width: savedState?.width || width,
331
+ ...this.getPlatformThemeConfig(isDarkMode),
264
332
  });
265
333
 
266
334
  this._browserWindow = browserWindow;
267
335
  logger.debug(`[${this.identifier}] BrowserWindow instance created.`);
268
336
 
269
- if (isWindows11) this.applyVisualEffects();
337
+ // Initialize theme listener for this window to handle theme changes
338
+ this.setupThemeListener();
339
+ logger.debug(`[${this.identifier}] Theme listener setup and applying initial visual effects.`);
340
+
341
+ // Apply initial visual effects
342
+ this.applyVisualEffects();
270
343
 
271
344
  logger.debug(`[${this.identifier}] Setting up nextInterceptor.`);
272
345
  this.stopInterceptHandler = this.app.nextInterceptor({
@@ -320,8 +393,9 @@ export default class Browser {
320
393
  } catch (error) {
321
394
  logger.error(`[${this.identifier}] Failed to save window state on quit:`, error);
322
395
  }
323
- // Need to clean up intercept handler
396
+ // Need to clean up intercept handler and theme manager
324
397
  this.stopInterceptHandler?.();
398
+ this.cleanupThemeListener();
325
399
  return;
326
400
  }
327
401
 
@@ -355,8 +429,9 @@ export default class Browser {
355
429
  } catch (error) {
356
430
  logger.error(`[${this.identifier}] Failed to save window state on close:`, error);
357
431
  }
358
- // Need to clean up intercept handler
432
+ // Need to clean up intercept handler and theme manager
359
433
  this.stopInterceptHandler?.();
434
+ this.cleanupThemeListener();
360
435
  }
361
436
  });
362
437
 
@@ -387,16 +462,6 @@ export default class Browser {
387
462
  this._browserWindow.webContents.send(channel, data);
388
463
  };
389
464
 
390
- applyVisualEffects() {
391
- // Windows 11 can use this new API
392
- if (this._browserWindow) {
393
- logger.debug(`[${this.identifier}] Setting window background material for Windows 11`);
394
- const isDarkMode = nativeTheme.shouldUseDarkColors;
395
- this._browserWindow?.setBackgroundMaterial(isDarkMode ? 'mica' : 'acrylic');
396
- this._browserWindow?.setVibrancy('under-window');
397
- }
398
- }
399
-
400
465
  toggleVisible() {
401
466
  logger.debug(`Toggling visibility for window: ${this.identifier}`);
402
467
  if (this._browserWindow.isVisible() && this._browserWindow.isFocused()) {
@@ -407,35 +472,11 @@ export default class Browser {
407
472
  }
408
473
  }
409
474
 
410
- getWindowsVersion() {
411
- if (process.platform !== 'win32') {
412
- return {
413
- isWindows: false,
414
- isWindows10: false,
415
- isWindows11: false,
416
- version: null,
417
- };
418
- }
419
-
420
- // 获取操作系统版本(如 "10.0.22621")
421
- const release = os.release();
422
- const parts = release.split('.');
423
-
424
- // 主版本和次版本
425
- const majorVersion = parseInt(parts[0], 10);
426
- const minorVersion = parseInt(parts[1], 10);
427
-
428
- // 构建号是第三部分
429
- const buildNumber = parseInt(parts[2], 10);
430
-
431
- // Windows 11 的构建号从 22000 开始
432
- const isWindows11 = majorVersion === 10 && minorVersion === 0 && buildNumber >= 22_000;
433
-
434
- return {
435
- buildNumber,
436
- isWindows: true,
437
- isWindows11,
438
- version: release,
439
- };
475
+ /**
476
+ * Manually reapply visual effects (useful for fixing lost effects after window state changes)
477
+ */
478
+ reapplyVisualEffects(): void {
479
+ logger.debug(`[${this.identifier}] Manually reapplying visual effects via Browser.`);
480
+ this.applyVisualEffects();
440
481
  }
441
482
  }
@@ -3,15 +3,15 @@ import { WebContents } from 'electron';
3
3
 
4
4
  import { createLogger } from '@/utils/logger';
5
5
 
6
- import { AppBrowsersIdentifiers, appBrowsers } from '../appBrowsers';
7
- import type { App } from './App';
6
+ import { AppBrowsersIdentifiers, appBrowsers } from '../../appBrowsers';
7
+ import type { App } from '../App';
8
8
  import type { BrowserWindowOpts } from './Browser';
9
9
  import Browser from './Browser';
10
10
 
11
11
  // Create logger
12
12
  const logger = createLogger('core:BrowserManager');
13
13
 
14
- export default class BrowserManager {
14
+ export class BrowserManager {
15
15
  app: App;
16
16
 
17
17
  browsers: Map<AppBrowsersIdentifiers, Browser> = new Map();
@@ -194,4 +194,14 @@ export default class BrowserManager {
194
194
  getIdentifierByWebContents(webContents: WebContents): AppBrowsersIdentifiers | null {
195
195
  return this.webContentsMap.get(webContents) || null;
196
196
  }
197
+
198
+ /**
199
+ * Handle application theme mode changes and reapply visual effects to all windows
200
+ */
201
+ handleAppThemeChange(): void {
202
+ logger.debug('Handling app theme change for all browser windows');
203
+ this.browsers.forEach((browser) => {
204
+ browser.handleAppThemeChange();
205
+ });
206
+ }
197
207
  }
@@ -5,7 +5,7 @@ import { LOCAL_STORAGE_URL_PREFIX } from '@/const/dir';
5
5
  import FileService from '@/services/fileSrv';
6
6
  import { createLogger } from '@/utils/logger';
7
7
 
8
- import type { App } from './App';
8
+ import type { App } from '../App';
9
9
 
10
10
  const logger = createLogger('core:StaticFileServerManager');
11
11
 
@@ -54,9 +54,12 @@ export class StaticFileServerManager {
54
54
  try {
55
55
  // 使用 get-port-please 获取可用端口
56
56
  this.serverPort = await getPort({
57
- port: 33250, // 首选端口
58
- ports: [33251, 33252, 33253, 33254, 33255], // 备用端口
57
+ // 备用端口
59
58
  host: '127.0.0.1',
59
+
60
+ port: 33_250,
61
+ // 首选端口
62
+ ports: [33_251, 33_252, 33_253, 33_254, 33_255],
60
63
  });
61
64
 
62
65
  logger.debug(`Found available port: ${this.serverPort}`);
@@ -64,7 +67,7 @@ export class StaticFileServerManager {
64
67
  return new Promise((resolve, reject) => {
65
68
  const server = createServer(async (req, res) => {
66
69
  // 设置请求超时
67
- req.setTimeout(30000, () => {
70
+ req.setTimeout(30_000, () => {
68
71
  logger.warn('Request timeout, closing connection');
69
72
  if (!res.destroyed && !res.headersSent) {
70
73
  res.writeHead(408, { 'Content-Type': 'text/plain' });
@@ -155,10 +158,13 @@ export class StaticFileServerManager {
155
158
 
156
159
  // 设置响应头
157
160
  res.writeHead(200, {
158
- 'Content-Type': fileResult.mimeType,
159
- 'Cache-Control': 'public, max-age=31536000', // 缓存一年
160
- 'Access-Control-Allow-Origin': 'http://localhost:*', // 允许 localhost 的任意端口
161
+ // 缓存一年
162
+ 'Access-Control-Allow-Origin': 'http://localhost:*',
163
+
164
+ 'Cache-Control': 'public, max-age=31536000',
165
+ // 允许 localhost 的任意端口
161
166
  'Content-Length': Buffer.byteLength(fileResult.content),
167
+ 'Content-Type': fileResult.mimeType,
162
168
  });
163
169
 
164
170
  // 发送文件内容
@@ -5,7 +5,7 @@ import { ElectronMainStore, StoreKey } from '@/types/store';
5
5
  import { makeSureDirExist } from '@/utils/file-system';
6
6
  import { createLogger } from '@/utils/logger';
7
7
 
8
- import { App } from './App';
8
+ import { App } from '../App';
9
9
 
10
10
  // Create logger
11
11
  const logger = createLogger('core:StoreManager');
@@ -5,7 +5,7 @@ import { isDev } from '@/const/env';
5
5
  import { UPDATE_CHANNEL as channel, updaterConfig } from '@/modules/updater/configs';
6
6
  import { createLogger } from '@/utils/logger';
7
7
 
8
- import type { App as AppCore } from './App';
8
+ import type { App as AppCore } from '../App';
9
9
 
10
10
  // Create logger
11
11
  const logger = createLogger('core:UpdaterManager');
@@ -3,12 +3,12 @@ import { Menu } from 'electron';
3
3
  import { IMenuPlatform, MenuOptions, createMenuImpl } from '@/menus';
4
4
  import { createLogger } from '@/utils/logger';
5
5
 
6
- import type { App } from './App';
6
+ import type { App } from '../App';
7
7
 
8
8
  // Create logger
9
9
  const logger = createLogger('core:MenuManager');
10
10
 
11
- export default class MenuManager {
11
+ export class MenuManager {
12
12
  app: App;
13
13
  private platformImpl: IMenuPlatform;
14
14
 
@@ -3,11 +3,22 @@ import { globalShortcut } from 'electron';
3
3
  import { DEFAULT_SHORTCUTS_CONFIG } from '@/shortcuts';
4
4
  import { createLogger } from '@/utils/logger';
5
5
 
6
- import type { App } from './App';
6
+ import type { App } from '../App';
7
7
 
8
8
  // Create logger
9
9
  const logger = createLogger('core:ShortcutManager');
10
10
 
11
+ export interface ShortcutUpdateResult {
12
+ errorType?:
13
+ | 'INVALID_ID'
14
+ | 'INVALID_FORMAT'
15
+ | 'NO_MODIFIER'
16
+ | 'CONFLICT'
17
+ | 'SYSTEM_OCCUPIED'
18
+ | 'UNKNOWN';
19
+ success: boolean;
20
+ }
21
+
11
22
  export class ShortcutManager {
12
23
  private app: App;
13
24
  private shortcuts: Map<string, () => void> = new Map();
@@ -40,18 +51,73 @@ export class ShortcutManager {
40
51
  /**
41
52
  * Update a single shortcut configuration
42
53
  */
43
- updateShortcutConfig(id: string, accelerator: string): boolean {
54
+ updateShortcutConfig(id: string, accelerator: string): ShortcutUpdateResult {
44
55
  try {
45
56
  logger.debug(`Updating shortcut ${id} to ${accelerator}`);
46
- // Update configuration
47
- this.shortcutsConfig[id] = accelerator;
57
+
58
+ // 1. 检查 ID 是否有效
59
+ if (!DEFAULT_SHORTCUTS_CONFIG[id]) {
60
+ logger.error(`Invalid shortcut ID: ${id}`);
61
+ return { errorType: 'INVALID_ID', success: false };
62
+ }
63
+
64
+ // 2. 基本格式校验
65
+ if (!accelerator || typeof accelerator !== 'string' || accelerator.trim() === '') {
66
+ logger.error(`Invalid accelerator format: ${accelerator}`);
67
+ return { errorType: 'INVALID_FORMAT', success: false };
68
+ }
69
+
70
+ const cleanAccelerator = accelerator.trim().toLowerCase();
71
+
72
+ // 3. 检查是否包含 + 号(修饰键格式)
73
+ if (!cleanAccelerator.includes('+')) {
74
+ logger.error(
75
+ `Invalid accelerator format: ${cleanAccelerator}. Must contain modifier keys like 'CommandOrControl+E'`,
76
+ );
77
+ return { errorType: 'INVALID_FORMAT', success: false };
78
+ }
79
+
80
+ // 4. 检查是否有基本的修饰键
81
+ const hasModifier = ['CommandOrControl', 'Command', 'Ctrl', 'Alt', 'Shift'].some((modifier) =>
82
+ cleanAccelerator.includes(modifier.toLowerCase()),
83
+ );
84
+
85
+ if (!hasModifier) {
86
+ logger.error(`Invalid accelerator format: ${cleanAccelerator}. Must contain modifier keys`);
87
+ return { errorType: 'NO_MODIFIER', success: false };
88
+ }
89
+
90
+ // 5. 检查冲突
91
+ for (const [existingId, existingAccelerator] of Object.entries(this.shortcutsConfig)) {
92
+ if (
93
+ existingId !== id &&
94
+ typeof existingAccelerator === 'string' &&
95
+ existingAccelerator.toLowerCase() === cleanAccelerator
96
+ ) {
97
+ logger.error(`Shortcut conflict: ${cleanAccelerator} already used by ${existingId}`);
98
+ return { errorType: 'CONFLICT', success: false };
99
+ }
100
+ }
101
+
102
+ // 6. 尝试注册测试(检查是否被系统占用)
103
+ const testSuccess = globalShortcut.register(cleanAccelerator, () => {});
104
+ if (!testSuccess) {
105
+ logger.error(`Shortcut ${cleanAccelerator} is already registered by system or other app`);
106
+ return { errorType: 'SYSTEM_OCCUPIED', success: false };
107
+ } else {
108
+ // 测试成功,立即取消注册
109
+ globalShortcut.unregister(cleanAccelerator);
110
+ }
111
+
112
+ // 7. 更新配置
113
+ this.shortcutsConfig[id] = cleanAccelerator;
48
114
 
49
115
  this.saveShortcutsConfig();
50
116
  this.registerConfiguredShortcuts();
51
- return true;
117
+ return { success: true };
52
118
  } catch (error) {
53
119
  logger.error(`Error updating shortcut ${id}:`, error);
54
- return false;
120
+ return { errorType: 'UNKNOWN', success: false };
55
121
  }
56
122
  }
57
123
 
@@ -164,6 +230,12 @@ export class ShortcutManager {
164
230
  Object.entries(this.shortcutsConfig).forEach(([id, accelerator]) => {
165
231
  logger.debug(`Registering shortcut '${id}' with ${accelerator}`);
166
232
 
233
+ // 只注册在 DEFAULT_SHORTCUTS_CONFIG 中存在的快捷键
234
+ if (!DEFAULT_SHORTCUTS_CONFIG[id]) {
235
+ logger.debug(`Skipping shortcut '${id}' - not found in DEFAULT_SHORTCUTS_CONFIG`);
236
+ return;
237
+ }
238
+
167
239
  const method = this.shortcuts.get(id);
168
240
  if (accelerator && method) {
169
241
  this.registerShortcut(accelerator, method);