diff --git a/web/backend/services/huya_runner.py b/web/backend/services/huya_runner.py index 2db2b83..518f7ba 100644 --- a/web/backend/services/huya_runner.py +++ b/web/backend/services/huya_runner.py @@ -22,6 +22,8 @@ HUYA_RECHARGE_SOURCE_ID = "yellowcarlist" HUYA_RECHARGE_SCENE = 4 HUYA_PAYMENT_POLL_SECONDS = 180 HUYA_PAYMENT_POLL_INTERVAL = 3 +HUYA_BIND_ROLE_POLL_SECONDS = 180 +HUYA_BIND_ROLE_POLL_INTERVAL = 3 HUYA_BIND_ZT_UUID = "b02faae1" HUYA_BIND_ROOM_ID = "30596253" HUYA_RECHARGE_EXTRA_PRODUCTS = [ @@ -255,6 +257,117 @@ class HuyaBatchRunner: f"&roomid={HUYA_BIND_ROOM_ID}" ) + def _wait_bind_role_result( + self, + client: HuyaHttpClient, + worker_db: Session, + task: HuyaTask, + account: HuyaAccount, + uid: int, + cookie: str, + b_act_id_int: int, + result: dict, + ) -> tuple[str, dict]: + """生成二维码后轮询扫码状态与角色同步,直到识别到角色、超时或停止。""" + deadline = time.monotonic() + HUYA_BIND_ROLE_POLL_SECONDS + qrcode_token = str(result.get("qrcode_token") or "") + qrcode_finished = not qrcode_token + result["bind_polling"] = True + self._update_task_progress(worker_db, task, "running", "已生成绑定小程序码,等待扫码绑定", result) + + while not self._stop.is_set() and time.monotonic() < deadline: + if self._stop.wait(HUYA_BIND_ROLE_POLL_INTERVAL): + break + + if qrcode_token and not qrcode_finished: + qrcode_status = client.get_livelink_qrcode_status(qrcode_token, timeout=10.0) + if qrcode_status is not None: + result["qrcode_status"] = qrcode_status + if qrcode_status["is_expired"] or qrcode_status["is_failure"]: + result.update({ + "bind_phase": "qrcode_expired", + "bind_ready_for_confirm": False, + "bind_polling": False, + }) + self._update_task_progress( + worker_db, + task, + "running", + "绑定小程序码已失效,请重新获取", + result, + ) + return "", result + if qrcode_status["is_completed"]: + qrcode_finished = True + result["bind_phase"] = "qrcode_completed" + elif qrcode_status["is_scan"]: + result["bind_phase"] = "qrcode_scanned" + else: + result["bind_phase"] = "waiting_scan" + + bind_status, bind_query_result = self._resolve_bind_status( + client=client, + uid=uid, + cookie=cookie, + b_act_id_int=b_act_id_int, + ) + if bind_status is None: + continue + result.update(bind_query_result) + + if bind_status.status != 200: + result.update({ + "bind_phase": "role_check_failed", + "bind_status": bind_status.to_dict(), + }) + self._update_task_progress( + worker_db, + task, + "running", + bind_status.msg or "等待绑定角色同步", + result, + ) + continue + + previous_phase = result.get("bind_phase") + ready = self._bind_ready_result(bind_status) + # 角色未就绪时保留扫码阶段文案,避免状态来回跳。 + if not ready["role_name"] and previous_phase in { + "waiting_scan", + "qrcode_scanned", + "qrcode_completed", + }: + ready["bind_phase"] = previous_phase + result.update(ready) + result["bind_polling"] = True + role_name = ready["role_name"] + if role_name: + self._apply_role_to_account(account, bind_status, "game_queried") + result["bind_polling"] = False + self._update_task_progress( + worker_db, + task, + "running", + f"已识别角色: {role_name},待确认绑定", + result, + ) + return role_name, result + + if result.get("bind_phase") == "qrcode_completed": + message = "小程序绑定已完成,等待角色同步" + elif result.get("bind_phase") == "qrcode_scanned": + message = "已扫码,等待小程序绑定完成" + else: + message = "已生成绑定小程序码,等待扫码绑定" + self._update_task_progress(worker_db, task, "running", message, result) + + result.update({ + "bind_phase": "role_timeout" if not self._stop.is_set() else "stopped", + "bind_ready_for_confirm": False, + "bind_polling": False, + }) + return "", result + def _mark_task( self, worker_db: Session, @@ -1094,7 +1207,7 @@ class HuyaBatchRunner: **bind_query_result, "bind_phase": "waiting_scan" if mini_qrcode.get("qrcode_token") else "waiting_role", "bind_ready_for_confirm": False, - "bind_polling": False, + "bind_polling": True, "bind_redirect_url": bind_redirect_url, **role_info, **change_state, @@ -1109,7 +1222,29 @@ class HuyaBatchRunner: account.game_channel = self._role_channel(bind_status) or account.game_channel account.nickname = profile_nick or account.nickname account.updated_at = datetime.now(timezone.utc) - self._mark_task(worker_db, task, "success", "已生成绑定小程序码", result) + + # 生成二维码后自动轮询扫码/角色,避免用户必须手动点「查询角色」。 + role_name, result = self._wait_bind_role_result( + client=client, + worker_db=worker_db, + task=task, + account=account, + uid=uid, + cookie=cookie, + b_act_id_int=b_act_id_int, + result=result, + ) + if role_name: + self._mark_task(worker_db, task, "success", f"已识别角色: {role_name},待确认绑定", result) + return + + if result.get("bind_phase") == "stopped": + self._mark_task(worker_db, task, "stopped", "任务已停止", result) + return + if result.get("bind_phase") == "qrcode_expired": + self._mark_task(worker_db, task, "failed", "绑定小程序码已失效,请重新获取", result) + return + self._mark_task(worker_db, task, "success", "已生成绑定小程序码,未检测到绑定角色", result) def _execute_query_game_name( self, diff --git a/web/frontend/src/pages/HuyaTasksPage.tsx b/web/frontend/src/pages/HuyaTasksPage.tsx index df634b1..fc0c59e 100644 --- a/web/frontend/src/pages/HuyaTasksPage.tsx +++ b/web/frontend/src/pages/HuyaTasksPage.tsx @@ -623,8 +623,8 @@ export default function HuyaTasksPage() { : `已选 ${selectedIds.length} 个账号`; const runningTaskBatchId = useMemo(() => { - // 支付监听会长期 running;绑定二维码若仍 running 也算活跃。 - // 但“已生成二维码/已识别角色”这类终态不应再锁 UI。 + // 支付监听会长期 running;绑定二维码自动轮询角色时也算活跃。 + // 仅当 get_bind_qr 已结束轮询但仍残留 running 时,不锁 UI。 const task = tasks.find((item) => { if (!['pending', 'running'].includes(item.status)) return false; if (item.task_type === 'create_recharge_order') { @@ -632,8 +632,10 @@ export default function HuyaTasksPage() { return !status || !['paid', 'timeout', 'stopped'].includes(status); } if (item.task_type === 'get_bind_qr') { - // 旧版轮询中断后可能残留 running,但已有二维码结果;不作为活跃批次。 - if (hasMiniQrcode(item)) return false; + // 正在自动轮询扫码/角色 + if (item.status === 'running' && item.result?.bind_polling === true) return true; + // 有二维码但未标记轮询:可能是历史僵尸,不锁 UI + if (hasMiniQrcode(item) && item.result?.bind_polling !== true) return false; } return true; }); @@ -919,9 +921,10 @@ export default function HuyaTasksPage() { const qrQueryFinished = Boolean(qrQueryTask && qrTask && qrQueryTask.id > qrTask.id && qrQueryTask.status !== 'running'); const qrImage = resultText(qrResult, 'mini_qrcode_image'); const qrBindPhase = resultText(qrResult, 'bind_phase'); + const qrAutoPolling = qrTask?.status === 'running' && qrResult?.bind_polling === true; const qrBindReady = bindReadyForConfirm(qrTask) || Boolean(resultText(qrQueryResult, 'role_name')); const qrRoleSourceResult = resultText(qrQueryResult, 'role_name') ? qrQueryResult : qrResult; - const qrWaitingRole = qrQueryRunning; + const qrWaitingRole = qrAutoPolling || qrQueryRunning; const qrGameTitle = resultText(qrRoleSourceResult, 'game_title'); const qrRoleName = resultText(qrRoleSourceResult, 'role_name'); const qrGameRole = resultObject(qrRoleSourceResult, 'game_role'); @@ -930,19 +933,29 @@ export default function HuyaTasksPage() { const qrRoleLine = qrBindReady ? [qrRolePlat, qrRoleArea, qrRoleName].filter(Boolean).join(' - ') : ''; const qrStatusText = qrBindReady ? '已识别角色,待确认' - : qrQueryRunning - ? '查询角色中' - : qrQueryFinished - ? '未检测到角色' - : qrBindPhase === 'role_timeout' + : qrAutoPolling + ? ( + qrBindPhase === 'qrcode_completed' + ? '等待角色同步' + : qrBindPhase === 'qrcode_scanned' + ? '已扫码' + : qrBindPhase === 'qrcode_expired' + ? '二维码已失效' + : '等待扫码绑定' + ) + : qrQueryRunning + ? '查询角色中' + : qrQueryFinished ? '未检测到角色' - : qrBindPhase === 'qrcode_completed' - ? '等待角色同步' - : qrBindPhase === 'qrcode_scanned' - ? '已扫码' - : qrBindPhase === 'qrcode_expired' - ? '二维码已失效' - : '等待绑定'; + : qrBindPhase === 'role_timeout' + ? '未检测到角色' + : qrBindPhase === 'qrcode_completed' + ? '等待角色同步' + : qrBindPhase === 'qrcode_scanned' + ? '已扫码' + : qrBindPhase === 'qrcode_expired' + ? '二维码已失效' + : '等待绑定'; const qrAccountName = qrTask ? resultProfileNick(qrResult) || qrTask.account_nickname || qrTask.account_uid || `#${qrTask.account_id}` : ''; @@ -986,9 +999,8 @@ export default function HuyaTasksPage() { }; const renderTaskResult = (value: Record | null, record: HuyaTaskItem) => { - const bindQrImage = resultText(value, 'mini_qrcode_image'); - if (bindQrImage) { - if (bindReadyForConfirm(record)) { + if (hasMiniQrcode(record) || resultText(value, 'mini_qrcode_image')) { + if (bindReadyForConfirm(record) || Boolean(resultText(value, 'role_name'))) { const roleName = resultText(value, 'role_name'); return ( @@ -1000,6 +1012,22 @@ export default function HuyaTasksPage() { ); } + if (record.status === 'running' && value?.bind_polling === true) { + const phase = resultText(value, 'bind_phase'); + const phaseText = phase === 'qrcode_scanned' + ? '已扫码' + : phase === 'qrcode_completed' + ? '等待角色' + : '等待扫码'; + return ( + + {phaseText} + + + ); + } return (