Files
19904408334 d699f7ce14 feat(workflow): 增加工作流计费与执行生命周期管理
- 新增计费模块:执行开始建单、终态结算/取消/失败处理,支持按条/按秒/按token计费
- 新增执行生命周期跟踪:优雅关停时取消运行中执行并等待落库
- 新增异步任务等待/通知机制(Wait/Notify)
- 重构执行记录落库与进度上报,统一失败分类与重试语义
- 重命名文件:async_task.go→async.go、flow_checkpoint_store.go→exec_checkpoint.go、flow_graph_util.go→exec_record.go
- 更新 .gitignore 与数据库密码配置
2026-09-03 13:22:22 +08:00

228 lines
6.3 KiB
Go

package values
import (
"encoding/json"
"reflect"
"strconv"
"strings"
"github.com/gogf/gf/v2/util/gconv"
)
// 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
}
// 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
}
// 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
}
}