实现了 Teams 机器人所需的全功能组件,包括: - 基础命令解析与帮助卡片生成 - 租户验证与访问控制 - 自定义 UA 与媒体工具 - 消息分块、批注处理与会话管理 - 防抖、缓存与配置路由能力 - 投票、配对、审计与运行时状态管理 - TTS 语音合成与卡片构建工具 - 群组管理与权限控制逻辑
170 lines
5.7 KiB
Python
170 lines
5.7 KiB
Python
"""Microsoft Teams invoke Activity 路由分发。
|
|
|
|
处理 adaptiveCard/action invoke 的投票、确认、输入等交互,
|
|
以及 messageReaction 事件的路由。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from yuxi.channels.models import ChannelIdentity, ChannelMessage, ChannelType, EventType, MessageType
|
|
|
|
|
|
def normalize_reaction(activity: dict[str, Any]) -> ChannelMessage:
|
|
"""标准化 messageReaction Activity → ChannelMessage。
|
|
|
|
处理 reactionsAdded 和 reactionsRemoved 两种方向。
|
|
"""
|
|
from_info = activity.get("from", {}) or {}
|
|
conversation = activity.get("conversation", {}) or {}
|
|
channel_data = activity.get("channelData") or {}
|
|
|
|
aad_object_id = from_info.get("aadObjectId", "") or from_info.get("id", "")
|
|
conversation_id = conversation.get("id", "")
|
|
|
|
added = activity.get("reactionsAdded", [])
|
|
removed = activity.get("reactionsRemoved", [])
|
|
|
|
reactions_data = {
|
|
"added": [{"type": r.get("type", "")} for r in added],
|
|
"removed": [{"type": r.get("type", "")} for r in removed],
|
|
}
|
|
|
|
direction = "added" if added else "removed" if removed else "unknown"
|
|
|
|
identity = ChannelIdentity(
|
|
channel_id="msteams",
|
|
channel_type=ChannelType.MS_TEAMS,
|
|
channel_user_id=aad_object_id,
|
|
channel_chat_id=conversation_id,
|
|
channel_message_id=activity.get("id"),
|
|
)
|
|
|
|
return ChannelMessage(
|
|
identity=identity,
|
|
event_type=EventType.SYSTEM_EVENT,
|
|
message_type=MessageType.TEXT,
|
|
chat_type="direct",
|
|
content=f"reaction_{direction}",
|
|
reply_to_message_id=activity.get("replyToId"),
|
|
metadata={
|
|
"reaction_direction": direction,
|
|
"reactions": reactions_data,
|
|
"raw_activity": activity,
|
|
"tenant_id": (channel_data.get("tenant") or {}).get("id", ""),
|
|
"from_name": from_info.get("name", ""),
|
|
},
|
|
)
|
|
|
|
|
|
def normalize_card_action(activity: dict[str, Any]) -> ChannelMessage:
|
|
"""标准化 adaptiveCard/action invoke → ChannelMessage。
|
|
|
|
提取 action 类型和提交数据,支持投票/确认/输入/通用 Submit。
|
|
"""
|
|
from_info = activity.get("from", {}) or {}
|
|
conversation = activity.get("conversation", {}) or {}
|
|
channel_data = activity.get("channelData") or {}
|
|
|
|
aad_object_id = from_info.get("aadObjectId", "") or from_info.get("id", "")
|
|
conversation_id = conversation.get("id", "")
|
|
value = activity.get("value", {}) or {}
|
|
invoke_name = activity.get("name", "")
|
|
|
|
action_type = _classify_invoke_action(invoke_name, value)
|
|
poll_id = _extract_poll_id(value) if action_type == "vote" else ""
|
|
|
|
identity = ChannelIdentity(
|
|
channel_id="msteams",
|
|
channel_type=ChannelType.MS_TEAMS,
|
|
channel_user_id=aad_object_id,
|
|
channel_chat_id=conversation_id,
|
|
channel_message_id=activity.get("id"),
|
|
)
|
|
|
|
return ChannelMessage(
|
|
identity=identity,
|
|
event_type=EventType.CARD_ACTION,
|
|
message_type=MessageType.TEXT,
|
|
chat_type="direct",
|
|
content=_format_action_content(action_type, value),
|
|
metadata={
|
|
"invoke_name": invoke_name,
|
|
"action_type": action_type,
|
|
"invoke_value": value,
|
|
"poll_id": poll_id,
|
|
"tenant_id": (channel_data.get("tenant") or {}).get("id", ""),
|
|
"from_name": from_info.get("name", ""),
|
|
"raw_activity": activity,
|
|
},
|
|
)
|
|
|
|
|
|
def _classify_invoke_action(invoke_name: str, value: dict[str, Any]) -> str:
|
|
if invoke_name == "adaptiveCard/action":
|
|
data = value.get("data", {}) or {}
|
|
if isinstance(data, dict):
|
|
if "voteOption" in value:
|
|
return "vote"
|
|
if data.get("action") == "confirm":
|
|
return "confirm"
|
|
if data.get("action") == "cancel":
|
|
return "cancel"
|
|
if data.get("action") == "submit":
|
|
return "input"
|
|
if "action" in data:
|
|
return data["action"]
|
|
return "card_action"
|
|
if invoke_name == "message/submitAction":
|
|
return "feedback"
|
|
if invoke_name == "fileConsent/invoke":
|
|
return "file_consent"
|
|
return invoke_name
|
|
|
|
|
|
def _extract_poll_id(value: dict[str, Any]) -> str:
|
|
"""7 路径 pollId 提取:覆盖 Teams 不同版本文档差异。"""
|
|
data = value.get("data", {}) or {}
|
|
|
|
sources = [
|
|
lambda: data.get("poll_id"),
|
|
lambda: value.get("poll_id"),
|
|
lambda: value.get("pollId"),
|
|
lambda: data.get("pollId"),
|
|
lambda: (data.get("data", {}) or {}).get("poll_id") if isinstance(data.get("data"), dict) else None,
|
|
lambda: str(data.get("poll_id", "")) or str(value.get("poll_id", "")),
|
|
lambda: (
|
|
(value.get("action", {}) or {}).get("data", {}).get("poll_id")
|
|
if isinstance(value.get("action"), dict)
|
|
else None
|
|
),
|
|
]
|
|
|
|
for source in sources:
|
|
try:
|
|
result = source()
|
|
if result:
|
|
return str(result)
|
|
except Exception:
|
|
continue
|
|
|
|
return value.get("poll_id", "") or str(data.get("poll_id", ""))
|
|
|
|
|
|
def _format_action_content(action_type: str, value: dict[str, Any]) -> str:
|
|
if action_type == "vote":
|
|
return str(value.get("voteOption", ""))
|
|
if action_type == "feedback":
|
|
fb_value = value.get("feedbackValue", "")
|
|
return str(fb_value)
|
|
if action_type in ("confirm", "cancel", "input"):
|
|
data = value.get("data", {}) or {}
|
|
return str(data)
|
|
return str(value)
|
|
|
|
|
|
def is_signin_invoke(activity: dict[str, Any]) -> bool:
|
|
name = activity.get("name", "")
|
|
return name in ("signin/tokenExchange", "signin/verifyState")
|