flow2api / src /services /browser_captcha_personal.py
genz27
feat: 添加浏览器打码依赖自动安装和Docker环境检测
8358864
"""
浏览器自动化获取 reCAPTCHA token
使用 nodriver (undetected-chromedriver 继任者) 实现反检测浏览器
支持常驻模式:为每个 project_id 自动创建常驻标签页,即时生成 token
"""
import asyncio
import time
import os
import sys
import subprocess
from typing import Optional
from ..core.logger import debug_logger
# ==================== Docker 环境检测 ====================
def _is_running_in_docker() -> bool:
"""检测是否在 Docker 容器中运行"""
# 方法1: 检查 /.dockerenv 文件
if os.path.exists('/.dockerenv'):
return True
# 方法2: 检查 cgroup
try:
with open('/proc/1/cgroup', 'r') as f:
content = f.read()
if 'docker' in content or 'kubepods' in content or 'containerd' in content:
return True
except:
pass
# 方法3: 检查环境变量
if os.environ.get('DOCKER_CONTAINER') or os.environ.get('KUBERNETES_SERVICE_HOST'):
return True
return False
IS_DOCKER = _is_running_in_docker()
# ==================== nodriver 自动安装 ====================
def _run_pip_install(package: str, use_mirror: bool = False) -> bool:
"""运行 pip install 命令
Args:
package: 包名
use_mirror: 是否使用国内镜像
Returns:
是否安装成功
"""
cmd = [sys.executable, '-m', 'pip', 'install', package]
if use_mirror:
cmd.extend(['-i', 'https://pypi.tuna.tsinghua.edu.cn/simple'])
try:
debug_logger.log_info(f"[BrowserCaptcha] 正在安装 {package}...")
print(f"[BrowserCaptcha] 正在安装 {package}...")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if result.returncode == 0:
debug_logger.log_info(f"[BrowserCaptcha] ✅ {package} 安装成功")
print(f"[BrowserCaptcha] ✅ {package} 安装成功")
return True
else:
debug_logger.log_warning(f"[BrowserCaptcha] {package} 安装失败: {result.stderr[:200]}")
return False
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] {package} 安装异常: {e}")
return False
def _ensure_nodriver_installed() -> bool:
"""确保 nodriver 已安装
Returns:
是否安装成功/已安装
"""
try:
import nodriver
debug_logger.log_info("[BrowserCaptcha] nodriver 已安装")
return True
except ImportError:
pass
debug_logger.log_info("[BrowserCaptcha] nodriver 未安装,开始自动安装...")
print("[BrowserCaptcha] nodriver 未安装,开始自动安装...")
# 先尝试官方源
if _run_pip_install('nodriver', use_mirror=False):
return True
# 官方源失败,尝试国内镜像
debug_logger.log_info("[BrowserCaptcha] 官方源安装失败,尝试国内镜像...")
print("[BrowserCaptcha] 官方源安装失败,尝试国内镜像...")
if _run_pip_install('nodriver', use_mirror=True):
return True
debug_logger.log_error("[BrowserCaptcha] ❌ nodriver 自动安装失败,请手动安装: pip install nodriver")
print("[BrowserCaptcha] ❌ nodriver 自动安装失败,请手动安装: pip install nodriver")
return False
# 尝试导入 nodriver
uc = None
NODRIVER_AVAILABLE = False
if IS_DOCKER:
debug_logger.log_warning("[BrowserCaptcha] 检测到 Docker 环境,内置浏览器打码不可用,请使用第三方打码服务")
print("[BrowserCaptcha] ⚠️ 检测到 Docker 环境,内置浏览器打码不可用")
print("[BrowserCaptcha] 请使用第三方打码服务: yescaptcha, capmonster, ezcaptcha, capsolver")
else:
if _ensure_nodriver_installed():
try:
import nodriver as uc
NODRIVER_AVAILABLE = True
except ImportError as e:
debug_logger.log_error(f"[BrowserCaptcha] nodriver 导入失败: {e}")
print(f"[BrowserCaptcha] ❌ nodriver 导入失败: {e}")
class ResidentTabInfo:
"""常驻标签页信息结构"""
def __init__(self, tab, project_id: str):
self.tab = tab
self.project_id = project_id
self.recaptcha_ready = False
self.created_at = time.time()
class BrowserCaptchaService:
"""浏览器自动化获取 reCAPTCHA token(nodriver 有头模式)
支持两种模式:
1. 常驻模式 (Resident Mode): 为每个 project_id 保持常驻标签页,即时生成 token
2. 传统模式 (Legacy Mode): 每次请求创建新标签页 (fallback)
"""
_instance: Optional['BrowserCaptchaService'] = None
_lock = asyncio.Lock()
def __init__(self, db=None):
"""初始化服务"""
self.headless = False # nodriver 有头模式
self.browser = None
self._initialized = False
self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
self.db = db
# 持久化 profile 目录
self.user_data_dir = os.path.join(os.getcwd(), "browser_data")
# 常驻模式相关属性 (支持多 project_id)
self._resident_tabs: dict[str, 'ResidentTabInfo'] = {} # project_id -> 常驻标签页信息
self._resident_lock = asyncio.Lock() # 保护常驻标签页操作
# 兼容旧 API(保留 single resident 属性作为别名)
self.resident_project_id: Optional[str] = None # 向后兼容
self.resident_tab = None # 向后兼容
self._running = False # 向后兼容
self._recaptcha_ready = False # 向后兼容
@classmethod
async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
"""获取单例实例"""
if cls._instance is None:
async with cls._lock:
if cls._instance is None:
cls._instance = cls(db)
return cls._instance
def _check_available(self):
"""检查服务是否可用"""
if IS_DOCKER:
raise RuntimeError(
"内置浏览器打码在 Docker 环境中不可用。"
"请使用第三方打码服务: yescaptcha, capmonster, ezcaptcha, capsolver"
)
if not NODRIVER_AVAILABLE or uc is None:
raise RuntimeError(
"nodriver 未安装或不可用。"
"请手动安装: pip install nodriver"
)
async def initialize(self):
"""初始化 nodriver 浏览器"""
# 检查服务是否可用
self._check_available()
if self._initialized and self.browser:
# 检查浏览器是否仍然存活
try:
# 尝试获取浏览器信息验证存活
if self.browser.stopped:
debug_logger.log_warning("[BrowserCaptcha] 浏览器已停止,重新初始化...")
self._initialized = False
else:
return
except Exception:
debug_logger.log_warning("[BrowserCaptcha] 浏览器无响应,重新初始化...")
self._initialized = False
try:
debug_logger.log_info(f"[BrowserCaptcha] 正在启动 nodriver 浏览器 (用户数据目录: {self.user_data_dir})...")
# 确保 user_data_dir 存在
os.makedirs(self.user_data_dir, exist_ok=True)
# 启动 nodriver 浏览器
self.browser = await uc.start(
headless=self.headless,
user_data_dir=self.user_data_dir,
sandbox=False, # nodriver 需要此参数来禁用 sandbox
browser_args=[
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-setuid-sandbox',
'--disable-gpu',
'--window-size=1280,720',
'--profile-directory=Default', # 跳过 Profile 选择器页面
]
)
self._initialized = True
debug_logger.log_info(f"[BrowserCaptcha] ✅ nodriver 浏览器已启动 (Profile: {self.user_data_dir})")
except Exception as e:
debug_logger.log_error(f"[BrowserCaptcha] ❌ 浏览器启动失败: {str(e)}")
raise
# ========== 常驻模式 API ==========
async def start_resident_mode(self, project_id: str):
"""启动常驻模式
Args:
project_id: 用于常驻的项目 ID
"""
if self._running:
debug_logger.log_warning("[BrowserCaptcha] 常驻模式已在运行")
return
await self.initialize()
self.resident_project_id = project_id
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
debug_logger.log_info(f"[BrowserCaptcha] 启动常驻模式,访问页面: {website_url}")
# 创建一个独立的新标签页(不使用 main_tab,避免被回收)
self.resident_tab = await self.browser.get(website_url, new_tab=True)
debug_logger.log_info("[BrowserCaptcha] 标签页已创建,等待页面加载...")
# 等待页面加载完成(带重试机制)
page_loaded = False
for retry in range(60):
try:
await asyncio.sleep(1)
ready_state = await self.resident_tab.evaluate("document.readyState")
debug_logger.log_info(f"[BrowserCaptcha] 页面状态: {ready_state} (重试 {retry + 1}/60)")
if ready_state == "complete":
page_loaded = True
break
except ConnectionRefusedError as e:
debug_logger.log_warning(f"[BrowserCaptcha] 标签页连接丢失: {e},尝试重新获取...")
# 标签页可能已关闭,尝试重新创建
try:
self.resident_tab = await self.browser.get(website_url, new_tab=True)
debug_logger.log_info("[BrowserCaptcha] 已重新创建标签页")
except Exception as e2:
debug_logger.log_error(f"[BrowserCaptcha] 重新创建标签页失败: {e2}")
await asyncio.sleep(2)
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] 等待页面异常: {e},重试 {retry + 1}/15...")
await asyncio.sleep(2)
if not page_loaded:
debug_logger.log_error("[BrowserCaptcha] 页面加载超时,常驻模式启动失败")
return
# 等待 reCAPTCHA 加载
self._recaptcha_ready = await self._wait_for_recaptcha(self.resident_tab)
if not self._recaptcha_ready:
debug_logger.log_error("[BrowserCaptcha] reCAPTCHA 加载失败,常驻模式启动失败")
return
self._running = True
debug_logger.log_info(f"[BrowserCaptcha] ✅ 常驻模式已启动 (project: {project_id})")
async def stop_resident_mode(self, project_id: Optional[str] = None):
"""停止常驻模式
Args:
project_id: 指定要关闭的 project_id,如果为 None 则关闭所有常驻标签页
"""
async with self._resident_lock:
if project_id:
# 关闭指定的常驻标签页
await self._close_resident_tab(project_id)
debug_logger.log_info(f"[BrowserCaptcha] 已关闭 project_id={project_id} 的常驻模式")
else:
# 关闭所有常驻标签页
project_ids = list(self._resident_tabs.keys())
for pid in project_ids:
resident_info = self._resident_tabs.pop(pid, None)
if resident_info and resident_info.tab:
try:
await resident_info.tab.close()
except Exception:
pass
debug_logger.log_info(f"[BrowserCaptcha] 已关闭所有常驻标签页 (共 {len(project_ids)} 个)")
# 向后兼容:清理旧属性
if not self._running:
return
self._running = False
if self.resident_tab:
try:
await self.resident_tab.close()
except Exception:
pass
self.resident_tab = None
self.resident_project_id = None
self._recaptcha_ready = False
async def _wait_for_recaptcha(self, tab) -> bool:
"""等待 reCAPTCHA 加载
Returns:
True if reCAPTCHA loaded successfully
"""
debug_logger.log_info("[BrowserCaptcha] 检测 reCAPTCHA...")
# 检查 grecaptcha.enterprise.execute
is_enterprise = await tab.evaluate(
"typeof grecaptcha !== 'undefined' && typeof grecaptcha.enterprise !== 'undefined' && typeof grecaptcha.enterprise.execute === 'function'"
)
if is_enterprise:
debug_logger.log_info("[BrowserCaptcha] reCAPTCHA Enterprise 已加载")
return True
# 尝试注入脚本
debug_logger.log_info("[BrowserCaptcha] 未检测到 reCAPTCHA,注入脚本...")
await tab.evaluate(f"""
(() => {{
if (document.querySelector('script[src*="recaptcha"]')) return;
const script = document.createElement('script');
script.src = 'https://www.google.com/recaptcha/api.js?render={self.website_key}';
script.async = true;
document.head.appendChild(script);
}})()
""")
# 等待脚本加载
await tab.sleep(3)
# 轮询等待 reCAPTCHA 加载
for i in range(20):
is_enterprise = await tab.evaluate(
"typeof grecaptcha !== 'undefined' && typeof grecaptcha.enterprise !== 'undefined' && typeof grecaptcha.enterprise.execute === 'function'"
)
if is_enterprise:
debug_logger.log_info(f"[BrowserCaptcha] reCAPTCHA Enterprise 已加载(等待了 {i * 0.5} 秒)")
return True
await tab.sleep(0.5)
debug_logger.log_warning("[BrowserCaptcha] reCAPTCHA 加载超时")
return False
async def _execute_recaptcha_on_tab(self, tab, action: str = "IMAGE_GENERATION") -> Optional[str]:
"""在指定标签页执行 reCAPTCHA 获取 token
Args:
tab: nodriver 标签页对象
action: reCAPTCHA action类型 (IMAGE_GENERATION 或 VIDEO_GENERATION)
Returns:
reCAPTCHA token 或 None
"""
# 生成唯一变量名避免冲突
ts = int(time.time() * 1000)
token_var = f"_recaptcha_token_{ts}"
error_var = f"_recaptcha_error_{ts}"
execute_script = f"""
(() => {{
window.{token_var} = null;
window.{error_var} = null;
try {{
grecaptcha.enterprise.ready(function() {{
grecaptcha.enterprise.execute('{self.website_key}', {{action: '{action}'}})
.then(function(token) {{
window.{token_var} = token;
}})
.catch(function(err) {{
window.{error_var} = err.message || 'execute failed';
}});
}});
}} catch (e) {{
window.{error_var} = e.message || 'exception';
}}
}})()
"""
# 注入执行脚本
await tab.evaluate(execute_script)
# 轮询等待结果(最多 15 秒)
token = None
for i in range(30):
await tab.sleep(0.5)
token = await tab.evaluate(f"window.{token_var}")
if token:
break
error = await tab.evaluate(f"window.{error_var}")
if error:
debug_logger.log_error(f"[BrowserCaptcha] reCAPTCHA 错误: {error}")
break
# 清理临时变量
try:
await tab.evaluate(f"delete window.{token_var}; delete window.{error_var};")
except:
pass
return token
# ========== 主要 API ==========
async def get_token(self, project_id: str, action: str = "IMAGE_GENERATION") -> Optional[str]:
"""获取 reCAPTCHA token
自动常驻模式:如果该 project_id 没有常驻标签页,则自动创建并常驻
Args:
project_id: Flow项目ID
action: reCAPTCHA action类型
- IMAGE_GENERATION: 图片生成和2K/4K图片放大 (默认)
- VIDEO_GENERATION: 视频生成和视频放大
Returns:
reCAPTCHA token字符串,如果获取失败返回None
"""
# 确保浏览器已初始化
await self.initialize()
# 尝试从常驻标签页获取 token
async with self._resident_lock:
resident_info = self._resident_tabs.get(project_id)
# 如果该 project_id 没有常驻标签页,则自动创建
if resident_info is None:
debug_logger.log_info(f"[BrowserCaptcha] project_id={project_id} 没有常驻标签页,正在创建...")
resident_info = await self._create_resident_tab(project_id)
if resident_info is None:
debug_logger.log_warning(f"[BrowserCaptcha] 无法为 project_id={project_id} 创建常驻标签页,fallback 到传统模式")
return await self._get_token_legacy(project_id, action)
self._resident_tabs[project_id] = resident_info
debug_logger.log_info(f"[BrowserCaptcha] ✅ 已为 project_id={project_id} 创建常驻标签页 (当前共 {len(self._resident_tabs)} 个)")
# 使用常驻标签页生成 token
if resident_info and resident_info.recaptcha_ready and resident_info.tab:
start_time = time.time()
debug_logger.log_info(f"[BrowserCaptcha] 从常驻标签页即时生成 token (project: {project_id}, action: {action})...")
try:
token = await self._execute_recaptcha_on_tab(resident_info.tab, action)
duration_ms = (time.time() - start_time) * 1000
if token:
debug_logger.log_info(f"[BrowserCaptcha] ✅ Token生成成功(耗时 {duration_ms:.0f}ms)")
return token
else:
debug_logger.log_warning(f"[BrowserCaptcha] 常驻标签页生成失败 (project: {project_id}),尝试重建...")
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] 常驻标签页异常: {e},尝试重建...")
# 常驻标签页失效,尝试重建
async with self._resident_lock:
await self._close_resident_tab(project_id)
resident_info = await self._create_resident_tab(project_id)
if resident_info:
self._resident_tabs[project_id] = resident_info
# 重建后立即尝试生成
try:
token = await self._execute_recaptcha_on_tab(resident_info.tab, action)
if token:
debug_logger.log_info(f"[BrowserCaptcha] ✅ 重建后 Token生成成功")
return token
except Exception:
pass
# 最终 Fallback: 使用传统模式
debug_logger.log_warning(f"[BrowserCaptcha] 所有常驻方式失败,fallback 到传统模式 (project: {project_id})")
return await self._get_token_legacy(project_id, action)
async def _create_resident_tab(self, project_id: str) -> Optional[ResidentTabInfo]:
"""为指定 project_id 创建常驻标签页
Args:
project_id: 项目 ID
Returns:
ResidentTabInfo 对象,或 None(创建失败)
"""
try:
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
debug_logger.log_info(f"[BrowserCaptcha] 为 project_id={project_id} 创建常驻标签页,访问: {website_url}")
# 创建新标签页
tab = await self.browser.get(website_url, new_tab=True)
# 等待页面加载完成
page_loaded = False
for retry in range(60):
try:
await asyncio.sleep(1)
ready_state = await tab.evaluate("document.readyState")
if ready_state == "complete":
page_loaded = True
break
except ConnectionRefusedError as e:
debug_logger.log_warning(f"[BrowserCaptcha] 标签页连接丢失: {e}")
return None
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] 等待页面异常: {e},重试 {retry + 1}/60...")
await asyncio.sleep(1)
if not page_loaded:
debug_logger.log_error(f"[BrowserCaptcha] 页面加载超时 (project: {project_id})")
try:
await tab.close()
except:
pass
return None
# 等待 reCAPTCHA 加载
recaptcha_ready = await self._wait_for_recaptcha(tab)
if not recaptcha_ready:
debug_logger.log_error(f"[BrowserCaptcha] reCAPTCHA 加载失败 (project: {project_id})")
try:
await tab.close()
except:
pass
return None
# 创建常驻信息对象
resident_info = ResidentTabInfo(tab, project_id)
resident_info.recaptcha_ready = True
debug_logger.log_info(f"[BrowserCaptcha] ✅ 常驻标签页创建成功 (project: {project_id})")
return resident_info
except Exception as e:
debug_logger.log_error(f"[BrowserCaptcha] 创建常驻标签页异常: {e}")
return None
async def _close_resident_tab(self, project_id: str):
"""关闭指定 project_id 的常驻标签页
Args:
project_id: 项目 ID
"""
resident_info = self._resident_tabs.pop(project_id, None)
if resident_info and resident_info.tab:
try:
await resident_info.tab.close()
debug_logger.log_info(f"[BrowserCaptcha] 已关闭 project_id={project_id} 的常驻标签页")
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] 关闭标签页时异常: {e}")
async def _get_token_legacy(self, project_id: str, action: str = "IMAGE_GENERATION") -> Optional[str]:
"""传统模式获取 reCAPTCHA token(每次创建新标签页)
Args:
project_id: Flow项目ID
action: reCAPTCHA action类型 (IMAGE_GENERATION 或 VIDEO_GENERATION)
Returns:
reCAPTCHA token字符串,如果获取失败返回None
"""
# 确保浏览器已启动
if not self._initialized or not self.browser:
await self.initialize()
start_time = time.time()
tab = None
try:
website_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
debug_logger.log_info(f"[BrowserCaptcha] [Legacy] 访问页面: {website_url}")
# 新建标签页并访问页面
tab = await self.browser.get(website_url)
# 等待页面完全加载(增加等待时间)
debug_logger.log_info("[BrowserCaptcha] [Legacy] 等待页面加载...")
await tab.sleep(3)
# 等待页面 DOM 完成
for _ in range(10):
ready_state = await tab.evaluate("document.readyState")
if ready_state == "complete":
break
await tab.sleep(0.5)
# 等待 reCAPTCHA 加载
recaptcha_ready = await self._wait_for_recaptcha(tab)
if not recaptcha_ready:
debug_logger.log_error("[BrowserCaptcha] [Legacy] reCAPTCHA 无法加载")
return None
# 执行 reCAPTCHA
debug_logger.log_info(f"[BrowserCaptcha] [Legacy] 执行 reCAPTCHA 验证 (action: {action})...")
token = await self._execute_recaptcha_on_tab(tab, action)
duration_ms = (time.time() - start_time) * 1000
if token:
debug_logger.log_info(f"[BrowserCaptcha] [Legacy] ✅ Token获取成功(耗时 {duration_ms:.0f}ms)")
return token
else:
debug_logger.log_error("[BrowserCaptcha] [Legacy] Token获取失败(返回null)")
return None
except Exception as e:
debug_logger.log_error(f"[BrowserCaptcha] [Legacy] 获取token异常: {str(e)}")
return None
finally:
# 关闭标签页(但保留浏览器)
if tab:
try:
await tab.close()
except Exception:
pass
async def close(self):
"""关闭浏览器"""
# 先停止所有常驻模式(关闭所有常驻标签页)
await self.stop_resident_mode()
try:
if self.browser:
try:
self.browser.stop()
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] 关闭浏览器时出现异常: {str(e)}")
finally:
self.browser = None
self._initialized = False
self._resident_tabs.clear() # 确保清空常驻字典
debug_logger.log_info("[BrowserCaptcha] 浏览器已关闭")
except Exception as e:
debug_logger.log_error(f"[BrowserCaptcha] 关闭浏览器异常: {str(e)}")
async def open_login_window(self):
"""打开登录窗口供用户手动登录 Google"""
await self.initialize()
tab = await self.browser.get("https://accounts.google.com/")
debug_logger.log_info("[BrowserCaptcha] 请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
print("请在打开的浏览器中登录账号。登录完成后,无需关闭浏览器,脚本下次运行时会自动使用此状态。")
# ========== Session Token 刷新 ==========
async def refresh_session_token(self, project_id: str) -> Optional[str]:
"""从常驻标签页获取最新的 Session Token
复用 reCAPTCHA 常驻标签页,通过刷新页面并从 cookies 中提取
__Secure-next-auth.session-token
Args:
project_id: 项目ID,用于定位常驻标签页
Returns:
新的 Session Token,如果获取失败返回 None
"""
# 确保浏览器已初始化
await self.initialize()
start_time = time.time()
debug_logger.log_info(f"[BrowserCaptcha] 开始刷新 Session Token (project: {project_id})...")
# 尝试获取或创建常驻标签页
async with self._resident_lock:
resident_info = self._resident_tabs.get(project_id)
# 如果该 project_id 没有常驻标签页,则创建
if resident_info is None:
debug_logger.log_info(f"[BrowserCaptcha] project_id={project_id} 没有常驻标签页,正在创建...")
resident_info = await self._create_resident_tab(project_id)
if resident_info is None:
debug_logger.log_warning(f"[BrowserCaptcha] 无法为 project_id={project_id} 创建常驻标签页")
return None
self._resident_tabs[project_id] = resident_info
if not resident_info or not resident_info.tab:
debug_logger.log_error(f"[BrowserCaptcha] 无法获取常驻标签页")
return None
tab = resident_info.tab
try:
# 刷新页面以获取最新的 cookies
debug_logger.log_info(f"[BrowserCaptcha] 刷新常驻标签页以获取最新 cookies...")
await tab.reload()
# 等待页面加载完成
for i in range(30):
await asyncio.sleep(1)
try:
ready_state = await tab.evaluate("document.readyState")
if ready_state == "complete":
break
except Exception:
pass
# 额外等待确保 cookies 已设置
await asyncio.sleep(2)
# 从 cookies 中提取 __Secure-next-auth.session-token
# nodriver 可以通过 browser 获取 cookies
session_token = None
try:
# 使用 nodriver 的 cookies API 获取所有 cookies
cookies = await self.browser.cookies.get_all()
for cookie in cookies:
if cookie.name == "__Secure-next-auth.session-token":
session_token = cookie.value
break
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] 通过 cookies API 获取失败: {e},尝试从 document.cookie 获取...")
# 备选方案:通过 JavaScript 获取 (注意:HttpOnly cookies 可能无法通过此方式获取)
try:
all_cookies = await tab.evaluate("document.cookie")
if all_cookies:
for part in all_cookies.split(";"):
part = part.strip()
if part.startswith("__Secure-next-auth.session-token="):
session_token = part.split("=", 1)[1]
break
except Exception as e2:
debug_logger.log_error(f"[BrowserCaptcha] document.cookie 获取失败: {e2}")
duration_ms = (time.time() - start_time) * 1000
if session_token:
debug_logger.log_info(f"[BrowserCaptcha] ✅ Session Token 获取成功(耗时 {duration_ms:.0f}ms)")
return session_token
else:
debug_logger.log_error(f"[BrowserCaptcha] ❌ 未找到 __Secure-next-auth.session-token cookie")
return None
except Exception as e:
debug_logger.log_error(f"[BrowserCaptcha] 刷新 Session Token 异常: {str(e)}")
# 常驻标签页可能已失效,尝试重建
async with self._resident_lock:
await self._close_resident_tab(project_id)
resident_info = await self._create_resident_tab(project_id)
if resident_info:
self._resident_tabs[project_id] = resident_info
# 重建后再次尝试获取
try:
cookies = await self.browser.cookies.get_all()
for cookie in cookies:
if cookie.name == "__Secure-next-auth.session-token":
debug_logger.log_info(f"[BrowserCaptcha] ✅ 重建后 Session Token 获取成功")
return cookie.value
except Exception:
pass
return None
# ========== 状态查询 ==========
def is_resident_mode_active(self) -> bool:
"""检查是否有任何常驻标签页激活"""
return len(self._resident_tabs) > 0 or self._running
def get_resident_count(self) -> int:
"""获取当前常驻标签页数量"""
return len(self._resident_tabs)
def get_resident_project_ids(self) -> list[str]:
"""获取所有当前常驻的 project_id 列表"""
return list(self._resident_tabs.keys())
def get_resident_project_id(self) -> Optional[str]:
"""获取当前常驻的 project_id(向后兼容,返回第一个)"""
if self._resident_tabs:
return next(iter(self._resident_tabs.keys()))
return self.resident_project_id