Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
飞书通讯录 API 客户端
|
||||
封装飞书开放平台部门和用户相关接口
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
|
||||
|
||||
|
||||
class FeishuClient:
|
||||
"""飞书通讯录 API 客户端"""
|
||||
|
||||
def __init__(self, app_id: str, app_secret: str):
|
||||
self.app_id = app_id
|
||||
self.app_secret = app_secret
|
||||
self._tenant_access_token: Optional[str] = None
|
||||
self._token_expires_at: float = 0
|
||||
|
||||
async def get_tenant_access_token(self) -> str:
|
||||
"""获取 tenant_access_token(带内存缓存,提前 5 分钟过期)"""
|
||||
now = time.time()
|
||||
if self._tenant_access_token and now < self._token_expires_at:
|
||||
return self._tenant_access_token
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.post(
|
||||
f"{FEISHU_API_BASE}/auth/v3/tenant_access_token/internal",
|
||||
json={"app_id": self.app_id, "app_secret": self.app_secret},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
|
||||
if result.get("code") != 0:
|
||||
raise Exception(f"获取飞书 tenant_access_token 失败: {result.get('msg', result)}")
|
||||
|
||||
self._tenant_access_token = result["tenant_access_token"]
|
||||
self._token_expires_at = now + result.get("expire", 7200) - 300
|
||||
return self._tenant_access_token
|
||||
|
||||
async def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""带 Bearer token 的 GET 请求"""
|
||||
token = await self.get_tenant_access_token()
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.get(
|
||||
f"{FEISHU_API_BASE}{path}",
|
||||
params=params,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
if result.get("code") != 0:
|
||||
raise Exception(f"飞书 API 调用失败 [{path}]: {result.get('msg', result)}")
|
||||
return result
|
||||
|
||||
async def _post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""带 Bearer token 的 POST 请求"""
|
||||
token = await self.get_tenant_access_token()
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.post(
|
||||
f"{FEISHU_API_BASE}{path}",
|
||||
json=body,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
if result.get("code") != 0:
|
||||
raise Exception(f"飞书 API 调用失败 [{path}]: {result.get('msg', result)}")
|
||||
return result
|
||||
|
||||
# ==================== 连接测试 ====================
|
||||
|
||||
async def test_connection(self) -> Dict[str, Any]:
|
||||
"""测试连接:获取 token + 拉取根部门信息验证"""
|
||||
await self.get_tenant_access_token()
|
||||
result = await self._get("/contact/v3/departments/0", {
|
||||
"department_id_type": "open_department_id",
|
||||
})
|
||||
dept_info = result.get("data", {}).get("department", {})
|
||||
return {
|
||||
"success": True,
|
||||
"corp_name": dept_info.get("name", ""),
|
||||
"dept_id": dept_info.get("open_department_id", "0"),
|
||||
}
|
||||
|
||||
# ==================== 部门 API ====================
|
||||
|
||||
async def get_dept_children(
|
||||
self,
|
||||
dept_id: str = "0",
|
||||
page_size: int = 50,
|
||||
page_token: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取子部门列表(分页)
|
||||
https://open.feishu.cn/document/server-docs/contact-v3/department/children
|
||||
"""
|
||||
params: Dict[str, Any] = {
|
||||
"department_id_type": "open_department_id",
|
||||
"page_size": page_size,
|
||||
}
|
||||
if page_token:
|
||||
params["page_token"] = page_token
|
||||
|
||||
result = await self._get(f"/contact/v3/departments/{dept_id}/children", params)
|
||||
return result.get("data", {})
|
||||
|
||||
async def get_dept_detail(self, dept_id: str) -> Dict[str, Any]:
|
||||
"""获取部门详情"""
|
||||
result = await self._get(f"/contact/v3/departments/{dept_id}", {
|
||||
"department_id_type": "open_department_id",
|
||||
})
|
||||
return result.get("data", {}).get("department", {})
|
||||
|
||||
async def get_all_depts(self, root_dept_id: str = "0") -> List[Dict[str, Any]]:
|
||||
"""
|
||||
递归获取指定根部门下的全量部门列表(广度优先 + 自动分页)
|
||||
返回扁平列表,包含 parent_department_id 信息
|
||||
"""
|
||||
all_depts: List[Dict[str, Any]] = []
|
||||
queue = [root_dept_id]
|
||||
|
||||
while queue:
|
||||
current_dept_id = queue.pop(0)
|
||||
page_token = None
|
||||
|
||||
while True:
|
||||
data = await self.get_dept_children(current_dept_id, page_size=50, page_token=page_token)
|
||||
items = data.get("items", [])
|
||||
for item in items:
|
||||
all_depts.append(item)
|
||||
child_id = item.get("open_department_id")
|
||||
if child_id:
|
||||
queue.append(child_id)
|
||||
|
||||
if not data.get("has_more", False):
|
||||
break
|
||||
page_token = data.get("page_token")
|
||||
|
||||
return all_depts
|
||||
|
||||
async def get_dept_tree(self, root_dept_id: str = "0") -> List[Dict[str, Any]]:
|
||||
"""获取部门树形结构(供前端选择同步范围)"""
|
||||
all_depts = await self.get_all_depts(root_dept_id)
|
||||
|
||||
dept_map: Dict[str, Dict[str, Any]] = {}
|
||||
for d in all_depts:
|
||||
did = d.get("open_department_id", "")
|
||||
dept_map[did] = {
|
||||
"dept_id": did,
|
||||
"name": d.get("name", ""),
|
||||
"parent_department_id": d.get("parent_department_id", "0"),
|
||||
"children": [],
|
||||
}
|
||||
|
||||
tree: List[Dict[str, Any]] = []
|
||||
for d in dept_map.values():
|
||||
parent_id = d["parent_department_id"]
|
||||
if parent_id in dept_map and parent_id != d["dept_id"]:
|
||||
dept_map[parent_id]["children"].append(d)
|
||||
else:
|
||||
tree.append(d)
|
||||
|
||||
return tree
|
||||
|
||||
# ==================== 用户 API ====================
|
||||
|
||||
async def get_user_list(
|
||||
self,
|
||||
dept_id: str,
|
||||
page_size: int = 50,
|
||||
page_token: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取部门用户列表(分页)
|
||||
https://open.feishu.cn/document/server-docs/contact-v3/user/find_by_department
|
||||
"""
|
||||
params: Dict[str, Any] = {
|
||||
"department_id": dept_id,
|
||||
"department_id_type": "open_department_id",
|
||||
"page_size": page_size,
|
||||
}
|
||||
if page_token:
|
||||
params["page_token"] = page_token
|
||||
|
||||
result = await self._get("/contact/v3/users/find_by_department", params)
|
||||
return result.get("data", {})
|
||||
|
||||
async def get_all_users_in_dept(self, dept_id: str) -> List[Dict[str, Any]]:
|
||||
"""获取部门下所有用户(自动翻页)"""
|
||||
users: List[Dict[str, Any]] = []
|
||||
page_token = None
|
||||
|
||||
while True:
|
||||
data = await self.get_user_list(dept_id, page_size=50, page_token=page_token)
|
||||
items = data.get("items", [])
|
||||
users.extend(items)
|
||||
|
||||
if not data.get("has_more", False):
|
||||
break
|
||||
page_token = data.get("page_token")
|
||||
|
||||
return users
|
||||
|
||||
async def get_user_detail(self, user_id: str) -> Dict[str, Any]:
|
||||
"""获取用户详情"""
|
||||
result = await self._get(f"/contact/v3/users/{user_id}", {
|
||||
"department_id_type": "open_department_id",
|
||||
})
|
||||
return result.get("data", {}).get("user", {})
|
||||
Reference in New Issue
Block a user