ForcePilot/backend/package/yuxi/channels/adapters/line/directives.py
Kris f736a0ebb3 refactor(line): 重构Line适配器相关代码,优化功能与可读性
1.  抽离alt文本截断逻辑为公共工具函数
2.  重构快速回复构建函数,支持更多action类型
3.  新增 narrowcast 消息发送与进度查询能力
4.  新增用户粉丝数、好友人口统计API调用
5.  优化回复消息逻辑,支持quote token
6.  改进审批ID生成逻辑,避免重复
7.  调整部分配置与异常处理逻辑
8.  新增熔断器与相关指标统计
2026-05-13 16:11:04 +08:00

121 lines
4.6 KiB
Python

from __future__ import annotations
import re
from yuxi.channels.adapters.line.flex_templates.common import truncate_alt_text
from yuxi.channels.adapters.line.quick_reply import build_quick_reply_items
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 = build_quick_reply_items(labels)
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": truncate_alt_text(question),
"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": truncate_alt_text(title),
"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