新增飞书机器人适配器全套功能,包括: - 基础适配器入口与工具导出 - 消息格式化、卡片渲染、回复调度逻辑 - 会话ID生成、模型覆盖策略 - 消息发送缓存、顺序队列管理 - 飞书签名验证、加解密webhook请求 - 审批权限校验、机器人菜单事件处理 - 文档评论、钉消息、语音转码处理 - 静态/动态目录管理、子代理生命周期管理 - 各类工具集:聊天、云盘、文档、知识库API封装
95 lines
3.5 KiB
Python
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()
|