diff --git a/src/backend/adapter/chatgpt.js b/src/backend/adapter/chatgpt.js index c1b874a..e960f28 100644 --- a/src/backend/adapter/chatgpt.js +++ b/src/backend/adapter/chatgpt.js @@ -74,7 +74,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { } return false; } - }); + }, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/chatgpt_text.js b/src/backend/adapter/chatgpt_text.js index 3f55091..25301b9 100644 --- a/src/backend/adapter/chatgpt_text.js +++ b/src/backend/adapter/chatgpt_text.js @@ -132,7 +132,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { } return false; } - }); + }, meta); } // 3. 输入提示词 diff --git a/src/backend/adapter/doubao.js b/src/backend/adapter/doubao.js index b1f5b9c..5295be4 100644 --- a/src/backend/adapter/doubao.js +++ b/src/backend/adapter/doubao.js @@ -71,7 +71,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { url.includes('bytedanceapi.com') && url.includes('Action=CommitImageUpload'); } - }); + }, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/doubao_text.js b/src/backend/adapter/doubao_text.js index 8847b6c..2f0d20b 100644 --- a/src/backend/adapter/doubao_text.js +++ b/src/backend/adapter/doubao_text.js @@ -60,7 +60,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { url.includes('bytedanceapi.com') && url.includes('Action=CommitImageUpload'); } - }); + }, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/gemini.js b/src/backend/adapter/gemini.js index 4415961..233fde2 100644 --- a/src/backend/adapter/gemini.js +++ b/src/backend/adapter/gemini.js @@ -60,7 +60,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { url.includes('google.com/upload/') && url.includes('upload_id='); } - }); + }, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/gemini_biz.js b/src/backend/adapter/gemini_biz.js index c169781..0b56fd7 100644 --- a/src/backend/adapter/gemini_biz.js +++ b/src/backend/adapter/gemini_biz.js @@ -105,7 +105,12 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { const targetUrl = config.backend?.adapter?.gemini_biz?.entryUrl || config.backend?.geminiBiz?.entryUrl; if (!targetUrl) { - throw new Error('GeminiBiz backend missing entry URL'); + throw new Error('未填写 gemini_biz 适配器的 entry URL'); + } + + // 验证 URL 域名 + if (!targetUrl.includes('business.gemini.google')) { + throw new Error('无效的 Gemini Business URL,必须包含 business.gemini.google 域名'); } // 开启新对话 - 先等待可能正在进行的登录处理完成 @@ -129,7 +134,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { // 只追踪 widgetAddContextFile 请求,每个请求代表一张图片上传 return response.status() === 200 && url.includes('global/widgetAddContextFile'); } - }); + }, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/gemini_biz_text.js b/src/backend/adapter/gemini_biz_text.js index 5d518f3..90ec5ff 100644 --- a/src/backend/adapter/gemini_biz_text.js +++ b/src/backend/adapter/gemini_biz_text.js @@ -104,7 +104,12 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { const targetUrl = config.backend?.adapter?.gemini_biz?.entryUrl || config.backend?.geminiBiz?.entryUrl; if (!targetUrl) { - throw new Error('GeminiBiz backend missing entry URL'); + throw new Error('未填写 gemini_biz 适配器的 entry URL'); + } + + // 验证 URL 域名 + if (!targetUrl.includes('business.gemini.google')) { + throw new Error('无效的 Gemini Business URL,必须包含 business.gemini.google 域名'); } // 开启新对话 - 先等待可能正在进行的登录处理完成 @@ -128,7 +133,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { // 只追踪 widgetAddContextFile 请求,每个请求代表一张图片上传 return response.status() === 200 && url.includes('global/widgetAddContextFile'); } - }); + }, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/gemini_text.js b/src/backend/adapter/gemini_text.js index ecef223..ca17450 100644 --- a/src/backend/adapter/gemini_text.js +++ b/src/backend/adapter/gemini_text.js @@ -57,7 +57,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { url.includes('google.com/upload/') && url.includes('upload_id='); } - }); + }, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/google_flow.js b/src/backend/adapter/google_flow.js index ff092a6..8623d22 100644 --- a/src/backend/adapter/google_flow.js +++ b/src/backend/adapter/google_flow.js @@ -152,7 +152,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { // 5.2 点击 upload 按钮并选择文件(不等待上传完成) const uploadBtn = page.getByRole('button', { name: /^upload/ }); - await uploadFilesViaChooser(page, uploadBtn, [imgPath]); + await uploadFilesViaChooser(page, uploadBtn, [imgPath], {}, meta); // 5.3 先启动上传监听,再点击 crop 按钮 const uploadResponsePromise = waitApiResponse(page, { diff --git a/src/backend/adapter/lmarena.js b/src/backend/adapter/lmarena.js index b30fe59..670944c 100644 --- a/src/backend/adapter/lmarena.js +++ b/src/backend/adapter/lmarena.js @@ -90,7 +90,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { // 3. 上传图片 if (imgPaths && imgPaths.length > 0) { logger.info('适配器', `开始上传 ${imgPaths.length} 张图片`, meta); - await pasteImages(page, textareaSelector, imgPaths); + await pasteImages(page, textareaSelector, imgPaths, {}, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/lmarena_text.js b/src/backend/adapter/lmarena_text.js index 7676a2e..1582ce3 100644 --- a/src/backend/adapter/lmarena_text.js +++ b/src/backend/adapter/lmarena_text.js @@ -75,7 +75,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { // 3. 上传图片 if (imgPaths && imgPaths.length > 0) { logger.info('适配器', `开始上传 ${imgPaths.length} 张图片`, meta); - await pasteImages(page, textareaSelector, imgPaths); + await pasteImages(page, textareaSelector, imgPaths, {}, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/nanobananafree_ai.js b/src/backend/adapter/nanobananafree_ai.js index 80cbefe..253ccc6 100644 --- a/src/backend/adapter/nanobananafree_ai.js +++ b/src/backend/adapter/nanobananafree_ai.js @@ -50,7 +50,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { if (imgPaths.length > 1) { logger.warn('适配器', `此后端仅支持1张图片, 已丢弃 ${imgPaths.length - 1} 张`, meta); } - await pasteImages(page, textareaSelector, singleImage); + await pasteImages(page, textareaSelector, singleImage, {}, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/sora.js b/src/backend/adapter/sora.js index a3fce8b..cf9ce02 100644 --- a/src/backend/adapter/sora.js +++ b/src/backend/adapter/sora.js @@ -63,7 +63,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { } return false; } - }); + }, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/zai_is.js b/src/backend/adapter/zai_is.js index e43e2e0..a0ae973 100644 --- a/src/backend/adapter/zai_is.js +++ b/src/backend/adapter/zai_is.js @@ -150,7 +150,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { } return false; } - }); + }, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/zai_is_text.js b/src/backend/adapter/zai_is_text.js index 657b5ac..bb4b34a 100644 --- a/src/backend/adapter/zai_is_text.js +++ b/src/backend/adapter/zai_is_text.js @@ -167,7 +167,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { } return false; } - }); + }, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/adapter/zenmux_ai_text.js b/src/backend/adapter/zenmux_ai_text.js index 858d499..f31abac 100644 --- a/src/backend/adapter/zenmux_ai_text.js +++ b/src/backend/adapter/zenmux_ai_text.js @@ -83,7 +83,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { } return false; } - }); + }, meta); logger.info('适配器', '图片上传完成', meta); } diff --git a/src/backend/engine/utils.js b/src/backend/engine/utils.js index 089b3f2..d5de00e 100644 --- a/src/backend/engine/utils.js +++ b/src/backend/engine/utils.js @@ -22,6 +22,7 @@ import path from 'path'; import { logger } from '../../utils/logger.js'; +import { TIMEOUTS } from '../../utils/constants.js'; /** * 生成指定范围内的随机数 @@ -152,6 +153,69 @@ export function getHumanClickPoint(box, type = 'random') { return { x, y }; } +/** + * 等待元素布局稳定(基于 requestAnimationFrame) + * @param {import('playwright-core').Locator} locator - 建议直接传 Locator + * @param {number} stableFrames - 需要连续稳定的帧数,建议提高到 10 (约160ms) + * @param {number} timeout - 总超时时间 (ms) + */ +async function waitForElementStable(locator, stableFrames = 10, timeout = 2000) { + const element = await locator.elementHandle(); + if (!element) return; + + try { + await element.evaluate((targetEl, { stableFrames, timeout }) => { + return new Promise((resolve) => { + let lastRect = targetEl.getBoundingClientRect(); + let consecutiveStable = 0; + const startTime = performance.now(); + + function check() { + // 1. 超时检查:如果超过总时间,不再等待直接返回,防止死循环 + if (performance.now() - startTime > timeout) { + resolve(); + return; + } + + const rect = targetEl.getBoundingClientRect(); + + // 检查位置和大小是否变化 (容差 1px) + const isSame = + Math.abs(rect.x - lastRect.x) < 1 && + Math.abs(rect.y - lastRect.y) < 1 && + Math.abs(rect.width - lastRect.width) < 1 && + Math.abs(rect.height - lastRect.height) < 1; + + if (isSame) { + consecutiveStable++; + // 2. 只有连续 N 帧都不动才确定 + if (consecutiveStable >= stableFrames) { + resolve(); + return; + } + } else { + // 只要动了一次,计数器归零,重新开始计数 + consecutiveStable = 0; + lastRect = rect; + } + + requestAnimationFrame(check); + } + + // 3. 稍微延迟启动检测,给响应式框架留启动时间 + setTimeout(() => { + requestAnimationFrame(check); + }, 50); + }); + }, { stableFrames, timeout }); + } catch (e) { + console.log('Stability check failed, continuing anyway:', e); + } finally { + // 清理 handle,防止内存泄漏 + await element.dispose(); + } +} + /** * 安全点击元素 (包含滚动、拟人化移动和点击) * 支持 CSS selector、ElementHandle 和 Locator 三种输入 @@ -161,12 +225,13 @@ export function getHumanClickPoint(box, type = 'random') { * @param {string} [options.bias='random'] - 偏移偏好: 'input' 或 'random' * @param {number} [options.clickCount=1] - 点击次数: 1=单击, 2=双击 * @param {number} [options.timeout=15000] - 超时时间 (毫秒) + * @param {boolean} [options.waitStable=false] - 是否等待元素布局稳定后再点击 * @returns {Promise} */ export async function safeClick(page, target, options = {}) { const clickCount = options.clickCount || 1; - const timeout = options.timeout || 15000; - const maxRetries = 1; + const timeout = options.timeout || TIMEOUTS.ELEMENT_CLICK; + const waitStable = options.waitStable !== false; // 默认 true const doClick = async () => { let el; @@ -189,6 +254,11 @@ export async function safeClick(page, target, options = {}) { // 确保元素在可视区域内 await el.scrollIntoViewIfNeeded().catch(() => { }); + // 如果开启了布局稳定等待,等待元素位置稳定 + if (waitStable) { + await waitForElementStable(page, el); + } + // 使用 ghost-cursor 点击 if (page.cursor) { const box = await el.boundingBox(); @@ -207,33 +277,25 @@ export async function safeClick(page, target, options = {}) { await el.click({ clickCount }); }; - // 带超时和重试的执行 - 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; + // 带超时的执行(移除了重试机制) + let timeoutId; + try { + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error('CLICK_TIMEOUT')), timeout); + }); - 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); - } + await Promise.race([ + doClick().finally(() => clearTimeout(timeoutId)), + timeoutPromise + ]); + } catch (err) { + clearTimeout(timeoutId); + const selector = typeof target === 'string' ? target : '元素'; + throw new Error(`点击操作失败 (${selector}): ${err.message}`); } } + /** * 安全滚动 (包含拟人化移动和滚轮滚动) * 支持 CSS selector、ElementHandle 和 Locator 三种输入 @@ -432,11 +494,12 @@ async function findAllFileInputs(page) { * @param {string[]} filePaths - 图片文件路径数组 * @param {Object} [options] - 可选配置 * @param {Function} [options.uploadValidator] - 自定义上传确认回调函数, 接收 response 参数 + * @param {Object} [meta] - 元数据 (用于日志) * @returns {Promise} */ -export async function pasteImages(page, target, filePaths, options = {}) { +export async function pasteImages(page, target, filePaths, options = {}, meta = {}) { if (!filePaths || filePaths.length === 0) return; - logger.info('浏览器', `正在处理 ${filePaths.length} 张图片...`); + logger.info('浏览器', `正在处理 ${filePaths.length} 张图片...`, meta); // 1. 拟人化: 先点击一下目标区域 (让后台看起来像是用户聚焦了输入框) await safeClick(page, target, { bias: 'input' }); @@ -450,7 +513,7 @@ export async function pasteImages(page, target, filePaths, options = {}) { throw new Error('未找到任何 input[type="file"] 控件,无法上传'); } - logger.info('浏览器', `找到 ${fileInputs.length} 个文件输入框,尝试上传...`); + logger.info('浏览器', `找到 ${fileInputs.length} 个文件输入框,尝试上传...`, meta); // LMArena 通常只有一个用于聊天的上传控件,或者我们尝试第一个可用的 // 如果有多个,通常最后一个是当前对话框的,或者我们可以尝试全部 (比较暴力但有效) @@ -485,14 +548,14 @@ export async function pasteImages(page, target, filePaths, options = {}) { const uploadPromise = new Promise((resolve) => { const timeout = setTimeout(() => { cleanup(); - logger.warn('浏览器', `图片上传等待超时 (已确认: ${validatedCount}/${expectedUploads})`); + logger.warn('浏览器', `图片上传等待超时 (已确认: ${validatedCount}/${expectedUploads})`, meta); resolve(); }, 60000); // 60s 超时 const onResponse = (response) => { if (options.uploadValidator(response)) { validatedCount++; - logger.info('浏览器', `图片上传进度: ${validatedCount}/${expectedUploads}`); + logger.info('浏览器', `图片上传进度: ${validatedCount}/${expectedUploads}`, meta); if (validatedCount >= expectedUploads) { cleanup(); resolve(); @@ -508,12 +571,12 @@ export async function pasteImages(page, target, filePaths, options = {}) { page.on('response', onResponse); }); - logger.info('浏览器', `已提交图片, 正在等待上传确认...`); + logger.info('浏览器', `已提交图片, 正在等待上传确认...`, meta); await uploadPromise; - logger.info('浏览器', `所有图片上传完成`); + logger.info('浏览器', `所有图片上传完成`, meta); } else { // 默认行为: 等待上传预览出现 - logger.info('浏览器', `已提交图片, 等待预览生成...`); + logger.info('浏览器', `已提交图片, 等待预览生成...`, meta); await sleep(500, 1000); } @@ -532,9 +595,10 @@ export async function pasteImages(page, target, filePaths, options = {}) { * @param {Function} [options.uploadValidator] - 自定义上传确认回调函数, 接收 response 参数,返回 true 表示该响应代表一次成功上传 * @param {number} [options.timeout=60000] - 上传超时时间 (毫秒) * @param {string} [options.clickAction='click'] - 点击动作: 'click' 或 'dblclick' + * @param {Object} [meta] - 元数据 (用于日志) * @returns {Promise} */ -export async function uploadFilesViaChooser(page, triggerTarget, filePaths, options = {}) { +export async function uploadFilesViaChooser(page, triggerTarget, filePaths, options = {}, meta = {}) { if (!filePaths || filePaths.length === 0) return; const timeout = options.timeout || 60000; @@ -542,7 +606,7 @@ export async function uploadFilesViaChooser(page, triggerTarget, filePaths, opti const expectedUploads = filePaths.length; let uploadedCount = 0; - logger.info('浏览器', `正在处理 ${filePaths.length} 张图片 (filechooser 模式)...`); + logger.info('浏览器', `正在处理 ${filePaths.length} 张图片 (filechooser 模式)...`, meta); // 设置上传确认监听 const uploadPromise = new Promise((resolve) => { @@ -554,14 +618,14 @@ export async function uploadFilesViaChooser(page, triggerTarget, filePaths, opti const timeoutId = setTimeout(() => { cleanup(); - logger.warn('浏览器', `图片上传等待超时 (已确认: ${uploadedCount}/${expectedUploads})`); + logger.warn('浏览器', `图片上传等待超时 (已确认: ${uploadedCount}/${expectedUploads})`, meta); resolve(); }, timeout); const onResponse = (response) => { if (options.uploadValidator(response)) { uploadedCount++; - logger.info('浏览器', `图片上传进度: ${uploadedCount}/${expectedUploads}`); + logger.info('浏览器', `图片上传进度: ${uploadedCount}/${expectedUploads}`, meta); if (uploadedCount >= expectedUploads) { cleanup(); resolve(); @@ -592,7 +656,7 @@ export async function uploadFilesViaChooser(page, triggerTarget, filePaths, opti // 等待上传完成(如果有验证器) if (options.uploadValidator) { await uploadPromise; - logger.info('浏览器', '所有图片上传完成'); + logger.info('浏览器', '所有图片上传完成', meta); } } diff --git a/src/backend/pool/PoolManager.js b/src/backend/pool/PoolManager.js index 78e78d8..91cfb50 100644 --- a/src/backend/pool/PoolManager.js +++ b/src/backend/pool/PoolManager.js @@ -23,6 +23,7 @@ export class PoolManager { this.strategy = config.backend.pool.strategy || 'least_busy'; this.strategySelector = createStrategySelector(this.strategy); this.initialized = false; + this.roundRobinIndex = 0; } /** diff --git a/src/backend/pool/Worker.js b/src/backend/pool/Worker.js index f609386..be421ea 100644 --- a/src/backend/pool/Worker.js +++ b/src/backend/pool/Worker.js @@ -74,7 +74,11 @@ export class Worker { navigationHandler = handlers.length > 0 ? async (page) => { for (const handler of handlers) { - try { await handler(page); } catch (e) { /* ignore */ } + try { + await handler(page); + } catch (e) { + logger.debug('工作池', `导航处理器执行失败: ${e.message}`); + } } } : null; diff --git a/src/backend/utils/error.js b/src/backend/utils/error.js index c9598fb..24d6992 100644 --- a/src/backend/utils/error.js +++ b/src/backend/utils/error.js @@ -4,7 +4,7 @@ */ import { logger } from '../../utils/logger.js'; -import { ADAPTER_ERRORS } from '../../utils/constants.js'; +import { ADAPTER_ERRORS } from '../../server/errors.js'; // ========================================== // 可重试判定 diff --git a/src/backend/utils/page.js b/src/backend/utils/page.js index 7072b13..6ed202a 100644 --- a/src/backend/utils/page.js +++ b/src/backend/utils/page.js @@ -4,6 +4,7 @@ */ import { sleep, safeClick, isPageValid, createPageCloseWatcher, getRealViewport, clamp, random } from '../engine/utils.js'; +import { TIMEOUTS } from '../../utils/constants.js'; // ========================================== // 页面认证锁 @@ -58,7 +59,7 @@ export function isPageAuthLocked(page) { * @returns {Promise} */ export async function waitForInput(page, selectorOrLocator, options = {}) { - const { timeout = 20000, click = true } = options; + const { timeout = TIMEOUTS.INPUT_WAIT, click = true } = options; const isLocator = typeof selectorOrLocator !== 'string'; const displayName = isLocator ? 'Locator' : selectorOrLocator; @@ -104,7 +105,7 @@ export async function waitForInput(page, selectorOrLocator, options = {}) { * @throws {Error} 导航失败时抛出错误 */ export async function gotoWithCheck(page, url, options = {}) { - const { timeout = 20000 } = options; + const { timeout = TIMEOUTS.NAVIGATION } = options; try { const response = await page.goto(url, { waitUntil: 'domcontentloaded', @@ -172,7 +173,7 @@ export async function moveMouseAway(page) { * @returns {Promise} 元素句柄,失败返回 null */ export async function scrollToElement(page, selectorOrLocator, options = {}) { - const { timeout = 30000 } = options; + const { timeout = TIMEOUTS.ELEMENT_SCROLL } = options; try { const isLocator = typeof selectorOrLocator !== 'string'; let element; @@ -209,7 +210,7 @@ export async function scrollToElement(page, selectorOrLocator, options = {}) { * @returns {Promise} 响应对象 */ export async function waitApiResponse(page, options = {}) { - const { urlMatch, urlContains, method = 'POST', timeout = 120000, errorText } = options; + const { urlMatch, urlContains, method = 'POST', timeout = TIMEOUTS.API_RESPONSE, errorText } = options; if (!isPageValid(page)) { throw new Error('PAGE_INVALID'); diff --git a/src/server/errors.js b/src/server/errors.js index 70d150c..43d3ec8 100644 --- a/src/server/errors.js +++ b/src/server/errors.js @@ -142,3 +142,44 @@ export function getErrorStatus(code) { export function getErrorDetails(code) { return ERROR_DETAILS[code] || { message: '未知错误', status: 500 }; } + +// ========================================== +// 适配器层错误码(从 constants.js 统一到此处) +// ========================================== + +/** + * 适配器错误码 + * @readonly + */ +export const ADAPTER_ERRORS = { + /** 页面已关闭 */ + PAGE_CLOSED: 'PAGE_CLOSED', + + /** 页面崩溃 */ + PAGE_CRASHED: 'PAGE_CRASHED', + + /** 页面状态无效 */ + PAGE_INVALID: 'PAGE_INVALID', + + /** 网络错误 */ + NETWORK_ERROR: 'NETWORK_ERROR', + + /** 超时错误 */ + TIMEOUT_ERROR: 'TIMEOUT_ERROR', + + /** HTTP 错误 */ + HTTP_ERROR: 'HTTP_ERROR', + + /** 限流 */ + RATE_LIMITED: 'RATE_LIMITED', + + /** 需要验证码 */ + CAPTCHA_REQUIRED: 'CAPTCHA_REQUIRED', + + /** 需要登录 */ + AUTH_REQUIRED: 'AUTH_REQUIRED', + + /** 内容被阻止 (API/页面检测到错误关键词) */ + CONTENT_BLOCKED: 'CONTENT_BLOCKED', +}; + diff --git a/src/server/queue.js b/src/server/queue.js index 0d6f540..564ffb0 100644 --- a/src/server/queue.js +++ b/src/server/queue.js @@ -77,9 +77,13 @@ export function createQueueManager(queueConfig, callbacks) { */ async function cleanupTask(task) { if (task?.imagePaths) { - const fs = await import('fs'); + const fs = await import('fs/promises'); for (const p of task.imagePaths) { - try { fs.unlinkSync(p); } catch (e) { /* ignore */ } + try { + await fs.unlink(p); + } catch (e) { + logger.debug('服务器', `临时文件清理失败: ${p}`); + } } } } diff --git a/src/server/server.js b/src/server/server.js index 630c9c1..9f578f4 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -58,6 +58,11 @@ const PORT = config.server?.port || 3000; /** @type {string} 认证令牌 */ const AUTH_TOKEN = config.server?.auth; +// 检测默认密钥 +if (AUTH_TOKEN === 'sk-change-me-to-your-secure-key') { + logger.warn('服务器', '检测到默认密钥!如果在公网环境下请修改默认密钥'); +} + /** @type {string} 心跳模式 */ const KEEPALIVE_MODE = config.server?.keepalive?.mode || 'comment'; diff --git a/src/utils/constants.js b/src/utils/constants.js index 0213992..30bf598 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -12,24 +12,30 @@ * @readonly */ export const TIMEOUTS = { - /** 导航超时(页面跳转) */ - NAVIGATION: 30000, + /** 元素点击超时 (safeClick) */ + ELEMENT_CLICK: 15000, + + /** 输入框等待超时 (waitForInput) */ + INPUT_WAIT: 20000, + + /** 导航超时(页面跳转 gotoWithCheck) */ + NAVIGATION: 20000, + + /** 元素滚动等待超时 (scrollToElement) */ + ELEMENT_SCROLL: 30000, /** 导航超时(扩展,带重试场景) */ NAVIGATION_EXTENDED: 60000, - /** 输入框等待超时 */ - INPUT_WAIT: 10000, - - /** API 响应超时(图片生成) */ - API_RESPONSE: 120000, - /** 上传确认超时 */ UPLOAD_CONFIRM: 60000, /** OAuth 登录流程超时 */ OAUTH_FLOW: 60000, + /** API 响应超时(图片生成 waitApiResponse) */ + API_RESPONSE: 120000, + /** 心跳间隔 */ HEARTBEAT_INTERVAL: 3000, @@ -60,46 +66,6 @@ export const RETRY = { ], }; -// ========================================== -// 错误码(适配器层,与 server/errors.js 互补) -// ========================================== - -/** - * 适配器错误码 - * @readonly - */ -export const ADAPTER_ERRORS = { - /** 页面已关闭 */ - PAGE_CLOSED: 'PAGE_CLOSED', - - /** 页面崩溃 */ - PAGE_CRASHED: 'PAGE_CRASHED', - - /** 页面状态无效 */ - PAGE_INVALID: 'PAGE_INVALID', - - /** 网络错误 */ - NETWORK_ERROR: 'NETWORK_ERROR', - - /** 超时错误 */ - TIMEOUT_ERROR: 'TIMEOUT_ERROR', - - /** HTTP 错误 */ - HTTP_ERROR: 'HTTP_ERROR', - - /** 限流 */ - RATE_LIMITED: 'RATE_LIMITED', - - /** 需要验证码 */ - CAPTCHA_REQUIRED: 'CAPTCHA_REQUIRED', - - /** 需要登录 */ - AUTH_REQUIRED: 'AUTH_REQUIRED', - - /** 内容被阻止 (API/页面检测到错误关键词) */ - CONTENT_BLOCKED: 'CONTENT_BLOCKED', -}; - // ========================================== // 人机模拟配置 // ==========================================