优化代码

This commit is contained in:
hero920103 committed 2026-10-28 08:47:09 +08:00
1 parent 3553790475
commit eda7e8bc96
23 files changed
+3113 -342

No files matched your search

+186 -63
View File
@@ -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
View File
@@ -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)
}
}