ForcePilot/backend/package/yuxi/channel/extensions/feishu/format.py
Kris 5e91bb9985 feat(feishu): 新增飞书渠道完整插件实现
新增飞书渠道插件,包含基础配置、事件处理、消息收发、工具调用、权限管理等完整功能模块,支持WebSocket长连接、卡片消息、流式响应、群管理、审批通知等能力
2026-05-21 10:46:42 +08:00

152 lines
4.8 KiB
Python

from __future__ import annotations
import re
from yuxi.channel.security.html_guard import sanitize_text_content_preserve_entities
_TEXT_CONTENT_RE = re.compile(r'{"text":"((?:[^"\\]|\\.)*)"}')
_ESCAPE_RE = re.compile(r"([\\*_{}\[\]()#+\-!|>~])")
def markdown_to_feishu_post(md_text: str) -> dict:
lines = md_text.split("\n")
elements: list[dict] = []
i = 0
while i < len(lines):
line = lines[i]
if line.startswith("```"):
language = line[3:].strip()
code_lines = []
i += 1
while i < len(lines) and not lines[i].startswith("```"):
code_lines.append(lines[i])
i += 1
i += 1
elements.append(_build_code_block("\n".join(code_lines), language))
continue
if line.startswith("### "):
elements.append(_build_paragraph([_build_text(line[4:], bold=True)]))
elif line.startswith("## "):
elements.append(_build_paragraph([_build_text(line[3:], bold=True)]))
elif line.startswith("# "):
elements.append(_build_paragraph([_build_text(line[2:], bold=True)]))
elif line.startswith("> "):
content = line[2:]
content = _ESCAPE_RE.sub(r"\\\1", content)
elements.append(_build_paragraph([_build_text(content)]))
elif line.startswith("- ") or line.startswith("* "):
content = _parse_inline_markdown(line[2:])
elements.append(_build_paragraph([_build_text("" + content)]))
elif re.match(r"^\d+\.\s", line):
content = _parse_inline_markdown(re.sub(r"^\d+\.\s", "", line))
elements.append(_build_paragraph([_build_text(content)]))
elif line.strip() == "":
pass
else:
content = _parse_inline_markdown(line)
if content:
elements.append(_build_paragraph([_build_text(content)]))
i += 1
if not elements:
elements.append(_build_paragraph([_build_text("")]))
return {
"zh_cn": {
"title": "",
"content": [elements],
}
}
def md_to_plain_text(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"\*([^*]+)\*", r"\1", text)
text = re.sub(r"_([^_]+)_", r"\1", text)
text = re.sub(r"~~(.+?)~~", r"\1", text)
text = re.sub(r"```(\w*)\n(.*?)```", r"\2", text, flags=re.DOTALL)
text = re.sub(r"`([^`]+)`", r"\1", text)
text = re.sub(r"\[([^\]]*)\]\([^)]+\)", r"\1", text)
text = re.sub(r"!\[([^\]]*)\]\([^)]+\)", r"[\1]", text)
text = re.sub(r"^#{1,6}\s+", "", text, flags=re.MULTILINE)
text = re.sub(r"^>\s+", "", text, flags=re.MULTILINE)
text = re.sub(r"^[*-]\s+", "", text, flags=re.MULTILINE)
return text.strip()
def _parse_inline_markdown(text: str) -> str:
result = _ESCAPE_RE.sub(r"\\\1", text)
result = re.sub(r"\*\*(.+?)\*\*", r"\1", result)
result = re.sub(r"__([^_]+)__", r"\1", result)
result = re.sub(r"\*([^*]+)\*", r"\1", result)
result = re.sub(r"`([^`]+)`", r"\1", result)
result = re.sub(r"~~(.+?)~~", r"\1", result)
return result
def _build_text(content: str, bold: bool = False) -> dict:
return {
"tag": "text",
"text": sanitize_text_content_preserve_entities(content),
"style": ["bold"] if bold else [],
}
def _build_paragraph(elements: list[dict]) -> dict:
return {
"tag": "p",
"children": elements if elements else [_build_text("")],
}
def _build_code_block(code: str, language: str = "") -> dict:
return {
"tag": "pre",
"children": [{
"tag": "code",
"code": sanitize_text_content_preserve_entities(code),
"code_language": sanitize_text_content_preserve_entities(language) or "plaintext",
}],
}
def chunk_post_content(elements: list[list[dict]], limit: int = 28000) -> list[list[list[dict]]]:
chunks: list[list[list[dict]]] = []
current: list[list[dict]] = []
current_size = 0
for para_elements in elements:
para_size = _estimate_paragraph_size(para_elements)
if current_size + para_size > limit and current:
chunks.append(current)
current = []
current_size = 0
current.append(para_elements)
current_size += para_size
if current:
chunks.append(current)
return chunks if chunks else [elements]
def _estimate_paragraph_size(para_elements: list[dict]) -> int:
size = 0
for elem in para_elements:
if isinstance(elem, dict):
size += len(str(elem.get("text", ""))) + 50
return size
def text_to_post_message(text: str) -> str:
import json
post = markdown_to_feishu_post(text)
return json.dumps(post, ensure_ascii=False)