Skip to content
Merged

Dev #151

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ RUN CGO_ENABLED=0 go build -o /server cmd/server/main.go

# ---- Stage 3: Final image ----
FROM nginx:alpine
RUN apk --no-cache add ca-certificates tzdata
RUN apk --no-cache add ca-certificates tzdata postgresql postgresql-contrib

# Nginx config
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
Expand All @@ -33,5 +33,5 @@ COPY --from=backend-builder /server /app/server
COPY deploy/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 80
EXPOSE 7860
CMD ["/entrypoint.sh"]
1 change: 1 addition & 0 deletions backend/internal/database/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func Migrate(db *gorm.DB) error {
&model.Collection{},
&model.ModerationLog{},
&model.ChatMemory{},
&model.ChatMemoryFact{},
&model.IdentityTag{},
&model.Memoir{},
&model.Photo{},
Expand Down
28 changes: 28 additions & 0 deletions backend/internal/dto/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,35 @@ type ChatProfile struct {
PetDetails *string `json:"pet_details"`
Interests []string `json:"interests"`
Concerns []string `json:"concerns"`
Facts []string `json:"facts"`
ImportantDates []string `json:"important_dates"`
BabyAgeWeeks *int `json:"baby_age_weeks"`
CommunityInteractions []string `json:"community_interactions"`
}

// ChatMemoryFactDTO is the response body for a single memory fact
type ChatMemoryFactDTO struct {
ID string `json:"id"`
Content string `json:"content"`
Category string `json:"category"`
CreatedAt string `json:"created_at"`
LastReferencedAt *string `json:"last_referenced_at"`
}

// ChatMemoryFactsResponse wraps the list of memory facts
type ChatMemoryFactsResponse struct {
Facts []ChatMemoryFactDTO `json:"facts"`
Total int `json:"total"`
}

// ConversationTurn represents a single user-assistant exchange
type ConversationTurn struct {
UserInput string `json:"user_input"`
AssistantResponse string `json:"assistant_response"`
}

// ConversationHistoryResponse wraps conversation turns and summary
type ConversationHistoryResponse struct {
Turns []ConversationTurn `json:"turns"`
Summary string `json:"summary"`
}
6 changes: 3 additions & 3 deletions backend/internal/dto/photo.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ type UpdatePhotoRequest struct {

type ToggleWallRequest struct {
IsOnWall bool `json:"is_on_wall"`
WallPosition *int `json:"wall_position" binding:"omitempty,min=0,max=8"`
WallPosition *int `json:"wall_position" binding:"omitempty,min=0,max=9"`
}

type BatchWallUpdateRequest struct {
Photos []WallItem `json:"photos" binding:"required,min=1,max=9"`
Photos []WallItem `json:"photos" binding:"required,min=1,max=10"`
}

type WallItem struct {
PhotoID string `json:"photo_id" binding:"required"`
Position int `json:"position" binding:"min=0,max=8"`
Position int `json:"position" binding:"min=0,max=9"`
}
44 changes: 44 additions & 0 deletions backend/internal/handler/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,47 @@ func (h *ChatHandler) GetProfile(c *gin.Context) {
profile := h.chatService.GetGuestProfile(sessionID)
c.JSON(http.StatusOK, profile)
}

// GET /api/v1/companion/memories
func (h *ChatHandler) GetMemories(c *gin.Context) {
userID := middleware.GetUserID(c)
resp, err := h.chatService.GetMemories(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}

// GET /api/v1/companion/history
func (h *ChatHandler) GetHistory(c *gin.Context) {
userID := middleware.GetUserID(c)
resp, err := h.chatService.GetConversationHistory(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, resp)
}

// DELETE /api/v1/companion/history
func (h *ChatHandler) ClearHistory(c *gin.Context) {
userID := middleware.GetUserID(c)
if err := h.chatService.ClearConversationHistory(userID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "对话历史已清除"})
}

// DELETE /api/v1/companion/memories/:id
func (h *ChatHandler) DeleteMemory(c *gin.Context) {
userID := middleware.GetUserID(c)
factID := c.Param("id")

if err := h.chatService.DeleteMemory(userID, factID); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已删除"})
}
47 changes: 43 additions & 4 deletions backend/internal/model/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,48 @@ import (
)

type ChatMemory struct {
ID string `gorm:"type:varchar(36);primaryKey" json:"id"`
UserID string `gorm:"type:varchar(36);uniqueIndex;not null" json:"user_id"`
ProfileData string `gorm:"type:text" json:"profile_data"` // JSON
ConversationTurns string `gorm:"type:text" json:"conversation_turns"` // JSON array
ID string `gorm:"type:varchar(36);primaryKey" json:"id"`
UserID string `gorm:"type:varchar(36);uniqueIndex;not null" json:"user_id"`
ProfileData string `gorm:"type:text" json:"profile_data"` // JSON
ConversationTurns string `gorm:"type:text" json:"conversation_turns"` // JSON array
ConversationSummary string `gorm:"type:text" json:"conversation_summary"` // compressed old turns

CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`

User User `gorm:"foreignKey:UserID" json:"-"`
}

type FactCategory string

const (
FactCategoryPersonalInfo FactCategory = "personal_info"
FactCategoryFamily FactCategory = "family"
FactCategoryInterest FactCategory = "interest"
FactCategoryConcern FactCategory = "concern"
FactCategoryPreference FactCategory = "preference"
FactCategoryOther FactCategory = "other"
)

type ChatMemoryFact struct {
ID string `gorm:"type:varchar(36);primaryKey" json:"id"`
UserID string `gorm:"type:varchar(36);index;not null" json:"user_id"`
Content string `gorm:"type:text;not null" json:"content"`
Category FactCategory `gorm:"type:varchar(30);default:'other'" json:"category"`
CreatedAt time.Time `json:"created_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
LastReferencedAt *time.Time `json:"last_referenced_at"`

User User `gorm:"foreignKey:UserID" json:"-"`
}

func (f *ChatMemoryFact) BeforeCreate(tx *gorm.DB) error {
if f.ID == "" {
f.ID = uuid.New().String()
}
return nil
}

func (m *ChatMemory) BeforeCreate(tx *gorm.DB) error {
if m.ID == "" {
m.ID = uuid.New().String()
Expand Down Expand Up @@ -58,3 +89,11 @@ func (m *ChatMemory) SetTurns(turns []map[string]interface{}) {
b, _ := json.Marshal(turns)
m.ConversationTurns = string(b)
}

func (m *ChatMemory) GetSummary() string {
return m.ConversationSummary
}

func (m *ChatMemory) SetSummary(summary string) {
m.ConversationSummary = summary
}
54 changes: 53 additions & 1 deletion backend/internal/repository/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,62 @@ func (r *ChatRepo) FindByUserID(userID string) (*model.ChatMemory, error) {
func (r *ChatRepo) Upsert(m *model.ChatMemory) error {
return r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}},
DoUpdates: clause.AssignmentColumns([]string{"profile_data", "conversation_turns", "updated_at"}),
DoUpdates: clause.AssignmentColumns([]string{"profile_data", "conversation_turns", "conversation_summary", "updated_at"}),
}).Create(m).Error
}

func (r *ChatRepo) Create(m *model.ChatMemory) error {
return r.db.Create(m).Error
}

// UpdateSummaryAndTurns updates only the summary and turns fields.
func (r *ChatRepo) UpdateSummaryAndTurns(userID string, summary string, turns string) error {
return r.db.Model(&model.ChatMemory{}).
Where("user_id = ?", userID).
Updates(map[string]any{
"conversation_summary": summary,
"conversation_turns": turns,
}).Error
}

// --- ChatMemoryFact methods ---

func (r *ChatRepo) FindFactsByUserID(userID string) ([]model.ChatMemoryFact, error) {
var facts []model.ChatMemoryFact
err := r.db.Where("user_id = ?", userID).Order("created_at desc").Find(&facts).Error
return facts, err
}

func (r *ChatRepo) FindFactByID(id string) (*model.ChatMemoryFact, error) {
var f model.ChatMemoryFact
err := r.db.Where("id = ?", id).First(&f).Error
if err != nil {
return nil, err
}
return &f, nil
}

func (r *ChatRepo) CreateFact(f *model.ChatMemoryFact) error {
return r.db.Create(f).Error
}

func (r *ChatRepo) DeleteFact(id string) error {
return r.db.Where("id = ?", id).Delete(&model.ChatMemoryFact{}).Error
}

func (r *ChatRepo) FactExistsByContent(userID, content string) (bool, error) {
var count int64
err := r.db.Unscoped().Model(&model.ChatMemoryFact{}).
Where("user_id = ? AND content = ?", userID, content).
Count(&count).Error
return count > 0, err
}

func (r *ChatRepo) TouchFactReferencedAt(ids []string) error {
if len(ids) == 0 {
return nil
}
return r.db.Model(&model.ChatMemoryFact{}).
Where("id IN ?", ids).
Update("last_referenced_at", gorm.Expr("NOW()")).Error
}
4 changes: 4 additions & 0 deletions backend/internal/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ func Setup(
{
companion.POST("/chat", aiLimiter, middleware.AuthOptional(cfg), chatHandler.Chat)
companion.GET("/profile", middleware.AuthOptional(cfg), chatHandler.GetProfile)
companion.GET("/memories", middleware.AuthRequired(cfg), chatHandler.GetMemories)
companion.DELETE("/memories/:id", middleware.AuthRequired(cfg), chatHandler.DeleteMemory)
companion.GET("/history", middleware.AuthRequired(cfg), chatHandler.GetHistory)
companion.DELETE("/history", middleware.AuthRequired(cfg), chatHandler.ClearHistory)
}

echo := api.Group("/echo", middleware.AuthRequired(cfg))
Expand Down
Loading
Loading