mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 04:31:27 +08:00
386 lines
17 KiB
Plaintext
386 lines
17 KiB
Plaintext
# 内置浏览器功能修改指南
|
||
|
||
本文档包含对内置浏览器功能的修改说明,包括:
|
||
1. 修复新标签页无法打开的问题
|
||
2. 处理登录弹窗 (HTTP 认证)
|
||
3. 增加切换链接打开方式 (新标签页/独立窗口) 的按钮和逻辑
|
||
|
||
---
|
||
|
||
## 1. 修复新标签页无法打开的问题
|
||
|
||
**问题原因:**
|
||
初步诊断发现,新创建的 webview 元素没有正确加载 URL,并且 `useWebviewEvents.ts` 文件中存在语法错误,导致事件监听器和链接点击拦截脚本未能正确执行。
|
||
|
||
**修改步骤:**
|
||
|
||
1. **修改 `src/renderer/src/pages/Browser/components/WebviewItem.tsx`:**
|
||
* 移除 `React.memo` 包裹,确保在父组件状态变化时 `WebviewItem` 总是重新渲染。
|
||
* 在 `<webview>` 元素的 `ref` 回调函数中,确保在获取到 webview 引用后,显式调用 `webview.loadURL(tab.url)` 来加载 URL。同时移除 `<webview>` 元素上的 `src` 属性,避免重复加载。
|
||
|
||
**需要修改的文件:** `src/renderer/src/pages/Browser/components/WebviewItem.tsx`
|
||
|
||
**修改内容 (使用 replace_in_file 格式):**
|
||
```
|
||
<<<<<<< SEARCH
|
||
export default React.memo(WebviewItem)
|
||
=======
|
||
export default WebviewItem
|
||
>>>>>>> REPLACE
|
||
|
||
<<<<<<< SEARCH
|
||
<webview
|
||
src={tab.url}
|
||
ref={(el: any) => {
|
||
if (el) {
|
||
// 保存webview引用到对应的tabId下
|
||
webviewRefs.current[tab.id] = el as WebviewTag
|
||
|
||
// 只有在尚未设置监听器时才设置
|
||
if (!hasSetupListenersRef.current) {
|
||
console.log(`[WebviewItem] Setting up listeners for tab: ${tab.id}`)
|
||
=======
|
||
<webview
|
||
ref={(el: any) => {
|
||
if (el) {
|
||
// 保存webview引用到对应的tabId下
|
||
webviewRefs.current[tab.id] = el as WebviewTag
|
||
|
||
// 只有在尚未设置监听器时才设置
|
||
if (!hasSetupListenersRef.current) {
|
||
console.log(`[WebviewItem] Setting up listeners for tab: ${tab.id}`)
|
||
|
||
// 显式加载URL
|
||
el.loadURL(tab.url);
|
||
>>>>>>> REPLACE
|
||
```
|
||
*(注意: 上述 diff 仅为示例,实际修改时请参考您当前文件的最新内容和格式进行调整。特别是移除 `src={tab.url}` 和添加 `el.loadURL(tab.url);`)*
|
||
|
||
2. **修改 `src/renderer/src/pages/Browser/hooks/useWebviewEvents.ts`:**
|
||
* 修复 `handleDomReady` 函数中注入的链接点击拦截脚本末尾多余的 `})();` 语法错误。
|
||
* 将本地变量 `ENABLE_BROWSER_EMULATION` 传递到注入的浏览器模拟脚本中,解决 ESLint 警告。
|
||
* 在 `handleNewWindow` 和 `handleConsoleMessage` 中添加日志,用于调试(可选,调试完成后可移除)。
|
||
* 在注入的链接点击拦截脚本中添加日志,用于调试(可选,调试完成后可移除)。
|
||
|
||
**需要修改的文件:** `src/renderer/src/pages/Browser/hooks/useWebviewEvents.ts`
|
||
|
||
**修改内容 (使用 replace_in_file 格式):**
|
||
```
|
||
<<<<<<< SEARCH
|
||
console.log('Link interceptor script injected successfully');
|
||
})();
|
||
console.log('Link interceptor script injected successfully');
|
||
})();
|
||
`)
|
||
|
||
// 注入浏览器模拟脚本 (在脚本内部检查 ENABLE_BROWSER_EMULATION)
|
||
webview.executeJavaScript(`
|
||
if (window.ENABLE_BROWSER_EMULATION) {
|
||
try {
|
||
// 覆盖navigator.userAgent
|
||
Object.defineProperty(navigator, 'userAgent', {
|
||
value: '${userAgent}',
|
||
writable: false
|
||
});
|
||
=======
|
||
console.log('Link interceptor script injected successfully');
|
||
})();
|
||
`)
|
||
|
||
// 注入浏览器模拟脚本
|
||
webview.executeJavaScript(`
|
||
(function() {
|
||
// 检查是否启用浏览器模拟
|
||
const ENABLE_BROWSER_EMULATION = ${ENABLE_BROWSER_EMULATION};
|
||
|
||
if (ENABLE_BROWSER_EMULATION) {
|
||
try {
|
||
// 覆盖navigator.userAgent
|
||
Object.defineProperty(navigator, 'userAgent', {
|
||
value: '${userAgent}',
|
||
writable: false
|
||
});
|
||
>>>>>>> REPLACE
|
||
```
|
||
*(注意: 上述 diff 仅为示例,实际修改时请参考您当前文件的最新内容和格式进行调整。特别是移除多余的 `})();` 和传递 `ENABLE_BROWSER_EMULATION` 变量)*
|
||
|
||
**预期结果:**
|
||
完成上述修改后,点击需要新标签页打开的链接应该能够成功创建一个新的标签页并加载对应的 URL。控制台应该能看到链接拦截脚本的日志。
|
||
|
||
---
|
||
|
||
## 2. 处理登录弹窗 (HTTP 认证)
|
||
|
||
**问题描述:**
|
||
当访问需要 HTTP 认证的网站时,内置浏览器没有弹出登录对话框。
|
||
|
||
**实现方案:**
|
||
Electron 的 `webview` 标签会触发 `show-login` 事件,当需要进行 HTTP 认证时。我们可以在 `useWebviewEvents.ts` 中监听这个事件,并通过 IPC 通道将认证请求发送到主进程。主进程可以显示一个原生的认证对话框,获取用户输入的用户名和密码,然后通过 IPC 将凭据返回给渲染进程,由渲染进程将凭据发送给 webview 进行认证。
|
||
|
||
**修改步骤:**
|
||
|
||
1. **在主进程中添加 IPC 处理:**
|
||
* 在主进程 (`src/main/index.ts` 或相关的 IPC 处理文件) 中,添加一个 IPC 监听器,例如 `ipcMain.handle('show-login-dialog', ...)`。
|
||
* 在这个处理函数中,使用 Electron 的 `dialog.showLoginDialog()` 方法显示认证对话框。
|
||
* 将对话框的结果(用户名和密码)通过 IPC 返回给渲染进程。
|
||
|
||
**需要修改的文件:** `src/main/index.ts` 或 IPC 处理文件
|
||
|
||
**示例代码 (主进程):**
|
||
```typescript
|
||
// src/main/index.ts 或 src/main/ipc.ts
|
||
import { ipcMain, dialog } from 'electron';
|
||
|
||
ipcMain.handle('show-login-dialog', async (event, args) => {
|
||
const { url, realm } = args;
|
||
const result = await dialog.showLoginDialog({
|
||
title: 'Authentication Required',
|
||
text: `Enter credentials for ${url}`,
|
||
message: `Realm: ${realm}`,
|
||
});
|
||
return result; // { username, password, response }
|
||
});
|
||
```
|
||
|
||
2. **在渲染进程中添加 IPC 调用和 `show-login` 事件处理:**
|
||
* 在 `src/renderer/src/pages/Browser/hooks/useWebviewEvents.ts` 中,添加 `show-login` 事件监听器。
|
||
* 在 `handleShowLogin` 函数中,通过 `window.api.invoke('show-login-dialog', { url: e.url, realm: e.realm })` 调用主进程的认证对话框。
|
||
* 获取对话框结果后,使用 `e.login(username, password)` 将凭据发送给 webview。
|
||
|
||
**需要修改的文件:** `src/renderer/src/pages/Browser/hooks/useWebviewEvents.ts`
|
||
|
||
**示例代码 (渲染进程 - `useWebviewEvents.ts`):**
|
||
```typescript
|
||
// 在 setupWebviewListeners 函数内部添加
|
||
const handleShowLogin = async (e: any) => {
|
||
console.log(`[Tab ${tabId}] Show login dialog for url: ${e.url}, realm: ${e.realm}`);
|
||
e.preventDefault(); // 阻止默认行为
|
||
|
||
try {
|
||
// 调用主进程显示认证对话框
|
||
const result = await window.api.invoke('show-login-dialog', { url: e.url, realm: e.realm });
|
||
|
||
if (result && result.response === 0) { // 0 表示用户点击了登录
|
||
// 将凭据发送给webview
|
||
e.login(result.username, result.password);
|
||
} else {
|
||
// 用户取消或关闭对话框
|
||
e.cancel();
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to show login dialog:', error);
|
||
e.cancel(); // 发生错误时取消认证
|
||
}
|
||
};
|
||
|
||
// 在添加事件监听器的部分添加
|
||
webview.addEventListener('show-login', handleShowLogin);
|
||
|
||
// 在清理函数中添加移除监听器
|
||
return () => {
|
||
// ... 其他移除监听器 ...
|
||
webview.removeEventListener('show-login', handleShowLogin);
|
||
};
|
||
```
|
||
*(注意: 上述示例代码假设您已经设置了 Electron 的 Context Bridge,并且在预加载脚本中将 `ipcRenderer.invoke` 暴露给了 `window.api`。如果您的 IPC 设置不同,请根据实际情况调整。)*
|
||
|
||
**预期结果:**
|
||
当访问需要 HTTP 认证的网站时,应该会弹出一个原生的登录对话框,用户输入凭据后可以进行认证。
|
||
|
||
---
|
||
|
||
## 3. 增加切换链接打开方式 (新标签页/独立窗口) 的按钮和逻辑
|
||
|
||
**问题描述:**
|
||
目前点击链接默认在新标签页打开,用户希望能够切换为在独立窗口中打开。
|
||
|
||
**实现方案:**
|
||
1. 在浏览器界面的工具栏中添加一个按钮,用于切换链接打开方式的状态。
|
||
2. 在状态管理中维护一个状态,记录当前的链接打开方式(例如 'newTab' 或 'newWindow')。
|
||
3. 修改链接点击拦截脚本和 `handleNewWindow` 函数,根据当前状态决定是调用 `openUrlInTab` 还是通过 IPC 调用主进程打开新窗口。
|
||
4. 在主进程中添加一个 IPC 处理函数,用于创建新的浏览器窗口并加载指定的 URL。
|
||
|
||
**修改步骤:**
|
||
|
||
1. **在状态管理中添加链接打开方式状态:**
|
||
* 在 `src/renderer/src/pages/Browser/hooks/useAnimatedTabs.ts` 或创建一个新的 Context 中,添加一个状态来存储当前的链接打开方式,例如 `linkOpenMode`,默认值为 `'newTab'`。
|
||
* 添加一个函数来切换这个状态,例如 `toggleLinkOpenMode`。
|
||
|
||
**需要修改的文件:** `src/renderer/src/pages/Browser/hooks/useAnimatedTabs.ts` (或新文件)
|
||
|
||
**示例代码 (useAnimatedTabs.ts):**
|
||
```typescript
|
||
// 在 useAnimatedTabs 钩子内部添加状态和切换函数
|
||
const [linkOpenMode, setLinkOpenMode] = useState<'newTab' | 'newWindow'>('newTab');
|
||
|
||
const toggleLinkOpenMode = useCallback(() => {
|
||
setLinkOpenMode(prevMode => prevMode === 'newTab' ? 'newWindow' : 'newTab');
|
||
}, []);
|
||
|
||
// 在返回的对象中暴露 linkOpenMode 和 toggleLinkOpenMode
|
||
return {
|
||
// ... 其他状态和函数 ...
|
||
linkOpenMode,
|
||
toggleLinkOpenMode,
|
||
};
|
||
```
|
||
|
||
2. **在 UI 中添加切换按钮:**
|
||
* 在浏览器工具栏组件 (`src/renderer/src/pages/Browser/components/NavBar.tsx`) 中,添加一个按钮。
|
||
* 按钮的文本或图标可以根据 `linkOpenMode` 状态变化。
|
||
* 按钮的点击事件调用 `toggleLinkOpenMode` 函数。
|
||
|
||
**需要修改的文件:** `src/renderer/src/pages/Browser/components/NavBar.tsx`
|
||
|
||
**示例代码 (NavBar.tsx):**
|
||
```typescript
|
||
// 假设 NavBar 组件接收 linkOpenMode 和 toggleLinkOpenMode 作为 props
|
||
interface NavBarProps {
|
||
// ... 其他 props ...
|
||
linkOpenMode: 'newTab' | 'newWindow';
|
||
toggleLinkOpenMode: () => void;
|
||
}
|
||
|
||
const NavBar: React.FC<NavBarProps> = ({ /* ... */ linkOpenMode, toggleLinkOpenMode }) => {
|
||
return (
|
||
<NavBarContainer>
|
||
{/* ... 其他工具栏元素 ... */}
|
||
<button onClick={toggleLinkOpenMode}>
|
||
{linkOpenMode === 'newTab' ? '新标签页模式' : '独立窗口模式'}
|
||
</button>
|
||
{/* ... 其他工具栏元素 ... */}
|
||
</NavBarContainer>
|
||
);
|
||
};
|
||
```
|
||
*(注意: 您需要将 `linkOpenMode` 和 `toggleLinkOpenMode` 从 `useAnimatedTabs` (或新 Context) 传递到 `NavBar` 组件。)*
|
||
|
||
3. **修改链接点击处理逻辑:**
|
||
* 在 `src/renderer/src/pages/Browser/hooks/useWebviewEvents.ts` 的 `handleConsoleMessage` 函数中,当处理 `LINK_CLICKED:` 消息时,根据当前的 `linkOpenMode` 状态决定是调用 `openUrlInTab` 还是触发 IPC 调用打开新窗口。
|
||
* 在 `handleNewWindow` 函数中,也需要根据 `linkOpenMode` 状态决定行为。
|
||
|
||
**需要修改的文件:** `src/renderer/src/pages/Browser/hooks/useWebviewEvents.ts`
|
||
|
||
**示例代码 (useWebviewEvents.ts - handleConsoleMessage):**
|
||
```typescript
|
||
// 在 setupWebviewListeners 函数签名中添加 linkOpenMode 参数
|
||
const setupWebviewListeners = (
|
||
// ... 其他参数 ...
|
||
linkOpenMode: 'newTab' | 'newWindow', // 添加 linkOpenMode 参数
|
||
// ... 其他参数 ...
|
||
) => {
|
||
// ...
|
||
|
||
const handleConsoleMessage = (event: any) => {
|
||
// ... (现有日志和 LINK_CLICKED 处理逻辑) ...
|
||
|
||
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)
|
||
|
||
// 根据 linkOpenMode 决定打开方式
|
||
if (linkOpenMode === 'newTab' && data.url && data.inNewTab) {
|
||
console.log(`[Tab ${tabId}] Opening link in new tab:`, data.url)
|
||
openUrlInTab(data.url, true, data.title || data.url)
|
||
} else if (linkOpenMode === 'newWindow' && data.url) {
|
||
console.log(`[Tab ${tabId}] Opening link in new window:`, data.url)
|
||
// 调用主进程打开新窗口 (需要实现 IPC)
|
||
window.api.invoke('open-new-browser-window', { url: data.url, title: data.title || data.url });
|
||
} else if (data.url && !data.inNewTab) {
|
||
// 在当前标签页打开 (如果不是新标签页模式且链接没有 target="_blank")
|
||
// 这个逻辑已经在注入的脚本中处理了 window.location.href = target.href;
|
||
// 这里可以根据需要添加额外的处理或日志
|
||
console.log(`[Tab ${tabId}] Link clicked, navigating in current tab:`, data.url);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Failed to parse link data:', error)
|
||
}
|
||
}
|
||
|
||
// ... (保留对旧消息格式的支持) ...
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**示例代码 (useWebviewEvents.ts - handleNewWindow):**
|
||
```typescript
|
||
// 在 setupWebviewListeners 函数签名中添加 linkOpenMode 参数 (如果尚未添加)
|
||
const setupWebviewListeners = (
|
||
// ... 其他参数 ...
|
||
linkOpenMode: 'newTab' | 'newWindow', // 确保 linkOpenMode 参数存在
|
||
// ... 其他参数 ...
|
||
) => {
|
||
// ...
|
||
|
||
// 处理新窗口打开请求
|
||
const handleNewWindow = (e: any) => {
|
||
console.log(`[Tab ${tabId}] handleNewWindow called for url: ${e.url}`)
|
||
e.preventDefault() // 阻止默认行为
|
||
|
||
console.log(`[Tab ${tabId}] New window request: ${e.url}, frameName: ${e.frameName || '未指定'}`)
|
||
|
||
// 根据 linkOpenMode 决定打开方式
|
||
if (linkOpenMode === 'newTab') {
|
||
// 始终在新标签页中打开
|
||
openUrlInTab(e.url, true, e.frameName || '加载中...')
|
||
} else if (linkOpenMode === 'newWindow') {
|
||
// 调用主进程打开新窗口 (需要实现 IPC)
|
||
window.api.invoke('open-new-browser-window', { url: e.url, title: e.frameName || e.url });
|
||
}
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
*(注意: 您需要将 `linkOpenMode` 从使用 `useAnimatedTabs` 的组件传递到 `setupWebviewListeners` 函数中。)*
|
||
|
||
4. **在主进程中添加打开新窗口的 IPC 处理:**
|
||
* 在主进程 (`src/main/index.ts` 或相关的 IPC 处理文件) 中,添加一个 IPC 监听器,例如 `ipcMain.handle('open-new-browser-window', ...)`。
|
||
* 在这个处理函数中,创建一个新的 Electron 浏览器窗口 (`new BrowserWindow(...)`) 并加载指定的 URL。
|
||
|
||
**需要修改的文件:** `src/main/index.ts` 或 IPC 处理文件
|
||
|
||
**示例代码 (主进程):**
|
||
```typescript
|
||
// src/main/index.ts 或 src/main/ipc.ts
|
||
import { ipcMain, BrowserWindow } from 'electron';
|
||
import { join } from 'path';
|
||
|
||
ipcMain.handle('open-new-browser-window', async (event, args) => {
|
||
const { url, title } = args;
|
||
|
||
// 创建新的浏览器窗口
|
||
const newWindow = new BrowserWindow({
|
||
width: 1000,
|
||
height: 800,
|
||
title: title || 'New Window',
|
||
webPreferences: {
|
||
preload: join(__dirname, '../preload/index.js'), // 根据您的项目结构调整预加载脚本路径
|
||
sandbox: false, // 根据您的安全需求调整
|
||
nodeIntegration: false, // 根据您的安全需求调整
|
||
contextIsolation: true, // 根据您的安全需求调整
|
||
},
|
||
});
|
||
|
||
// 加载URL
|
||
newWindow.loadURL(url);
|
||
|
||
// 可选: 打开开发者工具
|
||
// newWindow.webContents.openDevTools();
|
||
});
|
||
```
|
||
*(注意: 您需要根据您的项目结构调整 `preload` 路径和 `webPreferences` 设置。)*
|
||
|
||
**预期结果:**
|
||
浏览器工具栏中会出现一个切换按钮,点击可以切换链接打开方式。根据当前模式,点击链接会在新标签页或独立窗口中打开。
|
||
|
||
---
|
||
|
||
希望这份修改指南对您有帮助!如果您在修改过程中遇到任何问题,或者需要进一步的帮助,请随时告诉我。
|