From e5c3e57430efef55c5780c5b30b6354734110ef9 Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Sat, 26 Apr 2025 06:27:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B035.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .npmrc | 7 +- .yarnrc | 1 + check-electron-version.js | 4 + electron-builder-patched.js | 4 + package.json | 28 +- scripts/fix-electron-builder.js | 109 ++++ scripts/update-electron-direct.js | 130 +++++ scripts/update-electron.js | 266 ++++++++++ src/main/ipc.ts | 40 +- src/preload/index.ts | 4 + src/renderer/src/pages/Browser/index.tsx | 618 ++++++++++++++++++++--- temp.txt | 8 - yarn.lock | 39 +- 新建文本文档.txt | 1 - 14 files changed, 1156 insertions(+), 103 deletions(-) create mode 100644 .yarnrc create mode 100644 check-electron-version.js create mode 100644 electron-builder-patched.js create mode 100644 scripts/fix-electron-builder.js create mode 100644 scripts/update-electron-direct.js create mode 100644 scripts/update-electron.js delete mode 100644 temp.txt delete mode 100644 新建文本文档.txt diff --git a/.npmrc b/.npmrc index 535a03f585..98b2c46e65 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,6 @@ -electron_mirror=https://npmmirror.com/mirrors/electron/ \ No newline at end of file +registry=https://registry.npmmirror.com/ +electron_mirror=https://npmmirror.com/mirrors/electron/ +electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ +ELECTRON_CUSTOM_DIR="{{ version }}" +ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ +ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 0000000000..8348491d64 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +electron_mirror "https://npmmirror.com/mirrors/electron/" diff --git a/check-electron-version.js b/check-electron-version.js new file mode 100644 index 0000000000..0ed96a0f93 --- /dev/null +++ b/check-electron-version.js @@ -0,0 +1,4 @@ +const { app } = require('electron'); +console.log(`Electron version: ${process.versions.electron}`); +console.log(`Chrome version: ${process.versions.chrome}`); +console.log(`Node version: ${process.versions.node}`); diff --git a/electron-builder-patched.js b/electron-builder-patched.js new file mode 100644 index 0000000000..de095c7264 --- /dev/null +++ b/electron-builder-patched.js @@ -0,0 +1,4 @@ + +// 这是一个启动脚本,用于加载补丁并运行 electron-builder +require('J:\Cherry\cherry-studioTTS\node_modules\app-builder-lib\out\node-module-collector\nodeModulesCollector.patch.js') +require('electron-builder/cli') diff --git a/package.json b/package.json index b10a35dc5d..2d4a9fdeb3 100644 --- a/package.json +++ b/package.json @@ -22,16 +22,19 @@ "dev": "electron-vite dev", "build": "npm run typecheck && electron-vite build", "build:check": "yarn test && yarn typecheck && yarn check:i18n", - "build:unpack": "dotenv npm run build && electron-builder --dir", - "build:win": "dotenv npm run build && electron-builder --win", - "build:win:x64": "dotenv npm run build && electron-builder --win --x64", - "build:win:arm64": "dotenv npm run build && electron-builder --win --arm64", - "build:mac": "dotenv electron-vite build && electron-builder --mac", - "build:mac:arm64": "dotenv electron-vite build && electron-builder --mac --arm64", - "build:mac:x64": "dotenv electron-vite build && electron-builder --mac --x64", - "build:linux": "dotenv electron-vite build && electron-builder --linux", - "build:linux:arm64": "dotenv electron-vite build && electron-builder --linux --arm64", - "build:linux:x64": "dotenv electron-vite build && electron-builder --linux --x64", + "fix:electron-builder": "node scripts/fix-electron-builder.js", + "update:electron": "node scripts/update-electron.js", + "update:electron:direct": "node scripts/update-electron-direct.js", + "build:unpack": "dotenv npm run build && npm run fix:electron-builder && electron-builder --dir", + "build:win": "dotenv npm run build && npm run fix:electron-builder && electron-builder --win", + "build:win:x64": "dotenv npm run build && npm run fix:electron-builder && electron-builder --win --x64", + "build:win:arm64": "dotenv npm run build && npm run fix:electron-builder && electron-builder --win --arm64", + "build:mac": "dotenv electron-vite build && npm run fix:electron-builder && electron-builder --mac", + "build:mac:arm64": "dotenv electron-vite build && npm run fix:electron-builder && electron-builder --mac --arm64", + "build:mac:x64": "dotenv electron-vite build && npm run fix:electron-builder && electron-builder --mac --x64", + "build:linux": "dotenv electron-vite build && npm run fix:electron-builder && electron-builder --linux", + "build:linux:arm64": "dotenv electron-vite build && npm run fix:electron-builder && electron-builder --linux --arm64", + "build:linux:x64": "dotenv electron-vite build && npm run fix:electron-builder && electron-builder --linux --x64", "build:npm": "node scripts/build-npm.js", "release": "node scripts/version.js", "publish": "yarn build:check && yarn release patch push", @@ -52,7 +55,7 @@ "test:renderer:coverage": "vitest run --coverage", "format": "prettier --write .", "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", - "postinstall": "electron-builder install-app-deps", + "postinstall": "electron-builder install-app-deps && npm run fix:electron-builder", "prepare": "husky" }, "dependencies": { @@ -155,6 +158,7 @@ "turndown-plugin-gfm": "^1.0.2", "undici": "^7.4.0", "webdav": "^5.8.0", + "zipfile": "^0.5.12", "zipread": "^1.3.3", "zod": "^3.24.2" }, @@ -213,7 +217,7 @@ "dexie": "^4.0.8", "dexie-react-hooks": "^1.1.7", "dotenv-cli": "^7.4.2", - "electron": "31.7.6", + "electron": "35.2.0", "electron-builder": "26.0.13", "electron-devtools-installer": "^3.2.0", "electron-icon-builder": "^2.0.1", diff --git a/scripts/fix-electron-builder.js b/scripts/fix-electron-builder.js new file mode 100644 index 0000000000..2b00f263f3 --- /dev/null +++ b/scripts/fix-electron-builder.js @@ -0,0 +1,109 @@ +/** + * 修复 electron-builder 堆栈溢出问题的补丁脚本 + * + * 这个脚本修复了 electron-builder 在处理循环依赖时导致的堆栈溢出问题。 + * 主要修改了以下文件: + * 1. node_modules/app-builder-lib/out/node-module-collector/nodeModulesCollector.js + */ + +const fs = require('fs'); +const path = require('path'); + +// 获取 nodeModulesCollector.js 文件的路径 +const nodeModulesCollectorPath = path.join( + process.cwd(), + 'node_modules', + 'app-builder-lib', + 'out', + 'node-module-collector', + 'nodeModulesCollector.js' +); + +// 检查文件是否存在 +if (!fs.existsSync(nodeModulesCollectorPath)) { + console.error('找不到 nodeModulesCollector.js 文件,请确保已安装 electron-builder'); + process.exit(1); +} + +// 读取文件内容 +let content = fs.readFileSync(nodeModulesCollectorPath, 'utf8'); + +// 修复 1: 修改 _getNodeModules 方法,添加环路检测 +const oldGetNodeModulesMethod = /(_getNodeModules\(dependencies, result\) \{[\s\S]*?result\.sort\(\(a, b\) => a\.name\.localeCompare\(b\.name\)\);\s*\})/; +const newGetNodeModulesMethod = `_getNodeModules(dependencies, result, depth = 0, visited = new Set()) { + // 添加递归深度限制 + if (depth > 10) { + console.log("递归深度超过10,停止递归"); + return; + } + + if (dependencies.size === 0) { + return; + } + + for (const d of dependencies.values()) { + const reference = [...d.references][0]; + const moduleId = \`\${d.name}@\${reference}\`; + + // 环路检测:如果已经访问过这个模块,则跳过 + if (visited.has(moduleId)) { + console.log(\`检测到循环依赖: \${moduleId}\`); + continue; + } + + // 标记为已访问 + visited.add(moduleId); + + const p = this.dependencyPathMap.get(moduleId); + if (p === undefined) { + builder_util_1.log.debug({ name: d.name, reference }, "cannot find path for dependency"); + continue; + } + const node = { + name: d.name, + version: reference, + dir: p, + }; + result.push(node); + if (d.dependencies.size > 0) { + node.dependencies = []; + this._getNodeModules(d.dependencies, node.dependencies, depth + 1, visited); + } + + // 处理完成后,从已访问集合中移除,允许在其他路径中再次访问 + visited.delete(moduleId); + } + result.sort((a, b) => a.name.localeCompare(b.name)); + }`; + +content = content.replace(oldGetNodeModulesMethod, newGetNodeModulesMethod); + +// 修复 2: 修改 getNodeModules 方法,传递 visited 集合 +const oldGetNodeModulesCall = /(this\._getNodeModules\(hoisterResult\.dependencies, this\.nodeModules\);)/; +const newGetNodeModulesCall = `// 创建一个新的 visited 集合用于环路检测 + const visited = new Set(); + + this._getNodeModules(hoisterResult.dependencies, this.nodeModules, 0, visited);`; + +content = content.replace(oldGetNodeModulesCall, newGetNodeModulesCall); + +// 修复 3: 修改 convertToDependencyGraph 方法,跳过路径未定义的依赖 +const oldPathCheck = /(if \(!dependencies\.path\) \{[\s\S]*?throw new Error\("unable to parse `path` during `tree\.dependencies` reduce"\);[\s\S]*?\})/; +const newPathCheck = `if (!dependencies.path) { + builder_util_1.log.error({ + packageName, + data: dependencies, + parentModule: tree.name, + parentVersion: tree.version, + }, "dependency path is undefined"); + // 跳过这个依赖而不是抛出错误 + console.log(\`跳过路径未定义的依赖: \${packageName}\`); + return acc; + }`; + +content = content.replace(oldPathCheck, newPathCheck); + +// 写入修改后的内容 +fs.writeFileSync(nodeModulesCollectorPath, content, 'utf8'); + +console.log('成功应用 electron-builder 堆栈溢出修复补丁!'); diff --git a/scripts/update-electron-direct.js b/scripts/update-electron-direct.js new file mode 100644 index 0000000000..3a8d1f044c --- /dev/null +++ b/scripts/update-electron-direct.js @@ -0,0 +1,130 @@ +/** + * Electron 直接版本更新脚本 + * + * 这个脚本帮助您直接更新到指定版本的 Electron + * 使用方法: node scripts/update-electron-direct.js [version] + * + * 例如: node scripts/update-electron-direct.js 32.0.0 + */ + +const { execSync } = require('child_process') +const fs = require('fs') +const path = require('path') +const readline = require('readline') + +// 创建命令行交互界面 +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +// 获取当前 package.json 中的 Electron 版本 +function getCurrentElectronVersion() { + const packageJsonPath = path.join(process.cwd(), 'package.json') + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) + return packageJson.devDependencies.electron +} + +// 更新 package.json 中的 Electron 版本 +function updateElectronVersion(version) { + const packageJsonPath = path.join(process.cwd(), 'package.json') + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) + + // 保存旧版本 + const oldVersion = packageJson.devDependencies.electron + + // 更新版本 + packageJson.devDependencies.electron = version + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8') + + console.log(`已更新 package.json 中的 Electron 版本: ${oldVersion} -> ${version}`) + return oldVersion +} + +// 安装依赖 +function installDependencies() { + console.log('正在安装依赖...') + try { + execSync('yarn install', { stdio: 'inherit' }) + return true + } catch (error) { + console.error('安装依赖失败:', error.message) + return false + } +} + +// 测试应用 +function testApp() { + console.log('正在启动开发模式测试应用...') + try { + execSync('npm run dev', { stdio: 'inherit' }) + return true + } catch (error) { + console.error('测试应用失败:', error.message) + return false + } +} + +// 主函数 +async function main() { + const targetVersion = process.argv[2] + + if (!targetVersion) { + console.error('请指定目标 Electron 版本') + console.log('使用方法: node scripts/update-electron-direct.js [version]') + console.log('例如: node scripts/update-electron-direct.js 32.0.0') + rl.close() + return + } + + const currentVersion = getCurrentElectronVersion() + console.log(`当前 Electron 版本: ${currentVersion}`) + + rl.question(`确定要直接更新到 Electron ${targetVersion} 吗?(y/n) `, (answer) => { + if (answer.toLowerCase() !== 'y') { + console.log('操作已取消') + rl.close() + return + } + + const oldVersion = updateElectronVersion(targetVersion) + + if (!installDependencies()) { + console.log(`Electron ${targetVersion} 安装依赖失败,正在恢复到原版本 ${oldVersion}`) + updateElectronVersion(oldVersion) + installDependencies() + rl.close() + return + } + + rl.question('依赖安装成功,是否测试应用?(y/n) ', (answer) => { + if (answer.toLowerCase() !== 'y') { + console.log('跳过测试步骤') + rl.close() + return + } + + if (!testApp()) { + console.log(`Electron ${targetVersion} 测试失败`) + rl.question(`是否恢复到原版本 ${oldVersion}?(y/n) `, (answer) => { + if (answer.toLowerCase() === 'y') { + console.log(`正在恢复到原版本 ${oldVersion}`) + updateElectronVersion(oldVersion) + installDependencies() + } + rl.close() + }) + return + } + + console.log(`Electron ${targetVersion} 更新并测试成功!`) + rl.close() + }) + }) +} + +// 运行主函数 +main().catch((error) => { + console.error('发生错误:', error) + rl.close() +}) diff --git a/scripts/update-electron.js b/scripts/update-electron.js new file mode 100644 index 0000000000..04937c3e10 --- /dev/null +++ b/scripts/update-electron.js @@ -0,0 +1,266 @@ +/** + * Electron 版本更新脚本 + * + * 这个脚本帮助您逐步更新到更新版本的 Electron + * 使用方法: node scripts/update-electron.js [target-version] + * + * 例如: node scripts/update-electron.js 32.0.0 + */ + +const { execSync } = require('child_process') +const fs = require('fs') +const path = require('path') +const readline = require('readline') +const https = require('https') + +// 创建命令行交互界面 +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +// 获取当前 package.json 中的 Electron 版本 +function getCurrentElectronVersion() { + const packageJsonPath = path.join(process.cwd(), 'package.json') + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) + return packageJson.devDependencies.electron +} + +// 更新 package.json 中的 Electron 版本 +function updateElectronVersion(version) { + const packageJsonPath = path.join(process.cwd(), 'package.json') + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) + + // 保存旧版本 + const oldVersion = packageJson.devDependencies.electron + + // 更新版本 + packageJson.devDependencies.electron = version + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8') + + console.log(`已更新 package.json 中的 Electron 版本: ${oldVersion} -> ${version}`) + return oldVersion +} + +// 安装依赖 +function installDependencies() { + console.log('正在安装依赖...') + try { + execSync('yarn install', { stdio: 'inherit' }) + return true + } catch (error) { + console.error('安装依赖失败:', error.message) + return false + } +} + +// 测试应用 +function testApp() { + console.log('正在启动开发模式测试应用...') + try { + execSync('npm run dev', { stdio: 'inherit' }) + return true + } catch (error) { + console.error('测试应用失败:', error.message) + return false + } +} + +// 从 npm 获取 Electron 的可用版本 +function getAvailableElectronVersions() { + return new Promise((resolve, reject) => { + https + .get('https://registry.npmjs.org/electron', (res) => { + let data = '' + + res.on('data', (chunk) => { + data += chunk + }) + + res.on('end', () => { + try { + const json = JSON.parse(data) + const versions = Object.keys(json.versions) + .filter((v) => !v.includes('-')) // 过滤掉预发布版本 + .sort((a, b) => compareVersions(a, b)) + resolve(versions) + } catch (error) { + reject(error) + } + }) + }) + .on('error', (error) => { + reject(error) + }) + }) +} + +// 比较版本号 +function compareVersions(a, b) { + const partsA = a.split('.').map(Number) + const partsB = b.split('.').map(Number) + + for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { + const partA = partsA[i] || 0 + const partB = partsB[i] || 0 + + if (partA < partB) return -1 + if (partA > partB) return 1 + } + + return 0 +} + +// 获取从当前版本到目标版本的升级路径 +function getUpgradePath(currentVersion, targetVersion, allVersions) { + // 清理版本号,移除可能的前缀(如 ^、~) + currentVersion = currentVersion.replace(/[^0-9.]/g, '') + + // 过滤出在当前版本和目标版本之间的所有版本 + const relevantVersions = allVersions.filter( + (v) => compareVersions(v, currentVersion) > 0 && compareVersions(v, targetVersion) <= 0 + ) + + // 按主要版本分组 + const versionsByMajor = {} + relevantVersions.forEach((v) => { + const major = v.split('.')[0] + if (!versionsByMajor[major]) { + versionsByMajor[major] = [] + } + versionsByMajor[major].push(v) + }) + + // 为每个主要版本选择最新的次要版本 + const upgradePath = [] + Object.keys(versionsByMajor) + .sort((a, b) => Number(a) - Number(b)) + .forEach((major) => { + const versions = versionsByMajor[major] + // 添加该主要版本的最新版本 + upgradePath.push(versions[versions.length - 1]) + }) + + // 确保包含目标版本 + if (upgradePath.length === 0 || upgradePath[upgradePath.length - 1] !== targetVersion) { + upgradePath.push(targetVersion) + } + + return upgradePath +} + +// 主函数 +async function main() { + try { + const currentVersionFull = getCurrentElectronVersion() + const currentVersion = currentVersionFull.replace(/[^0-9.]/g, '') + console.log(`当前 Electron 版本: ${currentVersionFull} (${currentVersion})`) + + // 获取可用的 Electron 版本 + console.log('正在获取可用的 Electron 版本...') + const allVersions = await getAvailableElectronVersions() + + // 获取最新版本 + const latestVersion = allVersions[allVersions.length - 1] + console.log(`最新的 Electron 版本: ${latestVersion}`) + + // 确定目标版本 + let targetVersion = process.argv[2] || latestVersion + + if (compareVersions(targetVersion, currentVersion) <= 0) { + console.log(`目标版本 ${targetVersion} 不高于当前版本 ${currentVersion},无需更新`) + rl.close() + return + } + + // 获取升级路径 + const upgradePath = getUpgradePath(currentVersion, targetVersion, allVersions) + + console.log(`\n从 ${currentVersion} 到 ${targetVersion} 的推荐升级路径:`) + upgradePath.forEach((v, i) => { + console.log(`${i + 1}. ${v}`) + }) + + rl.question('\n是否按照推荐路径逐步升级?(y/n) ', async (answer) => { + if (answer.toLowerCase() !== 'y') { + console.log('操作已取消') + rl.close() + return + } + + // 保存原始版本,以便在失败时恢复 + const originalVersion = currentVersionFull + + // 逐步升级 + for (let i = 0; i < upgradePath.length; i++) { + const version = upgradePath[i] + console.log(`\n===== 正在升级到 Electron ${version} (${i + 1}/${upgradePath.length}) =====`) + + updateElectronVersion(version) + + if (!installDependencies()) { + console.log(`Electron ${version} 安装依赖失败`) + + const restoreAnswer = await new Promise((resolve) => { + rl.question('是否恢复到原始版本?(y/n) ', resolve) + }) + + if (restoreAnswer.toLowerCase() === 'y') { + console.log(`正在恢复到原始版本 ${originalVersion}`) + updateElectronVersion(originalVersion) + installDependencies() + } + + rl.close() + return + } + + const testAnswer = await new Promise((resolve) => { + rl.question(`依赖安装成功,是否测试 Electron ${version}?(y/n) `, resolve) + }) + + if (testAnswer.toLowerCase() === 'y') { + if (!testApp()) { + console.log(`Electron ${version} 测试失败`) + + const restoreAnswer = await new Promise((resolve) => { + rl.question('是否恢复到原始版本?(y/n) ', resolve) + }) + + if (restoreAnswer.toLowerCase() === 'y') { + console.log(`正在恢复到原始版本 ${originalVersion}`) + updateElectronVersion(originalVersion) + installDependencies() + } + + rl.close() + return + } + + console.log(`Electron ${version} 测试成功!`) + } + + if (i < upgradePath.length - 1) { + const continueAnswer = await new Promise((resolve) => { + rl.question('是否继续升级到下一个版本?(y/n) ', resolve) + }) + + if (continueAnswer.toLowerCase() !== 'y') { + console.log(`升级停止在 Electron ${version}`) + rl.close() + return + } + } + } + + console.log(`\n成功升级到 Electron ${targetVersion}!`) + rl.close() + }) + } catch (error) { + console.error('发生错误:', error) + rl.close() + } +} + +// 运行主函数 +main() diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 66e9336720..876a86cf6b 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -7,7 +7,7 @@ import { isMac, isWin } from '@main/constant' import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process' import { IpcChannel } from '@shared/IpcChannel' import { MCPServer, Shortcut, ThemeMode } from '@types' // Import MCPServer here -import { BrowserWindow, ipcMain, session, shell } from 'electron' +import { BrowserWindow, ipcMain, session, shell, webContents } from 'electron' import log from 'electron-log' import { titleBarOverlayDark, titleBarOverlayLight } from './config' @@ -193,6 +193,44 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { } }) + // 销毁webContents + ipcMain.handle('browser:destroy-webcontents', async (_, webContentsId: number) => { + try { + // 尝试通过ID获取webContents + const allWebContents = webContents.getAllWebContents() + const targetWebContents = allWebContents.find((wc) => wc.id === webContentsId) + + if (targetWebContents) { + // 如果找到了webContents,尝试销毁它 + if (!targetWebContents.isDestroyed()) { + // 先停止加载 + targetWebContents.stop() + + // 加载空白页面 + targetWebContents.loadURL('about:blank') + + // 等待一小段时间,让空白页面加载完成 + await new Promise((resolve) => setTimeout(resolve, 100)) + + // 销毁webContents - 使用close方法 + targetWebContents.close() // WebContents没有destroy方法,但有close方法 + + log.info(`Successfully destroyed webContents with ID: ${webContentsId}`) + return { success: true } + } else { + log.info(`WebContents with ID ${webContentsId} is already destroyed`) + return { success: true, alreadyDestroyed: true } + } + } else { + log.warn(`WebContents with ID ${webContentsId} not found`) + return { success: false, error: 'WebContents not found' } + } + } catch (error: any) { + log.error(`Failed to destroy webContents with ID ${webContentsId}:`, error) + return { success: false, error: error.message } + } + }) + // check for update ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => { const update = await appUpdater.autoUpdater.checkForUpdates() diff --git a/src/preload/index.ts b/src/preload/index.ts index a5cc251482..8f0c68f786 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -265,6 +265,10 @@ const api = { getSupportedLanguages: () => ipcRenderer.invoke(IpcChannel.CodeExecutor_GetSupportedLanguages), executeJS: (code: string) => ipcRenderer.invoke(IpcChannel.CodeExecutor_ExecuteJS, code), executePython: (code: string) => ipcRenderer.invoke(IpcChannel.CodeExecutor_ExecutePython, code) + }, + browser: { + clearData: () => ipcRenderer.invoke('browser:clear-data'), + destroyWebContents: (webContentsId: number) => ipcRenderer.invoke('browser:destroy-webcontents', webContentsId) } } diff --git a/src/renderer/src/pages/Browser/index.tsx b/src/renderer/src/pages/Browser/index.tsx index 36bc7ea604..d9788bc185 100644 --- a/src/renderer/src/pages/Browser/index.tsx +++ b/src/renderer/src/pages/Browser/index.tsx @@ -82,16 +82,23 @@ const WebviewContainer = styled.div` .webview-wrapper { width: 100%; height: 100%; - display: none; + position: absolute; + top: 0; + left: 0; + visibility: hidden; + z-index: 1; &.active { - display: block; + visibility: visible; + z-index: 2; } } & webview { width: 100%; height: 100%; + border: none; + outline: none; } ` @@ -134,108 +141,300 @@ interface Tab { const Browser = () => { const { t } = useTranslation() - // 选项卡状态管理 - const [tabs, setTabs] = useState([ - { - id: '1', - title: 'Google', - url: 'https://www.google.com', - isLoading: false, - canGoBack: false, - canGoForward: false + // 从本地存储加载选项卡状态 + const loadTabsFromStorage = (): { tabs: Tab[]; activeTabId: string } => { + try { + const savedTabs = localStorage.getItem('browser_tabs') + const savedActiveTabId = localStorage.getItem('browser_active_tab_id') + + if (savedTabs && savedActiveTabId) { + // 解析保存的选项卡 + const parsedTabs = JSON.parse(savedTabs) as Tab[] + + // 验证选项卡数据 + const validTabs = parsedTabs.filter( + (tab) => tab && tab.id && tab.url && typeof tab.id === 'string' && typeof tab.url === 'string' + ) + + // 确保至少有一个选项卡 + if (validTabs.length > 0) { + // 验证活动选项卡ID + const isActiveTabValid = validTabs.some((tab) => tab.id === savedActiveTabId) + const finalActiveTabId = isActiveTabValid ? savedActiveTabId : validTabs[0].id + + console.log('Loaded tabs from storage:', validTabs.length, 'tabs, active tab:', finalActiveTabId) + + return { + tabs: validTabs, + activeTabId: finalActiveTabId + } + } + } + } catch (error) { + console.error('Failed to load tabs from storage:', error) } - ]) - const [activeTabId, setActiveTabId] = useState('1') + + // 默认选项卡 + const defaultTabs = [ + { + id: '1', + title: 'Google', + url: 'https://www.google.com', + isLoading: false, + canGoBack: false, + canGoForward: false + } + ] + + console.log('Using default tabs') + + return { + tabs: defaultTabs, + activeTabId: '1' + } + } + + // 保存选项卡状态到本地存储 + const saveTabsToStorage = (tabs: Tab[], activeTabId: string) => { + try { + // 确保只保存当前有效的选项卡 + const validTabs = tabs.filter((tab) => tab && tab.id && tab.url) + + // 确保activeTabId是有效的 + const isActiveTabValid = validTabs.some((tab) => tab.id === activeTabId) + const finalActiveTabId = isActiveTabValid ? activeTabId : validTabs.length > 0 ? validTabs[0].id : '' + + // 保存到localStorage + localStorage.setItem('browser_tabs', JSON.stringify(validTabs)) + localStorage.setItem('browser_active_tab_id', finalActiveTabId) + + console.log('Saved tabs to storage:', validTabs.length, 'tabs, active tab:', finalActiveTabId) + } catch (error) { + console.error('Failed to save tabs to storage:', error) + } + } + + // 选项卡状态管理 + const initialTabState = loadTabsFromStorage() + const [tabs, setTabs] = useState(initialTabState.tabs) + const [activeTabId, setActiveTabId] = useState(initialTabState.activeTabId) // 获取当前活动选项卡 const activeTab = tabs.find((tab) => tab.id === activeTabId) || tabs[0] - // 兼容旧代码的状态 - const [url, setUrl] = useState(activeTab.url) + // 兼容旧代码的状态,只使用setter + const [, setUrl] = useState(activeTab.url) const [currentUrl, setCurrentUrl] = useState('') const [canGoBack, setCanGoBack] = useState(false) const [canGoForward, setCanGoForward] = useState(false) const [isLoading, setIsLoading] = useState(false) - // 使用对象存储多个webview引用 + // 使用对象存储多个webview引用 - 使用useRef确保在组件重新渲染时保持引用 const webviewRefs = useRef>({}) + // 使用useRef保存webview的会话状态 + const webviewSessionsRef = useRef>({}) + + // 使用useRef保存事件监听器清理函数 + const cleanupFunctionsRef = useRef void>>({}) + // 获取当前活动的webview引用 const webviewRef = { current: webviewRefs.current[activeTabId] || null } as React.RefObject - useEffect(() => { - const webview = webviewRef.current - if (!webview) return + // 创建一个函数来设置webview的所有事件监听器 + const setupWebviewListeners = (webview: WebviewTag, tabId: string) => { + console.log('Setting up event listeners for tab:', tabId) + // 处理加载开始事件 const handleDidStartLoading = () => { - setIsLoading(true) + // 只更新当前活动标签页的UI状态 + if (tabId === activeTabId) { + setIsLoading(true) + } + // 更新选项卡状态 - updateTabInfo(activeTabId, { isLoading: true }) + updateTabInfo(tabId, { isLoading: true }) } + // 处理加载结束事件 const handleDidStopLoading = () => { const currentURL = webview.getURL() - setIsLoading(false) - setCurrentUrl(currentURL) + + // 只更新当前活动标签页的UI状态 + if (tabId === activeTabId) { + setIsLoading(false) + setCurrentUrl(currentURL) + } // 更新选项卡状态 - updateTabInfo(activeTabId, { + updateTabInfo(tabId, { isLoading: false, url: currentURL, - title: webview.getTitle() || currentURL + title: webview.getTitle() || currentURL, + canGoBack: webview.canGoBack(), + canGoForward: webview.canGoForward() }) } + // 处理导航事件 const handleDidNavigate = (e: any) => { const canGoBackStatus = webview.canGoBack() const canGoForwardStatus = webview.canGoForward() - setCurrentUrl(e.url) - setCanGoBack(canGoBackStatus) - setCanGoForward(canGoForwardStatus) + // 只更新当前活动标签页的UI状态 + if (tabId === activeTabId) { + setCurrentUrl(e.url) + setCanGoBack(canGoBackStatus) + setCanGoForward(canGoForwardStatus) + } // 更新选项卡状态 - updateTabInfo(activeTabId, { + updateTabInfo(tabId, { url: e.url, canGoBack: canGoBackStatus, canGoForward: canGoForwardStatus }) } + // 处理页内导航事件 const handleDidNavigateInPage = (e: any) => { const canGoBackStatus = webview.canGoBack() const canGoForwardStatus = webview.canGoForward() - setCurrentUrl(e.url) - setCanGoBack(canGoBackStatus) - setCanGoForward(canGoForwardStatus) + // 只更新当前活动标签页的UI状态 + if (tabId === activeTabId) { + setCurrentUrl(e.url) + setCanGoBack(canGoBackStatus) + setCanGoForward(canGoForwardStatus) + } // 更新选项卡状态 - updateTabInfo(activeTabId, { + updateTabInfo(tabId, { url: e.url, canGoBack: canGoBackStatus, canGoForward: canGoForwardStatus }) } - // 处理页面标题变化 + // 处理页面标题更新事件 const handlePageTitleUpdated = (e: any) => { // 更新选项卡标题 - updateTabInfo(activeTabId, { title: e.title }) + updateTabInfo(tabId, { title: e.title }) } - // 处理网站图标更新 + // 处理网站图标更新事件 const handlePageFaviconUpdated = (e: any) => { // 更新选项卡图标 - updateTabInfo(activeTabId, { favicon: e.favicons[0] }) + updateTabInfo(tabId, { favicon: e.favicons[0] }) } - // 检测Cloudflare验证码 + // 处理DOM就绪事件 const handleDomReady = () => { const captchaNotice = t('browser.captcha_notice') + // 注入链接点击拦截脚本 + webview.executeJavaScript(` + (function() { + // 已经注入过脚本,不再重复注入 + if (window.__linkInterceptorInjected) return; + window.__linkInterceptorInjected = true; + + // 创建一个全局函数,用于在控制台中调用以打开新标签页 + window.__openInNewTab = function(url, title) { + console.log('OPEN_NEW_TAB:' + JSON.stringify({url: url, title: title || url})); + }; + + // 拦截所有链接点击 + document.addEventListener('click', function(e) { + // 查找被点击的链接元素 + let target = e.target; + while (target && target.tagName !== 'A') { + target = target.parentElement; + if (!target) return; // 不是链接,直接返回 + } + + // 找到了链接元素 + if (target.tagName === 'A' && target.href) { + // 检查是否应该在新标签页中打开 + const inNewTab = e.ctrlKey || e.metaKey || target.target === '_blank'; + + // 阻止默认行为 + e.preventDefault(); + e.stopPropagation(); + + // 使用一个特殊的数据属性来标记这个链接 + const linkData = { + url: target.href, + title: target.textContent || target.title || target.href, + inNewTab: inNewTab + }; + + // 将数据转换为字符串并存储在自定义属性中 + document.body.setAttribute('data-last-clicked-link', JSON.stringify(linkData)); + + // 触发一个自定义事件 + const event = new CustomEvent('link-clicked', { detail: linkData }); + document.dispatchEvent(event); + + // 使用控制台消息通知Electron + console.log('LINK_CLICKED:' + JSON.stringify(linkData)); + + if (!inNewTab) { + // 在当前标签页中打开链接 + window.location.href = target.href; + } + + return false; + } + }, true); + + // 打印一条消息,确认链接拦截脚本已经注入 + console.log('Link interceptor script injected successfully'); + + // 每5秒测试一次链接拦截功能 + setInterval(function() { + console.log('Testing link interceptor...'); + + // 尝试调用全局函数 + if (window.__openInNewTab) { + console.log('Link interceptor is working!'); + + // 创建一个测试链接 + const testLink = document.createElement('a'); + testLink.href = 'https://www.example.com'; + testLink.textContent = 'Test Link'; + testLink.target = '_blank'; // 在新标签页中打开 + + // 添加到DOM中 + if (!document.getElementById('test-link-container')) { + const container = document.createElement('div'); + container.id = 'test-link-container'; + container.style.position = 'fixed'; + container.style.top = '10px'; + container.style.right = '10px'; + container.style.zIndex = '9999'; + container.style.background = 'white'; + container.style.padding = '10px'; + container.style.border = '1px solid black'; + container.appendChild(testLink); + document.body.appendChild(container); + } + + // 模拟点击事件 + console.log('LINK_CLICKED:' + JSON.stringify({ + url: 'https://www.example.com', + title: 'Test Link', + inNewTab: true + })); + } else { + console.log('Link interceptor is NOT working!'); + } + }, 5000); + })(); + `) + // 注入浏览器模拟脚本 webview.executeJavaScript(` try { @@ -431,6 +630,62 @@ const Browser = () => { webview.executeJavaScript(finalScript) } + // 处理新窗口打开请求 + const handleNewWindow = (e: any) => { + e.preventDefault() // 阻止默认行为 + + // 始终在新标签页中打开 + openUrlInTab(e.url, true, e.frameName || 'New Tab') + } + + // 处理将要导航的事件 + const handleWillNavigate = (e: any) => { + // 更新当前标签页的URL + updateTabInfo(tabId, { url: e.url }) + } + + // 处理控制台消息事件 - 用于链接点击拦截 + const handleConsoleMessage = (event: any) => { + // 打印所有控制台消息,便于调试 + console.log(`[Tab ${tabId}] Console message:`, event.message) + + // 处理新的链接点击消息 + if (event.message && event.message.startsWith('LINK_CLICKED:')) { + try { + const dataStr = event.message.replace('LINK_CLICKED:', '') + const data = JSON.parse(dataStr) + + console.log(`[Tab ${tabId}] Link clicked:`, data) + + if (data.url && data.inNewTab) { + // 在新标签页中打开链接 + console.log(`[Tab ${tabId}] Opening link in new tab:`, data.url) + openUrlInTab(data.url, true, data.title || data.url) + } + } catch (error) { + console.error('Failed to parse link data:', error) + } + } + + // 保留对旧消息格式的支持 + else if (event.message && event.message.startsWith('OPEN_NEW_TAB:')) { + try { + const dataStr = event.message.replace('OPEN_NEW_TAB:', '') + const data = JSON.parse(dataStr) + + console.log(`[Tab ${tabId}] Opening link in new tab (legacy format):`, data) + + if (data.url) { + // 在新标签页中打开链接 + openUrlInTab(data.url, true, data.title || data.url) + } + } catch (error) { + console.error('Failed to parse link data:', error) + } + } + } + + // 添加所有事件监听器 webview.addEventListener('did-start-loading', handleDidStartLoading) webview.addEventListener('did-stop-loading', handleDidStopLoading) webview.addEventListener('did-navigate', handleDidNavigate) @@ -438,11 +693,13 @@ const Browser = () => { webview.addEventListener('dom-ready', handleDomReady) webview.addEventListener('page-title-updated', handlePageTitleUpdated) webview.addEventListener('page-favicon-updated', handlePageFaviconUpdated) + webview.addEventListener('new-window', handleNewWindow) + webview.addEventListener('will-navigate', handleWillNavigate) + webview.addEventListener('console-message', handleConsoleMessage) - // 初始加载URL - webview.src = url - + // 返回清理函数 return () => { + console.log('Cleaning up event listeners for tab:', tabId) webview.removeEventListener('did-start-loading', handleDidStartLoading) webview.removeEventListener('did-stop-loading', handleDidStopLoading) webview.removeEventListener('did-navigate', handleDidNavigate) @@ -450,8 +707,73 @@ const Browser = () => { webview.removeEventListener('dom-ready', handleDomReady) webview.removeEventListener('page-title-updated', handlePageTitleUpdated) webview.removeEventListener('page-favicon-updated', handlePageFaviconUpdated) + webview.removeEventListener('new-window', handleNewWindow) + webview.removeEventListener('will-navigate', handleWillNavigate) + webview.removeEventListener('console-message', handleConsoleMessage) } - }, [url, t, activeTabId]) + } + + // 通用的打开URL函数 + const openUrlInTab = (url: string, inNewTab: boolean = false, title: string = 'New Tab') => { + if (inNewTab) { + // 在新标签页中打开链接 + const newTabId = `tab-${Date.now()}` + const newTab: Tab = { + id: newTabId, + title: title, + url: url, + isLoading: true, + canGoBack: false, + canGoForward: false + } + + // 创建新的选项卡数组,确保不修改原数组 + const newTabs = [...tabs, newTab] + + // 更新状态 + setTabs(newTabs) + setActiveTabId(newTabId) + + // 保存到本地存储 + saveTabsToStorage(newTabs, newTabId) + + console.log('Opened URL in new tab:', url, 'tab ID:', newTabId) + } else { + // 在当前标签页中打开链接 + setUrl(url) + + // 更新当前选项卡的URL + updateTabInfo(activeTabId, { url: url }) + } + } + + // 当activeTabId变化时,更新UI状态 + useEffect(() => { + // 获取当前活动的webview + const webview = webviewRefs.current[activeTabId] + if (!webview) return + + // 从webview获取最新状态 + try { + const currentURL = webview.getURL() + if (currentURL && currentURL !== 'about:blank') { + setCurrentUrl(currentURL) + } else { + // 如果没有有效URL,使用存储的URL + const tab = tabs.find((tab) => tab.id === activeTabId) + if (tab) { + setCurrentUrl(tab.url) + } + } + + // 更新导航状态 + setCanGoBack(webview.canGoBack()) + setCanGoForward(webview.canGoForward()) + setIsLoading(webview.isLoading()) + } catch (error) { + console.error('Error updating UI state:', error) + } + }, [activeTabId, tabs]) const handleUrlChange = (e: React.ChangeEvent) => { setCurrentUrl(e.target.value) @@ -538,60 +860,192 @@ const Browser = () => { } // 选项卡管理功能 - const handleAddTab = () => { + const handleAddTab = (url: string = 'https://www.google.com', title: string = 'New Tab') => { const newTabId = `tab-${Date.now()}` const newTab: Tab = { id: newTabId, - title: 'New Tab', - url: 'https://www.google.com', + title: title, + url: url, isLoading: false, canGoBack: false, canGoForward: false } - setTabs([...tabs, newTab]) + const newTabs = [...tabs, newTab] + setTabs(newTabs) setActiveTabId(newTabId) - setUrl('https://www.google.com') + setUrl(url) + + // 保存到本地存储 + saveTabsToStorage(newTabs, newTabId) + + return newTabId } const handleCloseTab = (tabId: string, e: React.MouseEvent) => { e.stopPropagation() // 防止触发选项卡切换 + console.log('Closing tab:', tabId) if (tabs.length === 1) { // 如果只有一个选项卡,创建一个新的空白选项卡 handleAddTab() + return // 已经在handleAddTab中保存了状态,这里直接返回 } - // 如果关闭的是当前活动选项卡,切换到前一个选项卡 + // 计算新的活动选项卡ID + let newActiveTabId = activeTabId if (tabId === activeTabId) { const currentIndex = tabs.findIndex((tab) => tab.id === tabId) const newActiveIndex = currentIndex === 0 ? 1 : currentIndex - 1 - setActiveTabId(tabs[newActiveIndex].id) + newActiveTabId = tabs[newActiveIndex].id + setActiveTabId(newActiveTabId) } // 从选项卡列表中移除 - setTabs(tabs.filter((tab) => tab.id !== tabId)) + const newTabs = tabs.filter((tab) => tab.id !== tabId) + setTabs(newTabs) + + // 清理不再使用的webview引用和会话状态 + if (webviewRefs.current[tabId]) { + // 停止加载并清理webview + try { + const webview = webviewRefs.current[tabId] + if (webview) { + // 停止加载 + webview.stop() + + // 尝试获取webContentsId + try { + const webContentsId = webview.getWebContentsId() + if (webContentsId && window.api && window.api.ipcRenderer) { + // 通过IPC请求主进程销毁webContents + window.api.ipcRenderer + .invoke('browser:destroy-webcontents', webContentsId) + .then(() => { + console.log('Successfully requested destruction of webContents for tab:', tabId) + }) + .catch((error) => { + console.error('Error requesting destruction of webContents:', error) + }) + } + } catch (e) { + console.error('Error getting webContentsId:', e) + } + + // 加载空白页面,释放资源 + webview.src = 'about:blank' + + // 使用保存的清理函数移除事件监听器 + if (cleanupFunctionsRef.current[tabId]) { + console.log('Calling cleanup function for tab:', tabId) + cleanupFunctionsRef.current[tabId]() + delete cleanupFunctionsRef.current[tabId] + } + } + } catch (error) { + console.error('Error cleaning up webview:', error) + } + + // 删除引用 + delete webviewRefs.current[tabId] + console.log('Removed webview reference for tab:', tabId) + } + + // 删除会话状态 + delete webviewSessionsRef.current[tabId] + console.log('Removed session state for tab:', tabId) + + // 保存到本地存储 - 确保不包含已关闭的选项卡 + saveTabsToStorage(newTabs, newActiveTabId) + + console.log('Tab closed, remaining tabs:', newTabs.length) } const handleTabChange = (newActiveTabId: string) => { + console.log('Switching to tab:', newActiveTabId) + + // 更新活动标签页ID setActiveTabId(newActiveTabId) // 更新URL和其他状态 const newActiveTab = tabs.find((tab) => tab.id === newActiveTabId) if (newActiveTab) { - setUrl(newActiveTab.url) - setCurrentUrl(newActiveTab.url) - setCanGoBack(newActiveTab.canGoBack) - setCanGoForward(newActiveTab.canGoForward) - setIsLoading(newActiveTab.isLoading) + // 获取新活动的webview + const newWebview = webviewRefs.current[newActiveTabId] + + // 如果webview存在,从webview获取最新状态 + if (newWebview) { + try { + // 获取当前URL + const currentURL = newWebview.getURL() + if (currentURL && currentURL !== 'about:blank') { + // 使用webview的实际URL,而不是存储的URL + setUrl(currentURL) + setCurrentUrl(currentURL) + + // 更新选项卡信息 + updateTabInfo(newActiveTabId, { url: currentURL }) + } else { + // 如果没有有效URL,使用存储的URL + setUrl(newActiveTab.url) + setCurrentUrl(newActiveTab.url) + } + + // 更新导航状态 + setCanGoBack(newWebview.canGoBack()) + setCanGoForward(newWebview.canGoForward()) + setIsLoading(newWebview.isLoading()) + } catch (error) { + console.error('Error getting webview state:', error) + + // 出错时使用存储的状态 + setUrl(newActiveTab.url) + setCurrentUrl(newActiveTab.url) + setCanGoBack(newActiveTab.canGoBack) + setCanGoForward(newActiveTab.canGoForward) + setIsLoading(newActiveTab.isLoading) + } + } else { + // 如果webview不存在,使用存储的状态 + setUrl(newActiveTab.url) + setCurrentUrl(newActiveTab.url) + setCanGoBack(newActiveTab.canGoBack) + setCanGoForward(newActiveTab.canGoForward) + setIsLoading(newActiveTab.isLoading) + } + + // 保存到本地存储 + saveTabsToStorage(tabs, newActiveTabId) } } // 更新选项卡信息 const updateTabInfo = (tabId: string, updates: Partial) => { - setTabs((prevTabs) => prevTabs.map((tab) => (tab.id === tabId ? { ...tab, ...updates } : tab))) + setTabs((prevTabs) => { + const newTabs = prevTabs.map((tab) => (tab.id === tabId ? { ...tab, ...updates } : tab)) + + // 保存到本地存储 + saveTabsToStorage(newTabs, activeTabId) + + return newTabs + }) } + // 在组件挂载和卸载时处理webview会话 + useEffect(() => { + // 组件挂载时,确保webviewSessionsRef与tabs同步 + tabs.forEach((tab) => { + if (!webviewSessionsRef.current[tab.id]) { + webviewSessionsRef.current[tab.id] = false + } + }) + + // 组件卸载时保存状态 + return () => { + saveTabsToStorage(tabs, activeTabId) + } + }, [tabs, activeTabId]) + // 检测Google登录页面 useEffect(() => { // 检测是否是Google登录页面 @@ -619,6 +1073,7 @@ const Browser = () => { } else { setShowGoogleLoginTip(false) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentUrl, activeTabId]) return ( @@ -667,7 +1122,7 @@ const Browser = () => {