返回 DeepSeek-Reasonix
remoteWindows.ts
根目录 / desktop / electron / src / main / remoteWindows.ts
1 import { BrowserWindow, session } from "electron";
2 import { errorText, type Logger } from "./log.js";
3
4 const HOST_KEY = /^[A-Za-z0-9._-]{1,128}$/;
5 const REMOTE_PERMISSIONS = new Set(["clipboard-read", "clipboard-sanitized-write", "fullscreen"]);
6
7 interface RemoteEntry {
8 win: BrowserWindow;
9 origin: string;
10 }
11
12 export interface RemoteWindowInput {
13 hostKey: string;
14 url: string;
15 title: string;
16 }
17
18 function remoteURL(value: string): URL {
19 const url = new URL(value);
20 if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`remote window URL must be http(s): ${value}`);
21 return url;
22 }
23
24 function sameOrigin(candidate: string, origin: string): boolean {
25 try {
26 return new URL(candidate).origin === origin;
27 } catch {
28 return false;
29 }
30 }
31
32 export class RemoteWindowHost {
33 private readonly windows = new Map<string, RemoteEntry>();
34
35 constructor(private readonly deps: { platform: NodeJS.Platform; icon?: string; log: Logger; onClosed?: (hostKey: string) => void }) {}
36
37 open(input: RemoteWindowInput): { windowId: string } {
38 if (!HOST_KEY.test(input.hostKey)) throw new Error("invalid remote window host key");
39 const target = remoteURL(input.url);
40 const existing = this.entry(input.hostKey);
41 if (existing) {
42 this.navigate(input);
43 existing.win.show();
44 return { windowId: String(existing.win.id) };
45 }
46 const partition = session.fromPartition(`persist:remote-${input.hostKey}`);
47 partition.setPermissionRequestHandler((_contents, permission, callback) => callback(REMOTE_PERMISSIONS.has(permission)));
48 const win = new BrowserWindow({
49 width: 1180,
50 height: 820,
51 minWidth: 760,
52 minHeight: 480,
53 show: false,
54 title: input.title || "Reasonix",
55 backgroundColor: "#1a1a2e",
56 autoHideMenuBar: true,
57 icon: this.deps.icon,
58 webPreferences: { session: partition, sandbox: true, contextIsolation: true, nodeIntegration: false, spellcheck: false },
59 });
60 const entry: RemoteEntry = { win, origin: target.origin };
61 this.windows.set(input.hostKey, entry);
62 if (this.deps.platform !== "darwin") win.setMenuBarVisibility(false);
63 win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
64 const guard = (event: { preventDefault(): void }, next: string) => {
65 if (sameOrigin(next, entry.origin)) return;
66 event.preventDefault();
67 this.deps.log.warn(`remote window ${input.hostKey}: blocked navigation to ${next}`);
68 };
69 win.webContents.on("will-navigate", guard);
70 win.webContents.on("will-redirect", guard);
71 win.on("closed", () => {
72 if (this.windows.get(input.hostKey) !== entry) return;
73 this.windows.delete(input.hostKey);
74 this.deps.onClosed?.(input.hostKey);
75 });
76 win.once("ready-to-show", () => win.show());
77 void win.loadURL(target.href).catch((error: unknown) => {
78 this.deps.log.warn(`remote window ${input.hostKey}: load failed: ${errorText(error)}`);
79 });
80 return { windowId: String(win.id) };
81 }
82
83 navigate(input: RemoteWindowInput): void {
84 const entry = this.entry(input.hostKey);
85 if (!entry) throw new Error(`no remote window for ${input.hostKey}`);
86 const target = remoteURL(input.url);
87 entry.origin = target.origin;
88 if (input.title) entry.win.setTitle(input.title);
89 void entry.win.loadURL(target.href).catch((error: unknown) => {
90 this.deps.log.warn(`remote window ${input.hostKey}: navigation failed: ${errorText(error)}`);
91 });
92 }
93
94 focus(hostKey: string): void {
95 const entry = this.entry(hostKey);
96 if (!entry) throw new Error(`no remote window for ${hostKey}`);
97 if (entry.win.isMinimized()) entry.win.restore();
98 entry.win.show();
99 entry.win.focus();
100 }
101
102 close(hostKey: string): void {
103 this.entry(hostKey)?.win.close();
104 }
105
106 closeAll(): void {
107 for (const entry of this.windows.values()) {
108 if (!entry.win.isDestroyed()) entry.win.destroy();
109 }
110 this.windows.clear();
111 }
112
113 private entry(hostKey: string): RemoteEntry | null {
114 const entry = this.windows.get(hostKey);
115 if (!entry) return null;
116 if (entry.win.isDestroyed()) {
117 this.windows.delete(hostKey);
118 return null;
119 }
120 return entry;
121 }
122 }
123
123 lines TYPESCRIPT