mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
fix: 修复生成结果超时客户端无通知的问题
This commit is contained in:
+7
-1
@@ -5,7 +5,13 @@ 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).
|
||||
|
||||
## [2.0.0] - 2025-12-06
|
||||
## [2.0.1] - 2025-12-09
|
||||
|
||||
### Fixed
|
||||
- **修复超时逻辑**
|
||||
- 修复在等待生成结果时超时,但是客户端任务未终止且无任何通知的问题
|
||||
|
||||
## [2.0.0] - 2025-12-08
|
||||
|
||||
### Added
|
||||
- **自动续登**
|
||||
|
||||
+104
-54
@@ -10,7 +10,9 @@ import {
|
||||
safeClick,
|
||||
humanType,
|
||||
pasteImages,
|
||||
getHumanClickPoint
|
||||
getHumanClickPoint,
|
||||
isPageValid,
|
||||
createPageCloseWatcher
|
||||
} from '../browser/utils.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
@@ -327,70 +329,118 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
|
||||
logger.info('适配器', '等待生成结果中...', meta);
|
||||
|
||||
// 6. 等待结果 (使用 Playwright waitForResponse)
|
||||
// 我们需要等待两个响应:
|
||||
// 1. widgetStreamAssist (API 响应,检查是否成功)
|
||||
// 2. download/v1alpha/projects (图片下载请求)
|
||||
// 6. 创建页面关闭监听器
|
||||
const pageWatcher = createPageCloseWatcher(page);
|
||||
|
||||
const apiResponsePromise = page.waitForResponse(response =>
|
||||
response.url().includes('global/widgetStreamAssist') &&
|
||||
response.request().method() === 'POST' &&
|
||||
(response.status() === 200 || response.status() >= 400),
|
||||
{ timeout: 120000 }
|
||||
).catch(e => e);
|
||||
try {
|
||||
// 检查页面状态
|
||||
if (!isPageValid(page)) {
|
||||
throw new Error('PAGE_INVALID');
|
||||
}
|
||||
|
||||
const imageDownloadPromise = page.waitForResponse(response =>
|
||||
response.url().includes('download/v1alpha/projects') &&
|
||||
response.request().method() === 'GET' &&
|
||||
response.status() === 200,
|
||||
{ timeout: 120000 }
|
||||
).catch(e => e);
|
||||
// 7. 等待 API 响应 (使用 Promise.race 监听页面事件)
|
||||
const apiResponsePromise = page.waitForResponse(response =>
|
||||
response.url().includes('global/widgetStreamAssist') &&
|
||||
response.request().method() === 'POST' &&
|
||||
(response.status() === 200 || response.status() >= 400),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
|
||||
// 等待 API 响应
|
||||
const apiResponse = await apiResponsePromise;
|
||||
const apiResponse = await Promise.race([
|
||||
apiResponsePromise,
|
||||
pageWatcher.promise
|
||||
]).catch(e => {
|
||||
// 错误分类
|
||||
if (e.message === 'PAGE_CLOSED') {
|
||||
logger.error('适配器', '页面已关闭(API响应等待期间)', meta);
|
||||
throw new Error('页面已关闭,请勿在生图过程中刷新页面');
|
||||
}
|
||||
if (e.message === 'PAGE_CRASHED') {
|
||||
logger.error('适配器', '页面崩溃(API响应等待期间)', meta);
|
||||
throw new Error('页面崩溃,请重试');
|
||||
}
|
||||
if (e.name === 'TimeoutError') {
|
||||
logger.error('适配器', 'API 请求超时(120秒)', meta);
|
||||
throw new Error('API 请求超时(120秒),请检查网络或稍后重试');
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (apiResponse instanceof Error) {
|
||||
throw apiResponse;
|
||||
if (apiResponse.status() !== 200) {
|
||||
logger.error('适配器', `API 返回错误状态码: ${apiResponse.status()}`, meta);
|
||||
return { error: `API 错误: HTTP ${apiResponse.status()}` };
|
||||
}
|
||||
|
||||
// 8. 等待图片下载响应
|
||||
logger.info('适配器', 'API 请求成功,等待图片下载...', meta);
|
||||
|
||||
const imageDownloadPromise = page.waitForResponse(response =>
|
||||
response.url().includes('download/v1alpha/projects') &&
|
||||
response.request().method() === 'GET' &&
|
||||
response.status() === 200,
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
|
||||
const imageResponse = await Promise.race([
|
||||
imageDownloadPromise,
|
||||
pageWatcher.promise
|
||||
]).catch(e => {
|
||||
// 错误分类
|
||||
if (e.message === 'PAGE_CLOSED') {
|
||||
logger.error('适配器', '页面已关闭(图片下载期间)', meta);
|
||||
throw new Error('页面已关闭,请勿在生图过程中刷新页面');
|
||||
}
|
||||
if (e.message === 'PAGE_CRASHED') {
|
||||
logger.error('适配器', '页面崩溃(图片下载期间)', meta);
|
||||
throw new Error('页面崩溃,请重试');
|
||||
}
|
||||
if (e.name === 'TimeoutError') {
|
||||
logger.error('适配器', 'API 请求成功,但图片下载超时(120秒)', meta);
|
||||
throw new Error('API 请求成功,但图片下载超时(120秒),请重试');
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
|
||||
logger.info('适配器', '捕获到图片下载请求', meta);
|
||||
// 响应体本身就是 base64 字符串,直接获取文本即可,不需要再次 base64 编码
|
||||
const base64 = await imageResponse.text();
|
||||
const dataUri = `data:image/png;base64,${base64}`;
|
||||
|
||||
logger.info('适配器', '生图成功', meta);
|
||||
|
||||
// 任务结束,移开鼠标
|
||||
if (page.cursor) {
|
||||
const currentVp = await getRealViewport(page);
|
||||
const relativeX = currentVp.safeWidth * random(0.85, 0.95);
|
||||
const relativeY = currentVp.height * random(0.3, 0.7);
|
||||
const finalX = clamp(relativeX, 0, currentVp.safeWidth);
|
||||
const finalY = clamp(relativeY, 0, currentVp.safeHeight);
|
||||
await page.cursor.moveTo({ x: finalX, y: finalY });
|
||||
}
|
||||
|
||||
return { image: dataUri };
|
||||
|
||||
} finally {
|
||||
// 清理页面事件监听器
|
||||
pageWatcher.cleanup();
|
||||
}
|
||||
|
||||
if (apiResponse.status() !== 200) {
|
||||
logger.error('适配器', `请求返回错误状态码: ${apiResponse.status()}`, meta);
|
||||
return { error: `API Error: ${apiResponse.status()}` };
|
||||
}
|
||||
|
||||
// 等待图片下载响应
|
||||
logger.info('适配器', 'API 请求成功,等待图片下载...', meta);
|
||||
const imageResponse = await imageDownloadPromise;
|
||||
|
||||
if (imageResponse instanceof Error) {
|
||||
throw imageResponse;
|
||||
}
|
||||
|
||||
logger.info('适配器', '捕获到图片下载请求', meta);
|
||||
// 响应体本身就是 base64 字符串,直接获取文本即可,不需要再次 base64 编码
|
||||
const base64 = await imageResponse.text();
|
||||
const dataUri = `data:image/png;base64,${base64}`;
|
||||
|
||||
logger.info('适配器', '生图成功', meta);
|
||||
|
||||
// 任务结束,移开鼠标
|
||||
if (page.cursor) {
|
||||
const currentVp = await getRealViewport(page);
|
||||
const relativeX = currentVp.safeWidth * random(0.85, 0.95);
|
||||
const relativeY = currentVp.height * random(0.3, 0.7);
|
||||
const finalX = clamp(relativeX, 0, currentVp.safeWidth);
|
||||
const finalY = clamp(relativeY, 0, currentVp.safeHeight);
|
||||
await page.cursor.moveTo({ x: finalX, y: finalY });
|
||||
}
|
||||
|
||||
return { image: dataUri };
|
||||
|
||||
} catch (err) {
|
||||
// 详细的错误分类和日志
|
||||
if (err.message === 'PAGE_INVALID') {
|
||||
logger.error('适配器', '页面状态无效', meta);
|
||||
return { error: '页面状态无效,请重新初始化' };
|
||||
}
|
||||
|
||||
logger.error('适配器', '生成任务失败', { ...meta, error: err.message });
|
||||
return { error: err.message };
|
||||
} finally {
|
||||
// 清理拦截器
|
||||
await page.unroute('**/*').catch(() => { });
|
||||
try {
|
||||
await page.unroute('**/*').catch(() => { });
|
||||
} catch (e) {
|
||||
// 忽略清理错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+104
-74
@@ -10,7 +10,9 @@ import {
|
||||
safeClick,
|
||||
humanType,
|
||||
pasteImages,
|
||||
getHumanClickPoint
|
||||
getHumanClickPoint,
|
||||
isPageValid,
|
||||
createPageCloseWatcher
|
||||
} from '../browser/utils.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { loadConfig } from '../utils/config.js';
|
||||
@@ -130,81 +132,109 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 建立响应监听器
|
||||
// 只要 URL 匹配且是 POST,无论状态码是 200 还是 429,都立即返回,防止超时死等
|
||||
const responsePromise = page.waitForResponse(response =>
|
||||
response.url().includes('/nextjs-api/stream') &&
|
||||
response.request().method() === 'POST' &&
|
||||
(response.status() === 200 || response.status() >= 400),
|
||||
{ timeout: 120000 }
|
||||
).catch(e => e);
|
||||
// 4. 创建页面关闭监听器
|
||||
const pageWatcher = createPageCloseWatcher(page);
|
||||
|
||||
logger.debug('适配器', '点击发送...', meta);
|
||||
const btnSelector = 'button[type="submit"]';
|
||||
await safeClick(page, btnSelector, { bias: 'button' });
|
||||
|
||||
logger.info('适配器', '等待生成结果...', meta);
|
||||
|
||||
// 5. 等待并处理响应
|
||||
const response = await responsePromise;
|
||||
|
||||
if (response instanceof Error) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
// 检查状态码
|
||||
if (response.status() === 429) {
|
||||
logger.warn('适配器', '触发限流/人机验证', meta);
|
||||
return { error: 'Rate limit exceeded or CAPTCHA triggered (HTTP 429)' };
|
||||
}
|
||||
|
||||
if (response.status() !== 200) {
|
||||
logger.warn('适配器', `返回异常状态码: ${response.status()}`, meta);
|
||||
return { error: `Server error: HTTP ${response.status()}` };
|
||||
}
|
||||
|
||||
// 解析成功响应
|
||||
const content = await response.text();
|
||||
|
||||
// 检查业务错误
|
||||
if (content.includes('recaptcha validation failed')) {
|
||||
return { error: 'recaptcha validation failed' };
|
||||
}
|
||||
|
||||
const img = extractImage(content);
|
||||
if (img) {
|
||||
logger.info('适配器', '已获取生图结果,正在下载图片...', meta);
|
||||
try {
|
||||
// 获取代理配置
|
||||
const config = loadConfig();
|
||||
const proxyConfig = getProxyConfig(config);
|
||||
const proxyUrl = await getHttpProxy(proxyConfig);
|
||||
|
||||
const options = {
|
||||
url: img,
|
||||
responseType: 'buffer',
|
||||
http2: true,
|
||||
headerGeneratorOptions: {
|
||||
browsers: [{ name: 'firefox', minVersion: 100 }],
|
||||
devices: ['desktop'],
|
||||
locales: ['en-US'],
|
||||
operatingSystems: ['windows'],
|
||||
}
|
||||
};
|
||||
|
||||
if (proxyUrl) {
|
||||
options.proxyUrl = proxyUrl;
|
||||
}
|
||||
|
||||
const imgRes = await gotScraping(options);
|
||||
const base64 = imgRes.body.toString('base64');
|
||||
return { image: `data:image/png;base64,${base64}` };
|
||||
} catch (e) {
|
||||
return { error: `Image download failed: ${e.message}` };
|
||||
try {
|
||||
// 检查页面状态
|
||||
if (!isPageValid(page)) {
|
||||
throw new Error('PAGE_INVALID');
|
||||
}
|
||||
} else {
|
||||
logger.info('适配器', 'AI 返回文本回复', { ...meta, preview: content.substring(0, 150) });
|
||||
return { text: content };
|
||||
|
||||
// 5. 点击发送按钮
|
||||
logger.debug('适配器', '点击发送...', meta);
|
||||
const btnSelector = 'button[type="submit"]';
|
||||
await safeClick(page, btnSelector, { bias: 'button' });
|
||||
|
||||
logger.info('适配器', '等待生成结果...', meta);
|
||||
|
||||
// 6. 等待响应 (使用 Promise.race)
|
||||
const responsePromise = page.waitForResponse(response =>
|
||||
response.url().includes('/nextjs-api/stream') &&
|
||||
response.request().method() === 'POST' &&
|
||||
(response.status() === 200 || response.status() >= 400),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
|
||||
const response = await Promise.race([
|
||||
responsePromise,
|
||||
pageWatcher.promise
|
||||
]).catch(e => {
|
||||
// 错误分类
|
||||
if (e.message === 'PAGE_CLOSED') {
|
||||
logger.error('适配器', '页面已关闭(等待响应期间)', meta);
|
||||
throw new Error('页面已关闭,请勿在生图过程中刷新页面');
|
||||
}
|
||||
if (e.message === 'PAGE_CRASHED') {
|
||||
logger.error('适配器', '页面崩溃(等待响应期间)', meta);
|
||||
throw new Error('页面崩溃,请重试');
|
||||
}
|
||||
if (e.name === 'TimeoutError') {
|
||||
logger.error('适配器', 'API 请求超时(120秒)', meta);
|
||||
throw new Error('API 请求超时(120秒),请检查网络或稍后重试');
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
|
||||
// 检查状态码
|
||||
if (response.status() === 429 || content.includes('Too Many Requests')) {
|
||||
logger.warn('适配器', '触发限流/上游繁忙', meta);
|
||||
return { error: 'Rate limit exceeded or CAPTCHA triggered (HTTP 429)' };
|
||||
}
|
||||
|
||||
if (response.status() !== 200) {
|
||||
logger.warn('适配器', `返回异常状态码: ${response.status()}`, meta);
|
||||
return { error: `Server error: HTTP ${response.status()}` };
|
||||
}
|
||||
|
||||
// 解析成功响应
|
||||
const content = await response.text();
|
||||
|
||||
// 检查业务错误
|
||||
if (content.includes('recaptcha validation failed')) {
|
||||
logger.warn('适配器', '触发人机验证', meta);
|
||||
return { error: 'recaptcha validation failed' };
|
||||
}
|
||||
|
||||
const img = extractImage(content);
|
||||
if (img) {
|
||||
logger.info('适配器', '已获取生图结果,正在下载图片...', meta);
|
||||
try {
|
||||
// 获取代理配置
|
||||
const config = loadConfig();
|
||||
const proxyConfig = getProxyConfig(config);
|
||||
const proxyUrl = await getHttpProxy(proxyConfig);
|
||||
|
||||
const options = {
|
||||
url: img,
|
||||
responseType: 'buffer',
|
||||
http2: true,
|
||||
headerGeneratorOptions: {
|
||||
browsers: [{ name: 'firefox', minVersion: 100 }],
|
||||
devices: ['desktop'],
|
||||
locales: ['en-US'],
|
||||
operatingSystems: ['windows'],
|
||||
}
|
||||
};
|
||||
|
||||
if (proxyUrl) {
|
||||
options.proxyUrl = proxyUrl;
|
||||
}
|
||||
|
||||
const imgRes = await gotScraping(options);
|
||||
const base64 = imgRes.body.toString('base64');
|
||||
return { image: `data:image/png;base64,${base64}` };
|
||||
} catch (e) {
|
||||
return { error: `Image download failed: ${e.message}` };
|
||||
}
|
||||
} else {
|
||||
logger.info('适配器', 'AI 返回文本回复', { ...meta, preview: content.substring(0, 150) });
|
||||
return { text: content };
|
||||
}
|
||||
|
||||
} finally {
|
||||
// 清理页面事件监听器
|
||||
pageWatcher.cleanup();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
safeClick,
|
||||
humanType,
|
||||
pasteImages,
|
||||
getHumanClickPoint
|
||||
getHumanClickPoint,
|
||||
isPageValid,
|
||||
createPageCloseWatcher
|
||||
} from '../browser/utils.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
@@ -88,61 +90,87 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
await humanType(page, textareaSelector, prompt);
|
||||
await sleep(800, 1500);
|
||||
|
||||
// 3. 建立响应监听器
|
||||
// 监听包含 v1/generateContent 路径的 POST 请求
|
||||
const responsePromise = page.waitForResponse(response =>
|
||||
response.url().includes('v1/generateContent') &&
|
||||
response.request().method() === 'POST' &&
|
||||
(response.status() === 200 || response.status() >= 400),
|
||||
{ timeout: 120000 }
|
||||
).catch(e => e);
|
||||
// 3. 创建页面关闭监听器
|
||||
const pageWatcher = createPageCloseWatcher(page);
|
||||
|
||||
// 4. 点击发送按钮 (匹配 class 包含 _sendButton_ 的 div)
|
||||
logger.debug('适配器', '点击发送...', meta);
|
||||
|
||||
// 使用更通用的选择器匹配发送按钮
|
||||
const sendBtnSelector = 'div[class*="_sendButton_"]';
|
||||
await page.waitForSelector(sendBtnSelector, { timeout: 10000 });
|
||||
await safeClick(page, sendBtnSelector, { bias: 'button' });
|
||||
|
||||
logger.info('适配器', '等待生成结果...', meta);
|
||||
|
||||
// 5. 等待并处理响应
|
||||
const response = await responsePromise;
|
||||
|
||||
if (response instanceof Error) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
// 检查状态码
|
||||
if (response.status() !== 200) {
|
||||
// 非200状态,尝试读取错误信息
|
||||
try {
|
||||
const body = await response.json();
|
||||
const errMessage = body?.errMessage || body?.error?.message || `HTTP ${response.status()}`;
|
||||
logger.warn('适配器', `请求返回错误: ${errMessage}`, meta);
|
||||
return { error: errMessage };
|
||||
} catch (e) {
|
||||
logger.warn('适配器', `返回异常状态码: ${response.status()}`, meta);
|
||||
return { error: `Server error: HTTP ${response.status()}` };
|
||||
try {
|
||||
// 检查页面状态
|
||||
if (!isPageValid(page)) {
|
||||
throw new Error('PAGE_INVALID');
|
||||
}
|
||||
}
|
||||
|
||||
// 解析成功响应
|
||||
const body = await response.json();
|
||||
// 4. 点击发送按钮 (匹配 class 包含 _sendButton_ 的 div)
|
||||
logger.debug('适配器', '点击发送...', meta);
|
||||
|
||||
// 尝试从响应中提取 base64 图片
|
||||
// 路径: data.candidates[0].content.parts[0].inlineData.data
|
||||
const inlineData = body?.data?.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
|
||||
// 使用更通用的选择器匹配发送按钮
|
||||
const sendBtnSelector = 'div[class*="_sendButton_"]';
|
||||
await page.waitForSelector(sendBtnSelector, { timeout: 10000 });
|
||||
await safeClick(page, sendBtnSelector, { bias: 'button' });
|
||||
|
||||
if (inlineData) {
|
||||
logger.info('适配器', '已获取生图结果', meta);
|
||||
// 返回带有 data URI 前缀的 base64 图片
|
||||
return { image: `data:image/png;base64,${inlineData}` };
|
||||
} else {
|
||||
// 没有找到图片数据,可能是文本回复或其他格式
|
||||
logger.info('适配器', 'AI 返回非图片响应', { ...meta, preview: JSON.stringify(body).substring(0, 150) });
|
||||
return { text: JSON.stringify(body) };
|
||||
logger.info('适配器', '等待生成结果...', meta);
|
||||
|
||||
// 5. 等待响应 (使用 Promise.race)
|
||||
const responsePromise = page.waitForResponse(response =>
|
||||
response.url().includes('v1/generateContent') &&
|
||||
response.request().method() === 'POST' &&
|
||||
(response.status() === 200 || response.status() >= 400),
|
||||
{ timeout: 120000 }
|
||||
);
|
||||
|
||||
const response = await Promise.race([
|
||||
responsePromise,
|
||||
pageWatcher.promise
|
||||
]).catch(e => {
|
||||
// 错误分类
|
||||
if (e.message === 'PAGE_CLOSED') {
|
||||
logger.error('适配器', '页面已关闭(等待响应期间)', meta);
|
||||
throw new Error('页面已关闭,请勿在生图过程中刷新页面');
|
||||
}
|
||||
if (e.message === 'PAGE_CRASHED') {
|
||||
logger.error('适配器', '页面崩溃(等待响应期间)', meta);
|
||||
throw new Error('页面崩溃,请重试');
|
||||
}
|
||||
if (e.name === 'TimeoutError') {
|
||||
logger.error('适配器', 'API 请求超时(120秒)', meta);
|
||||
throw new Error('API 请求超时(120秒),请检查网络或稍后重试');
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
|
||||
// 检查状态码
|
||||
if (response.status() !== 200) {
|
||||
// 非200状态,尝试读取错误信息
|
||||
try {
|
||||
const body = await response.json();
|
||||
const errMessage = body?.errMessage || body?.error?.message || `HTTP ${response.status()}`;
|
||||
logger.warn('适配器', `请求返回错误: ${errMessage}`, meta);
|
||||
return { error: errMessage };
|
||||
} catch (e) {
|
||||
logger.warn('适配器', `返回异常状态码: ${response.status()}`, meta);
|
||||
return { error: `Server error: HTTP ${response.status()}` };
|
||||
}
|
||||
}
|
||||
|
||||
// 解析成功响应
|
||||
const body = await response.json();
|
||||
|
||||
// 尝试从响应中提取 base64 图片
|
||||
// 路径: data.candidates[0].content.parts[0].inlineData.data
|
||||
const inlineData = body?.data?.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
|
||||
|
||||
if (inlineData) {
|
||||
logger.info('适配器', '已获取生图结果', meta);
|
||||
// 返回带有 data URI 前缀的 base64 图片
|
||||
return { image: `data:image/png;base64,${inlineData}` };
|
||||
} else {
|
||||
// 没有找到图片数据,可能是文本回复或其他格式
|
||||
logger.info('适配器', 'AI 返回非图片响应', { ...meta, preview: JSON.stringify(body).substring(0, 150) });
|
||||
return { text: JSON.stringify(body) };
|
||||
}
|
||||
|
||||
} finally {
|
||||
// 清理页面事件监听器
|
||||
pageWatcher.cleanup();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
|
||||
@@ -405,3 +405,40 @@ export async function pasteImages(page, target, filePaths, options = {}) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查页面是否有效
|
||||
* @param {import('playwright-core').Page} page
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPageValid(page) {
|
||||
try {
|
||||
return page && !page.isClosed();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建页面关闭/崩溃监听Promise
|
||||
* @param {import('playwright-core').Page} page
|
||||
* @returns {{promise: Promise, cleanup: Function}}
|
||||
*/
|
||||
export function createPageCloseWatcher(page) {
|
||||
let closeHandler, crashHandler;
|
||||
|
||||
const promise = new Promise((_, reject) => {
|
||||
closeHandler = () => reject(new Error('PAGE_CLOSED'));
|
||||
crashHandler = () => reject(new Error('PAGE_CRASHED'));
|
||||
|
||||
page.once('close', closeHandler);
|
||||
page.once('crash', crashHandler);
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
if (closeHandler) page.off('close', closeHandler);
|
||||
if (crashHandler) page.off('crash', crashHandler);
|
||||
};
|
||||
|
||||
return { promise, cleanup };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user