Files
Local-Cursor/internal/backend/server/config/manager.go
FuDie0915 db73cf5a39 chore: import Local-Cursor v1.1.8 source snapshot
Includes the current working directory changes. Historical commits and release binaries are excluded to limit Gitea storage.
Source base: d7515973fbfd6dd2001fa156c483455b8081c7cb
2026-11-10 01:24:28 +08:00

257 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"context"
"fmt"
"log"
"strings"
"sync"
"sync/atomic"
"time"
legacyruntime "cursor/internal/runtime"
)
const configHotReloadMinInterval = 500 * time.Millisecond
type Manager struct {
store *Store
current atomic.Pointer[Config]
listenersMu sync.RWMutex
listeners []func(Config)
// saveMu 串行化所有 Save 路径,保证 read-modify-write(如 SaveLastAgentModelHash)
// 与并发的整份 Config 保存之间不会互相覆盖(后写者胜出)。
saveMu sync.Mutex
reloadMu sync.Mutex
snapshot fileSnapshot
lastReload time.Time
reloadError string
}
func NewManager(ctx context.Context, store *Store) (*Manager, error) {
if store == nil {
return nil, fmt.Errorf("config store is required")
}
cfg, err := store.Load(ctx)
if err != nil {
return nil, err
}
manager := &Manager{
store: store,
snapshot: store.snapshot(),
}
manager.setCurrent(cfg)
return manager, nil
}
func (manager *Manager) Current() Config {
if manager == nil {
return DefaultConfig()
}
manager.reloadIfChanged(context.Background())
return manager.currentConfig()
}
func (manager *Manager) currentConfig() Config {
if manager == nil {
return DefaultConfig()
}
if current := manager.current.Load(); current != nil {
return *current
}
return DefaultConfig()
}
func (manager *Manager) Load(ctx context.Context) (Config, error) {
if manager == nil {
return DefaultConfig(), nil
}
manager.reloadIfChanged(ctx)
return manager.currentConfig(), nil
}
func (manager *Manager) Save(ctx context.Context, cfg Config) (Config, error) {
if manager == nil || manager.store == nil {
return Config{}, fmt.Errorf("config manager is not initialized")
}
manager.saveMu.Lock()
defer manager.saveMu.Unlock()
normalized, err := manager.store.Save(ctx, cfg)
if err != nil {
return Config{}, err
}
manager.setCurrent(normalized)
manager.reloadMu.Lock()
manager.snapshot = manager.store.snapshot()
manager.lastReload = time.Now()
manager.reloadError = ""
manager.reloadMu.Unlock()
manager.notify(normalized)
return normalized, nil
}
func (manager *Manager) LastAgentModelHash() string {
if manager == nil {
return ""
}
return strings.TrimSpace(manager.Current().LastAgentModelHash)
}
func (manager *Manager) SaveLastAgentModelHash(ctx context.Context, value string) error {
if manager == nil {
return fmt.Errorf("config manager is not initialized")
}
normalizedValue := strings.TrimSpace(value)
// 在 saveMu 保护下完成 read-modify-write,避免与并发 Save 互相覆盖。
manager.saveMu.Lock()
defer manager.saveMu.Unlock()
current := manager.currentConfig()
if strings.TrimSpace(current.LastAgentModelHash) == normalizedValue {
return nil
}
current.LastAgentModelHash = normalizedValue
normalized, err := manager.store.Save(ctx, current)
if err != nil {
return err
}
manager.setCurrent(normalized)
manager.reloadMu.Lock()
manager.snapshot = manager.store.snapshot()
manager.lastReload = time.Now()
manager.reloadError = ""
manager.reloadMu.Unlock()
manager.notify(normalized)
return nil
}
func (manager *Manager) ProviderStreamIdleTimeout(ctx context.Context) time.Duration {
if manager == nil {
return time.Duration(DefaultProviderStreamIdleTimeoutSeconds) * time.Second
}
manager.reloadIfChanged(ctx)
seconds := normalizeProviderStreamIdleTimeout(manager.currentConfig().ProviderStreamIdleTimeout)
return time.Duration(seconds) * time.Second
}
func (manager *Manager) IsObservabilityLogEnabled(ctx context.Context) bool {
if manager == nil {
return false
}
manager.reloadIfChanged(ctx)
return manager.currentConfig().Log
}
func (manager *Manager) Subscribe(listener func(Config)) func() {
if manager == nil || listener == nil {
return func() {}
}
manager.listenersMu.Lock()
manager.listeners = append(manager.listeners, listener)
index := len(manager.listeners) - 1
manager.listenersMu.Unlock()
return func() {
manager.listenersMu.Lock()
defer manager.listenersMu.Unlock()
if index < 0 || index >= len(manager.listeners) {
return
}
manager.listeners[index] = nil
}
}
func (manager *Manager) LegacyRuntimeSnapshot(_ context.Context) (legacyruntime.RuntimeConfigSnapshot, error) {
cfg := manager.Current()
adapters := make([]legacyruntime.ModelAdapterConfig, 0, len(cfg.ModelAdapters))
for _, item := range cfg.ModelAdapters {
adapters = append(adapters, legacyruntime.ModelAdapterConfig{
ID: item.ID,
DisplayName: item.DisplayName,
Type: item.Type,
BaseURL: item.BaseURL,
APIKey: item.APIKey,
TooltipData: item.TooltipData,
ModelID: item.ModelID,
ReasoningEffort: item.ReasoningEffort,
OpenAIEndpoint: item.OpenAIEndpoint,
OpenAIExtraParamsEnabled: item.OpenAIExtraParamsEnabled,
OpenAIExtraParamsJSON: item.OpenAIExtraParamsJSON,
ContextWindowTokens: item.ContextWindowTokens,
MaxCompletionTokens: item.MaxCompletionTokens,
AnthropicMaxTokens: item.AnthropicMaxTokens,
AnthropicThinkingEffort: item.AnthropicThinkingEffort,
ThinkingBudgetTokens: item.ThinkingBudgetTokens,
})
}
return legacyruntime.RuntimeConfigSnapshot{
ObservabilityLogEnabled: cfg.Log,
ProviderStreamIdleTimeout: cfg.ProviderStreamIdleTimeout,
ModelAdapters: adapters,
}, nil
}
func (manager *Manager) RouteMode(hasUpstreamURL bool) string {
if !hasUpstreamURL {
return DefaultRoutingMode
}
if manager == nil {
return DefaultRoutingMode
}
mode := normalizeRoutingMode(manager.Current().Routing.Mode)
if mode == "" {
return DefaultRoutingMode
}
return mode
}
func (manager *Manager) setCurrent(cfg Config) {
next := cfg
manager.current.Store(&next)
}
func (manager *Manager) reloadIfChanged(ctx context.Context) {
if manager == nil || manager.store == nil {
return
}
if ctx == nil {
ctx = context.Background()
}
now := time.Now()
manager.reloadMu.Lock()
if !manager.lastReload.IsZero() && now.Sub(manager.lastReload) < configHotReloadMinInterval {
manager.reloadMu.Unlock()
return
}
manager.lastReload = now
nextSnapshot := manager.store.snapshot()
if nextSnapshot == manager.snapshot {
manager.reloadMu.Unlock()
return
}
cfg, err := manager.store.Load(ctx)
if err != nil {
errText := err.Error()
if errText != manager.reloadError {
log.Printf("config hot reload skipped path=%s error=%v", manager.store.Path(), err)
manager.reloadError = errText
}
manager.reloadMu.Unlock()
return
}
manager.snapshot = nextSnapshot
manager.reloadError = ""
manager.setCurrent(cfg)
manager.reloadMu.Unlock()
manager.notify(cfg)
}
func (manager *Manager) notify(cfg Config) {
manager.listenersMu.RLock()
listeners := append([]func(Config){}, manager.listeners...)
manager.listenersMu.RUnlock()
for _, listener := range listeners {
if listener != nil {
listener(cfg)
}
}
}