Files
workphone-sdk/sdk/app/routers/qrcode.py

259 lines
8.1 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.

"""
二维码生成路由
生成项目绑定二维码供手机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"<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")