Files
wxauto_api/doc/wxautorelogin/CLIENT.md
T
2026-04-07 14:11:45 +08:00

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. 注意事项

  1. 网络安全: 确保客户端Webhook端口能从公网访问
  2. 签名验证: 生产环境建议启用signature验证
  3. 超时设置: 二维码有效期通常120秒,需要及时处理
  4. 重试机制: 建议添加失败重试逻辑
  5. 日志记录: 保留完整日志便于排查问题