Files
Cyrene_v2/internal/persona/emotion/react.go
T
AskaEth 1a85de555d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(emotion): A1 情绪系统接线 + refactor: 拆分 channelbridge.go
A1 情绪接线:
- loop 回合边界钩子(TurnEnd/OnTurnEnd/settleTurn, Result.Mood); 每轮一次非每步
- persona/emotion: React(valence五档) + Machine.Move(BFS最短合法路径一步) + Observer(observe/decay/好感度,仅变化才emit); 好感度步长读角色配置(cyrene delta=10, 0..700)
- 落库 Mood(web session_persist.res.Mood + IM channelbridge/persist; user消息不带mood); WS mood事件(BroadcastMood,仅变化); per-persona隔离; 群聊/陌生人只读不写不发
- cmd/cyrene/emotion.go 生产接线 + wire.go 注入 loop.Options.OnTurnEnd
- 22 测试(状态机被loop调用/Mood非空落库/mood帧shape/只变化才发/群聊只读) + 真二进制冒烟
- Docs: api-client v0.32 §3.7 情绪事件 + 开源对照清单(companion 应用对照)
- 诚实: 本机无LLM凭据→未活体验证'模型返回后完整轮次'一环

拆分 channelbridge.go 816→257(纯搬移零逻辑): connect/inbound/outbound/blacklist/helpers.go; A1 的 IM mood 调用点落在新 inbound.go, 故与本提交同批

回滚 6 个 gofmt 顺手格式化文件(capacity_loop_test/flow/source_test/findchannel_test/admin_channels_platform_test/history) 缩 diff, 含 history.go 注释被转弯引号
2026-09-30 08:51:38 +08:00

105 lines
4.3 KiB
Go

package emotion
import "strings"
// Reaction is one turn's emotional reading of the user's message (design/01 §2:
// the persona config carries the emotion state-machine parameters and the
// affection curve; the machine itself only knows states and legal moves).
//
// design/01 fixes the model (states + transitions + decay + affection curve) but
// does not enumerate the trigger vocabulary, so the minimal reading implemented
// here is: a clearly angry turn, a clearly down turn, a strongly positive turn
// and a mildly positive turn, each moving the matching state and stepping the
// affection curve once. A turn with no cue carries no signal, which leaves the
// emotion to decay toward calm instead of jumping (see Observer.Observe).
type Reaction struct {
// State is the emotion state this turn moves the machine toward. It is a
// target, not a forced value: the machine walks its configured transition
// table toward it (Machine.Move), so a persona's own table always wins.
State EmotionState
// AffectionDelta is the signed number of affection *steps* for this turn
// (+1 warm, -1 cold, 0 neutral). The concrete magnitude is the persona's
// configured affection curve step, applied by the caller.
AffectionDelta int
// Signal reports whether the turn carried any emotional cue at all.
Signal bool
}
// React classifies one user turn into the persona's emotional response. Anger
// and negative cues win over positive ones, so a mixed message ("不开心" also
// contains "开心") never reads as positive.
func React(text string) Reaction {
t := strings.ToLower(text)
switch {
case t == "":
return Reaction{}
case containsCue(t, angerCues()):
return Reaction{State: Angry, AffectionDelta: affectionDown, Signal: true}
case containsCue(t, negativeCues()):
return Reaction{State: Sad, AffectionDelta: affectionDown, Signal: true}
case containsCue(t, strongPositiveCues()):
return Reaction{State: Excited, AffectionDelta: affectionUp, Signal: true}
case containsCue(t, positiveCues()):
return Reaction{State: Happy, AffectionDelta: affectionUp, Signal: true}
default:
return Reaction{}
}
}
// Affection step signs. The magnitude comes from the persona's own curve.
const (
affectionUp = 1
affectionDown = -1
)
// containsCue reports whether text contains any of the cues.
func containsCue(text string, cues []string) bool {
for _, c := range cues {
if strings.Contains(text, c) {
return true
}
}
return false
}
// angerCues are the protest/hostility cues that move the persona to angry. They
// are checked first so an angry turn is never read as merely sad or happy.
func angerCues() []string {
return []string{
"讨厌你", "烦你", "恨你", "别烦", "好烦", "烦死", "烦人", "生气", "气死", "气愤",
"滚开", "走开", "闭嘴", "不理你", "分手", "凶我", "骂你",
"angry", "annoying", "annoyed", "shut up", "hate you", "mad at",
}
}
// negativeCues are the down/low cues that move the persona to sad — including
// tiredness, which in the five built-in states reads as the gentle/caring lane.
func negativeCues() []string {
return []string{
"不开心", "很难过", "难过", "伤心", "委屈", "失望", "沮丧", "郁闷", "崩溃", "心累",
"累了", "好累", "疲惫", "困了", "想哭", "呜呜", "555", "糟糕", "焦虑", "孤独",
"孤单", "寂寞", "失眠", "睡不着", "被冷落",
"sad", "tired", "upset", "exhausted", "lonely", "depressed", "feel bad",
}
}
// strongPositiveCues are the high-arousal warm cues that move the persona to
// excited (a stronger state than happy).
func strongPositiveCues() []string {
return []string{
"太开心", "好开心", "最喜欢", "太喜欢", "爱死", "太棒", "好耶", "好激动",
"兴奋", "幸福死了", "amazing", "awesome", "so happy", "love you",
}
}
// positiveCues are the everyday warm cues that move the persona to happy.
func positiveCues() []string {
return []string{
"开心", "高兴", "喜欢", "爱你", "想你", "抱抱", "么么", "亲亲", "摸摸", "谢谢",
"辛苦", "哈哈", "嘿嘿", "嘻嘻", "真好", "厉害", "好棒", "期待", "满足", "幸福",
"好呀", "好哒", "早安", "早上好", "中午好", "下午好", "晚上好", "晚安",
"nice", "great", "thanks", "thank you", "haha", "hug", "miss you",
"good morning", "good night", "good evening",
}
}