Files
Cyrene_v2/internal/services/gateway/persona_env.go
T
AskaEth 074ffc4ebf
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(gateway): gateway 剩余文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批:
已推库 16 个文件 + 本批剩余文件)。
校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0);
全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
2026-10-03 22:28:25 +08:00

153 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gateway
import (
"context"
"errors"
"log/slog"
"net/http"
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
)
// errPersonaBindingUnsupported 由 newSubSession 在调用方要求一个
// 绑定角色的会话、但注入的 history 存储无法绑定角色时返回。
// handler 把它映射为 501,而不是假装绑定已生效。
var errPersonaBindingUnsupported = errors.New("gateway: history store does not support session persona binding")
// 本文件拥有网关的回合环境解析(DevDocs/design/22
// §2/§4.5):由哪个角色应答,表达为该回合运行的
// loop.SessionEnv 环境。
//
// 优先级(design/22 §2,自上而下):
// 1. 请求级覆盖 —— 本批次不实现(批次 1 暴露的是
// 会话级绑定;按请求覆盖是加性且不持久化的);
// 2. 会话级绑定 —— chat_sessions.persona_id,在此解析;
// 3. 渠道级映射 —— 批次 2(channels.json / channel_sessions);
// 4. 全局默认 —— 表达为 PersonaID == "",由 loop 解析到其
// 已配置的默认角色(Options.PersonaID)。这正是让每个
// 既有会话的行为与以往完全一致的原因。
//
// env 的其他字段不受影响:对这些面向 owner 的 Web/语音路径,
// ActorEnv 与 MemWritable 仍来自 loop.OwnerEnv()。这就是身份红线
// —— 谁可以做什么由 channel 的 actor 元数据决定(此处是一个 JWT
// owner),绝不是由所扮演的角色决定。人格选择只改变
// 提示词、记忆命名空间、情绪机与思考预算。
// sessionPersonaReader 是 HistoryStore 可选的更窄视图,用于读取
// 会话的绑定角色。*history.Store 实现了它;未实现的存储(测试
// 替身,或早于 design/22 的存储)只是得到 "" —— 全局默认 —— 因此
// 网关继续工作。
type sessionPersonaReader interface {
SessionPersona(ctx context.Context, sessionID string) (string, error)
}
// sessionPersonaWriter 是 HistoryStore 可选的更窄视图,用于写入
// 会话的角色绑定。会话角色在创建时决定(design/22 §8-1),
// 因此不再有任何请求路径重绑 —— 创建走 subSessionCreator ——
// 而网关也不再对该接口做类型断言。它留在读取器
// 与创建器旁边,使存储的角色绑定表面集中记录在一处:
// *history.Store 仍实现 SetSessionPersona(design/22 §4.1),而会用它的
// 管理/owner 写路径是独立端点,不是 PATCH。
type sessionPersonaWriter interface {
SetSessionPersona(ctx context.Context, sessionID, personaID string) error
}
// subSessionCreator 是 HistoryStore 可选的更窄视图,用于创建一个
// 已绑定角色的会话(POST /sessions {personaId},design/22 §4.5)。
// *history.Store 实现了它;没有它的存储只能创建未绑定的
// 会话,因此显式要求角色的请求以 501 拒绝。
type subSessionCreator interface {
NewSubSessionFor(ctx context.Context, ownerID, personaID string) (string, error)
}
// turnEnvResolver 为一个回合构建 loop 会话环境。它持有
// 解析所需的两个只读依赖,因此 HTTP/WS handler
// 与 hub 都经同一条代码路径解析角色(design/22 §4.5:一个
// 解析器,而不是每个入口各硬编码一份 loop.OwnerEnv())。
type turnEnvResolver struct {
history HistoryStore
personas PersonaStore
log *slog.Logger
}
// owner 返回 sessionID 一个面向 owner 回合的每回合环境:
// 全访问 owner env(身份不变)加上该会话绑定的角色。
// 空白角色意味着「继承全局默认」;loop 执行那最后一次
// 回退,因此网关永远不必知道默认角色的 id。
func (t turnEnvResolver) owner(ctx context.Context, sessionID string) loop.SessionEnv {
env := loop.OwnerEnv()
env.PersonaID = t.sessionPersona(ctx, sessionID)
return env
}
// sessionPersona 返回绑定到 sessionID 的角色,会话继承全局
// 默认时返回 ""。未知会话、没有读取器的存储、或
// 读错误都退化为 ""(默认),而不是让该回合失败:
// 聊天绝不能被一次元数据查找阻塞,而该回退正是
// 多角色之前的行为。
func (t turnEnvResolver) sessionPersona(ctx context.Context, sessionID string) string {
if t.history == nil || sessionID == "" {
return ""
}
reader, ok := t.history.(sessionPersonaReader)
if !ok {
return ""
}
personaID, err := reader.SessionPersona(ctx, sessionID)
if err != nil {
if t.log != nil && !errors.Is(err, history.ErrSessionNotFound) {
t.log.Debug("session persona read", "err", err, "session", sessionID)
}
return ""
}
return personaID
}
// defaultPersonaID 返回库中默认角色的人格 id,未接线人格
// 存储或没有角色被标记为默认时返回 ""。它是
// owner.yaml:defaultPersona 的网关侧视图,用于在没有会话时
// 需要角色 id 的场合(管理记忆搜索回退,design/22 §4.5)。
func (s *Server) defaultPersonaID() string {
if s.personas == nil {
return ""
}
for _, c := range s.personas.List() {
if c.Default && c.PersonaID != "" {
return c.PersonaID
}
}
return ""
}
// requireKnownPersona 拒绝注册表不认识的 personaId,返回
// 400 并返回 false。空 id 一律允许(它意味着「继承
// 全局默认」),而没有 persona 存储的网关无法校验,因此
// 那里接受该绑定,之后只是解析为未知角色。
func (s *Server) requireKnownPersona(w http.ResponseWriter, personaID string) bool {
if personaID == "" || s.personas == nil {
return true
}
if _, ok := s.personas.Persona(personaID); !ok {
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "unknown personaId %q", personaID))
return false
}
return true
}
// newSubSession 创建一个绑定到 personaID 的子会话,存储提供
// 角色感知创建器时使用它。没有它的存储只能创建
// 未绑定会话,因此显式的角色请求被拒绝(501),而不是
// 静默丢弃该绑定。
func (s *Server) newSubSession(ctx context.Context, ownerID, personaID string) (string, error) {
creator, ok := s.history.(subSessionCreator)
if !ok {
if personaID != "" {
return "", errPersonaBindingUnsupported
}
return s.history.NewSubSession(ctx, ownerID)
}
return creator.NewSubSessionFor(ctx, ownerID, personaID)
}