chore: import clean Agent Desk snapshot (2/7)

This commit is contained in:
2026-10-16 11:57:21 +00:00
parent 79176541c7
commit 89c5231c4c
461 changed files with 60092 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
package utils
import (
"agent-desk/internal/pkg/enums"
"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
AssetID string // image asset id
Provider enums.AssetProvider // image provider
StorageKey string // image storage key
}
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"))
assetID := strings.TrimSpace(findContentChunkHTMLAttr(node, "data-asset-id"))
provider := enums.AssetProvider(strings.TrimSpace(findContentChunkHTMLAttr(node, "data-provider")))
storageKey := strings.TrimSpace(findContentChunkHTMLAttr(node, "data-storage-key"))
if src == "" && assetID == "" {
return
}
chunks = append(chunks, ContentChunk{
Type: ContentChunkTypeImage,
Content: src,
AssetID: assetID,
Provider: provider,
StorageKey: storageKey,
})
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 ""
}

View File

@@ -0,0 +1,85 @@
package utils
import (
"agent-desk/internal/pkg/enums"
"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: "image with asset metadata only",
content: `<p><img data-asset-id="asset_1" data-provider="local" data-storage-key="images/a.png" alt="a"></p>`,
want: []ContentChunk{
{
Type: ContentChunkTypeImage,
AssetID: "asset_1",
Provider: enums.AssetProviderLocal,
StorageKey: "images/a.png",
},
},
},
{
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)
}
})
}
}

View File

@@ -0,0 +1,374 @@
package utils
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"agent-desk/internal/services/storage"
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/microcosm-cc/bluemonday"
"github.com/mlogclub/simple/sqls"
"golang.org/x/net/html"
)
type imMessageAssetPayload struct {
AssetID string `json:"assetId"`
Provider enums.AssetProvider `json:"provider,omitempty"`
StorageKey string `json:"storageKey,omitempty"`
Filename string `json:"filename,omitempty"`
FileSize int64 `json:"fileSize,omitempty"`
MimeType string `json:"mimeType,omitempty"`
URL string `json:"url,omitempty"`
}
func SanitizeMessageHTML(content string) string {
policy := bluemonday.UGCPolicy()
policy.AllowElements("img")
policy.AllowAttrs("src", "alt", "title", "data-asset-id", "data-provider", "data-storage-key").OnElements("img")
policy.AllowURLSchemes("http", "https")
policy.AllowStandardURLs()
policy.AllowElements("p", "br")
return stripHTMLImageSrcIfBound(strings.TrimSpace(policy.Sanitize(content)))
}
func NormalizeMessageHTMLAssets(content string) (string, error) {
content = strings.TrimSpace(content)
if content == "" {
return "", nil
}
doc, err := html.Parse(strings.NewReader("<div>" + content + "</div>"))
if err != nil {
return content, nil
}
var walkErr error
var walk func(*html.Node)
walk = func(node *html.Node) {
if node == nil || walkErr != nil {
return
}
if node.Type == html.ElementNode && node.Data == "img" {
asset, err := normalizeHTMLImageAsset(node)
if err != nil {
walkErr = err
return
}
if asset != nil {
setHTMLAttr(node, "data-asset-id", strings.TrimSpace(asset.AssetID))
setHTMLAttr(node, "data-provider", strings.TrimSpace(string(asset.Provider)))
setHTMLAttr(node, "data-storage-key", strings.TrimSpace(asset.StorageKey))
removeHTMLAttr(node, "src")
}
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
walk(child)
}
}
walk(doc)
if walkErr != nil {
return "", walkErr
}
return renderHTMLFragment(doc), nil
}
func BuildHTMLSummary(content string) string {
if strings.TrimSpace(content) == "" {
return ""
}
doc, err := html.Parse(strings.NewReader("<div>" + content + "</div>"))
if err != nil {
return strings.TrimSpace(content)
}
parts := make([]string, 0, 8)
var walk func(*html.Node)
walk = func(node *html.Node) {
if node == nil {
return
}
if node.Type == html.TextNode {
text := strings.TrimSpace(node.Data)
if text != "" {
parts = append(parts, text)
}
}
if node.Type == html.ElementNode && node.Data == "img" {
parts = append(parts, "[图片]")
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
walk(child)
}
}
walk(doc)
return strings.TrimSpace(strings.Join(parts, " "))
}
func BuildRuntimeMessageText(messageType enums.IMMessageType, content string) string {
content = strings.TrimSpace(content)
switch messageType {
case enums.IMMessageTypeHTML:
return BuildHTMLSummary(content)
case enums.IMMessageTypeImage:
if content != "" {
return "[图片] " + content
}
return "[图片]"
case enums.IMMessageTypeAttachment:
if content != "" {
return "[附件] " + content
}
return "[附件]"
default:
return content
}
}
func BuildRenderableMessage(item *models.Message) (content, payload string) {
if item == nil {
return "", ""
}
if item.RecalledAt != nil {
return "该消息已撤回", ""
}
if item.SendStatus == enums.IMMessageStatusRecalled {
return "该消息已撤回", ""
}
content = item.Content
payload = item.Payload
switch item.MessageType {
case enums.IMMessageTypeImage, enums.IMMessageTypeAttachment:
payload = buildIMMessageAssetPayloadForResponse(item.Payload)
case enums.IMMessageTypeHTML:
content = BuildMessageHTMLForResponse(item.Content)
}
return content, payload
}
func BuildMessageHTMLForResponse(content string) string {
content = strings.TrimSpace(content)
if content == "" {
return ""
}
doc, err := html.Parse(strings.NewReader("<div>" + content + "</div>"))
if err != nil {
return content
}
var walk func(*html.Node)
walk = func(node *html.Node) {
if node == nil {
return
}
if node.Type == html.ElementNode && node.Data == "img" {
provider := enums.AssetProvider(strings.TrimSpace(findHTMLAttr(node, "data-provider")))
storageKey := strings.TrimSpace(findHTMLAttr(node, "data-storage-key"))
if provider != "" && storageKey != "" {
if storageProvider, err := storage.NewProvider(provider); err == nil {
setHTMLAttr(node, "src", storageProvider.GetSignedURL(storageKey))
}
}
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
walk(child)
}
}
walk(doc)
return renderHTMLFragment(doc)
}
func stripHTMLImageSrcIfBound(content string) string {
content = strings.TrimSpace(content)
if content == "" {
return ""
}
doc, err := html.Parse(strings.NewReader("<div>" + content + "</div>"))
if err != nil {
return content
}
var walk func(*html.Node)
walk = func(node *html.Node) {
if node == nil {
return
}
if node.Type == html.ElementNode && node.Data == "img" {
provider := strings.TrimSpace(findHTMLAttr(node, "data-provider"))
storageKey := strings.TrimSpace(findHTMLAttr(node, "data-storage-key"))
if provider != "" && storageKey != "" {
removeHTMLAttr(node, "src")
}
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
walk(child)
}
}
walk(doc)
return renderHTMLFragment(doc)
}
func renderHTMLFragment(doc *html.Node) string {
if doc == nil {
return ""
}
root := findHTMLRoot(doc)
if root == nil {
return ""
}
var buf bytes.Buffer
for child := root.FirstChild; child != nil; child = child.NextSibling {
if err := html.Render(&buf, child); err != nil {
return ""
}
}
return strings.TrimSpace(buf.String())
}
func findHTMLRoot(doc *html.Node) *html.Node {
var walk func(*html.Node) *html.Node
walk = func(node *html.Node) *html.Node {
if node == nil {
return nil
}
if node.Type == html.ElementNode && node.Data == "div" {
return node
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
if found := walk(child); found != nil {
return found
}
}
return nil
}
return walk(doc)
}
func findHTMLAttr(node *html.Node, key string) string {
if node == nil {
return ""
}
for _, attr := range node.Attr {
if attr.Key == key {
return attr.Val
}
}
return ""
}
func setHTMLAttr(node *html.Node, key, value string) {
if node == nil {
return
}
for i := range node.Attr {
if node.Attr[i].Key == key {
node.Attr[i].Val = value
return
}
}
node.Attr = append(node.Attr, html.Attribute{Key: key, Val: value})
}
func removeHTMLAttr(node *html.Node, key string) {
if node == nil {
return
}
dst := node.Attr[:0]
for _, attr := range node.Attr {
if attr.Key != key {
dst = append(dst, attr)
}
}
node.Attr = dst
}
func buildIMMessageAssetPayloadForResponse(payload string) string {
assetPayload, err := parseIMMessageAssetPayload(payload)
if err != nil {
return strings.TrimSpace(payload)
}
assetPayload = hydrateIMMessageAssetPayload(assetPayload)
if assetPayload.Provider != "" && assetPayload.StorageKey != "" {
if provider, err := storage.NewProvider(assetPayload.Provider); err == nil {
assetPayload.URL = provider.GetSignedURL(assetPayload.StorageKey)
}
}
data, err := json.Marshal(assetPayload)
if err != nil {
return strings.TrimSpace(payload)
}
return string(data)
}
func parseIMMessageAssetPayload(payload string) (*imMessageAssetPayload, error) {
payload = strings.TrimSpace(payload)
if payload == "" {
return nil, nil
}
ret := &imMessageAssetPayload{}
if err := json.Unmarshal([]byte(payload), ret); err != nil {
return nil, err
}
ret.AssetID = strings.TrimSpace(ret.AssetID)
ret.Provider = enums.AssetProvider(strings.TrimSpace(string(ret.Provider)))
ret.StorageKey = strings.TrimSpace(ret.StorageKey)
return ret, nil
}
func hydrateIMMessageAssetPayload(payload *imMessageAssetPayload) *imMessageAssetPayload {
if payload == nil {
return nil
}
if payload.Provider != "" && payload.StorageKey != "" {
return payload
}
if payload.AssetID == "" {
return payload
}
asset := repositories.AssetRepository.GetByAssetID(sqls.DB(), payload.AssetID)
if asset == nil {
return payload
}
if payload.Provider == "" {
payload.Provider = asset.Provider
}
if payload.StorageKey == "" {
payload.StorageKey = strings.TrimSpace(asset.StorageKey)
}
if payload.Filename == "" {
payload.Filename = strings.TrimSpace(asset.Filename)
}
if payload.FileSize <= 0 {
payload.FileSize = asset.FileSize
}
if payload.MimeType == "" {
payload.MimeType = strings.TrimSpace(asset.MimeType)
}
return payload
}
func normalizeHTMLImageAsset(node *html.Node) (*models.Asset, error) {
if node == nil {
return nil, nil
}
assetID := strings.TrimSpace(findHTMLAttr(node, "data-asset-id"))
provider := enums.AssetProvider(strings.TrimSpace(findHTMLAttr(node, "data-provider")))
storageKey := strings.TrimSpace(findHTMLAttr(node, "data-storage-key"))
hasAssetID := assetID != ""
hasProvider := provider != ""
hasStorageKey := storageKey != ""
if hasAssetID || hasProvider || hasStorageKey {
if !(hasAssetID && hasProvider && hasStorageKey) {
return nil, fmt.Errorf("html message image asset attributes are incomplete")
}
asset := repositories.AssetRepository.GetByAssetID(sqls.DB(), assetID)
if asset == nil {
return nil, fmt.Errorf("html message image asset not found")
}
if asset.Provider != provider || strings.TrimSpace(asset.StorageKey) != storageKey {
return nil, fmt.Errorf("html message image asset attributes mismatch")
}
return asset, nil
}
return nil, fmt.Errorf("html message image must include asset metadata")
}

View File

@@ -0,0 +1,242 @@
package utils
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/enums"
"strings"
"testing"
"time"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
)
func TestBuildIMMessageAssetPayloadForResponseAddsSignedURL(t *testing.T) {
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
Default: enums.AssetProviderLocal,
Local: config.LocalStorageConfig{
BaseURL: "https://files.example.com",
},
},
})
payload := `{"assetId":"asset_1","provider":"local","storageKey":"attachments/demo.png","filename":"demo.png"}`
got := buildIMMessageAssetPayloadForResponse(payload)
if !strings.Contains(got, `"provider":"local"`) {
t.Fatalf("expected provider in payload, got: %s", got)
}
if !strings.Contains(got, `"storageKey":"attachments/demo.png"`) {
t.Fatalf("expected storageKey in payload, got: %s", got)
}
if !strings.Contains(got, `"url":"https://files.example.com/attachments/demo.png"`) {
t.Fatalf("expected signed url in payload, got: %s", got)
}
}
func TestSanitizeMessageHTMLStripsStoredSrcForManagedImages(t *testing.T) {
html := `<p><img src="https://files.example.com/demo.png" data-provider="local" data-storage-key="attachments/demo.png" alt="demo"></p>`
got := SanitizeMessageHTML(html)
if strings.Contains(got, `src=`) {
t.Fatalf("expected src removed from stored html, got: %s", got)
}
if !strings.Contains(got, `data-provider="local"`) {
t.Fatalf("expected data-provider kept, got: %s", got)
}
if !strings.Contains(got, `data-storage-key="attachments/demo.png"`) {
t.Fatalf("expected data-storage-key kept, got: %s", got)
}
}
func TestBuildMessageHTMLForResponseAddsSignedURL(t *testing.T) {
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
Default: enums.AssetProviderLocal,
Local: config.LocalStorageConfig{
BaseURL: "https://files.example.com",
},
},
})
html := `<p><img data-provider="local" data-storage-key="attachments/demo.png" alt="demo"></p>`
got := BuildMessageHTMLForResponse(html)
if !strings.Contains(got, `src="https://files.example.com/attachments/demo.png"`) {
t.Fatalf("expected signed src in response html, got: %s", got)
}
}
func TestBuildRuntimeMessageTextForHTML(t *testing.T) {
got := BuildRuntimeMessageText(enums.IMMessageTypeHTML, `<p>你好</p><p><img data-provider="local" data-storage-key="images/demo.png" alt="demo"></p>`)
if got != "你好 [图片]" {
t.Fatalf("expected html converted to plain text summary, got: %q", got)
}
}
func TestBuildRuntimeMessageTextForAssetMessages(t *testing.T) {
if got := BuildRuntimeMessageText(enums.IMMessageTypeImage, "demo.png"); got != "[图片] demo.png" {
t.Fatalf("unexpected image runtime text: %q", got)
}
if got := BuildRuntimeMessageText(enums.IMMessageTypeAttachment, "spec.pdf"); got != "[附件] spec.pdf" {
t.Fatalf("unexpected attachment runtime text: %q", got)
}
}
func TestBuildRenderableMessageTransformsPayloadAndHTML(t *testing.T) {
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
Default: enums.AssetProviderLocal,
Local: config.LocalStorageConfig{
BaseURL: "https://files.example.com",
},
},
})
image := &models.Message{
MessageType: enums.IMMessageTypeImage,
Payload: `{"assetId":"asset_1","provider":"local","storageKey":"attachments/demo.png","filename":"demo.png"}`,
}
_, imagePayload := BuildRenderableMessage(image)
if !strings.Contains(imagePayload, `"url":"https://files.example.com/attachments/demo.png"`) {
t.Fatalf("expected image payload signed url, got: %s", imagePayload)
}
htmlMsg := &models.Message{
MessageType: enums.IMMessageTypeHTML,
Content: `<p><img data-provider="local" data-storage-key="attachments/demo.png"></p>`,
}
htmlContent, _ := BuildRenderableMessage(htmlMsg)
if !strings.Contains(htmlContent, `src="https://files.example.com/attachments/demo.png"`) {
t.Fatalf("expected html content signed src, got: %s", htmlContent)
}
}
func TestNormalizeMessageHTMLAssetsKeepsValidAttrsAndRemovesSrc(t *testing.T) {
setupMessageTestDB(t)
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
Default: enums.AssetProviderLocal,
Local: config.LocalStorageConfig{
BaseURL: "https://files.example.com",
},
},
})
createTestAsset(t, &models.Asset{
AssetID: "asset_local_1",
Provider: enums.AssetProviderLocal,
StorageKey: "images/demo.png",
Filename: "demo.png",
FileSize: 123,
MimeType: "image/png",
Status: enums.AssetStatusSuccess,
})
got, err := NormalizeMessageHTMLAssets(`<p><img src="https://files.example.com/images/demo.png" data-asset-id="asset_local_1" data-provider="local" data-storage-key="images/demo.png" alt="demo"></p>`)
if err != nil {
t.Fatalf("expected normalization success, got error: %v", err)
}
if !strings.Contains(got, `data-asset-id="asset_local_1"`) {
t.Fatalf("expected data-asset-id added, got: %s", got)
}
if !strings.Contains(got, `data-provider="local"`) {
t.Fatalf("expected data-provider added, got: %s", got)
}
if !strings.Contains(got, `data-storage-key="images/demo.png"`) {
t.Fatalf("expected data-storage-key added, got: %s", got)
}
if strings.Contains(got, `src=`) {
t.Fatalf("expected src removed after asset binding, got: %s", got)
}
}
func TestNormalizeMessageHTMLAssetsRejectsMissingAssetMetadata(t *testing.T) {
setupMessageTestDB(t)
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
Default: enums.AssetProviderLocal,
Local: config.LocalStorageConfig{
BaseURL: "https://files.example.com",
},
},
})
_, err := NormalizeMessageHTMLAssets(`<p><img src="https://unknown.example.com/demo.png" alt="demo"></p>`)
if err == nil {
t.Fatalf("expected missing image asset metadata rejected")
}
}
func TestNormalizeMessageHTMLAssetsRejectsIncompleteAttrs(t *testing.T) {
setupMessageTestDB(t)
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
Default: enums.AssetProviderLocal,
Local: config.LocalStorageConfig{
BaseURL: "https://files.example.com",
},
},
})
_, err := NormalizeMessageHTMLAssets(`<p><img data-asset-id="asset1" data-provider="local" alt="demo"></p>`)
if err == nil {
t.Fatalf("expected incomplete asset attrs rejected")
}
}
func TestNormalizeMessageHTMLAssetsRejectsMismatchedAttrs(t *testing.T) {
setupMessageTestDB(t)
config.SetCurrent(&config.Config{
Storage: config.StorageConfig{
Default: enums.AssetProviderLocal,
Local: config.LocalStorageConfig{
BaseURL: "https://files.example.com",
},
},
})
createTestAsset(t, &models.Asset{
AssetID: "asset_local_2",
Provider: enums.AssetProviderLocal,
StorageKey: "images/real.png",
Filename: "real.png",
FileSize: 456,
MimeType: "image/png",
Status: enums.AssetStatusSuccess,
})
_, err := NormalizeMessageHTMLAssets(`<p><img data-asset-id="asset_local_2" data-provider="local" data-storage-key="images/wrong.png" alt="demo"></p>`)
if err == nil {
t.Fatalf("expected mismatched asset attrs rejected")
}
}
func setupMessageTestDB(t *testing.T) {
t.Helper()
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite failed: %v", err)
}
if err := db.AutoMigrate(&models.Asset{}); err != nil {
t.Fatalf("auto migrate asset failed: %v", err)
}
sqls.SetDB(db)
}
func createTestAsset(t *testing.T, item *models.Asset) {
t.Helper()
now := time.Now()
if item.CreatedAt.IsZero() {
item.CreatedAt = now
}
if item.UpdatedAt.IsZero() {
item.UpdatedAt = now
}
if err := sqls.DB().Create(item).Error; err != nil {
t.Fatalf("create asset failed: %v", err)
}
}

103
internal/pkg/utils/utils.go Normal file
View File

@@ -0,0 +1,103 @@
package utils
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/errorsx"
"crypto/rand"
"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.InvalidParamI18n("error.e0175")
}
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
}