ForcePilot/backend/package/yuxi/channel/extensions/helpscout/format.py
Kris 432d5db970 feat(helpscout): 新增Help Scout客服工单渠道插件
完成Help Scout渠道的完整功能实现,包含API客户端、webhook处理、轮询同步、自动回复、工单标签分配、客户资料管理等核心能力,附带完整的配置Schema与插件元信息
2026-05-21 10:49:56 +08:00

145 lines
3.9 KiB
Python

from __future__ import annotations
import logging
import re
from html import unescape
logger = logging.getLogger(__name__)
try:
from bs4 import BeautifulSoup
HAS_BS4 = True
except ImportError:
HAS_BS4 = False
logger.warning("beautifulsoup4 not installed — Help Scout HTML parsing will use fallback regex")
_EMAIL_QUOTE_PATTERNS = [
re.compile(r"^On\s+.*wrote:\s*$", re.IGNORECASE | re.MULTILINE),
re.compile(r"^>{1,}\s?", re.MULTILINE),
re.compile(r"^-{2,}\s*Original Message\s*-{2,}", re.IGNORECASE),
re.compile(r"^From:\s+.*$", re.IGNORECASE | re.MULTILINE),
re.compile(r"^Sent:\s+.*$", re.IGNORECASE | re.MULTILINE),
re.compile(r"^To:\s+.*$", re.IGNORECASE | re.MULTILINE),
re.compile(r"^Subject:\s+.*$", re.IGNORECASE | re.MULTILINE),
re.compile(r"^Date:\s+.*$", re.IGNORECASE | re.MULTILINE),
]
def html_to_text(html_body: str) -> str:
if not html_body:
return ""
if HAS_BS4:
return _bs4_html_to_text(html_body)
return _regex_html_to_text(html_body)
def _bs4_html_to_text(html_body: str) -> str:
soup = BeautifulSoup(html_body, "html.parser")
for tag in soup(["style", "script", "head"]):
tag.decompose()
for a_tag in soup.find_all("a", href=True):
text = a_tag.get_text(strip=True)
url = a_tag["href"]
if text and url:
a_tag.replace_with(f"[{text}]({url})")
for br in soup.find_all("br"):
br.replace_with("\n")
for p in soup.find_all("p"):
p.insert_before("\n")
p.insert_after("\n")
p.unwrap()
for tag in soup.find_all(["strong", "b"]):
if tag.string:
tag.string.replace_with(f"**{tag.string}**")
tag.unwrap()
for tag in soup.find_all(["em", "i"]):
if tag.string:
tag.string.replace_with(f"*{tag.string}*")
tag.unwrap()
for li in soup.find_all("li"):
li.insert_before("- ")
li.insert_after("\n")
li.unwrap()
text = soup.get_text(separator="\n")
text = unescape(text)
text = strip_email_quote(text)
text = re.sub(r"\n{3,}", "\n\n", text)
return text.strip()
def _regex_html_to_text(html_body: str) -> str:
import html
text = html_body
for tag in ("style", "script", "head"):
text = re.sub(
f"<{tag}[^>]*>.*?</{tag}>",
"",
text,
flags=re.DOTALL | re.IGNORECASE,
)
text = re.sub(
r'<a\s[^>]*href=["\']([^"\']*)["\'][^>]*>(.*?)</a>',
r"[\2](\1)",
text,
flags=re.DOTALL,
)
text = re.sub(r"<br\s*/?>", "\n", text, flags=re.IGNORECASE)
text = re.sub(r"<p[^>]*>", "\n", text, flags=re.IGNORECASE)
text = re.sub(r"</p>", "\n", text, flags=re.IGNORECASE)
text = re.sub(
r"<(?:strong|b)[^>]*>(.*?)</(?:strong|b)>",
r"**\1**",
text,
flags=re.DOTALL,
)
text = re.sub(r"<(?:em|i)[^>]*>(.*?)</(?:em|i)>", r"*\1*", text, flags=re.DOTALL)
text = re.sub(r"<li[^>]*>(.*?)</li>", r"- \1\n", text, flags=re.DOTALL)
text = re.sub(r"<[^>]+>", "", text)
text = html.unescape(text)
text = strip_email_quote(text)
text = re.sub(r"\n{3,}", "\n\n", text)
text = re.sub(r"[ \t]{2,}", " ", text)
return text.strip()
def strip_email_quote(text: str) -> str:
lines = text.split("\n")
cleaned_lines = []
for line in lines:
is_quote = False
for pattern in _EMAIL_QUOTE_PATTERNS:
if pattern.match(line.strip()):
is_quote = True
break
if not is_quote:
cleaned_lines.append(line)
else:
break
return "\n".join(cleaned_lines)
def extract_plain_text(text: str, max_length: int = 500) -> str:
plain = re.sub(r"[#*_>\-\[\]\(\)]", "", text)
plain = re.sub(r"\s+", " ", plain).strip()
if len(plain) > max_length:
plain = plain[: max_length - 3] + "..."
return plain