支持复用色花堂常驻浏览器会话
- 为 sehuatang_push 增加远程调试端口附着能力,优先复用常驻 Chrome 浏览器\n- 区分外部浏览器与自管理浏览器,避免任务结束时误关闭用户正在使用的浏览器\n- 从插件配置和任务 payload 读取浏览器复用参数,并补充 browser 配置项说明
This commit is contained in:
@@ -10,3 +10,16 @@ enabled = true
|
||||
[push]
|
||||
# 默认 @ 的用户名(可在后台任务 payload 中覆盖)
|
||||
at_user = "Jyunere"
|
||||
|
||||
[browser]
|
||||
# 是否优先复用已经长期在线的 Chrome 浏览器。
|
||||
# 复用模式要求该浏览器启动时带上 --remote-debugging-port=9222 这类参数。
|
||||
reuse_existing_browser = true
|
||||
|
||||
# 常驻浏览器暴露出来的远程调试地址。
|
||||
debugger_host = "127.0.0.1"
|
||||
debugger_port = 9222
|
||||
|
||||
# 当常驻浏览器不可用时,是否允许插件回退到“自己启动一个临时浏览器”的旧模式。
|
||||
# 如果你明确不希望插件再自己管理浏览器,可以改成 false。
|
||||
allow_launch_fallback = true
|
||||
|
||||
@@ -46,6 +46,23 @@ class SehuatangPushPlugin(MessagePluginInterface):
|
||||
super().__init__()
|
||||
self.feature = self.register_feature()
|
||||
|
||||
def _build_browser_config(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""整理浏览器配置,优先读取插件配置,再允许任务 payload 做局部覆盖。"""
|
||||
# 这里把浏览器连接参数集中整理出来,
|
||||
# 目的是让调度入口不需要关心太多细节,同时兼容后续从后台任务动态覆盖端口。
|
||||
browser_cfg = dict(self._config.get("browser", {}) or {})
|
||||
|
||||
if "reuse_existing_browser" in payload:
|
||||
browser_cfg["reuse_existing_browser"] = payload.get("reuse_existing_browser")
|
||||
if "debugger_host" in payload:
|
||||
browser_cfg["debugger_host"] = payload.get("debugger_host")
|
||||
if "debugger_port" in payload:
|
||||
browser_cfg["debugger_port"] = payload.get("debugger_port")
|
||||
if "allow_launch_fallback" in payload:
|
||||
browser_cfg["allow_launch_fallback"] = payload.get("allow_launch_fallback")
|
||||
|
||||
return browser_cfg
|
||||
|
||||
def initialize(self, context: Dict[str, Any]) -> bool:
|
||||
return True
|
||||
|
||||
@@ -94,11 +111,15 @@ class SehuatangPushPlugin(MessagePluginInterface):
|
||||
|
||||
payload = context.get("payload") or {}
|
||||
at_user = str(payload.get("at_user", "Jyunere") or "Jyunere").strip()
|
||||
browser_config = self._build_browser_config(payload)
|
||||
strict_reuse_existing_browser = bool(browser_config.get("reuse_existing_browser")) and not bool(
|
||||
browser_config.get("allow_launch_fallback", True)
|
||||
)
|
||||
|
||||
# 兼容历史逻辑:优先使用 undetected 方案,失败后回退普通抓取。
|
||||
try:
|
||||
ok, path = await asyncio.to_thread(pdf_file_path_undetected)
|
||||
if not ok:
|
||||
ok, path = await asyncio.to_thread(pdf_file_path_undetected, browser_config)
|
||||
if not ok and not strict_reuse_existing_browser:
|
||||
ok, path = await asyncio.to_thread(pdf_file_path)
|
||||
if not ok:
|
||||
return {"success": False, "summary": "PDF 生成失败", "detail": {}}
|
||||
|
||||
@@ -5,6 +5,7 @@ import subprocess
|
||||
import requests
|
||||
from io import BytesIO
|
||||
import undetected_chromedriver as uc
|
||||
from selenium import webdriver
|
||||
|
||||
# 注意:不要禁用析构函数,否则会导致Chrome进程泄漏
|
||||
# if os.name == 'nt':
|
||||
@@ -30,6 +31,57 @@ from PyPDF2 import PdfReader, PdfWriter
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def _build_chrome_options(debugger_address=None):
|
||||
"""构建 Chrome 启动参数;当提供调试地址时,表示附着到外部常驻浏览器。"""
|
||||
# 这里统一使用 Selenium 的 ChromeOptions,
|
||||
# 这样既能给 undetected_chromedriver 复用,也能给 Selenium 直连现有浏览器复用。
|
||||
options = webdriver.ChromeOptions()
|
||||
|
||||
if debugger_address:
|
||||
# 当需要复用已经启动的浏览器时,只需要告诉 ChromeDriver 去连接哪个调试地址。
|
||||
# 这种模式下不会新拉起浏览器进程,因此也不应该再塞 headless 等启动参数。
|
||||
options.add_experimental_option("debuggerAddress", debugger_address)
|
||||
return options
|
||||
|
||||
# 下面这组参数用于“自己启动浏览器”的场景。
|
||||
# Linux 服务器上继续使用 headless,避免任务依赖桌面环境。
|
||||
if os.name != 'nt':
|
||||
options.headless = True
|
||||
options.add_argument('--headless=new')
|
||||
else:
|
||||
options.headless = False
|
||||
|
||||
options.add_argument('--no-sandbox')
|
||||
options.add_argument('--disable-gpu')
|
||||
options.add_argument('--disable-dev-shm-usage')
|
||||
options.add_argument('--disable-extensions')
|
||||
options.add_argument('--disable-background-networking')
|
||||
options.add_argument('--disable-crash-reporter')
|
||||
options.add_argument('--disable-in-process-stack-traces')
|
||||
options.add_argument('--disable-logging')
|
||||
options.add_argument('--disable-dev-shm-usage')
|
||||
return options
|
||||
|
||||
|
||||
def _normalize_browser_config(browser_config=None):
|
||||
"""整理浏览器配置,保证后续逻辑总能拿到结构稳定的字典。"""
|
||||
browser_config = browser_config or {}
|
||||
return {
|
||||
"reuse_existing_browser": bool(browser_config.get("reuse_existing_browser", False)),
|
||||
"debugger_host": str(browser_config.get("debugger_host", "127.0.0.1") or "127.0.0.1").strip(),
|
||||
"debugger_port": int(browser_config.get("debugger_port", 9222) or 9222),
|
||||
"allow_launch_fallback": bool(browser_config.get("allow_launch_fallback", True)),
|
||||
}
|
||||
|
||||
|
||||
def _probe_existing_browser(debugger_host, debugger_port):
|
||||
"""探测常驻浏览器调试端口是否可用,并返回浏览器端的元信息。"""
|
||||
version_url = f"http://{debugger_host}:{debugger_port}/json/version"
|
||||
response = requests.get(version_url, timeout=5)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def _detect_local_chrome_major_version():
|
||||
"""检测本机 Chrome/Chromium 主版本号,尽量让 ChromeDriver 跟浏览器版本保持一致。"""
|
||||
# 这里按不同平台准备一组常见的 Chrome/Chromium 可执行文件位置。
|
||||
@@ -111,6 +163,42 @@ def _create_chrome_driver(options):
|
||||
raise
|
||||
|
||||
|
||||
def _attach_to_existing_browser(browser_config):
|
||||
"""附着到已经启动的 Chrome 调试会话,避免重复创建和管理浏览器进程。"""
|
||||
debugger_host = browser_config["debugger_host"]
|
||||
debugger_port = browser_config["debugger_port"]
|
||||
debugger_address = f"{debugger_host}:{debugger_port}"
|
||||
|
||||
# 先探测调试端口,能提前把“端口没开”与“连接成功”的原因写清楚,排查会更轻松。
|
||||
browser_meta = _probe_existing_browser(debugger_host, debugger_port)
|
||||
browser_version = browser_meta.get("Browser", "未知版本")
|
||||
logger.info(f"准备复用常驻浏览器: {debugger_address},浏览器信息: {browser_version}")
|
||||
|
||||
options = _build_chrome_options(debugger_address=debugger_address)
|
||||
driver = webdriver.Chrome(options=options)
|
||||
return driver
|
||||
|
||||
|
||||
def _create_browser_session(browser_config=None):
|
||||
"""根据配置决定是复用常驻浏览器,还是回退到自管理浏览器。"""
|
||||
normalized_config = _normalize_browser_config(browser_config)
|
||||
|
||||
if normalized_config["reuse_existing_browser"]:
|
||||
try:
|
||||
driver = _attach_to_existing_browser(normalized_config)
|
||||
# attached_to_existing_browser=True 表示后续清理时不能去关闭用户自己的常驻浏览器。
|
||||
return driver, True
|
||||
except Exception as exc:
|
||||
if not normalized_config["allow_launch_fallback"]:
|
||||
logger.error(f"复用常驻浏览器失败,且已禁止启动备用浏览器: {exc}")
|
||||
raise
|
||||
logger.warning(f"复用常驻浏览器失败,准备回退到自管理浏览器: {exc}")
|
||||
|
||||
options = _build_chrome_options()
|
||||
driver = _create_chrome_driver(options)
|
||||
return driver, False
|
||||
|
||||
|
||||
def download_image(url, session):
|
||||
"""使用同步的 session 下载图片,确保 Cookie 一致"""
|
||||
try:
|
||||
@@ -138,34 +226,13 @@ def add_pdf_encryption(pdf_file, password="4000"):
|
||||
logger.error(f"PDF加密失败: {e}")
|
||||
|
||||
|
||||
def fetch_and_create_pdf(url):
|
||||
def fetch_and_create_pdf(url, browser_config=None):
|
||||
driver = None
|
||||
service = None
|
||||
attached_to_existing_browser = False
|
||||
try:
|
||||
options = uc.ChromeOptions()
|
||||
# 规避检测的关键配置
|
||||
# 在Linux服务器上使用headless模式
|
||||
if os.name != 'nt':
|
||||
options.headless = True
|
||||
options.add_argument('--headless=new') # 使用新版headless模式
|
||||
else:
|
||||
options.headless = False
|
||||
|
||||
options.add_argument('--no-sandbox')
|
||||
options.add_argument('--disable-gpu')
|
||||
options.add_argument('--disable-dev-shm-usage')
|
||||
options.add_argument('--disable-extensions')
|
||||
options.add_argument('--disable-background-networking')
|
||||
# 确保进程能被正确清理
|
||||
options.add_argument('--disable-crash-reporter')
|
||||
options.add_argument('--disable-in-process-stack-traces')
|
||||
options.add_argument('--disable-logging')
|
||||
options.add_argument('--disable-dev-shm-usage')
|
||||
|
||||
# 创建 driver 实例。
|
||||
# 这里不再把版本硬编码成 144,而是优先跟随本机 Chrome 版本;
|
||||
# 如果首次启动时仍然遇到版本不匹配,再从报错里解析真实版本自动重试。
|
||||
driver = _create_chrome_driver(options)
|
||||
# 优先复用外部常驻浏览器,避免插件自己创建和管理浏览器进程;
|
||||
# 如果未启用复用,或者复用失败但允许回退,则继续使用原来的自管理浏览器方案。
|
||||
driver, attached_to_existing_browser = _create_browser_session(browser_config)
|
||||
|
||||
logger.info(f"正在访问: {url}")
|
||||
driver.get(url)
|
||||
@@ -264,24 +331,30 @@ def fetch_and_create_pdf(url):
|
||||
logger.exception(f"抓取异常: {e}")
|
||||
return ""
|
||||
finally:
|
||||
# --- 确保Chrome进程被完全关闭 ---
|
||||
if driver:
|
||||
try:
|
||||
logger.debug("正在安全关闭浏览器...")
|
||||
# 先关闭所有标签页和窗口
|
||||
if attached_to_existing_browser:
|
||||
try:
|
||||
driver.close()
|
||||
# 这里是“借用”用户已经在运行的浏览器,只释放当前 WebDriver 会话即可。
|
||||
# 明确不执行 close(),避免把用户正在用的标签页关掉。
|
||||
logger.debug("当前使用的是外部常驻浏览器,仅释放 WebDriver 会话,不关闭浏览器本体")
|
||||
driver.quit()
|
||||
except Exception as e:
|
||||
logger.warning(f"关闭浏览器窗口时出错: {e}")
|
||||
logger.error(f"释放外部浏览器会话时出错: {e}")
|
||||
else:
|
||||
try:
|
||||
logger.debug("正在安全关闭自管理浏览器...")
|
||||
try:
|
||||
driver.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"关闭浏览器窗口时出错: {e}")
|
||||
|
||||
# 强制退出所有Chrome进程
|
||||
driver.quit()
|
||||
logger.debug("浏览器已完全关闭")
|
||||
except Exception as e:
|
||||
logger.error(f"关闭浏览器时出错: {e}")
|
||||
driver.quit()
|
||||
logger.debug("浏览器已完全关闭")
|
||||
except Exception as e:
|
||||
logger.error(f"关闭浏览器时出错: {e}")
|
||||
|
||||
# 额外保险:强制清理残留的Chrome进程(仅Linux)
|
||||
if os.name != 'nt':
|
||||
# 只有当本次浏览器由当前任务自己拉起时,才需要额外清理潜在残留进程。
|
||||
if os.name != 'nt' and not attached_to_existing_browser:
|
||||
try:
|
||||
import psutil
|
||||
current_user = os.getlogin()
|
||||
@@ -302,10 +375,11 @@ def fetch_and_create_pdf(url):
|
||||
logger.warning(f"强制清理Chrome进程时出错: {e}")
|
||||
|
||||
|
||||
def pdf_file_path_undetected():
|
||||
def pdf_file_path_undetected(browser_config=None):
|
||||
try:
|
||||
url = 'https://www.sehuatang.net/forum.php?mod=forumdisplay&fid=103&filter=typeid&typeid=481'
|
||||
pdf_path = fetch_and_create_pdf(url)
|
||||
# 将插件配置透传给抓取函数,便于优先复用外部常驻浏览器会话。
|
||||
pdf_path = fetch_and_create_pdf(url, browser_config=browser_config)
|
||||
if pdf_path:
|
||||
logger.info(f"返回的PDF文件路径:{pdf_path}")
|
||||
return True, pdf_path
|
||||
|
||||
Reference in New Issue
Block a user