diff --git a/.gitignore b/.gitignore index 02677dc..498fe7f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ __pycache__/ */__pycache__ *.pyc +.claude +.vscode + # 环境变量(含敏感信息,勿提交) .env diff --git a/start_web.sh b/start_web.sh index 2c2f537..25b0942 100755 --- a/start_web.sh +++ b/start_web.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -e +set -eo pipefail ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$ROOT_DIR" @@ -8,20 +8,38 @@ PYTHON="$ROOT_DIR/.venv/bin/python" FRONTEND_DIR="$ROOT_DIR/web/frontend" BACKEND_PORT=8000 FRONTEND_PORT=5173 +HEALTH_TIMEOUT=30 # 后端健康检查最长等待秒数 + +# ── 工具函数 ────────────────────────────────────────────── + +log() { echo "[$(date '+%H:%M:%S')] $*"; } +warn() { echo "[$(date '+%H:%M:%S')] ⚠️ $*" >&2; } +die() { echo "[$(date '+%H:%M:%S')] ❌ $*" >&2; exit 1; } + +check_port() { + if lsof -iTCP:"$1" -sTCP:LISTEN -P &>/dev/null; then + die "端口 $1 已被占用,请先释放或更改端口号(lsof -i :$1)" + fi +} + +# ── 预检查 ──────────────────────────────────────────────── + +check_port "$BACKEND_PORT" +check_port "$FRONTEND_PORT" -# 检查 Python 虚拟环境 if [ ! -f "$PYTHON" ]; then - echo "❌ 未找到 .venv,正在创建..." + log "未找到 .venv,正在创建..." uv venv .venv uv pip install -e . fi -# 检查前端依赖 if [ ! -d "$FRONTEND_DIR/node_modules" ]; then - echo "❌ 前端依赖未安装,正在安装..." - cd "$FRONTEND_DIR" && npm install && cd "$ROOT_DIR" + log "前端依赖未安装,正在安装..." + (cd "$FRONTEND_DIR" && npm install) fi +# ── 启动信息 ────────────────────────────────────────────── + echo "" echo "==============================" echo " 斗鱼批量登录 - Web 后台" @@ -33,34 +51,58 @@ echo " 按 Ctrl+C 停止所有服务" echo "==============================" echo "" -# 清理子进程 +# ── 清理子进程 ──────────────────────────────────────────── + +PIDS=() + cleanup() { echo "" - echo "正在停止服务..." - kill $BACKEND_PID $FRONTEND_PID 2>/dev/null - wait $BACKEND_PID $FRONTEND_PID 2>/dev/null - echo "已停止" + log "正在停止服务..." + for pid in "${PIDS[@]}"; do + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null + fi + done + for pid in "${PIDS[@]}"; do + wait "$pid" 2>/dev/null + done + log "已停止" } trap cleanup EXIT INT TERM -# 启动后端 -echo "启动后端 (FastAPI :$BACKEND_PORT)..." +# ── 启动后端 ────────────────────────────────────────────── + +log "启动后端 (FastAPI :$BACKEND_PORT)..." RELOAD_FLAG="" if [ "${UVICORN_RELOAD:-true}" = "true" ]; then RELOAD_FLAG="--reload" fi -"$PYTHON" -m uvicorn web.backend.main:app --host 0.0.0.0 --port $BACKEND_PORT $RELOAD_FLAG & -BACKEND_PID=$! +"$PYTHON" -m uvicorn web.backend.main:app --host 0.0.0.0 --port "$BACKEND_PORT" $RELOAD_FLAG & +PIDS+=($!) -# 等后端就绪 -sleep 2 +# 等后端就绪(健康检查循环替代固定 sleep) +log "等待后端就绪(最多 ${HEALTH_TIMEOUT}s)..." +elapsed=0 +while [ "$elapsed" -lt "$HEALTH_TIMEOUT" ]; do + if curl -sf "http://localhost:$BACKEND_PORT/docs" -o /dev/null 2>/dev/null; then + log "后端已就绪 ✓" + break + fi + sleep 1 + elapsed=$((elapsed + 1)) +done +if [ "$elapsed" -ge "$HEALTH_TIMEOUT" ]; then + warn "后端未在 ${HEALTH_TIMEOUT}s 内就绪,前端可能无法正常连接" +fi -# 启动前端 -echo "启动前端 (Vite :$FRONTEND_PORT)..." +# ── 启动前端 ────────────────────────────────────────────── + +log "启动前端 (Vite :$FRONTEND_PORT)..." cd "$FRONTEND_DIR" -npm run dev -- --port $FRONTEND_PORT --host 0.0.0.0 & -FRONTEND_PID=$! +npm run dev -- --port "$FRONTEND_PORT" --host 0.0.0.0 & +PIDS+=($!) cd "$ROOT_DIR" -# 等待子进程 +# ── 等待子进程 ──────────────────────────────────────────── + wait diff --git a/web/frontend/src/pages/LoginTasksPage.tsx b/web/frontend/src/pages/LoginTasksPage.tsx index db4f4f8..6856c8b 100644 --- a/web/frontend/src/pages/LoginTasksPage.tsx +++ b/web/frontend/src/pages/LoginTasksPage.tsx @@ -38,8 +38,18 @@ export default function LoginTasksPage() { const [loading, setLoading] = useState(false); const [batchId, setBatchId] = useState(null); const [selectedTags, setSelectedTags] = useState([]); - const [concurrency, setConcurrency] = useState(3); - const [maxProxyRetries, setMaxProxyRetries] = useState(10); + const [concurrency, setConcurrency] = useState(() => { + const v = localStorage.getItem('login_concurrency'); + return v ? Math.max(1, Math.min(10, Number(v) || 3)) : 3; + }); + const [maxProxyRetries, setMaxProxyRetries] = useState(() => { + const v = localStorage.getItem('login_max_proxy_retries'); + return v ? Math.max(0, Number(v) || 10) : 10; + }); + + // 值变化时自动持久化 + useEffect(() => { localStorage.setItem('login_concurrency', String(concurrency)); }, [concurrency]); + useEffect(() => { localStorage.setItem('login_max_proxy_retries', String(maxProxyRetries)); }, [maxProxyRetries]); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const { logs, connected: wsConnected, connect: connectLogs } = useWebSocketLogs(); const { can } = usePermissions();