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

124 lines
4.6 KiB
Python

from __future__ import annotations
import re
QUICK_REPLIES_RE = re.compile(r"\[\[quick_replies:\s*(.+?)\]\]")
CONFIRM_RE = re.compile(r"\[\[confirm:\s*(.+?)\|\s*(.+?)\|\s*(.+?)\]\]")
BUTTONS_RE = re.compile(r"\[\[buttons:\s*(.+?)\]\]")
LOCATION_RE = re.compile(r"\[\[location:\s*(.+?)\]\]")
_MAX_DIRECTIVES = 5
def parse_line_directives(text: str) -> list[dict] | None:
if not text or not text.strip():
return None
result: list[dict] = []
qr_match = QUICK_REPLIES_RE.search(text)
if qr_match and len(result) < _MAX_DIRECTIVES:
labels = [lbl.strip() for lbl in qr_match.group(1).split(",") if lbl.strip()]
main_text = QUICK_REPLIES_RE.sub("", text).strip()
items = [
{
"type": "action",
"action": {"type": "message", "label": label[:20], "text": label},
}
for label in labels[:13]
]
if items:
result.append(
{
"type": "text",
"text": main_text[:5000],
"quickReply": {"items": items},
}
)
cf_match = CONFIRM_RE.search(text)
if cf_match and len(result) < _MAX_DIRECTIVES:
question = cf_match.group(1).strip()
yes_label = cf_match.group(2).strip()
no_label = cf_match.group(3).strip()
main_text = CONFIRM_RE.sub("", text).strip()
if main_text and question:
result.append({"type": "text", "text": main_text[:5000]})
if question:
result.append(
{
"type": "template",
"altText": question[:400],
"template": {
"type": "confirm",
"text": question[:240],
"actions": [
{"type": "message", "label": yes_label[:20], "text": "yes"},
{"type": "message", "label": no_label[:20], "text": "no"},
],
},
}
)
btn_match = BUTTONS_RE.search(text)
if btn_match and len(result) < _MAX_DIRECTIVES:
parts = [p.strip() for p in btn_match.group(1).split("|")]
if len(parts) >= 1:
title = parts[0] if len(parts) > 0 else ""
description = parts[1] if len(parts) > 1 else ""
btn_defs = parts[2] if len(parts) > 2 else ""
actions = []
if btn_defs.strip():
for btn_def in btn_defs.split(",")[:4]:
btn_def = btn_def.strip()
if not btn_def:
continue
if ":" in btn_def:
b_label, b_data = btn_def.split(":", 1)
b_label, b_data = b_label.strip(), b_data.strip()
if b_data.startswith("https://"):
actions.append({"type": "uri", "label": b_label[:20], "uri": b_data})
else:
actions.append({"type": "message", "label": b_label[:20], "text": b_data})
main_text = BUTTONS_RE.sub("", text).strip()
if main_text:
result.append({"type": "text", "text": main_text[:5000]})
if title and actions:
result.append(
{
"type": "template",
"altText": title[:400],
"template": {
"type": "buttons",
"title": title[:40],
"text": description[:160],
"actions": actions,
},
}
)
loc_match = LOCATION_RE.search(text)
if loc_match and len(result) < _MAX_DIRECTIVES:
parts = [p.strip() for p in loc_match.group(1).split("|")]
if len(parts) >= 4:
title, address = parts[0], parts[1]
try:
lat, lon = float(parts[2]), float(parts[3])
except ValueError:
lat, lon = 0.0, 0.0
main_text = LOCATION_RE.sub("", text).strip()
if main_text:
result.append({"type": "text", "text": main_text[:5000]})
result.append(
{
"type": "location",
"title": title[:100],
"address": address[:100],
"latitude": lat,
"longitude": lon,
}
)
return result[:_MAX_DIRECTIVES] if result else None