Files
Local-Cursor/internal/backend/server/config/manager.go
FuDie0915 188225fae6 Ð޸´ȫÏîĿÉó²éÎÊÌⲢ·¢²¼ v1.1.9
¸ÄÓñ¾»ú¶ÀÁ¢ CA£¬»ָ´ CLI TLS УÑ飬±£»¤ƾ¾ÝÓëÅäÖò¢·¢±£´档
Ð޸´ Cursor Êý¾ݡ¢»Ự·¾¶¡¢´úÀíÉúÃüÖÜÆڡ¢ģÐÍÇëÇóÓë Skills Îļþ²Ù×÷¡£
Ϊ¶ÀÁ¢ Tab ·þÎñÔö¼ӿͻ§¶˼øȨ£¬Éý¼¶ÒÀÀµÓ빤¾ßÁ´£¬ÍêÉƷ¢²¼ԪÐÅϢͬ²½¡£

Ñé֤£ºÁ½¸ö Go ģ¿éµIJâÊÔ/vet¡¢¹ؼüģ¿é race¡¢ǰ¶˲âÊÔ/Éú²ú¹¹½¨/Éó¼ơ¢
Go ¿ɴï©¶´ɨÃèÓë macOS/Linux ¿ÉÐÐģ¿齻²æ±àÒë¾ùͨ¹ý¡£
Windows ¸ôÀëÆô¶¯ͨ¹ý£»δÍê³ɵĵã»÷ðÑ̺ÍԭÉúƽ̨Ñé֤¼ûÉó²鱨¸档
2026-11-10 09:59:34 +08:00

281 lines
7.4 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 cloneConfig(*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()
locked := true
defer func() {
if locked {
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.saveMu.Unlock()
locked = false
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()
locked := true
defer func() {
if locked {
manager.saveMu.Unlock()
}
}()
current := manager.currentConfig()
if strings.TrimSpace(current.LastAgentModelHash) == normalizedValue {
return nil
}
normalized, err := manager.store.updateLastAgentModelHash(normalizedValue)
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.saveMu.Unlock()
locked = false
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,
CustomHeadersEnabled: item.CustomHeadersEnabled, CustomHeadersJSON: item.CustomHeadersJSON,
AnthropicExtraParamsEnabled: item.AnthropicExtraParamsEnabled, AnthropicExtraParamsJSON: item.AnthropicExtraParamsJSON,
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 := cloneConfig(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()
}
manager.saveMu.Lock()
reloadSaveLocked := true
defer func() {
if reloadSaveLocked {
manager.saveMu.Unlock()
}
}()
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.saveMu.Unlock()
reloadSaveLocked = false
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(cloneConfig(cfg))
}
}
}