返回 DeepSeek-Reasonix
browserControl.ts
根目录 / desktop / electron / src / main / browserControl.ts
1 import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2 import { dirname, join } from "node:path";
3 import type { BrowserControlState, BrowserControlWarning } from "../shared/ipc.js";
4
5 export interface BrowserControlBootstrap {
6 state: BrowserControlState;
7 configPath: string;
8 }
9
10 export interface BrowserControlPatch {
11 controlEnabled?: boolean;
12 ignoreCertificateErrors?: boolean;
13 }
14
15 type Stored = { version: 1; controlEnabled: boolean; ignoreCertificateErrors: boolean };
16
17 // Cache-only clearing keeps cookies and local storage, so a signed-in site stays
18 // signed in; "all" additionally drops every storage type Electron knows.
19 export const BROWSER_CACHE_STORAGES: readonly string[] = ["shadercache", "cachestorage", "serviceworkers"];
20
21 // The subset of Electron's Session the browser policy needs. Structural so the
22 // unit tests can drive it with a fake instead of a live shell.
23 export interface BrowserSession {
24 setCertificateVerifyProc(proc: ((request: unknown, callback: (result: number) => void) => void) | null): void;
25 clearCache(): Promise<void>;
26 clearStorageData(options?: { storages?: string[] }): Promise<void>;
27 }
28
29 function parseStored(path: string): { value: Stored | null; warning: BrowserControlWarning | null } {
30 if (!existsSync(path)) return { value: null, warning: null };
31 try {
32 const parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
33 if (parsed.version !== 1) return { value: null, warning: "unsupported-version" };
34 if (typeof parsed.controlEnabled !== "boolean" || typeof parsed.ignoreCertificateErrors !== "boolean") {
35 return { value: null, warning: "invalid-config" };
36 }
37 return { value: parsed as Stored, warning: null };
38 } catch {
39 return { value: null, warning: "unreadable-config" };
40 }
41 }
42
43 function defaultStored(): Stored {
44 // Control defaults on: the desktop browser is already the agent's capability,
45 // so hiding it behind an opt-in would change existing behaviour.
46 return { version: 1, controlEnabled: true, ignoreCertificateErrors: false };
47 }
48
49 export function loadBrowserControlBootstrap(dataHome: string): BrowserControlBootstrap {
50 const configPath = join(dataHome, "browser-control.json");
51 const parsed = parseStored(configPath);
52 const stored = parsed.value ?? defaultStored();
53 return {
54 configPath,
55 state: {
56 controlEnabled: stored.controlEnabled,
57 ignoreCertificateErrors: stored.ignoreCertificateErrors,
58 writable: parsed.warning !== "unsupported-version",
59 warning: parsed.warning,
60 },
61 };
62 }
63
64 export class BrowserControlStore {
65 private stored: Stored;
66 private state: BrowserControlState;
67 private writeQueue: Promise<void> = Promise.resolve();
68
69 constructor(private readonly path: string, bootstrap: BrowserControlBootstrap) {
70 this.stored = parseStored(path).value ?? defaultStored();
71 this.state = bootstrap.state;
72 }
73
74 get current(): BrowserControlState {
75 return this.state;
76 }
77
78 setControlEnabled(enabled: boolean): Promise<BrowserControlState> {
79 return this.patch({ controlEnabled: enabled });
80 }
81
82 setIgnoreCertificateErrors(enabled: boolean): Promise<BrowserControlState> {
83 return this.patch({ ignoreCertificateErrors: enabled });
84 }
85
86 private async patch(patch: BrowserControlPatch): Promise<BrowserControlState> {
87 for (const [key, value] of Object.entries(patch)) {
88 if (typeof value !== "boolean") throw new Error(`${key} must be boolean`);
89 }
90 if (!this.state.writable) throw new Error("browser control settings are read-only");
91 const next: Stored = { ...this.stored, ...patch, version: 1 };
92 const write = this.writeQueue.catch(() => undefined).then(() => {
93 mkdirSync(dirname(this.path), { recursive: true });
94 const existing = parseStored(this.path);
95 if (existing.warning) {
96 let backup = `${this.path}.invalid`;
97 let suffix = 1;
98 while (existsSync(backup)) backup = `${this.path}.invalid.${suffix++}`;
99 renameSync(this.path, backup);
100 }
101 const temp = `${this.path}.tmp`;
102 writeFileSync(temp, `${JSON.stringify(next)}\n`, "utf8");
103 renameSync(temp, this.path);
104 });
105 this.writeQueue = write.catch(() => undefined);
106 await write;
107 this.stored = next;
108 this.state = { ...this.state, ...patch, warning: null };
109 return this.state;
110 }
111 }
112
113 // Certificate policy belongs to the browser partition alone; the app window and
114 // every other Electron session keep Chromium's default verification.
115 export function applyBrowserSessionPolicy(target: BrowserSession, ignoreCertificateErrors: boolean): void {
116 if (!ignoreCertificateErrors) {
117 target.setCertificateVerifyProc(null);
118 return;
119 }
120 target.setCertificateVerifyProc((_request, callback) => callback(0));
121 }
122
123 export async function clearBrowserCache(target: BrowserSession): Promise<void> {
124 await target.clearCache();
125 await target.clearStorageData({ storages: [...BROWSER_CACHE_STORAGES] });
126 }
127
128 export async function clearBrowserData(target: BrowserSession): Promise<void> {
129 await target.clearCache();
130 await target.clearStorageData();
131 }
132
132 lines TYPESCRIPT