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

231 lines
7.5 KiB
Python

from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
import httpx
from yuxi.channels.models import DeliveryResult
if TYPE_CHECKING:
from yuxi.channels.adapters.dingding.token import DingDingTokenManager
logger = logging.getLogger(__name__)
DINGDING_EMOJI_MAP: dict[str, tuple[str, str]] = {
"👍": ("101", "like"),
"👎": ("102", "dislike"),
"❤️": ("103", "heart"),
"😂": ("104", "laugh"),
"😮": ("105", "surprise"),
"😢": ("106", "sad"),
"👀": ("107", "looking"),
"": ("108", "done"),
"": ("109", "error"),
"🤔": ("110", "thinking"),
"🎉": ("111", "celebrate"),
"🔥": ("112", "fire"),
"💯": ("113", "hundred"),
"🙏": ("114", "pray"),
}
EMOTION_TYPE_TO_EMOJI: dict[str, str] = {emotion_type: emoji for emoji, (emotion_type, _) in DINGDING_EMOJI_MAP.items()}
EMOTION_NAME_TO_EMOJI: dict[str, str] = {name: emoji for emoji, (_, name) in DINGDING_EMOJI_MAP.items()}
def resolve_emoji(emoji_or_name: str) -> tuple[str, str] | None:
if emoji_or_name in DINGDING_EMOJI_MAP:
return DINGDING_EMOJI_MAP[emoji_or_name]
lowered = emoji_or_name.lower()
for emoji, (emotion_type, name) in DINGDING_EMOJI_MAP.items():
if name == lowered:
return (emotion_type, name)
return None
async def send_reaction(
token_manager: DingDingTokenManager,
open_conversation_id: str,
robot_code: str,
msg_id: str,
emoji: str,
*,
http_client: httpx.AsyncClient | None = None,
) -> DeliveryResult:
emotion_info = DINGDING_EMOJI_MAP.get(emoji)
if not emotion_info:
emotion_info = resolve_emoji(emoji)
if not emotion_info:
return DeliveryResult(success=False, error=f"Unsupported emoji: {emoji}")
emotion_type, emotion_name = emotion_info
try:
token = await token_manager.get_token()
url = "https://api.dingtalk.com/v1.0/robot/emotion/reply"
headers = {
"x-acs-dingtalk-access-token": token,
"Content-Type": "application/json",
}
payload = {
"robotCode": robot_code,
"openMsgId": msg_id,
"openConversationId": open_conversation_id,
"emotionType": int(emotion_type),
"emotionName": emotion_name,
}
if http_client is not None:
resp = await http_client.post(url, json=payload, headers=headers)
else:
async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client:
resp = await client.post(url, json=payload, headers=headers)
if resp.status_code != 200:
return DeliveryResult(
success=False, error=f"DingDing emotion API HTTP {resp.status_code}: {resp.text[:200]}"
)
return DeliveryResult(success=True)
except Exception as e:
logger.error(f"[DingDing] send_reaction failed: {e}")
return DeliveryResult(success=False, error=str(e))
async def remove_reaction(
token_manager: DingDingTokenManager,
open_conversation_id: str,
robot_code: str,
msg_id: str,
emoji: str,
*,
http_client: httpx.AsyncClient | None = None,
) -> DeliveryResult:
emotion_info = DINGDING_EMOJI_MAP.get(emoji)
if not emotion_info:
emotion_info = resolve_emoji(emoji)
if not emotion_info:
return DeliveryResult(success=False, error=f"Unsupported emoji: {emoji}")
emotion_type, emotion_name = emotion_info
try:
token = await token_manager.get_token()
url = "https://api.dingtalk.com/v1.0/robot/emotion/remove"
headers = {
"x-acs-dingtalk-access-token": token,
"Content-Type": "application/json",
}
payload = {
"robotCode": robot_code,
"openMsgId": msg_id,
"openConversationId": open_conversation_id,
"emotionType": int(emotion_type),
}
if http_client is not None:
resp = await http_client.post(url, json=payload, headers=headers)
else:
async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client:
resp = await client.post(url, json=payload, headers=headers)
if resp.status_code != 200:
return DeliveryResult(
success=False, error=f"DingDing emotion remove API HTTP {resp.status_code}: {resp.text[:200]}"
)
return DeliveryResult(success=True)
except Exception as e:
logger.error(f"[DingDing] remove_reaction failed: {e}")
return DeliveryResult(success=False, error=str(e))
async def list_reactions(
token_manager: DingDingTokenManager,
open_conversation_id: str,
robot_code: str,
msg_id: str,
*,
http_client: httpx.AsyncClient | None = None,
) -> dict[str, Any]:
try:
token = await token_manager.get_token()
url = "https://api.dingtalk.com/v1.0/robot/emotion/query"
headers = {
"x-acs-dingtalk-access-token": token,
"Content-Type": "application/json",
}
payload = {
"robotCode": robot_code,
"openMsgId": msg_id,
"openConversationId": open_conversation_id,
}
if http_client is not None:
resp = await http_client.post(url, json=payload, headers=headers)
else:
async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client:
resp = await client.post(url, json=payload, headers=headers)
if resp.status_code != 200:
return {"reactions": [], "total": 0, "error": f"HTTP {resp.status_code}: {resp.text[:200]}"}
data = resp.json()
items = []
for item in data.get("emotionList", data.get("items", [])):
emotion_type = str(item.get("emotionType", ""))
items.append({
"emotion_type": emotion_type,
"emoji": EMOTION_TYPE_TO_EMOJI.get(emotion_type, ""),
"emotion_name": item.get("emotionName", ""),
"count": item.get("count", 0),
"user_id": item.get("userId", ""),
})
return {
"reactions": items,
"total": data.get("totalCount", len(items)),
}
except Exception as e:
logger.error(f"[DingDing] list_reactions failed: {e}")
return {"reactions": [], "total": 0, "error": str(e)}
async def clear_all_bot_reactions(
token_manager: DingDingTokenManager,
open_conversation_id: str,
robot_code: str,
msg_id: str,
*,
http_client: httpx.AsyncClient | None = None,
) -> DeliveryResult:
result = await list_reactions(
token_manager, open_conversation_id, robot_code, msg_id, http_client=http_client
)
items = result.get("reactions", [])
removed = 0
errors = 0
for item in items:
emotion_type = item.get("emotion_type", "")
emoji = EMOTION_TYPE_TO_EMOJI.get(emotion_type, "")
if not emoji:
continue
rm_result = await remove_reaction(
token_manager, open_conversation_id, robot_code, msg_id, emoji, http_client=http_client
)
if rm_result.success:
removed += 1
else:
errors += 1
logger.debug(f"[DingDing] Failed to remove reaction {emoji}: {rm_result.error}")
if errors > 0:
logger.warning(f"[DingDing] clear_all_bot_reactions: removed={removed}, errors={errors}")
return DeliveryResult(success=removed > 0, error=f"Removed {removed}/{removed + errors}")
return DeliveryResult(success=True)