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 落库并置位标记
→ 之后不再追问;反向对照(清标记 + 清会话上下文 + 重启)重新开始询问,
排除了「同一段对话里已问过」这一混淆。
This commit is contained in:
JianFeeeee
2026-09-12 13:55:04 +08:00
parent ea21803acb
commit 46e014a8ca
10 changed files with 476 additions and 67 deletions

View File

@ -89,6 +89,16 @@ func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
}
}
// 首启人格门禁(跨通道唯一闸口):人格未确认时,要求模型主动询问用户。
// 系统提示词每轮重建,因此 WebUI / QQ / CLI / ACP / 邮件等所有通道都会带上它;
// 模型调用 persona_set或用户在 WebUI 向导里选)落地后,标记置位,本段消失。
if a.personaStore != nil && !a.personaStore.PersonaInitialized() {
prompt += "\n\n【首启人格设定】你的**人格设定尚未确认**。请在本轮回复里先问用户一句:" +
"要用默认人格,还是自定义一个?拿到明确答复后**必须调用 persona_set 工具**落库:" +
"用户选默认 → mode=default自定义 → mode=custom 且把内容写进 content" +
"用户说以后再说 → mode=later。用户答复前不要假设已设置也不要反复追问同一件事。"
}
prompt += a.buildToolCatalog()
return prompt
@ -197,6 +207,24 @@ func (a *Agent) buildToolDefs() []interface{} {
}
}
if a.personaStore != nil {
tools = append(tools, map[string]interface{}{
"type": "function",
"function": map[string]interface{}{
"name": "persona_set",
"description": "【首启人格】落地用户的人格选择并记录「已经问过」。仅在用户明确答复后调用:默认用 mode=default自定义用 mode=custom 并把人格内容放进 content用户说以后再说用 mode=later。",
"parameters": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"mode": map[string]interface{}{"type": "string", "description": "default | custom | later"},
"content": map[string]interface{}{"type": "string", "description": "自定义人格内容mode=custom 时必填)"},
},
"required": []string{"mode"},
},
},
})
}
if a.memory != nil {
tools = append(tools, map[string]interface{}{
"type": "function",