新增大量渠道适配器相关的协议、策略、工具类与基础设施代码,包括: 1. 多协议定义:认证、消息、配置、网关等核心接口 2. 策略模块:上下文、群聊、去重、防抖等业务策略 3. 工具集:重试、去重、文本分块、消息格式化等SDK工具 4. 基础设施:外部进程管理、事件广播、熔断机制等 5. 账户与管道系统:账户管理、消息处理管道实现 6. 运行时服务:状态收集、维护任务、日志等后台服务
106 lines
3.3 KiB
Python
106 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from html import escape
|
|
from typing import Any
|
|
|
|
from yuxi.channels.models import ChannelResponse
|
|
|
|
_ESCAPE_CHARS = {"&": "&", "<": "<", ">": ">"}
|
|
|
|
_CODE_BLOCK_RE = re.compile(r"```(?:\w+)?\n.+?```", re.DOTALL)
|
|
_INLINE_CODE_RE = re.compile(r"`([^`\n]+?)`")
|
|
|
|
_PLACEHOLDER_PREFIX = "\x00MDPLACEHOLDER"
|
|
|
|
TRUNCATION_MARKER = "\n...(内容过长已截断)"
|
|
|
|
|
|
def markdown_to_html(text: str) -> str:
|
|
code_blocks: dict[str, str] = {}
|
|
text = _protect_code_spans(text, _CODE_BLOCK_RE, code_blocks)
|
|
text = _protect_code_spans(text, _INLINE_CODE_RE, code_blocks)
|
|
|
|
text = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", text)
|
|
text = re.sub(r"(?<!\*)\*([^*\n]+?)\*(?!\*)", r"<i>\1</i>", text)
|
|
text = re.sub(r"(?<!_)_([^_\n]+?)_(?!_)", r"<i>\1</i>", text)
|
|
text = re.sub(r"~~(.+?)~~", r"<s>\1</s>", text)
|
|
text = re.sub(r"__(\w.*?\w)__", r"<u>\1</u>", text)
|
|
text = re.sub(r"\[(.+?)\]\((.+?)\)", r'<a href="\2">\1</a>', text)
|
|
text = re.sub(r"\|\|(.+?)\|\|", r"<tg-spoiler>\1</tg-spoiler>", text)
|
|
|
|
text = _restore_code_spans(text, code_blocks)
|
|
text = _sanitize_html(text)
|
|
return text
|
|
|
|
|
|
def strip_html_tags(text: str) -> str:
|
|
return re.sub(r"<[^>]+>", "", text)
|
|
|
|
|
|
def truncate_text(text: str, limit: int, marker: str | None = None) -> str:
|
|
m = marker if marker is not None else TRUNCATION_MARKER
|
|
if len(text) <= limit:
|
|
return text
|
|
return text[: limit - len(m)] + m
|
|
|
|
|
|
def _protect_code_spans(text: str, pattern: re.Pattern, storage: dict[str, str]) -> str:
|
|
result = []
|
|
idx = 0
|
|
for m in pattern.finditer(text):
|
|
result.append(text[idx : m.start()])
|
|
placeholder = f"{_PLACEHOLDER_PREFIX}{len(storage)}"
|
|
storage[placeholder] = m.group(0)
|
|
result.append(placeholder)
|
|
idx = m.end()
|
|
result.append(text[idx:])
|
|
return "".join(result)
|
|
|
|
|
|
def _restore_code_spans(text: str, storage: dict[str, str]) -> str:
|
|
result = text
|
|
for placeholder, code_text in storage.items():
|
|
if placeholder.startswith(f"{_PLACEHOLDER_PREFIX}") and "```" in code_text:
|
|
inner = code_text[3:].rstrip("`").lstrip("\n")
|
|
lang_end = inner.find("\n")
|
|
inner = inner[lang_end + 1 :] if lang_end >= 0 else inner
|
|
safe = escape(inner, quote=False)
|
|
result = result.replace(placeholder, f"<pre>{safe}</pre>")
|
|
elif placeholder in result:
|
|
inner = code_text.strip("`")
|
|
safe = escape(inner, quote=False)
|
|
result = result.replace(placeholder, f"<code>{safe}</code>")
|
|
return result
|
|
|
|
|
|
def _sanitize_html(text: str) -> str:
|
|
result = []
|
|
depth = 0
|
|
for ch in text:
|
|
if ch == "<":
|
|
depth += 1
|
|
elif ch == ">":
|
|
depth = max(0, depth - 1)
|
|
elif depth == 0:
|
|
if ch in _ESCAPE_CHARS:
|
|
result.append(_ESCAPE_CHARS[ch])
|
|
continue
|
|
result.append(ch)
|
|
return "".join(result)
|
|
|
|
|
|
def build_basic_outbound(
|
|
response: ChannelResponse,
|
|
*,
|
|
text_field: str = "text",
|
|
text: str | None = None,
|
|
extra: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
payload: dict[str, Any] = {text_field: text or response.content}
|
|
if response.reply_to_message_id:
|
|
payload["reply_to_message_id"] = response.reply_to_message_id
|
|
if extra:
|
|
payload.update(extra)
|
|
return payload
|