package sysadmin import ( "context" "encoding/json" "errors" "fmt" "os" "strconv" "time" ) // 设置读写错误;HTTP 适配层据此映射状态码。 var ( ErrUnknownSetting = errors.New("sysadmin: unknown setting key") ErrSettingReadOnly = errors.New("sysadmin: setting is read-only") ErrInvalidSettingValue = errors.New("sysadmin: invalid setting value") ErrStoreUnavailable = errors.New("sysadmin: settings store unavailable") ) // 设置值类型。 const ( SettingTypeString = "string" SettingTypeBool = "bool" SettingTypeNumber = "number" ) // SettingSpec 白名单注册表条目;只有登记在册的 key 才可读写。 type SettingSpec struct { Key string Label string Group string Type string Default any Writable bool RequiresRestart bool EnvVar string // 可选 env 中间层:DB > env > default Description string } // Setting 设置视图,字段对齐前端 camelCase 契约。 type Setting struct { Key string `json:"key"` Label string `json:"label"` Group string `json:"group"` Type string `json:"type"` Value any `json:"value"` Default any `json:"default"` Writable bool `json:"writable"` RequiresRestart bool `json:"requiresRestart"` Description string `json:"description"` UpdatedAt *time.Time `json:"updatedAt,omitempty"` UpdatedBy *int64 `json:"updatedBy,omitempty"` } // defaultSettingSpecs 一期白名单:先做机制,键空间可后续扩展。 func defaultSettingSpecs() []SettingSpec { return []SettingSpec{ {Key: "ui.maintenance_notice", Label: "维护公告", Group: "ui", Type: SettingTypeString, Default: "", Writable: true, RequiresRestart: false, Description: "全站维护公告横幅文案;空字符串表示关闭"}, {Key: "ui.read_only_mode", Label: "只读模式提示", Group: "ui", Type: SettingTypeBool, Default: false, Writable: true, RequiresRestart: false, Description: "开启后前端进入只读提示模式,阻断写操作入口"}, {Key: "chat.default_session_role", Label: "新会话默认职责", Group: "chat", Type: SettingTypeString, Default: "engineer", Writable: true, RequiresRestart: false, Description: "未显式指定职责时新会话的默认角色"}, {Key: "tasks.default_auto_repair", Label: "任务默认自动修复", Group: "tasks", Type: SettingTypeBool, Default: false, Writable: true, RequiresRestart: false, Description: "独立任务运行未传 auto_repair 时的默认值"}, {Key: "kb.default_search_limit", Label: "KB 检索默认条数", Group: "kb", Type: SettingTypeNumber, Default: 8.0, Writable: true, RequiresRestart: false, Description: "知识库检索未指定 limit 时的默认返回条数"}, {Key: "notifications.stage_advance_reminder", Label: "阶段推进提醒总开关", Group: "notification", Type: SettingTypeBool, Default: true, Writable: true, RequiresRestart: false, Description: "阶段就绪 24h 提醒的总开关(一期先落机制)"}, } } func (s *Service) findSpec(key string) (SettingSpec, bool) { for _, spec := range s.specs { if spec.Key == key { return spec, true } } return SettingSpec{}, false } // ListSettings 返回白名单全部设置;读优先级 DB > env > default。 func (s *Service) ListSettings(ctx context.Context) ([]Setting, error) { out := make([]Setting, 0, len(s.specs)) for _, spec := range s.specs { setting, err := s.resolve(ctx, spec) if err != nil { return nil, err } out = append(out, setting) } return out, nil } // UpdateSetting 校验并写入白名单设置,返回更新后的视图。 func (s *Service) UpdateSetting(ctx context.Context, key string, raw json.RawMessage, updatedBy int64) (Setting, error) { spec, ok := s.findSpec(key) if !ok { return Setting{}, ErrUnknownSetting } if !spec.Writable { return Setting{}, ErrSettingReadOnly } if err := validateSettingValue(spec.Type, raw); err != nil { return Setting{}, err } if s.store == nil { return Setting{}, ErrStoreUnavailable } if _, err := s.store.Upsert(ctx, key, raw, updatedBy); err != nil { return Setting{}, err } return s.resolve(ctx, spec) } // resolve 按 DB > env > default 解析单个设置的当前值。 func (s *Service) resolve(ctx context.Context, spec SettingSpec) (Setting, error) { setting := Setting{ Key: spec.Key, Label: spec.Label, Group: spec.Group, Type: spec.Type, Value: spec.Default, Default: spec.Default, Writable: spec.Writable, RequiresRestart: spec.RequiresRestart, Description: spec.Description, } if spec.EnvVar != "" { if raw, set := os.LookupEnv(spec.EnvVar); set { if v, err := parseSettingValue(spec.Type, raw); err == nil { setting.Value = v } } } if s.store == nil { return setting, nil } raw, updatedBy, updatedAt, err := s.store.Get(ctx, spec.Key) if err != nil { if errors.Is(err, ErrSettingNotFound) { return setting, nil } return Setting{}, err } var value any if err := json.Unmarshal(raw, &value); err != nil { return Setting{}, fmt.Errorf("sysadmin: decode stored value for %s: %w", spec.Key, err) } setting.Value = value setting.UpdatedBy = updatedBy setting.UpdatedAt = updatedAt return setting, nil } // validateSettingValue 按声明类型校验 JSON 值。 func validateSettingValue(valueType string, raw json.RawMessage) error { if len(raw) == 0 { return ErrInvalidSettingValue } switch valueType { case SettingTypeString: var v string if err := json.Unmarshal(raw, &v); err != nil { return ErrInvalidSettingValue } case SettingTypeBool: var v bool if err := json.Unmarshal(raw, &v); err != nil { return ErrInvalidSettingValue } case SettingTypeNumber: var v float64 if err := json.Unmarshal(raw, &v); err != nil { return ErrInvalidSettingValue } default: return ErrInvalidSettingValue } return nil } // parseSettingValue 将 env 字符串按类型解析。 func parseSettingValue(valueType, raw string) (any, error) { switch valueType { case SettingTypeString: return raw, nil case SettingTypeBool: v, err := strconv.ParseBool(raw) if err != nil { return nil, err } return v, nil case SettingTypeNumber: v, err := strconv.ParseFloat(raw, 64) if err != nil { return nil, err } return v, nil default: return nil, ErrInvalidSettingValue } }