返回 DeepSeek-Reasonix
electronGuestViews.ts
根目录 / desktop / electron / src / main / browser / electronGuestViews.ts
1 import { WebContentsView, session as electronSession, type BrowserWindow, type Session, type WebContents, type WebPreferences } from "electron";
2 import type { Logger } from "../log.js";
3 import { isBlockedNavigation, isPopupURL, type GuestView, type GuestViewEvents, type GuestViewFactory } from "./guestView.js";
4
5 const GUEST_PERMISSIONS = new Set(["clipboard-sanitized-write", "fullscreen"]);
6
7 export interface ElectronGuestViewDeps {
8 window(): BrowserWindow | null;
9 preloadPath: string;
10 log: Logger;
11 onSession?(partition: string, session: Session): void;
12 }
13
14 // Website views are untrusted: sandboxed, isolated, no Node, only the guest
15 // preload that reports user input. Every partition session gets the same
16 // permission policy the first time it is seen.
17 export class ElectronGuestViewFactory implements GuestViewFactory {
18 private readonly sessions = new Set<string>();
19
20 constructor(private readonly deps: ElectronGuestViewDeps) {}
21
22 create(partition: string, inherited?: WebPreferences): GuestView {
23 const win = this.deps.window();
24 if (!win) throw new Error("browser tabs need the main window");
25 this.prepareSession(partition);
26 const view = new WebContentsView({
27 webPreferences: {
28 ...inherited,
29 partition,
30 preload: this.deps.preloadPath,
31 sandbox: true,
32 contextIsolation: true,
33 nodeIntegration: false,
34 nodeIntegrationInSubFrames: false,
35 nodeIntegrationInWorker: false,
36 webviewTag: false,
37 spellcheck: true,
38 backgroundThrottling: false,
39 },
40 });
41 win.contentView.addChildView(view);
42 view.setVisible(false);
43 return new ElectronGuestView(view, win, this, this.deps.log);
44 }
45
46 private prepareSession(partition: string): void {
47 if (this.sessions.has(partition)) return;
48 this.sessions.add(partition);
49 const session = electronSession.fromPartition(partition);
50 session.setPermissionRequestHandler((_contents, permission, callback) => callback(GUEST_PERMISSIONS.has(permission)));
51 session.setPermissionCheckHandler((_contents, permission) => GUEST_PERMISSIONS.has(permission));
52 this.deps.onSession?.(partition, session);
53 }
54 }
55
56 class ElectronGuestView implements GuestView {
57 private destroyed = false;
58
59 constructor(
60 private readonly view: WebContentsView,
61 private readonly win: BrowserWindow,
62 private readonly factory: GuestViewFactory,
63 private readonly log: Logger,
64 ) {}
65
66 get page(): WebContents {
67 return this.view.webContents;
68 }
69
70 bind(events: GuestViewEvents): void {
71 const wc = this.view.webContents;
72 wc.on("did-start-loading", () => events.onStartLoading());
73 wc.on("did-stop-loading", () => events.onStopLoading());
74 wc.on("did-navigate", (_event, url) => events.onNavigate(url, false));
75 wc.on("did-navigate-in-page", (_event, url, isMainFrame) => {
76 if (isMainFrame) events.onNavigate(url, true);
77 });
78 wc.on("page-title-updated", (_event, title) => events.onTitle(title));
79 wc.on("did-fail-load", (_event, code, description, url, isMainFrame) => {
80 if (isMainFrame && code !== -3) events.onFailLoad(code, description, url);
81 });
82 wc.on("render-process-gone", (_event, details) => events.onRenderProcessGone(details.reason));
83 wc.on("destroyed", () => events.onDestroyed());
84 wc.on("will-navigate", (details) => this.guardNavigation(details, details.url));
85 wc.on("will-frame-navigate", (details) => this.guardNavigation(details, details.url));
86 wc.on("will-redirect", (details) => this.guardNavigation(details, details.url));
87 wc.on("will-attach-webview", (event) => event.preventDefault());
88 wc.setWindowOpenHandler(({ url, disposition }) => {
89 if (!isPopupURL(url)) return { action: "deny" };
90 const adopt = events.onPopup(url, disposition);
91 if (!adopt) return { action: "deny" };
92 return {
93 action: "allow",
94 createWindow: (options) => {
95 const child = this.factory.create(String(options.webPreferences?.partition ?? ""), options.webPreferences);
96 adopt(child);
97 return child.page as WebContents;
98 },
99 };
100 });
101 }
102
103 private guardNavigation(details: { preventDefault(): void }, url: string): void {
104 if (!isBlockedNavigation(url)) return;
105 details.preventDefault();
106 this.log.warn(`blocked browser navigation to ${url.slice(0, 120)}`);
107 }
108
109 setBounds(bounds: Electron.Rectangle): void {
110 if (!this.destroyed) this.view.setBounds(bounds);
111 }
112
113 setVisible(visible: boolean): void {
114 if (!this.destroyed) this.view.setVisible(visible);
115 }
116
117 // Detach first so the window never paints a closing view, then close the
118 // WebContents; the prototype's quit order that never left orphans.
119 destroy(): void {
120 if (this.destroyed) return;
121 this.destroyed = true;
122 if (!this.win.isDestroyed()) {
123 try {
124 this.win.contentView.removeChildView(this.view);
125 } catch (error) {
126 this.log.warn(`removeChildView failed: ${String(error)}`);
127 }
128 }
129 const wc = this.view.webContents;
130 if (!wc.isDestroyed()) wc.close();
131 }
132 }
133
133 lines TYPESCRIPT