返回 DeepSeek-Reasonix
pageActions.ts
根目录 / desktop / electron / src / main / browser / pageActions.ts
1 /// <reference lib="dom" />
2
3 // Page-side helpers compiled into the standalone browser-page resource.
4
5 export interface RefInput {
6 key: string;
7 snapshotId: string;
8 docId: string;
9 ref: string;
10 }
11
12 export interface ResolveInput extends RefInput {
13 scroll: boolean;
14 localCoordinates?: 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 (!input.localCoordinates && 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 // Executed in each parent isolated world. WindowProxy identity is readable
107 // across origins, while the parent alone supplies the frame owner's geometry.
108 export function pageFrameGeometry(input: { index: number; x: number; y: number; scroll: boolean }) {
109 const target = window.frames[input.index];
110 const find = (root: Document | ShadowRoot): HTMLElement | undefined => {
111 for (const el of root.querySelectorAll("*")) {
112 if ((el instanceof HTMLIFrameElement || el instanceof HTMLFrameElement) && el.contentWindow === target) return el;
113 if (el.shadowRoot) { const nested = find(el.shadowRoot); if (nested) return nested; }
114 }
115 };
116 const owner = find(document);
117 if (!owner) return null;
118 if (input.scroll) owner.scrollIntoView({ block: "center", inline: "center" });
119 const style = getComputedStyle(owner);
120 const transform = style.transform === "none" ? null : new DOMMatrixReadOnly(style.transform);
121 if (transform && (transform.b !== 0 || transform.c !== 0 || transform.a <= 0 || transform.d <= 0)) return null;
122 const rect = owner.getBoundingClientRect();
123 if (!owner.offsetWidth || !owner.offsetHeight) return null;
124 const scaleX = rect.width / owner.offsetWidth, scaleY = rect.height / owner.offsetHeight;
125 const x = rect.left + (owner.clientLeft + input.x) * scaleX, y = rect.top + (owner.clientTop + input.y) * scaleY;
126 const hit = owner.getRootNode() instanceof ShadowRoot ? (owner.getRootNode() as ShadowRoot).elementFromPoint(x, y) : document.elementFromPoint(x, y);
127 if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight || hit !== owner) return null;
128 return { x, y, scaleX, scaleY };
129 }
130
131 export interface SelectInput extends RefInput {
132 options: string[];
133 }
134
135 export type SelectOutput = { ok: true; selected: string[] } | ResolveFailure;
136
137 // The one action that synthesises DOM events: a <select> has no trustworthy
138 // pointer path (its popup is native), so options are toggled directly and
139 // input/change are dispatched the way Chromium itself does after a pick.
140 export function pageSelect(input: SelectInput): SelectOutput {
141 const registry = (window as unknown as Record<string, unknown>)[input.key] as Registry | undefined;
142 if (!registry || registry.docId !== input.docId || registry.snapshotId !== input.snapshotId) return { ok: false, reason: "stale" };
143 const target = registry.refs.get(input.ref);
144 if (!target) return { ok: false, reason: "stale" };
145 const select = target.tagName === "SELECT" ? (target as HTMLSelectElement) : target.tagName === "OPTION" ? (target as HTMLOptionElement).closest("select") : null;
146 if (!select) return { ok: false, reason: "element is not a <select>" };
147 if (select.disabled) return { ok: false, reason: "select is disabled" };
148 const wanted = input.options.length ? input.options : target.tagName === "OPTION" ? [(target as HTMLOptionElement).value] : [];
149 const matches = [...select.options].filter((option) => wanted.includes(option.value) || wanted.includes(option.label) || wanted.includes(option.text.trim()));
150 if (matches.length === 0) return { ok: false, reason: "no option matches the requested values" };
151 const chosen = select.multiple ? matches : [matches[0]];
152 for (const option of select.options) option.selected = chosen.includes(option);
153 select.dispatchEvent(new Event("input", { bubbles: true }));
154 select.dispatchEvent(new Event("change", { bubbles: true }));
155 return { ok: true, selected: chosen.map((option) => option.value) };
156 }
157
158 export function pageFocus(input: RefInput): boolean {
159 const registry = (window as unknown as Record<string, unknown>)[input.key] as Registry | undefined;
160 if (!registry || registry.docId !== input.docId || registry.snapshotId !== input.snapshotId) return false;
161 const target = registry.refs.get(input.ref);
162 if (!target || !target.isConnected) return false;
163 (target as HTMLElement).focus({ preventScroll: true });
164 return document.activeElement === target;
165 }
166
167 export type LocateOutput = { ok: true; tag: string; type: string; path: string } | ResolveFailure;
168
169 // Locates a ref without visibility checks: uploads target hidden file inputs
170 // and the CSS path lets the DevTools protocol find the same node.
171 export function pageLocate(input: RefInput): LocateOutput {
172 const registry = (window as unknown as Record<string, unknown>)[input.key] as Registry | undefined;
173 if (!registry || registry.docId !== input.docId || registry.snapshotId !== input.snapshotId) return { ok: false, reason: "stale" };
174 const el = registry.refs.get(input.ref);
175 if (!el) return { ok: false, reason: "stale" };
176 if (!el.isConnected) return { ok: false, reason: "element is no longer in the document" };
177 const path: string[] = [];
178 let current: Element | null = el;
179 while (current && current !== document.documentElement) {
180 const parent: Element | null = current.parentElement;
181 if (!parent) break;
182 const index = [...parent.children].indexOf(current) + 1;
183 path.unshift(`${current.tagName.toLowerCase()}:nth-child(${index})`);
184 current = parent;
185 }
186 const tag = el.tagName.toLowerCase();
187 return { ok: true, tag, type: tag === "input" ? (el as HTMLInputElement).type.toLowerCase() : "", path: path.length ? `html > ${path.join(" > ")}` : "html" };
188 }
189
190 export interface IdentityInput {
191 key: string;
192 docId: string;
193 }
194
195 export function pageIdentity(input: IdentityInput): boolean {
196 const registry = (window as unknown as Record<string, unknown>)[input.key] as Registry | undefined;
197 return Boolean(registry) && registry?.docId === input.docId;
198 }
199
199 lines TYPESCRIPT