ForcePilot/backend/package/yuxi/channel/extensions/feishu/client.py
Kris 5e91bb9985 feat(feishu): 新增飞书渠道完整插件实现
新增飞书渠道插件,包含基础配置、事件处理、消息收发、工具调用、权限管理等完整功能模块,支持WebSocket长连接、卡片消息、流式响应、群管理、审批通知等能力
2026-05-21 10:46:42 +08:00

445 lines
14 KiB
Python

from __future__ import annotations
import logging
import time
from typing import Any
logger = logging.getLogger(__name__)
try:
import lark_oapi
HAS_LARK_SDK = True
except ImportError:
HAS_LARK_SDK = False
lark_oapi = None
_client_cache: dict[str, Any] = {}
_CACHE_TTL = 3600
MAX_CLIENTS = 32
def get_client(app_id: str, app_secret: str, domain: str = "feishu", http_timeout_ms: int = 30000) -> Any:
if not HAS_LARK_SDK:
raise ImportError("lark-oapi SDK is required. Install with: pip install lark-oapi")
domain_key = domain.lower()
if domain_key in ("lark", "larksuite"):
base_url = "https://open.larksuite.com"
elif domain_key.startswith("http"):
base_url = domain_key.rstrip("/")
else:
base_url = "https://open.feishu.cn"
cache_key = f"{app_id}:{base_url}"
entry = _client_cache.get(cache_key)
now = time.monotonic()
if entry and now - entry["ts"] < _CACHE_TTL and entry["secret"] == app_secret:
return entry["client"]
timeout_seconds = max(http_timeout_ms, 1000) / 1000.0
client = lark_oapi.Client.builder() \
.app_id(app_id) \
.app_secret(app_secret) \
.open_api_url(base_url) \
.timeout(timeout_seconds) \
.build()
_client_cache[cache_key] = {"client": client, "ts": now, "secret": app_secret}
if len(_client_cache) > MAX_CLIENTS:
oldest = min(_client_cache, key=lambda k: _client_cache[k]["ts"])
del _client_cache[oldest]
return client
def invalidate_client(app_id: str, domain: str = "feishu") -> None:
domain_key = domain.lower()
if domain_key in ("lark", "larksuite"):
base_url = "https://open.larksuite.com"
elif domain_key.startswith("http"):
base_url = domain_key.rstrip("/")
else:
base_url = "https://open.feishu.cn"
cache_key = f"{app_id}:{base_url}"
_client_cache.pop(cache_key, None)
def get_ws_client(app_id: str, app_secret: str, domain: str = "feishu") -> Any:
if not HAS_LARK_SDK:
raise ImportError("lark-oapi SDK is required")
domain_key = domain.lower()
if domain_key in ("lark", "larksuite"):
base_url = "https://open.larksuite.com"
elif domain_key.startswith("http"):
base_url = domain_key.rstrip("/")
else:
base_url = "https://open.feishu.cn"
return lark_oapi.ws.Client.builder() \
.app_id(app_id) \
.app_secret(app_secret) \
.open_api_url(base_url) \
.build()
async def get_bot_info(client: Any) -> dict:
resp = await client.im.v1.bot_info.get_async()
if resp.success():
data = resp.data
bot = getattr(data, "bot", {}) or {}
return {
"bot_name": getattr(bot, "app_name", "") if hasattr(bot, "app_name") else "",
"activate_status": getattr(bot, "activate_status", 0) if hasattr(bot, "activate_status") else 0,
"open_id": getattr(bot, "open_id", "") if hasattr(bot, "open_id") else "",
}
return {}
async def get_chat_info(client: Any, chat_id: str) -> dict | None:
resp = await client.im.v1.chat.get_async(lark_oapi.im.v1.GetChatRequest(chat_id=chat_id))
if resp.success() and resp.data:
return {
"name": getattr(resp.data, "name", ""),
"description": getattr(resp.data, "description", ""),
"chat_type": getattr(resp.data, "chat_type", ""),
"owner_id": getattr(resp.data, "owner_id", ""),
"owner_id_type": getattr(resp.data, "owner_id_type", ""),
}
return None
async def get_chat_members(client: Any, chat_id: str, page_token: str = "", page_size: int = 100) -> dict:
resp = await client.im.v1.chat_members.get_async(
lark_oapi.im.v1.GetChatMembersRequest(
chat_id=chat_id,
page_token=page_token,
page_size=page_size,
member_id_type="open_id",
)
)
if resp.success() and resp.data:
items = getattr(resp.data, "items", []) or []
return {
"members": [
{
"member_id": getattr(m, "member_id", ""),
"name": getattr(m, "name", ""),
"member_type": getattr(m, "member_type", ""),
}
for m in items
],
"page_token": getattr(resp.data, "page_token", ""),
"has_more": getattr(resp.data, "has_more", False),
}
return {"members": [], "page_token": "", "has_more": False}
async def get_user_info(client: Any, user_id: str) -> dict | None:
resp = await client.contact.v3.user.get_async(
lark_oapi.contact.v3.GetUserRequest(
user_id=user_id,
user_id_type="open_id",
)
)
if resp.success() and resp.data and resp.data.user:
u = resp.data.user
return {
"open_id": getattr(u, "open_id", ""),
"name": getattr(u, "name", ""),
"avatar_url": getattr(u, "avatar_url", ""),
"email": getattr(u, "email", ""),
"mobile": getattr(u, "mobile", ""),
"department_ids": getattr(u, "department_ids", []) or [],
}
return None
async def get_message(client: Any, message_id: str) -> dict | None:
resp = await client.im.v1.message.get_async(
lark_oapi.im.v1.GetMessageRequest(message_id=message_id)
)
if resp.success() and resp.data:
items = getattr(resp.data, "items", []) or []
if items:
item = items[0]
return {
"message_id": getattr(item, "message_id", ""),
"msg_type": getattr(item, "msg_type", ""),
"body": getattr(item, "body", {}),
"sender": {
"id": getattr(item, "sender", {}).get("id", ""),
"id_type": getattr(item, "sender", {}).get("id_type", ""),
},
"root_id": getattr(item, "root_id", ""),
"parent_id": getattr(item, "parent_id", ""),
"thread_id": getattr(item, "thread_id", ""),
}
return None
async def list_messages(
client: Any,
container_id_type: str = "chat",
container_id: str = "",
page_token: str = "",
page_size: int = 20,
start_time: str = "",
end_time: str = "",
sort_type: str = "ByCreateTimeDesc",
) -> dict:
req = lark_oapi.im.v1.ListMessageRequest(
container_id_type=container_id_type,
container_id=container_id,
page_token=page_token if page_token else None,
page_size=page_size,
start_time=start_time if start_time else None,
end_time=end_time if end_time else None,
sort_type=sort_type,
)
resp = await client.im.v1.message.list_async(req)
if resp.success() and resp.data:
items = getattr(resp.data, "items", []) or []
return {
"messages": [
{
"message_id": getattr(m, "message_id", ""),
"msg_type": getattr(m, "msg_type", ""),
"root_id": getattr(m, "root_id", ""),
"parent_id": getattr(m, "parent_id", ""),
"thread_id": getattr(m, "thread_id", ""),
"create_time": getattr(m, "create_time", ""),
}
for m in items
],
"has_more": getattr(resp.data, "has_more", False),
"page_token": getattr(resp.data, "page_token", ""),
}
return {"messages": [], "has_more": False, "page_token": ""}
async def batch_get_users(client: Any, user_ids: list[str]) -> list[dict]:
if not user_ids:
return []
results = []
for user_id in user_ids:
info = await get_user_info(client, user_id)
if info:
results.append(info)
return results
async def create_chat(
client: Any,
name: str,
description: str = "",
user_ids: list[str] | None = None,
chat_type: str = "group",
) -> dict:
resp = await client.im.v1.chat.create_async(
lark_oapi.im.v1.CreateChatRequest(
request_body=lark_oapi.im.v1.CreateChatRequestBody(
name=name,
description=description,
user_ids=user_ids or [],
chat_type=chat_type,
),
)
)
if resp.success() and resp.data:
return {
"success": True,
"chat_id": getattr(resp.data, "chat_id", ""),
}
return {"success": False, "error": getattr(resp, "msg", "")}
async def update_chat(
client: Any,
chat_id: str,
name: str = "",
description: str = "",
) -> dict:
body = {}
if name:
body["name"] = name
if description:
body["description"] = description
resp = await client.im.v1.chat.update_async(
lark_oapi.im.v1.UpdateChatRequest(
chat_id=chat_id,
request_body=lark_oapi.im.v1.UpdateChatRequestBody(**body),
)
)
if resp.success():
return {"success": True}
return {"success": False, "error": getattr(resp, "msg", "")}
async def delete_chat(client: Any, chat_id: str) -> dict:
resp = await client.im.v1.chat.delete_async(
lark_oapi.im.v1.DeleteChatRequest(chat_id=chat_id)
)
if resp.success():
return {"success": True}
return {"success": False, "error": getattr(resp, "msg", "")}
async def add_chat_members(
client: Any,
chat_id: str,
member_ids: list[str],
) -> dict:
resp = await client.im.v1.chat_members.create_async(
lark_oapi.im.v1.CreateChatMembersRequest(
chat_id=chat_id,
member_id_type="open_id",
request_body=lark_oapi.im.v1.CreateChatMembersRequestBody(
id_list=member_ids,
),
)
)
if resp.success():
return {"success": True}
return {"success": False, "error": getattr(resp, "msg", "")}
async def remove_chat_members(
client: Any,
chat_id: str,
member_ids: list[str],
) -> dict:
resp = await client.im.v1.chat_members.delete_async(
lark_oapi.im.v1.DeleteChatMembersRequest(
chat_id=chat_id,
member_id_type="open_id",
request_body=lark_oapi.im.v1.DeleteChatMembersRequestBody(
id_list=member_ids,
),
)
)
if resp.success():
return {"success": True}
return {"success": False, "error": getattr(resp, "msg", "")}
async def list_user_chats(
client: Any,
user_id: str = "",
page_token: str = "",
page_size: int = 50,
) -> dict:
req = lark_oapi.im.v1.ListChatRequest(
user_id_type="open_id" if user_id else None,
page_token=page_token if page_token else None,
page_size=page_size,
)
resp = await client.im.v1.chat.list_async(req)
if resp.success() and resp.data:
items = getattr(resp.data, "items", []) or []
return {
"chats": [
{
"chat_id": getattr(c, "chat_id", ""),
"name": getattr(c, "name", ""),
"chat_type": getattr(c, "chat_type", ""),
}
for c in items
],
"has_more": getattr(resp.data, "has_more", False),
"page_token": getattr(resp.data, "page_token", ""),
}
return {"chats": [], "has_more": False, "page_token": ""}
async def search_chats(
client: Any,
query: str = "",
page_token: str = "",
page_size: int = 50,
) -> dict:
req = lark_oapi.im.v1.SearchChatRequest(
query=query if query else None,
page_token=page_token if page_token else None,
page_size=page_size,
)
resp = await client.im.v1.chat.search_async(req)
if resp.success() and resp.data:
items = getattr(resp.data, "items", []) or []
return {
"chats": [
{
"chat_id": getattr(c, "chat_id", ""),
"name": getattr(c, "name", ""),
"chat_type": getattr(c, "chat_type", ""),
}
for c in items
],
"has_more": getattr(resp.data, "has_more", False),
"page_token": getattr(resp.data, "page_token", ""),
}
return {"chats": [], "has_more": False, "page_token": ""}
async def list_departments(
client: Any,
parent_department_id: str = "0",
page_token: str = "",
page_size: int = 50,
) -> dict:
resp = await client.contact.v3.department.list_async(
lark_oapi.contact.v3.ListDepartmentRequest(
parent_department_id=parent_department_id,
page_token=page_token if page_token else None,
page_size=page_size,
)
)
if resp.success() and resp.data:
items = getattr(resp.data, "items", []) or []
return {
"departments": [
{
"department_id": getattr(d, "department_id", ""),
"name": getattr(d, "name", ""),
"parent_department_id": getattr(d, "parent_department_id", ""),
}
for d in items
],
"has_more": getattr(resp.data, "has_more", False),
"page_token": getattr(resp.data, "page_token", ""),
}
return {"departments": [], "has_more": False, "page_token": ""}
async def find_users_by_department(
client: Any,
department_id: str,
page_token: str = "",
page_size: int = 50,
) -> dict:
resp = await client.contact.v3.user.find_by_department_async(
lark_oapi.contact.v3.FindByDepartmentUserRequest(
department_id=department_id,
page_token=page_token if page_token else None,
page_size=page_size,
)
)
if resp.success() and resp.data:
items = getattr(resp.data, "items", []) or []
return {
"users": [
{
"open_id": getattr(u, "open_id", ""),
"name": getattr(u, "name", ""),
"email": getattr(u, "email", ""),
}
for u in items
],
"has_more": getattr(resp.data, "has_more", False),
"page_token": getattr(resp.data, "page_token", ""),
}
return {"users": [], "has_more": False, "page_token": ""}