ForcePilot/backend/package/yuxi/channel/extensions/zalouser/format.py
Kris d5e36d33b7 feat(channel): 添加 Zalo OA、Zoom Chat 和 Zulip 渠道扩展
新增 Zalo OA、Zoom Chat、Zulip 三个渠道扩展。

Zalo OA 渠道扩展主要模块:sidecar_client, config, gateway, outbound, streaming, pairing, security, auth, dedupe, directory, monitor, status, session, reactions, tools

Zoom Chat 渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, crypto, dedupe, actions, media, mentions, monitor, status, session, reactions, threading

Zulip 渠道扩展主要模块:client, config, gateway, outbound, streaming, pairing, security, monitor, status
2026-05-21 12:06:26 +08:00

134 lines
4.2 KiB
Python

from __future__ import annotations
import re
from typing import Any
ZALO_TEXT_LIMIT = 2000
_FORMAT_TAG_PATTERN = re.compile(r"\{(\w+)\}(.*?)\{/(\w+)\}", re.DOTALL)
def parse_zalouser_text_styles(markdown: str) -> list[dict[str, Any]]:
styles: list[dict[str, Any]] = []
code_block_pattern = re.compile(r"```[\s\S]*?```")
code_blocks = code_block_pattern.findall(markdown)
placeholder = "\u0000"
text = markdown
for cb in code_blocks:
text = text.replace(cb, placeholder, 1)
def _capture_style(start: int, end: int, style_type: str) -> None:
styles.append(
{
"start": start,
"end": end,
"type": style_type,
}
)
def _apply_regex(pat: re.Pattern, style_type: str, text_cursor: str) -> str:
offset = 0
result = list(text_cursor)
for m in pat.finditer(text_cursor):
inner = m.group(2) if pat.groups >= 2 else m.group(1)
raw_start = m.start() - offset
raw_end = raw_start + len(inner)
_capture_style(raw_start, raw_end, style_type)
result[m.start() - offset : m.end() - offset] = [inner]
offset += m.end() - m.start() - len(inner)
return "".join(result)
text = _apply_regex(re.compile(r"\*\*(.+?)\*\*"), "b", text)
text = _apply_regex(re.compile(r"__(.+?)__"), "u", text)
text = _apply_regex(re.compile(r"\*(.+?)\*"), "i", text)
text = _apply_regex(re.compile(r"~~(.+?)~~"), "s", text)
heading_pat = re.compile(r"^#{1,3}\s+(.+)$", re.MULTILINE)
for m in heading_pat.finditer(text):
_capture_style(m.start(1), m.end(1), "b")
_capture_style(m.start(1), m.end(1), "f_18")
custom_tag_pat = re.compile(r"\{(\w+)\}(.*?)\{/(\w+)\}", re.DOTALL)
tag_style_map = {
"red": "c_db342e",
"orange": "c_f27806",
"yellow": "c_f7b503",
"green": "c_15a85f",
"blue": "c_4391d9",
"big": "f_18",
}
for m in custom_tag_pat.finditer(text):
tag = m.group(1)
if tag in tag_style_map:
_capture_style(m.start(2), m.end(2), tag_style_map[tag])
pid = 0
for pid, ph in enumerate(code_blocks):
text = text.replace(placeholder, ph, 1)
return styles
def chunk_text_for_outbound(text: str, limit: int = ZALO_TEXT_LIMIT) -> list[str]:
if len(text) <= limit:
return [text]
chunks: list[str] = []
paragraphs = text.split("\n\n")
current = ""
for para in paragraphs:
if len(current) + len(para) + 2 <= limit:
current = f"{current}\n\n{para}" if current else para
else:
if current:
chunks.append(current)
if len(para) <= limit:
current = para
else:
current = ""
para_remain = para
while len(para_remain) > limit:
split_at = para_remain.rfind("\n", 0, limit)
if split_at == -1:
split_at = para_remain.rfind(" ", 0, limit)
if split_at == -1:
split_at = limit
chunks.append(para_remain[:split_at].strip())
para_remain = para_remain[split_at:].strip()
if para_remain:
current = para_remain
if current:
chunks.append(current)
return chunks if chunks else [text[:limit]]
def resolve_reaction_icon(raw: str) -> str | None:
alias = raw.strip().lower()
from yuxi.channel.extensions.zalouser.types import REACTION_ALIAS_MAP
if alias in REACTION_ALIAS_MAP:
return REACTION_ALIAS_MAP[alias].value
for icon in REACTION_ALIAS_MAP.values():
if icon.value.lower() == alias:
return icon.value
return None
def strip_own_mentions(content: str, bot_name: str = "") -> str:
if not bot_name:
return content
pattern = re.compile(rf"@{re.escape(bot_name)}\s*", re.IGNORECASE)
return pattern.sub("", content).strip()
def extract_mention_ids(content: str) -> list[str]:
pattern = re.compile(r"@(\w+)")
return pattern.findall(content)
def format_for_zalouser_markdown(markdown: str) -> str:
return markdown