ForcePilot/backend/package/yuxi/channel/extensions/kakaotalk/card_builder.py
Kris 8b66218d9b feat(channel): 添加 KakaoTalk 渠道扩展
新增 KakaoTalk 渠道扩展,支持在 Yuxi 平台中集成 KakaoTalk 即时通讯渠道。

包含以下功能模块:
- bot: Bot 客户端封装
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- card_builder: KakaoTalk 卡片消息构建
- quick_reply: 快捷回复处理
- types: 类型定义
2026-05-21 11:12:31 +08:00

262 lines
8.7 KiB
Python

from __future__ import annotations
from yuxi.channel.extensions.kakaotalk.types import KakaoButton, KakaoQuickReply
class CardBuilder:
@staticmethod
def simple_text(text: str) -> dict:
return {"simpleText": {"text": text[:1000]}}
@staticmethod
def simple_image(image_url: str, alt_text: str) -> dict:
return {"simpleImage": {"imageUrl": image_url, "altText": alt_text}}
@staticmethod
def basic_card(
title: str,
description: str = "",
thumbnail_url: str | None = None,
buttons: list[KakaoButton] | None = None,
button_layout: str = "vertical",
profile_nickname: str | None = None,
profile_image_url: str | None = None,
forwardable: bool | None = None,
social_like: int = 0,
social_comment: int = 0,
social_share: int = 0,
) -> dict:
card: dict = {"title": title, "description": description}
if thumbnail_url:
card["thumbnail"] = {"imageUrl": thumbnail_url}
if profile_nickname:
card["profile"] = {"nickname": profile_nickname}
if profile_image_url:
card["profile"]["imageUrl"] = profile_image_url
if buttons:
card["buttons"] = [CardBuilder._build_button(b) for b in buttons]
card["buttonLayout"] = button_layout
if forwardable is not None:
card["forwardable"] = forwardable
if social_like or social_comment or social_share:
card["social"] = {
"like": social_like,
"comment": social_comment,
"share": social_share,
}
return {"basicCard": card}
@staticmethod
def commerce_card(
title: str,
description: str = "",
price: int = 0,
currency: str = "won",
discount: int = 0,
discount_rate: int = 0,
discounted_price: int = 0,
thumbnail_urls: list[str] | None = None,
profile_nickname: str | None = None,
profile_image_url: str | None = None,
buttons: list[KakaoButton] | None = None,
button_layout: str = "vertical",
) -> dict:
card: dict = {
"title": title,
"description": description,
"price": price,
"currency": currency,
}
if discount:
card["discount"] = discount
if discount_rate:
card["discountRate"] = discount_rate
if discounted_price:
card["discountedPrice"] = discounted_price
if thumbnail_urls:
card["thumbnails"] = [
{"imageUrl": url, "fixedRatio": True, "width": 800, "height": 800}
for url in thumbnail_urls
]
if profile_nickname:
card["profile"] = {"nickname": profile_nickname}
if profile_image_url:
card["profile"]["imageUrl"] = profile_image_url
if buttons:
card["buttons"] = [CardBuilder._build_button(b) for b in buttons]
card["buttonLayout"] = button_layout
return {"commerceCard": card}
@staticmethod
def list_card(
header_title: str,
items: list[dict],
buttons: list[KakaoButton] | None = None,
button_layout: str = "vertical",
) -> dict:
card: dict = {"header": {"title": header_title}, "items": items}
if buttons:
card["buttons"] = [CardBuilder._build_button(b) for b in buttons]
card["buttonLayout"] = button_layout
return {"listCard": card}
@staticmethod
def carousel(card_type: str, items: list[dict]) -> dict:
return {"carousel": {"type": card_type, "items": items}}
@staticmethod
def commerce_carousel(commerce_items: list[dict]) -> dict:
return {"carousel": {"type": "commerceCard", "items": commerce_items}}
@staticmethod
def text_card(
title: str,
description: str = "",
buttons: list[KakaoButton] | None = None,
button_layout: str = "vertical",
) -> dict:
card: dict = {"title": title[:50], "description": description[:400]}
if buttons:
card["buttons"] = [CardBuilder._build_button(b) for b in buttons]
card["buttonLayout"] = button_layout
return {"textCard": card}
@staticmethod
def item_card(
title: str = "",
description: str = "",
thumbnail_url: str | None = None,
item_list: list[dict] | None = None,
buttons: list[KakaoButton] | None = None,
button_layout: str = "vertical",
profile_nickname: str | None = None,
profile_image_url: str | None = None,
head_title: str = "",
image_title: str = "",
image_title_image_url: str | None = None,
) -> dict:
card: dict = {}
if title:
card["title"] = title
if description:
card["description"] = description
if thumbnail_url:
card["thumbnail"] = {"imageUrl": thumbnail_url}
if item_list:
card["itemList"] = item_list
if head_title:
card["head"] = {"title": head_title}
if image_title:
img_title: dict = {"title": image_title}
if image_title_image_url:
img_title["imageUrl"] = image_title_image_url
card["imageTitle"] = img_title
if profile_nickname:
card["profile"] = {"nickname": profile_nickname}
if profile_image_url:
card["profile"]["imageUrl"] = profile_image_url
if buttons:
card["buttons"] = [CardBuilder._build_button(b) for b in buttons]
card["buttonLayout"] = button_layout
return {"itemCard": card}
@staticmethod
def build_list_card_item(
title: str,
description: str = "",
image_url: str | None = None,
link_url: str | None = None,
) -> dict:
item: dict = {"title": title}
if description:
item["description"] = description
if image_url:
item["imageUrl"] = image_url
if link_url:
item["link"] = {"web": link_url}
return item
@staticmethod
def build_item_card_item(
title: str,
description: str = "",
image_url: str | None = None,
link_url: str | None = None,
) -> dict:
item: dict = {"title": title}
if description:
item["description"] = description
if image_url:
item["imageUrl"] = image_url
if link_url:
item["link"] = {"web": link_url}
return item
@staticmethod
def quick_reply(reply: KakaoQuickReply) -> dict:
qr: dict = {"label": reply.label, "action": reply.action}
if reply.message_text:
qr["messageText"] = reply.message_text
if reply.block_id:
qr["blockId"] = reply.block_id
if reply.web_link_url:
qr["webLinkUrl"] = reply.web_link_url
if reply.extra:
qr["extra"] = reply.extra
return qr
@staticmethod
def quick_replies(replies: list[dict]) -> list[dict]:
return replies
@staticmethod
def build_skill_response(
outputs: list[dict],
quick_replies: list[dict] | None = None,
context_values: list[dict] | None = None,
data: dict | None = None,
) -> dict:
template: dict = {"outputs": outputs}
if quick_replies:
template["quickReplies"] = quick_replies
skill_response: dict = {"version": "2.0", "template": template}
if context_values:
skill_response["context"] = {"values": context_values}
if data:
skill_response["data"] = data
return skill_response
@staticmethod
def build_context_value(
name: str,
life_span: int,
ttl: int = 0,
params: dict | None = None,
) -> dict:
cv: dict = {"name": name, "lifeSpan": life_span}
if ttl:
cv["ttl"] = ttl
if params:
cv["params"] = params
return cv
@staticmethod
def _build_button(button: KakaoButton) -> dict:
btn: dict = {"label": button.label, "action": button.action}
if button.message_text:
btn["messageText"] = button.message_text
if button.web_link_url:
btn["webLinkUrl"] = button.web_link_url
if button.phone_number:
btn["phoneNumber"] = button.phone_number
if button.block_id:
btn["blockId"] = button.block_id
if button.bot_command_id:
btn["botCommand"] = {"commandId": button.bot_command_id}
if button.operator_block_id:
btn["operatorBlock"] = {"blockId": button.operator_block_id}
if button.extra:
btn["extra"] = button.extra
return btn