ForcePilot/backend/package/yuxi/channel/extensions/line/card_command.py

288 lines
9.4 KiB
Python
Raw Normal View History

from __future__ import annotations
import shlex
from yuxi.channel.extensions.line.actions import create_message_action, create_uri_action, create_postback_action
from yuxi.channel.extensions.line.template_messages import (
create_buttons_template,
create_confirm_template,
create_image_carousel_template,
create_image_carousel_column,
create_yes_no_confirm,
create_button_menu,
create_link_menu,
create_product_carousel,
to_template_message,
)
from yuxi.channel.extensions.line.flex_templates import (
create_receipt_card,
create_event_card,
create_agenda_card,
create_media_player_card,
create_device_control_card,
create_apple_tv_remote,
)
LINE_CARD_COMMANDS: dict[str, dict] = {
"info": {
"syntax": '/card info "Title" "Body" ["Footer"]',
"description": "信息卡片(标题+正文+可选页脚)",
},
"image": {
"syntax": '/card image "Title" "Caption" --url <image-url>',
"description": "图片卡片",
},
"action": {
"syntax": '/card action "Title" "Body" --actions "Btn1|url1,Btn2|/cmd"',
"description": "操作卡片(自动识别 URL/命令/postback",
},
"list": {
"syntax": '/card list "Title" "Item1|Desc1,Item2|Desc2"',
"description": "列表卡片",
},
"receipt": {
"syntax": '/card receipt "Title" "Item1:$10,Item2:$20" --total "$30"',
"description": "收据/摘要卡片",
},
"confirm": {
"syntax": '/card confirm "Question?" --yes "Yes|data" --no "No|data"',
"description": "确认对话框",
},
"buttons": {
"syntax": '/card buttons "Title" "Text" --actions "Btn1|url,Btn2|data"',
"description": "按钮模板",
},
}
def parse_card_command(text: str) -> dict | None:
if not text.startswith("/card "):
return None
args_str = text[6:].strip()
try:
parts = shlex.split(args_str)
except ValueError:
return None
if not parts:
return None
card_type = parts[0].lower()
if card_type not in LINE_CARD_COMMANDS:
return None
positional: list[str] = []
flags: dict[str, str] = {}
current_flag: str | None = None
for item in parts[1:]:
if item.startswith("--"):
current_flag = item[2:]
flags[current_flag] = ""
elif current_flag:
flags[current_flag] = item
current_flag = None
else:
positional.append(item)
return {
"type": card_type,
"positional": positional,
"flags": flags,
}
def build_card_message(parsed: dict) -> dict | None:
card_type = parsed["type"]
positional = parsed.get("positional", [])
flags = parsed.get("flags", {})
match card_type:
case "info":
title = positional[0] if len(positional) > 0 else "Info"
body = positional[1] if len(positional) > 1 else ""
footer = positional[2] if len(positional) > 2 else None
return create_info_card(title, body, footer)
case "image":
title = positional[0] if len(positional) > 0 else "Image"
caption = positional[1] if len(positional) > 1 else ""
url = flags.get("url", "")
return create_image_card(title, caption, url)
case "action":
title = positional[0] if len(positional) > 0 else "Actions"
body = positional[1] if len(positional) > 1 else ""
actions_str = flags.get("actions", "")
return create_action_card(title, body, actions_str)
case "list":
title = positional[0] if len(positional) > 0 else "List"
items_str = positional[1] if len(positional) > 1 else ""
return create_list_card(title, items_str)
case "receipt":
title = positional[0] if len(positional) > 0 else "Receipt"
items_str = positional[1] if len(positional) > 1 else ""
total = flags.get("total")
footer = flags.get("footer")
return create_receipt_card_command(title, items_str, total, footer)
case "confirm":
question = positional[0] if positional else "Are you sure?"
yes_data = flags.get("yes", "Yes|yes")
no_data = flags.get("no", "No|no")
return create_confirm_card(question, yes_data, no_data)
case "buttons":
title = positional[0] if len(positional) > 0 else "Menu"
text = positional[1] if len(positional) > 1 else ""
actions_str = flags.get("actions", "")
return create_buttons_card(title, text, actions_str)
case _:
return None
def create_info_card(title: str, body: str, footer: str | None = None) -> dict:
contents = {
"type": "bubble",
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{"type": "text", "text": title, "weight": "bold", "size": "xl", "wrap": True},
{"type": "text", "text": body, "size": "md", "wrap": True, "margin": "md"},
],
},
}
if footer:
contents["footer"] = {
"type": "box",
"layout": "vertical",
"contents": [
{"type": "text", "text": footer, "size": "xs", "color": "#AAAAAA", "wrap": True},
],
}
return {"type": "flex", "altText": title[:400], "contents": contents}
def create_image_card(title: str, caption: str, url: str) -> dict:
return {
"type": "flex",
"altText": title[:400],
"contents": {
"type": "bubble",
"hero": {
"type": "image",
"url": url,
"size": "full",
"aspectRatio": "16:9",
"aspectMode": "cover",
},
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{"type": "text", "text": title, "weight": "bold", "size": "lg", "wrap": True},
{"type": "text", "text": caption, "size": "sm", "wrap": True, "color": "#888888",
"margin": "md"},
],
},
},
}
def create_action_card(title: str, body: str, actions_str: str) -> dict:
actions = parse_actions(actions_str)
return create_buttons_template(
title=title,
text=body,
actions=actions,
)
def create_list_card(title: str, items_str: str) -> dict:
items: list[tuple[str, str]] = []
for item in items_str.split(","):
parts = item.strip().split("|", 1)
label = parts[0].strip()
desc = parts[1].strip() if len(parts) > 1 else ""
items.append((label, desc))
return create_receipt_card(title, items)
def create_receipt_card_command(title: str, items_str: str, total: str | None, footer: str | None) -> dict:
items: list[tuple[str, str]] = []
for item in items_str.split(","):
parts = item.strip().split(":", 1)
label = parts[0].strip()
value = parts[1].strip() if len(parts) > 1 else ""
items.append((label, value))
return create_receipt_card(title, items, total=total, footer=footer)
def create_confirm_card(question: str, yes_data: str, no_data: str) -> dict:
yes_label, yes_d = _split_label_data(yes_data, "Yes", "yes")
no_label, no_d = _split_label_data(no_data, "No", "no")
action_type = _detect_action_type(yes_d)
confirm_action = _build_action(action_type, yes_label, yes_d)
cancel_action = _build_action(_detect_action_type(no_d), no_label, no_d)
template = create_confirm_template(question, confirm_action, cancel_action)
return to_template_message(question, template)
def create_buttons_card(title: str, text: str, actions_str: str) -> dict:
actions = parse_actions(actions_str)
template = create_buttons_template(title=title, text=text, actions=actions)
return to_template_message(title, template)
def parse_actions(actions_str: str) -> list[dict]:
actions: list[dict] = []
if not actions_str:
return actions
for item in actions_str.split(","):
parts = item.strip().split("|", 1)
label = parts[0].strip()
data = parts[1].strip() if len(parts) > 1 else label
action_type = _detect_action_type(data)
actions.append(_build_action(action_type, label, data))
return actions
def _split_label_data(raw: str, default_label: str, default_data: str) -> tuple[str, str]:
if "|" in raw:
parts = raw.split("|", 1)
return parts[0].strip(), parts[1].strip()
return default_label, raw if raw else default_data
def _detect_action_type(data: str) -> str:
if data.startswith(("http://", "https://")):
return "uri"
if "=" in data and ":" not in data:
return "postback"
return "message"
def _build_action(action_type: str, label: str, data: str) -> dict:
match action_type:
case "uri":
return {"type": "uri", "label": label[:20], "uri": data}
case "postback":
return {"type": "postback", "label": label[:20], "data": data}
case _:
return {"type": "message", "label": label[:20], "text": data}
def create_image_carousel_column(
image_url: str,
action_type: str = "uri",
label: str = "",
data: str = "",
) -> dict:
return {
"imageUrl": image_url,
"action": _build_action(action_type, label, data),
}