topfans/backend/services/aiChatService/repository/persona_repository.go
2026-07-03 22:04:11 +08:00

218 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}