新增 LINE 渠道扩展,支持在 Yuxi 平台中集成 LINE 即时通讯渠道。 包含以下功能模块: - bot: LINE Bot 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - signature: 请求签名验证 - token_manager: Token 管理 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - flex_templates: Flex 模板消息 - card_command: 卡片指令处理 - template_messages: 模板消息 - rich_menu: 富菜单管理 - actions: 动作处理 - directives: 指令处理 - delivery: 消息送达确认 - loading: 加载动画 - media: 媒体资源处理 - types: 类型定义
202 lines
6.0 KiB
Python
202 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
|
|
def markdown_to_line(md_text: str) -> str:
|
|
text = md_text
|
|
|
|
text = re.sub(r"\*\*(.+?)\*\*", r"【\1】", text)
|
|
text = re.sub(r"__([^_]+)__", r"【\1】", text)
|
|
|
|
text = re.sub(r"\*([^*\n]+)\*", r"_\1_", text)
|
|
text = re.sub(r"(?<!\w)_([^_\n]+)_(?!\w)", r"_\1_", text)
|
|
|
|
text = re.sub(r"`([^`\n]+)`", r"[ \1 ]", text)
|
|
|
|
text = re.sub(r"~~(.+?)~~", r"", text)
|
|
|
|
text = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", text)
|
|
|
|
text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1\n(\2)", text)
|
|
|
|
text = re.sub(r"^#{1,6}\s+(.+)$", r"■ \1", text, flags=re.MULTILINE)
|
|
|
|
text = re.sub(r"^>\s+(.+)$", r"┃ \1", text, flags=re.MULTILINE)
|
|
|
|
text = re.sub(r"^-{3,}$", "────────────", text, flags=re.MULTILINE)
|
|
|
|
text = re.sub(r"^[*-]\s+(.+)$", r"• \1", text, flags=re.MULTILINE)
|
|
|
|
return text.strip()
|
|
|
|
|
|
def extract_code_blocks(text: str) -> tuple[str, list[dict]]:
|
|
code_blocks: list[dict] = []
|
|
|
|
def _extract(match: re.Match) -> str:
|
|
language = match.group(1) or ""
|
|
code = match.group(2)
|
|
code_blocks.append({"language": language, "code": code[:2000]})
|
|
return ""
|
|
|
|
cleaned = re.sub(r"```(\w*)\n(.*?)```", _extract, text, flags=re.DOTALL)
|
|
return cleaned, code_blocks
|
|
|
|
|
|
def extract_flex_from_markdown(text: str) -> tuple[str, list[dict]]:
|
|
from yuxi.channel.extensions.line.flex_templates import create_code_block_card
|
|
|
|
text, code_blocks = extract_code_blocks(text)
|
|
text, table_flex_messages = convert_table_to_flex(text)
|
|
|
|
flex_messages: list[dict] = list(table_flex_messages)
|
|
|
|
for cb in code_blocks:
|
|
flex_messages.append(create_code_block_card(cb["code"], cb["language"]))
|
|
|
|
return text, flex_messages
|
|
|
|
|
|
def convert_table_to_flex(text: str) -> tuple[str, list[dict]]:
|
|
flex_messages: list[dict] = []
|
|
lines = text.split("\n")
|
|
result: list[str] = []
|
|
in_table = False
|
|
headers: list[str] = []
|
|
rows: list[list[str]] = []
|
|
|
|
for line in lines:
|
|
stripped = line.strip()
|
|
if stripped.startswith("|") and stripped.endswith("|"):
|
|
cells = [c.strip() for c in stripped[1:-1].split("|")]
|
|
if not in_table:
|
|
headers = cells
|
|
in_table = True
|
|
continue
|
|
if all(re.match(r"^[-:]+$", c) for c in cells):
|
|
continue
|
|
rows.append(cells)
|
|
else:
|
|
if in_table:
|
|
if len(headers) == 2:
|
|
flex_messages.append(_build_receipt_card(headers, rows))
|
|
elif len(headers) >= 3:
|
|
flex_messages.append(_build_flex_table(headers, rows[:10]))
|
|
headers = []
|
|
rows = []
|
|
in_table = False
|
|
result.append(line)
|
|
|
|
if in_table:
|
|
if len(headers) == 2:
|
|
flex_messages.append(_build_receipt_card(headers, rows))
|
|
elif len(headers) >= 3:
|
|
flex_messages.append(_build_flex_table(headers, rows[:10]))
|
|
|
|
return "\n".join(result), flex_messages
|
|
|
|
|
|
def _build_receipt_card(headers: list[str], rows: list[list[str]]) -> dict:
|
|
body_contents = []
|
|
for i, row in enumerate(rows):
|
|
label = row[0] if len(row) > 0 else ""
|
|
value = row[1] if len(row) > 1 else ""
|
|
bg_color = "#FFFFFF" if i % 2 == 0 else "#FAFAFA"
|
|
body_contents.append({
|
|
"type": "box",
|
|
"layout": "horizontal",
|
|
"backgroundColor": bg_color,
|
|
"contents": [
|
|
{"type": "text", "text": label, "flex": 1, "wrap": True},
|
|
{"type": "text", "text": value, "flex": 1, "wrap": True, "align": "end"},
|
|
],
|
|
})
|
|
|
|
return {
|
|
"type": "flex",
|
|
"altText": headers[0] if headers else "Receipt",
|
|
"contents": {
|
|
"type": "bubble",
|
|
"body": {
|
|
"type": "box",
|
|
"layout": "vertical",
|
|
"contents": body_contents,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _build_flex_table(headers: list[str], rows: list[list[str]]) -> dict:
|
|
header_items = []
|
|
for h in headers:
|
|
header_items.append({
|
|
"type": "text",
|
|
"text": h,
|
|
"weight": "bold",
|
|
"flex": 1,
|
|
"wrap": True,
|
|
"size": "sm",
|
|
})
|
|
|
|
body_contents = [
|
|
{
|
|
"type": "box",
|
|
"layout": "horizontal",
|
|
"contents": header_items,
|
|
},
|
|
{"type": "separator"},
|
|
]
|
|
|
|
for row in rows:
|
|
row_items = []
|
|
for i, h in enumerate(headers):
|
|
value = row[i] if i < len(row) else ""
|
|
row_items.append({
|
|
"type": "text",
|
|
"text": value,
|
|
"flex": 1,
|
|
"wrap": True,
|
|
"size": "sm",
|
|
})
|
|
body_contents.append({
|
|
"type": "box",
|
|
"layout": "horizontal",
|
|
"contents": row_items,
|
|
})
|
|
body_contents.append({"type": "separator"})
|
|
|
|
return {
|
|
"type": "flex",
|
|
"altText": f"Table: {headers[0]}" if headers else "Table",
|
|
"contents": {
|
|
"type": "bubble",
|
|
"body": {
|
|
"type": "box",
|
|
"layout": "vertical",
|
|
"contents": body_contents[: len(body_contents) - 1] if body_contents else [],
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def chunk_text(text: str, limit: int = 5000) -> list[str]:
|
|
if len(text) <= limit:
|
|
return [text]
|
|
|
|
chunks: list[str] = []
|
|
while len(text) > limit:
|
|
split_point = text.rfind("\n", 0, limit)
|
|
if split_point == -1 or split_point < limit // 2:
|
|
split_point = text.rfind(". ", 0, limit)
|
|
if split_point == -1 or split_point < limit // 2:
|
|
split_point = text.rfind(" ", 0, limit)
|
|
if split_point == -1 or split_point < limit // 2:
|
|
split_point = limit
|
|
|
|
chunks.append(text[:split_point].strip())
|
|
text = text[split_point:].strip()
|
|
|
|
if text:
|
|
chunks.append(text)
|
|
return chunks |