Skip to content

Commit 36f8cfc

Browse files
committed
fix(browser): 修复内置有头浏览器与并发端口占满
1 parent da72d58 commit 36f8cfc

9 files changed

Lines changed: 477 additions & 105 deletions

Dockerfile.headed

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@ WORKDIR /app
55
ENV PYTHONDONTWRITEBYTECODE=1 \
66
PYTHONUNBUFFERED=1 \
77
ALLOW_DOCKER_HEADED_CAPTCHA=true \
8+
DISPLAY=:99 \
9+
PERSONAL_BROWSER_HEADLESS=0 \
810
PLAYWRIGHT_BROWSERS_PATH=0
911

12+
RUN apt-get update \
13+
&& apt-get install -y --no-install-recommends \
14+
fluxbox \
15+
x11-utils \
16+
xauth \
17+
xvfb \
18+
&& rm -rf /var/lib/apt/lists/*
19+
1020
# 在镜像构建阶段预装 Playwright Chromium,供 personal/browser 模式复用
1121
COPY requirements.txt ./
1222

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ docker-compose -f docker-compose.warp.yml logs -f
7272
> 适用于你有虚拟化桌面需求、希望在容器里启用有头浏览器打码的场景。
7373
> 该模式默认启动 `Xvfb + Fluxbox` 实现容器内部可视化,并设置 `ALLOW_DOCKER_HEADED_CAPTCHA=true`
7474
> 仅开放应用端口,不提供任何远程桌面连接端口。
75+
> `personal` 内置浏览器现在默认按有头模式启动;如需临时切回无头,可额外设置环境变量 `PERSONAL_BROWSER_HEADLESS=true`
7576
7677
```bash
7778
# 启动有头模式(首次建议带 --build)

config/setting_example.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
5555

5656
[captcha]
5757
captcha_method = "browser" # 打码方式: yescaptcha/browser/personal/remote_browser
58+
browser_launch_background = true # 有头浏览器是否默认后台启动;设为 false 可直接看到窗口
5859
browser_recaptcha_settle_seconds = 3.0 # reload/clr 就绪后的额外稳态等待
5960
browser_count = 1 # browser 模式的有头浏览器实例数量
6061
personal_project_pool_size = 4 # personal 模式下单个 Token 默认维护的项目池数量(仅影响项目轮换,不决定打码标签页数量)

docker-compose.headed.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ version: '3.8'
22

33
services:
44
flow2api-headed:
5+
init: true
56
build:
67
context: .
78
dockerfile: Dockerfile.headed
@@ -16,5 +17,7 @@ services:
1617
environment:
1718
- PYTHONUNBUFFERED=1
1819
- ALLOW_DOCKER_HEADED_CAPTCHA=true
20+
- DISPLAY=:99
21+
- XVFB_SCREEN=1440x900x24
1922
shm_size: "2gb"
2023
restart: unless-stopped

docker/entrypoint.headed.sh

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
#!/bin/sh
22
set -eu
33

4+
DISPLAY_VALUE="${DISPLAY:-:99}"
5+
XVFB_SCREEN_VALUE="${XVFB_SCREEN:-1440x900x24}"
6+
export DISPLAY="${DISPLAY_VALUE}"
7+
48
resolve_browser_path() {
59
python - <<'PY'
610
from playwright.sync_api import sync_playwright
@@ -17,7 +21,32 @@ if [ -z "${BROWSER_EXECUTABLE_PATH:-}" ] || [ ! -x "${BROWSER_EXECUTABLE_PATH:-}
1721
fi
1822
fi
1923

20-
echo "[entrypoint] starting flow2api (headless browser mode)"
24+
if [ "${ALLOW_DOCKER_HEADED_CAPTCHA:-true}" = "true" ] || [ "${ALLOW_DOCKER_HEADED_CAPTCHA:-1}" = "1" ]; then
25+
display_suffix="$(printf '%s' "${DISPLAY}" | sed 's/^://; s/\..*$//')"
26+
socket_path="/tmp/.X11-unix/X${display_suffix}"
27+
28+
mkdir -p /tmp/.X11-unix
29+
rm -f "/tmp/.X${display_suffix}-lock"
30+
31+
echo "[entrypoint] starting Xvfb on DISPLAY=${DISPLAY} (${XVFB_SCREEN_VALUE})"
32+
Xvfb "${DISPLAY}" -screen 0 "${XVFB_SCREEN_VALUE}" -ac +extension RANDR >/tmp/xvfb.log 2>&1 &
33+
34+
waited=0
35+
while [ ! -S "${socket_path}" ] && [ "${waited}" -lt 100 ]; do
36+
sleep 0.1
37+
waited=$((waited + 1))
38+
done
39+
40+
if [ ! -S "${socket_path}" ]; then
41+
echo "[entrypoint] failed to start Xvfb, socket not ready: ${socket_path}" >&2
42+
exit 1
43+
fi
44+
45+
echo "[entrypoint] starting Fluxbox on DISPLAY=${DISPLAY}"
46+
fluxbox >/tmp/fluxbox.log 2>&1 &
47+
fi
48+
49+
echo "[entrypoint] starting flow2api (headed browser mode)"
2150
if [ -n "${BROWSER_EXECUTABLE_PATH:-}" ] && [ -x "${BROWSER_EXECUTABLE_PATH}" ]; then
2251
echo "[entrypoint] browser executable: ${BROWSER_EXECUTABLE_PATH}"
2352
"${BROWSER_EXECUTABLE_PATH}" --version || true

src/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ async def auto_unban_task():
144144
await auto_unban_task_handle
145145
except asyncio.CancelledError:
146146
pass
147+
# Close shared HTTP session pools
148+
try:
149+
await flow_client.close()
150+
print("✓ Flow client HTTP session closed")
151+
except Exception as e:
152+
print(f"⚠ Flow client close failed: {e}")
147153
# Close browser if initialized
148154
if browser_service:
149155
await browser_service.close()

src/services/browser_captcha_personal.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ def _is_truthy_env(name: str) -> bool:
5252
return value.strip().lower() in {"1", "true", "yes", "on"}
5353

5454

55+
def _get_optional_bool_env(name: str) -> Optional[bool]:
56+
"""读取可选布尔环境变量,未设置或无法识别时返回 None。"""
57+
value = os.environ.get(name)
58+
if value is None:
59+
return None
60+
61+
normalized = value.strip().lower()
62+
if normalized in {"1", "true", "yes", "on"}:
63+
return True
64+
if normalized in {"0", "false", "no", "off"}:
65+
return False
66+
return None
67+
68+
5569
ALLOW_DOCKER_HEADED = (
5670
_is_truthy_env("ALLOW_DOCKER_HEADED_CAPTCHA")
5771
or _is_truthy_env("ALLOW_DOCKER_BROWSER_CAPTCHA")
@@ -555,7 +569,7 @@ class BrowserCaptchaService:
555569

556570
def __init__(self, db=None):
557571
"""初始化服务"""
558-
self.headless = True # 无头模式
572+
self.headless = self._resolve_headless_mode() # 默认改为有头,可用环境变量回退到无头
559573
self.browser = None
560574
self._initialized = False
561575
self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
@@ -640,6 +654,18 @@ async def reload_config(self):
640654
f"fingerprint_ttl {old_fingerprint_ttl}s->{self._fingerprint_cache_ttl_seconds}s"
641655
)
642656

657+
def _resolve_headless_mode(self) -> bool:
658+
"""personal 模式默认改为有头,仅在显式环境变量要求时回退到无头。"""
659+
for env_name in ("PERSONAL_BROWSER_HEADLESS", "FLOW2API_PERSONAL_HEADLESS"):
660+
override = _get_optional_bool_env(env_name)
661+
if override is not None:
662+
debug_logger.log_info(
663+
f"[BrowserCaptcha] Personal headless 模式由环境变量 {env_name} 控制: {override}"
664+
)
665+
return override
666+
667+
return False
668+
643669
def _refresh_runtime_tunables(self):
644670
"""刷新运行时调优参数,缺省时使用保守的低开销默认值。"""
645671
try:
@@ -1454,6 +1480,7 @@ async def initialize(self):
14541480
self._proxy_url = f"{protocol}://{host}:{port}"
14551481
debug_logger.log_info(f"[BrowserCaptcha] Personal 浏览器代理: {self._proxy_url}")
14561482

1483+
launch_in_background = bool(getattr(config, "browser_launch_background", True))
14571484
browser_args = [
14581485
'--disable-quic',
14591486
'--disable-features=UseDnsHttpsSvcb',
@@ -1463,7 +1490,6 @@ async def initialize(self):
14631490
'--disable-infobars',
14641491
'--hide-scrollbars',
14651492
'--window-size=1280,720',
1466-
'--window-position=3000,3000',
14671493
'--profile-directory=Default',
14681494
'--disable-background-networking',
14691495
'--disable-sync',
@@ -1473,6 +1499,20 @@ async def initialize(self):
14731499
'--no-default-browser-check',
14741500
'--no-zygote',
14751501
]
1502+
if launch_in_background and not self.headless:
1503+
browser_args.extend([
1504+
'--start-minimized',
1505+
'--disable-background-timer-throttling',
1506+
'--disable-renderer-backgrounding',
1507+
'--disable-backgrounding-occluded-windows',
1508+
])
1509+
if sys.platform.startswith("win"):
1510+
browser_args.append('--window-position=-32000,-32000')
1511+
else:
1512+
browser_args.append('--window-position=3000,3000')
1513+
debug_logger.log_info("[BrowserCaptcha] Personal 有头浏览器将以后台模式启动")
1514+
elif not self.headless:
1515+
debug_logger.log_info("[BrowserCaptcha] Personal 有头浏览器将以可见窗口模式启动")
14761516
if proxy_server_arg:
14771517
browser_args.append(proxy_server_arg)
14781518
if self._proxy_ext_dir:
@@ -1503,7 +1543,7 @@ async def initialize(self):
15031543
debug_logger.log_info(
15041544
"[BrowserCaptcha] nodriver 启动上下文: "
15051545
f"docker={IS_DOCKER}, display={display_value or '<empty>'}, "
1506-
f"uid={effective_uid}, headless={self.headless}, sandbox=False, "
1546+
f"uid={effective_uid}, headless={self.headless}, background={launch_in_background}, sandbox=False, "
15071547
f"executable={browser_executable_path or '<auto>'}, "
15081548
f"args={' '.join(effective_launch_args)}"
15091549
)

0 commit comments

Comments
 (0)