优化代码

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

+76 -28
View File
@@ -30,8 +30,9 @@ type State struct {
TotalTokens int
LastLatency time.Duration
Region capture.Region
Model string
Profile string
Model string // 生效接入点里的模型名
Profile string // 生效接入点的名字
Format string // 生效接入点的接口格式
}
// Sink 是引擎向界面推送事件的回调集合。
@@ -39,7 +40,7 @@ type State struct {
type Sink interface {
OnUserMessage(text string, withImage bool)
OnAssistantStart(auto bool)
OnAssistantDelta(delta string)
OnAssistantDelta(delta llm.Delta)
OnAssistantEnd(usage llm.Usage, err error)
OnState(state State)
}
@@ -72,10 +73,12 @@ func New(cfg *config.Config, store *config.Store) *Engine {
store: store,
region: cfg.Region,
}
if prov, err := llm.New(toOptions(cfg.Active())); err != nil {
slog.Warn("模型接入点不可用", "err", err)
} else {
e.provider = prov
if active, ok := cfg.ActiveModel(); ok {
if prov, err := llm.New(toOptions(active)); err != nil {
slog.Warn("模型不可用", "err", err)
} else {
e.provider = prov
}
}
return e
}
@@ -83,6 +86,7 @@ func New(cfg *config.Config, store *config.Store) *Engine {
func toOptions(p config.Profile) llm.Options {
return llm.Options{
Name: p.Name,
Format: p.Format,
BaseURL: p.BaseURL,
APIKey: p.APIKey,
Model: p.Model,
@@ -120,7 +124,7 @@ func (e *Engine) State() State {
func (e *Engine) stateLocked() State {
active := config.Profile{}
if e.cfg != nil {
active = e.cfg.Active()
active, _ = e.cfg.ActiveModel()
}
return State{
Watching: e.watching,
@@ -131,6 +135,7 @@ func (e *Engine) stateLocked() State {
Region: e.region,
Model: active.Model,
Profile: active.Name,
Format: active.Format,
}
}
@@ -169,16 +174,28 @@ func (e *Engine) SetRegion(r capture.Region) error {
}
// ApplyConfig 在设置页保存后刷新引擎内部状态。
// 接入点还没填完整时不算失败:配置照样生效,等用户补齐后自动恢复。
func (e *Engine) ApplyConfig(cfg *config.Config) error {
prov, err := llm.New(toOptions(cfg.Active()))
if err != nil {
return err
if cfg == nil {
return errors.New("配置为空")
}
e.mu.Lock()
wasWatching := e.watching
// 区域只由框选修改,不被设置页的旧副本覆盖。
cfg.Region = e.region
e.cfg = cfg
e.mu.Unlock()
var prov llm.Provider
if active, ok := cfg.ActiveModel(); ok {
p, err := llm.New(toOptions(active))
if err != nil {
slog.Warn("模型初始化失败", "err", err)
} else {
prov = p
}
}
e.mu.Lock()
e.provider = prov
e.mu.Unlock()
@@ -192,8 +209,8 @@ func (e *Engine) ApplyConfig(cfg *config.Config) error {
return nil
}
// PingProfile 用给定接入点做一次连通性测试。
func (e *Engine) PingProfile(ctx context.Context, p config.Profile) error {
// PingModel 用给定的模型配置做一次连通性测试。
func (e *Engine) PingModel(ctx context.Context, p config.Profile) error {
prov, err := llm.New(toOptions(p))
if err != nil {
return err
@@ -201,6 +218,43 @@ func (e *Engine) PingProfile(ctx context.Context, p config.Profile) error {
return prov.Ping(ctx)
}
// PingActive 用当前生效的接入点做一次连通性测试。
func (e *Engine) PingActive(ctx context.Context) error {
e.mu.Lock()
p, _ := e.cfg.ActiveModel()
e.mu.Unlock()
return e.PingModel(ctx, p)
}
// EnsureReady 检查模型是否已经接好,供监听前预检。
func (e *Engine) EnsureReady() error {
e.mu.Lock()
m, ok := e.cfg.ActiveModel()
ready := e.provider != nil
e.mu.Unlock()
if !ok {
return errors.New("还没有接入模型,请到设置里添加接入点并填写 BaseURL 与模型名")
}
if m.BaseURL == "" || m.Model == "" {
return errors.New("还没有接入模型,请到设置里填写 BaseURL 与模型名")
}
if m.APIKey == "" && !isLocalEndpoint(m.BaseURL) {
return errors.New("还没有接入模型,请到设置里填写 API Key")
}
if !ready {
return errors.New("模型初始化失败,请到设置里检查 BaseURL 与模型名")
}
return nil
}
// isLocalEndpoint 判断是不是本机服务,本机服务通常不需要 API Key。
func isLocalEndpoint(baseURL string) bool {
return strings.Contains(baseURL, "localhost") ||
strings.Contains(baseURL, "127.0.0.1") ||
strings.Contains(baseURL, "[::1]")
}
// Snapshot 抓取当前区域的 JPEG 预览(用于界面缩略图)。
func (e *Engine) Snapshot() ([]byte, error) {
e.mu.Lock()
@@ -229,6 +283,9 @@ func (e *Engine) Ask(text string, attach bool) error {
// StartWatch 开始按帧差自动监听。
func (e *Engine) StartWatch() error {
if err := e.EnsureReady(); err != nil {
return err
}
e.mu.Lock()
if e.watching {
e.mu.Unlock()
@@ -264,18 +321,6 @@ func (e *Engine) StopWatch() {
e.emitState()
}
// ToggleWatch 在开始/停止之间切换。
func (e *Engine) ToggleWatch() error {
e.mu.Lock()
on := e.watching
e.mu.Unlock()
if on {
e.StopWatch()
return nil
}
return e.StartWatch()
}
// watchLoop 以低频抓屏 + 帧差判断是否需要提问。
func (e *Engine) watchLoop(stop chan struct{}) {
e.mu.Lock()
@@ -378,7 +423,7 @@ func (e *Engine) run(text string, attach, auto bool) error {
}
if e.provider == nil {
e.mu.Unlock()
return errors.New("还没有可用的模型接入点,请到设置里填写")
return errors.New("还没有接入模型,请到设置里填写")
}
e.busy = true
prov := e.provider
@@ -427,8 +472,11 @@ func (e *Engine) run(text string, attach, auto bool) error {
var reply strings.Builder
start := time.Now()
usage, err := prov.Chat(ctx, system, msgs, func(delta string) error {
reply.WriteString(delta)
usage, err := prov.Chat(ctx, system, msgs, func(delta llm.Delta) error {
// 思考过程只往界面送,不进历史:历史里留正文就够了。
if delta.Text != "" {
reply.WriteString(delta.Text)
}
if sink != nil {
sink.OnAssistantDelta(delta)
}
+83
View File
@@ -0,0 +1,83 @@
package agent
import (
"testing"
"viewsagent/internal/capture"
"viewsagent/internal/config"
"viewsagent/internal/llm"
)
// readyCfg 造一份只差一个接入点的配置。
func readyCfg(p config.Profile) *config.Config {
cfg := &config.Config{Models: []config.Profile{p}}
cfg.Active = p.Name
cfg.Normalize()
return cfg
}
func TestEnsureReadyRejectsIncompleteModel(t *testing.T) {
e := New(&config.Config{Models: []config.Profile{{Name: "空"}}}, nil)
if err := e.EnsureReady(); err == nil {
t.Fatal("模型未配置时应拒绝监听")
}
}
func TestEnsureReadyRequiresAPIKey(t *testing.T) {
e := New(readyCfg(config.Profile{
Name: "远程", BaseURL: "https://api.example.com/v1", Model: "vl-model",
}), nil)
if err := e.EnsureReady(); err == nil {
t.Fatal("远程服务缺少 API Key 时应拒绝监听")
}
}
func TestEnsureReadyAllowsLocalWithoutKey(t *testing.T) {
e := New(readyCfg(config.Profile{
Name: "本地", BaseURL: "http://localhost:11434/v1", Model: "qwen2.5vl:7b",
}), nil)
if err := e.EnsureReady(); err != nil {
t.Fatalf("本地服务不应要求 API Key: %v", err)
}
}
func TestEnsureReadyPassesWithKey(t *testing.T) {
e := New(readyCfg(config.Profile{
Name: "远程", BaseURL: "https://api.example.com/v1", Model: "vl-model", APIKey: "sk-test",
}), nil)
if err := e.EnsureReady(); err != nil {
t.Fatalf("配置完整时应通过检测: %v", err)
}
}
func TestStartWatchRefusesWhenModelNotReady(t *testing.T) {
e := New(readyCfg(config.Profile{
Name: "远程", BaseURL: "https://api.example.com/v1", Model: "vl-model",
}), nil)
e.region = capture.Region{X: 0, Y: 0, W: 100, H: 100}
if err := e.StartWatch(); err == nil {
t.Fatal("模型未接好时不应开始监听")
}
if e.State().Watching {
t.Fatal("监听不应被启动")
}
}
func TestActiveProfileIsUsed(t *testing.T) {
cfg := &config.Config{Models: []config.Profile{
{Name: "甲", Format: llm.FormatOpenAI, BaseURL: "https://a.example/v1", Model: "a-vl", APIKey: "sk-a"},
{Name: "乙", Format: llm.FormatAnthropic, BaseURL: "https://b.example", Model: "b-vl", APIKey: "sk-b"},
}}
cfg.Active = "乙"
cfg.Normalize()
e := New(cfg, nil)
st := e.State()
if st.Profile != "乙" || st.Model != "b-vl" {
t.Fatalf("状态应反映生效接入点: %+v", st)
}
if st.Format != llm.FormatAnthropic {
t.Fatalf("状态应带出接口格式: %s", st.Format)
}
}