194 lines
6.3 KiB
Python
194 lines
6.3 KiB
Python
|
|
import asyncio
|
||
|
|
import json
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
from loguru import logger
|
||
|
|
from playwright.async_api import (
|
||
|
|
Browser,
|
||
|
|
BrowserContext,
|
||
|
|
Page,
|
||
|
|
async_playwright,
|
||
|
|
)
|
||
|
|
|
||
|
|
from .config import settings
|
||
|
|
|
||
|
|
STEALTH_JS = """
|
||
|
|
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
|
||
|
|
Object.defineProperty(navigator, 'plugins', {
|
||
|
|
get: () => [1, 2, 3, 4, 5],
|
||
|
|
});
|
||
|
|
Object.defineProperty(navigator, 'languages', {
|
||
|
|
get: () => ['zh-CN', 'zh', 'en'],
|
||
|
|
});
|
||
|
|
Object.defineProperty(navigator, 'platform', {
|
||
|
|
get: () => 'Win32',
|
||
|
|
});
|
||
|
|
window.chrome = { runtime: {} };
|
||
|
|
const originalQuery = window.navigator.permissions.query;
|
||
|
|
window.navigator.permissions.query = (parameters) =>
|
||
|
|
parameters.name === 'notifications'
|
||
|
|
? Promise.resolve({ state: Notification.permission })
|
||
|
|
: originalQuery(parameters);
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
class BrowserManager:
|
||
|
|
_instance: Optional["BrowserManager"] = None
|
||
|
|
_lock: asyncio.Lock = asyncio.Lock()
|
||
|
|
|
||
|
|
def __new__(cls) -> "BrowserManager":
|
||
|
|
if cls._instance is None:
|
||
|
|
cls._instance = super().__new__(cls)
|
||
|
|
return cls._instance
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
if not hasattr(self, "_initialized"):
|
||
|
|
self._initialized = True
|
||
|
|
self.playwright = None
|
||
|
|
self.browser: Optional[Browser] = None
|
||
|
|
self.context: Optional[BrowserContext] = None
|
||
|
|
self.page: Optional[Page] = None
|
||
|
|
self._ready = False
|
||
|
|
|
||
|
|
@property
|
||
|
|
def ready(self) -> bool:
|
||
|
|
return self._ready and self.page is not None and not self.page.is_closed()
|
||
|
|
|
||
|
|
async def start(self) -> Page:
|
||
|
|
async with self._lock:
|
||
|
|
if self.ready:
|
||
|
|
return self.page
|
||
|
|
|
||
|
|
logger.info("Starting Playwright browser...")
|
||
|
|
self.playwright = await async_playwright().start()
|
||
|
|
|
||
|
|
self.browser = await self.playwright.chromium.launch(
|
||
|
|
headless=settings.browser_headless,
|
||
|
|
args=[
|
||
|
|
"--no-sandbox",
|
||
|
|
"--disable-setuid-sandbox",
|
||
|
|
"--disable-dev-shm-usage",
|
||
|
|
"--disable-blink-features=AutomationControlled",
|
||
|
|
"--disable-features=IsolateOrigins,site-per-process",
|
||
|
|
"--window-size=1280,720",
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
storage_state = (
|
||
|
|
str(settings.state_file) if settings.state_file.exists() else None
|
||
|
|
)
|
||
|
|
|
||
|
|
self.context = await self.browser.new_context(
|
||
|
|
viewport={
|
||
|
|
"width": settings.viewport_width,
|
||
|
|
"height": settings.viewport_height,
|
||
|
|
},
|
||
|
|
user_agent=(
|
||
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||
|
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||
|
|
"Chrome/131.0.0.0 Safari/537.36"
|
||
|
|
),
|
||
|
|
locale="zh-CN",
|
||
|
|
timezone_id="Asia/Shanghai",
|
||
|
|
storage_state=storage_state,
|
||
|
|
)
|
||
|
|
|
||
|
|
await self.context.add_init_script(STEALTH_JS)
|
||
|
|
self.context.set_default_timeout(settings.page_load_timeout)
|
||
|
|
self.context.set_default_navigation_timeout(60000)
|
||
|
|
|
||
|
|
self.page = await self.context.new_page()
|
||
|
|
|
||
|
|
await self._navigate_to_doubao()
|
||
|
|
self._ready = True
|
||
|
|
logger.info("Browser started and navigated to Doubao")
|
||
|
|
return self.page
|
||
|
|
|
||
|
|
async def _navigate_to_doubao(self):
|
||
|
|
if not self.page:
|
||
|
|
return
|
||
|
|
try:
|
||
|
|
await self.page.goto(settings.doubao_url, wait_until="domcontentloaded")
|
||
|
|
await self.page.wait_for_timeout(3000)
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"Navigation warning: {e}")
|
||
|
|
|
||
|
|
async def save_state(self):
|
||
|
|
if self.context:
|
||
|
|
await self.context.storage_state(path=str(settings.state_file))
|
||
|
|
cookies = await self.context.cookies()
|
||
|
|
with open(settings.cookies_file, "w", encoding="utf-8") as f:
|
||
|
|
json.dump(cookies, f, ensure_ascii=False, indent=2)
|
||
|
|
logger.info(f"Saved browser state ({len(cookies)} cookies)")
|
||
|
|
|
||
|
|
async def check_login(self) -> bool:
|
||
|
|
"""Check if the user is logged in by looking for login indicators."""
|
||
|
|
if not self.ready:
|
||
|
|
return False
|
||
|
|
try:
|
||
|
|
login_indicators = await self.page.evaluate(
|
||
|
|
"""
|
||
|
|
() => {
|
||
|
|
const body = document.body ? document.body.innerText : '';
|
||
|
|
const url = window.location.href;
|
||
|
|
if (url.includes('login') || url.includes('passport')) return false;
|
||
|
|
|
||
|
|
const loginBtn = document.querySelector(
|
||
|
|
'[class*="login"], [class*="sign-in"], [data-testid*="login"]'
|
||
|
|
);
|
||
|
|
if (loginBtn && loginBtn.offsetParent !== null) return false;
|
||
|
|
|
||
|
|
const inputArea = document.querySelector(
|
||
|
|
'textarea, [contenteditable="true"], [role="textbox"]'
|
||
|
|
);
|
||
|
|
return !!inputArea;
|
||
|
|
}
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
return login_indicators
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"Login check failed: {e}")
|
||
|
|
return False
|
||
|
|
|
||
|
|
async def get_page(self) -> Page:
|
||
|
|
if not self.ready:
|
||
|
|
await self.start()
|
||
|
|
return self.page
|
||
|
|
|
||
|
|
async def reload(self):
|
||
|
|
if self.page:
|
||
|
|
await self.page.reload(wait_until="domcontentloaded")
|
||
|
|
await self.page.wait_for_timeout(2000)
|
||
|
|
|
||
|
|
async def restart(self):
|
||
|
|
logger.info("Restarting browser session...")
|
||
|
|
await self.stop()
|
||
|
|
await asyncio.sleep(1)
|
||
|
|
await self.start()
|
||
|
|
|
||
|
|
async def stop(self):
|
||
|
|
self._ready = False
|
||
|
|
try:
|
||
|
|
if self.context:
|
||
|
|
await self.save_state()
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(f"Failed to save state during stop: {e}")
|
||
|
|
try:
|
||
|
|
if self.browser:
|
||
|
|
await self.browser.close()
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
try:
|
||
|
|
if self.playwright:
|
||
|
|
await self.playwright.stop()
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
self.page = None
|
||
|
|
self.context = None
|
||
|
|
self.browser = None
|
||
|
|
self.playwright = None
|
||
|
|
logger.info("Browser stopped")
|
||
|
|
|
||
|
|
|
||
|
|
browser_manager = BrowserManager()
|