fix(huya): 精英宝典绑定二维码对齐最新抓包并支持配置 bind_gid

This commit is contained in:
yml2213
2026-11-09 21:01:42 +08:00
parent 7282787398
commit 5f1d78e1e4
16 changed files with 446 additions and 80 deletions
+124 -47
View File
@@ -101,6 +101,41 @@ class BindMixin:
"bind_phase": "role_ready" if role_info["role_name"] else "waiting_role",
}
@staticmethod
def _bind_protocol(config_info: dict, payload: dict | None = None) -> dict:
"""读取绑定协议参数;有 gid 时用 2026-11 抓包协议,否则回退旧协议。"""
payload = payload or {}
bind_gid_text = str(
payload.get("bind_gid") or config_info.get("bind_gid") or ""
).strip()
bind_gid = HuyaBatchRunnerCore._to_int(bind_gid_text)
outer_act_id = str(
payload.get("outer_act_id")
or config_info.get("outer_act_id")
or HUYA_DEFAULT_OUTER_ACT_ID
).strip()
if bind_gid and outer_act_id:
return {
"bind_gid": bind_gid_text,
"b_act_id": 0,
"gid": bind_gid,
"outer_act_id": outer_act_id,
"is_use_outer_act_id": 0,
"scene": "undefined",
}
b_act_id_text = str(
payload.get("bind_act_id") or config_info.get("bind_act_id") or ""
).strip()
return {
"bind_gid": "",
"b_act_id": HuyaBatchRunnerCore._to_int(b_act_id_text),
"gid": 0,
# 旧协议最初不传 outerActId;需要旧路径的地方按原逻辑显式补默认值。
"outer_act_id": "",
"is_use_outer_act_id": 1,
"scene": "",
}
def _resolve_bind_status(
self,
client: Any,
@@ -110,16 +145,20 @@ class BindMixin:
*,
outer_act_id: str = "",
timeout: float | None = None,
gid: int = 0,
is_use_outer_act_id: int = 1,
scene: str = "",
):
"""按活动页逻辑解析绑定状态,优先返回含角色的状态。"""
probe_kwargs: dict[str, Any] = {
"uid": uid,
"cookie": cookie,
"b_act_id": b_act_id_int,
"is_use_outer_act_id": 1,
"gid": gid,
"outer_act_id": outer_act_id,
"is_use_outer_act_id": is_use_outer_act_id,
"scene": scene,
}
if outer_act_id:
probe_kwargs["outer_act_id"] = outer_act_id
if timeout is not None:
probe_kwargs["timeout"] = timeout
outer_status = client.check_user_bind_game_account(**probe_kwargs)
@@ -135,10 +174,16 @@ class BindMixin:
chosen_status = outer_status
account_data = outer_status.accountData
should_check_inner = not self._has_bind_role(outer_status) and bool(
account_data.isNeedActCheck
or not account_data.isBindAcount
or not account_data.isBindRole
# 最新抓包协议只用外层查询(gid + outerActId)。旧的内层兜底会带上
# gid=0/outerActId=空,被服务端判 501「参数错误」,仅在旧协议下保留。
should_check_inner = (
not gid
and not self._has_bind_role(outer_status)
and bool(
account_data.isNeedActCheck
or not account_data.isBindAcount
or not account_data.isBindRole
)
)
if should_check_inner:
inner_status = client.check_user_bind_game_account(
@@ -162,6 +207,9 @@ class BindMixin:
cookie: str,
b_act_id_int: int,
outer_act_id: str = "",
gid: int = 0,
is_use_outer_act_id: int = 1,
scene: str = "",
):
"""用外部单协作的新 CK 探针查角色。
@@ -177,6 +225,9 @@ class BindMixin:
b_act_id_int,
outer_act_id=outer_act_id or HUYA_DEFAULT_OUTER_ACT_ID,
timeout=HUYA_CK_PROBE_TIMEOUT_SECONDS,
gid=gid,
is_use_outer_act_id=is_use_outer_act_id,
scene=scene,
)
def _apply_role_to_account(self, account: HuyaAccount, bind_status, status: str):
@@ -209,6 +260,11 @@ class BindMixin:
cookie: str,
b_act_id_int: int,
result: dict,
*,
gid: int = 0,
outer_act_id: str = "",
is_use_outer_act_id: int = 1,
scene: str = "",
) -> tuple[str, dict]:
"""生成二维码后轮询扫码状态与角色同步,直到识别到角色、超时或停止。"""
deadline = time.monotonic() + HUYA_BIND_ROLE_POLL_SECONDS
@@ -258,6 +314,10 @@ class BindMixin:
uid=uid,
cookie=cookie,
b_act_id_int=b_act_id_int,
outer_act_id=outer_act_id,
gid=gid,
is_use_outer_act_id=is_use_outer_act_id,
scene=scene,
)
if bind_status is None:
continue
@@ -328,20 +388,12 @@ class BindMixin:
account_info: dict,
config_info: dict,
):
b_act_id = str(
self.payload.get("bind_act_id") or config_info.get("bind_act_id") or ""
).strip()
if not b_act_id:
protocol = self._bind_protocol(config_info, self.payload)
b_act_id_int = protocol["b_act_id"]
if not protocol["bind_gid"] and not b_act_id_int:
self._mark_task(worker_db, task, "failed", "请先配置虎牙绑定 bActId")
return
b_act_id_int = self._to_int(b_act_id)
if not b_act_id_int:
self._mark_task(
worker_db, task, "failed", f"虎牙绑定 bActId 无效: {b_act_id}"
)
return
uid = self._resolve_uid(account_info)
if not uid:
self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid")
@@ -358,6 +410,10 @@ class BindMixin:
uid=uid,
cookie=cookie,
b_act_id_int=b_act_id_int,
outer_act_id=protocol["outer_act_id"],
gid=protocol["gid"],
is_use_outer_act_id=protocol["is_use_outer_act_id"],
scene=protocol["scene"],
)
if bind_status is None:
self._mark_task(worker_db, task, "error", "虎牙绑定状态接口无响应")
@@ -383,6 +439,8 @@ class BindMixin:
available_at = change_state["change_available_at"]
result = {
"bind_act_id": b_act_id_int,
"bind_gid": protocol["bind_gid"],
"gid": protocol["gid"],
**role_info,
**change_state,
"bind_status": bind_status.to_dict(),
@@ -400,6 +458,8 @@ class BindMixin:
uid=uid,
cookie=cookie,
b_act_id=b_act_id_int,
gid=protocol["gid"],
outer_act_id=int(protocol["outer_act_id"] or 0),
game_auth_scene=bind_status.gameAuthScene,
)
if live_link is None:
@@ -450,6 +510,8 @@ class BindMixin:
result = {
"bind_act_id": b_act_id_int,
"bind_gid": protocol["bind_gid"],
"gid": protocol["gid"],
"mini_qrcode_image": mini_qrcode["mini_qrcode_image"],
"qrcode_token": mini_qrcode.get("qrcode_token") or "",
**bind_query_result,
@@ -483,6 +545,10 @@ class BindMixin:
cookie=cookie,
b_act_id_int=b_act_id_int,
result=result,
gid=protocol["gid"],
outer_act_id=protocol["outer_act_id"],
is_use_outer_act_id=protocol["is_use_outer_act_id"],
scene=protocol["scene"],
)
if role_name:
self._mark_task(
@@ -514,20 +580,12 @@ class BindMixin:
account_info: dict,
config_info: dict,
):
b_act_id = str(
self.payload.get("bind_act_id") or config_info.get("bind_act_id") or ""
).strip()
if not b_act_id:
protocol = self._bind_protocol(config_info, self.payload)
b_act_id_int = protocol["b_act_id"]
if not protocol["bind_gid"] and not b_act_id_int:
self._mark_task(worker_db, task, "failed", "请先配置虎牙绑定 bActId")
return
b_act_id_int = self._to_int(b_act_id)
if not b_act_id_int:
self._mark_task(
worker_db, task, "failed", f"虎牙绑定 bActId 无效: {b_act_id}"
)
return
uid = self._resolve_uid(account_info)
if not uid:
self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid")
@@ -538,16 +596,14 @@ class BindMixin:
self._mark_task(worker_db, task, "failed", "账号 Cookie 为空")
return
outer_act_id = str(
self.payload.get("outer_act_id")
or config_info.get("outer_act_id")
or HUYA_DEFAULT_OUTER_ACT_ID
).strip()
bind_status, bind_query_result = self._probe_bind_status(
uid=uid,
cookie=cookie,
b_act_id_int=b_act_id_int,
outer_act_id=outer_act_id,
outer_act_id=protocol["outer_act_id"],
gid=protocol["gid"],
is_use_outer_act_id=protocol["is_use_outer_act_id"],
scene=protocol["scene"],
)
if bind_status is None:
# 新探针没有回包 = 这份 CK 已经不认了,和外部单协作的判定一致。
@@ -560,6 +616,8 @@ class BindMixin:
"虎牙角色信息接口无响应(CK 已失效,请重新登录或重新导入 Cookie)",
{
"bind_act_id": b_act_id_int,
"bind_gid": protocol["bind_gid"],
"gid": protocol["gid"],
"ck_expired": True,
"ck_expired_reason": "角色信息接口无响应",
},
@@ -572,7 +630,12 @@ class BindMixin:
task,
"failed",
bind_status.msg or f"虎牙角色信息查询失败: {bind_status.status}",
{"bind_act_id": b_act_id_int, **bind_query_result},
{
"bind_act_id": b_act_id_int,
"bind_gid": protocol["bind_gid"],
"gid": protocol["gid"],
**bind_query_result,
},
)
return
@@ -580,6 +643,8 @@ class BindMixin:
change_state = self._bind_change_state(bind_status)
result = {
"bind_act_id": b_act_id_int,
"bind_gid": protocol["bind_gid"],
"gid": protocol["gid"],
**role_info,
**change_state,
**bind_query_result,
@@ -604,20 +669,12 @@ class BindMixin:
account_info: dict,
config_info: dict,
):
b_act_id = str(
self.payload.get("bind_act_id") or config_info.get("bind_act_id") or ""
).strip()
if not b_act_id:
protocol = self._bind_protocol(config_info, self.payload)
b_act_id_int = protocol["b_act_id"]
if not protocol["bind_gid"] and not b_act_id_int:
self._mark_task(worker_db, task, "failed", "请先配置虎牙绑定 bActId")
return
b_act_id_int = self._to_int(b_act_id)
if not b_act_id_int:
self._mark_task(
worker_db, task, "failed", f"虎牙绑定 bActId 无效: {b_act_id}"
)
return
uid = self._resolve_uid(account_info)
if not uid:
self._mark_task(worker_db, task, "failed", "无法从账号或 Cookie 解析 yyuid")
@@ -634,6 +691,10 @@ class BindMixin:
uid=uid,
cookie=cookie,
b_act_id_int=b_act_id_int,
outer_act_id=protocol["outer_act_id"],
gid=protocol["gid"],
is_use_outer_act_id=protocol["is_use_outer_act_id"],
scene=protocol["scene"],
)
if role_status is None:
self._mark_task(worker_db, task, "error", "虎牙绑定角色查询接口无响应")
@@ -653,6 +714,8 @@ class BindMixin:
if not role_name:
result = {
"bind_act_id": b_act_id_int,
"bind_gid": protocol["bind_gid"],
"gid": protocol["gid"],
"bind_confirmed": False,
**role_query_result,
}
@@ -669,6 +732,8 @@ class BindMixin:
uid=uid,
cookie=cookie,
b_act_id=b_act_id_int,
gid=protocol["gid"],
outer_act_id=protocol["outer_act_id"],
)
if confirm_resp is None:
self._mark_task(worker_db, task, "error", "虎牙确认绑定接口无响应")
@@ -676,6 +741,8 @@ class BindMixin:
if confirm_resp.status != 200:
result = {
"bind_act_id": b_act_id_int,
"bind_gid": protocol["bind_gid"],
"gid": protocol["gid"],
"bind_confirmed": False,
"confirm_result": confirm_resp.to_dict(),
"before_bind_status": role_status.to_dict(),
@@ -695,10 +762,16 @@ class BindMixin:
uid=uid,
cookie=cookie,
b_act_id_int=b_act_id_int,
outer_act_id=protocol["outer_act_id"],
gid=protocol["gid"],
is_use_outer_act_id=protocol["is_use_outer_act_id"],
scene=protocol["scene"],
)
if refreshed_status is None:
result = {
"bind_act_id": b_act_id_int,
"bind_gid": protocol["bind_gid"],
"gid": protocol["gid"],
"bind_confirmed": True,
"confirm_result": confirm_resp.to_dict(),
"before_bind_status": role_status.to_dict(),
@@ -713,6 +786,8 @@ class BindMixin:
if refreshed_status.status != 200:
result = {
"bind_act_id": b_act_id_int,
"bind_gid": protocol["bind_gid"],
"gid": protocol["gid"],
"bind_confirmed": True,
"confirm_result": confirm_resp.to_dict(),
"before_bind_status": role_status.to_dict(),
@@ -739,6 +814,8 @@ class BindMixin:
)
result = {
"bind_act_id": b_act_id_int,
"bind_gid": protocol["bind_gid"],
"gid": protocol["gid"],
"bind_confirmed": True,
"refreshed_is_bound": refreshed_confirmed,
"confirm_result": confirm_resp.to_dict(),
+5 -1
View File
@@ -228,7 +228,11 @@ class HuyaBatchRunnerCore:
def _format_local_time(timestamp: int) -> str:
if not timestamp:
return ""
return datetime.fromtimestamp(timestamp, UTC).strftime("%Y-%m-%d %H:%M:%S")
return (
datetime.fromtimestamp(timestamp, UTC)
.astimezone()
.strftime("%Y-%m-%d %H:%M:%S")
)
@staticmethod
def _parse_scheduled_time(value) -> datetime | None:
+7 -10
View File
@@ -285,23 +285,20 @@ class EliteMixin:
role_name = ""
# ---- 角色绑定 / CK 有效性(先做,失效就不空跑后面两个接口) ----
b_act_id_int = self._to_int(
self.payload.get("bind_act_id") or config_info.get("bind_act_id")
)
if not b_act_id_int:
protocol = self._bind_protocol(config_info, self.payload)
b_act_id_int = protocol["b_act_id"]
if not protocol["bind_gid"] and not b_act_id_int:
failures.append("未配置绑定 bActId")
result["ck_valid"] = False
else:
outer_act_id = str(
self.payload.get("outer_act_id")
or config_info.get("outer_act_id")
or ""
).strip()
bind_status, bind_query_result = self._probe_bind_status(
uid=uid,
cookie=cookie,
b_act_id_int=b_act_id_int,
outer_act_id=outer_act_id,
outer_act_id=protocol["outer_act_id"],
gid=protocol["gid"],
is_use_outer_act_id=protocol["is_use_outer_act_id"],
scene=protocol["scene"],
)
if bind_status is None:
account.status = "ck_expired"
+7 -11
View File
@@ -10,7 +10,6 @@ from sqlalchemy.orm import Session
from core.huya.wss_client import POINTS_REDEEM_GOODS_NUM
from ..huya_defaults import HUYA_DEFAULT_OUTER_ACT_ID
from ..models import HuyaAccount, HuyaGoodsSnapshot, HuyaTask
# 积分查询超时:HTTP 通道正常几百毫秒,10 秒足够;失败时别让整批任务长时间挂住。
@@ -123,20 +122,17 @@ class GoodsMixin:
# ---- 先验 CK:没有回包就立即判失效,不再空跑积分与旧接口 ----
ck_valid = False
b_act_id_int = self._to_int(
self.payload.get("bind_act_id") or config_info.get("bind_act_id")
)
if b_act_id_int:
outer_act_id = str(
self.payload.get("outer_act_id")
or config_info.get("outer_act_id")
or HUYA_DEFAULT_OUTER_ACT_ID
).strip()
protocol = self._bind_protocol(config_info, self.payload)
b_act_id_int = protocol["b_act_id"]
if protocol["bind_gid"] or b_act_id_int:
bind_status, _bind_query_result = self._probe_bind_status(
uid=uid,
cookie=cookie,
b_act_id_int=b_act_id_int,
outer_act_id=outer_act_id,
outer_act_id=protocol["outer_act_id"],
gid=protocol["gid"],
is_use_outer_act_id=protocol["is_use_outer_act_id"],
scene=protocol["scene"],
)
if bind_status is None:
account.status = "ck_expired"