ForcePilot/backend/package/yuxi/channel/extensions/line/format.py
Kris 7fd658af57 feat(channel): 添加 LINE 渠道扩展
新增 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: 类型定义
2026-05-21 11:16:16 +08:00

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