feat(asset): add AssetID to ContentChunk and update image handling in HTML content
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>",
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user