From 17da321f7f8adf72b9580a309b76ff27a3eefc7c Mon Sep 17 00:00:00 2001 From: leiyue <2286721642@qq.com> Date: Sat, 1 Aug 2026 21:11:12 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20meta.json=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20default=5Fmodel=20=E9=BB=98=E8=AE=A4=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E9=85=8D=E7=BD=AE(auto/pro/flash)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tui/meta.go | 14 ++++++++ tui/meta_test.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ tui/model.go | 7 +++- 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 tui/meta_test.go diff --git a/tui/meta.go b/tui/meta.go index 621cb6d..0828a9a 100644 --- a/tui/meta.go +++ b/tui/meta.go @@ -29,6 +29,10 @@ type meta struct { // ShowThinking 记忆是否把模型思考(reasoning_content)暗显进对话流(/thinking 切换)。默认关。 ShowThinking bool `json:"show_thinking,omitempty"` + // DefaultModel 默认起手模型("auto"/"flash"/"pro"),语义同 /model:auto=关键词路由, + // flash/pro=定死该模型。仅当会话从未用 /model 锁定过时生效;空 = auto(现有行为)。 + DefaultModel string `json:"default_model,omitempty"` + // MousePassthrough 记忆鼠标穿透模式(F2 切换):开启后关掉鼠标捕获,由终端原生处理 // 文字选择与右键粘贴。需要它的用户(WSL2 / 习惯终端原生选择)是一直需要,故重启保持。 MousePassthrough bool `json:"mouse_passthrough,omitempty"` @@ -57,6 +61,16 @@ func webHost() string { func webPort() int { return metaGet().WebPort } +// defaultModel 返回 meta.json 配置的默认起手模型(auto/flash/pro),空或非法回退 "auto"。 +// 语义与 /model 一致;只影响「会话从未锁定过模型」的初始起手,不覆盖用户的 /model 锁定。 +func defaultModel() string { + switch m := metaGet().DefaultModel; m { + case "flash", "pro": + return m + } + return "auto" +} + // modelCaps 是单个模型探测出的能力位,按维度独立存。 type modelCaps struct { Vision bool `json:"vision"` diff --git a/tui/meta_test.go b/tui/meta_test.go new file mode 100644 index 0000000..5853207 --- /dev/null +++ b/tui/meta_test.go @@ -0,0 +1,86 @@ +package tui + +import ( + "testing" + + "deepx/agent" + "deepx/tools" +) + +// TestDefaultModel 验证 meta.json 的 default_model 归一化:空/非法回退 auto, +// flash/pro 原样返回。语义与 /model 一致。 +func TestDefaultModel(t *testing.T) { + t.Setenv("HOME", t.TempDir()) // 写临时 ~/.deepx/meta.json,不碰真实配置 + cases := []struct { + in string + want string + }{ + {"", "auto"}, + {"auto", "auto"}, + {"flash", "flash"}, + {"pro", "pro"}, + {"bogus", "auto"}, + {"PRO", "auto"}, // 大小写敏感,非法回退 auto + } + for _, c := range cases { + metaUpdate(func(m *meta) { m.DefaultModel = c.in }) + if got := defaultModel(); got != c.want { + t.Errorf("defaultModel(%q) = %q, want %q", c.in, got, c.want) + } + } +} + +// TestRestoreModelPinDefaultMeta 验证 restoreModelPin 对空 pin 的回退逻辑: +// 用 meta.json 的 default_model 兜底起手模型,但显式保存的 /model 锁定优先。 +func TestRestoreModelPinDefaultMeta(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + cfg := agent.ModelConfig{ + Flash: agent.ModelEntry{Model: "flash-x"}, + Pro: agent.ModelEntry{Model: "pro-x"}, + } + newModel := func() *model { return &model{models: cfg} } + + // 空 pin + default_model=flash → 起手 flash + metaUpdate(func(m *meta) { m.DefaultModel = "flash" }) + m := newModel() + m.restoreModelPin("") + if m.modelPin != tools.RoleFlash { + t.Errorf("空 pin + default flash: modelPin = %q, want %q", m.modelPin, tools.RoleFlash) + } + + // 空 pin + default_model=pro → 起手 pro + metaUpdate(func(m *meta) { m.DefaultModel = "pro" }) + m = newModel() + m.restoreModelPin("") + if m.modelPin != tools.RolePro { + t.Errorf("空 pin + default pro: modelPin = %q, want %q", m.modelPin, tools.RolePro) + } + + // 空 pin + default_model 为空 → 回退 auto(现有行为) + metaUpdate(func(m *meta) { m.DefaultModel = "" }) + m = newModel() + m.restoreModelPin("") + if m.modelPin != "auto" { + t.Errorf("空 pin + 无默认: modelPin = %q, want auto", m.modelPin) + } + + // 显式 /model 锁定优先于 default_model + metaUpdate(func(m *meta) { m.DefaultModel = "pro" }) + m = newModel() + m.restoreModelPin("auto") + if m.modelPin != "auto" { + t.Errorf("显式 auto: modelPin = %q, want auto", m.modelPin) + } + m.restoreModelPin(tools.RoleFlash) + if m.modelPin != tools.RoleFlash { + t.Errorf("显式 flash: modelPin = %q, want %q", m.modelPin, tools.RoleFlash) + } + + // default_model=flash 但 flash 未配置 → 回退 auto(与现有锁定逻辑一致) + metaUpdate(func(m *meta) { m.DefaultModel = "flash" }) + m2 := &model{models: agent.ModelConfig{Pro: agent.ModelEntry{Model: "pro-x"}}} + m2.restoreModelPin("") + if m2.modelPin != "auto" { + t.Errorf("default flash 未配置: modelPin = %q, want auto", m2.modelPin) + } +} diff --git a/tui/model.go b/tui/model.go index 5879573..4df9850 100644 --- a/tui/model.go +++ b/tui/model.go @@ -710,7 +710,7 @@ func initialModel(models agent.ModelConfig, needsSetup bool, version string, hub visionByModel: visionByModel, activeModelRole: role, activeModelID: activeID, - modelPin: "auto", + modelPin: defaultModel(), version: version, mode: agent.AgentMode_Auto, workingMode: agent.WorkingModeDefault, // 默认 kp;下方从 session 恢复 @@ -3848,6 +3848,11 @@ func (m *model) applyModelPin(arg string) { // flash/pro 且对应模型已配置 → 锁定并即时切右栏显示;否则回退 auto(右栏起手 flash, // flash 未配置则 pro)。供 initialModel 启动恢复与 loadCurrentConversation 切会话恢复共用。 func (m *model) restoreModelPin(pin string) { + // 会话从未锁定过(空)→ 回退 meta.json 的 default_model 作为默认起手模型; + // 显式存过 auto/flash/pro 则按原样恢复 —— 用户的 /model 选择优先于全局默认。 + if pin == "" { + pin = defaultModel() + } switch { case pin == tools.RoleFlash && m.models.Flash.Model != "": m.modelPin = tools.RoleFlash