import qrcode from urllib.parse import parse_qs, urlparse 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/` 才是真正扫码值; # 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 登录二维码。 说明: 1. 864 不依赖 855 的 `device_name/device_id` 入参,但保留参数签名以兼容上层调用; 2. `proxy` 当前仅保留兼容占位,后续如需补实际代理登录,可直接映射到 swagger 的 Proxy 字段; 3. 返回值继续保持 `(uuid, url)`,方便 Dashboard 与运行时共用同一套二维码展示逻辑。 """ del device_name, device_id proxy_value = "" if proxy is not None: proxy_value = getattr(proxy, "proxy", "") or "" 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( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4, ) qr.add_data(f"http://weixin.qq.com/x/{uuid}") qr.make(fit=True) qr.print_ascii() return str(uuid), str(qr_url) async def check_login_status(self) -> tuple[bool, dict]: """检查当前二维码登录状态。""" 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 async def get_init_status(self) -> bool: """检查 server 侧初始化是否完成。""" data = await self._request_data("get", "/login/GetInItStatus", timeout=15) return bool(data) async def awaken_login(self, wxid: str = "") -> dict: """触发 864 的唤醒登录。""" del wxid return await self._request_data("post", "/login/WakeUpLogin", timeout=30) async def get_login_status(self, auto_login: bool = True) -> dict: """获取 864 在线状态。""" return await self._request_data( "get", "/login/GetLoginStatus", params={"autoLogin": str(bool(auto_login)).lower()}, timeout=20, ) async def log_out(self) -> bool: """退出当前 864 登录态。""" await self._request_data("get", "/login/LogOutRequest", timeout=15) return True