ForcePilot/backend/package/yuxi/channel/extensions/freshdesk/format.py
Kris 3e861d6cd1 feat(freshdesk): 新增Freshdesk/Freshchat渠道插件
实现完整的Freshdesk和Freshchat集成支持,包含会话守卫、错误定义、消息去重、配置管理、webhook处理、出站消息发送、状态监控、安全校验、配对功能和流式回复支持
2026-05-21 10:47:20 +08:00

188 lines
5.9 KiB
Python

import html as html_mod
import re
from time import time as _time
ALLOWED_TAGS = {
"p",
"br",
"b",
"strong",
"i",
"em",
"u",
"s",
"del",
"ul",
"ol",
"li",
"a",
"img",
"table",
"tr",
"td",
"th",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"hr",
"blockquote",
"code",
"pre",
"span",
}
ALLOWED_ATTRS = {
"a": {"href", "rel", "target"},
"img": {"src", "alt", "width", "height"},
"td": {"colspan", "rowspan"},
"th": {"colspan", "rowspan"},
}
FORBIDDEN_TAGS = {"script", "style", "iframe", "object", "embed", "form", "input"}
def sanitize_html(html_body: str) -> str:
for tag in FORBIDDEN_TAGS:
html_body = re.sub(
rf"<\s*{tag}[^>]*>.*?<\s*/\s*{tag}\s*>",
"",
html_body,
flags=re.DOTALL | re.IGNORECASE,
)
html_body = re.sub(
rf"<\s*{tag}[^>]*/?\s*>",
"",
html_body,
flags=re.IGNORECASE,
)
return html_body
def markdown_to_html(md_text: str) -> str:
text = html_mod.escape(md_text)
text = re.sub(r"```(\w+)?\n?(.*?)```", r"<pre><code>\2</code></pre>", text, flags=re.DOTALL)
text = re.sub(r"`([^`]+)`", r"<code>\1</code>", text)
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
text = re.sub(r"\*(.+?)\*", r"<em>\1</em>", text)
text = re.sub(r"~~(.+?)~~", r"<s>\1</s>", text)
text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2" rel="nofollow">\1</a>', text)
text = re.sub(r"^[\-\*]\s+(.+)$", r"<li>\1</li>", text, flags=re.MULTILINE)
text = re.sub(r"(<li>.*?</li>(\n?<li>.*?</li>)*)", r"<ul>\1</ul>", text)
text = re.sub(r"^\d+\.\s+(.+)$", r"<li>\1</li>", text, flags=re.MULTILINE)
text = re.sub(r"(<li>.*?</li>(\n?<li>.*?</li>)*)", r"<ol>\1</ol>", text)
lines = text.split("\n")
result_lines = []
in_table = False
table_rows = []
for line in lines:
if line.strip().startswith("|") and line.strip().endswith("|"):
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if all(c.startswith("-") and len(c) > 1 for c in cells):
continue
table_rows.append("<tr>" + "".join(f"<td>{c}</td>" for c in cells) + "</tr>")
in_table = True
else:
if in_table and table_rows:
result_lines.append("<table>" + "".join(table_rows) + "</table>")
table_rows = []
in_table = False
result_lines.append(line)
if in_table and table_rows:
result_lines.append("<table>" + "".join(table_rows) + "</table>")
text = "\n".join(result_lines)
text = re.sub(r"^>\s+(.+)$", r"<blockquote>\1</blockquote>", text, flags=re.MULTILINE)
text = re.sub(r"^---+$", r"<hr>", text, flags=re.MULTILINE)
text = re.sub(r"^#{1,2}\s+(.+)$", r"<h3>\1</h3>", text, flags=re.MULTILINE)
text = re.sub(r"^#{3,6}\s+(.+)$", r"<h4>\1</h4>", text, flags=re.MULTILINE)
lines = text.split("\n")
result = []
for line in lines:
stripped = line.strip()
if not stripped:
result.append("<br>")
elif stripped.startswith("<") and not stripped.startswith("<br"):
result.append(stripped)
else:
result.append(f"<p>{stripped}</p>")
return "\n".join(result)
def message_parts_to_text(parts: list[dict]) -> str:
texts = []
for part in parts:
if "text" in part:
texts.append(part["text"].get("content", ""))
elif "image" in part:
img = part["image"]
texts.append(f"[图片: {img.get('name', img.get('url', ''))}]")
elif "file" in part:
f = part["file"]
texts.append(f"[文件: {f.get('name', f.get('url', ''))}]")
elif "card" in part:
card = part["card"]
texts.append(f"[卡片: {card.get('title', '')}]")
return "\n".join(texts)
def get_agent_prompt_rules(mode: str = "both") -> list[str]:
rules = [
"Your response will be sent to Freshdesk/Freshworks as an AI agent reply.",
]
if mode in ("freshdesk", "both"):
rules.extend(
[
"--- Freshdesk (Ticket System) Format Rules ---",
"The message body is HTML. Use <strong> for bold, <em> for italic.",
'Use <a href="URL" rel="nofollow">text</a> for links.',
"Use <ul><li>item</li></ul> for unordered lists.",
"Use <ol><li>item</li></ol> for ordered lists.",
"Use <code> for inline code and <pre><code> for code blocks.",
"Use <blockquote>text</blockquote> for quotations.",
"Use <h3> or <h4> for headings.",
"Use <table><tr><td>...</td></tr></table> for tables.",
"Do NOT use <script>, <style>, <iframe>, <div> tags.",
"Do NOT use CSS class or id attributes.",
]
)
if mode in ("freshchat", "both"):
rules.extend(
[
"--- Freshchat (Messaging Platform) Format Rules ---",
"The message is plain text. Rich formatting via **bold** and *italic*.",
"URLs are auto-detected and rendered as clickable links.",
"Use Quick Reply buttons when asking for user choice (options: 2-5 items).",
"Keep messages concise and conversational.",
]
)
rules.extend(
[
"--- General Rules ---",
"Identify yourself as an AI assistant when relevant.",
"If the issue requires human intervention, suggest escalation.",
]
)
return rules
def parse_timestamp(ts: str) -> int:
try:
from datetime import datetime
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
return int(dt.timestamp() * 1000)
except (ValueError, TypeError):
return int(_time() * 1000)