ForcePilot/backend/package/yuxi/channels/adapters/feishu/oauth.py
Kris a6fa7245e5 feat(feishu): 完整实现飞书适配器核心模块
新增飞书机器人适配器全套功能,包括:
- 基础适配器入口与工具导出
- 消息格式化、卡片渲染、回复调度逻辑
- 会话ID生成、模型覆盖策略
- 消息发送缓存、顺序队列管理
- 飞书签名验证、加解密webhook请求
- 审批权限校验、机器人菜单事件处理
- 文档评论、钉消息、语音转码处理
- 静态/动态目录管理、子代理生命周期管理
- 各类工具集:聊天、云盘、文档、知识库API封装
2026-05-12 00:43:59 +08:00

127 lines
4.5 KiB
Python

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