切换864登录二维码到NewX并接入安全验证链接
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -48,6 +48,8 @@ class Server864RuntimeMixin:
|
||||
if not server_key:
|
||||
raise ValueError("server_864 启动失败:缺少 server_key,请在 .env 中配置 WECHAT_SERVER_KEY")
|
||||
self.server_key = server_key
|
||||
login_qr_api = str(ipad_config.get("login_qr_api", "new_x") or "new_x").strip()
|
||||
login_way = str(ipad_config.get("login_way", "mac") or "mac").strip()
|
||||
|
||||
await self._ensure_login(
|
||||
ipad_config=ipad_config,
|
||||
@@ -55,6 +57,8 @@ class Server864RuntimeMixin:
|
||||
logger=logger,
|
||||
on_login_qr_update=on_login_qr_update,
|
||||
on_login_qr_cleared=on_login_qr_cleared,
|
||||
login_qr_api=login_qr_api,
|
||||
login_way=login_way,
|
||||
)
|
||||
|
||||
await on_login_ready(self.get_login_identity())
|
||||
@@ -97,6 +101,8 @@ class Server864RuntimeMixin:
|
||||
logger,
|
||||
on_login_qr_update: AsyncCallback | None = None,
|
||||
on_login_qr_cleared: AsyncCallback | None = None,
|
||||
login_qr_api: str = "new_x",
|
||||
login_way: str = "mac",
|
||||
) -> None:
|
||||
"""确保 864 已完成登录。"""
|
||||
if await self.is_logged_in():
|
||||
@@ -128,7 +134,7 @@ class Server864RuntimeMixin:
|
||||
callback_name="on_login_qr_update",
|
||||
)
|
||||
|
||||
uuid, url = await self.get_qr_code(print_qr=True)
|
||||
uuid, url = await self.get_qr_code(print_qr=True, login_qr_api=login_qr_api, login_way=login_way)
|
||||
scan_url = f"http://weixin.qq.com/x/{uuid}" if uuid else ""
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
@@ -144,6 +150,8 @@ class Server864RuntimeMixin:
|
||||
"provider_stage": "waiting_scan",
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
@@ -174,11 +182,23 @@ class Server864RuntimeMixin:
|
||||
# 3. 一旦 server 侧切换了新的 uuid,这里也要及时覆盖本地展示态,避免前端一直盯着旧码。
|
||||
latest_uuid = str(login_status.get("uuid", "") or uuid).strip() or uuid
|
||||
effective_time = int(login_status.get("effective_time", 0) or 0)
|
||||
verification_url = str(login_status.get("verification_url", "") or "").strip()
|
||||
if latest_uuid != uuid:
|
||||
uuid = latest_uuid
|
||||
scan_url = f"http://weixin.qq.com/x/{uuid}" if uuid else ""
|
||||
url = f"https://api.2dcode.biz/v1/create-qr-code?data={scan_url}" if scan_url else url
|
||||
|
||||
provider_stage = "verification_required" if verification_url else ("waiting_scan" if uuid else "login_required")
|
||||
# 864 在“已扫码但待安全验证”阶段,`msg/loginState` 往往是空字符串:
|
||||
# 1. 若直接套用默认“等待扫码登录”,用户会误以为还没扫上;
|
||||
# 2. 因此这里优先识别 `verification_url`,给 Dashboard 一个更准确的引导文案;
|
||||
# 3. 只有完全拿不到状态提示时,才回退到普通扫码等待文案。
|
||||
raw_status_text = str(login_status.get("msg") or login_status.get("loginState") or "").strip()
|
||||
if verification_url and not raw_status_text:
|
||||
status_text = "扫码已完成,请继续打开验证链接完成安全验证"
|
||||
else:
|
||||
status_text = raw_status_text or "等待扫码登录"
|
||||
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
{
|
||||
@@ -187,12 +207,15 @@ class Server864RuntimeMixin:
|
||||
"scan_url": scan_url,
|
||||
"expires_in": effective_time if effective_time > 0 else None,
|
||||
"status": "waiting",
|
||||
"status_text": str(login_status.get("msg") or login_status.get("loginState") or "等待扫码登录"),
|
||||
"status_text": status_text,
|
||||
"login_source": "fresh_qr",
|
||||
"provider_name": "server_864",
|
||||
"provider_stage": "waiting_scan" if uuid else "login_required",
|
||||
"provider_stage": provider_stage,
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"verification_url": verification_url,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
@@ -203,7 +226,7 @@ class Server864RuntimeMixin:
|
||||
# 2. 也能让新环境登录时的交互与 855 保持一致,都是“过期就自动刷新”;
|
||||
# 3. 重新申请后直接回到当前 while 顶部继续轮询新的 uuid 状态。
|
||||
if effective_time <= 0:
|
||||
uuid, url = await self.get_qr_code(print_qr=True)
|
||||
uuid, url = await self.get_qr_code(print_qr=True, login_qr_api=login_qr_api, login_way=login_way)
|
||||
scan_url = f"http://weixin.qq.com/x/{uuid}" if uuid else ""
|
||||
await self._safe_callback(
|
||||
on_login_qr_update,
|
||||
@@ -219,6 +242,8 @@ class Server864RuntimeMixin:
|
||||
"provider_stage": "waiting_scan",
|
||||
"connection_ready": False,
|
||||
"login_required": True,
|
||||
"login_qr_api": login_qr_api,
|
||||
"login_way": login_way,
|
||||
},
|
||||
logger=logger,
|
||||
callback_name="on_login_qr_update",
|
||||
|
||||
Reference in New Issue
Block a user