- 新增钉钉工具调用审计日志功能 - 新增参数校验装饰器与基础工具集(文档、审批、多维表格) - 完善事件类型映射与会话ID生成逻辑 - 新增消息去重、访问策略匹配、配置校验与UI提示 - 新增酷应用卡片、目录管理、表情反应支持 - 优化消息发送逻辑,添加401重试机制 - 新增配置化的权限控制与流式输出支持
187 lines
7.3 KiB
Python
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 []
|