优化代码
This commit is contained in:
1 parent
3553790475
commit
eda7e8bc96
23 files changed
+3113
-342
No files matched your search
+186
-63
@@ -9,11 +9,14 @@ import (
|
||||
"strings"
|
||||
|
||||
"viewsagent/internal/capture"
|
||||
"viewsagent/internal/llm"
|
||||
)
|
||||
|
||||
// Profile 是一个 OpenAI 兼容协议的模型接入点。
|
||||
// Profile 描述一份模型接入配置。Name 是用户自己起的名字,
|
||||
// 既是左侧列表的显示文本,也是 Config.Active 引用的键。
|
||||
type Profile struct {
|
||||
Name string `json:"name"`
|
||||
Format string `json:"format"`
|
||||
BaseURL string `json:"baseURL"`
|
||||
APIKey string `json:"apiKey"`
|
||||
Model string `json:"model"`
|
||||
@@ -22,6 +25,20 @@ type Profile struct {
|
||||
Temperature float64 `json:"temperature"`
|
||||
}
|
||||
|
||||
// NewProfile 按接口格式生成一份空白模型配置,只带好该格式必需的采样默认值。
|
||||
// BaseURL 与模型名留空,由用户按自己用的服务商填写。
|
||||
func NewProfile(name, format string) Profile {
|
||||
if format != llm.FormatAnthropic {
|
||||
format = llm.FormatOpenAI
|
||||
}
|
||||
return Profile{
|
||||
Name: name,
|
||||
Format: format,
|
||||
MaxTokens: 1024,
|
||||
Temperature: 0.3,
|
||||
}
|
||||
}
|
||||
|
||||
// Capture 是抓屏与触发策略参数。
|
||||
type Capture struct {
|
||||
FPS float64 `json:"fps"` // 监听帧率,1~5
|
||||
@@ -48,8 +65,11 @@ type Template struct {
|
||||
|
||||
// Config 是完整的应用配置。
|
||||
type Config struct {
|
||||
ActiveProfile string `json:"activeProfile"`
|
||||
Profiles []Profile `json:"profiles"`
|
||||
// Models 是全部已保存的模型配置,Active 指向其中生效的那一条。
|
||||
Models []Profile `json:"models"`
|
||||
Active string `json:"active"`
|
||||
// Theme 是界面主题:system 跟随系统、contrast 黑白高对比、light 浅色高对比。
|
||||
Theme string `json:"theme"`
|
||||
Capture Capture `json:"capture"`
|
||||
Hotkeys Hotkeys `json:"hotkeys"`
|
||||
PromptTemplate string `json:"promptTemplate"`
|
||||
@@ -113,32 +133,11 @@ func DefaultTemplates() []Template {
|
||||
}
|
||||
}
|
||||
|
||||
// Default 返回一份可直接使用的默认配置。
|
||||
// Default 返回一份默认配置。
|
||||
// Models 故意留空:由 Normalize 补一个空白条目,这样旧配置文件反序列化后
|
||||
// 仍能通过 len(Models)==0 判断出走迁移分支。
|
||||
func Default() *Config {
|
||||
return &Config{
|
||||
ActiveProfile: "qwen",
|
||||
Profiles: []Profile{
|
||||
{
|
||||
Name: "qwen", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
Model: "qwen3-vl-plus", MaxTokens: 1024, Temperature: 0.3,
|
||||
},
|
||||
{
|
||||
Name: "doubao", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
|
||||
Model: "doubao-seed-1-6-vision", MaxTokens: 1024, Temperature: 0.3,
|
||||
},
|
||||
{
|
||||
Name: "zhipu", BaseURL: "https://open.bigmodel.cn/api/paas/v4",
|
||||
Model: "glm-4.6v", MaxTokens: 1024, Temperature: 0.3,
|
||||
},
|
||||
{
|
||||
Name: "kimi", BaseURL: "https://api.moonshot.cn/v1",
|
||||
Model: "kimi-latest", MaxTokens: 1024, Temperature: 0.3,
|
||||
},
|
||||
{
|
||||
Name: "ollama", BaseURL: "http://localhost:11434/v1",
|
||||
Model: "qwen2.5vl:7b", MaxTokens: 1024, Temperature: 0.3,
|
||||
},
|
||||
},
|
||||
Capture: Capture{
|
||||
FPS: 2, MaxEdge: 1280, JPEGQuality: 75,
|
||||
DiffThreshold: 5, CooldownMs: 3000, StableFrames: 2, HistoryTurns: 6,
|
||||
@@ -153,25 +152,85 @@ func Default() *Config {
|
||||
}
|
||||
}
|
||||
|
||||
// Profile 按名称查找接入点。
|
||||
func (c *Config) Profile(name string) (Profile, bool) {
|
||||
for _, p := range c.Profiles {
|
||||
if p.Name == name {
|
||||
return p, true
|
||||
// ActiveModel 返回当前生效的模型配置。
|
||||
func (c *Config) ActiveModel() (Profile, bool) {
|
||||
for i := range c.Models {
|
||||
if c.Models[i].Name == c.Active {
|
||||
return c.Models[i], true
|
||||
}
|
||||
}
|
||||
return Profile{}, false
|
||||
}
|
||||
|
||||
// Active 返回当前启用的接入点,找不到时回退到第一个。
|
||||
func (c *Config) Active() Profile {
|
||||
if p, ok := c.Profile(c.ActiveProfile); ok {
|
||||
return p
|
||||
// ModelNames 返回全部模型名字,顺序即列表顺序。
|
||||
func (c *Config) ModelNames() []string {
|
||||
names := make([]string, 0, len(c.Models))
|
||||
for _, m := range c.Models {
|
||||
names = append(names, m.Name)
|
||||
}
|
||||
if len(c.Profiles) > 0 {
|
||||
return c.Profiles[0]
|
||||
return names
|
||||
}
|
||||
|
||||
// ProfileIndex 按名字返回接入点在列表里的下标,找不到返回 -1。
|
||||
func (c *Config) ProfileIndex(name string) int {
|
||||
for i := range c.Models {
|
||||
if c.Models[i].Name == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return Profile{}
|
||||
return -1
|
||||
}
|
||||
|
||||
// SetActive 把指定接入点设为生效项;名字不存在时返回 false。
|
||||
func (c *Config) SetActive(name string) bool {
|
||||
if c.ProfileIndex(name) < 0 {
|
||||
return false
|
||||
}
|
||||
c.Active = name
|
||||
return true
|
||||
}
|
||||
|
||||
// AddProfile 追加一份接入点,名字为空或冲突时自动改名,并返回真正落库的副本。
|
||||
func (c *Config) AddProfile(p Profile) Profile {
|
||||
p.Name = strings.TrimSpace(p.Name)
|
||||
if p.Name == "" {
|
||||
p.Name = "接入点"
|
||||
}
|
||||
p.Name = c.UniqueName(p.Name)
|
||||
c.Models = append(c.Models, p)
|
||||
return p
|
||||
}
|
||||
|
||||
// UniqueName 基于前缀生成一个在现有模型里不重复的名字。
|
||||
func (c *Config) UniqueName(prefix string) string {
|
||||
used := make(map[string]bool, len(c.Models))
|
||||
for _, m := range c.Models {
|
||||
used[m.Name] = true
|
||||
}
|
||||
if !used[prefix] {
|
||||
return prefix
|
||||
}
|
||||
for n := 2; ; n++ {
|
||||
name := fmt.Sprintf("%s (%d)", prefix, n)
|
||||
if !used[name] {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveModel 删除指定名字的模型;删掉生效那条时把 Active 置空,由 Normalize 重新兜底。
|
||||
func (c *Config) RemoveModel(name string) bool {
|
||||
for i := range c.Models {
|
||||
if c.Models[i].Name != name {
|
||||
continue
|
||||
}
|
||||
c.Models = append(c.Models[:i], c.Models[i+1:]...)
|
||||
if c.Active == name {
|
||||
c.Active = ""
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Template 按 key 查找提示词模板。
|
||||
@@ -190,44 +249,66 @@ func (c *Config) Template(key string) Template {
|
||||
// Clone 返回一份深拷贝,避免设置窗口与引擎共享同一份可变配置。
|
||||
func (c *Config) Clone() *Config {
|
||||
if c == nil {
|
||||
return Default()
|
||||
cfg := Default()
|
||||
cfg.Normalize()
|
||||
return cfg
|
||||
}
|
||||
out := *c
|
||||
out.Profiles = append([]Profile(nil), c.Profiles...)
|
||||
for i := range out.Profiles {
|
||||
if c.Profiles[i].ExtraHeaders != nil {
|
||||
h := make(map[string]string, len(c.Profiles[i].ExtraHeaders))
|
||||
for k, v := range c.Profiles[i].ExtraHeaders {
|
||||
out.Templates = append([]Template(nil), c.Templates...)
|
||||
out.Models = make([]Profile, len(c.Models))
|
||||
for i, m := range c.Models {
|
||||
out.Models[i] = m
|
||||
if m.ExtraHeaders != nil {
|
||||
h := make(map[string]string, len(m.ExtraHeaders))
|
||||
for k, v := range m.ExtraHeaders {
|
||||
h[k] = v
|
||||
}
|
||||
out.Profiles[i].ExtraHeaders = h
|
||||
out.Models[i].ExtraHeaders = h
|
||||
}
|
||||
}
|
||||
out.Templates = append([]Template(nil), c.Templates...)
|
||||
return &out
|
||||
}
|
||||
|
||||
// Normalize 补全缺失字段,保证运行期不会出现零值。
|
||||
func (c *Config) Normalize() {
|
||||
def := Default()
|
||||
if len(c.Profiles) == 0 {
|
||||
c.Profiles = def.Profiles
|
||||
if len(c.Models) == 0 {
|
||||
c.Models = []Profile{NewProfile("接入点 1", llm.FormatOpenAI)}
|
||||
}
|
||||
if c.ActiveProfile == "" {
|
||||
c.ActiveProfile = c.Profiles[0].Name
|
||||
}
|
||||
for i := range c.Profiles {
|
||||
p := &c.Profiles[i]
|
||||
if p.MaxTokens <= 0 {
|
||||
p.MaxTokens = 1024
|
||||
for i := range c.Models {
|
||||
m := &c.Models[i]
|
||||
m.Name = strings.TrimSpace(m.Name)
|
||||
if m.Name == "" {
|
||||
m.Name = fmt.Sprintf("接入点 %d", i+1)
|
||||
}
|
||||
if p.Temperature <= 0 {
|
||||
p.Temperature = 0.3
|
||||
if m.Format != llm.FormatAnthropic {
|
||||
m.Format = llm.FormatOpenAI
|
||||
}
|
||||
m.BaseURL = strings.TrimRight(strings.TrimSpace(m.BaseURL), "/")
|
||||
m.Model = strings.TrimSpace(m.Model)
|
||||
if m.MaxTokens <= 0 {
|
||||
m.MaxTokens = 1024
|
||||
}
|
||||
if m.Temperature <= 0 {
|
||||
m.Temperature = 0.3
|
||||
}
|
||||
p.BaseURL = strings.TrimRight(strings.TrimSpace(p.BaseURL), "/")
|
||||
}
|
||||
// 名字必须唯一,否则 Active 无法定位到确定的那一条。
|
||||
used := make(map[string]bool, len(c.Models))
|
||||
for i := range c.Models {
|
||||
base := c.Models[i].Name
|
||||
name := base
|
||||
for n := 2; used[name]; n++ {
|
||||
name = fmt.Sprintf("%s (%d)", base, n)
|
||||
}
|
||||
c.Models[i].Name = name
|
||||
used[name] = true
|
||||
}
|
||||
if _, ok := c.ActiveModel(); !ok {
|
||||
c.Active = c.Models[0].Name
|
||||
}
|
||||
|
||||
if len(c.Templates) == 0 {
|
||||
c.Templates = def.Templates
|
||||
c.Templates = DefaultTemplates()
|
||||
}
|
||||
if c.Capture.FPS <= 0 {
|
||||
c.Capture.FPS = 2
|
||||
@@ -251,10 +332,15 @@ func (c *Config) Normalize() {
|
||||
c.Capture.HistoryTurns = 6
|
||||
}
|
||||
if c.Hotkeys.SelectRegion == "" {
|
||||
c.Hotkeys.SelectRegion = def.Hotkeys.SelectRegion
|
||||
c.Hotkeys.SelectRegion = "Ctrl+Alt+X"
|
||||
}
|
||||
switch c.Theme {
|
||||
case "contrast", "light":
|
||||
default:
|
||||
c.Theme = "system"
|
||||
}
|
||||
if c.Hotkeys.ToggleWatch == "" {
|
||||
c.Hotkeys.ToggleWatch = def.Hotkeys.ToggleWatch
|
||||
c.Hotkeys.ToggleWatch = "Ctrl+Alt+Space"
|
||||
}
|
||||
if c.PromptTemplate == "" {
|
||||
c.PromptTemplate = c.Templates[0].Key
|
||||
@@ -300,11 +386,48 @@ func (s *Store) Load() *Config {
|
||||
fresh.Normalize()
|
||||
return fresh
|
||||
}
|
||||
var legacy legacyConfig
|
||||
_ = json.Unmarshal(raw, &legacy)
|
||||
cfg.migrateLegacy(legacy)
|
||||
cfg.Normalize()
|
||||
cfg.migrate()
|
||||
return cfg
|
||||
}
|
||||
|
||||
// legacyConfig 是历史版本的两种配置形状,仅用于迁移。
|
||||
type legacyConfig struct {
|
||||
// 上一版:单份模型 + 厂商 key。
|
||||
Vendor string `json:"vendor"`
|
||||
Model Profile `json:"model"`
|
||||
// 更早版本:多接入点。
|
||||
ActiveProfile string `json:"activeProfile"`
|
||||
Profiles []Profile `json:"profiles"`
|
||||
}
|
||||
|
||||
// migrateLegacy 把旧版配置折叠成 Models 列表。
|
||||
// 旧格式一律是 OpenAI 兼容,Anthropic 格式是这次才引入的。
|
||||
func (c *Config) migrateLegacy(l legacyConfig) {
|
||||
if len(c.Models) > 0 {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case len(l.Profiles) > 0:
|
||||
c.Models = append([]Profile(nil), l.Profiles...)
|
||||
c.Active = l.ActiveProfile
|
||||
case l.Model.BaseURL != "" || l.Model.Model != "":
|
||||
c.Models = []Profile{l.Model}
|
||||
c.Active = l.Model.Name
|
||||
if c.Active == "" {
|
||||
c.Active = l.Vendor
|
||||
}
|
||||
}
|
||||
for i := range c.Models {
|
||||
if c.Models[i].Format == "" {
|
||||
c.Models[i].Format = llm.FormatOpenAI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// migrate 把旧版本的默认值升级到当前默认值,避免老配置文件一直沿用已知冲突的热键。
|
||||
func (c *Config) migrate() {
|
||||
if strings.EqualFold(strings.TrimSpace(c.Hotkeys.SelectRegion), "Ctrl+Alt+Q") {
|
||||
|
||||
+183
-19
@@ -1,16 +1,23 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"viewsagent/internal/llm"
|
||||
)
|
||||
|
||||
func TestDefaultIsUsable(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.Normalize()
|
||||
|
||||
if len(cfg.Profiles) == 0 {
|
||||
t.Fatal("默认配置应包含接入点")
|
||||
if len(cfg.Models) == 0 {
|
||||
t.Fatal("Normalize 后应至少有一份接入点")
|
||||
}
|
||||
if cfg.Active().Name != "qwen" {
|
||||
t.Fatalf("默认接入点应为 qwen,实际 %s", cfg.Active().Name)
|
||||
if _, ok := cfg.ActiveModel(); !ok {
|
||||
t.Fatal("Normalize 后应有生效接入点")
|
||||
}
|
||||
if cfg.Capture.FPS <= 0 || cfg.Capture.MaxEdge <= 0 || cfg.Capture.CooldownMs <= 0 {
|
||||
t.Fatalf("抓屏参数不应为零值: %+v", cfg.Capture)
|
||||
@@ -27,8 +34,14 @@ func TestNormalizeFillsZeroValues(t *testing.T) {
|
||||
cfg := &Config{}
|
||||
cfg.Normalize()
|
||||
|
||||
if cfg.ActiveProfile == "" {
|
||||
t.Fatal("Normalize 应补全 ActiveProfile")
|
||||
if len(cfg.Models) == 0 {
|
||||
t.Fatal("Normalize 应补全接入点列表")
|
||||
}
|
||||
if cfg.Models[0].Format != llm.FormatOpenAI {
|
||||
t.Fatalf("默认接入点应为 OpenAI 兼容格式,实际 %s", cfg.Models[0].Format)
|
||||
}
|
||||
if cfg.Models[0].MaxTokens <= 0 || cfg.Models[0].Temperature <= 0 {
|
||||
t.Fatalf("Normalize 应补全采样默认值: %+v", cfg.Models[0])
|
||||
}
|
||||
if len(cfg.Templates) == 0 {
|
||||
t.Fatal("Normalize 应补全模板")
|
||||
@@ -38,17 +51,72 @@ func TestNormalizeFillsZeroValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeKeepsCustomModel(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.Models = []Profile{{
|
||||
Name: "我的网关", Format: llm.FormatAnthropic,
|
||||
BaseURL: "https://my-gateway.example.com", Model: "my-vl-model",
|
||||
}}
|
||||
cfg.Normalize()
|
||||
|
||||
p := cfg.Models[0]
|
||||
if p.BaseURL != "https://my-gateway.example.com" || p.Model != "my-vl-model" {
|
||||
t.Fatalf("Normalize 不应覆盖手填的模型配置: %+v", p)
|
||||
}
|
||||
if p.Format != llm.FormatAnthropic {
|
||||
t.Fatalf("Normalize 不应改写接口格式: %s", p.Format)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeForcesUniqueNames(t *testing.T) {
|
||||
cfg := &Config{Models: []Profile{
|
||||
{Name: "同名", Format: llm.FormatOpenAI},
|
||||
{Name: "同名", Format: llm.FormatOpenAI},
|
||||
}}
|
||||
cfg.Normalize()
|
||||
|
||||
if cfg.Models[0].Name == cfg.Models[1].Name {
|
||||
t.Fatalf("重名接入点应被自动改名: %+v", cfg.Models)
|
||||
}
|
||||
if _, ok := cfg.ActiveModel(); !ok {
|
||||
t.Fatal("改名后仍应有生效接入点")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeTheme(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"": "system",
|
||||
"system": "system",
|
||||
"contrast": "contrast",
|
||||
"light": "light",
|
||||
"dark": "system",
|
||||
"Contrast": "system",
|
||||
"nonsense": "system",
|
||||
}
|
||||
for in, want := range cases {
|
||||
cfg := &Config{Theme: in}
|
||||
cfg.Normalize()
|
||||
if cfg.Theme != want {
|
||||
t.Fatalf("主题 %q 应归一化为 %q,实际 %q", in, want, cfg.Theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneIsIndependent(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.Normalize()
|
||||
|
||||
cp := cfg.Clone()
|
||||
cp.Profiles[0].Model = "changed"
|
||||
cp.Models[0].Model = "changed"
|
||||
cp.Models[0].ExtraHeaders = map[string]string{"X-Test": "1"}
|
||||
cp.Templates[0].System = "changed"
|
||||
cp.Capture.FPS = 9
|
||||
|
||||
if cfg.Profiles[0].Model == "changed" {
|
||||
t.Fatal("Clone 后修改接入点不应影响原配置")
|
||||
if cfg.Models[0].Model == "changed" {
|
||||
t.Fatal("Clone 后修改模型不应影响原配置")
|
||||
}
|
||||
if len(cfg.Models[0].ExtraHeaders) != 0 {
|
||||
t.Fatal("Clone 后修改请求头不应影响原配置")
|
||||
}
|
||||
if cfg.Templates[0].System == "changed" {
|
||||
t.Fatal("Clone 后修改模板不应影响原配置")
|
||||
@@ -58,18 +126,114 @@ func TestCloneIsIndependent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileLookup(t *testing.T) {
|
||||
func TestAddProfileMakesUniqueName(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.Normalize()
|
||||
|
||||
if _, ok := cfg.Profile("qwen"); !ok {
|
||||
t.Fatal("应能找到 qwen 接入点")
|
||||
added := cfg.AddProfile(NewProfile("接入点", llm.FormatAnthropic))
|
||||
if cfg.ProfileIndex(added.Name) < 0 {
|
||||
t.Fatalf("新增的接入点应能在列表里找到: %+v", cfg.Models)
|
||||
}
|
||||
if _, ok := cfg.Profile("不存在"); ok {
|
||||
t.Fatal("不应找到不存在的接入点")
|
||||
}
|
||||
cfg.ActiveProfile = "不存在"
|
||||
if cfg.Active().Name != cfg.Profiles[0].Name {
|
||||
t.Fatal("接入点丢失时应回退到第一个")
|
||||
again := cfg.AddProfile(NewProfile("接入点", llm.FormatOpenAI))
|
||||
if again.Name == added.Name {
|
||||
t.Fatalf("重名接入点应自动改名: %s", again.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetActive(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.Models = []Profile{{Name: "a"}, {Name: "b"}}
|
||||
cfg.Normalize()
|
||||
|
||||
if !cfg.SetActive("b") {
|
||||
t.Fatal("应能切换到存在的接入点")
|
||||
}
|
||||
if p, ok := cfg.ActiveModel(); !ok || p.Name != "b" {
|
||||
t.Fatalf("生效接入点应为 b,实际 %+v", cfg)
|
||||
}
|
||||
if cfg.SetActive("不存在") {
|
||||
t.Fatal("不存在的接入点不应被设为生效")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveModelClearsActive(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.Models = []Profile{{Name: "a"}, {Name: "b"}}
|
||||
cfg.Active = "a"
|
||||
cfg.Normalize()
|
||||
|
||||
if !cfg.RemoveModel("a") {
|
||||
t.Fatal("应能删除接入点")
|
||||
}
|
||||
if cfg.Active != "" {
|
||||
t.Fatalf("删掉生效接入点后应清空 Active,实际 %s", cfg.Active)
|
||||
}
|
||||
if _, ok := cfg.ActiveModel(); ok {
|
||||
t.Fatal("删掉生效接入点后不应再能取到它")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateLegacyProfiles(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.migrateLegacy(legacyConfig{
|
||||
ActiveProfile: "doubao",
|
||||
Profiles: []Profile{
|
||||
{Name: "qwen", BaseURL: "https://qwen.example/v1", Model: "qwen-vl"},
|
||||
{Name: "doubao", BaseURL: "https://doubao.example/v1", Model: "doubao-vl", APIKey: "sk-old"},
|
||||
},
|
||||
})
|
||||
cfg.Normalize()
|
||||
|
||||
active, ok := cfg.ActiveModel()
|
||||
if !ok {
|
||||
t.Fatal("应迁移出原启用接入点")
|
||||
}
|
||||
if active.Model != "doubao-vl" {
|
||||
t.Fatalf("应迁移原启用接入点的模型,实际 %s", active.Model)
|
||||
}
|
||||
if active.APIKey != "sk-old" {
|
||||
t.Fatal("应迁移原 API Key")
|
||||
}
|
||||
if active.Format != llm.FormatOpenAI {
|
||||
t.Fatalf("旧格式一律按 OpenAI 兼容迁移,实际 %s", active.Format)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreLoadMigratesLegacyFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config.json")
|
||||
legacy := `{
|
||||
"activeProfile": "doubao",
|
||||
"profiles": [
|
||||
{"name": "qwen", "baseURL": "https://qwen.example/v1", "model": "qwen-vl"},
|
||||
{"name": "doubao", "baseURL": "https://doubao.example/v1", "model": "doubao-vl", "apiKey": "sk-legacy"}
|
||||
]
|
||||
}`
|
||||
if err := os.WriteFile(path, []byte(legacy), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := &Store{path: path}
|
||||
cfg := s.Load()
|
||||
|
||||
active, ok := cfg.ActiveModel()
|
||||
if !ok {
|
||||
t.Fatal("应迁移出生效接入点")
|
||||
}
|
||||
if active.Model != "doubao-vl" || active.APIKey != "sk-legacy" {
|
||||
t.Fatalf("应迁移原模型配置: %+v", active)
|
||||
}
|
||||
|
||||
if err := s.Save(cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(raw), "activeProfile") || strings.Contains(string(raw), "profiles") {
|
||||
t.Fatalf("保存后不应再写入旧接入点字段: %s", raw)
|
||||
}
|
||||
if !strings.Contains(string(raw), `"models"`) {
|
||||
t.Fatalf("保存后应写入新的接入点列表: %s", raw)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user