完善自动注册代理和状态恢复

This commit is contained in:
yml2213
2026-07-06 02:58:17 +08:00
parent db4ca78280
commit 72e7947a39
7 changed files with 223 additions and 19 deletions
+5 -1
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import threading import threading
import time import time
from collections.abc import Mapping
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
@@ -55,6 +56,7 @@ def register_huya_with_sms_line(
change_password: bool = True, change_password: bool = True,
password_prefix: str = "hy", password_prefix: str = "hy",
fixed_password: str = "", fixed_password: str = "",
proxies: Mapping[str, str] | None = None,
stop_event: threading.Event | None = None, stop_event: threading.Event | None = None,
) -> HuyaAutoRegisterResult: ) -> HuyaAutoRegisterResult:
"""使用固定手机号和接码地址完成虎牙短信注册/登录。""" """使用固定手机号和接码地址完成虎牙短信注册/登录。"""
@@ -76,7 +78,7 @@ def register_huya_with_sms_line(
sent_at = datetime.now() sent_at = datetime.now()
try: try:
code_result = send_huya_sms_code(phone=phone) code_result = send_huya_sms_code(phone=phone, proxies=proxies)
except HuyaLoginError as exc: except HuyaLoginError as exc:
return HuyaAutoRegisterResult( return HuyaAutoRegisterResult(
phone=phone, phone=phone,
@@ -138,6 +140,7 @@ def register_huya_with_sms_line(
authcode=poll_result.code, authcode=poll_result.code,
state=code_result.state, state=code_result.state,
phone=phone, phone=phone,
proxies=proxies,
) )
except HuyaLoginError as exc: except HuyaLoginError as exc:
return HuyaAutoRegisterResult( return HuyaAutoRegisterResult(
@@ -213,6 +216,7 @@ def register_huya_with_sms_line(
wait_seconds=wait_seconds, wait_seconds=wait_seconds,
poll_interval=poll_interval, poll_interval=poll_interval,
ignore_codes={poll_result.code}, ignore_codes={poll_result.code},
proxies=proxies,
stop_event=stop_event, stop_event=stop_event,
) )
if not change_result.success: if not change_result.success:
+2 -1
View File
@@ -458,10 +458,11 @@ def change_huya_password_with_sms_line(
wait_seconds: float = 180, wait_seconds: float = 180,
poll_interval: float = 5, poll_interval: float = 5,
ignore_codes: set[str] | None = None, ignore_codes: set[str] | None = None,
proxies: Mapping[str, str] | None = None,
stop_event: threading.Event | None = None, stop_event: threading.Event | None = None,
) -> HuyaChangePasswordResult: ) -> HuyaChangePasswordResult:
"""使用同一手机号接码链接完成改密短信验证。""" """使用同一手机号接码链接完成改密短信验证。"""
changer = HuyaPasswordChanger(uid=uid, cookie=cookie) changer = HuyaPasswordChanger(uid=uid, cookie=cookie, proxies=proxies)
sent_at = datetime.now() sent_at = datetime.now()
code_result = changer.send_code() code_result = changer.send_code()
if not code_result.success or not code_result.session_data: if not code_result.success or not code_result.session_data:
+5 -1
View File
@@ -23,7 +23,7 @@ from core.sms_provider import parse_sms_lines
from ..database import SessionLocal, get_db from ..database import SessionLocal, get_db
from ..deps import authenticate_websocket, get_current_user, require_permission 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 ..permissions import user_has_permission
from ..schemas import ( from ..schemas import (
AccountAssign, AccountAssign,
@@ -344,6 +344,7 @@ def sms_login_account(
@router.post("/register/batches", response_model=HuyaAutoRegisterBatchOut) @router.post("/register/batches", response_model=HuyaAutoRegisterBatchOut)
def create_auto_register_batch( def create_auto_register_batch(
req: HuyaAutoRegisterRequest, req: HuyaAutoRegisterRequest,
db: Session = Depends(get_db),
current: User = Depends(get_current_user), current: User = Depends(get_current_user),
): ):
"""启动虎牙手机号自动注册批次。""" """启动虎牙手机号自动注册批次。"""
@@ -352,6 +353,7 @@ def create_auto_register_batch(
if not sms_lines: if not sms_lines:
raise HTTPException(status_code=400, detail="没有识别到有效手机号,格式为:手机号----短信查询URL") raise HTTPException(status_code=400, detail="没有识别到有效手机号,格式为:手机号----短信查询URL")
proxy_config = db.query(ProxyConfig).first() if req.use_proxy else None
runner = huya_register_registry.create( runner = huya_register_registry.create(
sms_lines=sms_lines, sms_lines=sms_lines,
tag=req.tag.strip(), tag=req.tag.strip(),
@@ -361,6 +363,8 @@ def create_auto_register_batch(
poll_interval=req.poll_interval, poll_interval=req.poll_interval,
password_prefix=req.password_prefix, password_prefix=req.password_prefix,
fixed_password=req.fixed_password, fixed_password=req.fixed_password,
use_proxy=req.use_proxy,
proxy_config=proxy_config,
) )
thread = threading.Thread(target=runner.run, daemon=True) thread = threading.Thread(target=runner.run, daemon=True)
thread.start() thread.start()
+2
View File
@@ -206,6 +206,7 @@ class HuyaAutoRegisterRequest(BaseModel):
poll_interval: float = Field(5, ge=1, le=30) poll_interval: float = Field(5, ge=1, le=30)
password_prefix: str = Field("hy", max_length=8) password_prefix: str = Field("hy", max_length=8)
fixed_password: str = Field("", max_length=64) fixed_password: str = Field("", max_length=64)
use_proxy: bool = False
class HuyaAutoRegisterItemOut(BaseModel): class HuyaAutoRegisterItemOut(BaseModel):
@@ -240,6 +241,7 @@ class HuyaAutoRegisterBatchOut(BaseModel):
wait_seconds: float wait_seconds: float
poll_interval: float poll_interval: float
password_prefix: str = "hy" password_prefix: str = "hy"
use_proxy: bool = False
total: int total: int
success_count: int success_count: int
failed_count: int failed_count: int
+68 -1
View File
@@ -7,12 +7,15 @@ import uuid
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timezone 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.auto_register import HuyaAutoRegisterResult, register_huya_with_sms_line
from core.huya.cookie_utils import normalize_huya_cookie from core.huya.cookie_utils import normalize_huya_cookie
from core.sms_provider import SmsLine from core.sms_provider import SmsLine
from ..database import SessionLocal from ..database import SessionLocal
from ..models import ProxyConfig as ProxyConfigModel
from .huya_service import upsert_huya_cookie from .huya_service import upsert_huya_cookie
@@ -88,6 +91,7 @@ class HuyaRegisterBatch:
items: list[HuyaRegisterItemState] items: list[HuyaRegisterItemState]
password_prefix: str = "hy" password_prefix: str = "hy"
fixed_password: str = "" fixed_password: str = ""
use_proxy: bool = False
status: str = "pending" status: str = "pending"
message: str = "等待开始" message: str = "等待开始"
created_at: datetime = field(default_factory=_now) created_at: datetime = field(default_factory=_now)
@@ -102,11 +106,39 @@ class HuyaRegisterRunner:
self, self,
batch: HuyaRegisterBatch, batch: HuyaRegisterBatch,
sms_lines: list[SmsLine], sms_lines: list[SmsLine],
proxy_config: Optional[ProxyConfigModel] = None,
): ):
self.batch = batch self.batch = batch
self.sms_lines = sms_lines self.sms_lines = sms_lines
self.proxy_config = proxy_config
self._lock = threading.Lock() self._lock = threading.Lock()
self._stop = threading.Event() 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): def stop(self):
self._stop.set() self._stop.set()
@@ -131,6 +163,7 @@ class HuyaRegisterRunner:
"wait_seconds": self.batch.wait_seconds, "wait_seconds": self.batch.wait_seconds,
"poll_interval": self.batch.poll_interval, "poll_interval": self.batch.poll_interval,
"password_prefix": self.batch.password_prefix, "password_prefix": self.batch.password_prefix,
"use_proxy": self.batch.use_proxy,
"total": total, "total": total,
"success_count": success, "success_count": success,
"failed_count": failed, "failed_count": failed,
@@ -166,11 +199,35 @@ class HuyaRegisterRunner:
finally: finally:
db.close() 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): def _run_one(self, index: int, item: SmsLine):
if self._stop.is_set(): if self._stop.is_set():
self._set_item(index, status="stopped", message="已停止", finished_at=_now()) self._set_item(index, status="stopped", message="已停止", finished_at=_now())
return 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) self._set_item(index, status="sending", message="注册并改密", started_at=_now(), finished_at=None)
result = register_huya_with_sms_line( result = register_huya_with_sms_line(
item, item,
@@ -178,6 +235,7 @@ class HuyaRegisterRunner:
poll_interval=self.batch.poll_interval, poll_interval=self.batch.poll_interval,
password_prefix=self.batch.password_prefix, password_prefix=self.batch.password_prefix,
fixed_password=self.batch.fixed_password, fixed_password=self.batch.fixed_password,
proxies=proxies,
stop_event=self._stop, stop_event=self._stop,
) )
@@ -236,6 +294,12 @@ class HuyaRegisterRunner:
self.batch.started_at = _now() self.batch.started_at = _now()
try: 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: with ThreadPoolExecutor(max_workers=self.batch.concurrency) as executor:
futures = [] futures = []
for index, item in enumerate(self.sms_lines): for index, item in enumerate(self.sms_lines):
@@ -280,6 +344,8 @@ class HuyaRegisterRegistry:
poll_interval: float, poll_interval: float,
password_prefix: str = "hy", password_prefix: str = "hy",
fixed_password: str = "", fixed_password: str = "",
use_proxy: bool = False,
proxy_config: Optional[ProxyConfigModel] = None,
) -> HuyaRegisterRunner: ) -> HuyaRegisterRunner:
batch_id = uuid.uuid4().hex[:12] batch_id = uuid.uuid4().hex[:12]
batch = HuyaRegisterBatch( batch = HuyaRegisterBatch(
@@ -291,12 +357,13 @@ class HuyaRegisterRegistry:
poll_interval=max(1.0, float(poll_interval or 5)), poll_interval=max(1.0, float(poll_interval or 5)),
password_prefix=(password_prefix or "hy").strip()[:8] or "hy", password_prefix=(password_prefix or "hy").strip()[:8] or "hy",
fixed_password=(fixed_password or "").strip(), fixed_password=(fixed_password or "").strip(),
use_proxy=bool(use_proxy),
items=[ items=[
HuyaRegisterItemState(line=index + 1, phone=item.phone, provider=item.provider, sms_url=item.url) HuyaRegisterItemState(line=index + 1, phone=item.phone, provider=item.provider, sms_url=item.url)
for index, item in enumerate(sms_lines) 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: with self._lock:
self._runners[batch_id] = runner self._runners[batch_id] = runner
return runner return runner
+2
View File
@@ -181,6 +181,7 @@ export interface HuyaAutoRegisterRequest {
poll_interval?: number; poll_interval?: number;
password_prefix?: string; password_prefix?: string;
fixed_password?: string; fixed_password?: string;
use_proxy?: boolean;
} }
export interface HuyaAutoRegisterItem { export interface HuyaAutoRegisterItem {
@@ -215,6 +216,7 @@ export interface HuyaAutoRegisterBatch {
wait_seconds: number; wait_seconds: number;
poll_interval: number; poll_interval: number;
password_prefix: string; password_prefix: string;
use_proxy: boolean;
total: number; total: number;
success_count: number; success_count: number;
failed_count: number; failed_count: number;
+139 -15
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { 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'; } from 'antd';
import type { TableProps } from 'antd'; import type { TableProps } from 'antd';
import { DownloadOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons'; import { DownloadOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons';
@@ -11,6 +11,34 @@ import { getErrorMessage } from '../utils/error';
const { TextArea } = Input; const { TextArea } = Input;
const { Text, Title } = Typography; 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<string, string> = { const STATUS_LABELS: Record<string, string> = {
pending: '等待', pending: '等待',
sending: '发码', sending: '发码',
@@ -35,35 +63,118 @@ const STATUS_COLORS: Record<string, string> = {
const RUNNING_STATUS = new Set(['pending', 'running']); 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<StoredRegisterForm>;
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() { export default function HuyaRegisterPage() {
const [text, setText] = useState(''); const initialFormRef = useRef<StoredRegisterForm | null>(null);
const [tag, setTag] = useState(''); if (initialFormRef.current === null) {
const [concurrency, setConcurrency] = useState(1); initialFormRef.current = readStoredForm();
const [waitSeconds, setWaitSeconds] = useState(180); }
const [pollInterval, setPollInterval] = useState(5); const initialForm = initialFormRef.current;
const [passwordMode, setPasswordMode] = useState<'random' | 'fixed'>('random');
const [passwordPrefix, setPasswordPrefix] = useState('hy'); const [text, setText] = useState(initialForm.text);
const [fixedPassword, setFixedPassword] = useState(''); 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<PasswordMode>(initialForm.passwordMode);
const [passwordPrefix, setPasswordPrefix] = useState(initialForm.passwordPrefix);
const [fixedPassword, setFixedPassword] = useState(initialForm.fixedPassword);
const [useProxy, setUseProxy] = useState(initialForm.useProxy);
const [batch, setBatch] = useState<HuyaAutoRegisterBatch | null>(null); const [batch, setBatch] = useState<HuyaAutoRegisterBatch | null>(null);
const [starting, setStarting] = useState(false); const [starting, setStarting] = useState(false);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [stopping, setStopping] = useState(false); const [stopping, setStopping] = useState(false);
const restoredBatchRef = useRef(false);
const batchId = batch?.batch_id || ''; const batchId = batch?.batch_id || '';
const isRunning = !!batch && RUNNING_STATUS.has(batch.status); const isRunning = !!batch && RUNNING_STATUS.has(batch.status);
const refreshBatch = useCallback(async () => { const loadBatchById = useCallback(async (id: string) => {
if (!batchId) return; if (!id) return;
setRefreshing(true); setRefreshing(true);
try { try {
const data = await huyaApi.getAutoRegisterBatch(batchId); const data = await huyaApi.getAutoRegisterBatch(id);
setBatch(data); setBatch(data);
localStorage.setItem(BATCH_STORAGE_KEY, data.batch_id);
} catch (e: unknown) { } 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 { } finally {
setRefreshing(false); 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(() => { useEffect(() => {
if (!isRunning || !batchId) return undefined; if (!isRunning || !batchId) return undefined;
@@ -97,6 +208,7 @@ export default function HuyaRegisterPage() {
poll_interval: pollInterval, poll_interval: pollInterval,
password_prefix: passwordMode === 'random' ? (passwordPrefix.trim() || 'hy') : 'hy', password_prefix: passwordMode === 'random' ? (passwordPrefix.trim() || 'hy') : 'hy',
fixed_password: passwordMode === 'fixed' ? fixedPassword.trim() : '', fixed_password: passwordMode === 'fixed' ? fixedPassword.trim() : '',
use_proxy: useProxy,
}); });
setBatch(data); setBatch(data);
message.success('自动注册批次已启动'); message.success('自动注册批次已启动');
@@ -274,6 +386,18 @@ export default function HuyaRegisterPage() {
)} )}
</Space.Compact> </Space.Compact>
</Col> </Col>
<Col xs={12} md={4}>
<Text type="secondary"></Text>
<div style={{ height: 32, display: 'flex', alignItems: 'center' }}>
<Switch
checked={useProxy}
checkedChildren="开"
unCheckedChildren="关"
onChange={setUseProxy}
disabled={isRunning}
/>
</div>
</Col>
<Col xs={24} md={4}> <Col xs={24} md={4}>
<Space style={{ width: '100%', paddingTop: 22 }}> <Space style={{ width: '100%', paddingTop: 22 }}>
<Button <Button