Files
19904408334 9d9bd71468 fix: 优化模型网关请求重试与业务字段前置写入
- 流式调用增加可重试错误码检测与指数退避重试
- 业务字段写入改为前置追加,不覆盖已有值
- 请求体按模板元数据递归合并,补充数组/对象字段处理
- 异步任务查询支持请求体映射与占位符替换
- 升级 common 与 gmq 依赖版本,移除本地 replace
2026-08-21 09:47:21 +08:00

261 lines
7.1 KiB
Go
Raw Permalink 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 "github.com/gogf/gf/v2/util/gconv"
// MergeNode 按模板节点的元数据把用户数据节点合并为模板可解析的节点:
// - 用户节点无 type 键 → 套上模板类型包装(标量取用户值,空则回落模板 value/defaultValue
// - 用户节点有 type 键 → 递归合并子节点(子节点缺 type 时同样补模板类型,避免模板包装泄漏)
// - 对象:模板 attrs 缺省字段补进用户容器(保留模板默认值)
// - 数组:用户 enumValues/attrs 元素逐个与模板元素原型合并(模板未提供的槽位克隆原型)
func MergeNode(user, tmpl any) any {
tmplMap, ok := tmpl.(map[string]any)
if !ok {
return user
}
fieldType, _ := tmplMap["type"].(string)
// 用户节点已带 type:按用户类型递归合并子节点;标量叶子已完备,直接返回
if userMap, ok := user.(map[string]any); ok {
if ut, isTpl := userMap["type"]; isTpl {
switch gconv.String(ut) {
case TypeObject:
return mergeObject(userMap, tmplMap)
case TypeArray:
return mergeArray(userMap, tmplMap)
default:
return userMap
}
}
}
switch fieldType {
case TypeString, TypeBool, TypeNumber, TypeNumberInt, TypeNumberFloat:
return wrapScalarNode(tmplMap, user)
case TypeObject:
return mergeObject(user, tmplMap)
case TypeArray:
return mergeArray(user, tmplMap)
default:
return user
}
}
// wrapScalarNode 把用户标量值包装为 {type, value} 节点;用户值为空时回落模板 value/defaultValue
func wrapScalarNode(tmplMap map[string]any, user any) map[string]any {
val := user
if m, ok := user.(map[string]any); ok {
if v, has := m["value"]; has {
val = v
} else {
val = nil
}
}
node := map[string]any{"type": normalizeScalarType(gconv.String(tmplMap["type"]))}
if hasUsableValue(val) {
node["value"] = val
} else if v, has := tmplMap["value"]; has {
node["value"] = v
} else if d, has := tmplMap["defaultValue"]; has {
node["value"] = d
}
return node
}
// normalizeScalarType integer/float 统一为 numberresolveField 只分发 string/boolean/number
func normalizeScalarType(t string) string {
if t == TypeNumberInt || t == TypeNumberFloat {
return TypeNumber
}
return t
}
// mergeObject 合并对象节点:模板 attrs 缺省字段补进用户容器(模板默认值保留)
func mergeObject(user any, tmplMap map[string]any) map[string]any {
tmplAttrs, _ := tmplMap["attrs"].(map[string]any)
node := map[string]any{"type": TypeObject}
if tmplAttrs == nil {
node["attrs"] = user
return node
}
var container map[string]any
switch u := user.(type) {
case map[string]any:
if a, has := u["attrs"]; has {
if am, ok := a.(map[string]any); ok {
container = copyMap(am)
} else {
container = map[string]any{}
}
} else if v, has := u["value"]; has {
if vm, ok := v.(map[string]any); ok {
container = copyMap(vm)
} else {
container = map[string]any{}
}
} else {
container = copyMap(u)
}
default:
container = map[string]any{}
}
for k, subTmpl := range tmplAttrs {
if _, has := container[k]; !has {
container[k] = DeepCopyNode(subTmpl)
} else {
container[k] = MergeNode(container[k], subTmpl)
}
}
node["attrs"] = container
return node
}
// mergeArray 合并数组节点:用户 enumValues/attrs 元素逐个与模板元素原型合并。
// 用户未填的槽位(sjson null 填充)用模板对应槽位原型补位,保留下标不塌缩。
func mergeArray(user any, tmplMap map[string]any) map[string]any {
proto := arrayElementTemplate(tmplMap)
tmplSlots, _ := tmplMap["enumValues"].([]any) // 模板各槽位原型,按下标一一对应
node := map[string]any{"type": TypeArray}
slotProto := func(i int) any {
if i < len(tmplSlots) {
return tmplSlots[i]
}
return proto
}
switch u := user.(type) {
case map[string]any:
if evs, ok := u["enumValues"].([]any); ok {
out := make([]any, len(evs))
for i, ev := range evs {
if ev == nil {
// 保留 null 槽位以维持下标;解析阶段 resolveArray 会丢弃空元素,
// 避免用模板原型填充 null 而物化出"幽灵"元素(用户未填的数组槽位)
out[i] = nil
continue
}
out[i] = mergeArrayElement(ev, slotProto(i))
}
node["enumValues"] = out
} else if a, ok := u["attrs"].([]any); ok {
out := make([]any, len(a))
for i, item := range a {
out[i] = mergeArrayElement(item, proto)
}
node["attrs"] = out
} else if v, has := u["value"]; has {
node["value"] = v
} else if len(u) > 0 {
node["attrs"] = []any{mergeArrayElement(u, proto)}
}
case []any:
if isSchemaElementList(u) {
out := make([]any, len(u))
for i, item := range u {
out[i] = mergeArrayElement(item, proto)
}
node["attrs"] = out
} else {
node["value"] = u
}
default:
node["value"] = user
}
return node
}
// mergeArrayElement 合并单个数组元素:元素缺 type 时按槽位原型补默认字段。
// slotProto 可为 nil(模板未提供槽位原型时)→ 原样返回用户元素
func mergeArrayElement(ev any, slotProto any) any {
if ev == nil {
return ev
}
proto, _ := slotProto.(map[string]any)
if proto == nil {
return ev
}
evMap, ok := ev.(map[string]any)
if !ok {
return ev
}
if _, isTpl := evMap["type"]; isTpl {
return ev
}
// 元素原型自身是包装节点(嵌套对象/数组)→ 按模板类型合并
if t, has := proto["type"].(string); has && t != "" {
return MergeNode(ev, proto)
}
protoAttrs, _ := proto["attrs"].(map[string]any)
var container map[string]any
if a, has := evMap["attrs"]; has {
if am, ok := a.(map[string]any); ok {
container = copyMap(am)
} else {
container = map[string]any{}
}
} else {
container = copyMap(evMap)
}
for k, subTmpl := range protoAttrs {
if _, has := container[k]; !has {
container[k] = DeepCopyNode(subTmpl)
} else {
container[k] = MergeNode(container[k], subTmpl)
}
}
return map[string]any{"attrs": container}
}
// isSchemaElementList 判断 []any 是否为 schema 元素列表(元素均为 map,且带 type 或 attrs 包装)
func isSchemaElementList(list []any) bool {
if len(list) == 0 {
return false
}
for _, item := range list {
m, ok := item.(map[string]any)
if !ok {
return false
}
if _, hasType := m["type"]; !hasType {
if _, hasAttrs := m["attrs"]; !hasAttrs {
return false
}
}
}
return true
}
// DeepCopyNode 深拷贝任意嵌套节点(模板节点整体拷贝进 src 时使用,避免共享引用被后续回填修改)
func DeepCopyNode(v any) any {
switch val := v.(type) {
case map[string]any:
out := make(map[string]any, len(val))
for k, sub := range val {
out[k] = DeepCopyNode(sub)
}
return out
case []any:
out := make([]any, len(val))
for i, sub := range val {
out[i] = DeepCopyNode(sub)
}
return out
default:
return v
}
}
// hasUsableValue 值是否可写入节点 value0/false 视为有效值
func hasUsableValue(v any) bool {
switch val := v.(type) {
case nil:
return false
case string:
return val != ""
case []any:
return len(val) > 0
case map[string]any:
return len(val) > 0
default:
return true
}
}