- 新增钉钉工具调用审计日志功能 - 新增参数校验装饰器与基础工具集(文档、审批、多维表格) - 完善事件类型映射与会话ID生成逻辑 - 新增消息去重、访问策略匹配、配置校验与UI提示 - 新增酷应用卡片、目录管理、表情反应支持 - 优化消息发送逻辑,添加401重试机制 - 新增配置化的权限控制与流式输出支持
234 lines
7.1 KiB
Python
234 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from yuxi.channels.models import (
|
|
Attachment,
|
|
ChannelIdentity,
|
|
ChannelMessage,
|
|
ChannelType,
|
|
ChatType,
|
|
EventType,
|
|
MentionsInfo,
|
|
MessageType,
|
|
)
|
|
|
|
from .session import generate_dingding_chat_id, resolve_dingding_chat_type
|
|
|
|
|
|
def map_event_type(raw: dict) -> EventType:
|
|
event_type = raw.get("event_type", raw.get("eventType", ""))
|
|
if event_type == "card_action" or raw.get("action_from") == "card_callback":
|
|
return EventType.CARD_ACTION
|
|
if event_type == "message_updated":
|
|
return EventType.MESSAGE_UPDATED
|
|
if event_type == "message_deleted":
|
|
return EventType.MESSAGE_DELETED
|
|
if event_type == "bot_removed":
|
|
return EventType.BOT_REMOVED
|
|
if event_type == "member_joined":
|
|
return EventType.MEMBER_JOINED
|
|
if event_type == "member_left":
|
|
return EventType.MEMBER_LEFT
|
|
if event_type == "member_added":
|
|
return EventType.MEMBER_ADDED
|
|
if event_type == "member_removed":
|
|
return EventType.MEMBER_REMOVED
|
|
if event_type == "channel_updated":
|
|
return EventType.SYSTEM_EVENT
|
|
if event_type == "reaction_added":
|
|
return EventType.REACTION_ADDED
|
|
if event_type == "reaction_removed":
|
|
return EventType.REACTION_REMOVED
|
|
if raw.get("robotCode"):
|
|
return EventType.BOT_ADDED
|
|
if event_type == "bot_added" or raw.get("event_type") == "bot_added":
|
|
return EventType.BOT_ADDED
|
|
return EventType.MESSAGE_RECEIVED
|
|
|
|
|
|
def map_msg_type(raw: dict) -> MessageType:
|
|
msg_type = raw.get("msgtype", "text")
|
|
_map: dict[str, MessageType] = {
|
|
"text": MessageType.TEXT,
|
|
"image": MessageType.IMAGE,
|
|
"file": MessageType.FILE,
|
|
"voice": MessageType.AUDIO,
|
|
"video": MessageType.VIDEO,
|
|
"markdown": MessageType.TEXT,
|
|
"link": MessageType.TEXT,
|
|
"actionCard": MessageType.CARD,
|
|
"feedCard": MessageType.CARD,
|
|
"oa": MessageType.TEXT,
|
|
"sticker": MessageType.STICKER,
|
|
}
|
|
return _map.get(msg_type, MessageType.TEXT)
|
|
|
|
|
|
def _parse_content(content: Any) -> dict:
|
|
if isinstance(content, dict):
|
|
return content
|
|
if isinstance(content, str):
|
|
try:
|
|
return json.loads(content)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return {}
|
|
return {}
|
|
|
|
|
|
def _describe_media(msg_type: str, parsed: dict, raw: dict | None = None) -> str:
|
|
if msg_type == "image":
|
|
return "[图片]"
|
|
if msg_type == "file":
|
|
filename = parsed.get("fileName", parsed.get("file_name", ""))
|
|
if not filename and raw:
|
|
filename = raw.get("fileName", "")
|
|
return f"[文件: {filename}]" if filename else "[文件]"
|
|
if msg_type == "voice":
|
|
return "[语音]"
|
|
if msg_type == "video":
|
|
return "[视频]"
|
|
if msg_type == "sticker":
|
|
return "[贴纸]"
|
|
return ""
|
|
|
|
|
|
def extract_text(raw: dict) -> str:
|
|
msg_type = raw.get("msgtype", "text")
|
|
text_block = raw.get("text", {})
|
|
|
|
if msg_type in ("image", "file", "voice", "video", "sticker"):
|
|
parsed = _parse_content(raw.get("content", text_block))
|
|
return _describe_media(msg_type, parsed, raw)
|
|
|
|
if isinstance(text_block, dict):
|
|
return text_block.get("content", "")
|
|
if isinstance(text_block, str):
|
|
return text_block
|
|
try:
|
|
return str(text_block) if text_block else ""
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def extract_attachments(raw: dict) -> list[Attachment]:
|
|
msg_type = raw.get("msgtype", "text")
|
|
content = raw.get("content", {})
|
|
|
|
if msg_type == "image":
|
|
download_code = raw.get("downloadCode", "") or raw.get("download_code", "")
|
|
if download_code:
|
|
return [Attachment(type="image", file_id=download_code)]
|
|
return []
|
|
|
|
if msg_type == "file":
|
|
download_code = raw.get("downloadCode", "") or raw.get("download_code", "")
|
|
if download_code:
|
|
return [
|
|
Attachment(
|
|
type="file",
|
|
file_id=download_code,
|
|
filename=raw.get("fileName", ""),
|
|
)
|
|
]
|
|
return []
|
|
|
|
if msg_type == "voice":
|
|
download_code = raw.get("downloadCode", "") or raw.get("download_code", "")
|
|
if download_code:
|
|
return [Attachment(type="audio", file_id=download_code)]
|
|
return []
|
|
|
|
if msg_type == "video":
|
|
download_code = raw.get("downloadCode", "") or raw.get("download_code", "")
|
|
if download_code:
|
|
return [Attachment(type="video", file_id=download_code)]
|
|
return []
|
|
|
|
if msg_type == "sticker":
|
|
parsed = _parse_content(content)
|
|
download_code = parsed.get("downloadCode", "") or parsed.get("download_code", "")
|
|
if download_code:
|
|
return [Attachment(type="sticker", file_id=download_code)]
|
|
return []
|
|
|
|
return []
|
|
|
|
|
|
def extract_mentions(raw: dict) -> MentionsInfo | None:
|
|
if not raw.get("isGroupChat"):
|
|
return None
|
|
|
|
mentioned_ids: list[str] = []
|
|
is_at_all = raw.get("isAtAll", False)
|
|
|
|
at_users = raw.get("atUsers", [])
|
|
if isinstance(at_users, list):
|
|
for u in at_users:
|
|
if isinstance(u, dict):
|
|
uid = u.get("dingtalkId", "")
|
|
if uid:
|
|
mentioned_ids.append(uid)
|
|
|
|
if is_at_all:
|
|
mentioned_ids.append("@all")
|
|
|
|
raw_text = extract_text(raw).strip()
|
|
|
|
return MentionsInfo(
|
|
mentioned_user_ids=mentioned_ids,
|
|
is_bot_mentioned=bool(mentioned_ids),
|
|
raw_text=raw_text,
|
|
)
|
|
|
|
|
|
def normalize_inbound(
|
|
channel_id: str,
|
|
channel_type: ChannelType,
|
|
raw_payload: dict[str, Any],
|
|
) -> ChannelMessage:
|
|
chat_type_str = resolve_dingding_chat_type(raw_payload)
|
|
chat_type = ChatType(chat_type_str)
|
|
|
|
chat_id = generate_dingding_chat_id(raw_payload, chat_type_str)
|
|
sender_id = (
|
|
raw_payload.get("senderId", "")
|
|
or raw_payload.get("senderStaffId", "")
|
|
or raw_payload.get("senderNick", "")
|
|
or "unknown"
|
|
)
|
|
message_id = raw_payload.get("msgId", "") or raw_payload.get("openMsgId", "")
|
|
|
|
event_type = map_event_type(raw_payload)
|
|
msg_type = map_msg_type(raw_payload)
|
|
content = extract_text(raw_payload)
|
|
attachments = extract_attachments(raw_payload)
|
|
mentions = extract_mentions(raw_payload)
|
|
|
|
metadata: dict[str, Any] = {
|
|
"msgtype": raw_payload.get("msgtype", "text"),
|
|
"isGroupChat": raw_payload.get("isGroupChat", False),
|
|
}
|
|
if raw_payload.get("rootId"):
|
|
metadata["root_id"] = raw_payload["rootId"]
|
|
if raw_payload.get("conversationId"):
|
|
metadata["conversation_id"] = raw_payload["conversationId"]
|
|
|
|
return ChannelMessage(
|
|
identity=ChannelIdentity(
|
|
channel_id=channel_id,
|
|
channel_type=channel_type,
|
|
channel_user_id=sender_id,
|
|
channel_chat_id=chat_id,
|
|
channel_message_id=message_id,
|
|
),
|
|
event_type=event_type,
|
|
message_type=msg_type,
|
|
chat_type=chat_type,
|
|
content=content,
|
|
attachments=attachments,
|
|
mentions=mentions,
|
|
metadata=metadata,
|
|
)
|