From a88cfebf8a32cfd837c8acedf735124b4709da20 Mon Sep 17 00:00:00 2001 From: Pine Date: Fri, 28 Aug 2026 17:40:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth-proxy):=20=E8=A1=A5=E9=BD=90=E7=9F=AD?= =?UTF-8?q?=E4=BF=A1/=E6=89=AB=E7=A0=81=E7=99=BB=E5=BD=95=E8=BD=AC?= =?UTF-8?q?=E5=8F=91=E7=AB=AF=E7=82=B9(send-code/phone-login/wx-qr/mp-qr?= =?UTF-8?q?=E7=AD=8911=E4=B8=AA)=20+=20status=E9=80=8F=E4=BC=A0login=5Fmod?= =?UTF-8?q?es=20+=20forward=E6=94=AF=E6=8C=81params?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pineagents/app/routers/auth.py | 142 ++++++++++++++++++++++++++++ src/pineagents/app/server_client.py | 2 + 2 files changed, 144 insertions(+) diff --git a/src/pineagents/app/routers/auth.py b/src/pineagents/app/routers/auth.py index cd2822f..bff63b2 100644 --- a/src/pineagents/app/routers/auth.py +++ b/src/pineagents/app/routers/auth.py @@ -62,6 +62,10 @@ class RegisterRequest(BaseModel): class AuthStatusResponse(BaseModel): enabled: bool has_users: bool + # 登录方式(password/phone/wechat/miniprogram),供登录页渲染 Tab + login_modes: list[str] = ["password", "phone"] + wechat_login: bool = False + wx_qr: bool = False class UpdateProfileRequest(BaseModel): @@ -187,9 +191,147 @@ async def auth_status(): return AuthStatusResponse( enabled=bool(data.get("enabled", True)), has_users=bool(data.get("has_users", True)), + login_modes=data.get("login_modes") or ["password", "phone"], + wechat_login=bool(data.get("wechat_login", False)), + wx_qr=bool(data.get("wx_qr", False)), ) +# ── 短信验证码登录 ──────────────────────────────────────────────────────── +class SendCodeRequest(BaseModel): + phone: str + + +class PhoneLoginRequest(BaseModel): + phone: str + code: str + + +@router.post("/send-code") +async def send_code(req: SendCodeRequest): + """发送短信验证码:转发到服务端(未接入短信网关时返回演示码 stub_code)。""" + return await forward("POST", "/auth/send-code", body={"phone": req.phone}) + + +@router.post("/phone-login") +async def phone_login(req: PhoneLoginRequest): + """手机号验证码登录(未注册自动建号):转发到服务端,签发平台令牌。""" + data = await forward( + "POST", "/auth/phone-login", + body={"phone": req.phone, "code": req.code}, + ) + save_auth_token(data.get("token", "")) + return data + + +# ── 微信登录 / 绑定 ────────────────────────────────────────────────────── +class WxLoginRequest(BaseModel): + code: str + nickName: str = "" + avatarUrl: str = "" + + +class BindPhoneRequest(BaseModel): + phone: str + code: str + + +class WxPhoneRequest(BaseModel): + code: str + + +class UnbindRequest(BaseModel): + type: str + + +@router.post("/wx-login") +async def wx_login(req: WxLoginRequest): + """小程序 wx.login code2session 登录:转发到服务端。""" + data = await forward("POST", "/auth/wx-login", body=req.model_dump()) + save_auth_token(data.get("token", "")) + return data + + +@router.post("/wx-phone") +async def wx_phone(req: WxPhoneRequest, request: Request): + """微信手机号快捷绑定(getPhoneNumber code):转发到服务端。""" + return await forward( + "POST", "/auth/wx-phone", body=req.model_dump(), + auth_header=request.headers.get("Authorization", ""), + ) + + +@router.post("/bind-phone") +async def bind_phone(req: BindPhoneRequest, request: Request): + """当前账号绑定手机号:转发到服务端,返回新令牌。""" + data = await forward( + "POST", "/auth/bind-phone", body=req.model_dump(), + auth_header=request.headers.get("Authorization", ""), + ) + save_auth_token(data.get("token", "")) + return data + + +@router.post("/unbind") +async def unbind(req: UnbindRequest, request: Request): + """解绑登录方式(保留至少一种):转发到服务端。""" + return await forward( + "POST", "/auth/unbind", body=req.model_dump(), + auth_header=request.headers.get("Authorization", ""), + ) + + +# ── 扫码登录(微信开放平台 / 小程序):代理只转发 start/poll ───────────── +@router.get("/wx-qr/start") +async def wx_qr_start(): + """发起微信开放平台扫码登录:转发到服务端。""" + return await forward("GET", "/auth/wx-qr/start") + + +@router.get("/wx-qr/poll") +async def wx_qr_poll(scene: str): + """轮询微信扫码登录状态:转发到服务端。""" + data = await forward("GET", "/auth/wx-qr/poll", params={"scene": scene}) + if data.get("status") == "done" and data.get("token"): + save_auth_token(data["token"]) + return data + + +@router.get("/mp-qr/start") +async def mp_qr_start(): + """发起小程序扫码登录(返回小程序码/二维码):转发到服务端。""" + return await forward("GET", "/auth/mp-qr/start") + + +@router.get("/mp-qr/poll") +async def mp_qr_poll(scene: str): + """轮询小程序扫码登录状态:转发到服务端。""" + data = await forward("GET", "/auth/mp-qr/poll", params={"scene": scene}) + if data.get("status") == "done" and data.get("token"): + save_auth_token(data["token"]) + return data + + +@router.get("/mp-qr/bind-start") +async def mp_qr_bind_start(request: Request): + """发起「绑定小程序」:转发到服务端(需登录)。""" + return await forward( + "GET", "/auth/mp-qr/bind-start", + auth_header=request.headers.get("Authorization", ""), + ) + + +class MpQrConfirmRequest(BaseModel): + scene: str + code: str + + +@router.post("/mp-qr/confirm") +async def mp_qr_confirm(req: MpQrConfirmRequest): + """小程序内确认扫码登录/绑定:转发到服务端。""" + return await forward("POST", "/auth/mp-qr/confirm", body=req.model_dump()) + + @router.get("/verify") async def verify(request: Request): """校验调用方 Bearer token:转发到演示 FastAPI。""" diff --git a/src/pineagents/app/server_client.py b/src/pineagents/app/server_client.py index 439b077..ea9df4a 100644 --- a/src/pineagents/app/server_client.py +++ b/src/pineagents/app/server_client.py @@ -19,6 +19,7 @@ async def forward( path: str, *, body: dict | None = None, + params: dict | None = None, auth_header: str | None = None, ) -> Any: """把请求转发到 PineAgentsServer,返回其 JSON 响应体。 @@ -38,6 +39,7 @@ async def forward( method, path, json=body, + params=params, headers=headers, ) except httpx.HTTPError as exc: