ForcePilot/backend/package/yuxi/channels/adapters/dingding/directory.py
Kris 9c01d480cc feat(dingding): 完整实现钉钉适配器功能模块
- 新增钉钉工具调用审计日志功能
- 新增参数校验装饰器与基础工具集(文档、审批、多维表格)
- 完善事件类型映射与会话ID生成逻辑
- 新增消息去重、访问策略匹配、配置校验与UI提示
- 新增酷应用卡片、目录管理、表情反应支持
- 优化消息发送逻辑,添加401重试机制
- 新增配置化的权限控制与流式输出支持
2026-05-13 16:06:10 +08:00

187 lines
7.3 KiB
Python

from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
import httpx
if TYPE_CHECKING:
from yuxi.channels.adapters.dingding.token import DingDingTokenManager
logger = logging.getLogger(__name__)
DINGTALK_API_BASE = "https://api.dingtalk.com"
class DingDingDirectoryClient:
def __init__(
self,
token_manager: DingDingTokenManager,
*,
http_client: httpx.AsyncClient | None = None,
):
self._token_manager = token_manager
self._http_client = http_client
async def _get_http_client(self) -> httpx.AsyncClient:
if self._http_client is not None and not self._http_client.is_closed:
return self._http_client
return httpx.AsyncClient(timeout=httpx.Timeout(15))
async def _request(
self,
method: str,
path: str,
*,
params: dict[str, Any] | None = None,
json_data: dict[str, Any] | None = None,
) -> httpx.Response:
token = await self._token_manager.get_token()
url = f"{DINGTALK_API_BASE}{path}"
headers = {
"x-acs-dingtalk-access-token": token,
"Content-Type": "application/json",
}
if self._http_client is not None and not self._http_client.is_closed:
return await self._http_client.request(method, url, headers=headers, params=params, json=json_data)
async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client:
return await client.request(method, url, headers=headers, params=params, json=json_data)
async def get_user_info(self, user_id: str) -> dict[str, Any]:
resp = await self._request("GET", f"/v1.0/contact/users/{user_id}")
if resp.status_code != 200:
logger.warning(f"[DingDing Directory] get_user_info HTTP {resp.status_code}")
return {}
data = resp.json()
return {
"user_id": data.get("openId", data.get("unionId", user_id)),
"name": data.get("nick", data.get("name", "")),
"avatar_url": data.get("avatarUrl", data.get("avatar", "")),
"email": data.get("email", ""),
"mobile": data.get("mobile", ""),
"title": data.get("title", data.get("position", "")),
"dept_ids": data.get("deptIdList", data.get("departmentIds", [])),
"raw": data,
}
async def list_peers(self, *, page_size: int = 100, page_token: str = "") -> dict[str, Any]:
try:
size = min(page_size, 200)
params: dict[str, Any] = {"maxResults": size}
if page_token:
params["nextToken"] = page_token
resp = await self._request("GET", "/v1.0/contact/users", params=params)
if resp.status_code != 200:
logger.warning(f"[DingDing Directory] list_peers HTTP {resp.status_code}")
return {"items": [], "has_more": False, "page_token": ""}
data = resp.json()
items = data.get("items", data.get("userList", []))
peers = []
for item in items:
peers.append({
"id": item.get("openId", item.get("userId", "")),
"name": item.get("nick", item.get("name", "")),
"avatar_url": item.get("avatarUrl", item.get("avatar", "")),
})
return {
"items": peers,
"has_more": data.get("hasMore", False),
"page_token": data.get("nextToken", ""),
}
except Exception as e:
logger.error(f"[DingDing Directory] list_peers failed: {e}")
return {"items": [], "has_more": False, "page_token": "", "error": str(e)}
async def list_groups(self, *, page_size: int = 100, page_token: str = "") -> dict[str, Any]:
try:
size = min(page_size, 200)
params: dict[str, Any] = {"maxResults": size}
if page_token:
params["nextToken"] = page_token
resp = await self._request("GET", "/v1.0/im/conversations", params=params)
if resp.status_code != 200:
logger.warning(f"[DingDing Directory] list_groups HTTP {resp.status_code}")
return {"items": [], "has_more": False, "page_token": ""}
data = resp.json()
items = data.get("items", data.get("conversationList", []))
groups = []
for item in items:
groups.append({
"id": item.get("openConversationId", ""),
"name": item.get("title", item.get("name", "")),
"avatar_url": item.get("icon", item.get("avatarUrl", "")),
"member_count": item.get("memberCount", 0),
"type": item.get("conversationType", "group"),
})
return {
"items": groups,
"has_more": data.get("hasMore", False),
"page_token": data.get("nextToken", ""),
}
except Exception as e:
logger.error(f"[DingDing Directory] list_groups failed: {e}")
return {"items": [], "has_more": False, "page_token": "", "error": str(e)}
async def get_channel_info(self, chat_id: str) -> dict[str, Any]:
open_conv_id = chat_id.replace("group_", "").replace("dm_", "")
try:
resp = await self._request("GET", f"/v1.0/im/conversations/{open_conv_id}")
if resp.status_code != 200:
logger.warning(f"[DingDing Directory] get_channel_info HTTP {resp.status_code}")
return {"chat_id": chat_id, "name": "", "member_count": 0}
data = resp.json()
return {
"chat_id": chat_id,
"name": data.get("title", data.get("name", "")),
"avatar_url": data.get("icon", data.get("avatarUrl", "")),
"member_count": data.get("memberCount", 0),
"type": data.get("conversationType", "group"),
"owner_id": data.get("ownerUserId", ""),
"create_time": data.get("createTime", ""),
}
except Exception as e:
logger.error(f"[DingDing Directory] get_channel_info failed: {e}")
return {"chat_id": chat_id, "name": "", "member_count": 0, "error": str(e)}
async def search_users(
self,
query: str,
*,
page_size: int = 20,
) -> list[dict[str, Any]]:
try:
size = min(page_size, 100)
params: dict[str, Any] = {
"maxResults": size,
"searchKey": query,
}
resp = await self._request("GET", "/v1.0/contact/users", params=params)
if resp.status_code != 200:
return []
data = resp.json()
items = data.get("items", data.get("userList", []))
return [
{
"user_id": item.get("openId", item.get("userId", "")),
"name": item.get("nick", item.get("name", "")),
"avatar_url": item.get("avatarUrl", item.get("avatar", "")),
"email": item.get("email", ""),
"mobile": item.get("mobile", ""),
}
for item in items
]
except Exception as e:
logger.error(f"[DingDing Directory] search_users failed: {e}")
return []