- Major refactor of grok_register_ttk.py: improved flow, error handling, logging - Update config.example.json and config.json with new fields - Replace turnstilePatch/content.js with new script.js approach - Update start.sh for better Chromium/Turnstile handling - Remove deprecated turnstilePatch/content.js
791 lines
28 KiB
Bash
Executable File
791 lines
28 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ============================================================================
|
||
# Grok 注册机 - 一键启动脚本 (Linux / macOS)
|
||
# 自动检测并安装缺失环境,然后启动 register_cli.py
|
||
#
|
||
# 用法:
|
||
# ./start.sh # 默认不限数量(持续注册)
|
||
# ./start.sh --count 10 # 跑 10 个
|
||
# ./start.sh --count 10 --threads 3 # 3 线程跑 10 个
|
||
# ./start.sh --extra 5 # 在已有基础上再注册 5 个
|
||
#
|
||
# 首次运行会自动:
|
||
# 1. 安装 uv(如缺失)
|
||
# 2. 安装 Python 3.13(如缺失,经 uv 管理)
|
||
# 3. 同步 Python 依赖
|
||
# 4. 优先安装/选用 Chromium(可加载 turnstilePatch;仅 Chrome 过 CF 易失败)
|
||
# 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)
|
||
# BROWSER_PREFER=chromium|chrome|auto # 覆盖浏览器偏好(默认有 Chromium 时强制 chromium)
|
||
# BROWSER_PATH=/path/to/browser # 强制浏览器可执行文件
|
||
# SKIP_BROWSER_INSTALL=1 # 跳过自动安装 Chromium
|
||
# ============================================================================
|
||
set -euo pipefail
|
||
|
||
# ── 颜色 ──
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'
|
||
BOLD='\033[1m'
|
||
NC='\033[0m'
|
||
|
||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
cd "$PROJECT_DIR"
|
||
|
||
# uv 常见安装路径,确保后续命令能找到
|
||
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
||
|
||
# 启动包装:默认直接 exec;无桌面时改为 xvfb-run / 自启 Xvfb
|
||
RUN_PREFIX=()
|
||
USE_XVFB=0
|
||
XVFB_MODE="" # xvfb-run | persistent | none
|
||
|
||
# 浏览器探测结果(step4 写入;main 启动前 export 给 Python)
|
||
SELECTED_BROWSER=""
|
||
SELECTED_BROWSER_KIND="" # chromium | chrome | none
|
||
|
||
echo -e "${CYAN}${BOLD}============================================${NC}"
|
||
echo -e "${CYAN}${BOLD} Grok 注册机 - 一键启动 (Linux/macOS)${NC}"
|
||
echo -e "${CYAN}${BOLD}============================================${NC}"
|
||
echo ""
|
||
|
||
have_cmd() {
|
||
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. 基础工具 ──
|
||
step0_basics() {
|
||
echo -e "${CYAN}[0/6] 检查基础工具...${NC}"
|
||
if ! have_cmd curl && ! have_cmd wget; then
|
||
echo -e " ${RED}✗${NC} 需要 curl 或 wget 才能自动安装环境"
|
||
echo " 请先安装: sudo apt install -y curl 或 sudo yum install -y curl"
|
||
return 1
|
||
fi
|
||
if have_cmd curl; then
|
||
echo -e " ${GREEN}✓${NC} curl 可用"
|
||
else
|
||
echo -e " ${GREEN}✓${NC} wget 可用"
|
||
fi
|
||
}
|
||
|
||
# ── 1. 检测/安装 uv ──
|
||
step1_install_uv() {
|
||
echo -e "${CYAN}[1/6] 检测/安装 uv 包管理器...${NC}"
|
||
|
||
if have_cmd uv; then
|
||
echo -e " ${GREEN}✓${NC} uv: $(uv --version 2>&1)"
|
||
return 0
|
||
fi
|
||
|
||
echo -e " ${YELLOW}⚠${NC} 未检测到 uv,正在自动安装..."
|
||
if have_cmd curl; then
|
||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||
else
|
||
wget -qO- https://astral.sh/uv/install.sh | sh
|
||
fi
|
||
|
||
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
||
|
||
# 部分环境写到 ~/.profile,再补一次常见路径
|
||
if [ -f "$HOME/.local/bin/env" ]; then
|
||
# shellcheck disable=SC1091
|
||
source "$HOME/.local/bin/env" 2>/dev/null || true
|
||
fi
|
||
|
||
if have_cmd uv; then
|
||
echo -e " ${GREEN}✓${NC} uv 安装成功: $(uv --version 2>&1)"
|
||
return 0
|
||
fi
|
||
|
||
echo -e " ${RED}✗${NC} uv 安装失败,请手动: curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||
return 1
|
||
}
|
||
|
||
# ── 2. 检测/安装 Python 3.13 ──
|
||
# 策略: 优先系统/mise 已有 3.13;否则用 uv 下载管理 Python
|
||
step2_install_python() {
|
||
echo -e "${CYAN}[2/6] 检测/安装 Python 3.13...${NC}"
|
||
PYTHON_BIN=""
|
||
|
||
# mise 托管的 python
|
||
if have_cmd mise && [ -f "$PROJECT_DIR/mise.toml" ]; then
|
||
# 尝试按项目配置安装工具链
|
||
if mise install 2>/dev/null; then
|
||
local py
|
||
py=$(mise which python 2>/dev/null || true)
|
||
if [ -n "$py" ] && "$py" -c "import sys; sys.exit(0 if sys.version_info >= (3,13) else 1)" 2>/dev/null; then
|
||
PYTHON_BIN="$py"
|
||
echo -e " ${GREEN}✓${NC} Python (mise): $($py --version 2>&1) at $py"
|
||
return 0
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# 系统 / PATH 上的 3.13+
|
||
local candidate
|
||
for candidate in python3.13 python3.14 python3 python; do
|
||
if have_cmd "$candidate"; then
|
||
if "$candidate" -c "import sys; sys.exit(0 if sys.version_info >= (3,13) else 1)" 2>/dev/null; then
|
||
PYTHON_BIN="$candidate"
|
||
echo -e " ${GREEN}✓${NC} Python: $($candidate --version 2>&1)"
|
||
return 0
|
||
fi
|
||
fi
|
||
done
|
||
|
||
# 项目 .venv
|
||
if [ -x "$PROJECT_DIR/.venv/bin/python" ]; then
|
||
if "$PROJECT_DIR/.venv/bin/python" -c "import sys; sys.exit(0 if sys.version_info >= (3,13) else 1)" 2>/dev/null; then
|
||
PYTHON_BIN="$PROJECT_DIR/.venv/bin/python"
|
||
echo -e " ${GREEN}✓${NC} Python (.venv): $($PYTHON_BIN --version 2>&1)"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# 用 uv 安装托管的 Python 3.13
|
||
echo -e " ${YELLOW}⚠${NC} 未找到 Python 3.13+,正在通过 uv 安装..."
|
||
if ! uv python install 3.13; then
|
||
echo -e " ${RED}✗${NC} uv python install 3.13 失败"
|
||
echo " 也可手动: uv python install 3.13"
|
||
return 1
|
||
fi
|
||
|
||
# 解析 uv 管理的 python 路径
|
||
local uv_py
|
||
uv_py=$(uv python find 3.13 2>/dev/null || true)
|
||
if [ -n "$uv_py" ] && [ -x "$uv_py" ]; then
|
||
PYTHON_BIN="$uv_py"
|
||
echo -e " ${GREEN}✓${NC} Python (uv): $($uv_py --version 2>&1) at $uv_py"
|
||
return 0
|
||
fi
|
||
|
||
# uv python find 失败时,后续 uv run 仍可自动解析
|
||
if uv run --python 3.13 python -c "import sys; sys.exit(0 if sys.version_info >= (3,13) else 1)" 2>/dev/null; then
|
||
PYTHON_BIN="uv-managed"
|
||
echo -e " ${GREEN}✓${NC} Python 3.13 已由 uv 管理(将通过 uv run 调用)"
|
||
return 0
|
||
fi
|
||
|
||
echo -e " ${RED}✗${NC} 无法安装或定位 Python 3.13"
|
||
return 1
|
||
}
|
||
|
||
# ── 3. 同步依赖 ──
|
||
step3_sync_deps() {
|
||
echo -e "${CYAN}[3/6] 同步 Python 依赖...${NC}"
|
||
|
||
# 固定使用 3.13,避免系统默认 python 过旧
|
||
if ! uv sync --python 3.13; then
|
||
echo -e " ${YELLOW}⚠${NC} uv sync --python 3.13 失败,尝试默认 uv sync..."
|
||
if ! uv sync; then
|
||
echo -e " ${RED}✗${NC} 依赖同步失败"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
echo -e " ${GREEN}✓${NC} 依赖同步完成"
|
||
}
|
||
|
||
# 是否是合法浏览器可执行路径(拒绝 /usr/bin/snap 这类 snap 软链目标)
|
||
_is_valid_browser_bin() {
|
||
local p="$1"
|
||
local base
|
||
[ -n "$p" ] || return 1
|
||
[ -x "$p" ] 2>/dev/null || return 1
|
||
base="$(basename "$p")"
|
||
case "$base" in
|
||
snap|snap-confine|snapd|env|bash|sh|python*|uv)
|
||
return 1
|
||
;;
|
||
esac
|
||
# 路径名应像浏览器
|
||
case "$p" in
|
||
*chromium*|*chrome*|*Chrome*|*Chromium*)
|
||
return 0
|
||
;;
|
||
esac
|
||
return 1
|
||
}
|
||
|
||
# 解析路径:command -v 或可执行绝对路径
|
||
# 注意:/snap/bin/chromium 经 readlink -f 会变成 /usr/bin/snap —— 绝不能用那个结果启动浏览器
|
||
_resolve_bin() {
|
||
local cand="$1"
|
||
local p="" real=""
|
||
if [ -z "$cand" ]; then
|
||
return 1
|
||
fi
|
||
|
||
if [ -x "$cand" ] 2>/dev/null; then
|
||
p="$cand"
|
||
elif have_cmd "$cand"; then
|
||
p="$(command -v "$cand" 2>/dev/null || true)"
|
||
else
|
||
return 1
|
||
fi
|
||
[ -n "$p" ] || return 1
|
||
|
||
# snap 包装器:保留 /snap/bin/chromium,不要 resolve 成 /usr/bin/snap
|
||
case "$p" in
|
||
/snap/bin/*)
|
||
if _is_valid_browser_bin "$p"; then
|
||
echo "$p"
|
||
return 0
|
||
fi
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
if have_cmd readlink; then
|
||
real="$(readlink -f "$p" 2>/dev/null || true)"
|
||
# readlink 落到 snap 宿主时,回退用原路径(若原路径仍有效)
|
||
if [ -n "$real" ]; then
|
||
case "$(basename "$real")" in
|
||
snap|snap-confine|snapd)
|
||
if _is_valid_browser_bin "$p"; then
|
||
echo "$p"
|
||
return 0
|
||
fi
|
||
return 1
|
||
;;
|
||
esac
|
||
if _is_valid_browser_bin "$real"; then
|
||
echo "$real"
|
||
return 0
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
if _is_valid_browser_bin "$p"; then
|
||
echo "$p"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
find_playwright_chromium() {
|
||
# Playwright Chromium 无 snap AppArmor 限制,ReinerBRO/grok-register 同款优先路径
|
||
local root cand
|
||
for root in \
|
||
"${HOME}/.cache/ms-playwright" \
|
||
"${LOCALAPPDATA:-}/ms-playwright"; do
|
||
[ -d "$root" ] || continue
|
||
# 取最新 chromium-* 目录
|
||
cand=$(find "$root" -maxdepth 3 -type f \( -path '*/chrome-linux64/chrome' -o -path '*/chrome-linux/chrome' -o -path '*/chrome-win64/chrome.exe' \) 2>/dev/null | sort -r | head -1 || true)
|
||
if [ -n "$cand" ] && [ -x "$cand" ] || [ -f "$cand" ]; then
|
||
echo "$cand"
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
find_chromium_bin() {
|
||
local candidate p
|
||
# 1) Playwright Chromium(推荐,服务器无 AppArmor/snap 坑)
|
||
if p="$(find_playwright_chromium 2>/dev/null)"; then
|
||
echo "$p"
|
||
return 0
|
||
fi
|
||
# 2) deb/系统包路径,snap 放后面(且不 resolve 成 /usr/bin/snap)
|
||
for candidate in \
|
||
/usr/bin/chromium \
|
||
/usr/bin/chromium-browser \
|
||
/usr/lib/chromium/chromium \
|
||
/usr/lib/chromium-browser/chromium-browser \
|
||
chromium \
|
||
chromium-browser \
|
||
/snap/bin/chromium \
|
||
"/Applications/Chromium.app/Contents/MacOS/Chromium"; do
|
||
[ -z "$candidate" ] && continue
|
||
if p="$(_resolve_bin "$candidate")"; then
|
||
case "$p" in
|
||
*google-chrome*) continue ;;
|
||
*/snap) continue ;;
|
||
esac
|
||
# 必须像 chromium
|
||
case "$p" in
|
||
*chromium*|*Chromium*|*chrome-linux*)
|
||
echo "$p"
|
||
return 0
|
||
;;
|
||
esac
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
find_chrome_bin() {
|
||
local candidate p
|
||
for candidate in \
|
||
/usr/bin/google-chrome-stable \
|
||
/usr/bin/google-chrome \
|
||
google-chrome-stable \
|
||
google-chrome \
|
||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; do
|
||
if p="$(_resolve_bin "$candidate")"; then
|
||
case "$(basename "$p")" in
|
||
snap) continue ;;
|
||
esac
|
||
echo "$p"
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# 选中浏览器并 export,供 register_cli / create_browser_options 使用
|
||
select_and_export_browser() {
|
||
local chromium_bin="" chrome_bin="" user_path=""
|
||
chromium_bin="$(find_chromium_bin 2>/dev/null || true)"
|
||
chrome_bin="$(find_chrome_bin 2>/dev/null || true)"
|
||
|
||
# 用户显式 BROWSER_PATH 优先,但必须通过校验(拒绝 /usr/bin/snap)
|
||
if [ -n "${BROWSER_PATH:-}" ]; then
|
||
user_path="$(_resolve_bin "$BROWSER_PATH" 2>/dev/null || true)"
|
||
if [ -z "$user_path" ] && _is_valid_browser_bin "$BROWSER_PATH"; then
|
||
user_path="$BROWSER_PATH"
|
||
fi
|
||
if [ -n "$user_path" ]; then
|
||
SELECTED_BROWSER="$user_path"
|
||
case "$SELECTED_BROWSER" in
|
||
*chromium*|*Chromium*) SELECTED_BROWSER_KIND="chromium" ;;
|
||
*) SELECTED_BROWSER_KIND="chrome" ;;
|
||
esac
|
||
else
|
||
echo -e " ${YELLOW}⚠${NC} 忽略无效 BROWSER_PATH=${BROWSER_PATH}(常见于 snap 解析成 /usr/bin/snap)"
|
||
unset BROWSER_PATH
|
||
user_path=""
|
||
fi
|
||
fi
|
||
|
||
if [ -z "${SELECTED_BROWSER:-}" ] || [ -z "${user_path:-}" ]; then
|
||
if [ -n "$chromium_bin" ]; then
|
||
SELECTED_BROWSER="$chromium_bin"
|
||
SELECTED_BROWSER_KIND="chromium"
|
||
elif [ -n "$chrome_bin" ]; then
|
||
SELECTED_BROWSER="$chrome_bin"
|
||
SELECTED_BROWSER_KIND="chrome"
|
||
else
|
||
SELECTED_BROWSER=""
|
||
SELECTED_BROWSER_KIND="none"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# 二次校验:绝不能导出 snap 宿主
|
||
if ! _is_valid_browser_bin "$SELECTED_BROWSER"; then
|
||
echo -e " ${RED}✗${NC} 浏览器路径无效: $SELECTED_BROWSER"
|
||
SELECTED_BROWSER=""
|
||
SELECTED_BROWSER_KIND="none"
|
||
return 1
|
||
fi
|
||
|
||
# 有 Chromium 时强制偏好 chromium,避免 Python 仍选到 Chrome
|
||
if [ "$SELECTED_BROWSER_KIND" = "chromium" ]; then
|
||
export BROWSER_PREFER="${BROWSER_PREFER:-chromium}"
|
||
export BROWSER_PATH="$SELECTED_BROWSER"
|
||
export CHROMIUM_PATH="$SELECTED_BROWSER"
|
||
else
|
||
export BROWSER_PREFER="${BROWSER_PREFER:-chrome}"
|
||
export BROWSER_PATH="$SELECTED_BROWSER"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
try_install_chromium() {
|
||
if [ "${SKIP_BROWSER_INSTALL:-0}" = "1" ]; then
|
||
echo -e " ${YELLOW}⚠${NC} SKIP_BROWSER_INSTALL=1,跳过自动安装 Chromium"
|
||
return 1
|
||
fi
|
||
|
||
local ok=false
|
||
|
||
# Debian/Ubuntu
|
||
if have_cmd apt-get && have_cmd sudo; then
|
||
if sudo -n true 2>/dev/null; then
|
||
echo " → apt 安装 chromium(免密 sudo)..."
|
||
sudo apt-get update -qq 2>/dev/null || true
|
||
# 不同发行版包名不一,逐个试
|
||
if sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq chromium 2>/dev/null \
|
||
|| sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq chromium-browser 2>/dev/null; then
|
||
ok=true
|
||
fi
|
||
# Ubuntu 常见 snap 包
|
||
if ! $ok && have_cmd snap && sudo -n true 2>/dev/null; then
|
||
echo " → snap 安装 chromium..."
|
||
if sudo snap install chromium 2>/dev/null; then
|
||
ok=true
|
||
fi
|
||
fi
|
||
else
|
||
echo -e " ${YELLOW}⚠${NC} 需要交互 sudo,无法自动装 Chromium"
|
||
echo " 请手动: sudo apt install -y chromium 或 sudo snap install chromium"
|
||
fi
|
||
# Arch
|
||
elif have_cmd pacman && have_cmd sudo && sudo -n true 2>/dev/null; then
|
||
echo " → pacman 安装 chromium..."
|
||
if sudo pacman -Sy --noconfirm chromium 2>/dev/null; then
|
||
ok=true
|
||
fi
|
||
# Fedora/RHEL
|
||
elif have_cmd dnf && have_cmd sudo && sudo -n true 2>/dev/null; then
|
||
echo " → dnf 安装 chromium..."
|
||
if sudo dnf install -y chromium 2>/dev/null; then
|
||
ok=true
|
||
fi
|
||
elif have_cmd yum && have_cmd sudo && sudo -n true 2>/dev/null; then
|
||
echo " → yum 安装 chromium..."
|
||
if sudo yum install -y chromium 2>/dev/null; then
|
||
ok=true
|
||
fi
|
||
# macOS
|
||
elif have_cmd brew; then
|
||
echo " → brew 安装 chromium..."
|
||
if brew install --cask chromium 2>/dev/null; then
|
||
ok=true
|
||
fi
|
||
fi
|
||
|
||
if $ok; then
|
||
echo -e " ${GREEN}✓${NC} Chromium 安装命令已成功"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# ── 4. 检测/尝试安装浏览器(优先 Chromium) ──
|
||
step4_check_browser() {
|
||
echo -e "${CYAN}[4/6] 检测/安装浏览器(优先 Chromium + turnstilePatch)...${NC}"
|
||
|
||
# 已有 Chromium → 直接选用
|
||
if select_and_export_browser && [ "$SELECTED_BROWSER_KIND" = "chromium" ]; then
|
||
echo -e " ${GREEN}✓${NC} Chromium: $SELECTED_BROWSER"
|
||
echo -e " ${CYAN}·${NC} export BROWSER_PREFER=$BROWSER_PREFER BROWSER_PATH=$BROWSER_PATH"
|
||
local chrome_extra
|
||
chrome_extra="$(find_chrome_bin 2>/dev/null || true)"
|
||
if [ -n "$chrome_extra" ]; then
|
||
echo -e " ${CYAN}·${NC} 另检测到 Chrome: $chrome_extra(已忽略,注册强制 Chromium)"
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
if [ "$SELECTED_BROWSER_KIND" = "chrome" ]; then
|
||
echo -e " ${YELLOW}⚠${NC} 仅有 Google Chrome: $SELECTED_BROWSER"
|
||
echo " Chrome 禁止 --load-extension,无法加载 turnstilePatch,Turnstile 常 token=0。"
|
||
echo " 将尝试安装 Chromium..."
|
||
else
|
||
echo -e " ${YELLOW}⚠${NC} 未检测到浏览器,尝试自动安装 Chromium..."
|
||
fi
|
||
|
||
try_install_chromium || true
|
||
|
||
# 刷新 PATH(snap 等)
|
||
export PATH="/snap/bin:$PATH"
|
||
|
||
if select_and_export_browser && [ "$SELECTED_BROWSER_KIND" = "chromium" ]; then
|
||
echo -e " ${GREEN}✓${NC} Chromium 已就绪: $SELECTED_BROWSER"
|
||
echo -e " ${CYAN}·${NC} export BROWSER_PREFER=$BROWSER_PREFER BROWSER_PATH=$BROWSER_PATH"
|
||
return 0
|
||
fi
|
||
|
||
if [ "$SELECTED_BROWSER_KIND" = "chrome" ]; then
|
||
echo -e " ${YELLOW}⚠${NC} 仍只有 Google Chrome: $SELECTED_BROWSER"
|
||
echo " 注册将继续,但 CF/Turnstile 成功率低。"
|
||
echo " 请尽快: sudo apt install -y chromium 或 sudo snap install chromium"
|
||
echo -e " ${CYAN}·${NC} 当前 BROWSER_PATH=$BROWSER_PATH"
|
||
return 0
|
||
fi
|
||
|
||
echo -e " ${YELLOW}⚠${NC} 未能找到任何浏览器"
|
||
echo " 注册需要 Chromium(推荐)才能稳定过 Turnstile。"
|
||
echo " 手动安装:"
|
||
echo " Ubuntu/Debian: sudo apt install -y chromium"
|
||
echo " 或 sudo snap install chromium"
|
||
echo " Arch: sudo pacman -S chromium"
|
||
echo " Fedora: sudo dnf install -y chromium"
|
||
echo " macOS: brew install --cask chromium"
|
||
echo " ${YELLOW}脚本将继续,注册很可能失败。${NC}"
|
||
}
|
||
|
||
# ── 5. 配置文件模板 ──
|
||
step5_config_templates() {
|
||
echo -e "${CYAN}[5/6] 检查配置文件...${NC}"
|
||
|
||
if [ ! -f "$PROJECT_DIR/.env" ] && [ -f "$PROJECT_DIR/.env.example" ]; then
|
||
cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env"
|
||
echo -e " ${YELLOW}⚠${NC} 已从 .env.example 创建 .env,请编辑填入真实配置"
|
||
elif [ -f "$PROJECT_DIR/.env" ]; then
|
||
echo -e " ${GREEN}✓${NC} .env 已存在"
|
||
else
|
||
echo -e " ${YELLOW}⚠${NC} 无 .env / .env.example(可直接写 config.json)"
|
||
fi
|
||
|
||
if [ ! -f "$PROJECT_DIR/config.json" ] && [ -f "$PROJECT_DIR/config.example.json" ]; then
|
||
cp "$PROJECT_DIR/config.example.json" "$PROJECT_DIR/config.json"
|
||
echo -e " ${YELLOW}⚠${NC} 已从 config.example.json 创建 config.json,请编辑填入真实配置"
|
||
elif [ -f "$PROJECT_DIR/config.json" ]; then
|
||
echo -e " ${GREEN}✓${NC} config.json 已存在"
|
||
else
|
||
echo -e " ${RED}✗${NC} 缺少 config.json 与 config.example.json"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# ── 主流程 ──
|
||
main() {
|
||
step0_basics || exit 1
|
||
echo ""
|
||
step1_install_uv || exit 1
|
||
echo ""
|
||
step2_install_python || exit 1
|
||
echo ""
|
||
step3_sync_deps || exit 1
|
||
echo ""
|
||
step4_check_browser
|
||
echo ""
|
||
step5_config_templates || exit 1
|
||
echo ""
|
||
step6_ensure_display
|
||
echo ""
|
||
|
||
# 启动前再确认一次浏览器 env(step4 可能装完后 PATH 变化)
|
||
select_and_export_browser || true
|
||
|
||
echo -e "${CYAN}${BOLD}============================================${NC}"
|
||
echo -e "${GREEN}${BOLD} 环境就绪,启动 register_cli.py${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
|
||
if [ -n "${SELECTED_BROWSER:-}" ]; then
|
||
if [ "${SELECTED_BROWSER_KIND:-}" = "chromium" ]; then
|
||
echo -e " 浏览器: ${GREEN}Chromium${NC} $SELECTED_BROWSER"
|
||
echo -e " 扩展: ${GREEN}turnstilePatch 可加载${NC}"
|
||
else
|
||
echo -e " 浏览器: ${YELLOW}Chrome(无扩展,Turnstile 易失败)${NC} $SELECTED_BROWSER"
|
||
echo -e " 建议: ${YELLOW}sudo apt install -y chromium${NC}"
|
||
fi
|
||
echo -e " env: BROWSER_PREFER=${BROWSER_PREFER:-} BROWSER_PATH=${BROWSER_PATH:-}"
|
||
else
|
||
echo -e " 浏览器: ${RED}未检测到${NC}"
|
||
fi
|
||
echo ""
|
||
|
||
# 默认不限数量(--count 0),除非用户显式传参
|
||
local -a args
|
||
if [ $# -eq 0 ]; then
|
||
echo -e "${YELLOW}[*] 默认模式:不限数量,持续注册(Ctrl+C 停止)${NC}"
|
||
echo ""
|
||
args=(--count 0)
|
||
else
|
||
args=("$@")
|
||
fi
|
||
|
||
# 统一走 uv run,确保用项目 venv + 正确 Python
|
||
# mise 仅作可选加速;有 mise.toml 时先确保工具已装,仍用 uv 启动更稳
|
||
if have_cmd mise && [ -f "$PROJECT_DIR/mise.toml" ]; then
|
||
mise install >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
# 显式把浏览器相关 env 传给子进程,避免 Python 仍选到 Chrome
|
||
export BROWSER_PREFER="${BROWSER_PREFER:-}"
|
||
export BROWSER_PATH="${BROWSER_PATH:-}"
|
||
if [ -n "${CHROMIUM_PATH:-}" ]; then
|
||
export CHROMIUM_PATH
|
||
fi
|
||
|
||
# 组装 env:绝不把空 DISPLAY= 传给子进程。
|
||
# 否则会覆盖 xvfb-run 自动分配的 DISPLAY,Python 侧 has_display=False → 强制 headless,
|
||
# Turnstile 会卡在 token长度=0(参考 grokRegister-cpa 的成功路径是纯 headed + extension)。
|
||
local -a env_args=(
|
||
"BROWSER_PREFER=${BROWSER_PREFER:-}"
|
||
"BROWSER_PATH=${BROWSER_PATH:-}"
|
||
"CHROMIUM_PATH=${CHROMIUM_PATH:-}"
|
||
)
|
||
if [ -n "${DISPLAY:-}" ]; then
|
||
env_args+=("DISPLAY=${DISPLAY}")
|
||
fi
|
||
if [ -n "${XAUTHORITY:-}" ]; then
|
||
env_args+=("XAUTHORITY=${XAUTHORITY}")
|
||
fi
|
||
if [ -n "${WAYLAND_DISPLAY:-}" ]; then
|
||
env_args+=("WAYLAND_DISPLAY=${WAYLAND_DISPLAY}")
|
||
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 -e "${CYAN}[*] 注意: 不向 xvfb-run 子进程注入空 DISPLAY(保留虚拟显示)${NC}"
|
||
echo ""
|
||
exec "${RUN_PREFIX[@]}" env \
|
||
"${env_args[@]}" \
|
||
uv run --python 3.13 python -u register_cli.py "${args[@]}"
|
||
else
|
||
exec env \
|
||
"${env_args[@]}" \
|
||
uv run --python 3.13 python -u register_cli.py "${args[@]}"
|
||
fi
|
||
}
|
||
|
||
main "$@"
|