// Package emotion implements Cyrene's mood state machine and affection level. // Mood drifts with user sentiment and decays back to calm over time; // affection grows with positive interactions. package emotion import ( "strings" "sync" "time" ) // Mood is the current emotional state. type Mood string const ( MoodCalm Mood = "平静" MoodHappy Mood = "开心" MoodSad Mood = "难过" MoodShy Mood = "害羞" MoodTouched Mood = "感动" ) // Snapshot is a point-in-time view of the emotional state. type Snapshot struct { Mood Mood `json:"mood"` Intensity float64 `json:"intensity"` // 0.0 - 1.0 Affection int `json:"affection"` // 0 - 100 UpdatedAt time.Time `json:"updated_at"` } // Tracker tracks mood and affection over time. type Tracker struct { mu sync.Mutex mood Mood intensity float64 affection int updatedAt time.Time decayAfter time.Duration onChange func(Snapshot) } // NewTracker creates a tracker starting calm with zero affection. func NewTracker() *Tracker { return &Tracker{ mood: MoodCalm, intensity: 0.3, updatedAt: time.Now(), decayAfter: 30 * time.Minute, } } // OnChange registers a callback fired whenever the snapshot changes. func (t *Tracker) OnChange(fn func(Snapshot)) { t.onChange = fn } // Snapshot returns the current state, applying time decay first. func (t *Tracker) Snapshot() Snapshot { t.mu.Lock() defer t.mu.Unlock() t.decayLocked() return Snapshot{Mood: t.mood, Intensity: t.intensity, Affection: t.affection, UpdatedAt: t.updatedAt} } // PromptLine renders the state for injection into the system prompt. func (t *Tracker) PromptLine() string { s := t.Snapshot() return moodPromptLine(s) } // RecordMessage updates mood/affection based on a user message. func (t *Tracker) RecordMessage(text string) { t.mu.Lock() defer t.mu.Unlock() t.decayLocked() sent := AnalyzeSentiment(text) switch { case sent > 0: t.affection += 2 t.shiftMood(MoodHappy, 0.6+0.2*float64(sent)) case sent < 0: t.affection -= 1 t.shiftMood(MoodSad, 0.5) default: t.affection += 1 // every interaction counts a little t.shiftMood(MoodCalm, 0.3) } if t.affection > 100 { t.affection = 100 } if t.affection < 0 { t.affection = 0 } t.updatedAt = time.Now() if t.onChange != nil { s := Snapshot{Mood: t.mood, Intensity: t.intensity, Affection: t.affection, UpdatedAt: t.updatedAt} go t.onChange(s) } } // shiftMood moves toward a mood, raising intensity when already there. func (t *Tracker) shiftMood(m Mood, intensity float64) { if t.mood == m { t.intensity = min64(1.0, t.intensity+0.1) return } t.mood = m t.intensity = intensity } // decayLocked relaxes mood toward calm after inactivity. Caller holds mu. func (t *Tracker) decayLocked() { if time.Since(t.updatedAt) < t.decayAfter { return } if t.mood != MoodCalm { t.mood = MoodCalm t.intensity = 0.3 } } // AnalyzeSentiment scores text: positive count minus negative count. func AnalyzeSentiment(text string) int { score := 0 for _, w := range positiveWords { if strings.Contains(text, w) { score++ } } for _, w := range negativeWords { if strings.Contains(text, w) { score-- } } return score } var positiveWords = []string{ "谢谢", "感谢", "开心", "喜欢", "爱你", "棒", "好耶", "哈哈", "可爱", "太好了", "厉害", "乖", } var negativeWords = []string{ "讨厌", "生气", "难过", "伤心", "笨", "滚", "烦", "垃圾", "无聊", "失望", } func moodPromptLine(s Snapshot) string { level := "初识" switch { case s.Affection >= 80: level = "羁绊" case s.Affection >= 50: level = "信赖" case s.Affection >= 20: level = "熟悉" } return "当前情绪状态:" + string(s.Mood) + "(好感度阶段:" + level + ")。让回复的语气与情绪一致,但不要刻意提及这些数值。" } func min64(a, b float64) float64 { if a < b { return a } return b }