"""Microsoft Teams ChannelResponse → Activity 格式构建。 将统一的 ChannelResponse 转换为 Bot Framework Activity 字典格式, 用于通过 REST API 发送消息到 Teams。 """ from __future__ import annotations import re from typing import Any from yuxi.channels.models import ChannelResponse, MessageType from .mentions import parse_outbound_mentions, apply_mentions_to_activity from .send import is_silent_reply_text, strip_silent_token def format_outbound(response: ChannelResponse, text_chunk_limit: int = 4000) -> dict[str, Any]: text = response.content[:text_chunk_limit] silent = is_silent_reply_text(text) if silent: text = strip_silent_token(text) table_mode = (response.metadata or {}).get("table_mode", "markdown") if table_mode != "markdown": text = _convert_tables(text, table_mode) activity: dict[str, Any] = { "type": "message", "text": text, "textFormat": "markdown", } if silent: activity.setdefault("channelData", {})["notification"] = {"alert": "false"} conversation_id = response.identity.channel_chat_id if conversation_id: activity["conversation"] = {"id": conversation_id} if response.reply_to_message_id: activity["replyToId"] = response.reply_to_message_id adaptive_card = (response.metadata or {}).get("adaptive_card") if adaptive_card: activity["attachments"] = [ { "contentType": "application/vnd.microsoft.card.adaptive", "content": adaptive_card, } ] elif response.attachments: activity["attachments"] = _format_attachments(response) if response.message_type == MessageType.IMAGE and response.attachments: activity["attachmentLayout"] = "list" _maybe_add_ai_generated_entity(activity, response) parsed_text, mention_entities = parse_outbound_mentions(text) if mention_entities: activity["text"] = parsed_text activity = apply_mentions_to_activity(activity, mention_entities) return activity def _format_attachments(response: ChannelResponse) -> list[dict[str, Any]]: attachments = [] for att in response.attachments: if att.url and att.url.startswith("data:"): attachments.append( { "contentType": att.mime_type or "application/octet-stream", "contentUrl": att.url, "name": att.filename or "file", } ) elif att.url: attachments.append( { "contentType": att.mime_type or "application/octet-stream", "contentUrl": att.url, "name": att.filename or "file", } ) return attachments def format_adaptive_card_response( conversation_id: str, card: dict[str, Any], reply_to_id: str | None = None, ) -> dict[str, Any]: activity: dict[str, Any] = { "type": "message", "conversation": {"id": conversation_id}, "attachments": [ { "contentType": "application/vnd.microsoft.card.adaptive", "content": card, } ], "textFormat": "markdown", } if reply_to_id: activity["replyToId"] = reply_to_id return activity AI_GENERATED_ENTITY = { "type": "https://schema.org/Message", "@context": "https://schema.org", "@type": "Message", "additionalType": ["AIGeneratedContent"], } def _maybe_add_ai_generated_entity(activity: dict[str, Any], response: ChannelResponse) -> None: ai_generated = (response.metadata or {}).get("ai_generated", True) if not ai_generated: return entities: list[dict] = activity.setdefault("entities", []) entities.append(AI_GENERATED_ENTITY) TABLE_MODE_OPTIONS = {"markdown", "plain", "list"} def _convert_tables(text: str, mode: str) -> str: if mode not in TABLE_MODE_OPTIONS: return text table_pattern = re.compile(r"(\|.*?\|\n\|[-:|\s]+\|\n(?:\|.*\|\n?)+)", re.MULTILINE) def _replace(match: re.Match) -> str: table_block = match.group(1) if mode == "plain": return _table_to_plain(table_block) if mode == "list": return _table_to_list(table_block) return table_block return table_pattern.sub(_replace, text) def _table_to_plain(table_text: str) -> str: lines = [line.strip() for line in table_text.strip().split("\n") if "---" not in line] result_lines = [] for line in lines: cells = [cell.strip() for cell in line.strip("|").split("|")] result_lines.append(" | ".join(cells)) return "\n".join(result_lines) + "\n" def _table_to_list(table_text: str) -> str: lines = [line.strip() for line in table_text.strip().split("\n") if "---" not in line] if not lines: return table_text header = [cell.strip() for cell in lines[0].strip("|").split("|")] result_lines = [] for row in lines[1:]: cells = [cell.strip() for cell in row.strip("|").split("|")] for h, c in zip(header, cells): result_lines.append(f"- **{h}**: {c}") result_lines.append("") return "\n".join(result_lines)