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

95 lines
3.5 KiB
Python

from __future__ import annotations
from typing import Any
class FeishuApiClient:
"""飞书 Open API HTTP 客户端,封装通用请求逻辑。"""
DEFAULT_TIMEOUT_S = 30.0
def __init__(self, lark_client, http_client: Any | None = None, timeout: float | None = None):
self._lark = lark_client
self._http = http_client
self._timeout = timeout or self.DEFAULT_TIMEOUT_S
@property
def domain(self) -> str:
return getattr(self._lark, "domain", "https://open.feishu.cn")
async def _get_token(self) -> str:
resp = self._lark.auth.tenant_access_token_internal()
if not resp.success():
raise RuntimeError(f"Token acquisition failed: {resp.msg}")
return resp.token
async def get(self, path: str, **params: Any) -> dict:
import httpx
token = await self._get_token()
client = self._http or httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
try:
headers = {"Authorization": f"Bearer {token}"}
url = f"{self.domain}{path}"
resp = await client.get(url, headers=headers, params=params)
if resp.status_code != 200:
raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}")
return resp.json() if resp.text else {}
finally:
if self._http is None:
await client.aclose()
async def post(self, path: str, body: dict | None = None) -> dict:
import httpx
token = await self._get_token()
client = self._http or httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
try:
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
url = f"{self.domain}{path}"
resp = await client.post(url, headers=headers, json=body)
if resp.status_code != 200:
raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}")
return resp.json() if resp.text else {}
finally:
if self._http is None:
await client.aclose()
async def patch(self, path: str, body: dict) -> dict:
import httpx
token = await self._get_token()
client = self._http or httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
try:
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
url = f"{self.domain}{path}"
resp = await client.patch(url, headers=headers, json=body)
if resp.status_code != 200:
raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}")
return resp.json() if resp.text else {}
finally:
if self._http is None:
await client.aclose()
async def delete(self, path: str) -> dict:
import httpx
token = await self._get_token()
client = self._http or httpx.AsyncClient(timeout=httpx.Timeout(self._timeout))
try:
headers = {"Authorization": f"Bearer {token}"}
url = f"{self.domain}{path}"
resp = await client.delete(url, headers=headers)
if resp.status_code != 200:
raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}")
return resp.json() if resp.text else {}
finally:
if self._http is None:
await client.aclose()