¸ÄÓñ¾»ú¶ÀÁ¢ CA£¬»ָ´ CLI TLS УÑ飬±£»¤ƾ¾ÝÓëÅäÖò¢·¢±£´档 д Cursor Êý¾ݡ¢»Ự·¾¶¡¢´úÀíÉúÃüÖÜÆڡ¢ģÐÍÇëÇóÓë Skills Îļþ²Ù×÷¡£ Ϊ¶ÀÁ¢ Tab ·þÎñÔö¼ӿͻ§¶˼øȨ£¬Éý¼¶ÒÀÀµÓ빤¾ßÁ´£¬ÍêÉƷ¢²¼ԪÐÅϢͬ²½¡£ Ñé֤£ºÁ½¸ö Go ģ¿éµIJâÊÔ/vet¡¢¹ؼüģ¿é race¡¢ǰ¶˲âÊÔ/Éú²ú¹¹½¨/Éó¼ơ¢ Go ¿ɴï©¶´ɨÃèÓë macOS/Linux ¿ÉÐÐģ¿齻²æ±àÒë¾ùͨ¹ý¡£ Windows ¸ôÀëÆô¶¯ͨ¹ý£»δÍê³ɵĵã»÷ðÑ̺ÍԭÉúƽ̨Ñé֤¼ûÉó²鱨¸档
219 lines
5.0 KiB
Go
219 lines
5.0 KiB
Go
package config
|
||
|
||
import (
|
||
"context"
|
||
"cursor/internal/securefile"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
|
||
"gopkg.in/yaml.v3"
|
||
)
|
||
|
||
type Store struct {
|
||
path string
|
||
logsRoot string
|
||
mu sync.Mutex
|
||
}
|
||
|
||
type fileSnapshot struct {
|
||
exists bool
|
||
modTime int64
|
||
size int64
|
||
}
|
||
|
||
func NewStore(path string, logsRoot string) *Store {
|
||
return &Store{
|
||
path: strings.TrimSpace(path),
|
||
logsRoot: strings.TrimSpace(logsRoot),
|
||
}
|
||
}
|
||
|
||
func (store *Store) Path() string {
|
||
if store == nil {
|
||
return ""
|
||
}
|
||
return store.path
|
||
}
|
||
|
||
func (store *Store) LogsRoot() string {
|
||
if store == nil {
|
||
return ""
|
||
}
|
||
return store.logsRoot
|
||
}
|
||
|
||
func (store *Store) snapshot() fileSnapshot {
|
||
if store == nil || strings.TrimSpace(store.path) == "" {
|
||
return fileSnapshot{}
|
||
}
|
||
info, err := os.Stat(store.path)
|
||
if err != nil {
|
||
return fileSnapshot{}
|
||
}
|
||
return fileSnapshot{
|
||
exists: true,
|
||
modTime: info.ModTime().UnixNano(),
|
||
size: info.Size(),
|
||
}
|
||
}
|
||
|
||
func (store *Store) Load(_ context.Context) (Config, error) {
|
||
if store == nil || strings.TrimSpace(store.path) == "" {
|
||
return DefaultConfig(), nil
|
||
}
|
||
|
||
store.mu.Lock()
|
||
defer store.mu.Unlock()
|
||
|
||
data, err := os.ReadFile(store.path)
|
||
if err != nil {
|
||
if errors.Is(err, os.ErrNotExist) {
|
||
defaultConfig := DefaultConfig()
|
||
if err := store.saveLocked(defaultConfig); err != nil {
|
||
return DefaultConfig(), err
|
||
}
|
||
return withRevision(defaultConfig), nil
|
||
}
|
||
return DefaultConfig(), fmt.Errorf("读取用户配置失败: %w", err)
|
||
}
|
||
|
||
var current Config
|
||
if err := yaml.Unmarshal(data, ¤t); err != nil {
|
||
return DefaultConfig(), fmt.Errorf("解析用户配置失败: %w", err)
|
||
}
|
||
normalized, err := NormalizeConfig(current)
|
||
if err != nil {
|
||
return DefaultConfig(), err
|
||
}
|
||
if shouldPersistNormalizedConfig(data, current, normalized) {
|
||
if err := store.saveLocked(normalized); err != nil {
|
||
return DefaultConfig(), err
|
||
}
|
||
}
|
||
return withRevision(normalized), nil
|
||
}
|
||
|
||
func (store *Store) Save(_ context.Context, cfg Config) (Config, error) {
|
||
if store == nil || strings.TrimSpace(store.path) == "" {
|
||
return Config{}, errors.New("配置存储未初始化")
|
||
}
|
||
|
||
normalized, err := NormalizeConfig(cfg)
|
||
if err != nil {
|
||
return Config{}, err
|
||
}
|
||
|
||
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 withRevision(normalized), nil
|
||
}
|
||
|
||
func (store *Store) saveLocked(normalized Config) error {
|
||
if err := os.MkdirAll(filepath.Dir(store.path), 0o755); err != nil {
|
||
return fmt.Errorf("创建用户配置目录失败: %w", err)
|
||
}
|
||
|
||
data, err := yaml.Marshal(normalized)
|
||
if err != nil {
|
||
return fmt.Errorf("序列化用户配置失败: %w", err)
|
||
}
|
||
|
||
if err := securefile.Write(store.path, data); err != nil {
|
||
return fmt.Errorf("保存用户配置失败: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// writeFileSync 写入并 fsync,保证 rename 前内容已落盘。
|
||
func writeFileSync(path string, data []byte) error {
|
||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if _, err := file.Write(data); err != nil {
|
||
file.Close()
|
||
return err
|
||
}
|
||
if err := file.Sync(); err != nil {
|
||
file.Close()
|
||
return err
|
||
}
|
||
return file.Close()
|
||
}
|
||
|
||
func shouldPersistNormalizedConfig(raw []byte, current Config, normalized Config) bool {
|
||
if !yamlHasKey(raw, "backendListenAddr") || !yamlHasKey(raw, "proxyListenAddr") {
|
||
return true
|
||
}
|
||
if current.BackendListenAddr != normalized.BackendListenAddr || current.ProxyListenAddr != normalized.ProxyListenAddr {
|
||
return true
|
||
}
|
||
if current.ProviderStreamIdleTimeout == normalized.ProviderStreamIdleTimeout {
|
||
return false
|
||
}
|
||
return yamlHasKey(raw, "providerStreamIdleTimeout")
|
||
}
|
||
|
||
func yamlHasKey(raw []byte, key string) bool {
|
||
var root yaml.Node
|
||
if err := yaml.Unmarshal(raw, &root); err != nil {
|
||
return false
|
||
}
|
||
if len(root.Content) == 0 || root.Content[0].Kind != yaml.MappingNode {
|
||
return false
|
||
}
|
||
mapping := root.Content[0]
|
||
for index := 0; index+1 < len(mapping.Content); index += 2 {
|
||
if mapping.Content[index].Value == key {
|
||
return true
|
||
}
|
||
}
|
||
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, ¤t); 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
|
||
}
|