支持消息 Markdown 与欢迎语富文本

聊天/离线提示使用 Markdown 安全渲染;欢迎语改为 TipTap 富文本(颜色字号),DOMPurify 白名单清洗后展示。
This commit is contained in:
yml2213
2026-07-19 00:03:05 +08:00
parent 970bae16d1
commit fea5f4acf5
12 changed files with 3235 additions and 32 deletions
+52 -7
View File
@@ -24,10 +24,34 @@ const (
defaultAgentNickname = "在线客服"
defaultTimezone = "Asia/Shanghai"
maxWelcomeSegments = 10
maxWelcomeTextLen = 500
maxWelcomeSegments = 10
maxWelcomeTextLen = 1000 // 纯文字上限(HTML 标签不计)
maxWelcomeHTMLLen = 8000 // 富文本原始 HTML 上限
)
// stripHTMLTags 粗暴去标签,用于字数与摘要(欢迎语 HTML)。
func stripHTMLTags(s string) string {
var b strings.Builder
inTag := false
for _, r := range s {
switch {
case r == '<':
inTag = true
case r == '>':
inTag = false
case !inTag:
b.WriteRune(r)
}
}
// 简单实体
out := b.String()
out = strings.ReplaceAll(out, "&nbsp;", " ")
out = strings.ReplaceAll(out, "&amp;", "&")
out = strings.ReplaceAll(out, "&lt;", "<")
out = strings.ReplaceAll(out, "&gt;", ">")
return strings.TrimSpace(out)
}
// WelcomeSegment 单段欢迎语:文本或图片(content 为正文或对象存储 URL)。
type WelcomeSegment struct {
Type string `json:"type"` // text | image
@@ -47,9 +71,21 @@ func defaultWelcomeMessagesJSON() string {
func firstWelcomeText(segs []WelcomeSegment) string {
for _, s := range segs {
if s.Type == "text" && strings.TrimSpace(s.Content) != "" {
return s.Content
if s.Type != "text" {
continue
}
plain := stripHTMLTags(s.Content)
if plain == "" {
plain = strings.TrimSpace(s.Content)
}
if plain == "" {
continue
}
// welcome_message 列 size:500
if utf8.RuneCountInString(plain) > 500 {
return string([]rune(plain)[:500])
}
return plain
}
return defaultWelcomeMessage
}
@@ -96,11 +132,20 @@ func normalizeWelcomeSegments(input []WelcomeSegment) ([]WelcomeSegment, string,
c := strings.TrimSpace(seg.Content)
switch t {
case "text":
if c == "" {
// 允许 HTML 富文本;禁止明显脚本
lower := strings.ToLower(c)
if strings.Contains(lower, "<script") || strings.Contains(lower, "javascript:") {
return nil, "", fmt.Errorf("第 %d 段欢迎语包含不安全内容", i+1)
}
plain := stripHTMLTags(c)
if plain == "" {
return nil, "", fmt.Errorf("第 %d 段文本不能为空", i+1)
}
if utf8.RuneCountInString(c) > maxWelcomeTextLen {
return nil, "", fmt.Errorf("第 %d 段文不能超过 %d 字", i+1, maxWelcomeTextLen)
if utf8.RuneCountInString(plain) > maxWelcomeTextLen {
return nil, "", fmt.Errorf("第 %d 段文不能超过 %d 字", i+1, maxWelcomeTextLen)
}
if utf8.RuneCountInString(c) > maxWelcomeHTMLLen {
return nil, "", fmt.Errorf("第 %d 段内容过长", i+1)
}
out = append(out, WelcomeSegment{Type: "text", Content: c})
case "image":