新增 Microsoft Teams 渠道扩展,支持在 Yuxi 平台中集成 Microsoft Teams 协作平台。 包含以下功能模块: - sdk: Bot Framework SDK 封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - auth: JWT 认证 - jwks: JWKS 密钥管理 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - state: 状态管理 - runtime: 运行时管理 - actions: 动作处理 - adaptive_card: 自适应卡片 - task_modules: 任务模块 - message_extension: 消息扩展 - proactive: Proactive Messaging - graph: Microsoft Graph API 集成 - graph_teams: Teams 操作 - graph_members: 成员管理 - graph_messages: 消息获取 - graph_thread: 线程管理 - graph_users: 用户管理 - graph_upload: 文件上传 - files: 文件处理 - file_consent: 文件授权 - conversations: 会话存储 - mentions: @提及处理 - threading: 线程管理 - reactions: 表情反应 - polls: 投票功能 - meetings: 会议集成 - feedback: 反馈处理 - sso: 单点登录 - deep_links: 深层链接 - incoming_webhook: 入站 Webhook - localization: 本地化 - user_agent: 用户代理 - sent_message_cache: 消息缓存 - types: 类型定义
232 lines
8.2 KiB
Python
232 lines
8.2 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
|
|
from yuxi.channel.message.models import GroupContext, MessageType, PeerInfo, UnifiedMessage
|
|
from yuxi.channel.routing.models import PeerKind
|
|
|
|
from .format import clean_inbound_text
|
|
from .sdk import was_bot_mentioned
|
|
from .types import BotFrameworkActivity, MSTeamsActivityType
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
AI_TAG_RE = re.compile(r"\[AI\]\s*", re.IGNORECASE)
|
|
|
|
|
|
def _resolve_chat_type(activity: BotFrameworkActivity) -> str:
|
|
ct = activity.conversation_type
|
|
if ct == "personal":
|
|
return "direct"
|
|
if ct == "channel":
|
|
return "channel" if not activity.reply_to_id else "thread"
|
|
if ct == "groupChat":
|
|
return "group"
|
|
return "direct" if not activity.is_group else "group"
|
|
|
|
|
|
def _resolve_message_type(activity: BotFrameworkActivity) -> MessageType:
|
|
if activity.attachments:
|
|
first = activity.attachments[0]
|
|
content_type = first.get("contentType", "")
|
|
if "image" in content_type:
|
|
return MessageType.IMAGE
|
|
if "video" in content_type:
|
|
return MessageType.VIDEO
|
|
if "audio" in content_type:
|
|
return MessageType.VOICE
|
|
return MessageType.FILE
|
|
if activity.type == MSTeamsActivityType.MESSAGE_REACTION:
|
|
return MessageType.EVENT
|
|
return MessageType.TEXT
|
|
|
|
|
|
def _resolve_peer_kind(activity: BotFrameworkActivity) -> PeerKind:
|
|
ct = _resolve_chat_type(activity)
|
|
if ct in ("direct",):
|
|
return PeerKind.DIRECT
|
|
return PeerKind.GROUP
|
|
|
|
|
|
def _build_group_context(activity: BotFrameworkActivity) -> GroupContext | None:
|
|
chat_type = _resolve_chat_type(activity)
|
|
if chat_type == "direct":
|
|
return None
|
|
|
|
conv = activity.conversation
|
|
cd = activity.channel_data
|
|
|
|
return GroupContext(
|
|
id=activity.conversation_id,
|
|
kind=chat_type,
|
|
name=conv.name if conv else None,
|
|
team_id=cd.team_id if cd else None,
|
|
thread_id=activity.reply_to_id if activity.is_channel_thread else None,
|
|
native_channel_id=cd.channel_id if cd else None,
|
|
)
|
|
|
|
|
|
def _build_debounce_key(activity: BotFrameworkActivity) -> str:
|
|
return f"msteams:{activity.from_id}:{activity.conversation_id}"
|
|
|
|
|
|
def is_echo_activity(activity: BotFrameworkActivity, bot_app_id: str) -> bool:
|
|
sender_id = activity.from_id
|
|
bot_id = f"28:{bot_app_id}"
|
|
return sender_id == bot_id or sender_id == bot_app_id
|
|
|
|
|
|
def _build_event_message(
|
|
activity: BotFrameworkActivity,
|
|
account_id: str,
|
|
event_type: str,
|
|
payload: dict | list | None = None,
|
|
) -> UnifiedMessage:
|
|
return UnifiedMessage(
|
|
msg_id=f"msteams:{event_type}:{activity.id}",
|
|
channel_type="msteams",
|
|
account_id=account_id,
|
|
content="",
|
|
sender=PeerInfo(id=activity.from_id, kind=_resolve_peer_kind(activity)),
|
|
message_type=MessageType.EVENT,
|
|
group=_build_group_context(activity),
|
|
raw_payload=activity.raw,
|
|
metadata={"event_type": event_type, "payload": payload},
|
|
conversation_label=f"msteams:{activity.conversation_id}",
|
|
native_channel_id=activity.conversation_id,
|
|
)
|
|
|
|
|
|
def activity_to_unified_message(
|
|
activity: BotFrameworkActivity,
|
|
bot_app_id: str,
|
|
account_id: str,
|
|
) -> UnifiedMessage | None:
|
|
if activity.type == MSTeamsActivityType.TYPING:
|
|
return None
|
|
|
|
if activity.type == MSTeamsActivityType.MESSAGE_REACTION:
|
|
from .reactions import parse_message_reaction
|
|
|
|
reaction_data = parse_message_reaction(activity.raw)
|
|
if reaction_data:
|
|
return UnifiedMessage(
|
|
msg_id=f"msteams:reaction:{activity.id}",
|
|
channel_type="msteams",
|
|
account_id=account_id,
|
|
content="",
|
|
sender=PeerInfo(id=activity.from_id, kind=_resolve_peer_kind(activity)),
|
|
message_type=MessageType.EVENT,
|
|
group=_build_group_context(activity),
|
|
raw_payload=activity.raw,
|
|
metadata={
|
|
"event_type": "message_reaction",
|
|
"reply_to_id": reaction_data.get("reply_to_id"),
|
|
"reactions_added": reaction_data.get("reactions_added", []),
|
|
"reactions_removed": reaction_data.get("reactions_removed", []),
|
|
},
|
|
conversation_label=f"msteams:{activity.conversation_id}",
|
|
native_channel_id=activity.conversation_id,
|
|
)
|
|
return None
|
|
|
|
if activity.type == MSTeamsActivityType.CONVERSATION_UPDATE:
|
|
members_added = activity.raw.get("membersAdded", [])
|
|
members_removed = activity.raw.get("membersRemoved", [])
|
|
bot_id = f"28:{bot_app_id}"
|
|
bot_was_added = any(m.get("id") in (bot_id, bot_app_id) for m in members_added)
|
|
bot_was_removed = any(m.get("id") in (bot_id, bot_app_id) for m in members_removed)
|
|
|
|
if bot_was_added:
|
|
return _build_event_message(activity, account_id, "bot_added", members_added)
|
|
if bot_was_removed:
|
|
return _build_event_message(activity, account_id, "bot_removed", members_removed)
|
|
|
|
event_type = activity.raw.get("eventType") or activity.raw.get("channelData", {}).get("eventType", "")
|
|
if event_type:
|
|
return _build_event_message(activity, account_id, event_type, activity.raw.get("membersAdded", []))
|
|
|
|
return None
|
|
|
|
if activity.type == MSTeamsActivityType.INSTALLATION_UPDATE:
|
|
action = activity.raw.get("action", "")
|
|
return _build_event_message(activity, account_id, "installation_update", {"action": action})
|
|
|
|
if activity.type == MSTeamsActivityType.MESSAGE_UPDATE:
|
|
return UnifiedMessage(
|
|
msg_id=f"msteams:msgupdate:{activity.id}",
|
|
channel_type="msteams",
|
|
account_id=account_id,
|
|
content=clean_inbound_text(activity.text or ""),
|
|
sender=PeerInfo(id=activity.from_id, kind=_resolve_peer_kind(activity)),
|
|
message_type=MessageType.EVENT,
|
|
group=_build_group_context(activity),
|
|
raw_payload=activity.raw,
|
|
metadata={"event_type": "message_update", "original_id": activity.id},
|
|
conversation_label=f"msteams:{activity.conversation_id}",
|
|
native_channel_id=activity.conversation_id,
|
|
)
|
|
|
|
if activity.type == MSTeamsActivityType.MESSAGE_DELETE:
|
|
return _build_event_message(activity, account_id, "message_delete")
|
|
|
|
if activity.type == MSTeamsActivityType.INVOKE:
|
|
return None
|
|
|
|
if is_echo_activity(activity, bot_app_id):
|
|
return None
|
|
|
|
text = clean_inbound_text(activity.text or "")
|
|
text = AI_TAG_RE.sub("", text)
|
|
|
|
if not text and not activity.attachments:
|
|
return None
|
|
|
|
chat_type = _resolve_chat_type(activity)
|
|
peer_kind = _resolve_peer_kind(activity)
|
|
|
|
sender = PeerInfo(
|
|
id=activity.from_id,
|
|
kind=peer_kind,
|
|
display_name=activity.from_name or activity.from_id,
|
|
)
|
|
|
|
media_urls_list = [att.get("contentUrl", "") for att in activity.attachments if att.get("contentUrl")]
|
|
|
|
mentioned = was_bot_mentioned(activity, bot_app_id)
|
|
|
|
thread_id = activity.reply_to_id if activity.is_channel_thread else None
|
|
|
|
metadata: dict = {
|
|
"service_url": activity.service_url,
|
|
"tenant_id": activity.tenant_id,
|
|
"debounce_key": _build_debounce_key(activity),
|
|
"chat_type": chat_type,
|
|
"locale": activity.locale,
|
|
}
|
|
|
|
if text.startswith("/"):
|
|
command, *args = text[1:].split(maxsplit=1)
|
|
metadata["command"] = command
|
|
metadata["command_args"] = args[0] if args else ""
|
|
|
|
return UnifiedMessage(
|
|
msg_id=f"msteams:{activity.id}",
|
|
channel_type="msteams",
|
|
account_id=account_id,
|
|
content=text,
|
|
sender=sender,
|
|
message_type=_resolve_message_type(activity),
|
|
group=_build_group_context(activity),
|
|
reply_to_id=activity.reply_to_id,
|
|
was_mentioned=mentioned,
|
|
media_urls=media_urls_list if media_urls_list else [],
|
|
raw_payload=activity.raw,
|
|
metadata=metadata,
|
|
conversation_label=f"msteams:{activity.conversation_id}",
|
|
native_channel_id=activity.conversation_id,
|
|
native_direct_user_id=activity.from_id,
|
|
thread_parent_id=thread_id,
|
|
)
|