feat(asset): add AssetID to ContentChunk and update image handling in HTML content

This commit is contained in:
mlogclub
2026-04-17 13:35:00 +08:00
parent f4761d768c
commit 0037b97f29
5 changed files with 18 additions and 108 deletions
+4 -1
View File
@@ -16,6 +16,7 @@ const (
type ContentChunk struct { type ContentChunk struct {
Type ContentChunkType // Type ContentChunkType //
Content string // text content or image url Content string // text content or image url
AssetID string // image asset id
} }
func SplitHTMLContentChunks(content string) ([]ContentChunk, error) { func SplitHTMLContentChunks(content string) ([]ContentChunk, error) {
@@ -57,12 +58,14 @@ func SplitHTMLContentChunks(content string) ([]ContentChunk, error) {
case "img": case "img":
flushText() flushText()
src := strings.TrimSpace(findContentChunkHTMLAttr(node, "src")) src := strings.TrimSpace(findContentChunkHTMLAttr(node, "src"))
if src == "" { assetID := strings.TrimSpace(findContentChunkHTMLAttr(node, "data-asset-id"))
if src == "" && assetID == "" {
return return
} }
chunks = append(chunks, ContentChunk{ chunks = append(chunks, ContentChunk{
Type: ContentChunkTypeImage, Type: ContentChunkTypeImage,
Content: src, Content: src,
AssetID: assetID,
}) })
return return
} }
+7
View File
@@ -51,6 +51,13 @@ func TestSplitHTMLContentChunks(t *testing.T) {
{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"},
},
},
{ {
name: "empty html returns empty chunks", name: "empty html returns empty chunks",
content: "<p><br></p>", content: "<p><br></p>",
+1 -93
View File
@@ -3,13 +3,11 @@ package utils
import ( import (
"bytes" "bytes"
"cs-agent/internal/models" "cs-agent/internal/models"
"cs-agent/internal/pkg/config"
"cs-agent/internal/pkg/enums" "cs-agent/internal/pkg/enums"
"cs-agent/internal/repositories" "cs-agent/internal/repositories"
"cs-agent/internal/services/storage" "cs-agent/internal/services/storage"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url"
"strings" "strings"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
@@ -335,7 +333,6 @@ func normalizeHTMLImageAsset(node *html.Node) (*models.Asset, error) {
assetID := strings.TrimSpace(findHTMLAttr(node, "data-asset-id")) assetID := strings.TrimSpace(findHTMLAttr(node, "data-asset-id"))
provider := enums.AssetProvider(strings.TrimSpace(findHTMLAttr(node, "data-provider"))) provider := enums.AssetProvider(strings.TrimSpace(findHTMLAttr(node, "data-provider")))
storageKey := strings.TrimSpace(findHTMLAttr(node, "data-storage-key")) storageKey := strings.TrimSpace(findHTMLAttr(node, "data-storage-key"))
src := strings.TrimSpace(findHTMLAttr(node, "src"))
hasAssetID := assetID != "" hasAssetID := assetID != ""
hasProvider := provider != "" hasProvider := provider != ""
@@ -353,94 +350,5 @@ func normalizeHTMLImageAsset(node *html.Node) (*models.Asset, error) {
} }
return asset, nil return asset, nil
} }
if src == "" { return nil, fmt.Errorf("html message image must include asset metadata")
return nil, fmt.Errorf("html message image is missing asset metadata")
}
asset := findAssetByMessageImageURL(src)
if asset == nil {
return nil, fmt.Errorf("html message image must reference an uploaded asset")
}
return asset, nil
}
func findAssetByMessageImageURL(rawURL string) *models.Asset {
storageKey, err := resolveStorageKeyFromMessageImageURL(rawURL)
if err != nil {
return nil
}
return repositories.AssetRepository.GetByStorageKey(sqls.DB(), storageKey)
}
func FindAssetByMessageImageURL(rawURL string) *models.Asset {
return findAssetByMessageImageURL(rawURL)
}
func resolveStorageKeyFromMessageImageURL(rawURL string) (string, error) {
cfg := config.Current().Storage
candidates := make([]string, 0, 3)
if baseURL := strings.TrimSpace(cfg.Local.BaseURL); baseURL != "" {
candidates = append(candidates, baseURL)
}
if baseURL := strings.TrimSpace(cfg.OSS.BaseURL); baseURL != "" {
candidates = append(candidates, baseURL)
}
if ossBucketBaseURL := buildOSSBucketBaseURL(cfg.OSS); ossBucketBaseURL != "" {
candidates = append(candidates, ossBucketBaseURL)
}
for _, baseURL := range candidates {
if storageKey, err := resolveStorageKeyFromAssetURL(baseURL, rawURL); err == nil && storageKey != "" {
return storageKey, nil
}
}
return "", fmt.Errorf("image url does not match any storage base url")
}
func buildOSSBucketBaseURL(cfg config.OSSStorageConfig) string {
endpoint := strings.TrimSpace(cfg.Endpoint)
bucket := strings.TrimSpace(cfg.Bucket)
if endpoint == "" || bucket == "" {
return ""
}
if !strings.Contains(endpoint, "://") {
endpoint = "https://" + endpoint
}
u, err := url.Parse(endpoint)
if err != nil || strings.TrimSpace(u.Host) == "" {
return ""
}
scheme := strings.TrimSpace(u.Scheme)
if scheme == "" {
scheme = "https"
}
return fmt.Sprintf("%s://%s.%s", scheme, bucket, u.Host)
}
func resolveStorageKeyFromAssetURL(baseURL, rawURL string) (string, error) {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
rawURL = strings.TrimSpace(rawURL)
if baseURL == "" || rawURL == "" {
return "", fmt.Errorf("invalid image url")
}
if strings.HasPrefix(rawURL, baseURL+"/") {
return strings.TrimLeft(strings.TrimPrefix(rawURL, baseURL), "/"), nil
}
baseParsed, baseErr := url.Parse(baseURL)
rawParsed, rawErr := url.Parse(rawURL)
if baseErr != nil || rawErr != nil {
return "", fmt.Errorf("invalid image url")
}
if !strings.EqualFold(baseParsed.Host, rawParsed.Host) {
return "", fmt.Errorf("image url host mismatch")
}
basePath := strings.TrimRight(baseParsed.Path, "/")
rawPath := strings.TrimLeft(rawParsed.Path, "/")
if basePath == "" {
return rawPath, nil
}
basePath = strings.TrimLeft(basePath, "/")
if !strings.HasPrefix(rawPath, basePath+"/") {
return "", fmt.Errorf("image url path mismatch")
}
return strings.TrimLeft(strings.TrimPrefix(rawPath, basePath), "/"), nil
} }
+4 -4
View File
@@ -100,7 +100,7 @@ func TestBuildRenderableMessageTransformsPayloadAndHTML(t *testing.T) {
} }
} }
func TestNormalizeMessageHTMLAssetsEnrichesImageDataAttrs(t *testing.T) { func TestNormalizeMessageHTMLAssetsKeepsValidAttrsAndRemovesSrc(t *testing.T) {
setupMessageTestDB(t) setupMessageTestDB(t)
config.SetCurrent(&config.Config{ config.SetCurrent(&config.Config{
Storage: config.StorageConfig{ Storage: config.StorageConfig{
@@ -120,7 +120,7 @@ func TestNormalizeMessageHTMLAssetsEnrichesImageDataAttrs(t *testing.T) {
Status: enums.AssetStatusSuccess, Status: enums.AssetStatusSuccess,
}) })
got, err := NormalizeMessageHTMLAssets(`<p><img src="https://files.example.com/images/demo.png" alt="demo"></p>`) 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 { if err != nil {
t.Fatalf("expected normalization success, got error: %v", err) t.Fatalf("expected normalization success, got error: %v", err)
} }
@@ -139,7 +139,7 @@ func TestNormalizeMessageHTMLAssetsEnrichesImageDataAttrs(t *testing.T) {
} }
} }
func TestNormalizeMessageHTMLAssetsKeepsUnknownImageSrc(t *testing.T) { func TestNormalizeMessageHTMLAssetsRejectsMissingAssetMetadata(t *testing.T) {
setupMessageTestDB(t) setupMessageTestDB(t)
config.SetCurrent(&config.Config{ config.SetCurrent(&config.Config{
Storage: config.StorageConfig{ Storage: config.StorageConfig{
@@ -152,7 +152,7 @@ func TestNormalizeMessageHTMLAssetsKeepsUnknownImageSrc(t *testing.T) {
_, err := NormalizeMessageHTMLAssets(`<p><img src="https://unknown.example.com/demo.png" alt="demo"></p>`) _, err := NormalizeMessageHTMLAssets(`<p><img src="https://unknown.example.com/demo.png" alt="demo"></p>`)
if err == nil { if err == nil {
t.Fatalf("expected unknown image src rejected") t.Fatalf("expected missing image asset metadata rejected")
} }
} }
@@ -454,8 +454,8 @@ func (s *wxWorkKFOutboundService) buildHTMLChunks(content string) ([]wxWorkKFOut
Content: text, Content: text,
}) })
case utils.ContentChunkTypeImage: case utils.ContentChunkTypeImage:
assetID, resolveErr := s.resolveAssetIDFromImageSrc(chunk.Content) assetID := strings.TrimSpace(chunk.AssetID)
if resolveErr != nil { if assetID == "" {
chunks = append(chunks, wxWorkKFOutboundChunk{ chunks = append(chunks, wxWorkKFOutboundChunk{
MessageType: enums.IMMessageTypeText, MessageType: enums.IMMessageTypeText,
Content: "[图片]", Content: "[图片]",
@@ -473,11 +473,3 @@ func (s *wxWorkKFOutboundService) buildHTMLChunks(content string) ([]wxWorkKFOut
} }
return chunks, nil return chunks, nil
} }
func (s *wxWorkKFOutboundService) resolveAssetIDFromImageSrc(src string) (string, error) {
asset := utils.FindAssetByMessageImageURL(src)
if asset == nil {
return "", fmt.Errorf("未找到图片资源")
}
return strings.TrimSpace(asset.AssetID), nil
}