ForcePilot/backend/package/yuxi/channels/adapters/zalo_user/cards.py
Kris 9285da5c55 feat(zalo-user): 实现完整的Zalo用户频道适配器
新增了Zalo用户频道的完整适配器实现,包括:
- 基础的适配器初始化与导出结构
- 群组同步与成员获取功能
- 请求限流与退避重试机制
- 健康检查与状态探针
- 消息反应/表情处理工具
- 贴纸缓存与消息去重功能
- 消息ID格式化与追踪
- TTS语音合成支持
- 消息发送权限校验
- 长文本分块发送
- 操作审批流程
- 常量配置与国际化支持
- 图像视觉分析功能
- 贴纸消息处理
- 登录与配置向导
- 群组上下文缓存
- 网关连接管理
- 配置Schema校验
- 状态问题与安全审计
- 内联按钮与交互组件
- 交互式回调分发
- 联系人与群组目录管理
- 富媒体卡片消息支持
2026-05-12 00:53:13 +08:00

201 lines
4.9 KiB
Python

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from .inline_buttons import ButtonGroup
@dataclass
class CardImage:
url: str
aspect_ratio: str = "1:1"
def to_dict(self) -> dict[str, str]:
return {"url": self.url, "aspect_ratio": self.aspect_ratio}
@dataclass
class CardAction:
url: str | None = None
callback_data: str | None = None
def to_dict(self) -> dict[str, Any]:
d: dict[str, Any] = {}
if self.url:
d["url"] = self.url
if self.callback_data:
d["callback_data"] = self.callback_data
return d
@dataclass
class ZaloCard:
title: str = ""
description: str = ""
image: CardImage | None = None
buttons: ButtonGroup | None = None
action: CardAction | None = None
def to_dict(self) -> dict[str, Any]:
d: dict[str, Any] = {
"type": "card",
}
if self.title:
d["title"] = self.title
if self.description:
d["description"] = self.description
if self.image:
d["image"] = self.image.to_dict()
if self.buttons:
d["buttons"] = self.buttons.to_dict()
if self.action:
d["action"] = self.action.to_dict()
return d
@dataclass
class CardMessage:
cards: list[ZaloCard] = field(default_factory=list)
layout: str = "horizontal" # "horizontal", "carousel", "vertical"
def to_dict(self) -> dict[str, Any]:
return {
"type": "card_list",
"layout": self.layout,
"elements": [c.to_dict() for c in self.cards],
}
def build_card_carousel(
conversation_id: str,
cards: list[ZaloCard],
) -> dict[str, Any]:
return build_card_payload(conversation_id, CardMessage(cards=cards, layout="carousel"))
def build_card_payload(
conversation_id: str,
cards: CardMessage,
) -> dict[str, Any]:
return {
"conversation_id": conversation_id,
"message_type": "card",
"cards": cards.to_dict(),
}
def simple_card(
title: str,
description: str = "",
image_url: str | None = None,
action_url: str | None = None,
) -> ZaloCard:
card = ZaloCard(title=title, description=description)
if image_url:
card.image = CardImage(url=image_url)
if action_url:
card.action = CardAction(url=action_url)
return card
def template_welcome_card(bot_name: str, description: str = "") -> ZaloCard:
return ZaloCard(
title=f"Welcome to {bot_name}",
description=description or "I'm an AI assistant. How can I help you today?",
)
def template_confirmation_card(
title: str,
confirm_label: str = "Confirm",
cancel_label: str = "Cancel",
confirm_data: str = "confirm",
cancel_data: str = "cancel",
) -> ZaloCard:
from .inline_buttons import (
ButtonGroup,
ButtonStyle,
callback_button,
InlineButtonRow,
)
card = ZaloCard(
title=title,
description="Please confirm your action.",
buttons=ButtonGroup(
rows=[
InlineButtonRow(
buttons=[
callback_button(confirm_label, confirm_data, ButtonStyle.PRIMARY),
callback_button(cancel_label, cancel_data, ButtonStyle.DEFAULT),
]
)
]
),
)
return card
def template_info_card(
title: str,
fields: dict[str, str],
image_url: str | None = None,
) -> ZaloCard:
description_lines = [f"**{k}**: {v}" for k, v in fields.items()]
card = ZaloCard(
title=title,
description="\n".join(description_lines),
)
if image_url:
card.image = CardImage(url=image_url)
return card
def template_product_card(
product_name: str,
price: str,
description: str = "",
image_url: str | None = None,
buy_url: str | None = None,
) -> ZaloCard:
from .inline_buttons import (
ButtonGroup,
ButtonStyle,
InlineButtonRow,
url_button,
)
card = ZaloCard(
title=product_name,
description=description or f"Price: {price}",
)
if image_url:
card.image = CardImage(url=image_url)
if buy_url:
card.buttons = ButtonGroup(
rows=[
InlineButtonRow(
buttons=[
url_button("Buy Now", buy_url, ButtonStyle.PRIMARY),
]
)
]
)
return card
async def send_card(
bridge: Any,
conversation_id: str,
card: ZaloCard,
):
from yuxi.channels.models import DeliveryResult
card_msg = CardMessage(cards=[card])
payload = build_card_payload(conversation_id, card_msg)
try:
return await bridge.send_message(payload)
except Exception as e:
return DeliveryResult(success=False, error=str(e))