返回 DeepSeek-Reasonix
pageScripts.ts
根目录 / desktop / electron / src / main / browser / pageScripts.ts
1 /// <reference lib="dom" />
2
3 // Page-side helpers for actions. Serialised with Function.prototype.toString
4 // like the snapshot walker: self-contained, no module-scope references.
5
6 export interface RefInput {
7 key: string;
8 snapshotId: string;
9 docId: string;
10 ref: string;
11 }
12
13 export interface ResolveInput extends RefInput {
14 scroll: boolean;
15 }
16
17 export interface ResolvedElement {
18 ok: true;
19 x: number;
20 y: number;
21 width: number;
22 height: number;
23 tag: string;
24 type: string;
25 disabled: boolean;
26 editable: boolean;
27 frameOffsetKnown: boolean;
28 }
29
30 export interface ResolveFailure {
31 ok: false;
32 reason: string;
33 }
34
35 export type ResolveOutput = ResolvedElement | ResolveFailure;
36
37 interface Registry {
38 docId: string;
39 snapshotId: string;
40 refs: Map<string, Element>;
41 }
42
43 // scroll:true brings the element to the viewport centre first; the returned
44 // rect is in top-document CSS pixels when every enclosing frame is
45 // same-origin, otherwise frameOffsetKnown is false and the caller refuses.
46 export function pageResolve(input: ResolveInput): ResolveOutput {
47 const registry = (window as unknown as Record<string, unknown>)[input.key] as Registry | undefined;
48 if (!registry || registry.docId !== input.docId || registry.snapshotId !== input.snapshotId) return { ok: false, reason: "stale" };
49 const el = registry.refs.get(input.ref);
50 if (!el) return { ok: false, reason: "stale" };
51 if (!el.isConnected) return { ok: false, reason: "element is no longer in the document" };
52 const style = window.getComputedStyle(el);
53 if (style.display === "none" || style.visibility === "hidden") return { ok: false, reason: "element is hidden" };
54 if (input.scroll) {
55 try {
56 el.scrollIntoView({ block: "center", inline: "center" });
57 } catch {
58 // Detached or non-scrollable ancestors; the rect check below decides.
59 }
60 }
61 const rect = el.getBoundingClientRect();
62 if (rect.width === 0 || rect.height === 0) return { ok: false, reason: "element has no size" };
63 const cx = rect.left + rect.width / 2;
64 const cy = rect.top + rect.height / 2;
65 if (cx < 0 || cy < 0 || cx > window.innerWidth || cy > window.innerHeight) return { ok: false, reason: "element is outside the viewport" };
66 if (el.tagName !== "OPTION") {
67 const hit = document.elementFromPoint(cx, cy);
68 if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) return { ok: false, reason: "element is covered by another element" };
69 }
70 let dx = 0;
71 let dy = 0;
72 let frameOffsetKnown = true;
73 try {
74 let win: Window = window;
75 while (win !== win.top) {
76 const owner = win.frameElement;
77 if (!owner) {
78 frameOffsetKnown = false;
79 break;
80 }
81 const ownerRect = owner.getBoundingClientRect();
82 dx += ownerRect.left + owner.clientLeft;
83 dy += ownerRect.top + owner.clientTop;
84 win = win.parent;
85 }
86 } catch {
87 frameOffsetKnown = false;
88 }
89 const tag = el.tagName.toLowerCase();
90 const type = tag === "input" ? (el as HTMLInputElement).type.toLowerCase() : "";
91 const editable = tag === "textarea" || (el as HTMLElement).isContentEditable || (tag === "input" && !["button", "submit", "reset", "image", "checkbox", "radio", "file", "range", "color"].includes(type));
92 return {
93 ok: true,
94 x: rect.left + dx,
95 y: rect.top + dy,
96 width: rect.width,
97 height: rect.height,
98 tag,
99 type,
100 disabled: (el as HTMLButtonElement).disabled === true,
101 editable,
102 frameOffsetKnown,
103 };
104 }
105
106 export interface SelectInput extends RefInput {
107 options: string[];
108 }
109
110 export type SelectOutput = { ok: true; selected: string[] } | ResolveFailure;
111
112 // The one action that synthesises DOM events: a <select> has no trustworthy
113 // pointer path (its popup is native), so options are toggled directly and
114 // input/change are dispatched the way Chromium itself does after a pick.
115 export function pageSelect(input: SelectInput): SelectOutput {
116 const registry = (window as unknown as Record<string, unknown>)[input.key] as Registry | undefined;
117 if (!registry || registry.docId !== input.docId || registry.snapshotId !== input.snapshotId) return { ok: false, reason: "stale" };
118 const target = registry.refs.get(input.ref);
119 if (!target) return { ok: false, reason: "stale" };
120 const select = target.tagName === "SELECT" ? (target as HTMLSelectElement) : target.tagName === "OPTION" ? (target as HTMLOptionElement).closest("select") : null;
121 if (!select) return { ok: false, reason: "element is not a <select>" };
122 if (select.disabled) return { ok: false, reason: "select is disabled" };
123 const wanted = input.options.length ? input.options : target.tagName === "OPTION" ? [(target as HTMLOptionElement).value] : [];
124 const matches = [...select.options].filter((option) => wanted.includes(option.value) || wanted.includes(option.label) || wanted.includes(option.text.trim()));
125 if (matches.length === 0) return { ok: false, reason: "no option matches the requested values" };
126 const chosen = select.multiple ? matches : [matches[0]];
127 for (const option of select.options) option.selected = chosen.includes(option);
128 select.dispatchEvent(new Event("input", { bubbles: true }));
129 select.dispatchEvent(new Event("change", { bubbles: true }));
130 return { ok: true, selected: chosen.map((option) => option.value) };
131 }
132
133 export function pageFocus(input: RefInput): boolean {
134 const registry = (window as unknown as Record<string, unknown>)[input.key] as Registry | undefined;
135 if (!registry || registry.docId !== input.docId || registry.snapshotId !== input.snapshotId) return false;
136 const target = registry.refs.get(input.ref);
137 if (!target || !target.isConnected) return false;
138 (target as HTMLElement).focus({ preventScroll: true });
139 return document.activeElement === target;
140 }
141
142 export type LocateOutput = { ok: true; tag: string; type: string; path: string } | ResolveFailure;
143
144 // Locates a ref without visibility checks: uploads target hidden file inputs
145 // and the CSS path lets the DevTools protocol find the same node.
146 export function pageLocate(input: RefInput): LocateOutput {
147 const registry = (window as unknown as Record<string, unknown>)[input.key] as Registry | undefined;
148 if (!registry || registry.docId !== input.docId || registry.snapshotId !== input.snapshotId) return { ok: false, reason: "stale" };
149 const el = registry.refs.get(input.ref);
150 if (!el) return { ok: false, reason: "stale" };
151 if (!el.isConnected) return { ok: false, reason: "element is no longer in the document" };
152 const path: string[] = [];
153 let current: Element | null = el;
154 while (current && current !== document.documentElement) {
155 const parent: Element | null = current.parentElement;
156 if (!parent) break;
157 const index = [...parent.children].indexOf(current) + 1;
158 path.unshift(`${current.tagName.toLowerCase()}:nth-child(${index})`);
159 current = parent;
160 }
161 const tag = el.tagName.toLowerCase();
162 return { ok: true, tag, type: tag === "input" ? (el as HTMLInputElement).type.toLowerCase() : "", path: path.length ? `html > ${path.join(" > ")}` : "html" };
163 }
164
165 export interface IdentityInput {
166 key: string;
167 docId: string;
168 }
169
170 export function pageIdentity(input: IdentityInput): boolean {
171 const registry = (window as unknown as Record<string, unknown>)[input.key] as Registry | undefined;
172 return Boolean(registry) && registry?.docId === input.docId;
173 }
174
175 export const RESOLVE_SCRIPT_SOURCE = pageResolve.toString();
176 export const SELECT_SCRIPT_SOURCE = pageSelect.toString();
177 export const FOCUS_SCRIPT_SOURCE = pageFocus.toString();
178 export const IDENTITY_SCRIPT_SOURCE = pageIdentity.toString();
179 export const LOCATE_SCRIPT_SOURCE = pageLocate.toString();
180
181 export function scriptCall(source: string, input: unknown): string {
182 return `(${source})(${JSON.stringify(input)})`;
183 }
184
184 lines TYPESCRIPT