更新
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"""运行时环境配置:住宅代理 + 浏览器显示。
|
||||
|
||||
用于解决「部署到云服务器后」两类常见问题:
|
||||
1. 机房 IP 触发抖音风控(7911)—— 通过 KEFU_DOUYIN_PROXY 让抖音请求走住宅代理。
|
||||
2. 无图形界面的 Linux 起不来有头浏览器 —— 自动拉起 Xvfb 虚拟显示。
|
||||
|
||||
全部通过环境变量控制,无需改代码:
|
||||
|
||||
KEFU_DOUYIN_PROXY 抖音 IM HTTP 请求与浏览器登录走的代理,绕开机房 IP 风控。
|
||||
形如 http://user:pass@host:port 或 socks5://host:port
|
||||
KEFU_BROWSER_HEADLESS 是否使用无头浏览器(1/true 开启)。默认 false:抖音安全 SDK
|
||||
对 headless 判定严格,无头易生成无效 ts_sign,反而刷新无效。
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
logger = logging.getLogger("rpa_engine.runtime")
|
||||
|
||||
_TRUE = {"1", "true", "yes", "on"}
|
||||
_FALSE = {"0", "false", "no", "off"}
|
||||
|
||||
_NO_DISPLAY_HINT = (
|
||||
"当前是无图形界面的 Linux 服务器,且无法启动虚拟显示来运行有头浏览器。"
|
||||
"抖音安全 SDK 对 headless 判定严格,扫码登录 / 刷新凭证需要有头 Chromium。请任选其一:\n"
|
||||
" 1) 安装 Xvfb + pyvirtualdisplay,让程序自动拉起虚拟显示:\n"
|
||||
" Debian/Ubuntu: apt install -y xvfb && pip install pyvirtualdisplay\n"
|
||||
" CentOS/Rocky : yum install -y xorg-x11-server-Xvfb && pip install pyvirtualdisplay\n"
|
||||
" 2) 或用 xvfb-run 启动后端:xvfb-run -a ./start_web.sh\n"
|
||||
" 3) 或设置 KEFU_BROWSER_HEADLESS=1 强制无头(更易触发抖音风控,不推荐)。"
|
||||
)
|
||||
|
||||
|
||||
def get_douyin_proxy() -> Optional[str]:
|
||||
"""读取抖音请求代理 URL(未配置返回 None)。"""
|
||||
val = (os.getenv("KEFU_DOUYIN_PROXY") or "").strip()
|
||||
return val or None
|
||||
|
||||
|
||||
def httpx_proxy() -> Optional[str]:
|
||||
"""供 httpx.AsyncClient(proxy=...) 使用的代理 URL。"""
|
||||
return get_douyin_proxy()
|
||||
|
||||
|
||||
def requests_proxies() -> Optional[dict]:
|
||||
"""供 requests.get(proxies=...) 使用的代理字典。"""
|
||||
url = get_douyin_proxy()
|
||||
if not url:
|
||||
return None
|
||||
return {"http": url, "https": url}
|
||||
|
||||
|
||||
def playwright_proxy() -> Optional[dict]:
|
||||
"""转成 Playwright launch(proxy=...) 所需结构(未配置或无法解析返回 None)。"""
|
||||
url = get_douyin_proxy()
|
||||
if not url:
|
||||
return None
|
||||
parsed = urlparse(url)
|
||||
if not parsed.hostname:
|
||||
logger.warning("KEFU_DOUYIN_PROXY 格式无法解析,已忽略:%s", url)
|
||||
return None
|
||||
server = f"{parsed.scheme or 'http'}://{parsed.hostname}"
|
||||
if parsed.port:
|
||||
server += f":{parsed.port}"
|
||||
proxy: dict[str, str] = {"server": server}
|
||||
if parsed.username:
|
||||
proxy["username"] = parsed.username
|
||||
if parsed.password:
|
||||
proxy["password"] = parsed.password
|
||||
return proxy
|
||||
|
||||
|
||||
def resolve_headless(default: bool = False) -> bool:
|
||||
"""根据 KEFU_BROWSER_HEADLESS 决定是否无头;未设置时用 default。"""
|
||||
val = (os.getenv("KEFU_BROWSER_HEADLESS") or "").strip().lower()
|
||||
if val in _TRUE:
|
||||
return True
|
||||
if val in _FALSE:
|
||||
return False
|
||||
return default
|
||||
|
||||
|
||||
# 进程内仅启动一次的虚拟显示(Xvfb)句柄
|
||||
_virtual_display = None
|
||||
_virtual_display_failed = False
|
||||
|
||||
|
||||
def _start_virtual_display_sync() -> Optional[str]:
|
||||
"""在无 DISPLAY 的 Linux 上启动一次 Xvfb 虚拟显示(阻塞,需放线程执行)。"""
|
||||
global _virtual_display, _virtual_display_failed
|
||||
|
||||
# 仅 Linux 且无 DISPLAY 时才需要虚拟显示;Windows/macOS 有桌面,直接返回。
|
||||
if os.name != "posix":
|
||||
return os.environ.get("DISPLAY")
|
||||
if os.environ.get("DISPLAY"):
|
||||
return os.environ["DISPLAY"]
|
||||
if _virtual_display is not None:
|
||||
return os.environ.get("DISPLAY")
|
||||
if _virtual_display_failed:
|
||||
raise RuntimeError(_NO_DISPLAY_HINT)
|
||||
|
||||
try:
|
||||
from pyvirtualdisplay import Display
|
||||
except ImportError as e:
|
||||
_virtual_display_failed = True
|
||||
raise RuntimeError(_NO_DISPLAY_HINT) from e
|
||||
|
||||
try:
|
||||
disp = Display(visible=False, size=(1280, 800))
|
||||
disp.start() # 设置 os.environ['DISPLAY']
|
||||
except Exception as e:
|
||||
_virtual_display_failed = True
|
||||
raise RuntimeError(_NO_DISPLAY_HINT) from e
|
||||
|
||||
_virtual_display = disp
|
||||
logger.info("已启动 Xvfb 虚拟显示 DISPLAY=%s 供有头浏览器使用", os.environ.get("DISPLAY"))
|
||||
return os.environ.get("DISPLAY")
|
||||
|
||||
|
||||
async def ensure_browser_display(headless: bool) -> None:
|
||||
"""有头模式在无 DISPLAY 的 Linux 上自动拉起 Xvfb 虚拟显示。
|
||||
|
||||
headless=True 时无需显示,直接返回;启动失败抛出带操作指引的 RuntimeError。
|
||||
"""
|
||||
if headless:
|
||||
return
|
||||
await asyncio.to_thread(_start_virtual_display_sync)
|
||||
Reference in New Issue
Block a user