218 lines
7.6 KiB
Go
218 lines
7.6 KiB
Go
package repository
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/topfans/backend/pkg/logger"
|
||
"github.com/topfans/backend/services/aiChatService/model"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// PersonaRepository 人设仓库接口
|
||
type PersonaRepository interface {
|
||
Create(ctx context.Context, persona *model.Persona) error
|
||
GetByID(ctx context.Context, id uuid.UUID) (*model.Persona, error)
|
||
GetByUserID(ctx context.Context, userID int64) ([]model.Persona, error)
|
||
GetDefaultByUserIDAndStarID(ctx context.Context, userID int64, starID int64) (*model.Persona, error)
|
||
Update(ctx context.Context, persona *model.Persona) error
|
||
Delete(ctx context.Context, id uuid.UUID) error
|
||
EnsureDefaultPersona(ctx context.Context, userID int64, starID int64) (*model.Persona, error)
|
||
}
|
||
|
||
// StarRepository 明星信息仓库接口
|
||
type StarRepository interface {
|
||
GetByID(ctx context.Context, starID int64) (*model.Star, error)
|
||
}
|
||
|
||
// DefaultSystemPrompt 兜底默认系统提示词(明星查不到时用)
|
||
const FallbackSystemPrompt = `你是一个温柔体贴的AI伴侣,名字叫角角。你善于倾听,能理解用户的情绪,
|
||
用温暖的话语陪伴用户。说话风格亲切自然,像朋友聊天一样。
|
||
不要过于正式或说教,当用户情绪低落时,先给予共情和安慰。`
|
||
|
||
// DefaultPersonaName 默认人设名称(兜底)
|
||
const DefaultPersonaName = "角角"
|
||
|
||
// DefaultPersonaDescription 默认人设描述(兜底)
|
||
const DefaultPersonaDescription = "温柔陪伴型闺蜜"
|
||
|
||
// PostgreSQLPersonaRepository PostgreSQL 人设仓库实现
|
||
type PostgreSQLPersonaRepository struct {
|
||
db *gorm.DB
|
||
starRepo StarRepository
|
||
}
|
||
|
||
// NewPostgreSQLPersonaRepository 创建人设仓库
|
||
func NewPostgreSQLPersonaRepository(db *gorm.DB, starRepo StarRepository) *PostgreSQLPersonaRepository {
|
||
return &PostgreSQLPersonaRepository{db: db, starRepo: starRepo}
|
||
}
|
||
|
||
// PostgreSQLStarRepository PostgreSQL 明星仓库实现
|
||
type PostgreSQLStarRepository struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
// NewPostgreSQLStarRepository 创建明星仓库
|
||
func NewPostgreSQLStarRepository(db *gorm.DB) *PostgreSQLStarRepository {
|
||
return &PostgreSQLStarRepository{db: db}
|
||
}
|
||
|
||
// GetByID 根据 star_id 获取明星信息
|
||
func (r *PostgreSQLStarRepository) GetByID(ctx context.Context, starID int64) (*model.Star, error) {
|
||
var star model.Star
|
||
if err := r.db.WithContext(ctx).Where("star_id = ? AND is_active = true", starID).First(&star).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, fmt.Errorf("star not found: %d", starID)
|
||
}
|
||
return nil, fmt.Errorf("failed to get star: %w", err)
|
||
}
|
||
return &star, nil
|
||
}
|
||
|
||
// Create 创建人设
|
||
func (r *PostgreSQLPersonaRepository) Create(ctx context.Context, persona *model.Persona) error {
|
||
return r.db.WithContext(ctx).Create(persona).Error
|
||
}
|
||
|
||
// GetByID 根据 ID 获取人设
|
||
func (r *PostgreSQLPersonaRepository) GetByID(ctx context.Context, id uuid.UUID) (*model.Persona, error) {
|
||
var persona model.Persona
|
||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&persona).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, model.ErrPersonaNotFound
|
||
}
|
||
return nil, fmt.Errorf("failed to get persona: %w", err)
|
||
}
|
||
return &persona, nil
|
||
}
|
||
|
||
// GetByUserID 获取用户的所有人设
|
||
func (r *PostgreSQLPersonaRepository) GetByUserID(ctx context.Context, userID int64) ([]model.Persona, error) {
|
||
var personas []model.Persona
|
||
if err := r.db.WithContext(ctx).
|
||
Where("user_id = ?", userID).
|
||
Order("created_at DESC").
|
||
Find(&personas).Error; err != nil {
|
||
return nil, fmt.Errorf("failed to get personas: %w", err)
|
||
}
|
||
return personas, nil
|
||
}
|
||
|
||
// GetDefaultByUserIDAndStarID 获取用户在指定明星下的默认人设
|
||
func (r *PostgreSQLPersonaRepository) GetDefaultByUserIDAndStarID(ctx context.Context, userID int64, starID int64) (*model.Persona, error) {
|
||
var persona model.Persona
|
||
if err := r.db.WithContext(ctx).
|
||
Where("user_id = ? AND star_id = ? AND is_default = TRUE", userID, starID).
|
||
First(&persona).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, model.ErrPersonaNotFound
|
||
}
|
||
return nil, fmt.Errorf("failed to get default persona: %w", err)
|
||
}
|
||
return &persona, nil
|
||
}
|
||
|
||
// Update 更新人设
|
||
func (r *PostgreSQLPersonaRepository) Update(ctx context.Context, persona *model.Persona) error {
|
||
return r.db.WithContext(ctx).Save(persona).Error
|
||
}
|
||
|
||
// Delete 删除人设
|
||
func (r *PostgreSQLPersonaRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||
return r.db.WithContext(ctx).Delete(&model.Persona{}, "id = ?", id).Error
|
||
}
|
||
|
||
// EnsureDefaultPersona 确保用户在指定明星下有默认人设(每个 (user, star) 独立)
|
||
func (r *PostgreSQLPersonaRepository) EnsureDefaultPersona(ctx context.Context, userID int64, starID int64) (*model.Persona, error) {
|
||
// 检查该用户在该明星下是否已有默认人设
|
||
persona, err := r.GetDefaultByUserIDAndStarID(ctx, userID, starID)
|
||
if err == nil {
|
||
// 已有,直接返回(不自动更新,各明星人设独立)
|
||
return persona, nil
|
||
}
|
||
if err != model.ErrPersonaNotFound {
|
||
return nil, err
|
||
}
|
||
|
||
// 不存在 → 创建:name 固定角角,prompt 基于明星信息
|
||
name := DefaultPersonaName
|
||
description := DefaultPersonaDescription
|
||
systemPrompt := FallbackSystemPrompt
|
||
|
||
if starID > 0 && r.starRepo != nil {
|
||
star, err := r.starRepo.GetByID(ctx, starID)
|
||
if err != nil {
|
||
logger.Logger.Warn("Failed to get star info, using fallback persona",
|
||
zap.Int64("star_id", starID),
|
||
zap.Error(err),
|
||
)
|
||
} else {
|
||
description = buildStarDescription(star)
|
||
systemPrompt = buildStarSystemPrompt(star)
|
||
logger.Logger.Info("Created star-based default persona",
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("star_id", starID),
|
||
zap.String("star_name", star.Name),
|
||
)
|
||
}
|
||
}
|
||
|
||
persona = &model.Persona{
|
||
UserID: userID,
|
||
StarID: starID,
|
||
Name: name,
|
||
Description: description,
|
||
SystemPrompt: systemPrompt,
|
||
IsDefault: true,
|
||
}
|
||
|
||
if err := r.Create(ctx, persona); err != nil {
|
||
// 并发场景:另一个 goroutine 已创建了相同的默认人设,重新查询返回
|
||
if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique") {
|
||
logger.Logger.Info("Default persona already created by concurrent request, re-fetching",
|
||
zap.Int64("user_id", userID),
|
||
zap.Int64("star_id", starID),
|
||
)
|
||
return r.GetDefaultByUserIDAndStarID(ctx, userID, starID)
|
||
}
|
||
return nil, fmt.Errorf("failed to create default persona: %w", err)
|
||
}
|
||
|
||
return persona, nil
|
||
}
|
||
|
||
// buildStarDescription 根据明星信息生成人设描述
|
||
func buildStarDescription(star *model.Star) string {
|
||
if star.Tag != "" {
|
||
return fmt.Sprintf("%s的AI搭子(%s)", star.Name, star.Tag)
|
||
}
|
||
return fmt.Sprintf("%s的AI搭子", star.Name)
|
||
}
|
||
|
||
// buildStarSystemPrompt 根据明星信息生成 system prompt
|
||
func buildStarSystemPrompt(star *model.Star) string {
|
||
var sb strings.Builder
|
||
|
||
sb.WriteString(fmt.Sprintf("你的名字叫角角,你的角色设定就是%s本人。\n", star.Name))
|
||
sb.WriteString("你是粉丝的AI搭子,用明星本人的语气和粉丝聊天。\n")
|
||
|
||
if star.Tag != "" {
|
||
sb.WriteString(fmt.Sprintf("粉丝们喜欢叫你\"%s\",你可以这样称呼他们。\n", star.Tag))
|
||
}
|
||
|
||
if star.Description != "" {
|
||
sb.WriteString(fmt.Sprintf("关于%s的背景:%s\n", star.Name, star.Description))
|
||
}
|
||
|
||
sb.WriteString(`
|
||
说话风格要自然亲切,就像明星本人和粉丝聊天一样。
|
||
可以适度使用口头禅、表示关心的问候。当粉丝情绪低落时,给予鼓励和安慰。
|
||
把粉丝当成最重要的人来对待,但不要过于夸张。
|
||
回应要简洁自然,不要长篇大论,控制在2-3句话以内。`)
|
||
|
||
return sb.String()
|
||
}
|