diff --git a/plugins/sehuatang_push/config.toml b/plugins/sehuatang_push/config.toml index d57bcb0..1880bff 100644 --- a/plugins/sehuatang_push/config.toml +++ b/plugins/sehuatang_push/config.toml @@ -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 diff --git a/plugins/sehuatang_push/main.py b/plugins/sehuatang_push/main.py index 0eec07b..5062d90 100644 --- a/plugins/sehuatang_push/main.py +++ b/plugins/sehuatang_push/main.py @@ -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": {}} diff --git a/plugins/sehuatang_push/shehuatang_undetected.py b/plugins/sehuatang_push/shehuatang_undetected.py index 29d438a..bcabccf 100644 --- a/plugins/sehuatang_push/shehuatang_undetected.py +++ b/plugins/sehuatang_push/shehuatang_undetected.py @@ -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