Ð޸´ȫÏîĿÉó²éÎÊÌⲢ·¢²¼ v1.1.9

¸ÄÓñ¾»ú¶ÀÁ¢ CA£¬»ָ´ CLI TLS УÑ飬±£»¤ƾ¾ÝÓëÅäÖò¢·¢±£´档
Ð޸´ Cursor Êý¾ݡ¢»Ự·¾¶¡¢´úÀíÉúÃüÖÜÆڡ¢ģÐÍÇëÇóÓë Skills Îļþ²Ù×÷¡£
Ϊ¶ÀÁ¢ Tab ·þÎñÔö¼ӿͻ§¶˼øȨ£¬Éý¼¶ÒÀÀµÓ빤¾ßÁ´£¬ÍêÉƷ¢²¼ԪÐÅϢͬ²½¡£

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

View File

@@ -57,7 +57,7 @@ func (manager *Manager) currentConfig() Config {
return DefaultConfig()
}
if current := manager.current.Load(); current != nil {
return *current
return cloneConfig(*current)
}
return DefaultConfig()
}
@@ -75,7 +75,12 @@ func (manager *Manager) Save(ctx context.Context, cfg Config) (Config, error) {
return Config{}, fmt.Errorf("config manager is not initialized")
}
manager.saveMu.Lock()
defer manager.saveMu.Unlock()
locked := true
defer func() {
if locked {
manager.saveMu.Unlock()
}
}()
normalized, err := manager.store.Save(ctx, cfg)
if err != nil {
return Config{}, err
@@ -86,6 +91,8 @@ func (manager *Manager) Save(ctx context.Context, cfg Config) (Config, error) {
manager.lastReload = time.Now()
manager.reloadError = ""
manager.reloadMu.Unlock()
manager.saveMu.Unlock()
locked = false
manager.notify(normalized)
return normalized, nil
}
@@ -104,13 +111,17 @@ func (manager *Manager) SaveLastAgentModelHash(ctx context.Context, value string
normalizedValue := strings.TrimSpace(value)
// 在 saveMu 保护下完成 read-modify-write,避免与并发 Save 互相覆盖。
manager.saveMu.Lock()
defer manager.saveMu.Unlock()
locked := true
defer func() {
if locked {
manager.saveMu.Unlock()
}
}()
current := manager.currentConfig()
if strings.TrimSpace(current.LastAgentModelHash) == normalizedValue {
return nil
}
current.LastAgentModelHash = normalizedValue
normalized, err := manager.store.Save(ctx, current)
normalized, err := manager.store.updateLastAgentModelHash(normalizedValue)
if err != nil {
return err
}
@@ -120,6 +131,8 @@ func (manager *Manager) SaveLastAgentModelHash(ctx context.Context, value string
manager.lastReload = time.Now()
manager.reloadError = ""
manager.reloadMu.Unlock()
manager.saveMu.Unlock()
locked = false
manager.notify(normalized)
return nil
}
@@ -164,7 +177,9 @@ func (manager *Manager) LegacyRuntimeSnapshot(_ context.Context) (legacyruntime.
adapters := make([]legacyruntime.ModelAdapterConfig, 0, len(cfg.ModelAdapters))
for _, item := range cfg.ModelAdapters {
adapters = append(adapters, legacyruntime.ModelAdapterConfig{
ID: item.ID,
ID: item.ID,
CustomHeadersEnabled: item.CustomHeadersEnabled, CustomHeadersJSON: item.CustomHeadersJSON,
AnthropicExtraParamsEnabled: item.AnthropicExtraParamsEnabled, AnthropicExtraParamsJSON: item.AnthropicExtraParamsJSON,
DisplayName: item.DisplayName,
Type: item.Type,
BaseURL: item.BaseURL,
@@ -204,7 +219,7 @@ func (manager *Manager) RouteMode(hasUpstreamURL bool) string {
}
func (manager *Manager) setCurrent(cfg Config) {
next := cfg
next := cloneConfig(cfg)
manager.current.Store(&next)
}
@@ -215,6 +230,13 @@ func (manager *Manager) reloadIfChanged(ctx context.Context) {
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 {
@@ -241,6 +263,8 @@ func (manager *Manager) reloadIfChanged(ctx context.Context) {
manager.reloadError = ""
manager.setCurrent(cfg)
manager.reloadMu.Unlock()
manager.saveMu.Unlock()
reloadSaveLocked = false
manager.notify(cfg)
}
@@ -250,7 +274,7 @@ func (manager *Manager) notify(cfg Config) {
manager.listenersMu.RUnlock()
for _, listener := range listeners {
if listener != nil {
listener(cfg)
listener(cloneConfig(cfg))
}
}
}

View File

@@ -16,8 +16,8 @@ const (
// 默认值越过服务方上限会直接返回 400(exceeds the configured maximum output length)。
defaultChannelMaxTokens = 278_000
defaultChannelAnthropicMaxTokens = 131_072
defaultChannelThinkingBudget = 4_096
defaultChannelAnthropicEffort = "xhigh"
defaultChannelThinkingBudget = 4_096
defaultChannelAnthropicEffort = "xhigh"
)
func (manager *Manager) SelectChannelForModel(_ context.Context, modelID string) (*legacyruntime.ResolvedChannel, error) {

View File

@@ -0,0 +1,33 @@
package config
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
)
var ErrConfigConflict = errors.New("CONFIG_CONFLICT: 配置已在其他窗口更改,请重新加载后再保存;当前编辑内容已保留")
func cloneConfig(cfg Config) Config {
cfg.ModelAdapters = append([]ModelAdapterConfig(nil), cfg.ModelAdapters...)
if cfg.HomeMetrics.PersistentMode != nil {
value := *cfg.HomeMetrics.PersistentMode
cfg.HomeMetrics.PersistentMode = &value
}
return cfg
}
func withRevision(cfg Config) Config {
cfg = cloneConfig(cfg)
view := cloneConfig(cfg)
view.Revision = ""
view.LastAgentModelHash = ""
for i := range view.ModelAdapters {
view.ModelAdapters[i].ID = ""
}
data, _ := json.Marshal(view)
sum := sha256.Sum256(data)
cfg.Revision = hex.EncodeToString(sum[:])
return cfg
}

View File

@@ -0,0 +1,83 @@
package config
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"sync"
"testing"
)
func TestConfigCASAndMetadataIsolation(t *testing.T) {
store := NewStore(filepath.Join(t.TempDir(), "config.yaml"), "")
m, err := NewManager(context.Background(), store)
if err != nil {
t.Fatal(err)
}
a, _ := m.Load(nil)
b, _ := m.Load(nil)
if a.Revision == "" {
t.Fatal("missing revision")
}
if err = m.SaveLastAgentModelHash(nil, "model-new"); err != nil {
t.Fatal(err)
}
a.CLIHTTPSListenAddr = "127.0.0.1:39099"
first, err := m.Save(nil, a)
if err != nil {
t.Fatal(err)
}
if first.LastAgentModelHash != "model-new" {
t.Fatal("UI overwrote backend metadata")
}
b.HomeMetrics.RefreshIntervalSeconds = 11
if _, err = m.Save(nil, b); !errors.Is(err, ErrConfigConflict) {
t.Fatalf("stale save accepted: %v", err)
}
current, _ := m.Load(nil)
if current.CLIHTTPSListenAddr != "127.0.0.1:39099" {
t.Fatal("CLI port lost")
}
data, _ := os.ReadFile(store.Path())
if strings.Contains(string(data), "revision:") {
t.Fatal("revision persisted in YAML")
}
if current.Revision == a.Revision {
t.Fatal("revision did not change")
}
}
func TestConcurrentSavesHaveExactlyOneWinner(t *testing.T) {
store := NewStore(filepath.Join(t.TempDir(), "config.yaml"), "")
m, err := NewManager(context.Background(), store)
if err != nil {
t.Fatal(err)
}
a, _ := m.Load(nil)
b := cloneConfig(a)
a.HomeMetrics.RefreshIntervalSeconds = 11
b.HomeMetrics.RefreshIntervalSeconds = 12
var wg sync.WaitGroup
results := make(chan error, 2)
for _, cfg := range []Config{a, b} {
wg.Add(1)
go func(c Config) { defer wg.Done(); _, err := m.Save(nil, c); results <- err }(cfg)
}
wg.Wait()
close(results)
success, conflict := 0, 0
for err := range results {
if err == nil {
success++
} else if errors.Is(err, ErrConfigConflict) {
conflict++
} else {
t.Fatal(err)
}
}
if success != 1 || conflict != 1 {
t.Fatalf("success=%d conflict=%d", success, conflict)
}
}

View File

@@ -2,6 +2,7 @@ package config
import (
"context"
"cursor/internal/securefile"
"errors"
"fmt"
"os"
@@ -75,7 +76,7 @@ func (store *Store) Load(_ context.Context) (Config, error) {
if err := store.saveLocked(defaultConfig); err != nil {
return DefaultConfig(), err
}
return defaultConfig, nil
return withRevision(defaultConfig), nil
}
return DefaultConfig(), fmt.Errorf("读取用户配置失败: %w", err)
}
@@ -93,7 +94,7 @@ func (store *Store) Load(_ context.Context) (Config, error) {
return DefaultConfig(), err
}
}
return normalized, nil
return withRevision(normalized), nil
}
func (store *Store) Save(_ context.Context, cfg Config) (Config, error) {
@@ -109,10 +110,20 @@ func (store *Store) Save(_ context.Context, cfg Config) (Config, error) {
store.mu.Lock()
defer store.mu.Unlock()
if cfg.Revision != "" {
existing, err := store.readCurrentLocked()
if err != nil {
return Config{}, err
}
if withRevision(existing).Revision != cfg.Revision {
return Config{}, ErrConfigConflict
}
normalized.LastAgentModelHash = existing.LastAgentModelHash
}
if err := store.saveLocked(normalized); err != nil {
return Config{}, err
}
return normalized, nil
return withRevision(normalized), nil
}
func (store *Store) saveLocked(normalized Config) error {
@@ -125,11 +136,7 @@ func (store *Store) saveLocked(normalized Config) error {
return fmt.Errorf("序列化用户配置失败: %w", err)
}
tempPath := store.path + ".tmp"
if err := writeFileSync(tempPath, data); err != nil {
return fmt.Errorf("写入临时配置失败: %w", err)
}
if err := os.Rename(tempPath, store.path); err != nil {
if err := securefile.Write(store.path, data); err != nil {
return fmt.Errorf("保存用户配置失败: %w", err)
}
return nil
@@ -181,3 +188,31 @@ func yamlHasKey(raw []byte, key string) bool {
}
return false
}
func (store *Store) readCurrentLocked() (Config, error) {
data, err := os.ReadFile(store.path)
if errors.Is(err, os.ErrNotExist) {
return DefaultConfig(), nil
}
if err != nil {
return Config{}, err
}
var current Config
if err = yaml.Unmarshal(data, &current); err != nil {
return Config{}, err
}
return NormalizeConfig(current)
}
func (store *Store) updateLastAgentModelHash(value string) (Config, error) {
store.mu.Lock()
defer store.mu.Unlock()
cfg, err := store.readCurrentLocked()
if err != nil {
return Config{}, err
}
cfg.LastAgentModelHash = value
if err = store.saveLocked(cfg); err != nil {
return Config{}, err
}
return withRevision(cfg), nil
}

View File

@@ -1,6 +1,7 @@
package config
import (
"cursor/internal/logger"
"encoding/json"
"errors"
"fmt"
@@ -62,6 +63,7 @@ type HomeMetricsConfig struct {
}
type Config struct {
Revision string `json:"revision" yaml:"-"`
Log bool `json:"log" yaml:"log"`
ProviderStreamIdleTimeout int `json:"providerStreamIdleTimeout" yaml:"providerStreamIdleTimeout"`
BackendListenAddr string `json:"backendListenAddr" yaml:"backendListenAddr"`
@@ -139,6 +141,13 @@ func NormalizeConfig(input Config) (Config, error) {
}
output.TabServerBaseURL = normalizeTabServerURL(input.TabServerBaseURL)
output.TabServerToken = strings.TrimSpace(input.TabServerToken)
if strings.TrimSpace(input.TabServerBaseURL) != "" && output.TabServerBaseURL == "" {
return Config{}, fmt.Errorf("tabServerBaseURL 无效")
}
if output.TabServerBaseURL != "" && output.TabServerToken == "" {
return Config{}, fmt.Errorf("配置 Tab 服务时必须提供独立 tabServerToken")
}
logger.RegisterSecrets(output.TabServerToken)
adapters, err := NormalizeModelAdapterConfigs(input.ModelAdapters)
if err != nil {
return Config{}, err
@@ -156,7 +165,7 @@ func normalizeTabServerURL(value string) string {
if err != nil {
return ""
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
if parsed.User != nil || parsed.Hostname() == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return ""
}
parsed.Path = strings.TrimRight(parsed.Path, "/")
@@ -193,6 +202,7 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon
AnthropicMaxTokens: normalizeMaxCompletionTokens(item.AnthropicMaxTokens),
ThinkingBudgetTokens: normalizeMaxCompletionTokens(item.ThinkingBudgetTokens),
}
logger.RegisterSecrets(next.APIKey)
if next.Type == "openai" {
next.OpenAIExtraParamsEnabled = item.OpenAIExtraParamsEnabled
next.OpenAIExtraParamsJSON = strings.TrimSpace(item.OpenAIExtraParamsJSON)
@@ -203,6 +213,15 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon
}
next.CustomHeadersEnabled = item.CustomHeadersEnabled
next.CustomHeadersJSON = strings.TrimSpace(item.CustomHeadersJSON)
var headers map[string]string
if json.Unmarshal([]byte(next.CustomHeadersJSON), &headers) == nil {
for key, value := range headers {
lower := strings.ToLower(key)
if strings.Contains(lower, "auth") || strings.Contains(lower, "key") || strings.Contains(lower, "token") || strings.Contains(lower, "cookie") {
logger.RegisterSecrets(value, strings.TrimPrefix(value, "Bearer "))
}
}
}
switch {
case next.DisplayName == "":
return nil, errors.New("模型适配器 displayName 不能为空")