mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 修复 gemini_biz 因懒加载等待图片超时的问题
This commit is contained in:
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.4.1] - 2025-12-24
|
||||
|
||||
### 🐛 Fixed
|
||||
- **Gemini Business**:修复因懒加载导致的等待图片超时问题
|
||||
|
||||
## [3.4.0] - 2025-12-23
|
||||
|
||||
### ✨ Added
|
||||
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
unlockPageAuth,
|
||||
isPageAuthLocked,
|
||||
waitForInput,
|
||||
gotoWithCheck
|
||||
gotoWithCheck,
|
||||
scrollToElement
|
||||
} from '../utils/index.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
@@ -190,7 +191,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
// 5. 提交 (submit - 使用公共函数)
|
||||
// 5. 提交
|
||||
logger.debug('适配器', '点击发送...', meta);
|
||||
await submit(page, {
|
||||
btnSelector: 'md-icon-button.send-button.submit, button[aria-label="提交"], button[aria-label="Send"], .send-button',
|
||||
@@ -207,6 +208,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
urlMatch: 'global/widgetStreamAssist',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
errorText: ['modelArmorViolation'],
|
||||
meta
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -227,12 +229,19 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
|
||||
let imageResponse;
|
||||
try {
|
||||
imageResponse = await waitApiResponse(page, {
|
||||
// 先启动监听器,再滚动触发懒加载,避免错过请求
|
||||
const imageResponsePromise = waitApiResponse(page, {
|
||||
urlMatch: 'download/v1alpha/projects',
|
||||
method: 'GET',
|
||||
timeout: 120000,
|
||||
errorText: ['is unable to reply as the prompt'],
|
||||
meta
|
||||
});
|
||||
|
||||
// 等待图片元素出现并滚动到可视范围,触发懒加载
|
||||
await scrollToElement(page, 'ucs-markdown-image', { timeout: 10000 });
|
||||
|
||||
imageResponse = await imageResponsePromise;
|
||||
} catch (e) {
|
||||
const pageError = normalizePageError(e, meta);
|
||||
if (pageError) {
|
||||
|
||||
@@ -215,6 +215,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
urlMatch: 'global/widgetStreamAssist',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
errorText: ['modelArmorViolation'],
|
||||
meta
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -35,6 +35,7 @@ export {
|
||||
tryGotoWithCheck,
|
||||
moveMouseAway,
|
||||
waitApiResponse,
|
||||
scrollToElement,
|
||||
} from './page.js';
|
||||
|
||||
// 错误归一化
|
||||
|
||||
@@ -215,23 +215,76 @@ export async function moveMouseAway(page) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待 API 响应 (带页面关闭监听)
|
||||
* 等待元素出现并滚动到可视范围
|
||||
* @param {import('playwright-core').Page} page - Playwright 页面对象
|
||||
* @param {string|import('playwright-core').Locator} selectorOrLocator - CSS 选择器或 Locator 对象
|
||||
* @param {object} [options={}] - 选项
|
||||
* @param {number} [options.timeout=30000] - 超时时间(毫秒)
|
||||
* @returns {Promise<import('playwright-core').ElementHandle|null>} 元素句柄,失败返回 null
|
||||
*/
|
||||
export async function scrollToElement(page, selectorOrLocator, options = {}) {
|
||||
const { timeout = 30000 } = options;
|
||||
try {
|
||||
const isLocator = typeof selectorOrLocator !== 'string';
|
||||
let element;
|
||||
|
||||
if (isLocator) {
|
||||
// Locator 对象 (getByRole, getByText 等)
|
||||
await selectorOrLocator.first().waitFor({ timeout, state: 'attached' });
|
||||
element = await selectorOrLocator.first().elementHandle();
|
||||
} else {
|
||||
// CSS 选择器字符串
|
||||
element = await page.waitForSelector(selectorOrLocator, { timeout, state: 'attached' });
|
||||
}
|
||||
|
||||
if (element) {
|
||||
await element.scrollIntoViewIfNeeded();
|
||||
return element;
|
||||
}
|
||||
} catch {
|
||||
// 元素未找到或超时
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 等待 API 响应 (带页面关闭监听和错误关键词检测)
|
||||
* @param {import('playwright-core').Page} page - Playwright 页面对象
|
||||
* @param {object} options - 等待选项
|
||||
* @param {string} options.urlMatch - URL 匹配字符串
|
||||
* @param {string|string[]} [options.urlContains] - URL 必须额外包含的字符串(可选,可以是数组)
|
||||
* @param {string} [options.method='POST'] - HTTP 方法
|
||||
* @param {number} [options.timeout=120000] - 超时时间(毫秒)
|
||||
* @param {string|string[]} [options.errorText] - 错误关键词,页面 UI 或 API 响应体中出现时立即停止并返回错误
|
||||
* @returns {Promise<import('playwright-core').Response>} 响应对象
|
||||
*/
|
||||
export async function waitApiResponse(page, options = {}) {
|
||||
const { urlMatch, urlContains, method = 'POST', timeout = 120000 } = options;
|
||||
const { urlMatch, urlContains, method = 'POST', timeout = 120000, errorText } = options;
|
||||
|
||||
if (!isPageValid(page)) {
|
||||
throw new Error('PAGE_INVALID');
|
||||
}
|
||||
|
||||
const pageWatcher = createPageCloseWatcher(page);
|
||||
const patterns = errorText ? (Array.isArray(errorText) ? errorText : [errorText]) : [];
|
||||
|
||||
// 页面 UI 错误关键词检测
|
||||
let uiErrorPromise = null;
|
||||
if (patterns.length > 0) {
|
||||
let combinedLocator = null;
|
||||
for (const pattern of patterns) {
|
||||
const loc = page.getByText(pattern);
|
||||
combinedLocator = combinedLocator ? combinedLocator.or(loc) : loc;
|
||||
}
|
||||
if (combinedLocator) {
|
||||
uiErrorPromise = combinedLocator.first().waitFor({ timeout, state: 'attached' })
|
||||
.then(async () => {
|
||||
const matchedText = await combinedLocator.first().textContent().catch(() => '未知错误');
|
||||
throw new Error(`PAGE_ERROR_DETECTED: ${matchedText}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const responsePromise = page.waitForResponse(
|
||||
@@ -254,7 +307,36 @@ export async function waitApiResponse(page, options = {}) {
|
||||
{ timeout }
|
||||
);
|
||||
|
||||
return await Promise.race([responsePromise, pageWatcher.promise]);
|
||||
const promises = [responsePromise, pageWatcher.promise];
|
||||
if (uiErrorPromise) promises.push(uiErrorPromise);
|
||||
|
||||
const response = await Promise.race(promises);
|
||||
|
||||
// API 响应体错误关键词检测 (在返回前同步检查)
|
||||
if (patterns.length > 0) {
|
||||
try {
|
||||
// 使用 body() 获取 Buffer,避免 text() 的某些内部状态问题
|
||||
const bodyBuffer = await response.body();
|
||||
const body = bodyBuffer.toString('utf-8');
|
||||
for (const pattern of patterns) {
|
||||
const keyword = typeof pattern === 'string' ? pattern : pattern.source;
|
||||
if (body.includes(keyword)) {
|
||||
throw new Error(`API_ERROR_DETECTED: ${keyword}`);
|
||||
}
|
||||
}
|
||||
// 返回代理对象,缓存 body 以支持调用方重复读取
|
||||
const cachedResponse = Object.create(response);
|
||||
cachedResponse.text = async () => body;
|
||||
cachedResponse.json = async () => JSON.parse(body);
|
||||
cachedResponse.body = async () => bodyBuffer;
|
||||
return cachedResponse;
|
||||
} catch (e) {
|
||||
if (e.message.startsWith('API_ERROR_DETECTED')) throw e;
|
||||
// 如果读取响应体失败,直接返回原始 response
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
} finally {
|
||||
pageWatcher.cleanup();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user