fix(loop,gateway,persona): awakening 逐 turn 按角色生效 + 修「面板保存角色抹掉 5 个块」的数据丢失(裁决 14.4.3)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- per-role 注入补齐:Options.PersonaAwakening(personaID) → Loop.personaAwakening → l.awakeningFor(ctx)
  → capacity.Input.Awakening(toolSchemas 是全仓唯一构造该 Input 处)
  · 按 turn 解析(不绑 Loop 构造期,避免并发会话数据竞争),与既有 personaFor/knobsFor 同构
  · 未知角色/空角色/档案无该块 ⇒ 零值 ⇒ 回落 Config.Awakening(不声明者行为与今天构造性一致)
- **数据丢失修复**:persona PUT 是整块替换,此前只保留 thinking ⇒ 保存一次会同时抹掉
  awakening / visual(头像·背景) / triggerWords(群聊提醒词) / emotion(情绪机参数) / affection(好感度曲线)。
  新增 applyAwakeningEdit + applyPersonaReadOnlyBlocks 逐一保留(cyrene.yaml 五块全写,属必现故障)
- 文件守 500 行:主人档案段原样搬移到 admin_owner_profile.go(零逻辑变更)
- 复核:build/vet 绿 + 全树 `go test ./... -count=1` 0 FAIL;端到端=真 YAML→loader→buildCapacity→真 loop
  流式请求→断言工具面(tasky 声明 force_state 唤醒 → 含 web-search;cyrene 不声明 → 与无解析器对照逐字相同);
  **8 条变异反证**先红后还原(含逐块删除各自的保留逻辑)
- 残留缺口(登记):wire_runtime.go:254 那一行本身无用例覆盖;loader 非 strict(未知 YAML 键静默忽略,
  需主人裁决是否开 KnownFields);POST /admin/personas 仍写不出这五个块
This commit is contained in:
AskaEth committed 2026-10-04 13:07:52 +08:00
1 parent 58cba0b8d2
commit 7afef8d037
11 files changed
+778 -142

No files matched your search

+29 -4
View File
@@ -4,6 +4,7 @@ import (
"context"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/capacity"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
@@ -13,11 +14,11 @@ import (
// 记忆命名空间、情绪机与推理旋钮 —— 改为从该 turn 的
// SessionEnv 解析。
//
// 解析刻意做成 ctx 上的三个小函数,而不是 Loop 上的
// 解析刻意做成 ctx 上的几个小函数,而不是 Loop 上的
// 字段:Loop 由所有并发会话共享,把解析出的角色存在
// 它上面会产生数据竞争,而 ctx 本就携带按 turn 的 env。
// 所有角色作用域读取都必须走这些辅助函数,使提示词、记忆、
// 情绪与思考永远不会对「谁在回答」产生分歧。
// 情绪、思考与唤醒档位永远不会对「谁在回答」产生分歧。
// LoopKnobs 是注入循环构建的每个请求的按角色模型/推理
// 旋钮。它镜像三个构造期选项
@@ -36,8 +37,8 @@ type LoopKnobs struct {
// personaFor 返回当前 turn 发言所作为的角色(design/22 §2):调用方
// 解析出角色时取会话环境的显式角色,否则取循环
// 配置的角色(Options.PersonaID,全局默认)。提示词/记忆
// 与情绪钩子都读它,因此一个 turn 绝不会混用两个角色。
// 配置的角色(Options.PersonaID,全局默认)。提示词/记忆、
// 情绪钩子与唤醒档位都读它,因此一个 turn 绝不会混用两个角色。
func (l *Loop) personaFor(ctx context.Context) string {
if pid := sessionEnvFrom(ctx).PersonaID; pid != "" {
return pid
@@ -84,3 +85,27 @@ func (l *Loop) knobsFor(ctx context.Context) LoopKnobs {
}
return knobs
}
// awakeningFor 解析当前 turn 的唤醒档位(design/14 §3,裁决 14.4.3):该 turn
// 所属角色在档案里声明的 `awakening` 块,映射成决策层的人格覆盖。档位随 turn
// 解析而不是在 Loop 构造期绑定,理由与提示词/记忆/旋钮相同 —— 一个进程同时
// 承载多个角色,而 Loop 由所有并发会话共享(存在 Loop 上会产生数据竞争)。
// 解析器由组合根注入(Options.PersonaAwakening,按角色 id 查那份 YAML),
// 与 knobsFor 一样按角色 id 解析,因此所有路径(网关、channelbridge、
// backthink、子代理)都从同一个输入落到正确角色的档位。
//
// 下列情形一律返回零值 = **无覆盖**,由判定器回落到 Config.Awakening(全局
// 兜底)与全局阈值,绝不在此处凭空补档位:
// - 未注入解析器(单角色接线、或未接线的调用方)——逐字节等于接线前的行为;
// - 该 turn 没有角色(SessionEnv.PersonaID 与 Options.PersonaID 都为空);
// - 该角色档案里没写 `awakening` 块(缺席就是没有)。
func (l *Loop) awakeningFor(ctx context.Context) capacity.Awakening {
if l.personaAwakening == nil {
return capacity.Awakening{}
}
pid := l.personaFor(ctx)
if pid == "" {
return capacity.Awakening{}
}
return l.personaAwakening(pid)
}