mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 17:47:27 +00:00
* ✨ feat(tool-detectors): add browser automation support and refactor tool detector categories - Introduced browser automation detectors to the tool detector manager. - Updated tool categories to include 'browser-automation'. - Refactored imports to use type imports where applicable for better clarity. - Cleaned up unnecessary comments in tool filters. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore: add browser automation tool detection UI * 🔧 chore: update react-scan version and enhance agent-browser documentation - Updated `react-scan` dependency from version 0.4.3 to 0.5.3 in package.json. - Improved documentation in `content.ts` for the agent-browser, clarifying command usage and workflows. - Added development mode flag `__DEV__` in sharedRendererConfig for better environment handling. - Integrated `scan` functionality in `initialize.ts` to enable scanning in development mode. - Updated global type definitions to include `__DEV__` constant for clarity. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore(builtin-skills): add dependency and refactor skill filtering logic - Added `@lobechat/const` as a dependency in package.json. - Introduced a new function `shouldEnableBuiltinSkill` to determine if a skill should be enabled based on the environment. - Refactored the `builtinSkills` export to filter skills using the new logic. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore(builtin-skills): refactor skill management and add filtering logic - Removed unnecessary dependency from package.json. - Simplified skill filtering logic by introducing `filterBuiltinSkills` and `shouldEnableBuiltinSkill` functions. - Updated various components to utilize the new filtering logic for managing builtin skills based on the environment. Signed-off-by: Innei <tukon479@gmail.com> * ✨ feat(builtin-skills): introduce new skill APIs and refactor manifest structure - Added new APIs for skill management: `runSkillApi`, `readReferenceApi`, and `exportFileApi` to enhance functionality. - Created a base manifest file (`manifest.base.ts`) to centralize API definitions. - Updated the desktop manifest (`manifest.desktop.ts`) to utilize the new base APIs. - Refactored existing manifest to streamline API integration and improve maintainability. - Introduced a detailed system prompt for better user guidance on skill usage. Signed-off-by: Innei <tukon479@gmail.com> * ✨ feat: desktop skill runtime, skill store inspectors, and tool UI updates Made-with: Cursor * ✨ feat: enhance skill import functionality and testing - Updated `importFromUrl` method in `SkillImporter` to accept additional options for identifier and source. - Modified `importFromMarket` in `agentSkillsRouter` to utilize the new options for better tracking of skill imports. - Added integration tests to ensure stable behavior when re-importing skills from the market, verifying that identifiers remain consistent across imports. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore: update .gitignore and package.json dependencies - Added 'bin' to .gitignore to exclude binary files from version control. - Included 'fflate' as a new dependency in package.json to support file compression in the application. - Updated writeFile method in LocalFileCtr to handle file content as Uint8Array for improved type safety. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore: update package.json dependencies - Removed 'fflate' from dependencies and added it to devDependencies for better organization. - Ensured proper formatting by adding a newline at the end of the file. Signed-off-by: Innei <tukon479@gmail.com> * ✨ feat: add agent-browser download script and integrate binary handling - Introduced a new script to download the `agent-browser` binary, ensuring it is available for the application. - Updated `electron-builder.mjs` to include the binary in the build process. - Modified `dir.ts` to define the binary directory path based on the packaging state. - Enhanced the `App` class to set environment variables for the agent-browser integration. Signed-off-by: Innei <tukon479@gmail.com> * ✨ feat: add DevTools toggle to Linux and Windows menus - Introduced a new menu item for toggling DevTools with the F12 accelerator key in both Linux and Windows menu implementations. - Added a separator for better organization of the view submenu items. Signed-off-by: Innei <tukon479@gmail.com> * ✨ feat: integrate agent-browser binary download into build process - Added functionality to download the `agent-browser` binary during the build process in `electron-builder.mjs`. - Enhanced the download script with detailed logging for better visibility of the download status and errors. - Updated the `App` class to log the binary directory path for improved debugging. - Reintroduced the `AuthRequiredModal` in the layout for desktop users. Signed-off-by: Innei <tukon479@gmail.com> * fix: mock binary directory path in tests - Added a mock for the binary directory path in the App tests to facilitate testing of the agent-browser integration. - This change enhances the test environment by providing a consistent path for the binary during test execution. Signed-off-by: Innei <tukon479@gmail.com> * 🐛 fix: improve authorization notification handling - Updated the `notifyAuthorizationRequired` method to implement trailing-edge debounce, ensuring that rapid 401 responses are coalesced and the IPC event is sent after the burst settles. - Refactored the notification logic to enhance clarity and maintainability. ✨ feat: add desktop onboarding redirect - Introduced a `useEffect` hook in `StoreInitialization` to redirect users to the `/desktop-onboarding` page if onboarding is not completed, ensuring a smoother user experience on fresh installs. Signed-off-by: Innei <tukon479@gmail.com> * 🐛 fix(desktop): hide Agent Browser skill on Windows Made-with: Cursor * 🔧 chore: update memory limits for build processes - Increased the `NODE_OPTIONS` memory limit for both `build:next` and `build:spa` scripts from 6144 to 7168, optimizing build performance and resource management. Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com>
445 lines
14 KiB
TypeScript
445 lines
14 KiB
TypeScript
import type { MenuItemConstructorOptions } from 'electron';
|
|
import { app, clipboard, dialog, Menu, shell } from 'electron';
|
|
|
|
import { isDev } from '@/const/env';
|
|
|
|
import type { ContextMenuData, IMenuPlatform, MenuOptions } from '../types';
|
|
import { BaseMenuPlatform } from './BaseMenuPlatform';
|
|
|
|
export class LinuxMenu extends BaseMenuPlatform implements IMenuPlatform {
|
|
private appMenu: Menu | null = null;
|
|
private trayMenu: Menu | null = null;
|
|
|
|
buildAndSetAppMenu(options?: MenuOptions): Menu {
|
|
const template = this.getAppMenuTemplate(options);
|
|
this.appMenu = Menu.buildFromTemplate(template);
|
|
Menu.setApplicationMenu(this.appMenu);
|
|
return this.appMenu;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
buildTrayMenu(): Menu {
|
|
const template = this.getTrayMenuTemplate();
|
|
this.trayMenu = Menu.buildFromTemplate(template);
|
|
return this.trayMenu;
|
|
}
|
|
|
|
refresh(options?: MenuOptions): void {
|
|
this.buildAndSetAppMenu(options);
|
|
}
|
|
|
|
// --- Private methods: define menu templates and logic ---
|
|
|
|
private getAppMenuTemplate(options?: MenuOptions): MenuItemConstructorOptions[] {
|
|
const showDev = isDev || options?.showDevItems;
|
|
const t = this.app.i18n.ns('menu');
|
|
|
|
const template: MenuItemConstructorOptions[] = [
|
|
{
|
|
label: t('file.title'),
|
|
submenu: [
|
|
{
|
|
accelerator: 'Ctrl+N',
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.show();
|
|
mainWindow.broadcast('createNewTopic');
|
|
},
|
|
label: t('file.newTopic'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
accelerator: 'Alt+Ctrl+A',
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.show();
|
|
mainWindow.broadcast('createNewAgent');
|
|
},
|
|
label: t('file.newAgent'),
|
|
},
|
|
{
|
|
accelerator: 'Alt+Ctrl+G',
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.show();
|
|
mainWindow.broadcast('createNewAgentGroup');
|
|
},
|
|
label: t('file.newAgentGroup'),
|
|
},
|
|
{
|
|
accelerator: 'Alt+Ctrl+P',
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.show();
|
|
mainWindow.broadcast('createNewPage');
|
|
},
|
|
label: t('file.newPage'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
click: () => this.app.browserManager.retrieveByIdentifier('settings').show(),
|
|
label: t('file.preferences'),
|
|
},
|
|
{
|
|
click: () => {
|
|
this.app.updaterManager.checkForUpdates({ manual: true });
|
|
},
|
|
label: t('common.checkUpdates'),
|
|
},
|
|
{ type: 'separator' },
|
|
{ label: t('window.close'), role: 'close' },
|
|
{ label: t('window.minimize'), role: 'minimize' },
|
|
{ type: 'separator' },
|
|
{ label: t('file.quit'), role: 'quit' },
|
|
],
|
|
},
|
|
{
|
|
label: t('edit.title'),
|
|
submenu: [
|
|
{ label: t('edit.undo'), role: 'undo' },
|
|
{ label: t('edit.redo'), role: 'redo' },
|
|
{ type: 'separator' },
|
|
{ label: t('edit.cut'), role: 'cut' },
|
|
{ label: t('edit.copy'), role: 'copy' },
|
|
{ label: t('edit.paste'), role: 'paste' },
|
|
{ type: 'separator' },
|
|
{ label: t('edit.selectAll'), role: 'selectAll' },
|
|
],
|
|
},
|
|
{
|
|
label: t('view.title'),
|
|
submenu: [
|
|
{ accelerator: 'F12', label: t('dev.devTools'), role: 'toggleDevTools' },
|
|
{ type: 'separator' },
|
|
{ label: t('view.resetZoom'), role: 'resetZoom' },
|
|
{ label: t('view.zoomIn'), role: 'zoomIn' },
|
|
{ label: t('view.zoomOut'), role: 'zoomOut' },
|
|
{ type: 'separator' },
|
|
{ label: t('view.toggleFullscreen'), role: 'togglefullscreen' },
|
|
],
|
|
},
|
|
{
|
|
label: t('history.title'),
|
|
submenu: [
|
|
{
|
|
accelerator: 'Alt+Left',
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.broadcast('historyGoBack');
|
|
},
|
|
label: t('history.back'),
|
|
},
|
|
{
|
|
accelerator: 'Alt+Right',
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.broadcast('historyGoForward');
|
|
},
|
|
label: t('history.forward'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
accelerator: 'Ctrl+Shift+H',
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.broadcast('navigate', { path: '/' });
|
|
},
|
|
label: t('history.home'),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
label: t('window.title'),
|
|
submenu: [
|
|
{ label: t('window.minimize'), role: 'minimize' },
|
|
{ label: t('window.close'), role: 'close' },
|
|
],
|
|
},
|
|
{
|
|
label: t('help.title'),
|
|
submenu: [
|
|
{
|
|
click: async () => {
|
|
await shell.openExternal('https://lobehub.com');
|
|
},
|
|
label: t('help.visitWebsite'),
|
|
},
|
|
{
|
|
click: async () => {
|
|
await shell.openExternal('https://github.com/lobehub/lobe-chat');
|
|
},
|
|
label: t('help.githubRepo'),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
click: () => {
|
|
const commonT = this.app.i18n.ns('common');
|
|
const dialogT = this.app.i18n.ns('dialog');
|
|
|
|
dialog.showMessageBox({
|
|
buttons: [commonT('actions.ok')],
|
|
detail: dialogT('about.detail'),
|
|
message: dialogT('about.message', {
|
|
appName: app.getName(),
|
|
appVersion: app.getVersion(),
|
|
}),
|
|
title: dialogT('about.title'),
|
|
type: 'info',
|
|
});
|
|
},
|
|
label: t('help.about'),
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
if (showDev) {
|
|
template.push({
|
|
label: t('dev.title'),
|
|
submenu: [
|
|
{ label: t('dev.reload'), role: 'reload' },
|
|
{ label: t('dev.forceReload'), role: 'forceReload' },
|
|
{ label: t('dev.devTools'), role: 'toggleDevTools' },
|
|
{ type: 'separator' },
|
|
{
|
|
click: () => {
|
|
this.app.browserManager.retrieveByIdentifier('devtools').show();
|
|
},
|
|
label: t('dev.devPanel'),
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
return template;
|
|
}
|
|
|
|
private getDefaultContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
|
const t = this.app.i18n.ns('menu');
|
|
const hasText = Boolean(data?.selectionText?.trim());
|
|
const hasLink = Boolean(data?.linkURL);
|
|
const hasImage = data?.mediaType === 'image' && Boolean(data?.srcURL);
|
|
|
|
const template: MenuItemConstructorOptions[] = [];
|
|
|
|
// Search with Google - only when text is selected
|
|
if (hasText) {
|
|
template.push({
|
|
click: () => {
|
|
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
|
shell.openExternal(searchUrl);
|
|
},
|
|
label: t('context.searchWithGoogle'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Link actions
|
|
if (hasLink) {
|
|
template.push({
|
|
click: () => shell.openExternal(data!.linkURL!),
|
|
label: t('context.openLink'),
|
|
});
|
|
template.push({
|
|
click: () => clipboard.writeText(data!.linkURL!),
|
|
label: t('context.copyLink'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Image actions
|
|
if (hasImage) {
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.downloadURL(data!.srcURL!);
|
|
},
|
|
label: t('context.saveImage'),
|
|
});
|
|
template.push({
|
|
click: () => {
|
|
clipboard.writeText(data!.srcURL!);
|
|
},
|
|
label: t('context.copyImageAddress'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Standard edit actions
|
|
template.push(
|
|
{ label: t('edit.cut'), role: 'cut' },
|
|
{ label: t('edit.copy'), role: 'copy' },
|
|
{ label: t('edit.paste'), role: 'paste' },
|
|
{ type: 'separator' },
|
|
{ label: t('edit.selectAll'), role: 'selectAll' },
|
|
);
|
|
|
|
// Inspect Element in dev mode
|
|
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
|
template.push({ type: 'separator' });
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
|
},
|
|
label: t('context.inspectElement'),
|
|
});
|
|
}
|
|
|
|
return template;
|
|
}
|
|
|
|
private getChatContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
|
const t = this.app.i18n.ns('menu');
|
|
const hasText = Boolean(data?.selectionText?.trim());
|
|
const hasLink = Boolean(data?.linkURL);
|
|
const hasImage = data?.mediaType === 'image' && Boolean(data?.srcURL);
|
|
|
|
const template: MenuItemConstructorOptions[] = [];
|
|
|
|
// Search with Google - only when text is selected
|
|
if (hasText) {
|
|
template.push({
|
|
click: () => {
|
|
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
|
shell.openExternal(searchUrl);
|
|
},
|
|
label: t('context.searchWithGoogle'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Link actions
|
|
if (hasLink) {
|
|
template.push({
|
|
click: () => shell.openExternal(data!.linkURL!),
|
|
label: t('context.openLink'),
|
|
});
|
|
template.push({
|
|
click: () => clipboard.writeText(data!.linkURL!),
|
|
label: t('context.copyLink'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Image actions
|
|
if (hasImage) {
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.downloadURL(data!.srcURL!);
|
|
},
|
|
label: t('context.saveImage'),
|
|
});
|
|
template.push({
|
|
click: () => {
|
|
clipboard.writeText(data!.srcURL!);
|
|
},
|
|
label: t('context.copyImageAddress'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Standard edit actions for chat
|
|
template.push(
|
|
{ label: t('edit.copy'), role: 'copy' },
|
|
{ label: t('edit.paste'), role: 'paste' },
|
|
{ type: 'separator' },
|
|
{ label: t('edit.selectAll'), role: 'selectAll' },
|
|
);
|
|
|
|
// Inspect Element in dev mode
|
|
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
|
template.push({ type: 'separator' });
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
|
},
|
|
label: t('context.inspectElement'),
|
|
});
|
|
}
|
|
|
|
return template;
|
|
}
|
|
|
|
private getEditorContextMenuTemplate(data?: ContextMenuData): MenuItemConstructorOptions[] {
|
|
const t = this.app.i18n.ns('menu');
|
|
const hasText = Boolean(data?.selectionText?.trim());
|
|
|
|
const template: MenuItemConstructorOptions[] = [];
|
|
|
|
// Search with Google - only when text is selected
|
|
if (hasText) {
|
|
template.push({
|
|
click: () => {
|
|
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(data!.selectionText!.trim())}`;
|
|
shell.openExternal(searchUrl);
|
|
},
|
|
label: t('context.searchWithGoogle'),
|
|
});
|
|
template.push({ type: 'separator' });
|
|
}
|
|
|
|
// Standard edit actions for editor
|
|
template.push(
|
|
{ label: t('edit.cut'), role: 'cut' },
|
|
{ label: t('edit.copy'), role: 'copy' },
|
|
{ label: t('edit.paste'), role: 'paste' },
|
|
{ type: 'separator' },
|
|
{ label: t('edit.selectAll'), role: 'selectAll' },
|
|
{ type: 'separator' },
|
|
{ label: t('edit.delete'), role: 'delete' },
|
|
);
|
|
|
|
// Inspect Element in dev mode
|
|
if (isDev && data?.x !== undefined && data?.y !== undefined) {
|
|
template.push({ type: 'separator' });
|
|
template.push({
|
|
click: () => {
|
|
const mainWindow = this.app.browserManager.getMainWindow();
|
|
mainWindow.webContents.inspectElement(data.x!, data.y!);
|
|
},
|
|
label: t('context.inspectElement'),
|
|
});
|
|
}
|
|
|
|
return template;
|
|
}
|
|
|
|
private getTrayMenuTemplate(): MenuItemConstructorOptions[] {
|
|
const t = this.app.i18n.ns('menu');
|
|
const appName = app.getName();
|
|
|
|
return [
|
|
{
|
|
click: () => this.app.browserManager.showMainWindow(),
|
|
label: t('tray.open', { appName }),
|
|
},
|
|
{ type: 'separator' },
|
|
{
|
|
click: () => this.app.browserManager.retrieveByIdentifier('settings').show(),
|
|
label: t('file.preferences'),
|
|
},
|
|
{ type: 'separator' },
|
|
{ label: t('tray.quit'), role: 'quit' },
|
|
];
|
|
}
|
|
}
|