import { BrowserWindow, screen, Menu, shell, session, app, globalShortcut, dialog ,systemPreferences } from 'electron' import { join } from 'path' import { is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' import { isApiDatasetsDocumentsPath, extractIdFromPath } from './utils/difyUtils.js' import XEUtils from 'xe-utils' import logger from './utils/logger' import { checkDefaultClientConnection } from './utils/networkUtils.js' import { createMenu } from './menu.js' import { setStoreValue, getStoreValue,deleteStore } from './store.js' import {checkForUpdates} from "./utils/updateUtils" import dayjs from 'dayjs' let mainWindow = null let difyfullScreenWindow = null let drageWindow = null let apiConfigWindow = null let configWindow = null import { clearAllSessionData } from './utils/cacheUtils.js' // 权限授权 async function checkMediaAccess(mediaType){ const result = systemPreferences.getMediaAccessStatus(mediaType) logger.info(`=====================systemPreferences.getMediaAccessStatus:${mediaType} result:${result}`) if(result !== "granted"){ await systemPreferences.askForMediaAccess(mediaType) } } async function checkAndApplyDeviceAccessPrivilege() { if (process.platform === "darwin" || process.platform === 'win32') { // 检查并申请摄像头权限 const cameraPrivilege = systemPreferences.getMediaAccessStatus("camera"); if (cameraPrivilege !== "granted") { await systemPreferences.askForMediaAccess("camera"); } // 检查并申请麦克风权限 const micPrivilege = systemPreferences.getMediaAccessStatus("microphone"); if (micPrivilege !== "granted") { await systemPreferences.askForMediaAccess("microphone"); } // 检查访问屏幕权限 const screenPrivilege = systemPreferences.getMediaAccessStatus("screen"); if (screenPrivilege !== 'granted') { // 没有屏幕访问权限,做后续处理... } } } export async function createWindow() { // 如果窗口已经存在,直接显示并返回 if (mainWindow) { mainWindow.show() return } logger.info(`启动主窗口`) Menu.setApplicationMenu(null) // Create the browser window. mainWindow = new BrowserWindow({ width: 1480, height: 980, minWidth: 1480, minHeight: 980, show: false, media: { audio: true, video: true }, icon: icon, webPreferences: { contextIsolation: false, nodeIntegration: true, nodeIntegrationInWorker: true, preload: join(__dirname, '../preload/index.js'), // sandbox: false } }) // 创建菜单 createMenu(mainWindow, difyfullScreenWindow) let code = `localStorage.setItem("IsHsAiApp","IsHsAiApp");localStorage.setItem("HsAppCode",${import.meta.env.VITE_HsAppCode});` const isAdmin = getStoreValue("isAdmin") let display=''; await checkAndApplyDeviceAccessPrivilege() mainWindow.webContents.on('did-finish-load', () => { mainWindow.webContents .executeJavaScript(code) .then((result) => { }) .catch((error) => { console.error('Error:', error) }) }) // 监听快捷键 F12 来打开/关闭 DevTools mainWindow.webContents.on('before-input-event', (event, input) => { if (input.key === 'F12') { mainWindow.webContents.toggleDevTools() } else if (input.key === 'F5') { logger.info('主窗口 F5 快捷键触发') mainWindow.reload() } }) mainWindow.on('resize',async () => { // logger.info('窗口大小发生变化...'); // 触发窗口内容重新布局,而不是重新加载 mainWindow.webContents.executeJavaScript(` if (document.body) { // 触发窗口重排 window.dispatchEvent(new Event('resize')); } `).catch(err => { console.error('执行布局调整时出错:', err); }); }); mainWindow.on('ready-to-show', () => { mainWindow.show() }) mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } }) mainWindow.on('close', (event) => { if (!app.isQuiting) { event.preventDefault() mainWindow.hide() return false } }) // setStoreValue("h5_client_url", "") // 检查本地存储中是否已有 h5_client_url const existingH5ClientUrl = getStoreValue("h5_client_url") if (!existingH5ClientUrl || existingH5ClientUrl.trim() === '') { // 只有当值不存在或为空时,才设置默认值 // setStoreValue("h5_client_url", "http://work.ii999.live:20040") // setStoreValue("h5_client_url", "http://10.102.8.56:18900") // setStoreValue("h5_client_url", "http://68.66.24.160:18900") setStoreValue("h5_client_url", import.meta.env.VITE_h5_client_url) } const h5_client_url=getStoreValue("h5_client_url")+"/pc_client/" logger.info("==================================== mainWindow.loadURL:"+h5_client_url) // 加载存储的 URL mainWindow.loadURL(h5_client_url) // 监听页面加载失败事件 let failLoadCount = 0; const maxRetries = 3; const retryDelay = 2000; // 2秒后重试 mainWindow.webContents.on('did-fail-load', async (event, errorCode, errorDescription, validatedURL) => { logger.error(`主窗口页面加载失败: code=${errorCode}, desc=${errorDescription}, url=${validatedURL}, 失败次数: ${failLoadCount + 1}`) // 增加失败计数 failLoadCount++; if (failLoadCount <= maxRetries) { logger.info(`页面加载失败,${retryDelay}ms后进行第${failLoadCount}次重试...`); setTimeout(async () => { try { const { checkNetworkConnection } = await import('./utils/networkUtils.js'); const isApiOk = await checkNetworkConnection(); if (isApiOk) { logger.info('接口正常,重新加载页面'); mainWindow.reload(); } else { logger.warn('接口异常,跳转到网络错误页面'); jumpToNetworkErrorPage(); } } catch (error) { logger.error('重试过程中出错:', error); jumpToNetworkErrorPage(); } }, retryDelay); } else { // 超过最大重试次数,跳转到网络错误页面 logger.error(`页面加载失败次数超过${maxRetries}次,跳转到网络错误页面`); jumpToNetworkErrorPage(); } }); // 页面加载成功时重置失败计数 mainWindow.webContents.on('did-finish-load', () => { if (failLoadCount > 0) { logger.info('页面加载成功,重置失败计数'); failLoadCount = 0; } }); // 跳转到网络错误页面的函数 function jumpToNetworkErrorPage() { if (is.dev && process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/#/network_error') } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html'), { hash: '/network_error' }) } } // 接口超过30分钟不活动,则退出登录 await tokenExpireTimer() setTimeout(async ()=>{ try { // 注册全局快捷键 registerShortcuts(mainWindow) // 先检测网络连接,如果网络正常再调用版本检测升级 const isNetworkConnected = await checkDefaultClientConnection(5000); if (isNetworkConnected) { logger.info('网络连接正常,开始检查版本更新'); checkForUpdates({},false) } else { logger.warn('网络连接异常,跳过版本检测升级'); } }catch (e) { logger.info(e) } },1500) session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => { if (permission === 'microphone') { // 用户是否允许使用麦克风 callback(true); // 或者根据实际情况返回false } else { // 对于其他权限,拒绝或根据实际情况处理 callback(false); } }); } // 添加一个专门的快捷键注册函数 function registerShortcuts(window=null) { // 先注销可能存在的F5快捷键 globalShortcut.unregister('F5') // 注册 F5 刷新快捷键 const success = globalShortcut.register('F5', () => { logger.info('F5 快捷键触发') try { // 获取当前焦点窗口 const focusedWindow = BrowserWindow.getFocusedWindow() logger.info('当前焦点窗口:', focusedWindow ? '存在' : '不存在') if (focusedWindow && !focusedWindow.isDestroyed()) { logger.info('刷新当前焦点窗口') focusedWindow.reload() } else if (mainWindow && !mainWindow.isDestroyed()) { logger.info('没有焦点窗口,刷新主窗口') mainWindow.reload() } else if (difyfullScreenWindow && !difyfullScreenWindow.isDestroyed()) { logger.info('主窗口不可用,刷新全屏窗口') difyfullScreenWindow.reload() } else { logger.warn('没有可用的窗口进行刷新') } } catch (error) { logger.error('F5快捷键执行出错:', error) } }) logger.info(`F5快捷键注册${success ? '成功' : '失败'}`) const isRegistered_F12 = globalShortcut.isRegistered('F12'); logger.info(`Is F12 registered: ${isRegistered_F12}`); // 桌面端快要退出的时候,注销快捷键 app.on('will-quit', () => { globalShortcut.unregisterAll() // 注销所有快捷键 }) } export async function createNewWindow(url, access_token, refresh_token,sandbox=false) { const origin_url = url logger.info('==========createNewWindow') logger.info('==========createNewWindow sandbox',sandbox) const csrf_token=getStoreValue("dify_csrf_token") await checkAndApplyDeviceAccessPrivilege() difyfullScreenWindow = new BrowserWindow({ width: 1920, height: 1200, show: false, icon: icon, webPreferences: { contextIsolation: false, nodeIntegration: true, nodeIntegrationInWorker: true, preload: join(__dirname, '../preload/index.js'), sandbox: sandbox, webSecurity: false, // 注意:在生产环境中应谨慎使用 enableRemoteModule: true, // Electron 10+ 默认禁用,需要显式启用 } }) difyfullScreenWindow.on('ready-to-show', () => { difyfullScreenWindow.show() // 确保快捷键在新窗口显示后也能工作 registerShortcuts(difyfullScreenWindow) }) const hs_version = getStoreValue("hs_version") const cookieStatements = [ `document.cookie = "access_token=${access_token}";`, `document.cookie = "refresh_token=${refresh_token}";`, `document.cookie = "csrf_token=${csrf_token}";`, `document.cookie = "hs_version=${hs_version}";` ].join('\n'); const localStorageStatements = [ `localStorage.setItem("IsHsAiApp", "IsHsAiApp");`, `localStorage.setItem("console_token", "${access_token}");`, `localStorage.setItem("refresh_token", "12");` ].join('\n'); let code = `${localStorageStatements}\n${cookieStatements};`; const isAdmin = getStoreValue("isAdmin") let display=''; if (isAdmin==true){ display = ` (function() { const stickyDivs = document.querySelectorAll('.sticky.top-0.left-0.right-0.basis-auto.shrink-0'); stickyDivs.forEach(div => { // div.style.display = 'none'; }); })(); ` }else { display = ` (function() { const stickyDivs = document.querySelectorAll('.sticky.top-0.left-0.right-0.basis-auto.shrink-0'); stickyDivs.forEach(div => { div.style.display = 'none'; }); })(); ` } code=code+`;document.cookie="hs_version=${hs_version}";` difyfullScreenWindow.webContents.on('did-finish-load', () => { difyfullScreenWindow.webContents .executeJavaScript(code + display) .then((result) => { }) .catch((error) => { console.error('Error:', error) }) let times = 0 let timer = setInterval(() => { if (times > 3) { clearInterval(timer) timer = null times = 0 return } if (difyfullScreenWindow != null) { difyfullScreenWindow.webContents .executeJavaScript(code + display) .then((result) => { times = times + 1 }) .catch((error) => { console.error('Error:', error) }) } }, 2000) }) // 监听快捷键 F12 来打开/关闭 DevTools difyfullScreenWindow.webContents.on('before-input-event', (event, input) => { if (input.key === 'F12') { difyfullScreenWindow.webContents.toggleDevTools() } else if (input.key === 'F5') { logger.info('全屏窗口 F5 快捷键触发') difyfullScreenWindow.reload() } }) difyfullScreenWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } }) difyfullScreenWindow.on('closed', () => { logger.info('主窗口已关闭') difyfullScreenWindow = null }) difyfullScreenWindow.webContents.on('did-navigate', (event, url) => { logger.info('navigate to new URL:', url) }) session.defaultSession.webRequest.onHeadersReceived((details, callback) => { callback({ responseHeaders: { ...details.responseHeaders, 'Access-Control-Allow-Origin': ['*'], 'Access-Control-Allow-Methods': ['GET, POST, PUT, DELETE, OPTIONS'], 'Access-Control-Allow-Headers': ['Content-Type, Authorization'] } }) }) let times = 0 difyfullScreenWindow.webContents.on('did-navigate-in-page', (event, url) => { logger.info('navigate URL change:', url) logger.info(`===============current:${access_token}`) if (url.includes('/signin')) { logger.info("token 注入失效,重新注入并且加载页面") if (times>3){ difyfullScreenWindow.close() difyfullScreenWindow=null times=0; return } let _display = ` (function() { const stickyDivs = document.querySelectorAll('body'); stickyDivs.forEach(div => { div.style.display = 'none'; }); })(); ` difyfullScreenWindow.webContents .executeJavaScript(code + _display) .then((result) => { logger.info('load url:', origin_url) setTimeout(()=>{ times=times+1; difyfullScreenWindow.loadURL(origin_url) },1000) }) .catch((error) => { console.error('Error:', error) }) } }) let dify_site = 'http://df.1024web.cn' const filter = { urls: [`${dify_site}/*`] } session.defaultSession.webRequest.onBeforeRequest(filter, (details, callback) => { if (isApiDatasetsDocumentsPath(details.url)) { logger.info('========== onBeforeRequest url:', details.url) logger.info('============datasetId :', extractIdFromPath(details.url)) logger.info(details) const url = new URL(details.url) const pathAndQuery = url.pathname + url.search const redirectURL = `http://192.168.50.22:8001/items/` logger.info(`[basehttp:setProxy] redirectURL = ${redirectURL}`) callback({ redirectURL }) } else { callback({ cancel: false }) } }) difyfullScreenWindow.on('resize', () => { logger.info('窗口大小发生变化,重新加载页面...'); if (difyfullScreenWindow!=null){ difyfullScreenWindow.webContents.executeJavaScript(` if (document.body) { // 触发窗口重排 window.dispatchEvent(new Event('resize')); } `).catch(err => { console.error('执行布局调整时出错:', err); }); } // 触发窗口内容重新布局,而不是重新加载 }); difyfullScreenWindow.loadURL(url) // 添加重试机制的错误处理 let failLoadCount = 0; const maxRetries = 3; const retryDelay = 2000; difyfullScreenWindow.webContents.on('did-fail-load', async (event, errorCode, errorDescription, validatedURL) => { logger.error(`全屏窗口页面加载失败: code=${errorCode}, desc=${errorDescription}, url=${validatedURL}, 失败次数: ${failLoadCount + 1}`) failLoadCount++; if (failLoadCount <= maxRetries) { logger.info(`全屏窗口页面加载失败,${retryDelay}ms后进行第${failLoadCount}次重试...`); setTimeout(async () => { try { // 在重试前先检查网络连接 const { checkDefaultClientConnection } = await import('./utils/networkUtils.js'); const isNetworkConnected = await checkDefaultClientConnection(3000); if (isNetworkConnected) { logger.info('网络连接正常,重新加载全屏窗口页面'); difyfullScreenWindow.reload(); } else { logger.warn('网络连接异常,关闭全屏窗口'); difyfullScreenWindow.close(); } } catch (error) { logger.error('全屏窗口重试过程中出错:', error); difyfullScreenWindow.close(); } }, retryDelay); } else { logger.error(`全屏窗口页面加载失败次数超过${maxRetries}次,关闭窗口`); difyfullScreenWindow.close(); } }); // 页面加载成功时重置失败计数 difyfullScreenWindow.webContents.on('did-finish-load', () => { if (failLoadCount > 0) { logger.info('全屏窗口页面加载成功,重置失败计数'); failLoadCount = 0; } }); } export function createDrageWindow() { logger.info('开始创建悬浮窗口') if (drageWindow) { drageWindow.focus() return } drageWindow = new BrowserWindow({ width: 120, height: 120, frame: false, show: true, skipTaskbar: true, transparent: true, resizable: false, alwaysOnTop: true, autoHideMenuBar: true, webPreferences: { contextIsolation: false, nodeIntegration: true, nodeIntegrationInWorker: true, preload: join(__dirname, '../preload/index.js') } }) drageWindow.setAlwaysOnTop(true, 'screen-saver') drageWindow.on('ready-to-show', () => { drageWindow.show() }) drageWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } }) drageWindow.on('close', (event) => { if (!app.isQuiting) { event.preventDefault() drageWindow.hide() return false } }) const h5_client_url=getStoreValue("h5_client_url")+"/electron_h5/" logger.log("======================== drageWindow :") logger.log(h5_client_url) drageWindow.loadURL(h5_client_url) // 添加重试机制的错误处理 let drageFailLoadCount = 0; const drageMaxRetries = 2; const drageRetryDelay = 1000; drageWindow.webContents.on('did-fail-load', async (event, errorCode, errorDescription, validatedURL) => { logger.error(`悬浮窗口页面加载失败: code=${errorCode}, desc=${errorDescription}, url=${validatedURL}, 失败次数: ${drageFailLoadCount + 1}`) drageFailLoadCount++; if (drageFailLoadCount <= drageMaxRetries) { logger.info(`悬浮窗口页面加载失败,${drageRetryDelay}ms后进行第${drageFailLoadCount}次重试...`); setTimeout(async () => { try { // 在重试前先检查网络连接 const { checkDefaultClientConnection } = await import('./utils/networkUtils.js'); const isNetworkConnected = await checkDefaultClientConnection(3000); if (isNetworkConnected) { logger.info('网络连接正常,重新加载悬浮窗口页面'); drageWindow.reload(); } else { logger.warn('网络连接异常,隐藏悬浮窗口'); drageWindow.hide(); } } catch (error) { logger.error('悬浮窗口重试过程中出错:', error); drageWindow.hide(); } }, drageRetryDelay); } else { logger.error(`悬浮窗口页面加载失败次数超过${drageMaxRetries}次,隐藏窗口`); drageWindow.hide(); } }); // 页面加载成功时重置失败计数 drageWindow.webContents.on('did-finish-load', () => { if (drageFailLoadCount > 0) { logger.info('悬浮窗口页面加载成功,重置失败计数'); drageFailLoadCount = 0; } }); const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize drageWindow.setPosition(screenWidth - drageWindow.getSize()[0] -100, screenHeight - drageWindow.getSize()[1] - 100) } export async function tokenExpireTimer(){ let lastLogTime = 0; // 记录上次打印日志的时间 const LOG_INTERVAL = 60000; // 日志打印间隔,60秒打印一次 const tokenExpireTimer = setInterval(async () => { return false; const currentTime = Date.now(); const lastActiveTime = getStoreValue("lastActiveTime")||null if (lastActiveTime!=null){ try { const nowTime=dayjs(); const lastTime=dayjs(lastActiveTime); const diff=nowTime.diff(lastTime, 'minute') // 只在特定条件下打印日志,减少日志频率 if (currentTime - lastLogTime > LOG_INTERVAL) { logger.info(`tokenExpireTimer 检查 - 时间差距: ${diff}分钟`) lastLogTime = currentTime; } if ( diff> 30) { logger.info(`用户超时退出登录 - 时间差距: ${diff}分钟`) deleteStore("lastActiveTime") try { // 清除所有窗口的浏览器缓存 const windows = BrowserWindow.getAllWindows() for (const window of windows) { const session = window.webContents.session await session.clearStorageData({ storages: [ 'appcache', 'cookies', 'filesystem', 'indexdb', 'localstorage', 'shadercache', 'websql', 'serviceworkers', 'cachestorage' ] }) } logger.info('浏览器缓存清除成功') } catch (error) { logger.error('清除浏览器缓存失败:', error) } if (mainWindow) { mainWindow.reload() } logger.info('用户已退出登录') }else { logger.info(`用户正常登录 - 时间差距: ${diff}分钟`) } } catch (e) { logger.error('tokenExpireTimer 执行错误:', e) } } }, 1000 * 10) } export function getMainWindow() { if (!mainWindow || mainWindow.isDestroyed()) { return null } return mainWindow } export function getDragWindow() { if (!drageWindow || drageWindow.isDestroyed()) { return null } return drageWindow } export function getDifyFullScreenWindow() { return difyfullScreenWindow; } export function getDrageWindow() { return drageWindow; } // 在应用退出时注销所有快捷键 export function unregisterAllShortcuts() { globalShortcut.unregisterAll() } export function closeApiConfigWindow() { if (apiConfigWindow) { apiConfigWindow.close() apiConfigWindow = null } } export function createConfigWindow() { if (configWindow) { configWindow.focus() return } configWindow = new BrowserWindow({ width: 600, height: 400, resizable: false, minimizable: false, maximizable: false, parent: getMainWindow(), modal: true, webPreferences: { contextIsolation: false, nodeIntegration: true, nodeIntegrationInWorker: true, preload: join(__dirname, '../preload/index.js') }, permissions: { microphone: 'allow' } }) if (is.dev && process.env['ELECTRON_RENDERER_URL']) { configWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/#/config') } else { configWindow.loadFile(join(__dirname, '../renderer/index.html'), { hash: '/config' }) } // configWindow.on('closed', () => { configWindow = null }) } // 添加关闭配置窗口的方法 export function closeConfigWindow() { if (configWindow) { configWindow.close() configWindow = null } } // 测试快捷键是否正常工作 export function testShortcuts() { logger.info('测试快捷键功能...') // 检查F5快捷键是否已注册 const isF5Registered = globalShortcut.isRegistered('F5') logger.info(`F5快捷键是否已注册: ${isF5Registered}`) // 获取所有已注册的快捷键 const allShortcuts = globalShortcut.isRegistered('F5') ? ['F5'] : [] logger.info(`已注册的快捷键: ${allShortcuts.join(', ')}`) // 获取当前焦点窗口 const focusedWindow = BrowserWindow.getFocusedWindow() logger.info(`当前焦点窗口: ${focusedWindow ? '存在' : '不存在'}`) if (focusedWindow) { logger.info(`焦点窗口标题: ${focusedWindow.getTitle()}`) logger.info(`焦点窗口是否销毁: ${focusedWindow.isDestroyed()}`) } return { f5Registered: isF5Registered, focusedWindow: focusedWindow ? 'exists' : 'none', mainWindow: mainWindow && !mainWindow.isDestroyed() ? 'exists' : 'none', difyWindow: difyfullScreenWindow && !difyfullScreenWindow.isDestroyed() ? 'exists' : 'none' } }