ForcePilot/backend/package/yuxi/channels/sdk/format.py
Kris 7a972055b7 feat: 新增渠道服务基础框架与核心工具类
新增大量渠道适配器相关的协议、策略、工具类与基础设施代码,包括:
1.  多协议定义:认证、消息、配置、网关等核心接口
2.  策略模块:上下文、群聊、去重、防抖等业务策略
3.  工具集:重试、去重、文本分块、消息格式化等SDK工具
4.  基础设施:外部进程管理、事件广播、熔断机制等
5.  账户与管道系统:账户管理、消息处理管道实现
6.  运行时服务:状态收集、维护任务、日志等后台服务
2026-05-12 00:53:57 +08:00

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 = {"&": "&amp;", "<": "&lt;", ">": "&gt;"}
_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