package plugin // config_schema.go 实现插件配置 Schema 验证(12.6 Plugin Config Schema). // // 插件通过 plugin.json 的 config_schema 字段声明需要哪些配置项, // 引擎在加载时调用 ValidatePluginConfig 验证用户提供的配置 map 是否满足约束. // // 设计原则: // - 三层防御(指令层=Schema定义 / 参数层=类型校验 / 兜底层=nil配置也能启动) // - Secret 字段全程脱敏,日志/错误消息不暴露敏感值 // - nil config 且无 Required 字段时:验证通过(向后兼容,插件可无配置运行) // // 升华改进(ELEVATED): 相比早期方案(无 schema 机制,靠文档约定): // 1. 机器可校验--引擎加载时自动报错,无需人工排查"为什么插件不工作" // 2. Secret 脱敏内置--不依赖插件自身记得 mask // 3. 类型约束前置--number/boolean 在进入插件前已经过格式检查 // 4. GetPluginConfig 运行时安全读取--隔离各插件的配置命名空间 // // 原方案:<让插件直接读 os.Getenv / 全局 map> - // 否决原因:全局命名空间冲突;插件无法声明依赖;机器无法校验完整性. import ( "fmt" "strconv" "strings" "sync" ) // validConfigTypes 是允许的配置字段类型集合. // 使用 map 而非 switch 方便后续扩展(只加一行,不需要改 switch 分支). var validConfigTypes = map[string]bool{ "string": true, "number": true, "boolean": true, } // ValidatePluginConfig 按 schema 验证用户提供的配置 map. // // 验证规则: // 1. Required=true 的字段必须在 config 中存在(且非空字符串) // 2. 字段的值类型须与 Type 声明匹配(number: 可解析为数字;boolean: "true"/"false") // 3. Secret 字段的值在错误消息中脱敏为 "***" // // config 为 nil 等同于空 map(没有任何配置). // schema 为空时直接返回 nil(无约束,向后兼容). // // 精妙之处(CLEVER): 收集所有错误后一次性返回(而非遇第一个错误就停)-- // 开发者能一次看到所有缺失/错误的字段,不需要改了一个再发现下一个. // 替代方案:第一个错误即返回(快速失败,代码更简单但用户体验差). func ValidatePluginConfig(pluginName string, schema []ConfigFieldDef, config map[string]string) error { if len(schema) == 0 { return nil // 无约束,直接通过 } var errs []string for _, field := range schema { val, exists := config[field.Key] // 1. Required 检查 if field.Required && (!exists || val == "") { errs = append(errs, fmt.Sprintf("required field %q is missing", field.Key)) continue // 字段不存在,类型检查跳过 } if !exists { continue // 非必填且缺失:允许 } // 2. 类型检查(仅在字段存在时) fieldType := field.Type if fieldType == "" { fieldType = "string" // 未声明类型默认 string,跳过类型校验 } if !validConfigTypes[fieldType] { // 未知类型:跳过类型校验,保留向前兼容性 continue } if typeErr := checkConfigFieldType(field.Key, fieldType, val, field.Secret); typeErr != "" { errs = append(errs, typeErr) } } if len(errs) > 0 { return PluginError{ Code: ErrConfigValidation, PluginName: pluginName, Message: fmt.Sprintf("config validation failed: %s", strings.Join(errs, "; ")), } } return nil } // checkConfigFieldType 验证单个字段的类型,返回错误描述字符串(空字符串表示通过). // // 精妙之处(CLEVER): 返回 string 而非 error--调用方收集所有字段错误后拼接, // 避免每次 wrap error 浪费内存分配(校验场景通常批量检查多字段). func checkConfigFieldType(key, fieldType, val string, secret bool) string { displayVal := val if secret { displayVal = "***" // ELEVATED: Secret 字段脱敏 } switch fieldType { case "number": if _, err := strconv.ParseFloat(val, 64); err != nil { return fmt.Sprintf("field %q: expected number, got %q", key, displayVal) } case "boolean": lower := strings.ToLower(val) if lower != "true" && lower != "false" && lower != "1" && lower != "0" { return fmt.Sprintf("field %q: expected boolean (true/false/1/0), got %q", key, displayVal) } case "string": // 任意字符串均合法,无类型检查 } return "" } // maskSecretFields 对 config map 中所有 Secret=true 字段的值进行脱敏, // 返回一个新 map(不修改原 map),供日志/审计安全使用. // // 升华改进(ELEVATED): 所有打印 config map 的地方统一调用此函数-- // 一处更改保护全部路径,防止 Secret 值泄漏到日志/错误消息/Observer 事件. func maskSecretFields(schema []ConfigFieldDef, config map[string]string) map[string]string { if len(config) == 0 { return config } // 构建 secret key 集合(通常 schema 很小,线性扫描比 map 分配更高效) var secretKeys []string for _, f := range schema { if f.Secret { secretKeys = append(secretKeys, f.Key) } } if len(secretKeys) == 0 { return config // 无 secret 字段:直接返回原 map } // 复制并脱敏 masked := make(map[string]string, len(config)) for k, v := range config { masked[k] = v } for _, k := range secretKeys { if _, ok := masked[k]; ok { masked[k] = "***" } } return masked } // pluginConfigStore 是插件运行时配置存储,线程安全. // // 精妙之处(CLEVER): 两层 map(pluginName → configMap)而非全局平铺 map-- // 不同插件的配置 key 命名空间天然隔离,互不干扰. // 例如插件 A 和插件 B 都可以有 "api_key" 而不冲突. // 替代方案:全局 map[pluginName+"."+key]string(前缀拼接,容易出 bug). type pluginConfigStore struct { mu sync.RWMutex data map[string]map[string]string // pluginName → (key → value) } // newPluginConfigStore 创建空配置存储. func newPluginConfigStore() *pluginConfigStore { return &pluginConfigStore{ data: make(map[string]map[string]string), } } // Set 设置指定插件的配置 map(覆盖整个 config,而非 merge). // 传入 nil config 等同于清空该插件的所有配置. func (s *pluginConfigStore) Set(pluginName string, config map[string]string) { s.mu.Lock() defer s.mu.Unlock() if config == nil { delete(s.data, pluginName) return } // 复制防止外部修改影响内部状态 copied := make(map[string]string, len(config)) for k, v := range config { copied[k] = v } s.data[pluginName] = copied } // Get 获取指定插件的配置 map. // 若插件未设置配置,返回空 map(非 nil),便于调用方直接读取 key. func (s *pluginConfigStore) Get(pluginName string) map[string]string { s.mu.RLock() defer s.mu.RUnlock() cfg, ok := s.data[pluginName] if !ok { return map[string]string{} } // 返回副本,防止外部修改内部状态 result := make(map[string]string, len(cfg)) for k, v := range cfg { result[k] = v } return result } // GetField 获取指定插件的单个配置字段值. // 若未设置配置或 key 不存在,返回空字符串和 false. func (s *pluginConfigStore) GetField(pluginName, key string) (string, bool) { s.mu.RLock() defer s.mu.RUnlock() cfg, ok := s.data[pluginName] if !ok { return "", false } v, ok := cfg[key] return v, ok }