fix(support-chat): support legacy mobile browsers
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var publicAssetURLRuntime struct {
|
||||
sync.RWMutex
|
||||
resolver func(string) string
|
||||
}
|
||||
|
||||
// SetPublicAssetURLResolver configures how host-owned asset keys are exposed
|
||||
// to standalone customer-service clients. A nil resolver keeps absolute URLs
|
||||
// and legacy values unchanged.
|
||||
func SetPublicAssetURLResolver(resolver func(string) string) {
|
||||
publicAssetURLRuntime.Lock()
|
||||
publicAssetURLRuntime.resolver = resolver
|
||||
publicAssetURLRuntime.Unlock()
|
||||
}
|
||||
|
||||
// SetPublicAssetURLSettings applies the same object-key joining rule used by
|
||||
// the management frontend: cloud material keys use the active oss_domain,
|
||||
// while absolute and same-origin URLs stay unchanged.
|
||||
func SetPublicAssetURLSettings(settings map[string]string) {
|
||||
engine := strings.TrimSpace(settings["oss_default_engine"])
|
||||
if engine == "" || engine == "local" {
|
||||
SetPublicAssetURLResolver(nil)
|
||||
return
|
||||
}
|
||||
domain := strings.TrimSpace(settings["oss_"+engine+"_domain"])
|
||||
SetPublicAssetURLResolver(func(value string) string {
|
||||
return resolvePublicAssetURLWithDomain(value, domain)
|
||||
})
|
||||
}
|
||||
|
||||
func resolvePublicAssetURLWithDomain(value, domain string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || strings.HasPrefix(value, "/") {
|
||||
return value
|
||||
}
|
||||
lower := strings.ToLower(value)
|
||||
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") || strings.HasPrefix(lower, "data:") {
|
||||
return value
|
||||
}
|
||||
domain = strings.TrimSpace(domain)
|
||||
if domain == "" {
|
||||
return value
|
||||
}
|
||||
if !strings.HasPrefix(strings.ToLower(domain), "http://") && !strings.HasPrefix(strings.ToLower(domain), "https://") {
|
||||
domain = "https://" + domain
|
||||
}
|
||||
return strings.TrimRight(domain, "/") + "/" + strings.TrimLeft(value, "/")
|
||||
}
|
||||
|
||||
// ResolvePublicAssetURL converts a host-owned object key into a browser-ready
|
||||
// URL. Resolver failures must fall back to the stored value so message delivery
|
||||
// is never affected by an optional avatar.
|
||||
func ResolvePublicAssetURL(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return value
|
||||
}
|
||||
publicAssetURLRuntime.RLock()
|
||||
resolver := publicAssetURLRuntime.resolver
|
||||
publicAssetURLRuntime.RUnlock()
|
||||
if resolver == nil {
|
||||
return value
|
||||
}
|
||||
resolved := strings.TrimSpace(resolver(value))
|
||||
if resolved == "" {
|
||||
return value
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package services
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolvePublicAssetURL(t *testing.T) {
|
||||
SetPublicAssetURLResolver(func(value string) string {
|
||||
return "https://cdn.example.com/" + value
|
||||
})
|
||||
t.Cleanup(func() { SetPublicAssetURLResolver(nil) })
|
||||
|
||||
if got := ResolvePublicAssetURL(" mall/materials/avatar.png "); got != "https://cdn.example.com/mall/materials/avatar.png" {
|
||||
t.Fatalf("ResolvePublicAssetURL() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePublicAssetURLFallsBackToStoredValue(t *testing.T) {
|
||||
SetPublicAssetURLResolver(func(string) string { return "" })
|
||||
t.Cleanup(func() { SetPublicAssetURLResolver(nil) })
|
||||
|
||||
if got := ResolvePublicAssetURL(" /storage/avatar.png "); got != "/storage/avatar.png" {
|
||||
t.Fatalf("ResolvePublicAssetURL() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPublicAssetURLSettingsUsesManagementDomainRule(t *testing.T) {
|
||||
SetPublicAssetURLSettings(map[string]string{
|
||||
"oss_default_engine": "qiniu",
|
||||
"oss_qiniu_domain": "cdn.example.com",
|
||||
})
|
||||
t.Cleanup(func() { SetPublicAssetURLResolver(nil) })
|
||||
|
||||
tests := map[string]string{
|
||||
"mall/materials/avatar.png": "https://cdn.example.com/mall/materials/avatar.png",
|
||||
"/storage/builtin-avatar.png": "/storage/builtin-avatar.png",
|
||||
"https://system.example.com/icon.png": "https://system.example.com/icon.png",
|
||||
}
|
||||
for input, want := range tests {
|
||||
if got := ResolvePublicAssetURL(input); got != want {
|
||||
t.Fatalf("ResolvePublicAssetURL(%q) = %q, want %q", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -364,7 +364,7 @@ func (s *wsService) fillRealtimeMessageSender(ret *response.MessageResponse, ite
|
||||
case enums.IMSenderTypeAI:
|
||||
if aiAgent := AIAgentService.Get(item.SenderID); aiAgent != nil {
|
||||
ret.SenderName = aiAgent.Name
|
||||
ret.SenderAvatar = strings.TrimSpace(aiAgent.Avatar)
|
||||
ret.SenderAvatar = ResolvePublicAssetURL(aiAgent.Avatar)
|
||||
}
|
||||
case enums.IMSenderTypeAgent:
|
||||
if profile := AgentProfileService.GetByUserID(item.SenderID); profile != nil {
|
||||
@@ -372,7 +372,7 @@ func (s *wsService) fillRealtimeMessageSender(ret *response.MessageResponse, ite
|
||||
ret.SenderName = displayName
|
||||
}
|
||||
if avatar := strings.TrimSpace(profile.Avatar); avatar != "" {
|
||||
ret.SenderAvatar = avatar
|
||||
ret.SenderAvatar = ResolvePublicAssetURL(avatar)
|
||||
}
|
||||
}
|
||||
if ret.SenderName == "" {
|
||||
|
||||
Reference in New Issue
Block a user