Files
geo/server/internal/tenant/app/kol_package_service.go
T
root b2605abd6a feat: Enhance Kol Generation Service with web search and knowledge group support
- Added `EnableWebSearch` and `KnowledgeGroupIDs` fields to `KolGenerationSubmitRequest`.
- Updated `Submit` method in `KolGenerationService` to handle new request fields.
- Integrated web search and knowledge group validation based on card configuration.
- Introduced caching mechanisms in `KolPackageService`, `KolPromptService`, and `KolSubscriptionAdminService` to improve performance.
- Implemented knowledge context resolution in `KolGenerationWorker` to enrich prompts with relevant knowledge snippets.
- Added utility functions for handling card configuration in both backend and frontend.
- Created tests for knowledge extraction and rendering to ensure accuracy and reliability.
2026-04-18 13:47:32 +08:00

332 lines
9.6 KiB
Go

package app
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/geo-platform/tenant-api/internal/shared/auth"
sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache"
"github.com/geo-platform/tenant-api/internal/shared/response"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
type CreateKolPackageInput struct {
Name string `json:"name" binding:"required"`
Description *string `json:"description"`
CoverURL *string `json:"cover_url"`
Industry *string `json:"industry"`
Tags []string `json:"tags"`
PriceNote *string `json:"price_note"`
}
type UpdateKolPackageInput = CreateKolPackageInput
type KolPackageResponse struct {
ID int64 `json:"id"`
KolProfileID int64 `json:"kol_profile_id"`
KolDisplayName string `json:"kol_display_name"`
KolAvatarURL *string `json:"kol_avatar_url"`
Name string `json:"name"`
Description *string `json:"description"`
CoverURL *string `json:"cover_url"`
Industry *string `json:"industry"`
Tags []string `json:"tags"`
PriceNote *string `json:"price_note"`
Status string `json:"status"`
PromptCount int `json:"prompt_count"`
SubscriberCount int `json:"subscriber_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type KolPackageService struct {
profileSvc *KolProfileService
repo repository.KolPackageRepository
promptRepo repository.KolPromptRepository
cache sharedcache.Cache
}
func NewKolPackageService(
profileSvc *KolProfileService,
repo repository.KolPackageRepository,
promptRepo repository.KolPromptRepository,
) *KolPackageService {
return &KolPackageService{
profileSvc: profileSvc,
repo: repo,
promptRepo: promptRepo,
}
}
func (s *KolPackageService) WithCache(c sharedcache.Cache) *KolPackageService {
s.cache = c
return s
}
func (s *KolPackageService) List(ctx context.Context, actor auth.Actor) ([]KolPackageResponse, error) {
profile, err := s.profileSvc.RequireActiveKol(ctx, actor)
if err != nil {
return nil, err
}
packages, err := s.repo.ListByProfile(ctx, actor.TenantID, profile.ID)
if err != nil {
return nil, response.ErrInternal(50062, "kol_package_query_failed", err.Error())
}
result := make([]KolPackageResponse, 0, len(packages))
for _, pkg := range packages {
item, err := s.buildPackageResponse(ctx, profile, &pkg)
if err != nil {
return nil, err
}
result = append(result, *item)
}
return result, nil
}
func (s *KolPackageService) Get(ctx context.Context, actor auth.Actor, id int64) (*KolPackageResponse, error) {
profile, pkg, err := s.loadOwnedPackage(ctx, actor, id)
if err != nil {
return nil, err
}
return s.buildPackageResponse(ctx, profile, pkg)
}
func (s *KolPackageService) Create(ctx context.Context, actor auth.Actor, input CreateKolPackageInput) (*KolPackageResponse, error) {
profile, err := s.profileSvc.RequireActiveKol(ctx, actor)
if err != nil {
return nil, err
}
name := strings.TrimSpace(input.Name)
if name == "" {
return nil, response.ErrBadRequest(40061, "kol_package_name_required", "package name is required")
}
tagsJSON, err := json.Marshal(normalizeKolStringList(input.Tags))
if err != nil {
return nil, response.ErrBadRequest(40062, "kol_package_tags_invalid", "tags must be valid json")
}
pkg, err := s.repo.Create(ctx, repository.CreateKolPackageInput{
TenantID: actor.TenantID,
KolProfileID: profile.ID,
Name: name,
Description: normalizeKolOptionalString(input.Description),
CoverURL: normalizeKolOptionalString(input.CoverURL),
Industry: normalizeKolOptionalString(input.Industry),
Tags: tagsJSON,
PriceNote: normalizeKolOptionalString(input.PriceNote),
})
if err != nil {
return nil, response.ErrInternal(50063, "kol_package_create_failed", err.Error())
}
InvalidateKolPromptCaches(ctx, s.cache)
return s.buildPackageResponse(ctx, profile, pkg)
}
func (s *KolPackageService) Update(ctx context.Context, actor auth.Actor, id int64, input UpdateKolPackageInput) (*KolPackageResponse, error) {
profile, _, err := s.loadOwnedPackage(ctx, actor, id)
if err != nil {
return nil, err
}
name := strings.TrimSpace(input.Name)
if name == "" {
return nil, response.ErrBadRequest(40061, "kol_package_name_required", "package name is required")
}
tagsJSON, err := json.Marshal(normalizeKolStringList(input.Tags))
if err != nil {
return nil, response.ErrBadRequest(40062, "kol_package_tags_invalid", "tags must be valid json")
}
if err := s.repo.Update(ctx, actor.TenantID, repository.UpdateKolPackageInput{
ID: id,
Name: name,
Description: normalizeKolOptionalString(input.Description),
CoverURL: normalizeKolOptionalString(input.CoverURL),
Industry: normalizeKolOptionalString(input.Industry),
Tags: tagsJSON,
PriceNote: normalizeKolOptionalString(input.PriceNote),
}); err != nil {
return nil, response.ErrInternal(50064, "kol_package_update_failed", err.Error())
}
updated, err := s.repo.GetByID(ctx, actor.TenantID, id)
if err != nil {
return nil, response.ErrInternal(50062, "kol_package_query_failed", err.Error())
}
if updated == nil {
return nil, errKolPackageNotFound
}
InvalidateKolPromptCaches(ctx, s.cache)
return s.buildPackageResponse(ctx, profile, updated)
}
func (s *KolPackageService) Publish(ctx context.Context, actor auth.Actor, id int64) (*KolPackageResponse, error) {
profile, _, err := s.loadOwnedPackage(ctx, actor, id)
if err != nil {
return nil, err
}
if err := s.repo.UpdateStatus(ctx, actor.TenantID, id, "published"); err != nil {
return nil, response.ErrInternal(50065, "kol_package_publish_failed", err.Error())
}
updated, err := s.repo.GetByID(ctx, actor.TenantID, id)
if err != nil {
return nil, response.ErrInternal(50062, "kol_package_query_failed", err.Error())
}
if updated == nil {
return nil, errKolPackageNotFound
}
InvalidateKolPromptCaches(ctx, s.cache)
return s.buildPackageResponse(ctx, profile, updated)
}
func (s *KolPackageService) Archive(ctx context.Context, actor auth.Actor, id int64) (*KolPackageResponse, error) {
profile, _, err := s.loadOwnedPackage(ctx, actor, id)
if err != nil {
return nil, err
}
if err := s.repo.UpdateStatus(ctx, actor.TenantID, id, "archived"); err != nil {
return nil, response.ErrInternal(50066, "kol_package_archive_failed", err.Error())
}
updated, err := s.repo.GetByID(ctx, actor.TenantID, id)
if err != nil {
return nil, response.ErrInternal(50062, "kol_package_query_failed", err.Error())
}
if updated == nil {
return nil, errKolPackageNotFound
}
InvalidateKolPromptCaches(ctx, s.cache)
return s.buildPackageResponse(ctx, profile, updated)
}
func (s *KolPackageService) Delete(ctx context.Context, actor auth.Actor, id int64) error {
if _, _, err := s.loadOwnedPackage(ctx, actor, id); err != nil {
return err
}
if err := s.repo.SoftDelete(ctx, actor.TenantID, id); err != nil {
return response.ErrInternal(50067, "kol_package_delete_failed", err.Error())
}
InvalidateKolPromptCaches(ctx, s.cache)
return nil
}
func (s *KolPackageService) loadOwnedPackage(ctx context.Context, actor auth.Actor, id int64) (*repository.KolProfile, *repository.KolPackage, error) {
profile, err := s.profileSvc.RequireActiveKol(ctx, actor)
if err != nil {
return nil, nil, err
}
pkg, err := s.repo.GetByID(ctx, actor.TenantID, id)
if err != nil {
return nil, nil, response.ErrInternal(50062, "kol_package_query_failed", err.Error())
}
if pkg == nil {
return nil, nil, errKolPackageNotFound
}
if pkg.KolProfileID != profile.ID {
return nil, nil, ErrPackageNotOwned
}
return profile, pkg, nil
}
func (s *KolPackageService) buildPackageResponse(
ctx context.Context,
profile *repository.KolProfile,
pkg *repository.KolPackage,
) (*KolPackageResponse, error) {
tags, err := decodeKolStringList(pkg.Tags)
if err != nil {
return nil, response.ErrInternal(50068, "kol_package_decode_failed", err.Error())
}
promptCount := 0
if s.promptRepo != nil {
prompts, err := s.promptRepo.ListByPackage(ctx, pkg.TenantID, pkg.ID)
if err != nil {
return nil, response.ErrInternal(50069, "kol_prompt_query_failed", err.Error())
}
promptCount = len(prompts)
}
return &KolPackageResponse{
ID: pkg.ID,
KolProfileID: pkg.KolProfileID,
KolDisplayName: profile.DisplayName,
KolAvatarURL: profile.AvatarURL,
Name: pkg.Name,
Description: pkg.Description,
CoverURL: pkg.CoverURL,
Industry: pkg.Industry,
Tags: tags,
PriceNote: pkg.PriceNote,
Status: pkg.Status,
PromptCount: promptCount,
SubscriberCount: 0,
CreatedAt: pkg.CreatedAt,
UpdatedAt: pkg.UpdatedAt,
}, nil
}
func normalizeKolOptionalString(value *string) *string {
if value == nil {
return nil
}
trimmed := strings.TrimSpace(*value)
if trimmed == "" {
return nil
}
return &trimmed
}
func normalizeKolStringList(values []string) []string {
if len(values) == 0 {
return []string{}
}
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
continue
}
if _, exists := seen[trimmed]; exists {
continue
}
seen[trimmed] = struct{}{}
result = append(result, trimmed)
}
return result
}
func decodeKolStringList(raw []byte) ([]string, error) {
if len(raw) == 0 {
return []string{}, nil
}
var values []string
if err := json.Unmarshal(raw, &values); err != nil {
return nil, err
}
return normalizeKolStringList(values), nil
}