本次提交对蓝泡泡适配器进行了多维度改进: 1. 新增缓存超限自动清理逻辑,防止内存溢出 2. 增强流式打字同步功能,增加超时自动停止逻辑 3. 重构markdown渲染逻辑,修复格式匹配问题并支持更多语法 4. 新增配置校验工具函数,增加streaming_mode和auth_strategy的合法性校验 5. 新增多项环境配置参数,完善配置项 6. 重构初始化流程,新增多个工具类实例 7. 优化TTS禁用逻辑,新增aiohttp依赖检查 8. 重构流式消息发送逻辑,整合打字指示器逻辑 9. 新增健康状态检查的问题检测功能
144 lines
4.1 KiB
Python
144 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
_ALLOWED_TAGS = {"b", "i", "u", "s", "a", "br", "code", "pre", "blockquote", "img"}
|
|
|
|
_UNSUPPORTED_REPLACEMENTS: dict[str, tuple[str, str]] = {
|
|
"strong": ("<b>", "</b>"),
|
|
"em": ("<i>", "</i>"),
|
|
"strike": ("<s>", "</s>"),
|
|
"del": ("<s>", "</s>"),
|
|
"ins": ("<u>", "</u>"),
|
|
"h1": ("<b>", "</b>\n"),
|
|
"h2": ("<b>", "</b>\n"),
|
|
"h3": ("<b>", "</b>\n"),
|
|
"h4": ("<b>", "</b>\n"),
|
|
"h5": ("<b>", "</b>\n"),
|
|
"h6": ("<b>", "</b>\n"),
|
|
"p": ("\n", ""),
|
|
"ul": ("", ""),
|
|
"ol": ("", ""),
|
|
"li": ("• ", "\n"),
|
|
"span": ("", ""),
|
|
"div": ("\n", ""),
|
|
"hr": ("\n---\n", ""),
|
|
}
|
|
|
|
_STRIP_TAGS = {"script", "style", "head", "meta", "link", "iframe", "object", "embed"}
|
|
|
|
|
|
def render_html_to_imessage(html: str) -> str:
|
|
if not html:
|
|
return ""
|
|
|
|
for tag in _STRIP_TAGS:
|
|
html = re.sub(rf"<{tag}[^>]*>.*?</{tag}>", "", html, flags=re.DOTALL | re.IGNORECASE)
|
|
html = re.sub(rf"<{tag}[^>]*/>", "", html, flags=re.IGNORECASE)
|
|
|
|
for tag, (open_tag, close_tag) in _UNSUPPORTED_REPLACEMENTS.items():
|
|
html = re.sub(rf"<{tag}(\s[^>]*)?>", open_tag, html, flags=re.IGNORECASE)
|
|
html = re.sub(rf"</{tag}>", close_tag, html, flags=re.IGNORECASE)
|
|
|
|
html = _strip_unsafe_attributes(html)
|
|
|
|
html = _normalize_links(html)
|
|
|
|
html = html.replace("\r\n", "\n").replace("\r", "\n")
|
|
|
|
return html.strip()
|
|
|
|
|
|
def _strip_unsafe_attributes(html: str) -> str:
|
|
def _clean_tag(tag_match: re.Match) -> str:
|
|
full_tag = tag_match.group(0)
|
|
tag_name_match = re.match(r"</?(\w+)", full_tag)
|
|
if not tag_name_match:
|
|
return full_tag
|
|
tag_name = tag_name_match.group(1).lower()
|
|
if tag_name not in _ALLOWED_TAGS:
|
|
return full_tag
|
|
safe_attrs = ["href", "src", "alt"]
|
|
result = full_tag
|
|
for attr in re.finditer(r'(\w+)="([^"]*)"', full_tag):
|
|
if attr.group(1).lower() not in safe_attrs:
|
|
result = result.replace(attr.group(0), "")
|
|
return result
|
|
|
|
return re.sub(r"<[^>]+>", _clean_tag, html)
|
|
|
|
|
|
def _normalize_links(text: str) -> str:
|
|
url_pattern = re.compile(r"(?<!href=\")(https?://[^\s<>\"\)]+)")
|
|
return url_pattern.sub(r'<a href="\1">\1</a>', text)
|
|
|
|
|
|
def render_markdown_to_imessage(text: str) -> str:
|
|
if not text:
|
|
return ""
|
|
|
|
codes: dict[str, str] = {}
|
|
|
|
def _save_code(m: re.Match) -> str:
|
|
key = f"\x00CODE{len(codes)}\x00"
|
|
codes[key] = m.group(0)
|
|
return key
|
|
|
|
text = re.sub(r"`[^`]+`", _save_code, text)
|
|
|
|
tokens = _tokenize_markdown(text)
|
|
result = _render_tokens(tokens)
|
|
|
|
for key, val in codes.items():
|
|
result = result.replace(key, val)
|
|
|
|
return result
|
|
|
|
|
|
def _tokenize_markdown(text: str) -> list[tuple[str, str]]:
|
|
TOKEN_RE = re.compile(r"(\*\*|__|(?<!\*)\*(?!\*)|(?<!\\)~{1,2})")
|
|
tokens: list[tuple[str, str]] = []
|
|
pos = 0
|
|
for m in TOKEN_RE.finditer(text):
|
|
if m.start() > pos:
|
|
tokens.append(("text", text[pos : m.start()]))
|
|
tokens.append(("marker", m.group()))
|
|
pos = m.end()
|
|
if pos < len(text):
|
|
tokens.append(("text", text[pos:]))
|
|
return tokens
|
|
|
|
|
|
def _render_tokens(tokens: list[tuple[str, str]]) -> str:
|
|
stack: list[tuple[str, str]] = []
|
|
result: list[str] = []
|
|
|
|
MARKER_MAP = {
|
|
"**": ("<b>", "</b>"),
|
|
"__": ("<b>", "</b>"),
|
|
"*": ("<i>", "</i>"),
|
|
"~": ("<s>", "</s>"),
|
|
"~~": ("<s>", "</s>"),
|
|
}
|
|
|
|
for kind, value in tokens:
|
|
if kind == "text":
|
|
result.append(value)
|
|
elif kind == "marker":
|
|
if value in MARKER_MAP:
|
|
if stack and stack[-1][0] == value:
|
|
stack.pop()
|
|
result.append(MARKER_MAP[value][1])
|
|
else:
|
|
stack.append((value, MARKER_MAP[value][0]))
|
|
result.append(MARKER_MAP[value][0])
|
|
else:
|
|
result.append(value)
|
|
|
|
for marker, _ in reversed(stack):
|
|
mapped = MARKER_MAP.get(marker)
|
|
if mapped:
|
|
result.append(mapped[1])
|
|
|
|
return "".join(result)
|