dootask/electron/electron.js

2515 lines
75 KiB
JavaScript
Vendored
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Node.js 核心模块
const fs = require('fs')
const os = require("os");
const path = require('path')
const spawn = require("child_process").spawn;
// Web 服务相关
const express = require('express')
const axios = require('axios');
// Electron 核心模块
const {
app,
ipcMain,
dialog,
clipboard,
nativeImage,
globalShortcut,
nativeTheme,
screen,
Tray,
Menu,
WebContentsView,
BrowserWindow
} = require('electron')
// 禁用渲染器后台化
app.commandLine.appendSwitch('disable-renderer-backgrounding');
app.commandLine.appendSwitch('disable-backgrounding-occluded-windows');
// Electron 扩展和工具
const {autoUpdater} = require("electron-updater")
const Store = require("electron-store");
const loger = require("electron-log");
const electronConf = require('electron-config')
const Screenshots = require("electron-screenshots-tool").Screenshots;
// 本地模块和配置
const utils = require('./lib/utils');
const navigation = require('./lib/navigation');
const config = require('./package.json');
const electronDown = require("./electron-down");
const electronMenu = require("./electron-menu");
const { startMCPServer, stopMCPServer } = require("./lib/mcp");
const {onRenderer, renderer} = require("./lib/renderer");
const {onExport} = require("./lib/pdf-export");
const {allowedCalls, isWin, isMac} = require("./lib/other");
// 实例初始化
const userConf = new electronConf()
const store = new Store();
// 路径和缓存配置
const cacheDir = path.join(os.tmpdir(), 'dootask-cache')
const updaterLockFile = path.join(cacheDir, '.dootask_updater.lock');
// 应用状态标志
let isReady = false,
willQuitApp = false,
isDevelopMode = false;
// 服务器配置
let serverPort = 22223,
mcpPort = 22224,
serverPublicDir = path.join(__dirname, 'public'),
serverUrl = "",
serverTimer = null;
// 截图相关变量
let screenshotObj = null,
screenshotKey = null;
// 窗口实例变量
let mainWindow = null,
mainTray = null,
mediaWindow = null;
// 多窗口 Tab 管理
// Map<windowId, {window, views: [{id, view, name, favicon}], activeTabId, mode: 'tab'|'window'}>
let webTabWindows = new Map();
let webTabWindowIdCounter = 1;
// 标签名称到标签位置的映射,用于复用已存在的标签
// Map<name, {windowId, tabId}>
let webTabNameMap = new Map();
// 窗口配置和状态
let mediaType = null,
webTabHeight = 40,
webTabClosedByShortcut = new Map(); // Map<windowId, boolean>
// 开发模式路径
let devloadPath = path.resolve(__dirname, ".devload");
// 窗口显示状态管理
let showState = {},
onShowWindow = (win) => {
try {
if (typeof showState[win.webContents.id] === 'undefined') {
showState[win.webContents.id] = true
win.show();
}
} catch (e) {
// loger.error(e)
}
}
// 开发模式加载
if (fs.existsSync(devloadPath)) {
let devloadContent = fs.readFileSync(devloadPath, 'utf8')
if (devloadContent.startsWith('http')) {
serverUrl = devloadContent;
isDevelopMode = true;
}
}
// 缓存目录检查
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
// 初始化下载
electronDown.initialize(() => {
if (mainWindow) {
mainWindow.webContents.send("openDownloadWindow", {})
}
})
/**
* 启动web服务
*/
async function startWebServer(force = false) {
if (serverUrl && !force) {
return Promise.resolve();
}
// 每次启动前清理缓存
utils.clearServerCache();
return new Promise((resolve, reject) => {
// 创建Express应用
const expressApp = express();
// 健康检查
expressApp.head('/health', (req, res) => {
res.status(200).send('OK');
});
// 使用express.static中间件提供静态文件服务
// Express内置了全面的MIME类型支持无需手动配置
expressApp.use(express.static(serverPublicDir, {
// 设置默认文件
index: ['index.html', 'index.htm'],
// 启用etag缓存
etag: true,
// 设置缓存时间(开发环境可以设置较短)
maxAge: '1h',
// 启用压缩
dotfiles: 'ignore',
// 自定义头部
setHeaders: (res, path, stat) => {
const ext = path.split('.').pop().toLowerCase();
// HTML、JS、CSS文件禁用缓存方便开发调试
if (['html', 'js', 'css'].includes(ext)){
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
res.set('Pragma', 'no-cache');
res.set('Expires', '0');
}
}
}));
// 404处理中间件
expressApp.use((req, res) => {
res.status(404).send('File not found');
});
// 错误处理中间件
expressApp.use((err, req, res, next) => {
// 不是ENOENT错误记录error级别日志
if (err.code !== 'ENOENT') {
loger.error('Server error:', err);
res.status(500).send('Internal Server Error');
return;
}
// 没有path说明是404错误
if (!err.path) {
loger.warn('File not found:', req.url);
res.status(404).send('File not found');
return;
}
// 不是临时文件错误普通404
if (!err.path.includes('.com.dootask.task.')) {
loger.warn('File not found:', err.path);
res.status(404).send('File not found');
return;
}
// 防止死循环 - 如果已经是重定向请求直接返回404
if (req.query._dt_restored) {
const redirectTime = parseInt(req.query._dt_restored);
const timeDiff = Date.now() - redirectTime;
// 10秒内的重定向认为是死循环直接返回404
if (timeDiff < 10000) {
loger.warn('Recent redirect detected, avoiding loop:', timeDiff + 'ms ago');
res.status(404).send('File not found');
return;
}
}
loger.warn('Temporary file cleaned up by system:', err.path, req.url);
// 临时文件被系统清理尝试从serverPublicDir重新读取并恢复
const requestedUrl = new URL(req.url, serverUrl);
const requestedFile = path.join(serverPublicDir, requestedUrl.pathname === '/' ? '/index.html' : requestedUrl.pathname);
try {
// 检查文件是否存在于serverPublicDir
fs.accessSync(requestedFile, fs.constants.F_OK);
// 确保目标目录存在
const targetDir = path.dirname(err.path);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, {recursive: true});
}
// 从ASAR文件中读取文件并写入到临时位置
fs.writeFileSync(err.path, fs.readFileSync(requestedFile));
// 文件恢复成功后301重定向到带__redirect参数的URL
requestedUrl.searchParams.set('_dt_restored', Date.now());
res.redirect(301, requestedUrl.toString());
} catch (accessErr) {
// 文件不存在于serverPublicDir返回404
loger.warn('Source file not found:', requestedFile, 'Error:', accessErr.message);
res.status(404).send('File not found');
}
});
// 启动服务器
const server = expressApp.listen(serverPort, 'localhost', () => {
loger.info(`Express static file server running at http://localhost:${serverPort}/`);
loger.info(`Serving files from: ${serverPublicDir}`);
serverUrl = `http://localhost:${serverPort}/`;
resolve(server);
// 启动健康检查定时器
serverTimeout();
});
// 错误处理
server.on('error', (err) => {
loger.error('Server error:', err);
reject(err);
});
});
}
/**
* 健康检查定时器
*/
function serverTimeout() {
clearTimeout(serverTimer)
serverTimer = setTimeout(async () => {
if (!serverUrl) {
return; // 没有服务器URL直接返回
}
try {
const res = await axios.head(serverUrl + 'health')
if (res.status === 200) {
serverTimeout() // 健康检查通过,重新设置定时器
return;
}
loger.error('Server health check failed with status: ' + res.status);
} catch (err) {
loger.error('Server health check error:', err);
}
// 如果健康检查失败,尝试重新启动服务器
try {
await startWebServer(true)
loger.info('Server restarted successfully');
} catch (error) {
loger.error('Failed to restart server:', error);
}
}, 10000)
}
/**
* 创建主窗口
*/
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
minWidth: 360,
minHeight: 360,
center: true,
autoHideMenuBar: true,
backgroundColor: utils.getDefaultBackgroundColor(),
webPreferences: {
preload: path.join(__dirname, 'electron-preload.js'),
webSecurity: true,
nodeIntegration: true,
contextIsolation: true,
backgroundThrottling: false,
}
})
mainWindow.on('page-title-updated', (event, title) => {
if (title == "index.html") {
event.preventDefault()
}
})
mainWindow.on('focus', () => {
mainWindow.webContents.send("browserWindowFocus", {})
})
mainWindow.on('blur', () => {
mainWindow.webContents.send("browserWindowBlur", {})
})
mainWindow.on('close', event => {
if (!willQuitApp) {
utils.onBeforeUnload(event, mainWindow).then(() => {
if (['darwin', 'win32'].includes(process.platform)) {
if (mainWindow.isFullScreen()) {
mainWindow.once('leave-full-screen', () => {
mainWindow.hide();
})
mainWindow.setFullScreen(false)
} else {
mainWindow.hide();
}
} else {
app.quit();
}
})
}
})
// 设置 UA
const originalUA = mainWindow.webContents.session.getUserAgent() || mainWindow.webContents.getUserAgent()
mainWindow.webContents.setUserAgent(originalUA + " MainTaskWindow/" + process.platform + "/" + os.arch() + "/1.0");
// 新窗口处理
mainWindow.webContents.setWindowOpenHandler(({url}) => {
if (allowedCalls.test(url)) {
renderer.openExternal(url).catch(() => {})
} else {
utils.onBeforeOpenWindow(mainWindow.webContents, url).then(() => {
renderer.openExternal(url).catch(() => {})
})
}
return {action: 'deny'}
})
// 设置右键菜单
electronMenu.webContentsMenu(mainWindow.webContents)
// 设置导航快捷键(返回/前进)
navigation.setup(mainWindow)
// 加载地址
utils.loadUrl(mainWindow, serverUrl)
}
/**
* 创建更新程序子进程
*/
function createUpdaterWindow(updateTitle) {
// 检查平台是否支持
if (!['darwin', 'win32'].includes(process.platform)) {
return;
}
try {
// 构建updater应用路径
let updaterPath;
if (isWin) {
updaterPath = path.join(process.resourcesPath, 'updater', 'updater.exe');
} else {
updaterPath = path.join(process.resourcesPath, 'updater', 'updater');
}
// 检查updater应用是否存在
if (!fs.existsSync(updaterPath)) {
loger.error('Updater not found:', updaterPath);
return;
}
// 检查文件权限
try {
fs.accessSync(updaterPath, fs.constants.X_OK);
} catch (e) {
if (isWin) {
try {
spawn('icacls', [updaterPath, '/grant', 'everyone:F'], { stdio: 'inherit', shell: true });
} catch (e) {
loger.error('Failed to set executable permission:', e);
}
} else if (process.platform === 'darwin') {
try {
spawn('chmod', ['+x', updaterPath], {stdio: 'inherit'});
} catch (e) {
loger.error('Failed to set executable permission:', e);
}
}
}
// 创建锁文件
fs.writeFileSync(updaterLockFile, Date.now().toString());
// 启动子进程,传入锁文件路径作为第一个参数
const child = spawn(updaterPath, [updaterLockFile], {
detached: true,
stdio: 'ignore',
shell: isWin,
env: {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
UPDATER_TITLE: updateTitle || ''
}
});
child.unref();
child.on('error', (err) => {
loger.error('Updater process error:', err);
});
} catch (e) {
loger.error('Failed to create updater process:', e);
}
}
/**
* 创建媒体浏览器窗口
* @param args
* @param type
*/
function createMediaWindow(args, type = 'image') {
if (mediaWindow === null) {
mediaWindow = new BrowserWindow({
width: Math.floor(args.width) || 970,
height: Math.floor(args.height) || 700,
minWidth: 360,
minHeight: 360,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
webSecurity: false,
plugins: true
},
show: false
});
// 监听关闭事件
mediaWindow.addListener('close', event => {
if (!willQuitApp) {
event.preventDefault()
if (mediaWindow.isFullScreen()) {
mediaWindow.once('leave-full-screen', () => {
mediaWindow.hide();
})
mediaWindow.setFullScreen(false)
} else {
mediaWindow.webContents.send('on-close');
mediaWindow.hide();
}
}
})
// 监听关闭事件
mediaWindow.addListener('closed', () => {
mediaWindow = null;
mediaType = null;
})
// 设置右键菜单
electronMenu.webContentsMenu(mediaWindow.webContents)
} else {
// 直接显示
mediaWindow.show();
}
// 加载图片浏览器的HTML
if (mediaType === type) {
// 更新窗口
mediaWindow.webContents.send('load-media', args);
} else {
// 重置窗口
mediaType = type;
let filePath = './render/viewer/index.html';
if (type === 'video') {
filePath = './render/video/index.html';
}
mediaWindow.loadFile(filePath, {}).then(_ => { }).catch(_ => { })
}
// 窗口准备好后事件
mediaWindow.removeAllListeners("ready-to-show");
mediaWindow.addListener('ready-to-show', () => {
mediaWindow.show();
mediaWindow.webContents.send('load-media', args);
});
}
/**
* 创建内置浏览器窗口(支持多窗口)
* @param args {url, windowId, position, afterId, insertIndex, name, force, userAgent, title, titleFixed, webPreferences, mode, ...}
* - mode: 'tab' | 'window'
* - 'window': 独立窗口模式(无导航栏)
* - 'tab': 标签页模式(默认,有导航栏)
* @returns {number} 窗口ID
*/
function createWebTabWindow(args) {
if (!args) {
return;
}
if (!utils.isJson(args)) {
args = {url: args}
}
const mode = args.mode || 'tab';
const isWindowMode = mode === 'window';
// 如果有 name先查找是否已存在同名标签/窗口
if (args.name) {
const existing = webTabNameMap.get(args.name);
if (existing) {
const existingWindowData = webTabWindows.get(existing.windowId);
if (existingWindowData && existingWindowData.window && !existingWindowData.window.isDestroyed()) {
const viewItem = existingWindowData.views.find(v => v.id === existing.tabId);
if (viewItem && viewItem.view && !viewItem.view.webContents.isDestroyed()) {
// 激活已存在的标签/窗口
if (existingWindowData.window.isMinimized()) {
existingWindowData.window.restore();
}
existingWindowData.window.focus();
existingWindowData.window.show();
activateWebTabInWindow(existing.windowId, existing.tabId);
// force=true 时重新加载
if (args.force === true && args.url) {
utils.loadContentUrl(viewItem.view.webContents, serverUrl, args.url);
}
return existing.windowId;
}
}
// 标签已失效,清理映射
webTabNameMap.delete(args.name);
}
}
// 确定目标窗口ID
let windowId = args.windowId;
let windowData = windowId ? webTabWindows.get(windowId) : null;
let webTabWindow = windowData ? windowData.window : null;
// 如果没有指定窗口或窗口不存在,查找可用窗口或创建新窗口
if (!webTabWindow) {
// window 模式总是创建新窗口tab 模式尝试使用第一个可用的 tab 窗口
if (!isWindowMode && !windowId) {
for (const [id, data] of webTabWindows) {
if (data.window && !data.window.isDestroyed() && data.mode !== 'window') {
windowId = id;
windowData = data;
webTabWindow = data.window;
break;
}
}
}
// 如果还是没有窗口,创建新窗口
if (!webTabWindow) {
windowId = webTabWindowIdCounter++;
// 从 args 中提取窗口尺寸
const position = {
x: args.x,
y: args.y,
width: args.width,
height: args.height,
minWidth: args.minWidth,
minHeight: args.minHeight,
};
webTabWindow = createWebTabWindowInstance(windowId, position, mode);
windowData = {
window: webTabWindow,
views: [],
activeTabId: null,
mode: mode
};
webTabWindows.set(windowId, windowData);
}
}
if (webTabWindow.isMinimized()) {
webTabWindow.restore()
}
webTabWindow.focus();
webTabWindow.show();
// 创建 tab 子视图
const browserView = createWebTabView(windowId, args);
// 确定插入位置
let insertIndex = windowData.views.length;
if (args.afterId) {
const afterIndex = windowData.views.findIndex(item => item.id === args.afterId);
if (afterIndex > -1) {
insertIndex = afterIndex + 1;
}
}
if (typeof args.insertIndex === 'number') {
insertIndex = Math.max(0, Math.min(args.insertIndex, windowData.views.length));
}
// 插入到指定位置,包含 name 信息
windowData.views.splice(insertIndex, 0, {
id: browserView.webContents.id,
view: browserView,
name: args.name || null
});
// 如果有 name注册到映射
if (args.name) {
webTabNameMap.set(args.name, {
windowId: windowId,
tabId: browserView.webContents.id
});
}
// tab 模式通知标签栏创建标签window 模式设置窗口标题
if (isWindowMode) {
// window 模式下,如果传入了 title 参数,设置窗口标题
if (args.title) {
webTabWindow.setTitle(args.title);
}
} else {
// tab 模式下通知标签栏创建新标签
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'create',
id: browserView.webContents.id,
url: args.url,
afterId: args.afterId,
windowId: windowId,
title: args.title,
}).then(_ => { });
}
activateWebTabInWindow(windowId, browserView.webContents.id);
return windowId;
}
/**
* 创建 WebTabWindow 实例
* @param windowId
* @param position {x, y, width, height}
* @param mode 'tab' | 'window'
* @returns {BrowserWindow}
*/
function createWebTabWindowInstance(windowId, position, mode = 'tab') {
const isWindowMode = mode === 'window';
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
const isHighRes = screenWidth >= 2560;
// 根据屏幕分辨率计算默认尺寸
const screenDefault = {
width: isHighRes ? 1440 : 1024,
height: isHighRes ? 900 : 768,
minWidth: 400,
minHeight: 300,
};
// 计算窗口尺寸和位置
const windowOptions = {
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'electron-preload.js'),
webSecurity: true,
nodeIntegration: true,
contextIsolation: true,
},
};
if (isWindowMode) {
// window 模式:使用 position 参数或屏幕默认值,永远居中
Object.assign(windowOptions, {
width: Math.floor(position?.width ?? screenDefault.width),
height: Math.floor(position?.height ?? screenDefault.height),
minWidth: Math.floor(position?.minWidth ?? screenDefault.minWidth),
minHeight: Math.floor(position?.minHeight ?? screenDefault.minHeight),
backgroundColor: utils.getDefaultBackgroundColor(),
center: true,
});
} else {
// tab 模式:使用 savedBounds 或屏幕默认值
const savedBounds = userConf.get('webTabWindow') || {};
const maxX = Math.floor(screenWidth * 0.9);
const maxY = Math.floor(screenHeight * 0.9);
Object.assign(windowOptions, {
width: savedBounds.width ?? screenDefault.width,
height: savedBounds.height ?? screenDefault.height,
minWidth: screenDefault.minWidth,
minHeight: screenDefault.minHeight,
backgroundColor: nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF',
center: true,
});
// 恢复保存的位置,并限制在屏幕 90% 范围内
if (savedBounds.x !== undefined && savedBounds.y !== undefined) {
Object.assign(windowOptions, {
x: Math.min(savedBounds.x, maxX),
y: Math.min(savedBounds.y, maxY),
center: false,
});
}
}
// tab 模式使用隐藏标题栏 + titleBarOverlay
if (!isWindowMode) {
const titleBarOverlay = {
height: webTabHeight
};
if (nativeTheme.shouldUseDarkColors) {
Object.assign(titleBarOverlay, {
color: '#3B3B3D',
symbolColor: '#C5C5C5',
});
}
Object.assign(windowOptions, {
titleBarStyle: 'hidden',
titleBarOverlay: titleBarOverlay,
});
}
const webTabWindow = new BrowserWindow(windowOptions);
// 保存窗口ID到窗口对象
webTabWindow.webTabWindowId = windowId;
const originalClose = webTabWindow.close;
webTabWindow.close = function() {
webTabClosedByShortcut.set(windowId, true);
return originalClose.apply(this, arguments);
};
webTabWindow.on('resize', () => {
resizeWebTabInWindow(windowId, 0);
});
webTabWindow.on('enter-full-screen', () => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'enter-full-screen',
}).then(_ => { });
});
webTabWindow.on('leave-full-screen', () => {
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'leave-full-screen',
}).then(_ => { });
});
webTabWindow.on('close', event => {
if (webTabClosedByShortcut.get(windowId)) {
webTabClosedByShortcut.set(windowId, false);
if (!willQuitApp) {
closeWebTabInWindow(windowId, 0);
event.preventDefault();
return;
}
}
// 只有 tab 模式才保存 bounds
const windowData = webTabWindows.get(windowId);
if (windowData && windowData.mode !== 'window') {
userConf.set('webTabWindow', webTabWindow.getBounds());
}
});
webTabWindow.on('closed', () => {
const windowData = webTabWindows.get(windowId);
if (windowData) {
windowData.views.forEach(({view, name}) => {
// 清理 name 映射
if (name) {
webTabNameMap.delete(name);
}
try {
view.webContents.close();
} catch (e) {
//
}
});
webTabWindows.delete(windowId);
}
webTabClosedByShortcut.delete(windowId);
});
webTabWindow.once('ready-to-show', () => {
onShowWindow(webTabWindow);
});
webTabWindow.webContents.once('dom-ready', () => {
onShowWindow(webTabWindow);
});
webTabWindow.webContents.on('before-input-event', (event, input) => {
if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'r') {
reloadWebTabInWindow(windowId, 0);
event.preventDefault();
} else if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'w') {
webTabClosedByShortcut.set(windowId, true);
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
devToolsWebTabInWindow(windowId, 0);
} else {
const item = currentWebTabInWindow(windowId);
if (item) {
navigation.handleInput(event, input, item.view.webContents);
}
}
});
// 设置鼠标侧键和触控板手势导航
navigation.setupWindowEvents(webTabWindow, () => {
const item = currentWebTabInWindow(windowId);
return item ? item.view.webContents : null;
});
// tab 模式加载标签栏界面window 模式不需要
if (!isWindowMode) {
webTabWindow.loadFile('./render/tabs/index.html', {query: {windowId: String(windowId)}}).then(_ => { }).catch(_ => { });
}
return webTabWindow;
}
/**
* 创建 WebTab 视图
* @param windowId
* @param args
* @returns {WebContentsView}
*/
function createWebTabView(windowId, args) {
const windowData = webTabWindows.get(windowId);
if (!windowData) return null;
const webTabWindow = windowData.window;
const isWindowMode = windowData.mode === 'window';
const effectiveTabHeight = isWindowMode ? 0 : webTabHeight;
const viewOptions = {
webPreferences: Object.assign({
preload: path.join(__dirname, 'electron-preload.js'),
nodeIntegration: true,
contextIsolation: true
}, args.webPreferences || {})
};
if (!viewOptions.webPreferences.contextIsolation) {
delete viewOptions.webPreferences.preload;
}
const browserView = new WebContentsView(viewOptions);
if (args.backgroundColor) {
browserView.setBackgroundColor(args.backgroundColor);
} else if (isWindowMode) {
browserView.setBackgroundColor(utils.getDefaultBackgroundColor());
} else if (nativeTheme.shouldUseDarkColors) {
browserView.setBackgroundColor('#575757');
} else {
browserView.setBackgroundColor('#FFFFFF');
}
browserView.setBounds({
x: 0,
y: effectiveTabHeight,
width: webTabWindow.getContentBounds().width || 1280,
height: (webTabWindow.getContentBounds().height || 800) - effectiveTabHeight,
});
// 保存所属窗口ID和元数据
browserView.webTabWindowId = windowId;
browserView.tabName = args.name || null;
browserView.titleFixed = args.titleFixed || false;
// 设置自定义 UserAgent
if (args.userAgent) {
const originalUA = browserView.webContents.getUserAgent();
browserView.webContents.setUserAgent(
originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0 " + args.userAgent
);
}
browserView.webContents.on('destroyed', () => {
if (browserView.tabName) {
webTabNameMap.delete(browserView.tabName);
}
if (browserView._loadingChecker) {
clearInterval(browserView._loadingChecker);
browserView._loadingChecker = null;
}
closeWebTabInWindow(windowId, browserView.webContents.id);
});
browserView.webContents.setWindowOpenHandler(({url}) => {
if (allowedCalls.test(url)) {
renderer.openExternal(url).catch(() => {});
} else if (isWindowMode) {
// window 模式下打开外部浏览器
utils.onBeforeOpenWindow(browserView.webContents, url).then(() => {
renderer.openExternal(url).catch(() => {});
});
} else if (url && url !== 'about:blank') {
// tab 模式下创建新标签
createWebTabWindow({url, afterId: browserView.webContents.id, windowId});
}
return {action: 'deny'};
});
browserView.webContents.on('page-title-updated', (_, title) => {
// titleFixed 时不更新标题
if (browserView.titleFixed) {
return;
}
// 使用动态窗口ID支持标签在窗口间转移
const currentWindowId = browserView.webTabWindowId;
const wd = webTabWindows.get(currentWindowId);
if (!wd || !wd.window) return;
// 根据模式更新标题
if (wd.mode === 'window') {
// window 模式下直接设置窗口标题
wd.window.setTitle(title);
} else {
// tab 模式下通知标签栏更新标题
utils.onDispatchEvent(wd.window.webContents, {
event: 'title',
id: browserView.webContents.id,
title: title,
url: browserView.webContents.getURL(),
}).then(_ => { });
}
});
browserView.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
if (!errorDescription) {
return;
}
// 主框架加载失败时,展示内置的错误页面
if (isMainFrame) {
const originalUrl = validatedURL || args.url || '';
const filePath = path.join(__dirname, 'render', 'tabs', 'error.html');
browserView.webContents.loadFile(filePath, {
query: {
id: String(browserView.webContents.id),
url: originalUrl,
code: String(errorCode),
desc: errorDescription,
}
}).then(_ => { }).catch(_ => { });
return;
}
// 使用动态窗口ID支持标签在窗口间转移
const currentWindowId = browserView.webTabWindowId;
const wd = webTabWindows.get(currentWindowId);
if (!wd || !wd.window) return;
utils.onDispatchEvent(wd.window.webContents, {
event: 'title',
id: browserView.webContents.id,
title: errorDescription,
url: browserView.webContents.getURL(),
}).then(_ => { });
});
browserView.webContents.on('page-favicon-updated', async (_, favicons) => {
// 使用动态窗口ID支持标签在窗口间转移
const currentWindowId = browserView.webTabWindowId;
const wd = webTabWindows.get(currentWindowId);
if (!wd || !wd.window) return;
const tabId = browserView.webContents.id;
const faviconUrl = favicons[favicons.length - 1] || '';
// 验证并转换 favicon 为 base64
const base64Favicon = await utils.fetchFaviconAsBase64(faviconUrl);
// 保存验证后的 favicon 到视图对象
const viewItem = wd.views.find(v => v.id === tabId);
if (viewItem) {
viewItem.favicon = base64Favicon || '';
}
// 发送验证后的 favicon 给前端
utils.onDispatchEvent(wd.window.webContents, {
event: 'favicon',
id: tabId,
favicon: base64Favicon || ''
}).then(_ => { });
});
// 页面加载状态管理忽略SPA路由切换(isSameDocument)
browserView._loadingActive = false;
browserView._loadingChecker = null;
const dispatchLoading = (event) => {
const wd = webTabWindows.get(browserView.webTabWindowId);
if (wd && wd.window) {
utils.onDispatchEvent(wd.window.webContents, {
event,
id: browserView.webContents.id,
}).then(_ => { });
}
};
const startLoading = () => {
if (browserView._loadingActive) return;
browserView._loadingActive = true;
dispatchLoading('start-loading');
if (!browserView._loadingChecker) {
browserView._loadingChecker = setInterval(() => {
if (browserView.webContents.isDestroyed() || !browserView.webContents.isLoading()) {
stopLoading();
}
}, 3000);
}
};
const stopLoading = () => {
if (browserView._loadingChecker) {
clearInterval(browserView._loadingChecker);
browserView._loadingChecker = null;
}
if (!browserView._loadingActive) return;
browserView._loadingActive = false;
dispatchLoading('stop-loading');
};
browserView.webContents.on('did-start-navigation', (_, _url, _isInPlace, isMainFrame, _frameProcessId, _frameRoutingId, _navigationId, isSameDocument) => {
if (isMainFrame && !isSameDocument) {
startLoading();
}
});
browserView.webContents.on('did-stop-loading', _ => {
stopLoading();
if (nativeTheme.shouldUseDarkColors) {
browserView.setBackgroundColor('#FFFFFF');
}
});
browserView.webContents.on('before-input-event', (event, input) => {
// 使用动态窗口ID支持标签在窗口间转移
const currentWindowId = browserView.webTabWindowId;
if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'r') {
browserView.webContents.reload();
event.preventDefault();
} else if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'w') {
webTabClosedByShortcut.set(currentWindowId, true);
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
browserView.webContents.toggleDevTools();
} else {
navigation.handleInput(event, input, browserView.webContents);
}
});
const originalUA = browserView.webContents.session.getUserAgent() || browserView.webContents.getUserAgent();
browserView.webContents.setUserAgent(originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0");
electronMenu.webContentsMenu(browserView.webContents, true);
// 加载地址
utils.loadContentUrl(browserView.webContents, serverUrl, args.url);
browserView.setVisible(true);
webTabWindow.contentView.addChildView(browserView);
return browserView;
}
/**
* 获取当前内置浏览器标签
* @returns {object|undefined}
*/
function currentWebTab() {
// 找到第一个活跃窗口
for (const [windowId, windowData] of webTabWindows) {
if (windowData.window && !windowData.window.isDestroyed()) {
return currentWebTabInWindow(windowId);
}
}
return undefined;
}
/**
* 获取指定窗口的当前内置浏览器标签
* @param windowId
* @returns {object|undefined}
*/
function currentWebTabInWindow(windowId) {
const windowData = webTabWindows.get(windowId);
if (!windowData || !windowData.window) return undefined;
const webTabView = windowData.views;
const webTabWindow = windowData.window;
// 第一:使用当前可见的标签
try {
const item = webTabView.find(({view}) => view?.getVisible && view.getVisible());
if (item) {
return item;
}
} catch (e) {}
// 第二:使用当前聚焦的 webContents
try {
const focused = require('electron').webContents.getFocusedWebContents?.();
if (focused) {
const item = webTabView.find(it => it.id === focused.id);
if (item) {
return item;
}
}
} catch (e) {}
// 兜底:根据 children 顺序选择最上层的可用视图
const children = webTabWindow.contentView?.children || [];
for (let i = children.length - 1; i >= 0; i--) {
const id = children[i]?.webContents?.id;
const item = webTabView.find(it => it.id === id);
if (item) {
return item;
}
}
return undefined;
}
/**
* 根据 tabId 查找所属窗口ID
* @param tabId
* @returns {number|null}
*/
function findWindowIdByTabId(tabId) {
for (const [windowId, windowData] of webTabWindows) {
if (windowData.views.some(v => v.id === tabId)) {
return windowId;
}
}
return null;
}
/**
* 重新加载内置浏览器标签
* @param windowId
* @param id
*/
function reloadWebTabInWindow(windowId, id) {
const windowData = webTabWindows.get(windowId);
if (!windowData) return;
const item = id === 0 ? currentWebTabInWindow(windowId) : windowData.views.find(item => item.id == id);
if (!item) {
return;
}
item.view.webContents.reload();
}
/**
* 内置浏览器标签打开开发者工具
* @param windowId
* @param id
*/
function devToolsWebTabInWindow(windowId, id) {
const windowData = webTabWindows.get(windowId);
if (!windowData) return;
const item = id === 0 ? currentWebTabInWindow(windowId) : windowData.views.find(item => item.id == id);
if (!item) {
return;
}
item.view.webContents.toggleDevTools();
}
/**
* 调整内置浏览器标签尺寸
* @param windowId
* @param id
*/
function resizeWebTabInWindow(windowId, id) {
const windowData = webTabWindows.get(windowId);
if (!windowData || !windowData.window) return;
const webTabWindow = windowData.window;
const isWindowMode = windowData.mode === 'window';
const effectiveTabHeight = isWindowMode ? 0 : webTabHeight;
const item = id === 0 ? currentWebTabInWindow(windowId) : windowData.views.find(item => item.id == id);
if (!item) {
return;
}
item.view.setBounds({
x: 0,
y: effectiveTabHeight,
width: webTabWindow.getContentBounds().width || 1280,
height: (webTabWindow.getContentBounds().height || 800) - effectiveTabHeight,
});
}
/**
* 切换内置浏览器标签
* @param windowId
* @param id
*/
function activateWebTabInWindow(windowId, id) {
const windowData = webTabWindows.get(windowId);
if (!windowData || !windowData.window) return;
const webTabView = windowData.views;
const webTabWindow = windowData.window;
const item = id === 0 ? currentWebTabInWindow(windowId) : webTabView.find(item => item.id == id);
if (!item) {
return;
}
windowData.activeTabId = item.id;
webTabView.forEach(({id: vid, view}) => {
view.setVisible(vid === item.id);
});
resizeWebTabInWindow(windowId, item.id);
item.view.webContents.focus();
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'switch',
id: item.id,
}).then(_ => { });
}
/**
* 关闭内置浏览器标签
* @param windowId
* @param id
*/
function closeWebTabInWindow(windowId, id) {
const windowData = webTabWindows.get(windowId);
if (!windowData || !windowData.window) return;
const webTabView = windowData.views;
const webTabWindow = windowData.window;
const isWindowMode = windowData.mode === 'window';
const item = id === 0 ? currentWebTabInWindow(windowId) : webTabView.find(item => item.id == id);
if (!item) {
return;
}
// window 模式下直接关闭整个窗口
if (isWindowMode) {
webTabView.forEach(({name}) => {
if (name) webTabNameMap.delete(name);
});
webTabWindow.destroy();
return;
}
if (webTabView.length === 1) {
webTabWindow.hide();
}
webTabWindow.contentView.removeChildView(item.view);
// 清理 name 映射
if (item.name) {
webTabNameMap.delete(item.name);
}
try {
item.view.webContents.close();
} catch (e) {
//
}
const index = webTabView.findIndex(({id}) => item.id == id);
if (index > -1) {
webTabView.splice(index, 1);
}
utils.onDispatchEvent(webTabWindow.webContents, {
event: 'close',
id: item.id,
}).then(_ => { });
if (webTabView.length === 0) {
userConf.set('webTabWindow', webTabWindow.getBounds());
webTabWindow.destroy();
} else {
activateWebTabInWindow(windowId, 0);
}
}
/**
* 分离标签到新窗口
* @param windowId 源窗口ID
* @param tabId 标签ID
* @param screenX 屏幕X坐标
* @param screenY 屏幕Y坐标
* @returns {number|null} 新窗口ID
*/
function detachWebTab(windowId, tabId, screenX, screenY) {
const sourceWindowData = webTabWindows.get(windowId);
if (!sourceWindowData) return null;
const tabIndex = sourceWindowData.views.findIndex(v => v.id === tabId);
if (tabIndex === -1) return null;
const tabItem = sourceWindowData.views[tabIndex];
const view = tabItem.view;
const favicon = tabItem.favicon || '';
const tabName = tabItem.name || null;
const sourceWindow = sourceWindowData.window;
// 从源窗口移除视图
sourceWindow.contentView.removeChildView(view);
sourceWindowData.views.splice(tabIndex, 1);
// 通知源窗口标签已关闭
utils.onDispatchEvent(sourceWindow.webContents, {
event: 'close',
id: tabId,
}).then(_ => { });
// 创建新窗口,使用源窗口的尺寸
const sourceBounds = sourceWindow.getBounds();
const newWindowId = webTabWindowIdCounter++;
// 先创建窗口实例
const newWindow = createWebTabWindowInstance(newWindowId, {
width: sourceBounds.width,
height: sourceBounds.height,
});
// 计算窗口位置,让鼠标大致在标签区域
const navAreaWidth = isMac ? 280 : 200;
const targetX = Math.round(screenX - navAreaWidth);
const targetY = Math.round(screenY - Math.floor(webTabHeight / 2));
// 显示窗口前先设置位置
newWindow.setPosition(targetX, targetY);
const newWindowData = {
window: newWindow,
views: [{
id: tabId,
name: tabName,
view,
favicon
}],
activeTabId: tabId
};
webTabWindows.set(newWindowId, newWindowData);
// 更新视图所属窗口
view.webTabWindowId = newWindowId;
// 更新 name 映射中的 windowId
if (tabName) {
webTabNameMap.set(tabName, {
windowId: newWindowId,
tabId: tabId
});
}
// 添加视图到新窗口
newWindow.contentView.addChildView(view);
view.setBounds({
x: 0,
y: webTabHeight,
width: newWindow.getContentBounds().width || 1280,
height: (newWindow.getContentBounds().height || 800) - webTabHeight,
});
view.setVisible(true);
// 显示新窗口
newWindow.show();
// 再次确保位置正确
newWindow.setPosition(targetX, targetY);
newWindow.focus();
// 通知新窗口创建标签(传递完整状态信息)
newWindow.webContents.once('dom-ready', () => {
const isLoading = view.webContents.isLoading();
utils.onDispatchEvent(newWindow.webContents, {
event: 'create',
id: tabId,
url: view.webContents.getURL(),
windowId: newWindowId,
title: view.webContents.getTitle(),
state: isLoading ? 'loading' : 'loaded',
favicon,
}).then(_ => { });
utils.onDispatchEvent(newWindow.webContents, {
event: 'switch',
id: tabId,
}).then(_ => { });
});
// 处理源窗口
if (sourceWindowData.views.length === 0) {
// 源窗口没有标签了,关闭它
userConf.set('webTabWindow', sourceWindow.getBounds());
sourceWindow.destroy();
} else {
// 激活源窗口的下一个标签
activateWebTabInWindow(windowId, 0);
}
return newWindowId;
}
/**
* 将标签附加到目标窗口
* @param sourceWindowId 源窗口ID
* @param tabId 标签ID
* @param targetWindowId 目标窗口ID
* @param insertIndex 插入位置(可选)
* @returns {boolean} 是否成功
*/
function attachWebTab(sourceWindowId, tabId, targetWindowId, insertIndex) {
if (sourceWindowId === targetWindowId) return false;
const sourceWindowData = webTabWindows.get(sourceWindowId);
const targetWindowData = webTabWindows.get(targetWindowId);
if (!sourceWindowData || !targetWindowData) return false;
const tabIndex = sourceWindowData.views.findIndex(v => v.id === tabId);
if (tabIndex === -1) return false;
const tabItem = sourceWindowData.views[tabIndex];
const view = tabItem.view;
const favicon = tabItem.favicon || '';
const tabName = tabItem.name || null;
const sourceWindow = sourceWindowData.window;
const targetWindow = targetWindowData.window;
// 从源窗口移除视图
sourceWindow.contentView.removeChildView(view);
sourceWindowData.views.splice(tabIndex, 1);
// 通知源窗口标签已关闭
utils.onDispatchEvent(sourceWindow.webContents, {
event: 'close',
id: tabId,
}).then(_ => { });
// 更新视图所属窗口
view.webTabWindowId = targetWindowId;
// 更新 name 映射中的 windowId
if (tabName) {
webTabNameMap.set(tabName, {
windowId: targetWindowId,
tabId: tabId
});
}
// 确定插入位置
const actualInsertIndex = typeof insertIndex === 'number'
? Math.max(0, Math.min(insertIndex, targetWindowData.views.length))
: targetWindowData.views.length;
// 添加到目标窗口,保留 name 信息
targetWindowData.views.splice(actualInsertIndex, 0, {
id: tabId,
name: tabName,
view,
favicon
});
targetWindow.contentView.addChildView(view);
// 调整视图尺寸
view.setBounds({
x: 0,
y: webTabHeight,
width: targetWindow.getContentBounds().width || 1280,
height: (targetWindow.getContentBounds().height || 800) - webTabHeight,
});
// 通知目标窗口创建标签(传递完整状态信息)
const isLoading = view.webContents.isLoading();
utils.onDispatchEvent(targetWindow.webContents, {
event: 'create',
id: tabId,
url: view.webContents.getURL(),
windowId: targetWindowId,
title: view.webContents.getTitle(),
insertIndex: actualInsertIndex,
state: isLoading ? 'loading' : 'loaded',
favicon,
}).then(_ => { });
// 激活新添加的标签
activateWebTabInWindow(targetWindowId, tabId);
// 聚焦目标窗口
targetWindow.focus();
// 处理源窗口
if (sourceWindowData.views.length === 0) {
userConf.set('webTabWindow', sourceWindow.getBounds());
sourceWindow.destroy();
} else {
activateWebTabInWindow(sourceWindowId, 0);
}
return true;
}
/**
* 获取所有 webTab 窗口信息(用于跨窗口拖拽检测)
* @returns {Array}
*/
function getAllWebTabWindowsInfo() {
const result = [];
for (const [windowId, windowData] of webTabWindows) {
if (windowData.window && !windowData.window.isDestroyed()) {
const bounds = windowData.window.getBounds();
result.push({
windowId,
bounds,
tabBarBounds: {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: webTabHeight
},
tabCount: windowData.views.length
});
}
}
return result;
}
/**
* 监听主题变化
*/
function monitorThemeChanges() {
let currentTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
nativeTheme.on('updated', () => {
const newTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
if (currentTheme === newTheme) {
return
}
currentTheme = newTheme;
// 更新背景
const backgroundColor = utils.getDefaultBackgroundColor()
mainWindow?.setBackgroundColor(backgroundColor);
mediaWindow?.setBackgroundColor(backgroundColor);
// 更新所有 webTab 窗口背景
for (const [, windowData] of webTabWindows) {
windowData.window?.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#575757' : '#FFFFFF');
}
// 通知所有窗口
BrowserWindow.getAllWindows().forEach(window => {
window.webContents.send('systemThemeChanged', {
theme: currentTheme,
});
});
})
}
const getTheLock = app.requestSingleInstanceLock()
if (!getTheLock) {
app.quit()
} else {
app.on('second-instance', () => {
utils.setShowWindow(mainWindow)
})
app.on('ready', async () => {
isReady = true
isWin && app.setAppUserModelId(config.appId)
// 启动 Web 服务器
try {
await startWebServer()
} catch (error) {
dialog.showErrorBox('启动失败', `Web 服务器启动失败:${error.message}`);
app.quit();
return;
}
// SameSite
utils.useCookie()
// 创建主窗口
createMainWindow()
// 监听主题变化
monitorThemeChanges()
// 创建托盘
if (['darwin', 'win32'].includes(process.platform) && utils.isJson(config.trayIcon)) {
mainTray = new Tray(path.join(__dirname, config.trayIcon[isDevelopMode ? 'dev' : 'prod'][process.platform === 'darwin' ? 'mac' : 'win']));
mainTray.on('click', () => {
utils.setShowWindow(mainWindow)
})
mainTray.setToolTip(config.name)
if (process.platform === 'win32') {
const trayMenu = Menu.buildFromTemplate([{
label: '显示',
click: () => {
utils.setShowWindow(mainWindow)
}
}, {
label: '退出',
click: () => {
app.quit()
}
}])
mainTray.setContextMenu(trayMenu)
}
}
// 删除updater锁文件如果存在
if (fs.existsSync(updaterLockFile)) {
try {
fs.unlinkSync(updaterLockFile);
} catch (e) {
//忽略错误
}
}
// 截图对象
screenshotObj = new Screenshots({
singleWindow: true,
mainWindow: mainWindow
})
})
}
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
if (isReady) {
createMainWindow()
}
} else if (mainWindow) {
if (!mainWindow.isVisible()) {
mainWindow.show()
}
}
})
app.on('window-all-closed', () => {
if (willQuitApp || process.platform !== 'darwin') {
app.quit()
}
})
app.on('before-quit', () => {
willQuitApp = true
})
app.on("will-quit", () => {
globalShortcut.unregisterAll();
})
/**
* 设置菜单语言包
* @param args {path}
*/
ipcMain.on('setMenuLanguage', (event, args) => {
if (utils.isJson(args)) {
electronMenu.setLanguage(args)
}
event.returnValue = "ok"
})
/**
* 打开文件
* @param args {path}
*/
ipcMain.on('openFile', (event, args) => {
utils.openFile(args.path)
event.returnValue = "ok"
})
/**
* 退出客户端
*/
ipcMain.on('windowQuit', (event) => {
event.returnValue = "ok"
app.quit();
})
/**
* 获取路由窗口信息(从 webTabWindows 中查找 mode='window' 的窗口)
*/
ipcMain.handle('getChildWindow', (event, args) => {
let windowData, viewItem;
if (!args) {
// 通过发送者查找
const sender = event.sender;
for (const [, data] of webTabWindows) {
if (data.mode === 'window') {
const found = data.views.find(v => v.view.webContents === sender);
if (found) {
windowData = data;
viewItem = found;
break;
}
}
}
} else {
// 通过名称查找
const location = webTabNameMap.get(args);
if (location) {
windowData = webTabWindows.get(location.windowId);
if (windowData && windowData.mode === 'window') {
viewItem = windowData.views.find(v => v.id === location.tabId);
}
}
}
if (windowData && viewItem) {
return {
name: viewItem.name,
id: viewItem.view.webContents.id,
url: viewItem.view.webContents.getURL()
};
}
return null;
});
/**
* 打开媒体浏览器
*/
ipcMain.on('openMediaViewer', (event, args) => {
createMediaWindow(args, ['image', 'video'].includes(args.type) ? args.type : 'image');
event.returnValue = "ok"
});
/**
* 统一窗口打开接口
* @param args {url, name, mode, force, config, userAgent, webPreferences, ...}
* - url: 要打开的地址
* - name: 窗口/标签名称
* - mode: 'tab' | 'window'
* - 'window': 独立窗口模式(无导航栏)
* - 'tab': 标签页模式(默认,有导航栏)
*/
ipcMain.on('openWindow', (event, args) => {
// 统一使用 createWebTabWindow通过 mode 区分窗口类型
createWebTabWindow(args)
event.returnValue = "ok"
})
/**
* 内置浏览器 - 激活标签
* @param args {windowId, tabId} 或 tabId
*/
ipcMain.on('webTabActivate', (event, args) => {
let windowId, tabId;
if (typeof args === 'object' && args !== null) {
windowId = args.windowId;
tabId = args.tabId;
} else {
tabId = args;
windowId = findWindowIdByTabId(tabId);
}
if (windowId) {
activateWebTabInWindow(windowId, tabId);
}
event.returnValue = "ok"
})
/**
* 内置浏览器 - 重排标签顺序
* @param args {windowId, newOrder} 或 newOrder数组
*/
ipcMain.on('webTabReorder', (event, args) => {
let windowId, newOrder;
if (Array.isArray(args)) {
newOrder = args;
// 找到包含这些标签的窗口
if (newOrder.length > 0) {
windowId = findWindowIdByTabId(newOrder[0]);
}
} else if (typeof args === 'object' && args !== null) {
windowId = args.windowId;
newOrder = args.newOrder;
}
if (!windowId || !Array.isArray(newOrder) || newOrder.length === 0) {
event.returnValue = "ok"
return
}
const windowData = webTabWindows.get(windowId);
if (!windowData) {
event.returnValue = "ok"
return
}
// 根据新顺序重排 views 数组
windowData.views.sort((a, b) => {
const indexA = newOrder.indexOf(a.id)
const indexB = newOrder.indexOf(b.id)
return indexA - indexB
})
event.returnValue = "ok"
})
/**
* 内置浏览器 - 关闭标签
* @param args {windowId, tabId} 或 tabId
*/
ipcMain.on('webTabClose', (event, args) => {
let windowId, tabId;
if (typeof args === 'object' && args !== null) {
windowId = args.windowId;
tabId = args.tabId;
} else {
tabId = args;
windowId = findWindowIdByTabId(tabId);
}
if (windowId) {
closeWebTabInWindow(windowId, tabId);
}
event.returnValue = "ok"
})
/**
* 内置浏览器 - 在外部浏览器打开
*/
ipcMain.on('webTabExternal', (event) => {
const item = currentWebTab()
if (!item) {
return
}
renderer.openExternal(item.view.webContents.getURL()).catch(() => {})
event.returnValue = "ok"
})
/**
* 内置浏览器 - 显示更多菜单
*/
ipcMain.on('webTabShowMenu', (event, args) => {
const windowId = args?.windowId
const tabId = args?.tabId
const windowData = windowId ? webTabWindows.get(windowId) : null
const webTabWindow = windowData?.window
if (!webTabWindow || webTabWindow.isDestroyed()) {
event.returnValue = "ok"
return
}
const item = currentWebTabInWindow(windowId)
const webContents = item?.view?.webContents
const currentUrl = webContents?.getURL() || ''
const canBrowser = !utils.isLocalHost(currentUrl)
const menuTemplate = [
{
label: electronMenu.language.reload,
click: () => {
if (webContents && !webContents.isDestroyed()) {
webContents.reload()
}
}
},
{
label: electronMenu.language.copyLinkAddress,
enabled: canBrowser,
click: () => {
if (currentUrl) {
clipboard.writeText(currentUrl)
}
}
},
{
label: electronMenu.language.openInDefaultBrowser,
enabled: canBrowser,
click: () => {
if (currentUrl) {
renderer.openExternal(currentUrl).catch(() => {})
}
}
},
{ type: 'separator' },
{
label: electronMenu.language.moveToNewWindow,
enabled: windowData?.views?.length > 1,
click: () => {
if (tabId) {
const bounds = webTabWindow.getBounds()
detachWebTab(windowId, tabId, bounds.x + 50, bounds.y + 50)
}
}
},
{ type: 'separator' },
{
label: electronMenu.language.print,
click: () => {
if (webContents && !webContents.isDestroyed()) {
webContents.print()
}
}
}
]
const menu = Menu.buildFromTemplate(menuTemplate)
menu.popup({
window: webTabWindow,
x: args?.x,
y: args?.y
})
event.returnValue = "ok"
})
/**
* 内置浏览器 - 打开开发者工具
*/
ipcMain.on('webTabOpenDevTools', (event) => {
const item = currentWebTab()
if (!item) {
return
}
item.view.webContents.openDevTools()
event.returnValue = "ok"
})
/**
* 内置浏览器 - 销毁所有标签及窗口
*/
ipcMain.on('webTabDestroyAll', (event) => {
for (const [, windowData] of webTabWindows) {
if (windowData.window && !windowData.window.isDestroyed()) {
windowData.window.destroy();
}
}
event.returnValue = "ok"
})
/**
* 内置浏览器 - 后退
* @param args {windowId} 可选
*/
ipcMain.on('webTabGoBack', (event, args) => {
const windowId = args?.windowId || getWindowIdFromSender(event.sender);
const item = windowId ? currentWebTabInWindow(windowId) : currentWebTab();
if (!item) {
event.returnValue = "ok"
return
}
if (item.view.webContents.canGoBack()) {
item.view.webContents.goBack()
// 导航后更新状态
setTimeout(() => {
const wd = webTabWindows.get(item.view.webTabWindowId);
if (wd && wd.window) {
utils.onDispatchEvent(wd.window.webContents, {
event: 'navigation-state',
id: item.id,
canGoBack: item.view.webContents.canGoBack(),
canGoForward: item.view.webContents.canGoForward()
}).then(_ => { })
}
}, 100)
}
event.returnValue = "ok"
})
/**
* 内置浏览器 - 前进
* @param args {windowId} 可选
*/
ipcMain.on('webTabGoForward', (event, args) => {
const windowId = args?.windowId || getWindowIdFromSender(event.sender);
const item = windowId ? currentWebTabInWindow(windowId) : currentWebTab();
if (!item) {
event.returnValue = "ok"
return
}
if (item.view.webContents.canGoForward()) {
item.view.webContents.goForward()
// 导航后更新状态
setTimeout(() => {
const wd = webTabWindows.get(item.view.webTabWindowId);
if (wd && wd.window) {
utils.onDispatchEvent(wd.window.webContents, {
event: 'navigation-state',
id: item.id,
canGoBack: item.view.webContents.canGoBack(),
canGoForward: item.view.webContents.canGoForward()
}).then(_ => { })
}
}, 100)
}
event.returnValue = "ok"
})
/**
* 内置浏览器 - 刷新
* @param args {windowId} 可选
*/
ipcMain.on('webTabReload', (event, args) => {
const windowId = args?.windowId || getWindowIdFromSender(event.sender);
const item = windowId ? currentWebTabInWindow(windowId) : currentWebTab();
if (!item) {
event.returnValue = "ok"
return
}
item.view.webContents.reload()
// 刷新完成后会触发 did-stop-loading 事件,在那里会更新导航状态
event.returnValue = "ok"
})
/**
* 内置浏览器 - 停止加载
* @param args {windowId} 可选
*/
ipcMain.on('webTabStop', (event, args) => {
const windowId = args?.windowId || getWindowIdFromSender(event.sender);
const item = windowId ? currentWebTabInWindow(windowId) : currentWebTab();
if (!item) {
event.returnValue = "ok"
return
}
item.view.webContents.stop()
event.returnValue = "ok"
})
/**
* 内置浏览器 - 获取导航状态
* @param args {windowId} 可选
*/
ipcMain.on('webTabGetNavigationState', (event, args) => {
const windowId = args?.windowId || getWindowIdFromSender(event.sender);
const item = windowId ? currentWebTabInWindow(windowId) : currentWebTab();
if (!item) {
event.returnValue = "ok"
return
}
const canGoBack = item.view.webContents.canGoBack()
const canGoForward = item.view.webContents.canGoForward()
const wd = webTabWindows.get(item.view.webTabWindowId);
if (wd && wd.window) {
utils.onDispatchEvent(wd.window.webContents, {
event: 'navigation-state',
id: item.id,
canGoBack,
canGoForward
}).then(_ => { })
}
event.returnValue = "ok"
})
/**
* 从事件发送者获取窗口ID
* @param sender
* @returns {number|null}
*/
function getWindowIdFromSender(sender) {
const win = BrowserWindow.fromWebContents(sender);
if (win && win.webTabWindowId) {
return win.webTabWindowId;
}
return null;
}
/**
* 内置浏览器 - 分离标签到新窗口
* @param args {windowId, tabId, screenX, screenY}
*/
ipcMain.on('webTabDetach', (event, args) => {
const {windowId, tabId, screenX, screenY} = args;
detachWebTab(windowId, tabId, screenX, screenY);
event.returnValue = "ok"
})
/**
* 内置浏览器 - 将标签附加到目标窗口
* @param args {sourceWindowId, tabId, targetWindowId, insertIndex}
*/
ipcMain.on('webTabAttach', (event, args) => {
const {sourceWindowId, tabId, targetWindowId, insertIndex} = args;
attachWebTab(sourceWindowId, tabId, targetWindowId, insertIndex);
event.returnValue = "ok"
})
/**
* 内置浏览器 - 获取所有窗口信息(用于跨窗口拖拽检测)
*/
ipcMain.handle('webTabGetAllWindows', () => {
return getAllWebTabWindowsInfo();
})
/**
* 隐藏窗口mac、win隐藏其他关闭
*/
ipcMain.on('windowHidden', (event) => {
if (['darwin', 'win32'].includes(process.platform)) {
app.hide();
} else {
app.quit();
}
event.returnValue = "ok"
})
/**
* 关闭窗口(或关闭 tab如果发送者是 tab 中的页面)
*/
ipcMain.on('windowClose', (event) => {
const tabId = event.sender.id;
const windowId = findWindowIdByTabId(tabId);
if (windowId !== null) {
// 发送者是 tab 中的页面,只关闭这个 tab
closeWebTabInWindow(windowId, tabId);
} else {
// 发送者是独立窗口,关闭整个窗口
const win = BrowserWindow.fromWebContents(event.sender);
win?.close()
}
event.returnValue = "ok"
})
/**
* 销毁窗口(或销毁 tab如果发送者是 tab 中的页面)
*/
ipcMain.on('windowDestroy', (event) => {
const tabId = event.sender.id;
const windowId = findWindowIdByTabId(tabId);
if (windowId !== null) {
// 发送者是 tab 中的页面,只关闭这个 tab
closeWebTabInWindow(windowId, tabId);
} else {
// 发送者是独立窗口,销毁整个窗口
const win = BrowserWindow.fromWebContents(event.sender);
win?.destroy()
}
event.returnValue = "ok"
})
/**
* 关闭所有子窗口mode='window' 的窗口)
*/
ipcMain.on('childWindowCloseAll', (event) => {
for (const [, data] of webTabWindows) {
if (data.mode === 'window' && data.window && !data.window.isDestroyed()) {
data.window.close();
}
}
mediaWindow?.close()
electronDown.close()
event.returnValue = "ok"
})
/**
* 销毁所有子窗口mode='window' 的窗口)
*/
ipcMain.on('childWindowDestroyAll', (event) => {
for (const [, data] of webTabWindows) {
if (data.mode === 'window' && data.window && !data.window.isDestroyed()) {
data.window.destroy();
}
}
mediaWindow?.destroy()
electronDown.destroy()
event.returnValue = "ok"
})
/**
* 设置窗口尺寸
* @param args {width, height, autoZoom, minWidth, minHeight, maxWidth, maxHeight}
*/
ipcMain.on('windowSize', (event, args) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
if (args.width || args.height) {
let [w, h] = win.getSize()
const width = args.width || w
const height = args.height || h
win.setSize(width, height, args.animate === true)
//
if (args.autoZoom === true) {
let move = false
let [x, y] = win.getPosition()
if (Math.abs(width - w) > 10) {
move = true
x -= (width - w) / 2
}
if (Math.abs(height - h) > 10) {
move = true
y -= (height - h) / 2
}
if (move) {
win.setPosition(Math.max(0, Math.floor(x)), Math.max(0, Math.floor(y)))
}
}
}
if (args.minWidth || args.minHeight) {
win.setMinimumSize(args.minWidth || win.getMinimumSize()[0], args.minHeight || win.getMinimumSize()[1])
}
if (args.maxWidth || args.maxHeight) {
win.setMaximumSize(args.maxWidth || win.getMaximumSize()[0], args.maxHeight || win.getMaximumSize()[1])
}
}
event.returnValue = "ok"
})
/**
* 设置窗口最小尺寸
* @param args {minWidth, minHeight}
*/
ipcMain.on('windowMinSize', (event, args) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.setMinimumSize(args.minWidth || win.getMinimumSize()[0], args.minHeight || win.getMinimumSize()[1])
}
event.returnValue = "ok"
})
/**
* 设置窗口最大尺寸
* @param args {maxWidth, maxHeight}
*/
ipcMain.on('windowMaxSize', (event, args) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.setMaximumSize(args.maxWidth || win.getMaximumSize()[0], args.maxHeight || win.getMaximumSize()[1])
}
event.returnValue = "ok"
})
/**
* 窗口居中
*/
ipcMain.on('windowCenter', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.center();
}
event.returnValue = "ok"
})
/**
* 窗口最大化或恢复
*/
ipcMain.on('windowMax', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win.isMaximized()) {
win.restore();
} else {
win.maximize();
}
event.returnValue = "ok"
})
/**
* 给所有窗口广播指令(除了本身)
* @param args {type, payload}
*/
ipcMain.on('broadcastCommand', (event, args) => {
const channel = args.channel || args.command
const payload = args.payload || args.data
BrowserWindow.getAllWindows().forEach(window => {
if (window.webContents.id !== event.sender.id) {
window.webContents.send(channel, payload)
}
})
event.returnValue = "ok"
})
/**
* 设置Dock标记window闪烁、macos标记
* @param args
*/
ipcMain.on('setDockBadge', (event, args) => {
if (process.platform === 'win32') {
// Window flash
if (!mainWindow.isFocused()) {
mainWindow.once('focus', () => mainWindow.flashFrame(false))
mainWindow.flashFrame(true)
}
return;
}
if (process.platform !== 'darwin') {
// Mac only
return;
}
let num = args;
let tray = true;
if (utils.isJson(args)) {
num = args.num
tray = !!args.tray
}
let text = typeof num === "string" ? num : (utils.runNum(num) > 0 ? String(num) : "")
app.dock.setBadge(text)
if (tray && mainTray) {
mainTray.setTitle(text)
}
event.returnValue = "ok"
})
/**
* MCP 服务器状态切换
* @param args
*/
ipcMain.on('mcpServerToggle', (event, args) => {
const { running } = args;
if (running === 'running') {
startMCPServer(mainWindow, mcpPort)
} else {
stopMCPServer()
}
})
/**
* 复制Base64图片
* @param args
*/
ipcMain.on('copyBase64Image', (event, args) => {
const { base64 } = args;
if (base64) {
const img = nativeImage.createFromDataURL(base64)
clipboard.writeImage(img)
}
event.returnValue = "ok"
})
/**
* 复制图片根据坐标
* @param args
*/
ipcMain.on('copyImageAt', (event, args) => {
try {
event.sender.copyImageAt(args.x, args.y);
} catch (e) {
loger.error('copyImageAt error:', e)
}
event.returnValue = "ok"
})
/**
* 保存图片
* @param args
*/
ipcMain.on('saveImageAt', async (event, args) => {
await electronMenu.saveImageAs(args.url, args.params)
event.returnValue = "ok"
})
/**
* 绑定截图快捷键
* @param args
*/
ipcMain.on('bindScreenshotKey', (event, args) => {
const { key } = args;
if (screenshotKey !== key) {
if (screenshotKey) {
globalShortcut.unregister(screenshotKey)
screenshotKey = null
}
if (key) {
screenshotKey = key
globalShortcut.register(key, () => {
screenshotObj.startCapture().then(_ => {
screenshotObj.view.webContents.executeJavaScript(`if(typeof window.__initializeShortcuts==='undefined'){window.__initializeShortcuts=true;document.addEventListener('keydown',function(e){console.log(e);if(e.keyCode===27){window.screenshots.cancel()}})}`, true).catch(() => {});
screenshotObj.view.webContents.focus()
})
})
}
}
event.returnValue = "ok"
})
/**
* 执行截图
*/
ipcMain.on('openScreenshot', (event) => {
if (screenshotObj) {
screenshotObj.startCapture().then(_ => {})
}
event.returnValue = "ok"
})
/**
* 关闭截图
*/
ipcMain.on('closeScreenshot', (event) => {
if (screenshotObj && screenshotObj.window?.isFocused()) {
screenshotObj.endCapture().then(_ => {});
}
event.returnValue = "ok"
})
/**
* 通知
*/
ipcMain.on('openNotification', (event, args) => {
utils.showNotification(args, mainWindow)
event.returnValue = "ok"
})
/**
* 保存缓存
*/
ipcMain.on('setStore', (event, args) => {
if (utils.isJson(args)) {
store.set(args.key, args.value)
}
event.returnValue = "ok"
})
/**
* 获取缓存
*/
ipcMain.handle('getStore', (event, args) => {
return store.get(args)
});
/**
* 清理服务器缓存
*/
ipcMain.on('clearServerCache', (event) => {
utils.clearServerCache();
event.returnValue = "ok";
});
//================================================================
// Update
//================================================================
let autoUpdating = 0
if (autoUpdater) {
autoUpdater.logger = loger
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.on('update-available', info => {
mainWindow.webContents.send("updateAvailable", info)
})
autoUpdater.on('update-downloaded', info => {
mainWindow.webContents.send("updateDownloaded", info)
})
}
/**
* 检查更新
*/
ipcMain.on('updateCheckAndDownload', (event, args) => {
event.returnValue = "ok"
if (autoUpdating + 3600 > utils.dayjs().unix()) {
return // 限制1小时仅执行一次
}
if (!autoUpdater) {
return
}
if (args.provider) {
autoUpdater.setFeedURL(args)
}
autoUpdater.checkForUpdates().then(info => {
if (!info) {
return
}
if (utils.compareVersion(config.version, info.updateInfo.version) >= 0) {
return
}
if (args.apiVersion) {
if (utils.compareVersion(info.updateInfo.version, args.apiVersion) <= 0) {
// 客户端版本 <= 接口版本
autoUpdating = utils.dayjs().unix()
autoUpdater.downloadUpdate().then(_ => {}).catch(_ => {})
}
} else {
autoUpdating = utils.dayjs().unix()
autoUpdater.downloadUpdate().then(_ => {}).catch(_ => {})
}
})
})
/**
* 将主窗口激活到顶层
*/
ipcMain.on('mainWindowTop', (event) => {
mainWindow.moveTop()
event.returnValue = "ok"
})
/**
* 将主窗口激活
*/
ipcMain.on('mainWindowActive', (event) => {
if (!mainWindow.isVisible()) {
mainWindow.show()
}
mainWindow.focus()
event.returnValue = "ok"
})
/**
* 退出并安装更新
*/
ipcMain.on('updateQuitAndInstall', (event, args) => {
if (!utils.isJson(args)) {
args = {}
}
event.returnValue = "ok"
// 关闭所有子窗口
willQuitApp = true
for (const [, data] of webTabWindows) {
if (data.mode === 'window' && data.window && !data.window.isDestroyed()) {
data.window.destroy();
}
}
mediaWindow?.destroy()
electronDown.destroy()
// 启动更新子窗口
createUpdaterWindow(args.updateTitle)
// 退出并安装更新
setTimeout(_ => {
mainWindow.hide()
autoUpdater?.quitAndInstall(true, true)
}, 600)
})
//================================================================
//================================================================
//================================================================
onExport()
onRenderer(mainWindow)