返回 DeepSeek-Reasonix
snapshot.ts
根目录 / desktop / electron / src / main / browser / snapshot.ts
1 import { randomToken, type DocumentRegistry, type FrameBinding } from "./documents.js";
2 import type { GuestFrame, GuestPage } from "./guestView.js";
3 import { scriptCall, SNAPSHOT_SCRIPT_SOURCE } from "./pageScripts.js";
4 import type { SnapshotOutput } from "./snapshotScript.js";
5 import { FrameRuntime, runChildFrame } from "./frameRuntime.js";
6 import { browserFailure } from "./errors.js";
7
8 export const REGISTRY_KEY = "__reasonixBrowserRegistry";
9 export const ISOLATED_WORLD = 1;
10 export const MAX_SNAPSHOT_NODES = 4000;
11
12 export interface SnapshotResult {
13 documentToken: string;
14 url: string;
15 title: string;
16 tree: string;
17 refs: number;
18 }
19
20 // The main frame uses Electron's isolated world; children use a verified CDP
21 // isolated context, with an owned session for out-of-process iframes.
22 export function runInFrame(page: GuestPage, frame: GuestFrame, code: string, runtime?: FrameRuntime): Promise<unknown> {
23 if (frame.frameTreeNodeId === page.mainFrame.frameTreeNodeId) return page.executeJavaScriptInIsolatedWorld(ISOLATED_WORLD, [{ code }]);
24 return runtime ? runtime.run(frame, code) : runChildFrame(page, frame, code);
25 }
26
27 export function findFrame(page: GuestPage, frameTreeNodeId: number): GuestFrame | null {
28 const main = page.mainFrame;
29 if (main.frameTreeNodeId === frameTreeNodeId) return main;
30 for (const frame of main.framesInSubtree) {
31 if (frame.frameTreeNodeId === frameTreeNodeId && !frame.detached) return frame;
32 }
33 return null;
34 }
35
36 function isSnapshotOutput(value: unknown): value is SnapshotOutput {
37 if (typeof value !== "object" || value === null) return false;
38 const out = value as Record<string, unknown>;
39 return typeof out.docId === "string" && typeof out.tree === "string" && typeof out.refs === "number" && typeof out.nodes === "number";
40 }
41
42 function clipURL(url: string): string {
43 return url.length > 120 ? `${url.slice(0, 119)}…` : url;
44 }
45
46 function indent(tree: string): string {
47 return tree.split("\n").map((line) => ` ${line}`).join("\n");
48 }
49
50 async function within<T>(promise: Promise<T>, milliseconds: number): Promise<T> {
51 let timer: ReturnType<typeof setTimeout> | undefined;
52 try { return await Promise.race([promise, new Promise<never>((_resolve, reject) => { timer = setTimeout(() => reject(browserFailure("page_not_ready", "DOM observation deadline exceeded")), Math.max(1, milliseconds)); })]); }
53 finally { if (timer) clearTimeout(timer); }
54 }
55
56 export async function takeSnapshot(page: GuestPage, tabId: string, epoch: number, selector: string, documents: DocumentRegistry, verify: () => void = () => {}): Promise<SnapshotResult> {
57 const snapshotId = randomToken(8);
58 const main = page.mainFrame;
59 let mainRaw: unknown;
60 try { mainRaw = await within(runInFrame(page, main, scriptCall(SNAPSHOT_SCRIPT_SOURCE, { key: REGISTRY_KEY, snapshotId, prefix: "", selector, budget: MAX_SNAPSHOT_NODES })), 3000); }
61 catch (error) { if (typeof error === "object" && error !== null && "code" in error) throw error; throw browserFailure("script_runtime_error", String(error).slice(0, 500)); }
62 if (!isSnapshotOutput(mainRaw)) throw new Error("snapshot script returned no tree");
63 const frames: FrameBinding[] = [{ prefix: "", frameTreeNodeId: main.frameTreeNodeId, docId: mainRaw.docId }];
64 const sections = [mainRaw.tree];
65 let refs = mainRaw.refs;
66 let budget = MAX_SNAPSHOT_NODES - mainRaw.nodes;
67 let index = 0;
68 const childrenDeadline = Date.now() + 1000;
69 const children = main.framesInSubtree.filter(frame => frame.frameTreeNodeId !== main.frameTreeNodeId && !frame.detached);
70 const runtime = children.length ? new FrameRuntime(page, childrenDeadline) : undefined;
71 try {
72 for (const frame of children) {
73 if (budget <= 0 || Date.now() >= childrenDeadline) break;
74 index += 1;
75 const prefix = `f${index}`;
76 let raw: unknown;
77 try {
78 raw = await runInFrame(page, frame, scriptCall(SNAPSHOT_SCRIPT_SOURCE, { key: REGISTRY_KEY, snapshotId, prefix, selector: "", budget }), runtime);
79 } catch (error) {
80 sections.push(`frame ${prefix} unavailable: ${error instanceof Error ? error.message.slice(0, 200) : "isolated runtime unavailable"}`);
81 continue;
82 }
83 if (!isSnapshotOutput(raw)) continue;
84 frames.push({ prefix, frameTreeNodeId: frame.frameTreeNodeId, docId: raw.docId });
85 refs += raw.refs;
86 budget -= raw.nodes;
87 if (raw.tree === "") continue;
88 sections.push(`frame ${prefix} ${JSON.stringify(clipURL(frame.url))}\n${indent(raw.tree)}`);
89 }
90 } finally { await runtime?.close(); }
91 verify();
92 const documentToken = documents.issue({ tabId, epoch, snapshotId, frames });
93 return { documentToken, url: page.getURL(), title: page.getTitle(), tree: sections.join("\n"), refs };
94 }
95
95 lines TYPESCRIPT