Files
model-gateway/service/utils/template_parse.go
T
19904408334 76c55fbb73 feat: 新增业务字段路径读写工具
新增 TakeBusinessFields、WriteBusinessFields、SetByPath 与 GetByPath 等工具,支持按映射路径写入请求体与解析响应,并更新相关依赖。
2026-08-18 10:11:09 +08:00

574 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package utils
import (
"regexp"
"strings"
"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 := IsFlatMap(cfg)
if flatMap {
var err error
flattenJSON, err = 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 float64:
if vv != 0 {
return vv
}
case bool:
return vv
default:
return vv
}
}
if d, has := field["defaultValue"]; has && d != nil {
switch dv := d.(type) {
case string:
if dv != "" {
return dv
}
case float64:
if dv != 0 {
return dv
}
case bool:
return dv
default:
return dv
}
}
return nil
}
// 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{} {
// 实际数据在 valueschema-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.Stringnumber → gconv.Float64boolean → 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
}