Files
ai-agent/workflow/service/flow/lambda_value_source.go
T
19904408334 cef837a35c feat: 支持工作流断点续跑并拆分错误信息存储
- 新增同会话+同工作流最近执行失败且参数一致时断点续跑逻辑
- exec_workflow/exec_chat 新增 error 字段存储原始错误,error_message 仅存友好提示
- 新增 UpdateExecChatReq 与 exec_chat_dao Update 方法
- 新增 GetLatestBySessionAndFlow 查询最近执行记录
- 修正 ListDates 分组与排序 SQL 表达式
- 新增 pipeline 配置结构,删除旧设计文档
2026-08-21 09:54:06 +08:00

701 lines
22 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 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: "<jsonType>", 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:
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 {
if field == "templates" {
value = nodeConfig.Templates
} 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.Numbergconv 反序列化数字的运行时类型)与字符串 "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: <schemaEditorType>} 包装节点
func isSchemaWrapperNode(m map[string]interface{}) bool {
t, ok := m["type"].(string)
return ok && isSchemaEditorType(t)
}
// isSchemaNodeEmpty 判断 schema 节点是否已无有效内容:
// 标量看 value0/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 为 Pathvalue 为参数值;保留 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
}
}