""" 二维码生成路由 生成项目绑定二维码,供手机APP扫描绑定 NAS armv7 等环境无 Pillow 编译链时,自动降级为 qrcode + pypng(纯 Python PNG)。 """ from fastapi import APIRouter, Response from pydantic import BaseModel from typing import Optional, Type import os import json import base64 import io # qrcode 核心 + 图像后端(PIL 优先,pypng 兜底 — 适配 DS213j 等无 zlib-dev 的 NAS) try: import qrcode from qrcode.constants import ERROR_CORRECT_H _QR_IMAGE_FACTORY: Optional[Type] = None _QR_USE_PIL_COLORS = False try: from PIL import Image # noqa: F401 from qrcode.image.pil import PilImage _QR_IMAGE_FACTORY = PilImage _QR_USE_PIL_COLORS = True except ImportError: try: from qrcode.image.pure import PyPNGImage _QR_IMAGE_FACTORY = PyPNGImage except ImportError: pass QRCODE_AVAILABLE = _QR_IMAGE_FACTORY is not None except ImportError: QRCODE_AVAILABLE = False _QR_IMAGE_FACTORY = None _QR_USE_PIL_COLORS = False router = APIRouter(prefix="/qrcode", tags=["二维码"]) _QR_INSTALL_HINT = ( "请安装: pip install qrcode pypng (或 pip install 'qrcode[pil]')" ) class ProjectBindConfig(BaseModel): """项目绑定配置""" project_id: str project_name: Optional[str] = "" server: Optional[str] = "wss://wpsdk.quwanzhi.com/ws/device" pwa: Optional[str] = "" ai_api_key: Optional[str] = "" ai_api_url: Optional[str] = "https://ckbapi.quwanzhi.com" # BIND-03/05:公网主服有序回退列表(逗号分隔)+ 开机自动寻服 public_servers: Optional[str] = "" auto_discover: Optional[bool] = False pairing_token: Optional[str] = "" def _build_qr_content( project_id: str, project_name: str = "", server: str = "wss://wpsdk.quwanzhi.com/ws/device", pwa: str = "", ai_api_key: str = "", ai_api_url: str = "", public_servers: str = "", auto_discover: bool = False, pairing_token: str = "", ) -> str: qr_data = {"server": server, "project": project_id, "project_name": project_name} if pwa: qr_data["pwa"] = pwa if ai_api_key: qr_data["ai_api_key"] = ai_api_key if ai_api_url: qr_data["ai_api_url"] = ai_api_url if public_servers: qr_data["public_servers"] = public_servers if auto_discover: qr_data["auto_discover"] = "1" token = pairing_token or os.environ.get("DEVICE_PAIRING_TOKEN", "") if token: qr_data["pairing_token"] = token return json.dumps(qr_data, ensure_ascii=False) def _render_qr_png_bytes(qr_content: str) -> bytes: qr = qrcode.QRCode( version=1, error_correction=ERROR_CORRECT_H, box_size=10, border=4, ) qr.add_data(qr_content) qr.make(fit=True) if _QR_USE_PIL_COLORS: img = qr.make_image( fill_color="black", back_color="white", image_factory=_QR_IMAGE_FACTORY, ) else: img = qr.make_image(image_factory=_QR_IMAGE_FACTORY) buffer = io.BytesIO() img.save(buffer) return buffer.getvalue() @router.post("/generate") async def generate_qrcode(config: ProjectBindConfig): """ 生成项目绑定二维码 返回: Base64编码的PNG图片 """ if not QRCODE_AVAILABLE: return { "success": False, "message": f"服务器未安装 qrcode 图像库,{_QR_INSTALL_HINT}", } qr_content = _build_qr_content( config.project_id, config.project_name or "", config.server or "", config.pwa or "", config.ai_api_key or "", config.ai_api_url or "", config.public_servers or "", bool(config.auto_discover), config.pairing_token or "", ) png_bytes = _render_qr_png_bytes(qr_content) img_base64 = base64.b64encode(png_bytes).decode() return { "success": True, "project_id": config.project_id, "project_name": config.project_name, "qr_content": qr_content, "image_base64": img_base64, "image_data_url": f"data:image/png;base64,{img_base64}", } @router.get("/image/{project_id}") async def get_qrcode_image( project_id: str, project_name: str = "", server: str = "wss://wpsdk.quwanzhi.com/ws/device", pwa: str = "", ): """直接获取二维码 PNG 图片""" if not QRCODE_AVAILABLE: return Response( content=f"QRCode not available. {_QR_INSTALL_HINT}", media_type="text/plain", status_code=500, ) qr_content = _build_qr_content(project_id, project_name, server, pwa) return Response( content=_render_qr_png_bytes(qr_content), media_type="image/png", ) @router.get("/html/{project_id}") async def get_qrcode_html( project_id: str, project_name: str = "", server: str = "wss://wpsdk.quwanzhi.com/ws/device", pwa: str = "", ): """获取带样式的二维码 HTML 页面""" if not QRCODE_AVAILABLE: return Response( content=f"

QRCode not available

{_QR_INSTALL_HINT}

", media_type="text/html", status_code=500, ) qr_content = _build_qr_content(project_id, project_name, server, pwa) img_base64 = base64.b64encode(_render_qr_png_bytes(qr_content)).decode() html = f""" 项目绑定二维码 - {project_name or project_id}

工作手机Agent

{project_name or project_id}
绑定二维码
项目ID: {project_id}
""" return Response(content=html, media_type="text/html")