LobeHub Desktop 菜单体系实战指南:App 菜单、右键菜单与托盘菜单的配置原理
本篇基于 LobeHub 仓库中的 Desktop 菜单配置指南 menu-config.md,系统讲解 Electron 桌面应用中三类菜单(App 菜单、右键上下文菜单、系统托盘菜单)的设计模式与配置方法,并结合 apps/desktop/src/main/menus 与 apps/desktop/src/main/controllers 下的真实实现源码,说明每个菜单从模板定义、平台分发到 IPC 调用的完整链路。读完之后,你将能够理解 LobeHub Desktop 跨平台菜单的分发机制、托盘菜单的动态导航快照设计,以及如何为 Electron 应用配置可维护、可国际化(i18n)的菜单体系。
三类菜单的定位与选型
Desktop 端菜单体系分为三种类型,各自承担不同职责:
- App Menu(应用菜单):位于 macOS 的窗口顶部菜单栏,或 Windows/Linux 的标题栏,承载文件、编辑、视图等全局操作;
- Context Menu(右键上下文菜单):在用户右键点击内容区时弹出,根据点击对象类型(文本、链接、媒体等)动态决定可用操作;
- Tray Menu(托盘菜单):挂载在系统托盘图标上,提供不激活主窗口即可快速跳转会话、打开设置、退出应用的能力。
原文档建议的文件组织如下:
apps/desktop/src/main/
├── menus/
│ ├── appMenu.ts # App menu config
│ ├── contextMenu.ts # Context menu config
│ └── factory.ts # Menu factory functions
├── controllers/
│ ├── MenuCtr.ts # Menu controller
│ └── TrayMenuCtr.ts # Tray menu controller
对照仓库实际结构,当前实现采用的是「menus(模板与平台实现)+ controllers(IPC 入口)」的清晰分层:menus/ 目录下按平台拆分实现(impls/ 子目录),控制器负责把渲染进程的 IPC 请求路由到菜单管理器。下文的所有代码事实均以仓库当前源码为准。
平台分发机制:一份接口,三套实现
菜单配置的入口是 createMenuImpl 工厂函数,它根据 node:os 的 platform() 返回值实例化对应平台的菜单类:
// apps/desktop/src/main/menus/index.ts
export const createMenuImpl = (app: App): IMenuPlatform => {
const currentPlatform = platform();
switch (currentPlatform) {
case 'darwin': {
return new MacOSMenu(app);
}
case 'win32': {
return new WindowsMenu(app);
}
case 'linux': {
return new LinuxMenu(app);
}
default: {
console.warn(
`Unsupported platform for menu: ${currentPlatform}, using Windows implementation as fallback.`,
);
return new WindowsMenu(app);
}
}
};
这正是原文档「Best Practices」中 process.platform 平台差异处理的工程化落地——通过工厂函数把平台分支收敛到一处,未知平台降级为 Windows 实现并输出告警日志,而不是在业务代码里散落 if (process.platform === 'darwin') 判断。
三套平台实现共同遵守 IMenuPlatform 接口,对外暴露四个核心能力:
// apps/desktop/src/main/menus/types.ts
export interface IMenuPlatform {
/** 构建并设置应用菜单 */
buildAndSetAppMenu: (options?: MenuOptions) => Menu;
/** 构建上下文菜单 */
buildContextMenu: (type: string, data?: ContextMenuData) => Menu;
/** 构建托盘菜单 */
buildTrayMenu: (snapshot?: TrayNavigationSnapshot) => Menu;
/** 刷新菜单 */
refresh: (options?: MenuOptions) => void;
}
其中 MenuOptions.showDevItems 控制是否显示开发相关菜单项。以 macOS 实现 为例,getAppMenuTemplate 中的 showDev = isDev || options?.showDevItems 表明:开发环境默认可见,生产环境可通过 IPC 显式开启(对应下文 setDevMenuVisibility 方法)。
App 菜单配置:从模板到 setApplicationMenu
原文档给出了 App 菜单的标准配置范式——用 MenuItemConstructorOptions[] 描述菜单树,再通过 Menu.buildFromTemplate 与 Menu.setApplicationMenu 完成挂载:
// apps/desktop/src/main/menus/appMenu.ts
import { BrowserWindow, Menu, MenuItemConstructorOptions } from 'electron';
export const createAppMenu = (win: BrowserWindow) => {
const template: MenuItemConstructorOptions[] = [
{
label: 'File',
submenu: [
{
label: 'New',
accelerator: 'CmdOrCtrl+N',
click: () => {
/* ... */
},
},
{ type: 'separator' },
{ role: 'quit' },
],
},
// ...
];
return Menu.buildFromTemplate(template);
};
// Register in MenuCtr.ts
Menu.setApplicationMenu(menu);
在仓库的真实实现中,MacOSMenu.buildAndSetAppMenu 完整走通了这一范式,并且额外构建了一个 Dock 菜单:
// apps/desktop/src/main/menus/impls/macOS.ts
buildAndSetAppMenu(options?: MenuOptions): Menu {
const template = this.getAppMenuTemplate(options);
this.appMenu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(this.appMenu);
this.buildAndSetDockMenu();
return this.appMenu;
}
macOS 应用菜单的模板体现了几个值得注意的配置细节(节选自 macOS.ts):
const template: MenuItemConstructorOptions[] = [
{
label: appName,
submenu: [
{
click: async () => {
const mainWindow = this.app.browserManager.getMainWindow();
mainWindow.show();
mainWindow.broadcast('navigate', { path: '/settings/about' });
},
label: t('macOS.about', { appName }),
},
this.getUpdateMenuItem(t),
{ type: 'separator' },
{
accelerator: 'Command+,',
click: async () => {
const mainWindow = this.app.browserManager.getMainWindow();
mainWindow.show();
mainWindow.broadcast('createNewTab', { path: '/settings' });
},
label: t('macOS.preferences'),
},
{ type: 'separator' },
{ label: t('macOS.services'), role: 'services', submenu: [] },
{ type: 'separator' },
{ label: t('macOS.hide', { appName }), role: 'hide' },
{ label: t('macOS.hideOthers'), role: 'hideOthers' },
{ label: t('macOS.unhide'), role: 'unhide' },
{ type: 'separator' },
{ label: t('file.quit'), role: 'quit' },
],
},
// File 菜单等...
];
从中可以归纳出原文档四条最佳实践的实际用法:
- 优先使用标准 role:
role: 'services'、role: 'hide'、role: 'hideOthers'、role: 'unhide'、role: 'quit'等由 Electron 内建处理,行为与原生应用一致,且自动适配平台惯例; - 跨平台快捷键:文档建议用
CmdOrCtrl组合保证 macOS 与 Windows/Linux 均可用;macOS 实现中对系统级偏好设置使用了Command+,这一更精确的写法(Electron 支持Command作为Cmd的别名); - 分隔线分组:
{ type: 'separator' }将「关于/更新」「偏好设置」「服务」「窗口显隐」「退出」分成视觉独立的组; - 平台差异处理:
role: 'appMenu'一类的 macOS 专属项仅在 darwin 平台加入模板,在真实实现中则被createMenuImpl的工厂分发所取代。
另外,BaseMenuPlatform 抽象基类把各平台共享的复杂行为抽成了受保护方法:buildZoomMenuItem / buildZoomMenuItems(缩放菜单项,通过 ZoomService 应用缩放动作)、buildDevToolsMenuItem(独立 DevTools 窗口管理,支持打开/聚焦/关闭三态切换)、closeFocusedTabOrWindow(关闭当前标签页或窗口的通用逻辑)。这是「menus 按平台拆实现、公共能力下沉基类」这一组织方式的直接证据。
上下文菜单:按类型构建模板 + IPC 触发
原文档的上下文菜单范式非常简洁:
export const createContextMenu = () => {
const template = [
{ label: 'Copy', role: 'copy' },
{ label: 'Paste', role: 'paste' },
];
return Menu.buildFromTemplate(template);
};
// Show on right-click
const menu = createContextMenu();
menu.popup();
仓库实现在此基础上扩展了「菜单类型 + 上下文数据」的双参数设计。types.ts 定义了 ContextMenuData,它对齐 Electron 的 ContextMenuParams 语义:
export interface ContextMenuData {
/** 点击位置是否可编辑(input、textarea、contenteditable) */
isEditable?: boolean;
/** 右键点击链接时,该链接的 URL */
linkURL?: string;
/** 右键点击媒体元素时的媒体类型 */
mediaType?: 'none' | 'image' | 'audio' | 'video' | 'canvas' | 'file' | 'plugin';
/** 选中的文本 */
selectionText?: string;
/** 媒体元素的源 URL */
srcURL?: string;
/** 上下文菜单坐标 */
x?: number;
y?: number;
}
MacOSMenu.buildContextMenu 根据 type 参数路由到不同模板:
// apps/desktop/src/main/menus/impls/macOS.ts
buildContextMenu(type: string, data?: ContextMenuData): Menu {
let template: MenuItemConstructorOptions[];
switch (type) {
case 'chat': {
template = this.getChatContextMenuTemplate(data);
break;
}
case 'editor': {
template = this.getEditorContextMenuTemplate(data);
break;
}
default: {
template = this.getDefaultContextMenuTemplate(data);
}
}
return Menu.buildFromTemplate(template);
}
即:聊天区域右键、编辑器(可编辑)右键、其他位置右键各有一套模板,模板内部再依据 isEditable、mediaType、selectionText 等字段决定具体项的启用与否——这是「基于 role 的通用项 + 基于 data 的动态项」组合的典型写法。
触发链路位于 MenuCtr.ts。该控制器注册在 menu 分组下,把渲染进程的四个 IPC 请求转发给 app.menuManager:
// apps/desktop/src/main/controllers/MenuCtr.ts
export default class MenuController extends ControllerModule {
static override readonly groupName = 'menu';
@IpcMethod()
refreshAppMenu() {
return this.app.menuManager.refreshMenus();
}
@IpcMethod()
showContextMenu(params: { data?: any; type: string }) {
return this.app.menuManager.showContextMenu(params.type, params.data);
}
@IpcMethod()
setDevMenuVisibility(visible: boolean) {
return this.app.menuManager.rebuildAppMenu({ showDevItems: visible });
}
@IpcMethod()
popupContextMenu(params: PopupContextMenuParams): Promise<PopupContextMenuResult> {
const context = getIpcContext();
const window = context ? BrowserWindow.fromWebContents(context.sender) : null;
return this.app.menuManager.popupContextMenu(params, window);
}
@IpcMethod()
closePopupContextMenu() {
return this.app.menuManager.closePopupContextMenu();
}
}
其中 popupContextMenu 通过 getIpcContext() 拿到发送方 webContents,再经 BrowserWindow.fromWebContents 反查窗口后交给菜单管理器,保证弹层式上下文菜单与发起它的窗口严格关联。对应的控制器级测试见 MenuCtr.test.ts。
托盘菜单:动态导航快照 + 平台受限的图标控制
原文档给出的托盘菜单最小实现是:
// TrayMenuCtr.ts
this.tray = new Tray(trayIconPath);
const contextMenu = Menu.buildFromTemplate([
{ label: 'Show Window', click: this.showMainWindow },
{ type: 'separator' },
{ label: 'Quit', click: () => app.quit() },
]);
this.tray.setContextMenu(contextMenu);
LobeHub 的真实实现远比「显示窗口/退出」两项丰富。buildTrayMenuTemplate 接收渲染进程上报的 TrayNavigationSnapshot(包含 pinned、agents、recent 三个列表),把它转换成带分区标题的动态菜单:
// apps/desktop/src/main/menus/trayMenu.ts
const PINNED_LIMIT = 3;
const RECENT_AGENT_LIMIT = 3;
const RECENT_LIMIT = 5;
const openRoute = (app: App, path: string) => {
const mainWindow = app.browserManager.getMainWindow();
mainWindow.show();
mainWindow.broadcast('navigate', { escape: true, path });
};
const createSection = (
label: string,
items: MenuItemConstructorOptions[],
): MenuItemConstructorOptions[] =>
items.length > 0 ? [{ enabled: false, label }, ...items, { type: 'separator' }] : [];
几个设计细节值得拆解:
- 数量上限裁剪:置顶项最多 3 条(
PINNED_LIMIT)、最近 Agent 最多 3 条(RECENT_AGENT_LIMIT)、最近会话最多 5 条(RECENT_LIMIT),超出部分折叠为一个「更多」项(t('tray.moreAgents')/t('tray.more')),点击后通过broadcast('openAllAgents')/broadcast('openRecentlyViewed')让主窗口自行展开完整列表; - 分区标题用禁用项实现:
createSection用{ enabled: false, label }作为分区头,空分区整体返回空数组,避免空标题残留; - 导航即广播:
openRoute先show()主窗口,再broadcast('navigate', ...)把路由请求广播给渲染进程,菜单本身不直接操纵 DOM,职责边界清晰; - 快捷入口:菜单尾部固定提供屏幕截图迷你工具条(快捷键
Alt+Shift+Space)、快速聊天弹窗(openQuickChatPopup)、新建对话(createNewTopic)、打开应用、设置、退出(role: 'quit')等项。
模板组装完成后,由平台类负责构建,例如 macOS.ts:
buildTrayMenu(snapshot: TrayNavigationSnapshot = { agents: [], pinned: [], recent: [] }): Menu {
const template = buildTrayMenuTemplate(this.app, snapshot);
this.trayMenu = Menu.buildFromTemplate(template);
return this.trayMenu;
}
默认参数 { agents: [], pinned: [], recent: [] } 保证快照缺失时仍能构建出一份只含固定入口项的可用菜单,属于防御式默认值设计。模板逻辑的单测覆盖见 trayMenu.test.ts。
与托盘相关的控制器 TrayMenuCtr.ts 注册在 tray 分组下,职责分两类:
- 快照与显隐的持久化:
updateNavigationSnapshot把渲染进程上报的导航快照同步给trayManager以触发菜单重建;getAppTrayVisible/setAppTrayVisible通过storeManager持久化appTrayVisible偏好并即时应用; - Windows 平台专属能力:托盘图标更新(
updateTrayIcon)、tooltip 文案更新(updateTrayTooltip)、气泡通知(showNotification,displayBalloon)三者在源码中均显式判断process.platform === 'win32',非 Windows 平台直接返回{ success: false, error: 'Tray functionality is only supported on Windows platform' }。这是原文档「Handle platform differences withprocess.platform」这一最佳实践在 IPC 服务层的直接体现,测试见 TrayMenuCtr.test.ts。
i18n 支持:命名空间翻译函数注入模板
原文档的 i18n 范式是把翻译函数引入菜单模板:
import { i18n } from '../locales';
const template = [
{
label: i18n.t('menu.file'),
submenu: [{ label: i18n.t('menu.new'), click: createNew }],
},
];
真实实现中使用的是带命名空间的翻译函数,在每个平台菜单类内部创建:
// apps/desktop/src/main/menus/impls/macOS.ts(getAppMenuTemplate 内)
const t = this.app.i18n.ns('menu');
// 用法:t('file.quit')、t('macOS.preferences')、t('tray.open', { appName })
app.i18n.ns('menu') 从应用基础设施(I18nManager)取出绑定 menu 命名空间的翻译函数,支持变量插值(如 t('tray.open', { appName }) 把应用名注入文案)。由于托盘菜单、macOS 应用菜单等所有模板共用同一套 key(tray.pinned、tray.recentAgents、tray.quickChat、macOS.about 等),菜单文案与渲染进程共享同一份语言资源,切换语言后调用 refresh 重建菜单即可生效。
最佳实践清单
综合原文档建议与仓库源码验证,Electron 菜单开发的工程实践可归纳为:
| 实践 | 说明 | 仓库印证 |
|---|---|---|
| 使用标准 role | role: 'quit'、'copy'、'hide' 等由 Electron 内建处理,行为原生且自动本地化 |
macOS.ts 中多处 role 项 |
| 跨平台快捷键 | 用 CmdOrCtrl 覆盖双平台;macOS 专属项(如 Command+,)仅出现在 darwin 模板 |
macOS.ts |
| 分隔线分组 | { type: 'separator' } 划分功能组;托盘菜单用 enabled: false 项作分区标题 |
trayMenu.ts 的 createSection |
| 平台差异收敛 | 平台分支收敛到 createMenuImpl 工厂与 IPC 服务内的 process.platform 判断,不散落业务层 |
menus/index.ts、TrayMenuCtr.ts |
| 动态菜单带上限与折叠 | 动态列表裁剪(3/3/5)并以「更多」项兜底,防止托盘菜单无限增长 | trayMenu.ts |
| 菜单与窗口解耦 | 菜单点击只 broadcast 事件到渲染进程,不直接操作页面状态 |
MenuCtr.ts、macOS.ts |
小结
LobeHub Desktop 的菜单体系完整实践了配置指南提出的三类菜单与四项最佳实践:以 IMenuPlatform 接口约束三平台实现、以 createMenuImpl 工厂做平台分发;以「菜单类型 + ContextMenuData」驱动上下文菜单模板;以 TrayNavigationSnapshot 快照驱动托盘菜单的动态重建,并把 Windows 专属的托盘图标/tooltip/气泡能力收敛在 TrayMenuCtr 内;全程菜单文案走 i18n.ns('menu') 命名空间翻译。若要继续深入,建议按以下路径阅读源码:menus/types.ts(接口契约)→ menus/impls/macOS.ts(最完整的平台实现)→ controllers/MenuCtr.ts 与 controllers/TrayMenuCtr.ts(IPC 入口),再配合 trayMenu.test.ts、MenuCtr.test.ts 等测试验证各分支行为。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00