haixunMaster/haixun-backend/internal/model/placement/usecase/settings.go

266 lines
8.0 KiB
Go
Raw Permalink Normal View History

2026-06-24 10:02:42 +00:00
package usecase
import (
"context"
"strings"
app "haixun-backend/internal/library/errors"
"haixun-backend/internal/library/errors/code"
2026-06-24 16:48:56 +00:00
libkg "haixun-backend/internal/library/knowledge"
2026-06-24 10:02:42 +00:00
"haixun-backend/internal/library/placement"
2026-06-25 08:20:03 +00:00
"haixun-backend/internal/library/websearch"
2026-06-24 10:02:42 +00:00
settingdomain "haixun-backend/internal/model/setting/domain/usecase"
)
const (
settingScopeUser = "user"
keyResearch = "placement.research"
)
type Settings struct {
2026-06-25 08:20:03 +00:00
WebSearchProvider string
2026-06-24 10:02:42 +00:00
BraveAPIKey string
BraveAPIKeyConfigured bool
2026-06-25 08:20:03 +00:00
ExaAPIKey string
ExaAPIKeyConfigured bool
2026-06-24 10:02:42 +00:00
BraveCountry string
BraveSearchLang string
2026-06-25 08:20:03 +00:00
ExaUserLocation string
2026-06-24 16:48:56 +00:00
ExpandStrategy string
2026-06-24 10:02:42 +00:00
}
type SettingsPatch struct {
2026-06-25 08:20:03 +00:00
WebSearchProvider *string
BraveAPIKey *string
ExaAPIKey *string
BraveCountry *string
BraveSearchLang *string
ExaUserLocation *string
ExpandStrategy *string
2026-06-24 10:02:42 +00:00
}
type UseCase interface {
Get(ctx context.Context, tenantID, ownerUID string) (*Settings, error)
Update(ctx context.Context, tenantID, ownerUID string, patch SettingsPatch) (*Settings, error)
ResearchSettings(ctx context.Context, tenantID, ownerUID string) (placement.ResearchSettings, error)
}
type placementUseCase struct {
settings settingdomain.UseCase
}
func NewUseCase(settings settingdomain.UseCase) UseCase {
return &placementUseCase{settings: settings}
}
func (u *placementUseCase) Get(ctx context.Context, tenantID, ownerUID string) (*Settings, error) {
if err := requireActor(tenantID, ownerUID); err != nil {
return nil, err
}
stored, err := u.load(ctx, ownerUID)
if err != nil {
return nil, err
}
return toPublic(stored), nil
}
func (u *placementUseCase) Update(ctx context.Context, tenantID, ownerUID string, patch SettingsPatch) (*Settings, error) {
if err := requireActor(tenantID, ownerUID); err != nil {
return nil, err
}
current, err := u.load(ctx, ownerUID)
if err != nil {
return nil, err
}
next := applyPatch(current, patch)
if err := u.save(ctx, ownerUID, next); err != nil {
return nil, err
}
return toPublic(next), nil
}
func (u *placementUseCase) ResearchSettings(ctx context.Context, tenantID, ownerUID string) (placement.ResearchSettings, error) {
if err := requireActor(tenantID, ownerUID); err != nil {
return placement.ResearchSettings{}, err
}
stored, err := u.load(ctx, ownerUID)
if err != nil {
return placement.ResearchSettings{}, err
}
return placement.ResearchSettings{
2026-06-25 08:20:03 +00:00
WebSearchProvider: stored.WebSearchProvider,
BraveAPIKey: stored.BraveAPIKey,
ExaAPIKey: stored.ExaAPIKey,
BraveCountry: stored.BraveCountry,
BraveSearchLang: stored.BraveSearchLang,
ExaUserLocation: stored.ExaUserLocation,
ExpandStrategy: stored.ExpandStrategy,
2026-06-24 10:02:42 +00:00
}, nil
}
func (u *placementUseCase) load(ctx context.Context, ownerUID string) (storedSettings, error) {
defaults := defaultSettings()
setting, err := u.settings.Get(ctx, settingScopeUser, ownerUID, keyResearch)
if err != nil {
if isSettingNotFound(err) {
return defaults, nil
}
return defaults, err
}
return mergeSettings(defaults, setting.Value), nil
}
func (u *placementUseCase) save(ctx context.Context, ownerUID string, value storedSettings) error {
_, err := u.settings.Upsert(ctx, settingdomain.UpsertRequest{
Scope: settingScopeUser,
ScopeID: ownerUID,
Key: keyResearch,
Value: value.toMap(),
})
return err
}
type storedSettings struct {
2026-06-25 08:20:03 +00:00
WebSearchProvider string
BraveAPIKey string
ExaAPIKey string
BraveCountry string
BraveSearchLang string
ExaUserLocation string
ExpandStrategy string
2026-06-24 10:02:42 +00:00
}
func defaultSettings() storedSettings {
return storedSettings{
2026-06-25 08:20:03 +00:00
WebSearchProvider: string(websearch.ProviderBrave),
BraveCountry: "tw",
BraveSearchLang: "zh-hant",
ExaUserLocation: "TW",
ExpandStrategy: string(libkg.ExpandStrategyHybrid),
2026-06-24 10:02:42 +00:00
}
}
func mergeSettings(defaults storedSettings, raw map[string]interface{}) storedSettings {
if raw == nil {
return defaults
}
2026-06-25 08:20:03 +00:00
if v, ok := raw["web_search_provider"].(string); ok && strings.TrimSpace(v) != "" {
defaults.WebSearchProvider = string(websearch.ParseProvider(v))
}
2026-06-24 10:02:42 +00:00
if v, ok := raw["brave_api_key"].(string); ok {
defaults.BraveAPIKey = strings.TrimSpace(v)
}
2026-06-25 08:20:03 +00:00
if v, ok := raw["exa_api_key"].(string); ok {
defaults.ExaAPIKey = strings.TrimSpace(v)
}
2026-06-24 10:02:42 +00:00
if v, ok := raw["brave_country"].(string); ok && strings.TrimSpace(v) != "" {
defaults.BraveCountry = strings.TrimSpace(v)
}
if v, ok := raw["brave_search_lang"].(string); ok && strings.TrimSpace(v) != "" {
defaults.BraveSearchLang = strings.TrimSpace(v)
}
2026-06-25 08:20:03 +00:00
if v, ok := raw["exa_user_location"].(string); ok && strings.TrimSpace(v) != "" {
defaults.ExaUserLocation = strings.TrimSpace(v)
}
2026-06-24 16:48:56 +00:00
if v, ok := raw["expand_strategy"].(string); ok && strings.TrimSpace(v) != "" {
defaults.ExpandStrategy = string(libkg.ParseExpandStrategy(v))
}
2026-06-24 10:02:42 +00:00
return defaults
}
func applyPatch(current storedSettings, patch SettingsPatch) storedSettings {
2026-06-25 08:20:03 +00:00
if patch.WebSearchProvider != nil && strings.TrimSpace(*patch.WebSearchProvider) != "" {
current.WebSearchProvider = string(websearch.ParseProvider(*patch.WebSearchProvider))
}
2026-06-24 10:02:42 +00:00
if patch.BraveAPIKey != nil {
value := strings.TrimSpace(*patch.BraveAPIKey)
if value != "" && !isMaskedAPIKey(value) {
current.BraveAPIKey = value
}
}
2026-06-25 08:20:03 +00:00
if patch.ExaAPIKey != nil {
value := strings.TrimSpace(*patch.ExaAPIKey)
if value != "" && !isMaskedAPIKey(value) {
current.ExaAPIKey = value
}
}
2026-06-24 10:02:42 +00:00
if patch.BraveCountry != nil && strings.TrimSpace(*patch.BraveCountry) != "" {
current.BraveCountry = strings.TrimSpace(*patch.BraveCountry)
}
if patch.BraveSearchLang != nil && strings.TrimSpace(*patch.BraveSearchLang) != "" {
current.BraveSearchLang = strings.TrimSpace(*patch.BraveSearchLang)
}
2026-06-25 08:20:03 +00:00
if patch.ExaUserLocation != nil && strings.TrimSpace(*patch.ExaUserLocation) != "" {
current.ExaUserLocation = strings.TrimSpace(*patch.ExaUserLocation)
}
2026-06-24 16:48:56 +00:00
if patch.ExpandStrategy != nil && strings.TrimSpace(*patch.ExpandStrategy) != "" {
current.ExpandStrategy = string(libkg.ParseExpandStrategy(*patch.ExpandStrategy))
}
2026-06-24 10:02:42 +00:00
return current
}
func (s storedSettings) toMap() map[string]interface{} {
return map[string]interface{}{
2026-06-25 08:20:03 +00:00
"web_search_provider": s.WebSearchProvider,
"brave_api_key": s.BraveAPIKey,
"exa_api_key": s.ExaAPIKey,
"brave_country": s.BraveCountry,
"brave_search_lang": s.BraveSearchLang,
"exa_user_location": s.ExaUserLocation,
"expand_strategy": s.ExpandStrategy,
2026-06-24 10:02:42 +00:00
}
}
func toPublic(stored storedSettings) *Settings {
2026-06-25 08:20:03 +00:00
braveMasked := ""
2026-06-24 10:02:42 +00:00
if stored.BraveAPIKey != "" {
2026-06-25 08:20:03 +00:00
braveMasked = maskAPIKey(stored.BraveAPIKey)
}
exaMasked := ""
if stored.ExaAPIKey != "" {
exaMasked = maskAPIKey(stored.ExaAPIKey)
2026-06-24 10:02:42 +00:00
}
2026-06-24 16:48:56 +00:00
strategy := string(libkg.ParseExpandStrategy(stored.ExpandStrategy))
2026-06-24 10:02:42 +00:00
return &Settings{
2026-06-25 08:20:03 +00:00
WebSearchProvider: string(websearch.ParseProvider(stored.WebSearchProvider)),
BraveAPIKey: braveMasked,
2026-06-24 10:02:42 +00:00
BraveAPIKeyConfigured: strings.TrimSpace(stored.BraveAPIKey) != "",
2026-06-25 08:20:03 +00:00
ExaAPIKey: exaMasked,
ExaAPIKeyConfigured: strings.TrimSpace(stored.ExaAPIKey) != "",
2026-06-24 10:02:42 +00:00
BraveCountry: stored.BraveCountry,
BraveSearchLang: stored.BraveSearchLang,
2026-06-25 08:20:03 +00:00
ExaUserLocation: stored.ExaUserLocation,
2026-06-24 16:48:56 +00:00
ExpandStrategy: strategy,
2026-06-24 10:02:42 +00:00
}
}
func requireActor(tenantID, ownerUID string) error {
if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" {
return app.For(code.Member).InputMissingRequired("tenant_id and uid are required")
}
return nil
}
func isSettingNotFound(err error) bool {
if err == nil {
return false
}
appErr := app.FromError(err)
return appErr != nil && strings.Contains(strings.ToLower(appErr.Error()), "not found")
}
func maskAPIKey(key string) string {
trimmed := strings.TrimSpace(key)
if trimmed == "" {
return ""
}
if len(trimmed) <= 4 {
return "••••"
}
return "••••" + trimmed[len(trimmed)-4:]
}
func isMaskedAPIKey(value string) bool {
return strings.HasPrefix(value, "••••")
}