ForcePilot/backend/package/yuxi/channel/extensions/telegram/format.py
Kris 8a3601250e feat(channel): 添加 Telegram 渠道扩展
新增 Telegram 渠道扩展,支持在 Yuxi 平台中集成 Telegram 即时通讯渠道。

包含以下功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- polling: 长轮询模式
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- actions: 动作处理
- inline_keyboard: 内联键盘
- native_commands: 原生指令
- chat: 聊天管理
- delivery: 消息送达确认
- media: 媒体资源处理
- profile: 用户资料
- reactions: 表情反应
- sticker: 贴纸处理
- types: 类型定义
2026-05-21 11:48:09 +08:00

126 lines
3.6 KiB
Python

from __future__ import annotations
import re
from html import escape
_HTML_TAGS = [
("<b>", "</b>"),
("<i>", "</i>"),
("<s>", "</s>"),
("<u>", "</u>"),
("<code>", "</code>"),
("<pre>", "</pre>"),
("<tg-spoiler>", "</tg-spoiler>"),
("<blockquote>", "</blockquote>"),
("<tg-emoji", "</tg-emoji>"),
]
_RE_MARKDOWN_TO_HTML = [
(re.compile(r"\*\*\*(.+?)\*\*\*"), r"<b><i>\1</i></b>"),
(re.compile(r"\*\*_(.+?)_\*\*"), r"<b><i>\1</i></b>"),
(re.compile(r"_\*\*(.+?)\*\*_"), r"<i><b>\1</b></i>"),
(re.compile(r"\*\*(.+?)\*\*"), r"<b>\1</b>"),
(re.compile(r"__(.+?)__"), r"<u>\1</u>"),
(re.compile(r"~~(.+?)~~"), r"<s>\1</s>"),
(re.compile(r"\|\|(.+?)\|\|"), r"<tg-spoiler>\1</tg-spoiler>"),
(re.compile(r"(?<!\w)_(.+?)_(?!\w)"), r"<i>\1</i>"),
(re.compile(r"(?<!\w)\*(.+?)\*(?!\w)"), r"<i>\1</i>"),
(re.compile(r"(?<!\w)`([^`]+)`(?!\w)"), r"<code>\1</code>"),
(re.compile(r"\[([^\]]+)\]\(([^)]+)\)"), r'<a href="\2">\1</a>'),
]
_BULLET_LINE_RE = re.compile(r"^(\s*)\*\s", re.MULTILINE)
_BLOCKQUOTE_RE = re.compile(r"^> (.+)$", re.MULTILINE)
_CODE_BLOCK_RE = re.compile(r"```(\w+)?\s*\n?(.+?)```", re.DOTALL)
def markdown_to_telegram_html(md_text: str) -> str:
text = md_text
code_blocks = []
def _save_code_block(m: re.Match) -> str:
lang = m.group(1) or ""
code = m.group(2)
code_blocks.append(f"<pre><code class=\"language-{lang}\">{escape(code)}</code></pre>")
return f"\x00CODEBLOCK{len(code_blocks) - 1}\x00"
text = _CODE_BLOCK_RE.sub(_save_code_block, text)
text = escape(text)
for pattern, replacement in _RE_MARKDOWN_TO_HTML:
try:
text = pattern.sub(replacement, text)
except re.error:
pass
text = _BULLET_LINE_RE.sub(r"\1• ", text)
text = _BLOCKQUOTE_RE.sub(r"<blockquote>\1</blockquote>", text)
text = text.replace("*", "").replace("_", "")
for i, block in enumerate(code_blocks):
text = text.replace(f"\x00CODEBLOCK{i}\x00", block)
return text.strip()
def split_telegram_html_chunks(html: str, limit: int = 4096) -> list[str]:
if len(html) <= limit:
return [html]
chunks: list[str] = []
tag_stack: list[str] = []
start = 0
i = 0
while i < len(html):
if i - start >= limit:
boundary = _find_safe_boundary(html, i, tag_stack)
chunk = html[start:boundary]
chunk = _close_open_tags(chunk, tag_stack)
chunks.append(chunk)
start = _skip_whitespace(html, boundary)
i = start
continue
i += 1
if start < len(html):
chunk = html[start:]
chunk = _close_open_tags(chunk, tag_stack)
chunks.append(chunk)
return chunks
def _find_safe_boundary(html: str, pos: int, tag_stack: list[str]) -> int:
end = min(pos, len(html))
for i in range(end - 1, max(pos - 200, 0), -1):
if html[i] == "\n":
return i
return end
def _close_open_tags(chunk: str, tag_stack: list[str]) -> str:
result = chunk
for tag in reversed(tag_stack):
close_tag = {"<b>": "</b>", "<i>": "</i>", "<s>": "</s>", "<u>": "</u>",
"<code>": "</code>", "<pre>": "</pre>",
'<a': '</a>',
"<tg-spoiler>": "</tg-spoiler>",
"<blockquote>": "</blockquote>",
"<tg-emoji": "</tg-emoji>"}.get(tag, "")
if close_tag:
result += close_tag
return result
def _skip_whitespace(html: str, pos: int) -> int:
while pos < len(html) and html[pos] in (" ", "\n", "\r", "\t"):
pos += 1
return pos