package sysadmin import ( "bytes" "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") ErrExpectedRevisionRequired = errors.New("sysadmin: expected_revision required") ) // 设置值类型。 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"` Revision int64 `json:"revision"` UpdatedAt *time.Time `json:"updatedAt,omitempty"` UpdatedBy *int64 `json:"updatedBy,omitempty"` } // SettingUpdate 写入入参;仅版本化策略 key 要求 ExpectedRevision。 type SettingUpdate struct { Key string Value json.RawMessage UpdatedBy int64 ExpectedRevision *int64 RequestID string } // 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 提醒的总开关(一期先落机制)"}, { Key: SettingKeyAllowAuthorOnlyApproval, Label: "允许编制人兼任审核批准", Group: "workflow", Type: SettingTypeBool, Default: false, Writable: true, RequiresRestart: false, Description: "授权新配置:开启后项目创建者可在激活前为文档启用编制人兼任。关闭后禁止新启用/激活兼任规划;已激活实例按冻结职责继续执行,不作为紧急熔断。", }, } } 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 校验并写入白名单设置,返回更新后的视图。 // 兼容旧调用:非版本化 key 可走此入口;版本化策略 key 必须带 ExpectedRevision,请用 UpdateSettingEx。 func (s *Service) UpdateSetting(ctx context.Context, key string, raw json.RawMessage, updatedBy int64) (Setting, error) { return s.UpdateSettingEx(ctx, SettingUpdate{Key: key, Value: raw, UpdatedBy: updatedBy}) } // UpdateSettingEx 校验并写入白名单设置;对 SettingKeyAllowAuthorOnlyApproval 强制 CAS + 同事务审计。 func (s *Service) UpdateSettingEx(ctx context.Context, in SettingUpdate) (Setting, error) { spec, ok := s.findSpec(in.Key) if !ok { return Setting{}, ErrUnknownSetting } if !spec.Writable { return Setting{}, ErrSettingReadOnly } if err := validateSettingValue(spec.Type, in.Value); err != nil { return Setting{}, err } if s.store == nil { return Setting{}, ErrStoreUnavailable } if in.Key == SettingKeyAllowAuthorOnlyApproval { if in.ExpectedRevision == nil { return Setting{}, ErrExpectedRevisionRequired } if _, _, err := s.store.UpsertCAS(ctx, in.Key, in.Value, in.UpdatedBy, *in.ExpectedRevision, in.RequestID); err != nil { return Setting{}, err } } else { if _, _, err := s.store.Upsert(ctx, in.Key, in.Value, in.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, revision, 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.Revision = revision 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 } trimmed := bytes.TrimSpace(raw) if string(trimmed) == "null" { return ErrInvalidSettingValue } switch valueType { case SettingTypeString: var v string if err := json.Unmarshal(raw, &v); err != nil { return ErrInvalidSettingValue } case SettingTypeBool: var v bool dec := json.NewDecoder(bytes.NewReader(raw)) dec.DisallowUnknownFields() if err := dec.Decode(&v); err != nil { return ErrInvalidSettingValue } // 拒绝尾随垃圾(如 truefalse) if dec.More() { 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 } }