feat: 支持配置全局网页元素等待
This commit is contained in:
1 parent
5e9f0df57f
commit
bec3d9daa4
8 files changed
+210
-12
No files matched your search
@@ -2,6 +2,16 @@ import { BrowserWindow } from "electron";
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { ElementPickResult } from "../../shared/types";
|
||||
|
||||
const DEFAULT_ELEMENT_WAIT_MS = 5000;
|
||||
let globalElementWaitMs = DEFAULT_ELEMENT_WAIT_MS;
|
||||
|
||||
export function setBrowserElementWaitMs(milliseconds: number): void {
|
||||
const value = Number(milliseconds);
|
||||
globalElementWaitMs = Number.isFinite(value)
|
||||
? Math.min(600000, Math.max(0, Math.round(value)))
|
||||
: DEFAULT_ELEMENT_WAIT_MS;
|
||||
}
|
||||
import {
|
||||
RuntimeError,
|
||||
type BrowserElementInfo,
|
||||
@@ -137,6 +147,18 @@ export class BrowserSession implements BrowserLike {
|
||||
}
|
||||
|
||||
private async withElement<T>(selector: unknown, operation: string): Promise<T> {
|
||||
const deadline = Date.now() + globalElementWaitMs;
|
||||
for (;;) {
|
||||
try {
|
||||
return await this.withElementOnce<T>(selector, operation);
|
||||
} catch (error) {
|
||||
if (!(error instanceof RuntimeError && error.code === "not_found") || Date.now() >= deadline) throw error;
|
||||
await this.wait(120);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async withElementOnce<T>(selector: unknown, operation: string): Promise<T> {
|
||||
const spec = selectorSpec(selector);
|
||||
const result = await this.evaluate<{ found: boolean; value?: T }>(`(() => {
|
||||
const spec = ${JSON.stringify(spec)};
|
||||
@@ -241,7 +263,31 @@ export class BrowserSession implements BrowserLike {
|
||||
return { url: this.win.webContents.getURL(), title: this.win.webContents.getTitle() };
|
||||
}
|
||||
|
||||
async locate(selector: unknown): Promise<ElementInfo & { selector: string }> {
|
||||
private async wait(milliseconds: number): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
async locate(selector: unknown, timeoutMs?: number): Promise<ElementInfo & { selector: string }> {
|
||||
const waitMs = Math.max(0, timeoutMs ?? globalElementWaitMs);
|
||||
const deadline = Date.now() + waitMs;
|
||||
for (;;) {
|
||||
try {
|
||||
return await this.locateOnce(selector);
|
||||
} catch (error) {
|
||||
const canRetry = error instanceof RuntimeError && error.code === "not_found" && Date.now() < deadline;
|
||||
if (!canRetry) {
|
||||
if (error instanceof RuntimeError && error.code === "not_found" && waitMs > 0) {
|
||||
error.detail = `已等待页面渲染 ${waitMs}ms 后仍未找到元素`;
|
||||
error.recommendation = "确认页面已加载完成;如果元素在 iframe 或 Shadow DOM 中,请用拾取器重新采集";
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await this.wait(120);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async locateOnce(selector: unknown): Promise<ElementInfo & { selector: string }> {
|
||||
const spec = selectorSpec(selector);
|
||||
const expression = `(() => {
|
||||
const spec = ${JSON.stringify(spec)};
|
||||
@@ -312,8 +358,8 @@ export class BrowserSession implements BrowserLike {
|
||||
await this.cdp("Input.dispatchMouseEvent", { type: "mouseReleased", ...point, button: "left", buttons: 0, clickCount: 1 });
|
||||
}
|
||||
|
||||
async click(selector: unknown): Promise<unknown> {
|
||||
const info = await this.locate(selector);
|
||||
async click(selector: unknown, timeoutMs?: number): Promise<unknown> {
|
||||
const info = await this.locate(selector, timeoutMs);
|
||||
if (!info.visible) {
|
||||
throw new RuntimeError("not_found", `元素不可见:${info.selector}`, { recommendation: "等待元素出现或检查页面状态" });
|
||||
}
|
||||
@@ -343,8 +389,8 @@ export class BrowserSession implements BrowserLike {
|
||||
return { selector: info.selector, tag: info.tag, text: info.text };
|
||||
}
|
||||
|
||||
async type(selector: unknown, text: string, clearFirst = true): Promise<unknown> {
|
||||
const info = await this.locate(selector);
|
||||
async type(selector: unknown, text: string, clearFirst = true, timeoutMs?: number): Promise<unknown> {
|
||||
const info = await this.locate(selector, timeoutMs);
|
||||
await this.mouseClick(info.rect.x + info.rect.width / 2, info.rect.y + info.rect.height / 2);
|
||||
if (clearFirst) {
|
||||
await this.cdp("Input.dispatchKeyEvent", { type: "rawKeyDown", key: "a", code: "KeyA", windowsVirtualKeyCode: 65, modifiers: 2 });
|
||||
@@ -374,7 +420,7 @@ export class BrowserSession implements BrowserLike {
|
||||
|
||||
async exists(selector: unknown): Promise<boolean> {
|
||||
try {
|
||||
await this.locate(selector);
|
||||
await this.locate(selector, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
Reference in new issue
Block a user