Spaces:
Running
Running
feat(browser): add automatic restart with exponential backoff for KeepAliveError
Browse files- browser/instance.py +176 -145
- browser/navigation.py +4 -1
browser/instance.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
import os
|
| 2 |
import signal
|
|
|
|
| 3 |
from playwright.sync_api import TimeoutError, Error as PlaywrightError
|
| 4 |
from utils.logger import setup_logging
|
| 5 |
from utils.cookie_manager import CookieManager
|
| 6 |
-
from browser.navigation import handle_successful_navigation
|
| 7 |
from browser.cookie_validator import CookieValidator
|
| 8 |
from camoufox.sync_api import Camoufox
|
| 9 |
from utils.paths import logs_dir
|
|
@@ -69,164 +70,194 @@ def run_browser_instance(config, shutdown_event=None):
|
|
| 69 |
screenshot_dir = logs_dir()
|
| 70 |
ensure_dir(screenshot_dir)
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
page = context.new_page()
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
|
| 92 |
-
if response:
|
| 93 |
-
logger.info(f"导航初步成功,服务器响应状态码: {response.status} {response.status_text}")
|
| 94 |
-
if not response.ok: # response.ok 检查状态码是否在 200-299 范围内
|
| 95 |
-
logger.warning(f"警告:页面加载成功,但HTTP状态码表示错误: {response.status}")
|
| 96 |
-
# 即使状态码错误,也保存快照以供分析
|
| 97 |
-
page.screenshot(path=os.path.join(screenshot_dir, f"WARN_http_status_{response.status}_{diagnostic_tag}.png"))
|
| 98 |
-
else:
|
| 99 |
-
# 对于非http/https的导航(如 about:blank),response可能为None
|
| 100 |
-
logger.warning("page.goto 未返回响应对象,可能是一个非HTTP导航")
|
| 101 |
-
|
| 102 |
-
except TimeoutError:
|
| 103 |
-
# 这是最常见的错误:超时
|
| 104 |
-
logger.error(f"导航到 {mask_url_for_logging(expected_url)} 超时 (超过90秒)")
|
| 105 |
-
logger.error("可能原因:网络连接缓慢、目标网站服务器无响应、代理问题、或页面资源被阻塞")
|
| 106 |
-
# 尝试保存诊断信息
|
| 107 |
try:
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
page.
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
#
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
# 捕获其他Playwright相关的网络错误,例如DNS解析失败、连接被拒绝等
|
| 124 |
-
error_message = str(e)
|
| 125 |
-
logger.error(f"导航到 {mask_url_for_logging(expected_url)} 时发生 Playwright 网络错误")
|
| 126 |
-
logger.error(f"错误详情: {error_message}")
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
logger.error("排查建议:检查DNS设置或域名是否正确")
|
| 131 |
-
elif "net::ERR_CONNECTION_REFUSED" in error_message:
|
| 132 |
-
logger.error("排查建议:目标服务��可能已关闭,或代理/防火墙阻止了连接")
|
| 133 |
-
elif "net::ERR_INTERNET_DISCONNECTED" in error_message:
|
| 134 |
-
logger.error("排查建议:检查本机的网络连接")
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
screenshot_path = os.path.join(screenshot_dir, f"FAIL_network_error_{diagnostic_tag}.png")
|
| 139 |
-
page.screenshot(path=screenshot_path)
|
| 140 |
-
logger.info(f"已截取网络错误时的屏幕快照: {screenshot_path}")
|
| 141 |
-
except Exception as diag_e:
|
| 142 |
-
logger.error(f"在尝试进行网络错误诊断(截图)时发生额外错误: {diag_e}")
|
| 143 |
-
return # 网络错误,终止
|
| 144 |
-
|
| 145 |
-
# --- 如果导航没有抛出异常,继续执行后续逻辑 ---
|
| 146 |
-
|
| 147 |
-
logger.info("页面初步加载完成,正在检查并处理初始弹窗...")
|
| 148 |
-
page.wait_for_timeout(2000)
|
| 149 |
-
|
| 150 |
-
final_url = page.url
|
| 151 |
-
logger.info(f"导航完成。最终URL为: {mask_url_for_logging(final_url)}")
|
| 152 |
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
return
|
| 205 |
|
| 206 |
-
#
|
| 207 |
-
|
|
|
|
|
|
|
| 208 |
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
else:
|
| 215 |
-
logger.
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
except KeyboardInterrupt:
|
| 223 |
-
logger.info(f"用户中断,正在关闭...")
|
| 224 |
-
except SystemExit as e:
|
| 225 |
-
# 捕获Cookie验证失败时的系统退出
|
| 226 |
-
if e.code == 1:
|
| 227 |
-
logger.error("Cookie验证失败,关闭进程实例")
|
| 228 |
-
else:
|
| 229 |
-
logger.info(f"实例正常退出,退出码: {e.code}")
|
| 230 |
-
except Exception as e:
|
| 231 |
-
# 这是一个最终的捕获,用于捕获所有未预料到的错误
|
| 232 |
-
logger.exception(f"运行 Camoufox 实例时发生未预料的严重错误: {e}")
|
|
|
|
| 1 |
import os
|
| 2 |
import signal
|
| 3 |
+
import time
|
| 4 |
from playwright.sync_api import TimeoutError, Error as PlaywrightError
|
| 5 |
from utils.logger import setup_logging
|
| 6 |
from utils.cookie_manager import CookieManager
|
| 7 |
+
from browser.navigation import handle_successful_navigation, KeepAliveError
|
| 8 |
from browser.cookie_validator import CookieValidator
|
| 9 |
from camoufox.sync_api import Camoufox
|
| 10 |
from utils.paths import logs_dir
|
|
|
|
| 70 |
screenshot_dir = logs_dir()
|
| 71 |
ensure_dir(screenshot_dir)
|
| 72 |
|
| 73 |
+
# 重启控制变量
|
| 74 |
+
max_retries = int(os.getenv("MAX_RESTART_RETRIES", "5"))
|
| 75 |
+
retry_count = 0
|
| 76 |
+
base_delay = 3
|
|
|
|
| 77 |
|
| 78 |
+
while True:
|
| 79 |
+
# 检查是否收到全局关闭信号
|
| 80 |
+
if shutdown_event and shutdown_event.is_set():
|
| 81 |
+
logger.info("检测到全局关闭事件,浏览器实例不再启动,准备退出")
|
| 82 |
+
return
|
| 83 |
|
| 84 |
+
try:
|
| 85 |
+
with Camoufox(**launch_options) as browser:
|
| 86 |
+
context = browser.new_context()
|
| 87 |
+
context.add_cookies(cookies)
|
| 88 |
+
page = context.new_page()
|
| 89 |
+
|
| 90 |
+
# 创建Cookie验证器
|
| 91 |
+
cookie_validator = CookieValidator(page, context, logger)
|
| 92 |
+
|
| 93 |
+
# ####################################################################
|
| 94 |
+
# ############ 增强的 page.goto() 错误处理和日志记录 ###############
|
| 95 |
+
# ####################################################################
|
| 96 |
|
| 97 |
+
response = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
try:
|
| 99 |
+
logger.info(f"正在导航到: {mask_url_for_logging(expected_url)} (超时设置为 90 秒)")
|
| 100 |
+
# page.goto() 会返回一个 response 对象,我们可以用它来获取状态码等信息
|
| 101 |
+
response = page.goto(expected_url, wait_until='domcontentloaded', timeout=90000)
|
| 102 |
+
|
| 103 |
+
# 检查HTTP响应状态码
|
| 104 |
+
if response:
|
| 105 |
+
logger.info(f"导航初步成功,服务器响应状态码: {response.status} {response.status_text}")
|
| 106 |
+
if not response.ok: # response.ok 检查状态码是否在 200-299 范围内
|
| 107 |
+
logger.warning(f"警告:页面加载成功,但HTTP状态码表示错误: {response.status}")
|
| 108 |
+
# 即使状态码错误,也保存快照以供分析
|
| 109 |
+
page.screenshot(path=os.path.join(screenshot_dir, f"WARN_http_status_{response.status}_{diagnostic_tag}.png"))
|
| 110 |
+
else:
|
| 111 |
+
# 对于非http/https的导航(如 about:blank),response可能为None
|
| 112 |
+
logger.warning("page.goto 未返回响应对象,可能是一个非HTTP导航")
|
| 113 |
+
|
| 114 |
+
except TimeoutError:
|
| 115 |
+
# 这是最常见的错误:超时
|
| 116 |
+
logger.error(f"导航到 {mask_url_for_logging(expected_url)} 超时 (超过90秒)")
|
| 117 |
+
logger.error("可能原因:网络连接缓慢、目标网站服务器无响应、代理问题、或页面资源被阻塞")
|
| 118 |
+
# 尝试保存诊断信息
|
| 119 |
+
try:
|
| 120 |
+
# 截图对于看到页面卡在什么状态非常有帮助(例如,空白页、加载中、Chrome错误页)
|
| 121 |
+
screenshot_path = os.path.join(screenshot_dir, f"FAIL_timeout_{diagnostic_tag}.png")
|
| 122 |
+
page.screenshot(path=screenshot_path, full_page=True)
|
| 123 |
+
logger.info(f"已截取超时时的屏幕快照: {screenshot_path}")
|
| 124 |
+
|
| 125 |
+
# 保存HTML可以帮助分析DOM结构,即使在无头模式下也很有用
|
| 126 |
+
html_path = os.path.join(screenshot_dir, f"FAIL_timeout_{diagnostic_tag}.html")
|
| 127 |
+
with open(html_path, 'w', encoding='utf-8') as f:
|
| 128 |
+
f.write(page.content())
|
| 129 |
+
logger.info(f"已保存超时时的页面HTML: {html_path}")
|
| 130 |
+
except Exception as diag_e:
|
| 131 |
+
logger.error(f"在尝试进行超时诊断(截图/保存HTML)时发生额外错误: {diag_e}")
|
| 132 |
+
return # 超时后,后续操作无意义,直接终止
|
| 133 |
+
|
| 134 |
+
except PlaywrightError as e:
|
| 135 |
+
# 捕获其他Playwright相关的网络错误,例如DNS解析失败、连接被拒绝等
|
| 136 |
+
error_message = str(e)
|
| 137 |
+
logger.error(f"导航到 {mask_url_for_logging(expected_url)} 时发生 Playwright 网络错误")
|
| 138 |
+
logger.error(f"错误详情: {error_message}")
|
| 139 |
+
|
| 140 |
+
# Playwright的错误信息通常很具体,例如 "net::ERR_CONNECTION_REFUSED"
|
| 141 |
+
if "net::ERR_NAME_NOT_RESOLVED" in error_message:
|
| 142 |
+
logger.error("排查建议:检查DNS设置或域名是否正确")
|
| 143 |
+
elif "net::ERR_CONNECTION_REFUSED" in error_message:
|
| 144 |
+
logger.error("排查建议:目标服务器可能已关闭,或代理/防火墙阻止了连接")
|
| 145 |
+
elif "net::ERR_INTERNET_DISCONNECTED" in error_message:
|
| 146 |
+
logger.error("排查建议:检查本机的网络连接")
|
| 147 |
|
| 148 |
+
# 同样,尝试截图,尽管此时页面可能完全无法访问
|
| 149 |
+
try:
|
| 150 |
+
screenshot_path = os.path.join(screenshot_dir, f"FAIL_network_error_{diagnostic_tag}.png")
|
| 151 |
+
page.screenshot(path=screenshot_path)
|
| 152 |
+
logger.info(f"已截取网络错误时的屏幕快照: {screenshot_path}")
|
| 153 |
+
except Exception as diag_e:
|
| 154 |
+
logger.error(f"在尝试进行网络错误诊断(截图)时发生额外错误: {diag_e}")
|
| 155 |
+
return # 网络错误,终止
|
| 156 |
+
|
| 157 |
+
# --- 如果导航没有抛出异常,继续执行后续逻辑 ---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
+
logger.info("页面初步加载完成,正在检查并处理初始弹窗...")
|
| 160 |
+
page.wait_for_timeout(2000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
+
final_url = page.url
|
| 163 |
+
logger.info(f"导航完成。最终URL为: {mask_url_for_logging(final_url)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
+
# ... 你原有的URL检查逻辑保持不变 ...
|
| 166 |
+
if "accounts.google.com/v3/signin/identifier" in final_url:
|
| 167 |
+
logger.error("检测到Google登录页面(需要输入邮箱)。Cookie已完全失效")
|
| 168 |
+
page.screenshot(path=os.path.join(screenshot_dir, f"FAIL_identifier_page_{diagnostic_tag}.png"))
|
| 169 |
+
return
|
| 170 |
|
| 171 |
+
# 提取路径部分进行匹配(允许域名重定向)
|
| 172 |
+
expected_path = extract_url_path(expected_url).split('?')[0]
|
| 173 |
+
final_path = extract_url_path(final_url)
|
| 174 |
|
| 175 |
+
if expected_path and expected_path in final_path:
|
| 176 |
+
logger.info(f"URL验证通过。预期路径: {mask_path_for_logging(expected_path)}")
|
| 177 |
|
| 178 |
+
# --- 新的健壮策略:等待加载指示器消失 ---
|
| 179 |
+
# 这是解决竞态条件的关键。错误消息或内容只在初始加载完成后才会出现。
|
| 180 |
+
spinner_locator = page.locator('mat-spinner')
|
| 181 |
+
try:
|
| 182 |
+
logger.info("正在等待加载指示器 (spinner) 消失... (最长等待30秒)")
|
| 183 |
+
# 我们等待spinner变为'隐藏'状态或从DOM中消失。
|
| 184 |
+
spinner_locator.wait_for(state='hidden', timeout=30000)
|
| 185 |
+
logger.info("加载指示器已消失。页面已完成异步加载")
|
| 186 |
+
except TimeoutError:
|
| 187 |
+
logger.error("页面加载指示器在30秒内未消失。页面可能已卡住")
|
| 188 |
+
page.screenshot(path=os.path.join(screenshot_dir, f"FAIL_spinner_stuck_{diagnostic_tag}.png"))
|
| 189 |
+
raise KeepAliveError("页面加载指示器超时")
|
| 190 |
+
|
| 191 |
+
# --- 现在我们可以安全地检查错误消息 ---
|
| 192 |
+
# 我们使用最具体的文本以避免误判。
|
| 193 |
+
auth_error_text = "authentication error"
|
| 194 |
+
auth_error_locator = page.get_by_text(auth_error_text, exact=False)
|
| 195 |
+
|
| 196 |
+
# 这里我们只需要很短的超时时间,因为页面应该是稳定的。
|
| 197 |
+
if auth_error_locator.is_visible(timeout=2000):
|
| 198 |
+
logger.error(f"检测到认证失败的错误横幅: '{auth_error_text}'. Cookie已过期或无效")
|
| 199 |
+
screenshot_path = os.path.join(screenshot_dir, f"FAIL_auth_error_banner_{diagnostic_tag}.png")
|
| 200 |
+
page.screenshot(path=screenshot_path)
|
| 201 |
+
|
| 202 |
+
# html_path = os.path.join(screenshot_dir, f"FAIL_auth_error_banner_{diagnostic_tag}.html")
|
| 203 |
+
# with open(html_path, 'w', encoding='utf-8') as f:
|
| 204 |
+
# f.write(page.content())
|
| 205 |
+
# logger.info(f"已保存包含错误信息的页面HTML: {html_path}")
|
| 206 |
+
return # 明确的失败,因此我们退出。
|
| 207 |
+
|
| 208 |
+
# --- 如果没有错误,进行最终确认(作为后备方案) ---
|
| 209 |
+
logger.info("未检测到认证错误横幅。进行最终确认")
|
| 210 |
+
login_button_cn = page.get_by_role('button', name='登录')
|
| 211 |
+
login_button_en = page.get_by_role('button', name='Login')
|
| 212 |
|
| 213 |
+
if login_button_cn.is_visible(timeout=1000) or login_button_en.is_visible(timeout=1000):
|
| 214 |
+
logger.error("页面上仍显示'登录'按钮。Cookie无效")
|
| 215 |
+
page.screenshot(path=os.path.join(screenshot_dir, f"FAIL_login_button_visible_{diagnostic_tag}.png"))
|
| 216 |
+
return
|
| 217 |
+
|
| 218 |
+
# --- 如果所有检查都通过,我们假设成功 ---
|
| 219 |
+
logger.info("所有验证通过,确认已成功登录")
|
| 220 |
+
|
| 221 |
+
handle_successful_navigation(page, logger, diagnostic_tag, shutdown_event, cookie_validator)
|
| 222 |
+
elif "accounts.google.com/v3/signin/accountchooser" in final_url:
|
| 223 |
+
logger.warning("检测到Google账户选择页面。登录失败或Cookie已过期")
|
| 224 |
+
page.screenshot(path=os.path.join(screenshot_dir, f"FAIL_chooser_click_failed_{diagnostic_tag}.png"))
|
| 225 |
+
return
|
| 226 |
+
else:
|
| 227 |
+
logger.error(f"导航到了意外的URL")
|
| 228 |
+
logger.error(f" 预期路径: {mask_path_for_logging(expected_path)}")
|
| 229 |
+
logger.error(f" 最终路径: {mask_path_for_logging(final_path)}")
|
| 230 |
+
logger.error(f" 最终URL: {mask_url_for_logging(final_url)}")
|
| 231 |
+
page.screenshot(path=os.path.join(screenshot_dir, f"FAIL_unexpected_url_{diagnostic_tag}.png"))
|
| 232 |
return
|
| 233 |
|
| 234 |
+
# 如果运行到这里且没有异常,表示实例正常结束(例如收到关闭信号)
|
| 235 |
+
# 正常结束时重置重试计数器
|
| 236 |
+
retry_count = 0
|
| 237 |
+
return
|
| 238 |
|
| 239 |
+
except KeepAliveError as e:
|
| 240 |
+
retry_count += 1
|
| 241 |
+
if retry_count > max_retries:
|
| 242 |
+
logger.error(f"重试次数已达上限 ({max_retries}),实例不再重启,退出")
|
| 243 |
return
|
| 244 |
+
|
| 245 |
+
# 指数退避:3秒、6秒、12秒、24秒...最长60秒
|
| 246 |
+
delay = min(base_delay * (2 ** (retry_count - 1)), 60)
|
| 247 |
+
logger.error(f"浏览器实例出现错误 (重试 {retry_count}/{max_retries}),将在 {delay} 秒后重启浏览器实例: {e}")
|
| 248 |
+
time.sleep(delay)
|
| 249 |
+
continue
|
| 250 |
+
except KeyboardInterrupt:
|
| 251 |
+
logger.info(f"用户中断,正在关闭...")
|
| 252 |
+
return
|
| 253 |
+
except SystemExit as e:
|
| 254 |
+
# 捕获Cookie验证失败时的系统退出
|
| 255 |
+
if e.code == 1:
|
| 256 |
+
logger.error("Cookie验证失败,关闭进程实例")
|
| 257 |
else:
|
| 258 |
+
logger.info(f"实例正常退出,退出码: {e.code}")
|
| 259 |
+
return
|
| 260 |
+
except Exception as e:
|
| 261 |
+
# 这是一个最终的捕获,用于捕获所有未预料到的错误
|
| 262 |
+
logger.exception(f"运行 Camoufox 实例时发生未预料的严重错误: {e}")
|
| 263 |
+
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
browser/navigation.py
CHANGED
|
@@ -4,6 +4,9 @@ from playwright.sync_api import Page, expect
|
|
| 4 |
from utils.paths import logs_dir
|
| 5 |
from utils.common import ensure_dir
|
| 6 |
|
|
|
|
|
|
|
|
|
|
| 7 |
def handle_untrusted_dialog(page: Page, logger=None):
|
| 8 |
"""
|
| 9 |
检查并处理 "Last modified by..." 的弹窗。
|
|
@@ -83,4 +86,4 @@ def handle_successful_navigation(page: Page, logger, cookie_file_config, shutdow
|
|
| 83 |
logger.info(f"已在保持活动循环出错时截屏: {screenshot_filename}")
|
| 84 |
except Exception as screenshot_e:
|
| 85 |
logger.error(f"在保持活动循环出错时截屏失败: {screenshot_e}")
|
| 86 |
-
|
|
|
|
| 4 |
from utils.paths import logs_dir
|
| 5 |
from utils.common import ensure_dir
|
| 6 |
|
| 7 |
+
class KeepAliveError(Exception):
|
| 8 |
+
pass
|
| 9 |
+
|
| 10 |
def handle_untrusted_dialog(page: Page, logger=None):
|
| 11 |
"""
|
| 12 |
检查并处理 "Last modified by..." 的弹窗。
|
|
|
|
| 86 |
logger.info(f"已在保持活动循环出错时截屏: {screenshot_filename}")
|
| 87 |
except Exception as screenshot_e:
|
| 88 |
logger.error(f"在保持活动循环出错时截屏失败: {screenshot_e}")
|
| 89 |
+
raise KeepAliveError(f"在保持活动循环时出错: {e}")
|