diff --git a/api_browser.py b/api_browser.py index a003ab8..92bf769 100755 --- a/api_browser.py +++ b/api_browser.py @@ -7,6 +7,7 @@ API 浏览器 - 用纯 HTTP 请求实现浏览功能 import requests from bs4 import BeautifulSoup +import os import re import time import atexit @@ -24,6 +25,12 @@ LOGIN_URL = getattr(config, "ZSGL_LOGIN_URL", f"{BASE_URL}/admin/login.aspx") INDEX_URL_PATTERN = getattr(config, "ZSGL_INDEX_URL_PATTERN", "index.aspx") COOKIES_DIR = getattr(config, "COOKIES_DIR", "data/cookies") +try: + _API_REQUEST_TIMEOUT_SECONDS = float(os.environ.get("API_REQUEST_TIMEOUT_SECONDS") or os.environ.get("API_REQUEST_TIMEOUT") or "15") +except Exception: + _API_REQUEST_TIMEOUT_SECONDS = 15.0 +_API_REQUEST_TIMEOUT_SECONDS = max(3.0, _API_REQUEST_TIMEOUT_SECONDS) + _cookie_domain_fallback = urlsplit(BASE_URL).hostname or "postoa.aidunsoft.com" _api_browser_instances: "weakref.WeakSet[APIBrowser]" = weakref.WeakSet() @@ -125,7 +132,7 @@ class APIBrowser: def _request_with_retry(self, method, url, max_retries=3, retry_delay=1, **kwargs): """带重试机制的请求方法""" - kwargs.setdefault('timeout', 5) + kwargs.setdefault('timeout', _API_REQUEST_TIMEOUT_SECONDS) last_error = None for attempt in range(1, max_retries + 1): diff --git a/app.py b/app.py index c99fc57..21fb550 100644 --- a/app.py +++ b/app.py @@ -226,7 +226,13 @@ if __name__ == "__main__": logger.warning(f"警告: 加载并发配置失败,使用默认值: {e}") logger.info("正在初始化浏览器管理器...") - init_browser_manager() + try: + from services.browser_manager import init_browser_manager_async + + logger.info("启动浏览器环境初始化(后台进行,不阻塞服务启动)...") + init_browser_manager_async() + except Exception as e: + logger.warning(f"警告: 启动浏览器初始化失败: {e}") logger.info("启动定时任务调度器...") threading.Thread(target=scheduled_task_worker, daemon=True, name="scheduled-task-worker").start() diff --git a/routes/api_accounts.py b/routes/api_accounts.py index d488f2d..bc8ce6a 100644 --- a/routes/api_accounts.py +++ b/routes/api_accounts.py @@ -11,7 +11,7 @@ from crypto_utils import encrypt_password as encrypt_account_password from flask import Blueprint, jsonify, request from flask_login import current_user, login_required from services.accounts_service import load_user_accounts -from services.browser_manager import init_browser_manager +from services.browser_manager import init_browser_manager_async from services.browse_types import BROWSE_TYPE_SHOULD_READ, normalize_browse_type, validate_browse_type from services.client_log import log_to_client from services.models import Account @@ -230,9 +230,9 @@ def start_account(account_id): if not browse_type: return jsonify({"error": "浏览类型无效"}), 400 enable_screenshot = data.get("enable_screenshot", True) - - if not init_browser_manager(): - return jsonify({"error": "浏览器初始化失败"}), 500 + if enable_screenshot: + # 异步初始化浏览器环境,避免首次下载/安装 Chromium 阻塞请求导致“网页无响应” + init_browser_manager_async() ok, message = submit_account_task( user_id=user_id, @@ -308,6 +308,9 @@ def manual_screenshot(account_id): account.last_browse_type = browse_type + # 异步初始化浏览器环境,避免首次下载/安装 Chromium 阻塞请求 + init_browser_manager_async() + threading.Thread( target=take_screenshot_for_account, args=(user_id, account_id, browse_type, "manual_screenshot"), @@ -333,6 +336,10 @@ def batch_start_accounts(): if not account_ids: return jsonify({"error": "请选择要启动的账号"}), 400 + if enable_screenshot: + # 异步初始化浏览器环境,避免首次下载/安装 Chromium 阻塞请求 + init_browser_manager_async() + started = [] failed = [] @@ -407,4 +414,3 @@ def batch_stop_accounts(): _emit("account_update", account.to_dict(), room=f"user_{user_id}") return jsonify({"success": True, "stopped_count": len(stopped), "stopped": stopped}) - diff --git a/services/browser_manager.py b/services/browser_manager.py index 83daeee..f2ef8f3 100644 --- a/services/browser_manager.py +++ b/services/browser_manager.py @@ -3,6 +3,7 @@ from __future__ import annotations import threading +import time from typing import Optional from app_logger import get_logger @@ -13,28 +14,99 @@ logger = get_logger("browser_manager") _browser_manager: Optional[PlaywrightBrowserManager] = None _lock = threading.Lock() +_cond = threading.Condition(_lock) +_init_in_progress = False +_init_error: Optional[str] = None +_init_thread: Optional[threading.Thread] = None def get_browser_manager() -> Optional[PlaywrightBrowserManager]: return _browser_manager -def init_browser_manager() -> bool: - global _browser_manager +def is_browser_manager_ready() -> bool: + return _browser_manager is not None - with _lock: + +def get_browser_manager_init_error() -> Optional[str]: + return _init_error + + +def init_browser_manager(*, block: bool = True, timeout: Optional[float] = None) -> bool: + global _browser_manager + global _init_in_progress, _init_error + + deadline = time.monotonic() + float(timeout) if timeout is not None else None + + with _cond: if _browser_manager is not None: return True + if _init_in_progress: + if not block: + return False + while _init_in_progress: + if deadline is None: + _cond.wait(timeout=0.5) + continue + remaining = deadline - time.monotonic() + if remaining <= 0: + break + _cond.wait(timeout=min(0.5, remaining)) + return _browser_manager is not None + + _init_in_progress = True + _init_error = None + + ok = False + error: Optional[str] = None + manager: Optional[PlaywrightBrowserManager] = None + + try: logger.info("正在初始化Playwright浏览器管理器...") if not check_and_install_browser(log_callback=lambda msg, account_id=None: logger.info(str(msg))): + error = "浏览器环境检查失败" logger.error("浏览器环境检查失败!") - return False + ok = False + else: + manager = PlaywrightBrowserManager( + headless=True, + log_callback=lambda msg, account_id=None: logger.info(str(msg)), + ) + ok = True + logger.info("Playwright浏览器管理器创建成功!") + except Exception as exc: + error = f"{type(exc).__name__}: {exc}" + logger.exception("初始化Playwright浏览器管理器时发生异常") + ok = False - _browser_manager = PlaywrightBrowserManager( - headless=True, - log_callback=lambda msg, account_id=None: logger.info(str(msg)), - ) - logger.info("Playwright浏览器管理器创建成功!") - return True + with _cond: + if ok and manager is not None: + _browser_manager = manager + else: + _init_error = error or "初始化失败" + _init_in_progress = False + _cond.notify_all() + return ok + + +def init_browser_manager_async() -> None: + """异步初始化浏览器环境,避免阻塞 Web 请求/服务启动。""" + global _init_thread + + def _worker(): + try: + init_browser_manager(block=True) + except Exception: + logger.exception("异步初始化浏览器管理器失败") + + with _cond: + if _browser_manager is not None: + return + if _init_thread and _init_thread.is_alive(): + return + if _init_in_progress: + return + _init_thread = threading.Thread(target=_worker, daemon=True, name="browser-manager-init") + _init_thread.start()