From 72e7947a396ad945bbdd17873feaaa87d02b8a3f Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 6 Jul 2026 02:58:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=87=AA=E5=8A=A8=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E4=BB=A3=E7=90=86=E5=92=8C=E7=8A=B6=E6=80=81=E6=81=A2?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/huya/auto_register.py | 6 +- core/huya/change_password.py | 3 +- web/backend/routers/huya.py | 6 +- web/backend/schemas.py | 2 + web/backend/services/huya_register_runner.py | 69 ++++++++- web/frontend/src/api/types.ts | 2 + web/frontend/src/pages/HuyaRegisterPage.tsx | 154 +++++++++++++++++-- 7 files changed, 223 insertions(+), 19 deletions(-) diff --git a/core/huya/auto_register.py b/core/huya/auto_register.py index 7a9866c..4c9c0c5 100644 --- a/core/huya/auto_register.py +++ b/core/huya/auto_register.py @@ -4,6 +4,7 @@ from __future__ import annotations import threading import time +from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime @@ -55,6 +56,7 @@ def register_huya_with_sms_line( change_password: bool = True, password_prefix: str = "hy", fixed_password: str = "", + proxies: Mapping[str, str] | None = None, stop_event: threading.Event | None = None, ) -> HuyaAutoRegisterResult: """使用固定手机号和接码地址完成虎牙短信注册/登录。""" @@ -76,7 +78,7 @@ def register_huya_with_sms_line( sent_at = datetime.now() try: - code_result = send_huya_sms_code(phone=phone) + code_result = send_huya_sms_code(phone=phone, proxies=proxies) except HuyaLoginError as exc: return HuyaAutoRegisterResult( phone=phone, @@ -138,6 +140,7 @@ def register_huya_with_sms_line( authcode=poll_result.code, state=code_result.state, phone=phone, + proxies=proxies, ) except HuyaLoginError as exc: return HuyaAutoRegisterResult( @@ -213,6 +216,7 @@ def register_huya_with_sms_line( wait_seconds=wait_seconds, poll_interval=poll_interval, ignore_codes={poll_result.code}, + proxies=proxies, stop_event=stop_event, ) if not change_result.success: diff --git a/core/huya/change_password.py b/core/huya/change_password.py index b1ff2a9..780bbb1 100644 --- a/core/huya/change_password.py +++ b/core/huya/change_password.py @@ -458,10 +458,11 @@ def change_huya_password_with_sms_line( wait_seconds: float = 180, poll_interval: float = 5, ignore_codes: set[str] | None = None, + proxies: Mapping[str, str] | None = None, stop_event: threading.Event | None = None, ) -> HuyaChangePasswordResult: """使用同一手机号接码链接完成改密短信验证。""" - changer = HuyaPasswordChanger(uid=uid, cookie=cookie) + changer = HuyaPasswordChanger(uid=uid, cookie=cookie, proxies=proxies) sent_at = datetime.now() code_result = changer.send_code() if not code_result.success or not code_result.session_data: diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index ee7f148..1a3add3 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -23,7 +23,7 @@ from core.sms_provider import parse_sms_lines from ..database import SessionLocal, get_db from ..deps import authenticate_websocket, get_current_user, require_permission -from ..models import HuyaAccount, HuyaConfig, HuyaGoodsSnapshot, HuyaRechargeGoodsSnapshot, HuyaTask, User +from ..models import HuyaAccount, HuyaConfig, HuyaGoodsSnapshot, HuyaRechargeGoodsSnapshot, HuyaTask, ProxyConfig, User from ..permissions import user_has_permission from ..schemas import ( AccountAssign, @@ -344,6 +344,7 @@ def sms_login_account( @router.post("/register/batches", response_model=HuyaAutoRegisterBatchOut) def create_auto_register_batch( req: HuyaAutoRegisterRequest, + db: Session = Depends(get_db), current: User = Depends(get_current_user), ): """启动虎牙手机号自动注册批次。""" @@ -352,6 +353,7 @@ def create_auto_register_batch( if not sms_lines: raise HTTPException(status_code=400, detail="没有识别到有效手机号,格式为:手机号----短信查询URL") + proxy_config = db.query(ProxyConfig).first() if req.use_proxy else None runner = huya_register_registry.create( sms_lines=sms_lines, tag=req.tag.strip(), @@ -361,6 +363,8 @@ def create_auto_register_batch( poll_interval=req.poll_interval, password_prefix=req.password_prefix, fixed_password=req.fixed_password, + use_proxy=req.use_proxy, + proxy_config=proxy_config, ) thread = threading.Thread(target=runner.run, daemon=True) thread.start() diff --git a/web/backend/schemas.py b/web/backend/schemas.py index 0032f03..5456c2f 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -206,6 +206,7 @@ class HuyaAutoRegisterRequest(BaseModel): poll_interval: float = Field(5, ge=1, le=30) password_prefix: str = Field("hy", max_length=8) fixed_password: str = Field("", max_length=64) + use_proxy: bool = False class HuyaAutoRegisterItemOut(BaseModel): @@ -240,6 +241,7 @@ class HuyaAutoRegisterBatchOut(BaseModel): wait_seconds: float poll_interval: float password_prefix: str = "hy" + use_proxy: bool = False total: int success_count: int failed_count: int diff --git a/web/backend/services/huya_register_runner.py b/web/backend/services/huya_register_runner.py index 53c2c81..5ba06e5 100644 --- a/web/backend/services/huya_register_runner.py +++ b/web/backend/services/huya_register_runner.py @@ -7,12 +7,15 @@ import uuid from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass, field from datetime import datetime, timezone +from typing import Optional +from core.douyu.proxy_fetcher import ProxyFetcher from core.huya.auto_register import HuyaAutoRegisterResult, register_huya_with_sms_line from core.huya.cookie_utils import normalize_huya_cookie from core.sms_provider import SmsLine from ..database import SessionLocal +from ..models import ProxyConfig as ProxyConfigModel from .huya_service import upsert_huya_cookie @@ -88,6 +91,7 @@ class HuyaRegisterBatch: items: list[HuyaRegisterItemState] password_prefix: str = "hy" fixed_password: str = "" + use_proxy: bool = False status: str = "pending" message: str = "等待开始" created_at: datetime = field(default_factory=_now) @@ -102,11 +106,39 @@ class HuyaRegisterRunner: self, batch: HuyaRegisterBatch, sms_lines: list[SmsLine], + proxy_config: Optional[ProxyConfigModel] = None, ): self.batch = batch self.sms_lines = sms_lines + self.proxy_config = proxy_config self._lock = threading.Lock() self._stop = threading.Event() + self._shared_proxy_fetcher = self._create_proxy_fetcher() + + def _create_proxy_fetcher(self) -> ProxyFetcher | None: + """按需创建 API 代理获取器。""" + if not self.batch.use_proxy or not self.proxy_config or not self.proxy_config.enabled: + return None + if not self.proxy_config.api_url: + return None + + wl_platform = "xiequ" + wl_credentials = None + if self.proxy_config.whitelist_enabled: + wl_platform = getattr(self.proxy_config, "whitelist_platform", None) or "xiequ" + wl_credentials = getattr(self.proxy_config, "whitelist_credentials", None) + if not wl_credentials and self.proxy_config.whitelist_uid and self.proxy_config.whitelist_ukey: + wl_credentials = { + "uid": self.proxy_config.whitelist_uid, + "ukey": self.proxy_config.whitelist_ukey, + } + + return ProxyFetcher( + api_url=self.proxy_config.api_url, + whitelist_platform=wl_platform, + whitelist_credentials=wl_credentials, + stop_event=self._stop, + ) def stop(self): self._stop.set() @@ -131,6 +163,7 @@ class HuyaRegisterRunner: "wait_seconds": self.batch.wait_seconds, "poll_interval": self.batch.poll_interval, "password_prefix": self.batch.password_prefix, + "use_proxy": self.batch.use_proxy, "total": total, "success_count": success, "failed_count": failed, @@ -166,11 +199,35 @@ class HuyaRegisterRunner: finally: db.close() + def _resolve_proxy(self) -> tuple[dict[str, str] | None, str]: + """为单个手机号解析代理;返回代理字典和错误消息。""" + if not self.batch.use_proxy: + return None, "" + if not self.proxy_config or not self.proxy_config.enabled: + return None, "已开启代理,但代理配置未启用" + + if self.proxy_config.http or self.proxy_config.https: + proxy_url = self.proxy_config.http or self.proxy_config.https + return {"http": proxy_url, "https": proxy_url}, "" + + if self._shared_proxy_fetcher: + proxy_url = self._shared_proxy_fetcher.fetch_new_proxy(max_attempts=3) + if proxy_url: + return {"http": proxy_url, "https": proxy_url}, "" + return None, "获取代理失败" + + return None, "已开启代理,但未配置静态代理或代理 API" + def _run_one(self, index: int, item: SmsLine): if self._stop.is_set(): self._set_item(index, status="stopped", message="已停止", finished_at=_now()) return + proxies, proxy_error = self._resolve_proxy() + if proxy_error: + self._set_item(index, status="error", message=proxy_error, finished_at=_now()) + return + self._set_item(index, status="sending", message="注册并改密", started_at=_now(), finished_at=None) result = register_huya_with_sms_line( item, @@ -178,6 +235,7 @@ class HuyaRegisterRunner: poll_interval=self.batch.poll_interval, password_prefix=self.batch.password_prefix, fixed_password=self.batch.fixed_password, + proxies=proxies, stop_event=self._stop, ) @@ -236,6 +294,12 @@ class HuyaRegisterRunner: self.batch.started_at = _now() try: + if self._shared_proxy_fetcher: + ok, msg = self._shared_proxy_fetcher.warmup_whitelist() + if not ok: + with self._lock: + self.batch.message = f"代理白名单预热失败: {msg}" + with ThreadPoolExecutor(max_workers=self.batch.concurrency) as executor: futures = [] for index, item in enumerate(self.sms_lines): @@ -280,6 +344,8 @@ class HuyaRegisterRegistry: poll_interval: float, password_prefix: str = "hy", fixed_password: str = "", + use_proxy: bool = False, + proxy_config: Optional[ProxyConfigModel] = None, ) -> HuyaRegisterRunner: batch_id = uuid.uuid4().hex[:12] batch = HuyaRegisterBatch( @@ -291,12 +357,13 @@ class HuyaRegisterRegistry: poll_interval=max(1.0, float(poll_interval or 5)), password_prefix=(password_prefix or "hy").strip()[:8] or "hy", fixed_password=(fixed_password or "").strip(), + use_proxy=bool(use_proxy), items=[ HuyaRegisterItemState(line=index + 1, phone=item.phone, provider=item.provider, sms_url=item.url) for index, item in enumerate(sms_lines) ], ) - runner = HuyaRegisterRunner(batch=batch, sms_lines=sms_lines) + runner = HuyaRegisterRunner(batch=batch, sms_lines=sms_lines, proxy_config=proxy_config) with self._lock: self._runners[batch_id] = runner return runner diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index cd2ef06..20fa159 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -181,6 +181,7 @@ export interface HuyaAutoRegisterRequest { poll_interval?: number; password_prefix?: string; fixed_password?: string; + use_proxy?: boolean; } export interface HuyaAutoRegisterItem { @@ -215,6 +216,7 @@ export interface HuyaAutoRegisterBatch { wait_seconds: number; poll_interval: number; password_prefix: string; + use_proxy: boolean; total: number; success_count: number; failed_count: number; diff --git a/web/frontend/src/pages/HuyaRegisterPage.tsx b/web/frontend/src/pages/HuyaRegisterPage.tsx index be0048b..b351b7d 100644 --- a/web/frontend/src/pages/HuyaRegisterPage.tsx +++ b/web/frontend/src/pages/HuyaRegisterPage.tsx @@ -1,6 +1,6 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { - Button, Card, Col, Input, InputNumber, message, Row, Segmented, Space, Statistic, Table, Tag, Typography, + Button, Card, Col, Input, InputNumber, message, Row, Segmented, Space, Statistic, Switch, Table, Tag, Typography, } from 'antd'; import type { TableProps } from 'antd'; import { DownloadOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons'; @@ -11,6 +11,34 @@ import { getErrorMessage } from '../utils/error'; const { TextArea } = Input; const { Text, Title } = Typography; +type PasswordMode = 'random' | 'fixed'; + +interface StoredRegisterForm { + text: string; + tag: string; + concurrency: number; + waitSeconds: number; + pollInterval: number; + passwordMode: PasswordMode; + passwordPrefix: string; + fixedPassword: string; + useProxy: boolean; +} + +const FORM_STORAGE_KEY = 'huya_auto_register_form'; +const BATCH_STORAGE_KEY = 'huya_auto_register_batch_id'; +const DEFAULT_FORM: StoredRegisterForm = { + text: '', + tag: '', + concurrency: 1, + waitSeconds: 180, + pollInterval: 5, + passwordMode: 'random', + passwordPrefix: 'hy', + fixedPassword: '', + useProxy: false, +}; + const STATUS_LABELS: Record = { pending: '等待', sending: '发码', @@ -35,35 +63,118 @@ const STATUS_COLORS: Record = { const RUNNING_STATUS = new Set(['pending', 'running']); +function safeNumber(value: unknown, fallback: number) { + const n = Number(value); + return Number.isFinite(n) ? n : fallback; +} + +function readStoredForm(): StoredRegisterForm { + try { + const raw = localStorage.getItem(FORM_STORAGE_KEY); + if (!raw) return DEFAULT_FORM; + const parsed = JSON.parse(raw) as Partial; + const passwordMode = parsed.passwordMode === 'fixed' ? 'fixed' : 'random'; + return { + ...DEFAULT_FORM, + ...parsed, + concurrency: safeNumber(parsed.concurrency, DEFAULT_FORM.concurrency), + waitSeconds: safeNumber(parsed.waitSeconds, DEFAULT_FORM.waitSeconds), + pollInterval: safeNumber(parsed.pollInterval, DEFAULT_FORM.pollInterval), + passwordMode, + useProxy: Boolean(parsed.useProxy), + }; + } catch { + return DEFAULT_FORM; + } +} + +function readStoredBatchId() { + try { + return localStorage.getItem(BATCH_STORAGE_KEY) || ''; + } catch { + return ''; + } +} + export default function HuyaRegisterPage() { - const [text, setText] = useState(''); - const [tag, setTag] = useState(''); - const [concurrency, setConcurrency] = useState(1); - const [waitSeconds, setWaitSeconds] = useState(180); - const [pollInterval, setPollInterval] = useState(5); - const [passwordMode, setPasswordMode] = useState<'random' | 'fixed'>('random'); - const [passwordPrefix, setPasswordPrefix] = useState('hy'); - const [fixedPassword, setFixedPassword] = useState(''); + const initialFormRef = useRef(null); + if (initialFormRef.current === null) { + initialFormRef.current = readStoredForm(); + } + const initialForm = initialFormRef.current; + + const [text, setText] = useState(initialForm.text); + const [tag, setTag] = useState(initialForm.tag); + const [concurrency, setConcurrency] = useState(initialForm.concurrency); + const [waitSeconds, setWaitSeconds] = useState(initialForm.waitSeconds); + const [pollInterval, setPollInterval] = useState(initialForm.pollInterval); + const [passwordMode, setPasswordMode] = useState(initialForm.passwordMode); + const [passwordPrefix, setPasswordPrefix] = useState(initialForm.passwordPrefix); + const [fixedPassword, setFixedPassword] = useState(initialForm.fixedPassword); + const [useProxy, setUseProxy] = useState(initialForm.useProxy); const [batch, setBatch] = useState(null); const [starting, setStarting] = useState(false); const [refreshing, setRefreshing] = useState(false); const [stopping, setStopping] = useState(false); + const restoredBatchRef = useRef(false); const batchId = batch?.batch_id || ''; const isRunning = !!batch && RUNNING_STATUS.has(batch.status); - const refreshBatch = useCallback(async () => { - if (!batchId) return; + const loadBatchById = useCallback(async (id: string) => { + if (!id) return; setRefreshing(true); try { - const data = await huyaApi.getAutoRegisterBatch(batchId); + const data = await huyaApi.getAutoRegisterBatch(id); setBatch(data); + localStorage.setItem(BATCH_STORAGE_KEY, data.batch_id); } catch (e: unknown) { - message.error(getErrorMessage(e)); + const err = getErrorMessage(e); + if (err.includes('批次不存在') || err.includes('服务已重启')) { + localStorage.removeItem(BATCH_STORAGE_KEY); + setBatch(null); + message.warning('上次自动注册批次已不存在,已清除恢复记录'); + } else { + message.error(err); + } } finally { setRefreshing(false); } - }, [batchId]); + }, []); + + const refreshBatch = useCallback(async () => { + if (!batchId) return; + await loadBatchById(batchId); + }, [batchId, loadBatchById]); + + useEffect(() => { + localStorage.setItem(FORM_STORAGE_KEY, JSON.stringify({ + text, + tag, + concurrency, + waitSeconds, + pollInterval, + passwordMode, + passwordPrefix, + fixedPassword, + useProxy, + })); + }, [text, tag, concurrency, waitSeconds, pollInterval, passwordMode, passwordPrefix, fixedPassword, useProxy]); + + useEffect(() => { + if (restoredBatchRef.current) return; + restoredBatchRef.current = true; + const storedBatchId = readStoredBatchId(); + if (storedBatchId) { + loadBatchById(storedBatchId); + } + }, [loadBatchById]); + + useEffect(() => { + if (batch?.batch_id) { + localStorage.setItem(BATCH_STORAGE_KEY, batch.batch_id); + } + }, [batch?.batch_id]); useEffect(() => { if (!isRunning || !batchId) return undefined; @@ -97,6 +208,7 @@ export default function HuyaRegisterPage() { poll_interval: pollInterval, password_prefix: passwordMode === 'random' ? (passwordPrefix.trim() || 'hy') : 'hy', fixed_password: passwordMode === 'fixed' ? fixedPassword.trim() : '', + use_proxy: useProxy, }); setBatch(data); message.success('自动注册批次已启动'); @@ -274,6 +386,18 @@ export default function HuyaRegisterPage() { )} + + 代理 +
+ +
+