- 四组并行完成:main/wire*.go、wire_capacity|channels|runtime、channels|persona_*|emotion|observability、 voice|plugins|stream|skills|vision|subagent_tool|extractor|mcp|sessionread_tool|terminal_tool。 - 机械校验(git diff -U0 去 // 行与空行)仅剩 wire_frontend.go:84 一处行尾注释:代码前缀 「\ta.gw = gw 」经 cat -A 与 HEAD 逐字节比对一致(allow 清单已声明)。 - 验收:隔离 worktree(HEAD + 本批 patch)go build/vet 绿、全树 58 包 ok、 linux/amd64 + darwin/arm64 交叉编译绿;gofmt -l 干净。 - 备注:整树两次红均为负载敏感 flake(backthink / orchestration 的 timeout 类用例单独 -count=20 全过)。
85 lines
4.4 KiB
Go
85 lines
4.4 KiB
Go
package main
|
||
|
||
import (
|
||
"strings"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/persona/emotion"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||
)
|
||
|
||
// 本文件把 persona 情绪状态机(design/01 §2:情绪状态机 + 好感度曲线跟随
|
||
// persona/角色,绝不跟随行为模式)接到 agent 循环的轮次边界上。循环保持与情绪无关:
|
||
// 它上报每个已结算轮次,并把返回的心情标签带到 Result.Mood,由持久化层写到助手消息上
|
||
// (design/16 §3.3)。
|
||
//
|
||
// 本里程碑(A1)端到端接通的范围:
|
||
// - 每个已结算的主人轮次推进所绑定 persona 的情绪状态机与好感度(内存态,按 persona
|
||
// 为键,由 registry 的 emotion Manager 持有);
|
||
// - 由此得到的心情随助手消息进入 chat_messages 与 `message_persisted` 帧,而每次
|
||
// *变化* 都作为 `mood` 帧广播。
|
||
//
|
||
// 未接通(design/01 未作规定):心情尚未反注入 system 提示,因此不会左右回复文本;
|
||
// 情绪状态也不跨重启持久化(design/01 两者都不要求;启动时状态重置为 persona 配置的
|
||
// initialState)。
|
||
|
||
// moodEmitter 是面向 gateway 的 `mood` 广播出口。*gateway.Server 实现它
|
||
// (BroadcastMood);这里持有接口是为了让组合根能用假实现做单元测试,与
|
||
// capacityEmitter 一致。
|
||
type moodEmitter interface {
|
||
BroadcastMood(personaID, mood string, affection int)
|
||
}
|
||
|
||
// buildEmotionObserver 装配 persona 情绪观察者:registry 的 emotion Manager 提供
|
||
// 每个 persona 的状态机与好感度,registry 提供 persona 配置的好感度步长
|
||
// (好感度曲线),每次变化都经 publishMood 推送到 Web UI。
|
||
func (a *app) buildEmotionObserver() *emotion.Observer {
|
||
return emotion.NewObserver(a.personas.Emotions(), a.personas.AffectionStep, a.publishMood)
|
||
}
|
||
|
||
// onTurnEnd 是循环的轮次边界钩子(design/01 §2):已结算的轮次被施加到循环所绑定的
|
||
// persona 上,并把该轮之后的心情返回给助手消息。persona id 缺失时退回默认 persona,
|
||
// 与循环的绑定方式一致。
|
||
//
|
||
// 两道闸门确保只有真实对话轮次才触发:
|
||
// - 只有面向用户的对话车道(Web 主会话及其子会话;IM 主人私聊走主会话,
|
||
// design/16 §4.4)会推动心情。自主与委派的循环调用 —— 后台思考(sess-backthink)、
|
||
// 子代理工作、隔离的 IM 会话 —— 不是主人对话的轮次,因此不得改写她的心情;
|
||
// - 受限(群聊/陌生人)对话是只读的:观察者拒绝写入,因此第三方永远无法左右她的
|
||
// 心情或好感度(design/02 §3、design/11 §2)。
|
||
func (a *app) onTurnEnd(end loop.TurnEnd) string {
|
||
if a.emotion == nil || !isConversationLane(end.SessionID) {
|
||
return ""
|
||
}
|
||
personaID := end.PersonaID
|
||
if personaID == "" {
|
||
personaID = a.defaultPersonaID()
|
||
}
|
||
return string(a.emotion.Observe(personaID, end.UserText, !end.Env.Restricted()))
|
||
}
|
||
|
||
// isConversationLane 报告某会话是否属于主人的实时对话:固定的主会话 "web" 及其
|
||
// "web:<uuid>" 子会话(design/16 §2.1 —— IM 主人私聊被路由进主会话)。
|
||
func isConversationLane(sessionID string) bool {
|
||
return sessionID == history.MainSessionID || strings.HasPrefix(sessionID, history.MainSessionID+":")
|
||
}
|
||
|
||
// publishMood 把一次情绪变化作为 `mood` 帧广播(design/07 §3、design/01 §2),
|
||
// 使 Web 的心情徽标无需刷新即可跟随 persona。它只在心情确实变化时调用,因此该帧
|
||
// 是新闻而不是每轮噪声。出口在调用时读取,因为 gateway 构建在循环之后;它还不存在
|
||
// 时该变化只记日志并丢弃。
|
||
func (a *app) publishMood(update emotion.Update) {
|
||
log := logger.WithSource(a.log, "cmd/cyrene")
|
||
if a.moodSink == nil {
|
||
log.Debug("mood change dropped: gateway not ready",
|
||
"persona", update.PersonaID, "from", string(update.Before), "to", string(update.After),
|
||
"affection", update.Affection, "signal", update.Signal)
|
||
return
|
||
}
|
||
log.Info("persona mood changed",
|
||
"persona", update.PersonaID, "from", string(update.Before), "to", string(update.After),
|
||
"affection", update.Affection, "signal", update.Signal)
|
||
a.moodSink.BroadcastMood(update.PersonaID, string(update.After), update.Affection)
|
||
}
|