diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index 4a9e5c6..91c9ce4 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -426,7 +426,7 @@ def retry_auto_register_batch( db: Session = Depends(get_db), current: User = Depends(get_current_user), ): - """从失败/停止/未完成条目续跑,成功项跳过。""" + """继续批次:默认从停止处往下跑(跳过成功与已失败);可传 mode 改行为。""" _require_huya_perm(current, "huya:import") use_proxy = req.use_proxy # 先看历史批次是否用过代理 @@ -439,6 +439,7 @@ def retry_auto_register_batch( runner = huya_register_registry.retry( batch_id, proxy_config=proxy_config, + mode=req.mode, concurrency=req.concurrency, wait_seconds=req.wait_seconds, poll_interval=req.poll_interval, diff --git a/web/backend/schemas.py b/web/backend/schemas.py index 4f28157..2aca665 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -249,7 +249,14 @@ class HuyaAutoRegisterRequest(BaseModel): class HuyaAutoRegisterRetryRequest(BaseModel): - """从失败/停止/未完成条目续跑。可覆盖运行参数。""" + """继续批次。默认 continue=从停止处往下跑(不重试已失败)。 + + mode: + - continue: 只跑 pending/stopped/中断中(默认) + - retry_failed: 只重试 error + - all_unfinished: 失败+未完成都跑 + """ + mode: str = Field("continue", max_length=32) concurrency: Optional[int] = Field(None, ge=1, le=5) wait_seconds: Optional[float] = Field(None, ge=15, le=600) poll_interval: Optional[float] = Field(None, ge=1, le=30) diff --git a/web/backend/services/huya_register_runner.py b/web/backend/services/huya_register_runner.py index a97ecc0..a1696fd 100644 --- a/web/backend/services/huya_register_runner.py +++ b/web/backend/services/huya_register_runner.py @@ -1,4 +1,4 @@ -"""虎牙自动注册批次执行器(支持持久化与失败续跑)。""" +"""虎牙自动注册批次执行器(支持持久化与从停止处继续)。""" from __future__ import annotations @@ -24,10 +24,15 @@ from ..models import ( from .huya_service import upsert_huya_cookie -# 运行中状态:服务中断后视为可续跑 +# 运行中状态:服务中断后视为未完成,可继续 RUNNING_ITEM_STATUSES = frozenset({"sending", "waiting", "changing", "logging"}) -# 可重试状态:失败 / 停止 / 中断后的运行态 / 尚未开始 -RETRYABLE_ITEM_STATUSES = frozenset({"error", "stopped", "pending"} | RUNNING_ITEM_STATUSES) +# 续跑(continue):只跑「还没跑完」的,不重试已明确失败(error)、不重跑成功(success) +# - pending: 队列里还没轮到 +# - stopped: 点停止时尚未开始或被中断 +# - running 中间态: 进程中断时卡在半路 +RESUME_ITEM_STATUSES = frozenset({"pending", "stopped"} | RUNNING_ITEM_STATUSES) +# 仅重试明确失败(可选模式) +RETRY_FAILED_ITEM_STATUSES = frozenset({"error"}) TERMINAL_BATCH_STATUSES = frozenset({"finished", "stopped", "error", "interrupted"}) @@ -216,16 +221,16 @@ def load_batch_snapshot(batch_id: str, *, recover_interrupted: bool = True) -> d if recover_interrupted and batch_row.status in {"running", "pending"}: now = _now() batch_row.status = "interrupted" - batch_row.message = "服务中断或未真正启动,可从失败/未完成项续跑" + batch_row.message = "服务中断或未真正启动,可继续未完成项" batch_row.finished_at = now for item in item_rows: if item.status in RUNNING_ITEM_STATUSES: - item.status = "error" - item.message = "服务中断,可续跑" + # 记为 stopped 而非 error,以便「继续未完成」能接着跑 + item.status = "stopped" + item.message = "服务中断,可继续" item.finished_at = now elif item.status == "pending": - # 保留 pending 供续跑,仅更新提示 - item.message = item.message or "等待续跑" + item.message = item.message or "等待继续" _refresh_batch_counts(batch_row, item_rows) db.commit() db.refresh(batch_row) @@ -259,7 +264,7 @@ def list_batch_summaries(limit: int = 50, live_batch_ids: set[str] | None = None # 无内存 runner 且状态仍是 running/pending,展示为 interrupted if status in {"running", "pending"} and row.batch_id not in live: status = "interrupted" - message = message or "服务中断或未真正启动,可从失败/未完成项续跑" + message = message or "服务中断或未真正启动,可继续未完成项" running_count = 0 if status == "running": running_count = max( @@ -855,6 +860,7 @@ class HuyaRegisterRegistry: batch_id: str, proxy_config: Optional[ProxyConfigModel] = None, *, + mode: str = "continue", concurrency: int | None = None, wait_seconds: float | None = None, poll_interval: float | None = None, @@ -862,12 +868,36 @@ class HuyaRegisterRegistry: fixed_password: str | None = None, use_proxy: bool | None = None, ) -> HuyaRegisterRunner: - """从失败/停止/未完成的条目续跑;成功项跳过。""" + """继续批次。 + + mode: + - continue(默认):从停止处往下跑,只处理 pending/stopped/中断中, + **不重试已失败(error)**,不重跑已成功。 + - retry_failed:只重试 error。 + - all_unfinished:error + pending + stopped + 中断中(旧行为)。 + """ with self._lock: existing = self._runners.get(batch_id) if existing and existing.batch.status == "running": raise RuntimeError("批次正在运行中,请先停止或等待完成") + mode = (mode or "continue").strip().lower() + if mode not in {"continue", "retry_failed", "all_unfinished"}: + raise ValueError("mode 仅支持 continue / retry_failed / all_unfinished") + + if mode == "continue": + target_statuses = RESUME_ITEM_STATUSES + empty_msg = "没有未完成条目可继续(已成功/已失败的不会自动重跑)" + run_label = "继续未完成" + elif mode == "retry_failed": + target_statuses = RETRY_FAILED_ITEM_STATUSES + empty_msg = "没有失败条目可重试" + run_label = "重试失败" + else: + target_statuses = RESUME_ITEM_STATUSES | RETRY_FAILED_ITEM_STATUSES + empty_msg = "没有可继续的未完成/失败条目" + run_label = "续跑未完成" + db = SessionLocal() try: batch_row = ( @@ -878,9 +908,9 @@ class HuyaRegisterRegistry: if not batch_row: raise ValueError("批次不存在") if batch_row.status == "running": - # 无内存 runner 的 running 视为中断,允许续跑 + # 无内存 runner 的 running 视为中断,允许继续 batch_row.status = "interrupted" - batch_row.message = "服务中断,准备续跑" + batch_row.message = "服务中断,准备继续" db.commit() item_rows = ( @@ -908,23 +938,26 @@ class HuyaRegisterRegistry: retry_indices: list[int] = [] for idx, row in enumerate(item_rows): - if (row.status or "pending") in RETRYABLE_ITEM_STATUSES and row.status != "success": - row.status = "pending" - row.message = "等待续跑" - row.finished_at = None - row.started_at = None - row.code = "" - row.change_code = "" - row.attempts = 0 - row.change_attempts = 0 - # 保留历史密码等成功字段不覆盖;失败项本来也没有 - retry_indices.append(idx) + status = row.status or "pending" + if status == "success": + continue + if status not in target_statuses: + continue + row.status = "pending" + row.message = "等待继续" if mode == "continue" else "等待重试" + row.finished_at = None + row.started_at = None + row.code = "" + row.change_code = "" + row.attempts = 0 + row.change_attempts = 0 + retry_indices.append(idx) if not retry_indices: - raise ValueError("没有可续跑的失败/未完成条目") + raise ValueError(empty_msg) batch_row.status = "running" - batch_row.message = f"续跑中({len(retry_indices)} 条)" + batch_row.message = f"{run_label}({len(retry_indices)} 条)" batch_row.started_at = batch_row.started_at or _now() batch_row.finished_at = None _refresh_batch_counts(batch_row, item_rows) @@ -941,7 +974,6 @@ class HuyaRegisterRegistry: if use_proxy is False: proxy_config = None elif batch.use_proxy and proxy_config is None: - # 由调用方传入;若未传则 runner 内会报代理未配置 pass runner = HuyaRegisterRunner( @@ -950,9 +982,8 @@ class HuyaRegisterRegistry: proxy_config=proxy_config if batch.use_proxy else None, item_indices=retry_indices, ) - # 内存侧也保持 running,与 DB 一致 runner.batch.status = "running" - runner.batch.message = f"续跑中({len(retry_indices)} 条)" + runner.batch.message = f"{run_label}({len(retry_indices)} 条)" 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 0780856..5693ef8 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -224,6 +224,8 @@ export interface HuyaAutoRegisterRequest { } export interface HuyaAutoRegisterRetryRequest { + /** continue=从停止处继续(默认,不重试失败);retry_failed=只重试失败;all_unfinished=失败+未完成 */ + mode?: 'continue' | 'retry_failed' | 'all_unfinished'; concurrency?: number; wait_seconds?: number; poll_interval?: number; diff --git a/web/frontend/src/pages/HuyaRegisterPage.tsx b/web/frontend/src/pages/HuyaRegisterPage.tsx index e5acf64..8072075 100644 --- a/web/frontend/src/pages/HuyaRegisterPage.tsx +++ b/web/frontend/src/pages/HuyaRegisterPage.tsx @@ -145,12 +145,34 @@ export default function HuyaRegisterPage() { const batchId = batch?.batch_id || ''; const isRunning = !!batch && batch.status === 'running'; - const hasUnfinished = !!batch && ( - (batch.failed_count || 0) + (batch.stopped_count || 0) > 0 - || (batch.items || []).some((item) => item.status !== 'success') - || ((batch.success_count || 0) < (batch.total || 0)) - ); - const canRetry = !!batch && !isRunning && hasUnfinished + // 默认可「继续」的条目:未开始/已停止/中断中(不含 error、success) + const resumableCount = useMemo(() => { + if (!batch?.items?.length) { + // 摘要无明细时:用 停止数 + (总数-成功-失败-停止) 估算未完成 + const total = batch?.total || 0; + const success = batch?.success_count || 0; + const failed = batch?.failed_count || 0; + const stopped = batch?.stopped_count || 0; + return Math.max(0, stopped + (total - success - failed - stopped)); + } + return batch.items.filter((item) => ( + item.status === 'pending' + || item.status === 'stopped' + || item.status === 'sending' + || item.status === 'waiting' + || item.status === 'changing' + || item.status === 'logging' + )).length; + }, [batch]); + + const failedCount = useMemo(() => { + if (!batch?.items?.length) return batch?.failed_count || 0; + return batch.items.filter((item) => item.status === 'error').length; + }, [batch]); + + const canContinue = !!batch && !isRunning && resumableCount > 0 + && (RETRYABLE_BATCH.has(batch.status) || batch.status === 'pending'); + const canRetryFailed = !!batch && !isRunning && failedCount > 0 && (RETRYABLE_BATCH.has(batch.status) || batch.status === 'pending'); const loadBatchById = useCallback(async (id: string) => { @@ -257,13 +279,6 @@ export default function HuyaRegisterPage() { [batch], ); - const retryableCount = useMemo(() => { - if (!batch?.items?.length) { - return (batch?.failed_count || 0) + (batch?.stopped_count || 0); - } - return batch.items.filter((item) => item.status !== 'success').length; - }, [batch]); - const handleStart = async () => { if (!text.trim()) { message.warning('请先粘贴手机号池'); @@ -309,20 +324,38 @@ export default function HuyaRegisterPage() { } }; - const handleRetry = async () => { + const buildRetryPayload = (mode: 'continue' | 'retry_failed' | 'all_unfinished' = 'continue') => ({ + mode, + concurrency, + wait_seconds: waitSeconds, + poll_interval: pollInterval, + password_prefix: passwordMode === 'random' ? (passwordPrefix.trim() || 'hy') : undefined, + fixed_password: passwordMode === 'fixed' ? fixedPassword.trim() : undefined, + use_proxy: useProxy, + }); + + const handleContinue = async () => { if (!batchId) return; setRetrying(true); try { - const data = await huyaApi.retryAutoRegisterBatch(batchId, { - concurrency, - wait_seconds: waitSeconds, - poll_interval: pollInterval, - password_prefix: passwordMode === 'random' ? (passwordPrefix.trim() || 'hy') : undefined, - fixed_password: passwordMode === 'fixed' ? fixedPassword.trim() : undefined, - use_proxy: useProxy, - }); + const data = await huyaApi.retryAutoRegisterBatch(batchId, buildRetryPayload('continue')); setBatch(data); - message.success(`已开始续跑 ${retryableCount} 条失败/未完成项`); + message.success(`已从停止处继续,处理 ${resumableCount} 条未完成号码(跳过已成功/已失败)`); + loadHistory(); + } catch (e: unknown) { + message.error(getErrorMessage(e)); + } finally { + setRetrying(false); + } + }; + + const handleRetryFailed = async () => { + if (!batchId) return; + setRetrying(true); + try { + const data = await huyaApi.retryAutoRegisterBatch(batchId, buildRetryPayload('retry_failed')); + setBatch(data); + message.success(`已开始重试 ${failedCount} 条失败号码`); loadHistory(); } catch (e: unknown) { message.error(getErrorMessage(e)); @@ -484,13 +517,14 @@ export default function HuyaRegisterPage() { try { setRetrying(true); const data = await huyaApi.retryAutoRegisterBatch(row.batch_id, { + mode: 'continue', concurrency, wait_seconds: waitSeconds, poll_interval: pollInterval, use_proxy: useProxy, }); setBatch(data); - message.success('已开始续跑失败/未完成项'); + message.success('已从停止处继续未完成号码'); loadHistory(); } catch (e: unknown) { message.error(getErrorMessage(e)); @@ -499,7 +533,7 @@ export default function HuyaRegisterPage() { } }} > - 续跑 + 继续 +