package flow import ( "ai-agent/workflow/consts/node" flowDto "ai-agent/workflow/model/dto/flow" "ai-agent/workflow/model/entity" "context" "encoding/json" "reflect" "regexp" "strconv" "strings" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/glog" "github.com/gogf/gf/v2/util/gconv" "github.com/tidwall/gjson" ) var ( // 匹配 [数字] regNumIndex = regexp.MustCompile(`\[\d+\]`) // 匹配 .attrs regAttrs = regexp.MustCompile(`\.attrs`) // 匹配带捕获组的数组下标,转扁平点分路径用 arrayIndexPath = regexp.MustCompile(`\[(\d+)\]`) ) // CleanFieldPath 清理字段路径:移除 .attrs、数字下标转为 [*] // 示例:usage.attrs.total_tokens → usage.total_tokens // 示例:choices.attrs[0].attrs.message.attrs.content → choices[*].message.content func CleanFieldPath(path string) string { //// 1. 替换 [数字] 为 [*] //s := regNumIndex.ReplaceAllString(path, `.#`) //// 2. 移除所有 .attrs //s = regAttrs.ReplaceAllString(s, "") index := CleanFieldPathReplaceNumIndex(path) attrs := CleanFieldPathRemoveAttrs(index) return attrs } func CleanFieldPathReplaceNumIndex(path string) string { // 1. 替换 [数字] 为 [*] s := regNumIndex.ReplaceAllString(path, `.#`) return s } func CleanFieldPathRemoveAttrs(path string) string { // 2. 移除所有 .attrs s := regAttrs.ReplaceAllString(path, "") return s } // UnwrapSchemaWrapper 递归剥掉 json-schema-editor 输出的 {type, value/attrs} 包裹层, // 只保留干净的 key/value 嵌套结构。 // 示例: // // {"a": {"type":"string","value":"hi"}} → {"a": "hi"} // {"b": {"type":"object","attrs":{"c":1}}} → {"b": {"c": 1}} // {"arr": {"type":"array","attrs":[{"type":"number","value":1}]}} → {"arr": [1]} func UnwrapSchemaWrapper(v any) any { switch val := v.(type) { case map[string]any: // 识别包裹节点:{type: "", value/attrs: <实际值>, ...} if t, ok := val["type"].(string); ok && isSchemaEditorType(t) { dataKey := "value" if t == "object" || t == "array" { dataKey = "attrs" } if raw, has := val[dataKey]; has { return UnwrapSchemaWrapper(raw) } } res := make(map[string]any, len(val)) for k, child := range val { res[k] = UnwrapSchemaWrapper(child) } return res case []any: res := make([]any, len(val)) for i, item := range val { res[i] = UnwrapSchemaWrapper(item) } return res default: return val } } // isSchemaEditorType 是否为 json-schema-editor 的 6 种类型标识 func isSchemaEditorType(t string) bool { switch t { case "string", "number", "boolean", "null", "object", "array": return true } return false } // MapResultByTemplate 按 template 定义的结构,从 source 中拷贝对应字段的值。 // 只保留 template 里出现的字段:对象字段按同名字段递归拷贝,数组字段按模板元素结构逐元素过滤,标量字段直接拷贝 source 的值。 func MapResultByTemplate(template map[string]any, source map[string]any) map[string]any { result := make(map[string]any, len(template)) for key, tmplVal := range template { srcVal, ok := source[key] if !ok { continue } if tmplMap, isMap := tmplVal.(map[string]any); isMap { if srcMap, isMap := srcVal.(map[string]any); isMap { result[key] = MapResultByTemplate(tmplMap, srcMap) } continue } if tmplArr, isArr := tmplVal.([]any); isArr { result[key] = mapTemplateArray(tmplArr, srcVal) continue } result[key] = srcVal } return result } // mapTemplateArray 按模板数组的元素结构映射 source 数组: // 模板首元素为对象时,逐元素按 MapResultByTemplate 过滤只保留模板字段; // 模板数组为空或首元素非对象(无法确定元素结构)时,原样拷贝 source 数组。 func mapTemplateArray(tmplArr []any, srcVal any) any { srcList, ok := srcVal.([]any) if !ok || len(tmplArr) == 0 { return srcVal } elemTmpl, ok := tmplArr[0].(map[string]any) if !ok { return srcVal } result := make([]any, 0, len(srcList)) for _, srcElem := range srcList { if srcMap, isMap := srcElem.(map[string]any); isMap { result = append(result, MapResultByTemplate(elemTmpl, srcMap)) } else { result = append(result, srcElem) } } return result } // ProcessValueSourceRecursive 递归遍历map,同级同时存在value和valueSource则把value设置为"AA" func ProcessValueSourceRecursive(rawParams map[string]interface{}, globalParams *flowDto.FlowExecutionInput) { walkMap(rawParams, globalParams) } // resolveValueSource 解析 valueSource {nodeId, fieldName} 引用的实际值。 // 返回 (value, refsName, ok);ok=false 表示引用节点不存在或引用值仍为空。 // - 开始/表单节点:OutputConfig 平铺条目按 field == fieldName 匹配(前端约定以 field 为主, // 不兼容 path),直接读 entry 的 value / refsName // - scriptTranscribe 节点:OutputResult 是各段扁平请求参数,按段序收集字段为数组(段位留 nil) // - 其他节点:读 OutputResult 中 fieldName 路径对应的值 func resolveValueSource(global *flowDto.FlowExecutionInput, nodeId, field string) (value any, refsName any, ok bool) { if global == nil || global.ConfigMap == nil { return nil, nil, false } nodeConfig := global.ConfigMap[nodeId] if nodeConfig == nil { return nil, nil, false } switch nodeConfig.NodeCode { case node.NodeTypeStart, node.NodeTypeForm: for _, output := range nodeConfig.OutputConfig { if gconv.String(output["field"]) != field { continue } if !g.IsEmpty(output["value"]) { return output["value"], output["refsName"], true } } case node.NodeTypeScriptTranscribe: // 脚本转写节点 OutputResult 是各段扁平请求参数(split_shots_pipeline 产出,key 为字面量 // prompt/duration/seed 等),按段序读取 output[field] 收集为数组,供分段模型节点整体引用。 // 每段都占一位(字段缺失/为空留 nil),保证数组与段序对齐,供 split_segment 按段取值。 var list []any for _, output := range nodeConfig.OutputResult { list = append(list, output[field]) } for _, v := range list { if !g.IsEmpty(v) { return list, "", true } } default: // templates 是模型节点在前端配置的静态输出模板,不在 OutputResult 中,需单独取 if field == "templates" { if !g.IsEmpty(nodeConfig.Templates) { return nodeConfig.Templates, "", true } return nil, nil, false } for _, output := range nodeConfig.OutputResult { // 模型节点输出记录是单 key 的字面量扁平 key(如 "choices.attrs[0].attrs.delta.attrs.content"), // gjson 会把 . 和 [0] 当结构路径解析,无法命中字面量 key,故先按字面量 key 直接取值; // 未命中再回退 gjson 路径查询(兼容真正嵌套的输出结构)。 if v, has := output[field]; has { value = v } else { value = gjson.Get(gconv.String(output), field).Value() } if !g.IsEmpty(value) { return value, gjson.Get(gconv.String(output), CleanFieldPath("refsName")).Value(), true } } } return nil, nil, false } // walkMap 递归处理map/数组 func walkMap(data interface{}, globalParams *flowDto.FlowExecutionInput) { switch v := data.(type) { case map[string]interface{}: // 有 valueSource:解析引用节点值 if valueSource, hasSource := v["valueSource"]; hasSource { sources := new([]entity.ValueSource) gconv.Structs(valueSource, sources) // 多个引用源:把各源解析出的值拼成 "label: value"(无 label 只拼值),逗号分隔 if len(*sources) > 1 { parts := make([]string, 0, len(*sources)) var refsName any for _, src := range *sources { value, rn, ok := resolveValueSource(globalParams, src.NodeId, src.Field) if !ok || schemaValueEmpty(value) { continue } // 引用非模型节点(开始/表单/HTTP/脚本转写等)时,值按当前字段声明的 type 做类型化转换; // 模型节点值由模型网关处理,复制时不需要转换 if !isModelSourceNode(globalParams, src.NodeId) { value = assignBySchemaType(v, value) } text := toPlainString(value) if src.Label != "" { text = src.Label + ": " + text } parts = append(parts, text) if !g.IsEmpty(rn) && g.IsEmpty(refsName) { refsName = rn } } if len(parts) > 0 { v["value"] = strings.Join(parts, ", ") if !g.IsEmpty(refsName) { v["refsName"] = refsName } return } } else if len(*sources) == 1 { // 单个引用源:保持旧行为,值按原样赋值(非模型节点按声明 type 转换) src := (*sources)[0] value, refsName, ok := resolveValueSource(globalParams, src.NodeId, src.Field) if ok && !isModelSourceNode(globalParams, src.NodeId) { value = assignBySchemaType(v, value) } if ok && !schemaValueEmpty(value) { v["value"] = value if !g.IsEmpty(refsName) { v["refsName"] = refsName } return } } } // 统一兜底:无 valueSource(或解析失败/值为空)时,value 为空或 0 则取 defaultValue if defaultValue, hasDefault := v["defaultValue"]; hasDefault && isEmptyForFallback(v["value"]) && !schemaValueEmpty(defaultValue) { v["value"] = defaultValue } // 递归遍历所有子元素 for _, child := range v { walkMap(child, globalParams) } case []interface{}: // 数组遍历 for _, item := range v { walkMap(item, globalParams) } } } // isEmptyForFallback 兜底场景判空:除 schemaValueEmpty 规则外,数字 0 也视为未填写, // 便于配置了 defaultValue 的字段在值为 0 时用默认值兜底。 // 覆盖 json.Number(gconv 反序列化数字的运行时类型)与字符串 "0"/"0.0"。 func isEmptyForFallback(v interface{}) bool { if schemaValueEmpty(v) { return true } switch val := v.(type) { case float32: return val == 0 case float64: return val == 0 case int: return val == 0 case int8: return val == 0 case int16: return val == 0 case int32: return val == 0 case int64: return val == 0 case uint: return val == 0 case uint8: return val == 0 case uint16: return val == 0 case uint32: return val == 0 case uint64: return val == 0 case json.Number: if f, err := val.Float64(); err == nil { return f == 0 } case string: if f, err := strconv.ParseFloat(val, 64); err == nil { return f == 0 } } return false } // isModelSourceNode 判断引用源节点是否为模型节点(值由模型网关处理,复制时不转换) func isModelSourceNode(global *flowDto.FlowExecutionInput, nodeId string) bool { if global == nil || global.ConfigMap == nil { return false } nodeConfig := global.ConfigMap[nodeId] return nodeConfig != nil && nodeConfig.NodeCode == node.NodeTypeModel } // assignBySchemaType 按当前字段声明的 schema 类型把值类型化: // string 遇数组/对象转 JSON 字符串;number/boolean 解析字符串;object/array 解析 JSON 字符串;其余原样返回 func assignBySchemaType(node map[string]interface{}, value any) any { t, _ := node["type"].(string) return assignByType(t, value) } // assignByType 按字段声明的 type 把值类型化;walkMap 的 schema 节点与 parseMap 的模型参数共用 func assignByType(t string, value any) any { switch t { case "string": return toSchemaString(value) case "number": return toSchemaNumber(value) case "boolean": return toSchemaBool(value) case "object", "array": return toSchemaStruct(value) default: return value } } // toSchemaString 转 string:字符串原样,数组元素拼成字符串(单元素取元素本身,多元素逗号连接),对象序列化为 JSON 字符串 func toSchemaString(v any) any { switch val := v.(type) { case []interface{}: parts := make([]string, 0, len(val)) for _, item := range val { parts = append(parts, toPlainString(item)) } return strings.Join(parts, ",") case map[string]interface{}: if b, err := json.Marshal(val); err == nil { return string(b) } } return v } // toPlainString 把数组元素转成不带括号的纯字符串: // 数组([]any / 类型化切片)逐元素取纯字符串,单元素取元素本身,多元素逗号连接; // 对象序列化为 JSON 字符串;其余原样字符串化。 func toPlainString(v any) string { if s, ok := v.(string); ok { return s } switch val := v.(type) { case []interface{}: parts := make([]string, 0, len(val)) for _, item := range val { parts = append(parts, toPlainString(item)) } return strings.Join(parts, ",") case map[string]interface{}: if b, err := json.Marshal(val); err == nil { return string(b) } } rv := reflect.ValueOf(v) if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) { parts := make([]string, 0, rv.Len()) for i := 0; i < rv.Len(); i++ { parts = append(parts, toPlainString(rv.Index(i).Interface())) } return strings.Join(parts, ",") } if b, err := json.Marshal(v); err == nil { return string(b) } return gconv.String(v) } // toSchemaNumber 转 number:数字原样,字符串尝试解析为 float64,失败原样返回 func toSchemaNumber(v any) any { if s, ok := v.(string); ok { if f, err := strconv.ParseFloat(s, 64); err == nil { return f } } return v } // toSchemaBool 转 boolean:布尔原样,字符串尝试解析为 bool,失败原样返回 func toSchemaBool(v any) any { if s, ok := v.(string); ok { if b, err := strconv.ParseBool(s); err == nil { return b } } return v } // toSchemaStruct 转 object/array:合法 JSON 字符串解析为结构化数据,否则原样返回 func toSchemaStruct(v any) any { s, ok := v.(string) if !ok { return v } if !json.Valid([]byte(s)) { return v } var out any if err := json.Unmarshal([]byte(s), &out); err != nil { return v } return out } // CleanEmptyModelParams 剔除模型请求参数中 value 为空的字段; // 数组/枚举(attrs / enumValues)元素整体为空时移除整个元素。0/false 视为有效值。 func CleanEmptyModelParams(params map[string]interface{}) { cleanSchemaMap(params) } // cleanSchemaMap 递归清理普通 map:包装节点按 schema 语义清理,空字段删除 func cleanSchemaMap(m map[string]interface{}) { for key, val := range m { switch v := val.(type) { case map[string]interface{}: if isSchemaWrapperNode(v) { cleanSchemaWrapper(v) if isSchemaNodeEmpty(v) { delete(m, key) } } else { cleanSchemaMap(v) } case []interface{}: m[key] = cleanSchemaSlice(v) } } } // cleanSchemaWrapper 清理单个 {type,...} 包装节点:递归 value / attrs / enumValues 容器 func cleanSchemaWrapper(node map[string]interface{}) { if mv, ok := node["value"].(map[string]interface{}); ok { cleanSchemaMap(mv) } if lv, ok := node["value"].([]interface{}); ok { node["value"] = cleanSchemaSlice(lv) } if attrs, ok := node["attrs"].(map[string]interface{}); ok { cleanSchemaMap(attrs) } if attrs, ok := node["attrs"].([]interface{}); ok { node["attrs"] = cleanSchemaSlice(attrs) } if evs, ok := node["enumValues"].([]interface{}); ok { node["enumValues"] = cleanSchemaSlice(evs) } } // cleanSchemaSlice 清理数组/枚举元素,元素为包装节点且整体为空时移除 func cleanSchemaSlice(list []interface{}) []interface{} { i := 0 for i < len(list) { if item, ok := list[i].(map[string]interface{}); ok { if isSchemaWrapperNode(item) { cleanSchemaWrapper(item) if isSchemaNodeEmpty(item) { list = append(list[:i], list[i+1:]...) continue } } else { cleanSchemaMap(item) } } i++ } return list } // isSchemaWrapperNode 是否为 {type: } 包装节点 func isSchemaWrapperNode(m map[string]interface{}) bool { t, ok := m["type"].(string) return ok && isSchemaEditorType(t) } // isSchemaNodeEmpty 判断 schema 节点是否已无有效内容: // 标量看 value(0/false 有效);object/array 看 value/attrs/enumValues 容器是否都为空 func isSchemaNodeEmpty(node map[string]interface{}) bool { t, _ := node["type"].(string) switch t { case "object": return schemaContainerEmpty(node, "value") && schemaContainerEmpty(node, "attrs") case "array": return schemaContainerEmpty(node, "value") && schemaContainerEmpty(node, "attrs") && schemaContainerEmpty(node, "enumValues") default: return schemaValueEmpty(node["value"]) } } // schemaContainerEmpty 容器(value/attrs/enumValues)是否为空 func schemaContainerEmpty(node map[string]interface{}, key string) bool { switch v := node[key].(type) { case []interface{}: return len(v) == 0 case map[string]interface{}: return len(v) == 0 default: return v == nil } } // schemaValueEmpty 值是否为空;0/false 视为有效值不剔除 func schemaValueEmpty(v interface{}) bool { switch val := v.(type) { case nil: return true case string: return val == "" case []interface{}: return len(val) == 0 case map[string]interface{}: return len(val) == 0 default: return false } } // BuildModelRequestBody 从参数定义 + 全局执行上下文构建最终嵌套 JSON 请求体。 func BuildModelRequestBody(params []entity.FlowModelParams, globalParams *flowDto.FlowExecutionInput) (map[string]interface{}, error) { // 1. 解析引用、过滤空值 resolved := parseMap(params, globalParams) // 2. 转扁平路径映射 flat := toFlatMap(resolved) // 3. 引用了脚本转写节点的字段打内部标记 __segment_fields(逗号分隔的扁平路径), // 供前置处理器 split_segment 按段拆批;__ 前缀内部键由 invokePreTool 统一剥离,不传给模型网关 if seg := segmentFields(resolved, globalParams); len(seg) > 0 { flat["__segment_fields"] = strings.Join(seg, ",") } return flat, nil } // segmentFields 收集引用了脚本转写节点的字段扁平路径(分段字段)。 // 脚本转写节点按段产出一份扁平参数列表,下游模型节点单源引用其字段时, // 值按段序聚合成数组(见 resolveValueSource 的 scriptTranscribe 分支),需随批拆分。 func segmentFields(resolved []entity.FlowModelParams, globalParams *flowDto.FlowExecutionInput) []string { if globalParams == nil || globalParams.ConfigMap == nil { return nil } var fields []string for _, p := range resolved { if g.IsEmpty(p.Path) || len(p.ValueSource) != 1 { continue } src := p.ValueSource[0] if nodeConfig := globalParams.ConfigMap[src.NodeId]; nodeConfig != nil && nodeConfig.NodeCode == node.NodeTypeScriptTranscribe { fields = append(fields, flatPath(p.Path)) } } return fields } // parseMap 解析模型请求参数 func parseMap(data []entity.FlowModelParams, globalParams *flowDto.FlowExecutionInput) []entity.FlowModelParams { newData := make([]entity.FlowModelParams, 0, len(data)) for _, item := range data { var d entity.FlowModelParams d.Path = item.Path d.Type = item.Type // 无引用源:直接取静态值 if g.IsEmpty(item.ValueSource) { if isParamEmpty(item.Value) { continue } d = item newData = append(newData, d) continue } // 有引用源:单源保持旧行为;多源把各源解析出的值拼成 "label: value"(无 label 只拼值),逗号分隔 var value, refsName any if len(item.ValueSource) > 1 { parts := make([]string, 0, len(item.ValueSource)) for _, src := range item.ValueSource { v, rn, ok := resolveValueSource(globalParams, src.NodeId, src.Field) if !ok || isParamEmpty(v) { continue } // 模型解析时引用其他节点的值不做类型化转换; // 多引用源需拼接为字符串,用 toPlainString 渲染(数组取元素去括号) text := toPlainString(v) if src.Label != "" { text = src.Label + ": " + text } parts = append(parts, text) if !g.IsEmpty(rn) && g.IsEmpty(refsName) { refsName = rn } } if len(parts) == 0 { // 解析失败不静默,留日志便于排查引用丢失 glog.Debugf(context.Background(), "resolve value source failed, nodeId=%+v path=%s", item.ValueSource, item.Path) continue } d.Value = strings.Join(parts, ", ") d.RefsName = gconv.String(refsName) d.ValueSource = item.ValueSource newData = append(newData, d) continue } // 单个引用源:解析取非空值;模型解析时引用其他节点的数组值不做类型化转换,整体传给模型 src := item.ValueSource[0] value, refsName, ok := resolveValueSource(globalParams, src.NodeId, src.Field) if !ok || isParamEmpty(value) { // 解析失败不静默,留日志便于排查引用丢失 glog.Debugf(context.Background(), "resolve value source failed, nodeId=%+v path=%s", item.ValueSource, item.Path) continue } d.Value = value d.RefsName = gconv.String(refsName) d.ValueSource = item.ValueSource newData = append(newData, d) } return newData } // toFlatMap 将解析后的参数列表转为 sjson 可用的扁平路径映射。 // key 为 Path,value 为参数值;保留 RefsName 供上层追踪引用来源。 func toFlatMap(params []entity.FlowModelParams) map[string]interface{} { m := make(map[string]interface{}, len(params)) for _, p := range params { if g.IsEmpty(p.Path) { continue } m[flatPath(p.Path)] = p.Value } return m } // flatPath 把数组下标路径转扁平点分路径:a[0].b → a.0.b。 func flatPath(path string) string { return arrayIndexPath.ReplaceAllString(path, `.$1`) } // isParamEmpty 判断参数值是否为"空"。 // 仅 nil、空字符串、空切片/映射视为空;0、false 等零值是合法值,保留。 func isParamEmpty(v interface{}) bool { if v == nil { return true } switch val := v.(type) { case string: return val == "" case []byte: return len(val) == 0 case []interface{}: return len(val) == 0 case map[string]interface{}: return len(val) == 0 default: // 数字、布尔、结构体等一律视为非空 return false } }