package utils import ( "regexp" "strings" "gitea.redpowerfuture.com/red-future/common/utils" "github.com/gogf/gf/v2/util/gconv" ) var ( // 匹配 [数字] regNumIndex = regexp.MustCompile(`\[\d+\]`) // 匹配 .attrs regAttrs = regexp.MustCompile(`\.attrs`) ) // NormalizeFieldPath 归一化字段路径到统一语法([*] 数组段): // - 移除模板残留 .attrs // - [数字] 下标 → [*](choices[0] → choices[*]) // - 兼容 gjson 风格 .# / .数字 下标 → [*](choices.#、choices.0 → choices[*]) // // 统一语法见 business_fields.go 的 SetByPath / GetByPath: // // a.b.c 普通点号路径 // a[*].b [*] 表示数组段 // a[*].b[*]?k=v&t=# 选择器:数组元素按 k==v 定位,值/读取目标为 t // a[*]?k=v&b[*]?k2=v2&t=# 多级选择器:级数不限,中间级定位容器元素,叶子写值 // // 正则归一(.attrs / [数字] / .#)作用于整个路径(含多级选择器中的数组段); // 纯数字段(gjson 下标)归一只作用于首个 ? 之前的 base 路径。 // // 示例: // // usage.attrs.total_tokens → usage.total_tokens // choices.attrs[0].attrs.message.attrs.content → choices[*].message.content // choices.#.message.content → choices[*].message.content // choices.0.message.content → choices[*].message.content func NormalizeFieldPath(path string) string { s := regAttrs.ReplaceAllString(path, "") s = regNumIndex.ReplaceAllString(s, "[*]") s = strings.ReplaceAll(s, ".#", "[*]") base, suffix := s, "" if i := strings.Index(s, "?"); i >= 0 { base, suffix = s[:i], s[i:] } // 逐段把纯数字段(gjson 下标)归一为 [*]:附着到前一段字段(choices.0 → choices[*]), // 避免误伤数字开头的字段名;选择器体用 # 作目标、不用数字段下标,故只归一 base segs := strings.Split(base, ".") var out []string for _, seg := range segs { if seg == "" { continue } if isAllDigits(seg) { if len(out) > 0 { out[len(out)-1] += "[*]" } else { out = append(out, "[*]") } continue } out = append(out, seg) } return strings.Join(out, ".") + suffix } // isAllDigits 判断字符串是否全部为数字字符 func isAllDigits(s string) bool { if s == "" { return false } for _, r := range s { if r < '0' || r > '9' { return false } } return true } // CleanFieldPath 清理字段路径(等价于 NormalizeFieldPath,保留旧名兼容) func CleanFieldPath(path string) string { return NormalizeFieldPath(path) } // CleanMapFieldPath 清理字段路径(Map) func CleanMapFieldPath(m map[string]string) map[string]string { if m == nil { return nil } newMap := make(map[string]string, len(m)) for k, _ := range m { newMap[k] = CleanFieldPath(k) } return newMap } // ParseConfigTemplate 解析配置模板生成简化请求结构 // // 输入: config 模板(含 type/value/defaultValue/attrs/enumValues 等元数据字段) // 输出: 简化后的请求结构体 // // 规则: // - 标量字段(string/number/boolean): value 非零则用 value,为空则跳过(不再取 defaultValue) // - 对象字段(object): 递归处理 attrs // - 数组字段(array): 遍历 enumValues,每个 enumValue 独立判断是否产出元素 // - 数组展开: enumValue 内某叶子字段 value 为数组时,按数组元素展开为多个项 func ParseConfigTemplate(cfg map[string]interface{}) map[string]interface{} { var flattenJSON map[string]interface{} flatMap := utils.IsFlatMap(cfg) if flatMap { var err error flattenJSON, err = utils.UnFlatBySjson(cfg) if err != nil { return nil } } else { flattenJSON = cfg } result := make(map[string]interface{}) for key, val := range flattenJSON { field, ok := val.(map[string]interface{}) if !ok { continue } if v := resolveField(field); v != nil { result[key] = v } } return result } // resolveField 按 type 分发解析 func resolveField(field map[string]interface{}) interface{} { fieldType, _ := field["type"].(string) switch fieldType { case TypeString, TypeBool, TypeNumber: return resolveScalar(field) case TypeObject: return resolveObject(field) case TypeArray: return resolveArray(field) } return nil } // resolveScalar 解析标量字段: value 非空则用 value,否则回落 defaultValue // (模板只声明结构、值由业务字段给出时,defaultValue 生效) func resolveScalar(field map[string]interface{}) interface{} { if v, has := field["value"]; has && v != nil { switch vv := v.(type) { case string: if vv != "" { return vv } case bool: return vv default: // 数值零值(int/float 各类型)视为未提供,跳过;bool/string 已在上方处理 if isNumericZero(v) { return nil } return vv } } if d, has := field["defaultValue"]; has && d != nil { switch dv := d.(type) { case string: if dv != "" { return dv } case bool: return dv default: if isNumericZero(d) { return nil } return dv } } return nil } // isNumericZero 判断是否为数值零值(模板 value 常为 int 字面量,经 gconv 可能为 float64) func isNumericZero(v interface{}) bool { switch vv := v.(type) { case int: return vv == 0 case int8: return vv == 0 case int16: return vv == 0 case int32: return vv == 0 case int64: return vv == 0 case uint: return vv == 0 case uint8: return vv == 0 case uint16: return vv == 0 case uint32: return vv == 0 case uint64: return vv == 0 case float32: return vv == 0 case float64: return vv == 0 default: return false } } // resolveObject 解析对象字段,递归处理 attrs // // 特殊处理「参数定义」结构:当 attrs 含 default 字段时,说明该对象是一个 // 参数定义(含 default/description/min/max/type/enum/required 等元数据), // 此时只提取 default 的值作为该参数的值,其余元数据字段忽略。 func resolveObject(field map[string]interface{}) interface{} { attrs, ok := field["attrs"].(map[string]interface{}) if !ok { return nil } // 参数定义:只提取 default 值,跳过元数据 if defaultField, hasDefault := attrs["default"]; hasDefault { if df, ok := defaultField.(map[string]interface{}); ok { return extractRawValueKeepZero(df) } return nil } // 普通对象:递归处理所有 attrs result := make(map[string]interface{}) for key, val := range attrs { subField, ok := val.(map[string]interface{}) if !ok { result[key] = val // 纯值字段原样保留 continue } if subType, _ := subField["type"].(string); subType == "" { result[key] = val // 无 type 键的纯对象原样保留 continue } if v := resolveField(subField); v != nil { result[key] = v } } if len(result) == 0 { return nil } return result } // resolveArray 解析数组字段,遍历 enumValues 或 attrs 生成元素列表 func resolveArray(field map[string]interface{}) []interface{} { // 实际数据在 value(schema-editor 数据存放处),直接返回 if v, has := field["value"]; has { if arr, ok := v.([]interface{}); ok && len(arr) > 0 { return arr } } enumValues, ok := field["enumValues"].([]interface{}) if ok { var result []interface{} for _, ev := range enumValues { evMap, ok := ev.(map[string]interface{}) if !ok { continue } items := resolveEnumObject(evMap) result = append(result, items...) } if len(result) > 0 { return result } } // enumValues 取不到或为空时,尝试从 attrs(数组)中取 attrs, ok := field["attrs"].([]interface{}) if !ok { return nil } var result []interface{} for _, item := range attrs { itemMap, ok := item.(map[string]interface{}) if !ok { continue } if v := resolveField(itemMap); v != nil { result = append(result, v) } } return result } // resolveEnumObject 解析 enumValue 对象,支持数组展开 func resolveEnumObject(ev map[string]interface{}) []interface{} { attrs, ok := ev["attrs"].(map[string]interface{}) if !ok { return nil } // 将 enumValue 级别的 value 注入 attrs.type.value(如果 type.value 为空) if evVal, has := ev["value"]; has && evVal != nil { if s, ok := evVal.(string); ok && s != "" { if typeField, has := attrs["type"]; has { if typeMap, ok := typeField.(map[string]interface{}); ok { if existing, has := typeMap["value"]; !has || existing == nil || existing == "" { typeMap["value"] = s } } } } } return resolveAttrs(attrs) } // resolveAttrs 递归解析 attrs map,支持字段级数组展开 func resolveAttrs(attrs map[string]interface{}) []interface{} { currentItems := []map[string]interface{}{{}} hasValue := false for key, val := range attrs { subField, isMap := val.(map[string]interface{}) var subType string if isMap { subType, _ = subField["type"].(string) } var nextItems []map[string]interface{} // 非包裹字段(纯值/纯对象,无 type 键):原样保留,数组值仍参与展开 if !isMap || subType == "" { raw := val if raw == nil { nextItems = currentItems currentItems = nextItems continue } hasValue = true if arr, ok := raw.([]interface{}); ok && len(arr) > 0 { for _, item := range currentItems { for _, elem := range arr { cp := copyMap(item) cp[key] = elem nextItems = append(nextItems, cp) } } } else { for _, item := range currentItems { cp := copyMap(item) cp[key] = raw nextItems = append(nextItems, cp) } } currentItems = nextItems continue } switch subType { case TypeString, TypeBool, TypeNumber: raw := extractRawValue(subField) if raw == nil { nextItems = currentItems continue } hasValue = true if arr, ok := raw.([]interface{}); ok && len(arr) > 0 { for _, item := range currentItems { for _, elem := range arr { cp := copyMap(item) cp[key] = elem nextItems = append(nextItems, cp) } } } else { for _, item := range currentItems { cp := copyMap(item) cp[key] = raw nextItems = append(nextItems, cp) } } case TypeObject: subAttrs, ok := subField["attrs"].(map[string]interface{}) if !ok { nextItems = currentItems continue } subItems := resolveAttrs(subAttrs) if len(subItems) == 0 { nextItems = currentItems continue } hasValue = true for _, item := range currentItems { for _, subI := range subItems { cp := copyMap(item) cp[key] = subI nextItems = append(nextItems, cp) } } case TypeArray: items := resolveArray(subField) if len(items) == 0 { nextItems = currentItems continue } hasValue = true for _, item := range currentItems { cp := copyMap(item) cp[key] = items nextItems = append(nextItems, cp) } default: nextItems = currentItems } currentItems = nextItems } if !hasValue { return nil } result := make([]interface{}, len(currentItems)) for i, item := range currentItems { result[i] = item } return result } // extractRawValue 提取原始值(保留数组值供上层展开) func extractRawValue(field map[string]interface{}) interface{} { if v, has := field["value"]; has && v != nil { switch vv := v.(type) { case string: if vv != "" { return vv } case float64: if vv != 0 { return vv } case bool: return vv case []interface{}: if len(vv) > 0 { return vv } default: return vv } } return nil } // extractRawValueKeepZero 同 extractRawValue,但不过滤零值 // 在参数定义场景下,default 可能是 false/0/"",需要保留 func extractRawValueKeepZero(field map[string]interface{}) interface{} { if v, has := field["value"]; has && v != nil { switch vv := v.(type) { case string: return vv case float64: return vv case bool: return vv case []interface{}: if len(vv) > 0 { return vv } return vv default: return vv } } if dv, has := field["defaultValue"]; has && dv != nil { switch dvv := dv.(type) { case string: if field["type"] == TypeBool { if dvv == "true" { return true } if dvv == "false" { return false } } return dvv case float64: return dvv case bool: return dvv case []interface{}: if len(dvv) > 0 { return dvv } return dvv default: return dvv } } return nil } // copyMap 浅拷贝 map func copyMap(src map[string]interface{}) map[string]interface{} { dst := make(map[string]interface{}, len(src)) for k, v := range src { dst[k] = v } return dst } // CoerceBodyTypes 按模板声明的 type 递归归一请求体字段值类型: // - string → gconv.String;number → gconv.Float64;boolean → gconv.Bool // - object → 按模板 attrs 递归子字段;array → 按元素模板逐个递归 // - 模板未声明的字段(业务字段写入且超出模板的部分)保持原样 // // 用于构建请求体后统一修正:模板字段 value 与业务字段写入的值都可能携带与声明 // 类型不一致的 Go 类型(如 number 字段 value 为字符串 "0.7"),在此统一转成模型 // API 期望的 JSON 类型。仅做类型归一,不增删字段。 func CoerceBodyTypes(out map[string]interface{}, templateParams map[string]interface{}) map[string]interface{} { if len(templateParams) == 0 { return out } for key, raw := range out { if tmplNode, has := templateParams[key]; has { out[key] = coerceNode(raw, tmplNode) } } return out } // coerceNode 按单个模板节点归一值类型 func coerceNode(value interface{}, tmplNode interface{}) interface{} { tmplMap, ok := tmplNode.(map[string]interface{}) if !ok { return value } fieldType, _ := tmplMap["type"].(string) switch fieldType { case TypeString: return gconv.String(value) case TypeNumber, TypeNumberInt, TypeNumberFloat: return gconv.Float64(value) case TypeBool: return gconv.Bool(value) case TypeObject: sub, ok := value.(map[string]interface{}) if !ok { return value } if attrs, ok := tmplMap["attrs"].(map[string]interface{}); ok { return coerceObject(sub, attrs) } return value case TypeArray: arr, ok := value.([]interface{}) if !ok { return value } proto := arrayElementTemplate(tmplMap) if proto == nil { return value } out := make([]interface{}, len(arr)) for i, elem := range arr { out[i] = coerceNode(elem, proto) } return out default: return value } } // coerceObject 按对象模板 attrs 归一对象子字段类型 func coerceObject(sub, attrs map[string]interface{}) map[string]interface{} { for key, raw := range sub { if tmplNode, has := attrs[key]; has { sub[key] = coerceNode(raw, tmplNode) } } return sub } // arrayElementTemplate 从数组模板节点提取元素模板(attrs 优先,其次 enumValues)。 // 与 arrayElementPrototype 语义一致,但直接工作在原始模板 map 上,供类型归一使用。 func arrayElementTemplate(field map[string]interface{}) map[string]interface{} { if attrs, ok := field["attrs"].([]interface{}); ok && len(attrs) > 0 { if m, ok := attrs[0].(map[string]interface{}); ok { return m } } if evs, ok := field["enumValues"].([]interface{}); ok && len(evs) > 0 { if m, ok := evs[0].(map[string]interface{}); ok { return m } } return nil }