Files
abot/wechat_ipad/providers/server_864/base.py
2026-05-07 15:47:14 +08:00

138 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import time
from typing import Any
import aiohttp
from wechat_ipad.errors import UserLoggedOut
class Server864APIClientBase:
"""864 provider 的基础 HTTP 访问封装。
设计说明:
1. 864 与 855 一样仍然是 HTTP 驱动,但核心鉴权从 `wxid` 切到了固定 `key`
2. 这里把请求拼装、错误转换、常见返回结构解析集中收口,避免每个 mixin 重复写样板代码;
3. 对外仍尽量返回项目当前可直接消费的 dict / list保持切换成本低。
"""
def __init__(self, ip: str, port: int, server_key: str = "", **kwargs):
del kwargs
self.ip = ip
self.port = port
self.server_key = str(server_key or "").strip()
self.wxid = ""
self.nickname = ""
self.alias = ""
self.phone = ""
self.signature = ""
# 864 的发送接口与 855 一样,很多业务链路仍会依赖这些消息回执字段:
# 1. 但 864 有些接口返回的是 protobuf JSON而不是统一 msg id 三元组;
# 2. 因此这里准备一份本地递增 client id在响应缺字段时作为兼容兜底
# 3. 这样可以保证上层至少拿到稳定结构,而不会因为个别 server 少字段直接崩掉。
self._fallback_client_msg_id = int(time.time() * 1000)
super().__init__()
@property
def base_url(self) -> str:
return f"http://{self.ip}:{self.port}"
def _ensure_server_key(self) -> str:
"""确保 864 固定鉴权 key 已配置。"""
if not self.server_key:
raise ValueError("server_864 缺少 server_key请在 .env 中配置 WECHAT_SERVER_KEY")
return self.server_key
async def _request_payload(
self,
method: str,
path: str,
*,
params: dict[str, Any] | None = None,
json_body: dict[str, Any] | None = None,
timeout: int = 20,
) -> dict[str, Any]:
"""向 864 server 发送请求,并保留原始 payload 便于上层按需解析。"""
merged_params = dict(params or {})
merged_params["key"] = self._ensure_server_key()
request_timeout = aiohttp.ClientTimeout(total=timeout)
async with aiohttp.ClientSession(timeout=request_timeout) as session:
async with session.request(
method.upper(),
f"{self.base_url}{path}",
params=merged_params,
json=json_body,
) as response:
try:
payload = await response.json(content_type=None)
except Exception:
raw_text = await response.text()
# 864 少数接口会直接返回纯文本而不是标准 DTO
# 1. 当前已在 `LogOut` 链路里碰到这种情况,旧逻辑会先炸 JSON 解析,再把真正状态信息吞掉;
# 2. 对 2xx 响应来说,这通常只是“接口风格不统一”,不应该直接视为致命协议错误;
# 3. 因此这里统一包成兼容 DTO保留原始文本给上层按需处理。
payload = {
"Code": 200 if response.status < 400 else response.status,
"Data": None,
"Text": str(raw_text or "").strip(),
}
return self._validate_payload(payload)
async def _request_data(
self,
method: str,
path: str,
*,
params: dict[str, Any] | None = None,
json_body: dict[str, Any] | None = None,
timeout: int = 20,
) -> Any:
"""获取成功返回中的 Data 字段。"""
payload = await self._request_payload(
method,
path,
params=params,
json_body=json_body,
timeout=timeout,
)
return payload.get("Data")
def _validate_payload(self, payload: Any) -> dict[str, Any]:
"""把 864 的 DTO 返回统一转换成 Python 异常或 dict。"""
if not isinstance(payload, dict):
raise ValueError(f"server_864 返回了无法识别的响应结构: {payload!r}")
code = payload.get("Code")
if code == 200:
return payload
message = str(payload.get("Text") or payload.get("Message") or "server_864 请求失败").strip()
# 864 某些登录接口会用非 200 编码表达“当前 key 已经在线”:
# 1. 对二维码申请链路来说,这更接近一种状态回执,而不是硬失败;
# 2. 如果这里直接抛异常,上层就会把“已经在线”误判成致命错误并退出线程;
# 3. 因此先保留 payload 原样放行,让 runtime 再决定是复用现有登录态还是继续走扫码。
if "该链接已绑定微信号" in message and "在线状态良好" in message:
return payload
lowered_message = message.lower()
if any(keyword in lowered_message for keyword in ("重新登录", "已退出登录", "离线", "账号需要重新登录")):
raise UserLoggedOut(message)
raise Exception(message)
def _next_fallback_message_ids(self) -> tuple[int, int, int]:
"""生成一组兼容旧调用面的消息回执兜底值。"""
self._fallback_client_msg_id += 1
client_msg_id = self._fallback_client_msg_id
create_time = int(time.time())
return client_msg_id, create_time, 0
@staticmethod
def _pick_first(data: Any, *keys: str) -> Any:
"""从 dict 中按优先级取第一个存在的字段。"""
if not isinstance(data, dict):
return None
for key in keys:
if key in data and data.get(key) is not None:
return data.get(key)
return None