231 lines
7.1 KiB
Python
231 lines
7.1 KiB
Python
"""
|
||
二维码生成路由
|
||
生成项目绑定二维码,供手机APP扫描绑定
|
||
|
||
NAS armv7 等环境无 Pillow 编译链时,自动降级为 qrcode + pypng(纯 Python PNG)。
|
||
"""
|
||
|
||
from fastapi import APIRouter, Response
|
||
from pydantic import BaseModel
|
||
from typing import Optional, Type
|
||
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] = "ws://sdk.quwanzhi.com:8899/ws/device"
|
||
pwa: Optional[str] = ""
|
||
|
||
|
||
def _build_qr_content(
|
||
project_id: str,
|
||
project_name: str = "",
|
||
server: str = "ws://sdk.quwanzhi.com:8899/ws/device",
|
||
pwa: str = "",
|
||
) -> str:
|
||
qr_data = {"server": server, "project": project_id, "project_name": project_name}
|
||
if pwa:
|
||
qr_data["pwa"] = pwa
|
||
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 "",
|
||
)
|
||
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 = "ws://sdk.quwanzhi.com:8899/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 = "ws://sdk.quwanzhi.com:8899/ws/device",
|
||
pwa: str = "",
|
||
):
|
||
"""获取带样式的二维码 HTML 页面"""
|
||
if not QRCODE_AVAILABLE:
|
||
return Response(
|
||
content=f"<h1>QRCode not available</h1><p>{_QR_INSTALL_HINT}</p>",
|
||
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"""
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>项目绑定二维码 - {project_name or project_id}</title>
|
||
<style>
|
||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||
body {{
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 20px;
|
||
}}
|
||
.card {{
|
||
background: white;
|
||
border-radius: 20px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||
max-width: 400px;
|
||
}}
|
||
.logo {{ font-size: 48px; margin-bottom: 20px; }}
|
||
h1 {{ color: #333; font-size: 24px; margin-bottom: 10px; }}
|
||
.project-name {{
|
||
color: #4CAF50; font-size: 20px; font-weight: bold; margin-bottom: 30px;
|
||
}}
|
||
.qr-container {{
|
||
background: #f5f5f5; padding: 20px; border-radius: 16px; margin-bottom: 30px;
|
||
}}
|
||
.qr-container img {{ max-width: 100%; height: auto; }}
|
||
.instructions {{
|
||
color: #666; font-size: 14px; line-height: 1.8; text-align: left;
|
||
}}
|
||
.instructions li {{ margin-bottom: 8px; }}
|
||
.project-id {{
|
||
background: #e8f5e9; color: #2e7d32; padding: 8px 16px;
|
||
border-radius: 20px; display: inline-block; font-size: 12px; margin-top: 20px;
|
||
}}
|
||
@media print {{ body {{ background: white; }} .card {{ box-shadow: none; }} }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="card">
|
||
<div class="logo">📱</div>
|
||
<h1>工作手机Agent</h1>
|
||
<div class="project-name">{project_name or project_id}</div>
|
||
<div class="qr-container">
|
||
<img src="data:image/png;base64,{img_base64}" alt="绑定二维码">
|
||
</div>
|
||
<ul class="instructions">
|
||
<li>打开手机上的「工作手机Agent」APP</li>
|
||
<li>点击「📷 扫码绑定项目」按钮</li>
|
||
<li>扫描上方二维码完成绑定</li>
|
||
<li>点击「连接服务器」开始使用</li>
|
||
</ul>
|
||
<div class="project-id">项目ID: {project_id}</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
return Response(content=html, media_type="text/html")
|