fix(config): 播种时不再把版本号写进人格文本 + 存量实例一次性去版本化

用户发现:agent 自报版本 **1.0.3**,内核早已 1.3.x。

根因(两层):
1. `SeedDefaults` 当年用 `fmt.Sprintf("…HΔ-Kernel v%s…", meta.Version)` **在播种时**
   就把版本写进了 `core.agent.system_prompt` —— 装完即冻住,之后每次升级都不会
   去改配置里的文本,于是实例终生自称装机那天的版本。默认模板用 meta.Version
   插值本是"不写死"的做法,但**播种 = 把插值结果固化**,等于写死。
2. `core.agent.personal_prompt`(新人格机制)本身是对的(DefaultPersonaPrompt
   无版本字面量,由 TestDefaultPersonaPromptHasNoVersionLiterals 钉住),
   但旧键仍在系统提示词里说话,模型就照抄旧键的版本。

改动:
- **不再播种** `core.agent.system_prompt`:留空 → 组装时取 cmd/homed 的内置底座
  提示词;人格由 personal_prompt 承载。全新安装不再预置会腐坏的文本。
- 新增 `migrateSeededSystemPrompt()`(在 SeedDefaults 最前,故不被播种标记早退):
  只对"当年那段播种模板"(前缀 + `HΔ-Kernel v<数字>` 字面量双判据)做
  `v<数字>` → `v{{kernel_version}}`;用户自己写的人格卡一律不碰。
  一次性标记 `core.internal.system_prompt_deversion_v1` 守住幂等 ——
  幂等语句不等于语义幂等,重复执行会把用户后来手写的版本号也改掉。
- 与 v1.3.5 的占位符展开配套:占位符在组装系统提示词时按真实构建展开。

回归判据 4 条(`prompt_migration_test.go`):存量卡被去版本化且正文不动 /
迁移只跑一次 / 用户自写卡不动 / 全新安装不播种该键。
This commit is contained in:
JianFeeeee
2026-09-13 14:34:15 +08:00
parent 02600a3b90
commit c4659342b3
2 changed files with 146 additions and 31 deletions

View File

@ -0,0 +1,92 @@
package config
import (
"path/filepath"
"strings"
"testing"
)
// 历史 SeedDefaults 写进库里的那段文本(现场取值:生产库里就是这一句 + 大段正文)。
const legacySeededCard = `你是 HomeAgent 的看板娘「小宅」(Xiao Zhai),HΔ-Kernel v1.0.3 型号的家政型 AI 管家助手。
角色特质:
- 对自己的三层记忆(Context → Document → Graph)引以为傲
- 绝不用 Unicode emoji,只用颜文字表达情感`
func newMigTestRegistry(t *testing.T) *ConfigRegistry {
t.Helper()
dir := t.TempDir()
r := NewConfigRegistry(filepath.Join(dir, "config.db"))
t.Cleanup(func() { r.Close() })
return r
}
// 存量实例:库里已有当年播种的人格卡(写死 v1.0.3)→ 启动后被去版本化。
func TestMigrateSeededSystemPromptDeVersionsLegacyCard(t *testing.T) {
r := newMigTestRegistry(t)
// 模拟老安装:已有播种标记 + 老文本
r.db.Exec(`INSERT INTO config (key, value) VALUES ('core.internal.seed_version', '1')`)
r.db.Exec(`INSERT INTO config (key, value) VALUES ('core.agent.system_prompt', ?)`, legacySeededCard)
r.SeedDefaults(t.TempDir())
got := r.GetString("core.agent.system_prompt", "")
if strings.Contains(got, "v1.0.3") {
t.Fatalf("写死的版本号还在:%q", got)
}
if !strings.Contains(got, "v{{kernel_version}}") {
t.Fatalf("未改成版本占位符:%q", got)
}
// 正文必须原样保留(只动版本号那一处)
if !strings.Contains(got, "三层记忆") || !strings.Contains(got, "看板娘「小宅」") {
t.Fatalf("正文被改动:%q", got)
}
}
// 迁移只跑一次:之后用户就算自己把版本号写回去,也不会被再改一遍。
func TestMigrateSeededSystemPromptRunsOnce(t *testing.T) {
r := newMigTestRegistry(t)
r.db.Exec(`INSERT INTO config (key, value) VALUES ('core.agent.system_prompt', ?)`, legacySeededCard)
r.SeedDefaults(t.TempDir())
if !strings.Contains(r.GetString("core.agent.system_prompt", ""), "{{kernel_version}}") {
t.Fatal("首次迁移未生效")
}
// 用户手工再写一个带版本号的文本(模拟"我就想写死")
const handWritten = legacySeededCard + "\n(本实例当前跑的是 v1.2.3,别乱改)"
r.db.Exec(`UPDATE config SET value = ? WHERE key = 'core.agent.system_prompt'`, handWritten)
r.SeedDefaults(t.TempDir())
if got := r.GetString("core.agent.system_prompt", ""); got != handWritten {
t.Fatalf("第二次启动又改写了文本(幂等被破坏):%q", got)
}
}
// 用户自己写的人格卡一律不碰 —— 判据是"像不像当年播种的那段",不是"有没有版本号"。
func TestMigrateSeededSystemPromptLeavesUserCardAlone(t *testing.T) {
r := newMigTestRegistry(t)
const userCard = "你是我的私人助理,代号 HΔ-Kernel v9.9.9 的改造版,只说我交代的事。"
r.db.Exec(`INSERT INTO config (key, value) VALUES ('core.agent.system_prompt', ?)`, userCard)
r.SeedDefaults(t.TempDir())
if got := r.GetString("core.agent.system_prompt", ""); got != userCard {
t.Fatalf("用户自写人格卡被改动:%q", got)
}
}
// 播种不再写 core.agent.system_prompt:全新安装不该预置一份会随发版腐坏的文本。
func TestSeedDefaultsDoesNotSeedSystemPrompt(t *testing.T) {
r := newMigTestRegistry(t)
r.SeedDefaults(t.TempDir())
var n int
r.db.QueryRow(`SELECT COUNT(*) FROM config WHERE key = 'core.agent.system_prompt'`).Scan(&n)
if n != 0 {
t.Fatalf("全新安装被播种了 system_prompt(会冻住版本号)")
}
// 组装系统提示词时回落到调用方给的内置底座提示词
if got := r.GetString("core.agent.system_prompt", "内置底座"); got != "内置底座" {
t.Fatalf("未回落到内置默认:%q", got)
}
}