Files
HomeAgent/internal/config/persona.go
JianFeeeee 46e014a8ca feat(persona): 首启人格门禁跨通道化 + 内核 persona_set 工具
WebUI 首启向导只覆盖 WebUI 这一条通道,而「人格该问一次」是所有通道的事:
走 QQ / CLI / ACP / 邮件来的人永远见不到那个向导,人格就永远是没确认过。

- 门禁移到 buildSystemPrompt(每轮重建 → WebUI/QQ/CLI/ACP/邮件全覆盖),
  以 core.internal.persona_initialized 为准:未确认时要求模型主动询问用户
  (默认 / 自定义 / 以后再说),确认后该段消失;personaStore 为 nil 时静默关闭。
- 新增内核内置工具 persona_set(mode=default|custom|later[, content]),
  落库逻辑与 WebUI 向导**共用 internal/config**(一个实现 + 两个薄入口:
  ConfigRegistry 直连 / 插件侧 SettingsAPI),避免两套语义各自漂移。
- AgentConfig 增加 PersonaStore 接口,cmd/homed 用 RegistryPersonaStore 实现。
- 非法输入(未知 mode / custom 空内容)在打标记**之前**拒绝:否则标记置位、
  向导被跳过,用户再没机会设。

E2E(隔离实例 + 真实 LLM 往返走 /v1/chat/completions,/var/tmp/persona/e2e.sh)9/9 PASS:
未确认时模型主动询问 → 用户答「用默认的」→ 模型调用 persona_set 落库并置位标记
→ 之后不再追问;反向对照(清标记 + 清会话上下文 + 重启)重新开始询问,
排除了「同一段对话里已问过」这一混淆。
2026-09-12 13:57:06 +08:00

140 lines
4.8 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 config
import (
"fmt"
"strings"
)
// 人格设定的两个键:内容与「首启向导/工具已经问过」的一次性标记。
//
// 为什么需要标记:人格曾经只有 <dataDir>/personal/personal.md 一个来源且无人维护,
// 里面写死的旧版本号反过来让实例自述旧版本v1.2.0 压测发现)。
// 现在人格是配置项(默认模板不含任何版本号),任何通道的第一次交互问一次,之后不再打扰。
const (
// PersonaPromptKey 是人格设定内容(【人格设定】块的正文)。
PersonaPromptKey = "core.agent.personal_prompt"
// PersonaInitMarkerKey 是「已经问过/已确认」的一次性标记。
PersonaInitMarkerKey = "core.internal.persona_initialized"
)
// 三种落库方式WebUI 首启向导与内核 persona_set 工具共用。
const (
PersonaModeDefault = "default" // 使用内置默认模板
PersonaModeCustom = "custom" // 使用调用方提供的内容
PersonaModeLater = "later" // 保留当前(默认)人格,只打标记不再问
)
// PersonaKV 是人格落库所需的最小读写面GetCore/SetCore 的签名与
// 公开 SDK 的 SettingsAPI 一致因此插件侧WebUI可直接传入
// 内核直连配置注册表时用 registryKV 适配(见文件末)。
type PersonaKV interface {
GetCore(key string) (interface{}, error)
SetCore(key string, value interface{}) error
}
// PersonaInitializedKV 报告人格是否已确认(向导或工具已问过)。
func PersonaInitializedKV(kv PersonaKV) bool {
if kv == nil {
return false
}
v, err := kv.GetCore(PersonaInitMarkerKey)
if err != nil {
return false
}
s, _ := v.(string)
return strings.TrimSpace(s) != ""
}
// CurrentPersonaKV 读当前人格内容;未设置时回落到内置默认模板。
func CurrentPersonaKV(kv PersonaKV) string {
if kv == nil {
return DefaultPersonaPrompt
}
if v, err := kv.GetCore(PersonaPromptKey); err == nil {
if s, ok := v.(string); ok && strings.TrimSpace(s) != "" {
return s
}
}
return DefaultPersonaPrompt
}
// SetPersonaKV 落库人格并打一次性标记,返回 restartRequired。
//
// 生效时机:人格在 homed 启动时载入(拼成【人格设定】块进系统提示词),
// 所以**自定义内容需重启**default 与 later 都不改变当前已生效的人格,无需重启。
//
// 非法输入一律在打标记之前拒绝——否则向导会被跳过,用户再也没机会设。
func SetPersonaKV(kv PersonaKV, mode, content string) (restartRequired bool, err error) {
if kv == nil {
return false, fmt.Errorf("persona: settings unavailable")
}
switch mode {
case PersonaModeDefault:
if err := kv.SetCore(PersonaPromptKey, DefaultPersonaPrompt); err != nil {
return false, err
}
case PersonaModeCustom:
if strings.TrimSpace(content) == "" {
return false, fmt.Errorf("persona: content required for custom mode")
}
if err := kv.SetCore(PersonaPromptKey, content); err != nil {
return false, err
}
restartRequired = true
case PersonaModeLater:
// 保持当前人格(通常是默认模板),只打标记
default:
return false, fmt.Errorf("persona: unknown mode %q (want default|custom|later)", mode)
}
if err := kv.SetCore(PersonaInitMarkerKey, "1"); err != nil {
return restartRequired, err
}
return restartRequired, nil
}
// RegistryPersonaStore 把配置注册表暴露成内核的 core.PersonaStore 接口
// (结构类型:方法集匹配即可,无需 import internal/agent/core
type RegistryPersonaStore struct{ Reg *ConfigRegistry }
func (p RegistryPersonaStore) PersonaInitialized() bool { return PersonaInitialized(p.Reg) }
func (p RegistryPersonaStore) SetPersona(mode, content string) (bool, error) {
return SetPersona(p.Reg, mode, content)
}
// registryKV 把内核直连的配置注册表适配成 PersonaKV。
type registryKV struct{ reg *ConfigRegistry }
func (r registryKV) GetCore(key string) (interface{}, error) {
v, err := r.reg.Get(key)
if err != nil || v == nil {
// 未设置的键对 PersonaKV 语义等同「没有」,不当作错误
if s := r.reg.GetString(key, ""); s != "" {
return s, nil
}
return "", nil
}
return v, nil
}
func (r registryKV) SetCore(key string, value interface{}) error {
s, ok := value.(string)
if !ok {
return fmt.Errorf("persona: value must be a string")
}
return r.reg.Set(key, s)
}
// PersonaInitialized 报告人格是否已确认(内核直连注册表)。
func PersonaInitialized(reg *ConfigRegistry) bool {
return reg != nil && PersonaInitializedKV(registryKV{reg})
}
// SetPersona 落库人格并打一次性标记(内核直连注册表)。
func SetPersona(reg *ConfigRegistry, mode, content string) (bool, error) {
if reg == nil {
return false, fmt.Errorf("persona: config registry unavailable")
}
return SetPersonaKV(registryKV{reg}, mode, content)
}