diff --git a/digital-human/model/dto/model_test_dto.go b/digital-human/model/dto/model_test_dto.go new file mode 100644 index 0000000..2346dff --- /dev/null +++ b/digital-human/model/dto/model_test_dto.go @@ -0,0 +1,34 @@ +package dto + +import ( + "gitea.redpowerfuture.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" +) + +// TestModelStreamReq 测试模型流式调用请求(流式调用上游 → 全量一次返回) +type TestModelStreamReq struct { + g.Meta `path:"/testModelStream" method:"post" tags:"模型流测试" summary:"测试模型流式调用" dc:"透传调用model-gateway的流式接口(上游流式调用,缓冲后全量一次返回)"` + ModelName string `json:"modelName" v:"required#modelName不能为空" dc:"模型名称"` + BizName string `json:"bizName" dc:"业务名称"` + SessionId string `json:"sessionId" v:"required#sessionId不能为空" dc:"会话ID"` + RequestParams map[string]any `json:"requestParams" v:"required#请求参数不能为空" dc:"模型请求参数"` +} + +// TestModelStreamAAReq 测试模型流式调用请求(流式调用上游 → SSE 分块返回) +type TestModelStreamAAReq struct { + g.Meta `path:"/testModelStreamAA" method:"post" tags:"模型流测试" summary:"测试模型流式调用AA" dc:"透传调用model-gateway的SSE流式接口(上游流式调用,逐分片SSE返回)"` + ModelName string `json:"modelName" v:"required#modelName不能为空" dc:"模型名称"` + BizName string `json:"bizName" dc:"业务名称"` + SessionId string `json:"sessionId" v:"required#sessionId不能为空" dc:"会话ID"` + RequestParams map[string]any `json:"requestParams" v:"required#请求参数不能为空" dc:"模型请求参数"` +} + +// TestModelStreamRes 测试模型流式调用响应(实际通过RawWriter直接写出,该结构体仅占位) +type TestModelStreamRes struct { + *beans.ResponseEmpty +} + +// TestModelStreamAARes 测试模型流式调用AA响应(实际通过RawWriter直接写出,该结构体仅占位) +type TestModelStreamAARes struct { + *beans.ResponseEmpty +} diff --git a/gateway/file.go b/gateway/file.go new file mode 100644 index 0000000..d4aa7db --- /dev/null +++ b/gateway/file.go @@ -0,0 +1,77 @@ +package gateway + +import ( + "bytes" + "context" + "io" + "mime/multipart" + "net/http" + "strings" + + commonHttp "gitea.redpowerfuture.com/red-future/common/http" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" +) + +// Upload 以 multipart 方式上传文件字节到 OSS,返回可访问 URL。 +// 原签名基于 workflow/model/dto 的 UploadFileBytesReq/Res,抽离时简化为直接传文件名与字节,便于业务侧解耦 workflow。 +func Upload(ctx context.Context, fileName string, fileBytes []byte) (string, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + part, err := writer.CreateFormFile("file", fileName) + if err != nil { + return "", err + } + if _, err = part.Write(fileBytes); err != nil { + return "", err + } + if err = writer.Close(); err != nil { + return "", err + } + + headers := make(map[string]string) + if r := g.RequestFromCtx(ctx); r != nil { + for k, v := range r.Request.Header { + if len(v) > 0 { + headers[k] = v[0] + } + } + } + headers["Content-Type"] = writer.FormDataContentType() + + res := &uploadFileRes{} + if err = commonHttp.Post(ctx, "oss/file/uploadFile", headers, res, body.Bytes()); err != nil { + return "", err + } + return res.FileURL, nil +} + +// uploadFileRes OSS 上传响应的最小字段(原 workflow/model/dto.UploadFileBytesRes 的 URL 部分)。 +type uploadFileRes struct { + FileURL string `json:"fileURL"` +} + +// GetFileBytesFromURL 下载远程文件内容(把 filePrefix 前缀替换为 minioPrefix 后经 GoFrame 客户端下载) +func GetFileBytesFromURL(ctx context.Context, fileUrl string) ([]byte, error) { + newS := strings.ReplaceAll(fileUrl, g.Cfg().MustGet(ctx, "filePrefix").String(), g.Cfg().MustGet(ctx, "minioPrefix").String()) + // 使用 GoFrame 客户端(自带超时、追踪、日志等能力) + resp, err := g.Client().Get(ctx, newS) + if err != nil { + return nil, gerror.Wrapf(err, "failed to request url: %s", newS) + } + defer resp.Close() + + // 校验状态码 + if resp.StatusCode != http.StatusOK { + return nil, gerror.Newf("request failed with status code: %d, url: %s", resp.StatusCode, newS) + } + + // 读取全部内容 + allBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, gerror.Wrapf(err, "failed to read response body, url: %s", fileUrl) + } + + return allBytes, nil +} diff --git a/gateway/model.go b/gateway/model.go new file mode 100644 index 0000000..4f75125 --- /dev/null +++ b/gateway/model.go @@ -0,0 +1,211 @@ +// Package gateway ai-agent 对外部服务的调用层:模型网关(model-gateway)的查询/提交/聊天调用,以及文件下载/上传(OSS)。 +// 从 workflow/service/flow 抽离为独立目录,供工作流节点与视频管线(video/)共用,业务侧不再直接依赖 workflow 引擎。 +package gateway + +import ( + "context" + "fmt" + "net/http" + "time" + + "ai-agent/workflow/consts/model" + "ai-agent/workflow/consts/public" + + "gitea.redpowerfuture.com/red-future/common/beans" + commonHttp "gitea.redpowerfuture.com/red-future/common/http" + gmq "github.com/bjang03/gmq/core/gmq" + "github.com/bjang03/gmq/mq" + "github.com/bjang03/gmq/types" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" + "github.com/google/uuid" +) + +// ListModelManageReq 配置列表 +type ListModelManageReq struct { + *beans.Page `json:"page"` + ModelName string `p:"modelName" json:"modelName" dc:"模型名称(模糊查询,可选)"` + ModelType int `p:"modelType" json:"modelType" dc:"模型类型"` +} + +// ModelManageListItem 模型配置列表项。listModelManage 返回平铺的 model-gateway entity(id 在顶层), +// 与 getModelManage 的 {modelManage:{...}} 嵌套不同,故独立成结构而非复用 GetModelInfoByIdRes。 +type ModelManageListItem struct { + Id int64 `json:"id" dc:"配置ID"` + ModelName string `json:"modelName" dc:"模型名称"` +} + +type ListModelManageRes struct { + List []*ModelManageListItem `json:"list" dc:"列表数据"` + Total int `json:"total" dc:"总数"` +} + +// GetModelInfoByIdReq 查询模型配置 +type GetModelInfoByIdReq struct { + ModelId int64 `json:"id" dc:"模型ID"` +} + +type GetModelInfoByIdRes struct { + ModelManage struct { + Id int64 `json:"id" dc:"模型ID"` + ModelType model.ModelType `json:"modelType" description:"模型类型"` + ModelName string `json:"modelName" description:"模型名称"` + MaxTokens int `json:"maxTokens" description:"最大token数"` + ChatModel *bool `json:"chatModel" description:"是否聊天模型"` + ResponseType model.ResponseType `json:"responseType" description:"返回类型:1同步,2异步,3流"` + Enabled *bool `json:"enabled" description:"是否启用"` + RequestBodyMapping map[string]any `json:"requestBodyMapping" description:"请求体映射(约等于视频模型 schema)"` + RequestBusinessFieldMapping map[string]string `json:"requestBusinessFieldMapping" description:"请求业务字段映射"` + MinDuration int `json:"minDuration" description:"最小时长(秒)"` + MaxDuration int `json:"maxDuration" description:"最大时长(秒)"` + } `json:"modelManage" dc:"模型配置"` +} + +// modelCallTopic 生成异步模型消息主题:带业务标识(bizName/modelId/sessionId)便于排查, +// uuid 保证每次调用唯一,避免并发调用共用同一主题导致结果串扰 +func modelCallTopic(bizName string, modelId int64, sessionId string) string { + return fmt.Sprintf("model-call-%s-%d-%s-%s", bizName, modelId, sessionId, uuid.NewString()) +} + +// ModelCallReq 调用模型网关 +type ModelCallReq struct { + ModelId int64 `json:"modelId" dc:"模型ID"` + BizName string `json:"bizName" dc:"业务名称(调用方模块/系统,用于统计)"` + SessionId string `json:"sessionId" dc:"会话ID"` + RequestParams map[string]any `json:"requestParams" dc:"请求参数"` + BusinessParams map[string]any `json:"businessParams" dc:"业务参数"` + MsgTopic string `json:"msgTopic" dc:"消息主题(异步必要参数)"` +} + +type ModelCallRes struct { + TaskId int64 `json:"id" dc:"任务ID"` + State int8 `json:"state" dc:"状态"` + TotalTokens int64 `json:"totalTokens" dc:"总token"` + PromptTokens int64 `json:"promptTokens" dc:"输入token"` + CompletionTokens int64 `json:"completionTokens" dc:"输出token"` + Tools []ModelTool `json:"tools" dc:"工具"` + Content map[string]any `json:"content" dc:"内容"` + Cost float64 `json:"cost" dc:"费用(元)"` + ErrorMsg string `json:"errorMsg" dc:"错误消息"` +} + +type ModelTool struct { + Id string `json:"id" dc:"工具ID"` + Type string `json:"type" dc:"工具类型"` + Function struct { + Name string `json:"name"` + Arguments string `json:"arguments"` + } `json:"function"` +} + +// requestHeaders 透传当前 HTTP 请求头(鉴权/链路信息)。 +// 浏览器 WebSocket 握手无法携带 Authorization 头,前端把 token 放在握手 URL query(?token=)里; +// 若请求头没有 Authorization,则从 query 补回,保证下游(model-gateway → admin-go)能拿到用户 token。 +func requestHeaders(ctx context.Context) map[string]string { + headers := make(map[string]string) + if r := g.RequestFromCtx(ctx); r != nil { + for k, v := range r.Request.Header { + if len(v) > 0 { + headers[k] = v[0] + } + } + if headers["Authorization"] == "" { + if t := r.Request.URL.Query().Get("token"); t != "" { + headers["Authorization"] = "Bearer " + t + } + } + } + return headers +} + +// ListModelManage 配置列表 +func ListModelManage(ctx context.Context, req *ListModelManageReq) (res *ListModelManageRes, err error) { + res = new(ListModelManageRes) + err = commonHttp.Get(ctx, "model-gateway/model/manage/listModelManage", requestHeaders(ctx), res, req) + return +} + +// GetModelInfoById 查询模型配置 +func GetModelInfoById(ctx context.Context, req *GetModelInfoByIdReq) (res *GetModelInfoByIdRes, err error) { + res = new(GetModelInfoByIdRes) + err = commonHttp.Get(ctx, "model-gateway/model/manage/getModelManage", requestHeaders(ctx), res, req) + return +} + +// ModelCallResult 调模型网关生成一段内容并等待结果。 +// 视频等异步模型为"提交+等待"一体:内部订阅 GMQ 直到结果返回。 +func ModelCallResult(ctx context.Context, modelId int64, responseType model.ResponseType, sessionId string, requestParams map[string]any, businessParams map[string]any) (responseParams *ModelCallRes, err error) { + // 异步模型必须绑定消息主题接收结果:自动生成唯一主题, + // 带业务标识(bizName/modelId/sessionId)便于排查,每次调用唯一避免并发串结果 + msgTopic := "" + if *responseType == *model.ResponseTypeAsync.Code() { + msgTopic = modelCallTopic(g.Cfg().MustGet(ctx, "server.name").String(), modelId, sessionId) + } + + req := ModelCallReq{ + ModelId: modelId, + BizName: g.Cfg().MustGet(ctx, "server.name").String(), + SessionId: sessionId, + RequestParams: requestParams, + BusinessParams: businessParams, + MsgTopic: msgTopic, + } + // 2. 克隆 commonHttp 客户端(保留 Consul 服务发现),显式设置超时和 ResponseHeaderTimeout + client := commonHttp.Httpclient.Clone() + client.SetTimeout(30 * time.Minute) + if tr, ok := client.Transport.(*http.Transport); ok { + tr.ResponseHeaderTimeout = 30 * time.Minute + } + + res := new(ModelCallRes) + err = commonHttp.Post(ctx, "model-gateway/model/call/modelCall", requestHeaders(ctx), res, &req) + if err != nil { + return nil, err + } + if g.IsEmpty(res.TaskId) || !g.IsEmpty(res.ErrorMsg) { + return nil, fmt.Errorf("创建模型任务失败:%v", res.ErrorMsg) + } + // 3. 订阅模型结果(异步模型) + if *responseType == *model.ResponseTypeAsync.Code() { + resultCh := make(chan *ModelCallRes, 1) + errCh := make(chan error, 1) + + _, err = gmq.GetGmq(public.GmqMsgPluginsName).GmqSubscribe(ctx, &mq.NatsSubMessage{ + SubMessage: types.SubMessage{ + Topic: msgTopic, + ConsumerName: fmt.Sprintf("model-call-result-%s", uuid.NewString()), + AutoAck: false, + AutoUnsubscribe: true, + FetchCount: 1, + HandleFunc: func(ctx context.Context, msg any) error { + r := new(ModelCallRes) + if err := gconv.Struct(msg, r); err != nil { + errCh <- err + return nil + } + if g.IsEmpty(r.TaskId) || !g.IsEmpty(r.ErrorMsg) { + errCh <- fmt.Errorf("创建模型任务失败:%v", r.ErrorMsg) + return nil + } + resultCh <- r + return nil + }, + }, + Durable: true, + }) + if err != nil { + return + } + + select { + case responseParams = <-resultCh: + case err = <-errCh: + return nil, err + case <-ctx.Done(): + return nil, ctx.Err() + } + return + } + + return res, nil +} diff --git a/gateway/model_stream.go b/gateway/model_stream.go new file mode 100644 index 0000000..b138caf --- /dev/null +++ b/gateway/model_stream.go @@ -0,0 +1,157 @@ +package gateway + +import ( + "bufio" + "context" + "encoding/json" + "io" + "strings" + + commonHttp "gitea.redpowerfuture.com/red-future/common/http" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +// ModelCallStreamReq 调用模型网关流式(client 侧请求体) +type ModelCallStreamReq struct { + ModelId int64 `json:"modelId" dc:"模型ID"` + BizName string `json:"bizName" dc:"业务名称"` + SessionId string `json:"sessionId" dc:"会话ID"` + RequestParams map[string]any `json:"requestParams" dc:"请求参数(模板字段)"` + BusinessParams map[string]any `json:"businessParams" dc:"业务参数"` +} + +// ModelCallStream 流式调用模型网关:POST /modelCallStream,逐 chunk 调 onChunk(文本增量转发), +// 流结束返回 ModelCallRes:Content 为累加全量文本({"respBody": ...}),Tools 为流末工具列表。 +func ModelCallStream(ctx context.Context, modelId int64, sessionId string, requestParams, businessParams map[string]any, onChunk func(chunk map[string]any) error) (*ModelCallRes, error) { + req := ModelCallStreamReq{ + ModelId: modelId, + BizName: g.Cfg().MustGet(ctx, "server.name").String(), + SessionId: sessionId, + RequestParams: requestParams, + BusinessParams: businessParams, + } + body, err := commonHttp.PostStream(ctx, "model-gateway/model/call/modelCallStream", requestHeaders(ctx), &req) + if err != nil { + return nil, err + } + defer body.Close() + + res := new(ModelCallRes) + var contentBuf strings.Builder + scanErr := parseSSEStream(ctx, body, func(chunk map[string]any) error { + tools, done := processStreamChunk(chunk) + if done { + res.Tools = tools + // 流末 done 事件携带该次调用累计 token 与费用:纯工具调用步骤无文本 chunk,靠它取数(>0 才覆盖,兼容旧网关无 token/cost 的 done 事件) + if total := gconv.Int64(chunk["totalTokens"]); total > 0 { + res.TotalTokens = total + res.PromptTokens = gconv.Int64(chunk["promptTokens"]) + res.CompletionTokens = gconv.Int64(chunk["completionTokens"]) + } + if cost := gconv.Float64(chunk["cost"]); cost > 0 { + res.Cost = cost + } + return nil + } + if onChunk != nil { + if err := onChunk(chunk); err != nil { + return err + } + } + contentBuf.WriteString(streamTextDelta(chunk)) + // 网关端每个 chunk 携带累计 token 值,取最后一个文本 chunk 即为该步总消耗 + res.TotalTokens = gconv.Int64(chunk["totalTokens"]) + res.PromptTokens = gconv.Int64(chunk["promptTokens"]) + res.CompletionTokens = gconv.Int64(chunk["completionTokens"]) + res.Cost = gconv.Float64(chunk["cost"]) + return nil + }) + if scanErr != nil { + // 流被中断(如 ctx 取消)时仍返回已累计的 res,调用方可读取已产生的 token 正常落库 + return res, scanErr + } + if contentBuf.Len() > 0 { + res.Content = map[string]any{"respBody": contentBuf.String()} + } + return res, nil +} + +// processStreamChunk 处理单个流式 chunk。done 事件({"type":"done","tools":[...]})返回解析出的 +// tools(可为空)且 done=true;其余为文本增量 chunk,返回 done=false。 +func processStreamChunk(chunk map[string]any) (tools []ModelTool, done bool) { + if typ, _ := chunk["type"].(string); typ == "done" { + raw, _ := chunk["tools"].([]any) + for _, t := range raw { + m := gconv.Map(t) + if m == nil { + continue + } + var tool ModelTool + tool.Id = gconv.String(m["id"]) + tool.Type = gconv.String(m["type"]) + if fn := gconv.Map(m["function"]); fn != nil { + tool.Function.Name = gconv.String(fn["name"]) + tool.Function.Arguments = gconv.String(fn["arguments"]) + } + tools = append(tools, tool) + } + return tools, true + } + return nil, false +} + +// streamTextDelta 提取文本增量 chunk 的文本(读 content 子对象,字段名由网关端结构体统一管理) +func streamTextDelta(chunk map[string]any) string { + var sb strings.Builder + if content, ok := chunk["content"].(map[string]any); ok { + for _, v := range content { + if s := gconv.String(v); s != "" { + sb.WriteString(s) + } + } + } + return sb.String() +} + +// parseSSEStream 标准 SSE 解析:逐 data 行 JSON 回调,支持 [DONE] 与上下文取消。 +// 对齐 model-gateway ParseSSEStream 语义:跳过空行、[DONE]、非 data 行。 +func parseSSEStream(ctx context.Context, reader io.Reader, onChunk func(chunk map[string]any) error) error { + scanner := bufio.NewScanner(reader) + scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) + var sb strings.Builder + for scanner.Scan() { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + line := scanner.Text() + trimmed := strings.TrimSpace(line) + if trimmed == "" { + if sb.Len() == 0 { + continue + } + data := sb.String() + sb.Reset() + if data == "[DONE]" { + continue + } + var chunk map[string]any + if err := json.Unmarshal([]byte(data), &chunk); err != nil { + continue + } + if onChunk != nil { + if err := onChunk(chunk); err != nil { + return err + } + } + continue + } + if strings.HasPrefix(line, "data:") { + sb.WriteString(strings.TrimSpace(strings.TrimPrefix(line, "data:"))) + } + } + return scanner.Err() +} diff --git a/go.mod b/go.mod index 79be24f..8e91b0c 100644 --- a/go.mod +++ b/go.mod @@ -4,19 +4,25 @@ go 1.26.0 require ( gitea.redpowerfuture.com/red-future/common v0.0.24 + github.com/bjang03/gmq v0.0.1 github.com/cloudwego/eino v0.9.5 github.com/cloudwego/eino-examples v0.0.0-20260611092511-bd64846fbc1d github.com/cloudwego/eino-ext/components/model/qwen v0.1.9 github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2 github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.2 + github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 github.com/gogf/gf/v2 v2.10.2 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.11.1 - github.com/tidwall/gjson v1.19.0 + github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 go.opentelemetry.io/otel/trace v1.44.0 ) +replace gitea.redpowerfuture.com/red-future/common v0.0.24 => ../common + +replace github.com/bjang03/gmq v0.0.1 => ../gmq + require ( github.com/BurntSushi/toml v1.5.0 // indirect github.com/armon/go-metrics v0.4.1 // indirect @@ -41,10 +47,13 @@ require ( github.com/evanphx/json-patch v0.5.2 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/go-ego/gse v1.0.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect @@ -67,8 +76,9 @@ require ( github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/serf v0.10.1 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.2 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mailru/easyjson v0.9.0 // indirect @@ -80,6 +90,9 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/nats-io/nats.go v1.49.0 // indirect + github.com/nats-io/nkeys v0.4.15 // indirect + github.com/nats-io/nuid v1.0.1 // indirect github.com/nikolalohinski/gonja v1.5.3 // indirect github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/ll v0.0.9 // indirect @@ -88,10 +101,12 @@ require ( github.com/pkg/errors v0.9.2-0.20201214064552-5dd12d0cfe7f // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/r3labs/diff/v2 v2.15.1 // indirect - github.com/redis/go-redis/v9 v9.17.2 // indirect + github.com/rabbitmq/amqp091-go v1.10.0 // indirect + github.com/redis/go-redis/v9 v9.18.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect + github.com/spf13/cast v1.10.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tiger1103/gfast-token v1.0.10 // indirect @@ -109,11 +124,13 @@ require ( go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.uber.org/atomic v1.11.0 // indirect golang.org/x/arch v0.19.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect diff --git a/go.sum b/go.sum index 0c5ec37..7b8043a 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,36 @@ +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -gitea.redpowerfuture.com/red-future/common v0.0.23 h1:xieoA00iKOCDm5SO9iXn+cSyMKBAlZwI0fuEVPWrHLg= -gitea.redpowerfuture.com/red-future/common v0.0.23/go.mod h1:50U1Xi+Ie56z09S5LQbZvaken0Mxv3OeS9LgR7U/ZRY= -gitea.redpowerfuture.com/red-future/common v0.0.24 h1:sXxhnmDmCgn+KwH/3gDnhAtAQ7FCmf/5AsMfvxRmri0= -gitea.redpowerfuture.com/red-future/common v0.0.24/go.mod h1:50U1Xi+Ie56z09S5LQbZvaken0Mxv3OeS9LgR7U/ZRY= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= +github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alibaba/sentinel-golang v1.0.4/go.mod h1:Lag5rIYyJiPOylK8Kku2P+a23gdKMMqzQS7wTnjWEpk= +github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -25,6 +38,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= @@ -36,8 +51,6 @@ github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqR github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= -github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= -github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/mockey v1.3.0 h1:ONLRdvhqmCfr9rTasUB8ZKCfvbdD2tohOg4u+4Q/ed0= @@ -46,6 +59,9 @@ github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uS github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cenk/backoff v2.2.1+incompatible/go.mod h1:7FtoeaSnHoZnmZzz47cM35Y9nSW7tNyaidugnHTaFDE= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -53,6 +69,9 @@ github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEex github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= @@ -64,13 +83,31 @@ github.com/cloudwego/eino v0.9.5 h1:0Nftjx9gPek/2S/hzm38LVxSjk5/6mqRr3I9VKrKvm4= github.com/cloudwego/eino v0.9.5/go.mod h1:OBD1mrkfkt/pJa4rkg1P0VnaMeOVl7l8IAdEqY//3IQ= github.com/cloudwego/eino-examples v0.0.0-20260611092511-bd64846fbc1d h1:NrAxhU58S5SgK5YbrYnaOQrQFwAz3x4/0qg46JM8Eo4= github.com/cloudwego/eino-examples v0.0.0-20260611092511-bd64846fbc1d/go.mod h1:VVmcWGhnLIxLkrQAaCoOtQoEbcvOCrMQvbXgbo9O34Q= +github.com/cloudwego/eino-ext/adk/backend/local v0.2.1/go.mod h1:os5Tq5FuSoz/MLqAdZER3ip49Oef9prc0kVsKsPYO48= +github.com/cloudwego/eino-ext/callbacks/cozeloop v0.3.0/go.mod h1:/biyKmCroUH3Y6fG2sBBiZyUmzwRhb3YKAsORGwxk1g= +github.com/cloudwego/eino-ext/components/document/parser/html v0.0.0-20251117090452-bd6375a0b3cf/go.mod h1:DBwsPrdNxPeE3HNr5XyjjFreG4ypqCUjN0T2C2JSy6k= +github.com/cloudwego/eino-ext/components/document/parser/pdf v0.0.0-20251117090452-bd6375a0b3cf/go.mod h1:kHC3xkGM/gv3IHpOk33p75BfBaEIYATOs2XmYFKffcs= +github.com/cloudwego/eino-ext/components/model/agenticark v0.2.0-beta.1/go.mod h1:dx+o4e/wfAmCNXIOTsXl+NiKokz2iU5P1f7eQClffnQ= +github.com/cloudwego/eino-ext/components/model/agenticopenai v0.2.0-beta.1/go.mod h1:aUmCsYjxXp6pkjDThNWmmEKnVEAFwv0XN1Qi2gdBByA= +github.com/cloudwego/eino-ext/components/model/ark v0.1.68/go.mod h1:IctHLV+EmEhf3o2fBw0N873mLIyNlEAAGcEpUGEQdvk= +github.com/cloudwego/eino-ext/components/model/deepseek v0.1.6/go.mod h1:YVjkAAxwLqk/dyA7AfVksaxPMG7snikx0hjje8iIZBE= +github.com/cloudwego/eino-ext/components/model/ollama v0.1.9/go.mod h1:C3rf3yy2nEoXFP/CQJne4gbiu1pREKplHKmFlhuOzPE= +github.com/cloudwego/eino-ext/components/model/openai v0.1.13/go.mod h1:mgIoqYYOc0eECCqvLbEYpOJrQNTNxkwXzSJzFU+v5sQ= github.com/cloudwego/eino-ext/components/model/qwen v0.1.9 h1:xCz/mp43JeWqupjPR3zLRArmwC6P29/6lTwbwh1yzYM= github.com/cloudwego/eino-ext/components/model/qwen v0.1.9/go.mod h1:slTGTuhzkzhNavf+1UtUg1FvUSA31iNAF+rq1mT4SnI= +github.com/cloudwego/eino-ext/components/retriever/volc_vikingdb v0.0.0-20251120060928-25485ef519b5/go.mod h1:He7AHJpLTs0MXKPx5JpI8MdYtLUcKhUAZwCsgNtaq6k= +github.com/cloudwego/eino-ext/components/tool/commandline v0.0.0-20251117090452-bd6375a0b3cf/go.mod h1:cMZb1KM71kM+2hTJwxLwXT+HC66HimZirDDA5Oo64Hw= +github.com/cloudwego/eino-ext/components/tool/duckduckgo/v2 v2.0.0-20251117090452-bd6375a0b3cf/go.mod h1:Np0BXy/9hPRu3wCgn+ij6L7YsjFcybVzg1k7uYOXh0M= +github.com/cloudwego/eino-ext/components/tool/mcp/officialmcp v0.1.0/go.mod h1:9kDYHgPkYf249MhpGxGxx21MRFdKzTRyK4wDxmowo7o= +github.com/cloudwego/eino-ext/devops v0.1.9/go.mod h1:A8EzSy78tyvRqZBaHkTG1r91HZSW0uBGYW+k2xGA+WU= github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 h1:EeVcR1TslRA2IdNW1h/2LaGbPlffwGhQm99jM3zWZiI= github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17/go.mod h1:Zkcx6DPTR2NfWmtSXbhItswGw6hqUezNPhNcke0pOG8= -github.com/cloudwego/hertz v0.10.5 h1:N4oBqAJShSjYQm2Jfr0ryTzzJ9fnY9qSvIvUBMkoFWg= -github.com/cloudwego/hertz v0.10.5/go.mod h1:Im9u6rUa1v2mL2HiDKKJoof/CPQ3mPBBpT92v67Cetg= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cohesion-org/deepseek-go v1.3.4/go.mod h1:bOVyKj38r90UEYZFrmJOzJKPxuAh8sIzHOCnLOpiXeI= +github.com/corpix/uarand v0.2.0/go.mod h1:/3Z1QIqWkDIhf6XWn/08/uMHoQ8JUoTIKc2iPchBOmM= +github.com/coze-dev/cozeloop-go v0.1.20/go.mod h1:lM7cmUEZlnAlQYdwfk4Li0SC3RdZ++QMHX75nvKceSc= +github.com/coze-dev/cozeloop-go/spec v0.1.8/go.mod h1:/f3BrWehffwXIpd4b5rYIqktLd/v5dlLBw0h9F/LQIU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -81,34 +118,51 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-jump v0.0.0-20211018200510-ba001c3ffce0/go.mod h1:4hKCXuwrJoYvHZxJ86+bRVTOMyJ0Ej+RqfSm8mHi6KA= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dslipak/pdf v0.0.2/go.mod h1:2L3SnkI9cQwnAS9gfPz2iUoLC0rUZwbucpbKi5R1mUo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/edwingeng/doublejump v1.0.1/go.mod h1:ykMWX8JWePtMtk2OGjNE9kwtgpI+SF2FNIyXV4gS36k= github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0= github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4= +github.com/eino-contrib/ollama v0.1.0/go.mod h1:mYsQ7b3DeqY8bHPuD3MZJYTqkgyL6LoemxoP/B7ZNhA= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-ego/gse v1.0.2 h1:+27lYFPhQEhA9igtdOsJPRKYL/k3TwYsxBF5jr6KFv4= github.com/go-ego/gse v1.0.2/go.mod h1:Fy35G+q7VV7Et1zIKO8o/sW1kkugV3znXap/lF/11zc= +github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -118,7 +172,26 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/godzie44/go-uring v0.0.0-20220926161041-69611e8b13d5/go.mod h1:ermjEDUoT/fS+3Ona5Vd6t6mZkw1eHp99ILO5jGRBkM= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2 h1:u8EpP24GkprogROnJ7htMov9Fc66pTP1eVYrWxiCYOs= github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.2/go.mod h1:GmvM3r8GVByVMi4RD2+MCs5+CfxVXPMeT8mVDkAaAXE= @@ -133,6 +206,7 @@ github.com/gogf/gf/v2 v2.10.2/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -161,6 +235,7 @@ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -170,6 +245,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -177,8 +254,11 @@ github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= @@ -228,21 +308,30 @@ github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4 github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kaptinlin/jsonrepair v0.2.4/go.mod h1:FRcIChI/abePdetnkc8x0JQfmHNEjQTW/LsTfI1X0oc= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kavu/go_reuseport v1.5.0/go.mod h1:CG8Ee7ceMFSMnx/xr25Vm0qXaj2Z4i5PWoUx+JZ5/CU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/reedsolomon v1.12.4/go.mod h1:d3CzOMOt0JXGIFZm1StgkyF14EYr3xneR2rNWo7NcMU= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -252,12 +341,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/libp2p/go-sockaddr v0.2.0/go.mod h1:5NxulaB17yJ07IpzRIleys4un0PJ7WLWgMDLBBWrGw8= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/matoous/go-nanoid v1.5.1/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -277,8 +370,10 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/meguminnnnnnnnn/go-openai v0.1.2 h1:iXombGGjqjBrmE9WaSidUhhi3YQhf42QTHvHLMkgvCA= github.com/meguminnnnnnnnn/go-openai v0.1.2/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY= +github.com/meilisearch/meilisearch-go v0.36.1/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= @@ -289,6 +384,7 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -297,28 +393,45 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE= +github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw= +github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= +github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= +github.com/nikolalohinski/gonja/v2 v2.3.1/go.mod h1:1Wcc/5huTu6y36e0sOFR1XQoFlylw3c3H3L5WOz0RDg= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= +github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k= +github.com/ollama/ollama v0.11.4/go.mod h1:9+1//yWPsDE2u+l1a5mpaKrYw4VdnSsRU3ioq5BvMms= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= +github.com/openai/openai-go/v3 v3.35.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.2-0.20201214064552-5dd12d0cfe7f h1:lJqhwddJVYAkyp72a4pwzMClI20xTwL7miDdm2W/KBM= github.com/pkg/errors v0.9.2-0.20201214064552-5dd12d0cfe7f/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -327,38 +440,63 @@ github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSg github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s= github.com/r3labs/diff/v2 v2.15.1 h1:EOrVqPUzi+njlumoqJwiS/TgGgmZo83619FNDB9xQUg= github.com/r3labs/diff/v2 v2.15.1/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc= -github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= -github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= +github.com/rpcxio/libkv v0.5.1/go.mod h1:zHGgtLr3cFhGtbalum0BrMPOjhFZFJXCKiws/25ewls= +github.com/rpcxio/rpcx-consul v0.1.1/go.mod h1:N4SjBS0M9HpVdq3CIIXm/MwWv6euXQ39ybhfH2iTh1c= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rubyist/circuitbreaker v2.2.1+incompatible/go.mod h1:Ycs3JgJADPuzJDwffe12k6BZT8hxVi6lFK+gWYJLN4A= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shirou/gopsutil/v3 v3.21.6/go.mod h1:JfVbDpIBLVzT8oKbvMg9P3wEIMDDpVn+LwHTKj0ST88= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI= github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg= +github.com/smallnest/quick v0.2.0/go.mod h1:ODNivpfZTaMgYrNb/fhDtqoEe2TTPxSRo8JaIT/QThI= +github.com/smallnest/rpcx v1.9.1/go.mod h1:owr4mDCReTn+dy9m5ilof0mBivFBeK0XrkYfZYdDGb4= +github.com/smallnest/rsocket v0.0.0-20241130031020-4a72eb6ff62a/go.mod h1:VJeIKKrDEzT4ZNVe87JN9uRLw1XLp/ZnnE9PfsyJ1jY= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -378,9 +516,11 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU= +github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= -github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -388,31 +528,58 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/tiger1103/gfast-token v1.0.10 h1:fNiBE/Dq5iTHvTGlCx3DmXa2o4hr0NtumFpffZ39k6s= github.com/tiger1103/gfast-token v1.0.10/go.mod h1:a/21mxmj7zFeNvjhZSC0XpEAFHfb1aT2k6DXnufFU1s= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vcaesar/cedar v0.30.0 h1:9fSDpM7FTjjUdPiBUUa0MWYMRGSEcqgFXvppZcZ4d7Y= github.com/vcaesar/cedar v0.30.0/go.mod h1:lyuGvALuZZDPNXwpzv/9LyxW+8Y6faN7zauFezNsnik= github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4= github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/volcengine/volc-sdk-golang v1.0.199/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ= +github.com/volcengine/volcengine-go-sdk v1.2.28/go.mod h1:oxoVo+A17kvkwPkIeIHPVLjSw7EQAm+l/Vau1YGHN+A= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xtaci/kcp-go v5.4.20+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= @@ -429,10 +596,14 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/ go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU= golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -440,8 +611,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= @@ -450,8 +621,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -468,9 +639,10 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -506,17 +678,18 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -526,8 +699,10 @@ golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -542,6 +717,7 @@ google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= @@ -569,14 +745,28 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/main.go b/main.go index 1b40ff3..3ff9b1e 100644 --- a/main.go +++ b/main.go @@ -2,19 +2,19 @@ package main import ( digitalhumanController "ai-agent/digital-human/controller" + "ai-agent/workflow/service/flow" + // 空导入:加载内置模型工具与工作流处理器(组合根统一装载,业务侧不感知实现位置) _ "ai-agent/tools/builtin/current_time" "ai-agent/workflow/consts/public" workController "ai-agent/workflow/controller" workflowController "ai-agent/workflow/controller/flow" workflowNodeController "ai-agent/workflow/controller/node" + sessionController "ai-agent/workflow/controller/session" workflowSkillController "ai-agent/workflow/controller/skill" toolController "ai-agent/workflow/controller/tool" - sessionController "ai-agent/workflow/controller/session" - flowService "ai-agent/workflow/service/flow" _ "ai-agent/workflow/service/flow/processor/builtin/split_batch" _ "ai-agent/workflow/service/flow/processor/builtin/split_shots_pipeline" - toolWsService "ai-agent/workflow/service/tool" "context" "os" "os/signal" @@ -43,7 +43,6 @@ func main() { digitalhumanController.DigitalHuman, // 数字人相关接口 digitalhumanController.Video, // 视频相关接口 digitalhumanController.AsyncTask, // 异步任务相关接口 - digitalhumanController.ModelTest, // 模型流式测试接口 workController.CreationInfo, workflowController.FlowExecution, workflowController.FlowUser, @@ -75,8 +74,7 @@ func main() { signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) go func() { <-sigCh - flowService.FlowWsService.Close() - toolWsService.ToolWsService.Close() + flow.SessionWsService.Close() }() // 保持应用运行 diff --git a/tools/builtin/current_time/current_time.go b/tools/builtin/current_time/current_time.go new file mode 100644 index 0000000..1983b04 --- /dev/null +++ b/tools/builtin/current_time/current_time.go @@ -0,0 +1,32 @@ +// Package current_time 内置示例工具:返回服务器当前时间。 +// 仅用于验证独立工具对话入口(/tool/agent 将全部注册工具交给模型 function calling),可按需移除。 +package current_time + +import ( + "context" + "time" + + "gitea.redpowerfuture.com/red-future/common/tools" +) + +func init() { + tools.Register(CurrentTimeTool()) +} + +// CurrentTimeTool 返回服务器当前时间,作为 model 作用域示例工具 +func CurrentTimeTool() *tools.Tool { + return &tools.Tool{ + Name: "current_time", + Description: "返回服务器当前时间。当用户询问时间/日期时调用。", + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{}, + "required": false, + }, + Func: func(ctx context.Context, args map[string]any) (tools.ToolResult, error) { + return tools.OK(map[string]any{ + "now": time.Now().Format("2006-01-02 15:04:05"), + }), nil + }, + } +} diff --git a/tools/runner/react_agent.go b/tools/runner/react_agent.go new file mode 100644 index 0000000..f700635 --- /dev/null +++ b/tools/runner/react_agent.go @@ -0,0 +1,258 @@ +// Package runner 工具框架的"使用面":ReAct 执行循环。 +// 模型调用按模型响应类型分流:流式走 gateway.ModelCallStream(逐 chunk 推增量),同步/异步走 gateway.ModelCallResult; +// 本包只保留"模型思考→调用工具→观察结果"的循环编排,工具的"使用"统一走 tools.Default.Call。 +package runner + +import ( + "ai-agent/gateway" + "ai-agent/workflow/consts/model" + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/gogf/gf/v2/util/gconv" + + "gitea.redpowerfuture.com/red-future/common/tools" +) + +// ReActEventType ReAct 过程事件类型(WebSocket 场景逐条推送,HTTP 同步场景不设置 OnEvent 则忽略) +type ReActEventType string + +const ( + ReActEventModelCall ReActEventType = "model_call" // 模型思考(step 开始) + ReActEventToolCall ReActEventType = "tool_call" // 模型请求调用工具 + ReActEventToolResult ReActEventType = "tool_result" // 工具返回结果 + ReActEventAnswer ReActEventType = "answer" // 最终回答 + ReActEventAnswerChunk ReActEventType = "answer_chunk" // 回答内容增量(逐 chunk) + ReActEventReasoningChunk ReActEventType = "reasoning_chunk" // 思考内容增量(逐 chunk) + ReActEventError ReActEventType = "error" +) + +// ReActEvent ReAct 过程事件,按发生顺序回调 +type ReActEvent struct { + Type ReActEventType + Step int + MaxStep int + Description string // 工具用途说明(tool_call/tool_result 推给前端展示,不暴露工具名/参数) + Answer string + Delta string // 流式文本增量(ReActEventAnswerChunk / ReActEventReasoningChunk 用) + Message string + Error string +} + +// ReActAgent 实现 ReAct 模式的智能体:模型思考→调用工具→观察工具结果→重复→最终回答。 +// 工具的"使用"统一走 tools.Default.Call:未知工具由 Server 返回结构化 not_found, +// 无需在 runner 内维护工具查找表。 +type ReActAgent struct { + ModelId int64 + SessionId string + ToolList []*tools.Tool + SystemPrompt string + MaxStep int + // OnEvent 过程回调(WebSocket 流式推送用;HTTP 同步调用不设置,忽略事件) + OnEvent func(ReActEvent) + // TotalTokens 本次 ReAct 循环累计 token 消耗(供调用方落库) + TotalTokens int64 + // TotalCost 本次 ReAct 循环累计费用(元,各步模型调用返回的 cost 求和,供调用方落库) + TotalCost float64 +} + +// NewReActAgent 创建 ReAct 智能体 +func NewReActAgent(modelId int64, sessionId string, toolList []*tools.Tool, systemPrompt string, maxStep int) *ReActAgent { + return &ReActAgent{ + ModelId: modelId, + SessionId: sessionId, + ToolList: toolList, + SystemPrompt: systemPrompt, + MaxStep: maxStep, + } +} + +// Run 执行 ReAct 循环 +// 标准流程: 思考 → 行动(调用工具) → 观察(工具结果) → 重复 → 最终回答 +func (a *ReActAgent) Run(ctx context.Context, userInput string) (string, error) { + // 模型须为已启用且勾选「聊天模型」的对话模型,再按其响应类型分流调用: + // 流式走 ModelCallStream(逐 chunk 推增量),同步/异步走 ModelCallResult(一次返回) + modelInfo, err := gateway.GetModelInfoById(ctx, &gateway.GetModelInfoByIdReq{ModelId: a.ModelId}) + if err != nil { + return "", fmt.Errorf("获取模型配置失败: %w", err) + } + if modelInfo.ModelManage.Enabled == nil || !*modelInfo.ModelManage.Enabled { + return "", fmt.Errorf("模型 [%s] 未启用", modelInfo.ModelManage.ModelName) + } + if modelInfo.ModelManage.ChatModel == nil || !*modelInfo.ModelManage.ChatModel { + return "", fmt.Errorf("模型 [%s] 不是对话模型,请在模型配置中勾选「聊天模型」", modelInfo.ModelManage.ModelName) + } + isStream := *modelInfo.ModelManage.ResponseType == *model.ResponseTypeStream.Code() + + messages := map[string]any{ + "user_prompt": userInput, + "system_prompt": a.SystemPrompt, + "tools": a.rawTools(a.ToolList), + } + for step := 0; step < a.MaxStep; step++ { + a.emit(ReActEvent{Type: ReActEventModelCall, Step: step + 1, MaxStep: a.MaxStep}) + var result *gateway.ModelCallRes + if isStream { + result, err = gateway.ModelCallStream(ctx, a.ModelId, a.SessionId, nil, messages, func(chunk map[string]any) error { + // 文本增量从 content 子对象取,字段名由网关端结构体统一管理 + if content, ok := chunk["content"].(map[string]any); ok { + for _, v := range content { + if s := gconv.String(v); s != "" { + a.emit(ReActEvent{Type: ReActEventAnswerChunk, Delta: s}) + } + } + } + // 思考内容增量从 reasoningContent 字段取(model-gateway 按 ResponseBusinessFieldMapping 配置返回) + if s := gconv.String(chunk["reasoningContent"]); s != "" { + a.emit(ReActEvent{Type: ReActEventReasoningChunk, Delta: s}) + } + return nil + }) + } else { + // 同步/异步对话模型:结果一次返回,无增量事件(异步 msgTopic 为空时由 gateway.ModelCallResult 自动生成) + result, err = gateway.ModelCallResult(ctx, a.ModelId, modelInfo.ModelManage.ResponseType, a.SessionId, nil, messages) + } + // 出错时 result 仍可能携带已累计的 token/cost(流中断返回 res),先累加再判错 + if result != nil { + a.TotalTokens += result.TotalTokens + a.TotalCost += result.Cost + } + if err != nil { + // 前端终止/上下文取消:静默停止,不推错误事件 + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return "", err + } + a.emit(ReActEvent{Type: ReActEventError, Message: "模型调用失败", Error: err.Error()}) + return "", fmt.Errorf("step %d: model call failed: %w", step, err) + } + var content string + var toolCalls []gateway.ModelTool + if isStream { + for _, v := range gconv.Map(result.Content) { + content += v.(string) + } + toolCalls = result.Tools + } else { + // 同步响应 Content 按 responseBodyMapping 抽取(content/toolCalls/finishReason),工具在 toolCalls 里 + content, toolCalls = syncChatResult(result.Content) + } + // 无工具调用 → 最终回答 + if len(toolCalls) == 0 { + a.emit(ReActEvent{Type: ReActEventAnswer, Answer: content}) + return content, nil + } + messages["assistant_prompt"] = content + for _, tool := range toolCalls { + fn := gconv.Map(tool.Function) + name := gconv.String(fn["name"]) + arguments := gconv.String(fn["arguments"]) + desc := a.toolDescription(name) + a.emit(ReActEvent{Type: ReActEventToolCall, Description: desc}) + // 执行每个工具调用 + toolMsg := a.executeToolCall(ctx, name, arguments) + a.emit(ReActEvent{Type: ReActEventToolResult, Description: desc}) + messages["tool_prompt"] = toolMsg + messages["tool_id"] = tool.Id + } + } + err = fmt.Errorf("max steps reached %d, generation incomplete", a.MaxStep) + a.emit(ReActEvent{Type: ReActEventError, Message: "最大步数达到,生成失败", Error: err.Error()}) + return "", err +} + +// syncChatResult 解析同步对话响应 Content(model-gateway 按 responseBodyMapping 抽取,固定 +// content/toolCalls/finishReason 三个 key):返回最终文本与工具调用列表(同步响应工具在 toolCalls 里,res.Tools 为空)。 +func syncChatResult(content map[string]any) (string, []gateway.ModelTool) { + text := gconv.String(content["content"]) + raw := content["toolCalls"] + if raw == nil { + raw = content["tools"] + } + var toolCalls []gateway.ModelTool + if arr, ok := raw.([]any); ok { + for _, t := range arr { + m := gconv.Map(t) + if m == nil { + continue + } + var tool gateway.ModelTool + tool.Id = gconv.String(m["id"]) + tool.Type = gconv.String(m["type"]) + if fn := gconv.Map(m["function"]); fn != nil { + tool.Function.Name = gconv.String(fn["name"]) + tool.Function.Arguments = gconv.String(fn["arguments"]) + } + toolCalls = append(toolCalls, tool) + } + } + return text, toolCalls +} + +// emit 触发过程事件回调;未设置 OnEvent 时忽略(HTTP 同步调用路径) +func (a *ReActAgent) emit(ev ReActEvent) { + if a.OnEvent != nil { + a.OnEvent(ev) + } +} + +// executeToolCall 执行单个工具调用,返回回灌给模型的工具结果消息。 +// 未知工具由 tools.Default.Call 返回结构化 not_found;工具业务失败进 ToolResult.Code。 +func (a *ReActAgent) executeToolCall(ctx context.Context, name, arguments string) string { + if arguments == "" { + return "工具参数为空" + } + + var args map[string]any + if err := json.Unmarshal([]byte(arguments), &args); err != nil { + return fmt.Sprintf("参数解析失败: %v", err) + } + + res, callErr := tools.Default.Call(ctx, name, args) + + switch { + case callErr != nil: + msg := fmt.Sprintf("工具执行失败: %v", callErr) + return msg + case res.Code != 0: + return res.Message + default: + dataJSON, err := json.Marshal(res.Data) + if err != nil { + return fmt.Sprintf("工具结果序列化失败: %v", err) + } + return string(dataJSON) + } +} + +// toolDescription 按工具名从 ToolList 中查找用途说明(推给前端展示用,不暴露工具名/参数) +func (a *ReActAgent) toolDescription(name string) string { + for _, t := range a.ToolList { + if t.Name == name { + return t.Description + } + } + return "" +} + +// RawTools 将工具定义转为 OpenAI 原始数组,parameters 保持 JSON Schema 原样。 +// businessParams 里传 tools 必须用它转换——直接传 []*tools.Tool 会被 json.Marshal 序列化 Func 字段而失败。 +func (a *ReActAgent) rawTools(toolList []*tools.Tool) []any { + raw := make([]any, 0, len(toolList)) + for _, t := range toolList { + params := t.Parameters + if params == nil { + params = map[string]any{} + } + raw = append(raw, map[string]any{ + "type": "function", + "function": map[string]any{ + "name": t.Name, + "description": t.Description, + "parameters": params, + }, + }) + } + return raw +} diff --git a/update.sql b/update.sql index 0a486d0..6b8df15 100644 --- a/update.sql +++ b/update.sql @@ -626,11 +626,15 @@ CREATE TABLE IF NOT EXISTS black_deacon_node_execution ( prompt_tokens INTEGER NOT NULL DEFAULT 0, -- 提示词token消耗 completion_tokens INTEGER NOT NULL DEFAULT 0, -- 补全token消耗 total_tokens INTEGER NOT NULL DEFAULT 0, -- 总token消耗 + token_info JSONB DEFAULT '[]', -- 节点token明细(汇总节点聚合 total_tokens/total_fee 用) status SMALLINT NOT NULL DEFAULT 1, -- 执行状态:1-运行中,2-成功,3-失败,4-暂停,5-等待执行 duration_ms BIGINT NOT NULL DEFAULT 0, -- 执行时长(毫秒) error_message TEXT DEFAULT '' -- 错误信息 ); +-- 存量库补列 +ALTER TABLE black_deacon_node_execution ADD COLUMN IF NOT EXISTS token_info JSONB DEFAULT '[]'; + -- 索引(高频查询) CREATE INDEX IF NOT EXISTS idx_bne_tenant_id ON black_deacon_node_execution(tenant_id); CREATE INDEX IF NOT EXISTS idx_bne_flow_execution_id ON black_deacon_node_execution(flow_execution_id); @@ -658,46 +662,54 @@ COMMENT ON COLUMN black_deacon_node_execution.output_params_path IS '节点输 COMMENT ON COLUMN black_deacon_node_execution.prompt_tokens IS '提示词token消耗'; COMMENT ON COLUMN black_deacon_node_execution.completion_tokens IS '补全token消耗'; COMMENT ON COLUMN black_deacon_node_execution.total_tokens IS '总token消耗'; +COMMENT ON COLUMN black_deacon_node_execution.token_info IS '节点token明细(汇总节点聚合 total_tokens/total_fee 用)'; COMMENT ON COLUMN black_deacon_node_execution.status IS '执行状态:1-运行中,2-成功,3-失败,4-暂停,5-等待执行'; COMMENT ON COLUMN black_deacon_node_execution.duration_ms IS '执行时长(毫秒)'; COMMENT ON COLUMN black_deacon_node_execution.error_message IS '错误信息'; --------------------pgsql创建black_deacon_node_execution表语句--------------------------- -- ========== 会话记录 + 工作流/普通会话结果表(2026-08-13) ========== + +--------------------pgsql创建black_deacon_session表语句--------------------------- +-- 会话记录表 CREATE TABLE IF NOT EXISTS black_deacon_session ( - id BIGINT PRIMARY KEY, -- 主键ID(非自增,hook snowflake) - tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID + -- 基础字段(完全对齐项目规范) + id BIGINT PRIMARY KEY, -- 主键ID(非自增) + tenant_id BIGINT NOT NULL DEFAULT 0, -- 租户ID int8 creator VARCHAR(64) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updater VARCHAR(64) NOT NULL, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at timestamp(6), + + -- 业务字段 + session_id VARCHAR(64) NOT NULL, -- 会话ID(前端传入,雪花ID) session_name VARCHAR(128) NOT NULL DEFAULT '' -- 会话名称 ); -CREATE TABLE IF NOT EXISTS black_deacon_workflow_session_result ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL DEFAULT 0, - creator VARCHAR(64) NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updater VARCHAR(64) NOT NULL, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - deleted_at timestamp(6), - session_id BIGINT NOT NULL DEFAULT 0, -- 所属会话ID - flow_id BIGINT NOT NULL DEFAULT 0, -- 工作流ID - flow_name VARCHAR(128) NOT NULL DEFAULT '', -- 工作流名称 - request_params JSONB DEFAULT '{}', -- 请求参数 - result_params JSONB DEFAULT '[]'::JSONB, -- 返回结果 - status SMALLINT NOT NULL DEFAULT 1, -- 状态:1-运行中,2-成功,3-失败 - total_tokens INTEGER NOT NULL DEFAULT 0, -- 总token - total_fee NUMERIC(10,4) NOT NULL DEFAULT 0, -- 总费用 - error_msg TEXT NOT NULL DEFAULT '' -- 错误信息 -); -CREATE INDEX idx_wsr_session ON black_deacon_workflow_session_result (session_id); -CREATE INDEX idx_wsr_flow ON black_deacon_workflow_session_result (flow_id, creator); -CREATE INDEX idx_wsr_created ON black_deacon_workflow_session_result (created_at); +-- 索引(高频查询) +CREATE UNIQUE INDEX IF NOT EXISTS uk_session_tenant_session_id ON black_deacon_session(tenant_id, session_id); +CREATE INDEX IF NOT EXISTS idx_session_tenant_id ON black_deacon_session(tenant_id); +CREATE INDEX IF NOT EXISTS idx_session_creator ON black_deacon_session(creator); +CREATE INDEX IF NOT EXISTS idx_session_deleted_at ON black_deacon_session(deleted_at); -CREATE TABLE IF NOT EXISTS black_deacon_chat_session_result ( +-- 表和字段注释 +COMMENT ON TABLE black_deacon_session IS '会话记录表'; +COMMENT ON COLUMN black_deacon_session.id IS '主键ID(非自增)'; +COMMENT ON COLUMN black_deacon_session.tenant_id IS '租户ID'; +COMMENT ON COLUMN black_deacon_session.creator IS '创建人'; +COMMENT ON COLUMN black_deacon_session.created_at IS '创建时间'; +COMMENT ON COLUMN black_deacon_session.updater IS '更新人'; +COMMENT ON COLUMN black_deacon_session.updated_at IS '更新时间'; +COMMENT ON COLUMN black_deacon_session.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN black_deacon_session.session_id IS '会话ID(前端传入,雪花ID)'; +COMMENT ON COLUMN black_deacon_session.session_name IS '会话名称'; +--------------------pgsql创建black_deacon_session表语句--------------------------- + +--------------------pgsql创建black_deacon_exec_chat表语句--------------------------- +-- 普通会话执行记录表 +CREATE TABLE IF NOT EXISTS black_deacon_exec_chat ( + -- 基础字段(完全对齐项目规范) id BIGINT PRIMARY KEY, tenant_id BIGINT NOT NULL DEFAULT 0, creator VARCHAR(64) NOT NULL, @@ -705,12 +717,125 @@ CREATE TABLE IF NOT EXISTS black_deacon_chat_session_result ( updater VARCHAR(64) NOT NULL, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at timestamp(6), - session_id BIGINT NOT NULL DEFAULT 0, -- 所属会话ID(级联软删) - question TEXT NOT NULL DEFAULT '', -- 用户问题 - answer TEXT NOT NULL DEFAULT '', -- 模型返回结果 - total_tokens INTEGER NOT NULL DEFAULT 0, -- 总token - total_fee NUMERIC(10,4) NOT NULL DEFAULT 0, -- 总费用 - error_msg TEXT NOT NULL DEFAULT '' -- 错误信息 + + -- 业务字段 + session_id VARCHAR(64) NOT NULL DEFAULT '', -- 会话ID + duration BIGINT NOT NULL DEFAULT 0, -- 执行时长(秒) + request_params JSONB DEFAULT '{}', -- 请求参数 + result_file_url VARCHAR(512) DEFAULT '', -- 结果文件路径(OSS) + total_tokens INTEGER NOT NULL DEFAULT 0, -- 总token消耗 + total_fee DOUBLE PRECISION NOT NULL DEFAULT 0, -- 总费用 + error_message TEXT DEFAULT '' -- 错误信息 ); -CREATE INDEX idx_csr_session ON black_deacon_chat_session_result (session_id); -CREATE INDEX idx_csr_created ON black_deacon_chat_session_result (created_at); \ No newline at end of file + +-- 索引(高频查询) +CREATE INDEX IF NOT EXISTS idx_exec_chat_tenant_id ON black_deacon_exec_chat(tenant_id); +CREATE INDEX IF NOT EXISTS idx_exec_chat_session_id ON black_deacon_exec_chat(session_id); +CREATE INDEX IF NOT EXISTS idx_exec_chat_deleted_at ON black_deacon_exec_chat(deleted_at); + +-- 表和字段注释 +COMMENT ON TABLE black_deacon_exec_chat IS '普通会话执行记录表'; +COMMENT ON COLUMN black_deacon_exec_chat.id IS '主键ID'; +COMMENT ON COLUMN black_deacon_exec_chat.tenant_id IS '租户ID'; +COMMENT ON COLUMN black_deacon_exec_chat.creator IS '创建人'; +COMMENT ON COLUMN black_deacon_exec_chat.created_at IS '创建时间'; +COMMENT ON COLUMN black_deacon_exec_chat.updater IS '更新人'; +COMMENT ON COLUMN black_deacon_exec_chat.updated_at IS '更新时间'; +COMMENT ON COLUMN black_deacon_exec_chat.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN black_deacon_exec_chat.session_id IS '会话ID'; +COMMENT ON COLUMN black_deacon_exec_chat.duration IS '执行时长(秒)'; +COMMENT ON COLUMN black_deacon_exec_chat.request_params IS '请求参数'; +COMMENT ON COLUMN black_deacon_exec_chat.result_file_url IS '结果文件路径(OSS)'; +COMMENT ON COLUMN black_deacon_exec_chat.total_tokens IS '总token消耗'; +COMMENT ON COLUMN black_deacon_exec_chat.total_fee IS '总费用'; +COMMENT ON COLUMN black_deacon_exec_chat.error_message IS '错误信息'; +--------------------pgsql创建black_deacon_exec_chat表语句--------------------------- + +--------------------pgsql创建black_deacon_exec_workflow表语句--------------------------- +-- 工作流执行记录表 +CREATE TABLE IF NOT EXISTS black_deacon_exec_workflow ( + -- 基础字段(完全对齐项目规范) + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL DEFAULT 0, + creator VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updater VARCHAR(64) NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at timestamp(6), + + -- 业务字段 + session_id VARCHAR(64) NOT NULL DEFAULT '', -- 会话ID + flow_id BIGINT NOT NULL DEFAULT 0, -- 工作流ID + duration BIGINT NOT NULL DEFAULT 0, -- 执行时长(秒) + request_params JSONB DEFAULT '{}', -- 请求参数 + status SMALLINT NOT NULL DEFAULT 1, -- 状态:1-运行中,2-成功,3-失败 + total_tokens INTEGER NOT NULL DEFAULT 0, -- 总token消耗 + total_fee DOUBLE PRECISION NOT NULL DEFAULT 0, -- 总费用 + error_message TEXT DEFAULT '' -- 错误信息 +); + +-- 索引(高频查询) +CREATE INDEX IF NOT EXISTS idx_exec_workflow_tenant_id ON black_deacon_exec_workflow(tenant_id); +CREATE INDEX IF NOT EXISTS idx_exec_workflow_session_id ON black_deacon_exec_workflow(session_id); +CREATE INDEX IF NOT EXISTS idx_exec_workflow_flow_id ON black_deacon_exec_workflow(flow_id); +CREATE INDEX IF NOT EXISTS idx_exec_workflow_status ON black_deacon_exec_workflow(status); +CREATE INDEX IF NOT EXISTS idx_exec_workflow_deleted_at ON black_deacon_exec_workflow(deleted_at); + +-- 表和字段注释 +COMMENT ON TABLE black_deacon_exec_workflow IS '工作流执行记录表'; +COMMENT ON COLUMN black_deacon_exec_workflow.id IS '主键ID'; +COMMENT ON COLUMN black_deacon_exec_workflow.tenant_id IS '租户ID'; +COMMENT ON COLUMN black_deacon_exec_workflow.creator IS '创建人'; +COMMENT ON COLUMN black_deacon_exec_workflow.created_at IS '创建时间'; +COMMENT ON COLUMN black_deacon_exec_workflow.updater IS '更新人'; +COMMENT ON COLUMN black_deacon_exec_workflow.updated_at IS '更新时间'; +COMMENT ON COLUMN black_deacon_exec_workflow.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN black_deacon_exec_workflow.session_id IS '会话ID'; +COMMENT ON COLUMN black_deacon_exec_workflow.flow_id IS '工作流ID'; +COMMENT ON COLUMN black_deacon_exec_workflow.duration IS '执行时长(秒)'; +COMMENT ON COLUMN black_deacon_exec_workflow.request_params IS '请求参数'; +COMMENT ON COLUMN black_deacon_exec_workflow.status IS '状态:1-运行中,2-成功,3-失败'; +COMMENT ON COLUMN black_deacon_exec_workflow.total_tokens IS '总token消耗'; +COMMENT ON COLUMN black_deacon_exec_workflow.total_fee IS '总费用'; +COMMENT ON COLUMN black_deacon_exec_workflow.error_message IS '错误信息'; +--------------------pgsql创建black_deacon_exec_workflow表语句--------------------------- + +--------------------pgsql创建black_deacon_exec_workflow_result表语句--------------------------- +-- 工作流执行结果表 +CREATE TABLE IF NOT EXISTS black_deacon_exec_workflow_result ( + -- 基础字段(完全对齐项目规范) + id BIGINT PRIMARY KEY, + tenant_id BIGINT NOT NULL DEFAULT 0, + creator VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updater VARCHAR(64) NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + deleted_at timestamp(6), + + -- 业务字段 + session_id VARCHAR(64) NOT NULL DEFAULT '', -- 会话ID + flow_id BIGINT NOT NULL DEFAULT 0, -- 工作流ID + exec_id BIGINT NOT NULL DEFAULT 0, -- 执行ID + result_file_url VARCHAR(512) DEFAULT '' -- 结果文件路径(OSS) +); + +-- 索引(高频查询) +CREATE INDEX IF NOT EXISTS idx_exec_workflow_result_tenant_id ON black_deacon_exec_workflow_result(tenant_id); +CREATE INDEX IF NOT EXISTS idx_exec_workflow_result_session_id ON black_deacon_exec_workflow_result(session_id); +CREATE INDEX IF NOT EXISTS idx_exec_workflow_result_exec_id ON black_deacon_exec_workflow_result(exec_id); +CREATE INDEX IF NOT EXISTS idx_exec_workflow_result_deleted_at ON black_deacon_exec_workflow_result(deleted_at); + +-- 表和字段注释 +COMMENT ON TABLE black_deacon_exec_workflow_result IS '工作流执行结果表'; +COMMENT ON COLUMN black_deacon_exec_workflow_result.id IS '主键ID'; +COMMENT ON COLUMN black_deacon_exec_workflow_result.tenant_id IS '租户ID'; +COMMENT ON COLUMN black_deacon_exec_workflow_result.creator IS '创建人'; +COMMENT ON COLUMN black_deacon_exec_workflow_result.created_at IS '创建时间'; +COMMENT ON COLUMN black_deacon_exec_workflow_result.updater IS '更新人'; +COMMENT ON COLUMN black_deacon_exec_workflow_result.updated_at IS '更新时间'; +COMMENT ON COLUMN black_deacon_exec_workflow_result.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN black_deacon_exec_workflow_result.session_id IS '会话ID'; +COMMENT ON COLUMN black_deacon_exec_workflow_result.flow_id IS '工作流ID'; +COMMENT ON COLUMN black_deacon_exec_workflow_result.exec_id IS '执行ID'; +COMMENT ON COLUMN black_deacon_exec_workflow_result.result_file_url IS '结果文件路径(OSS)'; +--------------------pgsql创建black_deacon_exec_workflow_result表语句--------------------------- diff --git a/workflow/consts/model/model_type.go b/workflow/consts/model/model_type.go new file mode 100644 index 0000000..7820df6 --- /dev/null +++ b/workflow/consts/model/model_type.go @@ -0,0 +1,40 @@ +package model + +// 模型类型编码常量 +const ( + TypeInference = 100 // 推理模型 + TypeImage = 200 // 图片模型 + TypeAudio = 300 // 音频模型 + TypeVector = 400 // 向量化模型 + TypeOmni = 500 // 全模态模型 + TypeVideo = 600 // 视频模型 + + // 图片子类型 + ImageSubTextToImage = 201 + ImageSubImageToImage = 202 + ImageSubImageEdit = 203 + ImageSubImageVariation = 204 + ImageSubImageTextToImage = 205 + + // 音频子类型 + AudioSubTextToSpeech = 301 + AudioSubSpeechToText = 302 + AudioSubSpeechToSpeech = 303 + + // 向量化子类型 + VectorSubEmbedding = 401 + VectorSubRerank = 402 + + // 全模态子类型 + OmniSubTextImageAudio = 501 + OmniSubVision = 502 + + // 视频子类型 + VideoSubTextToVideo = 601 + VideoSubImageToVideo = 602 + VideoSubImageTextToVideo = 603 + VideoSubVideoToVideo = 604 +) + +// ModelType 编码类型 +type ModelType *int diff --git a/workflow/consts/model/response_type.go b/workflow/consts/model/response_type.go new file mode 100644 index 0000000..3e14c82 --- /dev/null +++ b/workflow/consts/model/response_type.go @@ -0,0 +1,27 @@ +package model + +import "github.com/gogf/gf/v2/util/gconv" + +var ( + ResponseTypeSync = newResponseType(gconv.PtrInt8(1), "sync") // 同步 + ResponseTypeAsync = newResponseType(gconv.PtrInt8(2), "async") // 异步 + ResponseTypeStream = newResponseType(gconv.PtrInt8(3), "stream") // 流 +) + +type ResponseType *int8 + +type responseType struct { + code ResponseType + desc string +} + +func (s responseType) Code() ResponseType { + return s.code +} +func (s responseType) Desc() string { + return s.desc +} + +func newResponseType(code ResponseType, desc string) responseType { + return responseType{code: code, desc: desc} +} diff --git a/workflow/consts/node/node_template.go b/workflow/consts/node/node_template.go index 30fe692..95a7032 100644 --- a/workflow/consts/node/node_template.go +++ b/workflow/consts/node/node_template.go @@ -1,119 +1,449 @@ package node -// ======================== 【常量定义:所有中文文案放这里!】 ======================== -// 分组名称 -const ( - NodeGroupNameComponent = "组件" - NodeGroupNameBase = "基础" - NodeGroupNameCustom = "自定义" -) - -// 节点名称 -const ( - NodeNameTextModel = "生成文案" - NodeNameImageModel = "生成图片" - NodeNameVideoModel = "生成视频" - NodeNameAudioModel = "生成音频" - NodeNameBatchModel = "批量处理一起返回" - NodeNameDataConversionModel = "参数转换" - NodeNameModel = "模型" - NodeNameMerge = "结果合并" - NodeNameDataMerge = "结果汇集" - NodeNameJudge = "条件判断" - NodeNameLoop = "循环" - NodeNameForm = "表单" - NodeSubFlow = "子流程" - NodeNameHttp = "HTTP(S)接口" - NodeNameCustomNode = "自定义节点" - NodeNameSystemSum = "系统-结果汇总" -) - -// 表单字段 Label -const ( - FormLabelApiKey = "API Key" - FormLabelModel = "模型名称" - FormLabelCondition = "判断条件" -) - -// ======================== 枚举类型 ======================== +// NodeGroup 节点分组标识 type NodeGroup string -const ( - NodeGroupComponent NodeGroup = "component" - NodeGroupBase NodeGroup = "base" - NodeGroupCustom NodeGroup = "custom" -) - +// NodeType 节点类型标识 type NodeType string +// 分组常量定义 const ( - // 组件 - NodeTypeTextModel NodeType = "text_model" - NodeTypeImageModel NodeType = "image_model" - NodeTypeVideoModel NodeType = "video_model" - NodeTypeAudioModel NodeType = "audio_model" - NodeTypeBatchModel NodeType = "batch_model" - NodeTypeDataConversionModel NodeType = "data_conversion_model" - // 基础 - NodeTypeModel NodeType = "model" - NodeTypeMerge NodeType = "merge" - NodeTypeDataMerge NodeType = "data_merge" - NodeTypeJudge NodeType = "judge" - NodeTypeForm NodeType = "form" - NodeTypeIntent NodeType = "intent" - NodeTypeSubFlow NodeType = "sub_flow" - NodeTypeHttp NodeType = "http" - // 自定义 - NodeTypeCustomNode NodeType = "custom_node" - // 系统 - NodeTypeSystemSum NodeType = "system_sum" + NodeGroupBase NodeGroup = "base" + NodeGroupSystem NodeGroup = "system" ) +// 节点类型常量定义 const ( - ModelTypeText = 100 - ModelTypeImage = 200 - ModelTypeAudio = 300 - ModelTypeModality = 500 - ModelTypeVideo = 600 + NodeTypeStart NodeType = "__start__" + NodeTypeModel NodeType = "model" + NodeTypeDataMerge NodeType = "data_merge" + NodeTypeForm NodeType = "form" + NodeTypeSubFlow NodeType = "sub_flow" + NodeTypeHttp NodeType = "http" + NodeTypeSystemSum NodeType = "system_sum" + NodeTypeScriptTranscribe NodeType = "script_transcribe" ) -// ======================== 结构定义 ======================== -type NodeFormField struct { - Value any `json:"value"` - Field string `json:"field"` - Label string `json:"label"` // 从常量来 - Type string `json:"type"` - Required bool `json:"required"` - Default any `json:"default,omitempty"` - Options []SelectOption `json:"options"` - Expand any `json:"expand"` - FieldConstraint any `json:"fieldConstraint"` +// NodeGroupMeta 节点分组元数据 +type NodeGroupMeta struct { + Key NodeGroup `json:"key"` // 后端存储标识 + Name string `json:"name"` // 前端展示中文名称 +} + +// NodeTypeMeta 节点元数据 +type NodeTypeMeta struct { + Key NodeType `json:"key"` // 节点类型标识 + Name string `json:"name"` // 节点中文名称 + Group NodeGroup `json:"group"` // 所属分组 + Sort int `json:"sort"` // UI排序字段 + Desc string `json:"desc,omitempty"` // 可选:节点简介 + BatchExecOption bool `json:"batchExecOption"` + PreToolOption bool `json:"preToolOption"` + PostToolOption bool `json:"postToolOption"` + IsSaveFileOption bool `json:"isSaveFileOption"` + FormConfigOption bool `json:"formConfigOption"` + ModelConfigOption bool `json:"modelConfigOption"` + SkillOption bool `json:"skillOption"` + PromptOption bool `json:"promptOption"` + NegativePromptOption bool `json:"negativePromptOption"` + PresetOption []NodePresetField `json:"presetOption"` + OutputField []string `json:"outputField"` +} + +type NodePresetField struct { + Value string `json:"value"` + Field string `json:"field"` + Label string `json:"label"` + Type string `json:"type"` + IsFormField bool `json:"isFormField"` + Constraint FieldConstraint `json:"constraint"` + Required bool `json:"required"` + Options []SelectOption `json:"options"` +} + +// FieldConstraint 字段约束 +type FieldConstraint struct { + // 数字类型:int、float、double、string + Type string `json:"type" dc:"类型"` + Min any `json:"min" dc:"最小值"` + Max any `json:"max" dc:"最大值"` } type SelectOption struct { - Label string `json:"label"` - Value string `json:"value"` + Key string `json:"key"` + Value string `json:"value"` + Config []NodePresetField `json:"config"` } -type ModelItem struct { - ModelName string `json:"modelName"` - ModelForm []NodeFormField `json:"modelForm"` +// NodeGroupTree 对外输出树形结构:分组 + 分组下所有节点 +type NodeGroupTree struct { + Group NodeGroupMeta `json:"group"` + Nodes []NodeTypeMeta `json:"nodes"` } -type NodeItem struct { - NodeId string `json:"nodeId"` - NodeCode NodeType `json:"nodeCode"` - ModelType int `json:"modelType"` - NodeName string `json:"nodeName"` // 从常量来 - SkillOption bool `json:"skillOption"` - PromptOption bool `json:"promptOption"` - IsSaveFile bool `json:"isSaveFile"` - FormConfig []NodeFormField `json:"formConfig"` - ModelConfig []ModelItem `json:"modelConfig"` +// ===================== 元数据集中注册区(英文标识+中文文案统一维护) ===================== +var NodeGroupMetaList = []NodeGroupMeta{ + { + Key: NodeGroupBase, + Name: "基础", + }, } -type NodeGroupItem struct { - Group NodeGroup `json:"group"` - Label string `json:"label"` // 从常量来 - Items []NodeItem `json:"items"` +var NodeTypeMetaList = []NodeTypeMeta{ + { + Key: NodeTypeModel, + Name: "模型", + Group: NodeGroupBase, + Sort: 1, + Desc: "模型调用节点,可配置模型参数、模型配置、技能、提示语、结果汇集、结果保存、结果返回、结果展示等信息。", + BatchExecOption: true, + PreToolOption: true, + PostToolOption: true, + IsSaveFileOption: true, + FormConfigOption: false, + ModelConfigOption: true, + SkillOption: false, + PromptOption: true, + NegativePromptOption: true, + }, + { + Key: NodeTypeDataMerge, + Name: "结果汇集", + Group: NodeGroupBase, + Sort: 2, + Desc: "结果汇集节点,可配置结果汇集方式、结果保存、结果返回、结果展示等信息。", + BatchExecOption: false, + PreToolOption: false, + PostToolOption: false, + IsSaveFileOption: false, + FormConfigOption: false, + ModelConfigOption: false, + SkillOption: false, + PromptOption: false, + NegativePromptOption: false, + }, + { + Key: NodeTypeForm, + Name: "表单", + Group: NodeGroupBase, + Sort: 3, + Desc: "表单节点,可配置表单字段、表单配置、结果汇集、结果保存、结果返回、结果展示等信息。", + BatchExecOption: false, + PreToolOption: false, + PostToolOption: false, + IsSaveFileOption: false, + FormConfigOption: true, + ModelConfigOption: false, + SkillOption: false, + PromptOption: false, + NegativePromptOption: false, + }, + { + Key: NodeTypeSubFlow, + Name: "子流程", + Group: NodeGroupBase, + Sort: 4, + PresetOption: []NodePresetField{ + { + Field: "maxConcurrency", + Label: "生成次数", + Type: "input", + Value: "1", + IsFormField: true, + Constraint: FieldConstraint{ + Type: "int", + Min: 1, + Max: 10, + }, + Required: true, + }, + }, + }, + { + Key: NodeTypeHttp, + Name: "HTTP(S)接口", + Group: NodeGroupBase, + Sort: 5, + Desc: "HTTP(S)接口节点,可配置HTTP(S)接口地址、请求方式、请求头、请求体、结果返回结构、结果返回方式、结果返回结构、结果返回方式等信息。", + BatchExecOption: false, + PreToolOption: true, + PostToolOption: true, + IsSaveFileOption: false, + FormConfigOption: false, + ModelConfigOption: false, + SkillOption: false, + PromptOption: false, + NegativePromptOption: false, + PresetOption: []NodePresetField{ + { + Field: "method", + Label: "请求方式", + Type: "select", + Required: true, + Options: []SelectOption{ + {Key: "GET", Value: "GET"}, + {Key: "POST", Value: "POST"}, + }, + }, + { + Field: "url", + Label: "请求地址", + Type: "input", + Constraint: FieldConstraint{ + Type: "string", + Min: 1, + Max: -1, + }, + Required: true, + }, + { + Field: "headers", + Label: "请求头(支持Authorization鉴权)", + Type: "keyValue", + Required: false, + }, + { + Field: "bodyType", + Label: "请求体类型", + Type: "select", + Required: true, + Options: []SelectOption{ + {Key: "None", Value: "无"}, + {Key: "JSON", Value: "JSON"}, + }, + }, + { + Field: "body", + Label: "请求体内容", + Type: "schemaJson", + Required: false, + }, + { + Field: "response", + Label: "结果返回结构", + Type: "schemaJson", + Required: true, + }, + { + Field: "responseType", + Label: "结果返回方式", + Type: "select", + Required: true, + Options: []SelectOption{ + {Key: "sync", Value: "同步返回"}, + {Key: "callback", Value: "等候回调", Config: []NodePresetField{ + { + Field: "callbackUrl", + Label: "回调地址(只需要填写字段名称)", + Type: "input", + Constraint: FieldConstraint{ + Type: "string", + Min: 1, + Max: -1, + }, + Required: true, + }, + }}, + {Key: "pull", Value: "主动拉取", Config: []NodePresetField{ + { + Field: "method", + Label: "请求方式", + Type: "select", + Required: true, + Options: []SelectOption{ + {Key: "GET", Value: "GET"}, + {Key: "POST", Value: "POST"}, + }, + }, + { + Field: "url", + Label: "请求地址", + Type: "input", + Constraint: FieldConstraint{ + Type: "string", + Min: 1, + Max: -1, + }, + Required: true, + }, + { + Field: "headers", + Label: "请求头(支持Authorization鉴权)", + Type: "keyValue", + Required: false, + }, + { + Field: "bodyType", + Label: "请求体类型", + Type: "select", + Required: true, + Options: []SelectOption{ + {Key: "None", Value: "无"}, + {Key: "JSON", Value: "JSON"}, + }, + }, + { + Field: "body", + Label: "请求体内容", + Type: "schemaJson", + Required: true, + }, + { + Field: "response", + Label: "结果返回结构", + Type: "schemaJson", + Required: true, + }, + }}, + }, + }, + }, + }, + { + Key: NodeTypeScriptTranscribe, + Name: "脚本转写", + Group: NodeGroupBase, + Sort: 6, + Desc: "脚本转写节点,把文案/视频分析结果通过大模型转写为结构化分镜脚本([]Shot),作为视频生成节点的 shots 输入。", + BatchExecOption: false, + PreToolOption: false, + PostToolOption: false, + IsSaveFileOption: false, + FormConfigOption: false, + ModelConfigOption: true, + SkillOption: false, + PromptOption: false, + NegativePromptOption: false, + PresetOption: []NodePresetField{ + { + Field: "totalDuration", + Label: "视频总时长", + Type: "input", + Constraint: FieldConstraint{ + Type: "int", + Min: 1, + Max: 300, + }, + Required: true, + }, + { + Field: "modelId", + Label: "视频模型", + Type: "select", + Required: true, + Options: []SelectOption{}, + }, + }, + OutputField: []string{"prompt", "duration", "seed", "negative_prompt", "reference_urls"}, + }, + { + Key: NodeTypeSystemSum, + Name: "工作流内置节点-保存结果", + Group: NodeGroupSystem, + Sort: 7, + }, +} + +// ===================== 缓存map & 工具函数 ===================== +var ( + nodeTypeMetaMap map[NodeType]NodeTypeMeta + nodeGroupMap map[NodeGroup]NodeGroupMeta +) + +func init() { + // 初始化分组缓存 + nodeGroupMap = make(map[NodeGroup]NodeGroupMeta, len(NodeGroupMetaList)) + for _, meta := range NodeGroupMetaList { + nodeGroupMap[meta.Key] = meta + } + + // 初始化节点缓存 + nodeTypeMetaMap = make(map[NodeType]NodeTypeMeta, len(NodeTypeMetaList)) + for _, meta := range NodeTypeMetaList { + nodeTypeMetaMap[meta.Key] = meta + } +} + +// GetNodeTypeMeta 根据节点类型获取元信息 +func GetNodeTypeMeta(typ NodeType) (NodeTypeMeta, bool) { + meta, ok := nodeTypeMetaMap[typ] + return meta, ok +} + +// GetNodeTypeName 快速获取节点中文名称,不存在返回原始key +func GetNodeTypeName(typ NodeType) string { + meta, ok := nodeTypeMetaMap[typ] + if !ok { + return string(typ) + } + return meta.Name +} + +// GetNodeGroupMeta 根据分组标识获取分组元信息 +func GetNodeGroupMeta(g NodeGroup) (NodeGroupMeta, bool) { + meta, ok := nodeGroupMap[g] + return meta, ok +} + +// GetNodeGroupName 快速获取分组中文名称 +func GetNodeGroupName(g NodeGroup) string { + meta, ok := nodeGroupMap[g] + if !ok { + return string(g) + } + return meta.Name +} + +// GetAllNodeTypeMeta 返回全部节点元数据副本 +func GetAllNodeTypeMeta() []NodeTypeMeta { + list := make([]NodeTypeMeta, len(NodeTypeMetaList)) + copy(list, NodeTypeMetaList) + return list +} + +// GetNodeGroupTree 获取【分组+节点】树形列表,完整所有节点 +func GetNodeGroupTree() []NodeGroupTree { + return getFilteredNodeTree(nil) +} + +// GetFilterNodeTree 根据指定分组过滤节点树形结构 +// groups: 需要展示的分组key列表,传入nil返回全部 +func GetFilterNodeTree(groups []NodeGroup) []NodeGroupTree { + return getFilteredNodeTree(groups) +} + +// getFilteredNodeTree 内部通用树形构建逻辑 +func getFilteredNodeTree(filterGroups []NodeGroup) []NodeGroupTree { + // 构建分组过滤集合 + filterSet := make(map[NodeGroup]bool) + for _, g := range filterGroups { + filterSet[g] = true + } + + groupNodeMap := make(map[NodeGroup][]NodeTypeMeta) + for _, node := range NodeTypeMetaList { + // 有过滤条件 && 当前分组不在过滤列表,则跳过 + if len(filterSet) > 0 && !filterSet[node.Group] { + continue + } + groupNodeMap[node.Group] = append(groupNodeMap[node.Group], node) + } + + var tree []NodeGroupTree + // 按定义顺序组装分组 + for _, gMeta := range NodeGroupMetaList { + // 如果开启过滤,跳过不在筛选列表中的分组 + if len(filterSet) > 0 && !filterSet[gMeta.Key] { + continue + } + nodes := groupNodeMap[gMeta.Key] + if len(nodes) == 0 { + continue + } + tree = append(tree, NodeGroupTree{ + Group: gMeta, + Nodes: nodes, + }) + } + return tree } diff --git a/workflow/consts/public/public.go b/workflow/consts/public/public.go new file mode 100644 index 0000000..e94253a --- /dev/null +++ b/workflow/consts/public/public.go @@ -0,0 +1,3 @@ +package public + +const GmqMsgPluginsName = "gmq_model_msg" diff --git a/workflow/consts/public/table_name.go b/workflow/consts/public/table_name.go index 475553e..2665069 100644 --- a/workflow/consts/public/table_name.go +++ b/workflow/consts/public/table_name.go @@ -7,18 +7,19 @@ const ( // 数据库表名 const ( - TableNameCreationInfo = "creation_info" - TableNameFlowExecution = "flow_execution" - TableNameFlowTemplate = "flow_template" - TableNameFlowUser = "flow_user" - TableNameSkillTemplate = "skill_template" - TableNameSkillUser = "skill_user" - TableNameFileTemp = "file_temp" - TableNameActivePull = "active_pull" - TableNameFlowCheckpoint = "flow_checkpoint" - TableNameNodePrompt = "node_prompt" - TableNameNodeExecution = "node_execution" - TableNameSession = "session" - TableNameWorkflowSessionResult = "workflow_session_result" - TableNameChatSessionResult = "chat_session_result" + TableNameCreationInfo = "creation_info" + TableNameFlowExecution = "flow_execution" + TableNameFlowTemplate = "flow_template" + TableNameFlowUser = "flow_user" + TableNameSkillTemplate = "skill_template" + TableNameSkillUser = "skill_user" + TableNameFileTemp = "file_temp" + TableNameActivePull = "active_pull" + TableNameFlowCheckpoint = "flow_checkpoint" + TableNameNodePrompt = "node_prompt" + TableNameNodeExecution = "node_execution" + TableNameSession = "session" + TableNameExecChat = "exec_chat" + TableNameExecWorkflow = "exec_workflow" + TableNameExecWorkflowResult = "exec_workflow_result" ) diff --git a/workflow/controller/flow/flow_execution_controller.go b/workflow/controller/flow/flow_execution_controller.go index dd45492..c0fd29f 100644 --- a/workflow/controller/flow/flow_execution_controller.go +++ b/workflow/controller/flow/flow_execution_controller.go @@ -4,32 +4,19 @@ import ( flowDto "ai-agent/workflow/model/dto/flow" flowService "ai-agent/workflow/service/flow" "context" - - "gitea.redpowerfuture.com/red-future/common/beans" ) type flowExecution struct{} var FlowExecution = new(flowExecution) -func (c *flowExecution) Execute(ctx context.Context, req *flowDto.ExecuteReq) (res *flowDto.ExecuteRes, err error) { - return flowService.FlowExecutionService.Execute(ctx, req) -} - -func (c *flowExecution) ComposeCallBack(ctx context.Context, req *flowDto.ComposeCallbackReq) (res *beans.ResponseEmpty, err error) { - err = flowService.FlowExecutionService.ComposeCallback(ctx, req) - return -} - -func (c *flowExecution) ModelCallback(ctx context.Context, req *flowDto.ModelCallbackReq) (res *beans.ResponseEmpty, err error) { - err = flowService.FlowExecutionService.ModelCallback(ctx, req) - return -} - -func (c *flowExecution) VideoCallback(ctx context.Context, req *flowDto.VideoCallbackReq) (res *beans.ResponseEmpty, err error) { - err = flowService.FlowExecutionService.VideoCallback(ctx, req) - return -} +//func (c *flowExecution) Execute(ctx context.Context, req *flowDto.ExecuteReq) (res *flowDto.ExecuteRes, err error) { +// return flowService.FlowExecutionService.Execute(ctx, req) +//} +// +//func (c *flowExecution) ReExecute(ctx context.Context, req *flowDto.ReExecuteReq) (res *flowDto.ExecuteRes, err error) { +// return flowService.FlowExecutionService.ReExecute(ctx, req) +//} func (c *flowExecution) Get(ctx context.Context, req *flowDto.GetFlowExecutionReq) (res *flowDto.VOFlowExecution, err error) { return flowService.FlowExecutionService.Get(ctx, req) diff --git a/workflow/controller/session/session_controller.go b/workflow/controller/session/session_controller.go index 15ee212..e00efb6 100644 --- a/workflow/controller/session/session_controller.go +++ b/workflow/controller/session/session_controller.go @@ -1,18 +1,34 @@ package session import ( + flowDto "ai-agent/workflow/model/dto/flow" + sessionDto "ai-agent/workflow/model/dto/session" + "ai-agent/workflow/service/flow" + sessionService "ai-agent/workflow/service/session" "context" - sessionDto "ai-agent/workflow/model/dto/session" - sessionService "ai-agent/workflow/service/session" - "gitea.redpowerfuture.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/glog" ) type session struct{} var Session = new(session) +func (c *session) WsExecute(ctx context.Context, req *sessionDto.WebSocketConnectReq) (res *beans.ResponseEmpty, err error) { + r := g.RequestFromCtx(ctx) + // 首次连接仅升级 WebSocket,不区分普通对话/工作流;由后续消息 type 路由到对应处理器 + err = flow.WsConnect(ctx, r, req) + if err != nil { + glog.Errorf(ctx, "ws connect failed: %v", err) + return nil, err + } + // 标记响应缓冲区,使 MiddlewareHandlerResponse 跳过 + r.Response.Writeln("") + return +} + func (c *session) Get(ctx context.Context, req *sessionDto.GetSessionInfoReq) (res *sessionDto.GetSessionInfoRes, err error) { return sessionService.SessionService.Get(ctx, req) } @@ -27,3 +43,14 @@ func (c *session) Delete(ctx context.Context, req *sessionDto.DeleteSessionReq) } return &beans.ResponseEmpty{}, nil } + +func (c *session) DeleteRecord(ctx context.Context, req *sessionDto.DeleteSessionRecordReq) (res *beans.ResponseEmpty, err error) { + if err = sessionService.SessionService.DeleteRecord(ctx, req); err != nil { + return + } + return &beans.ResponseEmpty{}, nil +} + +func (c *session) ResultList(ctx context.Context, req *sessionDto.ListWorkflowResultReq) (res *flowDto.ListFlowExecutionTreeRes, err error) { + return sessionService.SessionService.ResultList(ctx, req) +} diff --git a/workflow/controller/tool/tool_controller.go b/workflow/controller/tool/tool_controller.go new file mode 100644 index 0000000..ab06b3a --- /dev/null +++ b/workflow/controller/tool/tool_controller.go @@ -0,0 +1,15 @@ +package tool + +import ( + toolDto "ai-agent/workflow/model/dto/tool" + toolService "ai-agent/workflow/service/tool" + "context" +) + +type tool struct{} + +var Tool = new(tool) + +func (c *tool) List(ctx context.Context, req *toolDto.ToolListReq) (res *toolDto.ToolListRes, err error) { + return toolService.ToolService.List(ctx, req) +} diff --git a/workflow/dao/flow/flow_checkpoint_dao.go b/workflow/dao/flow/flow_checkpoint_dao.go new file mode 100644 index 0000000..b9082c0 --- /dev/null +++ b/workflow/dao/flow/flow_checkpoint_dao.go @@ -0,0 +1,60 @@ +package flow + +import ( + "ai-agent/workflow/consts/public" + "ai-agent/workflow/model/entity" + "context" + + "gitea.redpowerfuture.com/red-future/common/db/gfdb" +) + +var FlowCheckpointDao = &flowCheckpointDao{} + +type flowCheckpointDao struct{} + +// Upsert 插入或更新checkpoint数据(按 checkpoint_id 冲突则更新) +func (d *flowCheckpointDao) Upsert(ctx context.Context, checkpointId string, data string) error { + record := &entity.FlowCheckpoint{ + CheckpointId: checkpointId, + Data: data, + } + // Save 在 PostgreSQL 中自动执行 INSERT ON CONFLICT DO UPDATE + _, err := gfdb.DB(ctx, public.DbNameBlackDeacon). + Model(ctx, public.TableNameFlowCheckpoint). + Save(record) + return err +} + +func (d *flowCheckpointDao) SaveOrUpdate(ctx context.Context, checkpointId string, data string) (err error) { + res := &entity.FlowCheckpoint{ + CheckpointId: checkpointId, + Data: data, + } + _, err = gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowCheckpoint).Data(res).OnConflict(entity.FlowCheckpointCol.CheckpointId).Save() + return err +} + +// Get 根据 checkpoint_id 获取 checkpoint 数据 +func (d *flowCheckpointDao) Get(ctx context.Context, checkpointId string) (res *entity.FlowCheckpoint, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon). + Model(ctx, public.TableNameFlowCheckpoint). + Where(entity.FlowCheckpointCol.CheckpointId, checkpointId). + One() + if err != nil { + return nil, err + } + if r == nil { + return nil, nil + } + err = r.Struct(&res) + return +} + +// Delete 根据 checkpoint_id 删除checkpoint数据 +func (d *flowCheckpointDao) Delete(ctx context.Context, checkpointId string) error { + _, err := gfdb.DB(ctx, public.DbNameBlackDeacon). + Model(ctx, public.TableNameFlowCheckpoint). + Where(entity.FlowCheckpointCol.CheckpointId, checkpointId). + Delete() + return err +} diff --git a/workflow/dao/node/node_execution_dao.go b/workflow/dao/node/node_execution_dao.go index 8933965..c054c18 100644 --- a/workflow/dao/node/node_execution_dao.go +++ b/workflow/dao/node/node_execution_dao.go @@ -86,6 +86,9 @@ func (d *nodeExecutionDao) Get(ctx context.Context, req *nodeDto.GetNodeExecutio func (d *nodeExecutionDao) ListByFlowExecutionId(ctx context.Context, req *nodeDto.ListNodeExecutionByFlowReq, fields ...string) (res []*entity.NodeExecution, total int, err error) { model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameNodeExecution).NoTenantId(ctx).Fields(fields).OmitEmpty() model.Where(entity.NodeExecutionCol.FlowExecutionId, req.FlowExecutionId) + model.Where(entity.NodeExecutionCol.NodeGroupId, req.NodeGroupId) + model.Where(entity.NodeExecutionCol.Status, req.Status) + model.Where(entity.NodeExecutionCol.NodeId, req.NodeId) model.OrderAsc(entity.NodeExecutionCol.CreatedAt) if req.Page != nil { model.Page(int(req.Page.PageNum), int(req.Page.PageSize)) diff --git a/workflow/dao/session/exec_chat_dao.go b/workflow/dao/session/exec_chat_dao.go index 7009443..e3822e1 100644 --- a/workflow/dao/session/exec_chat_dao.go +++ b/workflow/dao/session/exec_chat_dao.go @@ -51,7 +51,7 @@ func (d *execChatDao) List(ctx context.Context, creator string, page *beans.Page } // ListBySession 查询会话下普通对话执行记录(按创建时间倒序) -func (d *execChatDao) ListBySession(ctx context.Context, sessionId int64) (res []*entity.ExecChat, err error) { +func (d *execChatDao) ListBySession(ctx context.Context, sessionId string) (res []*entity.ExecChat, err error) { r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecChat). Where(entity.ExecChatCol.SessionId, sessionId). OrderDesc(entity.ExecChatCol.CreatedAt). diff --git a/workflow/dao/session/exec_workflow_dao.go b/workflow/dao/session/exec_workflow_dao.go index dc54dc5..dc54408 100644 --- a/workflow/dao/session/exec_workflow_dao.go +++ b/workflow/dao/session/exec_workflow_dao.go @@ -27,7 +27,7 @@ func (d *execWorkflowDao) Insert(ctx context.Context, req *sessionDto.CreateWork return r.LastInsertId() } -func (d *execWorkflowDao) Delete(ctx context.Context, req *sessionDto.DeleteWorkflowReq) (rows int64, err error) { +func (d *execWorkflowDao) Delete(ctx context.Context, req *sessionDto.DeleteExecWorkflowReq) (rows int64, err error) { r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).Where(entity.ExecWorkflowCol.Id, req.Id).Delete() if err != nil { return @@ -43,6 +43,17 @@ func (d *execWorkflowDao) Update(ctx context.Context, req *sessionDto.UpdateWork return r.RowsAffected() } +func (d *execWorkflowDao) GetById(ctx context.Context, id int64) (res *entity.ExecWorkflow, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow). + Where(entity.ExecWorkflowCol.Id, id). + One() + if err != nil { + return + } + err = r.Struct(&res) + return +} + func (d *execWorkflowDao) List(ctx context.Context, creator string, page *beans.Page) (res []*entity.ExecWorkflow, total int, err error) { m := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow). Where(entity.ExecWorkflowCol.Creator, creator) @@ -59,7 +70,7 @@ func (d *execWorkflowDao) List(ctx context.Context, creator string, page *beans. } // ListBySession 查询会话下工作流执行记录(按创建时间倒序) -func (d *execWorkflowDao) ListBySession(ctx context.Context, sessionId int64) (res []*entity.ExecWorkflow, err error) { +func (d *execWorkflowDao) ListBySession(ctx context.Context, sessionId string) (res []*entity.ExecWorkflow, err error) { r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow). Where(entity.ExecWorkflowCol.SessionId, sessionId). OrderDesc(entity.ExecWorkflowCol.CreatedAt). @@ -71,4 +82,39 @@ func (d *execWorkflowDao) ListBySession(ctx context.Context, sessionId int64) (r return } +// ListDates 按创建人查询去重后的创建日期(倒序,支持分页;page 为 nil 返回全部日期) +func (d *execWorkflowDao) ListDates(ctx context.Context, creator string, page *beans.Page) (dates []string, err error) { + m := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow). + Fields("DATE(created_at) AS create_date"). + Where(entity.ExecWorkflowCol.Creator, creator). + Group("create_date"). + OrderDesc("create_date") + if page != nil { + m.Page(int(page.PageNum), int(page.PageSize)) + } + r, err := m.All() + if err != nil { + return + } + for _, rec := range r { + dates = append(dates, rec["create_date"].String()) + } + return +} +// ListByDates 按创建人查询指定创建日期(DATE(created_at) 命中)内的执行记录,按创建时间倒序 +func (d *execWorkflowDao) ListByDates(ctx context.Context, creator string, dates []string) (res []*entity.ExecWorkflow, err error) { + if len(dates) == 0 { + return + } + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow). + Where(entity.ExecWorkflowCol.Creator, creator). + WhereIn("DATE(created_at)", dates). + OrderDesc(entity.ExecWorkflowCol.CreatedAt). + All() + if err != nil { + return + } + err = r.Structs(&res) + return +} diff --git a/workflow/dao/session/exec_workflow_result_dao.go b/workflow/dao/session/exec_workflow_result_dao.go index f996621..652adc4 100644 --- a/workflow/dao/session/exec_workflow_result_dao.go +++ b/workflow/dao/session/exec_workflow_result_dao.go @@ -27,6 +27,18 @@ func (d *execWorkflowResultDao) Insert(ctx context.Context, req *sessionDto.Crea return r.LastInsertId() } +func (d *execWorkflowResultDao) BatchInsert(ctx context.Context, req []*sessionDto.CreateWorkflowResultReq) (rows int64, err error) { + var res []*entity.ExecWorkflowResult + if err = gconv.Structs(req, &res); err != nil { + return + } + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult).Data(res).Save() + if err != nil { + return + } + return r.RowsAffected() +} + func (d *execWorkflowResultDao) Delete(ctx context.Context, req *sessionDto.DeleteWorkflowResultReq) (rows int64, err error) { r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult).Where(entity.ExecWorkflowResultCol.Id, req.Id).Delete() if err != nil { @@ -51,7 +63,7 @@ func (d *execWorkflowResultDao) List(ctx context.Context, creator string, page * } // ListBySession 查询会话下工作流结果(按创建时间倒序) -func (d *execWorkflowResultDao) ListBySession(ctx context.Context, sessionId int64) (res []*entity.ExecWorkflowResult, err error) { +func (d *execWorkflowResultDao) ListBySession(ctx context.Context, sessionId string) (res []*entity.ExecWorkflowResult, err error) { r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult). Where(entity.ExecWorkflowResultCol.SessionId, sessionId). OrderDesc(entity.ExecWorkflowResultCol.CreatedAt). @@ -62,3 +74,31 @@ func (d *execWorkflowResultDao) ListBySession(ctx context.Context, sessionId int err = r.Structs(&res) return } + +// ListByExecId 查询指定工作流执行记录下的结果文件路径 +func (d *execWorkflowResultDao) ListByExecId(ctx context.Context, execId int64) (res []*entity.ExecWorkflowResult, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult). + Where(entity.ExecWorkflowResultCol.ExecId, execId). + All() + if err != nil { + return + } + err = r.Structs(&res) + return +} + +// ListByExecIds 批量查询多个执行记录下的结果文件(按创建时间正序) +func (d *execWorkflowResultDao) ListByExecIds(ctx context.Context, execIds []int64) (res []*entity.ExecWorkflowResult, err error) { + if len(execIds) == 0 { + return + } + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflowResult). + WhereIn(entity.ExecWorkflowResultCol.ExecId, execIds). + OrderAsc(entity.ExecWorkflowResultCol.CreatedAt). + All() + if err != nil { + return + } + err = r.Structs(&res) + return +} diff --git a/workflow/dao/session/session_dao.go b/workflow/dao/session/session_dao.go index a9da7c9..2845217 100644 --- a/workflow/dao/session/session_dao.go +++ b/workflow/dao/session/session_dao.go @@ -27,8 +27,19 @@ func (d *sessionDao) Insert(ctx context.Context, req *sessionDto.CreateSessionRe return r.LastInsertId() } +func (d *sessionDao) GetById(ctx context.Context, id string) (res *entity.Session, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSession). + Where(entity.SessionCol.SessionId, id). + One() + if err != nil { + return + } + err = r.Struct(&res) + return +} + func (d *sessionDao) Delete(ctx context.Context, req *sessionDto.DeleteSessionReq) (rows int64, err error) { - r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSession).Where(entity.SessionCol.Id, req.Id).Delete() + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSession).Where(entity.SessionCol.SessionId, req.SessionId).Delete() if err != nil { return } diff --git a/workflow/model/dto/flow/flow_execution_dto.go b/workflow/model/dto/flow/flow_execution_dto.go index 2ffe70c..a8b8746 100644 --- a/workflow/model/dto/flow/flow_execution_dto.go +++ b/workflow/model/dto/flow/flow_execution_dto.go @@ -10,6 +10,14 @@ import ( "github.com/gogf/gf/v2/os/gtime" ) +// NodeExecutionState 记录图的节点执行进度,用于 checkpoint/resume 时追踪哪些节点已完成 +// SavedInput 用于在中断前保存节点入参,续跑后从状态中恢复 +type NodeExecutionState struct { + CompletedNodes []string `json:"completedNodes"` // 已成功执行的节点名称列表 + ExecutionCount int `json:"executionCount"` // 总执行调用次数 + SavedFlowInput *FlowExecutionInput `json:"savedFlowInput"` // 中断前保存的节点入参,续跑后读取 +} + // NodeExecutionInput 节点执行入参(包含配置+表单架构) type NodeExecutionInput struct { Config *entity.FlowNode `json:"config"` // 节点配置 @@ -17,130 +25,22 @@ type NodeExecutionInput struct { NodeExecutionId int64 `json:"nodeExecutionId"` } +// FlowExecutionInput 工作流执行入参(全程不变) +type FlowExecutionInput struct { + NodeGroupId string `json:"nodeGroupId"` + ExecutionId int64 `json:"executionId"` + FlowId int64 `json:"flowId"` + ConfigMap map[string]*entity.FlowNode `json:"configMap"` + SessionId string `json:"sessionId" dc:"会话ID"` + ExecutedNodes []ExecutedNode `json:"executedNodes"` // 已执行节点列表,包含执行状态 +} + // ExecutedNode 已执行节点记录,包含节点ID和执行状态 type ExecutedNode struct { NodeId string `json:"nodeId"` Status node.NodeExecutionStatus `json:"status"` // 执行状态:成功/失败 } -// FlowExecutionInput 工作流执行入参(全程不变) -type FlowExecutionInput struct { - NodeGroupId string `json:"nodeGroupId"` - IsDialogue bool `json:"isDialogue"` - ExecutionId int64 `json:"executionId"` - ConfigMap map[string]*entity.FlowNode `json:"configMap"` - SessionId string `json:"sessionId" dc:"会话ID"` - Desc string `json:"desc"` - SkillName string `json:"skillName"` - FileUrl []string `json:"fileUrl"` - ExecutedNodes []ExecutedNode `json:"executedNodes"` // 已执行节点列表,包含执行状态 -} - -type GetIsChatModelRes struct { - Model struct { - ModelName string `json:"modelName"` - ResponseBody map[string]any `json:"responseBody"` - } -} - -type GetModelInfoReq struct { - ModelName string `json:"modelName"` -} - -type GetModelInfoRes struct { - Model struct { - LastFrame string `json:"lastFrame"` - ResponseTokenField string `json:"responseTokenField"` - ResponseMapping map[string]any `json:"responseMapping"` - ResponseBody string `json:"responseBody"` - //QueryConfig struct { - // ResponseType string `json:"responseType"` - // CallbackUrl string `json:"callbackUrl"` - // Method string `json:"method"` - // Url string `json:"url"` - // Headers map[string]any `json:"headers"` - // Body map[string]any `json:"body"` - // Response []map[string]any `json:"response"` - // ResponseBody string `json:"responseBody"` - // ResponseTokenField string `json:"responseTokenField"` - //} `json:"queryConfig"` - } `json:"model"` -} - -type ComposeMessagesReq struct { - BuildType int `json:"buildType"` - ModelName string `json:"modelName"` - SkillName string `json:"skillName"` - CallbackUrl string `json:"callbackUrl"` - Form []map[string]any `json:"form"` - UserForm []map[string]any `json:"userForm"` - Consult []Consult `json:"consult"` - SessionId string `json:"sessionId" dc:"会话ID"` - NodeId string `json:"nodeId"` - Cause string `json:"cause"` -} - -type Consult struct { - Type string `json:"type"` - Url string `json:"url"` -} - -type ComposeMessagesRes struct { - TaskId string `json:"taskId"` -} - -type VideoConcatReq struct { - VideoUrls []string `json:"video_urls"` - Method string `json:"method"` - Upload bool `json:"upload"` - CallbackUrl string `json:"callback_url"` -} - -type VideoConcatRes struct { - TaskId string `json:"taskId"` -} - -type ModelGatewayReq struct { - ModelName string `json:"modelName"` - ModelKey string `json:"modelKey"` - BizName string `json:"bizName"` - CallbackUrl string `json:"callbackUrl"` - InputRef string `json:"inputRef"` - RequestPayload map[string]any `json:"requestPayload"` - EpicycleId int64 `json:"epicycleId" dc:"轮次ID"` -} - -type ModelGatewayRes struct { - TaskId string `json:"taskId"` -} - -type ComposeCallbackReq struct { - g.Meta `path:"/composeCallBack" method:"post" tags:"提示词处理" summary:"提示词 回调" dc:"提示词 成功后 GET 回调:callbackUrl/{bizName}"` - TaskId string `json:"taskId"` - Status string `json:"status"` - Messages struct { - TotalRounds int `json:"total_rounds"` // 总轮数 - Rounds []map[string]any `json:"rounds"` // 每轮详情(动态类型) - } `json:"messages,omitempty"` - EpicycleId int64 `json:"epicycleId"` - ErrorMsg string `json:"errorMsg,omitempty"` -} - -type ModelCallbackReq struct { - g.Meta `path:"/modelCallback" method:"post" tags:"提示词处理" summary:"model-gateway 回调" dc:"model-gateway 成功后 GET 回调:callbackUrl/{bizName}"` - TaskId string `p:"task_id" json:"task_id" v:"required#task_id不能为空" dc:"网关任务ID"` - State int `p:"state" json:"state" dc:"网关任务状态"` - OssFile string `p:"oss_file" json:"oss_file" dc:"结果文件地址"` - FileType string `p:"file_type" json:"file_type" dc:"结果文件类型"` - ErrorMsg string `json:"error_msg"` -} - -type VideoCallbackReq struct { - g.Meta `path:"/videoCallback" method:"post" tags:"视频处理" summary:"media 回调" dc:"media 成功后 GET 回调:callbackUrl/{bizName}"` - TaskId string `json:"taskId"` - FileURL string `json:"fileUrl"` -} - //============================================================================= // 原始入参结构体 @@ -175,11 +75,12 @@ type ExecuteReq struct { g.Meta `path:"/execute" method:"post" tags:"任务管理" summary:"执行任务" dc:"执行任务"` FlowId int64 `json:"flowId" dc:"用户流程ID"` + FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"` + SessionId string `json:"sessionId" dc:"会话ID"` + SessionName string `json:"sessionName"` FlowName string `json:"flowName"` NodeGroupId string `json:"nodeGroupId"` - FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"` NodeInputParams []*entity.FlowNode `json:"nodeInputParams" description:"节点输入参数"` - SessionId string `json:"sessionId" dc:"会话ID"` Desc string `json:"desc"` SkillName string `json:"skillName"` FileUrl []string `json:"fileUrl"` @@ -190,6 +91,12 @@ type ExecuteRes struct { Id int64 `json:"id,string" dc:"执行记录ID,用于查询执行状态和结果"` } +type ReExecuteReq struct { + g.Meta `path:"/reExecute" method:"post" tags:"任务管理" summary:"重新执行任务" dc:"重新执行任务"` + + ExecutionId int64 `json:"executionId"` +} + type CancelReq struct { g.Meta `path:"/cancel" method:"post" tags:"任务管理" summary:"取消任务" dc:"取消任务"` @@ -218,11 +125,14 @@ type CreateFlowExecutionRes struct { type UpdateFlowExecutionReq struct { Id int64 `json:"id" v:"required#ID不能为空"` NodeGroupId string `json:"nodeGroupId"` + FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"` DurationMs int64 `json:"durationMs" description:"执行时长(毫秒)"` Status flow.FlowExecutionStatus `json:"status" description:"状态:1-运行中,2-成功,3-失败"` OutputParams []map[string]interface{} `json:"outputParams" description:"输出参数"` ErrorMessage string `json:"errorMessage" description:"错误信息"` TraceId string `json:"traceId" description:"跟踪ID"` + TotalTokens int `json:"totalTokens" description:"总token"` + TotalFee float64 `json:"totalFee" description:"总费用"` } type GetFlowExecutionReq struct { @@ -265,10 +175,9 @@ type VOFlowExecution struct { // ========== 核心:构建树状结构 ========== // 定义树结构 type OutputItem struct { - Timestamp string `json:"timestamp" description:"时间戳key"` - Content string `json:"content" description:"内容值"` - Type string `json:"type" description:"类型"` - Label string `json:"label" description:"后缀+数字标号"` + Content string `json:"content" description:"内容值"` + Type string `json:"type" description:"类型"` + Label string `json:"label" description:"后缀+数字标号"` } type FlowNode struct { FlowName string `json:"flowName" description:"流程名称"` diff --git a/workflow/model/dto/flow/flow_user_dto.go b/workflow/model/dto/flow/flow_user_dto.go index 6dd450b..8d64dce 100644 --- a/workflow/model/dto/flow/flow_user_dto.go +++ b/workflow/model/dto/flow/flow_user_dto.go @@ -52,6 +52,7 @@ type ListFlowUserReq struct { g.Meta `path:"/list" method:"get" tags:"用户流程管理" summary:"获取用户流程列表" dc:"分页查询用户流程列表,支持多条件筛选"` Page *beans.Page `json:"page"` + IsOwn bool `json:"isOwn" dc:"是否是自己的流程"` Creator string `json:"creator"` Keyword string `json:"keyword" dc:"关键词搜索"` } diff --git a/workflow/model/dto/node/node_execution_dto.go b/workflow/model/dto/node/node_execution_dto.go index e91ec1c..4480966 100644 --- a/workflow/model/dto/node/node_execution_dto.go +++ b/workflow/model/dto/node/node_execution_dto.go @@ -38,6 +38,7 @@ type UpdateNodeExecutionReq struct { PromptTokens int `json:"promptTokens"` CompletionTokens int `json:"completionTokens"` TotalTokens int `json:"totalTokens"` + TokenInfo []map[string]any `json:"tokenInfo"` Status node.NodeExecutionStatus `json:"status"` DurationMs int64 `json:"durationMs"` ErrorMessage string `json:"errorMessage"` @@ -58,8 +59,11 @@ type GetNodeExecutionReq struct { // ListNodeExecutionByFlowReq 查询流程下所有节点执行记录请求 type ListNodeExecutionByFlowReq struct { g.Meta `path:"/listByFlow" method:"get" tags:"节点执行记录" summary:"查询流程节点执行列表" dc:"查询指定流程执行下的所有节点执行记录"` - Page *beans.Page `json:"page"` - FlowExecutionId int64 `json:"flowExecutionId" v:"required#流程执行ID不能为空"` + Page *beans.Page `json:"page"` + FlowExecutionId int64 `json:"flowExecutionId" v:"required#流程执行ID不能为空"` + NodeGroupId string `json:"nodeGroupId"` + NodeId string `json:"nodeId"` + Status node.NodeExecutionStatus `json:"status"` } // NodeExecutionResp 节点执行记录响应 diff --git a/workflow/model/dto/node/node_library_dto.go b/workflow/model/dto/node/node_library_dto.go index 8b6848a..f8c77dc 100644 --- a/workflow/model/dto/node/node_library_dto.go +++ b/workflow/model/dto/node/node_library_dto.go @@ -13,21 +13,5 @@ type WorkflowNodeTreeReq struct { } type WorkflowNodeTreeRes struct { - Groups []node.NodeGroupItem `json:"groups"` -} - -type TypeGroup struct { - TypeId int `json:"typeId"` - Type string `json:"type"` - Items []ModelItem `json:"items"` -} - -type ModelItem struct { - ID int64 `json:"id"` - Name string `json:"name"` - Form []node.NodeFormField `json:"form"` -} - -type ModelTypeResponse struct { - Type map[int]string `json:"type"` // key 自动解析为整数 100/200/300... + Groups []node.NodeGroupTree `json:"groups"` } diff --git a/workflow/model/dto/session/exec_chat_dto.go b/workflow/model/dto/session/exec_chat_dto.go new file mode 100644 index 0000000..60d747e --- /dev/null +++ b/workflow/model/dto/session/exec_chat_dto.go @@ -0,0 +1,19 @@ +package session + +import ( + "ai-agent/workflow/model/entity" +) + +type CreateExecChatReq struct { + SessionId string `json:"sessionId" description:"所属会话ID"` + Duration int64 `json:"duration" description:"执行时长(秒)"` + RequestParams entity.ExecChatRequestParams `json:"requestParams" description:"请求参数"` + ResultFileUrl string `json:"resultFileUrl" description:"结果文件路径"` + TotalTokens int `json:"totalTokens" description:"总token消耗"` + TotalFee float64 `json:"totalFee" description:"总费用"` + ErrorMessage string `json:"errorMessage" description:"错误信息"` +} + +type DeleteExecChatReq struct { + Id []int64 `json:"id" v:"required#会话执行记录ID不能为空"` +} diff --git a/workflow/model/dto/session/exec_workflow_dto.go b/workflow/model/dto/session/exec_workflow_dto.go new file mode 100644 index 0000000..f5886f2 --- /dev/null +++ b/workflow/model/dto/session/exec_workflow_dto.go @@ -0,0 +1,29 @@ +package session + +import ( + "ai-agent/workflow/consts/flow" + "ai-agent/workflow/model/entity" +) + +type CreateWorkflowReq struct { + SessionId string `json:"sessionId" description:"所属会话ID"` + FlowId int64 `json:"flowId" description:"工作流ID"` + NodeGroupId string `json:"nodeGroupId" description:"节点组ID"` + Status flow.FlowExecutionStatus `json:"status" description:"状态:1-运行中,2-成功,3-失败"` + RequestParams *entity.FlowInfo `json:"requestParams" description:"请求参数"` + ErrorMessage string `json:"errorMessage" description:"错误信息"` +} + +type DeleteExecWorkflowReq struct { + Id []int64 `json:"id" v:"required#工作流执行记录ID不能为空"` +} + +type UpdateWorkflowReq struct { + Id int64 `json:"id" v:"required#工作流执行记录ID不能为空"` + NodeGroupId string `json:"nodeGroupId" description:"节点组ID"` + Status flow.FlowExecutionStatus `json:"status" description:"状态:1-运行中,2-成功,3-失败"` + Duration int64 `json:"duration" description:"执行时长(秒)"` + TotalTokens int `json:"totalTokens" description:"总token消耗"` + TotalFee float64 `json:"totalFee" description:"总费用"` + ErrorMessage string `json:"errorMessage" description:"错误信息"` +} diff --git a/workflow/model/dto/session/exec_workflow_result_dto.go b/workflow/model/dto/session/exec_workflow_result_dto.go new file mode 100644 index 0000000..f72c71d --- /dev/null +++ b/workflow/model/dto/session/exec_workflow_result_dto.go @@ -0,0 +1,12 @@ +package session + +type CreateWorkflowResultReq struct { + SessionId string `json:"sessionId" description:"所属会话ID"` + FlowId int64 `json:"flowId" description:"工作流ID"` + ExecId int64 `json:"execId" description:"执行ID"` + ResultFileUrl string `json:"resultFileUrl" description:"结果文件路径"` +} + +type DeleteWorkflowResultReq struct { + Id int64 `json:"id" v:"required#ID不能为空"` +} diff --git a/workflow/model/dto/session/session_dto.go b/workflow/model/dto/session/session_dto.go index 67115c6..ea93b28 100644 --- a/workflow/model/dto/session/session_dto.go +++ b/workflow/model/dto/session/session_dto.go @@ -1,30 +1,39 @@ package session import ( + "ai-agent/workflow/model/entity" + "gitea.redpowerfuture.com/red-future/common/beans" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" ) -// 结果状态(自包含,不复用 flow 的 *int8 指针类型) -const ( - ResultStatusRunning = 1 // 运行中 - ResultStatusSuccess = 2 // 成功 - ResultStatusFailed = 3 // 失败 -) - -type CreateSessionReq struct { - g.Meta `path:"/create" method:"post" tags:"会话管理" summary:"新建会话" dc:"新建会话"` - SessionName string `json:"sessionName" v:"required#会话名称不能为空" dc:"会话名称"` +// WebSocketConnectReq WebSocket 连接请求(query string 参数) +type WebSocketConnectReq struct { + g.Meta `path:"/wsExecute" method:"get" tags:"工作流执行" summary:"WebSocket执行工作流" dc:"通过WebSocket连接实时执行工作流,支持进度推送和取消"` + SessionId string `p:"sessionId" dc:"会话ID"` } -type CreateSessionRes struct { - Id int64 `json:"id,string" dc:"会话ID"` +type WebSocketExecChatReq struct { + ModelId int64 `json:"modelId" dc:"模型ID" v:"required#模型ID不能为空"` + Question string `json:"question" dc:"用户提问" v:"required#用户提问不能为空"` + SystemPrompt string `json:"systemPrompt" dc:"系统提示词"` +} + +type WebSocketExecWorkflowReq struct { + FlowId int64 `json:"flowId" dc:"用户流程ID" v:"required#用户流程ID不能为空"` + FlowContent *entity.FlowInfo `json:"flowContent" dc:"用户流程内容" v:"required#用户流程内容不能为空"` +} + +type CreateSessionReq struct { + SessionId string `json:"sessionId" dc:"会话ID"` + SessionName string `json:"sessionName" dc:"会话名称"` } type ListSessionReq struct { - g.Meta `path:"/list" method:"get" tags:"会话管理" summary:"会话列表" dc:"会话列表"` - Page *beans.Page `json:"page"` + g.Meta `path:"/list" method:"get" tags:"会话管理" summary:"会话列表" dc:"会话列表"` + PageNum int64 `json:"pageNum" dc:"页码,从1开始"` + PageSize int64 `json:"pageSize" dc:"每页数量"` } type ListSessionRes struct { @@ -33,69 +42,52 @@ type ListSessionRes struct { } type VOSession struct { - Id int64 `json:"id,string" dc:"会话ID"` + SessionId string `json:"sessionId" dc:"会话ID"` SessionName string `json:"sessionName" dc:"会话名称"` CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` } type DeleteSessionReq struct { - g.Meta `path:"/delete" method:"post" tags:"会话管理" summary:"删除会话" dc:"删除会话,级联删除该会话下普通对话结果,工作流结果保留"` - Id int64 `json:"id" v:"required#会话ID不能为空"` + g.Meta `path:"/delete" method:"post" tags:"会话管理" summary:"删除会话" dc:"删除会话"` + SessionId string `json:"sessionId" v:"required#会话ID不能为空"` } -type ListSessionResultsReq struct { - g.Meta `path:"/results" method:"get" tags:"会话管理" summary:"会话内结果列表" dc:"会话内结果列表,工作流+普通对话混排按时间倒序"` - SessionId int64 `json:"sessionId" v:"required#会话ID不能为空"` +type DeleteSessionRecordReq struct { + g.Meta `path:"/deleteRecord" method:"post" tags:"会话管理" summary:"删除会话记录" dc:"删除会话记录"` + Ids []struct { + Id int64 `json:"id" v:"required#ID不能为空"` + Type string `json:"type" v:"required#类型不能为空"` + } } -type ListSessionResultsRes struct { - List []*VOSessionResult `json:"list"` +type GetSessionInfoReq struct { + g.Meta `path:"/get" method:"get" tags:"会话管理" summary:"会话内结果列表" dc:"会话内结果列表,工作流+普通对话混排按时间倒序"` + PageNum int64 `json:"pageNum" dc:"页码,从1开始"` + PageSize int64 `json:"pageSize" dc:"每页数量"` + SessionId string `json:"sessionId" v:"required#会话ID不能为空"` } -// VOSessionResult 会话内单条结果(工作流/普通对话混排) -type VOSessionResult struct { - ResultId int64 `json:"resultId,string" dc:"结果ID"` - Type string `json:"type" dc:"workflow-工作流结果,chat-普通对话结果"` - Status int `json:"status" dc:"1-运行中,2-成功,3-失败"` - FlowId int64 `json:"flowId,string" dc:"工作流ID(chat类型为空)"` - FlowName string `json:"flowName" dc:"工作流名称(chat类型为空)"` - Question string `json:"question" dc:"用户问题(workflow类型为空)"` - Answer string `json:"answer" dc:"模型返回结果(workflow类型为空)"` - RequestParams map[string]any `json:"requestParams" dc:"请求参数(workflow类型)"` - ResultParams []map[string]any `json:"resultParams" dc:"返回结果"` - TotalTokens int `json:"totalTokens" dc:"总token消耗"` - TotalFee float64 `json:"totalFee" dc:"总费用"` - ErrorMsg string `json:"errorMsg" dc:"错误信息"` - CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` +type GetSessionInfoRes struct { + List []*VOSessionInfoResult `json:"list"` + Total int `json:"total"` } -type DeleteSessionResultReq struct { - g.Meta `path:"/resultDelete" method:"post" tags:"会话管理" summary:"删除单条结果" dc:"删除单条结果,不影响会话"` - Type string `json:"type" v:"required#结果类型不能为空" dc:"workflow-工作流结果,chat-普通对话结果"` - Id int64 `json:"id" v:"required#结果ID不能为空"` +// VOSessionInfoResult 会话内单条结果(工作流/普通对话混排) +type VOSessionInfoResult struct { + Id int64 `json:"id,string" dc:"结果ID"` + Type string `json:"type" dc:"workflow-工作流结果,chat-普通对话结果"` + Status int `json:"status" dc:"1-运行中,2-成功,3-失败"` + FlowId int64 `json:"flowId,string" dc:"工作流ID(chat类型为空)"` + RequestParams map[string]any `json:"requestParams" description:"请求参数"` + ResultFileUrl string `json:"resultFileUrl" description:"结果文件路径(供预览/下载)"` + ResultContent string `json:"resultContent" description:"结果文件内容(服务端已读取,前端直接展示)"` + TotalTokens int `json:"totalTokens" dc:"总token消耗"` + TotalFee float64 `json:"totalFee" dc:"总费用"` + ErrorMsg string `json:"errorMsg" dc:"错误信息"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` } -type ListWorkflowResultsReq struct { - g.Meta `path:"/workflowResults" method:"get" tags:"会话管理" summary:"工作流结果平铺列表" dc:"工作流维度所有结果平铺列表,含已删除会话下的结果"` +type ListWorkflowResultReq struct { + g.Meta `path:"/resultList" method:"get" tags:"会话管理" summary:"工作流执行结果树" dc:"按创建人分页查询工作流执行结果,按天分组返回树结构(日期→流程→结果文件),pageSize=每页天数,不传返回全部"` Page *beans.Page `json:"page"` - FlowId int64 `json:"flowId" dc:"按工作流ID过滤,可选"` } - -type ListWorkflowResultsRes struct { - List []*VOWorkflowResult `json:"list"` - Total int `json:"total"` -} - -type VOWorkflowResult struct { - ResultId int64 `json:"resultId,string" dc:"结果ID"` - SessionId int64 `json:"sessionId,string" dc:"所属会话ID"` - FlowId int64 `json:"flowId,string" dc:"工作流ID"` - FlowName string `json:"flowName" dc:"工作流名称"` - RequestParams map[string]any `json:"requestParams" dc:"请求参数"` - ResultParams []map[string]any `json:"resultParams" dc:"返回结果"` - Status int `json:"status" dc:"1-运行中,2-成功,3-失败"` - TotalTokens int `json:"totalTokens" dc:"总token消耗"` - TotalFee float64 `json:"totalFee" dc:"总费用"` - ErrorMsg string `json:"errorMsg" dc:"错误信息"` - CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` -} \ No newline at end of file diff --git a/workflow/model/dto/tool/tool_dto.go b/workflow/model/dto/tool/tool_dto.go new file mode 100644 index 0000000..055431e --- /dev/null +++ b/workflow/model/dto/tool/tool_dto.go @@ -0,0 +1,35 @@ +package tool + +import ( + "github.com/gogf/gf/v2/frame/g" +) + +// ToolListReq 工具列表查询请求 +type ToolListReq struct { + g.Meta `path:"/list" method:"get" tags:"工具管理" summary:"工具列表" dc:"查询已注册的模型工具列表"` +} + +// ToolVO 工具对外信息 +type ToolVO struct { + Name string `json:"name"` + Description string `json:"description"` +} + +// ToolListRes 工具列表查询响应 +type ToolListRes struct { + List []*ToolVO `json:"list"` +} + +// ToolAgentReq 工具对话请求:前端用户提问,模型通过 function calling 使用全部已注册模型工具作答 +type ToolAgentReq struct { + g.Meta `path:"/agent" method:"post" tags:"工具管理" summary:"工具对话" dc:"用户提问,模型通过 function calling 使用全部已注册模型工具作答"` + Question string `json:"question" dc:"用户提问"` + ModelId int64 `json:"modelId,string" dc:"模型ID"` + SessionId string `json:"sessionId" dc:"会话ID,透传给网关记账"` + SystemPrompt string `json:"systemPrompt" dc:"系统提示词,为空使用默认"` +} + +// ToolAgentRes 工具对话响应 +type ToolAgentRes struct { + Answer string `json:"answer"` +} diff --git a/workflow/model/entity/exec_chat.go b/workflow/model/entity/exec_chat.go new file mode 100644 index 0000000..ad5343b --- /dev/null +++ b/workflow/model/entity/exec_chat.go @@ -0,0 +1,41 @@ +package entity + +import "gitea.redpowerfuture.com/red-future/common/beans" + +// ExecChat 执行会话 +type ExecChat struct { + beans.SQLBaseDO `orm:",inherit"` + SessionId string `orm:"session_id" json:"sessionId" description:"会话ID"` + Duration int64 `orm:"duration" json:"duration" description:"执行时长(秒)"` + RequestParams ExecChatRequestParams `orm:"request_params" json:"requestParams" description:"请求参数"` + ResultFileUrl string `orm:"result_file_url" json:"resultFileUrl" description:"结果文件路径"` + TotalTokens int `orm:"total_tokens" json:"totalTokens" description:"总token消耗"` + TotalFee float64 `orm:"total_fee" json:"totalFee" description:"总费用"` + ErrorMessage string `orm:"error_message" json:"errorMessage" description:"错误信息"` +} + +type ExecChatRequestParams struct { + Question string `json:"question"` +} + +type execChatCol struct { + beans.SQLBaseCol + SessionId string + Duration string + RequestParams string + ResultFileUrl string + TotalTokens string + TotalFee string + ErrorMessage string +} + +var ExecChatCol = execChatCol{ + SQLBaseCol: beans.DefSQLBaseCol, + SessionId: "session_id", + Duration: "duration", + RequestParams: "request_params", + ResultFileUrl: "result_file_url", + TotalTokens: "total_tokens", + TotalFee: "total_fee", + ErrorMessage: "error_message", +} diff --git a/workflow/model/entity/exec_workflow.go b/workflow/model/entity/exec_workflow.go new file mode 100644 index 0000000..19c62ef --- /dev/null +++ b/workflow/model/entity/exec_workflow.go @@ -0,0 +1,47 @@ +package entity + +import ( + "ai-agent/workflow/consts/flow" + + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// ExecWorkflow 执行工作流 +type ExecWorkflow struct { + beans.SQLBaseDO `orm:",inherit"` + SessionId string `orm:"session_id" json:"sessionId" description:"会话ID"` + FlowId int64 `orm:"flow_id" json:"flowId" description:"工作流ID"` + NodeGroupId string `orm:"node_group_id" json:"nodeGroupId" description:"节点组ID"` + Duration int64 `orm:"duration" json:"duration" description:"执行时长(秒)"` + RequestParams *FlowInfo `orm:"request_params" json:"requestParams" description:"请求参数"` + Status flow.FlowExecutionStatus `orm:"status" json:"status" description:"状态:1-运行中,2-成功,3-失败"` + TotalTokens int `orm:"total_tokens" json:"totalTokens" description:"总token消耗"` + TotalFee float64 `orm:"total_fee" json:"totalFee" description:"总费用"` + ErrorMessage string `orm:"error_message" json:"errorMessage" description:"错误信息"` +} + +type execWorkflowCol struct { + beans.SQLBaseCol + SessionId string + FlowId string + NodeGroupId string + Duration string + RequestParams string + Status string + TotalTokens string + TotalFee string + ErrorMessage string +} + +var ExecWorkflowCol = execWorkflowCol{ + SQLBaseCol: beans.DefSQLBaseCol, + SessionId: "session_id", + FlowId: "flow_id", + NodeGroupId: "node_group_id", + Duration: "duration", + RequestParams: "request_params", + Status: "status", + TotalTokens: "total_tokens", + TotalFee: "total_fee", + ErrorMessage: "error_message", +} diff --git a/workflow/model/entity/exec_workflow_result.go b/workflow/model/entity/exec_workflow_result.go new file mode 100644 index 0000000..84d2ec1 --- /dev/null +++ b/workflow/model/entity/exec_workflow_result.go @@ -0,0 +1,28 @@ +package entity + +import "gitea.redpowerfuture.com/red-future/common/beans" + +// ExecWorkflowResult 执行工作流结果 +type ExecWorkflowResult struct { + beans.SQLBaseDO `orm:",inherit"` + SessionId string `orm:"session_id" json:"sessionId" description:"会话ID"` + FlowId int64 `orm:"flow_id" json:"flowId" description:"工作流ID"` + ExecId int64 `orm:"exec_id" json:"execId" description:"执行ID"` + ResultFileUrl string `orm:"result_file_url" json:"resultFileUrl" description:"结果文件路径"` +} + +type execWorkflowResultCol struct { + beans.SQLBaseCol + SessionId string + FlowId string + ExecId string + ResultFileUrl string +} + +var ExecWorkflowResultCol = execWorkflowResultCol{ + SQLBaseCol: beans.DefSQLBaseCol, + SessionId: "session_id", + FlowId: "flow_id", + ExecId: "exec_id", + ResultFileUrl: "result_file_url", +} diff --git a/workflow/model/entity/flow_checkpoint.go b/workflow/model/entity/flow_checkpoint.go new file mode 100644 index 0000000..1930f8f --- /dev/null +++ b/workflow/model/entity/flow_checkpoint.go @@ -0,0 +1,25 @@ +package entity + +import ( + "gitea.redpowerfuture.com/red-future/common/beans" +) + +// FlowCheckpoint checkpoint数据实体,对应 workflow_interrupt 表 +type FlowCheckpoint struct { + beans.SQLBaseDO `orm:",inherit"` // 嵌入基础字段:Id, TenantId, Creator, CreatedAt, Updater, UpdatedAt, DeletedAt + // 业务字段 + CheckpointId string `orm:"checkpoint_id" json:"checkpointId" description:"Checkpoint ID(执行ID)"` + Data string `orm:"data" json:"data" description:"Checkpoint序列化数据"` +} + +type flowCheckpointCol struct { + beans.SQLBaseCol + CheckpointId string + Data string +} + +var FlowCheckpointCol = flowCheckpointCol{ + SQLBaseCol: beans.DefSQLBaseCol, + CheckpointId: "checkpoint_id", + Data: "data", +} diff --git a/workflow/model/entity/flow_execution.go b/workflow/model/entity/flow_execution.go index 6f90b0e..0bbdc3f 100644 --- a/workflow/model/entity/flow_execution.go +++ b/workflow/model/entity/flow_execution.go @@ -22,6 +22,7 @@ type FlowExecution struct { TraceId string `orm:"trace_id" json:"traceId" description:"跟踪ID"` SessionId string `orm:"session_id" json:"sessionId" description:"会话ID"` TotalTokens int `orm:"total_tokens" json:"totalTokens" description:"总token消耗"` + TotalFee int `orm:"total_fee" json:"totalFee" description:"总费用"` } type flowExecutionCol struct { @@ -39,6 +40,7 @@ type flowExecutionCol struct { TraceId string SessionId string TotalTokens string + TotalFee string } var FlowExecutionCol = flowExecutionCol{ @@ -56,4 +58,5 @@ var FlowExecutionCol = flowExecutionCol{ TraceId: "trace_id", SessionId: "session_id", TotalTokens: "total_tokens", + TotalFee: "total_fee", } diff --git a/workflow/model/entity/flow_user.go b/workflow/model/entity/flow_user.go index bb92c2f..66bc276 100644 --- a/workflow/model/entity/flow_user.go +++ b/workflow/model/entity/flow_user.go @@ -15,19 +15,33 @@ type FlowInfo struct { } type FlowNode struct { - Id string `json:"id"` - NodeCode node.NodeType `json:"nodeCode"` - Name string `json:"name"` - Config map[string]interface{} `json:"config"` - SkillName string `json:"skillName"` - PromptContent string `json:"promptContent"` - IsSaveFile bool `json:"isSaveFile"` - InputSource []FlowNodeInputSource `json:"inputSource"` // 前端指定:来源节点ID - SubConfig *SubFlowConfig `json:"subConfig"` - FormConfig []node.NodeFormField `json:"formConfig"` - ModelConfig node.ModelItem `json:"modelConfig"` - OutputConfig []node.NodeFormField `json:"outputConfig"` - OutputResult []node.NodeFormField `json:"outputResult" ds:"节点输出结果"` + Id string `json:"id"` + NodeCode node.NodeType `json:"nodeCode"` + Name string `json:"name"` + Desc string `json:"desc"` + Config map[string]interface{} `json:"config"` + IsBatchExec bool `json:"isBatchExec"` + PreTool string `json:"preTool"` + PostTool string `json:"postTool"` + IsSaveFile bool `json:"isSaveFile"` + SubConfig *SubFlowConfig `json:"subConfig"` + ModelConfig ModelItem `json:"modelConfig"` + OutputConfig []map[string]any `json:"outputConfig"` + Prompt string `json:"prompt"` + NegativePrompt string `json:"negativePrompt"` + //SkillName string `json:"skillName"` + //PromptContent string `json:"promptContent"` + //InputSource []FlowNodeInputSource `json:"inputSource"` // 前端指定:来源节点ID + //FormConfig []node.NodeFormField `json:"formConfig"` + OutputResult []map[string]any `json:"outputResult" ds:"节点输出结果"` +} + +type ModelItem struct { + ModelId int64 `json:"modelId,string"` + ModelName string `json:"modelName"` + ModelFormFields []map[string]any `json:"modelFormFields"` + ModelRequestParams map[string]any `json:"modelRequestParams"` + ModelResponseBodyMapping map[string]any `json:"modelResponseBodyMapping"` } type FlowNodeInputSource struct { @@ -45,7 +59,9 @@ type FlowField struct { // SubFlowConfig 子流程节点配置 type SubFlowConfig struct { - FlowId int64 `json:"flowId"` + WorkflowId int64 `json:"workflowId"` + WorkflowName string `json:"workflowName"` + Fields []map[string]any `json:"fields"` MaxConcurrency int `json:"maxConcurrency"` // 子流程并发数 InputSource []FlowNodeInputSource `json:"inputSource"` // 前端指定:来源节点ID } diff --git a/workflow/model/entity/node_execution.go b/workflow/model/entity/node_execution.go index b8b5e08..d9d9f3a 100644 --- a/workflow/model/entity/node_execution.go +++ b/workflow/model/entity/node_execution.go @@ -22,6 +22,7 @@ type NodeExecution struct { PromptTokens int `orm:"prompt_tokens" json:"promptTokens" description:"提示词token消耗"` CompletionTokens int `orm:"completion_tokens" json:"completionTokens" description:"补全token消耗"` TotalTokens int `orm:"total_tokens" json:"totalTokens" description:"总token消耗"` + TokenInfo []map[string]interface{} `orm:"token_info" json:"tokenInfo" description:"token信息"` Status node.NodeExecutionStatus `orm:"status" json:"status" description:"执行状态:1-运行中,2-成功,3-失败,4-暂停,5-等待执行"` DurationMs int64 `orm:"duration_ms" json:"durationMs" description:"执行时长(毫秒)"` ErrorMessage string `orm:"error_message" json:"errorMessage" description:"错误信息"` @@ -40,6 +41,7 @@ type nodeExecutionCol struct { PromptTokens string CompletionTokens string TotalTokens string + TokenInfo string Status string DurationMs string ErrorMessage string @@ -58,6 +60,7 @@ var NodeExecutionCol = nodeExecutionCol{ PromptTokens: "prompt_tokens", CompletionTokens: "completion_tokens", TotalTokens: "total_tokens", + TokenInfo: "token_info", Status: "status", DurationMs: "duration_ms", ErrorMessage: "error_message", diff --git a/workflow/model/entity/session.go b/workflow/model/entity/session.go index c345535..34daae3 100644 --- a/workflow/model/entity/session.go +++ b/workflow/model/entity/session.go @@ -5,86 +5,18 @@ import "gitea.redpowerfuture.com/red-future/common/beans" // Session 会话记录 type Session struct { beans.SQLBaseDO `orm:",inherit"` // 嵌入基础字段:Id, TenantId, Creator, CreatedAt, Updater, UpdatedAt, DeletedAt + SessionId string `orm:"session_id" json:"sessionId" description:"会话ID"` SessionName string `orm:"session_name" json:"sessionName" description:"会话名称"` } type sessionCol struct { beans.SQLBaseCol + SessionId string SessionName string } var SessionCol = sessionCol{ SQLBaseCol: beans.DefSQLBaseCol, + SessionId: "session_id", SessionName: "session_name", } - -// WorkflowSessionResult 工作流会话结果 -type WorkflowSessionResult struct { - beans.SQLBaseDO `orm:",inherit"` - SessionId int64 `orm:"session_id" json:"sessionId" description:"所属会话ID"` - FlowId int64 `orm:"flow_id" json:"flowId" description:"工作流ID"` - FlowName string `orm:"flow_name" json:"flowName" description:"工作流名称"` - RequestParams map[string]any `orm:"request_params" json:"requestParams" description:"请求参数"` - ResultParams []map[string]any `orm:"result_params" json:"resultParams" description:"返回结果"` - Status int `orm:"status" json:"status" description:"状态:1-运行中,2-成功,3-失败"` - TotalTokens int `orm:"total_tokens" json:"totalTokens" description:"总token消耗"` - TotalFee float64 `orm:"total_fee" json:"totalFee" description:"总费用"` - ErrorMessage string `orm:"error_message" json:"errorMessage" description:"错误信息"` -} - -type workflowSessionResultCol struct { - beans.SQLBaseCol - SessionId string - FlowId string - FlowName string - RequestParams string - ResultParams string - Status string - TotalTokens string - TotalFee string - ErrorMessage string -} - -var WorkflowSessionResultCol = workflowSessionResultCol{ - SQLBaseCol: beans.DefSQLBaseCol, - SessionId: "session_id", - FlowId: "flow_id", - FlowName: "flow_name", - RequestParams: "request_params", - ResultParams: "result_params", - Status: "status", - TotalTokens: "total_tokens", - TotalFee: "total_fee", - ErrorMessage: "error_message", -} - -// ChatSessionResult 普通会话结果 -type ChatSessionResult struct { - beans.SQLBaseDO `orm:",inherit"` - SessionId int64 `orm:"session_id" json:"sessionId" description:"所属会话ID"` - Question string `orm:"question" json:"question" description:"用户问题"` - Answer string `orm:"answer" json:"answer" description:"模型返回结果"` - TotalTokens int `orm:"total_tokens" json:"totalTokens" description:"总token消耗"` - TotalFee float64 `orm:"total_fee" json:"totalFee" description:"总费用"` - ErrorMessage string `orm:"error_message" json:"errorMessage" description:"错误信息"` -} - -type chatSessionResultCol struct { - beans.SQLBaseCol - SessionId string - Question string - Answer string - TotalTokens string - TotalFee string - ErrorMessage string -} - -var ChatSessionResultCol = chatSessionResultCol{ - SQLBaseCol: beans.DefSQLBaseCol, - SessionId: "session_id", - Question: "question", - Answer: "answer", - TotalTokens: "total_tokens", - TotalFee: "total_fee", - ErrorMessage: "error_message", -} diff --git a/workflow/service/flow/flow_checkpoint_store.go b/workflow/service/flow/flow_checkpoint_store.go new file mode 100644 index 0000000..31349b3 --- /dev/null +++ b/workflow/service/flow/flow_checkpoint_store.go @@ -0,0 +1,66 @@ +package flow + +import ( + "ai-agent/workflow/consts/node" + flowDto "ai-agent/workflow/model/dto/flow" + "ai-agent/workflow/model/entity" + "context" + "encoding/json" + "time" + + flowDao "ai-agent/workflow/dao/flow" + + "github.com/cloudwego/eino/compose" + "github.com/cloudwego/eino/schema" +) + +// 注册 checkpoint 序列化类型 +func init() { + // ========== 1. Eino 断点核心状态(根类型) ========== + schema.RegisterName[*flowDto.NodeExecutionState]("flow.NodeExecutionState") + schema.RegisterName[*flowDto.NodeExecutionInput]("flow.NodeExecutionInput") + schema.RegisterName[*flowDto.FlowExecutionInput]("flow.FlowExecutionInput") + + // ========== 2. 原有第三方类型 ========== + schema.RegisterName[json.Number]("json.Number") + schema.RegisterName[time.Time]("time.Time") + schema.RegisterName[time.Duration]("time.Duration") + + // ========== 3. flowDto 内部嵌套类型 ========== + schema.RegisterName[flowDto.ExecutedNode]("flow.ExecutedNode") + + // ========== 4. entity 核心链路类型(递归自 *entity.FlowNode) ========== + schema.RegisterName[*entity.FlowNode]("entity.FlowNode") + schema.RegisterName[node.NodeType]("node.NodeType") + schema.RegisterName[entity.FlowNodeInputSource]("entity.FlowNodeInputSource") + schema.RegisterName[entity.FlowField]("entity.FlowField") + schema.RegisterName[*entity.SubFlowConfig]("entity.SubFlowConfig") + //schema.RegisterName[node.NodeFormField]("node.NodeFormField") + schema.RegisterName[entity.ModelItem]("node.ModelItem") + + // ========== 5. FlowInfo 相关(流程拓扑结构) ========== + schema.RegisterName[entity.FlowInfo]("entity.FlowInfo") + schema.RegisterName[entity.FlowEdge]("entity.FlowEdge") +} + +// DbCheckPointStore 数据库存储实现 +type DbCheckPointStore struct{} + +func NewDbCheckPointStore() compose.CheckPointStore { + return &DbCheckPointStore{} +} + +func (d *DbCheckPointStore) Get(ctx context.Context, id string) ([]byte, bool, error) { + record, err := flowDao.FlowCheckpointDao.Get(ctx, id) + if err != nil { + return nil, false, err + } + if record == nil || record.Data == "" { + return nil, false, nil + } + return []byte(record.Data), true, nil +} + +func (d *DbCheckPointStore) Set(ctx context.Context, id string, val []byte) error { + return flowDao.FlowCheckpointDao.SaveOrUpdate(ctx, id, string(val)) +} diff --git a/workflow/service/flow/flow_execution_service.go b/workflow/service/flow/flow_execution_service.go index ea5a74a..da2edcc 100644 --- a/workflow/service/flow/flow_execution_service.go +++ b/workflow/service/flow/flow_execution_service.go @@ -1,32 +1,18 @@ package flow import ( - "ai-agent/workflow/consts/flow" - "ai-agent/workflow/consts/node" - fileDao "ai-agent/workflow/dao/file" flowDao "ai-agent/workflow/dao/flow" - nodeDao "ai-agent/workflow/dao/node" - "ai-agent/workflow/model/dto" - fileDto "ai-agent/workflow/model/dto/file" flowDto "ai-agent/workflow/model/dto/flow" - nodeDto "ai-agent/workflow/model/dto/node" "ai-agent/workflow/model/entity" "context" - "errors" "fmt" "sort" "strconv" - "strings" - "sync" - "time" "gitea.redpowerfuture.com/red-future/common/utils" - "github.com/cloudwego/eino/compose" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/util/gconv" - "github.com/google/uuid" - "go.opentelemetry.io/otel/trace" ) var FlowExecutionService = &flowExecutionService{} @@ -58,43 +44,34 @@ func (s *flowExecutionService) List(ctx context.Context, req *flowDto.ListFlowEx return nil, err } - // ===================== 核心修复:只统计【有数据】的执行记录,空的直接跳过 ===================== - - executionNumber := make(map[int64]int) // executionId -> 倒序编号(最新=1) - - // 第一次遍历:只处理【有输出参数】的记录,统计并分配编号 - var validList []*entity.FlowExecution // 只存有效(非空)记录 + executionNumber := make(map[int64]int) + var validList []*entity.FlowExecution for _, execution := range list { if g.IsEmpty(execution.OutputParams) { - continue // 空数据直接过滤,不参与编号、不展示 + continue } validList = append(validList, execution) } - // 给有效记录分配【时间倒序编号】(最新=1) totalValid := len(validList) for idx, execution := range validList { executionNumber[execution.Id] = totalValid - idx } - // 2. 分组映射:日期 -> 流程节点 type flowWrap struct { flowNode flowDto.FlowNode createdAt *gtime.Time } dateMap := make(map[string]*[]flowWrap) - // 遍历【有效数据】构建结构 for _, execution := range validList { createDate := execution.CreatedAt.Format("Y-m-d") flowName := execution.FlowName outputParams := execution.OutputParams - // 编号只算有效数据,不会把空的算进去 num := executionNumber[execution.Id] displayFlowName := fmt.Sprintf("会话-%d(%s)", num, flowName) - // 3. 解析 outputParams var tempItems []flowDto.OutputItem for _, paramMap := range outputParams { for tsKey, value := range paramMap { @@ -102,32 +79,21 @@ func (s *flowExecutionService) List(ctx context.Context, req *flowDto.ListFlowEx continue } tempItems = append(tempItems, flowDto.OutputItem{ - Timestamp: tsKey, - Content: gconv.String(value), + Content: gconv.String(value), }) } } - // ===================== 修复1:如果解析后依然为空,直接跳过,不生成第二层节点 ===================== if len(tempItems) == 0 { continue } - // 时间戳正序 - sort.Slice(tempItems, func(i, j int) bool { - t1, _ := strconv.ParseInt(tempItems[i].Timestamp, 10, 64) - t2, _ := strconv.ParseInt(tempItems[j].Timestamp, 10, 64) - return t1 < t2 - }) - - // 标号:相同类型递增,不同重置 suffixCount := make(map[string]int) for idx := range tempItems { item := &tempItems[idx] val := item.Content suffix := "内容" - ext := "" - ext = GetFileTypeByPath(val) + ext := GetFileTypeByPath(val) if ext == "image" { suffix = "图片" } @@ -148,7 +114,6 @@ func (s *flowExecutionService) List(ctx context.Context, req *flowDto.ListFlowEx item.Label = fmt.Sprintf("%s_%d", suffix, suffixCount[suffix]) } - // 组装节点 flowNode := flowDto.FlowNode{ FlowName: displayFlowName, Id: execution.Id, @@ -165,10 +130,8 @@ func (s *flowExecutionService) List(ctx context.Context, req *flowDto.ListFlowEx }) } - // 6. 构建树 + 排序 var tree []flowDto.DateNode for date, wraps := range dateMap { - // 第二层按创建时间倒序(最新在前) sort.Slice(*wraps, func(i, j int) bool { return (*wraps)[i].createdAt.After((*wraps)[j].createdAt) }) @@ -178,7 +141,6 @@ func (s *flowExecutionService) List(ctx context.Context, req *flowDto.ListFlowEx flowNodes = append(flowNodes, w.flowNode) } - // ===================== 修复2:日期下没有流程,也过滤掉 ===================== if len(flowNodes) == 0 { continue } @@ -189,7 +151,6 @@ func (s *flowExecutionService) List(ctx context.Context, req *flowDto.ListFlowEx }) } - // 第一层日期倒序 sort.Slice(tree, func(i, j int) bool { return tree[i].CreateDate > tree[j].CreateDate }) @@ -201,26 +162,6 @@ func (s *flowExecutionService) List(ctx context.Context, req *flowDto.ListFlowEx }, nil } -// ComposeCallback 提示词回调接口 -func (s *flowExecutionService) ComposeCallback(ctx context.Context, req *flowDto.ComposeCallbackReq) (err error) { - Notify(req.TaskId, req) - return nil -} - -// ModelCallback 模型回调接口 -func (s *flowExecutionService) ModelCallback(ctx context.Context, req *flowDto.ModelCallbackReq) (err error) { - // 唤醒等待的任务 - Notify(req.TaskId, req) - return nil -} - -// VideoCallback 视频拼接回调接口 -func (s *flowExecutionService) VideoCallback(ctx context.Context, req *flowDto.VideoCallbackReq) (err error) { - // 唤醒等待的任务 - Notify(req.TaskId, req) - return nil -} - // HttpNodeCallback http节点回调接口 func (s *flowExecutionService) HttpNodeCallback(ctx context.Context) (err error) { r := g.RequestFromCtx(ctx) @@ -228,511 +169,3 @@ func (s *flowExecutionService) HttpNodeCallback(ctx context.Context) (err error) Notify(taskId, r) return nil } - -// ===================== 核心改造:替换为 sync.Map 存储取消上下文 ===================== -var ( - // cancelMap: traceID -> context.CancelFunc - cancelMap sync.Map -) - -func (s *flowExecutionService) Cancel(ctx context.Context, req *flowDto.CancelReq) (err error) { - getRes, err := flowDao.FlowExecutionDao.Get(ctx, &flowDto.GetFlowExecutionReq{ - SessionId: req.SessionId, - }) - if err != nil { - return err - } - if g.IsEmpty(getRes) { - return fmt.Errorf("会话[%s] 不存在", req.SessionId) - } - // 从 sync.Map 获取取消函数 - cancelVal, exist := cancelMap.Load(getRes.TraceId) - if !exist { - return fmt.Errorf("traceID[%s] 不存在或已执行完成", getRes.TraceId) - } - - // 执行取消 - cancel, ok := cancelVal.(context.CancelFunc) - if !ok { - return fmt.Errorf("traceID[%s] 对应的取消函数类型错误", getRes.TraceId) - } - cancel() - - // 取消后清理(可选:也可以在流程结束时统一清理) - cancelMap.Delete(getRes.TraceId) - - // 同步更新流程执行状态为已取消 - _, err = flowDao.FlowExecutionDao.Update(ctx, &flowDto.UpdateFlowExecutionReq{ - Id: getRes.Id, - Status: flow.FlowExecutionStatusCancel.Code(), - }) - if err != nil { - return fmt.Errorf("更新取消状态失败: %v", err) - } - - return nil -} - -func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.ExecuteReq) (res *flowDto.ExecuteRes, err error) { - // ===================== 核心改造1:创建可取消的上下文 ===================== - execCtx, cancel := context.WithCancel(ctx) - traceId := "" - defer func() { - // 流程结束(成功/失败)时清理 cancelMap - if traceId != "" { - cancelMap.Delete(traceId) - } - cancel() - }() - - //getRes, err := FlowUserService.Get(ctx, &flowDto.GetFlowUserReq{ - // Id: req.FlowId, - //}) - //if err != nil { - // return nil, err - //} - nodeInputParams := ExtractFlowNodeFrom(req.FlowContent) - flowInfo, err := flowDao.FlowExecutionDao.Get(ctx, &flowDto.GetFlowExecutionReq{ - SessionId: req.SessionId, - }) - if err != nil { - return - } - var executionId int64 - var isDialogue bool - var nodeGroupId = uuid.NewString() - if flowInfo == nil { - isDialogue = false - var r = new(flowDto.CreateFlowExecutionReq) - r.FlowUserId = req.FlowId - r.FlowName = req.FlowName - r.NodeGroupId = nodeGroupId - r.TriggerType = flow.FlowExecutionTriggerTypeManual.Code() - r.FlowContent = req.FlowContent - //r.NodeInputParams = nodeInputParams - r.SessionId = req.SessionId - r.Status = flow.FlowExecutionStatusRunning.Code() - span := trace.SpanFromContext(ctx) - if span != nil && span.SpanContext().HasTraceID() { - r.TraceId = span.SpanContext().TraceID().String() - traceId = r.TraceId - cancelMap.Store(traceId, cancel) - } - executionId, err = flowDao.FlowExecutionDao.Insert(ctx, r) - if err != nil { - return - } - } else { - isDialogue = true - executionId = flowInfo.Id - span := trace.SpanFromContext(ctx) - if span != nil && span.SpanContext().HasTraceID() { - traceId = span.SpanContext().TraceID().String() - cancelMap.Store(traceId, cancel) - } - executionReq := flowDto.UpdateFlowExecutionReq{ - Id: executionId, - NodeGroupId: nodeGroupId, - Status: flow.FlowExecutionStatusRunning.Code(), - TraceId: traceId, - } - _, err = flowDao.FlowExecutionDao.Update(ctx, &executionReq) - if err != nil { - return - } - } - - if !g.IsEmpty(req.FileUrl) { - createFileTempReq := make([]*fileDto.CreateFileTempReq, 0, len(req.FileUrl)) - for _, fileUrl := range req.FileUrl { - var createReq = new(fileDto.CreateFileTempReq) - createReq.BusinessId = req.SessionId - createReq.FileUrl = fileUrl - createFileTempReq = append(createFileTempReq, createReq) - } - _, err = fileDao.FileTempDao.BatchInsert(ctx, createFileTempReq) - if err != nil { - return nil, err - } - } - - if isDialogue && !g.IsEmpty(flowInfo) && !g.IsEmpty(req.ResultUrl) { - req.NodeGroupId = nodeGroupId - if strings.HasSuffix(gconv.String(req.ResultUrl), ".inc") { - err = TextModelSingleLambda(ctx, req, flowInfo) - return - } else if strings.HasSuffix(gconv.String(req.ResultUrl), ".png") { - err = ImgModelSingleLambda(ctx, req, flowInfo) - return - } else if strings.HasSuffix(gconv.String(req.ResultUrl), ".html") { - err = TextImgModelSingleLambda(ctx, req, flowInfo) - return - } - return nil, errors.New("文件格式不支持") - } - // ========================================================================= - // ✅【第2步】构建执行图 - // ========================================================================= - var nodeList []entity.FlowNode - var runGraph compose.Runnable[any, any] - nodeList, runGraph, err = BuildGraphFromFlowContent(execCtx, req.FlowContent) - if err != nil { - executionReq := flowDto.UpdateFlowExecutionReq{ - Id: executionId, - Status: flow.FlowExecutionStatusFailed.Code(), - ErrorMessage: err.Error(), - } - _, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq) - if err1 != nil { - return - } - return nil, fmt.Errorf("执行工作流失败: %v", err) - } - // ========================================================================= - // ✅【第3步】构建 ConfigMap - // ========================================================================= - configMap := make(map[string]*entity.FlowNode) - for _, cfg := range nodeInputParams { - configMap[cfg.Id] = cfg - } - for _, i := range nodeList { - configMap[i.Id] = &i - } - // ========================================================================= - // ✅【第4步】构建全局执行入参(现在 schemaMap 是有值的!) - // ========================================================================= - execInput := &flowDto.FlowExecutionInput{ - NodeGroupId: nodeGroupId, - IsDialogue: isDialogue, - ExecutionId: executionId, - ConfigMap: configMap, - SessionId: req.SessionId, - Desc: req.Desc, - SkillName: req.SkillName, - FileUrl: req.FileUrl, - } - // 执行工作流 - _, err = runGraph.Invoke(execCtx, execInput) - if err != nil { - // 检测是否是取消导致的错误 - if errors.Is(execCtx.Err(), context.Canceled) { - executionReq := flowDto.UpdateFlowExecutionReq{ - Id: executionId, - Status: flow.FlowExecutionStatusCancel.Code(), - } - _, _ = flowDao.FlowExecutionDao.Update(ctx, &executionReq) - return nil, fmt.Errorf("工作流已被取消: %v", err) - } - executionReq := flowDto.UpdateFlowExecutionReq{ - Id: executionId, - Status: flow.FlowExecutionStatusFailed.Code(), - ErrorMessage: err.Error(), - } - _, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq) - if err1 != nil { - return - } - return nil, fmt.Errorf("执行工作流失败: %v", err) - } - return -} - -func BuildGraph(ctx context.Context, flowContent *entity.FlowInfo) ([]entity.FlowNode, *compose.Graph[any, any]) { - // 注册自定义合并函数:处理 *flowDto.FlowExecutionInput 类型合并 - // 由于 ConfigMap 是 map 引用类型,所有并行分支修改已经写入共享内存 - // 直接返回第一个实例即可,所有修改都已经可见 - compose.RegisterValuesMergeFunc(func(values []*flowDto.FlowExecutionInput) (*flowDto.FlowExecutionInput, error) { - if len(values) == 0 { - return nil, nil - } - // 返回第一个实例,ConfigMap 是指针,所有修改都已经写入共享数据结构 - return values[0], nil - }) - - graph := compose.NewGraph[any, any]() - - var nodeList []entity.FlowNode - nodeId := uuid.NewString() - originalEndNodes := findEndNodes(flowContent.StartNodeId, flowContent.Edges) - for i := range originalEndNodes { - sprintf := fmt.Sprintf("%v_%d", nodeId, i) - summaryNode := entity.FlowNode{ - Id: sprintf, - NodeCode: node.NodeTypeSystemSum, - Name: node.NodeNameSystemSum, - InputSource: []entity.FlowNodeInputSource{}, // 后续自动聚合所有节点输出 - FormConfig: nil, - ModelConfig: node.ModelItem{}, - } - nodeList = append(nodeList, summaryNode) - flowContent.Nodes = append(flowContent.Nodes, summaryNode) - } - - // 注册所有节点 - nodeMap := make(map[string]entity.FlowNode) - for _, item := range flowContent.Nodes { - nodeMap[item.Id] = item - if item.NodeCode != node.NodeTypeJudge { - registerNodeToGraph(graph, item) - } - } - - // 注册所有边 - if flowContent.StartNodeId != "" { - _ = graph.AddEdge(compose.START, flowContent.StartNodeId) - } - for i, endID := range originalEndNodes { - sprintf := fmt.Sprintf("%v_%d", nodeId, i) - _ = graph.AddEdge(endID, sprintf) - _ = graph.AddEdge(sprintf, compose.END) - } - - // 构建边关系 - upstreamMap := make(map[string][]string) - edgeMap := make(map[string][]entity.FlowEdge) - for _, edge := range flowContent.Edges { - edgeMap[edge.From] = append(edgeMap[edge.From], edge) - upstreamMap[edge.To] = append(upstreamMap[edge.To], edge.From) - } - - // 处理连线 & 分支 - for fromNodeID, edges := range edgeMap { - fromNode := nodeMap[fromNodeID] - - // 判断节点 → 分支处理 - if fromNode.NodeCode == node.NodeTypeJudge { - branchMap := make(map[string]bool) - for _, e := range edges { - branchMap[e.To] = true - } - - judgeLambda := func(ctx context.Context, input any) (string, error) { - execInput, ok := input.(*flowDto.FlowExecutionInput) - if !ok { - return "", fmt.Errorf("入参类型错误") - } - - currentConfig := execInput.ConfigMap[fromNodeID] - if currentConfig == nil { - return "", fmt.Errorf("判断节点%s无配置", fromNodeID) - } - - branchIdNameMap := make(map[string]string) - var branchIDs []string - for nodeID := range branchMap { - branchIDs = append(branchIDs, nodeID) - // 从configMap获取分支节点的名称 - if branchNodeCfg, ok := execInput.ConfigMap[nodeID]; ok { - branchIdNameMap[nodeID] = branchNodeCfg.Name - } else { - branchIdNameMap[nodeID] = "未命名节点" // 兜底 - } - } - - // 把分支ID-名称映射塞进 ModelConfig,带给意图节点 - m := make(map[string]interface{}) - m["branch_ids"] = branchIDs - m["branch_id_name_map"] = branchIdNameMap // 传递ID-名称映射 - currentConfig.Config = m - - // 关键修改:构造 NodeExecutionInput 传入 JudgeLambda - nodeExecInput := &flowDto.NodeExecutionInput{ - Config: currentConfig, // 当前判断节点配置 - Global: execInput, // 全局执行入参 - } - return JudgeLambda(ctx, nodeExecInput) // 传入 NodeExecutionInput 类型 - } - - _ = graph.AddBranch(upstreamMap[fromNodeID][0], compose.NewGraphBranch(judgeLambda, branchMap)) - continue - } - - // 普通节点连线 - for _, e := range edges { - toNode := nodeMap[e.To] - if toNode.NodeCode == node.NodeTypeJudge { - continue - } - _ = graph.AddEdge(e.From, e.To) - } - } - return nodeList, graph -} - -// BuildGraphFromFlowContent 根据前端保存的工作流JSON,自动构建执行图 -func BuildGraphFromFlowContent(ctx context.Context, flowContent *entity.FlowInfo) ([]entity.FlowNode, compose.Runnable[any, any], error) { - nodeList, graph := BuildGraph(ctx, flowContent) - compile, err := graph.Compile(ctx, compose.WithGraphName("auto_build_workflow")) - return nodeList, compile, err -} - -// -------------------------- 节点自动注册器(核心分发) -------------------------- -func registerNodeToGraph(graph *compose.Graph[any, any], flowNode entity.FlowNode) { - // 通用包装:全程入参都是 *FlowExecutionInput - wrapLambda := func(lambda func(ctx context.Context, input any) (any, error)) func(ctx context.Context, input any) (any, error) { - return func(ctx context.Context, input any) (any, error) { - // ✅ 【关键】全程入参类型永远不变 - execInput, ok := input.(*flowDto.FlowExecutionInput) - if !ok { - return nil, fmt.Errorf("入参必须是 *FlowExecutionInput, 实际是 %T", input) - } - - configMap := execInput.ConfigMap - currentConfig := configMap[flowNode.Id] - if currentConfig == nil { - return nil, fmt.Errorf("节点%s无配置", flowNode.Id) - } - - // 获取入参 - 适配切片类型:遍历所有来源节点 - realInput := new(flowDto.NodeExecutionInput) - if len(flowNode.InputSource) > 0 { // 改为判断切片长度 - // 遍历所有指定的来源节点,聚合输出结果 - for _, inputSource := range flowNode.InputSource { // 遍历切片 - if sourceConfig, ok := configMap[inputSource.NodeId]; ok { - currentConfig.OutputResult = append(currentConfig.OutputResult, sourceConfig.OutputResult...) - } - } - } - - // ✅ 封装节点执行入参(配置+表单架构) - realInput = &flowDto.NodeExecutionInput{ - Config: currentConfig, - Global: execInput, // ✅ 把【全部节点】的对象直接塞进来 - } - // ✅ 插入节点执行记录,初始状态为运行中 - startTime := time.Now() - - // 上传OSS(每条独立上传) - ossResult, err := Upload(ctx, &dto.UploadFileBytesReq{ - FileBytes: gconv.Bytes(gconv.String(realInput)), - FileName: fmt.Sprintf("nodeInput:%v.txt", time.Now().UnixMilli()), - }) - if err != nil { - return nil, err - } - - nodeExecutionId, err := nodeDao.NodeExecutionDao.Insert(ctx, &nodeDto.CreateNodeExecutionReq{ - FlowExecutionId: execInput.ExecutionId, - NodeId: flowNode.Id, - NodeName: flowNode.Name, - NodeGroupId: execInput.NodeGroupId, - InputParamsPath: ossResult.FileURL, - Status: node.NodeExecutionStatusRunning.Code(), - }) - if err != nil { - // 记录失败到已执行列表 - execInput.ExecutedNodes = append(execInput.ExecutedNodes, flowDto.ExecutedNode{ - NodeId: flowNode.Id, - Status: node.NodeExecutionStatusFailed.Code(), - }) - return nil, err - } - realInput.NodeExecutionId = nodeExecutionId - // 执行节点 - _, err = lambda(ctx, realInput) - durationMs := time.Since(startTime).Milliseconds() - updateReq := &nodeDto.UpdateNodeExecutionReq{ - Id: nodeExecutionId, - DurationMs: durationMs, - } - if err != nil { - // 执行失败,更新状态 - updateReq.Status = node.NodeExecutionStatusFailed.Code() - updateReq.ErrorMessage = err.Error() - _, _ = nodeDao.NodeExecutionDao.Update(ctx, updateReq) - // 记录失败到已执行列表 - execInput.ExecutedNodes = append(execInput.ExecutedNodes, flowDto.ExecutedNode{ - NodeId: flowNode.Id, - Status: node.NodeExecutionStatusFailed.Code(), - }) - return nil, err - } - // 上传OSS(每条独立上传) - ossResult1, err := Upload(ctx, &dto.UploadFileBytesReq{ - FileBytes: gconv.Bytes(gconv.String(realInput)), - FileName: fmt.Sprintf("nodeInput:%v.txt", time.Now().UnixMilli()), - }) - if err != nil { - return nil, err - } - updateReq.OutputParamsPath = ossResult1.FileURL - // 执行成功,更新状态 - updateReq.Status = node.NodeExecutionStatusSuccess.Code() - _, _ = nodeDao.NodeExecutionDao.Update(ctx, updateReq) - // 记录成功到已执行列表 - execInput.ExecutedNodes = append(execInput.ExecutedNodes, flowDto.ExecutedNode{ - NodeId: flowNode.Id, - Status: node.NodeExecutionStatusSuccess.Code(), - }) - - // ✅ 关键:返回整个 execInput,让下一个节点继续用! - return execInput, nil - } - } - switch flowNode.NodeCode { - case "__start__": - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(StartLambda))) - case node.NodeTypeSystemSum: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(SummaryLambda))) - case node.NodeTypeTextModel: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(TextModelLambda))) - case node.NodeTypeImageModel: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(ImageModelLambda))) - case node.NodeTypeVideoModel: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(VideoModelLambda))) - case node.NodeTypeAudioModel: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(AudioModelLambda))) - case node.NodeTypeBatchModel: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(BatchModelLambda))) - case node.NodeTypeDataConversionModel: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(DataConversionLambda))) - case node.NodeTypeCustomNode: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(CustomLambda))) - case node.NodeTypeForm: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(FormLambda))) - //case node.NodeTypeIntent: - // _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(IntentLambda))) - case node.NodeTypeMerge: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(MergeLambda))) - case node.NodeTypeDataMerge: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(DataMergeLambda)), compose.WithGraphCompileOptions(compose.WithNodeTriggerMode(compose.AllPredecessor))) - case node.NodeTypeSubFlow: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(SubFlowLambda))) - case node.NodeTypeHttp: - _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(HttpLambda))) - } -} - -func findEndNodes(startNodeId string, edges []entity.FlowEdge) []string { - nextMap := make(map[string][]string) - for _, e := range edges { - nextMap[e.From] = append(nextMap[e.From], e.To) - } - - endNodeSet := make(map[string]struct{}) - visited := make(map[string]struct{}) - queue := []string{startNodeId} - - for len(queue) > 0 { - node := queue[0] - queue = queue[1:] - - if _, exist := visited[node]; exist { - continue - } - visited[node] = struct{}{} - - nextList := nextMap[node] - if len(nextList) == 0 { - endNodeSet[node] = struct{}{} - continue - } - queue = append(queue, nextList...) - } - - res := make([]string, 0, len(endNodeSet)) - for k := range endNodeSet { - res = append(res, k) - } - return res -} diff --git a/workflow/service/flow/flow_graph_builder.go b/workflow/service/flow/flow_graph_builder.go new file mode 100644 index 0000000..c0be8fe --- /dev/null +++ b/workflow/service/flow/flow_graph_builder.go @@ -0,0 +1,262 @@ +package flow + +import ( + "ai-agent/gateway" + "ai-agent/workflow/consts/node" + flowDto "ai-agent/workflow/model/dto/flow" + "ai-agent/workflow/model/entity" + "context" + "fmt" + "time" + + "github.com/cloudwego/eino/compose" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +// BuildGraph 根据 FlowInfo 构建完整的 Eino Graph 拓扑 +func BuildGraph(ctx context.Context, flowContent *entity.FlowInfo) ([]entity.FlowNode, *compose.Graph[any, any]) { + // 注册自定义合并函数:处理 *flowDto.FlowExecutionInput 类型合并 + // 由于 ConfigMap 是 map 引用类型,所有并行分支修改已经写入共享内存 + // 直接返回第一个实例即可,所有修改都已经可见 + compose.RegisterValuesMergeFunc(func(values []*flowDto.FlowExecutionInput) (*flowDto.FlowExecutionInput, error) { + if len(values) == 0 { + return nil, nil + } + // 返回第一个实例,ConfigMap 是指针,所有修改都已经写入共享数据结构 + return values[0], nil + }) + + graph := compose.NewGraph[any, any]( + // 本地状态初始化 + compose.WithGenLocalState(func(ctx context.Context) *flowDto.NodeExecutionState { + return &flowDto.NodeExecutionState{} + }), + ) + + // 注册所有节点 + nodeMap := make(map[string]entity.FlowNode) + for _, item := range flowContent.Nodes { + nodeMap[item.Id] = item + //if item.NodeCode != node.NodeTypeJudge { + registerNodeToGraph(graph, item) + //} + } + + // 注册开始节点 + if flowContent.StartNodeId != "" { + _ = graph.AddEdge(compose.START, flowContent.StartNodeId) + } + + var nodeList []entity.FlowNode + originalEndNodes := FindEndNodes(flowContent.StartNodeId, flowContent.Edges) + for _, endID := range originalEndNodes { + // 保存结果节点 ID 必须稳定:ReExecute 续跑重建图时复用同一 ID, + // 否则 checkpoint 里 ConfigMap 存的是上次的旧 ID,续跑时新图按新 ID 查不到配置, + // 报"节点信息为空"。一个末端节点对应一个保存结果节点,用 endID 派生即唯一且稳定。 + summaryNodeId := fmt.Sprintf("%s_%s", node.NodeTypeSystemSum, endID) + summaryNode := entity.FlowNode{ + Id: summaryNodeId, + NodeCode: node.NodeTypeSystemSum, + Name: node.GetNodeTypeName(node.NodeTypeSystemSum), + } + nodeList = append(nodeList, summaryNode) + flowContent.Nodes = append(flowContent.Nodes, summaryNode) + + registerNodeToGraph(graph, summaryNode) + _ = graph.AddEdge(endID, summaryNodeId) + _ = graph.AddEdge(summaryNodeId, compose.END) + } + + // 构建边关系 + upstreamMap := make(map[string][]string) + edgeMap := make(map[string][]entity.FlowEdge) + for _, edge := range flowContent.Edges { + edgeMap[edge.From] = append(edgeMap[edge.From], edge) + upstreamMap[edge.To] = append(upstreamMap[edge.To], edge.From) + } + + // 处理连线 & 分支 + for _, edges := range edgeMap { + //fromNode := nodeMap[fromNodeID] + + // 判断节点 → 分支处理 + //if fromNode.NodeCode == node.NodeTypeJudge { + // branchMap := make(map[string]bool) + // for _, e := range edges { + // branchMap[e.To] = true + // } + // + // judgeLambda := func(ctx context.Context, input any) (string, error) { + // execInput, ok := input.(*flowDto.FlowExecutionInput) + // if !ok { + // return "", fmt.Errorf("入参类型错误") + // } + // + // currentConfig := execInput.ConfigMap[fromNodeID] + // if currentConfig == nil { + // return "", fmt.Errorf("判断节点%s无配置", fromNodeID) + // } + // + // branchIdNameMap := make(map[string]string) + // var branchIDs []string + // for nodeID := range branchMap { + // branchIDs = append(branchIDs, nodeID) + // // 从configMap获取分支节点的名称 + // if branchNodeCfg, ok := execInput.ConfigMap[nodeID]; ok { + // branchIdNameMap[nodeID] = branchNodeCfg.Name + // } else { + // branchIdNameMap[nodeID] = "未命名节点" // 兜底 + // } + // } + // + // // 把分支ID-名称映射塞进 ModelConfig,带给意图节点 + // m := make(map[string]interface{}) + // m["branch_ids"] = branchIDs + // m["branch_id_name_map"] = branchIdNameMap + // currentConfig.Config = m + // + // // 构造 NodeExecutionInput 传入 JudgeLambda + // nodeExecInput := &flowDto.NodeExecutionInput{ + // Config: currentConfig, + // Global: execInput, + // } + // return JudgeLambda(ctx, nodeExecInput) + // } + // + // _ = graph.AddBranch(upstreamMap[fromNodeID][0], compose.NewGraphBranch(judgeLambda, branchMap)) + // continue + //} + + // 普通节点连线 + for _, e := range edges { + //toNode := nodeMap[e.To] + //if toNode.NodeCode == node.NodeTypeJudge { + // continue + //} + _ = graph.AddEdge(e.From, e.To) + } + } + return nodeList, graph +} + +// BuildGraphFromFlowContent 根据前端保存的工作流JSON,自动构建执行图并编译 +func BuildGraphFromFlowContent(ctx context.Context, flowContent *entity.FlowInfo) ([]entity.FlowNode, compose.Runnable[any, any], error) { + nodeList, graph := BuildGraph(ctx, flowContent) + compile, err := graph.Compile(ctx, compose.WithGraphName("auto_build_workflow"), compose.WithCheckPointStore(NewDbCheckPointStore())) + return nodeList, compile, err +} + +// registerNodeToGraph 将单个节点注册到图中(包含通用包装逻辑) +func registerNodeToGraph(graph *compose.Graph[any, any], flowNode entity.FlowNode) { + // 通用包装:全程入参都是 *FlowExecutionInput + wrapLambda := func(lambda func(ctx context.Context, input any) (any, error)) func(ctx context.Context, input any) (any, error) { + return func(ctx context.Context, input any) (any, error) { + startTime := time.Now() + + // 构建节点执行入参(含中断恢复) + execInput, realInput, err := BuildNodeExecutionInput(ctx, input, flowNode) + if err != nil { + return nil, err + } + + flowNodeDesc := flowNode.Desc + if g.IsEmpty(flowNodeDesc) { + flowNodeDesc = flowNode.Name + } + + // 上报节点执行进度(WebSocket场景下推送进度给前端) + if reporter := GetProgressReporter(ctx); reporter != nil { + nodeIndex := len(execInput.ExecutedNodes) + 1 + if IndexOf(execInput.ExecutedNodes, flowNode.Id) != -1 { + nodeIndex = IndexOf(execInput.ExecutedNodes, flowNode.Id) + } + reporter.ReportStart(flowNode.Id, flowNodeDesc, nodeIndex, len(execInput.ConfigMap)) + } + + // 上传入参到OSS + ossResult, err := gateway.Upload(ctx, fmt.Sprintf("nodeInput:%v.txt", time.Now().UnixMilli()), gconv.Bytes(gconv.String(realInput))) + if err != nil { + return nil, HandleFailedNodeExecution(ctx, execInput, 0, flowNode, err, 0) + } + + // 创建节点执行记录 + nodeExecutionId, err := CreateNodeExecutionRecord(ctx, execInput, flowNode, ossResult) + if err != nil { + return nil, HandleFailedNodeExecution(ctx, execInput, 0, flowNode, err, 0) + } + realInput.NodeExecutionId = nodeExecutionId + + // 执行节点 + _, err = lambda(ctx, realInput) + durationMs := time.Since(startTime).Milliseconds() + + if err != nil { + // 执行失败处理 + return nil, HandleFailedNodeExecution(ctx, execInput, nodeExecutionId, flowNode, err, durationMs) + } + + // 执行成功处理 + if err = HandleSuccessfulNodeExecution(ctx, execInput, realInput, nodeExecutionId, flowNode, durationMs); err != nil { + return nil, HandleFailedNodeExecution(ctx, execInput, nodeExecutionId, flowNode, err, durationMs) + } + + // 上报节点执行进度(WebSocket场景下推送进度给前端) + if reporter := GetProgressReporter(ctx); reporter != nil { + nodeIndex := len(execInput.ExecutedNodes) + if IndexOf(execInput.ExecutedNodes, flowNode.Id) != -1 { + nodeIndex = IndexOf(execInput.ExecutedNodes, flowNode.Id) + } + reporter.ReportComplete(flowNode.Id, flowNodeDesc, nodeIndex, len(execInput.ConfigMap)) + } + + // 返回整个 execInput,让下一个节点继续用 + return execInput, nil + } + } + + switch flowNode.NodeCode { + case node.NodeTypeStart: + _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(StartLambda))) + case node.NodeTypeSystemSum: + _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(SummaryLambda))) + case node.NodeTypeModel: + _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(ModelLambda))) + case node.NodeTypeForm: + _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(FormLambda))) + case node.NodeTypeDataMerge: + _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(DataMergeLambda)), compose.WithGraphCompileOptions(compose.WithNodeTriggerMode(compose.AllPredecessor))) + case node.NodeTypeSubFlow: + _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(SubFlowLambda))) + case node.NodeTypeHttp: + _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(HttpLambda))) + case node.NodeTypeScriptTranscribe: + _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(ScriptTranscribeLambda))) + //case node.NodeTypeTextModel: + // _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(TextModelLambda))) + //case node.NodeTypeImageModel: + // _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(ImageModelLambda))) + //case node.NodeTypeVideoModel: + // _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(VideoModelLambda))) + //case node.NodeTypeAudioModel: + // _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(AudioModelLambda))) + //case node.NodeTypeBatchModel: + // _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(BatchModelLambda))) + //case node.NodeTypeDataConversionModel: + // _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(DataConversionLambda))) + //case node.NodeTypeCustomNode: + // _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(CustomLambda))) + //case node.NodeTypeMerge: + // _ = graph.AddLambdaNode(flowNode.Id, compose.InvokableLambda(wrapLambda(MergeLambda))) + } +} + +// IndexOf 返回元素第一次出现的下标,不存在返回 -1 +func IndexOf(slice []flowDto.ExecutedNode, target string) int { + for i, v := range slice { + if v.NodeId == target { + return i + 1 + } + } + return -1 +} diff --git a/workflow/service/flow/flow_graph_util.go b/workflow/service/flow/flow_graph_util.go new file mode 100644 index 0000000..01bf692 --- /dev/null +++ b/workflow/service/flow/flow_graph_util.go @@ -0,0 +1,160 @@ +package flow + +import ( + "ai-agent/gateway" + "ai-agent/workflow/consts/node" + nodeDao "ai-agent/workflow/dao/node" + flowDto "ai-agent/workflow/model/dto/flow" + nodeDto "ai-agent/workflow/model/dto/node" + "ai-agent/workflow/model/entity" + "context" + "fmt" + "time" + + "github.com/cloudwego/eino/compose" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +// BuildNodeExecutionInput 构建节点执行入参,包含中断恢复逻辑 +func BuildNodeExecutionInput(ctx context.Context, input any, flowNode entity.FlowNode) (*flowDto.FlowExecutionInput, *flowDto.NodeExecutionInput, error) { + execInput := new(flowDto.FlowExecutionInput) + + wasInterrupted, _, _ := compose.GetInterruptState[any](ctx) + if wasInterrupted { + if err := compose.ProcessState(ctx, func(_ context.Context, s *flowDto.NodeExecutionState) error { + execInput = s.SavedFlowInput + return nil + }); err != nil { + return nil, nil, fmt.Errorf("节点:%v 进程状态读取失败: %v", flowNode.Name, err) + } + // 兼容旧 checkpoint(无 SavedFlowInput 时,降级使用 input 参数) + if execInput == nil { + var ok bool + execInput, ok = input.(*flowDto.FlowExecutionInput) + if !ok { + return nil, nil, fmt.Errorf("节点:%v 进程状态为空,节点入参类型不匹配", flowNode.Name) + } + if g.IsEmpty(execInput) { + return nil, nil, fmt.Errorf("节点:%v 进程状态为空,节点入参参数为空", flowNode.Name) + } + } + } else { + var ok bool + execInput, ok = input.(*flowDto.FlowExecutionInput) + if !ok { + return nil, nil, fmt.Errorf("节点:%v 入参类型不匹配", flowNode.Name) + } + if g.IsEmpty(execInput) { + return nil, nil, fmt.Errorf("节点:%v 入参参数为空", flowNode.Name) + } + } + + configMap := execInput.ConfigMap + currentConfig := configMap[flowNode.Id] + if currentConfig == nil { + return nil, nil, fmt.Errorf("节点:%v 节点信息为空", flowNode.Name) + } + + // 聚合输入来源 + //if len(flowNode.InputSource) > 0 { + // for _, inputSource := range currentConfig.InputSource { + // if sourceConfig, ok := configMap[inputSource.NodeId]; ok { + // currentConfig.OutputResult = append(currentConfig.OutputResult, sourceConfig.OutputResult...) + // } + // } + //} + + // 构建节点执行入参 + realInput := &flowDto.NodeExecutionInput{ + Config: currentConfig, + Global: execInput, + } + + return execInput, realInput, nil +} + +// HandleSuccessfulNodeExecution 处理节点执行成功的后续操作 +func HandleSuccessfulNodeExecution(ctx context.Context, execInput *flowDto.FlowExecutionInput, realInput *flowDto.NodeExecutionInput, nodeExecutionId int64, flowNode entity.FlowNode, durationMs int64) error { + // 上传输出到OSS + ossResult, err := gateway.Upload(ctx, fmt.Sprintf("nodeInput:%v.txt", time.Now().UnixMilli()), gconv.Bytes(gconv.String(realInput))) + if err != nil { + return fmt.Errorf("节点:%v 上传OSS失败: %v", realInput.Config.Name, err) + } + + // 更新执行记录为成功 + if err := UpdateNodeExecutionRecord(ctx, nodeExecutionId, durationMs, node.NodeExecutionStatusSuccess.Code(), ossResult, ""); err != nil { + return fmt.Errorf("节点:%v 更新成功状态错误: %v", flowNode.Name, err) + } + + // 记录成功到已执行列表 + RecordExecutionResult(execInput, flowNode.Id, node.NodeExecutionStatusSuccess.Code()) + return nil +} + +// HandleFailedNodeExecution 处理节点执行失败的后续操作 +func HandleFailedNodeExecution(ctx context.Context, execInput *flowDto.FlowExecutionInput, nodeExecutionId int64, flowNode entity.FlowNode, err error, durationMs int64) error { + // 保存状态用于续跑 + if stateErr := compose.ProcessState(ctx, func(_ context.Context, s *flowDto.NodeExecutionState) error { + s.CompletedNodes = append(s.CompletedNodes, flowNode.Name) + s.SavedFlowInput = execInput + s.ExecutionCount++ + return nil + }); stateErr != nil { + fmt.Printf("节点:%v 进程状态保存失败: %v", flowNode.Name, stateErr) + } + + if !g.IsEmpty(nodeExecutionId) { + // 更新执行记录为失败 + if updateErr := UpdateNodeExecutionRecord(ctx, nodeExecutionId, durationMs, node.NodeExecutionStatusFailed.Code(), "", err.Error()); updateErr != nil { + fmt.Printf("节点:%v 更新失败状态错误: %v", flowNode.Name, updateErr) + } + } + + // 记录失败到已执行列表 + //RecordExecutionResult(execInput, flowNode.Id, node.NodeExecutionStatusFailed.Code()) + + // 触发中断 + return compose.Interrupt(ctx, map[string]string{ + "node": flowNode.Name, + "error": err.Error(), + }) +} + +// RecordExecutionResult 将节点执行结果写入 Global.ExecutedNodes +func RecordExecutionResult(execInput *flowDto.FlowExecutionInput, nodeId string, status node.NodeExecutionStatus) { + execInput.ExecutedNodes = append(execInput.ExecutedNodes, flowDto.ExecutedNode{ + NodeId: nodeId, + Status: status, + }) +} + +// UpdateNodeExecutionRecord 更新节点执行记录 +func UpdateNodeExecutionRecord(ctx context.Context, nodeExecutionId int64, durationMs int64, status node.NodeExecutionStatus, outputParamsPath string, errMsg string) error { + if _, err := nodeDao.NodeExecutionDao.Update(ctx, &nodeDto.UpdateNodeExecutionReq{ + Id: nodeExecutionId, + DurationMs: durationMs, + Status: status, + OutputParamsPath: outputParamsPath, + ErrorMessage: errMsg, + }); err != nil { + return err + } + return nil +} + +// CreateNodeExecutionRecord 创建节点执行记录,返回记录ID +func CreateNodeExecutionRecord(ctx context.Context, execInput *flowDto.FlowExecutionInput, flowNode entity.FlowNode, inputOssUrl string) (int64, error) { + id, err := nodeDao.NodeExecutionDao.Insert(ctx, &nodeDto.CreateNodeExecutionReq{ + FlowExecutionId: execInput.ExecutionId, + NodeId: flowNode.Id, + NodeName: flowNode.Name, + NodeGroupId: execInput.NodeGroupId, + InputParamsPath: inputOssUrl, + Status: node.NodeExecutionStatusRunning.Code(), + }) + if err != nil { + return 0, fmt.Errorf("节点:%v 创建节点执行记录失败: %v", flowNode.Name, err) + } + return id, nil +} diff --git a/workflow/service/flow/flow_helper.go b/workflow/service/flow/flow_helper.go new file mode 100644 index 0000000..09ac4bf --- /dev/null +++ b/workflow/service/flow/flow_helper.go @@ -0,0 +1,217 @@ +package flow + +import ( + "ai-agent/workflow/model/entity" + "net/url" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +// FindEndNodes 从指定起始节点开始,遍历图找到所有末端节点(没有出边的节点) +func FindEndNodes(startNodeId string, edges []entity.FlowEdge) []string { + nextMap := make(map[string][]string) + for _, e := range edges { + nextMap[e.From] = append(nextMap[e.From], e.To) + } + + endNodeSet := make(map[string]struct{}) + visited := make(map[string]struct{}) + queue := []string{startNodeId} + + for len(queue) > 0 { + node := queue[0] + queue = queue[1:] + + if _, exist := visited[node]; exist { + continue + } + visited[node] = struct{}{} + + nextList := nextMap[node] + if len(nextList) == 0 { + endNodeSet[node] = struct{}{} + continue + } + queue = append(queue, nextList...) + } + + res := make([]string, 0, len(endNodeSet)) + for k := range endNodeSet { + res = append(res, k) + } + return res +} + +// ExtractFlowNodeFrom 从 FlowInfo 中提取节点列表,并自动补齐 DataMerge 节点的 InputSource +func ExtractFlowNodeFrom(flowContent *entity.FlowInfo) []*entity.FlowNode { + // 构建每个节点的上游节点映射 + upstreamMap := make(map[string][]string) + for _, edge := range flowContent.Edges { + upstreamMap[edge.To] = append(upstreamMap[edge.To], edge.From) + } + + // 同时更新 flowContent.Nodes 中的 DataMerge 节点 + //for i := range flowContent.Nodes { + // n := &flowContent.Nodes[i] + // // 对于 DataMerge 节点,自动根据边关系填充 InputSource + // if n.NodeCode == node.NodeTypeDataMerge { + // n.InputSource = nil + // for _, fromId := range upstreamMap[n.Id] { + // n.InputSource = append(n.InputSource, entity.FlowNodeInputSource{ + // NodeId: fromId, + // }) + // } + // } + //} + + var flowNodes []*entity.FlowNode + for _, item := range flowContent.Nodes { + flowNodes = append(flowNodes, &item) + } + return flowNodes +} + +// GetFileTypeByPath 根据文件路径/URL的后缀名判断文件类型 +func GetFileTypeByPath(filePath string) string { + if filePath == "" { + return "" + } + + // 解析 URL,获取真实路径(兼容 http 链接) + u, err := url.Parse(filePath) + if err == nil { + filePath = u.Path + } + + // 获取后缀(小写) + ext := filepath.Ext(filePath) + ext = strings.ToLower(ext) + + // 判断类型 + switch ext { + case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp": + return "image" + case ".mp4", ".mov", ".avi", ".flv", ".wmv", ".mkv": + return "video" + case ".mp3", ".wav", ".m4a", ".flac", ".aac", ".ogg": + return "audio" + case ".txt", ".md", ".log", ".json", ".xml", ".inc": + return "text" + case ".html": + return "html" + case ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx": + return "document" + default: + return "" + } +} + +// GetUrlSuffix 获取URL文件后缀 +// rawUrl: 原始链接 +// withDot: true 返回 .mp4 false 返回 mp4 +func GetUrlSuffix(rawUrl string, withDot bool) string { + // 解析URL,剥离查询参数 + u, err := url.Parse(rawUrl) + if err != nil { + return "" + } + + // 提取路径部分 + filePath := u.Path + // 获取文件名 + fileName := path.Base(filePath) + if fileName == "" || !strings.Contains(fileName, ".") { + return "" + } + + // 截取后缀 + suffix := path.Ext(fileName) + if !withDot { + suffix = strings.TrimPrefix(suffix, ".") + } + return suffix +} + +// ExtractImageCount 修复:支持单引号/双引号 + 换行 + 空格 +func ExtractImageCount(content string) int { + // 🔥 关键:支持 class='image-count' (单引号) + re := regexp.MustCompile(`
]*>.*?(\d+).*?
`) + match := re.FindStringSubmatch(content) + if len(match) >= 2 { + num, err := strconv.Atoi(match[1]) + if err == nil { + return num + } + } + return 0 +} + +func ImageTagRegex(html string) string { + // 🔥 修复:支持单引号、双引号、空格、换行,100% 删除+ imageTagRegex := regexp.MustCompile(`
]*>[\s\S]*?
`) + return imageTagRegex.ReplaceAllString(html, "") +} + +// StripHtmlTags 去掉所有HTML标签,保留换行和文本结构,并删除配图标记行 +func StripHtmlTags(html string) string { + // 1. 替换块级标签为换行,保证排版 + blockTags := regexp.MustCompile(`?(div|p|h1|h2|h3|h4|h5|h6|li|ul|ol|br|tr|td|th)[^>]*>`) + text := blockTags.ReplaceAllString(html, "\n") + + // 2. 去掉所有剩余的 HTML 标签 + allTags := regexp.MustCompile(`<[^>]+>`) + text = allTags.ReplaceAllString(text, "") + + // 4. 清理多余空行(多个换行只保留一个) + text = regexp.MustCompile(`\n\s*\n`).ReplaceAllString(text, "\n") + + // 5. 只去掉首尾空白,中间换行保留 + text = strings.TrimSpace(text) + + return text +} + +// SplitMultiContents 拆分模型返回的多条文案(基于HTML标签分隔) +func SplitMultiContents(htmlContent string) []string { + var contents []string + // 正则匹配,6、列表使用
需要配图:N 张
N 是这条文案需要的图片数量,只能是数字,不能是其他文字,11、只输出 HTML 结构,不输出任何额外文字" - - mapTaskResult, err := GetModelResult(ctx, nodeInput.Global.SessionId, nodeInput, skillName, form, userForm) - if err != nil { - return nil, err - } - if g.IsEmpty(mapTaskResult) { - return nil, fmt.Errorf("生成内容为空") - } - - outputRes := make([]node.NodeFormField, 0) - for _, item := range mapTaskResult { - for k, v := range item { - // 拆分多条文案 - contentList := SplitMultiContents(gconv.String(v)) - for i, contentItem := range contentList { - if nodeInput.Config.IsSaveFile { - // 1. 构建html文本 - plainText := BuildText(contentItem) - // 2. 上传纯文本到 OSS - textFileName := fmt.Sprintf("ai_text_%d_%d.inc", time.Now().UnixMilli(), i) - var textUrl *dto.UploadFileBytesRes - textUrl, err = Upload(ctx, &dto.UploadFileBytesReq{ - FileBytes: []byte(plainText), - FileName: textFileName, - }) - if err != nil { - return nil, err - } - // 3. 把纯文本地址存入输出 - outputRes = append(outputRes, node.NodeFormField{ - Field: fmt.Sprintf("text_url:%v:%d", k, i), - Value: textUrl.FileURL, - Label: fmt.Sprintf("text_url:%v:%d", k, i), - Type: "string", - Expand: ExtractImageCount(contentItem), - }) - } - outputRes = append(outputRes, node.NodeFormField{ - Field: fmt.Sprintf("text_content:%v:%d", k, i), - Value: contentItem, - Label: fmt.Sprintf("文案内容%v:%d", k, i), - Type: "string", - Expand: ExtractImageCount(gconv.String(v)), - }) - } - } - } - return outputRes, nil -} - -func ImgNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput, skillName string, form []map[string]any, userForm []map[string]any) ([]node.NodeFormField, error) { - mapTaskResult, err := GetModelResult(ctx, nodeInput.Global.SessionId, nodeInput, skillName, form, userForm) - if err != nil { - return nil, err - } - if g.IsEmpty(mapTaskResult) { - return nil, fmt.Errorf("生成内容为空") - } - outputRes := make([]node.NodeFormField, 0) - for i, item := range mapTaskResult { - for k, v := range item { - if nodeInput.Config.IsSaveFile { - outputRes = append(outputRes, node.NodeFormField{ - Field: fmt.Sprintf("img_oss_url:%v:%d", k, i), - Value: v, - Label: fmt.Sprintf("img_oss_url%v:%d", k, i), - Type: "string", - }) - } - outputRes = append(outputRes, node.NodeFormField{ - Field: fmt.Sprintf("img_url:%v:%d", k, i), - Value: v, - Label: fmt.Sprintf("图片内容%v:%d", k, i), - Type: "string", - }) - } - } - - //var resultContent []string - //for _, item := range mapTaskResult { - // for _, i := range gconv.Strings(item[modelInfo.Model.ResponseBody]) { - // resultContent = append(resultContent, i) - // } - //} - //var images []string - //for _, item := range resultContent { - // mapItem := gconv.Map(item) - // for _, value := range mapItem { - // values, ok := value.(string) - // if !ok { - // return nil, fmt.Errorf("图片地址类型错误") - // } - // // 下载官方临时图片 - // var imgBytes []byte - // imgBytes, err = GetFileBytesFromURL(ctx, values) - // if err != nil { - // return nil, fmt.Errorf("下载图片失败: %w", err) - // } - // // 构造文件名 - // fileName := fmt.Sprintf("ai_image_%d.png", time.Now().UnixMilli()) - // // 上传到你的OSS(你项目已有的Upload方法) - // var upResp *dto.UploadFileBytesRes - // upResp, err = Upload(ctx, &dto.UploadFileBytesReq{ - // FileName: fileName, - // FileBytes: imgBytes, - // }) - // if err != nil { - // return nil, fmt.Errorf("上传OSS失败: %w", err) - // } - // images = append(images, upResp.FileURL) - // } - //} - // - //var url string - //url, err = utils.GetFileAddressPrefix(ctx) - //if err != nil { - // return nil, err - //} - //outputRes := make([]node.NodeFormField, 0) - // - //for i, item := range images { - // // 额外存储关联关系 - // outputRes = append(outputRes, node.NodeFormField{ - // Field: fmt.Sprintf("img_url:%d", i), - // Value: fmt.Sprintf("%s%s", url, item), - // Label: fmt.Sprintf("图片路径:%d", i), - // Type: "string", - // }) - //} - - return outputRes, nil -} - -func AudioOptimizeNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput, skillName string, form []map[string]any, userForm []map[string]any) ([]node.NodeFormField, error) { - mapTaskResult, err := GetModelResult(ctx, "", nodeInput, skillName, form, userForm) - if err != nil { - return nil, err - } - if g.IsEmpty(mapTaskResult) { - return nil, fmt.Errorf("生成内容为空") - } - outputRes := make([]node.NodeFormField, 0) - for i, item := range mapTaskResult { - for k, v := range item { - if nodeInput.Config.IsSaveFile { - outputRes = append(outputRes, node.NodeFormField{ - Field: fmt.Sprintf("audio_oss_url:%v:%d", k, i), - Value: v, - Label: fmt.Sprintf("audio_oss_url:%v:%d", k, i), - Type: "string", - }) - } - if k == "sentences" { - a := new([]flowDto.Sentence) - err = gconv.Structs(v, a) - v, err = BuildSubtitles(a) - if err != nil { - return nil, err - } - } - outputRes = append(outputRes, node.NodeFormField{ - Field: fmt.Sprintf("audio_url:%v:%d", k, i), - Value: v, - Label: fmt.Sprintf("音频内容:%v:%d", k, i), - Type: "string", - }) - } - } - - return outputRes, nil -} - -func splitTextByPunct(raw string) []string { - // 按标点切分+拼接标点 - slice := regexp.MustCompile(`([,。;!?])`).Split(raw, -1) - var res []string - var builder strings.Builder - for idx, s := range slice { - if s == "" { - continue - } - builder.WriteString(s) - // 偶数位是分隔标点(split后规律:文本、标点、文本、标点...) - if idx%2 == 1 { - res = append(res, builder.String()) - builder.Reset() - } - } - if builder.Len() > 0 { - res = append(res, builder.String()) - } - return res -} - -// BuildSubtitles 核心工具:单个sentence生成多条subtitle -func BuildSubtitles(sents *[]flowDto.Sentence) ([]flowDto.Subtitle, error) { - var subtitles []flowDto.Subtitle - for _, sent := range *sents { - segList := splitTextByPunct(sent.Text) - if len(segList) == 0 { - return nil, nil - } - - var subs []flowDto.Subtitle - wordIdx := 0 - allWords := sent.Words - - for _, seg := range segList { - var collectWords []flowDto.Word - currentText := "" - // 循环取 word,直到拼接内容 包含/匹配 seg - for { - if wordIdx >= len(allWords) { - break - } - word := allWords[wordIdx] - currentText += word.Word - collectWords = append(collectWords, word) - wordIdx++ - - // 只要包含分段文本,就认为匹配(无视末尾标点差异) - if strings.Contains(currentText, seg) { - break - } - } - if len(collectWords) == 0 { - continue - } - // 生成字幕 - sub := flowDto.Subtitle{ - Start: collectWords[0].StartTime, - End: collectWords[len(collectWords)-1].EndTime, - Text: seg, - } - subs = append(subs, sub) - } - subtitles = append(subtitles, subs...) - } - - return subtitles, nil -} - -func VideoOptimizeNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput, skillName string, form []map[string]any, userForm []map[string]any) ([]node.NodeFormField, error) { - mapTaskResult, err := GetModelResult(ctx, nodeInput.Global.SessionId, nodeInput, skillName, form, userForm) - if err != nil { - return nil, err - } - if g.IsEmpty(mapTaskResult) { - return nil, fmt.Errorf("生成内容为空") - } - outputRes := make([]node.NodeFormField, 0) - for i, item := range mapTaskResult { - for k, v := range item { - outputRes = append(outputRes, node.NodeFormField{ - Field: fmt.Sprintf("video_url:%v:%d", k, i), - Value: v, - Label: fmt.Sprintf("video_url:%v:%d", k, i), - Type: "string", - }) - } - } - return outputRes, nil -} - -func DataConversionNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput, skillName string, form []map[string]any, userForm []map[string]any) ([]node.NodeFormField, error) { - jsonStr := `` - jsonVal := "输出字段规范:" - for _, field := range nodeInput.Config.OutputConfig { - jsonStr, _ = sjson.Set(jsonStr, field.Field, "") - jsonVal += fmt.Sprintf("%s:%s;", field.Field, field.Value) - } - jsonVal += fmt.Sprintf("输出模板结构,仅修改每个字段对应数值:%s", jsonStr) - nodeInput.Config.PromptContent = fmt.Sprintf("%s;%s", nodeInput.Config.PromptContent, jsonVal) - - mapTaskResult, err := GetModelResult(ctx, "", nodeInput, skillName, form, userForm) - if err != nil { - return nil, err - } - if g.IsEmpty(mapTaskResult) { - return nil, fmt.Errorf("生成内容为空") - } - outputRes := make([]node.NodeFormField, 0) - for i, item := range mapTaskResult { - for k, v := range item { - outputRes = append(outputRes, node.NodeFormField{ - Field: fmt.Sprintf("data_conversion:%v:%d", k, i), - Value: v, - Label: fmt.Sprintf("data_conversion:%v:%d", k, i), - Type: "string", - }) - } - } - return outputRes, nil -} - -func HttpNode(ctx context.Context, nodeInput *flowDto.NodeExecutionInput) ([]node.NodeFormField, error) { - var method, url, responseType, callbackUrl string - var headers map[string]string - var body map[string]any - var responseMapping map[string]any - for _, item := range nodeInput.Config.FormConfig { - switch item.Field { - case "method": - method = gconv.String(item.Value) - case "url": - url = gconv.String(item.Value) - case "headers": - headers = gconv.MapStrStr(item.Value) - case "body": - body = gconv.Map(item.Value) - case "response": - responseMapping = gconv.Map(item.Value) - case "responseType": - responseType = gconv.String(item.Value) - case "callbackUrl": - callbackUrl = gconv.String(item.Value) - } - } - - if method == "" { - return nil, fmt.Errorf("method为空") - } - if url == "" { - return nil, fmt.Errorf("url为空") - } - - if headers == nil { - headers = make(map[string]string) - if r := g.RequestFromCtx(ctx); r != nil { - for k, v := range r.Request.Header { - if len(v) > 0 { - headers[k] = v[0] - } - } - } - } - - // 构建请求参数 - newBody := BuildNestedJson(body, nodeInput.Global.ConfigMap) - // 1. 自己生成唯一 taskId(不用前端给) - taskId := "my_task_" + uuid.New().String() // 自己生成唯一ID - if responseType == "callback" { - newBody[callbackUrl] = utils.GetCallbackURL(ctx, "/httpNodeCallback?task_id="+taskId) - } - // ====================== 核心改动 ====================== - // 1. 定义一个空map接收原始HTTP返回结果 - var rawHttpResult map[string]any - // 2. 发送请求(不变) - var err error - if method == "GET" { - err = commonHttp.Get(ctx, url, headers, &rawHttpResult, newBody) - } else if method == "POST" { - err = commonHttp.Post(ctx, url, headers, &rawHttpResult, newBody) - } else if method == "PUT" { - err = commonHttp.Put(ctx, url, headers, &rawHttpResult, newBody) - } else if method == "DELETE" { - err = commonHttp.Delete(ctx, url, headers, &rawHttpResult, newBody) - } else { - return nil, fmt.Errorf("method 不支持") - } - if err != nil { - return nil, err - } - - finalResult := make(map[string]any) - if responseType == "sync" { - httpResultJson := gconv.String(rawHttpResult) - for key, jsonPath := range responseMapping { - path := gconv.String(jsonPath) - if !g.IsEmpty(gjson.Get(httpResultJson, path).Value()) { - finalResult[key] = gjson.Get(httpResultJson, path).Value() - } - } - } - if responseType == "callback" { - var waitResult any - waitResult, err = Wait(ctx, taskId) - if err != nil { - return nil, err - } - request, ok := waitResult.(*ghttp.Request) - if !ok { - return nil, fmt.Errorf("入参类型错误") - } - - bodyStr := request.GetBodyString() - for key, jsonPath := range responseMapping { - path := gconv.String(jsonPath) - val := gjson.Get(bodyStr, path) - // 如果是数组,直接返回整个数组 - if val.IsArray() { - finalResult[key] = val.Value() - } else { - // 普通值,非空才赋值 - if !g.IsEmpty(val.Value()) { - finalResult[key] = val.Value() - } - } - } - } - if responseType == "pull" { - - } - - outputRes := make([]node.NodeFormField, 0) - for i, item := range finalResult { - if nodeInput.Config.IsSaveFile { - outputRes = append(outputRes, node.NodeFormField{ - Field: fmt.Sprintf("http_file_url:%v", i), - Value: item, - Label: fmt.Sprintf("http_file_url:%v", i), - Type: "string", - }) - } - outputRes = append(outputRes, node.NodeFormField{ - Field: fmt.Sprintf("%v", i), - Value: item, - Label: fmt.Sprintf("%v", i), - Type: "string", - }) - } - - return outputRes, nil -} - -func BuildParam(nodeInput *flowDto.NodeExecutionInput) (skillName string, resultFrom []map[string]any, resultUserFrom []map[string]any) { - inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config) - var outputResult []node.NodeFormField - outputResult = append(outputResult, inputMap...) - - resultUserFrom = []map[string]any{} - for _, field := range outputMap { - if !strings.Contains(field.Field, "text_url") && !strings.Contains(field.Field, "img_url") { - if strings.Contains(field.Field, "text_content") { - field.Value = StripHtmlTags(gconv.String(field.Value)) - } - resultUserFrom = append(resultUserFrom, map[string]any{ - field.Label: field.Value, - }) - } - } - for _, valueAny := range modelMap { - if field, ok := valueAny.(node.NodeFormField); ok { - outputResult = append(outputResult, field) - } - } - if !nodeInput.Global.IsDialogue { - for _, item := range outputResult { - resultUserFrom = append(resultUserFrom, map[string]any{ - item.Label: item.Value, - }) - } - } - if !g.IsEmpty(nodeInput.Global.Desc) { - resultUserFrom = append(resultUserFrom, map[string]any{ - "desc": nodeInput.Global.Desc, - }) - } - - resultFrom = []map[string]any{} - for _, item := range nodeInput.Config.ModelConfig.ModelForm { - if g.IsEmpty(item.Value) { - continue - } - resultFrom = append(resultFrom, map[string]any{ - item.Label: item.Value, - }) - } - skillName = nodeInput.Config.SkillName - if g.IsEmpty(nodeInput.Config.SkillName) { - skillName = nodeInput.Global.SkillName - } - - return skillName, resultFrom, resultUserFrom -} - -func GetNodeContextContent(execInput *flowDto.FlowExecutionInput, nodeEntity *entity.FlowNode) ([]node.NodeFormField, []node.NodeFormField, map[string]any) { - var input []node.NodeFormField - var output []node.NodeFormField - model := make(map[string]any) - if len(nodeEntity.InputSource) > 0 { - for _, source := range nodeEntity.InputSource { - refNode, ok := execInput.ConfigMap[source.NodeId] - if !ok { - continue - } - if len(source.Field) > 0 { - // 取指定字段 - for _, f := range source.Field { - for _, v := range refNode.FormConfig { - if strings.Contains(v.Label, f) { - input = append(input, v) - } - } - for _, v := range refNode.ModelConfig.ModelForm { - if g.IsEmpty(v.Value) { - continue - } - if strings.Contains(v.Label, f) { - model[f] = v - } - } - for _, v := range refNode.OutputResult { - if strings.Contains(v.Label, f) { - output = append(output, v) - } - } - } - } - } - } - return input, output, model -} diff --git a/workflow/service/flow/lambda_node_util.go b/workflow/service/flow/lambda_node_util.go index 923dc08..3686ba7 100644 --- a/workflow/service/flow/lambda_node_util.go +++ b/workflow/service/flow/lambda_node_util.go @@ -1,31 +1,19 @@ package flow import ( + "ai-agent/gateway" "ai-agent/workflow/consts/node" - nodeDao "ai-agent/workflow/dao/node" - "ai-agent/workflow/model/dto" flowDto "ai-agent/workflow/model/dto/flow" - nodeDto "ai-agent/workflow/model/dto/node" - "ai-agent/workflow/model/entity" - "bytes" "context" "fmt" - "io" - "mime/multipart" - "net/http" - "net/url" - "path/filepath" - "regexp" - "strconv" - "strings" "sync" commonHttp "gitea.redpowerfuture.com/red-future/common/http" "gitea.redpowerfuture.com/red-future/common/utils" - "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/util/gconv" - "github.com/tidwall/sjson" + "github.com/google/uuid" ) // 全局等待任务回调的工具 @@ -69,804 +57,155 @@ func Notify(taskId string, result any) { delete(asyncTasks, taskId) } -func GetIsChatModel(ctx context.Context) (res *flowDto.GetIsChatModelRes, err error) { - headers := make(map[string]string) - if r := g.RequestFromCtx(ctx); r != nil { - for k, v := range r.Request.Header { - if len(v) > 0 { - headers[k] = v[0] - } - } +// ModelCallResultLambda 调用模型并返回输出内容列表,同时回传本次调用的 token/费用(*gateway.ModelCallRes), +// 供调用方(ModelLambda)累计写入节点执行记录 token_info,最后由汇总节点聚合到 exec_workflow。 +func ModelCallResultLambda(ctx context.Context, modelId int64, sessionId string, modelRequestParams map[string]any) ([]map[string]any, *gateway.ModelCallRes, error) { + modelInfo, err := gateway.GetModelInfoById(ctx, &gateway.GetModelInfoByIdReq{ModelId: modelId}) + if err != nil { + return nil, nil, fmt.Errorf("获取模型配置失败: %w", err) } - res = new(flowDto.GetIsChatModelRes) - err = commonHttp.Get(ctx, "model-gateway/model/getIsChatModel", headers, res, nil) - return -} - -func GetModelInfo(ctx context.Context, req *flowDto.GetModelInfoReq) (res *flowDto.GetModelInfoRes, err error) { - headers := make(map[string]string) - if r := g.RequestFromCtx(ctx); r != nil { - for k, v := range r.Request.Header { - if len(v) > 0 { - headers[k] = v[0] - } - } + // 异步模型 msgTopic 由 gateway.ModelCallResult 在为空时自动生成(唯一、带业务标识),调用方无需管理 + responseParams, err := gateway.ModelCallResult(ctx, modelId, modelInfo.ModelManage.ResponseType, sessionId, modelRequestParams, nil) + if err != nil { + return nil, nil, err } - res = new(flowDto.GetModelInfoRes) - err = commonHttp.Get(ctx, "model-gateway/model/getModel", headers, res, req) - return -} - -func GetComposeResult(ctx context.Context, buildType int, modelName, promptContent, skillName string, form []map[string]any, userForm []map[string]any, fileUrl []string, sessionId, nodeId string, cause string) (res *flowDto.ComposeCallbackReq, err error) { - if !g.IsEmpty(promptContent) { - userForm = append(userForm, map[string]any{ - "prompt": promptContent, + if g.IsEmpty(responseParams) { + return nil, nil, fmt.Errorf("生成内容为空") + } + outputRes := make([]map[string]any, 0) + for key, val := range responseParams.Content { + outputRes = append(outputRes, map[string]any{ + key: val, }) } - var callbackUrl = utils.GetCallbackURL(ctx, "/flow/execution/composeCallBack") - var consult = make([]flowDto.Consult, 0) - var collectFileUrls func(val any) (fullyConsumed bool) - collectFileUrls = func(val any) (fullyConsumed bool) { - switch { - case g.NewVar(val).IsSlice(): - slice := gconv.SliceAny(val) - allConsumed := false - for _, item := range slice { - if collectFileUrls(item) { - allConsumed = true - } + return outputRes, responseParams, nil +} + +func HttpCallResultLambda(ctx context.Context, nodeInput *flowDto.NodeExecutionInput) ([]map[string]any, error) { + var method, url, responseType, callbackUrl string + var headers map[string]string + var body map[string]any + var responseMapping map[string]any + + n := new([]node.NodePresetField) + err := gconv.Structs(nodeInput.Config.OutputConfig, n) + if err != nil { + return nil, err + } + + for _, item := range *n { + switch item.Field { + case "method": + method = gconv.String(item.Value) + case "url": + url = gconv.String(item.Value) + case "headers": + headers = gconv.MapStrStr(item.Value) + case "body": + body = gconv.Map(item.Value) + case "response": + // 先剥掉 {type, value/attrs} 包裹层,得到干净的输出结构模板 + responseMapping = gconv.Map(UnwrapSchemaWrapper(gconv.Map(item.Value))) + case "responseType": + responseType = gconv.String(item.Value) + if responseType == "callback" { + callbackUrl = item.Options[0].Config[0].Value } - return allConsumed - case g.NewVar(val).IsMap(): - m := gconv.Map(val) - allConsumed := false - for _, item := range m { - if collectFileUrls(item) { - allConsumed = true - } - } - return allConsumed - default: - s := gconv.String(val) - if s != "" { - getFileTypeByPath := GetFileTypeByPath(s) - if getFileTypeByPath != "" { - consult = append(consult, flowDto.Consult{ - Type: getFileTypeByPath, - Url: s, - }) - return true - } - } - return false } } - var newUserForm []map[string]any - for _, m := range userForm { - for k, v := range m { - if collectFileUrls(v) { - delete(m, k) + + if method == "" { + return nil, fmt.Errorf("method为空") + } + if url == "" { + return nil, fmt.Errorf("url为空") + } + + if headers == nil { + headers = make(map[string]string) + if r := g.RequestFromCtx(ctx); r != nil { + for k, v := range r.Request.Header { + if len(v) > 0 { + headers[k] = v[0] + } } } - if len(m) > 0 { - newUserForm = append(newUserForm, m) - } } - for _, v := range fileUrl { - getFileTypeByPath := GetFileTypeByPath(gconv.String(v)) - if getFileTypeByPath != "" { - consult = append(consult, flowDto.Consult{ - Type: getFileTypeByPath, - Url: gconv.String(v), + + // 构建请求参数 + ProcessValueSourceRecursive(body, nodeInput.Global) + // 递归剥掉 {type, value/attrs} 包裹层,只保留 key/value + wrapper := UnwrapSchemaWrapper(body) + newBody := gconv.Map(wrapper) + + // 1. 自己生成唯一 taskId(不用前端给) + taskId := "my_task_" + uuid.New().String() // 自己生成唯一ID + if responseType == "callback" { + newBody[callbackUrl] = utils.GetCallbackURL(ctx, "/httpNodeCallback?task_id="+taskId) + } + // ====================== 核心改动 ====================== + // 1. 定义一个空map接收原始HTTP返回结果 + var rawHttpResult map[string]any + // 2. 发送请求(不变) + if method == "GET" { + err = commonHttp.Get(ctx, url, headers, &rawHttpResult, newBody) + } else if method == "POST" { + err = commonHttp.Post(ctx, url, headers, &rawHttpResult, newBody) + } else if method == "PUT" { + err = commonHttp.Put(ctx, url, headers, &rawHttpResult, newBody) + } else if method == "DELETE" { + err = commonHttp.Delete(ctx, url, headers, &rawHttpResult, newBody) + } else { + return nil, fmt.Errorf("method 不支持") + } + if err != nil { + return nil, err + } + + var e = "" + + finalResult := make(map[string]any) + if responseType == "sync" { + httpResultJson := gconv.String(rawHttpResult) + // 按 responseMapping 定义的结构,从 http 返回结果中拷贝对应字段 + finalResult = MapResultByTemplate(responseMapping, rawHttpResult) + e = httpResultJson + } + if responseType == "callback" { + var waitResult any + waitResult, err = Wait(ctx, taskId) + if err != nil { + return nil, err + } + request, ok := waitResult.(*ghttp.Request) + if !ok { + return nil, fmt.Errorf("入参类型错误") + } + + bodyStr := request.GetBodyString() + // 按 responseMapping 定义的结构,从回调结果中拷贝对应字段 + finalResult = MapResultByTemplate(responseMapping, gconv.Map(bodyStr)) + e = bodyStr + } + if responseType == "pull" { + return nil, fmt.Errorf("pull 暂不支持") + } + + if g.IsEmpty(finalResult) { + return nil, fmt.Errorf("http请求异常,返回结果为空:%v", e) + } + + outputRes := make([]map[string]any, 0) + for i, item := range finalResult { + if nodeInput.Config.IsSaveFile { + outputRes = append(outputRes, map[string]any{ + fmt.Sprintf("http_file_url:%v", i): item, }) } + outputRes = append(outputRes, map[string]any{ + fmt.Sprintf("%v", i): item, + }) } - msgReq := flowDto.ComposeMessagesReq{ - BuildType: buildType, - ModelName: modelName, - SkillName: skillName, - CallbackUrl: callbackUrl, - Cause: cause, - Form: form, - UserForm: newUserForm, - Consult: consult, - SessionId: sessionId, - NodeId: nodeId, - } - headers := make(map[string]string) - if r := g.RequestFromCtx(ctx); r != nil { - for k, v := range r.Request.Header { - if len(v) > 0 { - headers[k] = v[0] - } - } - } - msgRes := new(flowDto.ComposeMessagesRes) - err = commonHttp.Post(ctx, "prompts-core/prompt/composeMessages", headers, msgRes, &msgReq) - if err != nil { - return - } - if g.IsEmpty(msgRes.TaskId) { - return nil, fmt.Errorf("msg is empty") - } - waitRes, err := Wait(ctx, msgRes.TaskId) - if err != nil { - return nil, err - } - msg := new(flowDto.ComposeCallbackReq) - if err = gconv.Struct(waitRes, msg); err != nil { - return nil, err - } - if !g.IsEmpty(msg.ErrorMsg) { - return nil, fmt.Errorf(msg.ErrorMsg) - } - return msg, nil -} -func CreateGatewayTask(ctx context.Context, epicycleId int64, model string, content map[string]any) (map[string]any, error) { - taskId, err := createGatewayTaskOnly(ctx, epicycleId, model, content) - if err != nil { - return nil, err - } - return waitGatewayResult(ctx, taskId) -} - -// createGatewayTaskOnly creates a gateway task and returns the taskId only -// doesn't wait for completion -func createGatewayTaskOnly(ctx context.Context, epicycleId int64, model string, content map[string]any) (string, error) { - callbackUrl := utils.GetCallbackURL(ctx, "/flow/execution/modelCallback") - req := flowDto.ModelGatewayReq{ - ModelName: model, - BizName: g.Cfg().MustGet(ctx, "server.name").String(), - CallbackUrl: callbackUrl, - RequestPayload: content, - EpicycleId: epicycleId, - } - - headers := make(map[string]string) - if r := g.RequestFromCtx(ctx); r != nil { - for k, v := range r.Request.Header { - if len(v) > 0 { - headers[k] = v[0] - } - } - } - - res := new(flowDto.ModelGatewayRes) - err := commonHttp.Post(ctx, "model-gateway/task/createTask", headers, res, &req) - if err != nil { - return "", err - } - if g.IsEmpty(res.TaskId) { - return "", fmt.Errorf("创建模型任务失败,taskId为空") - } - - return res.TaskId, nil -} - -// waitGatewayResult waits for a created gateway task to complete and returns the result -func waitGatewayResult(ctx context.Context, taskId string) (map[string]any, error) { - waitRes, err := Wait(ctx, taskId) - if err != nil { - return nil, err - } - - task := new(flowDto.ModelCallbackReq) - if err = gconv.Struct(waitRes, task); err != nil { - return nil, err - } - if task.State == 3 || !g.IsEmpty(task.ErrorMsg) { - return nil, fmt.Errorf("模型执行失败:%s", task.ErrorMsg) - } - if g.IsEmpty(task.OssFile) { - return nil, fmt.Errorf("模型返回结果为空") - } - // 获取远程文件内容 - file, err := GetFileBytesFromURL(ctx, task.OssFile) - if err != nil { - return nil, err - } - return gconv.Map(file), nil -} - -// updateTokenCount updates the token count in node execution -func updateTokenCount(ctx context.Context, nodeExecutionId int64, responseField string, result map[string]any) { - if responseField == "" { - return - } - _, _ = nodeDao.NodeExecutionDao.Update(ctx, &nodeDto.UpdateNodeExecutionReq{ - Id: nodeExecutionId, - CompletionTokens: gconv.Int(result[responseField]), - TotalTokens: gconv.Int(result[responseField]), - }) -} - -func GetModelResult(ctx context.Context, sessionId string, nodeInput *flowDto.NodeExecutionInput, skillName string, form []map[string]any, userForm []map[string]any) (mapTaskResult []map[string]any, err error) { - buildType := 1 - if nodeInput.Config.NodeCode == node.NodeTypeDataConversionModel { - buildType = 3 - } - - if !nodeInput.Global.IsDialogue { - sessionId = "" - } - - composeResult, err := GetComposeResult(ctx, buildType, nodeInput.Config.ModelConfig.ModelName, nodeInput.Config.PromptContent, skillName, form, userForm, nodeInput.Global.FileUrl, sessionId, nodeInput.Config.Id, nodeInput.Config.Name) - if err != nil { - return nil, err - } - if composeResult.Status != "success" { - return nil, fmt.Errorf("模型提示词构建错误") - } - - modelInfo, err := GetModelInfo(ctx, &flowDto.GetModelInfoReq{ModelName: nodeInput.Config.ModelConfig.ModelName}) - if err != nil { - return nil, err - } - - mapTaskResult = make([]map[string]any, len(composeResult.Messages.Rounds)) - var taskResultMap map[string]any - - needSequential := false - if buildType == 1 { - if needSequential { - for idx, item := range composeResult.Messages.Rounds { - if !g.IsEmpty(taskResultMap) { - var set string - set, err = sjson.Set(gconv.String(item), modelInfo.Model.LastFrame, gconv.String(taskResultMap[modelInfo.Model.ResponseBody])) - if err != nil { - return nil, err - } - item = gconv.Map(set) - } - - var taskResult map[string]any - taskResult, err = CreateGatewayTask(ctx, composeResult.EpicycleId, nodeInput.Config.ModelConfig.ModelName, item) - if err != nil { - return nil, err - } - if g.IsEmpty(taskResult) { - return nil, fmt.Errorf("模型返回结果为空") - } - - if nodeInput.Config.NodeCode == node.NodeTypeVideoModel { - ext := GetFileTypeByPath(gconv.String(taskResult[modelInfo.Model.ResponseBody])) - if ext == "image" { - taskResultMap = taskResult - } else { - taskResultMap = make(map[string]any) - } - } else { - taskResultMap = make(map[string]any) - } - - mapTaskResult[idx] = taskResult - updateTokenCount(ctx, nodeInput.NodeExecutionId, modelInfo.Model.ResponseTokenField, taskResult) - } - } else { - taskIdList := make([]string, len(composeResult.Messages.Rounds)) - - for idx, item := range composeResult.Messages.Rounds { - taskId, err := createGatewayTaskOnly(ctx, composeResult.EpicycleId, nodeInput.Config.ModelConfig.ModelName, item) - if err != nil { - return nil, err - } - taskIdList[idx] = taskId - } - - // 全局共享子上下文,实现一处报错全部终止 - subCtx, globalCancel := context.WithCancel(ctx) - defer globalCancel() // 函数退出兜底释放 - - var wg sync.WaitGroup - errChan := make(chan error, len(taskIdList)) - - // 加互斥锁保护结果map - var mu sync.Mutex - - for idx, taskId := range taskIdList { - wg.Add(1) - - go func(idx int, taskId string) { - defer wg.Done() - - taskResult, err := waitGatewayResult(subCtx, taskId) - if err != nil { - errChan <- err - globalCancel() // 全局取消,所有协程收到ctx取消信号快速退出 - return - } - - // 加锁写入map,解决并发竞态 - mu.Lock() - mapTaskResult[idx] = taskResult - mu.Unlock() - - updateTokenCount(ctx, nodeInput.NodeExecutionId, modelInfo.Model.ResponseTokenField, taskResult) - }(idx, taskId) - } - - wg.Wait() - close(errChan) - - // 收集全部错误,而非只读一条 - var errs []error - for len(errChan) > 0 { - errs = append(errs, <-errChan) - } - - if len(errs) > 0 { - // 返回第一个错误;如需汇总所有错误可拼接 - return nil, errs[0] - } - } - } else { - for idx, item := range composeResult.Messages.Rounds { - mapTaskResult[idx] = item - updateTokenCount(ctx, nodeInput.NodeExecutionId, modelInfo.Model.ResponseTokenField, item) - } - } - - return mapTaskResult, nil -} - -func BuildNestedJson(body g.Map, mockConfigMap map[string]*entity.FlowNode) g.Map { - jsonStr := "{}" - for originKey, originItem := range body { - bodyItemMap := gconv.Map(originItem) - val := bodyItemMap["value"] - if v, ok := bodyItemMap["value"]; ok { - jsonStr, _ = sjson.Set(jsonStr, originKey, v) - } - // 判断 value 是不是引用结构(map) - if g.NewVar(val).IsMap() { - valMap := gconv.Map(val) - nodeId := gconv.String(valMap["nodeId"]) - fieldName := gconv.String(valMap["field"]) - if configValue, ok := mockConfigMap[nodeId]; ok { - if !g.IsEmpty(configValue.OutputResult) { - for _, v := range configValue.OutputResult { - if strings.Contains(v.Field, fieldName) { - if configValue.NodeCode == node.NodeTypeDataConversionModel { - switch { - case g.NewVar(v.Value).IsSlice() || g.NewVar(v.Value).IsMap(): - // 核心:自动判断两种结构,精准赋值 - vm := gconv.Map(v.Value) - // 先判断是否是 单个key包裹的对象(如 {"subtitle_style": {...}}) - if len(vm) == 1 { - // 遍历取出唯一的 key 和 真实值 - for innerKey, innerVal := range vm { - // 直接用 innerKey(subtitle_style)赋值 - jsonStr, _ = sjson.Set(jsonStr, innerKey, innerVal) - } - } else { - // 直接是对象,用 originKey 赋值 - jsonStr, _ = sjson.Set(jsonStr, originKey, v.Value) - } - default: - jsonStr, _ = sjson.Set(jsonStr, originKey, v.Value) - } - } else { - jsonStr, _ = sjson.Set(jsonStr, originKey, v.Value) - } - } - } - } - if !g.IsEmpty(configValue.FormConfig) { - for _, v := range configValue.FormConfig { - if v.Field == fieldName { - if v.Type == "uploadMultiple" { - if g.NewVar(v.FieldConstraint).IsMap() { - mapFieldConstraint := gconv.Map(v.FieldConstraint) - for key, value := range mapFieldConstraint { - if key == "maxFileCount" { - if gconv.Int(value) == 1 { - // 如果是单文件上传,则替换成字符串重新赋值给v.Value - if g.NewVar(v.Value).IsSlice() { - sliceVal := gconv.SliceAny(v.Value) - if len(sliceVal) > 0 { - v.Value = sliceVal[0] - } - } - } - } - } - } - } - jsonStr, _ = sjson.Set(jsonStr, originKey, v.Value) - } - } - } - } - } - } - return gconv.Map(jsonStr) -} - -func VideoConcat(ctx context.Context, videoUrls []string) (r any, err error) { - var httpUrl = "media/video/concat/async" - headers := make(map[string]string) - if r := g.RequestFromCtx(ctx); r != nil { - for k, v := range r.Request.Header { - if len(v) > 0 { - headers[k] = v[0] - } - } - } - var callbackUrl = utils.GetCallbackURL(ctx, "/flow/execution/videoCallback") - var newBody = flowDto.VideoConcatReq{ - VideoUrls: videoUrls, - Method: "auto", - Upload: true, - CallbackUrl: callbackUrl, - } - res := new(flowDto.VideoConcatRes) - err = commonHttp.Post(ctx, httpUrl, headers, &res, newBody) - if err != nil { - return nil, err - } - return Wait(ctx, res.TaskId) -} - -func GetFileBytesFromURL(ctx context.Context, fileUrl string) ([]byte, error) { - newS := strings.ReplaceAll(fileUrl, g.Cfg().MustGet(ctx, "filePrefix").String(), g.Cfg().MustGet(ctx, "minioPrefix").String()) - // 使用 GoFrame 客户端(自带超时、追踪、日志等能力) - resp, err := g.Client().Get(ctx, newS) - if err != nil { - return nil, gerror.Wrapf(err, "failed to request url: %s", newS) - } - defer resp.Close() - - // 校验状态码 - if resp.StatusCode != http.StatusOK { - return nil, gerror.Newf("request failed with status code: %d, url: %s", resp.StatusCode, newS) - } - - // 读取全部内容 - allBytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil, gerror.Wrapf(err, "failed to read response body, url: %s", fileUrl) - } - - return allBytes, nil -} - -func Upload(ctx context.Context, req *dto.UploadFileBytesReq) (*dto.UploadFileBytesRes, error) { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - - part, err := writer.CreateFormFile("file", req.FileName) - if err != nil { - return nil, err - } - if _, err = part.Write(req.FileBytes); err != nil { - return nil, err - } - if err = writer.Close(); err != nil { - return nil, err - } - - headers := make(map[string]string) - headers["Content-Type"] = writer.FormDataContentType() - if r := g.RequestFromCtx(ctx); r != nil { - if auth := r.Header.Get("Authorization"); auth != "" { - headers["Authorization"] = auth - } - } - - // 发起上传请求 - res := &dto.UploadFileBytesRes{} - httpUrl := "oss/file/uploadFile" - if err = commonHttp.Post(ctx, httpUrl, headers, res, body.Bytes()); err != nil { - return nil, err - } - - g.Log().Infof(ctx, "[Upload] success url=%s size=%d", res.FileURL, res.FileSize) - return res, nil -} - -func GetFileTypeByPath(filePath string) string { - if filePath == "" { - return "" - } - - // 解析 URL,获取真实路径(兼容 http 链接) - u, err := url.Parse(filePath) - if err == nil { - filePath = u.Path - } - - // 获取后缀(小写) - ext := filepath.Ext(filePath) - ext = strings.ToLower(ext) - - // 判断类型 - switch ext { - case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp": - return "image" - case ".mp4", ".mov", ".avi", ".flv", ".wmv", ".mkv": - return "video" - case ".mp3", ".wav", ".m4a", ".flac", ".aac", ".ogg": - return "audio" - case ".txt", ".md", ".log", ".json", ".xml", ".inc": - return "text" - case ".html": - return "html" - case ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx": - return "document" - default: - return "" - } -} - -func BuildText(text string) string { - // 生成单条HTML - var htmlBuilder strings.Builder - htmlBuilder.WriteString(` - - - - - - - - -需要配图:X 张
- if text != "" { - // 写入清理后的文案 - htmlBuilder.WriteString(fmt.Sprintf(`]*>.*?(\d+).*?
`) - match := re.FindStringSubmatch(content) - if len(match) >= 2 { - num, err := strconv.Atoi(match[1]) - if err == nil { - return num - } - } - return 0 -} - -func ImageTagRegex(html string) string { - // 🔥 修复:支持单引号、双引号、空格、换行,100% 删除- imageTagRegex := regexp.MustCompile(`
]*>[\s\S]*?
`) - return imageTagRegex.ReplaceAllString(html, "") -} - -// StripHtmlTags 去掉所有HTML标签,保留换行和文本结构,并删除配图标记行 -func StripHtmlTags(html string) string { - // 1. 替换块级标签为换行,保证排版 - blockTags := regexp.MustCompile(`?(div|p|h1|h2|h3|h4|h5|h6|li|ul|ol|br|tr|td|th)[^>]*>`) - text := blockTags.ReplaceAllString(html, "\n") - - // 2. 去掉所有剩余的 HTML 标签 - allTags := regexp.MustCompile(`<[^>]+>`) - text = allTags.ReplaceAllString(text, "") - - // 4. 清理多余空行(多个换行只保留一个) - text = regexp.MustCompile(`\n\s*\n`).ReplaceAllString(text, "\n") - - // 5. 只去掉首尾空白,中间换行保留 - text = strings.TrimSpace(text) - - return text -} - -// SplitMultiContents 拆分模型返回的多条文案(基于HTML标签分隔) -func SplitMultiContents(htmlContent string) []string { - var contents []string - // 正则匹配