- 流式调用增加可重试错误码检测与指数退避重试 - 业务字段写入改为前置追加,不覆盖已有值 - 请求体按模板元数据递归合并,补充数组/对象字段处理 - 异步任务查询支持请求体映射与占位符替换 - 升级 common 与 gmq 依赖版本,移除本地 replace
606 lines
15 KiB
Go
606 lines
15 KiB
Go
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
|
||
}
|