package main import ( "io" "log/slog" "os" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/loader" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" ) // identityKnowledgeHeader 是主人身份说明书注入上下文时的段头: // 与【性格】【价值观】等并列,使模型能把它读成「人格知识」,而不是一条指令。 const identityKnowledgeHeader = "【主人身份说明书】" // ownerIdentityKnowledge 读取 owner.yaml 的 identityFile 指向的正文 // (configs/users/owner_identity.md),供按会话注入。 // // 它是**人格知识**,不是权限来源:该文件只描述「你在陪伴谁、你们是什么关系」, // 身份识别始终只看渠道层 actor 元数据 + 白名单(design/11 §0),文件内容 // 既不参与 IsOwner,也不改变任何授权判定。文件里自己也把这条边界写在开头。 // // fail-soft:未配置、文件缺失或读不动(含路径指向目录)都返回空串 —— 只记一条 // WARN,绝不报错、绝不阻断启动或对话。这是「宁可少一段知识,也不能因为一个 // 可选文件起步失败」的取舍。 func (a *app) ownerIdentityKnowledge() string { if a.owner == nil { return "" } path := strings.TrimSpace(a.owner.IdentityFile) if path == "" { return "" } raw, err := os.ReadFile(path) if err != nil { if a.log != nil { logger.WithSource(a.log, "cmd/cyrene").Warn( "owner identity file skipped", "path", path, "error", err) } return "" } return strings.TrimSpace(string(raw)) } // identityKnowledgeBlock 把身份说明书正文渲染成一个可独立注入的系统块; // knowledge 为空(未配置 / 读不出)时返回空串 = 不注入任何块。 func identityKnowledgeBlock(knowledge string) string { if strings.TrimSpace(knowledge) == "" { return "" } return identityKnowledgeHeader + "\n" + knowledge } // personaInstructions 装配某个角色 agent 的**静态** system 指令:人格指令 // (instructionsFromPersona,design/01 §2.2)。默认 agent 与角色池里的每个 // agent 都走这里,因此两个装配点不会漂移。 // // 主人身份说明书**刻意不在这里**:静态指令在装配期定死,会让那段知识进入该 // 角色的每一个会话(含群聊与陌生人私聊)并随请求发给模型供应商。它现在按 // 会话注入 —— 见 personaKnowledgeFor 与 loop.Options.PersonaKnowledgeFor。 func (a *app) personaInstructions(p *loader.Persona) string { if p == nil { return "" } return instructionsFromPersona(p) } // personaKnowledgeFor 返回循环的按会话「人格知识」解析器 // (loop.Options.PersonaKnowledgeFor):该 turn 的会话在注入范围内时返回主人 // 身份说明书整块(段头 + 正文),否则返回空串 —— 空串意味着那段知识 // **完全不进**该会话的上下文,也就绝不随请求发给模型供应商。 // // 默认档 owner_only:只有渠道层 actor 元数据判定的主人车道拿得到它。群聊与 // 陌生人私聊(design/11 §0 隔离红线)返回空串。开关见 OWNER_IDENTITY_INJECTION。 // // 正文在装配期读一次并捕获进闭包(不是每轮读盘):它只在启动时变一次, // 每轮重读一个启动期就不变的可选文件纯属浪费。 func (a *app) personaKnowledgeFor(envFilePath string) func(loop.SessionEnv) string { log := sourcedWiringLog(a.log) mode := ownerIdentityInjectionMode(log, envFilePath) block := identityKnowledgeBlock(a.ownerIdentityKnowledge()) log.Info("owner identity injection", "mode", mode, "env", config.OwnerIdentityInjectionEnv, "knowledge", block != "") return func(env loop.SessionEnv) string { if !injectsIdentityKnowledge(mode, env) { return "" } return block } } // sourcedWiringLog 返回带来源标记 "cmd/cyrene" 的 logger;nil 基础 logger // 换成丢弃 logger(与 loop 包的 sourcedLog 同一约定),使这里的日志调用 // 永远不会因为「没接 logger」而空指针。 func sourcedWiringLog(base *slog.Logger) *slog.Logger { if base == nil { base = slog.New(slog.NewTextHandler(io.Discard, nil)) } return logger.WithSource(base, "cmd/cyrene") } // ownerIdentityInjectionMode 解析注入范围开关,任何读不出/不认识的情形都 // **明确回退**到默认的 owner_only 并记一条带原始取值的 WARN —— 回退方向永远 // 是最安全的一侧,且绝不静默(非法取值不会被当成默认值悄悄放行)。 func ownerIdentityInjectionMode(log *slog.Logger, envFilePath string) string { raw, err := config.LoadOwnerIdentityInjection(envFilePath) if err != nil { log.Warn("owner identity injection: cannot read config, falling back", "env", config.OwnerIdentityInjectionEnv, "fallback", config.OwnerIdentityOwnerOnly, "error", err) return config.OwnerIdentityOwnerOnly } mode, perr := config.ParseOwnerIdentityInjection(raw) if perr != nil { log.Warn("owner identity injection: unknown value, falling back", "env", config.OwnerIdentityInjectionEnv, "value", raw, "fallback", config.OwnerIdentityOwnerOnly, "error", perr) return config.OwnerIdentityOwnerOnly } return mode } // injectsIdentityKnowledge 报告该会话是否在注入范围内。 // // owner_only 的判定读**强类型的渠道层 actor 元数据**(SessionEnv.ActorEnv), // 与 memory / 工具准入门用的是同一个车道 —— 绝不从提示词文本或会话 id 猜。 // 注意反向也成立:身份说明书的内容永远不参与这个判定,也不参与任何授权。 func injectsIdentityKnowledge(mode string, env loop.SessionEnv) bool { switch mode { case config.OwnerIdentityAlways: return true case config.OwnerIdentityOff: return false default: // owner_only(默认,也是未知取值的回退档) return env.ActorEnv == restrict.ActorOwner } }