fix: close and recreate macOS windows safely

This commit is contained in:
鲨鱼辣椒
2026-09-29 19:50:25 +08:00
parent f443f3bc66
commit b5e6c663ef
19 changed files with 1079 additions and 172 deletions

View File

@@ -1,6 +1,7 @@
import type { BrowserWindow, Session, WebContents } from 'electron';
import { WebContentsView, session } from 'electron';
import type { AgentBrowserBounds } from '../../shared/agent-browser';
import { AgentBrowserFault } from './fault';
import type {
AgentBrowserAdapter,
AgentBrowserDebuggerPort,
@@ -88,11 +89,18 @@ function wrapWebContents(contents: WebContents, isVisible: () => boolean): Agent
export class ElectronAgentBrowserAdapter implements AgentBrowserAdapter {
private readonly nativeViews = new WeakMap<AgentBrowserViewPort, WebContentsView>();
private readonly viewOwners = new WeakMap<AgentBrowserViewPort, BrowserWindow>();
private readonly guardedSessions = new WeakSet<Session>();
constructor(private readonly mainWindow: BrowserWindow) {}
constructor(private readonly mainWindow: BrowserWindow | (() => BrowserWindow | null)) {}
private currentWindow(): BrowserWindow | null {
const window = typeof this.mainWindow === 'function' ? this.mainWindow() : this.mainWindow;
return window && !window.isDestroyed() ? window : null;
}
createView(partition: string): AgentBrowserViewPort {
const owner = this.currentWindow();
const nativeView = new WebContentsView({
webPreferences: {
partition,
@@ -119,25 +127,29 @@ export class ElectronAgentBrowserAdapter implements AgentBrowserAdapter {
const view: AgentBrowserViewPort = {
webContents: wrapWebContents(contents, () => nativeView.getVisible()),
setBounds: (bounds: AgentBrowserBounds) =>
nativeView.setBounds(this.toNativeBounds(bounds)),
nativeView.setBounds(this.toNativeBounds(bounds, owner)),
setVisible: (visible: boolean) => nativeView.setVisible(visible),
};
this.nativeViews.set(view, nativeView);
if (owner) this.viewOwners.set(view, owner);
return view;
}
mount(view: AgentBrowserViewPort): void {
const nativeView = this.requireNativeView(view);
if (!this.mainWindow.isDestroyed()) {
this.mainWindow.contentView.addChildView(nativeView);
const owner = this.viewOwners.get(view);
if (!owner || owner.isDestroyed()) {
throw new AgentBrowserFault('CLOSED', '请先打开麦洛窗口,再查看作品。', true);
}
owner.contentView.addChildView(nativeView);
}
unmount(view: AgentBrowserViewPort): void {
const nativeView = this.nativeViews.get(view);
if (!nativeView || this.mainWindow.isDestroyed()) return;
const owner = this.viewOwners.get(view);
if (!nativeView || !owner || owner.isDestroyed()) return;
try {
this.mainWindow.contentView.removeChildView(nativeView);
owner.contentView.removeChildView(nativeView);
} catch {
// Removing an already detached view is harmless during shutdown.
}
@@ -147,6 +159,7 @@ export class ElectronAgentBrowserAdapter implements AgentBrowserAdapter {
const nativeView = this.nativeViews.get(view);
if (!nativeView) return;
this.nativeViews.delete(view);
this.viewOwners.delete(view);
if (!nativeView.webContents.isDestroyed()) {
nativeView.webContents.close({ waitForBeforeUnload: false });
}
@@ -201,10 +214,12 @@ export class ElectronAgentBrowserAdapter implements AgentBrowserAdapter {
});
}
private toNativeBounds(bounds: AgentBrowserBounds): AgentBrowserBounds {
const zoomFactor = this.mainWindow.webContents.getZoomFactor();
private toNativeBounds(bounds: AgentBrowserBounds, owner: BrowserWindow | null): AgentBrowserBounds {
// Detached release preflight views also work while there is no main window.
if (!owner || owner.isDestroyed()) return bounds;
const zoomFactor = owner.webContents.getZoomFactor();
const zoom = Number.isFinite(zoomFactor) && zoomFactor > 0 ? zoomFactor : 1;
const contentBounds = this.mainWindow.getContentBounds();
const contentBounds = owner.getContentBounds();
const contentWidth = Math.max(0, Math.trunc(contentBounds.width));
const contentHeight = Math.max(0, Math.trunc(contentBounds.height));
const x = clamp(Math.round(bounds.x * zoom), 0, contentWidth);