Files
abot/wechat_ipad/providers/server_864/base.py
liuwei ff33edb0d1 新增864 provider并打通server_key配置
- 新增 server_864 独立 provider 目录,接入登录、消息轮询、联系人、群资料、用户资料与朋友圈基础能力

- 扩展 gateway、robot 与配置归一化逻辑,支持 server_864/864 别名和 WECHAT_SERVER_KEY

- 更新配置示例与多版本适配路线图,明确 864 第一版接入范围和后续待补项
2026-05-07 11:24:33 +08:00

120 lines
4.4 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:
payload = await response.json(content_type=None)
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()
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