Scope knowledge and image libraries by brand
Frontend CI / Frontend (push) Successful in 2m59s
Backend CI / Backend (push) Successful in 14m58s

This commit is contained in:
2026-06-30 12:30:10 +08:00
parent 95ddb3057f
commit ced0c4ec0f
35 changed files with 1136 additions and 326 deletions
@@ -809,6 +809,7 @@ func (s *EnterpriseSiteService) PublishArticle(ctx context.Context, connectionID
contentHTML, coverURL := s.prepareEnterpriseSitePublishAssets(
ctx,
actor.TenantID,
brandID,
contentHTML,
coverURLInput,
coverImageAssetID,
@@ -1865,20 +1866,20 @@ func enterpriseSitePublishCoverInput(article enterpriseSitePublishArticle, req P
return coverURL, coverImageAssetID
}
func (s *EnterpriseSiteService) prepareEnterpriseSitePublishAssets(ctx context.Context, tenantID int64, contentHTML, coverURL string, coverImageAssetID *int64) (string, string) {
func (s *EnterpriseSiteService) prepareEnterpriseSitePublishAssets(ctx context.Context, tenantID int64, brandID int64, contentHTML, coverURL string, coverImageAssetID *int64) (string, string) {
base := s.enterpriseSitePublicBaseURL()
preparedHTML := s.prepareEnterpriseSiteImageURLs(ctx, tenantID, contentHTML, base)
preparedCover := s.prepareEnterpriseSiteCoverURL(ctx, tenantID, coverURL, coverImageAssetID, base)
preparedHTML := s.prepareEnterpriseSiteImageURLs(ctx, tenantID, brandID, contentHTML, base)
preparedCover := s.prepareEnterpriseSiteCoverURL(ctx, tenantID, brandID, coverURL, coverImageAssetID, base)
return preparedHTML, preparedCover
}
func (s *EnterpriseSiteService) prepareEnterpriseSiteCoverURL(ctx context.Context, tenantID int64, coverURL string, coverImageAssetID *int64, baseURL string) string {
func (s *EnterpriseSiteService) prepareEnterpriseSiteCoverURL(ctx context.Context, tenantID int64, brandID int64, coverURL string, coverImageAssetID *int64, baseURL string) string {
if coverImageAssetID != nil && *coverImageAssetID > 0 {
if dataURI := s.enterpriseSiteDataURIFromImageAssetID(ctx, tenantID, *coverImageAssetID); dataURI != "" {
if dataURI := s.enterpriseSiteDataURIFromImageAssetID(ctx, tenantID, brandID, *coverImageAssetID); dataURI != "" {
return dataURI
}
}
return s.prepareEnterpriseSiteAssetURL(ctx, tenantID, coverURL, baseURL)
return s.prepareEnterpriseSiteAssetURL(ctx, tenantID, brandID, coverURL, baseURL)
}
func (s *EnterpriseSiteService) enterpriseSitePublicBaseURL() string {
@@ -1918,7 +1919,7 @@ func normalizeEnterpriseSiteAssetBaseURL(value string) string {
return strings.TrimRight(parsed.String(), "/")
}
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLs(ctx context.Context, tenantID int64, fragment string, baseURL string) string {
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLs(ctx context.Context, tenantID int64, brandID int64, fragment string, baseURL string) string {
if strings.TrimSpace(fragment) == "" || !strings.Contains(strings.ToLower(fragment), "<img") {
return fragment
}
@@ -1929,37 +1930,37 @@ func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLs(ctx context.Conte
}
nodes, err := html.ParseFragment(strings.NewReader(fragment), contextNode)
if err != nil {
return s.prepareEnterpriseSiteImageURLsFallback(ctx, tenantID, fragment, baseURL)
return s.prepareEnterpriseSiteImageURLsFallback(ctx, tenantID, brandID, fragment, baseURL)
}
for _, node := range nodes {
s.prepareEnterpriseSiteImageNodeURLs(ctx, tenantID, node, baseURL)
s.prepareEnterpriseSiteImageNodeURLs(ctx, tenantID, brandID, node, baseURL)
}
nodes = enterpriseSitePruneEmptyImageWrapperNodes(nodes)
return enterpriseSiteRenderHTMLNodes(nodes, s.prepareEnterpriseSiteImageURLsFallback(ctx, tenantID, fragment, baseURL))
return enterpriseSiteRenderHTMLNodes(nodes, s.prepareEnterpriseSiteImageURLsFallback(ctx, tenantID, brandID, fragment, baseURL))
}
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageNodeURLs(ctx context.Context, tenantID int64, node *html.Node, baseURL string) {
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageNodeURLs(ctx context.Context, tenantID int64, brandID int64, node *html.Node, baseURL string) {
if node == nil {
return
}
if node.Type == html.ElementNode && strings.EqualFold(node.Data, "img") {
if !s.prepareEnterpriseSiteImageAttrs(ctx, tenantID, node, baseURL) {
if !s.prepareEnterpriseSiteImageAttrs(ctx, tenantID, brandID, node, baseURL) {
enterpriseSiteRemoveHTMLNode(node)
return
}
}
for child := node.FirstChild; child != nil; {
next := child.NextSibling
s.prepareEnterpriseSiteImageNodeURLs(ctx, tenantID, child, baseURL)
s.prepareEnterpriseSiteImageNodeURLs(ctx, tenantID, brandID, child, baseURL)
child = next
}
}
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageAttrs(ctx context.Context, tenantID int64, node *html.Node, baseURL string) bool {
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageAttrs(ctx context.Context, tenantID int64, brandID int64, node *html.Node, baseURL string) bool {
if node == nil {
return true
}
dataURI := s.enterpriseSiteDataURIFromImageNode(ctx, tenantID, node)
dataURI := s.enterpriseSiteDataURIFromImageNode(ctx, tenantID, brandID, node)
hasImageURL := false
for idx := range node.Attr {
key := strings.ToLower(strings.TrimSpace(node.Attr[idx].Key))
@@ -1971,7 +1972,7 @@ func (s *EnterpriseSiteService) prepareEnterpriseSiteImageAttrs(ctx context.Cont
node.Attr[idx].Val = dataURI
continue
}
node.Attr[idx].Val = s.prepareEnterpriseSiteAssetURL(ctx, tenantID, node.Attr[idx].Val, baseURL)
node.Attr[idx].Val = s.prepareEnterpriseSiteAssetURL(ctx, tenantID, brandID, node.Attr[idx].Val, baseURL)
if strings.TrimSpace(node.Attr[idx].Val) == "" {
return false
}
@@ -2046,12 +2047,12 @@ func enterpriseSitePruneEmptyImageWrapperNodes(nodes []*html.Node) []*html.Node
return pruned
}
func (s *EnterpriseSiteService) prepareEnterpriseSiteAssetURL(ctx context.Context, tenantID int64, value string, baseURL string) string {
func (s *EnterpriseSiteService) prepareEnterpriseSiteAssetURL(ctx context.Context, tenantID int64, brandID int64, value string, baseURL string) string {
value = strings.TrimSpace(value)
if value == "" || strings.HasPrefix(strings.ToLower(value), "data:") {
return value
}
if dataURI := s.enterpriseSiteDataURIFromAssetURL(ctx, tenantID, value); dataURI != "" {
if dataURI := s.enterpriseSiteDataURIFromAssetURL(ctx, tenantID, brandID, value); dataURI != "" {
return dataURI
}
if enterpriseSiteLooksLikeSaaSAssetURL(value) {
@@ -2060,34 +2061,34 @@ func (s *EnterpriseSiteService) prepareEnterpriseSiteAssetURL(ctx context.Contex
return enterpriseSiteAbsoluteAssetURL(value, baseURL)
}
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLsFallback(ctx context.Context, tenantID int64, fragment string, baseURL string) string {
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLsFallback(ctx context.Context, tenantID int64, brandID int64, fragment string, baseURL string) string {
return enterpriseSiteImageAttrPattern.ReplaceAllStringFunc(fragment, func(match string) string {
parts := enterpriseSiteImageAttrPattern.FindStringSubmatch(match)
if len(parts) != 4 {
return match
}
return parts[1] + s.prepareEnterpriseSiteAssetURL(ctx, tenantID, parts[2], baseURL) + parts[3]
return parts[1] + s.prepareEnterpriseSiteAssetURL(ctx, tenantID, brandID, parts[2], baseURL) + parts[3]
})
}
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromAssetURL(ctx context.Context, tenantID int64, value string) string {
objectKey, ok := s.enterpriseSiteObjectKeyFromAssetURL(ctx, tenantID, value)
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromAssetURL(ctx context.Context, tenantID int64, brandID int64, value string) string {
objectKey, ok := s.enterpriseSiteObjectKeyFromAssetURL(ctx, tenantID, brandID, value)
if !ok || s == nil || s.storage == nil {
return ""
}
return s.enterpriseSiteDataURIFromObjectKey(ctx, objectKey)
}
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromImageNode(ctx context.Context, tenantID int64, node *html.Node) string {
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromImageNode(ctx context.Context, tenantID int64, brandID int64, node *html.Node) string {
assetID := enterpriseSiteImageNodeAssetID(node)
if assetID <= 0 {
return ""
}
return s.enterpriseSiteDataURIFromImageAssetID(ctx, tenantID, assetID)
return s.enterpriseSiteDataURIFromImageAssetID(ctx, tenantID, brandID, assetID)
}
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromImageAssetID(ctx context.Context, tenantID, assetID int64) string {
objectKey, ok := s.loadEnterpriseSiteImageAssetObjectKey(ctx, tenantID, assetID)
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromImageAssetID(ctx context.Context, tenantID, brandID, assetID int64) string {
objectKey, ok := s.loadEnterpriseSiteImageAssetObjectKey(ctx, tenantID, brandID, assetID)
if !ok || s == nil || s.storage == nil {
return ""
}
@@ -2143,27 +2144,54 @@ func enterpriseSiteImageNodeAssetID(node *html.Node) int64 {
return 0
}
func (s *EnterpriseSiteService) enterpriseSiteObjectKeyFromAssetURL(ctx context.Context, tenantID int64, value string) (string, bool) {
func (s *EnterpriseSiteService) enterpriseSiteObjectKeyFromAssetURL(ctx context.Context, tenantID int64, brandID int64, value string) (string, bool) {
if objectKey, ok := enterpriseSiteObjectKeyFromPublicAssetURL(value, tenantID, s.enterpriseSiteAssetTokenSecret()); ok {
if !s.enterpriseSiteImageObjectKeyBelongsToBrand(ctx, tenantID, brandID, objectKey) {
return "", false
}
return objectKey, true
}
assetID, ok := enterpriseSiteAssetIDFromURL(value)
if !ok {
return "", false
}
return s.loadEnterpriseSiteImageAssetObjectKey(ctx, tenantID, assetID)
return s.loadEnterpriseSiteImageAssetObjectKey(ctx, tenantID, brandID, assetID)
}
func (s *EnterpriseSiteService) loadEnterpriseSiteImageAssetObjectKey(ctx context.Context, tenantID, assetID int64) (string, bool) {
if s == nil || s.pool == nil || tenantID <= 0 || assetID <= 0 {
func (s *EnterpriseSiteService) enterpriseSiteImageObjectKeyBelongsToBrand(ctx context.Context, tenantID, brandID int64, objectKey string) bool {
if s == nil || s.pool == nil || !enterpriseSiteObjectKeyIsImageLibraryAsset(objectKey, tenantID) {
return true
}
if tenantID <= 0 || brandID <= 0 || strings.TrimSpace(objectKey) == "" {
return false
}
var exists bool
if err := s.pool.QueryRow(ctx, `
SELECT EXISTS (
SELECT 1
FROM image_assets
WHERE tenant_id = $1
AND brand_id = $2
AND object_key = $3
AND status = 'active'
AND deleted_at IS NULL
)
`, tenantID, brandID, objectKey).Scan(&exists); err != nil {
return false
}
return exists
}
func (s *EnterpriseSiteService) loadEnterpriseSiteImageAssetObjectKey(ctx context.Context, tenantID, brandID, assetID int64) (string, bool) {
if s == nil || s.pool == nil || tenantID <= 0 || brandID <= 0 || assetID <= 0 {
return "", false
}
var objectKey string
if err := s.pool.QueryRow(ctx, `
SELECT object_key
FROM image_assets
WHERE id = $1 AND tenant_id = $2 AND status = 'active' AND deleted_at IS NULL
`, assetID, tenantID).Scan(&objectKey); err != nil {
SELECT object_key
FROM image_assets
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND status = 'active' AND deleted_at IS NULL
`, assetID, tenantID, brandID).Scan(&objectKey); err != nil {
return "", false
}
if !enterpriseSiteObjectKeyAllowedForTenant(objectKey, tenantID) {
@@ -2264,6 +2292,14 @@ func enterpriseSiteObjectKeyAllowedForTenant(objectKey string, tenantID int64) b
strings.HasPrefix(objectKey, tenantPrefix+"articles/")
}
func enterpriseSiteObjectKeyIsImageLibraryAsset(objectKey string, tenantID int64) bool {
if tenantID <= 0 {
return false
}
tenantPrefix := "tenants/" + strconv.FormatInt(tenantID, 10) + "/"
return strings.HasPrefix(strings.TrimSpace(objectKey), tenantPrefix+"images/")
}
func enterpriseSiteAbsoluteImageURLs(fragment string, baseURL string) string {
if strings.TrimSpace(fragment) == "" || !strings.Contains(strings.ToLower(fragment), "<img") {
return fragment