From 9cd1b82545904c35954469bb357896175aeb36e7 Mon Sep 17 00:00:00 2001 From: qxo <49526356@qq.com> Date: Sat, 21 Mar 2026 22:43:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(engine):=20=E6=89=A9=E5=B1=95=E6=8B=9F?= =?UTF-8?q?=E4=BA=BA=E7=82=B9=E5=87=BB=E7=AD=96=E7=95=A5=E5=B9=B6=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E5=9D=90=E6=A0=87=E5=AE=89=E5=85=A8=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 为 getHumanClickPoint 增加 center/top-left/top-right/bottom-right 等点击偏置, 并在 box 尺寸异常时返回兜底坐标。 * 新增边界裁剪逻辑,确保点击点始终位于元素内部,降低自动化点击越界和误点风险。 --- src/backend/engine/utils.js | 69 ++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/src/backend/engine/utils.js b/src/backend/engine/utils.js index 0a349e7..19e4114 100644 --- a/src/backend/engine/utils.js +++ b/src/backend/engine/utils.js @@ -135,21 +135,63 @@ export async function queryDeep(page, selector, rootHandle = null) { /** * 计算拟人化的随机点击坐标 * @param {object} box - 元素边界框 {x, y, width, height} - * @param {string} [type='random'] - 点击类型: 'input'(偏左偏底部) 或 'random'/'button'(随机) + * @param {string} [type='random'] - 点击类型: + * - 'input': 偏左偏底部 (5%-40% x, 60%-90% y) + * - 'center': 中心区域 (40%-60% x, 40%-60% y) + * - 'top-left': 偏左偏上 (10%-30% x, 10%-30% y) + * - 'top-right': 偏右偏上 (70%-90% x, 10%-30% y) + * - 'bottom-right': 偏右偏下 (70%-90% x, 70%-90% y) + * - 'random'/'button': 中心附近随机 (20%-80% x, 20%-80% y) * @returns {{x: number, y: number}} 计算出的坐标 */ export function getHumanClickPoint(box, type = 'random') { - let x, y; - if (type === 'input') { - // 输入框: 偏左 (5% - 40% 宽度), 偏底部 (60% - 90% 高度) - // 偏底部以适应富文本编辑器上方可能有附件预览的情况 - x = box.x + box.width * random(0.05, 0.4); - y = box.y + box.height * random(0.60, 0.90); - } else { - // 按钮/其他: 中心附近随机 (20% - 80% 宽度/高度) - x = box.x + box.width * random(0.2, 0.8); - y = box.y + box.height * random(0.2, 0.8); + // 确保 box 有有效的尺寸 + if (!box || box.width <= 0 || box.height <= 0) { + return { x: box?.x || 0, y: box?.y || 0 }; } + + let xRatio, yRatio; + switch (type) { + case 'input': + // 输入框: 偏左 (5% - 40% 宽度), 偏底部 (60% - 90% 高度) + xRatio = random(0.05, 0.4); + yRatio = random(0.60, 0.90); + break; + case 'center': + // 中心区域 + xRatio = random(0.4, 0.6); + yRatio = random(0.4, 0.6); + break; + case 'top-left': + // 偏左偏上 + xRatio = random(0.1, 0.3); + yRatio = random(0.1, 0.3); + break; + case 'top-right': + // 偏右偏上 + xRatio = random(0.7, 0.9); + yRatio = random(0.1, 0.3); + break; + case 'bottom-right': + // 偏右偏下 + xRatio = random(0.7, 0.9); + yRatio = random(0.7, 0.9); + break; + default: + // 按钮/其他: 中心附近随机 (20% - 80% 宽度/高度) + xRatio = random(0.2, 0.8); + yRatio = random(0.2, 0.8); + } + + // 边界检查:确保点击位置在元素范围内(留 1px 边距) + let x = box.x + box.width * xRatio; + let y = box.y + box.height * yRatio; + + // 限制在元素边界内(左右各留 1px) + x = Math.max(box.x + 1, Math.min(box.x + box.width - 1, x)); + // 限制在元素边界内(上下各留 1px) + y = Math.max(box.y + 1, Math.min(box.y + box.height - 1, y)); + return { x, y }; } @@ -262,9 +304,10 @@ export async function safeClick(page, target, options = {}) { const doClick = async () => { // 1. 首次获取元素(用于滚动和等待稳定) - logger.debug('浏览器', `[safeClick] 开始查找: ${selector}`); + const logKey = `${selector} ${target} ${options.bias || 'random'}`; + logger.debug('浏览器', `[safeClick] 开始查找: ${logKey}`); let el = await resolveElement(); - logger.debug('浏览器', `[safeClick] 已找到元素`); + logger.debug('浏览器', `[safeClick] 已找到 ${logKey}`); // 2. 确保元素在可视区域内 logger.debug('浏览器', `[safeClick] 滚动到可视区域...`);