from __future__ import annotations from typing import Any from yuxi.utils.logging_config import logger class FeishuOAuthClient: def __init__(self, app_id: str, app_secret: str, redirect_uri: str = ""): self._app_id = app_id self._app_secret = app_secret self._redirect_uri = redirect_uri def get_authorization_url(self, state: str = "", scope: str = "") -> str: base_url = "https://open.feishu.cn/open-apis/authen/v1/index" params = { "app_id": self._app_id, "redirect_uri": self._redirect_uri or "http://localhost/callback", } if state: params["state"] = state if scope: params["scope"] = scope query = "&".join(f"{k}={v}" for k, v in params.items()) return f"{base_url}?{query}" async def exchange_code_for_token(self, code: str) -> dict[str, Any]: import httpx url = "https://open.feishu.cn/open-apis/authen/v1/oidc/access_token" headers = {"Content-Type": "application/json"} body = { "app_id": self._app_id, "app_secret": self._app_secret, "grant_type": "authorization_code", "code": code, } async with httpx.AsyncClient(timeout=httpx.Timeout(30)) as client: resp = await client.post(url, headers=headers, json=body) if resp.status_code == 200: return resp.json() logger.error("[FeishuOAuth] Token exchange failed: HTTP %d", resp.status_code) return {} async def refresh_user_access_token(self, refresh_token: str) -> dict[str, Any]: import httpx url = "https://open.feishu.cn/open-apis/authen/v1/oidc/refresh_access_token" headers = {"Content-Type": "application/json"} body = { "app_id": self._app_id, "app_secret": self._app_secret, "grant_type": "refresh_token", "refresh_token": refresh_token, } async with httpx.AsyncClient(timeout=httpx.Timeout(30)) as client: resp = await client.post(url, headers=headers, json=body) if resp.status_code == 200: return resp.json() logger.error("[FeishuOAuth] Refresh failed: HTTP %d", resp.status_code) return {} class FeishuDeviceCodeClient: """飞书 OAuth 2.0 Device Authorization Grant 客户端。 实现设备授权码流程,用于终端扫码注册场景。 """ DEVICE_CODE_URL = "https://open.feishu.cn/open-apis/authen/v1/device/code" DEVICE_TOKEN_URL = "https://open.feishu.cn/open-apis/authen/v1/oidc/access_token" def __init__(self, app_id: str, app_secret: str): self._app_id = app_id self._app_secret = app_secret def init_device_flow(self) -> dict[str, Any] | None: import httpx body = { "app_id": self._app_id, "app_secret": self._app_secret, "scope": "user:read", } try: resp = httpx.post( self.DEVICE_CODE_URL, json=body, headers={"Content-Type": "application/json"}, timeout=httpx.Timeout(30), ) if resp.status_code == 200: data = resp.json() if data.get("code") == 0: return data.get("data", {}) logger.error("[FeishuDeviceCode] init failed: code=%s, msg=%s", data.get("code"), data.get("msg")) else: logger.error("[FeishuDeviceCode] init HTTP %d: %s", resp.status_code, resp.text[:300]) except Exception as e: logger.error("[FeishuDeviceCode] init error: %s", e) return None def poll_device_token(self, device_code: str) -> dict[str, Any] | None: import httpx body = { "app_id": self._app_id, "app_secret": self._app_secret, "grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": device_code, } try: resp = httpx.post( self.DEVICE_TOKEN_URL, json=body, headers={"Content-Type": "application/json"}, timeout=httpx.Timeout(30), ) if resp.status_code == 200: return resp.json() logger.error("[FeishuDeviceCode] poll HTTP %d: %s", resp.status_code, resp.text[:300]) except Exception as e: logger.error("[FeishuDeviceCode] poll error: %s", e) return None