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 注释被转弯引号
105 lines
4.3 KiB
Go
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",
|
|
}
|
|
}
|