package main import ( "log/slog" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" personareg "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/sessionread" ) // wireSessionReadTool 注册 design/22 §8-3 的跨会话读取工具,使运行中的角色能把 // 另一个 Web 会话的上下文作为外部只读材料拉取过来(design/22 §8.1)。 // // 该能力以默认的可达性标记(两者皆 false)注册,从而保持仅主人可用:design/11 §3 的 // 策略引擎会对群聊/陌生人会话隐藏其 schema,循环的安全门也会在那里拒绝调用。工具 // 自身的 fail-closed 检查(主人环境 + web/web: 白名单)运行在那道门之后, // 因此未授权的读取会被双重拒绝。审计链路就是循环的 gate/OnAudit,与其他每个工具 // 完全一致 —— 这里不添加任何绕过路径。 func (a *app) wireSessionReadTool() { log := logger.WithSource(a.log, "cmd/cyrene") if a.history == nil { log.Warn("session read tool skipped: no history store") return } if a.dispatcher == nil { log.Warn("session read tool skipped: no tool dispatcher") return } reader := sessionread.New( sessionread.WithHistory(a.history), sessionread.WithPersonas(sessionReadPersonas{reg: a.personas}), sessionread.WithLocation(a.sessionReadLocation(log)), sessionread.WithLogger(a.log), ) if err := a.dispatcher.Register(reader); err != nil { log.Warn("session read tool register skipped", "error", err) return } log.Info("session read tool registered", "tool", sessionread.ToolName, "owner_only", true) } // sessionReadLocation 解析材料时间戳所用的已配置 IANA 时区,使外部 transcript 与 // 循环逐轮注入的「当前时间」读同一个时钟(internal/runtime/loop/time.go)。未设置 // 时区或加载失败时返回 nil,保持工具的进程本地默认值 —— 读取不该因为一个展示设置 // 而失败。 func (a *app) sessionReadLocation(log *slog.Logger) *time.Location { if a.cfg == nil || a.cfg.Timezone == "" { return nil } loc, err := time.LoadLocation(a.cfg.Timezone) if err != nil { log.Warn("session read tool timezone load failed; using the process-local zone", "tz", a.cfg.Timezone, "error", err) return nil } return loc } // sessionReadPersonas 把人格注册表适配为 sessionread.PersonaNamer。该工具包刻意不 // 导入 internal/persona/registry(而是持有一个窄接口,保持分层单向),因此由组合根 // 提供显示名查询。 type sessionReadPersonas struct { reg *personareg.Registry } // PersonaName 实现 sessionread.PersonaNamer:返回人格的显示名;对未知/未注册的角色 // 返回 ok=false,使工具展示原始 id 而不是编造一个名字。 func (p sessionReadPersonas) PersonaName(id string) (string, bool) { if p.reg == nil || id == "" { return "", false } persona, ok := p.reg.Persona(id) if !ok || persona == nil || persona.Name == "" { return "", false } return persona.Name, true } // DefaultPersonaID 实现 sessionread.PersonaNamer:返回库默认角色所绑定的人格,与 // (*app).defaultPersonaID 保持一致。空 id 表示材料渲染显式的「未设置角色」标记, // 而不是靠猜。 func (p sessionReadPersonas) DefaultPersonaID() string { if p.reg == nil { return "" } if c, ok := p.reg.DefaultCharacter(); ok && c != nil { return c.PersonaID } return "" }