From 7c26914353b3f67238558ad86c53c3c0b859ad83 Mon Sep 17 00:00:00 2001 From: qhd <1766646056@qq.com> Date: Tue, 12 May 2026 13:34:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E6=89=A7=E8=A1=8C=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增流程执行记录的实体、DTO、DAO、控制器和服务层,支持工作流的执行、回调及结果树状列表查询;同时更新服务名称为 ai-agent。 --- config.yml | 3 +- go.mod | 12 +- go.sum | 13 + main.go | 15 +- update.sql | 133 ++- workflow/consts/flow/flow_execution_status.go | 28 + .../flow/flow_execution_trigger_type.go | 27 + workflow/consts/flow/flow_template_status.go | 26 + .../consts/flow/flow_user_access_level.go | 27 + workflow/consts/node/node_execution_status.go | 29 + workflow/consts/node/node_template.go | 186 +++ workflow/consts/public/table_name.go | 1 + .../flow/flow_execution_controller.go | 26 + .../flow/flow_template_controller.go | 41 + .../controller/flow/flow_user_controller.go | 41 + .../node/node_library_controller.go | 15 + .../skill/skill_template_controller.go | 26 + .../controller/skill/skill_user_controller.go | 30 + workflow/dao/flow/flow_execution_dao.go | 64 ++ workflow/dao/flow/flow_template_dao.go | 70 ++ workflow/dao/flow/flow_user_dao.go | 71 ++ workflow/dao/skill/skill_template_dao.go | 51 + workflow/dao/skill/skill_user_dao.go | 52 + workflow/model/dto/flow/flow_execution_dto.go | 235 ++++ workflow/model/dto/flow/flow_template_dto.go | 76 ++ workflow/model/dto/flow/flow_user_dto.go | 80 ++ workflow/model/dto/node/node_library_dto.go | 29 + .../model/dto/skill/skill_template_dto.go | 50 + workflow/model/dto/skill/skill_user_dto.go | 59 + workflow/model/entity/flow_execution.go | 53 + workflow/model/entity/flow_template.go | 41 + workflow/model/entity/flow_user.go | 70 ++ workflow/model/entity/skill_template.go | 33 + workflow/model/entity/skill_user.go | 31 + .../service/flow/flow_execution_service.go | 612 ++++++++++ .../service/flow/flow_template_service.go | 55 + workflow/service/flow/flow_user_service.go | 185 +++ workflow/service/flow/lambda_node.go | 1001 +++++++++++++++++ workflow/service/flow/lambda_node_util.go | 245 ++++ workflow/service/node/node_library_service.go | 147 +++ .../service/skill/skill_template_service.go | 38 + workflow/service/skill/skill_user_service.go | 130 +++ 42 files changed, 4146 insertions(+), 11 deletions(-) create mode 100644 workflow/consts/flow/flow_execution_status.go create mode 100644 workflow/consts/flow/flow_execution_trigger_type.go create mode 100644 workflow/consts/flow/flow_template_status.go create mode 100644 workflow/consts/flow/flow_user_access_level.go create mode 100644 workflow/consts/node/node_execution_status.go create mode 100644 workflow/consts/node/node_template.go create mode 100644 workflow/controller/flow/flow_execution_controller.go create mode 100644 workflow/controller/flow/flow_template_controller.go create mode 100644 workflow/controller/flow/flow_user_controller.go create mode 100644 workflow/controller/node/node_library_controller.go create mode 100644 workflow/controller/skill/skill_template_controller.go create mode 100644 workflow/controller/skill/skill_user_controller.go create mode 100644 workflow/dao/flow/flow_execution_dao.go create mode 100644 workflow/dao/flow/flow_template_dao.go create mode 100644 workflow/dao/flow/flow_user_dao.go create mode 100644 workflow/dao/skill/skill_template_dao.go create mode 100644 workflow/dao/skill/skill_user_dao.go create mode 100644 workflow/model/dto/flow/flow_execution_dto.go create mode 100644 workflow/model/dto/flow/flow_template_dto.go create mode 100644 workflow/model/dto/flow/flow_user_dto.go create mode 100644 workflow/model/dto/node/node_library_dto.go create mode 100644 workflow/model/dto/skill/skill_template_dto.go create mode 100644 workflow/model/dto/skill/skill_user_dto.go create mode 100644 workflow/model/entity/flow_execution.go create mode 100644 workflow/model/entity/flow_template.go create mode 100644 workflow/model/entity/flow_user.go create mode 100644 workflow/model/entity/skill_template.go create mode 100644 workflow/model/entity/skill_user.go create mode 100644 workflow/service/flow/flow_execution_service.go create mode 100644 workflow/service/flow/flow_template_service.go create mode 100644 workflow/service/flow/flow_user_service.go create mode 100644 workflow/service/flow/lambda_node.go create mode 100644 workflow/service/flow/lambda_node_util.go create mode 100644 workflow/service/node/node_library_service.go create mode 100644 workflow/service/skill/skill_template_service.go create mode 100644 workflow/service/skill/skill_user_service.go diff --git a/config.yml b/config.yml index 6b4f6a2..d582232 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,6 @@ server: address: ":3006" - name: "digital-human" + name: "ai-agent" workerId: 1 # 雪花算法worker ID database: default: @@ -57,6 +57,7 @@ redis: readTimeout: "30s" #TCP的Read操作超时时间,使用时间字符串例如30s/1m/1d writeTimeout: "30s" #TCP的Write操作超时时间,使用时间字符串例如30s/1m/1d maxActive: 100 + consul: address: 116.204.74.41:8500 diff --git a/go.mod b/go.mod index e080b35..9e0b3e1 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,16 @@ module ai-agent go 1.26.0 require ( - gitea.com/red-future/common v0.0.12 + gitea.com/red-future/common v0.0.19 github.com/cloudwego/eino v0.8.13 github.com/cloudwego/eino-ext/components/model/qwen v0.1.9 github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0 github.com/gogf/gf/v2 v2.10.0 + go.opentelemetry.io/otel/trace v1.38.0 ) -// replace gitea.com/red-future/common => ../common +//replace gitea.com/red-future/common => ../common require ( github.com/BurntSushi/toml v1.5.0 // indirect @@ -50,7 +51,7 @@ require ( github.com/google/flatbuffers v1.12.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/goph/emperror v0.17.2 // indirect - github.com/gorilla/websocket v1.5.3 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/grokify/html-strip-tags-go v0.1.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/consul/api v1.26.1 // indirect @@ -75,13 +76,14 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect 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.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // 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 github.com/olekukonko/tablewriter v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.0.9 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/r3labs/diff/v2 v2.15.1 // indirect github.com/redis/go-redis/v9 v9.12.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -89,6 +91,7 @@ require ( github.com/tiger1103/gfast-token v1.0.10 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/vcaesar/cedar v0.30.0 // indirect + github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yargevad/filepathx v1.0.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect @@ -99,7 +102,6 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect golang.org/x/arch v0.11.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect diff --git a/go.sum b/go.sum index 5a573db..469dec5 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= gitea.com/red-future/common v0.0.12 h1:whaCAiH33orl0P+oDpxzC4VoNluHKNYKGZ+FcUWw85Q= gitea.com/red-future/common v0.0.12/go.mod h1:3a7cwZNvgpKw5FzE8x5MZImd7NBePGXRGFSMjt90158= +gitea.com/red-future/common v0.0.19 h1:9/WrfCFUCeFUYwuhBYF+JOQi5F5xuOy+gVnf2ZvHZu4= +gitea.com/red-future/common v0.0.19/go.mod h1:6/nqIucVzmjOyqDTIq71feYBXXFNBy0rFwzaQ0/Ueoo= 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= @@ -171,6 +173,8 @@ github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25d github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/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= @@ -288,6 +292,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= @@ -326,6 +332,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b 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/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.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg= github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -356,6 +364,7 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -374,6 +383,8 @@ 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/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= @@ -434,6 +445,7 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -510,6 +522,7 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 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= diff --git a/main.go b/main.go index 00906fd..46fb1c6 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,14 @@ package main import ( digitalhumanController "ai-agent/digital-human/controller" - workflowController "ai-agent/workflow/controller" + workController "ai-agent/workflow/controller" + workflowController "ai-agent/workflow/controller/flow" + workflowNodeController "ai-agent/workflow/controller/node" + workflowSkillController "ai-agent/workflow/controller/skill" "context" - _ "gitea.com/red-future/common/config" "gitea.com/red-future/common/http" "gitea.com/red-future/common/jaeger" - _ "gitea.com/red-future/common/ragflow" _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" _ "github.com/gogf/gf/contrib/nosql/redis/v2" ) @@ -24,7 +25,13 @@ func main() { digitalhumanController.DigitalHuman, // 数字人相关接口 digitalhumanController.Video, // 视频相关接口 digitalhumanController.AsyncTask, // 异步任务相关接口 - workflowController.CreationInfo, + workController.CreationInfo, + workflowController.FlowExecution, + workflowController.FlowUser, + workflowController.FlowTemplate, + workflowNodeController.NodeLibrary, + workflowSkillController.SkillTemplate, + workflowSkillController.SkillUser, }) // 保持应用运行 select {} diff --git a/update.sql b/update.sql index 4787301..0510525 100644 --- a/update.sql +++ b/update.sql @@ -279,6 +279,87 @@ COMMENT ON COLUMN black_deacon_creation_info.title IS '标题'; --------------------pgsql创建creation_info表语句--------------------------- +--------------------pgsql创建black_deacon_skill_template表语句--------------------------- +-- 技能模板表 +CREATE TABLE IF NOT EXISTS black_deacon_skill_template ( + -- 基础字段(完全对齐项目规范) + 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), + + -- 业务字段 + name VARCHAR(128) NOT NULL DEFAULT '', + description TEXT DEFAULT '', + category VARCHAR(64) NOT NULL DEFAULT '', + file_name VARCHAR(255) NOT NULL DEFAULT '', + file_url VARCHAR(512) NOT NULL DEFAULT '' + ); + +-- 索引 +CREATE INDEX idx_skill_template_tenant_id ON black_deacon_skill_template(tenant_id); +CREATE INDEX idx_skill_template_category ON black_deacon_skill_template(category); +CREATE INDEX idx_skill_template_deleted_at ON black_deacon_skill_template(deleted_at); + +-- 注释 +COMMENT ON TABLE black_deacon_skill_template IS '技能模板表'; +COMMENT ON COLUMN black_deacon_skill_template.id IS '主键ID'; +COMMENT ON COLUMN black_deacon_skill_template.tenant_id IS '租户ID'; +COMMENT ON COLUMN black_deacon_skill_template.creator IS '创建人'; +COMMENT ON COLUMN black_deacon_skill_template.created_at IS '创建时间'; +COMMENT ON COLUMN black_deacon_skill_template.updater IS '更新人'; +COMMENT ON COLUMN black_deacon_skill_template.updated_at IS '更新时间'; +COMMENT ON COLUMN black_deacon_skill_template.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN black_deacon_skill_template.name IS '技能模板名称'; +COMMENT ON COLUMN black_deacon_skill_template.description IS '描述'; +COMMENT ON COLUMN black_deacon_skill_template.category IS '分类'; +COMMENT ON COLUMN black_deacon_skill_template.file_name IS '文件名称'; +COMMENT ON COLUMN black_deacon_skill_template.file_url IS '文件地址'; +--------------------pgsql创建black_deacon_skill_template表语句--------------------------- + +--------------------pgsql创建black_deacon_skill_user表语句--------------------------- +-- 技能用户表 +CREATE TABLE IF NOT EXISTS black_deacon_skill_user ( + -- 基础字段(完全对齐项目规范) + 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), + + -- 业务字段 + name VARCHAR(128) NOT NULL DEFAULT '', + description TEXT DEFAULT '', + category VARCHAR(64) NOT NULL DEFAULT '', + file_name VARCHAR(255) NOT NULL DEFAULT '', + file_url VARCHAR(512) NOT NULL DEFAULT '' + ); + +-- 索引 +CREATE INDEX idx_skill_user_tenant_id ON black_deacon_skill_user(tenant_id); +CREATE INDEX idx_skill_user_category ON black_deacon_skill_user(category); +CREATE INDEX idx_skill_user_deleted_at ON black_deacon_skill_user(deleted_at); + +-- 注释 +COMMENT ON TABLE black_deacon_skill_user IS '技能用户表'; +COMMENT ON COLUMN black_deacon_skill_user.id IS '主键ID'; +COMMENT ON COLUMN black_deacon_skill_user.tenant_id IS '租户ID'; +COMMENT ON COLUMN black_deacon_skill_user.creator IS '创建人'; +COMMENT ON COLUMN black_deacon_skill_user.created_at IS '创建时间'; +COMMENT ON COLUMN black_deacon_skill_user.updater IS '更新人'; +COMMENT ON COLUMN black_deacon_skill_user.updated_at IS '更新时间'; +COMMENT ON COLUMN black_deacon_skill_user.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN black_deacon_skill_user.name IS '技能名称'; +COMMENT ON COLUMN black_deacon_skill_user.description IS '描述'; +COMMENT ON COLUMN black_deacon_skill_user.category IS '分类'; +COMMENT ON COLUMN black_deacon_skill_user.file_name IS '文件名称'; +COMMENT ON COLUMN black_deacon_skill_user.file_url IS '文件地址'; +--------------------pgsql创建black_deacon_skill_user表语句--------------------------- --------------------pgsql创建black_deacon_flow_execution表语句--------------------------- -- 流程执行记录表 @@ -294,6 +375,7 @@ CREATE TABLE IF NOT EXISTS black_deacon_flow_execution ( -- 业务字段 flow_user_id BIGINT NOT NULL, -- 流程ID + flow_name VARCHAR(128) NOT NULL DEFAULT '', trigger_type VARCHAR(32) NOT NULL DEFAULT '', -- 触发类型 duration_ms BIGINT NOT NULL DEFAULT 0, -- 执行时长(毫秒) status SMALLINT NOT NULL DEFAULT 1, -- 状态:1-运行中,2-成功,3-失败 @@ -302,6 +384,7 @@ CREATE TABLE IF NOT EXISTS black_deacon_flow_execution ( output_params JSONB DEFAULT '[]'::JSONB, error_message TEXT DEFAULT '', -- 错误信息 trace_id VARCHAR(64) DEFAULT '' -- 跟踪ID + session_id VARCHAR(64) DEFAULT '' -- 会话ID ); -- 索引(高频查询) @@ -321,6 +404,7 @@ COMMENT ON COLUMN black_deacon_flow_execution.updater IS '更新人'; COMMENT ON COLUMN black_deacon_flow_execution.updated_at IS '更新时间'; COMMENT ON COLUMN black_deacon_flow_execution.deleted_at IS '删除时间(软删)'; COMMENT ON COLUMN black_deacon_flow_execution.flow_user_id IS '流程ID'; +COMMENT ON COLUMN black_deacon_flow_execution.flow_name IS '流程名称'; COMMENT ON COLUMN black_deacon_flow_execution.trigger_type IS '触发类型'; COMMENT ON COLUMN black_deacon_flow_execution.duration_ms IS '执行时长(毫秒)'; COMMENT ON COLUMN black_deacon_flow_execution.status IS '状态:1-运行中,2-成功,3-失败'; @@ -329,6 +413,7 @@ COMMENT ON COLUMN black_deacon_flow_execution.node_input_params IS '节点输入 COMMENT ON COLUMN black_deacon_flow_execution.output_params IS '输出参数'; COMMENT ON COLUMN black_deacon_flow_execution.error_message IS '错误信息'; COMMENT ON COLUMN black_deacon_flow_execution.trace_id IS '跟踪ID'; +COMMENT ON COLUMN black_deacon_flow_execution.session_id IS '会话ID'; --------------------pgsql创建black_deacon_flow_execution表语句--------------------------- --------------------pgsql创建black_deacon_flow_user表语句--------------------------- @@ -357,4 +442,50 @@ COMMENT ON COLUMN black_deacon_flow_user.flow_content IS '流程内容'; COMMENT ON COLUMN black_deacon_flow_user.node_input_params IS '节点输入参数'; COMMENT ON COLUMN black_deacon_flow_user.access_level IS '访问权限:1私有,2团队,3公开'; COMMENT ON COLUMN black_deacon_flow_user.source_flow_template_id IS '来源流程模板ID'; ---------------------pgsql创建black_deacon_flow_user表语句--------------------------- \ No newline at end of file +--------------------pgsql创建black_deacon_flow_user表语句--------------------------- + +--------------------pgsql创建black_deacon_flow_template表语句--------------------------- +-- 流程模板表 +CREATE TABLE IF NOT EXISTS black_deacon_flow_template ( + -- 基础字段(完全对齐项目规范) + 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), + + -- 业务字段 + flow_template_name VARCHAR(128) NOT NULL DEFAULT '', + description TEXT DEFAULT '', + category_code VARCHAR(64) NOT NULL DEFAULT '', + category_name VARCHAR(64) NOT NULL DEFAULT '', + flow_content JSONB DEFAULT '{}', + node_input_params JSONB DEFAULT '[]', + status SMALLINT NOT NULL DEFAULT 1 + ); + +-- 索引 +CREATE INDEX idx_flow_template_tenant_id ON black_deacon_flow_template(tenant_id); +CREATE INDEX idx_flow_template_category_code ON black_deacon_flow_template(category_code); +CREATE INDEX idx_flow_template_status ON black_deacon_flow_template(status); +CREATE INDEX idx_flow_template_deleted_at ON black_deacon_flow_template(deleted_at); + +-- 注释 +COMMENT ON TABLE black_deacon_flow_template IS '流程模板表'; +COMMENT ON COLUMN black_deacon_flow_template.id IS '主键ID'; +COMMENT ON COLUMN black_deacon_flow_template.tenant_id IS '租户ID'; +COMMENT ON COLUMN black_deacon_flow_template.creator IS '创建人'; +COMMENT ON COLUMN black_deacon_flow_template.created_at IS '创建时间'; +COMMENT ON COLUMN black_deacon_flow_template.updater IS '更新人'; +COMMENT ON COLUMN black_deacon_flow_template.updated_at IS '更新时间'; +COMMENT ON COLUMN black_deacon_flow_template.deleted_at IS '删除时间(软删)'; +COMMENT ON COLUMN black_deacon_flow_template.flow_template_name IS '流程模板名称'; +COMMENT ON COLUMN black_deacon_flow_template.description IS '流程描述'; +COMMENT ON COLUMN black_deacon_flow_template.category_code IS '流程分类编码'; +COMMENT ON COLUMN black_deacon_flow_template.category_name IS '流程分类名称'; +COMMENT ON COLUMN black_deacon_flow_template.flow_content IS '流程内容'; +COMMENT ON COLUMN black_deacon_flow_template.node_input_params IS '节点输入参数'; +COMMENT ON COLUMN black_deacon_flow_template.status IS '流程状态:1启用/0停用'; +--------------------pgsql创建black_deacon_flow_template表语句--------------------------- \ No newline at end of file diff --git a/workflow/consts/flow/flow_execution_status.go b/workflow/consts/flow/flow_execution_status.go new file mode 100644 index 0000000..9798f0a --- /dev/null +++ b/workflow/consts/flow/flow_execution_status.go @@ -0,0 +1,28 @@ +package flow + +import "github.com/gogf/gf/v2/util/gconv" + +var ( + FlowExecutionStatusRunning = newFlowExecutionStatus(gconv.PtrInt8(1), "running") // 运行中 + FlowExecutionStatusSuccess = newFlowExecutionStatus(gconv.PtrInt8(2), "success") // 成功 + FlowExecutionStatusFailed = newFlowExecutionStatus(gconv.PtrInt8(3), "failed") // 失败 + FlowExecutionStatusPaused = newFlowExecutionStatus(gconv.PtrInt8(4), "paused") // 暂停 +) + +type FlowExecutionStatus *int8 + +type flowExecutionStatus struct { + code FlowExecutionStatus + desc string +} + +func (s flowExecutionStatus) Code() FlowExecutionStatus { + return s.code +} +func (s flowExecutionStatus) Desc() string { + return s.desc +} + +func newFlowExecutionStatus(code FlowExecutionStatus, desc string) flowExecutionStatus { + return flowExecutionStatus{code: code, desc: desc} +} diff --git a/workflow/consts/flow/flow_execution_trigger_type.go b/workflow/consts/flow/flow_execution_trigger_type.go new file mode 100644 index 0000000..9745429 --- /dev/null +++ b/workflow/consts/flow/flow_execution_trigger_type.go @@ -0,0 +1,27 @@ +package flow + +import "github.com/gogf/gf/v2/util/gconv" + +type FlowExecutionTriggerType *int8 + +var ( + FlowExecutionTriggerTypeManual = newFlowExecutionTriggerType(gconv.PtrInt8(1), "manual") + FlowExecutionTriggerTypeAuto = newFlowExecutionTriggerType(gconv.PtrInt8(2), "auto") + FlowExecutionTriggerTypeTime = newFlowExecutionTriggerType(gconv.PtrInt8(3), "time") +) + +type flowExecutionTriggerType struct { + code FlowExecutionTriggerType + desc string +} + +func (s flowExecutionTriggerType) Code() FlowExecutionTriggerType { + return s.code +} +func (s flowExecutionTriggerType) Desc() string { + return s.desc +} + +func newFlowExecutionTriggerType(code FlowExecutionTriggerType, desc string) flowExecutionTriggerType { + return flowExecutionTriggerType{code: code, desc: desc} +} diff --git a/workflow/consts/flow/flow_template_status.go b/workflow/consts/flow/flow_template_status.go new file mode 100644 index 0000000..3cc3536 --- /dev/null +++ b/workflow/consts/flow/flow_template_status.go @@ -0,0 +1,26 @@ +package flow + +import "github.com/gogf/gf/v2/util/gconv" + +var ( + FlowTemplateStatusDisable = newFlowTemplateStatus(gconv.PtrInt8(0), "disable") + FlowTemplateStatusEnable = newFlowTemplateStatus(gconv.PtrInt8(1), "enable") +) + +type FlowTemplateStatus *int8 + +type flowTemplateStatus struct { + code FlowTemplateStatus + desc string +} + +func (s flowTemplateStatus) Code() FlowTemplateStatus { + return s.code +} +func (s flowTemplateStatus) Desc() string { + return s.desc +} + +func newFlowTemplateStatus(code FlowTemplateStatus, desc string) flowTemplateStatus { + return flowTemplateStatus{code: code, desc: desc} +} diff --git a/workflow/consts/flow/flow_user_access_level.go b/workflow/consts/flow/flow_user_access_level.go new file mode 100644 index 0000000..48fae6b --- /dev/null +++ b/workflow/consts/flow/flow_user_access_level.go @@ -0,0 +1,27 @@ +package flow + +import "github.com/gogf/gf/v2/util/gconv" + +var ( + FlowUserAccessLevelPrivate = newFlowUserAccessLevel(gconv.PtrInt8(1), "private") + FlowUserAccessLevelTeam = newFlowUserAccessLevel(gconv.PtrInt8(2), "team") + FlowUserAccessLevelPublic = newFlowUserAccessLevel(gconv.PtrInt8(3), "public") +) + +type FlowUserAccessLevel *int8 + +type flowUserAccessLevel struct { + code FlowUserAccessLevel + desc string +} + +func (s flowUserAccessLevel) Code() FlowUserAccessLevel { + return s.code +} +func (s flowUserAccessLevel) Desc() string { + return s.desc +} + +func newFlowUserAccessLevel(code FlowUserAccessLevel, desc string) flowUserAccessLevel { + return flowUserAccessLevel{code: code, desc: desc} +} diff --git a/workflow/consts/node/node_execution_status.go b/workflow/consts/node/node_execution_status.go new file mode 100644 index 0000000..7d71eb6 --- /dev/null +++ b/workflow/consts/node/node_execution_status.go @@ -0,0 +1,29 @@ +package node + +import "github.com/gogf/gf/v2/util/gconv" + +var ( + NodeExecutionStatusRunning = newNodeExecutionStatus(gconv.PtrInt8(1), "running") // 运行中 + NodeExecutionStatusSuccess = newNodeExecutionStatus(gconv.PtrInt8(2), "success") // 成功 + NodeExecutionStatusFailed = newNodeExecutionStatus(gconv.PtrInt8(3), "failed") // 失败 + NodeExecutionStatusPaused = newNodeExecutionStatus(gconv.PtrInt8(4), "paused") // 暂停 + NodeExecutionStatusWait = newNodeExecutionStatus(gconv.PtrInt8(5), "wait") // 等待执行 +) + +type NodeExecutionStatus *int8 + +type nodeExecutionStatus struct { + code NodeExecutionStatus + desc string +} + +func (s nodeExecutionStatus) Code() NodeExecutionStatus { + return s.code +} +func (s nodeExecutionStatus) Desc() string { + return s.desc +} + +func newNodeExecutionStatus(code NodeExecutionStatus, desc string) nodeExecutionStatus { + return nodeExecutionStatus{code: code, desc: desc} +} diff --git a/workflow/consts/node/node_template.go b/workflow/consts/node/node_template.go new file mode 100644 index 0000000..0ac6405 --- /dev/null +++ b/workflow/consts/node/node_template.go @@ -0,0 +1,186 @@ +package node + +// ======================== 【常量定义:所有中文文案放这里!】 ======================== +// 分组名称 +const ( + NodeGroupNameComponent = "组件" + NodeGroupNameBase = "基础" + NodeGroupNameCustom = "自定义" +) + +// 节点名称 +const ( + NodeNameTextModel = "生成文案" + NodeNameImageModel = "生成图片" + NodeNameVideoModel = "视频" + NodeNameAudioModel = "音频" + NodeNameModel = "模型" + NodeNameMerge = "结果合并" + NodeNameJudge = "判断节点" + NodeNameForm = "表单" + NodeNameCustomNode = "自定义节点" +) + +// 表单字段 Label +const ( + FormLabelApiKey = "API Key" + FormLabelModel = "模型名称" + + FormLabelCondition = "判断条件" +) + +// ======================== 枚举类型 ======================== +type NodeGroup string + +const ( + NodeGroupComponent NodeGroup = "component" + NodeGroupBase NodeGroup = "base" + NodeGroupCustom NodeGroup = "custom" +) + +type NodeType string + +const ( + // 组件 + NodeTypeTextModel NodeType = "text_model" + NodeTypeImageModel NodeType = "image_model" + NodeTypeVideoModel NodeType = "video_model" + NodeTypeAudioModel NodeType = "audio_model" + + // 基础 + NodeTypeModel NodeType = "model" + NodeTypeMerge NodeType = "merge" + NodeTypeJudge NodeType = "judge" + NodeTypeForm NodeType = "form" + NodeTypeIntent NodeType = "intent" + + // 自定义 + NodeTypeCustomNode NodeType = "custom_node" +) + +// ======================== 结构定义 ======================== +type NodeFormField struct { + Value string `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"` +} + +type SelectOption struct { + Label string `json:"label"` + Value string `json:"value"` +} + +type ModelItem struct { + ModelApiKey string `json:"modelApiKey"` + ModelName string `json:"modelName"` + ModelForm map[string]any `json:"modelForm"` + ModelResponse map[string]any `json:"modelResponse"` +} + +type NodeItem struct { + NodeId string `json:"nodeId"` + NodeCode NodeType `json:"nodeCode"` + NodeName string `json:"nodeName"` // 从常量来 + SkillOption bool `json:"skillOption"` + FormConfig []NodeFormField `json:"formConfig"` + ModelConfig []ModelItem `json:"modelConfig"` +} + +type NodeGroupItem struct { + Group NodeGroup `json:"group"` + Label string `json:"label"` // 从常量来 + Items []NodeItem `json:"items"` +} + +// +//// 文案模型节点定义 +//func NewTextModelNode() NodeItem { +// return NodeItem{ +// NodeCode: NodeTypeTextModel, +// NodeName: NodeNameTextModel, +// FormConfig: []ModelItem{}, +// } +//} +// +//// 图片模型节点 +//func NewImageModelNode() NodeItem { +// return NodeItem{ +// NodeCode: NodeTypeImageModel, +// NodeName: NodeNameImageModel, +// FormConfig: []ModelItem{}, +// } +//} +// +//// 音频模型节点 +//func NewAudioModelNode() NodeItem { +// return NodeItem{ +// NodeCode: NodeTypeAudioModel, +// NodeName: NodeNameAudioModel, +// FormConfig: []ModelItem{}, +// } +//} +// +//// 视频模型节点 +//func NewVideoModelNode() NodeItem { +// return NodeItem{ +// NodeCode: NodeTypeVideoModel, +// NodeName: NodeNameVideoModel, +// FormConfig: []ModelItem{}, +// } +//} +// +//// 基础模型节点 +//func NewModelNode() NodeItem { +// return NodeItem{ +// NodeCode: NodeTypeModel, +// NodeName: NodeNameModel, +// FormConfig: []ModelItem{ +// { +// ModelName: "模型名称", +// ModelForm: []NodeFormField{ +// {Field: "apiKey", Label: FormLabelApiKey, Type: "input", Required: true}, +// {Field: "model", Label: FormLabelModel, Type: "input", Required: true}, +// }, +// }, +// }, +// } +//} +// +//// 判断节点 +//func NewJudgeNode() NodeItem { +// return NodeItem{ +// NodeCode: NodeTypeJudge, +// NodeName: NodeNameJudge, +// FormConfig: []ModelItem{ +// { +// ModelName: "判断条件", +// ModelForm: []NodeFormField{ +// {Field: "condition", Label: FormLabelCondition, Type: "input", Required: true}, +// }, +// }, +// }, +// } +//} +// +//// 表单参数节点 +//func NewFormNode() NodeItem { +// return NodeItem{ +// NodeCode: NodeTypeForm, +// NodeName: NodeNameForm, +// FormConfig: []ModelItem{}, +// } +//} +// +//// 自定义节点 +//func NewCustomNode() NodeItem { +// return NodeItem{ +// NodeCode: NodeTypeCustomNode, +// NodeName: NodeNameCustomNode, +// FormConfig: []ModelItem{}, +// } +//} diff --git a/workflow/consts/public/table_name.go b/workflow/consts/public/table_name.go index ff0229f..84e88c9 100644 --- a/workflow/consts/public/table_name.go +++ b/workflow/consts/public/table_name.go @@ -9,6 +9,7 @@ const ( const ( TableNameCreationInfo = "creation_info" TableNameFlowExecution = "flow_execution" + TableNameFlowTemplate = "flow_template" TableNameFlowUser = "flow_user" TableNameSkillTemplate = "skill_template" TableNameSkillUser = "skill_user" diff --git a/workflow/controller/flow/flow_execution_controller.go b/workflow/controller/flow/flow_execution_controller.go new file mode 100644 index 0000000..65107b0 --- /dev/null +++ b/workflow/controller/flow/flow_execution_controller.go @@ -0,0 +1,26 @@ +package flow + +import ( + flowDto "ai-agent/workflow/model/dto/flow" + flowService "ai-agent/workflow/service/flow" + "context" + + "gitea.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) ModelCallback(ctx context.Context, req *flowDto.ModelCallbackReq) (res *beans.ResponseEmpty, err error) { + err = flowService.FlowExecutionService.ModelCallback(ctx, req) + return +} + +func (c *flowExecution) List(ctx context.Context, req *flowDto.ListFlowExecutionReq) (res *flowDto.ListFlowExecutionTreeRes, err error) { + return flowService.FlowExecutionService.List(ctx, req) +} diff --git a/workflow/controller/flow/flow_template_controller.go b/workflow/controller/flow/flow_template_controller.go new file mode 100644 index 0000000..bf6f552 --- /dev/null +++ b/workflow/controller/flow/flow_template_controller.go @@ -0,0 +1,41 @@ +package flow + +import ( + flowDto "ai-agent/workflow/model/dto/flow" + flowService "ai-agent/workflow/service/flow" + "context" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" +) + +type flowTemplate struct{} + +var FlowTemplate = new(flowTemplate) + +func (c *flowTemplate) Create(ctx context.Context, req *flowDto.CreateFlowTemplateReq) (res *flowDto.CreateFlowTemplateRes, err error) { + res, err = flowService.FlowTemplateService.Create(ctx, req) + return +} + +func (c *flowTemplate) Update(ctx context.Context, req *flowDto.UpdateFlowTemplateReq) (res *beans.ResponseEmpty, err error) { + err = flowService.FlowTemplateService.Update(ctx, req) + return +} + +func (c *flowTemplate) Delete(ctx context.Context, req *flowDto.DeleteFlowTemplateReq) (res *beans.ResponseEmpty, err error) { + err = flowService.FlowTemplateService.Delete(ctx, req) + return +} + +func (c *flowTemplate) Get(ctx context.Context, req *flowDto.GetFlowTemplateReq) (res *flowDto.FlowTemplateVO, err error) { + return flowService.FlowTemplateService.Get(ctx, req) +} + +func (c *flowTemplate) List(ctx context.Context, req *flowDto.ListFlowTemplateReq) (res *flowDto.ListFlowTemplateRes, err error) { + if !g.IsEmpty(req.Page) { + req.Page = &beans.Page{PageNum: 1, PageSize: 20} + } + res, err = flowService.FlowTemplateService.List(ctx, req) + return +} diff --git a/workflow/controller/flow/flow_user_controller.go b/workflow/controller/flow/flow_user_controller.go new file mode 100644 index 0000000..8978b9a --- /dev/null +++ b/workflow/controller/flow/flow_user_controller.go @@ -0,0 +1,41 @@ +package flow + +import ( + flowDto "ai-agent/workflow/model/dto/flow" + flowService "ai-agent/workflow/service/flow" + "context" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" +) + +type flowUser struct{} + +var FlowUser = new(flowUser) + +func (c *flowUser) Create(ctx context.Context, req *flowDto.CreateFlowUserReq) (res *flowDto.CreateFlowUserRes, err error) { + res, err = flowService.FlowUserService.Create(ctx, req) + return +} + +func (c *flowUser) Update(ctx context.Context, req *flowDto.UpdateFlowUserReq) (res *beans.ResponseEmpty, err error) { + err = flowService.FlowUserService.Update(ctx, req) + return +} + +func (c *flowUser) Delete(ctx context.Context, req *flowDto.DeleteFlowUserReq) (res *beans.ResponseEmpty, err error) { + err = flowService.FlowUserService.Delete(ctx, req) + return +} + +func (c *flowUser) Get(ctx context.Context, req *flowDto.GetFlowUserReq) (res *flowDto.FlowUserVO, err error) { + return flowService.FlowUserService.Get(ctx, req) +} + +func (c *flowUser) List(ctx context.Context, req *flowDto.ListFlowUserReq) (res *flowDto.ListFlowRes, err error) { + if !g.IsEmpty(req.Page) { + req.Page = &beans.Page{PageNum: 1, PageSize: 20} + } + res, err = flowService.FlowUserService.List(ctx, req) + return +} diff --git a/workflow/controller/node/node_library_controller.go b/workflow/controller/node/node_library_controller.go new file mode 100644 index 0000000..ca60008 --- /dev/null +++ b/workflow/controller/node/node_library_controller.go @@ -0,0 +1,15 @@ +package node + +import ( + nodeDto "ai-agent/workflow/model/dto/node" + nodeService "ai-agent/workflow/service/node" + "context" +) + +type nodeLibrary struct{} + +var NodeLibrary = new(nodeLibrary) + +func (c *nodeLibrary) List(ctx context.Context, req *nodeDto.WorkflowNodeTreeReq) (res *nodeDto.WorkflowNodeTreeRes, err error) { + return nodeService.NodeLibraryService.GetNodeLibrary(ctx, req) +} diff --git a/workflow/controller/skill/skill_template_controller.go b/workflow/controller/skill/skill_template_controller.go new file mode 100644 index 0000000..b784151 --- /dev/null +++ b/workflow/controller/skill/skill_template_controller.go @@ -0,0 +1,26 @@ +package skill + +import ( + skillDto "ai-agent/workflow/model/dto/skill" + skillService "ai-agent/workflow/service/skill" + "context" + + "gitea.com/red-future/common/beans" +) + +type skillTemplate struct{} + +var SkillTemplate = new(skillTemplate) + +func (c *skillTemplate) Create(ctx context.Context, req *skillDto.CreateSkillTemplateReq) (res *skillDto.CreateSkillTemplateRes, err error) { + return skillService.SkillTemplateService.Create(ctx, req) +} + +func (c *skillTemplate) Delete(ctx context.Context, req *skillDto.DeleteSkillTemplateReq) (res *beans.ResponseEmpty, err error) { + err = skillService.SkillTemplateService.Delete(ctx, req) + return +} + +func (c *skillTemplate) List(ctx context.Context, req *skillDto.ListSkillTemplateReq) (res *skillDto.ListSkillTemplateRes, err error) { + return skillService.SkillTemplateService.List(ctx, req) +} diff --git a/workflow/controller/skill/skill_user_controller.go b/workflow/controller/skill/skill_user_controller.go new file mode 100644 index 0000000..8b5014f --- /dev/null +++ b/workflow/controller/skill/skill_user_controller.go @@ -0,0 +1,30 @@ +package skill + +import ( + skillDto "ai-agent/workflow/model/dto/skill" + skillService "ai-agent/workflow/service/skill" + "context" + + "gitea.com/red-future/common/beans" +) + +type skillUser struct{} + +var SkillUser = new(skillUser) + +func (c *skillUser) Create(ctx context.Context, req *skillDto.CreateSkillUserReq) (res *skillDto.CreateSkillUserRes, err error) { + return skillService.SkillUserService.Create(ctx, req) +} + +func (c *skillUser) Delete(ctx context.Context, req *skillDto.DeleteSkillUserReq) (res *beans.ResponseEmpty, err error) { + err = skillService.SkillUserService.Delete(ctx, req) + return +} + +func (c *skillUser) List(ctx context.Context, req *skillDto.ListSkillReq) (res *skillDto.ListSkillUserRes, err error) { + return skillService.SkillUserService.List(ctx, req) +} + +func (c *skillUser) ListUser(ctx context.Context, req *skillDto.ListSkillUserReq) (res *skillDto.ListSkillUserRes, err error) { + return skillService.SkillUserService.ListUser(ctx, req) +} diff --git a/workflow/dao/flow/flow_execution_dao.go b/workflow/dao/flow/flow_execution_dao.go new file mode 100644 index 0000000..1b1e595 --- /dev/null +++ b/workflow/dao/flow/flow_execution_dao.go @@ -0,0 +1,64 @@ +package flow + +import ( + "ai-agent/workflow/consts/public" + flowDto "ai-agent/workflow/model/dto/flow" + "ai-agent/workflow/model/entity" + "context" + + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/util/gconv" +) + +var FlowExecutionDao = &flowExecutionDao{} + +type flowExecutionDao struct{} + +// Insert 创建执行记录 +func (d *flowExecutionDao) Insert(ctx context.Context, req *flowDto.CreateFlowExecutionReq) (id int64, err error) { + var flowExecution = new(entity.FlowExecution) + err = gconv.Struct(req, &flowExecution) + if err != nil { + return + } + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowExecution).Insert(flowExecution) + if err != nil { + return + } + return r.LastInsertId() +} + +func (d *flowExecutionDao) Update(ctx context.Context, req *flowDto.UpdateFlowExecutionReq) (rows int64, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowExecution).OmitEmpty().Data(&req).Where(entity.FlowExecutionCol.Id, req.Id).Update() + if err != nil { + return + } + return r.RowsAffected() +} + +func (d *flowExecutionDao) Get(ctx context.Context, req *flowDto.GetFlowExecutionReq, fields ...string) (res *entity.FlowExecution, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowExecution).OmitEmpty(). + Where(entity.FlowExecutionCol.Id, req.Id). + Where(entity.FlowExecutionCol.SessionId, req.SessionId). + Fields(fields).One() + if err != nil { + return + } + err = r.Struct(&res) + return +} + +func (d *flowExecutionDao) List(ctx context.Context, req *flowDto.ListFlowExecutionReq, fields ...string) (res []*entity.FlowExecution, total int, err error) { + model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowExecution).Fields(fields).OmitEmpty() + model.Where(entity.FlowExecutionCol.Creator, req.Creator) + model.OrderDesc(entity.FlowExecutionCol.CreatedAt) + if req.Page != nil { + model.Page(int(req.Page.PageNum), int(req.Page.PageSize)) + } + r, total, err := model.AllAndCount(false) + if err != nil { + return + } + err = r.Structs(&res) + return +} diff --git a/workflow/dao/flow/flow_template_dao.go b/workflow/dao/flow/flow_template_dao.go new file mode 100644 index 0000000..083a1c4 --- /dev/null +++ b/workflow/dao/flow/flow_template_dao.go @@ -0,0 +1,70 @@ +package flow + +import ( + "ai-agent/workflow/consts/public" + flowDto "ai-agent/workflow/model/dto/flow" + "ai-agent/workflow/model/entity" + "context" + + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/util/gconv" +) + +var FlowTemplateDao = &flowTemplateDao{} + +type flowTemplateDao struct{} + +func (d *flowTemplateDao) Insert(ctx context.Context, req *flowDto.CreateFlowTemplateReq) (id int64, err error) { + var e = new(entity.FlowTemplate) + err = gconv.Struct(req, &e) + if err != nil { + return 0, err + } + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowTemplate).Insert(e) + if err != nil { + return + } + return r.LastInsertId() +} + +func (d *flowTemplateDao) Update(ctx context.Context, req *flowDto.UpdateFlowTemplateReq) (rows int64, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowTemplate).OmitEmpty(). + Where(entity.FlowTemplateCol.Id, req.Id). + Data(req). + Update() + if err != nil { + return + } + return r.RowsAffected() +} + +func (d *flowTemplateDao) Delete(ctx context.Context, req *flowDto.DeleteFlowTemplateReq) (rows int64, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowTemplate).Where(entity.FlowTemplateCol.Id, req.Id).Delete() + if err != nil { + return + } + return r.RowsAffected() +} + +func (d *flowTemplateDao) Get(ctx context.Context, req *flowDto.GetFlowTemplateReq, fields ...string) (res *entity.FlowTemplate, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowTemplate).NoTenantId(ctx).Where(entity.FlowTemplateCol.Id, req.Id).Fields(fields).One() + if err != nil { + return + } + err = r.Struct(&res) + return +} + +func (d *flowTemplateDao) List(ctx context.Context, req *flowDto.ListFlowTemplateReq, fields ...string) (res []*entity.FlowTemplate, total int, err error) { + model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowTemplate).NoTenantId(ctx).Fields(fields).OmitEmpty() + model.OrderDesc(entity.FlowTemplateCol.CreatedAt) + if req.Page != nil { + model.Page(int(req.Page.PageNum), int(req.Page.PageSize)) + } + r, total, err := model.AllAndCount(false) + if err != nil { + return + } + err = r.Structs(&res) + return +} diff --git a/workflow/dao/flow/flow_user_dao.go b/workflow/dao/flow/flow_user_dao.go new file mode 100644 index 0000000..afbb833 --- /dev/null +++ b/workflow/dao/flow/flow_user_dao.go @@ -0,0 +1,71 @@ +package flow + +import ( + "ai-agent/workflow/consts/public" + flowDto "ai-agent/workflow/model/dto/flow" + "ai-agent/workflow/model/entity" + "context" + + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/util/gconv" +) + +var FlowUserDao = &flowUserDao{} + +type flowUserDao struct{} + +func (d *flowUserDao) Insert(ctx context.Context, req *flowDto.CreateFlowUserReq) (id int64, err error) { + var e = new(entity.FlowUser) + err = gconv.Struct(req, &e) + if err != nil { + return 0, err + } + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowUser).Insert(e) + if err != nil { + return + } + return r.LastInsertId() +} + +func (d *flowUserDao) Update(ctx context.Context, req *flowDto.UpdateFlowUserReq) (rows int64, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowUser).OmitEmpty(). + Where(entity.FlowUserCol.Id, req.Id). + Data(req). + Update() + if err != nil { + return + } + return r.RowsAffected() +} + +func (d *flowUserDao) Delete(ctx context.Context, req *flowDto.DeleteFlowUserReq) (rows int64, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowUser).Where(entity.FlowUserCol.Id, req.Id).Delete() + if err != nil { + return + } + return r.RowsAffected() +} + +func (d *flowUserDao) Get(ctx context.Context, req *flowDto.GetFlowUserReq, fields ...string) (res *entity.FlowUser, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowUser).NoTenantId(ctx).Where(entity.FlowUserCol.Id, req.Id).Fields(fields).One() + if err != nil { + return + } + err = r.Struct(&res) + return +} + +func (d *flowUserDao) List(ctx context.Context, req *flowDto.ListFlowUserReq, fields ...string) (res []*entity.FlowUser, total int, err error) { + model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameFlowUser).NoTenantId(ctx).Fields(fields).OmitEmpty() + model.Where(entity.FlowUserCol.Creator, req.Creator) + model.OrderDesc(entity.FlowUserCol.CreatedAt) + if req.Page != nil { + model.Page(int(req.Page.PageNum), int(req.Page.PageSize)) + } + r, total, err := model.AllAndCount(false) + if err != nil { + return + } + err = r.Structs(&res) + return +} diff --git a/workflow/dao/skill/skill_template_dao.go b/workflow/dao/skill/skill_template_dao.go new file mode 100644 index 0000000..564b156 --- /dev/null +++ b/workflow/dao/skill/skill_template_dao.go @@ -0,0 +1,51 @@ +package skill + +import ( + "ai-agent/workflow/consts/public" + skillDto "ai-agent/workflow/model/dto/skill" + "ai-agent/workflow/model/entity" + "context" + + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +var SkillTemplateDao = &skillTemplateDao{} + +type skillTemplateDao struct{} + +func (d *skillTemplateDao) Insert(ctx context.Context, req *skillDto.CreateSkillTemplateReq) (id int64, err error) { + skillTemplate := new(entity.SkillTemplate) + err = gconv.Struct(req, &skillTemplate) + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillTemplate).Insert(&skillTemplate) + if err != nil { + return + } + return r.LastInsertId() +} + +func (d *skillTemplateDao) Delete(ctx context.Context, req *skillDto.DeleteSkillTemplateReq) (rows int64, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillTemplate).Where(entity.SkillTemplateCol.Id, req.Id).Delete() + if err != nil { + return + } + return r.RowsAffected() +} + +func (d *skillTemplateDao) List(ctx context.Context, req *skillDto.ListSkillTemplateReq, fields ...string) (res []*entity.SkillTemplate, total int, err error) { + model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillTemplate).NoTenantId(ctx).Fields(fields).OmitEmpty() + if !g.IsEmpty(req.Keyword) { + model.WhereLike(entity.SkillTemplateCol.Name, "%"+req.Keyword+"%") + } + model.OrderDesc(entity.SkillTemplateCol.CreatedAt) + if req.Page != nil { + model.Page(int(req.Page.PageNum), int(req.Page.PageSize)) + } + r, total, err := model.AllAndCount(false) + if err != nil { + return + } + err = r.Structs(&res) + return +} diff --git a/workflow/dao/skill/skill_user_dao.go b/workflow/dao/skill/skill_user_dao.go new file mode 100644 index 0000000..bf55be0 --- /dev/null +++ b/workflow/dao/skill/skill_user_dao.go @@ -0,0 +1,52 @@ +package skill + +import ( + "ai-agent/workflow/consts/public" + skillDto "ai-agent/workflow/model/dto/skill" + "ai-agent/workflow/model/entity" + "context" + + "gitea.com/red-future/common/db/gfdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +var SkillUserDao = &skillUserDao{} + +type skillUserDao struct{} + +func (d *skillUserDao) Insert(ctx context.Context, req *skillDto.CreateSkillUserReq) (id int64, err error) { + skillUser := new(entity.SkillUser) + err = gconv.Struct(req, &skillUser) + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillUser).Insert(&skillUser) + if err != nil { + return + } + return r.LastInsertId() +} + +func (d *skillUserDao) Delete(ctx context.Context, req *skillDto.DeleteSkillUserReq) (rows int64, err error) { + r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillUser).Where(entity.SkillUserCol.Id, req.Id).Delete() + if err != nil { + return + } + return r.RowsAffected() +} + +func (d *skillUserDao) List(ctx context.Context, req *skillDto.ListSkillUserReq, fields ...string) (res []*entity.SkillUser, total int, err error) { + model := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameSkillUser).NoTenantId(ctx).Fields(fields).OmitEmpty() + model.Where(entity.SkillUserCol.Creator, req.Creator) + if !g.IsEmpty(req.Keyword) { + model.WhereLike(entity.SkillUserCol.Name, "%"+req.Keyword+"%") + } + model.OrderDesc(entity.SkillUserCol.CreatedAt) + if req.Page != nil { + model.Page(int(req.Page.PageNum), int(req.Page.PageSize)) + } + r, total, err := model.AllAndCount(false) + if err != nil { + return + } + err = r.Structs(&res) + return +} diff --git a/workflow/model/dto/flow/flow_execution_dto.go b/workflow/model/dto/flow/flow_execution_dto.go new file mode 100644 index 0000000..fca2c31 --- /dev/null +++ b/workflow/model/dto/flow/flow_execution_dto.go @@ -0,0 +1,235 @@ +package flow + +import ( + "ai-agent/workflow/consts/flow" + "ai-agent/workflow/model/entity" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// NodeExecutionInput 节点执行入参(包含配置+表单架构) +type NodeExecutionInput struct { + Config *entity.FlowNode // 节点配置 + Global *FlowExecutionInput `json:"-"` +} + +// FlowExecutionInput 工作流执行入参(全程不变) +type FlowExecutionInput struct { + 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 []string `json:"executedNodes"` +} + +type ComposeMessagesReq struct { + ModelTypeId int `json:"modelTypeId"` + ModelName string `json:"modelName"` + SkillName string `json:"skillName"` + Form map[string]any `json:"form"` + UserForm map[string]any `json:"userForm"` + UserFiles []string `json:"userFiles"` + SessionId string `json:"sessionId" dc:"会话ID"` + IsBuild bool `json:"isBuild"` + Cause string `json:"cause"` +} + +type ComposeMessagesRes struct { + Messages map[string]any `json:"messages"` + EpicycleId int64 `json:"epicycleId" dc:"轮次ID"` +} + +type CreateTaskReq 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 CreateTaskRes struct { + TaskId string `json:"taskId"` +} + +type GetIsChatModelRes struct { + ModelName string `json:"modelName"` + ResponseBody map[string]any `json:"responseBody"` +} + +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:"结果文件类型"` + Text string `p:"text" json:"text" dc:"文本结果(可选,最多约 2000 字符)"` +} + +type TaskCallback struct { + TaskID string `json:"taskId"` + State int `json:"state"` // 0排队中/1执行中/2成功/3失败/4已下载 + OssFile string `json:"ossFile"` + FileType string `json:"fileType"` + Text string `json:"text"` + //ImgContent *Image `json:"imgContent"` +} + +type Text struct { + Choices []struct { + FinishReason string `json:"finish_reason"` + Index int `json:"index"` + Message struct { + Content string `json:"content"` + Role string `json:"role"` + } `json:"message"` + } `json:"choices"` + Created int `json:"created"` + Id string `json:"id"` + Model string `json:"model"` + Object string `json:"object"` + Usage struct { + CompletionTokens int `json:"completion_tokens"` + PromptTokens int `json:"prompt_tokens"` + PromptTokensDetails struct { + CachedTokens int `json:"cached_tokens"` + } + TotalTokens int `json:"total_tokens"` + } `json:"usage"` +} + +type Image struct { + Output struct { + Choices []struct { + FinishReason string `json:"finish_reason"` + Message struct { + Content []struct { + Image string `json:"image"` + } `json:"content"` + Role string `json:"role"` + } `json:"message"` + } `json:"choices"` + } `json:"output"` + Usage struct { + Height int `json:"height"` + ImageCount int `json:"image_count"` + Width int `json:"width"` + } `json:"usage"` + RequestId string `json:"request_id"` +} + +//============================================================================= + +type ExecuteReq struct { + g.Meta `path:"/execute" method:"post" tags:"任务管理" summary:"执行任务" dc:"执行任务"` + + FlowId int64 `json:"flowId" dc:"用户流程ID"` + FlowName string `json:"flowName"` + 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"` +} + +type ExecuteRes struct { + Id int64 `json:"id,string" dc:"执行记录ID,用于查询执行状态和结果"` +} + +type CancelReq struct { + g.Meta `path:"/cancel" method:"get" tags:"任务管理" summary:"取消任务" dc:"取消任务"` + + FlowId int64 `json:"flowId" dc:"用户流程ID"` +} + +type CreateFlowExecutionReq struct { + FlowUserId int64 `json:"flowUserId" description:"流程ID"` + FlowName string `json:"flowName"` + TriggerType flow.FlowExecutionTriggerType `json:"triggerType" description:"触发类型"` + DurationMs int64 `json:"durationMs" description:"执行时长(毫秒)"` + Status flow.FlowExecutionStatus `json:"status" description:"状态:1-运行中,2-成功,3-失败"` + FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"` + NodeInputParams []*entity.FlowNode `json:"nodeInputParams" description:"节点输入参数"` + OutputParams []map[string]interface{} `json:"outputParams" description:"输出参数"` + ErrorMessage string `json:"errorMessage" description:"错误信息"` + TraceId string `json:"traceId" description:"跟踪ID"` + SessionId string `json:"sessionId" dc:"会话ID"` +} + +type CreateFlowExecutionRes struct { + Id int64 `json:"id,string"` +} + +type UpdateFlowExecutionReq struct { + Id int64 `json:"id" v:"required#ID不能为空"` + 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"` +} + +type GetFlowExecutionReq struct { + g.Meta `path:"/get" method:"get" tags:"任务管理" summary:"获取任务详情" dc:"获取任务详情"` + + Id int64 `json:"id" v:"required#ID不能为空"` + SessionId string `json:"sessionId" dc:"会话ID"` +} + +type ListFlowExecutionReq struct { + g.Meta `path:"/list" method:"get" tags:"任务管理" summary:"任务列表" dc:"任务列表"` + + Page *beans.Page `json:"page"` + Creator string `json:"creator"` +} + +type ListFlowExecutionRes struct { + List []*VOFlowExecution `json:"list"` + Total int `json:"total"` +} + +type VOFlowExecution struct { + Id int64 `json:"id,string" dc:"id"` + FlowUserId int64 `json:"flowUserId,string" description:"流程ID"` + FlowName string `json:"flowName"` + TriggerType flow.FlowExecutionTriggerType `json:"triggerType" description:"触发类型"` + DurationMs int64 `json:"durationMs" description:"执行时长(毫秒)"` + Status flow.FlowExecutionStatus `json:"status" description:"状态:1-运行中,2-成功,3-失败"` + FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"` + NodeInputParams []*entity.FlowNode `json:"nodeInputParams" description:"节点输入参数"` + OutputParams []map[string]interface{} `json:"outputParams" description:"输出参数"` + ErrorMessage string `json:"errorMessage" description:"错误信息"` + TraceId string `json:"traceId" description:"跟踪ID"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +// ========== 核心:构建树状结构 ========== +// 定义树结构 +type OutputItem struct { + Timestamp string `json:"timestamp" description:"时间戳key"` + Content string `json:"content" description:"内容值"` + Label string `json:"label" description:"后缀+数字标号"` +} +type FlowNode struct { + FlowName string `json:"flowName" description:"流程名称"` + FlowId int64 `json:"flowId,string" description:"流程ID"` + SessionId string `json:"sessionId" description:"会话ID"` + Items []OutputItem `json:"items" description:"输出项列表"` +} +type DateNode struct { + CreateDate string `json:"createDate" description:"创建日期"` + Flows []FlowNode `json:"flows" description:"流程列表"` +} + +// 最终树结构返回体 +type ListFlowExecutionTreeRes struct { + Tree []DateNode `json:"tree"` + ImgAddressPrefix string `json:"imgAddressPrefix"` +} diff --git a/workflow/model/dto/flow/flow_template_dto.go b/workflow/model/dto/flow/flow_template_dto.go new file mode 100644 index 0000000..123f5a9 --- /dev/null +++ b/workflow/model/dto/flow/flow_template_dto.go @@ -0,0 +1,76 @@ +package flow + +import ( + "ai-agent/workflow/consts/flow" + "ai-agent/workflow/model/entity" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +type CreateFlowTemplateReq struct { + g.Meta `path:"/create" method:"post" tags:"系统流程管理" summary:"创建系统流程" dc:"创建系统流程"` + + FlowTemplateName string `json:"flowTemplateName" description:"流程模板名称"` + Description string `json:"description" description:"流程描述"` + CategoryCode string `json:"categoryCode" description:"流程分类"` + CategoryName string `json:"categoryName" description:"流程分类名称"` + FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"` + NodeInputParams []*entity.FlowNode `json:"nodeInputParams" description:"节点输入参数"` + Status flow.FlowTemplateStatus `description:"流程状态:1启用/0停用"` +} + +type CreateFlowTemplateRes struct { + Id int64 `json:"id,string"` +} + +type UpdateFlowTemplateReq struct { + g.Meta `path:"/update" method:"put" tags:"系统流程管理" summary:"更新系统流程" dc:"更新系统流程"` + + Id int64 `json:"id" v:"required#ID不能为空"` + FlowTemplateName string `json:"flowTemplateName" description:"流程模板名称"` + Description string `json:"description" description:"流程描述"` + CategoryCode string `json:"categoryCode" description:"流程分类"` + CategoryName string `json:"categoryName" description:"流程分类名称"` + FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"` + NodeInputParams []*entity.FlowNode `json:"nodeInputParams" description:"节点输入参数"` + Status flow.FlowTemplateStatus `description:"流程状态:1启用/0停用"` +} + +type DeleteFlowTemplateReq struct { + g.Meta `path:"/delete" method:"delete" tags:"系统流程管理" summary:"删除系统流程" dc:"删除系统流程"` + + Id int64 `json:"id" v:"required#ID不能为空"` +} + +type GetFlowTemplateReq struct { + g.Meta `path:"/get" method:"get" tags:"系统流程管理" summary:"获取系统流程详情" dc:"获取系统流程详情"` + + Id int64 `json:"id" v:"required#ID不能为空"` +} + +type ListFlowTemplateReq struct { + g.Meta `path:"/list" method:"get" tags:"系统流程管理" summary:"获取系统流程列表" dc:"分页查询系统流程列表,支持多条件筛选"` + + Page *beans.Page `json:"page"` + Keyword string `json:"keyword" dc:"关键词搜索"` +} + +type ListFlowTemplateRes struct { + List []*FlowTemplateVO `json:"list"` + Total int `json:"total"` +} + +type FlowTemplateVO struct { + Id int64 `json:"id,string" dc:"id"` + FlowTemplateName string `json:"flowTemplateName" description:"流程模板名称"` + Description string `json:"description" description:"流程描述"` + CategoryCode string `json:"categoryCode" description:"流程分类"` + CategoryName string `json:"categoryName" description:"流程分类名称"` + FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"` + NodeInputParams []*entity.FlowNode `json:"nodeInputParams" description:"节点输入参数"` + Status flow.FlowTemplateStatus `description:"流程状态:1启用/0停用"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} diff --git a/workflow/model/dto/flow/flow_user_dto.go b/workflow/model/dto/flow/flow_user_dto.go new file mode 100644 index 0000000..c290840 --- /dev/null +++ b/workflow/model/dto/flow/flow_user_dto.go @@ -0,0 +1,80 @@ +package flow + +import ( + "ai-agent/workflow/consts/flow" + "ai-agent/workflow/model/entity" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +type CreateFlowUserReq struct { + g.Meta `path:"/create" method:"post" tags:"用户流程管理" summary:"创建用户流程" dc:"创建用户流程"` + + FlowName string `json:"flowName" description:"流程名称"` + Description string `json:"description" description:"流程描述"` + FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"` + NodeInputParams []*entity.FlowNode `json:"nodeInputParams" description:"节点输入参数"` + AccessLevel flow.FlowUserAccessLevel `json:"accessLevel" description:"访问权限:1私有,2团队,3公开"` + SourceFlowTemplateId int64 `json:"sourceFlowTemplateId" description:"来源流程模板ID"` +} + +type CreateFlowUserRes struct { + Id int64 `json:"id,string"` +} + +type UpdateFlowUserReq struct { + g.Meta `path:"/update" method:"put" tags:"用户流程管理" summary:"更新用户流程" dc:"更新用户流程"` + + Id int64 `json:"id" v:"required#ID不能为空"` + FlowName string `json:"flowName" description:"流程名称"` + Description string `json:"description" description:"流程描述"` + FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"` + NodeInputParams []*entity.FlowNode `json:"nodeInputParams" description:"节点输入参数"` + AccessLevel flow.FlowUserAccessLevel `json:"accessLevel" description:"访问权限:1私有,2团队,3公开"` + SourceFlowTemplateId int64 `json:"sourceFlowTemplateId" description:"来源流程模板ID"` +} + +type DeleteFlowUserReq struct { + g.Meta `path:"/delete" method:"delete" tags:"用户流程管理" summary:"删除用户流程" dc:"删除用户流程"` + + Id int64 `json:"id" v:"required#ID不能为空"` +} + +type GetFlowUserReq struct { + g.Meta `path:"/get" method:"get" tags:"用户流程管理" summary:"获取用户流程详情" dc:"获取用户流程详情"` + + Id int64 `json:"id" v:"required#ID不能为空"` +} + +type ListFlowUserReq struct { + g.Meta `path:"/list" method:"get" tags:"用户流程管理" summary:"获取用户流程列表" dc:"分页查询用户流程列表,支持多条件筛选"` + + Page *beans.Page `json:"page"` + Creator string `json:"creator"` + Keyword string `json:"keyword" dc:"关键词搜索"` +} + +type ListFlowUserRes struct { + List []*FlowUserVO `json:"list"` + Total int `json:"total"` +} + +type FlowUserVO struct { + Id int64 `json:"id,string" dc:"id"` + FlowName string `json:"flowName" description:"流程名称"` + Description string `json:"description" description:"流程描述"` + FlowContent *entity.FlowInfo `json:"flowContent" description:"流程内容"` + NodeInputParams []*entity.FlowNode `json:"nodeInputParams" description:"节点输入参数"` + AccessLevel flow.FlowUserAccessLevel `json:"accessLevel" description:"访问权限:1私有,2团队,3公开"` + SourceFlowTemplateId int64 `json:"sourceFlowTemplateId,string" description:"来源流程模板ID"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +type ListFlowRes struct { + ListFlowUserRes *ListFlowUserRes `json:"listFlowUserRes"` + ListFlowTemplateRes *ListFlowTemplateRes `json:"listFlowTemplateRes"` + IsAdmin bool `json:"isAdmin"` +} diff --git a/workflow/model/dto/node/node_library_dto.go b/workflow/model/dto/node/node_library_dto.go new file mode 100644 index 0000000..495be55 --- /dev/null +++ b/workflow/model/dto/node/node_library_dto.go @@ -0,0 +1,29 @@ +package node + +import ( + "ai-agent/workflow/consts/node" + + "github.com/gogf/gf/v2/frame/g" +) + +type WorkflowNodeTreeReq struct { + g.Meta `path:"/list" method:"get" tags:"节点组件库管理" summary:"节点组件库列表" dc:"节点组件库列表"` + + Creator string `json:"creator"` +} + +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"` +} diff --git a/workflow/model/dto/skill/skill_template_dto.go b/workflow/model/dto/skill/skill_template_dto.go new file mode 100644 index 0000000..8a80e7d --- /dev/null +++ b/workflow/model/dto/skill/skill_template_dto.go @@ -0,0 +1,50 @@ +package skill + +import ( + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +type CreateSkillTemplateReq struct { + g.Meta `path:"/create" method:"post" tags:"Skill技能管理" summary:"创建Skill技能" dc:"创建Skill技能"` + + Name string `json:"name"` + Description string `json:"description"` + Category string `json:"category"` + FileName string `json:"fileName"` + FileUrl string `json:"fileUrl"` +} + +type CreateSkillTemplateRes struct { + Id int64 `json:"id,string"` +} + +type DeleteSkillTemplateReq struct { + g.Meta `path:"/delete" method:"delete" tags:"Skill技能管理" summary:"删除Skill技能" dc:"删除Skill技能"` + + Id int64 `json:"id" v:"required#ID不能为空"` +} + +type ListSkillTemplateReq struct { + g.Meta `path:"/list" method:"get" tags:"Skill技能管理" summary:"Skill技能列表" dc:"Skill技能列表"` + + Page *beans.Page `json:"page"` + Keyword string `json:"keyword" dc:"关键词搜索"` +} + +type ListSkillTemplateRes struct { + List []*SkillTemplateVO `json:"list"` + Total int `json:"total"` +} + +type SkillTemplateVO struct { + Id int64 `json:"id,string" dc:"id"` + Name string `json:"name"` + Description string `json:"description"` + Category string `json:"category"` + FileName string `json:"fileName"` + FileUrl string `json:"fileUrl"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} diff --git a/workflow/model/dto/skill/skill_user_dto.go b/workflow/model/dto/skill/skill_user_dto.go new file mode 100644 index 0000000..6ac2d08 --- /dev/null +++ b/workflow/model/dto/skill/skill_user_dto.go @@ -0,0 +1,59 @@ +package skill + +import ( + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +type CreateSkillUserReq struct { + g.Meta `path:"/create" method:"post" tags:"Skill用户技能管理" summary:"创建Skill用户技能" dc:"创建Skill用户技能"` + + Name string `json:"name"` + Description string `json:"description"` + Category string `json:"category"` + FileName string `json:"fileName"` + FileUrl string `json:"fileUrl"` +} + +type CreateSkillUserRes struct { + Id int64 `json:"id,string"` +} + +type DeleteSkillUserReq struct { + g.Meta `path:"/delete" method:"delete" tags:"Skill用户技能管理" summary:"删除Skill用户技能" dc:"删除Skill用户技能"` + + Id int64 `json:"id" v:"required#ID不能为空"` +} + +type ListSkillReq struct { + g.Meta `path:"/list" method:"get" tags:"Skill用户技能管理" summary:"Skill用户技能列表" dc:"Skill用户技能列表"` + + Page *beans.Page `json:"page"` + Creator string `json:"creator"` + Keyword string `json:"keyword" dc:"关键词搜索"` +} + +type ListSkillUserReq struct { + g.Meta `path:"/listUser" method:"get" tags:"Skill用户技能管理" summary:"Skill仅用户技能列表" dc:"Skill仅用户技能列表"` + + Page *beans.Page `json:"page"` + Creator string `json:"creator"` + Keyword string `json:"keyword" dc:"关键词搜索"` +} + +type ListSkillUserRes struct { + List []*SkillUserVO `json:"list"` + Total int `json:"total"` +} + +type SkillUserVO struct { + Id int64 `json:"id,string" dc:"id"` + Name string `json:"name"` + Description string `json:"description"` + Category string `json:"category"` + FileName string `json:"fileName"` + FileUrl string `json:"fileUrl"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} diff --git a/workflow/model/entity/flow_execution.go b/workflow/model/entity/flow_execution.go new file mode 100644 index 0000000..9d8961e --- /dev/null +++ b/workflow/model/entity/flow_execution.go @@ -0,0 +1,53 @@ +package entity + +import ( + "ai-agent/workflow/consts/flow" + + "gitea.com/red-future/common/beans" +) + +type FlowExecution struct { + beans.SQLBaseDO `orm:",inherit"` // 嵌入基础字段:Id, TenantId, Creator, CreatedAt, Updater, UpdatedAt, DeletedAt + // 业务字段 + FlowUserId int64 `orm:"flow_user_id" json:"flowUserId" description:"流程ID"` + FlowName string `orm:"flow_name" json:"flowName" description:"流程名称"` + TriggerType flow.FlowExecutionTriggerType `orm:"trigger_type" json:"triggerType" description:"触发类型"` + DurationMs int64 `orm:"duration_ms" json:"durationMs" description:"执行时长(毫秒)"` + Status flow.FlowExecutionStatus `orm:"status" json:"status" description:"状态:1-运行中,2-成功,3-失败"` + FlowContent *FlowInfo `orm:"flow_content" json:"flowContent" description:"流程内容"` + NodeInputParams []*FlowNode `orm:"node_input_params" json:"nodeInputParams" description:"节点输入参数"` + OutputParams []map[string]interface{} `orm:"output_params" json:"outputParams" description:"输出参数"` + ErrorMessage string `orm:"error_message" json:"errorMessage" description:"错误信息"` + TraceId string `orm:"trace_id" json:"traceId" description:"跟踪ID"` + SessionId string `orm:"session_id" json:"sessionId" description:"会话ID"` +} + +type flowExecutionCol struct { + beans.SQLBaseCol + FlowUserId string + FlowName string + TriggerType string + DurationMs string + Status string + FlowContent string + NodeInputParams string + OutputParams string + ErrorMessage string + TraceId string + SessionId string +} + +var FlowExecutionCol = flowExecutionCol{ + SQLBaseCol: beans.DefSQLBaseCol, + FlowUserId: "flow_user_id", + FlowName: "flow_name", + TriggerType: "trigger_type", + DurationMs: "duration_ms", + Status: "status", + FlowContent: "flow_content", + NodeInputParams: "node_input_params", + OutputParams: "output_params", + ErrorMessage: "error_message", + TraceId: "trace_id", + SessionId: "session_id", +} diff --git a/workflow/model/entity/flow_template.go b/workflow/model/entity/flow_template.go new file mode 100644 index 0000000..68c5655 --- /dev/null +++ b/workflow/model/entity/flow_template.go @@ -0,0 +1,41 @@ +package entity + +import ( + "ai-agent/workflow/consts/flow" + + "gitea.com/red-future/common/beans" +) + +type FlowTemplate struct { + beans.SQLBaseDO `orm:",inherit"` // 嵌入基础字段:Id, TenantId, Creator, CreatedAt, Updater, UpdatedAt, DeletedAt + // 业务字段 + FlowTemplateName string `orm:"flow_template_name" json:"flowTemplateName" description:"流程模板名称"` + Description string `orm:"description" json:"description" description:"流程描述"` + CategoryCode string `orm:"category_code" json:"categoryCode" description:"流程分类"` + CategoryName string `orm:"category_name" json:"categoryName" description:"流程分类名称"` + FlowContent *FlowInfo `orm:"flow_content" json:"flowContent" description:"流程内容"` + NodeInputParams []*FlowNode `orm:"node_input_params" json:"nodeInputParams" description:"节点输入参数"` + Status flow.FlowTemplateStatus `orm:"status" json:"status" description:"流程状态:1启用/0停用"` +} + +type flowTemplateCol struct { + beans.SQLBaseCol + FlowTemplateName string + Description string + CategoryCode string + CategoryName string + FlowContent string + NodeInputParams string + Status string +} + +var FlowTemplateCol = flowTemplateCol{ + SQLBaseCol: beans.DefSQLBaseCol, + FlowTemplateName: "flow_template_name", + Description: "description", + CategoryCode: "category_code", + CategoryName: "category_name", + FlowContent: "flow_content", + NodeInputParams: "node_input_params", + Status: "status", +} diff --git a/workflow/model/entity/flow_user.go b/workflow/model/entity/flow_user.go new file mode 100644 index 0000000..21f60ce --- /dev/null +++ b/workflow/model/entity/flow_user.go @@ -0,0 +1,70 @@ +package entity + +import ( + "ai-agent/workflow/consts/flow" + "ai-agent/workflow/consts/node" + + "gitea.com/red-future/common/beans" +) + +type FlowInfo struct { + Version string `json:"version"` + StartNodeId string `json:"startNodeId"` + Nodes []FlowNode `json:"nodes"` + Edges []FlowEdge `json:"edges"` +} + +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"` + InputSource []FlowNodeInputSource `json:"inputSource"` // 前端指定:来源节点ID + FormConfig []node.NodeFormField `json:"formConfig"` + ModelConfig node.ModelItem `json:"modelConfig"` + OutputResult []node.NodeFormField `json:"outputResult" ds:"节点输出结果"` +} + +type FlowNodeInputSource struct { + NodeId string `json:"nodeId"` + QuoteOutput bool `json:"quoteOutput"` + Field []string `json:"field"` +} + +type FlowEdge struct { + Id string `json:"id"` + From string `json:"from"` + To string `json:"to"` +} + +type FlowUser struct { + beans.SQLBaseDO `orm:",inherit"` // 嵌入基础字段:Id, TenantId, Creator, CreatedAt, Updater, UpdatedAt, DeletedAt + // 业务字段 + FlowName string `orm:"flow_name" json:"flowName" description:"流程名称"` + Description string `orm:"description" json:"description" description:"流程描述"` + FlowContent *FlowInfo `orm:"flow_content" json:"flowContent" description:"流程内容"` + NodeInputParams []*FlowNode `orm:"node_input_params" json:"nodeInputParams" description:"节点输入参数"` + AccessLevel flow.FlowUserAccessLevel `orm:"access_level" json:"accessLevel" description:"访问权限:1私有,2团队,3公开"` + SourceFlowTemplateId int64 `orm:"source_flow_template_id" json:"sourceFlowTemplateId" description:"来源流程模板ID"` +} + +type flowUserCol struct { + beans.SQLBaseCol + FlowName string + Description string + FlowContent string + NodeInputParams string + AccessLevel string + SourceFlowTemplateId string +} + +var FlowUserCol = flowUserCol{ + SQLBaseCol: beans.DefSQLBaseCol, + FlowName: "flow_name", + Description: "description", + FlowContent: "flow_content", + NodeInputParams: "node_input_params", + AccessLevel: "access_level", + SourceFlowTemplateId: "source_flow_template_id", +} diff --git a/workflow/model/entity/skill_template.go b/workflow/model/entity/skill_template.go new file mode 100644 index 0000000..57a7d1a --- /dev/null +++ b/workflow/model/entity/skill_template.go @@ -0,0 +1,33 @@ +package entity + +import ( + "gitea.com/red-future/common/beans" +) + +type SkillTemplate struct { + beans.SQLBaseDO `orm:",inherit"` // 嵌入基础字段:Id, TenantId, Creator, CreatedAt, Updater, UpdatedAt, DeletedAt + + Name string `orm:"name" json:"name"` + Description string `orm:"description" json:"description"` + Category string `orm:"category" json:"category"` + FileName string `orm:"file_name" json:"fileName"` + FileUrl string `orm:"file_url" json:"fileUrl"` +} + +type skillTemplateCol struct { + beans.SQLBaseCol + Name string + Description string + Category string + FileName string + FileUrl string +} + +var SkillTemplateCol = skillTemplateCol{ + SQLBaseCol: beans.DefSQLBaseCol, + Name: "name", + Description: "description", + Category: "category", + FileName: "file_name", + FileUrl: "file_url", +} diff --git a/workflow/model/entity/skill_user.go b/workflow/model/entity/skill_user.go new file mode 100644 index 0000000..d99ea51 --- /dev/null +++ b/workflow/model/entity/skill_user.go @@ -0,0 +1,31 @@ +package entity + +import "gitea.com/red-future/common/beans" + +type SkillUser struct { + beans.SQLBaseDO `orm:",inherit"` // 嵌入基础字段:Id, TenantId, Creator, CreatedAt, Updater, UpdatedAt, DeletedAt + + Name string `orm:"name" json:"name"` + Description string `orm:"description" json:"description"` + Category string `orm:"category" json:"category"` + FileName string `orm:"file_name" json:"fileName"` + FileUrl string `orm:"file_url" json:"fileUrl"` +} + +type skillUserCol struct { + beans.SQLBaseCol + Name string + Description string + Category string + FileName string + FileUrl string +} + +var SkillUserCol = skillUserCol{ + SQLBaseCol: beans.DefSQLBaseCol, + Name: "name", + Description: "description", + Category: "category", + FileName: "file_name", + FileUrl: "file_url", +} diff --git a/workflow/service/flow/flow_execution_service.go b/workflow/service/flow/flow_execution_service.go new file mode 100644 index 0000000..0532de1 --- /dev/null +++ b/workflow/service/flow/flow_execution_service.go @@ -0,0 +1,612 @@ +package flow + +import ( + "ai-agent/workflow/consts/flow" + "ai-agent/workflow/consts/node" + flowDao "ai-agent/workflow/dao/flow" + flowDto "ai-agent/workflow/model/dto/flow" + "ai-agent/workflow/model/entity" + "context" + "fmt" + "sort" + "strconv" + "strings" + "sync" + + "gitea.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" + "go.opentelemetry.io/otel/trace" +) + +var FlowExecutionService = &flowExecutionService{} + +type flowExecutionService struct{} + +func (s *flowExecutionService) List(ctx context.Context, req *flowDto.ListFlowExecutionReq) (res *flowDto.ListFlowExecutionTreeRes, err error) { + user, err := utils.GetUserInfo(ctx) + if err != nil { + return + } + req.Creator = user.UserName + list, _, err := flowDao.FlowExecutionDao.List(ctx, req) + if err != nil { + return nil, err + } + + // ===================== 核心修复:只统计【有数据】的执行记录,空的直接跳过 ===================== + + executionNumber := make(map[int64]int) // executionId -> 倒序编号(最新=1) + + // 第一次遍历:只处理【有输出参数】的记录,统计并分配编号 + var validList []*entity.FlowExecution // 只存有效(非空)记录 + for _, execution := range list { + if g.IsEmpty(execution.OutputParams) { + 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 { + if _, err := strconv.ParseInt(tsKey, 10, 64); err != nil { + continue + } + tempItems = append(tempItems, flowDto.OutputItem{ + Timestamp: tsKey, + 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 := "内容" + + switch { + case strings.Contains(val, "img") || strings.Contains(val, "png") || strings.Contains(val, "jpg"): + suffix = "图片" + case strings.Contains(val, "html") || strings.Contains(val, "HTML"): + suffix = "HTML" + case strings.Contains(val, "txt") || len(val) > 50: + suffix = "文案" + } + + suffixCount[suffix]++ + item.Label = fmt.Sprintf("%s_%d", suffix, suffixCount[suffix]) + } + + // 组装节点 + node := flowDto.FlowNode{ + FlowName: displayFlowName, + FlowId: execution.FlowUserId, + SessionId: gconv.String(execution.SessionId), + Items: tempItems, + } + + if dateMap[createDate] == nil { + dateMap[createDate] = &[]flowWrap{} + } + *dateMap[createDate] = append(*dateMap[createDate], flowWrap{ + flowNode: node, + createdAt: execution.CreatedAt, + }) + } + + // 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) + }) + + var flowNodes []flowDto.FlowNode + for _, w := range *wraps { + flowNodes = append(flowNodes, w.flowNode) + } + + // ===================== 修复2:日期下没有流程,也过滤掉 ===================== + if len(flowNodes) == 0 { + continue + } + + tree = append(tree, flowDto.DateNode{ + CreateDate: date, + Flows: flowNodes, + }) + } + + // 第一层日期倒序 + sort.Slice(tree, func(i, j int) bool { + return tree[i].CreateDate > tree[j].CreateDate + }) + + imgPrefix, err := utils.GetFileAddressPrefix(ctx) + return &flowDto.ListFlowExecutionTreeRes{ + Tree: tree, + ImgAddressPrefix: imgPrefix, + }, nil +} + +// ModelCallback 模型回调接口 +func (s *flowExecutionService) ModelCallback(ctx context.Context, req *flowDto.ModelCallbackReq) (err error) { + // 唤醒等待的任务 + Notify(req.TaskId, req) + return nil +} + +// 全局等待任务回调的工具 +var ( + asyncMu sync.Mutex + asyncTasks = make(map[string]chan any) +) + +// Wait 阻塞等待回调结果 +// 调用后会一直卡住,直到 Notify 唤醒 或 超时/取消 +func Wait(ctx context.Context, taskId string) (any, error) { + asyncMu.Lock() + ch := make(chan any, 1) + asyncTasks[taskId] = ch + asyncMu.Unlock() + + select { + case result := <-ch: + return result, nil + case <-ctx.Done(): + asyncMu.Lock() + delete(asyncTasks, taskId) + asyncMu.Unlock() + return nil, ctx.Err() + } +} + +// Notify 回调时调用,唤醒等待的任务 +func Notify(taskId string, result any) { + asyncMu.Lock() + defer asyncMu.Unlock() + + ch, exist := asyncTasks[taskId] + if !exist { + return + } + + ch <- result + delete(asyncTasks, taskId) +} + +//func (s *flowExecutionService) Cancel(ctx context.Context, req *flowDto.CancelReq) (err error) { +// res, err := flowDao.FlowExecutionDao.Get(ctx, &flowDto.GetFlowExecutionReq{ +// Id: req.FlowId, +// }) +// res.TraceId +// return +//} + +func (s *flowExecutionService) Execute(ctx context.Context, req *flowDto.ExecuteReq) (res *flowDto.ExecuteRes, err error) { + + flowInfo, err := flowDao.FlowExecutionDao.Get(ctx, &flowDto.GetFlowExecutionReq{ + SessionId: req.SessionId, + }) + if err != nil { + return + } + var executionId int64 + if flowInfo == nil { + var r = new(flowDto.CreateFlowExecutionReq) + r.FlowUserId = req.FlowId + r.FlowName = req.FlowName + r.TriggerType = flow.FlowExecutionTriggerTypeManual.Code() + r.FlowContent = req.FlowContent + r.NodeInputParams = req.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() + } + executionId, err = flowDao.FlowExecutionDao.Insert(ctx, r) + if err != nil { + return + } + } else { + executionId = flowInfo.Id + } + + _, err = flowDao.FlowUserDao.Update(ctx, &flowDto.UpdateFlowUserReq{ + Id: req.FlowId, + FlowContent: req.FlowContent, + NodeInputParams: req.NodeInputParams, + }) + if err != nil { + return nil, err + } + + //nodeInsert := make([]*nodeDto.CreateNodeExecutionReq, 0, len(flowInfo.NodeInputParams)) + //for _, flowNode := range flowInfo.NodeInputParams { + // nodeInsert = append(nodeInsert, &nodeDto.CreateNodeExecutionReq{ + // FlowExecutionId: executionId, + // NodeId: flowNode.Id, + // Status: node.NodeExecutionStatusWait.Code(), + // NodeInputParams: flowNode, + // TraceId: r.TraceId, + // }) + //} + //_, err = nodeDao.NodeExecutionDao.BatchInsert(ctx, nodeInsert) + //if err != nil { + // return + //} + + // ========================================================================= + // ✅【第1步】给所有判断节点自动生成意图识别节点 + // ========================================================================= + judge2IntentNodeMap := make(map[string]string) + finalNodes := make([]entity.FlowNode, 0, len(req.FlowContent.Nodes)*2) + for _, item := range req.FlowContent.Nodes { + finalNodes = append(finalNodes, item) + // 判断节点自动加 intent 节点 + if item.NodeCode == node.NodeTypeJudge { + intentNodeID := fmt.Sprintf("intent_%s", item.Id) + intentNode := entity.FlowNode{ + Id: intentNodeID, + NodeCode: node.NodeTypeIntent, + Name: fmt.Sprintf("意图识别-%s", item.Name), + InputSource: item.InputSource, // ✅ 正确赋值 + FormConfig: item.FormConfig, // ✅ 用户配置 + ModelConfig: item.ModelConfig, // ✅ 系统配置 + } + finalNodes = append(finalNodes, intentNode) + judge2IntentNodeMap[item.Id] = intentNodeID + } + } + + summaryNodeID := "summary_node" + summaryNode := entity.FlowNode{ + Id: summaryNodeID, + NodeCode: node.NodeTypeCustomNode, // 复用自定义节点类型,也可新增专属类型 + Name: "结果汇总节点", + InputSource: []entity.FlowNodeInputSource{}, // 后续自动聚合所有节点输出 + FormConfig: nil, + ModelConfig: node.ModelItem{}, + } + finalNodes = append(finalNodes, summaryNode) + + // 替换节点列表 + req.FlowContent.Nodes = finalNodes + + // ========================================================================= + // ✅【第2步】构建执行图 + // ========================================================================= + runGraph, err := BuildGraphFromFlowContent(ctx, req.FlowContent, judge2IntentNodeMap, summaryNodeID) + if err != nil { + executionReq := flowDto.UpdateFlowExecutionReq{ + Id: executionId, + } + executionReq.Status = flow.FlowExecutionStatusFailed.Code() + executionReq.ErrorMessage = err.Error() + _, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq) + if err1 != nil { + return + } + return nil, err + } + + // ========================================================================= + // ✅【第3步】构建 ConfigMap + // ========================================================================= + configMap := make(map[string]*entity.FlowNode) + for _, cfg := range req.NodeInputParams { + configMap[cfg.Id] = cfg + } + // 自动给意图节点复制配置 + for judgeID, intentID := range judge2IntentNodeMap { + if cfg, ok := configMap[judgeID]; ok { + configMap[intentID] = cfg + } + } + // 初始化汇总节点配置 + configMap[summaryNodeID] = &summaryNode + + // ========================================================================= + // ✅【第4步】构建全局执行入参(现在 schemaMap 是有值的!) + // ========================================================================= + execInput := &flowDto.FlowExecutionInput{ + ExecutionId: executionId, + ConfigMap: configMap, + SessionId: req.SessionId, + Desc: req.Desc, + SkillName: req.SkillName, + FileUrl: req.FileUrl, + } + // 执行工作流 + _, err = runGraph.Invoke(ctx, execInput) + if err != nil { + executionReq := flowDto.UpdateFlowExecutionReq{ + Id: executionId, + } + executionReq.Status = flow.FlowExecutionStatusFailed.Code() + executionReq.ErrorMessage = err.Error() + _, err1 := flowDao.FlowExecutionDao.Update(ctx, &executionReq) + if err1 != nil { + return + } + return nil, err + } + + return + +} + +// BuildGraphFromFlowContent 根据前端保存的工作流JSON,自动构建执行图 +func BuildGraphFromFlowContent(ctx context.Context, flowContent *entity.FlowInfo, judge2IntentNodeMap map[string]string, summaryNodeID string) (compose.Runnable[any, any], error) { + graph := compose.NewGraph[any, any]() + nodeMap := make(map[string]entity.FlowNode) + + // 注册所有节点 + for _, item := range flowContent.Nodes { + nodeMap[item.Id] = item + if item.NodeCode != node.NodeTypeJudge { + registerNodeToGraph(graph, item) + } + } + + // 构建边关系 + 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 { + intentNodeID, ok := judge2IntentNodeMap[fromNodeID] + if !ok { + return nil, fmt.Errorf("判断节点[%s]未生成意图节点", fromNodeID) + } + + 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 + + // 从意图节点取输出 + if intentCfg, ok := execInput.ConfigMap[intentNodeID]; ok { + currentConfig.OutputResult = intentCfg.OutputResult + } + + // 关键修改:构造 NodeExecutionInput 传入 JudgeLambda + nodeExecInput := &flowDto.NodeExecutionInput{ + Config: currentConfig, // 当前判断节点配置 + Global: execInput, // 全局执行入参 + } + return JudgeLambda(ctx, nodeExecInput) // 传入 NodeExecutionInput 类型 + } + + _ = graph.AddBranch(intentNodeID, compose.NewGraphBranch(judgeLambda, branchMap)) + continue + } + + // -------------------------- + // 普通节点连线 + // -------------------------- + for _, e := range edges { + toNode := nodeMap[e.To] + if toNode.NodeCode == node.NodeTypeJudge { + _ = graph.AddEdge(e.From, fmt.Sprintf("intent_%s", toNode.Id)) + continue + } + _ = graph.AddEdge(e.From, e.To) + } + } + + // ==================== 第四步:处理开始/结束节点 ==================== + if flowContent.StartNodeId != "" { + _ = graph.AddEdge(compose.START, flowContent.StartNodeId) + } + originalEndNodes := findEndNodes(flowContent.StartNodeId, flowContent.Edges) + for _, endID := range originalEndNodes { + _ = graph.AddEdge(endID, summaryNodeID) + } + _ = graph.AddEdge(summaryNodeID, compose.END) + + return graph.Compile(ctx, compose.WithGraphName("auto_build_workflow")) +} + +// -------------------------- 节点自动注册器(核心分发) -------------------------- +func registerNodeToGraph(graph *compose.Graph[any, any], flowNode entity.FlowNode) { + nodeID := flowNode.Id + code := flowNode.NodeCode + + // 通用包装:全程入参都是 *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[nodeID] + if currentConfig == nil { + return nil, fmt.Errorf("节点%s无配置", nodeID) + } + + // 获取入参 - 适配切片类型:遍历所有来源节点 + var realInput any + 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, // ✅ 把【全部节点】的对象直接塞进来 + } + + // 执行节点 + output, err := lambda(ctx, realInput) + if err != nil { + return nil, err + } + // ✅ 自动把当前节点ID 加入已执行列表 + execInput.ExecutedNodes = append(execInput.ExecutedNodes, nodeID) + + // 输出存入 FlowNodeConfig + if outConfig, ok := output.(*entity.FlowNode); ok { + currentConfig.OutputResult = outConfig.OutputResult + } + + // ✅ 关键:返回整个 execInput,让下一个节点继续用! + return execInput, nil + } + } + if nodeID == "summary_node" { + _ = graph.AddLambdaNode(nodeID, compose.InvokableLambda(wrapLambda(SummaryLambda))) + return + } + switch code { + case "__start__": + _ = graph.AddLambdaNode(nodeID, compose.InvokableLambda(wrapLambda(StartLambda))) + case node.NodeTypeTextModel: + _ = graph.AddLambdaNode(nodeID, compose.InvokableLambda(wrapLambda(TextModelLambda))) + case node.NodeTypeImageModel: + _ = graph.AddLambdaNode(nodeID, compose.InvokableLambda(wrapLambda(ImageModelLambda))) + case node.NodeTypeVideoModel: + _ = graph.AddLambdaNode(nodeID, compose.InvokableLambda(wrapLambda(VideoModelLambda))) + case node.NodeTypeAudioModel: + _ = graph.AddLambdaNode(nodeID, compose.InvokableLambda(wrapLambda(AudioModelLambda))) + case node.NodeTypeCustomNode: + _ = graph.AddLambdaNode(nodeID, compose.InvokableLambda(wrapLambda(CustomLambda))) + case node.NodeTypeForm: + _ = graph.AddLambdaNode(nodeID, compose.InvokableLambda(wrapLambda(FormLambda))) + case node.NodeTypeIntent: + _ = graph.AddLambdaNode(nodeID, compose.InvokableLambda(wrapLambda(IntentLambda))) + case node.NodeTypeMerge: + _ = graph.AddLambdaNode(nodeID, compose.InvokableLambda(wrapLambda(MergeLambda))) + } +} + +// -------------------------------------------------------------------- +// ✅【工具方法】找出所有没有出边的节点 → 作为结束节点连接 END +// -------------------------------------------------------------------- +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{}) + + // 🚀 只从【开始节点】递归遍历(关键修复) + findLeafNodes(startNodeId, nextMap, endNodeSet) + + // 转成数组返回 + endNodes := make([]string, 0, len(endNodeSet)) + for id := range endNodeSet { + endNodes = append(endNodes, id) + } + return endNodes +} + +// -------------------------------------------------------------------- +// ✅ 递归:查找以 nodeId 开头的所有叶子节点 +// -------------------------------------------------------------------- +func findLeafNodes(nodeId string, nextMap map[string][]string, endNodeSet map[string]struct{}) { + nextNodes := nextMap[nodeId] + + // 🚩 没有下一个节点 = 真实结束节点 + if len(nextNodes) == 0 { + endNodeSet[nodeId] = struct{}{} + return + } + + // 递归继续找下一个 + for _, nextId := range nextNodes { + findLeafNodes(nextId, nextMap, endNodeSet) + } +} diff --git a/workflow/service/flow/flow_template_service.go b/workflow/service/flow/flow_template_service.go new file mode 100644 index 0000000..987869a --- /dev/null +++ b/workflow/service/flow/flow_template_service.go @@ -0,0 +1,55 @@ +package flow + +import ( + flowDao "ai-agent/workflow/dao/flow" + flowDto "ai-agent/workflow/model/dto/flow" + "context" + + "github.com/gogf/gf/v2/util/gconv" +) + +var FlowTemplateService = &flowTemplateService{} + +type flowTemplateService struct{} + +func (s *flowTemplateService) Create(ctx context.Context, req *flowDto.CreateFlowTemplateReq) (res *flowDto.CreateFlowTemplateRes, err error) { + req.NodeInputParams = ExtractFlowNodeFrom(req.FlowContent) + id, err := flowDao.FlowTemplateDao.Insert(ctx, req) + if err != nil { + return + } + return &flowDto.CreateFlowTemplateRes{Id: id}, nil +} + +func (s *flowTemplateService) Update(ctx context.Context, req *flowDto.UpdateFlowTemplateReq) (err error) { + req.NodeInputParams = ExtractFlowNodeFrom(req.FlowContent) + _, err = flowDao.FlowTemplateDao.Update(ctx, req) + return +} + +func (s *flowTemplateService) Delete(ctx context.Context, req *flowDto.DeleteFlowTemplateReq) (err error) { + _, err = flowDao.FlowTemplateDao.Delete(ctx, req) + return +} + +func (s *flowTemplateService) Get(ctx context.Context, req *flowDto.GetFlowTemplateReq) (res *flowDto.FlowTemplateVO, err error) { + flowInfo, err := flowDao.FlowTemplateDao.Get(ctx, req) + if err != nil { + return + } + res = new(flowDto.FlowTemplateVO) + err = gconv.Struct(flowInfo, res) + return +} + +func (s *flowTemplateService) List(ctx context.Context, req *flowDto.ListFlowTemplateReq) (res *flowDto.ListFlowTemplateRes, err error) { + list, total, err := flowDao.FlowTemplateDao.List(ctx, req) + if err != nil { + return + } + res = &flowDto.ListFlowTemplateRes{ + Total: total, + } + err = gconv.Struct(list, &res.List) + return +} diff --git a/workflow/service/flow/flow_user_service.go b/workflow/service/flow/flow_user_service.go new file mode 100644 index 0000000..d2e17ff --- /dev/null +++ b/workflow/service/flow/flow_user_service.go @@ -0,0 +1,185 @@ +package flow + +import ( + "ai-agent/workflow/consts/flow" + flowDao "ai-agent/workflow/dao/flow" + flowDto "ai-agent/workflow/model/dto/flow" + "ai-agent/workflow/model/entity" + "context" + + commonHttp "gitea.com/red-future/common/http" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +var FlowUserService = &flowUserService{} + +type flowUserService struct{} + +// IsAdmin 调用admin-go服务检查是否是管理员 +func IsAdmin(ctx context.Context) (res bool, 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] + } + } + } + var r = make(map[string]bool) + if err = commonHttp.Get(ctx, "admin-go/api/v1/system/user/checkIsSuperAdmin", headers, &r); err != nil { + return false, err + } + return r["isSuperAdmin"], err +} + +func (s *flowUserService) Create(ctx context.Context, req *flowDto.CreateFlowUserReq) (res *flowDto.CreateFlowUserRes, err error) { + admin, err := IsAdmin(ctx) + if err != nil { + return + } + req.NodeInputParams = ExtractFlowNodeFrom(req.FlowContent) + var id int64 + if admin { + id, err = flowDao.FlowTemplateDao.Insert(ctx, &flowDto.CreateFlowTemplateReq{ + FlowTemplateName: req.FlowName, + Description: req.Description, + FlowContent: req.FlowContent, + NodeInputParams: req.NodeInputParams, + Status: flow.FlowTemplateStatusEnable.Code(), + }) + } else { + id, err = flowDao.FlowUserDao.Insert(ctx, req) + } + return &flowDto.CreateFlowUserRes{Id: id}, err +} + +func (s *flowUserService) Update(ctx context.Context, req *flowDto.UpdateFlowUserReq) (err error) { + admin, err := IsAdmin(ctx) + if err != nil { + return + } + req.NodeInputParams = ExtractFlowNodeFrom(req.FlowContent) + get, err := flowDao.FlowTemplateDao.Get(ctx, &flowDto.GetFlowTemplateReq{ + Id: req.Id, + }) + if err != nil { + return err + } + if !g.IsEmpty(get) && !admin { + _, err = flowDao.FlowUserDao.Insert(ctx, &flowDto.CreateFlowUserReq{ + FlowName: req.FlowName, + Description: req.Description, + FlowContent: req.FlowContent, + NodeInputParams: req.NodeInputParams, + SourceFlowTemplateId: get.Id, + }) + if err != nil { + return + } + } + + if admin { + _, err = flowDao.FlowTemplateDao.Update(ctx, &flowDto.UpdateFlowTemplateReq{ + Id: req.Id, + FlowTemplateName: req.FlowName, + Description: req.Description, + FlowContent: req.FlowContent, + NodeInputParams: req.NodeInputParams, + Status: flow.FlowTemplateStatusEnable.Code(), + }) + } else { + _, err = flowDao.FlowUserDao.Update(ctx, req) + } + return +} + +func ExtractFlowNodeFrom(flowContent *entity.FlowInfo) []*entity.FlowNode { + var flowNodes []*entity.FlowNode + for _, item := range flowContent.Nodes { + flowNodes = append(flowNodes, &item) + } + return flowNodes +} + +func (s *flowUserService) Delete(ctx context.Context, req *flowDto.DeleteFlowUserReq) (err error) { + admin, err := IsAdmin(ctx) + if err != nil { + return + } + if admin { + _, err = flowDao.FlowTemplateDao.Delete(ctx, &flowDto.DeleteFlowTemplateReq{ + Id: req.Id, + }) + } else { + _, err = flowDao.FlowUserDao.Delete(ctx, req) + } + return +} + +func (s *flowUserService) Get(ctx context.Context, req *flowDto.GetFlowUserReq) (res *flowDto.FlowUserVO, err error) { + + var flowInfo *entity.FlowTemplate + flowInfo, err = flowDao.FlowTemplateDao.Get(ctx, &flowDto.GetFlowTemplateReq{ + Id: req.Id, + }) + if err != nil { + return + } + if flowInfo != nil { + res = new(flowDto.FlowUserVO) + res.FlowName = flowInfo.FlowTemplateName + err = gconv.Struct(flowInfo, res) + return + } + + var flowUserInfo *entity.FlowUser + flowUserInfo, err = flowDao.FlowUserDao.Get(ctx, req) + if err != nil { + return + } + res = new(flowDto.FlowUserVO) + err = gconv.Struct(flowUserInfo, res) + return +} + +func (s *flowUserService) List(ctx context.Context, req *flowDto.ListFlowUserReq) (res *flowDto.ListFlowRes, err error) { + + l, t, err := flowDao.FlowTemplateDao.List(ctx, &flowDto.ListFlowTemplateReq{ + Keyword: req.Keyword, + Page: req.Page, + }) + if err != nil { + return + } + r := &flowDto.ListFlowTemplateRes{ + Total: t, + } + err = gconv.Struct(l, &r.List) + if err != nil { + return + } + + list, total, err := flowDao.FlowUserDao.List(ctx, req) + if err != nil { + return + } + re := &flowDto.ListFlowUserRes{ + Total: total, + } + err = gconv.Struct(list, &re.List) + if err != nil { + return + } + admin, err := IsAdmin(ctx) + if err != nil { + return + } + res = &flowDto.ListFlowRes{ + ListFlowUserRes: re, + ListFlowTemplateRes: r, + IsAdmin: admin, + } + + return +} diff --git a/workflow/service/flow/lambda_node.go b/workflow/service/flow/lambda_node.go new file mode 100644 index 0000000..893d84b --- /dev/null +++ b/workflow/service/flow/lambda_node.go @@ -0,0 +1,1001 @@ +package flow + +import ( + "ai-agent/workflow/consts/flow" + "ai-agent/workflow/consts/node" + flowDao "ai-agent/workflow/dao/flow" + "ai-agent/workflow/model/dto" + flowDto "ai-agent/workflow/model/dto/flow" + "ai-agent/workflow/model/entity" + "context" + "fmt" + "regexp" + "strconv" + "strings" + "time" + + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +func GetNodeContextContent(execInput *flowDto.FlowExecutionInput, node *entity.FlowNode) (map[string]any, map[string]any, map[string]any) { + + input := make(map[string]any) + output := make(map[string]any) + model := make(map[string]any) + // 1. 有引用 → 取引用节点的字段值 + if len(node.InputSource) > 0 { + for _, source := range node.InputSource { + refNodeID := source.NodeId + isQuoteOutput := source.QuoteOutput + fields := source.Field + + refNode, ok := execInput.ConfigMap[refNodeID] + if !ok { + continue + } + + inputMap := buildInputMap(refNode) + outputMap := mergeOutput(refNode.OutputResult) + modelMap := mergeModel(refNode.ModelConfig) + if isQuoteOutput { + for k, v := range outputMap { + output[k] = v + } + } + if len(fields) > 0 { + // 取指定字段 + for _, f := range fields { + + if v, ok := inputMap[f]; ok { + input[f] = v + } + if v, ok := modelMap[f]; ok { + model[f] = v + } + } + } else { + // 取全部 + for k, v := range inputMap { + input[k] = v + } + for k, v := range modelMap { + model[k] = v + } + } + } + } + + return input, output, model +} + +// buildInputMap 从 FormConfig 构造输入map +func buildInputMap(node *entity.FlowNode) map[string]any { + m := make(map[string]any) + for _, item := range node.FormConfig { + m[item.Label] = item + } + return m +} + +// mergeOutput 合并节点输出 []map → 单map +func mergeOutput(output []node.NodeFormField) map[string]any { + m := make(map[string]any) + for _, item := range output { + m[item.Label] = item + } + return m +} + +// mergeOutput 合并节点输出 []map → 单map +// 合并成你需要的 { key: { value: xxx } } 结构 +func mergeModel(output node.ModelItem) map[string]any { + m := make(map[string]any) + + // 遍历 output.ModelForm 里的每一个 key 和原始值 + for key, rawValue := range output.ModelForm { + // 包装成 { "value": 原始值 } + m[key] = map[string]any{ + "value": rawValue, + } + } + + return m +} + +func StartLambda(ctx context.Context, input any) (any, error) { + return input, nil +} + +func FormLambda(ctx context.Context, input any) (any, error) { + return input, nil +} + +func IntentLambda(ctx context.Context, input any) (any, error) { + nodeInput, ok := input.(*flowDto.NodeExecutionInput) + if !ok { + return nil, fmt.Errorf("入参类型错误,期望 *flowDto.NodeExecutionInput,实际 %T", input) + } + // 1. 直接用你原来的方法(返回两个 map) + inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config) + var outputResult []node.NodeFormField + for _, valueAny := range inputMap { + if field, ok := valueAny.(node.NodeFormField); ok { + outputResult = append(outputResult, field) + } + } + for _, valueAny := range outputMap { + if field, ok := valueAny.(node.NodeFormField); ok { + if !strings.Contains(field.Field, "html") && !strings.Contains(field.Field, "img") { + outputResult = append(outputResult, field) + } + } + } + for _, valueAny := range modelMap { + if field, ok := valueAny.(node.NodeFormField); ok { + outputResult = append(outputResult, field) + } + } + + nodeInput.Config.OutputResult = outputResult + + return nodeInput, nil +} + +// JudgeLambda 分支判断核心:读取IntentLambda的输出 → 返回目标节点ID做路由 +func JudgeLambda(ctx context.Context, input any) (string, error) { + nodeInput, ok := input.(*flowDto.NodeExecutionInput) + if !ok { + return "", fmt.Errorf("入参类型错误,期望 *flowDto.NodeExecutionInput,实际 %T", input) + } + + out := new([]node.NodeFormField) + err := gconv.Structs(nodeInput.Config.OutputResult, out) + if err != nil { + return "", err + } + + contextParts := "" + for _, v := range nodeInput.Config.FormConfig { + contextParts = fmt.Sprintf("%s,%s:%s", contextParts, v.Label, v.Value) + } + for _, v := range *out { + contextParts = fmt.Sprintf("%s,%s:%s", contextParts, v.Label, v.Value) + } + + configMap := gconv.Map(nodeInput.Config.Config) + ids := gconv.Strings(configMap["branch_ids"]) + branchIdNameMap := gconv.Map(configMap["branch_id_name_map"]) + + // 【重构】构建提示词:展示ID和对应的名称 + var branchIdNameLines []string + for _, id := range ids { + name := gconv.String(branchIdNameMap[id]) + branchIdNameLines = append(branchIdNameLines, fmt.Sprintf("%s: %s", id, name)) + } + + prompt := fmt.Sprintf(` +你是流程路由助手,你的任务是根据上下文,选择一个正确的节点ID返回。 + +规则: +1. 只允许从下面的可选节点ID列表中选择一个返回 +2. 不要返回任何多余文字、标点、解释、标题 +3. 只返回纯节点ID + +可选节点ID(ID: 节点描述): +%s + +上下文内容: +%s +`, strings.Join(branchIdNameLines, "\n"), contextParts) + + getIsChatModel, err := GetIsChatModel(ctx) + if err != nil { + return "", err + } + + req := flowDto.ComposeMessagesReq{ + ModelName: getIsChatModel.ModelName, + SkillName: "", + IsBuild: true, + Cause: "判断节点", + Form: map[string]any{}, + UserForm: map[string]any{"prompt": prompt}, + UserFiles: nodeInput.Global.FileUrl, + SessionId: nodeInput.Global.SessionId, + } + msg, err := ComposeMessages(ctx, &req) + if err != nil { + return "", err + } + taskResult, err := GatewayTask(ctx, msg.EpicycleId, getIsChatModel.ModelName, msg.Messages) + if err != nil { + return "", err + } + result, err := GetTaskResult(ctx, taskResult) + if err != nil { + return "", err + } + mapTaskResult := gconv.Map(result.Text) + + content := "" + for key, _ := range getIsChatModel.ResponseBody { + content = gconv.String(mapTaskResult[key]) + } + + fmt.Printf("JudgeLambda路由:目标节点ID=%s\n", gconv.String(content)) + + return content, nil +} + +// TextModelLambda 构建文案 +func TextModelLambda(ctx context.Context, input any) (any, error) { + nodeInput, ok := input.(*flowDto.NodeExecutionInput) + if !ok { + return nil, fmt.Errorf("入参类型错误") + } + + // 1. 直接用你原来的方法(返回两个 map) + inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config) + var outputResult []node.NodeFormField + for _, valueAny := range inputMap { + if field, ok := valueAny.(node.NodeFormField); ok { + outputResult = append(outputResult, field) + } + } + for _, valueAny := range outputMap { + if field, ok := valueAny.(node.NodeFormField); ok { + if !strings.Contains(field.Field, "html") && !strings.Contains(field.Field, "img") { + outputResult = append(outputResult, field) + } + } + } + for _, valueAny := range modelMap { + if field, ok := valueAny.(node.NodeFormField); ok { + outputResult = append(outputResult, field) + } + } + + resultUserFrom := make(map[string]any) + for _, item := range outputResult { + resultUserFrom[item.Label] = item + } + for _, item := range nodeInput.Config.FormConfig { + resultUserFrom[item.Label] = item + } + if !g.IsEmpty(nodeInput.Global.Desc) { + resultUserFrom["desc"] = node.NodeFormField{ + Value: nodeInput.Global.Desc, + Field: "desc", + Label: "描述", + Type: "text", + } + } + resultFrom := make(map[string]any) + for key, item := range nodeInput.Config.ModelConfig.ModelForm { + resultFrom[key] = map[string]any{ + "value": item, + } + } + var skillName = nodeInput.Config.SkillName + if g.IsEmpty(nodeInput.Config.SkillName) { + skillName = nodeInput.Global.SkillName + } + contentStr := "你是专业内容生成助手,请严格按以下规则输出内容:\n1. 输出标准 HTML 片段,不要 Markdown,不要 ``` 符号,不要多余解释\n2. 整体用
包裹\n3. 主标题使用

\n4. 章节标题使用

\n5. 正文段落使用

\n6. 列表使用

\n7. 重点内容使用 加粗\n8. 段落之间清晰分隔,结构规整\n9. 如果生成多条文案,每条文案独立用
包裹(序号从1开始)\n10. 每条文案内部必须在最上方添加一行固定格式:

需要配图:N 张

N 是这条文案需要的图片数量,只能是数字,不能是其他文字\n11. 只输出 HTML 结构,不输出任何额外文字" + resultUserFrom["prompt"] = contentStr + + req := flowDto.ComposeMessagesReq{ + ModelName: nodeInput.Config.ModelConfig.ModelName, + SkillName: skillName, + IsBuild: true, + Cause: "文案节点", + Form: resultFrom, + UserForm: resultUserFrom, + UserFiles: nodeInput.Global.FileUrl, + SessionId: nodeInput.Global.SessionId, + } + //contentStr := "你是专业内容生成助手,请按以下通用规则输出内容:\n1. 输出标准 HTML 片段,不要 Markdown,不要 ``` 符号,不要多余解释\n2. 整体用
包裹\n3. 主标题使用

\n4. 章节标题使用

\n5. 正文段落使用

\n6. 列表使用

  • ...
\n7. 重点内容使用 加粗\n8. 段落之间清晰分隔,结构规整\n9. 只输出 HTML 结构,不输出任何额外文字" + + //contentStr := "你是专业内容生成助手,请按以下通用规则输出内容:\n1. 输出标准 HTML 片段,不要 Markdown,不要 ``` 符号,不要多余解释\n2. 整体用
包裹\n3. 主标题使用

\n4. 章节标题使用

\n5. 正文段落使用

\n6. 列表使用

  • ...
\n7. 重点内容使用 加粗\n8. 段落之间清晰分隔,结构规整\n9. 如果生成多条文案,每条文案独立用
包裹(序号从1开始)\n10. 只输出 HTML 结构,不输出任何额外文字" + + msg, err := ComposeMessages(ctx, &req) + if err != nil { + return nil, err + } + + taskResult, err := GatewayTask(ctx, msg.EpicycleId, nodeInput.Config.ModelConfig.ModelName, msg.Messages) + if err != nil { + return "", err + } + + result, err := GetTaskResult(ctx, taskResult) + if err != nil { + return "", err + } + mapTaskResult := gconv.Map(result.Text) + + resultContent := "" + for key, _ := range nodeInput.Config.ModelConfig.ModelResponse { + resultContent = gconv.String(mapTaskResult[key]) + } + + // 拆分多条文案 + contentList := SplitMultiContents(resultContent) + + outputRes := make([]node.NodeFormField, 0) + for i, content := range contentList { + // 文案内容:content_0, content_1, content_2... + outputRes = append(outputRes, node.NodeFormField{ + Field: fmt.Sprintf("text_content_%d", i), + Value: content, + Label: fmt.Sprintf("文案内容_%d", i), + Type: "string", + Expand: extractImageCount(content), + }) + + // 1. 去掉 HTML 标签,生成纯文本 + plainText := stripHtmlTags(content) + // 2. 上传纯文本到 OSS + textFileName := fmt.Sprintf("ai_text_%d_%d.txt", time.Now().UnixMilli(), i) + 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("%v:text_url:%d", nodeInput.Config.Id, i), + Value: textUrl.FileURL, + Label: fmt.Sprintf("文案纯文本_%d", i), + Type: "string", + Expand: extractImageCount(content), + }) + } + nodeInput.Config.OutputResult = outputRes + + return nodeInput, nil +} + +// 从 HTML 内容里提取图片数量(例如从

需要配图:3 张

拿到 3) +func extractImageCount(content string) int { + re := regexp.MustCompile(`

需要配图:(\d+) 张

`) + match := re.FindStringSubmatch(content) + if len(match) >= 2 { + num, _ := strconv.Atoi(match[1]) + return num + } + return 0 // 没找到默认 0 +} + +// stripHtmlTags 去掉所有HTML标签,保留换行和文本结构,并删除配图标记行 +func stripHtmlTags(html string) string { + // 1. 替换块级标签为换行,保证排版 + blockTags := regexp.MustCompile(`]*>`) + text := blockTags.ReplaceAllString(html, "\n") + + // 2. 去掉所有剩余的 HTML 标签 + allTags := regexp.MustCompile(`<[^>]+>`) + text = allTags.ReplaceAllString(text, "") + + // 3. 🔥 新增:删除 "需要配图:X 张" 这一行(含前后可能的空格/换行) + imageCountLine := regexp.MustCompile(`(?m)^\s*需要配图:\d+\s*张\s*$`) + text = imageCountLine.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 + // 正则匹配
包裹的内容 + re := regexp.MustCompile(`
([\s\S]*?)
`) + matches := re.FindAllStringSubmatch(htmlContent, -1) + for _, match := range matches { + if len(match) > 1 { + // 清理空内容 + trimmed := strings.TrimSpace(match[1]) + if trimmed != "" { + contents = append(contents, trimmed) + } + } + } + // 兜底:如果没有匹配到结构化内容,按换行/分隔符拆分 + if len(contents) == 0 { + contents = strings.Split(htmlContent, "===分隔符===") // 提示词中可新增此兜底规则 + } + return contents +} + +// ImageModelLambda 构建图片 +func ImageModelLambda(ctx context.Context, input any) (any, error) { + nodeInput, ok := input.(*flowDto.NodeExecutionInput) + if !ok { + return nil, fmt.Errorf("入参类型错误") + } + // 1. 直接用你原来的方法(返回两个 map) + inputMap, outputMap, modelMap := GetNodeContextContent(nodeInput.Global, nodeInput.Config) + var outputResult []node.NodeFormField + for _, valueAny := range inputMap { + if field, ok := valueAny.(node.NodeFormField); ok { + outputResult = append(outputResult, field) + } + } + for _, valueAny := range outputMap { + if field, ok := valueAny.(node.NodeFormField); ok { + if !strings.Contains(field.Field, "html") && !strings.Contains(field.Field, "img") { + outputResult = append(outputResult, field) + } + } + } + for _, valueAny := range modelMap { + if field, ok := valueAny.(node.NodeFormField); ok { + outputResult = append(outputResult, field) + } + } + + resultUserFrom := make(map[string]any) + for _, item := range outputResult { + resultUserFrom[item.Label] = item + } + for _, item := range nodeInput.Config.FormConfig { + resultUserFrom[item.Label] = item + } + if !g.IsEmpty(nodeInput.Global.Desc) { + resultUserFrom["desc"] = node.NodeFormField{ + Value: nodeInput.Global.Desc, + Field: "desc", + Label: "描述", + Type: "text", + } + } + resultFrom := make(map[string]any) + for key, item := range nodeInput.Config.ModelConfig.ModelForm { + resultFrom[key] = map[string]any{ + "value": item, + } + } + var skillName = nodeInput.Config.SkillName + if g.IsEmpty(nodeInput.Config.SkillName) { + skillName = nodeInput.Global.SkillName + } + + req := flowDto.ComposeMessagesReq{ + ModelName: nodeInput.Config.ModelConfig.ModelName, + SkillName: skillName, + IsBuild: true, + Cause: "图片节点", + Form: resultFrom, + UserForm: resultUserFrom, + UserFiles: nodeInput.Global.FileUrl, + SessionId: nodeInput.Global.SessionId, + } + fmt.Println(req) + msg, err := ComposeMessages(ctx, &req) + if err != nil { + return nil, err + } + + taskResult, err := GatewayTask(ctx, msg.EpicycleId, nodeInput.Config.ModelConfig.ModelName, msg.Messages) + if err != nil { + return "", err + } + + result, err := GetTaskResult(ctx, taskResult) + if err != nil { + return "", err + } + + //result := new(flowDto.TaskCallback) + //result.Text = "{\n \"content\": [\n {\n \"image\": \"https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/8d/20260512/76483b06/306aac7b-915e-479d-94d4-adc3cf1d6f22.png?Expires=1779159512&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=3a3KDmPNeO%2BVjHJbAV8t0R7UF6Q%3D\"\n },\n {\n \"image\": \"https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/c9/20260512/76483b06/f8f3e9be-2920-48b8-93f5-acbf26e52b0c.png?Expires=1779159512&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=li%2FpcoX5i7FJrk3PCpw5jrbWy2k%3D\"\n },\n {\n \"image\": \"https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/89/20260512/76483b06/38d55abe-8230-4837-85d3-426265139be0.png?Expires=1779159512&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=uNRV9RQY2O60frAtIg6JvCcVhDw%3D\"\n },\n {\n \"image\": \"https://dashscope-7c2c.oss-accelerate.aliyuncs.com/7d/82/20260512/76483b06/e100070d-2a79-4ec8-be72-105226854bab.png?Expires=1779159512&OSSAccessKeyId=LTAI5tPxpiCM2hjmWrFXrym1&Signature=7UCh7FmYt0%2FYxyItNoLELp7zPF0%3D\"\n }\n ]\n}" + mapTaskResult := gconv.Map(result.Text) + + imgs := []string{} + for key, _ := range nodeInput.Config.ModelConfig.ModelResponse { + imgs = gconv.Strings(mapTaskResult[key]) + } + + var images []string + for _, item := range imgs { + mapItem := gconv.Map(item) + for _, value := range mapItem { + values := "" + values, ok = value.(string) + if !ok { + return nil, fmt.Errorf("图片地址类型错误") + } + // 下载官方临时图片 + imgBytes, _, err := GetImageBytesFromURL(values) + if err != nil { + return nil, fmt.Errorf("下载图片失败: %w", err) + } + // 构造文件名 + fileName := fmt.Sprintf("ai_image_%d.png", time.Now().UnixMilli()) + // 上传到你的OSS(你项目已有的Upload方法) + 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) + } + } + + url, err := utils.GetFileAddressPrefix(ctx) + if err != nil { + return nil, err + } + outputRes := make([]node.NodeFormField, 0) + + for i, item := range images { + // 图片:image_0, image_1, image_2... + outputRes = append(outputRes, node.NodeFormField{ + Field: fmt.Sprintf("image_%d", i), + Value: fmt.Sprintf("%s%s", url, item), + Label: fmt.Sprintf("图片_%d", i), + Type: "string", + }) + // 额外存储关联关系 + outputRes = append(outputRes, node.NodeFormField{ + Field: fmt.Sprintf("%v:img_url:%d", nodeInput.Config.Id, i), + Value: fmt.Sprintf("%s%s", url, item), + Label: fmt.Sprintf("图片_%d关联文案ID", i), + Type: "string", + }) + } + nodeInput.Config.OutputResult = outputRes + return input, nil +} + +func MergeLambda(ctx context.Context, input any) (any, error) { + nodeInput, ok := input.(*flowDto.NodeExecutionInput) + if !ok { + return nil, fmt.Errorf("汇总节点入参类型错误") + } + + // 1. 把所有节点输出拍平成 字段名->内容 的map + dataMap := make(map[string]node.NodeFormField) + _, outputMap, _ := GetNodeContextContent(nodeInput.Global, nodeInput.Config) + for _, valueAny := range outputMap { + if field, ok := valueAny.(node.NodeFormField); ok { + dataMap[field.Field] = field + } + } + + // 2. 提取所有文案:text_content_0,1,2... + var contents []node.NodeFormField + for i := 0; ; i++ { + key := fmt.Sprintf("text_content_%d", i) + val, has := dataMap[key] + if !has || val.Value == "" { + break + } + contents = append(contents, val) + } + + // 3. 提取所有图片:image_0,1,2... + var images []string + for i := 0; ; i++ { + key := fmt.Sprintf("image_%d", i) + val, has := dataMap[key] + if !has || val.Value == "" { + break + } + images = append(images, val.Value) + } + + // 4. 🔥 核心算法:图片按顺序连续归属给每条文案 + textImgMap := make(map[int][]string) // key:文案下标,value:图片列表 + if len(contents) > 0 && len(images) > 0 { + imgIndex := 0 // 当前用到第几张图片 + totalImg := len(images) + + for i, item := range contents { + // 图片已分配完,直接退出 + if imgIndex >= totalImg { + break + } + + // 当前文案需要挂载的图片数量 + needCount := gconv.Int(item.Expand) + if needCount <= 0 { + continue + } + + var imgList []string + for imgc := 0; imgc < needCount; imgc++ { + // 关键:必须判断是否越界 + if imgIndex >= totalImg { + break + } + imgList = append(imgList, images[imgIndex]) + imgIndex++ + } + + // 有图片才存入 map + if len(imgList) > 0 { + textImgMap[i] = imgList + } + } + } + type Item struct { + Content string // 文案(可为空) + Images []string // 图片(可空、可多张) + } + + // 🔥 把现有数据转换成通用 Item 列表(支持:纯文案、纯图片、图文任意组合) + var allItems []Item + + // 情况1:有文案 → 按文案条目生成 Item(每条文案+对应图片) + if len(contents) > 0 { + for i, val := range contents { + item := Item{ + Content: val.Value, // 文案 + Images: textImgMap[i], // 自动绑定该条目的图片(没有则为空切片) + } + allItems = append(allItems, item) + } + } else { + // 情况2:没有文案,只有图片 → 每张/每组图片生成独立 Item(纯图片条目) + if len(images) > 0 { + for _, img := range images { + allItems = append(allItems, Item{ + Content: "", + Images: []string{img}, + }) + } + } + } + + // 5. 生成多条独立HTML记录(通用方案:任意图文组合,每条独立生成+独立上传) + var outputRecords []node.NodeFormField + + // 遍历所有【独立图文条目】 → 每条生成独立HTML、独立上传OSS、独立输出记录 + for idx, item := range allItems { + // item 结构包含:Content(string) + Images([]string) + // 支持任意来源:文生图、图生文、单独文、单独图、文图合并 + + // 生成单条HTML + var htmlBuilder strings.Builder + htmlBuilder.WriteString(` + + + + + + + + +
+
+`) + + // 写入图片(支持0张、1张、多张) + if len(item.Images) > 0 { + htmlBuilder.WriteString(`
`) + for _, imgUrl := range item.Images { + htmlBuilder.WriteString(fmt.Sprintf(`图片`, imgUrl)) + } + htmlBuilder.WriteString(`
`) + } + + // 🔥 写入文案前:删除

需要配图:X 张

+ if item.Content != "" { + // 正则删除整行 + re := regexp.MustCompile(`

需要配图:\d+ 张

`) + cleanContent := re.ReplaceAllString(item.Content, "") + + // 写入清理后的文案 + htmlBuilder.WriteString(fmt.Sprintf(`
%s
`, cleanContent)) + } + + htmlBuilder.WriteString(`
+
+ +`) + htmlContent := htmlBuilder.String() + + // 上传OSS(每条独立上传) + fileName := fmt.Sprintf("item_%d_%d.html", idx, time.Now().UnixMilli()) + ossResult, err := Upload(ctx, &dto.UploadFileBytesReq{ + FileBytes: []byte(htmlContent), + FileName: fileName, + }) + if err != nil { + return nil, err + } + + // 拼接成一条输出记录 + // 每条记录包含:HTML内容 + 访问URL + 文案 + 图片列表 + outputRecords = append(outputRecords, + node.NodeFormField{ + Field: fmt.Sprintf("item_html_%d", idx), + Value: htmlContent, + Label: fmt.Sprintf("条目%d HTML", idx+1), + Type: "textarea", + }, + node.NodeFormField{ + Field: fmt.Sprintf("item_html_url_%d", idx), + Value: ossResult.FileURL, + Label: fmt.Sprintf("条目%d 地址", idx+1), + Type: "text", + }, + node.NodeFormField{ + Field: fmt.Sprintf("item_text_%d", idx), + Value: item.Content, + Label: fmt.Sprintf("条目%d 文案", idx+1), + Type: "text", + }, + node.NodeFormField{ + Field: fmt.Sprintf("item_images_%d", idx), + Value: strings.Join(item.Images, ","), + Label: fmt.Sprintf("条目%d 图片", idx+1), + Type: "text", + }, + ) + } + + // 最终输出多条记录 + nodeInput.Config.OutputResult = outputRecords + return nodeInput, nil +} + +func SummaryLambda(ctx context.Context, input any) (any, error) { + execInput, ok := input.(*flowDto.NodeExecutionInput) + if !ok { + return nil, fmt.Errorf("汇总节点入参类型错误,实际是 %T", input) + } + + // 聚合所有已执行节点的输出结果 + var summaryResult []map[string]interface{} + for _, nodeID := range execInput.Global.ExecutedNodes { + nodeConfig := execInput.Global.ConfigMap[nodeID] + if nodeConfig != nil && len(nodeConfig.OutputResult) > 0 { + for _, field := range nodeConfig.OutputResult { + if strings.Contains(field.Field, "item_html_url") || strings.Contains(field.Field, "img_url") || strings.Contains(field.Field, "text_url") { + // 生成 毫秒时间戳 作为 KEY + timeKey := strconv.FormatInt(time.Now().UnixMilli(), 10) + item := make(map[string]interface{}) + item[timeKey] = field.Value + summaryResult = append(summaryResult, item) + } + } + } + } + + // 把汇总结果存入当前节点的输出 + g.Log().Info(ctx, fmt.Sprintf("结果汇总完成,汇总数据:%+v", summaryResult)) + + executionReq := flowDto.UpdateFlowExecutionReq{ + Id: execInput.Global.ExecutionId, + OutputParams: summaryResult, + } + executionReq.Status = flow.FlowExecutionStatusSuccess.Code() + _, err := flowDao.FlowExecutionDao.Update(ctx, &executionReq) + + return execInput, err +} + +//func SummaryLambda(ctx context.Context, input any) (any, error) { +// execInput, ok := input.(*flowDto.NodeExecutionInput) +// if !ok { +// return nil, fmt.Errorf("汇总节点入参类型错误,实际是 %T", input) +// } +// +// // 1. 定义临时映射:按条目序号(如item_0)聚合html/img/text +// // key: 条目序号(如0/1/2), value: {html:"", img:"", text:""} +// itemMap := make(map[int]map[string]string) +// // 存储每个条目对应的时间戳(一个条目一个唯一时间戳) +// itemTimeMap := make(map[int]int64) +// +// // 2. 遍历已执行节点,解析输出字段并分组 +// for _, nodeID := range execInput.Global.ExecutedNodes { +// nodeConfig := execInput.Global.ConfigMap[nodeID] +// if nodeConfig == nil || len(nodeConfig.OutputResult) == 0 { +// continue +// } +// +// // 遍历节点的输出字段 +// for _, field := range nodeConfig.OutputResult { +// var itemIndex int +// var fieldType string +// var fieldValue string +// +// // 匹配「条目HTML地址」字段(如item_html_url_0) +// if match := regexp.MustCompile(`item_html_url_(\d+)`).FindStringSubmatch(field.Field); len(match) == 2 { +// itemIndex, _ = strconv.Atoi(match[1]) +// fieldType = "html" +// fieldValue = gconv.String(field.Value) +// } else if match := regexp.MustCompile(`img_url:(\d+)`).FindStringSubmatch(field.Field); len(match) == 2 { +// itemIndex, _ = strconv.Atoi(match[1]) +// fieldType = "img" +// fieldValue = gconv.String(field.Value) +// } else if match := regexp.MustCompile(`text_url:(\d+)`).FindStringSubmatch(field.Field); len(match) == 2 { +// itemIndex, _ = strconv.Atoi(match[1]) +// fieldType = "text" +// fieldValue = gconv.String(field.Value) +// } else { +// // 非目标字段,跳过 +// continue +// } +// +// // 初始化条目映射(首次遇到该条目时) +// if _, exists := itemMap[itemIndex]; !exists { +// itemMap[itemIndex] = map[string]string{ +// "html": "", +// "img": "", +// "text": "", +// } +// // 为该条目生成唯一时间戳(毫秒级) +// itemTimeMap[itemIndex] = time.Now().UnixMilli() +// } +// +// // 填充该条目对应的字段值 +// itemMap[itemIndex][fieldType] = fieldValue +// } +// } +// +// // 3. 组装最终的汇总结构:[{内容N:{html:"",img:"",text:""},时间戳:xxx}, ...] +// var summaryResult []map[string]interface{} +// // 按条目序号排序(保证顺序一致) +// itemIndexes := make([]int, 0, len(itemMap)) +// for idx := range itemMap { +// itemIndexes = append(itemIndexes, idx) +// } +// sort.Ints(itemIndexes) +// +// // 遍历排序后的条目,组装结构 +// for _, idx := range itemIndexes { +// itemData := itemMap[idx] +// timeStamp := itemTimeMap[idx] +// +// // 单条目结构:{"内容X": {html:"",img:"",text:""}, "时间戳": xxx} +// itemResult := make(map[string]interface{}) +// itemResult[fmt.Sprintf("内容%d", idx+1)] = map[string]string{ +// "html": itemData["html"], +// "img": itemData["img"], +// "text": itemData["text"], +// } +// itemResult["时间戳"] = timeStamp +// +// summaryResult = append(summaryResult, itemResult) +// } +// +// // 4. 打印调试&更新数据库 +// g.Log().Info(ctx, fmt.Sprintf("结果汇总完成,汇总数据:%+v", summaryResult)) +// executionReq := flowDto.UpdateFlowExecutionReq{ +// Id: execInput.Global.ExecutionId, +// OutputParams: summaryResult, +// Status: flow.FlowExecutionStatusSuccess.Code(), +// } +// _, err := flowDao.FlowExecutionDao.Update(ctx, &executionReq) +// +// return execInput, err +//} + +// VideoModelLambda 构建视频 +func VideoModelLambda(ctx context.Context, input any) (any, error) { + fmt.Println("VideoModelLambda:", input) + return input, nil +} + +// AudioModelLambda 构建音频 +func AudioModelLambda(ctx context.Context, input any) (any, error) { + fmt.Println("AudioModelLambda:", input) + return input, nil +} + +// CustomLambda 构建自定义 +func CustomLambda(ctx context.Context, input any) (any, error) { + fmt.Println("CustomLambda:", input) + return input, nil +} diff --git a/workflow/service/flow/lambda_node_util.go b/workflow/service/flow/lambda_node_util.go new file mode 100644 index 0000000..7b58c96 --- /dev/null +++ b/workflow/service/flow/lambda_node_util.go @@ -0,0 +1,245 @@ +package flow + +import ( + "ai-agent/workflow/model/dto" + flowDto "ai-agent/workflow/model/dto/flow" + "bytes" + "context" + "fmt" + "io" + "mime/multipart" + "net/http" + "strings" + + commonHttp "gitea.com/red-future/common/http" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +func GetIsChatModel(ctx context.Context) (*flowDto.GetIsChatModelRes, 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] + } + } + } + res := new(flowDto.GetIsChatModelRes) + err := commonHttp.Get(ctx, "model-gateway/model/getIsChatModel", headers, res, nil) + if err != nil { + return nil, err + } + + return res, nil +} + +func CreateGatewayTask(ctx context.Context, req *flowDto.CreateTaskReq) (string, 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] + } + } + } + res := new(flowDto.CreateTaskRes) + err := commonHttp.Post(ctx, "model-gateway/task/createTask", headers, res, &req) + if err != nil { + return "", err + } + + return res.TaskId, nil +} + +func ComposeMessages(ctx context.Context, req *flowDto.ComposeMessagesReq) (*flowDto.ComposeMessagesRes, 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] + } + } + } + res := new(flowDto.ComposeMessagesRes) + err := commonHttp.Post(ctx, "prompts-core/prompt/composeMessages", headers, res, &req) + if err != nil { + return nil, err + } + + return res, nil +} + +func GatewayTask(ctx context.Context, epicycleId int64, model string, content map[string]any) (any, error) { + modelTaskId, err := CreateGatewayTask(ctx, &flowDto.CreateTaskReq{ + ModelName: model, + BizName: g.Cfg().MustGet(ctx, "server.name").String(), + CallbackUrl: "/flow/execution/modelCallback", + RequestPayload: content, + EpicycleId: epicycleId, + }) + if err != nil { + return nil, err + } + return Wait(ctx, modelTaskId) +} + +func GetTaskResult(ctx context.Context, result any) (*flowDto.TaskCallback, error) { + task := new(flowDto.TaskCallback) + if err := gconv.Struct(result, task); err != nil { + return nil, err + } + + url, err := utils.GetFileAddressPrefix(ctx) + if err != nil { + return nil, err + } + + // 获取远程文件内容 + file, err := FetchRemoteJsonFile(ctx, url+task.OssFile) + if err != nil { + return nil, err + } + + task.Text = gconv.String(file) + + return task, nil +} + +func FetchRemoteJsonFile(ctx context.Context, fileUrl string) ([]byte, error) { + // 1. 下载文件 + resp, err := g.Client().Get(ctx, fileUrl) + if err != nil { + return nil, fmt.Errorf("get file failed: %w", err) + } + defer resp.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("http status error: %d", resp.StatusCode) + } + + return io.ReadAll(resp.Body) +} + +func GetImageBytesFromURL(url string) (all []byte, contentType string, err error) { + resp, err := http.Get(url) + if err != nil { + return + } + defer resp.Body.Close() + all, err = io.ReadAll(resp.Body) + if err != nil { + return + } + contentType = resp.Header.Get("Content-Type") + return +} + +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{} + url := "oss/file/uploadFile" + if err = commonHttp.Post(ctx, url, 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 buildMergeHtml(texts []string, images []string) string { + html := strings.Builder{} + + html.WriteString(` + + + + + + + + +
+`) + + // 1. 先渲染图片(无任何上下边距,占满宽度) + if len(images) > 0 { + html.WriteString(`
`) + for _, img := range images { + html.WriteString(fmt.Sprintf(``, img)) + } + html.WriteString(`
`) + } + + // 2. 渲染文案(紧贴图片下方,仅用内边距留白) + if len(texts) > 0 { + html.WriteString(`
`) + // 段落之间用
而不是

,减少空行 + html.WriteString(strings.Join(texts, "
")) + html.WriteString(`
`) + } + + html.WriteString(` +
+ + +`) + + return html.String() +} diff --git a/workflow/service/node/node_library_service.go b/workflow/service/node/node_library_service.go new file mode 100644 index 0000000..3db4e6b --- /dev/null +++ b/workflow/service/node/node_library_service.go @@ -0,0 +1,147 @@ +package node + +import ( + "ai-agent/workflow/consts/node" + nodeDto "ai-agent/workflow/model/dto/node" + "context" + "fmt" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/frame/g" +) + +var NodeLibraryService = &nodeLibraryService{} + +type nodeLibraryService struct{} + +func (s *nodeLibraryService) GetNodeLibrary(ctx context.Context, req *nodeDto.WorkflowNodeTreeReq) (*nodeDto.WorkflowNodeTreeRes, error) { + + WorkflowNodeGroups := []node.NodeGroupItem{ + { + Group: node.NodeGroupComponent, + Label: node.NodeGroupNameComponent, + Items: []node.NodeItem{ + { + NodeCode: node.NodeTypeTextModel, + NodeName: node.NodeNameTextModel, + SkillOption: true, + FormConfig: []node.NodeFormField{}, // 技能下拉 + ModelConfig: []node.ModelItem{}, + }, + { + NodeCode: node.NodeTypeImageModel, + NodeName: node.NodeNameImageModel, + SkillOption: true, + FormConfig: []node.NodeFormField{}, // 技能下拉 + ModelConfig: []node.ModelItem{}, + }, + }, + }, + { + Group: node.NodeGroupBase, + Label: node.NodeGroupNameBase, + Items: []node.NodeItem{ + { + NodeCode: node.NodeTypeMerge, + NodeName: node.NodeNameMerge, + SkillOption: false, + FormConfig: []node.NodeFormField{}, + ModelConfig: []node.ModelItem{}, + }, + { + NodeCode: node.NodeTypeJudge, + NodeName: node.NodeNameJudge, + SkillOption: false, + FormConfig: []node.NodeFormField{ + {Field: "condition", Label: node.FormLabelCondition, Type: "input", Required: true}, + }, + ModelConfig: []node.ModelItem{}, + }, + { + NodeCode: node.NodeTypeForm, + NodeName: node.NodeNameForm, + SkillOption: false, + FormConfig: []node.NodeFormField{}, + ModelConfig: []node.ModelItem{}, + }, + //{ + // NodeCode: node.NodeTypeModel, + // NodeName: node.NodeNameModel, + // SkillOption: true, + // FormConfig: []node.NodeFormField{}, + // ModelConfig: []node.ModelItem{}, + //}, + }, + }, + { + Group: node.NodeGroupCustom, + Label: node.NodeGroupNameCustom, + Items: []node.NodeItem{ + { + NodeCode: node.NodeTypeCustomNode, + NodeName: node.NodeNameCustomNode, + SkillOption: true, + FormConfig: []node.NodeFormField{ + {Field: "nodeName", Label: node.FormLabelApiKey, Type: "input", Required: true}, + {Field: "nodeType", Label: node.FormLabelModel, Type: "input", Required: true}, + }, + ModelConfig: []node.ModelItem{}, + }, + }, + }, + } + tree := &nodeDto.WorkflowNodeTreeRes{ + Groups: WorkflowNodeGroups, + } + + // 3. 遍历分组,根据 typeId=1 给【文本模型节点】追加固定表单 + for gIdx := range tree.Groups { + group := &tree.Groups[gIdx] + + // 遍历分组下的每个节点 + for itemIdx := range group.Items { + item := &group.Items[itemIdx] + if item.NodeCode == node.NodeTypeTextModel { + item.ModelConfig = append(item.ModelConfig, node.ModelItem{ + ModelName: "自定义", + }) + } + if item.NodeCode == node.NodeTypeImageModel { + item.ModelConfig = append(item.ModelConfig, node.ModelItem{ + ModelName: "自定义", + }) + } + + } + } + + return tree, nil +} + +// SetUserInfo 设置用户信息 +func (s *nodeLibraryService) SetUserInfo(ctx context.Context, creator string, tenantId uint64) (headers map[string]string, err error) { + // 创建完整的用户信息 + userInfo := &beans.User{ + UserName: creator, + TenantId: tenantId, + } + ctx = context.WithValue(ctx, "user", *userInfo) + // 提取并保存请求头(在连接升级前) + headers = make(map[string]string) + // 提取其他headers + if r := g.RequestFromCtx(ctx); r != nil { + for k, v := range r.Request.Header { + if len(v) > 0 { + headers[k] = v[0] + } + } + } + // 将完整用户信息序列化为JSON,放到X-User-Info请求头 + userInfoJson, err := gjson.Encode(userInfo) + if err != nil { + return nil, fmt.Errorf("用户信息序列化失败: %w", err) + } + headers["X-User-Info"] = string(userInfoJson) + return +} diff --git a/workflow/service/skill/skill_template_service.go b/workflow/service/skill/skill_template_service.go new file mode 100644 index 0000000..fc7b753 --- /dev/null +++ b/workflow/service/skill/skill_template_service.go @@ -0,0 +1,38 @@ +package skill + +import ( + skillDao "ai-agent/workflow/dao/skill" + skillDto "ai-agent/workflow/model/dto/skill" + "context" + + "github.com/gogf/gf/v2/util/gconv" +) + +var SkillTemplateService = &skillTemplateService{} + +type skillTemplateService struct{} + +func (s *skillTemplateService) Create(ctx context.Context, req *skillDto.CreateSkillTemplateReq) (res *skillDto.CreateSkillTemplateRes, err error) { + id, err := skillDao.SkillTemplateDao.Insert(ctx, req) + if err != nil { + return + } + return &skillDto.CreateSkillTemplateRes{Id: id}, nil +} + +func (s *skillTemplateService) Delete(ctx context.Context, req *skillDto.DeleteSkillTemplateReq) (err error) { + _, err = skillDao.SkillTemplateDao.Delete(ctx, req) + return +} + +func (s *skillTemplateService) List(ctx context.Context, req *skillDto.ListSkillTemplateReq) (res *skillDto.ListSkillTemplateRes, err error) { + list, total, err := skillDao.SkillTemplateDao.List(ctx, req) + if err != nil { + return nil, err + } + res = &skillDto.ListSkillTemplateRes{ + Total: total, + } + err = gconv.Struct(list, &res.List) + return +} diff --git a/workflow/service/skill/skill_user_service.go b/workflow/service/skill/skill_user_service.go new file mode 100644 index 0000000..bd8a522 --- /dev/null +++ b/workflow/service/skill/skill_user_service.go @@ -0,0 +1,130 @@ +package skill + +import ( + skillDao "ai-agent/workflow/dao/skill" + skillDto "ai-agent/workflow/model/dto/skill" + "ai-agent/workflow/model/entity" + "context" + + commonHttp "gitea.com/red-future/common/http" + "gitea.com/red-future/common/utils" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +var SkillUserService = &skillUserService{} + +type skillUserService struct{} + +// IsAdmin 调用admin-go服务检查是否是管理员 +func IsAdmin(ctx context.Context) (res bool, 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] + } + } + } + var r = make(map[string]bool) + if err = commonHttp.Get(ctx, "admin-go/api/v1/system/user/checkIsSuperAdmin", headers, &r); err != nil { + return false, err + } + return r["isSuperAdmin"], err +} + +func (s *skillUserService) Create(ctx context.Context, req *skillDto.CreateSkillUserReq) (res *skillDto.CreateSkillUserRes, err error) { + admin, err := IsAdmin(ctx) + if err != nil { + return + } + var id int64 + if admin { + id, err = skillDao.SkillTemplateDao.Insert(ctx, &skillDto.CreateSkillTemplateReq{ + Name: req.Name, + Description: req.Description, + Category: req.Category, + FileName: req.FileName, + FileUrl: req.FileUrl, + }) + } else { + id, err = skillDao.SkillUserDao.Insert(ctx, req) + } + return &skillDto.CreateSkillUserRes{Id: id}, err +} + +func (s *skillUserService) Delete(ctx context.Context, req *skillDto.DeleteSkillUserReq) (err error) { + admin, err := IsAdmin(ctx) + if err != nil { + return + } + if admin { + _, err = skillDao.SkillTemplateDao.Delete(ctx, &skillDto.DeleteSkillTemplateReq{ + Id: req.Id, + }) + } else { + _, err = skillDao.SkillUserDao.Delete(ctx, req) + } + return +} + +func (s *skillUserService) List(ctx context.Context, req *skillDto.ListSkillReq) (res *skillDto.ListSkillUserRes, err error) { + admin, err := IsAdmin(ctx) + if err != nil { + return + } + if admin { + var total int + var list []*entity.SkillTemplate + list, total, err = skillDao.SkillTemplateDao.List(ctx, &skillDto.ListSkillTemplateReq{ + Keyword: req.Keyword, + Page: req.Page, + }) + if err != nil { + return nil, err + } + res = &skillDto.ListSkillUserRes{ + Total: total, + } + err = gconv.Struct(list, &res.List) + return + } + + user, err := utils.GetUserInfo(ctx) + if err != nil { + return + } + req.Creator = user.UserName + list, total, err := skillDao.SkillUserDao.List(ctx, &skillDto.ListSkillUserReq{ + Keyword: req.Keyword, + Creator: req.Creator, + Page: req.Page, + }) + if err != nil { + return nil, err + } + res = &skillDto.ListSkillUserRes{ + Total: total, + } + err = gconv.Struct(list, &res.List) + + return +} + +func (s *skillUserService) ListUser(ctx context.Context, req *skillDto.ListSkillUserReq) (res *skillDto.ListSkillUserRes, err error) { + user, err := utils.GetUserInfo(ctx) + if err != nil { + return + } + req.Creator = user.UserName + list, total, err := skillDao.SkillUserDao.List(ctx, req) + if err != nil { + return nil, err + } + res = &skillDto.ListSkillUserRes{ + Total: total, + } + err = gconv.Struct(list, &res.List) + + return +}