ForcePilot/backend/package/yuxi/channels/adapters/line/template_messages.py
Kris a4ec94ef9d feat(line): 实现完整的 LINE 聊天适配器功能
新增 LINE 官方账号对接的全套功能,包括:
1. 基础的 Bot 探测、会话解析、消息格式化能力
2. 富媒体消息模板、快速回复、卡片指令支持
3. Webhook 签名验证、重放防护、多账户路由管理
4. 消息发送、回复、分块传输、用户绑定管理
5. 交互式配置向导与诊断工具
2026-05-12 00:45:33 +08:00

224 lines
6.0 KiB
Python

from __future__ import annotations
from typing import Any
def build_buttons_template(
title: str,
text: str,
actions: list[dict],
thumbnail_url: str | None = None,
alt_text: str = "Buttons Template",
) -> dict:
template: dict = {
"type": "buttons",
"title": title[:40] if title else None,
"text": text[:160],
"actions": actions[:4],
}
if thumbnail_url:
template["thumbnailImageUrl"] = thumbnail_url
return {
"type": "template",
"altText": alt_text[:400],
"template": template,
}
def build_confirm_template(
text: str,
actions: list[dict],
alt_text: str = "Confirm",
) -> dict:
return {
"type": "template",
"altText": alt_text[:400],
"template": {
"type": "confirm",
"text": text[:240],
"actions": actions[:2],
},
}
def build_carousel_template(columns: list[dict], alt_text: str = "Carousel") -> dict:
return {
"type": "template",
"altText": alt_text[:400],
"template": {
"type": "carousel",
"columns": columns[:10],
},
}
def build_image_carousel_template(columns: list[dict], alt_text: str = "Image Carousel") -> dict:
return {
"type": "template",
"altText": alt_text[:400],
"template": {
"type": "image_carousel",
"columns": columns[:10],
},
}
def build_image_carousel_column(
image_url: str,
action: dict | None = None,
) -> dict:
col: dict = {
"imageUrl": image_url,
"imageSize": "cover",
}
if action:
col["action"] = action
return col
def build_product_carousel(products: list[dict], alt_text: str = "Product Carousel") -> dict:
columns = []
for product in products[:10]:
column = {
"thumbnailImageUrl": product.get("image_url", ""),
"title": product.get("title", "")[:40],
"text": product.get("description", "")[:120],
"actions": [
{
"type": "uri",
"label": product.get("action_label", "View")[:20],
"uri": product.get("action_url", ""),
}
],
}
if product.get("price"):
column["defaultAction"] = {
"type": "uri",
"label": "View",
"uri": product.get("action_url", ""),
}
columns.append(column)
return build_carousel_template(columns, alt_text)
def build_link_menu(
title: str,
links: list[dict],
alt_text: str = "Link Menu",
) -> dict:
actions = []
for link in links[:4]:
actions.append(
{
"type": "uri",
"label": link.get("label", "")[:20],
"uri": link.get("uri", ""),
}
)
return (
{
"type": "template",
"altText": alt_text[:400],
"template": {
"type": "buttons",
"title": title[:40],
"text": "",
"actions": actions,
},
}
if actions
else {}
)
def build_yes_no_confirm(
question: str,
yes_label: str = "Yes",
no_label: str = "No",
alt_text: str = "Confirm",
) -> dict:
return build_confirm_template(
text=question,
actions=[
{"type": "message", "label": yes_label[:20], "text": "yes"},
{"type": "message", "label": no_label[:20], "text": "no"},
],
alt_text=alt_text,
)
def build_template_message_from_payload(payload: dict[str, Any]) -> dict | None:
tmpl_type = payload.get("type", "")
if tmpl_type == "buttons":
return build_buttons_template(
title=payload.get("title", ""),
text=payload.get("text", ""),
actions=payload.get("actions", []),
thumbnail_url=payload.get("thumbnail_url"),
alt_text=payload.get("alt_text", "Buttons Template"),
)
if tmpl_type == "confirm":
return build_confirm_template(
text=payload.get("text", ""),
actions=payload.get("actions", []),
alt_text=payload.get("alt_text", "Confirm"),
)
if tmpl_type == "carousel":
return build_carousel_template(
columns=payload.get("columns", []),
alt_text=payload.get("alt_text", "Carousel"),
)
if tmpl_type == "image_carousel":
return build_image_carousel_template(
columns=payload.get("columns", []),
alt_text=payload.get("alt_text", "Image Carousel"),
)
if tmpl_type == "product_carousel":
return build_product_carousel(
products=payload.get("products", []),
alt_text=payload.get("alt_text", "Product Carousel"),
)
if tmpl_type == "link_menu":
return build_link_menu(
title=payload.get("title", ""),
links=payload.get("links", []),
alt_text=payload.get("alt_text", "Link Menu"),
)
if tmpl_type == "yes_no":
return build_yes_no_confirm(
question=payload.get("question", ""),
yes_label=payload.get("yes_label", "Yes"),
no_label=payload.get("no_label", "No"),
alt_text=payload.get("alt_text", "Confirm"),
)
return None
def build_datetime_picker_action(
mode: str = "datetime",
label: str = "Select",
data: str = "",
initial: str | None = None,
max_value: str | None = None,
min_value: str | None = None,
) -> dict:
if mode not in ("date", "time", "datetime"):
mode = "datetime"
action: dict = {
"type": "datetimepicker",
"label": label[:20],
"data": data[:300],
"mode": mode,
}
if initial:
action["initial"] = initial
if max_value:
action["max"] = max_value
if min_value:
action["min"] = min_value
return action