Revert "支持复用色花堂常驻浏览器会话"

This reverts commit a8070a7214.
This commit is contained in:
liuwei
2026-04-27 15:44:55 +08:00
parent a8070a7214
commit 9a84b6b313
3 changed files with 43 additions and 151 deletions

View File

@@ -10,16 +10,3 @@ 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

View File

@@ -46,23 +46,6 @@ 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
@@ -111,15 +94,11 @@ 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, browser_config)
if not ok and not strict_reuse_existing_browser:
ok, path = await asyncio.to_thread(pdf_file_path_undetected)
if not ok:
ok, path = await asyncio.to_thread(pdf_file_path)
if not ok:
return {"success": False, "summary": "PDF 生成失败", "detail": {}}

View File

@@ -5,7 +5,6 @@ import subprocess
import requests
from io import BytesIO
import undetected_chromedriver as uc
from selenium import webdriver
# 注意不要禁用析构函数否则会导致Chrome进程泄漏
# if os.name == 'nt':
@@ -31,57 +30,6 @@ 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 可执行文件位置。
@@ -163,42 +111,6 @@ 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:
@@ -226,13 +138,34 @@ def add_pdf_encryption(pdf_file, password="4000"):
logger.error(f"PDF加密失败: {e}")
def fetch_and_create_pdf(url, browser_config=None):
def fetch_and_create_pdf(url):
driver = None
attached_to_existing_browser = False
service = None
try:
# 优先复用外部常驻浏览器,避免插件自己创建和管理浏览器进程;
# 如果未启用复用,或者复用失败但允许回退,则继续使用原来的自管理浏览器方案。
driver, attached_to_existing_browser = _create_browser_session(browser_config)
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)
logger.info(f"正在访问: {url}")
driver.get(url)
@@ -331,30 +264,24 @@ def fetch_and_create_pdf(url, browser_config=None):
logger.exception(f"抓取异常: {e}")
return ""
finally:
# --- 确保Chrome进程被完全关闭 ---
if driver:
if attached_to_existing_browser:
try:
logger.debug("正在安全关闭浏览器...")
# 先关闭所有标签页和窗口
try:
# 这里是“借用”用户已经在运行的浏览器,只释放当前 WebDriver 会话即可。
# 明确不执行 close(),避免把用户正在用的标签页关掉。
logger.debug("当前使用的是外部常驻浏览器,仅释放 WebDriver 会话,不关闭浏览器本体")
driver.quit()
driver.close()
except Exception as e:
logger.error(f"释放外部浏览器会话时出错: {e}")
else:
try:
logger.debug("正在安全关闭自管理浏览器...")
try:
driver.close()
except Exception as e:
logger.warning(f"关闭浏览器窗口时出错: {e}")
logger.warning(f"关闭浏览器窗口时出错: {e}")
driver.quit()
logger.debug("浏览器已完全关闭")
except Exception as e:
logger.error(f"关闭浏览器时出错: {e}")
# 强制退出所有Chrome进程
driver.quit()
logger.debug("浏览器已完全关闭")
except Exception as e:
logger.error(f"关闭浏览器时出错: {e}")
# 只有当本次浏览器由当前任务自己拉起时,才需要额外清理潜在残留进程。
if os.name != 'nt' and not attached_to_existing_browser:
# 额外保险强制清理残留的Chrome进程仅Linux
if os.name != 'nt':
try:
import psutil
current_user = os.getlogin()
@@ -375,11 +302,10 @@ def fetch_and_create_pdf(url, browser_config=None):
logger.warning(f"强制清理Chrome进程时出错: {e}")
def pdf_file_path_undetected(browser_config=None):
def pdf_file_path_undetected():
try:
url = 'https://www.sehuatang.net/forum.php?mod=forumdisplay&fid=103&filter=typeid&typeid=481'
# 将插件配置透传给抓取函数,便于优先复用外部常驻浏览器会话。
pdf_path = fetch_and_create_pdf(url, browser_config=browser_config)
pdf_path = fetch_and_create_pdf(url)
if pdf_path:
logger.info(f"返回的PDF文件路径{pdf_path}")
return True, pdf_path