front/xhs-agent-desktop/app/pair_server.py
2026-07-02 15:52:09 +08:00

122 lines
4.6 KiB
Python

"""本地配对服务:接收浏览器发来的 OMS Token。"""
from __future__ import annotations
import json
import logging
import threading
from dataclasses import dataclass
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Callable
logger = logging.getLogger(__name__)
DEFAULT_PAIR_PORT = 19333
@dataclass(frozen=True)
class PairPayload:
token: str
oms_url: str
web_origin: str = ""
class PairServer:
"""127.0.0.1 本地 HTTP 服务,供 OMS 网页推送 JWT。"""
def __init__(self, port: int = DEFAULT_PAIR_PORT) -> None:
self.port = port
self._server: ThreadingHTTPServer | None = None
self._thread: threading.Thread | None = None
self._waiting = False
self._on_pair: Callable[[PairPayload], None] | None = None
@property
def is_waiting(self) -> bool:
return self._waiting
def start(self, on_pair: Callable[[PairPayload], None]) -> None:
self.stop()
self._on_pair = on_pair
self._waiting = True
server_ref = self
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt: str, *args) -> None:
logger.debug(fmt, *args)
def _write_cors(self) -> None:
origin = self.headers.get("Origin", "")
if origin.startswith(("http://localhost", "http://127.0.0.1", "https://")):
self.send_header("Access-Control-Allow-Origin", origin)
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
def do_OPTIONS(self) -> None:
self.send_response(204)
self._write_cors()
self.end_headers()
def do_GET(self) -> None:
if self.path.split("?", 1)[0].rstrip("/") != "/status":
self.send_error(404)
return
body = json.dumps({"waiting": server_ref._waiting})
self.send_response(200)
self._write_cors()
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(body.encode("utf-8"))
def do_POST(self) -> None:
if self.path.split("?", 1)[0].rstrip("/") != "/pair":
self.send_error(404)
return
length = int(self.headers.get("Content-Length", 0))
raw = self.rfile.read(length) if length else b"{}"
try:
data = json.loads(raw.decode("utf-8"))
except json.JSONDecodeError:
self.send_error(400)
return
if not isinstance(data, dict):
self.send_error(400)
return
token = str(data.get("token") or "").strip()
oms_url = str(data.get("omsUrl") or data.get("oms_url") or "").strip().rstrip("/")
web_origin = str(data.get("webOrigin") or data.get("web_origin") or "").strip().rstrip("/")
if not token or not oms_url:
payload = json.dumps({"ok": False, "message": "token 与 omsUrl 必填"})
self.send_response(400)
self._write_cors()
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(payload.encode("utf-8"))
return
server_ref._waiting = False
if server_ref._on_pair:
server_ref._on_pair(
PairPayload(token=token, oms_url=oms_url, web_origin=web_origin)
)
payload = json.dumps({"ok": True})
self.send_response(200)
self._write_cors()
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(payload.encode("utf-8"))
self._server = ThreadingHTTPServer(("127.0.0.1", self.port), Handler)
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
self._thread.start()
logger.info("配对服务已启动 127.0.0.1:%s", self.port)
def stop(self) -> None:
self._waiting = False
if self._server:
self._server.shutdown()
self._server.server_close()
self._server = None
self._thread = None