切换864登录二维码到NewX并接入安全验证链接

This commit is contained in:
liuwei
2026-05-07 14:09:54 +08:00
parent 050e537ba3
commit cf6b676a56
13 changed files with 156 additions and 28 deletions

View File

@@ -7,12 +7,40 @@ from wechat_ipad.providers.server_864.base import Server864APIClientBase
class LoginMixin(Server864APIClientBase):
"""864 登录相关接口。"""
@staticmethod
def _normalize_login_way(login_way: str) -> str:
"""标准化 864 `GetLoginQrCodeNewX` 的 way 参数。"""
normalized_way = str(login_way or "").strip().lower()
return normalized_way if normalized_way in {"mac", "win", "harmony", "car", "watch"} else "mac"
def _extract_qr_response(self, data) -> tuple[str, str]:
"""从 864 的二维码返回结构中提取 uuid 与二维码地址。"""
qr_code_url = (
self._pick_first(data, "QrCodeUrl", "QRCodeUrl", "qrCodeUrl")
or self._pick_first(data, "QrUrl", "QRUrl", "qrUrl")
or self._pick_first(self._pick_first(data, "Qrcode", "QrCode", "qrcode") or {}, "Src", "src")
or ""
)
uuid = self._pick_first(data, "UUID", "Uuid", "uuid") or ""
if not uuid and qr_code_url:
# 864 的真实返回经常只给“二维码图片地址”,并不会直接带出 uuid
# 1. 图床链接 query 中的 `data=http://weixin.qq.com/x/<uuid>` 才是真正扫码值;
# 2. Dashboard 当前以 uuid 作为刷新与历史记录主键,缺失后前端体验会明显变差;
# 3. 因此这里统一做一次反解,让 New / NewX 两种接口都复用同一份展示逻辑。
parsed_qs = parse_qs(urlparse(str(qr_code_url)).query)
scan_data = str((parsed_qs.get("data") or [""])[0] or "")
if "/x/" in scan_data:
uuid = scan_data.rsplit("/x/", 1)[-1].strip()
return str(uuid), str(qr_code_url)
async def get_qr_code(
self,
device_name: str = "",
device_id: str = "",
proxy=None,
print_qr: bool = False,
login_qr_api: str = "new_x",
login_way: str = "mac",
) -> tuple[str, str]:
"""获取 864 登录二维码。
@@ -26,30 +54,38 @@ class LoginMixin(Server864APIClientBase):
if proxy is not None:
proxy_value = getattr(proxy, "proxy", "") or ""
data = await self._request_data(
"post",
"/login/GetLoginQrCodeNew",
json_body={"Proxy": proxy_value, "Check": False},
timeout=30,
)
qr_code_url = (
self._pick_first(data, "QrCodeUrl", "QRCodeUrl", "qrCodeUrl")
or self._pick_first(data, "QrUrl", "QRUrl", "qrUrl")
or self._pick_first(self._pick_first(data, "Qrcode", "QrCode", "qrcode") or {}, "Src", "src")
or ""
)
uuid = self._pick_first(data, "UUID", "Uuid", "uuid") or ""
if not uuid and qr_code_url:
# 864 当前真实返回的是“二维码图片服务地址”,
# 其中真正的扫码链接藏在 `data=http://weixin.qq.com/x/<uuid>` 这个 query 里:
# 1. Dashboard 需要 uuid 才能稳定生成扫码地址与展示文案;
# 2. 因此这里把 query 中的真实扫码链接反解出来,兼容当前 server 的返回格式;
# 3. 若未来某些 864 版本直接返回 UUID本逻辑仍会优先使用原始字段不会互相冲突。
parsed_qs = parse_qs(urlparse(str(qr_code_url)).query)
scan_data = str((parsed_qs.get("data") or [""])[0] or "")
if "/x/" in scan_data:
uuid = scan_data.rsplit("/x/", 1)[-1].strip()
qr_url = str(qr_code_url)
normalized_login_qr_api = str(login_qr_api or "new_x").strip().lower()
normalized_login_way = self._normalize_login_way(login_way)
if normalized_login_qr_api in {"new_x", "x", "newx"}:
try:
# NewX 是当前 864 联调里更完整的一条登录链路:
# 1. 它支持 `Way` 指定登录端形态,兼容更多 server 变体;
# 2. 用户实测也验证该接口可正常返回二维码;
# 3. 若某些旧版 864 仍未提供 NewX则自动回退到旧接口避免新配置直接打挂启动。
data = await self._request_data(
"post",
"/login/GetLoginQrCodeNewX",
json_body={"Proxy": proxy_value, "Check": False, "Way": normalized_login_way},
timeout=30,
)
uuid, qr_url = self._extract_qr_response(data)
except Exception:
data = await self._request_data(
"post",
"/login/GetLoginQrCodeNew",
json_body={"Proxy": proxy_value, "Check": False},
timeout=30,
)
uuid, qr_url = self._extract_qr_response(data)
else:
data = await self._request_data(
"post",
"/login/GetLoginQrCodeNew",
json_body={"Proxy": proxy_value, "Check": False},
timeout=30,
)
uuid, qr_url = self._extract_qr_response(data)
if print_qr and uuid:
qr = qrcode.QRCode(
@@ -68,6 +104,19 @@ class LoginMixin(Server864APIClientBase):
"""检查当前二维码登录状态。"""
data = await self._request_data("get", "/login/CheckLoginStatus", timeout=20)
normalized = dict(data or {})
# 864 扫码后可能不会直接变成 online而是先返回一个安全验证链接
# 1. 该字段在 swagger / 实机联调里都存在,但大小写并不稳定;
# 2. 上层 runtime 只关心统一字段名,因此这里先做一层归一化;
# 3. 这样 Dashboard 只需要消费 `verification_url`,不用感知各 server 的原始差异。
verification_url = self._pick_first(
normalized,
"VerificationUrl",
"VerificationURL",
"verificationUrl",
"verification_url",
)
if verification_url:
normalized["verification_url"] = str(verification_url).strip()
state = int(normalized.get("state", 0) or 0)
login_state = str(normalized.get("loginState", "") or "").strip().lower()
return state == 2 or login_state == "online", normalized