Auto headless + Xvfb for headless servers
Enable Chromium on machines without DISPLAY: Python auto headless with Xvfb hints, and start.sh installs/starts Xvfb (xvfb-run or persistent :99) so registration can run headed under a virtual display.
This commit is contained in:
1 parent
4feac70c91
commit
cd4a2351ae
8 files changed
+534
-19
No files matched your search
@@ -51,12 +51,30 @@
|
|||||||
|
|
||||||
| 依赖 | 说明 |
|
| 依赖 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| Linux / macOS | 协议 mint **不需要**浏览器;回退浏览器时需要桌面 `DISPLAY` |
|
| Linux / macOS | 协议 mint **不需要**浏览器;注册 / 回退浏览器需要 Chromium |
|
||||||
|
| 无桌面服务器 | **`./start.sh` 自动 Xvfb**(无 DISPLAY 时);失败再回退 Python headless |
|
||||||
| `uv` + Python 3.13 | 见 `pyproject.toml` / `uv.lock`;可选 `mise` |
|
| `uv` + Python 3.13 | 见 `pyproject.toml` / `uv.lock`;可选 `mise` |
|
||||||
| Chromium | 注册流程 + 协议失败回退时需要 |
|
| Chromium / Chrome | 注册流程 + 协议失败回退时需要 |
|
||||||
| 代理(按需) | 访问 xAI / accounts.x.ai 不通时配置 `proxy` / `cpa_proxy` |
|
| 代理(按需) | 访问 xAI / accounts.x.ai 不通时配置 `proxy` / `cpa_proxy` |
|
||||||
| 可选 | grok2api `:8000`、CLIProxyAPI(CPA) `:8317` |
|
| 可选 | grok2api `:8000`、CLIProxyAPI(CPA) `:8317` |
|
||||||
|
|
||||||
|
无头服务器(推荐直接 `./start.sh`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 一键:缺啥装啥 + 无 DISPLAY 自动 xvfb-run / 常驻 Xvfb
|
||||||
|
./start.sh --extra 1 --threads 1
|
||||||
|
|
||||||
|
# 跳过 Xvfb,强制走 Python 自动 headless
|
||||||
|
SKIP_XVFB=1 ./start.sh --extra 1
|
||||||
|
|
||||||
|
# 即使已有 DISPLAY 也包一层 xvfb-run
|
||||||
|
FORCE_XVFB=1 ./start.sh --extra 1
|
||||||
|
|
||||||
|
# 手动(不经过 start.sh)
|
||||||
|
sudo apt install -y xvfb google-chrome-stable
|
||||||
|
xvfb-run -a uv run python -u register_cli.py --extra 1 --threads 1
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /path/to/grok-reg-protocol-cpa
|
cd /path/to/grok-reg-protocol-cpa
|
||||||
uv sync
|
uv sync
|
||||||
@@ -305,7 +323,8 @@ cpa_proxy > proxy > 环境变量 https_proxy/http_proxy
|
|||||||
| `cpa_copy_to_hotload` | `false` | 是否复制到 CPA 热加载目录 |
|
| `cpa_copy_to_hotload` | `false` | 是否复制到 CPA 热加载目录 |
|
||||||
| `cpa_hotload_dir` | `""` | CPA `auth-dir`(仅 copy 时) |
|
| `cpa_hotload_dir` | `""` | CPA `auth-dir`(仅 copy 时) |
|
||||||
| `cpa_base_url` | `https://cli-chat-proxy.grok.com/v1` | 免费 Build **必须**此上游 |
|
| `cpa_base_url` | `https://cli-chat-proxy.grok.com/v1` | 免费 Build **必须**此上游 |
|
||||||
| `cpa_headless` | `false` | 回退浏览器建议有头 |
|
| `browser_headless` | `auto` | 注册浏览器:`auto`=无 DISPLAY 自动 headless;`true`/`false` 强制 |
|
||||||
|
| `cpa_headless` | `false` | 回退浏览器:有 DISPLAY 时建议有头;无 DISPLAY 时强制 headless |
|
||||||
| `cpa_force_standalone` | `true` | 回退时独立 Chromium |
|
| `cpa_force_standalone` | `true` | 回退时独立 Chromium |
|
||||||
| `cpa_mint_cookie_inject` | `true` | 回退时注入注册 cookie |
|
| `cpa_mint_cookie_inject` | `true` | 回退时注入注册 cookie |
|
||||||
| `cpa_mint_workers` | `-1` | mint 并发:`-1` 自动;`0` 内联;`1-10` 固定 |
|
| `cpa_mint_workers` | `-1` | mint 并发:`-1` 自动;`0` 内联;`1-10` 固定 |
|
||||||
@@ -463,7 +482,8 @@ uv run python -u scripts/backfill_cpa_xai_from_accounts.py \
|
|||||||
| `--out-dir` | 主导出目录 |
|
| `--out-dir` | 主导出目录 |
|
||||||
| `--cpa-dir` | 成功后复制到 CPA 热加载目录 |
|
| `--cpa-dir` | 成功后复制到 CPA 热加载目录 |
|
||||||
| `--probe` | 检查是否列出 `grok-4.5` |
|
| `--probe` | 检查是否列出 `grok-4.5` |
|
||||||
| `--headless` | 回退浏览器时无头(不推荐) |
|
| `--headless` | 强制无头浏览器(无 DISPLAY 时默认也会自动 headless) |
|
||||||
|
| `--headed` | 强制有头(需 DISPLAY / xvfb-run;无显示仍回退 headless) |
|
||||||
|
|
||||||
### C. 从 `~/.grok/auth.json` 导出
|
### C. 从 `~/.grok/auth.json` 导出
|
||||||
|
|
||||||
@@ -523,7 +543,8 @@ curl -sS http://127.0.0.1:8317/v1/chat/completions \
|
|||||||
| 协议 `sso invalid` | SSO 过期/无效;会回退浏览器;检查账本第三段 |
|
| 协议 `sso invalid` | SSO 过期/无效;会回退浏览器;检查账本第三段 |
|
||||||
| 协议 verify/approve 失败 | 会话态变化 / 风控;看日志后自动回退浏览器 |
|
| 协议 verify/approve 失败 | 会话态变化 / 风控;看日志后自动回退浏览器 |
|
||||||
| 一直 `authorization_pending` | 浏览器路径未完成 consent;需到「设备已授权」且 token 200 |
|
| 一直 `authorization_pending` | 浏览器路径未完成 consent;需到「设备已授权」且 token 200 |
|
||||||
| Cloudflare / Turnstile | 回退浏览器时关 headless、开 turnstilePatch、检查代理 |
|
| Cloudflare / Turnstile | 优先 `xvfb-run` 有头;纯 headless 易拦;Chromium+turnstilePatch、检查代理 |
|
||||||
|
| 浏览器 connection fails / no interface | 无 DISPLAY:已自动 headless;仍失败则装 Chrome 并用 `xvfb-run -a ...` |
|
||||||
| Hotmail 收不到码 | 检查四段凭证、ClientID/Token、IMAP 主机与 alias 计数 |
|
| Hotmail 收不到码 | 检查四段凭证、ClientID/Token、IMAP 主机与 alias 计数 |
|
||||||
| 有 token 但无 grok-4.5 | `cpa_base_url` 是否为 `cli-chat-proxy` |
|
| 有 token 但无 grok-4.5 | `cpa_base_url` 是否为 `cli-chat-proxy` |
|
||||||
| 注册成功但无 `cpa_auths` | `cpa_export_enabled`?看 `cpa_auths/cpa_auth_failed.txt` |
|
| 注册成功但无 `cpa_auths` | `cpa_export_enabled`?看 `cpa_auths/cpa_auth_failed.txt` |
|
||||||
|
|||||||
+26
-1
@@ -196,6 +196,13 @@ def resolve_browser_path() -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def has_display() -> bool:
|
||||||
|
"""True if Chromium can open a real GUI window (Windows always; else DISPLAY/WAYLAND)."""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
return True
|
||||||
|
return bool((os.environ.get("DISPLAY") or "").strip() or (os.environ.get("WAYLAND_DISPLAY") or "").strip())
|
||||||
|
|
||||||
|
|
||||||
def create_browser_options(headless: bool = False, proxy: str = "") -> ChromiumOptions:
|
def create_browser_options(headless: bool = False, proxy: str = "") -> ChromiumOptions:
|
||||||
opts = ChromiumOptions()
|
opts = ChromiumOptions()
|
||||||
opts.auto_port()
|
opts.auto_port()
|
||||||
@@ -217,8 +224,26 @@ def create_browser_options(headless: bool = False, proxy: str = "") -> ChromiumO
|
|||||||
print("[!] skip turnstilePatch: Google Chrome disallows --load-extension; use Chromium")
|
print("[!] skip turnstilePatch: Google Chrome disallows --load-extension; use Chromium")
|
||||||
else:
|
else:
|
||||||
opts.add_extension(EXTENSION_PATH)
|
opts.add_extension(EXTENSION_PATH)
|
||||||
if headless:
|
# Auto headless when no DISPLAY (unless caller already requested headless)
|
||||||
|
use_headless = bool(headless) or not has_display()
|
||||||
|
if use_headless:
|
||||||
|
try:
|
||||||
opts.headless()
|
opts.headless()
|
||||||
|
except Exception:
|
||||||
|
opts.set_argument("--headless=new")
|
||||||
|
for flag in ("--headless=new", "--disable-gpu", "--no-sandbox", "--disable-dev-shm-usage"):
|
||||||
|
try:
|
||||||
|
opts.set_argument(flag)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not headless and not has_display():
|
||||||
|
print(
|
||||||
|
f"[*] 无图形界面 (DISPLAY={os.environ.get('DISPLAY', '')!r}),已自动启用 headless。\n"
|
||||||
|
" 推荐: xvfb-run -a python auto_register.py ... (过 CF 更稳)",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("[*] browser headless=True", flush=True)
|
||||||
if proxy:
|
if proxy:
|
||||||
try:
|
try:
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|||||||
+5
-1
@@ -56,6 +56,10 @@
|
|||||||
"// ===== 网络 / 浏览器 =====": "",
|
"// ===== 网络 / 浏览器 =====": "",
|
||||||
"// proxy": "注册机主代理:Chromium + 邮箱 HTTP。可带账号 http://user:pass@host:port。优先于系统 https_proxy。",
|
"// proxy": "注册机主代理:Chromium + 邮箱 HTTP。可带账号 http://user:pass@host:port。优先于系统 https_proxy。",
|
||||||
"proxy": "http://127.0.0.1:7890",
|
"proxy": "http://127.0.0.1:7890",
|
||||||
|
"// browser_headless": "注册 Chromium:true=强制无头,false=强制有头,auto/省略=无 DISPLAY 时自动 headless。也可用 env BROWSER_HEADLESS / CLI --headless|--headed。无头服务器建议 xvfb-run。",
|
||||||
|
"browser_headless": "auto",
|
||||||
|
"// browser_path": "可选,强制 Chromium/Chrome 可执行文件路径;默认自动探测 google-chrome-stable / chromium。",
|
||||||
|
"browser_path": "",
|
||||||
"// user_agent": "Chromium UA;一般保持默认 Chrome 桌面串即可。",
|
"// user_agent": "Chromium UA;一般保持默认 Chrome 桌面串即可。",
|
||||||
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
||||||
"// enable_nsfw": "注册成功后是否尝试打开 NSFW(历史字段;与 CPA 无关)",
|
"// enable_nsfw": "注册成功后是否尝试打开 NSFW(历史字段;与 CPA 无关)",
|
||||||
@@ -145,7 +149,7 @@
|
|||||||
"cpa_base_url": "https://cli-chat-proxy.grok.com/v1",
|
"cpa_base_url": "https://cli-chat-proxy.grok.com/v1",
|
||||||
"// cpa_proxy": "OIDC mint 专用代理(device-code/token/probe/浏览器)。优先级: cpa_proxy > proxy > 环境 https_proxy。空=用 proxy。",
|
"// cpa_proxy": "OIDC mint 专用代理(device-code/token/probe/浏览器)。优先级: cpa_proxy > proxy > 环境 https_proxy。空=用 proxy。",
|
||||||
"cpa_proxy": "http://127.0.0.1:7890",
|
"cpa_proxy": "http://127.0.0.1:7890",
|
||||||
"// cpa_headless": "device 确认浏览器是否无头。accounts.x.ai 建议 false(有头)",
|
"// cpa_headless": "回退浏览器是否无头。false=有头(推荐,过 CF 更稳);true=无头;省略/auto 时无 DISPLAY 自动无头。无显示时即使 false 也会强制 headless 以免起不来。",
|
||||||
"cpa_headless": false,
|
"cpa_headless": false,
|
||||||
"// cpa_force_standalone": "true=新开 Chromium+turnstilePatch 做 OIDC;勿复用注册 tab",
|
"// cpa_force_standalone": "true=新开 Chromium+turnstilePatch 做 OIDC;勿复用注册 tab",
|
||||||
"cpa_force_standalone": true,
|
"cpa_force_standalone": true,
|
||||||
|
|||||||
+32
-2
@@ -351,8 +351,38 @@ def export_cpa_xai_for_account(
|
|||||||
or os.environ.get("http_proxy")
|
or os.environ.get("http_proxy")
|
||||||
or ""
|
or ""
|
||||||
).strip()
|
).strip()
|
||||||
# Default headed: headless is frequently Cloudflare-blocked on accounts.x.ai
|
# Default headed when a display exists; headless is frequently Cloudflare-blocked.
|
||||||
headless = bool(cfg.get("cpa_headless", False))
|
# On servers without DISPLAY, always force headless so Chromium can start.
|
||||||
|
has_disp = sys.platform == "win32" or bool(
|
||||||
|
(os.environ.get("DISPLAY") or "").strip()
|
||||||
|
or (os.environ.get("WAYLAND_DISPLAY") or "").strip()
|
||||||
|
)
|
||||||
|
cpa_hl_raw = cfg.get("cpa_headless", None)
|
||||||
|
if cpa_hl_raw is None:
|
||||||
|
# inherit browser_headless auto / explicit if set
|
||||||
|
cpa_hl_raw = cfg.get("browser_headless", "auto")
|
||||||
|
if isinstance(cpa_hl_raw, bool):
|
||||||
|
headless = cpa_hl_raw
|
||||||
|
else:
|
||||||
|
s = str(cpa_hl_raw or "auto").strip().lower()
|
||||||
|
if s in ("1", "true", "yes", "on", "headless"):
|
||||||
|
headless = True
|
||||||
|
elif s in ("0", "false", "no", "off", "headed", "gui"):
|
||||||
|
headless = False
|
||||||
|
else:
|
||||||
|
headless = not has_disp
|
||||||
|
if not has_disp and not headless:
|
||||||
|
headless = True
|
||||||
|
_log(
|
||||||
|
f"cpa browser forced headless (no DISPLAY={os.environ.get('DISPLAY', '')!r}); "
|
||||||
|
"prefer xvfb-run for Turnstile; protocol mint still preferred first"
|
||||||
|
)
|
||||||
|
elif headless and not bool(cfg.get("cpa_headless", False)):
|
||||||
|
_log(
|
||||||
|
"cpa browser headless auto/on "
|
||||||
|
f"(DISPLAY={os.environ.get('DISPLAY', '')!r}); "
|
||||||
|
"protocol mint still preferred first"
|
||||||
|
)
|
||||||
probe = bool(cfg.get("cpa_probe_after_write", True))
|
probe = bool(cfg.get("cpa_probe_after_write", True))
|
||||||
probe_chat = bool(cfg.get("cpa_probe_chat", False))
|
probe_chat = bool(cfg.get("cpa_probe_chat", False))
|
||||||
timeout = float(cfg.get("cpa_mint_timeout_sec", 240))
|
timeout = float(cfg.get("cpa_mint_timeout_sec", 240))
|
||||||
|
|||||||
@@ -203,18 +203,33 @@ def create_standalone_page(
|
|||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
log(f"extension add failed: {e}")
|
log(f"extension add failed: {e}")
|
||||||
|
|
||||||
if headless:
|
# Auto-headless when no display (Linux server). Prefer register helpers if present.
|
||||||
|
display = (os.environ.get("DISPLAY") or "").strip()
|
||||||
|
wayland = (os.environ.get("WAYLAND_DISPLAY") or "").strip()
|
||||||
|
has_disp = sys.platform == "win32" or bool(display or wayland)
|
||||||
|
effective_headless = bool(headless) or not has_disp
|
||||||
|
if effective_headless and not headless and not has_disp:
|
||||||
|
log(
|
||||||
|
f"no DISPLAY/WAYLAND (DISPLAY={display!r}) — auto headless; "
|
||||||
|
"prefer: xvfb-run -a ... for better Turnstile success"
|
||||||
|
)
|
||||||
|
if effective_headless:
|
||||||
try:
|
try:
|
||||||
opts.headless(True)
|
opts.headless(True)
|
||||||
except Exception:
|
except Exception:
|
||||||
opts.set_argument("--headless=new")
|
opts.set_argument("--headless=new")
|
||||||
|
for flag in ("--headless=new", "--disable-gpu", "--no-sandbox", "--disable-dev-shm-usage"):
|
||||||
|
try:
|
||||||
|
opts.set_argument(flag)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
log("headless=True (may hit Cloudflare / break real clicks)")
|
log("headless=True (may hit Cloudflare / break real clicks)")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
opts.headless(False)
|
opts.headless(False)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
log(f"headed browser DISPLAY={os.environ.get('DISPLAY', '')!r}")
|
log(f"headed browser DISPLAY={display!r}")
|
||||||
|
|
||||||
# Prefer Chromium: Google Chrome blocks --load-extension (turnstilePatch).
|
# Prefer Chromium: Google Chrome blocks --load-extension (turnstilePatch).
|
||||||
for cand in (
|
for cand in (
|
||||||
|
|||||||
+168
-1
@@ -57,6 +57,8 @@ DEFAULT_CONFIG = {
|
|||||||
"cloudmail_admin_email": "",
|
"cloudmail_admin_email": "",
|
||||||
"cloudmail_password": "",
|
"cloudmail_password": "",
|
||||||
"cpa_gui_close_mint_browser": True,
|
"cpa_gui_close_mint_browser": True,
|
||||||
|
"browser_headless": "auto",
|
||||||
|
"browser_path": "",
|
||||||
"hotmail_accounts_file": "mail_credentials.txt",
|
"hotmail_accounts_file": "mail_credentials.txt",
|
||||||
"hotmail_alias_mode": "random",
|
"hotmail_alias_mode": "random",
|
||||||
"hotmail_alias_random_length": 8,
|
"hotmail_alias_random_length": 8,
|
||||||
@@ -622,6 +624,154 @@ CHROMIUM_SLIM_FLAGS = [
|
|||||||
"--disable-blink-features=AutomationControlled",
|
"--disable-blink-features=AutomationControlled",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Headless / display helpers (Linux servers without desktop)
|
||||||
|
_HEADLESS_HINT_SHOWN = False
|
||||||
|
_HEADLESS_HINT_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def has_display() -> bool:
|
||||||
|
"""Return True if a GUI display is likely available for Chromium.
|
||||||
|
|
||||||
|
- Windows: always True (Win32 GUI subsystem).
|
||||||
|
- Linux/macOS: need DISPLAY or WAYLAND_DISPLAY (Xvfb counts as DISPLAY).
|
||||||
|
"""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
return True
|
||||||
|
display = (os.environ.get("DISPLAY") or "").strip()
|
||||||
|
wayland = (os.environ.get("WAYLAND_DISPLAY") or "").strip()
|
||||||
|
return bool(display or wayland)
|
||||||
|
|
||||||
|
|
||||||
|
def _truthy_headless(val) -> bool | None:
|
||||||
|
"""Parse headless preference. True/False explicit; None means auto."""
|
||||||
|
if val is None:
|
||||||
|
return None
|
||||||
|
if isinstance(val, bool):
|
||||||
|
return val
|
||||||
|
s = str(val).strip().lower()
|
||||||
|
if s in ("", "auto", "default", "detect"):
|
||||||
|
return None
|
||||||
|
if s in ("1", "true", "yes", "on", "headless"):
|
||||||
|
return True
|
||||||
|
if s in ("0", "false", "no", "off", "headed", "gui"):
|
||||||
|
return False
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_headless_preference(preference=None) -> bool | None:
|
||||||
|
"""Resolve headless preference from arg → env → config → auto.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True = force headless
|
||||||
|
False = force headed
|
||||||
|
None = auto (headless when no DISPLAY)
|
||||||
|
"""
|
||||||
|
if preference is not None:
|
||||||
|
parsed = _truthy_headless(preference)
|
||||||
|
if parsed is not None or str(preference).strip().lower() in ("", "auto", "default", "detect"):
|
||||||
|
return parsed
|
||||||
|
env_raw = (os.environ.get("BROWSER_HEADLESS") or os.environ.get("GROK_HEADLESS") or "").strip()
|
||||||
|
if env_raw:
|
||||||
|
return _truthy_headless(env_raw)
|
||||||
|
try:
|
||||||
|
cfg_val = config.get("browser_headless", "auto") if isinstance(config, dict) else "auto"
|
||||||
|
except Exception:
|
||||||
|
cfg_val = "auto"
|
||||||
|
return _truthy_headless(cfg_val)
|
||||||
|
|
||||||
|
|
||||||
|
def should_use_headless(preference=None, *, log_hint: bool = True) -> bool:
|
||||||
|
"""Decide whether Chromium should start headless.
|
||||||
|
|
||||||
|
Auto (default): headless when no DISPLAY/WAYLAND_DISPLAY.
|
||||||
|
Force headed without display is impossible — still headless + warn.
|
||||||
|
"""
|
||||||
|
pref = resolve_headless_preference(preference)
|
||||||
|
display_ok = has_display()
|
||||||
|
if pref is True:
|
||||||
|
if log_hint:
|
||||||
|
log_headless_mode(forced=True, auto=False, display_ok=display_ok)
|
||||||
|
return True
|
||||||
|
if pref is False:
|
||||||
|
if display_ok:
|
||||||
|
return False
|
||||||
|
# Cannot start headed without display — force headless
|
||||||
|
if log_hint:
|
||||||
|
log_headless_mode(forced=True, auto=True, display_ok=False, wanted_headed=True)
|
||||||
|
return True
|
||||||
|
# auto
|
||||||
|
use = not display_ok
|
||||||
|
if use and log_hint:
|
||||||
|
log_headless_mode(forced=False, auto=True, display_ok=False)
|
||||||
|
return use
|
||||||
|
|
||||||
|
|
||||||
|
def log_headless_mode(
|
||||||
|
*,
|
||||||
|
forced: bool = False,
|
||||||
|
auto: bool = False,
|
||||||
|
display_ok: bool = False,
|
||||||
|
wanted_headed: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Print one-shot guidance for headless / Xvfb on servers."""
|
||||||
|
global _HEADLESS_HINT_SHOWN
|
||||||
|
with _HEADLESS_HINT_LOCK:
|
||||||
|
if _HEADLESS_HINT_SHOWN:
|
||||||
|
return
|
||||||
|
_HEADLESS_HINT_SHOWN = True
|
||||||
|
disp = (os.environ.get("DISPLAY") or "").strip() or "(unset)"
|
||||||
|
wayland = (os.environ.get("WAYLAND_DISPLAY") or "").strip() or "(unset)"
|
||||||
|
if wanted_headed and not display_ok:
|
||||||
|
print(
|
||||||
|
f"[!] browser_headless=false/--headed 但无图形界面 "
|
||||||
|
f"(DISPLAY={disp}, WAYLAND_DISPLAY={wayland}),仍强制 headless,否则无法启动。",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
elif auto and not display_ok:
|
||||||
|
print(
|
||||||
|
f"[*] 无图形界面 (DISPLAY={disp}),已自动启用 --headless=new。",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
elif forced:
|
||||||
|
print("[*] 已强制启用 headless 浏览器。", flush=True)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
" 提示: headless 更容易被 Cloudflare/Turnstile 拦截;无头服务器推荐虚拟显示:\n"
|
||||||
|
" sudo apt install -y xvfb\n"
|
||||||
|
" xvfb-run -a uv run python -u register_cli.py --extra 1 --threads 1\n"
|
||||||
|
" 或常驻: Xvfb :99 -screen 0 1920x1080x24 & export DISPLAY=:99\n"
|
||||||
|
" 强制无头: config browser_headless=true / env BROWSER_HEADLESS=1 / CLI --headless\n"
|
||||||
|
" 强制有头: config browser_headless=false / CLI --headed(需 DISPLAY 或 xvfb-run)",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_headless_to_options(options, headless: bool = True) -> None:
|
||||||
|
"""Apply headless flags to ChromiumOptions (DrissionPage)."""
|
||||||
|
if not headless:
|
||||||
|
try:
|
||||||
|
options.headless(False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
options.headless(True)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
options.set_argument("--headless=new")
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
options.set_argument("--headless")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Extra flags commonly needed on Linux servers
|
||||||
|
for flag in ("--headless=new", "--disable-gpu", "--no-sandbox", "--disable-dev-shm-usage"):
|
||||||
|
try:
|
||||||
|
options.set_argument(flag)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Prefer Google Chrome for registration reliability (email delivery path).
|
# Prefer Google Chrome for registration reliability (email delivery path).
|
||||||
# Chromium is fallback; turnstilePatch extension only loads on Chromium.
|
# Chromium is fallback; turnstilePatch extension only loads on Chromium.
|
||||||
# On Chrome we inject the same patch via CDP add_init_js (see apply_stealth_patches).
|
# On Chrome we inject the same patch via CDP add_init_js (see apply_stealth_patches).
|
||||||
@@ -723,7 +873,14 @@ def apply_stealth_patches(page=None, log_callback=None) -> bool:
|
|||||||
return ok
|
return ok
|
||||||
|
|
||||||
|
|
||||||
def create_browser_options():
|
def create_browser_options(headless=None):
|
||||||
|
"""Build ChromiumOptions for register / TabPool.
|
||||||
|
|
||||||
|
headless:
|
||||||
|
None → resolve from config/env/auto (no DISPLAY → headless)
|
||||||
|
True → force headless
|
||||||
|
False → prefer headed (still headless if no DISPLAY)
|
||||||
|
"""
|
||||||
options = ChromiumOptions()
|
options = ChromiumOptions()
|
||||||
options.auto_port()
|
options.auto_port()
|
||||||
options.set_timeouts(base=1)
|
options.set_timeouts(base=1)
|
||||||
@@ -766,6 +923,16 @@ def create_browser_options():
|
|||||||
options.set_argument(f"--proxy-server={scheme}://{host}:{port}")
|
options.set_argument(f"--proxy-server={scheme}://{host}:{port}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [proxy] set browser proxy failed: {e}")
|
print(f" [proxy] set browser proxy failed: {e}")
|
||||||
|
|
||||||
|
use_hl = should_use_headless(headless, log_hint=True)
|
||||||
|
apply_headless_to_options(options, use_hl)
|
||||||
|
if use_hl:
|
||||||
|
print(" [browser] headless=True", flush=True)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f" [browser] headed DISPLAY={os.environ.get('DISPLAY', '')!r}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+78
-1
@@ -33,9 +33,11 @@ _orig_create_browser_options = reg.create_browser_options
|
|||||||
|
|
||||||
|
|
||||||
def _patched_create_browser_options():
|
def _patched_create_browser_options():
|
||||||
# Prefer original factory (proxy + CHROMIUM_SLIM_FLAGS + extension)
|
# Prefer original factory (proxy + CHROMIUM_SLIM_FLAGS + extension + auto-headless)
|
||||||
|
used_orig = False
|
||||||
try:
|
try:
|
||||||
opts = _orig_create_browser_options()
|
opts = _orig_create_browser_options()
|
||||||
|
used_orig = True
|
||||||
except Exception:
|
except Exception:
|
||||||
from DrissionPage import ChromiumOptions
|
from DrissionPage import ChromiumOptions
|
||||||
|
|
||||||
@@ -107,6 +109,30 @@ def _patched_create_browser_options():
|
|||||||
opts.add_extension(ext_path)
|
opts.add_extension(ext_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Fallback path only: original factory already applied headless when used_orig.
|
||||||
|
if not used_orig:
|
||||||
|
try:
|
||||||
|
apply = getattr(reg, "apply_headless_to_options", None)
|
||||||
|
should = getattr(reg, "should_use_headless", None)
|
||||||
|
if callable(apply) and callable(should):
|
||||||
|
use_hl = should(None, log_hint=True)
|
||||||
|
apply(opts, use_hl)
|
||||||
|
print(f"[browser] headless={use_hl} (fallback options)", flush=True)
|
||||||
|
else:
|
||||||
|
# Minimal fallback without helpers
|
||||||
|
has_disp = sys.platform == "win32" or bool(
|
||||||
|
(os.environ.get("DISPLAY") or "").strip()
|
||||||
|
or (os.environ.get("WAYLAND_DISPLAY") or "").strip()
|
||||||
|
)
|
||||||
|
if not has_disp:
|
||||||
|
try:
|
||||||
|
opts.headless(True)
|
||||||
|
except Exception:
|
||||||
|
opts.set_argument("--headless=new")
|
||||||
|
print("[browser] headless=True (no DISPLAY, minimal fallback)", flush=True)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[browser] headless apply failed: {exc}", flush=True)
|
||||||
return opts
|
return opts
|
||||||
|
|
||||||
|
|
||||||
@@ -573,6 +599,16 @@ def main() -> int:
|
|||||||
parser.add_argument("--browser-recycle-every", type=int, default=25, help="复用 N 次后完整回收")
|
parser.add_argument("--browser-recycle-every", type=int, default=25, help="复用 N 次后完整回收")
|
||||||
parser.add_argument("--cookie-snapshot", action="store_true", help="注册成功写 cookie 快照(默认关,fast)")
|
parser.add_argument("--cookie-snapshot", action="store_true", help="注册成功写 cookie 快照(默认关,fast)")
|
||||||
parser.add_argument("--inline-mint", action="store_true", help="强制注册线程内联 mint(调试用)")
|
parser.add_argument("--inline-mint", action="store_true", help="强制注册线程内联 mint(调试用)")
|
||||||
|
parser.add_argument(
|
||||||
|
"--headless",
|
||||||
|
action="store_true",
|
||||||
|
help="强制无头浏览器(无 DISPLAY 时默认也会自动 headless)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--headed",
|
||||||
|
action="store_true",
|
||||||
|
help="强制有头浏览器(需 DISPLAY;无显示时仍会回退 headless 并提示 Xvfb)",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
reg.load_config()
|
reg.load_config()
|
||||||
@@ -580,6 +616,19 @@ def main() -> int:
|
|||||||
threads = max(1, min(args.threads, 10))
|
threads = max(1, min(args.threads, 10))
|
||||||
fast = bool(args.fast) and not bool(args.no_fast)
|
fast = bool(args.fast) and not bool(args.no_fast)
|
||||||
|
|
||||||
|
# Headless preference: CLI > config/env/auto (no DISPLAY → headless)
|
||||||
|
if args.headless and args.headed:
|
||||||
|
print("[!] --headless 与 --headed 互斥,忽略 --headed", flush=True)
|
||||||
|
args.headed = False
|
||||||
|
if args.headless:
|
||||||
|
cfg0["browser_headless"] = True
|
||||||
|
if isinstance(getattr(reg, "config", None), dict):
|
||||||
|
reg.config["browser_headless"] = True
|
||||||
|
elif args.headed:
|
||||||
|
cfg0["browser_headless"] = False
|
||||||
|
if isinstance(getattr(reg, "config", None), dict):
|
||||||
|
reg.config["browser_headless"] = False
|
||||||
|
|
||||||
mint_workers = resolve_mint_workers(
|
mint_workers = resolve_mint_workers(
|
||||||
cli_value=args.mint_workers,
|
cli_value=args.mint_workers,
|
||||||
threads=threads,
|
threads=threads,
|
||||||
@@ -639,6 +688,28 @@ def main() -> int:
|
|||||||
print("[*] 所有账号已完成,无需继续(可用 --extra N 再注册)", flush=True)
|
print("[*] 所有账号已完成,无需继续(可用 --extra N 再注册)", flush=True)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
# Surface display/headless decision early (before TabPool cold-start)
|
||||||
|
try:
|
||||||
|
will_hl = reg.should_use_headless(None, log_hint=True)
|
||||||
|
has_disp = reg.has_display()
|
||||||
|
print(
|
||||||
|
f"[*] browser mode: {'headless' if will_hl else 'headed'} "
|
||||||
|
f"(DISPLAY={os.environ.get('DISPLAY', '')!r} has_display={has_disp})",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
# Keep cpa browser fallback aligned when config left cpa_headless unset/false
|
||||||
|
# and we are on a headless host — only auto-enable if user did not force headed.
|
||||||
|
if will_hl and not args.headed:
|
||||||
|
if "cpa_headless" not in cfg0 or cfg0.get("cpa_headless") is False:
|
||||||
|
# Auto-headless host: enable cpa headless unless explicitly headed CLI
|
||||||
|
if not has_disp:
|
||||||
|
cfg0["cpa_headless"] = True
|
||||||
|
if isinstance(getattr(reg, "config", None), dict):
|
||||||
|
reg.config["cpa_headless"] = True
|
||||||
|
print("[*] cpa_headless 自动=true(无 DISPLAY;协议 mint 仍优先纯 HTTP)", flush=True)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[!] headless probe failed: {exc}", flush=True)
|
||||||
|
|
||||||
log_thread = threading.Thread(target=_log_writer, daemon=True)
|
log_thread = threading.Thread(target=_log_writer, daemon=True)
|
||||||
log_thread.start()
|
log_thread.start()
|
||||||
|
|
||||||
@@ -646,6 +717,12 @@ def main() -> int:
|
|||||||
reg.TabPool.init(reg.create_browser_options, log_callback=lambda m: log(0, m))
|
reg.TabPool.init(reg.create_browser_options, log_callback=lambda m: log(0, m))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"[!] 浏览器初始化失败: {exc}", flush=True)
|
print(f"[!] 浏览器初始化失败: {exc}", flush=True)
|
||||||
|
if not reg.has_display():
|
||||||
|
print(
|
||||||
|
"[!] 无 DISPLAY 时若仍失败,请检查 Chrome/Chromium 是否安装,并尝试:\n"
|
||||||
|
" xvfb-run -a uv run python -u register_cli.py --extra 1 --threads 1",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
task_queue: queue.Queue = queue.Queue()
|
task_queue: queue.Queue = queue.Queue()
|
||||||
|
|||||||
@@ -15,6 +15,12 @@
|
|||||||
# 3. 同步 Python 依赖
|
# 3. 同步 Python 依赖
|
||||||
# 4. 尝试安装 Chrome/Chromium(如缺失且有权限)
|
# 4. 尝试安装 Chrome/Chromium(如缺失且有权限)
|
||||||
# 5. 从模板复制 .env / config.json(如缺失)
|
# 5. 从模板复制 .env / config.json(如缺失)
|
||||||
|
# 6. 无 DISPLAY 时自动装/启 Xvfb(虚拟显示,过 CF 比纯 headless 稳)
|
||||||
|
#
|
||||||
|
# 环境变量:
|
||||||
|
# SKIP_XVFB=1 # 强制不用 Xvfb(走 Python 侧自动 headless)
|
||||||
|
# FORCE_XVFB=1 # 即使已有 DISPLAY 也用 xvfb-run 包一层
|
||||||
|
# XVFB_DISPLAY=:99 # 常驻 Xvfb 使用的 display(默认 :99)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -32,6 +38,11 @@ cd "$PROJECT_DIR"
|
|||||||
# uv 常见安装路径,确保后续命令能找到
|
# uv 常见安装路径,确保后续命令能找到
|
||||||
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
||||||
|
|
||||||
|
# 启动包装:默认直接 exec;无桌面时改为 xvfb-run / 自启 Xvfb
|
||||||
|
RUN_PREFIX=()
|
||||||
|
USE_XVFB=0
|
||||||
|
XVFB_MODE="" # xvfb-run | persistent | none
|
||||||
|
|
||||||
echo -e "${CYAN}${BOLD}============================================${NC}"
|
echo -e "${CYAN}${BOLD}============================================${NC}"
|
||||||
echo -e "${CYAN}${BOLD} Grok 注册机 - 一键启动 (Linux/macOS)${NC}"
|
echo -e "${CYAN}${BOLD} Grok 注册机 - 一键启动 (Linux/macOS)${NC}"
|
||||||
echo -e "${CYAN}${BOLD}============================================${NC}"
|
echo -e "${CYAN}${BOLD}============================================${NC}"
|
||||||
@@ -41,9 +52,159 @@ have_cmd() {
|
|||||||
command -v "$1" &>/dev/null
|
command -v "$1" &>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 是否已有可用图形显示(含用户已 export 的 DISPLAY / Wayland)
|
||||||
|
has_display() {
|
||||||
|
[ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 尝试用包管理器安装 xvfb(仅免密 sudo 时自动装)
|
||||||
|
try_install_xvfb() {
|
||||||
|
if have_cmd xvfb-run || have_cmd Xvfb; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e " ${YELLOW}⚠${NC} 未检测到 Xvfb,尝试自动安装..."
|
||||||
|
|
||||||
|
if have_cmd apt-get && have_cmd sudo && sudo -n true 2>/dev/null; then
|
||||||
|
echo " → apt 安装 xvfb..."
|
||||||
|
if sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq \
|
||||||
|
&& sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq xvfb 2>/dev/null; then
|
||||||
|
echo -e " ${GREEN}✓${NC} xvfb 安装成功"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
elif have_cmd dnf && have_cmd sudo && sudo -n true 2>/dev/null; then
|
||||||
|
echo " → dnf 安装 xorg-x11-server-Xvfb..."
|
||||||
|
if sudo dnf install -y xorg-x11-server-Xvfb 2>/dev/null; then
|
||||||
|
echo -e " ${GREEN}✓${NC} Xvfb 安装成功"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
elif have_cmd pacman && have_cmd sudo && sudo -n true 2>/dev/null; then
|
||||||
|
echo " → pacman 安装 xorg-server-xvfb..."
|
||||||
|
if sudo pacman -Sy --noconfirm xorg-server-xvfb 2>/dev/null; then
|
||||||
|
echo -e " ${GREEN}✓${NC} Xvfb 安装成功"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
elif have_cmd yum && have_cmd sudo && sudo -n true 2>/dev/null; then
|
||||||
|
echo " → yum 安装 xorg-x11-server-Xvfb..."
|
||||||
|
if sudo yum install -y xorg-x11-server-Xvfb 2>/dev/null; then
|
||||||
|
echo -e " ${GREEN}✓${NC} Xvfb 安装成功"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}⚠${NC} 无法自动安装(需要免密 sudo 或手动装)"
|
||||||
|
echo " Ubuntu/Debian: sudo apt install -y xvfb"
|
||||||
|
echo " Fedora: sudo dnf install -y xorg-x11-server-Xvfb"
|
||||||
|
echo " Arch: sudo pacman -S xorg-server-xvfb"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if have_cmd xvfb-run || have_cmd Xvfb; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 启动常驻 Xvfb 并 export DISPLAY
|
||||||
|
start_persistent_xvfb() {
|
||||||
|
local disp="${XVFB_DISPLAY:-:99}"
|
||||||
|
local sock="/tmp/.X11-unix/X${disp#:}"
|
||||||
|
|
||||||
|
# 已有该 display 的 X socket 则直接复用
|
||||||
|
if [ -S "$sock" ] 2>/dev/null; then
|
||||||
|
export DISPLAY="$disp"
|
||||||
|
echo -e " ${GREEN}✓${NC} 复用已有 Xvfb DISPLAY=$DISPLAY"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! have_cmd Xvfb; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo " → 启动常驻 Xvfb $disp ..."
|
||||||
|
# 后台起;失败不阻断(上层会回退 headless)
|
||||||
|
Xvfb "$disp" -screen 0 1920x1080x24 -ac +extension GLX +render -noreset \
|
||||||
|
>/tmp/grok-xvfb.log 2>&1 &
|
||||||
|
local xvfb_pid=$!
|
||||||
|
sleep 0.5
|
||||||
|
if [ -S "$sock" ] 2>/dev/null || kill -0 "$xvfb_pid" 2>/dev/null; then
|
||||||
|
export DISPLAY="$disp"
|
||||||
|
echo -e " ${GREEN}✓${NC} Xvfb 已启动 DISPLAY=$DISPLAY pid=$xvfb_pid (log: /tmp/grok-xvfb.log)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo -e " ${YELLOW}⚠${NC} 常驻 Xvfb 启动失败,见 /tmp/grok-xvfb.log"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 无桌面时准备虚拟显示;成功则 RUN_PREFIX / DISPLAY 已设好
|
||||||
|
step6_ensure_display() {
|
||||||
|
echo -e "${CYAN}[6/6] 检测图形显示 / 自动 Xvfb...${NC}"
|
||||||
|
|
||||||
|
# macOS 一般有 Aqua,不强制 Xvfb
|
||||||
|
if [ "$(uname -s 2>/dev/null || echo unknown)" = "Darwin" ]; then
|
||||||
|
if has_display; then
|
||||||
|
echo -e " ${GREEN}✓${NC} macOS 已有显示 DISPLAY=${DISPLAY:-} WAYLAND=${WAYLAND_DISPLAY:-}"
|
||||||
|
else
|
||||||
|
echo -e " ${GREEN}✓${NC} macOS:不强制 Xvfb(Python 侧可 headless)"
|
||||||
|
fi
|
||||||
|
USE_XVFB=0
|
||||||
|
XVFB_MODE="none"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${SKIP_XVFB:-0}" = "1" ]; then
|
||||||
|
echo -e " ${YELLOW}⚠${NC} SKIP_XVFB=1:跳过虚拟显示,走 Python 自动 headless"
|
||||||
|
USE_XVFB=0
|
||||||
|
XVFB_MODE="none"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local need_xvfb=0
|
||||||
|
if [ "${FORCE_XVFB:-0}" = "1" ]; then
|
||||||
|
need_xvfb=1
|
||||||
|
echo -e " ${CYAN}→${NC} FORCE_XVFB=1,将使用虚拟显示"
|
||||||
|
elif ! has_display; then
|
||||||
|
need_xvfb=1
|
||||||
|
echo -e " ${YELLOW}⚠${NC} 无 DISPLAY/WAYLAND(无头服务器),准备自动启用 Xvfb"
|
||||||
|
else
|
||||||
|
echo -e " ${GREEN}✓${NC} 已有显示 DISPLAY=${DISPLAY:-} WAYLAND=${WAYLAND_DISPLAY:-}"
|
||||||
|
USE_XVFB=0
|
||||||
|
XVFB_MODE="none"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$need_xvfb" -ne 1 ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
try_install_xvfb || true
|
||||||
|
|
||||||
|
# 优先 xvfb-run(自动分配 display,最省心)
|
||||||
|
if have_cmd xvfb-run; then
|
||||||
|
RUN_PREFIX=(xvfb-run -a -s "-screen 0 1920x1080x24")
|
||||||
|
USE_XVFB=1
|
||||||
|
XVFB_MODE="xvfb-run"
|
||||||
|
echo -e " ${GREEN}✓${NC} 将用 xvfb-run 启动(虚拟有头,利于过 CF/Turnstile)"
|
||||||
|
echo " 若需纯 headless: SKIP_XVFB=1 ./start.sh ..."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 其次常驻 Xvfb + export DISPLAY
|
||||||
|
if start_persistent_xvfb; then
|
||||||
|
RUN_PREFIX=()
|
||||||
|
USE_XVFB=1
|
||||||
|
XVFB_MODE="persistent"
|
||||||
|
echo -e " ${GREEN}✓${NC} 使用常驻 Xvfb DISPLAY=$DISPLAY"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 都失败 → Python 侧 create_browser_options 会自动 headless
|
||||||
|
echo -e " ${YELLOW}⚠${NC} 未能启用 Xvfb,将依赖 Python 自动 headless(可能更易被 CF 拦)"
|
||||||
|
echo " 手动: sudo apt install -y xvfb && xvfb-run -a ./start.sh"
|
||||||
|
USE_XVFB=0
|
||||||
|
XVFB_MODE="none"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# ── 0. 基础工具 ──
|
# ── 0. 基础工具 ──
|
||||||
step0_basics() {
|
step0_basics() {
|
||||||
echo -e "${CYAN}[0/5] 检查基础工具...${NC}"
|
echo -e "${CYAN}[0/6] 检查基础工具...${NC}"
|
||||||
if ! have_cmd curl && ! have_cmd wget; then
|
if ! have_cmd curl && ! have_cmd wget; then
|
||||||
echo -e " ${RED}✗${NC} 需要 curl 或 wget 才能自动安装环境"
|
echo -e " ${RED}✗${NC} 需要 curl 或 wget 才能自动安装环境"
|
||||||
echo " 请先安装: sudo apt install -y curl 或 sudo yum install -y curl"
|
echo " 请先安装: sudo apt install -y curl 或 sudo yum install -y curl"
|
||||||
@@ -58,7 +219,7 @@ step0_basics() {
|
|||||||
|
|
||||||
# ── 1. 检测/安装 uv ──
|
# ── 1. 检测/安装 uv ──
|
||||||
step1_install_uv() {
|
step1_install_uv() {
|
||||||
echo -e "${CYAN}[1/5] 检测/安装 uv 包管理器...${NC}"
|
echo -e "${CYAN}[1/6] 检测/安装 uv 包管理器...${NC}"
|
||||||
|
|
||||||
if have_cmd uv; then
|
if have_cmd uv; then
|
||||||
echo -e " ${GREEN}✓${NC} uv: $(uv --version 2>&1)"
|
echo -e " ${GREEN}✓${NC} uv: $(uv --version 2>&1)"
|
||||||
@@ -92,7 +253,7 @@ step1_install_uv() {
|
|||||||
# ── 2. 检测/安装 Python 3.13 ──
|
# ── 2. 检测/安装 Python 3.13 ──
|
||||||
# 策略: 优先系统/mise 已有 3.13;否则用 uv 下载管理 Python
|
# 策略: 优先系统/mise 已有 3.13;否则用 uv 下载管理 Python
|
||||||
step2_install_python() {
|
step2_install_python() {
|
||||||
echo -e "${CYAN}[2/5] 检测/安装 Python 3.13...${NC}"
|
echo -e "${CYAN}[2/6] 检测/安装 Python 3.13...${NC}"
|
||||||
PYTHON_BIN=""
|
PYTHON_BIN=""
|
||||||
|
|
||||||
# mise 托管的 python
|
# mise 托管的 python
|
||||||
@@ -160,7 +321,7 @@ step2_install_python() {
|
|||||||
|
|
||||||
# ── 3. 同步依赖 ──
|
# ── 3. 同步依赖 ──
|
||||||
step3_sync_deps() {
|
step3_sync_deps() {
|
||||||
echo -e "${CYAN}[3/5] 同步 Python 依赖...${NC}"
|
echo -e "${CYAN}[3/6] 同步 Python 依赖...${NC}"
|
||||||
|
|
||||||
# 固定使用 3.13,避免系统默认 python 过旧
|
# 固定使用 3.13,避免系统默认 python 过旧
|
||||||
if ! uv sync --python 3.13; then
|
if ! uv sync --python 3.13; then
|
||||||
@@ -176,7 +337,7 @@ step3_sync_deps() {
|
|||||||
|
|
||||||
# ── 4. 检测/尝试安装浏览器 ──
|
# ── 4. 检测/尝试安装浏览器 ──
|
||||||
step4_check_browser() {
|
step4_check_browser() {
|
||||||
echo -e "${CYAN}[4/5] 检测/安装浏览器...${NC}"
|
echo -e "${CYAN}[4/6] 检测/安装浏览器...${NC}"
|
||||||
|
|
||||||
local candidate
|
local candidate
|
||||||
for candidate in \
|
for candidate in \
|
||||||
@@ -262,7 +423,7 @@ step4_check_browser() {
|
|||||||
|
|
||||||
# ── 5. 配置文件模板 ──
|
# ── 5. 配置文件模板 ──
|
||||||
step5_config_templates() {
|
step5_config_templates() {
|
||||||
echo -e "${CYAN}[5/5] 检查配置文件...${NC}"
|
echo -e "${CYAN}[5/6] 检查配置文件...${NC}"
|
||||||
|
|
||||||
if [ ! -f "$PROJECT_DIR/.env" ] && [ -f "$PROJECT_DIR/.env.example" ]; then
|
if [ ! -f "$PROJECT_DIR/.env" ] && [ -f "$PROJECT_DIR/.env.example" ]; then
|
||||||
cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env"
|
cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env"
|
||||||
@@ -298,10 +459,19 @@ main() {
|
|||||||
echo ""
|
echo ""
|
||||||
step5_config_templates || exit 1
|
step5_config_templates || exit 1
|
||||||
echo ""
|
echo ""
|
||||||
|
step6_ensure_display
|
||||||
|
echo ""
|
||||||
|
|
||||||
echo -e "${CYAN}${BOLD}============================================${NC}"
|
echo -e "${CYAN}${BOLD}============================================${NC}"
|
||||||
echo -e "${GREEN}${BOLD} 环境就绪,启动 register_cli.py${NC}"
|
echo -e "${GREEN}${BOLD} 环境就绪,启动 register_cli.py${NC}"
|
||||||
echo -e "${CYAN}${BOLD}============================================${NC}"
|
echo -e "${CYAN}${BOLD}============================================${NC}"
|
||||||
|
if [ "$USE_XVFB" = "1" ]; then
|
||||||
|
echo -e " 显示模式: ${GREEN}Xvfb (${XVFB_MODE})${NC} DISPLAY=${DISPLAY:-(xvfb-run 自动分配)}"
|
||||||
|
elif has_display; then
|
||||||
|
echo -e " 显示模式: ${GREEN}headed${NC} DISPLAY=${DISPLAY:-} WAYLAND=${WAYLAND_DISPLAY:-}"
|
||||||
|
else
|
||||||
|
echo -e " 显示模式: ${YELLOW}headless (Python 自动)${NC}"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# 默认不限数量(--count 0),除非用户显式传参
|
# 默认不限数量(--count 0),除非用户显式传参
|
||||||
@@ -320,7 +490,13 @@ main() {
|
|||||||
mise install >/dev/null 2>&1 || true
|
mise install >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ${#RUN_PREFIX[@]} -gt 0 ]; then
|
||||||
|
echo -e "${CYAN}[*] 启动命令: ${RUN_PREFIX[*]} uv run --python 3.13 python -u register_cli.py ${args[*]}${NC}"
|
||||||
|
echo ""
|
||||||
|
exec "${RUN_PREFIX[@]}" uv run --python 3.13 python -u register_cli.py "${args[@]}"
|
||||||
|
else
|
||||||
exec uv run --python 3.13 python -u register_cli.py "${args[@]}"
|
exec uv run --python 3.13 python -u register_cli.py "${args[@]}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
Reference in new issue
Block a user