This commit is contained in:
mlogclub
2026-04-09 10:01:23 +08:00
commit efe801b8bf
707 changed files with 110595 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
package utils
import (
"strings"
"golang.org/x/net/html"
)
type ContentChunkType string
const (
ContentChunkTypeText ContentChunkType = "text"
ContentChunkTypeImage ContentChunkType = "image"
)
type ContentChunk struct {
Type ContentChunkType //
Content string // text content or image url
}
func SplitHTMLContentChunks(content string) ([]ContentChunk, error) {
root, err := html.Parse(strings.NewReader("<div>" + strings.TrimSpace(content) + "</div>"))
if err != nil {
return nil, err
}
var (
chunks []ContentChunk
buffer strings.Builder
)
flushText := func() {
text := normalizeContentChunkText(buffer.String())
buffer.Reset()
if text == "" {
return
}
chunks = append(chunks, ContentChunk{
Type: ContentChunkTypeText,
Content: text,
})
}
var walk func(*html.Node)
walk = func(node *html.Node) {
if node == nil {
return
}
switch node.Type {
case html.TextNode:
buffer.WriteString(node.Data)
case html.ElementNode:
switch node.Data {
case "br":
buffer.WriteString("\n")
case "p", "div", "li", "blockquote":
buffer.WriteString("\n")
case "img":
flushText()
src := strings.TrimSpace(findContentChunkHTMLAttr(node, "src"))
if src == "" {
return
}
chunks = append(chunks, ContentChunk{
Type: ContentChunkTypeImage,
Content: src,
})
return
}
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
walk(child)
}
if node.Type == html.ElementNode {
switch node.Data {
case "p", "div", "li", "blockquote":
buffer.WriteString("\n")
}
}
}
walk(root)
flushText()
return chunks, nil
}
func normalizeContentChunkText(value string) string {
value = strings.ReplaceAll(value, "\u00a0", " ")
lines := strings.Split(value, "\n")
normalizedLines := make([]string, 0, len(lines))
for _, line := range lines {
line = strings.Join(strings.Fields(strings.TrimSpace(line)), " ")
if line == "" {
if len(normalizedLines) > 0 && normalizedLines[len(normalizedLines)-1] != "" {
normalizedLines = append(normalizedLines, "")
}
continue
}
normalizedLines = append(normalizedLines, line)
}
for len(normalizedLines) > 0 && normalizedLines[0] == "" {
normalizedLines = normalizedLines[1:]
}
for len(normalizedLines) > 0 && normalizedLines[len(normalizedLines)-1] == "" {
normalizedLines = normalizedLines[:len(normalizedLines)-1]
}
return strings.Join(normalizedLines, "\n")
}
func findContentChunkHTMLAttr(node *html.Node, key string) string {
for _, attr := range node.Attr {
if strings.EqualFold(attr.Key, key) {
return attr.Val
}
}
return ""
}
+72
View File
@@ -0,0 +1,72 @@
package utils
import (
"reflect"
"testing"
)
func TestSplitHTMLContentChunks(t *testing.T) {
tests := []struct {
name string
content string
want []ContentChunk
}{
{
name: "plain text paragraph",
content: "<p>你好,微信用户</p>",
want: []ContentChunk{
{Type: ContentChunkTypeText, Content: "你好,微信用户"},
},
},
{
name: "single image",
content: `<p><img src="https://example.com/a.png" alt="a"></p>`,
want: []ContentChunk{
{Type: ContentChunkTypeImage, Content: "https://example.com/a.png"},
},
},
{
name: "mixed text and images keep order",
content: `<p>第一段</p><p><img src="https://example.com/1.png"></p><p>第二段<img src="https://example.com/2.png">第三段</p>`,
want: []ContentChunk{
{Type: ContentChunkTypeText, Content: "第一段"},
{Type: ContentChunkTypeImage, Content: "https://example.com/1.png"},
{Type: ContentChunkTypeText, Content: "第二段"},
{Type: ContentChunkTypeImage, Content: "https://example.com/2.png"},
{Type: ContentChunkTypeText, Content: "第三段"},
},
},
{
name: "normalize blank lines and spaces",
content: "<div> 第一行 <br><br> 第二行 </div>",
want: []ContentChunk{
{Type: ContentChunkTypeText, Content: "第一行\n\n第二行"},
},
},
{
name: "ignore image without src",
content: `<p>前文</p><img alt="missing-src"><p>后文</p>`,
want: []ContentChunk{
{Type: ContentChunkTypeText, Content: "前文"},
{Type: ContentChunkTypeText, Content: "后文"},
},
},
{
name: "empty html returns empty chunks",
content: "<p><br></p>",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := SplitHTMLContentChunks(tt.content)
if err != nil {
t.Fatalf("SplitHTMLContentChunks() error = %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("SplitHTMLContentChunks() = %#v, want %#v", got, tt.want)
}
})
}
}
+103
View File
@@ -0,0 +1,103 @@
package utils
import (
"crypto/rand"
"cs-agent/internal/models"
"cs-agent/internal/pkg/dto"
"cs-agent/internal/pkg/errorsx"
"strconv"
"strings"
"time"
"github.com/spf13/cast"
)
func NormalizeNullableString(value *string) *string {
if value == nil {
return nil
}
v := strings.TrimSpace(*value)
if v == "" {
return nil
}
return &v
}
func BuildAuditFields(operator *dto.AuthPrincipal) models.AuditFields {
now := time.Now()
fields := models.AuditFields{
CreatedAt: now,
UpdatedAt: now,
}
if operator != nil {
fields.CreateUserID = operator.UserID
fields.CreateUserName = operator.Username
fields.UpdateUserID = operator.UserID
fields.UpdateUserName = operator.Username
}
return fields
}
func FormatTimePtr(t *time.Time) string {
if t == nil || t.IsZero() {
return ""
}
return t.Format(time.DateTime)
}
func FormatTime(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Format(time.DateTime)
}
func GenerateRandomPassword(length int) (string, error) {
if length <= 0 {
return "", errorsx.InvalidParam("密码长度不合法")
}
const charset = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"
buf := make([]byte, length)
random := make([]byte, length)
if _, err := rand.Read(random); err != nil {
return "", err
}
for i := range buf {
buf[i] = charset[int(random[i])%len(charset)]
}
return string(buf), nil
}
func JoinInt64s(values []int64) string {
if len(values) == 0 {
return ""
}
parts := make([]string, 0, len(values))
for _, value := range values {
parts = append(parts, cast.ToString(value))
}
return strings.Join(parts, ",")
}
func SplitInt64s(raw string) []int64 {
parts := strings.Split(strings.TrimSpace(raw), ",")
ret := make([]int64, 0, len(parts))
seen := make(map[int64]struct{})
for _, part := range parts {
value := strings.TrimSpace(part)
if value == "" {
continue
}
id, err := strconv.ParseInt(value, 10, 64)
if err != nil || id <= 0 {
continue
}
if _, exists := seen[id]; exists {
continue
}
seen[id] = struct{}{}
ret = append(ret, id)
}
return ret
}