1. 移除多个文件中的空行、冗余导入
2. 修复文件末尾缺少换行符的问题
3. 新增并补全飞书多类工具API实现:
- 多维表格:更新、删除记录,列出视图
- 文档:更新、追加、删除块
- 云文档:上传、下载文件
- 群组:创建、添加成员、更新信息、创建公告
- 目录:重构用户部门缓存逻辑
4. 优化消息发送、回复、转发等API的错误处理和逻辑
5. 新增消息列表查询、已读状态查询等功能
185 lines
6.0 KiB
Python
185 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import time
|
|
from typing import Any
|
|
|
|
PAGE_SIZE_DEFAULT = 100
|
|
PAGE_SIZE_MAX = 200
|
|
|
|
CACHE_TTL_USERS_S = 300
|
|
CACHE_TTL_DEPARTMENTS_S = 600
|
|
|
|
|
|
class FeishuDirectoryClient:
|
|
def __init__(self, api_client: Any, page_size: int = PAGE_SIZE_DEFAULT):
|
|
self._api = api_client
|
|
self._page_size = min(page_size, PAGE_SIZE_MAX)
|
|
self._lock = asyncio.Lock()
|
|
self._user_cache: dict[str, tuple[dict, float]] = {}
|
|
self._user_list_cache: list[dict] = []
|
|
self._user_list_age: float = 0
|
|
self._dept_cache: dict[str, tuple[dict, float]] = {}
|
|
self._dept_list_cache: list[dict] = []
|
|
self._dept_list_age: float = 0
|
|
|
|
async def get_user(self, user_id: str, *, user_id_type: str = "open_id") -> dict | None:
|
|
cache_key = f"{user_id_type}:{user_id}"
|
|
async with self._lock:
|
|
cached = self._user_cache.get(cache_key)
|
|
if cached is not None:
|
|
data, ts = cached
|
|
if time.monotonic() - ts < CACHE_TTL_USERS_S:
|
|
return data
|
|
|
|
path = f"/open-apis/contact/v3/users/{user_id}"
|
|
try:
|
|
resp = await self._api.get(path, user_id_type=user_id_type)
|
|
user = resp.get("data", {}).get("user", {})
|
|
except Exception:
|
|
return None
|
|
|
|
async with self._lock:
|
|
if user:
|
|
self._user_cache[cache_key] = (user, time.monotonic())
|
|
else:
|
|
self._user_cache.pop(cache_key, None)
|
|
|
|
return user
|
|
|
|
async def list_users(
|
|
self,
|
|
*,
|
|
department_id: str = "",
|
|
page_token: str = "",
|
|
page_size: int = 0,
|
|
) -> dict[str, Any]:
|
|
size = min(page_size or self._page_size, PAGE_SIZE_MAX)
|
|
path = "/open-apis/contact/v3/users"
|
|
params: dict[str, Any] = {"page_size": size}
|
|
if department_id:
|
|
params["department_id"] = department_id
|
|
if page_token:
|
|
params["page_token"] = page_token
|
|
|
|
if not department_id and not page_token:
|
|
async with self._lock:
|
|
if time.monotonic() - self._user_list_age < CACHE_TTL_USERS_S and self._user_list_cache:
|
|
return {"items": list(self._user_list_cache), "has_more": False, "page_token": ""}
|
|
|
|
resp = await self._api.get(path, **params)
|
|
data = resp.get("data", {})
|
|
items = data.get("items", [])
|
|
|
|
if not department_id and not page_token:
|
|
async with self._lock:
|
|
self._user_list_cache = items
|
|
self._user_list_age = time.monotonic()
|
|
|
|
return {
|
|
"items": items,
|
|
"has_more": data.get("has_more", False),
|
|
"page_token": data.get("page_token", ""),
|
|
}
|
|
|
|
async def get_department(self, department_id: str) -> dict | None:
|
|
cache_key = department_id
|
|
async with self._lock:
|
|
cached = self._dept_cache.get(cache_key)
|
|
if cached is not None:
|
|
data, ts = cached
|
|
if time.monotonic() - ts < CACHE_TTL_DEPARTMENTS_S:
|
|
return data
|
|
|
|
path = f"/open-apis/contact/v3/departments/{department_id}"
|
|
try:
|
|
resp = await self._api.get(path)
|
|
dept = resp.get("data", {}).get("department", {})
|
|
except Exception:
|
|
return None
|
|
|
|
async with self._lock:
|
|
if dept:
|
|
self._dept_cache[cache_key] = (dept, time.monotonic())
|
|
else:
|
|
self._dept_cache.pop(cache_key, None)
|
|
|
|
return dept
|
|
|
|
async def list_departments(
|
|
self,
|
|
*,
|
|
parent_department_id: str = "",
|
|
page_token: str = "",
|
|
page_size: int = 0,
|
|
) -> dict[str, Any]:
|
|
size = min(page_size or self._page_size, PAGE_SIZE_MAX)
|
|
path = "/open-apis/contact/v3/departments"
|
|
params: dict[str, Any] = {"page_size": size}
|
|
if parent_department_id:
|
|
params["parent_department_id"] = parent_department_id
|
|
if page_token:
|
|
params["page_token"] = page_token
|
|
|
|
if not parent_department_id and not page_token:
|
|
async with self._lock:
|
|
if time.monotonic() - self._dept_list_age < CACHE_TTL_DEPARTMENTS_S and self._dept_list_cache:
|
|
return {"items": list(self._dept_list_cache), "has_more": False, "page_token": ""}
|
|
|
|
resp = await self._api.get(path, **params)
|
|
data = resp.get("data", {})
|
|
items = data.get("items", [])
|
|
|
|
if not parent_department_id and not page_token:
|
|
async with self._lock:
|
|
self._dept_list_cache = items
|
|
self._dept_list_age = time.monotonic()
|
|
|
|
return {
|
|
"items": items,
|
|
"has_more": data.get("has_more", False),
|
|
"page_token": data.get("page_token", ""),
|
|
}
|
|
|
|
def invalidate_cache(self, scope: str = "all") -> None:
|
|
if scope in ("all", "users"):
|
|
self._user_cache.clear()
|
|
self._user_list_cache.clear()
|
|
self._user_list_age = 0
|
|
if scope in ("all", "departments"):
|
|
self._dept_cache.clear()
|
|
self._dept_list_cache.clear()
|
|
self._dept_list_age = 0
|
|
|
|
|
|
async def list_groups(client: Any) -> list[dict]:
|
|
resp = await client.chat.v1.list(page_size=200)
|
|
if not resp.success():
|
|
return []
|
|
items = []
|
|
data = resp.data
|
|
for group in getattr(data, "items", []) or []:
|
|
items.append(
|
|
{
|
|
"id": getattr(group, "chat_id", ""),
|
|
"name": getattr(group, "name", ""),
|
|
}
|
|
)
|
|
return items
|
|
|
|
|
|
async def list_peers(client: Any) -> list[dict]:
|
|
resp = await client.contact.v3.scope.list()
|
|
if not resp.success():
|
|
return []
|
|
items = []
|
|
data = resp.data
|
|
for user in getattr(data, "items", []) or []:
|
|
items.append(
|
|
{
|
|
"id": getattr(user, "open_id", ""),
|
|
"name": getattr(user, "name", ""),
|
|
}
|
|
)
|
|
return items
|