ForcePilot/backend/package/yuxi/channel/extensions/msteams/adaptive_card.py
Kris 94444ced96 feat(channel): 添加 Microsoft Teams 渠道扩展
新增 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: 类型定义
2026-05-21 11:28:42 +08:00

284 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from typing import Any
ADAPTIVE_CARD_VERSION = "1.4"
def build_card_base(
title: str,
body_items: list[dict] | None = None,
actions: list[dict] | None = None,
*,
fallback_text: str = "",
) -> dict:
card: dict[str, Any] = {
"type": "AdaptiveCard",
"version": ADAPTIVE_CARD_VERSION,
}
if title:
card.setdefault("body", [])
card["body"].append(
{
"type": "TextBlock",
"text": title,
"weight": "Bolder",
"size": "Medium",
"wrap": True,
}
)
if body_items:
card.setdefault("body", [])
card["body"].extend(body_items)
if actions:
card.setdefault("actions", [])
card["actions"].extend(actions)
if fallback_text:
card["fallbackText"] = fallback_text
return card
def build_text_block(text: str, *, weight: str = "Default", size: str = "Default", wrap: bool = True) -> dict:
return {
"type": "TextBlock",
"text": text,
"weight": weight,
"size": size,
"wrap": wrap,
}
def build_action_button(title: str, data: dict, *, style: str = "default") -> dict:
return {
"type": "Action.Submit",
"title": title,
"data": data,
"style": style,
}
def build_action_url(title: str, url: str) -> dict:
return {
"type": "Action.OpenUrl",
"title": title,
"url": url,
}
def build_action_execute(title: str, data: dict, *, verb: str = "", fallback: str = "submit") -> dict:
return {
"type": "Action.Execute",
"title": title,
"data": data,
"verb": verb,
"fallback": fallback,
}
def build_refresh_properties(refresh_enabled: bool = True, *, user_ids: list[str] | None = None, refresh_interval_s: int | None = None) -> dict:
refresh: dict = {"action": {"type": "Action.Execute", "verb": "refresh"}}
if user_ids:
refresh["userIds"] = user_ids
if refresh_interval_s:
refresh["action"]["refreshInterval"] = refresh_interval_s
return refresh
def build_user_specific_card(base_card: dict, user_id: str, user_body_items: list[dict]) -> dict:
card = {**base_card}
card["refresh"] = build_refresh_properties(refresh_enabled=True, user_ids=[user_id])
return card
def build_hero_card(title: str, *, subtitle: str = "", text: str = "", image_url: str = "", buttons: list[dict] | None = None) -> dict:
card = {
"contentType": "application/vnd.microsoft.card.hero",
"content": {
"title": title,
"subtitle": subtitle,
"text": text,
"images": [{"url": image_url}] if image_url else [],
"buttons": buttons or [],
},
}
return card
def build_thumbnail_card(title: str, *, subtitle: str = "", text: str = "", image_url: str = "", buttons: list[dict] | None = None) -> dict:
card = {
"contentType": "application/vnd.microsoft.card.thumbnail",
"content": {
"title": title,
"subtitle": subtitle,
"text": text,
"images": [{"url": image_url}] if image_url else [],
"buttons": buttons or [],
},
}
return card
def build_receipt_card(title: str, items: list[dict], *, total: str = "", tax: str = "", buttons: list[dict] | None = None) -> dict:
card = {
"contentType": "application/vnd.microsoft.card.receipt",
"content": {
"title": title,
"facts": [],
"items": items,
"total": total,
"tax": tax,
"buttons": buttons or [],
},
}
return card
def build_receipt_item(title: str, price: str, *, quantity: str = "1", image_url: str = "", subtitle: str = "") -> dict:
return {
"title": title,
"subtitle": subtitle,
"price": price,
"quantity": quantity,
"image": {"url": image_url} if image_url else None,
}
def build_welcome_card(
bot_name: str,
description: str = "",
*,
features: list[str] | None = None,
) -> dict:
items = [
build_text_block(f"👋 欢迎使用 **{bot_name}**", weight="Bolder", size="Large"),
]
if description:
items.append(build_text_block(description))
if features:
feature_text = "\n".join(f"- {f}" for f in features)
items.append(build_text_block(feature_text))
items.append(build_text_block("发送消息开始对话,或使用交互按钮探索功能。"))
return build_card_base(
title="",
body_items=items,
fallback_text=f"欢迎使用 {bot_name}!发送消息开始对话。",
)
def build_poll_card(
question: str,
options: list[str],
*,
poll_id: str = "",
is_multi_select: bool = False,
) -> dict:
body_items = [
build_text_block(question, weight="Bolder", size="Medium"),
{
"type": "Input.ChoiceSet",
"id": "pollChoice",
"choices": [{"title": opt, "value": opt} for opt in options],
"style": "expanded",
"isMultiSelect": is_multi_select,
},
]
actions = [
build_action_button("投票", {"action": "vote", "poll_id": poll_id}, style="positive"),
]
return build_card_base(
title="",
body_items=body_items,
actions=actions,
fallback_text=f"投票: {question}",
)
def build_confirm_card(
message: str,
confirm_text: str = "确认",
cancel_text: str = "取消",
*,
confirm_data: dict | None = None,
cancel_data: dict | None = None,
) -> dict:
body_items = [
build_text_block(message, size="Medium"),
]
actions = []
if confirm_text:
actions.append(build_action_button(confirm_text, confirm_data or {"action": "confirm"}, style="positive"))
if cancel_text:
actions.append(build_action_button(cancel_text, cancel_data or {"action": "cancel"}, style="default"))
return build_card_base(
title="",
body_items=body_items,
actions=actions,
fallback_text=message,
)
def build_demo_card(
title: str,
content: str,
*,
action_title: str | None = None,
action_data: dict | None = None,
action_url: str | None = None,
) -> dict:
body_items = [
build_text_block(content, size="Default"),
]
actions = []
if action_title and action_data:
actions.append(build_action_button(action_title, action_data))
if action_title and action_url:
actions.append(build_action_url(action_title, action_url))
return build_card_base(
title=title,
body_items=body_items,
actions=actions,
fallback_text=f"{title}: {content}",
)
def build_attachment(card: dict) -> dict:
return {
"contentType": "application/vnd.microsoft.card.adaptive",
"content": card,
}
def build_card_activity(
card: dict,
*,
reply_to_id: str | None = None,
tenant_id: str | None = None,
) -> dict:
activity: dict[str, Any] = {
"type": "message",
"attachments": [build_attachment(card)],
}
if reply_to_id:
activity["replyToId"] = reply_to_id
if tenant_id:
activity.setdefault("channelData", {})
activity["channelData"]["tenant"] = {"id": tenant_id}
return activity