@lobehub/chat 1.88.8 → 1.88.9

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.
@@ -0,0 +1,154 @@
1
+ ---
2
+ description:
3
+ globs:
4
+ alwaysApply: false
5
+ ---
6
+ **桌面端新功能实现指南**
7
+
8
+ ## 桌面端应用架构概述
9
+
10
+ LobeChat 桌面端基于 Electron 框架构建,采用主进程-渲染进程架构:
11
+
12
+ 1. **主进程 (Main Process)**:
13
+ - 位置:`apps/desktop/src/main`
14
+ - 职责:控制应用生命周期、系统API交互、窗口管理、后台服务
15
+
16
+ 2. **渲染进程 (Renderer Process)**:
17
+ - 复用 Web 端代码,位于 `src` 目录
18
+ - 通过 IPC 与主进程通信
19
+
20
+ 3. **预加载脚本 (Preload)**:
21
+ - 位置:`apps/desktop/src/preload`
22
+ - 职责:安全地暴露主进程功能给渲染进程
23
+
24
+ ## 添加新桌面端功能流程
25
+
26
+ ### 1. 确定功能需求与设计
27
+
28
+ 首先确定新功能的需求和设计,包括:
29
+ - 功能描述和用例
30
+ - 是否需要系统级API(如文件系统、网络等)
31
+ - UI/UX设计(如必要)
32
+ - 与现有功能的交互方式
33
+
34
+ ### 2. 在主进程中实现核心功能
35
+
36
+ 1. **创建控制器 (Controller)**
37
+ - 位置:`apps/desktop/src/main/controllers/`
38
+ - 示例:创建 `NewFeatureCtr.ts`
39
+ - 规范:按 `_template.ts` 模板格式实现
40
+ - 注册:在 `apps/desktop/src/main/controllers/index.ts` 导出
41
+
42
+ 2. **定义 IPC 事件处理器**
43
+ - 使用 `@ipcClientEvent('eventName')` 装饰器注册事件处理函数
44
+ - 处理函数应接收前端传递的参数并返回结果
45
+ - 处理可能的错误情况
46
+
47
+ 3. **实现业务逻辑**
48
+ - 可能需要调用 Electron API 或 Node.js 原生模块
49
+ - 对于复杂功能,可以创建专门的服务类 (`services/`)
50
+
51
+ ### 3. 定义 IPC 通信类型
52
+
53
+ 1. **在共享类型定义中添加新类型**
54
+ - 位置:`packages/electron-client-ipc/src/types.ts`
55
+ - 添加参数类型接口(如 `NewFeatureParams`)
56
+ - 添加返回结果类型接口(如 `NewFeatureResult`)
57
+
58
+ ### 4. 在渲染进程实现前端功能
59
+
60
+ 1. **创建服务层**
61
+ - 位置:`src/services/electron/`
62
+ - 添加服务方法调用 IPC
63
+ - 使用 `dispatch` 或 `invoke` 函数
64
+
65
+ ```typescript
66
+ // src/services/electron/newFeatureService.ts
67
+ import { dispatch } from '@lobechat/electron-client-ipc';
68
+ import { NewFeatureParams } from 'types';
69
+
70
+ export const newFeatureService = async (params: NewFeatureParams) => {
71
+ return dispatch('newFeatureEventName', params);
72
+ };
73
+ ```
74
+
75
+ 2. **实现 Store Action**
76
+ - 位置:`src/store/`
77
+ - 添加状态更新逻辑和错误处理
78
+
79
+ 3. **添加 UI 组件**
80
+ - 根据需要在适当位置添加UI组件
81
+ - 通过 Store 或 Service 层调用功能
82
+
83
+ ### 5. 如果是新增内置工具,遵循工具实现流程
84
+
85
+ 参考 [desktop-local-tools-implement.mdc](mdc:desktop-local-tools-implement.mdc) 了解更多关于添加内置工具的详细步骤。
86
+
87
+ ### 6. 添加测试
88
+
89
+ 1. **单元测试**
90
+ - 位置:`apps/desktop/src/main/controllers/__tests__/`
91
+ - 测试主进程组件功能
92
+
93
+ 2. **集成测试**
94
+ - 测试 IPC 通信和功能完整流程
95
+
96
+ ## 最佳实践
97
+
98
+ 1. **安全性考虑**
99
+ - 谨慎处理用户数据和文件系统访问
100
+ - 适当验证和清理输入数据
101
+ - 限制暴露给渲染进程的API范围
102
+
103
+ 2. **性能优化**
104
+ - 对于耗时操作,考虑使用异步方法
105
+ - 大型数据传输考虑分批处理
106
+
107
+ 3. **用户体验**
108
+ - 为长时间操作添加进度指示
109
+ - 提供适当的错误反馈
110
+ - 考虑操作的可撤销性
111
+
112
+ 4. **代码组织**
113
+ - 遵循项目现有的命名和代码风格约定
114
+ - 为新功能添加适当的文档和注释
115
+ - 功能模块化,避免过度耦合
116
+
117
+ ## 示例:实现系统通知功能
118
+
119
+ ```typescript
120
+ // apps/desktop/src/main/controllers/NotificationCtr.ts
121
+ import { BrowserWindow, Notification } from 'electron';
122
+ import { ipcClientEvent } from 'electron-client-ipc';
123
+
124
+ interface ShowNotificationParams {
125
+ title: string;
126
+ body: string;
127
+ }
128
+
129
+ export class NotificationCtr {
130
+ @ipcClientEvent('showNotification')
131
+ async handleShowNotification({ title, body }: ShowNotificationParams) {
132
+ try {
133
+ if (!Notification.isSupported()) {
134
+ return { success: false, error: 'Notifications not supported' };
135
+ }
136
+
137
+ const notification = new Notification({
138
+ title,
139
+ body,
140
+ });
141
+
142
+ notification.show();
143
+
144
+ return { success: true };
145
+ } catch (error) {
146
+ console.error('Failed to show notification:', error);
147
+ return {
148
+ success: false,
149
+ error: error instanceof Error ? error.message : 'Unknown error'
150
+ };
151
+ }
152
+ }
153
+ }
154
+ ```
@@ -0,0 +1,197 @@
1
+ ---
2
+ description:
3
+ globs:
4
+ alwaysApply: false
5
+ ---
6
+ **桌面端菜单配置指南**
7
+
8
+ ## 菜单系统概述
9
+
10
+ LobeChat 桌面应用有三种主要的菜单类型:
11
+
12
+ 1. **应用菜单 (App Menu)**:显示在应用窗口顶部(macOS)或窗口标题栏(Windows/Linux)
13
+ 2. **上下文菜单 (Context Menu)**:右键点击时显示的菜单
14
+ 3. **托盘菜单 (Tray Menu)**:点击系统托盘图标显示的菜单
15
+
16
+ ## 菜单相关文件结构
17
+
18
+ ```
19
+ apps/desktop/src/main/
20
+ ├── menus/ # 菜单定义
21
+ │ ├── appMenu.ts # 应用菜单配置
22
+ │ ├── contextMenu.ts # 上下文菜单配置
23
+ │ └── factory.ts # 菜单工厂函数
24
+ ├── controllers/
25
+ │ ├── MenuCtr.ts # 菜单控制器
26
+ │ └── TrayMenuCtr.ts # 托盘菜单控制器
27
+ ```
28
+
29
+ ## 菜单配置流程
30
+
31
+ ### 1. 应用菜单配置
32
+
33
+ 应用菜单在 `apps/desktop/src/main/menus/appMenu.ts` 中定义:
34
+
35
+ 1. **导入依赖**
36
+ ```typescript
37
+ import { app, BrowserWindow, Menu, MenuItem, MenuItemConstructorOptions } from 'electron';
38
+ import { is } from 'electron-util';
39
+ ```
40
+
41
+ 2. **定义菜单项**
42
+ - 使用 `MenuItemConstructorOptions` 类型定义菜单结构
43
+ - 每个菜单项可以包含:label, accelerator (快捷键), role, submenu, click 等属性
44
+
45
+ 3. **创建菜单工厂函数**
46
+ ```typescript
47
+ export const createAppMenu = (win: BrowserWindow) => {
48
+ const template = [
49
+ // 定义菜单项...
50
+ ];
51
+
52
+ return Menu.buildFromTemplate(template);
53
+ };
54
+ ```
55
+
56
+ 4. **注册菜单**
57
+ - 在 `MenuCtr.ts` 控制器中使用 `Menu.setApplicationMenu(menu)` 设置应用菜单
58
+
59
+ ### 2. 上下文菜单配置
60
+
61
+ 上下文菜单通常在特定元素上右键点击时显示:
62
+
63
+ 1. **在主进程中定义菜单模板**
64
+ ```typescript
65
+ // apps/desktop/src/main/menus/contextMenu.ts
66
+ export const createContextMenu = () => {
67
+ const template = [
68
+ // 定义菜单项...
69
+ ];
70
+
71
+ return Menu.buildFromTemplate(template);
72
+ };
73
+ ```
74
+
75
+ 2. **在适当的事件处理器中显示菜单**
76
+ ```typescript
77
+ const menu = createContextMenu();
78
+ menu.popup();
79
+ ```
80
+
81
+ ### 3. 托盘菜单配置
82
+
83
+ 托盘菜单在 `TrayMenuCtr.ts` 中配置:
84
+
85
+ 1. **创建托盘图标**
86
+ ```typescript
87
+ this.tray = new Tray(trayIconPath);
88
+ ```
89
+
90
+ 2. **定义托盘菜单**
91
+ ```typescript
92
+ const contextMenu = Menu.buildFromTemplate([
93
+ { label: '显示主窗口', click: this.showMainWindow },
94
+ { type: 'separator' },
95
+ { label: '退出', click: () => app.quit() },
96
+ ]);
97
+ ```
98
+
99
+ 3. **设置托盘菜单**
100
+ ```typescript
101
+ this.tray.setContextMenu(contextMenu);
102
+ ```
103
+
104
+ ## 多语言支持
105
+
106
+ 为菜单添加多语言支持:
107
+
108
+ 1. **导入本地化工具**
109
+ ```typescript
110
+ import { i18n } from '../locales';
111
+ ```
112
+
113
+ 2. **使用翻译函数**
114
+ ```typescript
115
+ const template = [
116
+ {
117
+ label: i18n.t('menu.file'),
118
+ submenu: [
119
+ { label: i18n.t('menu.new'), click: createNew },
120
+ // ...
121
+ ]
122
+ },
123
+ // ...
124
+ ];
125
+ ```
126
+
127
+ 3. **在语言切换时更新菜单**
128
+ 在 `MenuCtr.ts` 中监听语言变化事件并重新创建菜单
129
+
130
+ ## 添加新菜单项流程
131
+
132
+ 1. **确定菜单位置**
133
+ - 决定添加到哪个菜单(应用菜单、上下文菜单或托盘菜单)
134
+ - 确定在菜单中的位置(主菜单项或子菜单项)
135
+
136
+ 2. **定义菜单项**
137
+ ```typescript
138
+ const newMenuItem: MenuItemConstructorOptions = {
139
+ label: '新功能',
140
+ accelerator: 'CmdOrCtrl+N',
141
+ click: (_, window) => {
142
+ // 处理点击事件
143
+ if (window) window.webContents.send('trigger-new-feature');
144
+ }
145
+ };
146
+ ```
147
+
148
+ 3. **添加到菜单模板**
149
+ 将新菜单项添加到相应的菜单模板中
150
+
151
+ 4. **对于与渲染进程交互的功能**
152
+ - 使用 `window.webContents.send()` 发送 IPC 消息到渲染进程
153
+ - 在渲染进程中监听该消息并处理
154
+
155
+ ## 菜单项启用/禁用控制
156
+
157
+ 动态控制菜单项状态:
158
+
159
+ 1. **保存对菜单项的引用**
160
+ ```typescript
161
+ this.menuItems = {};
162
+ const menu = Menu.buildFromTemplate(template);
163
+ this.menuItems.newFeature = menu.getMenuItemById('new-feature');
164
+ ```
165
+
166
+ 2. **根据条件更新状态**
167
+ ```typescript
168
+ updateMenuState(state) {
169
+ if (this.menuItems.newFeature) {
170
+ this.menuItems.newFeature.enabled = state.canUseNewFeature;
171
+ }
172
+ }
173
+ ```
174
+
175
+ ## 最佳实践
176
+
177
+ 1. **使用标准角色**
178
+ - 尽可能使用 Electron 预定义的角色(如 `role: 'copy'`)以获得本地化和一致的行为
179
+
180
+ 2. **平台特定菜单**
181
+ - 使用 `process.platform` 检查为不同平台提供不同菜单
182
+ ```typescript
183
+ if (process.platform === 'darwin') {
184
+ template.unshift({ role: 'appMenu' });
185
+ }
186
+ ```
187
+
188
+ 3. **快捷键冲突**
189
+ - 避免与系统快捷键冲突
190
+ - 使用 `CmdOrCtrl` 代替 `Ctrl` 以支持 macOS 和 Windows/Linux
191
+
192
+ 4. **保持菜单简洁**
193
+ - 避免过多嵌套的子菜单
194
+ - 将相关功能分组在一起
195
+
196
+ 5. **添加分隔符**
197
+ - 使用 `{ type: 'separator' }` 在逻辑上分隔不同组的菜单项
@@ -0,0 +1,296 @@
1
+ ---
2
+ description:
3
+ globs:
4
+ alwaysApply: false
5
+ ---
6
+ **桌面端窗口管理指南**
7
+
8
+ ## 窗口管理概述
9
+
10
+ LobeChat 桌面应用使用 Electron 的 `BrowserWindow` 管理应用窗口。主要的窗口管理功能包括:
11
+
12
+ 1. **窗口创建和配置**
13
+ 2. **窗口状态管理**(大小、位置、最大化等)
14
+ 3. **多窗口协调**
15
+ 4. **窗口事件处理**
16
+
17
+ ## 相关文件结构
18
+
19
+ ```
20
+ apps/desktop/src/main/
21
+ ├── appBrowsers.ts # 窗口管理的核心文件
22
+ ├── controllers/
23
+ │ └── BrowserWindowsCtr.ts # 窗口控制器
24
+ └── modules/
25
+ └── browserWindowManager.ts # 窗口管理模块
26
+ ```
27
+
28
+ ## 窗口管理流程
29
+
30
+ ### 1. 窗口创建
31
+
32
+ 在 `appBrowsers.ts` 或 `BrowserWindowsCtr.ts` 中定义窗口创建逻辑:
33
+
34
+ ```typescript
35
+ export const createMainWindow = () => {
36
+ const mainWindow = new BrowserWindow({
37
+ width: 1200,
38
+ height: 800,
39
+ minWidth: 600,
40
+ minHeight: 400,
41
+ webPreferences: {
42
+ preload: path.join(__dirname, '../preload/index.js'),
43
+ contextIsolation: true,
44
+ nodeIntegration: false,
45
+ },
46
+ // 其他窗口配置项...
47
+ });
48
+
49
+ // 加载应用内容
50
+ if (isDev) {
51
+ mainWindow.loadURL('http://localhost:3000');
52
+ mainWindow.webContents.openDevTools();
53
+ } else {
54
+ mainWindow.loadFile(path.join(__dirname, '../../renderer/index.html'));
55
+ }
56
+
57
+ return mainWindow;
58
+ };
59
+ ```
60
+
61
+ ### 2. 窗口状态管理
62
+
63
+ 实现窗口状态持久化保存和恢复:
64
+
65
+ 1. **保存窗口状态**
66
+ ```typescript
67
+ const saveWindowState = (window: BrowserWindow) => {
68
+ if (!window.isMinimized() && !window.isMaximized()) {
69
+ const position = window.getPosition();
70
+ const size = window.getSize();
71
+
72
+ settings.set('windowState', {
73
+ x: position[0],
74
+ y: position[1],
75
+ width: size[0],
76
+ height: size[1],
77
+ });
78
+ }
79
+ };
80
+ ```
81
+
82
+ 2. **恢复窗口状态**
83
+ ```typescript
84
+ const restoreWindowState = (window: BrowserWindow) => {
85
+ const savedState = settings.get('windowState');
86
+
87
+ if (savedState) {
88
+ window.setBounds({
89
+ x: savedState.x,
90
+ y: savedState.y,
91
+ width: savedState.width,
92
+ height: savedState.height,
93
+ });
94
+ }
95
+ };
96
+ ```
97
+
98
+ 3. **监听窗口事件**
99
+ ```typescript
100
+ window.on('close', () => saveWindowState(window));
101
+ window.on('moved', () => saveWindowState(window));
102
+ window.on('resized', () => saveWindowState(window));
103
+ ```
104
+
105
+ ### 3. 实现多窗口管理
106
+
107
+ 对于需要多窗口支持的功能:
108
+
109
+ 1. **跟踪窗口**
110
+ ```typescript
111
+ export class WindowManager {
112
+ private windows: Map<string, BrowserWindow> = new Map();
113
+
114
+ createWindow(id: string, options: BrowserWindowConstructorOptions) {
115
+ const window = new BrowserWindow(options);
116
+ this.windows.set(id, window);
117
+
118
+ window.on('closed', () => {
119
+ this.windows.delete(id);
120
+ });
121
+
122
+ return window;
123
+ }
124
+
125
+ getWindow(id: string) {
126
+ return this.windows.get(id);
127
+ }
128
+
129
+ getAllWindows() {
130
+ return Array.from(this.windows.values());
131
+ }
132
+ }
133
+ ```
134
+
135
+ 2. **窗口间通信**
136
+ ```typescript
137
+ // 从一个窗口向另一个窗口发送消息
138
+ sendMessageToWindow(targetWindowId, channel, data) {
139
+ const targetWindow = this.getWindow(targetWindowId);
140
+ if (targetWindow) {
141
+ targetWindow.webContents.send(channel, data);
142
+ }
143
+ }
144
+ ```
145
+
146
+ ### 4. 窗口与渲染进程通信
147
+
148
+ 通过 IPC 实现窗口操作:
149
+
150
+ 1. **在主进程中注册 IPC 处理器**
151
+ ```typescript
152
+ // BrowserWindowsCtr.ts
153
+ @ipcClientEvent('minimizeWindow')
154
+ handleMinimizeWindow() {
155
+ const focusedWindow = BrowserWindow.getFocusedWindow();
156
+ if (focusedWindow) {
157
+ focusedWindow.minimize();
158
+ }
159
+ return { success: true };
160
+ }
161
+
162
+ @ipcClientEvent('maximizeWindow')
163
+ handleMaximizeWindow() {
164
+ const focusedWindow = BrowserWindow.getFocusedWindow();
165
+ if (focusedWindow) {
166
+ if (focusedWindow.isMaximized()) {
167
+ focusedWindow.restore();
168
+ } else {
169
+ focusedWindow.maximize();
170
+ }
171
+ }
172
+ return { success: true };
173
+ }
174
+
175
+ @ipcClientEvent('closeWindow')
176
+ handleCloseWindow() {
177
+ const focusedWindow = BrowserWindow.getFocusedWindow();
178
+ if (focusedWindow) {
179
+ focusedWindow.close();
180
+ }
181
+ return { success: true };
182
+ }
183
+ ```
184
+
185
+ 2. **在渲染进程中调用**
186
+ ```typescript
187
+ // src/services/electron/windowService.ts
188
+ import { dispatch } from '@lobechat/electron-client-ipc';
189
+
190
+ export const windowService = {
191
+ minimize: () => dispatch('minimizeWindow'),
192
+ maximize: () => dispatch('maximizeWindow'),
193
+ close: () => dispatch('closeWindow'),
194
+ };
195
+ ```
196
+
197
+ ### 5. 自定义窗口控制 (无边框窗口)
198
+
199
+ 对于自定义窗口标题栏:
200
+
201
+ 1. **创建无边框窗口**
202
+ ```typescript
203
+ const window = new BrowserWindow({
204
+ frame: false,
205
+ titleBarStyle: 'hidden',
206
+ // 其他选项...
207
+ });
208
+ ```
209
+
210
+ 2. **在渲染进程中实现拖拽区域**
211
+ ```css
212
+ /* CSS */
213
+ .titlebar {
214
+ -webkit-app-region: drag;
215
+ }
216
+
217
+ .titlebar-button {
218
+ -webkit-app-region: no-drag;
219
+ }
220
+ ```
221
+
222
+ ## 最佳实践
223
+
224
+ 1. **性能考虑**
225
+ - 避免创建过多窗口
226
+ - 使用 `show: false` 创建窗口,在内容加载完成后再显示,避免白屏
227
+
228
+ 2. **安全性**
229
+ - 始终设置适当的 `webPreferences` 确保安全
230
+ ```typescript
231
+ webPreferences: {
232
+ preload: path.join(__dirname, '../preload/index.js'),
233
+ contextIsolation: true,
234
+ nodeIntegration: false,
235
+ sandbox: true,
236
+ }
237
+ ```
238
+
239
+ 3. **跨平台兼容性**
240
+ - 考虑不同操作系统的窗口行为差异
241
+ - 使用 `process.platform` 为不同平台提供特定实现
242
+
243
+ 4. **崩溃恢复**
244
+ - 监听 `webContents.on('crashed')` 事件处理崩溃
245
+ - 提供崩溃恢复选项
246
+
247
+ 5. **内存管理**
248
+ - 确保窗口关闭时清理所有相关资源
249
+ - 使用 `window.on('closed')` 而不是 `window.on('close')` 进行最终清理
250
+
251
+ ## 示例:创建设置窗口
252
+
253
+ ```typescript
254
+ // apps/desktop/src/main/controllers/BrowserWindowsCtr.ts
255
+
256
+ @ipcClientEvent('openSettings')
257
+ handleOpenSettings() {
258
+ // 检查设置窗口是否已经存在
259
+ if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
260
+ // 如果窗口已存在,将其置于前台
261
+ this.settingsWindow.focus();
262
+ return { success: true };
263
+ }
264
+
265
+ // 创建新窗口
266
+ this.settingsWindow = new BrowserWindow({
267
+ width: 800,
268
+ height: 600,
269
+ title: 'Settings',
270
+ parent: this.mainWindow, // 设置父窗口,使其成为模态窗口
271
+ modal: true,
272
+ webPreferences: {
273
+ preload: path.join(__dirname, '../preload/index.js'),
274
+ contextIsolation: true,
275
+ nodeIntegration: false,
276
+ },
277
+ });
278
+
279
+ // 加载设置页面
280
+ if (isDev) {
281
+ this.settingsWindow.loadURL('http://localhost:3000/settings');
282
+ } else {
283
+ this.settingsWindow.loadFile(
284
+ path.join(__dirname, '../../renderer/index.html'),
285
+ { hash: 'settings' }
286
+ );
287
+ }
288
+
289
+ // 监听窗口关闭事件
290
+ this.settingsWindow.on('closed', () => {
291
+ this.settingsWindow = null;
292
+ });
293
+
294
+ return { success: true };
295
+ }
296
+ ```
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.88.9](https://github.com/lobehub/lobe-chat/compare/v1.88.8...v1.88.9)
6
+
7
+ <sup>Released on **2025-05-26**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Auto sync theme mode in desktop.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Auto sync theme mode in desktop, closes [#7970](https://github.com/lobehub/lobe-chat/issues/7970) ([a16fa02](https://github.com/lobehub/lobe-chat/commit/a16fa02))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ### [Version 1.88.8](https://github.com/lobehub/lobe-chat/compare/v1.88.7...v1.88.8)
6
31
 
7
32
  <sup>Released on **2025-05-26**</sup>
@@ -1,4 +1,4 @@
1
- import { ElectronAppState } from '@lobechat/electron-client-ipc';
1
+ import { ElectronAppState, ThemeMode } from '@lobechat/electron-client-ipc';
2
2
  import { app, shell, systemPreferences } from 'electron';
3
3
  import { macOS } from 'electron-is';
4
4
  import { readFileSync, writeFileSync } from 'node:fs';
@@ -68,6 +68,11 @@ export default class SystemController extends ControllerModule {
68
68
  return { success: true };
69
69
  }
70
70
 
71
+ @ipcClientEvent('updateThemeMode')
72
+ async updateThemeModeHandler(themeMode: ThemeMode) {
73
+ this.app.browserManager.broadcastToAllWindows('themeChanged', { themeMode });
74
+ }
75
+
71
76
  @ipcServerEvent('getDatabasePath')
72
77
  async getDatabasePath() {
73
78
  return join(this.app.appStoragePath, LOCAL_DATABASE_DIR);
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Auto sync theme mode in desktop."
6
+ ]
7
+ },
8
+ "date": "2025-05-26",
9
+ "version": "1.88.9"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.88.8",
3
+ "version": "1.88.9",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -2,7 +2,7 @@ import { LocalFilesDispatchEvents } from './localFile';
2
2
  import { MenuDispatchEvents } from './menu';
3
3
  import { RemoteServerBroadcastEvents, RemoteServerDispatchEvents } from './remoteServer';
4
4
  import { ShortcutDispatchEvents } from './shortcut';
5
- import { SystemDispatchEvents } from './system';
5
+ import { SystemBroadcastEvents, SystemDispatchEvents } from './system';
6
6
  import { TrayDispatchEvents } from './tray';
7
7
  import { AutoUpdateBroadcastEvents, AutoUpdateDispatchEvents } from './update';
8
8
  import { UploadFilesDispatchEvents } from './upload';
@@ -35,7 +35,8 @@ export type ClientEventReturnType<T extends ClientDispatchEventKey> = ReturnType
35
35
  // eslint-disable-next-line @typescript-eslint/no-empty-interface
36
36
  export interface MainBroadcastEvents
37
37
  extends AutoUpdateBroadcastEvents,
38
- RemoteServerBroadcastEvents {}
38
+ RemoteServerBroadcastEvents,
39
+ SystemBroadcastEvents {}
39
40
 
40
41
  export type MainBroadcastEventKey = keyof MainBroadcastEvents;
41
42
 
@@ -1,4 +1,4 @@
1
- import { ElectronAppState } from '../types';
1
+ import { ElectronAppState, ThemeMode } from '../types';
2
2
 
3
3
  export interface SystemDispatchEvents {
4
4
  checkSystemAccessibility: () => boolean | undefined;
@@ -12,4 +12,9 @@ export interface SystemDispatchEvents {
12
12
  * @param locale 语言设置
13
13
  */
14
14
  updateLocale: (locale: string) => { success: boolean };
15
+ updateThemeMode: (themeMode: ThemeMode) => void;
16
+ }
17
+
18
+ export interface SystemBroadcastEvents {
19
+ themeChanged: (data: { themeMode: ThemeMode }) => void;
15
20
  }
@@ -22,3 +22,5 @@ export interface UserPathData {
22
22
  userData: string;
23
23
  videos?: string; // User's home directory
24
24
  }
25
+
26
+ export type ThemeMode = 'auto' | 'dark' | 'light';
@@ -0,0 +1,21 @@
1
+ import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
2
+ import { useTheme } from 'antd-style';
3
+ import { rgba } from 'polished';
4
+ import { useEffect } from 'react';
5
+
6
+ import { useGlobalStore } from '@/store/global';
7
+
8
+ export const useWatchThemeUpdate = () => {
9
+ const switchThemeMode = useGlobalStore((s) => s.switchThemeMode);
10
+
11
+ const token = useTheme();
12
+
13
+ useWatchBroadcast('themeChanged', ({ themeMode }) => {
14
+ switchThemeMode(themeMode, { skipBroadcast: true });
15
+ });
16
+
17
+ useEffect(() => {
18
+ document.documentElement.style.background = 'none';
19
+ document.body.style.background = rgba(token.colorBgLayout, 0.66);
20
+ }, [token]);
21
+ };
@@ -11,6 +11,7 @@ import { UpdateModal } from './UpdateModal';
11
11
  import { UpdateNotification } from './UpdateNotification';
12
12
  import WinControl from './WinControl';
13
13
  import { TITLE_BAR_HEIGHT } from './const';
14
+ import { useWatchThemeUpdate } from './hooks/useWatchThemeUpdate';
14
15
 
15
16
  const isMac = isMacOS();
16
17
 
@@ -21,6 +22,7 @@ const TitleBar = memo(() => {
21
22
  ]);
22
23
 
23
24
  initElectronAppState();
25
+ useWatchThemeUpdate();
24
26
 
25
27
  const showWinControl = isAppStateInit && !isMac;
26
28
  return (
@@ -21,7 +21,7 @@ const n = setNamespace('g');
21
21
 
22
22
  export interface GlobalGeneralAction {
23
23
  switchLocale: (locale: LocaleMode) => void;
24
- switchThemeMode: (themeMode: ThemeMode) => void;
24
+ switchThemeMode: (themeMode: ThemeMode, params?: { skipBroadcast?: boolean }) => void;
25
25
  updateSystemStatus: (status: Partial<SystemStatus>, action?: any) => void;
26
26
  useCheckLatestVersion: (enabledCheck?: boolean) => SWRResponse<string>;
27
27
  useInitSystemStatus: () => SWRResponse;
@@ -50,10 +50,21 @@ export const generalActionSlice: StateCreator<
50
50
  })();
51
51
  }
52
52
  },
53
- switchThemeMode: (themeMode) => {
53
+ switchThemeMode: (themeMode, { skipBroadcast } = {}) => {
54
54
  get().updateSystemStatus({ themeMode });
55
55
 
56
56
  setCookie(LOBE_THEME_APPEARANCE, themeMode === 'auto' ? undefined : themeMode);
57
+
58
+ if (isDesktop && !skipBroadcast) {
59
+ (async () => {
60
+ try {
61
+ const { dispatch } = await import('@lobechat/electron-client-ipc');
62
+ await dispatch('updateThemeMode', themeMode);
63
+ } catch (error) {
64
+ console.error('Failed to update theme in main process:', error);
65
+ }
66
+ })();
67
+ }
57
68
  },
58
69
  updateSystemStatus: (status, action) => {
59
70
  if (!get().isStatusInit) return;
@@ -1,7 +1,4 @@
1
1
  import { Theme, css } from 'antd-style';
2
- import { rgba } from 'polished';
3
-
4
- import { isDesktop } from '@/const/version';
5
2
 
6
3
  // fix ios input keyboard
7
4
  // overflow: hidden;
@@ -25,15 +22,10 @@ export default ({ token }: { prefixCls: string; token: Theme }) => css`
25
22
  }
26
23
  }
27
24
 
28
- html {
29
- background: ${isDesktop ? 'none' : token.colorBgLayout};
30
- }
31
-
32
25
  body {
33
26
  /* 提高合成层级,强制硬件加速,否则会有渲染黑边出现 */
34
27
  will-change: opacity;
35
28
  transform: translateZ(0);
36
- background: ${isDesktop ? rgba(token.colorBgLayout, 0.66) : token.colorBgLayout};
37
29
  }
38
30
 
39
31
  * {