10 KiB
10 KiB
WXAuto Relogin - 客户端开发指南
1. 概述
本文档说明如何开发 WXAuto Relogin 客户端程序,用于接收中控告警并自动处理微信重登录。
2. 技术栈
- Python 3.11+
- FastAPI (Webhook服务)
- httpx (HTTP客户端)
- asyncio (异步处理)
3. 项目结构
wxauto-relogin-client/
├── client/
│ ├── __init__.py
│ ├── webhook_server.py # Webhook接收服务
│ ├── qrcode_handler.py # 二维码处理
│ ├── node_client.py # 节点通信
│ └── config.py # 配置
├── requirements.txt
└── README.md
4. 配置
# client/config.py
from pydantic import BaseModel
class ClientConfig(BaseModel):
center_url: str = "http://localhost:8080"
center_api_key: str = "your-external-api-key"
client_name: str = "relogin-client-1"
secret_key: str = "" # 可选,用于签名验证
listen_host: str = "0.0.0.0"
listen_port: int = 8081
config = ClientConfig()
5. Webhook 接收服务
# client/webhook_server.py
from fastapi import FastAPI, HTTPException, Header
from pydantic import BaseModel
from typing import Optional
import asyncio
import logging
from .qrcode_handler import QRCodeHandler
from .config import config
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("relogin-client")
app = FastAPI()
qrcode_handler = QRCodeHandler()
class ReloginAlert(BaseModel):
event: str
node_id: str
node_name: str
wechat_status: str
timestamp: str
@app.post("/webhook/relogin")
async def receive_alert(
alert: ReloginAlert,
x_signature: Optional[str] = Header(None)
):
"""
接收中控的掉线告警
"""
logger.info(f"Received alert: {alert.node_id} - {alert.wechat_status}")
# 验证签名(如果配置了secret_key)
if config.secret_key:
# TODO: 实现签名验证
pass
# 触发重登录流程
asyncio.create_task(handle_relogin(alert))
return {"status": "accepted", "node_id": alert.node_id}
async def handle_relogin(alert: ReloginAlert):
"""
处理重登录流程
"""
try:
logger.info(f"Starting relogin for {alert.node_id}")
# 1. 获取二维码
qrcode_base64 = await qrcode_handler.get_and_upload_qrcode(alert.node_id)
if qrcode_base64:
logger.info(f"QR code uploaded for {alert.node_id}")
# 2. 等待扫码确认
success = await qrcode_handler.wait_for_scan(alert.node_id, timeout=180)
if success:
logger.info(f"ReLogin successful for {alert.node_id}")
else:
logger.warning(f"ReLogin timeout for {alert.node_id}")
except Exception as e:
logger.error(f"ReLogin failed for {alert.node_id}: {e}")
@app.get("/health")
async def health():
return {"status": "healthy"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host=config.listen_host, port=config.listen_port)
6. 二维码处理
# client/qrcode_handler.py
import httpx
import asyncio
import time
import base64
from typing import Optional
from .config import config
class QRCodeHandler:
def __init__(self):
self.center_url = config.center_url
self.api_key = config.center_api_key
async def get_qrcode_from_node(self, api_url: str, node_api_key: str) -> Optional[bytes]:
"""
从wxauto节点获取二维码
"""
try:
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(
f"{api_url}/api/wechat/qrcode",
headers={"X-API-Key": node_api_key}
)
if response.status_code == 200:
return response.content
else:
return None
except Exception as e:
print(f"Failed to get QR code from node: {e}")
return None
async def upload_qrcode_to_center(self, node_id: str, qrcode_bytes: bytes, expires_in: int = 120):
"""
上传二维码到中控
"""
qrcode_base64 = base64.b64encode(qrcode_bytes).decode()
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
f"{self.center_url}/api/v1/callback/relogin/qrcode",
headers={"X-API-Key": self.api_key},
json={
"node_id": node_id,
"qrcode_base64": f"data:image/png;base64,{qrcode_base64}",
"expires_in": expires_in
}
)
return response.json().get("success", False)
async def get_node_info(self, node_id: str) -> Optional[dict]:
"""
从中控获取节点信息(包含API地址和密钥)
"""
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
f"{self.center_url}/api/v1/nodes/{node_id}",
headers={"X-API-Key": self.api_key}
)
if response.status_code == 200:
return response.json().get("data")
return None
async def get_and_upload_qrcode(self, node_id: str) -> Optional[str]:
"""
获取二维码并上传到中控
"""
# 1. 获取节点信息
node_info = await self.get_node_info(node_id)
if not node_info:
print(f"Node {node_id} not found")
return None
# 2. 获取二维码
qrcode_bytes = await self.get_qrcode_from_node(
api_url=node_info["api_url"],
node_api_key=node_info["api_key"]
)
if not qrcode_bytes:
print(f"Failed to get QR code for {node_id}")
return None
# 3. 上传到中控
success = await self.upload_qrcode_to_center(node_id, qrcode_bytes)
if success:
return f"data:image/png;base64,{base64.b64encode(qrcode_bytes).decode()}"
return None
async def wait_for_scan(self, node_id: str, timeout: int = 180) -> bool:
"""
轮询中控,等待扫码确认
"""
start_time = time.time()
interval = 2 # 每2秒轮询一次
while time.time() - start_time < timeout:
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
f"{self.center_url}/api/v1/nodes/{node_id}/relogin/status",
headers={"X-API-Key": self.api_key}
)
if response.status_code == 200:
data = response.json().get("data", {})
status = data.get("status")
print(f"Status: {status}")
if status == "completed":
return True
elif status == "failed":
return False
except Exception as e:
print(f"Polling error: {e}")
await asyncio.sleep(interval)
return False
7. 节点通信客户端
# client/node_client.py
import httpx
from typing import Optional
class NodeClient:
def __init__(self, api_url: str, api_key: str):
self.api_url = api_url.rstrip("/")
self.api_key = api_key
async def get_qrcode(self) -> Optional[bytes]:
"""
获取登录二维码
"""
try:
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(
f"{self.api_url}/api/wechat/qrcode",
headers={"X-API-Key": self.api_key}
)
if response.status_code == 200:
return response.content
return None
except Exception as e:
print(f"Failed to get QR code: {e}")
return None
async def check_login_status(self) -> dict:
"""
检查登录状态
"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
f"{self.api_url}/api/wechat/status",
headers={"X-API-Key": self.api_key}
)
if response.status_code == 200:
return response.json()
return {"logged_in": False}
except Exception as e:
return {"logged_in": False, "error": str(e)}
async def logout(self) -> bool:
"""
登出微信
"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
f"{self.api_url}/api/wechat/logout",
headers={"X-API-Key": self.api_key}
)
return response.status_code == 200
except Exception as e:
print(f"Logout failed: {e}")
return False
8. 依赖
# requirements.txt
fastapi>=0.100.0
uvicorn>=0.23.0
httpx>=0.24.0
pydantic>=2.0.0
python-dotenv>=1.0.0
9. 运行
# 安装依赖
pip install -r requirements.txt
# 运行客户端
python -m client.webhook_server
10. Docker 部署
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY client/ ./client/
CMD ["python", "-m", "client.webhook_server"]
# docker-compose.yml
services:
relogin-client:
build: .
ports:
- "8081:8081"
environment:
- CENTER_URL=http://your-center:8080
- CENTER_API_KEY=your-api-key
- CLIENT_NAME=relogin-1
restart: unless-stopped
11. 完整示例
# main.py
import asyncio
from client.webhook_server import app
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8081)
12. 注意事项
- 网络安全: 确保客户端Webhook端口能从公网访问
- 签名验证: 生产环境建议启用signature验证
- 超时设置: 二维码有效期通常120秒,需要及时处理
- 重试机制: 建议添加失败重试逻辑
- 日志记录: 保留完整日志便于排查问题