feat: 支持工作流断点续跑并拆分错误信息存储
- 新增同会话+同工作流最近执行失败且参数一致时断点续跑逻辑 - exec_workflow/exec_chat 新增 error 字段存储原始错误,error_message 仅存友好提示 - 新增 UpdateExecChatReq 与 exec_chat_dao Update 方法 - 新增 GetLatestBySessionAndFlow 查询最近执行记录 - 修正 ListDates 分组与排序 SQL 表达式 - 新增 pipeline 配置结构,删除旧设计文档
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
// Package split_segment 工作流前置处理器:把聚合了脚本转写各段参数的扁平请求体按段拆成多份,
|
||||
// 供分段模型请求并行执行。
|
||||
//
|
||||
// 脚本转写节点(split_shots_pipeline 产出)的下游模型节点单源引用其字段时,
|
||||
// BuildModelRequestBody 把这些字段的数组值收集进 __segment_fields(逗号分隔的扁平路径),
|
||||
// 处理器按段序把每份参数拆成独立请求体,段与段之间互不影响(深拷贝)。
|
||||
// 处理器自包含(算法随处理器走,不依赖业务包),通过 init 注册进 processor 注册表。
|
||||
package split_segment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"ai-agent/workflow/service/flow/processor"
|
||||
)
|
||||
|
||||
// ProcessorName 处理器注册名,供模型节点前置工具分发按名判定入参形态(扁平请求体 + __segment_fields 标记)。
|
||||
const ProcessorName = "split_segment"
|
||||
|
||||
func init() {
|
||||
processor.Register(SplitSegmentProcessor())
|
||||
}
|
||||
|
||||
// SplitSegmentProcessor 按段拆分模型请求参数的前置处理器。
|
||||
// 入参 args 即扁平模型请求体(BuildModelRequestBody 输出,含 __segment_fields 标记)。
|
||||
func SplitSegmentProcessor() *processor.Processor {
|
||||
return &processor.Processor{
|
||||
Name: ProcessorName,
|
||||
Description: "按段拆分脚本转写聚合的模型请求参数",
|
||||
IsShow: true,
|
||||
Func: func(ctx context.Context, args map[string]any) (any, error) {
|
||||
if args == nil {
|
||||
return nil, fmt.Errorf("缺少模型请求参数")
|
||||
}
|
||||
return SplitSegmentModelParams(args)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// segField 描述一个待拆分的分段字段
|
||||
type segField struct {
|
||||
path string // 扁平点分路径
|
||||
items []any // 按段序排列的元素(元素为 nil 表示该段无此字段)
|
||||
}
|
||||
|
||||
// SplitSegmentModelParams 把扁平模型请求体按段拆成多份,供分段请求模型使用。
|
||||
// 处理流程:
|
||||
// 1. 读取 __segment_fields(逗号分隔的扁平路径),无标记则返回单份参数;
|
||||
// 2. 逐个解析分段字段的数组值,要求非空且各字段长度一致(不一致直接报错,避免按错位拆分);
|
||||
// 3. 按段数深拷贝整份参数,各段覆盖其分段字段为该段元素;元素为 nil(该段无此字段)时删除该键,
|
||||
// 避免把 null 传给模型网关。
|
||||
//
|
||||
// 返回的每份参数仍保留 __segment_fields(由 invokePreTool 统一剥离 __ 前缀内部键)。
|
||||
func SplitSegmentModelParams(rawParams map[string]any) ([]map[string]any, error) {
|
||||
paths := segmentFieldsFromArgs(rawParams)
|
||||
if len(paths) == 0 {
|
||||
return []map[string]any{rawParams}, nil
|
||||
}
|
||||
|
||||
fields := make([]segField, 0, len(paths))
|
||||
segmentCount := 0
|
||||
for _, path := range paths {
|
||||
v, has := rawParams[path]
|
||||
if !has {
|
||||
return nil, fmt.Errorf("分段字段[%s]缺失", path)
|
||||
}
|
||||
items, ok := asItems(v)
|
||||
if !ok || len(items) == 0 {
|
||||
return nil, fmt.Errorf("分段字段[%s]值不是数组或为空", path)
|
||||
}
|
||||
if segmentCount == 0 {
|
||||
segmentCount = len(items)
|
||||
} else if len(items) != segmentCount {
|
||||
return nil, fmt.Errorf("分段字段长度不一致: %s=%d, 期望 %d", path, len(items), segmentCount)
|
||||
}
|
||||
fields = append(fields, segField{path: path, items: items})
|
||||
}
|
||||
|
||||
batches := make([]map[string]any, 0, segmentCount)
|
||||
for i := 0; i < segmentCount; i++ {
|
||||
batch, _ := deepCopyAny(rawParams).(map[string]any)
|
||||
for _, f := range fields {
|
||||
if f.items[i] == nil {
|
||||
delete(batch, f.path)
|
||||
continue
|
||||
}
|
||||
batch[f.path] = f.items[i]
|
||||
}
|
||||
batches = append(batches, batch)
|
||||
}
|
||||
return batches, nil
|
||||
}
|
||||
|
||||
// segmentFieldsFromArgs 解析 __segment_fields 标记为扁平路径列表,空值返回 nil。
|
||||
func segmentFieldsFromArgs(args map[string]any) []string {
|
||||
raw, ok := args["__segment_fields"].(string)
|
||||
if !ok || strings.TrimSpace(raw) == "" {
|
||||
return nil
|
||||
}
|
||||
var paths []string
|
||||
for _, p := range strings.Split(raw, ",") {
|
||||
if t := strings.TrimSpace(p); t != "" {
|
||||
paths = append(paths, t)
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// asItems 取分段字段值作为元素列表:[]any 直接返回(元素保持原样,nil 段位保留);
|
||||
// 类型化切片([]string 等)经反射逐元素转 any。非切片返回 false。
|
||||
func asItems(v any) ([]any, bool) {
|
||||
if list, ok := v.([]any); ok {
|
||||
return list, true
|
||||
}
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) {
|
||||
n := rv.Len()
|
||||
if n == 0 {
|
||||
return nil, true
|
||||
}
|
||||
items := make([]any, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
items = append(items, rv.Index(i).Interface())
|
||||
}
|
||||
return items, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// deepCopyAny 深拷贝 map[string]any / []any / 类型化切片嵌套结构,避免批次之间互相影响
|
||||
func deepCopyAny(v any) any {
|
||||
switch val := v.(type) {
|
||||
case map[string]any:
|
||||
res := make(map[string]any, len(val))
|
||||
for k, child := range val {
|
||||
res[k] = deepCopyAny(child)
|
||||
}
|
||||
return res
|
||||
case []any:
|
||||
res := make([]any, len(val))
|
||||
for i, child := range val {
|
||||
res[i] = deepCopyAny(child)
|
||||
}
|
||||
return res
|
||||
}
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.IsValid() && (rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array) {
|
||||
n := rv.Len()
|
||||
out := reflect.MakeSlice(rv.Type(), n, n)
|
||||
for i := 0; i < n; i++ {
|
||||
d := deepCopyAny(rv.Index(i).Interface())
|
||||
if d != nil {
|
||||
out.Index(i).Set(reflect.ValueOf(d))
|
||||
}
|
||||
}
|
||||
return out.Interface()
|
||||
}
|
||||
return v
|
||||
}
|
||||
Reference in New Issue
Block a user