From 3acd8bb17c7349d3e6966d86df9e52796f76dc39 Mon Sep 17 00:00:00 2001 From: foxhui Date: Thu, 15 Jan 2026 03:17:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=99=E7=82=B9=E5=87=BB=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E6=B7=BB=E5=8A=A0=E8=B6=85=E6=97=B6=E4=BF=9D=E6=8A=A4?= =?UTF-8?q?=E5=92=8C=E4=B8=80=E6=AC=A1=E9=87=8D=E8=AF=95=E7=9A=84=E6=9C=BA?= =?UTF-8?q?=E4=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/adapter/gemini_biz_text.js | 2 +- src/backend/engine/utils.js | 40 ++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/backend/adapter/gemini_biz_text.js b/src/backend/adapter/gemini_biz_text.js index 89bbdd0..5d518f3 100644 --- a/src/backend/adapter/gemini_biz_text.js +++ b/src/backend/adapter/gemini_biz_text.js @@ -133,8 +133,8 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { } // 3. 输入提示词 - await safeClick(page, INPUT_SELECTOR, { bias: 'input' }); logger.info('适配器', '输入提示词...', meta); + await safeClick(page, INPUT_SELECTOR, { bias: 'input' }); await humanType(page, INPUT_SELECTOR, prompt); // 4. 设置请求拦截器(根据模型类型修改请求) diff --git a/src/backend/engine/utils.js b/src/backend/engine/utils.js index 668b436..089b3f2 100644 --- a/src/backend/engine/utils.js +++ b/src/backend/engine/utils.js @@ -134,15 +134,16 @@ 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'(偏左偏底部) 或 'random'/'button'(随机) * @returns {{x: number, y: number}} 计算出的坐标 */ export function getHumanClickPoint(box, type = 'random') { let x, y; if (type === 'input') { - // 输入框: 偏左 (5% - 40% 宽度), 垂直居中附近 (20% - 80% 高度) + // 输入框: 偏左 (5% - 40% 宽度), 偏底部 (60% - 90% 高度) + // 偏底部以适应富文本编辑器上方可能有附件预览的情况 x = box.x + box.width * random(0.05, 0.4); - y = box.y + box.height * random(0.2, 0.8); + y = box.y + box.height * random(0.60, 0.90); } else { // 按钮/其他: 中心附近随机 (20% - 80% 宽度/高度) x = box.x + box.width * random(0.2, 0.8); @@ -159,12 +160,15 @@ export function getHumanClickPoint(box, type = 'random') { * @param {object} [options] - 点击选项 * @param {string} [options.bias='random'] - 偏移偏好: 'input' 或 'random' * @param {number} [options.clickCount=1] - 点击次数: 1=单击, 2=双击 + * @param {number} [options.timeout=15000] - 超时时间 (毫秒) * @returns {Promise} */ export async function safeClick(page, target, options = {}) { const clickCount = options.clickCount || 1; + const timeout = options.timeout || 15000; + const maxRetries = 1; - try { + const doClick = async () => { let el; // 判断输入类型 @@ -201,8 +205,32 @@ export async function safeClick(page, target, options = {}) { // 降级逻辑 await el.click({ clickCount }); - } catch (err) { - throw err; + }; + + // 带超时和重试的执行 + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + await Promise.race([ + doClick(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('CLICK_TIMEOUT')), timeout) + ) + ]); + return; // 成功则退出 + } catch (err) { + const isTimeout = err.message === 'CLICK_TIMEOUT'; + const isLastAttempt = attempt === maxRetries; + + if (isLastAttempt) { + // 最后一次尝试失败,抛出明确的错误 + const selector = typeof target === 'string' ? target : '元素'; + throw new Error(`点击操作失败 (${selector}): ${isTimeout ? '超时' : err.message}`); + } + + // 非最后一次,记录日志并重试 + logger.warn('浏览器', `点击操作${isTimeout ? '超时' : '失败'},正在重试... (${attempt + 1}/${maxRetries + 1})`); + await sleep(300, 500); + } } }